Skip to content
This repository was archived by the owner on Sep 20, 2026. It is now read-only.

refactor(tooltips): extract native paired tooltip host - #206

Merged
pengx17 merged 1 commit into
masterfrom
refactor/native-paired-tooltip-host
Aug 4, 2026
Merged

pengx17 merged 1 commit into
masterfrom
refactor/native-paired-tooltip-host

Conversation

@pengx17

@pengx17 pengx17 commented Aug 4, 2026

Copy link
Copy Markdown
Member

Refs #204

Context

NativePostCombatImpactTooltipView 同时承担 Combat Impact 内容策略与原生双 Tooltip 的宿主、布局、淡入淡出和状态恢复,导致可复用的 Unity/游戏运行时基建被锁在单一 feature 内。此 PR 只把与业务无关的“主 Card Tooltip + 相邻 Auxiliary Tooltip”宿主能力抽到 GameInterop/Tooltips/;Combat Impact 的内容、视角、裁剪顺序、文案和 reason-code 日志语义保持在 feature 内。

硬约束是行为、视觉、布局和日志语义不变。PostCombatImpactControllerPostCombatImpactModulePostCombatImpactRecapPatchIPostCombatImpactTooltipView 均无改动。

改动重点

  • 共享宿主:新增 plugin-lifetime NativePairedTooltipHost 与每个 owner 的 NativePairedTooltipSession,接管 native controller 状态、Canvas gate、背景克隆、fade、布局和恢复。
  • 纯几何决策:新增 NativePairedTooltipPlacementMath,锁定左右选边、宽度 clamp、竖直修正、碰撞与 overflow 公式。
  • 业务边界:Combat Impact 继续拥有内容构建、Caused/Received 视角、+N more 裁剪顺序、文案/颜色/图标和全部 reason-code 日志。
  • 生命周期约束:保留两阶段 Release(bool)、host/view 共用 generation、native displacement 仲裁与 fade 强制收尾。
  • 架构护栏:新增 placement 单测及源码 ratchet,防止通用宿主重新泄漏回 feature。

协作过程

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: 补充简单实机验收,未见问题
Loading

关键转折:初版把 cleanup 设想为 once-only,并计划让通用 host 编排 perspective rollback;red-team 对照现有调用点后证明这两点都会改变行为。最终方案保留两阶段 native 内容归还,并只让 host 提供 masked layout 与 placement 原语,回滚决策仍由 Combat Impact 执行。

方案讨论

ADR-001:只抽宿主机制,不抽内容策略

  • Context:View 中同时存在 native controller 管理、Unity 布局与 Combat Impact 业务渲染。
  • Considered Options:整体抽成通用 Tooltip feature;只抽 native paired host;继续留在 View。
  • Decision:只抽 native 状态捕获/恢复、Canvas gate、背景、fade、测量与定位;内容、视角、裁剪和日志留在 View。
  • Consequences:边界较窄,但不会把 Combat Impact DTO、文案或 reason code 变成共享 API。

ADR-002:生命周期采用两阶段 Release 与单一 generation

  • ContextRelease(false) 先释放自绘呈现但保留 native 内容快照,后续 Release(true) 才归还 header/body/divider;异步 preview 与 fade 还必须共享同一个失效世代。
  • Considered Options:幂等一次性 cleanup;host/view 各自 generation;保留现有两阶段语义与单一 generation。
  • Decision:session 先停 fade 并 bump generation,再回调 View 释放 preview,最后销毁 UI、恢复 native 状态。
  • Consequences:避免 native auxiliary 变空框,也避免迟到 continuation 重新挂回已销毁呈现。

ADR-003:Host 驱动测量,Feature 决定裁剪

  • Context:高度 fit 循环夹在 host 的定位流程中,但“下一步隐藏哪一行/哪一块”属于 Combat Impact。
  • Considered Options:整段留在 View;把裁剪策略搬进 Host;通过 IPairedContentBudget 分离测量与策略。
  • Decision:Host 循环测量并重建,View 仅实现 RestoreAll / TryShrinkOneStep
  • Consequences:共享层不认识 Combat Impact 类型;原有从末块、末行开始的裁剪顺序仍由 feature 保留。

ADR-004:不把 Host 宣称为任意 Tooltip 通用层

  • Context:纸面适配 CurrentReplayRecordingButtonController 时发现它没有 primary tooltip、使用 native 文本通道且每帧跟随按钮重定位。
  • Considered Options:继续扩展 API 覆盖任意 anchor;限定为 paired-shaped consumer。
  • Decision:Host 只服务“主 Card Tooltip + 相邻自绘 Auxiliary panel”形态。
  • Consequences:当前通用性是同形态下的 feature 复用;第二个 paired consumer 出现前不继续泛化。

最终方案

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"| V
Loading
  • BppComposition 创建一个 plugin-lifetime host,并把它注入 Combat Impact View。
  • session 接管 prepare/cancel、open/attach、layout、fade、hide 与两阶段 restore。
  • View 通过 IPairedContentBudget 提供自身裁剪策略,并把 PlacementResult 转换为既有 reason code。
  • architecture tests 将 CanvasGroupGateNativeAuxiliaryHostState 等宿主实现约束在 GameInterop/Tooltips/

验证情况

  • ./run.sh format-check:通过。
  • Debug、Release、-t:BuildAll:全部通过,0 warning。
  • ./run.sh test:22 个测试工程全部通过。
  • NativePairedTooltipHost.Tests:25 个 placement 测试通过。
  • Architecture.Tests:148 个测试通过,其中包含 4 个新增护栏。
  • 部署产物与构建 DLL 哈希一致:5e305d30…
  • CC 实机 smoke:确认重构构建加载;右置、左置、宽度收缩、顶部修正、CJK 与 Shift Caused/Received 切换正常。
  • CC 稳定性 smoke:30 次以上快速 hover 切换并 pointer exit 后,无 stale panel、残留背景、永久透明或新增异常;PostCombatImpact degraded 仍为同一既有 reason code,未随循环增长。
  • 用户补充:已做简单实机验收,未见问题。

已知局限 / 后续工作

  • 当前 replay 的内容量不足以触发 +N more,因此裁剪顺序完成了代码对照,但没有在这轮 CC smoke 中实际触发。
  • Shift 成功切换已验证;“定位失败后回滚”的异常分支未被实机制造。
  • 未完成与录制按钮之间的 native auxiliary singleton 争用场景;仲裁仍由现有 PostCombatImpact patch 链负责。
  • 未采集 pre-refactor geometry JSON;这是用户明确选择的验证取舍,只有发现视觉差异时才加定向探针。
  • SetLockedFlag(false) 无条件解锁与 View 位于 Patches/ 均为既有债务,本 PR 不顺带修复。
  • Host 目前只有 Combat Impact 一个 paired-shaped consumer;不会据此推断它适配任意 tooltip/anchor。

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 / NativePairedTooltipSession plus shared contracts (PlacementResult, PairSide, NativePairedTooltipOptions, IPairedContentBudget) under GameInterop/Tooltips.
  • Moves the pure placement/geometry formulas into NativePairedTooltipPlacementMath and adds a dedicated unit test project to lock placement behavior (including the shared 0.5f epsilon semantics).
  • Updates NativePostCombatImpactTooltipView to 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.

@pengx17

pengx17 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

Step 7 实机 smoke 结果

环境:macOS,Steam 启动,History → Replay 同一场战斗。分支构建 t20260804.123415.dev,对照 master 952d8b23 构建 t20260804.124950.dev

场景 结果
右置(PairSide.Right
左置(PairSide.Left
宽度收缩 clamp ✅ 面板从 660 收到 ~405 单位
竖直修正 pair_top_alignment_adjusted 正常触发
Shift 视角切换 Caused ↔ Received ✅ 内容重建、重新定位,无闪烁、无 alpha 卡死
快速切换 hover ×30 ✅ 无 stale panel
pointer exit 清理 ✅ 无残留面板/背景,native tooltip 未被损坏
CJK 文案 ✅ 霜绝祭坛 / 巨龙心 / 劳雷尔堡垒 / 仙灵
异常 / 超时 ✅ 零 render exception、零 EntityPreviewCreateTimedOut

日志等价性:分支与 master 各跑一遍相同操作,均为恰好 2 条 post_combat_impact.interaction.degraded reason_code=pair_top_alignment_adjusted,无其它 reason code。30 次悬停切换过程中计数没有增长,说明 LogPlacementDegradationOnce 的 latch / 复位语义保持不变。

interaction.observed 是 Debug 级,BepInEx 磁盘日志配置不落 Debug,属既有配置,与本次改动无关。)

仍未覆盖

+N more 裁剪路径未被触发。实测探针数据显示该 replay 的最重内容也只到 panel_h=2158.7 vs 可用 2177.7,始终判定 fits=True,因此裁剪循环从不进入 —— 在 master 上同样如此。这条路(本次从嵌套 for 改为迭代器状态机)仍是本 PR 唯一未经实机验证的部分,需要一张内容量更大的卡才能覆盖。

附带澄清

过程中曾怀疑小窗口下面板溢出可见区域(见 #207)。加主路径临时探针实测后确认不是 bug_canvasForWorldBounds 与根 Canvas 等高,面板确实放得下,原始现象是窗口 resize 中间态的渲染视口滞后。探针已撤除。

@pengx17
pengx17 merged commit 792d315 into master Aug 4, 2026
1 check passed
@pengx17
pengx17 deleted the refactor/native-paired-tooltip-host branch August 4, 2026 05:02
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants