feat(upserver): 保存M2客户端KV迁移至本人统计写检查点

保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。
保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。

验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。
包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
This commit is contained in:
Liubing\LB
2026-09-30 19:19:05 +08:00
parent 1c0cdf7a99
commit 6813da149a
511 changed files with 151858 additions and 2376 deletions

View File

@@ -0,0 +1,358 @@
using asap.core;
using PlayFab;
using PlayFab.ClientModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using game.PlayerDataStorage;
public class PlayFabMgr : MonoBehaviour
{
const int USER_DATA_UPDATE_INTERVAL = 40;
const int Buffer_Count = 5;
const string Last_Update_Time_Key_Prefix = "LastUpdateTime";
const string Local_Data_Key_prefix = "LocalData";
private string Last_Update_Time_Key;
private string Local_Data_Key;
private LinkedList<Dictionary<string, string>> dataBuffer;
private Dictionary<string, string> swapData;
private Dictionary<string, long> lastUpdateTime;
private Dictionary<string, string> localData;
private CancellationTokenSource tokenSource;
static PlayFabMgr _instance = null;
// An inactive, never-awakened component proves that no timer or PF send has ever been started.
internal bool IsUnstartedForKVLoad => tokenSource==null && dataBuffer==null && swapData==null && localData==null && lastUpdateTime==null && _instance!=this;
internal static bool HasLiveInstanceForKVLoad => _instance!=null;
public static PlayFabMgr Instance
{
get
{
if (_instance == null)
{
GameDebug.LogError("PlayFabMgr Instance doesn't exist yet!");
}
return _instance;
}
}
public Dictionary<string, string> CompareDatas(Dictionary<string, UserDataRecord> remoteDatas, string userId)
{
#if UNITY_EDITOR && !SERVICE_HOST
if(PlayerKVGameplayLoop.Enabled) throw PlayerKVGameplayLoop.Stop("PF archive comparison is disabled in KV loop.");
#endif
PlayerKVGameLoadGuard.RequireLegacyAllowed(PlayerKVGuardPoint.Compare);
Last_Update_Time_Key = $"{Last_Update_Time_Key_Prefix}_{userId}";
Local_Data_Key = $"{Local_Data_Key_prefix}_{userId}";
var lastUpdateTimeStr = PlayerPrefs.GetString(Last_Update_Time_Key, string.Empty);
var now = ZZTimeHelper.UtcNow().Ticks;
localData = new Dictionary<string, string>();
lastUpdateTime = new Dictionary<string, long>();
if (string.IsNullOrEmpty(lastUpdateTimeStr))
{
if (remoteDatas != null && remoteDatas.Count > 0)
{
lastUpdateTime = remoteDatas.ToDictionary(x => x.Key, x => x.Value.LastUpdated.Ticks);
localData = remoteDatas.ToDictionary(x => x.Key, x => x.Value.Value);
}
}
else
{
var tempLastUpdateTime = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, long>>(lastUpdateTimeStr);
var localDataStr = PlayerPrefs.GetString(Local_Data_Key);
var tempLocalData = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, string>>(localDataStr);
var allKeys = tempLocalData.Keys.Concat(remoteDatas.Keys).Distinct();
foreach (var key in allKeys)
{
var localValue = tempLocalData.ContainsKey(key) ? tempLocalData[key] : null;
var remoteValue = remoteDatas.ContainsKey(key) ? remoteDatas[key].Value : null;
var localTime = tempLastUpdateTime.ContainsKey(key) ? tempLastUpdateTime[key] : 0;
var remoteTime = remoteDatas.ContainsKey(key) ? remoteDatas[key].LastUpdated.Ticks : 0;
if (localValue == null)
{
localData[key] = remoteValue;
lastUpdateTime[key] = remoteTime;
continue;
}
if (remoteValue == null)
{
UpdateUserDataValue(key, localValue);
lastUpdateTime[key] = now;
localData[key] = localValue;
continue;
}
var timeDelta = localTime - remoteTime;
if (Math.Abs(timeDelta) > TimeSpan.TicksPerSecond * 60)
{
if (localTime > remoteTime)
{
UpdateUserDataValue(key, localValue);
lastUpdateTime[key] = now;
localData[key] = localValue;
}
else
{
lastUpdateTime[key] = remoteTime;
localData[key] = remoteValue;
}
}
else
{
lastUpdateTime[key] = localTime;
localData[key] = localValue;
}
}
}
PlayerPrefs.SetString(Last_Update_Time_Key, LitJson.JsonMapper.ToJson(lastUpdateTime));
PlayerPrefs.SetString(Local_Data_Key, LitJson.JsonMapper.ToJson(localData));
return localData;
}
void Awake()
{
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Lifecycle)) return;
_instance = this;
#if UNITY_EDITOR && !SERVICE_HOST
if(PlayerKVGameplayLoop.Enabled) return;
#endif
InitBuffer();
tokenSource = new CancellationTokenSource();
UpdateUserDataValue(tokenSource.Token);
}
#if UNITY_EDITOR && !SERVICE_HOST
internal void StartKVGameplayLoopSync()
{
if(!PlayerKVGameplayLoop.Ready || tokenSource!=null) return;
tokenSource=new CancellationTokenSource();
UpdateUserDataValue(tokenSource.Token);
}
internal void StopKVGameplayLoopSync()
{ tokenSource?.Cancel(); tokenSource?.Dispose(); tokenSource=null; }
#endif
void InitBuffer()
{
dataBuffer = new LinkedList<Dictionary<string, string>>();
for (int i = 0; i < Buffer_Count; i++)
{
dataBuffer.AddLast(new Dictionary<string, string>());
}
swapData = new Dictionary<string, string>();
}
void OnApplicationPause(bool is_pause)
{
#if UNITY_EDITOR && !SERVICE_HOST
if(PlayerKVGameplayLoop.Enabled)
{
if(is_pause) { StopKVGameplayLoopSync(); _=PlayerKVGameplayLoop.FlushObservedAsync(); }
else StartKVGameplayLoopSync();
return;
}
#endif
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Lifecycle)) return;
if (is_pause)
{
tokenSource.Cancel();
tokenSource = null;
SyncData();
}
else
{
tokenSource = new CancellationTokenSource();
UpdateUserDataValue(tokenSource.Token);
}
}
void OnApplicationQuit()
{
#if UNITY_EDITOR && !SERVICE_HOST
if(PlayerKVGameplayLoop.Enabled) { StopKVGameplayLoopSync(); _=PlayerKVGameplayLoop.CloseAsync(); return; }
#endif
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Lifecycle)) return;
tokenSource.Cancel();
tokenSource = null;
SyncData();
}
void OnDestroy()
{
#if UNITY_EDITOR && !SERVICE_HOST
if(PlayerKVGameplayLoop.Enabled) { StopKVGameplayLoopSync(); _=PlayerKVGameplayLoop.CloseAsync(); return; }
#endif
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Lifecycle)) return;
tokenSource?.Cancel();
tokenSource = null;
if(!PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.PrefsSave)) PlayerPrefs.Save();
SyncData();
}
private async void UpdateUserDataValue(CancellationToken token)
{
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Lifecycle)) return;
try
{
while (!token.IsCancellationRequested)
{
SyncData();
await Task.Delay(USER_DATA_UPDATE_INTERVAL * 1000);
}
}
catch (System.Exception) { }
}
public void UpdateUserDataValue(string key, string data, bool remoteStore = true)
{
#if UNITY_EDITOR && !SERVICE_HOST
if(PlayerKVGameplayLoop.Enabled) { PlayerKVGameplayLoop.Write(key,data,remoteStore); return; }
#endif
PlayerKVGameLoadGuard.RequireLegacyAllowed(PlayerKVGuardPoint.Update);
if (remoteStore)
{
var cbuff = dataBuffer.First;
while (cbuff != null)
{
var datas = cbuff.Value;
if (!datas.ContainsKey(key) && datas.Count >= 10)
{
cbuff = cbuff.Next;
continue;
}
cbuff.Value[key] = data;
break;
}
}
localData[key] = data;
lastUpdateTime[key] = ZZTimeHelper.UtcNow().Ticks;
//Debug.Log($"[PlayFabMgr]UpdateUserDataValue key:{key} data:{data}");
PlayerPrefs.SetString(Last_Update_Time_Key, LitJson.JsonMapper.ToJson(lastUpdateTime));
PlayerPrefs.SetString(Local_Data_Key, LitJson.JsonMapper.ToJson(localData));
}
/// <summary>
/// 没有数据返回 null 一定一定一定判空!!!
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public string GetLocalData(string key)
{
#if UNITY_EDITOR && !SERVICE_HOST
if(PlayerKVGameplayLoop.Enabled) return PlayerKVGameplayLoop.Read(key);
#endif
PlayerKVGameLoadGuard.RequireLegacyAllowed(PlayerKVGuardPoint.ReadLocal);
if (localData.ContainsKey(key))
{
return localData[key];
}
else
{
return null;
}
}
private void SyncData()
{
#if UNITY_EDITOR && !SERVICE_HOST
if(PlayerKVGameplayLoop.Enabled) { _=PlayerKVGameplayLoop.FlushObservedAsync(); return; }
#endif
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Sync)) return;
// Logout clears credentials before RootCtx is destroyed. Keep pending data local.
if (!PlayFabClientAPI.IsClientLoggedIn()) return;
var buf = dataBuffer.First;
if (buf.Value.Count == 0)
return;
var datas = buf.Value;
swapData.Clear();
buf.Value = swapData;
swapData = datas;
dataBuffer.RemoveFirst();
dataBuffer.AddLast(buf);
var request = new UpdateUserDataRequest()
{
Data = swapData,
Permission = UserDataPermission.Public
};
PlayFabClientAPI.UpdateUserData(request, null, UpdateFaild);
#if UNITY_EDITOR
//IConfig config = GContext.container.Resolve<IConfig>();
//if (config != null && config.Get<string>("DBUG", string.Empty) == "1")
//{
try
{
//写入本地文件记录 key 和时间
string persistentPath = Application.persistentDataPath;
// 拼接文件名,这里使用txt文件记录数据
string filePath = System.IO.Path.Combine(persistentPath, "syncData.csv");
string currentTime = ZZTimeHelper.UtcNow().ToString("yyyy-MM-dd HH:mm:ss");
string dataLine = $"{currentTime},{string.Join(',', swapData.Keys)}";
// 使用追加模式写入文件,每次写入一行数据
using (System.IO.StreamWriter writer = System.IO.File.AppendText(filePath))
{
writer.WriteLine(dataLine);
}
}
catch (Exception e)
{
Debug.LogError($"记录数据时出错: {e.Message}");
}
//}
#endif
}
private void UpdateFaild(PlayFabError error)
{
GameDebug.LogError("[PlayFabMgr]UpdateUserData Error: {0}", error.ToString());
}
internal void FlushBufferedData() => SyncData();
public async Task<bool> SyncDataImmidiate(string key, string data)
{
#if UNITY_EDITOR && !SERVICE_HOST
if(PlayerKVGameplayLoop.Enabled) throw PlayerKVGameplayLoop.Stop("Immediate/consuming saves are outside the equipment loop.");
#endif
if(PlayerKVGameLoadGuard.Reject(PlayerKVGuardPoint.Immediate)) return false;
foreach (var buff in dataBuffer)
{
if (buff.ContainsKey(key))
{
buff.Remove(key);
}
}
var taskSource = new TaskCompletionSource<bool>();
var request = new UpdateUserDataRequest()
{
Data = new Dictionary<string, string>() { { key, data } },
Permission = UserDataPermission.Public
};
PlayFabClientAPI.UpdateUserData(request,
(r) => { taskSource.SetResult(true); },
(e) =>
{
taskSource.SetResult(false);
UnityEngine.Debug.LogError($"[PlayFabMgr]UpdateUserData Error: {e.ToString()}");
}
);
return await taskSource.Task;
}
}