18 KiB
阶段4B分期与4B1登录存档准备方案(待独立评审)
日期:2026-09-23。前置:4A实施复核裁决与关闭已完成。
本文是下一步提案,不是实施批准。当前仅完成文档准备,4B尚未编码,游戏仍使用原存档路径。 沿用主会话写方案→用户指定外部AI审查→主会话逐条裁决→明确边界后编码。正式地址固定 http://s101.local:18080/;Unity CLI Loop;不使用UnitySkills;暂不打包。
1. 现状与分期理由
| 已核源码 | 观察 | 设计约束 |
|---|---|---|
| UserService.cs,LoginCore、LoginPlayFab、InitData | LoginPlayFab在API登录前调用InitData;InitData调用CompareDatas、MMTest写入、LoadPlayerData和OnInitData | 不能只在当前登录尾部插入一次KV Get,否则已产生PF初始化及写入副作用 |
| UserService.cs,LoginApiServerCore、Logout、401订阅 | 重登/注销直接Dispose apiSession;401触发后台LoginApiServer | KV Owner关闭前不能换Cookie或销毁session;401必须撤销当前存档上下文,不能在旧Owner上静默重新认证 |
| PlayFabMgr.cs,CompareDatas、Awake、UpdateUserDataValue、SyncData | PF时间戳与PlayerPrefs合并可写回,Awake启动40秒任务,普通写为void,全局Instance在回调执行时解析 | KV路径不能复用PF缓存比较;全局当前账号路由无法识别旧回调归属;必须审查调用起点 |
| AggUserService.cs,Login、InitData | OnInitData订阅只在base.Login等待期间有效;初始化会补写渠道标记并修改传入字典 | 不能把不可变KV快照直接传给现有初始化,也不能延后到base.Login返回后再调用 |
| DataCenter.PlayerData.cs,LoadPlayerData、LoadPlayerDataNew | 先设置默认值再解析;NewPlayerDataDic仅在key存在时替换;Init/体力恢复链可保存 | 空KV、缺主档、解析失败和旧业务对象残留必须分别阻断;通用字符串字典合法不代表完整游戏档合法 |
| QuitAct.cs,StartAsync | 注销先同步失效,再await Hub释放,最后销毁RootCtx | 保留先撤销身份的语义,后续增加可等待KV关闭;不得以等待网络为由延迟身份失效 |
上述观察是本轮静态读取,不宣称已经实现或运行验证。
2. 路线比较与推荐边界
| 方案 | 优点 | 代价/本提案选择 |
|---|---|---|
| 一次实施完整4B(登录、业务初始化、所有延后读写与退出) | 一次交付实际游戏效果 | 跨全局facade、渠道回调和大量DataCenter/UI调用,当前只有分期合同,缺完整逐调用点清单;不推荐直接开工 |
| 4B内再分三个验收单元 | 每次可给出完整文件白名单和独立用例;先锁定session所有权与认证/读取顺序 | 推荐;4B1只做准备内核,不宣称游戏已切换,4B2/4B3继续独立详审 |
| 按业务key逐个PF/KV混用 | 单次改动表面小 | 同次初始化可能拼接两份档;跨key依赖和自动回退难证明,维持原方案不采用 |
| 单元 | 要解决的具体问题 | 编码前所需审查 | 启用范围 |
|---|---|---|---|
| 4B1 登录存档准备 | API session所有权、认证→Bridge Open/Get→安全快照、失败清理、注销/取消真正排水 | 本文§3–§8完整合同、文件边界与矩阵 | 仅离线合成宿主;不接UserService/游戏,不向正式服发请求 |
| 4B2 游戏加载与内存隔离 | PF只供暂留资料;严格业务档校验;清空/重建本账号业务对象;初始化副作用和OnInitData时序;401/退出接线 | 单独列完整加载字段/模块、缺失与损坏策略、默认值来源、写入副作用、初始化失败清理和精确文件清单 | 在完整启用门前只做替身/受控加载验收 |
| 4B3 普通读写入口 | 固定后端facade、固定句柄、逐个延后回调捕获、结构化持久化失败、定时/暂停Flush | 重新生成调用盘点,逐项标明同步/异步、句柄捕获起点、取消点、可变业务对象归属及允许修改文件 | 4C消费流程、tickets有界历史和正式场景数据边界未完成前,不启用实际含消费玩法 |
此分期仅细化4B,不撤回整体阶段4目标。4B1通过不等于4B通过;4B通过不等于G4通过。
3. 4B1职责与公开合同
建议新增 PlayerKVLoginContext(每次登录尝试一个实例),命名空间沿用 game.PlayerDataStorage。它拥有一个新建的S101AccountSession和一个4A Bridge,持有但不向外发布固定Handle。它不引用Unity、PlayFab、GContext、全局manager或任何玩法类。
与4A的职责差异:4A要求外部已经认证且不拥有session;4B1负责建立认证session、按顺序取档,并保证Bridge真正Close后才Dispose该session。复用4A串行/票据/读接口,不复制或修改其实现。
3.1 输入与输出
- 创建时捕获数字ID、绝对缓存根、long epoch、轻量
Func<long,bool> isCurrent,之后不可变;ID规则复用现有正数Int64十进制规则并规范化。正式公开构造使用固定s101基址,测试替身/API覆盖仅internal。 PrepareAsync(CancellationToken lifetimeToken):单次操作;同实例重复调用返回同一任务,不再登录、不换Cookie、不产生第二个Owner。首个调用的token定义本次上下文生命期,后续调用的token不替换它;接口文档和测试明确这一点。调用者若仅停止等待,不得据此新建上下文绕过Close。- 返回
PlayerKVLoginPrepareResult:Prepared、Error、AccountError、OpenError、OwnerError、KVError、SyncStatus、HTTP状态(可空)、CleanupFailed,以及非敏感身份字段PlayerId/Epoch/OwnerInstanceId。失败时Data=null。 - Prepared仅表示“认证成功且取到当前Ready快照”,不表示新档/有效业务档/已初始化/可以进游戏。成功Data为独立不可变字符串字典;允许成功且Count=0,必须显式
IsEmpty=true。该空结果来源于成功Get,不能被消费方当作建档批准。 CloseAsync():首次调用同步撤销上下文(方法返回Task前已不可再发布有效结果),返回共享可等待任务;重复调用返回同一任务。不提供同步Dispose或超时后强制释放。IsCurrent:当前代际仍有效、未撤销、session/Cookie仍匹配;业务不能仅凭历史Prepared布尔值判断当前有效。已返回快照不会被销毁,但其归属过期后不得用于初始化或写入。- 本阶段不公开session、Cookie、Handle、Update/Delete/Flush接口,不调用CustomServerMgr.UseAccountSession;未来接线若需非拥有式session绑定或写句柄,必须在4B2/4B3方案中明确扩展,不临时绕过私有所有权。
3.2 错误分类
| Error | 条件 | 数据/收尾规则 |
|---|---|---|
| None | 认证、Open、Read及最终归属检查成功 | Prepared=true;返回快照与身份;IsEmpty只表示空字典 |
| Validation | ID/缓存根/委托等无效 | 不创建传输;Data=null |
| Cancelled / Stale | 本次token取消/外部代际失效/关闭 | 不发布可用快照;清理完成前不完成失败返回(CloseFailed除外),不借新session |
| AuthenticationFailed | S101登录结构化失败 | 原AccountError和HTTP状态保留;Dispose本次session,未发KV Get |
| OwnerRejected | Bridge Open失败 | 原OpenError/OwnerError/KVError/SyncStatus按可得信息保留,不用默认Ready代替未知状态 |
| NotReady | Open完成但Read拒绝,如Conflict/Unknown/StorageError | 原状态保留,Data=null,不读旧PF/PlayerPrefs补档 |
| Unexpected | 工厂/回调或预期外异常 | Data=null;只记录异常类别,不记录值/请求/Cookie;仍执行清理 |
| CloseFailed | Bridge或session清理失败 | CleanupFailed=true,保留原失败原因与所持对象/任务;禁止重开;不把失败Close标成功 |
AccountError等现有错误字段没有适用值时用可空字段,不能用None暗示该阶段已成功。
4. 顺序、关闭与并发规则
4.1 准备链
- 同步验证固定输入和外部归属;进入单次Preparing。取消或过期请求不分配新传输。
- 新建S101AccountSession,await LoginAsync;失败不调用Bridge。成功后再次核对epoch、生命周期及session身份。
- 使用同一session和固定缓存根/epoch调用4A Bridge.OpenAsync,接受4A“真正收尾后才放行”的保证。期间任何取消必须继续等待底层构造/传输收尾,不只看上层排队取消返回。
- 固定Handle.ReadLocalSnapshot;保留Ready/NotReady/Stale区别,获取不可变快照。最后同步检查上下文撤销标志和外部归属后,才完成Prepared结果。
- 不调用业务初始化、默认建档、PF加载、PlayerPrefs、写入或Flush。任何失败走统一关闭路径。
4.2 单次实例与跨实例责任
一个context只代表一次登录,不提供重登方法。调用方必须await旧context.CloseAsync成功后才能创建下一context;这一顺序在4B1组合测试中演示,并作为4B2登录Gate的必测合同。4B1不宣称能阻止任意外部代码故意新建多个context。Close失败的对象必须保留,禁止通过重新new对象规避关闭失败;同账号缓存锁只是兜底,不能替代跨账号关闭屏障。
4.3 防止失败与Close互等
必须区分两项任务:底层prepare工作结束信号(workSettled)与对外Prepare结果。workSettled只表示认证/打开/读取不再执行;对外失败结果可继续等待清理。
- Close先原子撤销本context,锁外取消context自身的CTS,再await workSettled,随后await bridge.CloseAsync,最后Dispose账号session。
- prepare路径成功或失败都在finally完成workSettled;失败路径只能在完成该信号后等待统一Close。Close不得等待对外Prepare任务,避免“Prepare等Close、Close又等Prepare”的环。
- workSettled完成时Bridge内部可能仍有排队收尾;仍必须await Bridge.CloseAsync,不能把它省掉。
- Close在Prepare之前被调用时,将本实例永久关闭,并使工作信号无需等待;后来Prepare返回结构化Cancelled,不发请求。
- Prepare/Close并发发布、同实例重复Prepare/Close、token注册/取消/释放的临界区必须统一设计。锁内只改状态/捕获任务,不Cancel、不调用外部委托、不执行I/O、不await。
- 取消回调只同步撤销并触发非阻塞收尾,不等待Close;后台收尾任务必须保留并可由Close观察,不能遗失异常。
- 外部epoch可以任意时刻变化;4B1不轮询。下一边界检查立即拒绝失效结果,调用方在代际切换时必须调用Close。已返回快照不是永久授权。
4.4 资源关闭次序
撤销业务准入 → 取消本次生命周期 → 等prepare工作settled → await Bridge.CloseAsync → Dispose S101AccountSession → 释放context CTS/注册。
Bridge Close失败时保留session、bridge及故障任务,不提前销毁Cookie/HttpClient;失败状态永久阻塞该实例。session.Dispose失败也保留CloseFailed,不能当作可继续登录。Close从不隐式Flush,Unknown不自动重试。关闭期间不处理游戏退出、Hub或渠道SDK;这些由4B2在组合层按此顺序接入。
4.5 外部归属委托
采纳4A P3-3:只读取捕获ID/epoch及撤销标志;不I/O、不等待任务、不触发登录/关闭,不获取与Close反向依赖的锁。异常视为失效并保留异常类别诊断,进入安全清理。不要使用“当前全局账号相等”替代本次context身份;同号重登也必须区分epoch。
5. 业务档及未来接线的硬边界
- 4B1返回成功空快照是协议事实,不是新档批准;
newCreate=true亦不自动授权。4B2默认策略为现有账号空KV阻塞,不复制PF/PlayerPrefs,不写默认档。新玩家建档另定显式流程。 - 4B1不验证NewPlayerDataDic、GuideSaveData、MMTest等业务格式,不声明可玩。4B2在调用现有LoadPlayerData前完成完整业务校验与清理方案;不能只验证一项JSON就宣布完整档有效。
- 4B2需要为InitData/MMTest/Agg渠道标记/体力恢复等写入副作用逐项选择延迟、暂存或禁写策略,并验证失败回滚/重建业务内存;未审清不得接游戏。
- 4B3必须按回调创建时的context捕获归属。仅让PlayFabMgr.Instance转发到“当前Handle”不能阻止旧回调写新账号。每条修改还须防止旧回调先修改共享业务对象、再被存储层拒写的情况。
- PF身份暂留作资料及外围依赖,ApiPlayerId作KV身份;不全局改UserId、不删SDK。普通保存本地Accepted与远端Confirmed明确区分;立即消费留4C结构化处理。
- tickets有界历史仍为长期会话启用门,4B1不新增票据、不修改Coordinator来顺便解决此问题。
6. 4B1文件白名单(仅待审提案)
| 类型 | 精确路径/职责 |
|---|---|
| 新生产文件 | Client/Assets/Scripts/Services/PlayerData/PlayerKVLoginContext.cs:单次认证/准备/资源所有权与统一关闭 |
| 新结果模型 | Client/Assets/Scripts/Services/PlayerData/PlayerKVLoginPrepareResult.cs:错误、阶段证据、不可变快照及身份 |
| 新meta | 上述两文件对应 .meta |
| 新合成测试目录 | CodexOutput/upserver/KVLoginContextTests/:Program.cs、显式链接真实生产源码的csproj、独立runs及诊断输出 |
| 文档 | 本方案、4B1任务/实施/交付/审查文件、主日志与共享状态入口 |
禁止修改S101AccountSession、4A Bridge/Owner/结果、3A状态/磁盘、3B Coordinator、KV Client、旧测试Program/csproj、UserService、PlayFabMgr、DataCenter、Stages、服务器、包/asmdef。如发现现有公开接口不足或旧测试包含通配符导致编译问题,先记录最小差异并另行补审,不能边写边扩大范围。
测试需注入创建session/bridge的internal工厂,用现有S101AccountSession的handlerFactory和Bridge internal handler构造,不新增公共任意传输入口。生产默认固定地址、Cookie和禁重定向沿用现有实现。工厂只允许离线测试故障注入,不替换生产所有权约束。
7. 4B1验收矩阵
以下均为计划,尚未运行。并发使用TaskCompletionSource/事件闸门,不用Sleep断言先后。所有登录/Get只走synthetic.invalid替身;缓存位于本次run目录。
| 编号 | 场景 | 必须观察结果 |
|---|---|---|
| B1-01 | 正常认证/Get/读取 | 请求顺序正确,同一Cookie归属;Prepared及快照正确;无Set/Delete/PF/初始化回调 |
| B1-02 | 非法ID/根/空委托、提前取消、提前代际失效 | Data=null,0传输,结构化错误 |
| B1-03 | 认证HTTP/协议/拒绝/网络失败 | 保留具体AccountError/HTTP状态;0 KV Get;本session已清理 |
| B1-04 | 初始Get失败、Busy、Conflict、Unknown、StorageError | 不返回可玩数据,保留原错误和状态,不回退PF、不写空档;清理成功后新context可由调用者打开 |
| B1-05 | 成功Get返回空字典,分别newCreate true/false | Prepared且IsEmpty=true;无建档、初始化或任何变更请求 |
| B1-06 | 同context重复Prepare(不同token) | 同一任务、仅一次登录/Get,后续token不替换首个生命期;定义有测试 |
| B1-07 | 认证忽略取消时Close | 同步失效,Close与对外失败结果等真实返回;不提前Dispose传输;底层结束后清理 |
| B1-08 | Get忽略取消时Close,之后准备102 | 101底层任务与Bridge排水完成、session再Dispose;测试调用者等待旧Close后才启动102;无Cookie/快照串号 |
| B1-09 | Read前/后epoch失效及同号重登 | 失效优先,不发布旧快照;旧结果归属检查false;Owner不复用 |
| B1-10 | Prepare失败与Close同时发生 | workSettled/对外结果分离,无互等;测试以闸门推进并有整体超时保护 |
| B1-11 | Prepare前Close、并发重复Close、取消回调重入 | 关闭幂等、任务不遗失、CTS无重复释放问题;关闭后Prepare零请求拒绝 |
| B1-12 | Bridge Close/清理异常 | CloseFailed与原错误都保留;session未提前Dispose;context不重开;不假成功 |
| B1-13 | isCurrent/工厂异常 | 结构化Unexpected或Stale,Data=null;可释放资源全部按顺序释放,失败仍记录 |
| B1-14 | pending/Unknown已有缓存 | pending只读呈现且Close零写;Unknown拒读且不重试;不改变既有恢复语义 |
| B1-15 | 输出与不可变性 | 仅用例名/状态/计数/脱敏身份摘要;无Cookie/Token/完整值/用户路径;快照脱离内部字典 |
| B1-16 | 回归与编译 | 4A18组、42/86/29、Review、3C离线8/8、KVLiveTests只build、Unity CLI Loop编译;未授权修改文件hash不变 |
采纳P3-1:每条命令保留完整stdout/stderr和真实退出码;若86套件再次失败,保留首轮用例名及原输出,再诊断,不覆盖或只报告后续成功。已有偶发失败根因未定位。
8. 交付、评审问题与自审
实施获批后交付:生产/测试文件完整SHA256、实施前后范围hash、逐行矩阵、各命令/退出码、每个检查点日志、限定自审及外部实施复核提示词。所有实际测试以独立run记录;不访问正式服,不运行KVLiveTests或3C --live-s101。
外部审查必须分别裁决:
- 是否接受4B分期,以及4B1仅为准备内核、不提前接游戏的边界。
- session所有权、workSettled与失败结果分离、Close异常保留是否完整,是否存在取消/重复操作互等或资源泄漏。
- 成功空快照与失败Data=null是否足够清楚,是否会被误解为游戏初始化放行。
- internal替身工厂和精确白名单是否足以实现;若需扩旧接口,先给最小差异,禁止默认扩大。
- 矩阵是否覆盖生命周期竞争,P3待查项是否准确保留,4B2/4B3启用门是否存在被绕过的路径。
主会话文档自审:分期目的、职责/非职责、错误、生命周期、资源关闭、文件边界和测试均已写明;没有将后续业务档校验或所有回调隔离当成4B1已经解决。4B2/4B3明确属于未来独立方案,不能以本文件批准其编码。原工作区改动与历史证据保留,不自动提交Git。
流程检查表:
- 读取既有阶段方案、4A复核、登录/存档/退出及渠道初始化相关源码。
- 沿用已有用户约束,比较三条路线并提出明确推荐;本轮无需重问地址、工具或打包策略。
- 写出4B1完整提案和逐条验收合同,完成文档自审。
- 用户指定外部AI独立审查本方案;主会话核对版本后逐条裁决。
- 书面方案及边界获批后再实施;当前未开始编码。