1
0

存档支持云同步

This commit is contained in:
ALIENJACK\alien 2025-04-18 13:38:45 +08:00
parent 13cddd8a3b
commit eb2f5d268c
15 changed files with 236 additions and 112 deletions

View File

@ -1,6 +1,7 @@
using AxibugEmuOnline.Client.ClientCore; using AxibugEmuOnline.Client.ClientCore;
using AxibugEmuOnline.Client.Tools; using AxibugEmuOnline.Client.Tools;
using AxibugProtobuf; using AxibugProtobuf;
using MAME.Core;
using System; using System;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -8,7 +9,7 @@ using System.Runtime.InteropServices;
namespace AxibugEmuOnline.Client namespace AxibugEmuOnline.Client
{ {
/// <summary> 存档文件抽象类 </summary> /// <summary> 存档文件抽象类 </summary>
public class SaveFile public partial class SaveFile
{ {
public SavCloudApi CloudAPI => App.SavMgr.CloudApi; public SavCloudApi CloudAPI => App.SavMgr.CloudApi;
@ -38,7 +39,24 @@ namespace AxibugEmuOnline.Client
} }
} }
public bool IsBusy
{
get
{
if (FSM.CurrentState is DownloadingState) return true;
else if (FSM.CurrentState is UploadingState) return true;
return false;
}
}
public SimpleFSM<SaveFile>.State GetState()
{
return FSM.CurrentState;
}
public event Action OnSavSuccessed; public event Action OnSavSuccessed;
public event Action OnStateChanged;
/// <summary> 存档顺序号,用于判断云存档和本地存档的同步状态,是否存在冲突 </summary> /// <summary> 存档顺序号,用于判断云存档和本地存档的同步状态,是否存在冲突 </summary>
public uint Sequecen { get; private set; } public uint Sequecen { get; private set; }
@ -55,11 +73,12 @@ namespace AxibugEmuOnline.Client
EmuPlatform = platform; EmuPlatform = platform;
SlotIndex = slotIndex; SlotIndex = slotIndex;
FSM = new SimpleFSM<SaveFile>(this); FSM = new SimpleFSM<SaveFile>(this);
FSM.AddState<UnkownState>(); FSM.AddState<IdleState>();
FSM.AddState<CheckingState>(); FSM.AddState<CheckingState>();
FSM.AddState<DownloadingState>(); FSM.AddState<DownloadingState>();
FSM.AddState<UploadingState>(); FSM.AddState<UploadingState>();
FSM.AddState<SyncedState>(); FSM.AddState<SyncedState>();
FSM.OnStateChanged += FSM_OnStateChanged;
IsEmpty = !File.Exists(FilePath); IsEmpty = !File.Exists(FilePath);
@ -82,7 +101,12 @@ namespace AxibugEmuOnline.Client
streaming.Dispose(); streaming.Dispose();
} }
FSM.ChangeState<UnkownState>(); FSM.ChangeState<IdleState>();
}
private void FSM_OnStateChanged()
{
OnStateChanged?.Invoke();
} }
public void Update() public void Update()
@ -145,6 +169,8 @@ namespace AxibugEmuOnline.Client
public unsafe void Save(uint sequence, byte[] savData, byte[] screenShotData) public unsafe void Save(uint sequence, byte[] savData, byte[] screenShotData)
{ {
if (IsBusy) return;
var filePath = FilePath; var filePath = FilePath;
var header = new Header var header = new Header
@ -190,9 +216,9 @@ namespace AxibugEmuOnline.Client
/// </summary> /// </summary>
public void TrySync() public void TrySync()
{ {
if (FSM.CurrentState is not UnkownState && FSM.CurrentState is not SyncedState) return; if (FSM.CurrentState is not IdleState && FSM.CurrentState is not SyncedState) return;
FSM.ChangeState<CheckingState>(); FSM.ChangeState<CheckingNetworkState>();
} }
@ -210,14 +236,5 @@ namespace AxibugEmuOnline.Client
[FieldOffset(20)] [FieldOffset(20)]
public uint ScreenShotLength; public uint ScreenShotLength;
} }
public enum EnumState
{
Unkown,
Checking,
Downloading,
Uploading,
Synced
}
} }
} }

View File

@ -0,0 +1,21 @@
using AxibugEmuOnline.Client.ClientCore;
using AxibugEmuOnline.Client.Tools;
using System;
namespace AxibugEmuOnline.Client
{
public partial class SaveFile
{
public class CheckingNetworkState : SimpleFSM<SaveFile>.State
{
public override void OnUpdate()
{
if (App.network.isConnected)
{
FSM.ChangeState<CheckingState>();
}
}
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 288e73ce3cb773e46b7d37a53a3b43ee

View File

@ -4,45 +4,48 @@ using UnityEngine;
namespace AxibugEmuOnline.Client namespace AxibugEmuOnline.Client
{ {
public class CheckingState : SimpleFSM<SaveFile>.State public partial class SaveFile
{ {
private float m_timeOut; public class CheckingState : SimpleFSM<SaveFile>.State
public Protobuf_Mine_GameSavInfo NetData { get; private set; }
public override void OnEnter(SimpleFSM<SaveFile>.State preState)
{ {
m_timeOut = 5f; private float m_timeOut;
Host.CloudAPI.OnFetchGameSavList += CloudAPI_OnFetchGameSavList;
Host.CloudAPI.SendGetGameSavList(Host.RomID);
}
public override void OnExit(SimpleFSM<SaveFile>.State nextState) public Protobuf_Mine_GameSavInfo NetData { get; private set; }
{
Host.CloudAPI.OnFetchGameSavList -= CloudAPI_OnFetchGameSavList;
}
public override void OnUpdate() public override void OnEnter(SimpleFSM<SaveFile>.State preState)
{
m_timeOut -= Time.deltaTime;
if (m_timeOut < 0) //已超时
{ {
FSM.ChangeState<UnkownState>(); m_timeOut = 5f;
Host.CloudAPI.OnFetchGameSavList += CloudAPI_OnFetchGameSavList;
Host.CloudAPI.SendGetGameSavList(Host.RomID);
} }
}
private void CloudAPI_OnFetchGameSavList(int romID, Protobuf_Mine_GameSavInfo[] savSlotData) public override void OnExit(SimpleFSM<SaveFile>.State nextState)
{
if (romID != Host.RomID) return;
NetData = savSlotData[Host.SlotIndex];
if (NetData == null) //云存档不存在,上传本地存档
{ {
FSM.ChangeState<UploadingState>(); Host.CloudAPI.OnFetchGameSavList -= CloudAPI_OnFetchGameSavList;
} }
else
public override void OnUpdate()
{ {
FSM.ChangeState<DownloadingState>(); m_timeOut -= Time.deltaTime;
if (m_timeOut < 0) //已超时
{
FSM.ChangeState<IdleState>();
}
}
private void CloudAPI_OnFetchGameSavList(int romID, Protobuf_Mine_GameSavInfo[] savSlotData)
{
if (romID != Host.RomID) return;
NetData = savSlotData[Host.SlotIndex];
if (NetData == null) //云存档不存在,上传本地存档
{
FSM.ChangeState<UploadingState>();
}
else
{
FSM.ChangeState<DownloadingState>();
}
} }
} }
} }

View File

@ -2,8 +2,11 @@
namespace AxibugEmuOnline.Client namespace AxibugEmuOnline.Client
{ {
public class ConflictState : SimpleFSM<SaveFile>.State public partial class SaveFile
{ {
public class ConflictState : SimpleFSM<SaveFile>.State
{
}
} }
} }

View File

@ -3,51 +3,54 @@ using AxibugEmuOnline.Client.Tools;
namespace AxibugEmuOnline.Client namespace AxibugEmuOnline.Client
{ {
public class DownloadingState : SimpleFSM<SaveFile>.State public partial class SaveFile
{ {
uint m_sequece; public class DownloadingState : SimpleFSM<SaveFile>.State
private AxiHttpProxy.SendDownLoadProxy m_downloadTask;
private AxiHttpProxy.SendDownLoadProxy m_downloadTaskImg;
public override void OnEnter(SimpleFSM<SaveFile>.State preState)
{ {
var checkState = preState as CheckingState; uint m_sequece;
private AxiHttpProxy.SendDownLoadProxy m_downloadTask;
private AxiHttpProxy.SendDownLoadProxy m_downloadTaskImg;
var netData = checkState.NetData; public override void OnEnter(SimpleFSM<SaveFile>.State preState)
if (Host.Sequecen >= (uint)netData.Sequence)
{ {
FSM.ChangeState<ConflictState>(); var checkState = preState as CheckingState;
return;
var netData = checkState.NetData;
if (Host.Sequecen >= (uint)netData.Sequence)
{
FSM.ChangeState<ConflictState>();
return;
}
m_sequece = (uint)netData.Sequence;
m_downloadTask = AxiHttpProxy.GetDownLoad($"{App.httpAPI.WebHost}/{netData.SavUrl}");
m_downloadTaskImg = AxiHttpProxy.GetDownLoad($"{App.httpAPI.WebHost}/{netData.SavImgUrl}");
} }
m_sequece = (uint)netData.Sequence; public override void OnUpdate()
m_downloadTask = AxiHttpProxy.GetDownLoad($"{App.httpAPI.WebHost}/{netData.SavUrl}");
m_downloadTaskImg = AxiHttpProxy.GetDownLoad($"{App.httpAPI.WebHost}/{netData.SavImgUrl}");
}
public override void OnUpdate()
{
if (!m_downloadTask.downloadHandler.isDone) return;
if (m_downloadTask.downloadHandler.bHadErr) //下载失败
{ {
FSM.ChangeState<UnkownState>(); if (!m_downloadTask.downloadHandler.isDone) return;
return;
if (m_downloadTask.downloadHandler.bHadErr) //下载失败
{
FSM.ChangeState<IdleState>();
return;
}
if (!m_downloadTaskImg.downloadHandler.isDone) return;
if (m_downloadTaskImg.downloadHandler.bHadErr) //下载失败
{
FSM.ChangeState<IdleState>();
return;
}
var savData = Host.CloudAPI.UnGzipData(m_downloadTask.downloadHandler.data);
var imgData = Host.CloudAPI.UnGzipData(m_downloadTaskImg.downloadHandler.data);
Host.Save(m_sequece, savData, imgData);
FSM.ChangeState<SyncedState>();
} }
if (!m_downloadTaskImg.downloadHandler.isDone) return;
if (m_downloadTaskImg.downloadHandler.bHadErr) //下载失败
{
FSM.ChangeState<UnkownState>();
return;
}
var savData = Host.CloudAPI.UnGzipData(m_downloadTask.downloadHandler.data);
var imgData = Host.CloudAPI.UnGzipData(m_downloadTaskImg.downloadHandler.data);
Host.Save(m_sequece, savData, imgData);
FSM.ChangeState<SyncedState>();
} }
} }
} }

View File

@ -0,0 +1,11 @@
using AxibugEmuOnline.Client.Tools;
namespace AxibugEmuOnline.Client
{
public partial class SaveFile
{
public class IdleState : SimpleFSM<SaveFile>.State
{
}
}
}

View File

@ -2,7 +2,10 @@
namespace AxibugEmuOnline.Client namespace AxibugEmuOnline.Client
{ {
public class SyncedState : SimpleFSM<SaveFile>.State public partial class SaveFile
{ {
public class SyncedState : SimpleFSM<SaveFile>.State
{
}
} }
} }

View File

@ -1,8 +0,0 @@
using AxibugEmuOnline.Client.Tools;
namespace AxibugEmuOnline.Client
{
public class UnkownState : SimpleFSM<SaveFile>.State
{
}
}

View File

@ -2,27 +2,30 @@
namespace AxibugEmuOnline.Client namespace AxibugEmuOnline.Client
{ {
public class UploadingState : SimpleFSM<SaveFile>.State public partial class SaveFile
{ {
public override void OnEnter(SimpleFSM<SaveFile>.State preState) public class UploadingState : SimpleFSM<SaveFile>.State
{ {
Host.CloudAPI.OnUploadedSavData += Api_OnUploadedSavData; public override void OnEnter(SimpleFSM<SaveFile>.State preState)
{
Host.CloudAPI.OnUploadedSavData += Api_OnUploadedSavData;
Host.GetSavData(out byte[] savData, out byte[] screenData); Host.GetSavData(out byte[] savData, out byte[] screenData);
Host.CloudAPI.SendUpLoadGameSav(Host.RomID, Host.SlotIndex, Host.Sequecen, savData, screenData); Host.CloudAPI.SendUpLoadGameSav(Host.RomID, Host.SlotIndex, Host.Sequecen, savData, screenData);
} }
public override void OnExit(SimpleFSM<SaveFile>.State nextState) public override void OnExit(SimpleFSM<SaveFile>.State nextState)
{ {
Host.CloudAPI.OnUploadedSavData -= Api_OnUploadedSavData; Host.CloudAPI.OnUploadedSavData -= Api_OnUploadedSavData;
} }
private void Api_OnUploadedSavData(int romID, int slotIndex, AxibugProtobuf.Protobuf_Mine_GameSavInfo savInfo) private void Api_OnUploadedSavData(int romID, int slotIndex, AxibugProtobuf.Protobuf_Mine_GameSavInfo savInfo)
{ {
if (Host.RomID != romID) return; if (Host.RomID != romID) return;
if (Host.SlotIndex != slotIndex) return; if (Host.SlotIndex != slotIndex) return;
FSM.ChangeState<SyncedState>(); FSM.ChangeState<SyncedState>();
}
} }
} }
} }

View File

@ -4,10 +4,10 @@ using System.Linq;
namespace AxibugEmuOnline.Client.Tools namespace AxibugEmuOnline.Client.Tools
{ {
public class SimpleFSM<HOST> public partial class SimpleFSM<HOST>
{ {
public event Action OnStateChanged;
private Dictionary<Type, State> m_states = new Dictionary<Type, State>(); private Dictionary<Type, State> m_states = new Dictionary<Type, State>();
private State m_current;
public HOST Host { get; private set; } public HOST Host { get; private set; }
@ -16,7 +16,18 @@ namespace AxibugEmuOnline.Client.Tools
Host = host; Host = host;
} }
public State CurrentState => m_current; private State m_current;
public State CurrentState
{
get => m_current;
set
{
if (m_current == value) return;
m_current = value;
OnStateChanged?.Invoke();
}
}
public T AddState<T>() where T : State, new() public T AddState<T>() where T : State, new()
{ {
var stateType = typeof(T); var stateType = typeof(T);

View File

@ -46,6 +46,13 @@ namespace AxibugEmuOnline.Client
m_subOptions.Add(new LoadMenuItem(inGameui, savFile)); m_subOptions.Add(new LoadMenuItem(inGameui, savFile));
} }
public override void OnShow(OptionUI_MenuItem ui)
{
base.OnShow(ui);
SavFile.TrySync();
}
protected override List<InternalOptionMenu> GetOptionMenus() protected override List<InternalOptionMenu> GetOptionMenus()
{ {
return m_subOptions; return m_subOptions;
@ -65,6 +72,13 @@ namespace AxibugEmuOnline.Client
public override void OnExcute(OptionUI optionUI, ref bool cancelHide) public override void OnExcute(OptionUI optionUI, ref bool cancelHide)
{ {
if (m_savFile.IsBusy)
{
OverlayManager.PopTip("存档正在同步中");
cancelHide = true;
return;
}
var stateData = m_ingameUI.Core.GetStateBytes(); var stateData = m_ingameUI.Core.GetStateBytes();
var tex = m_ingameUI.Core.OutputPixel; var tex = m_ingameUI.Core.OutputPixel;
var screenData = tex.ToJPG(m_ingameUI.Core.DrawCanvas.transform.localScale); var screenData = tex.ToJPG(m_ingameUI.Core.DrawCanvas.transform.localScale);

View File

@ -439,7 +439,6 @@ namespace AxibugEmuOnline.Client
public abstract string Name { get; } public abstract string Name { get; }
public virtual Sprite Icon { get; } public virtual Sprite Icon { get; }
public virtual bool Visible => true; public virtual bool Visible => true;
public virtual bool Enable => true;
public virtual void OnFocus() { } public virtual void OnFocus() { }
public virtual void OnShow(OptionUI_MenuItem ui) { } public virtual void OnShow(OptionUI_MenuItem ui) { }

View File

@ -1,4 +1,7 @@
using UnityEngine; using AxibugEmuOnline.Client.Tools;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
using static AxibugEmuOnline.Client.InGameUI_SaveStateMenu; using static AxibugEmuOnline.Client.InGameUI_SaveStateMenu;
@ -10,15 +13,38 @@ namespace AxibugEmuOnline.Client
public Image UI_Empty; public Image UI_Empty;
public Text UI_SavTime; public Text UI_SavTime;
public GameObject UI_Disconnect;
public GameObject UI_Syncing;
public GameObject UI_Conflict;
public GameObject UI_Synced;
Texture2D m_screenTex; Texture2D m_screenTex;
Dictionary<Type, GameObject> m_stateNodes = new Dictionary<Type, GameObject>();
private void Awake()
{
m_stateNodes[typeof(SaveFile.CheckingState)] = UI_Syncing;
m_stateNodes[typeof(SaveFile.ConflictState)] = UI_Conflict;
m_stateNodes[typeof(SaveFile.DownloadingState)] = UI_Syncing;
m_stateNodes[typeof(SaveFile.SyncedState)] = UI_Synced;
m_stateNodes[typeof(SaveFile.UploadingState)] = UI_Syncing;
m_stateNodes[typeof(SaveFile.CheckingNetworkState)] = UI_Disconnect;
}
protected override void OnSetData(InternalOptionMenu menuData) protected override void OnSetData(InternalOptionMenu menuData)
{ {
base.OnSetData(menuData); base.OnSetData(menuData);
RefreshUI(); RefreshUI();
MenuData.SavFile.OnSavSuccessed += SavFile_OnSavSuccessed;
MenuData.SavFile.OnStateChanged += UpdateStateNode;
}
MenuData.SavFile.OnSavSuccessed += RefreshUI; private void SavFile_OnSavSuccessed()
{
MenuData.SavFile.TrySync();
RefreshUI();
} }
private void RefreshUI() private void RefreshUI()
@ -48,6 +74,21 @@ namespace AxibugEmuOnline.Client
m_screenTex.LoadImage(screenShotData); m_screenTex.LoadImage(screenShotData);
UI_ScreenShot.texture = m_screenTex; UI_ScreenShot.texture = m_screenTex;
} }
UpdateStateNode();
}
private void UpdateStateNode()
{
var stateType = MenuData.SavFile.GetState().GetType();
foreach (var item in m_stateNodes)
{
var type = item.Key;
var nodeGo = item.Value;
nodeGo.SetActiveEx(type == stateType);
}
} }
public override void OnHide() public override void OnHide()
@ -60,7 +101,8 @@ namespace AxibugEmuOnline.Client
m_screenTex = null; m_screenTex = null;
} }
MenuData.SavFile.OnSavSuccessed -= RefreshUI; MenuData.SavFile.OnSavSuccessed -= SavFile_OnSavSuccessed;
MenuData.SavFile.OnStateChanged -= UpdateStateNode;
} }
public override void OnExecute(OptionUI optionUI, ref bool cancelHide) public override void OnExecute(OptionUI optionUI, ref bool cancelHide)