21 KiB
阶段4B3分期与4B3A固定存档入口方案(待独立评审)
日期:2026-09-24。作者:主会话。前置:4B2关闭裁决。
本包申请两项独立裁决:A=4B3内部分期是否合理;B=4B3A限定方案是否完整、可实施。不是整个4B3已经完成设计,更不是批准全量游戏接线。 4B3B/C的业务调用起点、可变对象隔离及启用接线仍须分别形成完整方案;本包不以扫描表代替这些方案。当前没有4B3业务代码。
1. 目标、现状和最小改动原则
整个4B3最终交付:单次登录固定后端;普通KV读写使用创建操作时捕获的固定句柄;旧回调在改变共享内存前即失效;持久化失败可辨;定时/暂停/退出属于原会话。游戏正式启用仍受4C、tickets有界历史、身份专项、4D、P2-α真实档专项及用户产品确认约束。
当前4A固定句柄已有安全快照、Update/Delete/Flush/WaitForTicket,4B1/4B2已负责所有权和受控加载。不再造Owner、缓存、传输、会话管理器或另一套验证专用普通存档框架。 首单元只在既有PlayFabMgr增加显式固定句柄入口及一个键读取结果类型;不替换旧调用、不改模块字段、不放开旧guard、不公开Context的私有handle。
文本初扫234行/84文件包含注释和定义。Roslyn语法盘点及逐位置台账见调用事实与分期附件和B3DesignAudit/calls.json。它记录实际语法调用及按名称匹配的上游候选;没有语义绑定,也未证明所有事件、反射、UnityEvent持久订阅和接口实现链完整。这个事实是分期理由,不能被隐藏为“全链隔离已经设计完成”。
2. 路线选择与分期裁决请求
| 路线 | 具体影响 | 建议 |
|---|---|---|
| 让旧PlayFabMgr.Instance自动取当前KV句柄 | 改动少,但A回调到B时会写B;保存前的共享内存污染也未解决 | 不采用 |
| 一次改全部普通调用、消费、支付和身份 | 跨越现有4C/身份边界,逐调用证据不足,白名单过大 | 不采用 |
| 显式固定入口→业务操作隔离→加载和生命周期接线 | 复用已验基础;每次只批准已完整定义的单元;直到全链通过仍禁用正式玩法 | 推荐,待独立评审 |
| 单元 | 交付与关闭条件 | 不包含/下一前置 |
|---|---|---|
| 4B3A 固定存档入口 | 本方案§3~8全部通过,旧PF路径字节保持、4B1/4B2回归及独立实施复核关闭 | 不发布游戏写权限、不更换任何业务调用、不接定时器;不能代替4B3B/C |
| 4B3B 业务操作及内存隔离 | 每个业务入口明确捕获点/对象归属/取消点;同步、await、广告/支付/事件/协程跨账号验证;接口拒读不再当缺档 | 完整逐调用合同及白名单另审;未完整审查的入口不得在KV模式运行;4C交易结果语义不在此伪造 |
| 4B3C 加载发布与生命周期 | 私有handle受限发布、后端冻结、初始化意图、40秒周期/暂停/恢复/退出,真实服务组合与31对象回归 | 必须先完成B;生产准入仍关;不能删除B2guard或把LoadedForValidation改为Done |
A→B→C全部独立关闭才可称4B3完成。每个单元完成后,主会话按用户最新授权自动做无需复审的后续准备,不在“是否继续”处停顿;遇到完整新方案/必要差异/独立实施复核门时,准备好可审材料后交接。本次没有请求免除既定审查门。
3. 4B3A开发范围与接口
新增方法放在Core/PlayFabMgr.cs一个有起止注释的KV explicit fixed-handle access块中,均为internal static;不访问Instance、Unity场景、PlayerPrefs、旧缓冲、SDK或任何全局“当前账号”。静态方法不触发组件Awake;不新增静态字段、timer、事件或内部重试。生产Player编译保留这些普通内部方法;不存在用户配置/公共登录路径调用它们。
调用者必须显式提供已有PlayerKVGameSessionHandle,方法只调用这个实例,绝不寻找替代句柄。该类型由4A Bridge产生,4B1 Context的private handle保持private,4B2 LoadedForValidation不会因此获得它。
| 计划接口 | 输出与用途 |
|---|---|
ReadKV(handle, string key) |
新PlayerKVKeyReadResult;严格区分有效快照内缺键与拒读 |
WriteKV(handle, string key, string value, bool remoteStore = true) |
既有PlayerKVEditResult,完整保留本地接受、原始错误、票据和不确定提交标志 |
FlushKVAsync(handle, CancellationToken waitToken = default) |
既有Task<PlayerKVFlushResult>;透传水位和分类结果,不合并成bool |
WaitForKVTicketAsync(handle, PlayerKVSyncTicket ticket, CancellationToken waitToken = default) |
既有Task<PlayerKVTicketResult>;确认的是原句柄的原票据 |
不新增Delete facade:当前业务没有显式Delete调用需求,空字符串写入仍是存在值,不能偷改为删除。底层已有Delete保持;未来确有调用需求再纳入对应单元合同。原GetLocalData/UpdateUserDataValue/SyncDataImmidiate/CompareDatas签名和实现一律不改。
3.1 新键读取结果
新增Services/PlayerData/PlayerKVKeyReadResult.cs,命名空间game.PlayerDataStorage。只含internal enum和internal sealed不可变结果;不增加业务DTO或可变字典。
PlayerKVKeyReadError:None、InvalidInput、LoadGuardActive、StaleOrClosing、NotReady。
结果属性:Error、Success => Error == None、可空bool Found、string Value、OwnerInstanceId、PlayerId、long? Epoch。创建器internal;没有set、隐式bool/string转换。失败时Found=null且Value=null,禁止构造伪缺键;null handle时身份全null;有效handle失败仍可带该原始身份用于诊断,但不带Cookie/存档正文。
读取顺序固定:
- 先调用已有
PlayerKVGameLoadGuard.Reject(ReadLocal)。命中则返回LoadGuardActive,记录已有违规计数,零快照读取。不能另开不计数的guard旁路。 - null handle或
!KVSnapshot.ValidKey(key)返回InvalidInput。使用已有规则:非null、非空、长度≤128;不Trim、不改大小写、不替换key、不额外拒绝当前规则允许的空格。 - 仅调用指定handle的
ReadLocalSnapshot()一次。其StaleOrClosing/NotReady分别映射;不再读取全局身份或重新Get远端。 - 成功快照按Ordinal键查询:存在→Found=true/原字符串;缺键→Found=false/Value=null;空字符串是Found=true。
- 读取成功是一次快照观察,不是未来业务操作许可。调用后账号失效时,B3B仍须在首次内存修改前重检其操作身份。A不宣称返回值能自动撤销已经拷出的字符串。
既有Snapshot实现负责操作租约及读后归属检查。4B3A不修改其并发线性化点,也不把普通结果对象当授权令牌。
3.2 写入与错误合同
先调用guard.Reject(Update);命中返回PlayerKVEditResult(StateResult=Blocked),无票据。null handle返回StateResult.Validation。随后原样返回指定handle.Update(key,value,remoteStore);key/value非法与Ready/关闭优先级沿用底层,不在facade复制第二套校验流程。
| 结果 | 可以作出的判断 | 不允许作出的判断 |
|---|---|---|
| Accepted=true、remoteStore=true | 该原Owner缓存已接受,Ticket保留 | 远端已确认、可发奖、可重放交易 |
| Accepted=true、remoteStore=false | localOnly接受,通常无Ticket | 该key所有历史远端pending被撤销,或永远不会发送历史值 |
| Validation | 本次输入未被接受,保留原字段 | 自动规范化输入后重试 |
| Blocked | 原Owner/状态不准入 | 改取当前B账号句柄再写 |
| StorageError | 保留AttemptSequence/Revision/LocalCommitMayHaveSucceeded | 将不确定提交改成false并回滚/重试业务动作 |
remoteStore=false沿用3A:写localOnly不清除同key已有remote pending;以后Flush可能发送此前remote值。先remote后localOnly、localOnly后remote必须分别测,不按旧PF名称猜语义。value=null不等于删除,沿用Validation/Blocked结果;value=""是正常存在值。
写操作与Close竞争时,已被底层准入的A操作可以完成于A;结果保留实际发生的接受事实。它永远不改发B,也不因返回后失效而把可能已持久化的Accepted伪装成未提交。新的调用在旧handle失效后由原Owner拒绝。B3B负责结果返回后的UI/内存归属,不能凭Accepted绕过。
底层预期错误结构化透传;未预期编程异常不catch成缺档、成功或可安全重试。允许向调用方传播,后续B3B入口按故障态中止,不能继续业务。A测试显式验证不吞异常。
3.3 Flush与票据
Flush先guard.Reject(Sync);命中返回计数Confirmed=0/Superseded=0/Blocked=1/Unknown=0、TransportError=Cancelled、CacheStatus=handle?.Status或NotLoaded。null handle同样Blocked=1,但TransportError=Validation、CacheStatus=NotLoaded。通过后直接返回handle.FlushAsync(waitToken),不另开Task.Run/定时器/水位,不改Confirmed/Superseded/Unknown。
票据等待先guard.Reject(Sync);命中返回Status=Blocked、TransportError=Cancelled及原handle.Status/NotLoaded。null handle返回Blocked/Validation/NotLoaded。通过后直接返回handle.WaitForTicketAsync(ticket,waitToken)。票据null/外来Owner/epoch/sequence由已有Coordinator检查,失效handle优先级沿用4A;不创建替代ticket。
取消token只结束调用方等待,不撤销已经发出的操作,不重试,不释放Owner资源。已有Owner跟踪的内部任务继续收尾,Close继续等待排水。预取消token也沿用现有4A调用语义,不在facade声称“保证未发送”。票据Confirmed仅表示该原票据确认;Superseded不等于当前值确认,Unknown不能当失败可重扣。等待不主动触发Flush。
4. 4B2兼容与账号归属
四个新入口全部服从现有B2加载guard。guard活跃期间任何新入口调用都拒绝,并增加现有计数;不允许为测试普通写临时释放B2guard。4B2仍是单认证、加载事务、零业务写、LoadedForValidation,旧受控工厂与私有handle不改。
这里的拒绝点是调用入口处的guard观察,不是跨整个异步操作持有的全局租约。已经在guard建立前准入的原Owner操作仍由原Owner排水,不能宣称新静态方法凭一次检查就解决“普通写→下一加载”的切换竞争。A没有生产调用者,也不开放加载后写权限;C必须在上一普通会话撤销/排水后才能进入新加载guard,不能与未排水普通访问并存。若外部认为此限制仍不足以允许单独实现A,应退回分期或要求最小准入合同补审,不能在实施中临时修改Guard。
普通A单元测试通过现有4A合成session+Bridge取得句柄,独立调用新facade;这不等于“4B2加载后写入已接通”,二者组合属于C。Editor验证沿用固定反射入口模式,但新验证host仅用既有internal合成工厂,没有运行时可用的网络工厂、反射通用执行器或配置开关。
验证host固定使用http://kv-route.invalid/,合成handler直接返回响应且检查固定host/path,禁止调用默认HttpClientHandler或任何转发代理;不更改项目正式地址。缓存仅在CodexOutput/upserver/KVSaveRouteTests/unity-runs/<runId>/。不创建PlayFabMgr组件/不触发Awake,不启动游戏容器。guard用TryEnter取得本次所有权,失败即停止该用例,不释放他人的guard;finally只释放本次取得的guard,先Close Bridge排水再Dispose认证session,异常和未完成Task保留为失败。不得覆盖既有静态单例来让测试通过。
固定身份为OwnerInstanceId+PlayerId+Epoch;同账号再次登录也必须是新操作归属。方法参数永不替换。A验证旧A句柄在B打开后不能读/写/Flush B,并核对B缓存和请求数,而不只看返回错误。
5. 后续业务合同(约束,不是本轮实施批准)
| 场景 | B/C必须满足的合同 |
|---|---|
| 同步按钮/业务入口 | 在首次可变状态写、事件或奖励前建立原会话操作归属。只在末端保存加校验不足。 |
| await/协程/SDK回调 | 发起时捕获操作;恢复后先重检再Resolve/修改业务对象、更新UI或发奖;同账号新epoch也拒绝旧结果。 |
| 常驻订阅/计时器 | 注册时捕获epoch与对象生命周期,关闭先撤销再退订;已经入队的回调仍需复查。 |
| 原地Reset/Replace的31对象 | 旧引用可能仍指向已装入B数据的同一实例,必须在变更入口校验原epoch;不能只比较ReferenceEquals或ID。 |
| 仓库读取异常 | EventDataRepository等不能把Stale/NotReady转换为new T并继续保存;只有有效快照的缺键可进入明确的新数据策略。 |
| 奖励/支付/消费 | B只处理归属与中止;持久化失败后的奖励一致性必须有单独业务裁决,不能凭本地Accepted发奖或重新扣减。两个立即保存消费点继续归4C。 |
| 普通写入失败 | 一次操作内停止后续依赖副作用;不以改写全局缓存/从新账号重试补救;是否可恢复以具体业务合同为准。 |
| 生命周期 | C才能建立原会话40秒周期、暂停Flush/恢复、同步撤销及有界退出观察;等待超时不释放占用资源、不宣称已确认。 |
上述合同尚需B/C逐入口落到源方法/字段/事件和文件白名单,未落完则B/C不得编码或关闭。本方案没有将全部业务方法的安全性归功于四个静态facade方法。
6. 4B3A精确白名单
| 文件 | 允许的唯一内容 |
|---|---|
Client/Assets/Scripts/Core/PlayFabMgr.cs |
新增§3四个internal static方法及块内私有结果构造helper;旧代码块字节保持,不调整Awake/Compare/普通PF/立即PF/生命周期 |
Client/Assets/Scripts/Services/PlayerData/PlayerKVKeyReadResult.cs及meta |
§3.1不可变结果和枚举 |
Client/Assets/Scripts/Services/PlayerData/PlayerKVSaveRouteEditorValidationHost.cs及meta |
整个文件UNITY_EDITOR包裹;固定合成传输、固定case,实际调用新增facade,无动态代码执行 |
Client/Assets/Editor/KVSaveRouteValidation.cs及meta |
Editor固定类型/方法/case白名单反射转接,无任意类型/路径/参数执行 |
CodexOutput/upserver/KVSaveRouteTests/ |
新显式链接源码测试项目/替身/证据;链接现有底层和PlayFabMgr,不复制实现 |
| 本阶段文档、入口、状态、TODO、日志 | 主会话检查点、送审/交付/hash材料;保留旧受审证据 |
禁止改:User/Agg/Context/Bridge/Owner/Guard/Registry/Schema/State/Cache/Coordinator、CustomServerMgr、QuitAct、任何业务DataCenter/UI、场景/字体/配置/包/SDK、旧测试源码与csproj。当前唯一显式链接PlayFabMgr的旧项目是KVGameLoadTests/ServiceHost/ServiceHost.csproj,PlayerKV源码为通配纳入;新增Editor host需使用UNITY_EDITOR且额外!SERVICE_HOST条件排除内容,避免旧宿主误编译验证实现。既有旧Editor host排除项不改。
普通新结果文件被该通配纳入,旧宿主不需要csproj补丁。其他旧显式Compile项目不链接PlayFabMgr,新增facade依赖不影响它们。如实施发现依赖与此不同,停止相关修改先提出精确差异,不默认扩大白名单。
新meta使用新GUID;不重新序列化场景或资产。新测试依赖用现有Unity Newtonsoft/PlayFab程序集与本地.NET SDK,不安装新包。测试stub仅替代SDK/Unity边界,必须说明它不能证明真实渠道全流程。
7. 4B3A验收矩阵
全部测试合成账号/传输,缓存位于独立运行目录;无正式服请求。按语义组计数,不预造断言总数。每条应有实际日志和对应源测试名。
| 编号 | 场景与必须观察到的结果 |
|---|---|
| B3A-01 | Ready读存在/缺键/空字符串,Found分别true/false/true;精确大小写、空格、Unicode/边界128字符按现有ValidKey规则;无新Get。 |
| B3A-02 | null handle、null/空/129字符key为InvalidInput;guard活跃优先LoadGuardActive;失败Found=null/Value=null。 |
| B3A-03 | 旧/关闭handle返回StaleOrClosing;先沿用4A Seed制造真实Conflict/Unknown,state.json.tmp目录冲突制造StorageError,合成401制造授权故障,验证失败Found=null。ReadFailed等底层状态覆盖沿用4A/3A回归;本层证明两个Snapshot错误的映射,不冒称所有状态均由新facade端到端重造。 |
| B3A-04 | remote=true写接受、返回真实票据;网络尚未发送,读取见本地合并值;Accepted不被测试命名为远端成功。 |
| B3A-05 | remote=false接受无ticket;remote→localOnly后Flush仍可发送旧pending;localOnly→remote后读/发送新remote值,分别验证。 |
| B3A-06 | value=null、非法key按底层Validation/Blocked,空串保留;无自动Delete、无规范化输入。 |
| B3A-07 | 沿用KVGameBridgeTests.NonReady:在本run唯一state.json旁创建state.json.tmp目录使实际写失败,facade返回StorageError及实际sequence/revision/uncertain=false,无重试。uncertain=true的AfterReplace分支沿用KVSyncTests现有五阶段fault回归+facade原样return源码审计,不宣称新facade已端到端注入AfterReplace;不得修改readonly字段或底层增加故障钩子。 |
| B3A-08 | 正常Flush真实计数/水位;调用后的新写不被旧水位虚报确认;同keySuperseded保留,后续ticket必须独立确认。 |
| B3A-09 | 合成Set已执行但响应丢失→Unknown;401及前置拒绝保留原结果;无再次Set、无改发PF、无自动Refresh。 |
| B3A-10 | null/外来Owner/同账号新epoch票据拒绝;旧A票据Confirmed也不代表B的新值确认;Wait不主动发送。 |
| B3A-11 | Flush等待取消/预取消仅取消观察;忽略取消传输仍排水,Close未完成前不能复用;保留原Task直至收尾。 |
| B3A-12 | A句柄捕获→关闭→B打开→A的四个facade调用拒绝,B缓存、请求、业务替身内存不变;同账号新epoch复测。内存断言仅证明测试回调自身遵循合同,不称所有业务已迁移。 |
| B3A-13 | Close与Write/Read/Flush竞争:允许底层已准入A操作完成在A;不改写接受事实,无跨Owner请求,随后调用拒绝。 |
| B3A-14 | B2guard活跃时四入口全部拒绝;ReadLocal/Update/Sync计数符合次数;SDK/Prefs/Set/Delete均0,guard不释放。 |
| B3A-15 | facade静态调用无需MonoBehaviour/Instance;未触发Awake/旧timer/缓冲/Prefs;四个方法及helper移除后PlayFabMgr与基线字节一致。 |
| B3A-16 | 新源码宿主两平台条件;无UNITY_EDITOR时验证host消失,四普通入口仍存在但无生产调用者;旧B2 ServiceHost双条件和PlayerProbe保持。 |
| B3A-17 | Unity CLI Loop实际HotUpdate程序集调用固定case,合成A/B固定句柄读写/guard测试,关闭所有资源并恢复静态guard;0编译错误/新文件0警告,无PlayMode。 |
| B3A-18 | 原4A18组、4B1全23组、4B2全75组/关闭失败三进程/服务双条件/Unity B2-20,42/86/29/Review/3C离线回归;KVLive及旧NetworkTransport仅build。间歇首败完整保留,不以重跑覆盖。 |
| B3A-19 | 源码范围/hash一致,新增生产依赖正确,限定自审、外部实施复核与主会话逐条裁决完成才关闭A;不将A通过等同B3或G4通过。 |
不在A测试真实广告、支付、PlayFab渠道、活动启动或全场景。这样的测试不属于A能力,不能声称通过。实际普通游戏启用证据属于后续B/C/4C/4D。
8. 实施检查点(仅在本方案独立通过并裁决后生效)
- 核对受审hash、A/B两部分裁决及白名单;保存PlayFabMgr开工副本、现有源hash;记录实施批准。任一P1/P2未关闭不编码。
- 新键结果与ReadKV;先测B3A-01~03、14/15相关项,旧PF区字节比对,更新日志。
- WriteKV/FlushKVAsync/WaitForKVTicketAsync透传;B3A-04~14全部故障/竞争用例,记录最初失败与修复;不增加后台机制,更新日志。
- 两平台真实源码宿主、非Editor表面、Unity CLI Loop及4B1/4B2全量回归;观察首败根因不明时保留原状态,必要问题单独裁决,更新日志。
- 范围/hash/限定自审/交付矩阵及外部实施复核提示词封板;外部接受后主会话逐条裁决关闭A。接着自动开始B的完整方案准备,不询问是否继续;B方案未经审查不编码。
每检查点更新开发日志.md与新阶段4B3A实施记录-2026-09-24.md。只有文档准备时写“设计检查点”,不冒充实施检查点已完成。不Git提交、不打包、不运行正式转发代理。
9. 作者自审及外部重点
已核对A依赖复用、guard计数、无新会话拥有权、key规则、localOnly与已有pending语义、异步取消、旧PF不变、旧宿主通配依赖和Editor/Player隔离。作者自审不代替独立评审。
外部应分别回答:分期是否合理,A是否过薄/重复或存在必要接口缺口;四入口是否留下guard/所有权旁路;只改PlayFabMgr新增块是否可验证PF保持;A的测试是否可用现有故障能力实现;后续B/C不完整是否被正确限制为未批准,而非包装成全量完成。
审查若认为必须先完整证明全部业务调用再实施A,可明确退回分期A部分;主会话按报告裁决,不以“未来不启用”自动豁免任何本单元实际副作用问题。