diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 20661f2d..394ec1c7 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -342,7 +342,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | AP-187 | **Filed 2026-08-10 (Campaign CH slice CH6b — floating chat windows).** The four floating chat windows' text-type filters (`AcDream.Core.Chat.ChatWindowState`, retail's `0x1000007F` per-window option) persist only in local `settings.json` (`ChatSettings.ChatWindow1Filter`..`ChatWindow4Filter`, `SettingsStore.LoadChat`/`SaveChat`). Retail's authoritative store for this same data is the per-window option array (`0x1000008C`) packed inside the character-scoped `GameplayOptions` blob, which ACE stores and echoes as opaque bytes without parsing (window-shell research doc §4.1/§4.4); acdream has no reader or writer for that blob (CH3 already deleted one malformed attempt at the outbound `SetCharacterOptions 0x01A1` builder — `SocialActions.cs`). Geometry and open/visible state for these same four windows do NOT need a row of their own: they persist through the pre-existing generic `RetailWindowLayoutPersistence` path (X/Y/W/H/visible/collapsed/maximized per window name), which is retail's OWN local-file mechanism too (`gmGamePlayUI::SaveScreenLayout`/`LoadScreenLayout`, window-shell research doc §4.3) — only the filter mask lacks any such local-file precedent in retail and is acdream's own addition to make the feature usable before CH6f lands. `src/AcDream.UI.Abstractions/Panels/Settings/ChatSettings.cs`; `src/AcDream.UI.Abstractions/Panels/Settings/SettingsStore.cs` (`LoadChat`/`SaveChat`/`BuildChatObject`); `src/AcDream.App/UI/RetailUiRuntime.cs` (`MountFloatingChatWindows`, `SaveChatWindowFilters`) | CH6a/CH6b's own port-shape recommendation (window-shell research doc §6.1) explicitly chose local persistence first and deferred the `0x1000008B`/`0x1000008C` wire to a dedicated CH6f slice, citing CH3's deleted malformed builder as the reason not to rush it | A character's floating-window filter customization does not travel to a different acdream install, and would not round-trip through a retail client sharing the same character (retail would see acdream's local-only values as unset, falling back to its own `PostInit` defaults) — cosmetic/preference-only, no gameplay effect | `PlayerModule::GetChatOptionStructure @0x005D5300`; `PlayerModule::InqChatWindowOption/SetChatWindowOption @0x005D5540/:70`; `docs/research/2026-08-09-chat-retail-window-shell.md` §4.1/§4.4/§6.1; `docs/plans/2026-08-09-chat-parity-campaign.md` (CH6f row) | | AP-188 | **Filed 2026-08-10 (Campaign CH slice CH6b — floating chat windows).** A floating chat window's chat entry always sends on the `Say` channel (`FloatingChatWindowController.Bind`'s `OnSubmit` hardcodes `ChatChannelKind.Say`). The floaty LayoutDesc (`0x2100005B`) authors no talk-focus menu (window-shell research doc §2.2 — only the main window's layout has one, element `0x10000014`), so there is no visible channel picker on a floaty window either way, matching retail's authored UI exactly. What is UNVERIFIED is whether retail's actual SEND path for a floaty window's typed message reads a per-window channel or the single globally-current talk-focus channel/target the main window's menu (or the last-selected/last-speakable-target state `gmMainChatUI::UseTime @0x004CDB20` tracks) last set — if the latter, a real retail floaty window would send on whatever channel the player most recently picked from the MAIN window, not always `Say`. Confirming this requires tracing `gmCCommunicationSystem`'s send-command path from a floaty `ChatInterface` instance, not yet done. Filed as ISSUES.md #369. `src/AcDream.App/UI/Layout/FloatingChatWindowController.cs` (`Bind`, the `OnSubmit` wiring) | Building genuine cross-window shared-channel state (reading `ChatWindowController`'s private `_activeChannel` from four independent sibling controllers, or promoting it to a shared owner) is a real design decision outside this slice's explicit scope (task items 1-6 do not ask for cross-window channel sharing); `Say` is retail's own default channel and the safest fixed value absent confirmation | If retail's actual mechanism is "send on the currently-selected global channel," a user who selects e.g. Fellowship from the main window's talk-focus menu and then types into a floaty window would see it sent as Fellowship in retail but as Say in acdream — no data loss (the message still sends), only channel-selection mismatch | `gmMainChatUI::InitTalkFocusMenu @0x004CDC50`; `gmMainChatUI::UseTime @0x004CDB20`; `docs/research/2026-08-09-chat-retail-window-shell.md` §2.2 | | AP-189 | **Filed 2026-08-10 at the CH6a/b REJECT-review rework (SHOULD-FIX 5, `docs/research/2026-08-10-ch6ab-review-findings.md`).** Retail keeps a PER-`ChatInterface` `m_chatLog`, truncated at 10,000 lines (`ChatInterface::RecvNotice_DisplayFinalStringInfo @0x004F4711` → `TruncateChatLog`) — each of the five windows (main + 4 floaty) owns its OWN 10,000-line backlog, and a closed window keeps accumulating into its own log because `gmFloatyMainChatUI::SetVisible @0x004CE9B0` never unregisters the handler. acdream instead shares ONE canonical `ChatLog` capped at 500 entries (`RuntimeCommunicationState`'s ctor, `maximumChatEntries: 500`) with a 200-line display tail every window filters from (`InteractionRetainedUiComposition.cs:564`'s `displayLimit: 200` feeding `ChatVM.RecentLinesDetailed`; `ChatWindowState.ShouldDisplay` does the per-window filtering). The accumulate-while-closed and independent-per-window-scroll BEHAVIORS both fall out correctly from this shared-log shape, but the EFFECTIVE per-window scrollback DEPTH differs from retail's: a window whose filter accepts only a rare message type (e.g. a Fellowship-only floaty) sees only the fellowship lines that happen to still be inside the shared log's last 200-of-500 lines, not up to 10,000 like retail's own per-window log. `src/AcDream.Core/Chat/ChatLog.cs` (`_maxEntries`); `src/AcDream.App/Composition/InteractionRetainedUiComposition.cs:564` (`displayLimit: 200`); `src/AcDream.App/UI/Layout/ChatWindowController.cs`/`FloatingChatWindowController.cs` (`GetTranscriptLines`) | A single shared canonical log matches acdream's Slice-J "one canonical transcript, many filtered presentations" pattern and keeps memory bounded regardless of how many windows are open; 500 shared entries covers many minutes of typical mixed-channel play, and both retail-observable BEHAVIORS this row could have broken (closed-window accumulation, independent per-window scroll position) are reproduced correctly — only the numeric DEPTH ceiling differs | In a busy mixed-channel session (heavy General/Trade traffic), a rarely-used channel (Fellowship, a Turbine room) can scroll out of the shared 500-entry window long before a floaty window filtered to just that channel would have neared retail's 10,000-line depth — a user who opens that floaty window after a long session sees a much shorter backlog than retail would show for the same play session | `ChatInterface::RecvNotice_DisplayFinalStringInfo @0x004F4640`/`TruncateChatLog @0x004F4711`; `gmFloatyMainChatUI::SetVisible @0x004CE9B0`; `docs/research/2026-08-09-chat-retail-window-shell.md` §1.2 | -| AP-190 | **Filed 2026-08-10 (Campaign CH slice CH6c — window opacity + transparency setting; retires AP-40).** Two divergences from retail's focus-driven window opacity, both decomp-verified (`docs/research/2026-08-09-chat-retail-window-shell.md` §3). (1) SCOPE: retail's `ChatInterface::SetOpacity`/`SetDefaultOpacity`/`SetActiveOpacity` only ever run on `ChatInterface`-derived windows (the main chat window + the four floaties) — every other retail window (vitals, toolbar, inventory, ...) has no opacity fade at all. acdream's `RetailWindowOpacityController` subscribes to `RetailWindowManager.WindowRegistered` and applies the SAME focus-driven fade to every window the manager ever registers, so the one Settings → Chat tab transparency slider pair affects the whole retained UI. (2) DEFAULT VALUE: retail's shipped defaults are PER WINDOW CLASS — the base `ChatInterface` ctor (`0x004F4550`) sets DefaultOpacity=0.5/ActiveOpacity=1.0, but `gmMainChatUI`'s own ctor (`0x004CD0F0`, called after the base ctor) overrides DefaultOpacity to 1.0 (the main window is ALWAYS fully opaque in both states); `gmFloatyChatUI::Create` (`0x004CE2C0`) calls the base ctor directly with no override, so only the four floating windows keep 0.5/1.0. acdream ships ONE shared global default (the base ChatInterface value, 0.5/1.0) applied uniformly including to the main chat window, rather than replicating the per-class 1.0/1.0 override. The linked active>=default invariant itself (`SetDefaultOpacity`/`SetActiveOpacity`'s mutual-correction bodies) IS ported exactly — `ChatOpacityLink` in `AcDream.UI.Abstractions`. | `src/AcDream.App/UI/RetailWindowOpacityController.cs`; `src/AcDream.App/UI/RetailWindowManager.cs` (`WindowRegistered`); `src/AcDream.UI.Abstractions/Panels/Settings/ChatOpacityLink.cs`; `src/AcDream.UI.Abstractions/Panels/Settings/ChatSettings.cs` (`DefaultOpacity`/`ActiveOpacity`) | Extending the fade to every window is the shape the user's requested "transparency setting" actually wants (a general UI preference, not a chat-only one); a single global default keeps the setting's starting behavior predictable across every window rather than silently varying the main chat window's fade floor from every other window's | A user who compares acdream's main chat window against retail side-by-side at the DEFAULT (never-touched) setting will see acdream's main window fade to 50% while idle, where retail's stays fully opaque always; every other window fading at all (vitals, toolbar, ...) has no retail analogue to compare against by construction | `ChatInterface::ChatInterface @0x004F4550`; `gmMainChatUI::gmMainChatUI @0x004CD0F0`; `gmFloatyChatUI::Create @0x004CE2C0`; `ChatInterface::SetDefaultOpacity @0x004F3BC0`/`SetActiveOpacity @0x004F3C40` | +| AP-190 | **Filed 2026-08-10 (Campaign CH slice CH6c — window opacity + transparency setting; retires AP-40). AMENDED 2026-08-10 at the CH6c review-fix round: reworded (2), added (3)/(4).** Four divergences from retail's focus-driven window opacity, all decomp-verified (`docs/research/2026-08-09-chat-retail-window-shell.md` §3). (1) SCOPE: retail's `ChatInterface::SetOpacity`/`SetDefaultOpacity`/`SetActiveOpacity` only ever run on `ChatInterface`-derived windows (the main chat window + the four floaties) — every other retail window (vitals, toolbar, inventory, ...) has no opacity fade at all. acdream's `RetailWindowOpacityController` subscribes to `RetailWindowManager.WindowRegistered` and applies the SAME focus-driven fade to every window the manager ever registers, so the one Settings → Chat tab transparency slider pair affects the whole retained UI. (2) DEFAULT VALUE — REWORDED at the review-fix round: retail's shipped defaults are PER WINDOW CLASS — the base `ChatInterface` ctor (`0x004F4550`) sets DefaultOpacity=0.5/ActiveOpacity=1.0, but `gmMainChatUI`'s own ctor (`0x004CD0F0`, called after the base ctor) overrides DefaultOpacity to 1.0 (the main window is ALWAYS fully opaque in both states); `gmFloatyChatUI::Create` (`0x004CE2C0`) calls the base ctor directly with no override, so only the four floating windows keep 0.5/1.0. acdream originally shipped the base ChatInterface value (0.5/1.0) as ONE shared global default applied to EVERY registered window — combined with (1)'s scope extension this faded the WHOLE registered UI (radar, vitals, toolbar, main chat, ...) to 50% opacity out of the box, including several windows that can never take keyboard focus at all and so were PERMANENTLY stuck at 0.5. Fixed at the review round to `gmMainChatUI`'s 1.0/1.0 override as the shared default instead: this reduces the remaining divergence to acdream's four floating chat windows shipping OPAQUE where retail's floaties ship 0.5-while-idle — user-settable via the same Settings → Chat opacity slider pair, so it is now a default-VALUE divergence only, not a missing mechanism. (3) EASING (new, filed at the review-fix round): retail's `ChatInterface::ListenToGlobalMessage @0x004F3840` — armed on the focus element-messages `0x1A`/`0x1E`/`0x28`/`0x29`/`0x2E` at `0x004F5275` via `UIListener::RegisterForGlobalMessage(this, 3)` — eases the live opacity toward its target by 5% of the target-delta per tick, unregistering from the global tick once within FP-epsilon of the target. acdream's `RetailWindowOpacityController.Apply` snaps to the target opacity immediately on every focus-change event; porting the per-tick lerp needs a UI frame-tick hook the controller does not have today, so it is deferred rather than implemented this round. (4) FOCUS PREDICATE (new, filed at the review-fix round): retail's `ChatInterface::IsTextEntryFocused @0x004F30A0` tests specifically whether `GetFocusDescendant(rootElement) == this->m_chatEntry` — the chat ENTRY FIELD, not the window generally. acdream's `RetailWindowHandle.DescendantFocusChanged` fires whenever ANY focusable descendant of the window gains focus, a strictly broader predicate for any window with more than one focusable child. The linked active>=default invariant itself (`SetDefaultOpacity`/`SetActiveOpacity`'s mutual-correction bodies) IS ported exactly — `ChatOpacityLink` in `AcDream.UI.Abstractions`. | `src/AcDream.App/UI/RetailWindowOpacityController.cs`; `src/AcDream.App/UI/RetailWindowManager.cs` (`WindowRegistered`); `src/AcDream.UI.Abstractions/Panels/Settings/ChatOpacityLink.cs`; `src/AcDream.UI.Abstractions/Panels/Settings/ChatSettings.cs` (`DefaultOpacity`/`ActiveOpacity`) | Extending the fade to every window is the shape the user's requested "transparency setting" actually wants (a general UI preference, not a chat-only one); shipping the shared default at 1.0 keeps the out-of-box render retail-identical for the 11 non-chat windows AND the main chat window (the windows retail keeps opaque, several of which can never take focus at all), while the Settings → Chat transparency slider remains fully user-settable for anyone who wants the four floaties' retail translucence back. (3) and (4) are both presentation-only refinements — the fade direction and the linked-invariant math stay retail-exact, only the transition curve (snap vs. 5%-per-tick ease) and the focus predicate's granularity (any descendant vs. the text-entry specifically) diverge — so recording them without implementing the frame-tick hook (3) or narrowing the focus event (4) is the correct scope for a review-fix round rather than opening new implementation work | A user who compares acdream's default install against retail side-by-side now sees the 11 non-chat windows AND the main chat window matching (opaque); only the four floating chat windows still diverge (opaque vs. retail's 50%-while-idle) until the slider is dragged. (3) is visible as the opacity change happening in a single frame instead of retail's ~20-tick fade — low severity, since the START and END states are both retail-exact, only the transition is instant instead of eased. (4) is visible on any window with more than one distinct focusable descendant (e.g. a settings panel with several controls): acdream stays at ActiveOpacity while ANY of them holds focus, where retail would already have faded back to DefaultOpacity once focus left the specific text-entry element — for single-focusable-child windows (most of the retained UI today) the two predicates coincide and there is no observable difference | `ChatInterface::ChatInterface @0x004F4550`; `gmMainChatUI::gmMainChatUI @0x004CD0F0`; `gmFloatyChatUI::Create @0x004CE2C0`; `ChatInterface::SetDefaultOpacity @0x004F3BC0`/`SetActiveOpacity @0x004F3C40`; `ChatInterface::ListenToGlobalMessage @0x004F3840`; `ChatInterface::IsTextEntryFocused @0x004F30A0`; global-message arming switch @0x004F5275 (`UIListener::RegisterForGlobalMessage(this, 3)` on element messages `0x1A`/`0x1E`/`0x28`/`0x29`/`0x2E`) | ## 4. Temporary stopgap (TS) — 39 active rows (TS-70 RETIRED 2026-08-09 at Campaign CH user-gate round 1, item E (#362) — `ClientCommandResponses.cs` now parses and renders all four named inbound GameEvents (`ChannelIndex 0x0149`, `ChannelList 0x0148`, `AvailableHouses 0x0271`, `AllegianceInfoResponse 0x027C`), each wired into `GameEventWiring.cs` and rendering retail-shaped `LogTextType 0x00` lines ported from the named-retail decomp (`Handle_Communication__ChannelIndex`/`ChannelList` @0x0057d0c0/@0x0057d230, `Handle_House__Recv_AvailableHouses` + `DisplayListOfCoords` @0x00585d50/@0x00585c20, `Handle_Allegiance__AllegianceInfoResponseEvent` @0x0056a1d0); the row's `@on`/`@off` mention was never itself missing a handler (both already resolve through the pre-existing `WeenieErrorWithString` registration) so nothing there needed a fix; TS-68/TS-69 filed 2026-08-09, Campaign CH slice CH4 — the deferred allegiance/house subcommand dispatchers, the three unported pure-local commands (day/log/render); TS-66/TS-67 filed and TS-29 retired 2026-08-08, Campaign A slice A5 — the region ambient system landed, so TS-29's ambient half is ported and its music half turned out to have nothing to port; TS-66 is the omitted `seen_outside` interior case and TS-67 the in-plane contribution weight. TS-64/TS-65 filed 2026-08-08, Campaign A slice A2 — TS-64 the two unimplemented retail sound preferences (unfocused-app silence, pan disable) plus the three enable bools; TS-65 the volume-squared quirk, applied on the ambient path where two lanes byte-confirmed it and deliberately NOT on the hook path where the pre-multiplying overload is unpinned. TS-62/TS-63 filed 2026-08-02, continuation-executor slice; TS-4 and TS-8 retired 2026-07-31; Campaign P's goal-enumerated physics stopgaps are now zero. TS-4's graph/flat Path-6 branches match retail's foot SetCollide/Adjusted and head CollisionNormal/Collided split with no BSP-layer sliding-normal write; TS-8's live 0x02C2 carries its complete StatMod through the canonical enchantment record and updates effective stats immediately. Campaign P P7 2026-07-30: TS-25 retired — outbound stance has shipped via RawState.CurrentStyle since #219; TS-24 re-argued to AD-57; TS-40 re-argued to AD-58; TS-35 retired at P5; earlier same campaign: TS-1/TS-5/TS-23/TS-46 retired by ports; TS-23 retired 2026-07-30 at Campaign P Slice P3 — every mover-flags call site (local player world-entry ×2, remote DR sweep ×2, remote teleport, ordinary movers) now ORs in the mover's real PK/PKLite/Impenetrable `ObjectInfoState` bits via the new `ClientObjectTable`-backed `EntityCollisionFlagsExt.ResolveMoverPvpState` — **narrative corrected 2026-08-03 (#297): "real" only became true at #297. Until then the bits existed but the source `PublicWeenieBitfield` was frozen at CreateObject, so every one of those sites read a stale value for the whole session. The site enumeration is also incomplete: `RuntimeSetPositionMoverPreparation.cs:183-188` is a SEVENTH mover-flags site that decodes `record.Snapshot.ObjectDescriptionFlags` directly rather than calling `ResolveMoverPvpState`, and it also derives `ObjectInfoState.IsPlayer` from the PWD bit, contradicting `EntityCollisionFlags.cs:119-123`'s claim that every site uses a GUID-prefix heuristic. See AP-134.** — and `PlayerWeenie.JumpStaminaCost`'s `pk` parameter reads the real `PlayerKillerStatus`/`LastPkAttackTimestamp` pair against a 20-second window instead of a hardcoded `false`; the non-PK invariant (every ACE default-created character) is bit-identical to the pre-P3 value since `ResolveMoverPvpState` and the PK-timer predicate both resolve to a no-op for `PublicWeenieBitfield` absent/0; TS-46 retired 2026-07-30 at Campaign P Slice P3 — the Setup's verbatim ≤2-sphere list (`CPhysicsObj::transition` 0x00512dc0 → `SPHEREPATH::init_sphere` 0x0050c670) now seeds the sweep for the local player, remote dead-reckoning, and ordinary movers alike, replacing the two-scalar (radius, height) capsule reconstruction; remote/ordinary step-up/step-down are now Setup-derived (`CPartArray::GetStepUpHeight`/`GetStepDownHeight`, 0x005180d0/0x005180f0, ×ObjScale) instead of a hardcoded 0.4 m, closing both residuals the row named; TS-5 retired 2026-07-30 at Campaign P Slice P1 — real burden-gated CanJump + real JumpStaminaCost, both decomp-verbatim; TS-1 retired 2026-07-30 at Campaign P Slice P2 — the row was stale; the EdgeSlide → PrecipiceSlide/CliffSlide chain is already a real, tested port; TS-57..TS-61 filed 2026-07-29 during Campaign N — no outbound RejectRetransmit; TS-27 narrowed same slice to the inbound direction) + TS-37 historical note (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-42 retired 2026-07-19 — semantic animation completion now precedes the ordered Target/Movement/PartArray/Position tail; TS-44 narrowed again 2026-07-19 — complete orientation joined interpolation, only during-stick enqueue suppression remains) diff --git a/docs/plans/2026-08-09-chat-parity-campaign.md b/docs/plans/2026-08-09-chat-parity-campaign.md index 0132665a..e4dca9d0 100644 --- a/docs/plans/2026-08-09-chat-parity-campaign.md +++ b/docs/plans/2026-08-09-chat-parity-campaign.md @@ -287,7 +287,7 @@ implementer per slice against a pinned contract (per | Jump-in-air root cause (round-2 item 1, resolved) | `a5a7eb4f` | Runtime tests 1,323/0 | — | round-3 probe evidence pinpointed a missing `OnInterfaceText` wire on the production controller-commit path (`RuntimeLocalPlayerMovementState.CommitRuntimeOwnedController`); FIXED, regression test added | | User gate round 3 | `98de4f5a` | Debug (all projects): 12,329 passed / 4 skipped / 1 failed (pre-existing #351 Debug-only flake — reproduces identically on the pristine pre-round-3 commit, not a regression); Release (every project reachable while a live `AcDream.App.exe` client — PID 15064, must not be killed per project policy — holds its own Release binaries locked, blocking `AcDream.App`/`AcDream.App.Tests`/`AcDream.Core.Tests` specifically): `AcDream.UI.Abstractions.Tests` (the layer this round's `/help` fix lives in) 867/867, plus `Core.Net.Tests` 823/823, `Runtime.Tests` 1,323/1,323, `Content.Tests` 130/130, `Headless.Tests` 89/89, `Bake.Tests` 15/15, `Cli.Tests` 4/4 — all 0 failed | — | findings (a)-(c) fixed this commit — SpewBox flush-top + retail dat font, `/help`/`/help death` exact retail print sequence (see "User gate — round 3" below) | | CH6b floating windows 1–4 | `22020ef2`, reworked `1aa77099` | 12,420 passed / 4 skipped / 0 failed | REJECT (docs/research/2026-08-10-ch6ab-review-findings.md) → reworked `1aa77099` — SHOULD-FIXES 2/3/4/5 + NITs 1-5 applied | pending — no client launches this session (hard constraint); needs the next connected round for keybind/mirror/filter visual confirmation, plus the new 0x2100005B fixture's resolved-type assumptions | -| CH6c opacity | `a819687c` | 12,459 passed / 4 skipped / 0 failed | pending (no subagent review pass this session — implementer-only) | pending — needs the next connected round for visual confirmation (window fade on focus change, Settings slider live-apply) | +| CH6c opacity | `a819687c` | 12,459 passed / 4 skipped / 0 failed | APPROVE-WITH-FIXES; fixed this commit — BLOCKER (out-of-box `DefaultOpacity` 0.5→1.0), AP-190 reworded + two new decomp-verified clauses (retail's per-tick ease, retail's entry-field-specific focus predicate), NITs (`UiElement.cs` stale comment, `WindowUnregistered` detach, post-Dispose `Set*` guards, `DrawString`/outline-pass alpha tests) | pending — needs the next connected round for visual confirmation (window fade on focus change, Settings slider live-apply) | ### CH4 closeout (2026-08-09) diff --git a/docs/research/2026-08-09-chat-retail-window-shell.md b/docs/research/2026-08-09-chat-retail-window-shell.md index 7573ecff..67e1388b 100644 --- a/docs/research/2026-08-09-chat-retail-window-shell.md +++ b/docs/research/2026-08-09-chat-retail-window-shell.md @@ -503,15 +503,63 @@ than per-window: only a NEVER-SAVED option (a fresh character, nothing in the through, and even then only until the user's first slider drag pushes one shared value into every live window via `RecvNotice_GameplayOptionChanged`. -acdream ships ONE shared global default — the base `ChatInterface` value, -0.5/1.0 — applied uniformly to every window including the main chat window -(register row AP-190 in `docs/architecture/retail-divergence-register.md`), -rather than replicating `gmMainChatUI`'s per-class 1.0/1.0 override. The +**CH6c review-fix round (2026-08-10): the shared default above was WRONG.** +Shipping the base `ChatInterface` value (0.5/1.0) as ONE shared global +default, combined with the scope extension to every registered window, faded +the WHOLE registered UI (radar, vitals, toolbar, main chat, ...) to 50% +opacity out of the box — including several windows that can never take +keyboard focus at all, so they were stuck at 0.5 permanently. acdream now +ships `gmMainChatUI`'s per-class 1.0/1.0 override (`0x004CD0F0`) as the +shared global default instead (register row AP-190 in +`docs/architecture/retail-divergence-register.md`), which is retail-identical +for the 11 non-chat windows and the main chat window and leaves only the four +floating chat windows diverging from retail's 0.5-while-idle fade — a +default-VALUE divergence the transparency slider still fully covers. The linking invariant (active >= default, restored by dragging the OTHER value — verified from `SetDefaultOpacity`/`SetActiveOpacity`'s own bodies, matching the summary already recorded above) is ported exactly regardless of which default seeds it. +### 3.1 Two more residuals found at the CH6c review (not yet ported) + +Both are decomp-verified and both are recorded as new AP-190 clauses; neither +is implemented this round. + +**(a) Retail eases opacity between endpoints; acdream snaps.** +`ChatInterface::ListenToGlobalMessage @0x004F3840` is the handler for global +message id `3`, armed (`UIListener::RegisterForGlobalMessage(this, 3)`) from +the element-focus messages `0x1A`/`0x1E`/`0x28`/`0x29`/`0x2E` inside the +window's `ListenToElementMessage` switch at `0x004F5275`. Once armed, every +tick nudges the live opacity toward whichever endpoint +`IsTextEntryFocused` currently selects by 5% of the endpoint delta +(`fabsl(target - current) * 0.05f`), and unregisters from the global message +once the value lands within FP-epsilon of the target. acdream's +`RetailWindowOpacityController.Apply` sets the target opacity directly on the +focus-change event — the START and END states are retail-exact, but the +transition is an instant snap instead of a roughly 20-tick fade. Porting the +lerp needs a UI frame-tick hook `RetailWindowOpacityController` does not have +today (it only reacts to `DescendantFocusChanged`); deferred. + +**(b) Retail's focus predicate is the chat entry field specifically; acdream's +is any focusable descendant.** `ChatInterface::IsTextEntryFocused @0x004F30A0` +tests `GetFocusDescendant(rootElement) == this->m_chatEntry` — literally the +window's text-entry element, not "some descendant of this window has focus." +acdream's `RetailWindowHandle.DescendantFocusChanged` (the event +`RetailWindowOpacityController` subscribes to) fires whenever ANY focusable +descendant of the window gains focus. For a window with exactly one +focusable child the two predicates coincide; for a window with several (a +settings panel's multiple controls, for example) acdream's broader predicate +holds ActiveOpacity while retail would already have faded back to +DefaultOpacity once focus left the specific text-entry widget. + +**Pre-existing, unrelated: `UiMenu.cs:293`'s opacity bypass.** Popup menus +call `ctx.PushAlphaAbsolute(1f)` before drawing so a menu always reads solid +even when it is opened from a translucent (faded) window — this is a +deliberate acdream-only presentation choice (menus must stay legible +regardless of the host window's current fade state), not a divergence from +either of the two opacity mechanisms documented in this section, and it +predates the CH6c slice. + --- ## 4. Persistence — how filters, geometry, visibility and title survive diff --git a/src/AcDream.App/Rendering/TextRenderer.cs b/src/AcDream.App/Rendering/TextRenderer.cs index b9ea2bb3..7c714bb4 100644 --- a/src/AcDream.App/Rendering/TextRenderer.cs +++ b/src/AcDream.App/Rendering/TextRenderer.cs @@ -115,6 +115,21 @@ public sealed class TextRenderer : IDisposable } } + /// + /// Test-only snapshot of the current frame's queued NORMAL-layer BITMAP FONT + /// text buffer (/, + /// the path used for + /// D.6 world-space HUD text): (vertex count, alpha of the first emitted + /// vertex — color.W at float index 7 of the 8-float vertex layout). Unlike + /// this buffer is not split per-texture — + /// bitmap-font glyphs all sample the one atlas — so there is exactly one + /// (count, alpha) pair to check. CH6c review NIT: pins that + /// 's alpha chokepoint is guarded + /// the same way 's already was. + /// + internal (int VertexCount, float Alpha) DebugTextBuffer + => (_textVerts, _textBuf.Count > 0 ? _textBuf[7] : 0f); + // Overlay layer — a parallel set of buckets drawn AFTER the normal sprite/rect/text // buckets, so open popups/menus composite on top of EVERYTHING, including translucent // rect panel backgrounds (which otherwise always win because rects flush after diff --git a/src/AcDream.App/UI/RetailWindowManager.cs b/src/AcDream.App/UI/RetailWindowManager.cs index c037d055..51cab64c 100644 --- a/src/AcDream.App/UI/RetailWindowManager.cs +++ b/src/AcDream.App/UI/RetailWindowManager.cs @@ -43,6 +43,17 @@ public sealed class RetailWindowManager : IDisposable /// public event Action? WindowRegistered; + /// + /// Fires when a window is REMOVED from the registry (), + /// after teardown (NotifyClosed/DisposeController) but before the + /// handle is discarded. CH6c review NIT: + /// subscribes here so it can drop its per-handle DescendantFocusChanged + /// subscription and forget the handle from its focused-window set — without this, + /// a window unregistered while it held keyboard focus stayed referenced by the + /// controller for the rest of the session. + /// + public event Action? WindowUnregistered; + public RetailWindowHandle Register( string name, UiElement outerFrame, @@ -215,6 +226,7 @@ public sealed class RetailWindowManager : IDisposable _defaultInputs.Remove(handle); handle.NotifyClosed(); handle.DisposeController(); + WindowUnregistered?.Invoke(handle); return true; } diff --git a/src/AcDream.App/UI/RetailWindowOpacityController.cs b/src/AcDream.App/UI/RetailWindowOpacityController.cs index 2109b4b7..46251781 100644 --- a/src/AcDream.App/UI/RetailWindowOpacityController.cs +++ b/src/AcDream.App/UI/RetailWindowOpacityController.cs @@ -29,9 +29,24 @@ namespace AcDream.App.UI; /// (0x004F4550, DefaultOpacity=0.5/ActiveOpacity=1.0) to DefaultOpacity=1.0 /// (always fully opaque); gmFloatyChatUI::Create (0x004CE2C0) calls /// ChatInterface::ChatInterface directly with no override, so the four -/// floating windows keep the base 0.5/1.0. acdream ships ONE shared default (the -/// base ChatInterface value, 0.5/1.0) applied uniformly, including to the main -/// chat window — a simplification recorded alongside the scope extension above. +/// floating windows keep the base 0.5/1.0. CH6c review fix: acdream ships ONE +/// shared default — gmMainChatUI's 1.0/1.0 override, not the base +/// ChatInterface value — applied uniformly to every window including the four +/// floating chat windows. Shipping the base 0.5/1.0 pair globally (the original +/// CH6c behavior) faded the WHOLE registered UI to 50% opacity out of the box, +/// including windows that can never take keyboard focus and so were stuck at 0.5 +/// permanently; 1.0/1.0 is retail-identical for the 11 non-chat windows and the +/// main chat window, and only the four floaties diverge from retail's +/// 0.5-while-idle default now — user-settable via the same slider (register row +/// AP-190). +/// +/// +/// +/// Retail also EASES the live opacity toward its target by 5% of the delta per +/// tick (ChatInterface::ListenToGlobalMessage @0x004F3840) rather than +/// snapping, and its focus predicate is the chat ENTRY FIELD specifically +/// (ChatInterface::IsTextEntryFocused @0x004F30A0), not "any descendant has +/// focus". Both are recorded as AP-190 residuals — CH6c review, not yet ported. /// /// public sealed class RetailWindowOpacityController : IDisposable @@ -55,6 +70,7 @@ public sealed class RetailWindowOpacityController : IDisposable System.Math.Clamp(activeOpacity, 0f, 1f)); _manager.WindowRegistered += OnWindowRegistered; + _manager.WindowUnregistered += OnWindowUnregistered; foreach (RetailWindowHandle handle in _manager.Windows) Attach(handle); } @@ -72,6 +88,7 @@ public sealed class RetailWindowOpacityController : IDisposable /// public void SetDefaultOpacity(float value) { + if (_disposed) return; (DefaultOpacity, ActiveOpacity) = ChatOpacityLink.SetDefault(ActiveOpacity, value); ReapplyAll(); } @@ -82,6 +99,7 @@ public sealed class RetailWindowOpacityController : IDisposable /// public void SetActiveOpacity(float value) { + if (_disposed) return; (DefaultOpacity, ActiveOpacity) = ChatOpacityLink.SetActive(DefaultOpacity, value); ReapplyAll(); } @@ -94,6 +112,7 @@ public sealed class RetailWindowOpacityController : IDisposable /// public void SetOpacity(float defaultOpacity, float activeOpacity) { + if (_disposed) return; (DefaultOpacity, ActiveOpacity) = ChatOpacityLink.SetDefault(ActiveOpacity, defaultOpacity); (DefaultOpacity, ActiveOpacity) = ChatOpacityLink.SetActive(DefaultOpacity, activeOpacity); ReapplyAll(); @@ -101,6 +120,19 @@ public sealed class RetailWindowOpacityController : IDisposable private void OnWindowRegistered(RetailWindowHandle handle) => Attach(handle); + /// + /// CH6c review NIT: without this, a window unregistered while it held + /// keyboard focus stayed in — and its + /// subscription + /// stayed live — for the rest of the session, because the only prior + /// detach point was . + /// + private void OnWindowUnregistered(RetailWindowHandle handle) + { + handle.DescendantFocusChanged -= OnDescendantFocusChanged; + _focused.Remove(handle); + } + private void Attach(RetailWindowHandle handle) { handle.DescendantFocusChanged += OnDescendantFocusChanged; @@ -133,7 +165,9 @@ public sealed class RetailWindowOpacityController : IDisposable _disposed = true; _manager.WindowRegistered -= OnWindowRegistered; + _manager.WindowUnregistered -= OnWindowUnregistered; foreach (RetailWindowHandle handle in _manager.Windows) handle.DescendantFocusChanged -= OnDescendantFocusChanged; + _focused.Clear(); } } diff --git a/src/AcDream.App/UI/UiElement.cs b/src/AcDream.App/UI/UiElement.cs index f2fe46d1..4af90e90 100644 --- a/src/AcDream.App/UI/UiElement.cs +++ b/src/AcDream.App/UI/UiElement.cs @@ -460,7 +460,10 @@ public abstract class UiElement if (!Visible) return; // Translate into our local space + push this window's opacity (multiplies into - // descendants' sprite/rect draws; text bypasses the alpha so it stays sharp). + // descendants' sprite, rect, AND text draws — CH6c ported DrawStringDat/DrawString + // through the same ApplyAlpha chokepoint as sprites/rects, matching retail's + // ChatInterface::SetOpacity (0x004F3120), which fades the whole composited window + // surface, chrome and glyphs together, not text-stays-sharp over a translucent panel). ctx.PushTransform(Left, Top); ctx.PushAlpha(Opacity); try diff --git a/src/AcDream.UI.Abstractions/Panels/Settings/ChatSettings.cs b/src/AcDream.UI.Abstractions/Panels/Settings/ChatSettings.cs index 7e023c8c..41b8d373 100644 --- a/src/AcDream.UI.Abstractions/Panels/Settings/ChatSettings.cs +++ b/src/AcDream.UI.Abstractions/Panels/Settings/ChatSettings.cs @@ -70,7 +70,20 @@ public sealed record ChatSettings( // ChatOpacityLink at every setter, not by clamping here. acdream applies this // GLOBALLY to every RetailWindowManager-registered window (register row // AP-190), where retail scopes it to ChatInterface-derived windows only. - float DefaultOpacity = 0.5f, + // + // CH6c review fix (2026-08-10): the base ChatInterface ctor's 0.5/1.0 pair + // is retail-correct ONLY for the four floating chat windows — + // gmMainChatUI overrides to 1.0/1.0 (0x004CD0F0), and every other + // RetailWindowManager-registered window (radar, vitals, toolbar, ...) has + // no retail opacity fade at all, so applying 0.5 to them out of the box + // rendered the whole registered UI half-transparent forever, including + // several windows that can never take keyboard focus and so were + // PERMANENTLY stuck at 0.5. DefaultOpacity now ships 1.0, matching + // retail-identical opaque presentation for the 11 non-chat windows and + // the main chat window; the four floating chat windows lose their + // retail 0.5-while-idle fade by default, but the Settings → Chat + // transparency slider remains fully user-settable (AP-190). + float DefaultOpacity = 1.0f, float ActiveOpacity = 1.0f) { /// diff --git a/tests/AcDream.App.Tests/Settings/RuntimeSettingsControllerTests.cs b/tests/AcDream.App.Tests/Settings/RuntimeSettingsControllerTests.cs index 4b187dbd..1ebebb20 100644 --- a/tests/AcDream.App.Tests/Settings/RuntimeSettingsControllerTests.cs +++ b/tests/AcDream.App.Tests/Settings/RuntimeSettingsControllerTests.cs @@ -312,7 +312,7 @@ public sealed class RuntimeSettingsControllerTests "save-gameplay", "target-ui-lock:True", "save-chat", - "target-chat-opacity:0.5:1", + "target-chat-opacity:1:1", "save-character:default", ], events); diff --git a/tests/AcDream.App.Tests/UI/RetailWindowOpacityControllerTests.cs b/tests/AcDream.App.Tests/UI/RetailWindowOpacityControllerTests.cs index dc48d5e3..c85a72bc 100644 --- a/tests/AcDream.App.Tests/UI/RetailWindowOpacityControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/RetailWindowOpacityControllerTests.cs @@ -185,4 +185,63 @@ public sealed class RetailWindowOpacityControllerTests // focus change after Dispose is not observed anymore. Assert.Equal(0.5f, handle.Opacity); } + + [Fact] + public void WindowUnregistered_DetachesSubscription_AndForgetsFocusedState() + { + // CH6c review NIT: before this fix, the only detach point for a + // handle's DescendantFocusChanged subscription (and its membership in + // _focused) was the CONTROLLER's own Dispose — a window unregistered + // while it held focus stayed subscribed and pinned in _focused for + // the rest of the session. Prove the RetailWindowManager.WindowUnregistered + // wiring actually detaches: a stray post-unregister notification (the + // kind a lingering external reference to the handle could still fire) + // must not reach the controller anymore. + UiRoot root = NewRoot(); + (RetailWindowHandle handle, UiElement child) = RegisterWindow(root, "Chat"); + var controller = new RetailWindowOpacityController( + root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f); + + root.SetKeyboardFocus(child); + Assert.Equal(1.0f, handle.Opacity); + + root.WindowManager.Unregister("Chat"); + + // Unregister hides the outer frame, which drops keyboard focus off + // the now-invisible child — the manager's (still-live at that point) + // focus-change plumbing reapplies DefaultOpacity naturally. Expected + // either way; not itself the thing this test pins. + Assert.Equal(0.5f, handle.Opacity); + + controller.SetActiveOpacity(0.7f); + + // A stray post-unregister focus-gain notification (the kind a + // lingering external reference to the handle could still fire). If + // the controller were STILL subscribed, this would re-add the stale + // handle to _focused and apply the NEW active opacity (0.7). + handle.NotifyDescendantFocusChanged(child); + + Assert.Equal(0.5f, handle.Opacity); + } + + [Fact] + public void SetMutators_AfterDispose_AreNoOps() + { + UiRoot root = NewRoot(); + (RetailWindowHandle handle, _) = RegisterWindow(root, "Chat"); + var controller = new RetailWindowOpacityController( + root.WindowManager, defaultOpacity: 0.5f, activeOpacity: 1.0f); + + controller.Dispose(); + controller.SetDefaultOpacity(0.9f); + controller.SetActiveOpacity(0.9f); + controller.SetOpacity(0.2f, 0.3f); + + // None of the three post-Dispose calls changed anything — no + // ObjectDisposedException either, matching Dispose's own idempotent + // shape. + Assert.Equal(0.5f, controller.DefaultOpacity); + Assert.Equal(1.0f, controller.ActiveOpacity); + Assert.Equal(0.5f, handle.Opacity); + } } diff --git a/tests/AcDream.App.Tests/UI/UiRenderContextAlphaTests.cs b/tests/AcDream.App.Tests/UI/UiRenderContextAlphaTests.cs index 3d31cdc3..f152b8da 100644 --- a/tests/AcDream.App.Tests/UI/UiRenderContextAlphaTests.cs +++ b/tests/AcDream.App.Tests/UI/UiRenderContextAlphaTests.cs @@ -5,6 +5,7 @@ using AcDream.App.Rendering.Gpu; using AcDream.App.Tests.Rendering.Gpu; using AcDream.App.UI; using DatReaderWriter.Types; +using Xunit.Sdk; namespace AcDream.App.Tests.UI; @@ -62,6 +63,43 @@ public sealed class UiRenderContextAlphaTests }, }); + /// Same shape as but with a non-zero + /// background (outline) atlas, so 's + /// outline: true branch actually emits its background sprite pass. + private static UiDatFont BuildOutlinedFont() => new( + fgTex: 1, fgW: 64, fgH: 64, + bgTex: 2, bgW: 64, bgH: 64, + lineHeight: 16f, baselineOffset: 12f, + glyphs: new Dictionary + { + ['A'] = new FontCharDesc + { + Unicode = 'A', + Width = 8, + Height = 16, + OffsetX = 0, + OffsetY = 0, + HorizontalOffsetBefore = 0, + HorizontalOffsetAfter = 0, + VerticalOffsetBefore = 0, + }, + }); + + /// Bakes a real from a system TTF so + /// (the BitmapFont path, distinct + /// from the dat-font above) can + /// be exercised end-to-end. Skips rather than fails on a machine with none + /// of 's well-known + /// paths — matches the skip pattern other environment-dependent tests in + /// this suite already use (e.g. RetailSelectionAssetTests). + private static BitmapFont BuildBitmapFontOrSkip(IGpuDevice device) + { + byte[]? ttf = BitmapFont.TryLoadSystemMonospaceFont(); + if (ttf is null) + throw SkipException.ForSkip("No system TTF font found for BitmapFont construction."); + return new BitmapFont(device, ttf, pixelHeight: 16f); + } + // -- DrawSprite: full-opacity identity --------------------------------- [Fact] @@ -172,4 +210,68 @@ public sealed class UiRenderContextAlphaTests var seg = Assert.Single(renderer.DebugSpriteSegments); Assert.Equal(0.5f, seg.Alpha); } + + [Fact] + public void HalfOpacityWindow_MultipliesDatFontOutlineAndForegroundPassAlpha() + { + // CH6c review NIT: BuildFont() above has bgTex == 0, so every existing + // DrawStringDat alpha test only ever exercised the foreground (fill) + // sprite pass. The background (outline) pass is a SEPARATE + // DrawSpriteAbsolute(applyAlpha: true) call site — this pins it too, + // with a font whose background atlas is actually present (bgTex != 0) + // and outline: true so both passes fire. + (TextRenderer renderer, UiRenderContext ctx) = Build(); + UiDatFont font = BuildOutlinedFont(); + + ctx.PushAlpha(0.5f); + ctx.DrawStringDat(font, "A", 0, 0, new Vector4(1f, 1f, 1f, 1f), outline: true); + ctx.PopAlpha(); + + // Background pass (texture 2) submitted first, then foreground (texture 1) — + // both routes guarded by the same ApplyAlpha chokepoint. + Assert.Equal(2, renderer.DebugSpriteSegments.Count); + foreach (var seg in renderer.DebugSpriteSegments) + Assert.Equal(0.5f, seg.Alpha); + } + + // -- DrawString (BitmapFont path): the same alpha chokepoint, guarded --- + + [Fact] + public void FullOpacity_DrawString_BitmapFontPath_MatchesRequestedAlpha_Identity() + { + // CH6c review NIT: DrawStringDat (retail dat-font glyphs) had its own + // alpha regression tests above; UiRenderContext.DrawString — the + // BitmapFont path used for D.6 world-space HUD text — had none. Both + // route through the SAME private ApplyAlpha, but nothing pinned it for + // this path specifically. + var device = new RecordingGpuDevice(); + using BitmapFont font = BuildBitmapFontOrSkip(device); + var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused"); + renderer.Begin(new Vector2(800f, 600f)); + var ctx = new UiRenderContext(renderer, new Vector2(800f, 600f)); + + ctx.DrawString("A", 0, 0, new Vector4(1f, 1f, 1f, 1f), font); + + (int vertexCount, float alpha) = renderer.DebugTextBuffer; + Assert.True(vertexCount > 0); + Assert.Equal(1f, alpha); + } + + [Fact] + public void HalfOpacityWindow_MultipliesBitmapFontGlyphAlpha() + { + var device = new RecordingGpuDevice(); + using BitmapFont font = BuildBitmapFontOrSkip(device); + var renderer = new TextRenderer(device, new NullGpuFrameSource(), "unused"); + renderer.Begin(new Vector2(800f, 600f)); + var ctx = new UiRenderContext(renderer, new Vector2(800f, 600f)); + + ctx.PushAlpha(0.5f); + ctx.DrawString("A", 0, 0, new Vector4(1f, 1f, 1f, 1f), font); + ctx.PopAlpha(); + + (int vertexCount, float alpha) = renderer.DebugTextBuffer; + Assert.True(vertexCount > 0); + Assert.Equal(0.5f, alpha); + } } diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/ChatSettingsTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/ChatSettingsTests.cs index 2d825f9b..53ab26b8 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/ChatSettingsTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/ChatSettingsTests.cs @@ -28,9 +28,18 @@ public sealed class ChatSettingsTests // Campaign CH slice CH6c: retail's base ChatInterface constructor // (0x004F4550) sets DefaultOpacity=0.5/ActiveOpacity=1.0 — the value // every gmFloatyChatUI (the four floating windows) keeps unmodified. - // acdream ships that pair as ONE shared global default (register row - // AP-190), rather than gmMainChatUI's own 1.0/1.0 override. - Assert.Equal(0.5f, d.DefaultOpacity); + // CH6c review fix (2026-08-10): shipping the base pair as acdream's + // ONE shared global default faded every registered window (radar, + // vitals, toolbar, main chat, ...) to 50% opacity out of the box, + // including several that can never take keyboard focus and so were + // permanently half-transparent. acdream now ships gmMainChatUI's own + // 1.0/1.0 override (0x004CD0F0) as the shared global default instead + // — retail-identical opaque presentation for the 11 non-chat windows + // and the main chat window; only the four floating chat windows + // diverge from retail's 0.5-while-idle fade, and the Settings → Chat + // transparency slider remains fully user-settable (register row + // AP-190). + Assert.Equal(1.0f, d.DefaultOpacity); Assert.Equal(1.0f, d.ActiveOpacity); } diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsPanelTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsPanelTests.cs index 2ed12948..60243e3a 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsPanelTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsPanelTests.cs @@ -523,7 +523,8 @@ public sealed class SettingsPanelTests // LAST-rendered opacity slider ("Active", rendered after "Background") // determines the final draft. That exercises ChatOpacityLink.SetActive's // drag-background-down path end-to-end through the real panel code, - // starting from ChatSettings.Default (DefaultOpacity=0.5, ActiveOpacity=1.0). + // starting from ChatSettings.Default (DefaultOpacity=1.0, ActiveOpacity=1.0 + // as of the CH6c review fix). var (panel, vm, _, _) = Build(); var r = new FakePanelRenderer { diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsStoreTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsStoreTests.cs index 2667aca6..8fba1869 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsStoreTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsStoreTests.cs @@ -333,12 +333,17 @@ public sealed class SettingsStoreTests : System.IDisposable // -- Campaign CH slice CH6c: window opacity round-trip ----------------- [Fact] - public void LoadChat_returns_retail_ChatInterface_opacity_defaults_when_file_is_missing() + public void LoadChat_returns_acdream_opaque_opacity_defaults_when_file_is_missing() { + // CH6c review fix (2026-08-10): the pre-fix default was retail's base + // ChatInterface value (0.5/1.0), which faded every registered window + // to 50% opacity out of the box. acdream now ships gmMainChatUI's own + // 1.0/1.0 override (0x004CD0F0) as the shared global default — see + // ChatSettings.DefaultOpacity and register row AP-190. var store = new SettingsStore(_tempPath); ChatSettings loaded = store.LoadChat(); - Assert.Equal(0.5f, loaded.DefaultOpacity); + Assert.Equal(1.0f, loaded.DefaultOpacity); Assert.Equal(1.0f, loaded.ActiveOpacity); }