Files
ft/CodexOutput/upserver/阶段4B2字段与模块加载合同-正式版3-2026-09-23.md

272 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 阶段4B2字段与模块加载合同(正式版3:合并D1–D5)
日期:2026-09-23。D4/D5依据[配置与派生裁决](阶段4B2检查点2配置与派生审查裁决-2026-09-23.md)批准。正式版2与原受审资料保留。状态:已审设计合同,供检查点2继续实施;不代表已实现或验收。正文合并修订2附件与D1/D2/D3差异修订,依据[差异裁决](阶段4B2检查点2字段差异审查裁决-2026-09-23.md)生效。D3依据[值类型空态裁决](阶段4B2检查点2值类型空态审查裁决-2026-09-23.md)生效。后续实施首读本正式版2;[原受审附件](阶段4B2字段与模块加载合同-2026-09-23.md)及其hash保留供历史核对,不再作为D1/D2的当前规则。其余合同沿用[修订2方案](阶段4B2游戏加载与内存隔离方案-修订2-待评审-2026-09-23.md)及原实施批准。本清单限定于当前PlayerData.LoadPlayerData/LoadPlayerDataNew同步消费链和登录标记,不等于全项目玩法存档盘点。其余按需玩法读取由4B3/4C另审,受控入口不启动那些流程。
## 1. 统一规则
- 所有适配器为本次epoch产生独立的加载状态;不与原快照、上次账号、静态默认字典或其他模块共享可变集合。
- `D<K,V>`为JSON对象解码到相应字典,`L<T>`为JSON数组,`T`为现有保存DTO的序列化字段形状。必须先检查token类型、重复属性、null、数值溢出与有限浮点数;禁止靠Json.NET字符串/浮点到整数的宽松转换判合法。键转为整数/枚举后重复也拒绝。
- 原保存DTO有构造副作用时用新文件中的同形状纯数据结构解码,不能在Decode里Resolve服务;应用时仅传已解码字段。不得反射复制真实模块全部字段,配置/服务/订阅不属于存档。
- 已存在的可选键坏值也使整个加载Malformed。缺失才生成下面列明的空态。JSON文字null不是缺失;集合可以为空但不能为null,活动对象缺失由Missing状态表达,不向旧loader传`"null"`。
- 未识别顶层键保留原快照、计数为Unconsumed,不送给总入口或参与主档判定。注释掉的MonthCardData、ChallengeData、TargetEventPackData、PokerScoreData、EventPartnerData、EventBreak/EventCan不算当前消费键。
- 配置引用只验证该加载步骤会解引用的表项;0作为尚未激活/未选择的明确哨兵保留,不统一要求所有活动ID都为正。活动已失效但载荷可解析时保持Dormant,不启动活动、不自动清档/写回。
- 数字紧凑串按InvariantCulture且无千位分隔解析;时间兼容按`O`(RoundtripKind)优先,之后仅允许本次捕获的CurrentCulture与InvariantCulture的TryParse;若两者成功却得到不同日期,返回UnsupportedSchema,不猜测。newData中日期含分隔逗号导致槽数不是19同样拒绝。既有文化存档的不兼容必须明确报错,未来迁移另审,不改PF解析规则。
## 2. 主档格式:完整核心字段,不再使用“任一特征键命中”
支持两种已知布局:
1. **现代格式**:顶层`NewPlayerDataDic`为`D<string,string>`,其`newData`恰好19个逗号槽;每槽必须可解析。只有NewPlayerDataDic而没有完整newData,不推断为新档。
2. **完整旧格式**:下表的19个顶层键全部存在且合法。不支持用部分旧键加部分新槽拼出主档。NewPlayerDataDic内的旧散键只作一致性校验,不单独授权完整档;缺核心时返回MissingCore,要求显式迁移,不造默认资产。
两格式同时完整存在时逐字段比较规范值,任意不同返回ConflictingRepresentations。现代格式存在但损坏,不回落旧格式。现存非完整旧格式只要包含与现代格式重叠的字段,也逐字段检查一致;不能让残余冲突值被静默覆盖。
| 槽 | 顶层旧键 | NewPlayerDataDic旧散键 | 类型/主档规则 |
|---|---|---|---|
| 0 | Lv | lv | Int32,>=1 |
| 1 | Gold | gold | UInt64 |
| 2 | vipAllScore | vipS | Int32,>=0 |
| 3 | Energy | energy | Int32,>=0,坏/负值拒绝而非改0 |
| 4 | Diamond | pearl | Int32,>=0;源码PlayerData.Pearl常量实际是Diamond |
| 5 | Magnification | mag | Int32,>=0 |
| 6 | EquipRodID | rodID | Int32,必须在固定TbRodData存在,不以默认杆掩盖坏档 |
| 7 | LastEnergyRecoveryTime | EnergyTime | 时间,按§1解析规则 |
| 8 | RewardNewMap | rewardNewMap | Int32原始位集;保持有符号位表示,不按普通计数限制 |
| 9 | EnergyRM | energyRM | Int32,>=0 |
| 10 | GoldRM | goldRM | UInt64 |
| 11 | DiamondTrue | diamond | Int32,>=0;不能错认成Diamond |
| 12 | CurrentMapId | cMapID | Int32;0为原入口哨兵,非0须存在于TbMapData |
| 13 | LastMapId | lMapID | 同上 |
| 14 | FishingFrequency | FF | Int32,>=0 |
| 15 | AllFishingFrequency | AFF | Int32,>=0 |
| 16 | HeistGameCount | HGC | Int32,>=0 |
| 17 | BombGameCount | BGC | Int32,>=0 |
| 18 | FishHuntGameCount | FHGC | Int32,>=0 |
这是一项比原PF加载更严格的KV准入提案,不声称所有历史PF档都兼容;不使用正式账号猜测规则。本阶段只用合成完整档/残档验证。凡不符合支持布局的真实档将来必须走单独迁移,不自动建档。
## 3. 全部当前消费键登记及优先级
表中的模块编号也用于产物诊断。主档19键及NewPlayerDataDic由M01/M02消费;其余顶层键如下,均可缺失但不可损坏。
| 模块 | 顶层键/模式 | 格式合同 |
|---|---|---|
| M01 玩家附加 | NewRodList、SeasonId、IsOpenAppraise、InfiniteBuildingData | L<Int32>、Int32、存在标记(任意非null字符串,仅存在性转true)、原始字符串(加载期不解释建筑载荷) |
| M03 鱼/杆/地图 | RodData、RodLevelData、FishSwimToMap、FishSwimToMapCount、MapProgress、HonorLevelData | D<Int32,Int32>;整型负哨兵按原类型保留,不误把所有键看成表ID |
| M03 鱼/杆/地图 | InitRodAccessoriesData、Map_5_{mapId}、Map_{mapId}、LuckDataDic | RodGSAData形状;D<Int32,string>每项鱼紧凑串;旧D<Int32,FishWeightData>;D<string,float> |
| M04 商店 | ShopPackBuyCount、ShopAllPackBuyCount、ShopGiftBuyCount | D<string,Int32>、D<Int32,Int32>、D<Int32,Int32> |
| M04 商店 | Voucher、ChainPackDataNew、VIPAdPackData、TurntableAdData、BuyData、AdvertAllCount | Int32、DailySignEvent、AdvertData、DailySignEvent、L<float>、D<Int32,Int32> |
| M04 商店 | DiamondShopRefreshCount、DiamondShopRefreshTime、DiamondShopItemRecords、BlackFridayCouponDate | Int32、时间、D<Int32,ItemShopRecord>、优惠券紧凑格式 |
| M05 活动基础 | LimitedTimeEventDic、FixedTimeEventDic、ETDropAfterDropDic、RankTargetCount、WheelLevelCount | D<Int32,LimitedTimeEvent>、D<Int32,FixedTimeEvent>、后三者D<Int32,Int32> |
| M05 活动基础 | TurntableRoll、WheelOperateCount、AllWheelOperateCount、WheelLevelFreeCount、PVPToken | Int32 |
| M06 增益 | BuffDataList、TargetBuffDataList、FishDailyBuff、GlobalBuffDataList | D<Int32,FishBuffTimeData>、L<FishBuffTimeData>、FishBuffTimeData、D<Int32,FishBuffTimeData> |
| M07 营地 | IsFix、IsOpenSkyscraper、CampDataDic | bool、精确0/1字符串、D<string,Int32>;不能调用SetIsFix默认isSave=true |
| M08 成就 | AchievementData | D<ConditionType,AchievementData> |
| M09 钓鱼箱 | FishingBoxData | 原FishingBoxData的序列化形状,实际加载FishingBoxOpenNum;FishingNewBoxs按§5重置 |
| M10 引导 | GuideSaveData、GuideSaveDataNew | D<string,GuideSaveData>;新格式`group|index|0或1`以`;`连接 |
| M11 战令 | BattlePassRecordTask、BattlePassRecord、BattleTask | 新组合紧凑格式;旧BattlePassRecord/旧BattleTask JSON形状 |
| M12 签到 | MonthlySignEvent | MonthlySignEvent形状;dailySignEvent不允许null |
| M13 触发礼包 | TriggerPackDataList、GiftVIPDData、FixedTimeShopDic | L<TriggerPackBuyData>、D<string,GiftVIPDData>、D<Int32,FixedTimeEvent> |
| M14 公会加载态 | ClubSignin、LureDonateData、IsOpenClubRed | 时间或空字符串(无签到)、LureDonateData形状、存在标记(非null字符串→false) |
| M15 挑战 | FishingChallengeData | 现有InitChallengeData(string)的DTO形状,仅数据解码,不启动挑战 |
| M16 宝箱链礼包 | OfferChainsChestData | TOfferChainsData |
| M17 Disco礼包 | OfferDiscoPartyManager | TOfferDiscoPartyData |
| M18 伙伴 | EventPartnerGatherData | EventPartnerGatherData,不能调用带Save的ClearData |
| M19 消块 | EventCubeMeltData | EventCubeMeltData |
| M20 活动礼包 | EventPackData | 现有LoadEventPackData的数据形状;配置派生仅在Apply前解码计算 |
| M21 储蓄罐 | PiggyBankPackData | PiggyBankPackData.Data;旧兼容redirectID=0规范为4055001并检查配置存在,不写回 |
| M22 砍价礼包 | GeneralEventPackData | GeneralEventPackData;redirectID须存在且PackType=3才可交BargainPackData,否则UnsupportedSchema,不能静默丢弃 |
| M23 足迹 | FootPrint | UInt64位集,完整值由新增私有ulong受控字段保存;现有Int32枚举仅提供低32位视图,不代表全部64位;不调用UploadData,不拒未声明高位 |
| M24 靶场 | EventShootingRangeData | EventShootingRangeData.RawData |
| M25 药水 | epdk | EventPotionData |
| M26 弹珠 | epbdk | EventPinballData |
| M27 刮刮卡 | estdk | EventScratchTicketData |
| M28 节日礼包 | ThanksGivingPackData、ThanksGivingPackMigrationData | 各自4个Int32逗号槽:EventId,RedirectId,TokenProgress,ChainProgress |
| M29 Pinata | EventPinataPackData | EventPinataPackData;仅SetData对应数据赋值,不GetFreshPinataPackData |
| M30 挖矿加载壳 | 无 | InitServerData当前忽略字典,仅在_dataModel为空时new;KV必须替换成新未启动模型,不能复用旧实例 |
| M31 登录附属 | MMTest、hwGoogle、hwFacebook、Apple、EAccoutUrl、TapJoy_Diamond | MMTest规则见主方案;渠道精确0/1;D<string,string>;Int32余额。额外当前非游客渠道名只允许来自测试身份枚举,不能任意字符串入意图 |
### 3.1 NewPlayerDataDic的附属键
| 键 | 解析及覆盖语义 |
|---|---|
| vipL | `Int32,Int32`记录以`|`连接;空串为空字典,重复键拒绝;替换vipScoreDic |
| ld | 恰好3个有限float,以`|`连接:InitFishLuck、InitBotLuck、BotCount;替换LuckDataDic;BotCount必须能无损转Int32 |
| ad | `ConditionType:value1|value2|value3`记录以`;`连接;ConditionType需有定义,三值按AchievementData.ToObject的声明类型严格解析,不容忍半条忽略;替换成就字典 |
| bd | `Int32,Int32`记录以`;`连接;替换FishingBoxOpenNum,空串为空字典 |
| ct、rt | `Int32:Int32`记录以`,`连接;分别替换ETDropAfterDropDic、RankTargetCount,空串为空字典 |
| LDD | 旧JSON格式同LuckDataDic;与ld共存时先分别严格解码,规范值必须一致,否则冲突 |
| AD | 旧JSON为D<ConditionType,string>,每个字符串恰好`Id\|Target\|day`三槽,按Int32/UInt64/Int32、InvariantCulture严格解析;不同于顶层AchievementData的对象值。键须为已定义ConditionType,归一化后重复键拒绝。与ad/顶层共存时全部先独立严格解码,共有字段冲突则ConflictingRepresentations;一致按`ad > AD > 顶层AchievementData`选择并整字典替换,不合并旧账号条目 |
NewPlayerDataDic中的其余旧散键为§2的19项,全部校验。其它内部键保留为Unconsumed,不能参与初始化。
同义表示同时出现时不依赖Dictionary迭代顺序:核心先作一致性检查;其余 `Map_5_x`与`Map_x`、`GuideSaveDataNew`与`GuideSaveData`、`BattlePassRecordTask`与旧战令、`ld/LDD/LuckDataDic`、`ad/AD/AchievementData`、`bd/FishingBoxData`、`ct/ETDropAfterDropDic`、`rt/RankTargetCount`全部先分别严格解码,共有字段不同则Conflict;一致时选新格式,缺新则选旧。新引导替换整个字典,不把未出现在新格式的旧组静默合并。
### 3.2 紧凑格式与动态键边界
- Map动态后缀必须正整数且在固定TbMapData内;同一数值的非规范十进制后缀拒绝。只匹配`Map_5_数字`与`Map_数字`,其它前缀属于Unconsumed。鱼记录支持源码现存5/9槽两种:5槽Level/MaxG/TotalCard/TotalNum/NewFish;9槽采用0/1/5/6/7这五个位置,其余兼容槽必须保留但不作为当前字段。所有被消费槽严格解析。
- BattlePassRecordTask恰好两个`#`分段(即一个分隔符);第一段按现存BattlePassRecord 7个`|`字段解析(ID、Level、Exp、BuyVipToastIndex、IsUnlockVip、两份逗号ID列表),第二段按BattleTask 5个`|`字段(两份`key:value`逗号字典、loginDays逗号列表、两个截止ticks)。空集合串允许,记录字段数量不符拒绝;ticks须在DateTime范围内。以受审源码SerializeObject顺序为准,不能改存档格式。
- BlackFridayCouponDate支持7槽旧格式或9槽新格式,6个日期分量必须组成合法DateTime;旧格式RedirectId=0/IsBought=false,新格式末位只允许0/1;8槽或多余槽拒绝。
- ThanksGiving两种键都严格4槽;不能复用原Data.Deserialize的“错误时打印原值并返回零对象”容错。
- 所有分隔集合末尾空记录按当前保存器的Join格式:空整体表示空集合,非空整体中的空记录/重复key为Malformed。不得日志输出被拒绝的原字符串。
## 4. 无副作用加载路径及写点裁决
修订2不再调用旧LoadPlayerData/LoadPlayerDataNew总入口,以下旧路径仅为提取数据规则的依据。
| 旧同步链/风险点 | KV路径明确处置 |
|---|---|
| CompareDatas的PF×PlayerPrefs合并与写回 | 完全不调用;只有4B1不可变快照和独立解码状态 |
| InitData MMTest缺失/旧格式两处写+SetMMTestValue写 | 内存规范化及本epoch意图;不运行GEvent统计 |
| Agg InitData补渠道、AvatarToPF写EAccoutUrl | 受控仅纯标记合并;不运行真实SDK和Login返回后的头像/邀请链 |
| Camp.SetIsFix(isSave:false) | 原路径本次抽查安全,但KV直接赋私有字段,禁止误调用默认true |
| PlayerData.energy相关setter、体力恢复、SavePlayerData | 直接给energy/_lastEnergyRecoveryTime等赋值,不调用SetEnergy/恢复/保存或事件发布 |
| PlayerFishData InitData中的GEvent、地图派生调用 | 纯decode后整组替换;不统计、不发解锁提示 |
| FishingEventData Init/SetEvent/目标/排名恢复 | 不调用;只替换数据和置空运行派生对象;LimitedTime按冻结时钟做纯过滤 |
| Potion/Pinball/Scratch GetDataFromServer→Init | 不调用;只解析并替换原始数据,运行配置状态未激活 |
| Pinata.LoadData→GetFreshPinataPackData | 不调用刷新;只替换纯数据,奖励、红点不启动 |
| Bargain.LoadData→AddGiftFaceUIForce | 不调用旧函数;只替换_data |
| Partner.ClearData→SaveData、Cube.ClearData、活动默认建档 | 不借这些函数Reset;新增纯空态替换 |
| Shop.Init、BattlePass刷新任务/领奖、Signin刷新 | 不调用;加载不等于功能激活 |
| Digging.InitServerData之后的模型InitServerData异步服务 | 本阶段只new空模型;禁止调用异步业务初始化 |
| 所有旧LoadData带ELog/Debug输出原JSON、TryParse吞错 | 新KV纯入口只用结构化诊断;不能借用其日志和容错路径 |
| PlayFabMgr定时/暂停/销毁/普通/立即写 | 主方案guard前置拦截;违反计数导致受控加载失败 |
因此无需声称未经读取的所有玩法保存调用都被4B2重构。受控路径只允许运行附件的纯适配器;未来新调用/活动激活属于范围变化,先补方案再审。
## 5. 每模块空态、替换职责和精确文件清单
以下路径相对`Client/Assets/Scripts/`。每行都要实现/测试 `DecodeForKVLoad`(纯)、`ResetForKVLoad`、`ReplaceFromKVDecoded`;同一类可合并方法但不能遗漏状态。固定配置/注入服务引用保留;旧账号的已加载数据和从它派生的缓存必须清除。无玩法运行时的订阅/计时器要求为零,非零拒绝准入,不在此模块中擅自扩展玩法退出。
| ID | 文件(白名单) | 缺键/撤销时的明确空态与替换集合 |
|---|---|---|
| M01 | DataCenter/DataCenter.PlayerData.cs | NewPlayerDataDic/vipScoreDic/newRodList新空集合;19核心字段置为不可发布空态(数值0、时间MinValue),合法Apply后全部由核心档替换;seasonId=0、IsOpenAppraise=false、InfiniteBuildingData=null;vip/vipLastScore/PriceLv=0、priceLevel=null,地图缓存0。空态仅用于未就绪,不允许作为新档进入玩法 |
| M02 | DataCenter/DataCenter.SmallGame.cs | 三个游戏次数=0;加载派生的对手/预选缓存空、当前对手null;不触发取对手或网络 |
| M03 | DataCenter/DataCenter.PlayerFishData.cs;DataCenter/PlayerFishDataRod.cs | mapFishData/mapProgress/fishData/fishCardData/rodData/rodLevelData/fishSwimToMap/fishSwimToMapCount/LuckDataDic/_honorlevel全新空集合;rodAccessoriesData=null;InitBotLuck/InitFishLuck/BotCount=0;所有地图/杆加载派生开关false、选择0。缺配件不调用默认建档方法 |
| M04 | DataCenter/DataCenter.PlayerShopData.cs;UI/Shop/BlackFridayCouponData.cs | 购买/广告/钻石商店字典和buyData空,Voucher=0,ChainPackData/VIPAdPackData/TurntableAdData=null,刷新计数0/时间MinValue;CouponData原对象中账号字段归零/MinValue/false;SKU及配置引用不替换,不触发IAP查询 |
| M05 | DataCenter/DataCenter.FishingEventData.cs;DataCenter/FishingEvent_PVPData.cs | 限时/固定/目标/排名/WheelLevelCount全新空集合;TurntableCoupon取固定表InitWheelTicket,WheelOperateCount/AllWheelOperateCount/freecount/PVPToken=0;目标队列、rankInit/rankTarget、wheelInit/wheelLevel和活动实例引用null;解锁集合空。缺活动是未激活,不启动Init |
| M06 | DataCenter/BuffDataCenter.cs | map/target/global buffs和showFishBuffIds空,fishDailyBuff=null |
| M07 | Camp/CampDataMM.cs | CampDataDic空,_isFix=false、_isOpenSkyscraper="0";_id=101、PreID/AreaIndex/AreaStep/AreaId/AreaStepCount/TimelineId=0;当前场景/StepInfo/AreaList派生引用清空,不加载建筑/资源 |
| M08 | DataCenter/AchievementDataManager.cs | _achievementDatas整字典替换;空态空字典,不继续合并旧条目 |
| M09 | DataCenter/FishingBoxData.cs | FishingNewBoxs/FishingBoxOpenNum新空字典,不领取、生成箱子或发布奖励 |
| M10 | DataCenter/DataCenter.GuideData.cs | GuideSaveDataDic空、curSaveData/curDefine/curGroupDefine/popupPanelNode=null;配置指引定义保留,不启动引导 |
| M11 | DataCenter/BattlePassData.cs | BattlePassRecord新构造态(ID/Exp/BuyVipToastIndex/IsUnlockVip/InflationRate=0,Level=1,三份奖励列表空),BattleTask新零态且集合显式空;不Refresh/Save/领奖 |
| M12 | DataCenter/DataCenter.SignupData.cs | _monthlySignEvent=null;存在时替换,不读取会刷新日期/保存的dailySignEvent getter |
| M13 | DataCenter/DataCenter.TriggerPackData.cs | triggerPackList/giftVIPDData/fixedTimeShopDic新空;不Init、不注册条件事件 |
| M14 | Services/ClubService.cs | signTime=null、lureDonateData新零态、IsOpenClubRed=true;只处理三项加载态,不登录公会/取网络成员信息 |
| M15 | DataCenter/FishingChallengeManager.cs | 原始挑战存档对象null,挑战运行态未激活;有数据直接替换,不调用无参InitChallengeData |
| M16 | DataCenter/OfferChainsChestManager.cs | OfferChainsData=null;ValidIds空、活动派生引用null,不进场、不改FirstOpen |
| M17 | DataCenter/OfferDiscoPartyManager.cs | OfferDiscoPartyData=null、ValidIds空、活动派生引用null;FreeTime回构造值1 |
| M18 | DataCenter/EventPartnerGatherManager.cs | GatherData=null、_isEventDataChanged=false、当前伙伴运行引用null;PartnerInfoManager/RobotInvitationSystem替换为空的新实例,不调用含Save的ClearData |
| M19 | EventCubeMelt/Data/EventCubeMeltManager.cs | EventData=null、_isInit=false、活动运行引用null;不调用Init/ClearData |
| M20 | DataCenter/EventPackData.cs | _data是单个Data结构,空态四字段currentEventID/purchaseCount/VIPLvlWhenEventActivate/redirectID全0;存在时整体替换,活动有效且redirectID=0时沿用4054001的旧兼容映射并校验表,不TriggerEventPack、不发布红点 |
| M21 | DataCenter/PiggyBankPackData.cs | 原Data struct保持;Reset currentEventID/purchaseCount/VIPLvlWhenEventActivate/savingProgress/redirectID=0,doNeedTrigger/isTriggered=false,inflation=0,_kvLoadPresent=false(internal只读KVLoadPresent)。Missing保持该状态;Present八字段全赋后标记true;4055001映射仅用于存在载荷,不UpdateData |
| M22 | DataCenter/BargainPackData.cs | 原GeneralEventPackData struct保持;Reset十一整数、两bool、inflation清零/false,selections全新空List,_kvLoadPresent=false(internal只读KVLoadPresent)。Present通过PackType=3验证后完整替换、集合深复制、标记true,不经会触发FaceUI的LoadData;GeneralEventPackData.cs不改 |
| M23 | Services/FootPrintService.cs | 新增私有ulong完整KV位集及internal只读验证访问;Reset完整字段和_footPrint同时归零,Replace完整保留UInt64并用显式unchecked低32位投影到_footPrint(注释说明视图位宽)。公开EFootPrint : int、IFootPrintService及旧LoadData/UpdateFootPrint/UploadData不改;受控范围禁止旧保存,完整值的4B3消费另审,不承诺退出受控后可玩 |
| M24 | UI/ShootingRange/ShootingRangeData.cs | 原RawData struct保持;Reset EventId/CycleId/RedirectId/AmmoCount/CurrentStageIdx/ChainProgress=0,RewardFlag/TargetFlag按Int64归零,HasBeenOpened=false,Inflation=0,_kvLoadPresent=false(internal只读KVLoadPresent)。Missing不赋null,Present全字段替换后标记true;未就绪不得读玩法getter |
| M25 | DataCenter/EventPotionDataManager.cs | m_PotionData=null,运行关卡/表缓存回未激活;存在只装载原始数据,不调用Init |
| M26 | DataCenter/EventPinballDataManager.cs | m_PinballData=null,运行关卡/表缓存回未激活;同上 |
| M27 | DataCenter/EventScratchTicketDataManager.cs | m_ScratchTicketData=null,运行关卡/表缓存回未激活;同上 |
| M28 | UI/Shop/ThanksGivingPackData.cs;UI/Shop/ChainPackWithProgressData.cs | 两个实例的_data各自Missing/null,无共享可变对象;不做礼包迁移/领奖 |
| M29 | UI/PinataPack/EventPinataPackDataManager.cs | 同一容器的EventPinataPackData实例直接将EventID/PinataMainID/PurchasedCount三个自动属性全置0,然后由已解码状态全量赋值;不调用AddPurchasedCount或GetFreshPinataPackData,不替换容器注册、不反射。Decode不得new该数据类(其字段初始化会Resolve表),用纯三整数DTO |
| M30 | SandDig/DiggingGameManager.cs | _dataModel替换全新未初始化实例、_broadLogic=null、游戏状态Normal;已有玩法资源/音频/board时拒绝受控准入,不在此释放活跃玩法资源 |
| M31 | Services/UserService.cs及其新增KV partial;Services/AggUserService.cs | 本次副本/意图/绑定集合独立,MMTest=0、EAccoutUrl空、TapJoy余额0;真实SDK集合不作跨账号复用;PF公开身份默认不因受控加载而变成已授权 |
上表的“回未激活/派生引用”限定为相应loader能够写入或由该存档直接缓存的状态,不授权全面重构模块。实现需将各行定义映射到字段并在测试中显式断言,不能只测一个代表字段。若源码出现清单外的同步消费或状态写入,视为设计差异先补审,不能自动扩白名单。
固定Apply顺序:M01基础核心→M02→M03→M04→M05→M06→M07→M08→M09→M10→M11→M12→M13→M14→M15…M30→M31通知。全部Reset先执行,再开始任何Replace;物化DTO之间的引用由本次状态创建,配置引用只能来自同一只读快照。
## 6. 附件验收与证据口径
- 每行至少:存在合法值、完全缺失、存在坏值;A有数据/B缺数据在同对象上检查整组字段;所有三类用例不能只靠字典已清空来冒充实际模块状态。
- 每个现代/旧表示同时出现的组合验证一致与冲突;动态地图多ID、缺某地图、旧地图不会残留;同一模块多个可选键只缺其中一项也须清空该项。
- 数据类型测试覆盖两种货币键别名、19槽精确长度、时间文化歧义、负位掩码与非负计数区别、null/重复属性/未知表项;无正式账号值。
- 零副作用计数同时覆盖写入、SDK/网络调用、PlayerPrefs、事件、UI及计时器;任何非白名单调用导致失败,不能用最终数据库没变化代替。
- 本附件为已审设计约束与待验清单,没有声称31模块已经实测通过,也没有声称现有业务支持重载;主会话须在代码交付后逐行填入实测证据。
## 7. D1/D2补审合并的边界与必验项
D1:空对象和空紧凑整体表示空集合。JSON null、对象值冒充旧AD字符串、缺槽/多槽、空记录、溢出、重复属性、枚举归一化后重复和未定义枚举均为Malformed;不尝试另一值类型解码、不回落合法新表示掩盖坏旧表示。分别验证三种成就表示的合法/缺失/坏值、AD字符串正例/对象值反例、Target超UInt64、重复枚举别名、三者共存一致/冲突、坏AD与合法ad共存仍失败。M08在同对象A→B缺失时为空,零保存/事件。
D2:明确投影表达式例如`unchecked((EFootPrint)(int)(uint)completeBits)`,注释视图仅含低32位、完整值保留在ulong字段;不依赖默认溢出检查设置。验证0、低位组合、2^31、2^32、UInt64.MaxValue的完整值与投影视图,拒负值/溢出,A高位→B缺失时两个字段一起归零。必须在Unity真实对象上验证,离线算术不能替代;旧PF原方法及公开面保持,既定PF行为回归仍执行。guard禁止受控范围内UploadData写入,未来4B3完整值消费另审。
P2-α迁移盘点输入新增:旧PF成就加载器曾静默跳过的AD坏槽条目。当前受控KV按严格合同整体Malformed;未来真实档方案必须覆盖识别、备份、修复或人工退出,并经独立评审及用户产品确认。此次不读取或迁移真实档,不授权自动跳过/清档/默认建档。
版本历史:原附件SHA256为9E1A49E389CD6E9B38FA2CB55E5839ED5356E915AECCD4787FFE6F8276340745,保持不变。本正式版仅合并已审D1/D2及三条P3落地,未增加业务文件白名单;4B2实施完成仍需独立实施复核,检查点1不重做,检查点2–4顺序不变。
## 8. D3合并历史与验收补充
正式版1及原受审附件保留。D3已外部通过并由主会话裁决,三处值类型空态使用显式存在标记+全字段清零;具体字段与测试以《阶段4B2检查点2值类型空态差异补审方案》§2/§4为准。JSON null仍Malformed,缺键不规范成默认礼包,不改PF原字段类型/方法。同一真实对象A全部字段有值→B缺失→Present,验证所有字段及标记;M22集合不共享、M24 Int64高位清零;失败Reset后标记false,Reset失败Poisoned。D1/D2及其他合同不变。
## 7. D4/D5已批准实施细则(对应原M03/M18行)
以下细则及本轮裁决是M03/M18当前完整合同,原行须结合本节执行。引用差异提案的“建议/提请”均已按裁决接受;此为实施批准,不是验收。
## 2. D4推荐方案:配置由实例持有,容量不再静态缓存
新增唯一生产文件白名单:`Client/Assets/Scripts/EventPartnerGather/PartnerInfoManager.cs`。仅以下修改获审后可实施:
1. 私有`_tables`从静态缓存改为实例只读引用,在现有公开构造路径的字段初始化阶段解析当前容器表;不新增网络、订阅或PlayerPrefs调用。每个实例持有其创建时的表,随该实例释放,不留静态表根。
2. `public static readonly int BuilderCapacity`改为`public static int BuilderCapacity { get; }`形式的计算属性(表达式getter),从当前容器只读表取得`InvitationCount[1]`,不写缓存。**源代码读取表达式保持;字段→属性并非二进制兼容。** 全量Unity编译、所有引用盘点,以及预编译程序集依赖核查是强制验收项,不能仅凭编辑器能跑忽略公开面变化。
3. 实例的`BuildPartners`仍为公开readonly数组,构造长度取同一实例表的`InvitationCount[1]`,与静态属性分开读取;本类`AddBuildPartner`的容量判断和诊断改读`BuildPartners.Length`,避免实例受后来容器切换影响。其它现有调用方继续读取同名静态属性,本轮不改调用方正文。
4. `_friendService`、`_userService`仍为实例依赖;受控宿主只注册受控依赖,不调用`Init`、缓存读写、推荐、匹配或网络。若构造链触发额外服务构造,必须在实例化前拒绝或补齐已审替身,不容许借用真实账号服务。
5. M18 Reset在未启动的受控对象上新建两个伙伴管理对象,GatherData=null、派生引用及标志清空;不得对活跃玩法对象执行此操作。无需改`EventPartnerGatherRobotInvitationSystem.cs`:其默认构造无初始化动作,不调用其Init/CheckAndFire。
源代码目前的有效外部引用:EventPartnerGatherManager三处、EventPartnerGatherRobotInvitationSystem两处,均为数值读取;Mining面板仅注释。没有在源码中发现ref/out或反射取字段调用。这个盘点不自动排除预编译依赖,实施前仍须检查。
不推荐的替代方案:
- 反射改私有静态表并写readonly容量:违反既定反射边界,跨运行时不可依赖。
- 不执行M18真实对象,只用假模块宣布通过:违反B2-20。
- 保留静态表,测试完只恢复GContext:不能证明原全局环境恢复;冷启动和已初始化两种情况都有问题。
- 在正式Root里预热该类型:触碰真实配置/服务环境,且不能解决已缓存旧表与本epoch快照不一致。
若审核者认为字段→属性不可接受,应明确退回D4并给出可验证且不扩大为全局反射的替代,不将当前代码默认为可实施。
## 3. D5推荐方案:先比较原值,再做可证明的纯派生
白名单不扩展:仅既有PlayerKVSaveSchema/PlayerKVDecodedSave、DataCenter.PlayerFishData、PlayerFishDataRod及新验证宿主/测试。不得修改FishData/FishCard生成表、原PF方法或真实存档。
### 3.1 原始解码与配置快照
- 沿用正式版2的6个整数字典、RodGSAData完整嵌套集合、Luck三表示和Map动态键严格语法。Map后缀必须规范正十进制且存在于固定地图表。
- 每个Map旧对象/5槽/9槽先独立严格解码,**先按当前五个消费字段比较重叠记录**;原值冲突直接ConflictingRepresentations,不能先截断、修正后把冲突掩盖。优先新格式整张地图替换。9槽的2/3/4/8兼容槽仍保留在只读原始记录中、不参与当前五字段派生。
- 在epoch建立时复制必要表字段及数组:鱼ID、AreaID、Quality、BaseWeight、PointExtraParam、WeightRandom、FishCardID;鱼卡ID、MaxLevel、CardsRequiredList、WeightMagList。只读纯结构不可持有cfg.Tables或可变配置列表。所有派生只使用这份快照,无Resolve、时钟、Unity对象调用。
- 选中记录不存在鱼配置、AreaID与Map键不符,建议返回UnsupportedSchema;不复刻旧方法的静默丢弃。所需卡配置或下标缺失也返回UnsupportedSchema,不能在Apply中才发生异常。此项严格化必须被明确批准,并加入P2-α真实档兼容盘点。
### 3.2 具体数值变换(按旧步骤顺序)
1. 五个保存字段复制为工作值,Level/MaxG/TotalCard/TotalNum/NewFish原值保留在只读原始层。
2. Quality≤5:计算单精度`base = BaseWeight * PointExtraParam * (1 + WeightRandom)`。原Level>0时,先要求对应鱼卡和`WeightMagList[原Level-1]`存在,再乘`1 + weightMag`;乘法各步及乘100结果均须有限。不先把Level截小来绕过原先缺失的权重下标。
3. 基准重量按旧Mathf.Round的最近整数/中点到偶数规则对`base*100`取整,再以单精度除100;仅当原MaxG大于该结果时取较小值。纯实现与Unity Mathf.Round在0、正负中点、相邻float、普通小数及大值上做对照;不以双精度重排乘法。非有限配置/中间结果返回UnsupportedSchema。
4. 原Level>0:要求鱼卡MaxLevel≥1;应用`Level=min(原Level,MaxLevel)`,再要求`CardsRequiredList[Level-1]`存在,`TotalCard=max(原TotalCard,该阈值)`。Quality>5跳过重量修正,但仍执行本步骤。Level≤0沿用原逻辑,不凭本次补审额外定义负数禁令。TotalNum/NewFish直接保留。
5. 负的有限重量、负卡数等未触发上述既有补足规则的值,仍按声明类型保留,不加未经审查的经济规则。所有保存数值的token/位宽/有限性校验保持。
这些步骤是建议保留的内存派生,不创建持久化意图,不写回修正值,不表示真实旧档可直接启用。
### 3.3 集合物化、冲突及幸运值
- 解码输出同时持有只读原始地图、规范化地图、鱼索引和卡片数量索引。规范化地图与索引在纯阶段完成,不留表引用到Apply。不同已选地图中同一鱼ID无法同时满足归属校验;同一卡ID得到不同TotalCard时返回UnsupportedSchema,不依赖字典遍历顺序决定数量。
- Replace新建每个Map内字典及FishWeightData。`fishData`指向本模块本次Map中的同一FishWeightData实例,保持旧模块内部的索引一致性;不跨账号、快照或模块共享。`fishCardData`从已校验卡索引复制。Reset把三类索引及全部其余保存集合清为新空对象。
- RodGSAData六字段:GloveID、RodToSkin、GloveIds、SkinIds、GloveNewIds、SkinNewIds(一个整数、一字典、四列表)。各集合深复制;缺配件null,不调用默认建档方法。
- Luck表示全部严格解析且共有字段一致,优先`ld > LDD > LuckDataDic`整字典替换;BotCount存在时必须是有限float且可无损落入Int32。建议Replace只替换LuckDataDic,InitFishLuck/InitBotLuck/BotCount仍保持Reset的0:它们原本由后续`PlayerFishData.Init→InitLuck`建立,本4B2只处理Load链,不运行玩法Init。此处请审核者明确裁决,避免自行套用表默认幸运值。
- 其它Rod/Map字典仅按原值复制;地图/杆派生开关false、选择0,不调用InitRod、红点、GEvent或解锁提示。
## 4. 必须补的验证
| 编号 | 必须证明的结果 |
|---|---|
| D4-01 | 无活跃Root且编辑器冷类型状态时,临时容器A构造M18;恢复后无静态临时表/账号依赖残留。测试不反射改字段、不触碰真实Root。 |
| D4-02 | 先后两个隔离容器A/B,使用不同合法容量/配置替身,旧A实例继续用A容量,新B实例和静态属性用B;各次恢复原container/aggregator。本项提请允许仅在内存复制当前配置JSON并改变InvitationCount测试容量,构造后按epoch冻结;不得改配置文件或共享Root,最终B2-20仍使用未改值的当前表快照。 |
| D4-03 | M18同对象A→缺键B→A;PartnerInfoManager/RobotInvitationSystem引用每次换新,BuildPartners/Cache空,旧引用不参与新epoch;不Init、不读写Prefs/SDK/网络。 |
| D4-04 | 全项目引用及预编译依赖核查;固定PF配置时原数值行为一致;公开字段→属性影响写进最小diff和回归说明。 |
| D5-01 | 五/九槽/旧JSON全部消费字段,9槽兼容槽保留,原值冲突不能被归一化消除;未知map/非规范后缀拒绝。 |
| D5-02 | Quality两侧、Level≤0/>0、等级封顶、卡数下限、最大重量上下界、float舍入边界;所需配置缺失和下标越界全部在Decode失败。 |
| D5-03 | 未知鱼/错地图归属/同卡冲突拒绝;互换JSON属性/地图表示遍历顺序不能改变判定及输出。 |
| D5-04 | 三套实际索引一致;A→缺键B后空,A→B不同地图/鱼不会残留;模块内部允许对应索引同实例,跨输入/账号不共享。 |
| D5-05 | RodGSA各集合深复制;ld/LDD/顶层多表示、坏新不回退、非整数/越界BotCount、缺键空态和不执行InitLuck。 |
| D45-06 | 全部通过后才能继续31模块聚合Decode/Reset/Replace、检查点3/4。任一Reset失败仍Poisoned;完整B2-20仍须真实31模块和服务guard/零副作用证据。 |
P3补充:列清全部实例表随动读取者;承认热更时静态容量不再冻结;必须验证原MaxG较小时不抬高。