--- name: combat-semantic-change description: 修改 CombatSolver 的卡牌、Power、遗物、药水、球、怪物行动、死亡召唤、选择、RNG、Fork 或跨回合战斗状态时,选择正确语义层并验证根快照、分支状态和实际结算。 --- # CombatSolver 战斗语义修改 ## 适用边界 本 skill 处理会改变合法动作或战斗结算的语义。纯 UI、职责移动、Beam/评分调优和发布工作分别使用对应 skill。 开始前读取 `docs/ARCHITECTURE.md` 的 Runtime、Search、模拟引擎与 Prediction 章节。若 actual/simulated、增量回放和续用均一致,问题才可能属于搜索质量,转用 `search-performance-optimization`。 ## 1. 沿当前调用链定位 玩家问题包先按 `issue-bundle-triage` 读取异常栈、状态差异和动作记录,再检查对应源码。已有证据能证明错误链时直接定位,原包重跑用于解决尚未确定的问题;修复的正确性另由下面的最小行为验证证明。 按实际路径追踪,不从最终战损倒推: ```text CombatRootSnapshot.Capture(主线程根) -> CombatPredictionSimulator / Engine Mirrors -> Prediction support / SimulatedCombatState partial -> CombatBeamSolver.Expansion(动作入口) -> CombatBeamSolver.Phases(跨回合推进) -> StateEvaluation / Terminal -> ContinuationStamp / actual-simulated 严格差分 ``` 检查同一效果是否同时存在于: - `CardOnPlayMirrors` 与各 Hook registry; - `CardEffectSpecRegistry` / `CalculatedVarSpecRegistry`; - `CardOnPlaySupport*` / `CardPowerOnPlaySupport*`; - `CorePowerSupport` 与具体生命周期 support; - `MonsterMoveEffects` / `MonsterMoveSemantics` / `BranchMonsterAi`; - `SimulatedCombatState` 的对应 partial。 确定唯一权威结算点后再改代码。不能靠执行顺序抵消双结算。 攻击意图直接从分支当前怪物 AI 行动派生;死亡变形、眩晕和强制行动可在同一玩家回合内替换行动,回合开始的派生缓存不足以支持伤害后条件。准备阶段重搜与普通跨回合使用同一延迟资源消费语义,基础最大能量在根冻结,监听表不读取实机战斗是否结束。回合末自动出牌先于 BeforeSideTurnEndEarly 的手牌消耗,额外回合资格在该阶段结算后判断。 动态目标类型的分支覆盖必须同时定义能力存在和不存在两侧。君王之剑/小刀在分支无群攻能力时不能回退到实机 owner 的原生 TargetType;最小合同交错改变实机能力与独立分支,验证后台目标枚举不读 live。 ## 2. 选择实现层 - 通用命令时序、资源、集合、历史、RNG:`src/Engine/InCombat/Simulation`。 - 某个原版 Hook / Model 方法的精确实现:`src/Engine/InCombat/Mirrors`。 - 跨 Hook 生命周期、隐藏状态、怪物 AI、死亡/召唤、异步事务和第三方 subscriber:`src/Prediction` 或对应 `SimulatedCombatState.*.cs`。 - 候选展开入口:`CombatBeamSolver.Expansion.cs`,这里只调用语义,不实现具体结算。 - Beam 保路、最终排序与预算不是语义修复位置。 - live 部署和 UI 不反向修正预测结果。 - 补货在 `AfterDeath` 内、旧个体移出阵容前生成替补,使随机生命判重与原版使用相同候选集合。死亡清理继续处理生命周期,生成动作由该 Hook 镜像独占。 - 温柔等逐次出牌完成效果在对应 `AfterCardPlayed` 镜像内结算;外层效果的历史范围包含内层自动牌,按范围扫描逐牌效果会重复处理内层事件。保留原有分支计数、Fork 与回合末恢复所有权。 - 历史敏感倍率使用冻结的根历史与当前分支新增事件。验证应让实机在根捕获后推进同类事件,再检查父分支、Fork 与跨回合结果,覆盖回合准备根早于实机抽牌的时间窗口。 - 出牌限制先对照原版事件口径:CardPlayStarted 包含仍在执行的外层卡牌及重放,不能换成已完成次数或手动动作数。凡庸的权威入口是 ShouldPlay mirror,手动与自动打牌共用分支手牌/开始计数;只在候选枚举入口拦截会漏掉倾泻等嵌套自动牌。 - 终局回合由模拟器在原版安全检查点首次锁定,Snapshot 按值保留并供标注/排序共用;不从最后动作回合推断、不统一加一,也不在已经开始的 Hook 监听器序列中逐个插入胜利中断。 - 单场永久成本上限按逐来源累计值判断,手动出牌历史在主线程冻结,预测增量由该来源结算点拥有,Fork/重算/跨回合保持;最大生命增长不返还已花额度。策略约束在候选产出前检查,覆盖自动出牌和重放,模拟器继续按原版效果结算。新政策同步设置、缓存键、导出与恢复。 - 命令本身的终局门仍应在对应调用点核对。例如遗物 AfterCardPlayed 计数会在末击后递增,但 PowerCmd.Apply 在 IsEnding 拒绝加属性;不能省掉命令门,也不能把属性延后到整个监听器序列结束再统一补偿。 - 生产选牌部署通过 `NativeChoiceRuntime` 驱动原版页面;`ICardSelector` 只用于无 UI 测试和原版明确自动选择。部署计划/页面失配时释放求解器输入锁、暂停自动执行并保留原生选择给玩家;只有退出场景才取消原生选择,不能取消未完成原生动作后直接排队重算。恢复测试必须等待具体 GameAction.CompletionTask,队列临时空闲不足以证明重放/选牌动作结束。 - 计划卡牌按 ID、升级和影响后续结算的逐实例语义状态匹配;附魔、重放、费用、关键词、动态变量或临时标志不同时,不能仅凭同名卡牌的序号回放。 - 首回合准备没有既有路线,原生页面可见后再搜索。全自动后续回合消费上一轮 `EndTurn.TurnStartChoices` 并以 continuation 核对结果,不能为了展示页面重复搜索;单步执行在上一回合路线结束后交还控制,下一回合原生页面默认等待玩家,玩家在该页面请求执行或全自动时接管既有选择并继续复用,仍不得从选择中间态重搜。 - 原生准备会话与搜索 worker 分别持有取消源;停止只取消并排空 worker,保留原生选择任务。等待页面后以原子状态转换确定唯一搜索所有者,重算沿同一流程重新创建 worker。结果发布结束采用/应用标志。 - 搜索期间保留原生页面输入;原生 Task 完成或页面序号推进后淘汰旧根,按真实阶段继续。实际部署独占输入锁,先清除未提交的手动勾选再驱动计划。后续回合没有既有选择路线时,在选择发生前捕获稳定准备根。 新增或修改 mirror 注册时,由 `MethodMirrorRegistryDescriptor` 自动向 CoverageCatalog 描述支持状态;不要在工具侧复制 registry 私有布局或另建平行登记。 `AfterSideTurnEndLate` 的扩展使用 `AfterSideTurnEndLateMirrors.Register`,在根捕获前完成登记;玩家和敌方共用 Hook facade,DisintegrationPower 不得恢复到独立晚期补偿。新增其他阶段时逐一核对原版顺序、选择暂停和状态所有权,不能把晚期入口当作所有回合事件的通用回调。 额外回合的 `ShouldTakeExtraTurn` / `AfterTakingExtraTurn` 由 `ExtraTurnMirrors` 登记原版及第三方单项效果。判断按原生顺序短路,后置回调先固定成员再依次结算;选择暂停沿原动作重放。状态读写使用分支 Power 和遗物记录;原生顺序、完整状态和 Fork 隔离用 `EXTRA-TURN-MIRROR-ORDER` 验证。 ## 3. 状态所有权清单 卡牌费用等价须核对 `CardCostStateSupport` 的能量/星能基础值、有序修改层和失效条件,不能只比较当前费用。该writer同时服务出牌指纹、选牌与续用;没有修改层时保留原键。费用规则变化须覆盖原版出牌/回合末清理、Fork隔离与续用诊断字段边界。 - 历史六项累计值由 `CombatPredictionHistory.Record` 维护,普通、手动选牌与执行续接 Fork 均继承已有总数,复制尾段不重复入账。更改历史事件或续接路径时使用 `VerifyHistoryCounters=true` 核对独立扫描;Started/Finished 与原始/Resolved 事件的计数时点不能混用。 新增分支状态必须回答: 1. 根值从哪里、在哪个主线程时点捕获; 2. 所有者是基础 shadow、`SimulatedCombatState`、克隆 Model 还是 `PredictionStateStore`; 3. Fork 是深拷贝、COW 或不可变共享; 4. 内部引用如何通过同一个 `PredictionForkContext` 重映射; 5. 是否改变未来合法动作或结算,因而进入状态键; 6. 是否跨回合存活,因而进入 `ContinuationStamp`; 7. actual/simulated 严格状态如何捕获; 8. 创建、叠加、归零、移除、清空和 Fork 稳定边界。 第三方遗物/Modifier 的根内隐藏状态优先使用 `ModelPredictionStateMirrors`,同时登记 capture、 writeLive 和 writePredicted,复用 store 的 Fork context。状态描述按有序实例绑定并进入续用核对; 首次根或续用捕获后不可登记,不允许未捕获时读取 live 或默认初始化。状态登记不代表 Hook 或 补丁语义已适配,仍需沿实际结算链验证。签名和范围见 `docs/third-party/model-state.md`。 活动 roster 和已知怪物状态是不同生命周期。怪物死亡或离开可行动阵容后,其正在执行行动仍可能读取根 AI/静态参数;不要随 roster 移除提前删除这些数据。 纯派生搜索启发式不属于战斗状态,不进入状态键或续用文本。 ## 4. 隔离与失败语义 - worker 只消费 `CombatRootSnapshot`,不补做 live 捕获。 - 真实 Model 只作稳定身份、类型或根阶段只读元数据。 - 写卡牌前取得 `PredictedCard.MutablePreview`;不得写 `Original`。 - 分支可变对象显式克隆或 `RequireRemap`。 - 不在 worker 推进真实动作队列、牌堆、Power、Creature 或 run RNG。 - 不新增宽泛 catch、静默默认值或“跳过该候选”。未支持行为让搜索明确失败或形成已定义边界。 - gameplay mod subscriber 必须在根阶段识别所有权;未知来源显式拒绝,不做通用浅拷贝。 - 根可达卡牌的第三方 OnPlay Harmony 补丁由 `PredictionModPatchAudit` 检查;跨根读取当前补丁表,避免缓存已卸载或后来安装的补丁。新增适配时明确其来源与语义,不能用未知来源放行代替适配;此入口不代表所有第三方方法已覆盖。 - 已适配 OnPlay 必须登记完整组合,由根冻结唯一标准 registry 镜像;命中后直接返回,不能再运行 vanilla/spec。配置变更只在主线程 live stamp 检查,worker 消费根标记;适配状态机另有 MoveNext 补丁、Inner 补丁及未审计新类型明确失败。条件支持通过标准 descriptor 加组合签名描述,不增加无条件原版覆盖。见 `docs/third-party/onplay-patches.md`。 ## 5. 验证选择 定位依据和修复验证分别取证。先在未改行为源码上建立最小失败基线,再停在能覆盖根因的最低层;包内日志已说明的异常可以直接转成最小夹具,无需先启动原包恢复来重复确认: 1. 默认只跑目标效果的 actual/simulated 严格差分,比较有序牌堆、逐实例状态、Power、怪物 AI、球与相关 RNG; 2. 新增 Fork 状态时,在同一最小夹具断言 Fork、指纹和重映射;新增跨回合历史或续用字段时,用两回合生命周期或最早 continuation 边界核对 live/predicted; 3. `-VerifyIncrementalSearch` / `--verify-incremental-search` 只加在实际启动正式搜索并回放候选的 fixture 上,不能给纯一步差分增加无效成本; 4. 覆盖根因直接相邻的生命周期,例如回合开始/结束、叠加/移除、死亡/复活或嵌套选择;不要自动扩成整场战斗和全部同类模型; 5. 普通快速迭代的单个 unattended 请求总超时不超过 `120` 秒。搜索使用短预算并在首个目标动作或最早复用回合停止;超时后缩小 fixture 或明确写未验证,不在同一轮延长到 `180/360` 秒; 6. 完整自动 headless 只在改动搜索/部署编排、较小边界无法覆盖、用户明确要求完整回归/门禁,或要声称整场零重算时运行;固定 `Instant / 0 秒`; 7. 改 mirror 支持面、状态字段或 coverage 分类时运行对应 CoverageCatalog verify;只有改变目录覆盖面或明确完整门禁时跑全量; 8. UI、动画或真实卡顿另做可见 Steam 验收。 多敌已知路线的原版对照须在全部正式预测冻结后才推进live,使用固定原始Creature身份逐敌比较;末击在真实清理前取证并等待对应CombatEnded,不能以总敌HP代替死亡/阵容/完整状态。测试选择器未提供的原生来源或上下文参数应明确限定证明范围,不声称直接比对;累计伤害、原生洗牌事件与Search统计也应分别记账。 性能数字不能来自 `-VerifyIncrementalSearch` / `--verify-incremental-search`。通用 helper 改动应覆盖其调用类型族,不只跑最初报告的一个模型。 ## 6. 记录与提交 提交前同步受影响的 `docs/DEVELOPMENT_NOTES.md`、`docs/TEST_MATRIX.md` 和必要的结构化证据。职责边界有变化时同时更新 `docs/ARCHITECTURE.md` 与结构门禁。 普通语义修复直接提交。是否随该项提升版本和打包,以 `AGENTS.md` 的活动发布批次和发布口令为准;不要由本 skill 另立发包规则。汇报应说明首个错误状态、权威实现层、状态所有权、实际运行的 fixture 和未执行项。 - 预测卡牌/Power 克隆的免锁路径由 `NativeModelCloneConcurrency` 核对:仅隔离域、普通原版卡牌或默认内部初始化 Power、已物化原版变量、原生克隆阶段与精确 BaseLib/Ritsu 稀疏复制补丁组合。Power 还须核对默认 InitInternalData、AbstractModel.DeepCloneFields 与 Power.DynamicVars 物化保护补丁;自定义初始化、附魔/灾厄、第三方模型或变量、未知补丁均走原锁;不得为判定路径而物化共享源变量。证据限线程当前最外层隔离域,跨域刷新,不缓存模型或分支值;原版 `MutableClone` 的 BaseLib 锁保持。合同须真实加载 BaseLib,并持锁验证并行、变量独占与跨域补丁失效。 - `PredictionStateStore` 的三槽计数表只保存 Type/条目数,不保存模型或 state;空 store 不创建计数对象,溢出仍使用独占字典,Fork 丢弃零计数。工厂可以重入并扩容,禁止跨工厂调用持有主字典 ref;计数更新的 ref 必须立即消费。验证覆盖溢出、清空后 Fork、父子隔离与工厂重入,不能只测常见一类状态。 - 根生成池仅缓存逐项核对的原生过滤:无色、角色攻击、非Basic/Ancient、Power及Common;保留角色/规范池/AllCards引用身份、约束、原生只读模型与自定义池回退门禁。后三类由TurnStartPowerSupport每次Power触发准备一次;回退路径GetUnlockedCards仍只调用一次,原谓词与战斗过滤仍逐次抽取执行,不能把取N次一张改成一次取N张。不得混用有放回NextItem与distinct TakeRandom,即使只取一张。候选模型只读共享,RNG与生成卡始终属当前分支;其他过滤未经核对不能获得缓存资格,合同须覆盖可变池回退调用次数与枚举语义。 - `RoundTransition` 只在无计划选择的EndTurn初探保存无挂起事务的前缀:普通抽牌与历史补偿后为原稳定点;抽牌准备及一次性抽牌修正消费完毕、Simulator.Draw之前为洗牌选择的更早稳定点。后者只在将发生洗牌、当前worker已观察到该处产生有效选择层、且对应SourceId的玩家Power当前仍有效时预留;提示只存字符串,不持有模型。未命中保留较晚稳定点,未知非Power来源完整回放。抽牌前checkpoint保存已消费的drawCount,续接重建BeforeNextTake回调且不重复消费修正或提前触发SideTurnStart。ToolsOfTheTradePower继续立即预留抽牌后前缀。学习提示仅属worker的运行上下文,不跨搜索、不进入战斗键/候选政策;未到稳定点的选牌不得启用。原Fork事务断言保持,复制前临时关闭空cursor并在finally恢复。前缀匹配父节点引用、EndTurn回合与PlayerTurnStart选择,Knowledge选择完整回放;frontier拥有checkpoint,同父gate串行Fork,排空后释放。新增捕获计数包含额外物理Fork,DOP等价比较扣除该项后的转移Fork;完整状态/续用/历史、连续洗牌与变牌选择、延迟抽牌修正及兄弟隔离须直接对账。 - RNG 惰性物化只共享完整计数器/四段状态值的不可变快照;已有可变实例的流必须在 Fork 当时捕获,不能把原生 Rng 当成 COW 共享,因为调用方可能继续持有旧引用。只读状态键/续用/投影读取不物化源流,真正随机操作仍使用分支独占的原生实例。合同覆盖九条流的原生序列、保留引用、兄弟/多代 Fork、只读未物化与 live 不变;实际整搜分配和时间分别判断,不把未访问流比例当作整搜收益。 - 卡牌首次进场检查由 `PredictedCard.HasCheckedPowerAfflictionEntry` 按 wrapper 保存,根牌也标记已经检查;Fork继承,Clone重新检查,根身份集合仅捕获一次、只读共享。不得改成按卡名判断或把新wrapper当作旧卡已经处理。污染清除及数量变化仍在每次归一化检查。 - 跑局监听表仅在前缀与 `_rootRunHookListeners` 引用相同时省去重映射;该冻结前缀只含根牌组CardModel/Enchantment,State.Fork不会登记这些模型,StateStore.Fork仍在其后。其他前缀、战斗后缀与Power继续原重映射/失效逻辑。更改Fork顺序或模型登记范围时必须重新核对这条前提。 - 正式手动自身选牌续执行支持当前清单中的41张原版单人卡,要求无附魔/污染、单次手动打出、空显式cursor、单层card scope、可重映射活动历史及无不透明/事务StateStore。Engine保存显式CardPlay/frame并复用唯一结算尾部;Prediction独占seed/frame/deaths和Fork锁,Search仅在同父同动作选择链或frontier内持有。普通Fork仍拒绝挂起种子;私有Fork临时移走所属pending request并运行原事务断言,全部模型/trace/play/history使用同一PredictionForkContext。再次挂起时退出全部子scope再完整回放,额外物理Fork单列fallback,不多扣逻辑transition/选择预算。旧路径不得运行捕获诊断或持有检查点;释放必须在生产者/消费者排空后完成。不保留Task/闭包,不跨父、搜索或战斗缓存。完整状态/历史/RNG、兄弟修改、原生结算、DOP、取消/异常排空与增量等价直接验证;各来源的命中和整搜收益分别报告。已生成的请求/spec必须一并捕获,不能在恢复时再次GetSpec(探寻打击会再次消耗RNG);同一个Fork context复制请求候选、生成历史中的非牌堆wrapper、CardPlay及其格挡金额/事件计数。 - 9种原版手动选牌药水共用 `PotionExecutionSupport.Prepare/Complete`;检查点在消费槽位和Use完成后、选择应用及AfterPotionUsed之前,种子仍须通过普通Fork断言。四种生成药水从检查点运行原空选择探测,使用后钩子执行完才读取候选;其他五种仍从父状态准备候选。Search的串行/并行准备共用入口,同父完整动作匹配且仅Choice可替换;frontier或串行枚举拥有检查点并在排空后释放。生成候选历史只读共享,Apply继续Clone选中牌;分支可变牌/RNG由普通Fork隔离。嵌套再次挂起从原父完整回放;额外前缀Fork与fallback分别记账,不改变transition/choice预算;worker合并和归零须包含四个药水计数。第三方药水或登记覆盖原版选择的药水不进入此特化。不保存Task或闭包。验证全部九种原生结算、完整状态/历史/RNG、消耗/后置钩子、兄弟修改及DOP/取消/异常/增量对账。 - 嵌套执行检查点保存纯数据帧与明确程序阶段/下一循环序号。所有CLR作用域退出后,核对领域事务、StateStore、活动CardPlay及延迟抽牌/生成历史的精确配对;普通Fork继续拒绝捕获/挂起/已准备种子。一次PredictionForkContext重映射状态、帧、候选、历史、CardPlay、Power来源及共享死亡集合,保留trace来源身份和抽牌深度限制;外层列表所持但已离开所有牌堆的wrapper也必须显式Fork,不能假设State已登记。未知派发必须拒绝整次捕获,继续原完整回放,不能默认缺失尾部已执行。已确认的抽牌、弃牌、Hook、重复子出牌与回合来源循环复用唯一普通执行体,恢复可以再次挂起。Search匹配同父完整动作及已消费选择前缀,只追加下一选择;选择层/frontier排空后释放全部图引用。不保存Task/闭包,不跨搜索缓存;严格增量基线禁用捕获。ExecutionChoiceCaptures/Reuses不扣选择预算,reuse替代一次原转移Fork,不能作为额外物理Fork从比较器扣除。源循环、深层选牌、DOP/取消/异常、有限预算耗尽与原生完整状态分别验证。 - 金币 Modify/AfterModify 使用带 combat child 的跑局监听表,AfterGoldGained 使用 null-child 跑局序列。只在主线程冻结根成员,资源与 HP 消费分支状态;未知 override 不能依 manifest 的 Ignored 分类放行。最大生命增加后只回复实际封顶差值,并经过通用 Heal 的 HP 回调;原生严格差分覆盖小数、金币修正顺序、owner、熔化、封顶和成长/长期资源记录。 - Hook 参与位图的金币方法族共用一位时,只允许保守保留更多监听成员;精确 dispatch 仍按方法查表。不得用超过 ulong 位宽的移位产生别名,也不得借共用位跳过有回调的成员。