36 KiB
阶段4B2字段与模块加载合同(正式版3:合并D1–D5)
日期:2026-09-23。D4/D5依据配置与派生裁决批准。正式版2与原受审资料保留。状态:已审设计合同,供检查点2继续实施;不代表已实现或验收。正文合并修订2附件与D1/D2/D3差异修订,依据差异裁决生效。D3依据值类型空态裁决生效。后续实施首读本正式版2;原受审附件及其hash保留供历史核对,不再作为D1/D2的当前规则。其余合同沿用修订2方案及原实施批准。本清单限定于当前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. 主档格式:完整核心字段,不再使用“任一特征键命中”
支持两种已知布局:
- 现代格式:顶层
NewPlayerDataDic为D<string,string>,其newData恰好19个逗号槽;每槽必须可解析。只有NewPlayerDataDic而没有完整newData,不推断为新档。 - 完整旧格式:下表的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、存在标记(任意非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、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、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 |
| M11 战令 | BattlePassRecordTask、BattlePassRecord、BattleTask | 新组合紧凑格式;旧BattlePassRecord/旧BattleTask JSON形状 |
| M12 签到 | MonthlySignEvent | MonthlySignEvent形状;dailySignEvent不允许null |
| M13 触发礼包 | TriggerPackDataList、GiftVIPDData、FixedTimeShopDic | L、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记录以` |
| ld | 恰好3个有限float,以` |
| ad | `ConditionType:value1 |
| 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。仅以下修改获审后可实施:
- 私有
_tables从静态缓存改为实例只读引用,在现有公开构造路径的字段初始化阶段解析当前容器表;不新增网络、订阅或PlayerPrefs调用。每个实例持有其创建时的表,随该实例释放,不留静态表根。 public static readonly int BuilderCapacity改为public static int BuilderCapacity { get; }形式的计算属性(表达式getter),从当前容器只读表取得InvitationCount[1],不写缓存。源代码读取表达式保持;字段→属性并非二进制兼容。 全量Unity编译、所有引用盘点,以及预编译程序集依赖核查是强制验收项,不能仅凭编辑器能跑忽略公开面变化。- 实例的
BuildPartners仍为公开readonly数组,构造长度取同一实例表的InvitationCount[1],与静态属性分开读取;本类AddBuildPartner的容量判断和诊断改读BuildPartners.Length,避免实例受后来容器切换影响。其它现有调用方继续读取同名静态属性,本轮不改调用方正文。 _friendService、_userService仍为实例依赖;受控宿主只注册受控依赖,不调用Init、缓存读写、推荐、匹配或网络。若构造链触发额外服务构造,必须在实例化前拒绝或补齐已审替身,不容许借用真实账号服务。- 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 具体数值变换(按旧步骤顺序)
- 五个保存字段复制为工作值,Level/MaxG/TotalCard/TotalNum/NewFish原值保留在只读原始层。
- Quality≤5:计算单精度
base = BaseWeight * PointExtraParam * (1 + WeightRandom)。原Level>0时,先要求对应鱼卡和WeightMagList[原Level-1]存在,再乘1 + weightMag;乘法各步及乘100结果均须有限。不先把Level截小来绕过原先缺失的权重下标。 - 基准重量按旧Mathf.Round的最近整数/中点到偶数规则对
base*100取整,再以单精度除100;仅当原MaxG大于该结果时取较小值。纯实现与Unity Mathf.Round在0、正负中点、相邻float、普通小数及大值上做对照;不以双精度重排乘法。非有限配置/中间结果返回UnsupportedSchema。 - 原Level>0:要求鱼卡MaxLevel≥1;应用
Level=min(原Level,MaxLevel),再要求CardsRequiredList[Level-1]存在,TotalCard=max(原TotalCard,该阈值)。Quality>5跳过重量修正,但仍执行本步骤。Level≤0沿用原逻辑,不凭本次补审额外定义负数禁令。TotalNum/NewFish直接保留。 - 负的有限重量、负卡数等未触发上述既有补足规则的值,仍按声明类型保留,不加未经审查的经济规则。所有保存数值的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较小时不抬高。