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,93 @@
import hashlib, json, subprocess
from pathlib import Path
root=Path('Client/Assets/Scripts')
m=Path('CodexOutput/upserver/B3BatchMigration/M2')
baseline={x['path']:x for x in json.loads((m/'M2-合批正式受审基线.json').read_text(encoding='utf-8-sig'))['items']}
def original(suffix, require_match=True):
path=root/suffix
raw=subprocess.run(['git','show','HEAD:'+path.as_posix()],capture_output=True,check=True).stdout
s=raw.decode('utf-8-sig').replace('\r\n','\n')
variants=[s.encode('utf-8'),s.encode('utf-8-sig'),s.replace('\n','\r\n').encode('utf-8'),s.replace('\n','\r\n').encode('utf-8-sig')]
if require_match:
assert any(hashlib.sha256(b).hexdigest().upper()==baseline[path.as_posix()]['sha256'] for b in variants),suffix
return s
def write(suffix,s):
path=root/suffix; raw=path.read_bytes()
text=s.replace('\r\n','\n')
if b'\r\n' in raw: text=text.replace('\n','\r\n')
path.write_bytes(text.encode('utf-8-sig' if raw.startswith(b'\xef\xbb\xbf') else 'utf-8'))
def calls(s,old,new,tail):
pos=0
while True:
start=s.find(old+'(',pos)
if start<0:return s
if s[s.rfind('\n',0,start)+1:start].lstrip().startswith('//'):
pos=start+len(old);continue
i=start+len(old);depth=0;quote=None;esc=False
while i<len(s):
c=s[i]
if quote:
if esc:esc=False
elif c=='\\':esc=True
elif c==quote:quote=None
elif c in ('"',"'"):quote=c
elif c=='(':depth+=1
elif c==')':
depth-=1
if depth==0:break
i+=1
assert depth==0
text=new+s[start+len(old):i+1]+tail
s=s[:start]+text+s[i+1:];pos=start+len(text)
def storage(s,name):
s=calls(s,'PlayFabMgr.Instance.GetLocalData',name+'.Read','.ValueOrThrow()')
return calls(s,'PlayFabMgr.Instance.UpdateUserDataValue',name+'.Write','.RequireAccepted()')
# All ten source bodies below were independently matched against the frozen formal baseline.
for f in ['Services/ChatService.cs','Services/InGameMailHandler.cs','DataCenter/DataCenter.PlayerItemData.cs','DataCenter/PlayerItemData_Inflation.cs','UI/Setting/SurveyPanel.cs','UI/Friends/EventInviteFriends.cs']:
write(f,original(f))
s=original('Services/SurveyService.cs')
s=s.replace(' IUserService userService;',' readonly game.PlayerDataStorage.IPlayerSaveStore saveStore;\n IUserService userService;',1)
s=s.replace('public SurveyService(IUserService userService, cfg.Tables tables)','public SurveyService(IUserService userService, cfg.Tables tables, game.PlayerDataStorage.IPlayerSaveStore saveStore)',1)
s=s.replace(' this.userService = userService;',' this.userService = userService;\n this.saveStore = saveStore;',1)
s=s.replace('Convert.ToUInt64(userService.UserId, 16)','Convert.ToUInt64(userService.UserId, saveStore is game.PlayerDataStorage.KVPlayerSaveStore ? 10 : 16)',1)
write('Services/SurveyService.cs',storage(s,'saveStore'))
s=original('Services/FriendService.cs')
s=s.replace(' public class FriendService\n {',' public class FriendService\n {\n [Inject] public PlayerDataStorage.IPlayerSaveStore SaveStore { get; set; }',1)
write('Services/FriendService.cs',storage(s,'SaveStore'))
s=original('DataCenter/DataCenter.PlayerShopData.cs')
binding=''' game.PlayerDataStorage.IPlayerSaveStore boundSaveStore;
[asap.core.Inject] public game.PlayerDataStorage.IPlayerSaveStore SaveStore
{
get => boundSaveStore;
set { CouponData.BindSaveStore(value); boundSaveStore = value; }
}
'''
s=s.replace(' public partial class PlayerShopData : IDisposable\n {',' public partial class PlayerShopData : IDisposable\n {\n'+binding,1)
write('DataCenter/DataCenter.PlayerShopData.cs',storage(s,'SaveStore'))
s=original('UI/Turntable/FishingTurntablePanel.cs')
s=s.replace(' Tables _tables;',' game.PlayerDataStorage.IPlayerSaveStore saveStore;\n Tables _tables;',1)
s=s.replace(' void Awake()\n {',' void Awake()\n {\n saveStore = GContext.container.Resolve<game.PlayerDataStorage.IPlayerSaveStore>();',1)
s=calls(s,'await PlayFabMgr.Instance.SyncDataImmidiate','(await saveStore.WriteAndConfirmAsync',').Confirmed')
write('UI/Turntable/FishingTurntablePanel.cs',s)
# Preserve all pre-existing uncommitted KV-loop/prompt work: replace only the inspected business methods.
f='GampPlay/FishingB/FishingBehaviorB.cs';s=(root/f).read_text(encoding='utf-8-sig');old=original(f,False)
start=old.index(' private void DoConsumeBait()');end=old.index(' void LoadFish()',start)
body=calls(old[start:end],'await PlayFabMgr.Instance.SyncDataImmidiate','(await saveStore.WriteAndConfirmAsync',').Confirmed')
start=s.index(' private async void DoConsumeBait()');end=s.index(' void LoadFish()',start)
s=s[:start]+body+s[end:]
for line in [' game.IUserService loginUser;\n',' bool consumptionPending;\n',' bool SaveSessionCurrent => this != null && isActiveAndEnabled && saveStore != null && saveStore.IsCurrent && !loginUser.LoginLifetime.IsCancellationRequested;\n',' loginUser = GContext.container.Resolve<game.IUserService>();\n']:
assert line in s,line;s=s.replace(line,'',1)
write(f,s)
f='Services/ClubService.cs';s=(root/f).read_text(encoding='utf-8-sig');old=original(f,False)
start=old.index(' private int? signingSession;');end=old.index(' public void SendSystemMessage2',start)
body=storage(old[start:end],'SaveStore')
start=s.index(' private int? signingSession;');end=s.index(' public void SendSystemMessage2',start)
write(f,s[:start]+body+s[end:])
# This present wrapper was introduced only by C2c; retain the surrounding original deferred reward queue.
f='Services/DeferredRewardStashService.cs';s=(root/f).read_text(encoding='utf-8-sig')
s=s.replace('game.PlayerDataStorage.PlayerSaveAction.Present(() => GContext.Publish(new ShowData()));','GContext.Publish(new ShowData());')
s=s.replace('PlayerSaveAction.Present(() => GContext.Publish(new ShowData()));','GContext.Publish(new ShowData());')
write(f,s)
print('Verified baseline + minimum storage/identity substitutions applied; historical partial-file work retained.')