# 4C1普通钓鱼消费确认方案(待独立评审) 日期:2026-09-24。作者:主会话。前置:R1已关闭且关闭复验通过。本文申请限定设计评审,**尚未批准编码/真实消费**。 ## 1. 目标和范围选择 在现有Editor KV专用模式、102专用存档、单账号和重启式重登限制下,完成一次普通收费抛竿的“扣体力→原动作保存确认→允许拉杆→正常退出→新进程恢复”。首轮到拉杆按钮可用即停止操作,不点击拉杆、不结算鱼获/领取任务奖励。它验证消费前置保存,不宣称完整捕鱼事务、4C整体或G4完成。 路线比较:全B/C热换号先行会扩大改动;全局接通SyncDataImmidiate会同时开放未处理的转盘且保留旧体力副本冲突;选择仅普通钓鱼增加可等待确认支路。保留现有Owner、Bridge、Write/Flush/Ticket、整档序列化、扣费及任务算法,不增加通用交易框架或另一套缓存。 本期支持条件:`IsGeneralFish()==true`、`!IsFree`、`!IsRetry`、`!IsDuel`、`!IsPackItemFish()`,对象/配置/会话有效,费用来自原TbEnergyDef.DataList[magnification].Mag且大于0,当前体力足够。免费、重试、礼包鱼、特殊鱼、转盘、广告和发奖均不获启用;不自动选鱼、改配置、补充体力或修改102档来满足条件。条件不符则在扣费前终止该测试run。 ## 2. 源码依据和写入矩阵 引用行号以同目录受审清单为准;实施前须重核hash。 | 触发链 | 现有行为、可能写入 | 本期处理 | |---|---|---| | FishingUpdateSys.Start→CastLine→FishingBehaviorB.CastLineBehavior→CastBait→BaitMove→WaitForFishBite→ConsumeBait | 进入捕鱼对象后Start会自动抛竿;动画/协程推进,收费调用async void后立即WaitForFishBiteED | 实际UI进入捕鱼即可能开始动作,不通过CLI直接调用消费伪装UI验收;只在KV模式捕获一次动作,收费分支等待确认后继续一次;普通PF分支保持 | | ConsumeBaitOnPlayfab | 扣前复制PlayerDataDic,仅改energy别名,旧newData仍带扣前体力 | KV支路完全不使用该副本/旧立即接口;复用现有整档序列化 | | PlayerData.ReduceEnergy→SetEnergy→SavePlayerData | 修改energy、energyRM,发布PlayerData,写NewPlayerDataDic(原调用remote=false) | 原扣费算法执行一次;最终另取现有序列化产生的明确remote票据,不能把localOnly当确认 | | 从满体力降到阈值下→RecoveryEnergy(0) | 设置_lastEnergyRecoveryTime,再SavePlayerData,启动CoRecoveryEnergy | 不重写恢复算法;同步写收进本动作;后续定时写不是本动作票据 | | CoRecoveryEnergy | 每秒等待、到期回血,可能多次NewPlayerDataDic写 | 保持运行;若取代待确认的本动作最终票据则停止run,不暂停计时伪造稳定结果 | | ConsumeEnergy→AchievementDataManager.UpdateData | 更新成就内存,独立保存调用已注释 | 必须在事件发布之后作最后一次整档序列化,包含新的ad | | ConsumeEnergy→BattlePassData.UpdateTaskData | 活动有效且任务变化时SaveBattleTask→BattlePassRecordTask | 同步原写入与原票据纳入确认;不领奖 | | ConsumeEnergy→MiniBattlePassModel.UpdateTaskData | 更新Exp→SaveBattlePassRecord→provider→MiniBPKeyNew | 同上;ConditionTypeEvent是struct,各订阅收到副本,不改倍率算法 | | ConsumeEnergy→LuckyTaskData.UpdateTaskData | 有任务变化时SaveTaskData;两个实现分别写EventLuckGameRecord、EventLuckMagicPlayfabData | 同步原写入纳入;红点/任务计算保持 | | ConsumeEnergy→TriggerPackData.UpdateTriggerPackData | 配置匹配时ConditionTypeAdd/AddTriggerPack,可写FixedTimeShopDic、TriggerPackDataList并排入礼包UI | 原同步写受许可与捕获约束;不购买/领取礼包 | | ConsumeEnergy→FishingEventData.OnEventDataChange | 配置匹配时SetEvent;可初始化活动、写FixedTimeEventDic/模块数据,甚至启动其他加载 | 本期真实run要求当前配置没有以ConsumeEnergy触发该SetEvent链的记录,否则扣费前拒绝;不试图盘点/开放所有活动激活 | | PlayerData通知→PlayDataEventSubscriber→ActivityManager | 重新检查活动,可Open/Close;已查等级/钓鱼次数条件,本动作不变更二者,但时间条件可能变化 | 真运行前记录活动状态与相关条件;同步副作用受捕获约束,观察到活动切换/异步初始化则该run不计通过,先关闭再另立差异审查 | | HomeBtnRecharge的ConditionTypeEvent订阅 | 仅RechargeAmount过滤 | 不接收ConsumeEnergy | 以上是直接订阅和明确转发链,不冒称枚举了反射或动态注册的全部游戏事件。真实运行的日志、写入key和状态矩阵是验收条件;发现新增调用链必须停止本期扩展,不能临时扩大白名单。 ## 3. 运行许可和动作生命周期 1. 保留Version=1许可的equipment语义;增加Version=2且Operation必须为固定枚举`equipment`或`fishing-consume-once`,未知版本/操作拒绝。Version=1中出现试图授权消费的字段必须拒绝,不能靠未识别字段启用。Version=2设备/账号/API/备份摘要/AllowedKeys检查全部沿用,所有允许键仍必须出现在现有备份中。 2. Editor新增固定PrepareFishingPermit入口,沿用PreparePermit的校验过程,操作固定为fishing-consume-once;不接受URL、任意代码或任意方法名。每个Unity进程仍只领取一次运行许可,不删除claimed、不允许域重载绕过。 3. KV模式进入CastLineBehavior之前检查操作许可;旧equipment许可不能尝试抛竿。消费许可在本进程只能占用一个动作(成功/失败/不支持都不能自动恢复配额)。在同一组件的CastLine首次入口捕获原handle、组件引用、递增的本地动作版本;重复入口直接返回,不重复动画/扣费,记录duplicate-suppressed。 4. 鱼种可能在抛竿流程中确定,因此在真正ConsumeBait之前再次检验本节及§1所有条件,固定鱼/地图/鱼竿/倍率/费用;CastLineBehavior、CastBait的每个await后及Tween完成回调、BaitMove/WaitForFishBite继续处检查原对象存活、active/enabled、动作版本一致、原handle仍可用。不得仅在最终网络await后检查,放任旧动画回调重新推进。普通PF模式不执行新限制。 5. OnDisable/OnDestroy只撤销本对象动作继续资格,不撤销已准入网络写;若消费已开始或等待中,走现有Fault/Close。退出/401已经撤销会话时不再重复退出、不清标记。生命周期回调不能重置进程配额。 6. ConsumeBait只准入一次;WaitForFishBite、断网弹窗重复回调、第二次动画事件不能再次扣费。KV支路不调用旧NoInternet→DoConsumeBait的自动重试;网络不可用时在修改内存前终止run,不能仅凭internetReachability宣称网络提交结果。 ## 4. 消费方法和保存顺序 在PlayerData内增加仅`UNITY_EDITOR && !SERVICE_HOST`可用的窄方法,由FishingBehaviorB的可等待KV支路调用。费用计算在原位置复用,参数必须是本次已验证的正费用和固定动作;不接受外部传来的序列化字符串。 同步段顺序固定,段内不await: 1. 复查原handle、动作和体力;开始现有WriteBatch捕获,禁止与装备批次/另一消费嵌套。 2. 调用**一次**现有ReduceEnergy(cost,false)。保存现有energyRM、阈值和恢复时间逻辑,不直接改私有energy来绕过副作用。 3. 发布**一次**原ConditionTypeEvent(ConsumeEnergy,cost)。本期只接受普通鱼,因此不省略任务事件。 4. 直接调用已有私有SerializeAndSavePlayerData(true),取得NewPlayerDataDic最后一次原票据;不再次Publish PlayerData、不复制19槽序列化、不写energy别名。最终整档包含扣费、累计消耗、恢复时间和刚更新的成就。 5. 关闭同步捕获,固定本次记录集合。异常立即Fault,不自动回滚内存,不再补偿保存、不补发事件。 等待段:对原handle发起一次Flush观察,随后验证§5的原票据。最多30秒观察(取消仅结束等待,不取消底层已准入写);超时按不确定结果停止run,原Owner/Registry继续排水。每个await后检查动作有效性,成功后仅调用一次WaitForFishBiteED,再记录`fishing-consume-ready`,记录费用、前后体力、原动作身份和最终原票据,不记录凭据或整档正文。 `PlayFabMgr.SyncDataImmidiate`保持原KV拒绝;本期不把它改成通用消费适配器。转盘不会因为本期修订得到可用接口。 ## 5. 多次写同一key的明确合同 需要完整评审的新增规则:R1装备仍逐张票据要求Confirmed,不修改原ConfirmEquipAsync含义。消费由于同一同步调用本身会重复序列化状态,采用**本动作每个key的最终原写入**确认,不能要求被本动作最终状态正常替代的中间票据也Confirmed。 最小实现:现有WriteBatch保留Edits列表原行为,另记录每笔key、remote标志、原EditResult、值摘要和顺序;Write在原Accepted之后同时记录。不是延迟写缓冲,不拦截或重排底层Update,不创建新Owner或网络队列。 - 对每个key确定同步段最后一次Accepted记录;NewPlayerDataDic最后一笔必须等于§4返回的primary且有原remote票据。 - 每个key最终记录若为localOnly,本run拒绝成功(主档已由最后序列化转为remote);不偷偷提升其他模块的localOnly语义或补写。不能用早先remote值代替后来localOnly值。 - 每个key的最终原票据必须Confirmed。较早remote票据只允许Confirmed,或能由**同一批次、同一Owner/key、更后remote票据**解释的Superseded;最后票据必须Confirmed。Unknown/Blocked/StorageError不因后面有票据而被掩盖。 - 异步晚到的新写不追加进已封存集合;它若使本动作最终票据Superseded,则停止run,不寻找替代票据。不同key的晚写也不声称属于已确认动作;若来自本次消费引发的异步活动链,该真实run不通过。 - Flush计数/无异常不是业务成功凭据;TransportError、Unknown、Blocked保持原信息。部分key成功、部分失败视为run失败,不宣称原子提交或做整档回滚。 - 对底层本来就已Confirmed的票据,不要求服务器永远保持旧值;最终真实对账若出现其他业务改值,要另行归因并判断是否仍满足本期闭环,不能只用票据掩盖后续覆盖。 ## 6. 失败与体验边界 | 情形 | 本期行为 | |---|---| | 无许可/错误操作/不支持鱼种/体力不足/配置缺失 | 首次消费内存修改前拒绝;无扣费、无ConsumeEnergy、无拉杆继续 | | 原动作重复/生命周期失效 | 不再次消费;等待中的结果不能继续已销毁对象;已发生的保存保留原结果 | | Accepted且Pending | 不开放拉杆,不报成功;没有重试按钮触发二次扣费 | | Confirmed且动作有效 | 仅开放一次拉杆;不自动收鱼、不发奖 | | 明确拒绝/StorageError/Unknown/401/超时/最终票据Superseded | 现有Fault撤销业务准入并正常关闭;保留原票据/错误,不重扣、不退款补写、不重登重放 | | 已确认扣费但本地对象失效,或多键部分提交 | 可能已扣体力但未继续玩法;如实记录,专用账号保留结果供对账。普通用户补偿/恢复方案不在本期启用范围 | 这是专用测试模式的停止策略,不是消费事务的完整恢复承诺。因而本期通过不能开放普通用户、连续多次钓鱼、鱼获奖励或最终包。不得把Unknown改成false来调用旧重连逻辑。 ## 7. 精确实施白名单 以下在独立审查通过、主会话裁决后才生效: | 文件(Client/Assets/下) | 允许变化 | |---|---| | Scripts/GampPlay/FishingB/FishingBehaviorB.cs | CastLineBehavior/ConsumeBait KV分支、窄async Task消费方法、原动作字段、OnDisable/OnDestroy撤销;CastBait/BaitMove/WaitForFishBite仅增加KV动作继续/重复检查。旧PF消费/动画/奖励代码不重写 | | Scripts/DataCenter/DataCenter.PlayerData.cs | 一新增Editor消费方法复用ReduceEnergy、事件、SerializeAndSavePlayerData;不改原扣费、恢复、序列化或奖励公式 | | Scripts/Services/UserService.KVGameplayLoop.cs | 许可v2验证、一次动作占用/确认、WriteBatch附加记录、Write采样接线、固定验证入口。R1装备确认语义保持 | | Editor/KVGameplayLoopSetup.cs | 固定PrepareFishingPermit及`fishing-consume`验证case,沿用限定反射入口;无任意执行扩展 | | Scripts/Services/UserService.KVFishingValidation.cs及meta(新增) | 固定Editor合成验证host;如采用partial,原class只增加partial修饰。固定.invalid后端,不转发真实网络,不含运行许可 | | CodexOutput/upserver/KVFishingConsumeTests/(新增) | 离线宿主/用例/证据与CLI脚本,避免生成.cs混入其他csproj glob;不新增包依赖 | 只读依据但不允许修改:PlayFabMgr、Schema、Context/Owner/Coordinator/Lease/Registry/Guard、FishingData、各任务和活动订阅者、转盘、场景/prefab、SDK及服务器。若通过测试必须动这些文件,停在具体差异方案评审,不借“修复”扩展白名单。 ## 8. 验收矩阵与检查点 | 编号 | 必验内容 | |---|---| | C1-01 | 旧代码先证明“不等待便继续”和“旧newData/新energy冲突”;保留红测。不得只测试新代码的镜像 | | C1-02 | 真实PlayerData与原ReduceEnergy:恰好一次扣费/累计消耗/事件,最终19槽整档Schema有效,其他槽保持 | | C1-03 | 满体力跨阈值会产生恢复时间写;低于阈值、恢复定时在等待期再次写两个分支;不暂停原计时 | | C1-04 | 主档重复写、任务多key、localOnly最终写、中间票据Superseded与最终票据Superseded区分;原票据身份不可替换 | | C1-05 | 注入延迟:未确认时WaitForFishBiteED调用数0;确认后1;重复Cast/Consume/动画/断网回调不会二次扣费 | | C1-06 | 写后丢响应、401、取消/超时、StorageError(含可能本地提交)、部分确认、退出/Disable/Destroy;无重扣、无发奖、无跨对象继续 | | C1-07 | 许可v1装备不能消费,v2合法消费仅一次,未知版本/操作拒绝;免费/重试/特殊鱼/转盘无新增放行 | | C1-08 | 配置及任务真实绑定验证:上述直接订阅的同步保存进入本动作;发现活动异步激活或新增key不得记通过 | | C1-09 | 4B2全量、B3A三条件全量、R1固定装备链/许可/退出,Unity真实M01/B2-20;默认PF与SERVICE_HOST编译行为保持 | | C1-10 | 102一次真实UI抛竿到可拉杆,记录原最终票据Confirmed与服务器逐键对账;不点击拉杆/领取,正常退出;新进程新缓存恢复体力/累计消耗/关联任务 | 检查点0:审查报告/hash/逐项裁决和源码快照,更新日志。检查点1:红测、最小实现、合成失败矩阵。检查点2:Unity固定真实对象与既有回归,确认没有新增活动分支。检查点3:§9真实102准备/运行/恢复与证据;阶段内每个检查点更新入口、TODO、开发日志。检查点4:限定代码自审/受审hash/独立实施复核,主会话裁决后关闭4C1。 测试不得实例化假的通用公开网络工厂:固定验证host使用现有合成Bridge,与真实PlayerData/实际消费入口相连;捕获的继续次数来自实际控制流或编译真实方法的宿主,不靠复制一份理想伪实现。新增host应恢复静态状态且排水完成才能结束,沿用R1固定测试约束。 ## 9. 真实数据和操作边界 已有101/102授权沿用,无需再询问账号。只用102现有原档,不重放import-102.claimed、不初始化101。正式地址固定http://s101.local:18080/,Unity CLI Loop,不用UnitySkills,暂不打包。 实施验证后先只读备份102及101,记录实际服务时间、config摘要、体力、MaxEnergy、倍率/费用、恢复到期时间、目标鱼种及当前活动。历史403体力仅为旧证据,不能替代当时真实值;不写死本次费用。 允许键仍限备份中的原45键;消费实际预计键集合为NewPlayerDataDic、BattlePassRecordTask、MiniBPKeyNew、EventLuckGameRecord、EventLuckMagicPlayfabData、FixedTimeShopDic、TriggerPackDataList及正常初始化原已批准键。实施运行前输出“预计写键→当前存在→关联订阅→允许变化字段”矩阵;主档精确核对Energy/energyRM/恢复时间/ad及其他槽,任务键按原事件费用计算。任何新key、新激活链或无法解释的变化拒绝验收,不能添加空键绕过许可。 收费前只读确认无ConsumeEnergy触发旧FishingEventData.SetEvent的配置;若存在就停止,提交具体活动分支的最小差异方案。活动的等级/次数不因本动作变化;如果观察到时间边界导致Open/Close或异步初始化,本run也不记通过,不冻结服务器时间或重写业务回调过关。 实际UI普通抛竿一次,确认后观察拉杆可用即停止输入,用独立CLI请求发起正常QuitStage,另取日志/状态(不依赖跨Stop的await返回)。未确认时不能靠手动Stop伪造成功。Registry未Ready则保留进程/排水任务。已关闭后归档permit;下一轮启动新的Unity进程、验证新缓存不存在,再只读恢复比较,不执行第二次消费。期间自然体力恢复允许按原时间公式解释;未解释的差异不放行。 默认保留测试结果;失败时不回滚整档、重扣或自动补偿。若需人工恢复,另列最新服务器态和精确恢复范围,不能拿旧45键备份直接覆盖运行后状态。 ## 10. 作者自审及交接 - 目标、一次动作、成功终点与不支持分支已明确;不把“可拉杆”表述成完整钓鱼奖励闭环。 - 避免旧体力双表示;同键替代和不同键部分提交已区分,不作原子性承诺。 - 直接事件链有明确矩阵;任意活动激活明确未覆盖,设真实run拒绝条件,不冒称全游戏调用图完整。 - 白名单不包含底层/转盘/活动算法;R1语义有回归约束。没有为了测试顺利允许删claimed或伪造存档。 - §5消费最终状态确认规则、§6失败可能扣费但不继续、§9活动拒绝边界需评审者特别裁决。若评审认为此范围不足以称4C1,应退回分期名称/验收范围,不在编码时临时扩大。 评审提示词见《其他AI审查提示词-4C1普通钓鱼消费确认-2026-09-24.md》。本方案可交审,**没有代码已实现、测试已通过或真实消费已运行的声明**。