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

@@ -131,7 +131,7 @@ namespace game.PlayerDataStorage
}
internal static string Read(string key)
{
var result=PlayFabMgr.ReadKV(Capture(),key);
var result=KVMgr.ReadKV(Capture(),key);
if(!result.Success) throw Stop("Read: "+result.Error);
return result.Found==true ? result.Value : null;
}
@@ -142,7 +142,7 @@ namespace game.PlayerDataStorage
CheckRewardWrite(key,value);
var valueHash=value==null?null:PlayerKVLoopPermit.Hash(Encoding.UTF8.GetBytes(value));
Audit("write-attempt",new{key,remote,valueHash});
var result=PlayFabMgr.WriteKV(owner,key,value,remote);
var result=KVMgr.WriteKV(owner,key,value,remote);
if(!result.Accepted) throw Stop("Write: "+result.StateResult.Error+" uncertain="+result.StateResult.LocalCommitMayHaveSucceeded);
if(collecting!=null)
{
@@ -220,7 +220,7 @@ namespace game.PlayerDataStorage
last.Values.Any(r=>!r.Remote || r.Edit.Ticket==null)) throw Stop("Fishing final writes require original remote tickets.");
using(var timeout=new CancellationTokenSource(TimeSpan.FromSeconds(30)))
{
var flush=await PlayFabMgr.FlushKVAsync(batch.Owner,timeout.Token);
var flush=await KVMgr.FlushKVAsync(batch.Owner,timeout.Token);
if(!FishingCurrent(action) || flush.Unknown>0 || flush.Blocked>0 || flush.TransportError!=PlayerKVError.None)
throw Stop("Fishing flush: "+flush.TransportError);
foreach(var record in batch.Records)
@@ -231,7 +231,7 @@ namespace game.PlayerDataStorage
if(ticket.OwnerInstanceId!=batch.Owner.OwnerInstanceId || ticket.Epoch!=batch.Owner.Epoch ||
ticket.Key!=record.Key || final.OwnerInstanceId!=ticket.OwnerInstanceId || final.Epoch!=ticket.Epoch)
throw Stop("Fishing ticket identity mismatch.");
var result=await PlayFabMgr.WaitForKVTicketAsync(batch.Owner,ticket,timeout.Token);
var result=await KVMgr.WaitForKVTicketAsync(batch.Owner,ticket,timeout.Token);
if(!FishingCurrent(action)) throw Stop("Fishing action changed while waiting.");
bool explained=result.Status==PlayerKVTicketStatus.Superseded && !ReferenceEquals(ticket,final) && final.Sequence>ticket.Sequence;
if(result.Status!=PlayerKVTicketStatus.Confirmed && !explained) throw Stop("Fishing ticket: "+result.Status);
@@ -254,12 +254,12 @@ namespace game.PlayerDataStorage
{
if(primary?.Ticket==null || !batch.Edits.Contains(primary) || !ReferenceEquals(Capture(),batch.Owner))
throw Stop("Missing original equipment ticket.");
var flush=await PlayFabMgr.FlushKVAsync(batch.Owner);
var flush=await KVMgr.FlushKVAsync(batch.Owner);
if(flush.Unknown>0 || flush.Blocked>0 || flush.TransportError!=PlayerKVError.None) throw Stop("Equipment flush: "+flush.TransportError);
foreach(var edit in batch.Edits)
{
if(edit.Ticket==null) continue; // localOnly is audited, never claimed as remotely confirmed.
var result=await PlayFabMgr.WaitForKVTicketAsync(batch.Owner,edit.Ticket);
var result=await KVMgr.WaitForKVTicketAsync(batch.Owner,edit.Ticket);
if(result.Status!=PlayerKVTicketStatus.Confirmed) throw Stop("Equipment ticket: "+result.Status);
}
if(!ReferenceEquals(Capture(),batch.Owner)) throw Stop("Equipment session changed.");
@@ -270,7 +270,7 @@ namespace game.PlayerDataStorage
if(!Ready) return;
try
{
var result=await PlayFabMgr.FlushKVAsync(Capture());
var result=await KVMgr.FlushKVAsync(Capture());
if(result.Unknown>0 || result.Blocked>0 || result.TransportError!=PlayerKVError.None) throw Stop("Periodic flush: "+result.TransportError);
}
catch(Exception e) { Fault(e.Message); }
@@ -342,14 +342,14 @@ namespace game.PlayerDataStorage
Check(batch.Edits.Count==2 && ReferenceEquals(primary,batch.Edits[1]),"all original edits collected");
Check(fixture.Sets==0,"write acceptance is local only");
await ConfirmEquipAsync(batch,primary);
foreach(var edit in batch.Edits) Check((await PlayFabMgr.WaitForKVTicketAsync(batch.Owner,edit.Ticket)).Status==PlayerKVTicketStatus.Confirmed,"each original ticket confirmed");
foreach(var edit in batch.Edits) Check((await KVMgr.WaitForKVTicketAsync(batch.Owner,edit.Ticket)).Status==PlayerKVTicketStatus.Confirmed,"each original ticket confirmed");
Check(fixture.Remote["k"]=="nested" && fixture.Remote["NewPlayerDataDic"]=="whole-save" && Ready && fixture.Faults==0,"complete batch committed");
await Start(); batch=BeginEquip();
var first=Write("k","first",true); Write("k","second",true);
primary=Write("NewPlayerDataDic","whole-save",true); batch.Dispose();
await Reject(ConfirmEquipAsync(batch,primary),"Superseded");
Check((await PlayFabMgr.WaitForKVTicketAsync(batch.Owner,first.Ticket)).Status==PlayerKVTicketStatus.Superseded,"original nested ticket never replaced");
Check((await KVMgr.WaitForKVTicketAsync(batch.Owner,first.Ticket)).Status==PlayerKVTicketStatus.Superseded,"original nested ticket never replaced");
await Start(); batch=BeginEquip();
Write("k","nested",true); primary=Write("NewPlayerDataDic","watermark",true); batch.Dispose();
@@ -362,8 +362,8 @@ namespace game.PlayerDataStorage
Check(batch.Edits.Count==2,"later write excluded from synchronous batch");
fixture.ReleaseSet.TrySetResult(true); await confirmation; pendingConfirmation=null;
Check(!fixture.Remote.ContainsKey("late"),"flush watermark excludes later write");
await PlayFabMgr.FlushKVAsync(batch.Owner);
Check((await PlayFabMgr.WaitForKVTicketAsync(batch.Owner,late.Ticket)).Status==PlayerKVTicketStatus.Confirmed,"later write needs its own flush");
await KVMgr.FlushKVAsync(batch.Owner);
Check((await KVMgr.WaitForKVTicketAsync(batch.Owner,late.Ticket)).Status==PlayerKVTicketStatus.Confirmed,"later write needs its own flush");
await Start(); batch=BeginEquip(); primary=Write("NewPlayerDataDic","unauthorized",true); batch.Dispose();
fixture.Unauthorized=true;
@@ -374,7 +374,7 @@ namespace game.PlayerDataStorage
fixture.LoseResponse=true;
await Reject(ConfirmEquipAsync(batch,primary),"Equipment flush");
Check(fixture.Remote["NewPlayerDataDic"]=="lost-response" && fixture.Sets==1,"unknown remote commit is not replayed");
Check((await PlayFabMgr.WaitForKVTicketAsync(batch.Owner,primary.Ticket)).Status==PlayerKVTicketStatus.Unknown,"unknown original ticket preserved");
Check((await KVMgr.WaitForKVTicketAsync(batch.Owner,primary.Ticket)).Status==PlayerKVTicketStatus.Unknown,"unknown original ticket preserved");
await Start(); batch=BeginEquip(); primary=Write("NewPlayerDataDic","closed",true); batch.Dispose();
await fixture.Close();