feat(upserver): 保存M2客户端KV迁移至本人统计写检查点
保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。 保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。 验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。 包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user