Files
ft/CodexOutput/upserver/B3BatchMigration/M2/C2d修改前快照/004-PlayFabMgr.cs.txt
Liubing\LB 6813da149a feat(upserver): 保存M2客户端KV迁移至本人统计写检查点
保存独立KV用户服务、固定会话存档与原业务接线,以及C3d资料缓存和C3e本人统计写。
保留原PF/AGG路线、业务公式和时序;榜单读取、好友资料、公告、匹配及首场景闸仍待完成。

验证:ServiceHost两条件各33组646断言;Unity CLI Loop编译0错误269警告。
包含相关方案、评审裁决、测试及状态文档。未操作服务器、正式账号或打包。
2026-09-30 19:19:05 +08:00

359 lines
12 KiB
Plaintext
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 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;
}
}