AxibugEmuOnline/AxibugEmuOnline.Client/Assets/Script/AppMain/Manager/SaveSlotManager/SaveFile.cs
2025-08-21 10:34:57 +08:00

370 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using AxibugEmuOnline.Client.ClientCore;
using AxibugEmuOnline.Client.Tools;
using AxibugProtobuf;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using UnityEngine;
using SyncingFileRecord = System.Collections.Generic.Dictionary<AxibugProtobuf.RomPlatformType, System.Collections.Generic.Dictionary<int, System.Collections.Generic.HashSet<AxibugEmuOnline.Client.SaveFile.SyncingFiles>>>;
namespace AxibugEmuOnline.Client
{
/// <summary> 存档文件抽象类 </summary>
public partial class SaveFile
{
public SavCloudApi CloudAPI => App.SavMgr.CloudApi;
/// <summary> 指示该存档是否是自动存档 </summary>
public bool AutoSave => SlotIndex == 0;
/// <summary> 指示该存档所在槽位 </summary>
public int SlotIndex { get; private set; }
/// <summary> 指示该存档所属Rom的ID </summary>
public int RomID { get; private set; }
/// <summary> 指示该存档所属模拟器平台 </summary>
public RomPlatformType EmuPlatform { get; private set; }
/// <summary> 指示该存档是否为空 </summary>
public bool IsEmpty { get; private set; }
/// <summary> 存档文件路径 </summary>
public string FilePath
{
get
{
var path = App.UserPersistenDataPath(EmuPlatform);
path = $"{path}/Slot/{RomID}";
AxiIO.Directory.CreateDirectory(path);
var filePath = $"{path}/slot{SlotIndex}.SlotSav";
return filePath;
}
}
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 GetCurrentState()
{
return FSM.CurrentState;
}
public event Action OnSavSuccessed;
public event Action OnStateChanged;
/// <summary> 存档顺序号,用于判断云存档和本地存档的同步状态,是否存在冲突 </summary>
public uint Sequecen { get; private set; }
public SimpleFSM<SaveFile> FSM { get; private set; }
byte[] m_savDataCaches;
byte[] m_screenShotCaches;
Header m_headerCache;
bool m_cacheOutdate = true;
public SaveFile(int romID, RomPlatformType platform, int slotIndex)
{
RomID = romID;
EmuPlatform = platform;
SlotIndex = slotIndex;
FSM = new SimpleFSM<SaveFile>(this);
FSM.AddState<IdleState>();
FSM.AddState<CheckingNetworkState>();//
FSM.AddState<CheckingState>();
FSM.AddState<DownloadingState>();
FSM.AddState<ConflictState>();
FSM.AddState<UploadingState>();
FSM.AddState<SyncedState>();
FSM.AddState<SyncFailedState>();
FSM.OnStateChanged += FSM_OnStateChanged;
IsEmpty = !AxiIO.File.Exists(FilePath);
if (IsEmpty) Sequecen = 0;
else //从文件头读取存储序号
{
byte[] saveOrderData = new byte[4];
int res = AxiIO.File.ReadBytesToArr(FilePath, saveOrderData, 0, 4);
if (res < 4) //无效的存档文件
{
IsEmpty = true;
AxiIO.File.Delete(FilePath);
}
else
{
Sequecen = BitConverter.ToUInt32(saveOrderData, 0);
}
}
FSM.ChangeState<IdleState>();
}
private void FSM_OnStateChanged()
{
OnStateChanged?.Invoke();
}
public void Update()
{
FSM.Update();
}
/// <summary>
/// 获得存档的保存时间(UTC)
/// </summary>
/// <returns></returns>
public DateTime GetSavTimeUTC()
{
GetSavData(out _, out _);
return new DateTime((long)m_headerCache.SavTicks, DateTimeKind.Utc);
}
public unsafe void GetSavData(out byte[] savData, out byte[] screenShotData)
{
if (!m_cacheOutdate)
{
savData = m_savDataCaches;
screenShotData = m_screenShotCaches;
return;
}
m_cacheOutdate = false;
savData = null;
screenShotData = null;
if (!AxiIO.File.Exists(FilePath)) return;
var raw = AxiIO.File.ReadAllBytes(FilePath);
int headerSize = Marshal.SizeOf(typeof(Header));
if (raw.Length < headerSize)
{
App.log.Warning("无效存档");
return;
}
m_headerCache = new Header();
fixed (Header* headPtr = &m_headerCache)
{
var headP = (byte*)headPtr;
Marshal.Copy(raw, 0, (IntPtr)headP, sizeof(Header));
}
savData = new byte[m_headerCache.DataLength];
Array.Copy(raw, headerSize, savData, 0, savData.Length);
screenShotData = new byte[m_headerCache.ScreenShotLength];
Array.Copy(raw, headerSize + savData.Length, screenShotData, 0, screenShotData.Length);
m_savDataCaches = savData;
m_screenShotCaches = screenShotData;
return;
}
public unsafe void Save(uint sequence, byte[] savData, byte[] screenShotData)
{
if (IsBusy) return;
var filePath = FilePath;
var header = new Header
{
Sequence = sequence,
RomID = RomID,
SavTicks = (ulong)DateTime.UtcNow.Ticks,
DataLength = (uint)savData.Length,
ScreenShotLength = (uint)screenShotData.Length,
};
int headerSize = Marshal.SizeOf(typeof(Header));
IntPtr ptr = Marshal.AllocHGlobal(headerSize);
var totalSize = headerSize + savData.Length + screenShotData.Length;
byte[] raw = new byte[totalSize];
try
{
Marshal.StructureToPtr(header, ptr, false);
Marshal.Copy(ptr, raw, 0, headerSize);
}
finally
{
Marshal.FreeHGlobal(ptr);
}
Array.Copy(savData, 0, raw, headerSize, savData.Length);
Array.Copy(screenShotData, 0, raw, headerSize + savData.Length, screenShotData.Length);
AxiIO.File.WriteAllBytes(filePath, raw);
Sequecen = sequence;
m_headerCache = header;
m_savDataCaches = savData;
m_screenShotCaches = screenShotData;
IsEmpty = false;
OnSavSuccessed?.Invoke();
}
/// <summary>
/// 尝试同步存档
/// </summary>
public void TrySync()
{
if (FSM.CurrentState is not IdleState && FSM.CurrentState is not SyncedState && FSM.CurrentState is not SyncFailedState) return;
FSM.ChangeState<CheckingNetworkState>();
}
private void SetSavingFlag()
{
SyncingFilesUtility.Add(this);
}
private void ClearSavingFlag()
{
SyncingFilesUtility.Remove(this);
}
public override string ToString()
{
return $"{EmuPlatform}|{RomID}|{SlotIndex}";
}
public static class SyncingFilesUtility
{
static SyncingFileRecord m_syncFiles = new SyncingFileRecord();
public static void ReadFromPlayprefs()
{
var jsonStr = AxiPlayerPrefs.GetString("SYNCING_SAVE");
if (string.IsNullOrEmpty(jsonStr)) return;
var temp = JsonUtility.FromJson<SyncingFilesList>(jsonStr);
if (temp.List == null || temp.List.Count == 0) return;
foreach (var file in temp.List)
{
Add(file);
}
}
public static void ContinueSync()
{
var allFiles = m_syncFiles.Values.SelectMany(v => v.Values).SelectMany(v => v);
foreach (var file in allFiles)
{
var savFile = App.SavMgr.GetSaveFile(file.romID, file.platform, file.slotIndex);
savFile.TrySync();
}
}
public static void Add(SaveFile savFile)
{
SyncingFiles file = new SyncingFiles { romID = savFile.RomID, platform = savFile.EmuPlatform, slotIndex = savFile.SlotIndex };
Add(file);
}
private static void Add(SyncingFiles file)
{
if (!m_syncFiles.TryGetValue(file.platform, out var mapper))
{
mapper = new Dictionary<int, HashSet<SyncingFiles>>();
m_syncFiles.Add(file.platform, mapper);
}
if (!mapper.TryGetValue(file.romID, out var syncingTables))
{
syncingTables = new HashSet<SyncingFiles>();
mapper[file.romID] = syncingTables;
}
if (syncingTables.Add(file))
{
WritePlayerprefs();
}
}
public static void Remove(SaveFile savFile)
{
SyncingFiles file = new SyncingFiles { romID = savFile.RomID, platform = savFile.EmuPlatform, slotIndex = savFile.SlotIndex };
if (!m_syncFiles.TryGetValue(file.platform, out var mapper))
{
return;
}
if (!mapper.TryGetValue(savFile.RomID, out var syncingTables))
{
return;
}
if (syncingTables.Remove(file))
{
WritePlayerprefs();
}
}
private static void WritePlayerprefs()
{
var allFiles = m_syncFiles.Values.SelectMany(v => v.Values).SelectMany(v => v);
var temp = new SyncingFilesList { List = new List<SyncingFiles>(allFiles) };
var jsonStr = JsonUtility.ToJson(temp);
AxiPlayerPrefs.SetString("SYNCING_SAVE", jsonStr);
}
}
[Serializable]
class SyncingFilesList
{
[SerializeField]
public List<SyncingFiles> List;
}
[Serializable]
public struct SyncingFiles
{
public int romID;
public RomPlatformType platform;
public int slotIndex;
public override int GetHashCode()
{
return HashCode.Combine(romID, platform, slotIndex);
}
public override bool Equals(object obj)
{
if (obj == null) return false;
if (obj.GetHashCode() != GetHashCode()) return false;
return true;
}
}
[StructLayout(LayoutKind.Explicit, Size = 24)]
struct Header
{
[FieldOffset(0)]
public uint Sequence;
[FieldOffset(4)]
public int RomID;
[FieldOffset(8)]
public ulong SavTicks;
[FieldOffset(16)]
public uint DataLength;
[FieldOffset(20)]
public uint ScreenShotLength;
}
}
}