refactor(tooltips): extract native paired tooltip host - #206
Conversation
Move the business-agnostic "custom content inside the native auxiliary tooltip, pinned beside the native card tooltip" plumbing out of the Combat Impact view into GameInterop/Tooltips. Behavior, visuals, layout and log semantics are unchanged; the view keeps all Combat Impact content, perspective, trimming policy and reason codes. Moved to the shared host: native state capture/restore, CanvasGroupGate, native background cloning, the paired fade, side selection, coordinate conversion, native width/height and layout rebuilds. Shaped by an independent red-team review of the plan (see the design draft for file:line evidence): - Release is two-phase, not once-only. restoreNativeContent is a real two-state contract; an idempotent cleanup would leave the game's own auxiliary tooltip permanently blank, with no log. - One generation counter, bumped before the owner releases its resources. Splitting it would let a cancelled preview continuation re-attach and strand IsReadyToReveal until the 120-frame timeout. - Position takes an IPairedContentBudget so the fit loop stays inside the host instead of re-entering the feature per trimmed row. - Arbitration stays with the feature: the host only answers "is this controller mine". The native auxiliary controller is a game-wide singleton other code drives directly, so one active session serializes this host's own usage only. - ForceSettle exists because the fade coroutine is hosted on the native controller and dies silently when that MonoBehaviour is deactivated. - Perspective rollback stays in the view; the host only supplies a masked layout scope and a rebuild primitive. The rollback pass must reach the same reason-code logic, whose latches reset on recovery and therefore decide how many records are emitted. PostCombatImpactController, PostCombatImpactModule, PostCombatImpactRecapPatch and IPostCombatImpactTooltipView are unchanged. Known debts, recorded in the design draft: the unconditional SetLockedFlag(false) is preserved as-is; the view stays under Patches/ despite not being a Harmony patch; and a paper adaptation of a second consumer showed the host is generic over features, not over anchor kinds, so its generality remains unverified. Refs #204
There was a problem hiding this comment.
Pull request overview
This PR extracts the native “paired tooltip” hosting/placement plumbing from the Combat Impact tooltip view into a shared GameInterop/Tooltips host, aiming to keep user-visible behavior and Combat Impact’s reason-code logging semantics unchanged while making the geometry and native host lifecycle testable and reusable.
Changes:
- Introduces
NativePairedTooltipHost/NativePairedTooltipSessionplus shared contracts (PlacementResult,PairSide,NativePairedTooltipOptions,IPairedContentBudget) underGameInterop/Tooltips. - Moves the pure placement/geometry formulas into
NativePairedTooltipPlacementMathand adds a dedicated unit test project to lock placement behavior (including the shared0.5fepsilon semantics). - Updates
NativePostCombatImpactTooltipViewto consume the shared session for native capture/restore, fade, placement, and trimming orchestration; updates composition to provide a plugin-lifetime host; adds architecture guardrails and documentation.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
src/BazaarPlusPlus/GameInterop/Tooltips/NativePairedTooltipContracts.cs |
Adds shared enums/options/results and the IPairedContentBudget seam for feature-driven trimming. |
src/BazaarPlusPlus/GameInterop/Tooltips/NativePairedTooltipPlacementMath.cs |
Extracts pure placement math into a unit-testable helper. |
src/BazaarPlusPlus/GameInterop/Tooltips/NativePairedTooltipHost.cs |
Implements the plugin-lifetime host and per-owner session managing native capture/restore, fades, background cloning, and placement workflow. |
src/BazaarPlusPlus/Patches/PostCombatImpact/NativePostCombatImpactTooltipView.cs |
Refactors Combat Impact view to delegate host/session responsibilities while keeping feature policy + logging in the view. |
src/BazaarPlusPlus/BppComposition.cs |
Creates and reuses a plugin-lifetime NativePairedTooltipHost for the view. |
src/BazaarPlusPlus/Properties/AssemblyAttributes.cs |
Grants internals access to the new test assembly. |
tests/NativePairedTooltipHost.Tests/NativePairedTooltipHost.Tests.csproj |
Adds the new xUnit test project (including UnityEngine.CoreModule reference for Rect/Vector2). |
tests/NativePairedTooltipHost.Tests/NativePairedTooltipPlacementMathTests.cs |
Adds unit tests locking side-selection, width clamping, vertical adjustment, overflow/collision, and rect helper behavior. |
tests/Architecture.Tests/NativePairedTooltipArchitectureTests.cs |
Adds ratchet/guardrail tests to keep paired-tooltip plumbing centralized and feature-agnostic. |
docs/drafts/2026-08-04-native-paired-tooltip-host-extraction-design.md |
Adds the design writeup with constraints, evidence, and boundary decisions. |
docs/ARCHITECTURE.md |
Documents the separation between NativeCardTooltipContentRefresher and the new paired-tooltip host. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Step 7 实机 smoke 结果环境:macOS,Steam 启动,History → Replay 同一场战斗。分支构建
日志等价性:分支与 master 各跑一遍相同操作,均为恰好 2 条 ( 仍未覆盖
附带澄清过程中曾怀疑小窗口下面板溢出可见区域(见 #207)。加主路径临时探针实测后确认不是 bug: |
Refs #204
Context
NativePostCombatImpactTooltipView同时承担 Combat Impact 内容策略与原生双 Tooltip 的宿主、布局、淡入淡出和状态恢复,导致可复用的 Unity/游戏运行时基建被锁在单一 feature 内。此 PR 只把与业务无关的“主 Card Tooltip + 相邻 Auxiliary Tooltip”宿主能力抽到GameInterop/Tooltips/;Combat Impact 的内容、视角、裁剪顺序、文案和 reason-code 日志语义保持在 feature 内。硬约束是行为、视觉、布局和日志语义不变。
PostCombatImpactController、PostCombatImpactModule、PostCombatImpactRecapPatch与IPostCombatImpactTooltipView均无改动。改动重点
NativePairedTooltipHost与每个 owner 的NativePairedTooltipSession,接管 native controller 状态、Canvas gate、背景克隆、fade、布局和恢复。NativePairedTooltipPlacementMath,锁定左右选边、宽度 clamp、竖直修正、碰撞与 overflow 公式。+N more裁剪顺序、文案/颜色/图标和全部 reason-code 日志。Release(bool)、host/view 共用 generation、native displacement 仲裁与 fade 强制收尾。协作过程
sequenceDiagram participant U as 用户 participant CC as Claude Code participant R as 独立 red-team reviewer U->>CC: 只抽通用基建;行为、视觉、布局、日志不变 CC->>R: 只读评审宿主边界与生命周期 R-->>CC: 指出两阶段清理、单一 generation、fit 回调、仲裁、fade 与回滚六项阻断点 CC-->>U: 提交修订设计与证据 U->>CC: 取消全量 pre-refactor geometry 采集,改做定向 smoke CC->>CC: 实现 Host、接入 View、补测试和架构护栏 CC->>CC: 自审发现 ForceSettle 多调用一次 native hide,修正为 conceal + Release(false) CC->>CC: 运行全量构建/测试并在游戏内验证左右布局、Shift、清理与日志 U-->>CC: 补充简单实机验收,未见问题关键转折:初版把 cleanup 设想为 once-only,并计划让通用 host 编排 perspective rollback;red-team 对照现有调用点后证明这两点都会改变行为。最终方案保留两阶段 native 内容归还,并只让 host 提供 masked layout 与 placement 原语,回滚决策仍由 Combat Impact 执行。
方案讨论
ADR-001:只抽宿主机制,不抽内容策略
ADR-002:生命周期采用两阶段 Release 与单一 generation
Release(false)先释放自绘呈现但保留 native 内容快照,后续Release(true)才归还 header/body/divider;异步 preview 与 fade 还必须共享同一个失效世代。ADR-003:Host 驱动测量,Feature 决定裁剪
IPairedContentBudget分离测量与策略。RestoreAll/TryShrinkOneStep。ADR-004:不把 Host 宣称为任意 Tooltip 通用层
CurrentReplayRecordingButtonController时发现它没有 primary tooltip、使用 native 文本通道且每帧跟随按钮重定位。最终方案
flowchart LR C["PostCombatImpactController(未改)"] --> V["NativePostCombatImpactTooltipView"] V -->|"内容 / perspective / trim / logs"| F["Combat Impact policy"] V -->|"Acquire + session calls"| H["NativePairedTooltipHost"] H --> S["NativePairedTooltipSession"] S --> N["Card + Auxiliary native controllers"] S --> M["NativePairedTooltipPlacementMath"] S -->|"PlacementResult"| VBppComposition创建一个 plugin-lifetime host,并把它注入 Combat Impact View。IPairedContentBudget提供自身裁剪策略,并把PlacementResult转换为既有 reason code。CanvasGroupGate、NativeAuxiliaryHostState等宿主实现约束在GameInterop/Tooltips/。验证情况
./run.sh format-check:通过。-t:BuildAll:全部通过,0 warning。./run.sh test:22 个测试工程全部通过。NativePairedTooltipHost.Tests:25 个 placement 测试通过。Architecture.Tests:148 个测试通过,其中包含 4 个新增护栏。5e305d30…。已知局限 / 后续工作
+N more,因此裁剪顺序完成了代码对照,但没有在这轮 CC smoke 中实际触发。SetLockedFlag(false)无条件解锁与 View 位于Patches/均为既有债务,本 PR 不顺带修复。