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

@@ -1,4 +1,4 @@
#if UNITY_EDITOR && !SERVICE_HOST
#if UNITY_EDITOR && !SERVICE_HOST
using System;
using System.Collections.Generic;
using System.IO;
@@ -31,6 +31,9 @@ namespace game.PlayerDataStorage
var oldLanguages=LocalizationMgr.enumLanguage;
var textTable=typeof(LocalizationMgr).GetField("table",System.Reflection.BindingFlags.Static|System.Reflection.BindingFlags.NonPublic);
var oldTextTable=textTable.GetValue(null);
GameObject saveShell=null;
var pfField=typeof(PlayFabMgr).GetField("_instance",System.Reflection.BindingFlags.Static|System.Reflection.BindingFlags.NonPublic);
var oldPF=pfField.GetValue(null);
FishingSubscribers subscribers=null;
int checks=0,groups=0; var passed=new List<string>(); string result=null;
void Check(bool yes,string reason) { checks++; if(!yes) throw new InvalidOperationException("Fishing validation: "+reason); }
@@ -40,6 +43,7 @@ namespace game.PlayerDataStorage
player?.StopRecoveryEnergy(); player=null; subscription?.Dispose(); subscription=null;
if(fixture!=null) await fixture.Close();
if(pending!=null) { try { await pending; } catch { } pending=null; }
pfField.SetValue(null,oldPF);if(saveShell!=null)UnityEngine.Object.DestroyImmediate(saveShell);saveShell=null;
FishingContextAccess.Use(oldContainer); FishingContextAccess.Events=oldEvents; container?.Dispose(); container=null;
fixture=new EquipmentFixture(Path.Combine(folder,(++groups).ToString()));
var owner=await fixture.Open();
@@ -177,12 +181,17 @@ namespace game.PlayerDataStorage
var tables=new cfg.Tables(n=>SimpleJSON.JSON.Parse(File.ReadAllText(Path.GetFullPath(Path.Combine(Application.dataPath,"../../cfgData",n+".json")))));
container=new Container(); FishingContextAccess.Use(container); FishingContextAccess.Events=new EventAggregator();
container.RegisterInstance(tables);
saveShell=new GameObject("KV inactive consumption storage fixture");saveShell.SetActive(false);saveShell.hideFlags=HideFlags.HideAndDontSave;
var pf=saveShell.AddComponent<PlayFabMgr>();Check(pf.IsUnstartedForKVLoad,"consumption fixture never starts PF lifecycle");pfField.SetValue(null,pf);
var store=new PlayFabPlayerSaveStore(pf,CancellationToken.None);
container.RegisterInstance<IPlayerSaveStore>(store);
LocalizationMgr.enumLanguage=tables.TbLanguageConfig.DataList.Select(x=>x.ID).ToList();
textTable.SetValue(null,null);
player=new PlayerData(tables,null); var small=new SmallGameData(tables,null,null); var fish=new PlayerFishData(tables,null);
var achievements=new AchievementDataManager(tables); var box=new FishingBoxData(); var events=new FishingEventData(tables);
container.RegisterInstance(player); container.RegisterInstance(small); container.RegisterInstance(fish);
container.RegisterInstance(achievements); container.RegisterInstance(box); container.RegisterInstance(events);
player.playerSaveStore=store;fish.playerSaveStore=store;small.playerSaveStore=store;events.playerSaveStore=store;
container.RegisterInstance<ILocalNotificationService>(new FishingNotifications());
fish.ResetForKVLoad(); small.ResetForKVLoad(); achievements.ResetForKVLoad(); box.ResetForKVLoad();
achievements.Init();
@@ -228,7 +237,7 @@ namespace game.PlayerDataStorage
Check(scenario=="cross-limit"?savedTime>new DateTime(2026,1,1):savedTime==new DateTime(2026,1,1),"recovery timestamp crossing behavior preserved");
if(fixture.HoldSet)
{
await PlayFabMgr.FlushKVAsync(action.Owner);
await KVMgr.FlushKVAsync(action.Owner);
var recovered=JsonConvert.DeserializeObject<Dictionary<string,string>>(fixture.Remote["NewPlayerDataDic"]);
Check(recovered["newData"].Split(',')[3]==(initialEnergy-2).ToString(),"later recovery has separate flush, not original action confirmation");
}
@@ -254,6 +263,7 @@ namespace game.PlayerDataStorage
player?.StopRecoveryEnergy(); subscription?.Dispose();
if(fixture!=null) await fixture.Close();
if(pending!=null) { try { await pending; } catch(Exception e) { Debug.LogWarning("Observed fishing test failure: "+e.GetType().Name); } }
pfField.SetValue(null,oldPF);if(saveShell!=null)UnityEngine.Object.DestroyImmediate(saveShell);
FishingContextAccess.Use(oldContainer); FishingContextAccess.Events=oldEvents; container?.Dispose();
LocalizationMgr.enumLanguage=oldLanguages; textTable.SetValue(null,oldTextTable);
(Enabled,Ready,Error,Permit,RunPath,handle,onFault,onClose,collecting,mainThread,failed,mainContext,fishingAction)=saved;
@@ -280,8 +290,14 @@ namespace game.PlayerDataStorage
var pf=shell.AddComponent<PlayFabMgr>();
if(!pf.IsUnstartedForKVLoad)throw new InvalidOperationException("No PF lifecycle may start in this fixture.");
PF.SetValue(null,pf);
var store=container.Resolve<IPlayerSaveStore>();
container.RegisterInstance<IPlayerSaveStore>(store);
container.Resolve<PlayerData>().playerSaveStore=store;
container.Resolve<PlayerFishData>().playerSaveStore=store;
container.Resolve<SmallGameData>().playerSaveStore=store;
events.playerSaveStore=store;
var items=new PlayerItemData(tables);container.RegisterInstance(items);
var data=new BattlePassData(tables);
var data=new BattlePassData(tables){playerSaveStore=store};
data.BattlePassRecord.ID=tables.TbFishingEvent.DataList.First(e=>tables.TbBattlePassMain.GetOrDefault(e.RedirectID)!=null).ID;
data.BattlePassRecord.Level=1;
data.BattleTask.dic_dailyTasks.Add(tables.TbBattlePassTask.DataList.First(t=>t.Type==cfg.ConditionType.ConsumeEnergy).TaskID,0);