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 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();',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();\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.')