master #67

Merged
sin365 merged 2 commits from Alienjack/AxibugEmuOnline:master into master 2024-12-19 20:46:41 +08:00
13 changed files with 455 additions and 302 deletions

2
.gitignore vendored
View File

@ -18,3 +18,5 @@
/AxibugEmuOnline.Server/config.cfg /AxibugEmuOnline.Server/config.cfg
/AxibugEmuOnline.Server/bin/ /AxibugEmuOnline.Server/bin/
/AxibugEmuOnline.Client/.editorconfig /AxibugEmuOnline.Client/.editorconfig
/AxibugEmuOnline.Client/*.user
/AxibugEmuOnline.Client/.idea

View File

@ -63,5 +63,10 @@
/// 服务器等待Step更新 /// 服务器等待Step更新
/// </summary> /// </summary>
OnRoomWaitStepChange, OnRoomWaitStepChange,
/// <summary>
/// 当房间中手柄位信息发生任何变化时触发,进入房间后也应该触发
/// </summary>
OnRoomSlotDataChanged, //todo : 实现这个事件
} }
} }

View File

@ -1,6 +1,7 @@
using AxibugEmuOnline.Client.ClientCore; using AxibugEmuOnline.Client.ClientCore;
using AxibugEmuOnline.Client.Event; using AxibugEmuOnline.Client.Event;
using UnityEngine; using UnityEngine;
using VirtualNes.Core;
namespace AxibugEmuOnline.Client.Manager namespace AxibugEmuOnline.Client.Manager
{ {
@ -11,6 +12,9 @@ namespace AxibugEmuOnline.Client.Manager
/// 但是Equals方法可以,所以,这个接口判断为空请使用Equals /// 但是Equals方法可以,所以,这个接口判断为空请使用Equals
/// </summary> /// </summary>
private IEmuCore m_emuCore; private IEmuCore m_emuCore;
private IControllerSetuper m_controllerSetuper;
/// <summary> /// <summary>
/// unity的c#实现有bug,以接口类型保存的monobehaviour引用,!=和==运算符没有调用到monobehaviour重写过的运算符 /// unity的c#实现有bug,以接口类型保存的monobehaviour引用,!=和==运算符没有调用到monobehaviour重写过的运算符
/// 但是Equals方法可以,所以,这个接口判断为空请使用Equals /// 但是Equals方法可以,所以,这个接口判断为空请使用Equals
@ -28,7 +32,7 @@ namespace AxibugEmuOnline.Client.Manager
if (!m_emuCore.IsNull()) StopGame(); if (!m_emuCore.IsNull()) StopGame();
var roomInfo = App.roomMgr.mineRoomMiniInfo; var roomInfo = App.roomMgr.mineRoomMiniInfo;
roomInfo.FetchRomFileInRoomInfo(EnumPlatform.NES, (room, romFile) => roomInfo.FetchRomFileInRoomInfo(EnumPlatform.NES, (_, romFile) =>
{ {
if (!romFile.RomReady) //这个rom并没有下载,所以取消进入房间 if (!romFile.RomReady) //这个rom并没有下载,所以取消进入房间
{ {
@ -58,6 +62,35 @@ namespace AxibugEmuOnline.Client.Manager
InGameUI.Instance.Show(romFile, m_emuCore); InGameUI.Instance.Show(romFile, m_emuCore);
m_emuCore.SetupScheme(); m_emuCore.SetupScheme();
m_controllerSetuper = Supporter.GetControllerSetuper();
SetupController();
Eventer.Instance.RegisterEvent(EEvent.OnRoomSlotDataChanged,OnSlotDataChanged);
}
private void OnSlotDataChanged()
{
SetupController();
}
private void SetupController()
{
if (!App.roomMgr.InRoom) //不在房间中,自动分配0号手柄到0号手柄位
{
m_controllerSetuper.SetConnect(con0ToSlot: 0);
}
else //在房间中则使用服务器下发的手柄槽位信息分配本地手柄
{
long selfUID = App.user.userdata.UID;
App.roomMgr.mineRoomMiniInfo.GetPlayerSlotIdxByUid(selfUID, 0, out var con0Slot);
App.roomMgr.mineRoomMiniInfo.GetPlayerSlotIdxByUid(selfUID, 1, out var con1Slot);
App.roomMgr.mineRoomMiniInfo.GetPlayerSlotIdxByUid(selfUID, 2, out var con2Slot);
App.roomMgr.mineRoomMiniInfo.GetPlayerSlotIdxByUid(selfUID, 3, out var con3Slot);
m_controllerSetuper.SetConnect(con0Slot, con1Slot, con2Slot, con3Slot);
}
} }
public void StopGame() public void StopGame()
@ -68,6 +101,8 @@ namespace AxibugEmuOnline.Client.Manager
InGameUI.Instance.Hide(); InGameUI.Instance.Hide();
LaunchUI.Instance.ShowMainMenu(); LaunchUI.Instance.ShowMainMenu();
m_controllerSetuper = null;
Eventer.Instance.UnregisterEvent(EEvent.OnRoomSlotDataChanged,OnSlotDataChanged);
} }
public void ResetGame() public void ResetGame()
@ -77,4 +112,4 @@ namespace AxibugEmuOnline.Client.Manager
m_emuCore.DoReset(); m_emuCore.DoReset();
} }
} }
} }

View File

@ -492,6 +492,26 @@ namespace AxibugEmuOnline.Client.Manager
return freeSlots.Length > 0; return freeSlots.Length > 0;
} }
/// <summary>
/// 指定uid和该uid的本地手柄序号,获取占用的手柄位
/// </summary>
public static bool GetPlayerSlotIdxByUid(this Protobuf_Room_MiniInfo roomMiniInfo, long uid ,int controllerIndex, out uint? slotID)
{
slotID = null;
//controllerIndex取值返回[0,3],这个序号代表玩家本地的手柄编号
//todo : 根据uid和controllerIndex 返回占用的位置
//目前未实现,所有非0号位置的手柄,都返回false
if (controllerIndex != 0) return false;
if (roomMiniInfo.Player1UID == uid) slotID = 0;
if (roomMiniInfo.Player2UID == uid) slotID = 1;
if (roomMiniInfo.Player3UID == uid) slotID = 2;
if (roomMiniInfo.Player4UID == uid) slotID = 3;
return true;
}
/// <summary> /// <summary>
/// 按照房间玩家下标获取昵称 /// 按照房间玩家下标获取昵称
/// </summary> /// </summary>

View File

@ -129,6 +129,11 @@ namespace AxibugEmuOnline.Client
} }
} }
public IControllerSetuper GetControllerSetuper()
{
return ControllerMapper;
}
public ControllerState FromNet(AxiReplay.ReplayStep step) public ControllerState FromNet(AxiReplay.ReplayStep step)
{ {
var temp = new ServerInputSnapShot(); var temp = new ServerInputSnapShot();

View File

@ -3,77 +3,69 @@ using VirtualNes.Core;
namespace AxibugEmuOnline.Client namespace AxibugEmuOnline.Client
{ {
public class NesControllerMapper public class NesControllerMapper : IControllerSetuper
{ {
public MapperSetter Player1 = new MapperSetter(1); public Controller Controller0 { get; } = new(0);
public MapperSetter Player2 = new MapperSetter(2); public Controller Controller1 { get; } = new(1);
public MapperSetter Player3 = new MapperSetter(3); public Controller Controller2 { get; } = new(2);
public MapperSetter Player4 = new MapperSetter(4); public Controller Controller3 { get; } = new(3);
private readonly EnumButtonType[] m_states = new EnumButtonType[4];
public ControllerState CreateState() public ControllerState CreateState()
{ {
var state1 = Player1.GetButtons(); m_states[0] = m_states[1] = m_states[2] = m_states[3] = 0;
var state2 = Player2.GetButtons();
var state3 = Player3.GetButtons();
var state4 = Player4.GetButtons();
var result = new ControllerState(state1, state2, state3, state4); if (Controller0.ConnectSlot.HasValue) m_states[Controller0.ConnectSlot.Value] = Controller0.GetButtons();
if (Controller1.ConnectSlot.HasValue) m_states[Controller1.ConnectSlot.Value] = Controller1.GetButtons();
if (Controller2.ConnectSlot.HasValue) m_states[Controller2.ConnectSlot.Value] = Controller2.GetButtons();
if (Controller3.ConnectSlot.HasValue) m_states[Controller3.ConnectSlot.Value] = Controller3.GetButtons();
var result = new ControllerState(m_states);
return result; return result;
} }
public class Mapper /// <summary>
/// Nes控制器
/// </summary>
public class Controller
{ {
MapperSetter m_setter; /// <summary>
EnumButtonType m_buttonType; /// 控制器编号
IKeyListener m_keyListener; /// <para><c>此编号并非对应游戏中的player1,player2,player3,player4,仅仅作为本地4个手柄的实例</c></para>
int m_controllerIndex => m_setter.ControllerIndex; /// <value>[0,3]</value>
/// </summary>
public Mapper(MapperSetter setter, EnumButtonType buttonType)
{
m_setter = setter;
m_buttonType = buttonType;
loadConfig();
}
private void loadConfig()
{
m_keyListener = MapperSetter.GetKey(m_controllerIndex, m_buttonType);
}
public EnumButtonType SampleKey()
{
return m_keyListener.IsPressing() ? m_buttonType : 0;
}
}
public class MapperSetter
{
/// <summary> 控制器序号(手柄1,2,3,4) </summary>
public int ControllerIndex { get; } public int ControllerIndex { get; }
public Mapper UP { get; private set; }
public Mapper DOWN { get; private set; }
public Mapper LEFT { get; private set; }
public Mapper RIGHT { get; private set; }
public Mapper A { get; private set; }
public Mapper B { get; private set; }
public Mapper SELECT { get; private set; }
public Mapper START { get; private set; }
public Mapper MIC { get; private set; }
public MapperSetter(int controllerIndex) /// <summary>
/// 指示该手柄连接的手柄插槽
/// <para><c>这个值代表了该手柄在实际游戏中控制的Player</c></para>
/// <value>[0,3] 例外:为空代表未连接</value>
/// </summary>
public uint? ConnectSlot { get; set; }
public Button UP { get; }
public Button DOWN { get; }
public Button LEFT { get; }
public Button RIGHT { get; }
public Button A { get; }
public Button B { get; }
public Button SELECT { get; }
public Button START { get; }
public Button MIC { get; }
public Controller(int controllerIndex)
{ {
ControllerIndex = controllerIndex; ControllerIndex = controllerIndex;
UP = new Mapper(this, EnumButtonType.UP); UP = new Button(this, EnumButtonType.UP);
DOWN = new Mapper(this, EnumButtonType.DOWN); DOWN = new Button(this, EnumButtonType.DOWN);
LEFT = new Mapper(this, EnumButtonType.LEFT); LEFT = new Button(this, EnumButtonType.LEFT);
RIGHT = new Mapper(this, EnumButtonType.RIGHT); RIGHT = new Button(this, EnumButtonType.RIGHT);
A = new Mapper(this, EnumButtonType.A); A = new Button(this, EnumButtonType.A);
B = new Mapper(this, EnumButtonType.B); B = new Button(this, EnumButtonType.B);
SELECT = new Mapper(this, EnumButtonType.SELECT); SELECT = new Button(this, EnumButtonType.SELECT);
START = new Mapper(this, EnumButtonType.START); START = new Button(this, EnumButtonType.START);
MIC = new Mapper(this, EnumButtonType.MIC); MIC = new Button(this, EnumButtonType.MIC);
} }
public EnumButtonType GetButtons() public EnumButtonType GetButtons()
@ -93,7 +85,7 @@ namespace AxibugEmuOnline.Client
return res; return res;
} }
public static IKeyListener GetKey(int controllerInput, EnumButtonType nesConBtnType) public static KeyListener GetKey(int controllerInput, EnumButtonType nesConBtnType)
{ {
string configKey = $"NES_{controllerInput}_{nesConBtnType}"; string configKey = $"NES_{controllerInput}_{nesConBtnType}";
if (PlayerPrefs.HasKey(configKey)) if (PlayerPrefs.HasKey(configKey))
@ -102,62 +94,60 @@ namespace AxibugEmuOnline.Client
} }
else else
{ {
var defaultKeyCode = GetDefaultKey(); var defaultKeyCode = KeyListener.GetDefaultKey(controllerInput, nesConBtnType);
PlayerPrefs.SetString(configKey, defaultKeyCode.ToString()); PlayerPrefs.SetString(configKey, defaultKeyCode.ToString());
return defaultKeyCode; return defaultKeyCode;
} }
KeyListener GetDefaultKey()
{
switch (controllerInput)
{
case 1:
if (nesConBtnType == EnumButtonType.LEFT) return new KeyListener(KeyCode.A);
if (nesConBtnType == EnumButtonType.RIGHT) return new KeyListener(KeyCode.D);
if (nesConBtnType == EnumButtonType.UP) return new KeyListener(KeyCode.W);
if (nesConBtnType == EnumButtonType.DOWN) return new KeyListener(KeyCode.S);
if (nesConBtnType == EnumButtonType.START) return new KeyListener(KeyCode.B);
if (nesConBtnType == EnumButtonType.SELECT) return new KeyListener(KeyCode.V);
if (nesConBtnType == EnumButtonType.A) return new KeyListener(KeyCode.K);
if (nesConBtnType == EnumButtonType.B) return new KeyListener(KeyCode.J);
if (nesConBtnType == EnumButtonType.MIC) return new KeyListener(KeyCode.M);
break;
case 2:
if (nesConBtnType == EnumButtonType.LEFT) return new KeyListener(KeyCode.Delete);
if (nesConBtnType == EnumButtonType.RIGHT) return new KeyListener(KeyCode.PageDown);
if (nesConBtnType == EnumButtonType.UP) return new KeyListener(KeyCode.Home);
if (nesConBtnType == EnumButtonType.DOWN) return new KeyListener(KeyCode.End);
if (nesConBtnType == EnumButtonType.START) return new KeyListener(KeyCode.PageUp);
if (nesConBtnType == EnumButtonType.SELECT) return new KeyListener(KeyCode.Insert);
if (nesConBtnType == EnumButtonType.A) return new KeyListener(KeyCode.Keypad5);
if (nesConBtnType == EnumButtonType.B) return new KeyListener(KeyCode.Keypad4);
if (nesConBtnType == EnumButtonType.MIC) return new KeyListener(KeyCode.KeypadPeriod);
break;
}
return default;
}
} }
} }
public interface IKeyListener /// <summary>
/// NES控制器按键类
/// </summary>
public class Button
{ {
bool IsPressing(); /// <summary> 所属控制器 </summary>
readonly Controller m_hostController;
/// <summary> 按键 </summary>
readonly EnumButtonType m_buttonType;
/// <summary> 按键监听器 </summary>
KeyListener m_keyListener;
public Button(Controller controller, EnumButtonType buttonType)
{
m_hostController = controller;
m_buttonType = buttonType;
CreateListener();
}
/// <summary>
/// 采集按钮按下状态
/// </summary>
/// <returns></returns>
public EnumButtonType SampleKey()
{
return m_keyListener.IsPressing() ? m_buttonType : 0;
}
private void CreateListener()
{
m_keyListener = Controller.GetKey(m_hostController.ControllerIndex, m_buttonType);
}
} }
public struct KeyListener : IKeyListener public readonly struct KeyListener
{ {
private KeyCode m_key; private readonly KeyCode m_key;
public KeyListener(KeyCode key) public KeyListener(KeyCode key)
{ {
m_key = key; m_key = key;
} }
/// <summary> /// <summary> 从配置字符串构建 </summary>
/// 从配置表字符串构建
/// </summary>
public KeyListener(string confStr) public KeyListener(string confStr)
{ {
m_key = KeyCode.None; m_key = KeyCode.None;
@ -166,19 +156,83 @@ namespace AxibugEmuOnline.Client
m_key = (KeyCode)result; m_key = (KeyCode)result;
} }
public readonly bool IsPressing() public bool IsPressing()
{ {
if (Input.GetKey(m_key)) return true; return Input.GetKey(m_key);
return false;
} }
public override string ToString() public override string ToString()
{ {
return ((int)(m_key)).ToString(); return ((int)(m_key)).ToString();
} }
public static KeyListener GetDefaultKey(int controllerIndex, EnumButtonType nesConBtnType)
{
switch (controllerIndex)
{
case 1:
switch (nesConBtnType)
{
case EnumButtonType.LEFT:
return new KeyListener(KeyCode.A);
case EnumButtonType.RIGHT:
return new KeyListener(KeyCode.D);
case EnumButtonType.UP:
return new KeyListener(KeyCode.W);
case EnumButtonType.DOWN:
return new KeyListener(KeyCode.S);
case EnumButtonType.START:
return new KeyListener(KeyCode.B);
case EnumButtonType.SELECT:
return new KeyListener(KeyCode.V);
case EnumButtonType.A:
return new KeyListener(KeyCode.K);
case EnumButtonType.B:
return new KeyListener(KeyCode.J);
case EnumButtonType.MIC:
return new KeyListener(KeyCode.M);
}
break;
case 2:
switch (nesConBtnType)
{
case EnumButtonType.LEFT:
return new KeyListener(KeyCode.Delete);
case EnumButtonType.RIGHT:
return new KeyListener(KeyCode.PageDown);
case EnumButtonType.UP:
return new KeyListener(KeyCode.Home);
case EnumButtonType.DOWN:
return new KeyListener(KeyCode.End);
case EnumButtonType.START:
return new KeyListener(KeyCode.PageUp);
case EnumButtonType.SELECT:
return new KeyListener(KeyCode.Insert);
case EnumButtonType.A:
return new KeyListener(KeyCode.Keypad5);
case EnumButtonType.B:
return new KeyListener(KeyCode.Keypad4);
case EnumButtonType.MIC:
return new KeyListener(KeyCode.KeypadPeriod);
}
break;
}
return default;
}
}
public void SetConnect(uint? con0ToSlot = null,
uint? con1ToSlot = null,
uint? con2ToSlot = null,
uint? con3ToSlot = null)
{
Controller0.ConnectSlot = con0ToSlot;
Controller1.ConnectSlot = con1ToSlot;
Controller2.ConnectSlot = con2ToSlot;
Controller3.ConnectSlot = con3ToSlot;
} }
} }
}
}

View File

@ -1,44 +1,61 @@
using AxibugEmuOnline.Client.ClientCore; using System;
using System;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
using System.Xml.Linq; using System.Xml.Linq;
using AxibugEmuOnline.Client.ClientCore;
using UnityEditor;
using UnityEngine; using UnityEngine;
using VirtualNes.Core; using VirtualNes.Core;
using VirtualNes.Core.Debug; using VirtualNes.Core.Debug;
using Debug = System.Diagnostics.Debug;
namespace AxibugEmuOnline.Client namespace AxibugEmuOnline.Client
{ {
public class NesEmulator : MonoBehaviour, IEmuCore public class NesEmulator : MonoBehaviour, IEmuCore
{ {
public EnumPlatform Platform => EnumPlatform.NES; public VideoProvider VideoProvider;
public AudioProvider AudioProvider;
//模拟器核心实例化对象
//模拟器核心实例化对象
public NES NesCore { get; private set; } public NES NesCore { get; private set; }
//视频驱动这里是Unity接收模拟器画面数据的并渲染出来的实现 /// <summary> 是否暂停 </summary>
public VideoProvider VideoProvider; public bool IsPause { get; private set; }
//音频驱动这里是Unity接收模拟器音频数据的并播放出来的实现
public AudioProvider AudioProvider;
//是否暂停
private bool m_bPause;
/// <summary> 是否暂停 </summary>
public bool IsPause => m_bPause;
private void Start() private void Start()
{ {
//关闭垂直同步 //关闭垂直同步
QualitySettings.vSyncCount = 0; QualitySettings.vSyncCount = 0;
//设为60帧 //设为60帧
Application.targetFrameRate = 60; Application.targetFrameRate = 60;
VideoProvider.NesEmu = this; VideoProvider.NesEmu = this;
AudioProvider.NesEmu = this; AudioProvider.NesEmu = this;
} }
/// <summary> /// <summary>
/// 指定ROM开始游戏 /// Unity的逐帧驱动
/// </summary>
private unsafe void Update()
{
if (IsPause) return;
if (NesCore != null)
{
PushEmulatorFrame();
if (InGameUI.Instance.IsNetPlay)
FixEmulatorFrame();
var screenBuffer = NesCore.ppu.GetScreenPtr();
VideoProvider.SetDrawData(screenBuffer);
}
}
public EnumPlatform Platform => EnumPlatform.NES;
/// <summary>
/// 指定ROM开始游戏
/// </summary> /// </summary>
/// <param name="rom"></param>
public void StartGame(RomFile rom) public void StartGame(RomFile rom)
{ {
StopGame(); StopGame();
@ -59,78 +76,14 @@ namespace AxibugEmuOnline.Client
} }
} }
/// <summary>
/// 停止游戏
/// </summary>
public void StopGame()
{
NesCore?.Dispose();
NesCore = null;
}
/// <summary>
/// Unity的逐帧驱动
/// </summary>
private unsafe void Update()
{
if (m_bPause) return;
if (NesCore != null)
{
PushEmulatorFrame();
if (InGameUI.Instance.IsNetPlay)
FixEmulatorFrame();
var screenBuffer = NesCore.ppu.GetScreenPtr();
VideoProvider.SetDrawData(screenBuffer);
}
}
//是否跳帧,单机无效
private void FixEmulatorFrame()
{
var skipFrameCount = App.roomMgr.netReplay.GetSkipFrameCount();
if (skipFrameCount > 0) App.log.Debug($"SKIP FRAME : {skipFrameCount}");
for (int i = 0; i < skipFrameCount; i++)
{
if (!PushEmulatorFrame()) break;
}
}
ControllerState lastState;
//推进帧
private bool PushEmulatorFrame()
{
Supporter.SampleInput(NesCore.FrameCount);
var controlState = Supporter.GetControllerState();
//如果未收到Input数据,核心帧不推进
if (!controlState.valid) return false;
#if UNITY_EDITOR
if (controlState != lastState)
{
App.log.Info($"[LOCALDEBUG]{NesCore.FrameCount}-->{controlState}");
}
#endif
NesCore.pad.Sync(controlState);
NesCore.EmulateFrame(true);
lastState = controlState;
return true;
}
public void Pause() public void Pause()
{ {
m_bPause = true; IsPause = true;
} }
public void Resume() public void Resume()
{ {
m_bPause = false; IsPause = false;
} }
@ -155,7 +108,7 @@ namespace AxibugEmuOnline.Client
} }
/// <summary> /// <summary>
/// 获取即时存档 /// 获取即时存档
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
public byte[] GetStateBytes() public byte[] GetStateBytes()
@ -164,21 +117,68 @@ namespace AxibugEmuOnline.Client
} }
/// <summary> /// <summary>
/// 加载即时存档 /// 加载即时存档
/// </summary> /// </summary>
/// <param name="data"></param> /// <param
/// name="data">
/// </param>
public void LoadStateFromBytes(byte[] data) public void LoadStateFromBytes(byte[] data)
{ {
State st = new State(); var st = new State();
st.FromByte(data); st.FromByte(data);
NesCore.LoadState(st); NesCore.LoadState(st);
} }
public uint Frame => NesCore.FrameCount; public uint Frame => NesCore.FrameCount;
/// <summary>
/// 停止游戏
/// </summary>
public void StopGame()
{
NesCore?.Dispose();
NesCore = null;
}
#if UNITY_EDITOR
private ControllerState m_lastState;
#endif
//是否跳帧,单机无效
private void FixEmulatorFrame()
{
var skipFrameCount = App.roomMgr.netReplay.GetSkipFrameCount();
if (skipFrameCount > 0) App.log.Debug($"SKIP FRAME : {skipFrameCount}");
for (var i = 0; i < skipFrameCount; i++)
if (!PushEmulatorFrame())
break;
}
//推进帧
private bool PushEmulatorFrame()
{
Supporter.SampleInput(NesCore.FrameCount);
var controlState = Supporter.GetControllerState();
//如果未收到Input数据,核心帧不推进
if (!controlState.valid) return false;
#if UNITY_EDITOR
if (controlState != m_lastState) App.log.Info($"[LOCALDEBUG]{NesCore.FrameCount}-->{controlState}");
m_lastState = controlState;
#endif
NesCore.pad.Sync(controlState);
NesCore.EmulateFrame(true);
return true;
}
#if UNITY_EDITOR #if UNITY_EDITOR
/// <summary> /// <summary>
/// 编辑器用 /// 编辑器用
/// </summary> /// </summary>
[Conditional("UNITY_EDITOR")] [Conditional("UNITY_EDITOR")]
[ContextMenu("ImportNesDB")] [ContextMenu("ImportNesDB")]
@ -189,22 +189,23 @@ namespace AxibugEmuOnline.Client
var xmlStr = File.ReadAllText("nes20db.xml"); var xmlStr = File.ReadAllText("nes20db.xml");
var xml = XDocument.Parse(xmlStr); var xml = XDocument.Parse(xmlStr);
var games = xml.Element("nes20db").Elements("game"); var games = xml.Element("nes20db")?.Elements("game");
Debug.Assert(games != null, nameof(games) + " != null");
foreach (var game in games) foreach (var game in games)
{ {
var crcStr = game.Element("rom").Attribute("crc32").Value; var crcStr = game.Element("rom")?.Attribute("crc32")?.Value;
var crc = uint.Parse($"{crcStr}", System.Globalization.NumberStyles.HexNumber); var crc = uint.Parse($"{crcStr}", NumberStyles.HexNumber);
var mapper = int.Parse($"{game.Element("pcb").Attribute("mapper").Value}"); var mapper = int.Parse($"{game.Element("pcb")?.Attribute("mapper")?.Value}");
if (mapper > 255) continue; if (mapper > 255) continue;
db.AddInfo(new RomDB.RomInfo { CRC = crc, Mapper = mapper }); db.AddInfo(new RomDB.RomInfo { CRC = crc, Mapper = mapper });
} }
UnityEditor.EditorUtility.SetDirty(db); EditorUtility.SetDirty(db);
UnityEditor.AssetDatabase.SaveAssets(); AssetDatabase.SaveAssets();
} }
#endif #endif
} }
} }

View File

@ -1,4 +1,4 @@
using AxibugEmuOnline.Client.ClientCore; using AxibugEmuOnline.Client.ClientCore;
using AxibugEmuOnline.Client.UI; using AxibugEmuOnline.Client.UI;
using UnityEngine; using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
@ -59,7 +59,7 @@ namespace AxibugEmuOnline.Client
{ {
if (!m_romfile.InfoReady) if (!m_romfile.InfoReady)
{ {
SetBaseInfo("正在拉取", "---", "---"); SetBaseInfo("正在拉取", "---", "---");
} }
else else
{ {
@ -82,8 +82,6 @@ namespace AxibugEmuOnline.Client
} }
else else
{ {
//加载一个用户自己提供的Rom时,使用这个方法
//App.emu.BeginGame(App.nesRomLib.GetExistRom("bad_apple_2_5.nes"));
App.emu.BeginGame(m_romfile); App.emu.BeginGame(m_romfile);
return false; return false;

View File

@ -1,15 +1,21 @@
using AxibugEmuOnline.Client.ClientCore; using System.Collections.Generic;
using AxibugEmuOnline.Client.ClientCore;
using AxibugEmuOnline.Client.Event; using AxibugEmuOnline.Client.Event;
using System; using AxibugProtobuf;
using System.Collections.Generic;
namespace AxibugEmuOnline.Client namespace AxibugEmuOnline.Client
{ {
public class InGameUI : CommandExecuter public class InGameUI : CommandExecuter
{ {
private bool m_delayCreateRoom;
private object m_state;
private StepPerformer m_stepPerformer;
private readonly List<OptionMenu> menus = new();
public static InGameUI Instance { get; private set; } public static InGameUI Instance { get; private set; }
public RomFile RomFile => m_rom; public RomFile RomFile { get; private set; }
public override bool Enable => gameObject.activeInHierarchy; public override bool Enable => gameObject.activeInHierarchy;
/// <summary> 指示该游戏实例是否处于联机模式 </summary> /// <summary> 指示该游戏实例是否处于联机模式 </summary>
@ -19,18 +25,13 @@ namespace AxibugEmuOnline.Client
{ {
if (!App.user.IsLoggedIn) return false; if (!App.user.IsLoggedIn) return false;
if (App.roomMgr.mineRoomMiniInfo == null) return false; if (App.roomMgr.mineRoomMiniInfo == null) return false;
if (App.roomMgr.RoomState <= AxibugProtobuf.RoomGameState.OnlyHost) return false; if (App.roomMgr.RoomState <= RoomGameState.OnlyHost) return false;
return true; return true;
} }
} }
private RomFile m_rom;
public IEmuCore Core { get; private set; } public IEmuCore Core { get; private set; }
private object m_state;
private List<OptionMenu> menus = new List<OptionMenu>();
private StepPerformer m_stepPerformer;
protected override void Awake() protected override void Awake()
{ {
@ -59,36 +60,36 @@ namespace AxibugEmuOnline.Client
{ {
m_state = state; m_state = state;
} }
/// <summary> /// <summary>
/// 读取快速快照 /// 读取快速快照
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public object GetQuickState() public object GetQuickState()
{ {
return m_state; return m_state;
} }
private bool m_delayCreateRoom;
public void Show(RomFile currentRom, IEmuCore core) public void Show(RomFile currentRom, IEmuCore core)
{ {
m_delayCreateRoom = false; m_delayCreateRoom = false;
m_state = null;//清空游戏快照 m_state = null; //清空游戏快照
CommandDispatcher.Instance.RegistController(this); CommandDispatcher.Instance.RegistController(this);
m_rom = currentRom; RomFile = currentRom;
Core = core; Core = core;
m_stepPerformer.Reset(); m_stepPerformer.Reset();
if (!App.roomMgr.InRoom) if (!App.roomMgr.InRoom)
{ {
if (App.user.IsLoggedIn) if (App.user.IsLoggedIn)
App.roomMgr.SendCreateRoom(m_rom.ID, 0, m_rom.Hash); {
else App.roomMgr.SendCreateRoom(RomFile.ID, 0, RomFile.Hash);
{ }
m_delayCreateRoom = true; else
OverlayManager.PopTip("稍后将会建立房间"); {
} m_delayCreateRoom = true;
OverlayManager.PopTip("稍后将会建立房间");
}
} }
Eventer.Instance.RegisterEvent(EEvent.OnLoginSucceed, OnLoggedIn); Eventer.Instance.RegisterEvent(EEvent.OnLoginSucceed, OnLoggedIn);
@ -106,21 +107,18 @@ namespace AxibugEmuOnline.Client
filter.ApplyPreset(preset); filter.ApplyPreset(preset);
App.filter.EnableFilter(filter); App.filter.EnableFilter(filter);
} }
} }
private void OnRoomCreated() private void OnRoomCreated()
{ {
m_delayCreateRoom = false; m_delayCreateRoom = false;
} }
private void OnLoggedIn() private void OnLoggedIn()
{ {
if (m_delayCreateRoom) if (m_delayCreateRoom) App.roomMgr.SendCreateRoom(RomFile.ID, 0, RomFile.Hash);
{ }
App.roomMgr.SendCreateRoom(m_rom.ID, 0, m_rom.Hash);
}
}
private void OnServerStepUpdate(int step) private void OnServerStepUpdate(int step)
{ {
m_stepPerformer.Perform(step); m_stepPerformer.Perform(step);
@ -138,16 +136,17 @@ namespace AxibugEmuOnline.Client
{ {
OverlayManager.PopSideBar(menus, 0, PopMenu_OnHide); OverlayManager.PopSideBar(menus, 0, PopMenu_OnHide);
if (!IsNetPlay)//单人模式暂停模拟器 if (!IsNetPlay) //单人模式暂停模拟器
Core.Pause(); Core.Pause();
} }
//菜单关闭时候 //菜单关闭时候
private void PopMenu_OnHide() private void PopMenu_OnHide()
{ {
if (!IsNetPlay)//单人模式恢复模拟器的暂停 if (!IsNetPlay) //单人模式恢复模拟器的暂停
Core.Resume(); Core.Resume();
} }
public void QuitGame() public void QuitGame()
{ {
Eventer.Instance.UnregisterEvent<int>(EEvent.OnRoomWaitStepChange, OnServerStepUpdate); Eventer.Instance.UnregisterEvent<int>(EEvent.OnRoomWaitStepChange, OnServerStepUpdate);
@ -157,4 +156,4 @@ namespace AxibugEmuOnline.Client
App.emu.StopGame(); App.emu.StopGame();
} }
} }
} }

View File

@ -6,7 +6,7 @@ namespace AxibugEmuOnline.Client
{ {
public class StepPerformer public class StepPerformer
{ {
private InGameUI m_inGameUI; private readonly InGameUI m_inGameUI;
private int m_step = -1; private int m_step = -1;
public StepPerformer(InGameUI inGameUI) public StepPerformer(InGameUI inGameUI)

View File

@ -1,28 +1,25 @@
using AxibugEmuOnline.Client.ClientCore; using AxibugEmuOnline.Client.ClientCore;
using AxibugEmuOnline.Client.Event; using AxibugEmuOnline.Client.Event;
using AxibugEmuOnline.Client.Manager; using AxibugEmuOnline.Client.Manager;
using AxibugEmuOnline.Client.UI; using AxibugEmuOnline.Client.UI;
using AxibugProtobuf; using AxibugProtobuf;
using UnityEngine; using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
using Debug = System.Diagnostics.Debug;
namespace AxibugEmuOnline.Client namespace AxibugEmuOnline.Client
{ {
public class RoomItem : MenuItem, IVirtualItem public class RoomItem : MenuItem, IVirtualItem
{ {
[SerializeField] [SerializeField] Image m_roomPreview;
Image m_roomPreview; [SerializeField] Slider m_downloadProgress;
[SerializeField] [SerializeField] GameObject m_downloadingFlag;
Slider m_downloadProgress; [SerializeField] GameObject m_romReadyFlag;
[SerializeField]
GameObject m_downloadingFlag;
[SerializeField]
GameObject m_romReadyFlag;
private RomFile m_romFile; private RomFile m_romFile;
public int Index { get; set; } public int Index { get; set; }
public int roomID { get; private set; } public int RoomID { get; private set; }
protected override void Awake() protected override void Awake()
{ {
@ -33,7 +30,7 @@ namespace AxibugEmuOnline.Client
private void OnRoomSignelUpdate(int roomID) private void OnRoomSignelUpdate(int roomID)
{ {
if (this.roomID != roomID) return; if (this.RoomID != roomID) return;
if (App.roomMgr.GetRoomListMiniInfo(roomID, out var roomInfo)) if (App.roomMgr.GetRoomListMiniInfo(roomID, out var roomInfo))
UpdateUI(roomInfo); UpdateUI(roomInfo);
@ -41,8 +38,10 @@ namespace AxibugEmuOnline.Client
public void SetData(object data) public void SetData(object data)
{ {
var roomInfo = data as Protobuf_Room_MiniInfo; Debug.Assert(data is Protobuf_Room_MiniInfo);
roomID = roomInfo.RoomID;
var roomInfo = (Protobuf_Room_MiniInfo)data;
RoomID = roomInfo.RoomID;
UpdateUI(roomInfo); UpdateUI(roomInfo);
} }
@ -58,20 +57,19 @@ namespace AxibugEmuOnline.Client
} }
else else
{ {
if (!App.roomMgr.GetRoomListMiniInfo(roomID, out Protobuf_Room_MiniInfo MiniInfo)) if (!App.roomMgr.GetRoomListMiniInfo(RoomID, out Protobuf_Room_MiniInfo MiniInfo))
{ {
OverlayManager.PopTip("房间不存在"); OverlayManager.PopTip("房间不存在");
return false; return false;
} }
int[] freeSlots = null; if (!MiniInfo.GetFreeSlot(out var freeSlots))
if (!MiniInfo.GetFreeSlot(out freeSlots))
{ {
OverlayManager.PopTip("无空闲位置"); OverlayManager.PopTip("无空闲位置");
return false; return false;
} }
App.roomMgr.SendJoinRoom(roomID, freeSlots[0]); App.roomMgr.SendJoinRoom(RoomID, freeSlots[0]);
return true; return true;
} }
} }
@ -80,12 +78,12 @@ namespace AxibugEmuOnline.Client
{ {
var hostNick = roomInfo.GetHostNickName(); var hostNick = roomInfo.GetHostNickName();
roomInfo.GetRoomPlayers(out var cur, out var max); roomInfo.GetRoomPlayers(out var cur, out var max);
SetBaseInfo("--", $"<b>{hostNick}</b>的房间", $"{cur}/{max}"); SetBaseInfo("--", $"<b>{hostNick}</b>的房间", $"{cur}/{max}");
SetIcon(null); SetIcon(null);
roomInfo.FetchRomFileInRoomInfo(EnumPlatform.NES, (room, romFile) => roomInfo.FetchRomFileInRoomInfo(EnumPlatform.NES, (room, romFile) =>
{ {
if (room.RoomID != roomID) return; if (room.RoomID != RoomID) return;
m_romFile = romFile; m_romFile = romFile;
Txt.text = romFile.Alias; Txt.text = romFile.Alias;
@ -139,4 +137,4 @@ namespace AxibugEmuOnline.Client
Reset(); Reset();
} }
} }
} }

View File

@ -2,7 +2,7 @@
namespace VirtualNes.Core namespace VirtualNes.Core
{ {
public struct ControllerState public struct ControllerState : IEquatable<ControllerState>
{ {
public uint raw0; public uint raw0;
public uint raw1; public uint raw1;
@ -11,19 +11,49 @@ namespace VirtualNes.Core
public bool valid; public bool valid;
public ControllerState( public ControllerState(EnumButtonType[] states)
EnumButtonType player0_buttons,
EnumButtonType player1_buttons,
EnumButtonType player2_buttons,
EnumButtonType player3_buttons)
{ {
raw0 = (uint)player0_buttons; raw0 = (uint)states[0];
raw1 = (uint)player1_buttons; raw1 = (uint)states[1];
raw2 = (uint)player2_buttons; raw2 = (uint)states[2];
raw3 = (uint)player3_buttons; raw3 = (uint)states[3];
valid = true; valid = true;
} }
public bool HasButton(int player, EnumButtonType button)
{
uint raw = player switch
{
0 => raw0,
1 => raw1,
2 => raw2,
3 => raw3,
_ => 0
};
return (raw & (uint)button) == (uint)button;
}
public override string ToString()
{
return $"{raw0}|{raw1}|{raw2}|{raw3}";
}
#region Impl_Equals
public bool Equals(ControllerState other)
{
return raw0 == other.raw0 && raw1 == other.raw1 && raw2 == other.raw2 && raw3 == other.raw3 && valid == other.valid;
}
public override bool Equals(object obj)
{
return obj is ControllerState other && Equals(other);
}
public override int GetHashCode()
{
return HashCode.Combine(raw0, raw1, raw2, raw3, valid);
}
public static bool operator ==(ControllerState left, ControllerState right) public static bool operator ==(ControllerState left, ControllerState right)
{ {
return return
@ -37,24 +67,7 @@ namespace VirtualNes.Core
{ {
return !(left == right); return !(left == right);
} }
#endregion
public override string ToString()
{
return $"{raw0}|{raw1}|{raw2}|{raw3}";
}
public bool HasButton(int player, EnumButtonType button)
{
uint raw = 0;
switch (player)
{
case 0: raw = raw0; break;
case 1: raw = raw1; break;
case 2: raw = raw2; break;
case 3: raw = raw3; break;
}
return (raw & (uint)button) == (uint)button;
}
} }
[Flags] [Flags]

View File

@ -5,6 +5,7 @@ namespace VirtualNes.Core
public static class Supporter public static class Supporter
{ {
private static ISupporterImpl s_support; private static ISupporterImpl s_support;
public static void Setup(ISupporterImpl supporter) public static void Setup(ISupporterImpl supporter)
{ {
s_support = supporter; s_support = supporter;
@ -44,6 +45,7 @@ namespace VirtualNes.Core
{ {
s_support.SaveFile(fileData, directPath, fileName); s_support.SaveFile(fileData, directPath, fileName);
} }
public static Stream OpenFile(string directPath, string fileName) public static Stream OpenFile(string directPath, string fileName)
{ {
return s_support.OpenFile(directPath, fileName); return s_support.OpenFile(directPath, fileName);
@ -64,6 +66,11 @@ namespace VirtualNes.Core
s_support.SampleInput(frameCount); s_support.SampleInput(frameCount);
} }
public static IControllerSetuper GetControllerSetuper()
{
return s_support.GetControllerSetuper();
}
public static EmulatorConfig Config => s_support.Config; public static EmulatorConfig Config => s_support.Config;
} }
@ -82,5 +89,21 @@ namespace VirtualNes.Core
bool TryGetMapperNo(ROM rom, out int mapperNo); bool TryGetMapperNo(ROM rom, out int mapperNo);
ControllerState GetControllerState(); ControllerState GetControllerState();
void SampleInput(uint frameCount); void SampleInput(uint frameCount);
IControllerSetuper GetControllerSetuper();
} }
}
/// <summary>
/// 负责管理本地控制器与具体游戏之间的槽位分配
/// </summary>
public interface IControllerSetuper
{
/// <summary>
/// 设置本地手柄与游戏手柄槽位的映射
/// </summary>
void SetConnect(
uint? con0ToSlot = null,
uint? con1ToSlot = null,
uint? con2ToSlot = null,
uint? con3ToSlot = null);
}
}