feat(ui): mark store-only option rows dimmed (user-directed, gate 2)
User directive (gate 2, verbatim): "mark all options that are not implemented now, so I can clearly see what is not implemented." Store-only rows keep full interactivity (still persist/send) but render their caption in a shared dimmed grey (UiRenderContext.StoreOnlyCaptionColor, matching the existing UiMenu.TextColorGhosted convention) instead of white/DAT color. No invented marker text anywhere -- the dim IS the marker. Config tab (ConfigOptionsPageController, 21 of 27 rows dimmed): Sound Features menu, Interface Sound trio, Play Sound Only When Active (AP-199); Screen Brightness, Automatic Degrades, Graphics Performance, Degrade Distance, the four Rendering Quality menus, Building Detail Textures, Multi-Pass Alpha (AP-198); Camera Stiffness, Camera Adjustment Speed, Align To Slope, Mouse Look Sensitivity, Invert Mouselook Y Axis, Use Mouse Turning (TS-74); Chat Font Face/Size (AP-200). NOT dimmed: Sound/Ambient trios, Resolution, Full Screen (LIVE), VSync and Field of View (NEXT-LAUNCH -- still implemented, just deferred to next process start, per the controller's own doc). Character tab (CharacterOptionsPageController, 35 of 50 rows dimmed): every Group A (wire+store only) and Group D (deferred) row, plus the Group B rows the OP4 gate script's own step 16 confirms are unbound (ShowTooltips, SideBySideVitals, SpellDuration, AdvancedCombatUI, StayInChatMode, DisableMostWeatherEffects, PersistentAtDay, FilterLanguage, MainPackPreferred). NOT dimmed (15 rows): the six ListenTo*Chat ids (TurbineChatMembershipGate), DisableDistanceFog/ DisplayTimeStamps/ToggleRun (bound at GameWindow.cs), the Group-C re-point (ViewCombatTarget/VividTargetingIndicator/CoordinatesOnRadar/ AutoTarget/AutoRepeatAttack), and DragItemOnPlayerOpensSecureTrade (TS-48). Cross-checked against actual shipped consumers via source grep, not just the research doc's Group table, since OP4 only wired a subset of the doc's aspirational Group B. Configure Keyboard (KeyboardConfigController): a row whose RetailActionIdentityTable lookup fails (MappedAction null -- AP-203's Emote/CharacterSettings set) dims its synthesized caption; the key buttons stay fully bindable/persisted/conflict-checked. Chat tab (ChatOptionsPageController): audited, zero store-only rows -- every filter block and both opacity sliders already have a live consumer (ChatWindowState / RetailWindowOpacityController). Ambiguity flagged, not guessed: the character-options-map.md research doc lists AcceptLootPermits in BOTH Group A and Group C; its only code site (LiveSessionRuntimeFactory.cs, the /consent command) is a second setter for the same server bit, not a behavioral reader, so it is classified Group A / dimmed here. Register: AD-78 documents the convention (retail dims nothing; this is a deliberate acdream-only divergence that retires as consumers land). New per-surface conformance tests pin the exact dimmed/live set against a literal expected list, so wiring a future consumer without also flipping its row's literal fails the build: CharacterOptionsPageControllerTests.StoreOnlyRows_MatchTheDerivationTableExactly + Bind_AppliesDimmedCaptionColor_ForStoreOnlyRows_AndWhiteForLiveRows, ConfigOptionsPageControllerTests.CaptionDimming_MatchesTheStoreOnlySetExactly, KeyboardConfigControllerTests.UnmappedRows_DimTheirCaption_MappedRowsStayWhite. Build green; full Release suite 13,086 passed / 4 skipped / 0 failed (baseline 13,082/4/0 -- delta is exactly the four new tests above). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8bd7e3b88d
commit
3441a71833
10 changed files with 563 additions and 93 deletions
|
|
@ -62,7 +62,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
|
||||
---
|
||||
|
||||
## 2. Adaptation (AD) — 57 active rows (AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice)
|
||||
## 2. Adaptation (AD) — 58 active rows (AD-78 filed 2026-08-11 at Campaign OP's gate-2 follow-up (user-directed, verbatim "mark all options that are not implemented now, so I can clearly see what is not implemented") — the shared store-only-caption-dimming convention across the Character/Config option tabs and Configure Keyboard; AD-77 filed 2026-08-11 at the Campaign OP OP3 review-fix round — the client-wide floating-only `gmPanelUI` host divergence (retail also exposes a docked `0x21000017` host) the plan's §5 delegated to the OP3 dual review, scoped to every main panel not just Options; AD-76/AD-75/AD-74 filed 2026-08-11 at Campaign OP slice OP3 — the Options panel's Exit to Character Selection "behaves as Exit Game" adaptation (D6), the Urgent Assistance/Report Abuse dead-URL interface-text short-circuit (D5), and In-Game Help Files' asset-missing inert button (D5); AD-73 filed 2026-08-11 at the Campaign OP OP2 rework — `UiTabPanel`'s dormant-until-`ActivateTabBehavior()` activation model, replacing retail's unconditional per-instance tab-table wiring, so the four already-shipped Type-8 hosts keep their existing controller-owned switching without a double-driver race; AD-72 filed 2026-08-08 at the Slice 5.3 review corrections — `VendorPricing`'s double-precision narrowing versus retail's x87 extended precision, same class as AD-33; AD-65 RETIRED and AD-69 FILED 2026-08-07 at Campaign S S4 — the away-arm now snaps per retail @0x00509c50, while AD-66's byte-confirmed sibling landing is WITHHELD pending #341's measurement-anomaly apparatus, and AD-69 records the seam-frame dist gap the same pass discovered; AD-56 RESTORED 2026-08-07 — the a8a7d64b revert had collaterally DELETED it, the inverse of the AD-55 zombie it also created; its plumb-fall-freeze condition is live again since TS-4’s real retirement at Slice 2B; AD-55 RE-RETIRED 2026-08-07 — its 2026-07-30 retirement at 252e8068 was collaterally resurrected by the a8a7d64b revert of the unrelated TS-4 commit; the code kept the cos(10°) fix throughout; AD-68 filed 2026-08-07 at the #338 closure — the async-residency placeholder mover shape (0.4/0.4 steps + capsule) has no retail counterpart because retail loads synchronously; AD-67 filed 2026-08-07 at the #32 closeout — the narrowed `SetContactPlane` keeps its per-write `ContactPlaneCellId`, which retail writes only at `init_contact_plane`; AD-49 filed 2026-08-06 at the #334 fix — the BSP part-array flood runs its outdoor cell rectangle at seed time rather than only from retail’s residency-gated walk, keeping both registration floods on one residency rule; AD-64 filed 2026-08-05 at the C5b architecture review's D1 fix — AD-60's W2 wire-cell REACHABILITY decision is expressed once per host because the two hosts run parallel non-shared inbound routes; the committed VALUE is single-sourced at `RuntimeEntityObjectLifetime.CommitWireCellRebucket`, and unification is filed as #324; AD-60 CORRECTED the same day — its surviving-channel enumeration presented "the local force path, the missile arm" as exhaustive when the entire no-window host belonged in it; AD-1 RETIRED 2026-08-05, C5a deletion sweep — the legacy outdoor demote/restore lift this row described was `PhysicsEngine.Resolve`'s own body, deleted with zero production callers; AD-42 DELETED 2026-08-04, C4 route 3 — its last surviving citation, the headless portal-arrival resync's two-call Resolve/ResolvePlacement split, was retired by the canonical `RuntimeAcceptedPositionDriveController` portal arm; AD-2 amended same route with the deferred-place timing adaptation, the T8 tolerated-overwrite note, and the leash-anchor nuance; AD-63 filed 2026-08-04, cancelled-park presentation rollback — the rollback restores every presentation registration the park's Withdraw removed EXCEPT the player's selection, which is user intent rather than a projection; AD-62 filed 2026-08-03, C4 route 2 round 2 — a deferred ForcePosition retired without committing is not re-applied and its ack is not sent; AD-61 filed 2026-08-02, C3c review round 1 — the #270 settle compression now covers the local player; AD-59/AD-60 filed 2026-08-02, continuation-executor slice)
|
||||
|
||||
Recent retirements: AD-3/AD-4 retired 2026-07-31 by exact active/per-candidate
|
||||
visible-cell availability, full-catalog containment-root validation, and the
|
||||
|
|
@ -172,6 +172,7 @@ readiness/requeue adaptation. See
|
|||
| AD-75 | **Filed 2026-08-11 at Campaign OP slice OP3 (D5).** Urgent Assistance (`0x10000206`) and Report Abuse (`0x10000207`) never call `ShellExecuteA` against `http://support.turbine.com/ics/support/ticketnewwizard.asp?style=classic` — the endpoint is dead in 2026. Each button instead ALWAYS emits its own byte-verified retail failure body (the `ShellExecuteA`-failure `MessageBoxA` text, `(Error code %d)` dropped since no real Win32 error ever occurs, the URL kept verbatim) through the interface-text seam (`RetailLogTextType.ClientLocal`) instead of a native `MessageBoxA` popup. | `src/AcDream.Core/Chat/OptionsPanelText.cs` (`UrgentAssistanceUnavailable`/`ReportAbuseUnavailable`); `src/AcDream.App/UI/Layout/OptionsPanelController.cs` (button wiring) | The URL genuinely does not resolve to a live Turbine support endpoint; attempting `ShellExecuteA` would open a browser to a dead page rather than usefully fail. The retained failure TEXT is retail's own (byte-verified), just always shown instead of conditionally on a real launch failure, and routed to acdream's existing interface-text channel rather than a modal OS dialog (retail's own EoR-era mechanism has no acdream analogue for a one-off native `MessageBoxA`). | If Turbine ever revives the endpoint, both buttons would still short-circuit instead of opening it — a silent staleness, not a crash. | `gmGameplayOptionsUI::ListenToElementMessage @0x0049E110`; `ShellExecuteA` call sites `0x0049E154`/`0x0049E1F0`; research doc `2026-08-10-keyboard-config-and-gameplay-tab.md` §4.1/§4.2 |
|
||||
| AD-76 | **Filed 2026-08-11 at Campaign OP slice OP3 (D5).** In-Game Help Files (`0x10000205`) is authored and clickable but has no handler — clicking it does nothing visible. | `src/AcDream.App/UI/Layout/OptionsPanelController.cs` (button wiring — no callback bound) | Retail's own `KeyStone::OpenHelp` loads a third-party embedded help viewer (`plugins\ACHelpPlugin.dll` via `keystone.dll`) that acdream does not have and cannot port (no DAT-resident help content, no source). Retail ITSELF fails silently with the plugin absent (`KeyStone::m_fnAC2HelpPluginExecute` unresolved) — mirroring that as an inert button is the faithful behavior for "the asset is missing", not an invented stub screen. | A user clicking In-Game Help Files gets no feedback at all, same as retail with the plugin missing — indistinguishable from a dead button unless they already expect the asset-missing case. | `KeyStone::OpenHelp @0x00557010`; `KeyStone::Init @0x00556CF0` (the unresolved plugin function pointer); research doc `2026-08-10-keyboard-config-and-gameplay-tab.md` §4.5 |
|
||||
| AD-77 | **Filed 2026-08-11 at the Campaign OP OP3 review-fix round (dual-review S4/MUST-FIX 2 — the plan's §5 "out of scope" list explicitly delegated this ruling to the OP3 review).** Retail exposes TWO `gmPanelUI` host variants for the same panel stack — a floating host (`0x2100006E`, `gmFloatyPanelUI`) and a docked host (`0x21000017`) — so a retail user can dock the Options panel (and every other `gmPanelUI` sibling) into a fixed screen position instead of leaving it freely floating. acdream mounts every main panel through `RetailWindowFrame.Mount` + `RetailPanelUiController.RegisterMainPanel` against the floating host ONLY; no code path resolves or mounts `0x21000017` at all. | `src/AcDream.App/UI/RetailUiRuntime.cs` (every `Mount*`/`RegisterMainPanel` call site for a `gmPanelUI` sibling — Character/Inventory/Spellbook/Effects/the four indicator-detail panels/Options); `src/AcDream.App/UI/Layout/RetailWindowFrame.cs` | This predates OP3 — every `gmPanelUI` sibling has shipped floating-only since its own slice landed; OP3 did not introduce the gap, it just added a tenth panel to an already-floating-only cohort. The plan explicitly scoped filing the row to "whichever slice's review deems it a divergence" rather than blocking any one panel's slice on building a docked-host variant no prior panel has either. | A user who expects to dock the Options panel (or any other main panel) the way retail allows cannot — every `gmPanelUI` sibling is floating-only in acdream, client-wide, not an Options-specific gap. | research doc `2026-08-10-options-panel-structure.md` §10.1 (docked/floating host pair); `docs/plans/2026-08-10-options-panel-campaign.md` §5 |
|
||||
| AD-78 | **Filed 2026-08-11, user-directed (verbatim: "mark all options that are not implemented now, so I can clearly see what is not implemented"), gate 2 of Campaign OP's follow-up.** Retail dims nothing on any Options-panel row or Configure-Keyboard action row — every retail row drives its own real consumer by construction, so retail has no "does this actually do anything" ambiguity to signal. acdream, by contrast, ships a large honest store-only set (AP-198/AP-199/AP-200/AP-203, TS-73/TS-74/TS-75/TS-76/TS-77/TS-78/TS-79/TS-80, and the Character-tab Group A/D rows) that persist and, where auto-save, send the wire bit, but drive nothing observable client-side. Per explicit user direction, every such row's CAPTION now renders in a shared neutral grey (`UiRenderContext.StoreOnlyCaptionColor`, `(0.5,0.5,0.5,1)` — the SAME value the existing disabled/ghosted convention already used, `UiMenu.TextColorGhosted`) instead of its normal white/DAT-authored color, while the row itself stays fully interactive (click/drag/persist exactly as before — only the caption's paint color changes). No invented marker text is added anywhere (the project's "no user-visible strings outside the DAT" rule stands); the dim IS the marker. | `src/AcDream.App/UI/UiRenderContext.cs` (`StoreOnlyCaptionColor`, the one shared constant); `src/AcDream.App/UI/Layout/ConfigOptionsPageController.cs` (21 of 27 rows dimmed — `ApplyLabelAndTooltip`/`SetLabelText`'s `storeOnly` parameter, threaded from each `BindXxxSection` call site); `src/AcDream.App/UI/Layout/CharacterOptionsPageController.cs` (35 of 50 rows dimmed — `RowSpec.StoreOnly`, derived per-row in the class doc's table, cross-checked against actual shipped consumers rather than the research doc alone); `src/AcDream.App/UI/Layout/KeyboardConfigController.cs` (`BuildActionRow` dims a row when `RetailActionIdentityTable.TryResolve` fails, i.e. `MappedAction` is null — AP-203's set); `src/AcDream.App/UI/Layout/ChatOptionsPageController.cs` (audited, zero dimmed rows — every row already has a live consumer). | Explicit, unambiguous user direction (this session, gate 2) overriding the earlier per-slice register rows' silence on presentation; the four controllers' own conformance tests (`ConfigOptionsPageControllerTests.CaptionDimming_MatchesTheStoreOnlySetExactly`, `CharacterOptionsPageControllerTests.StoreOnlyRows_MatchTheDerivationTableExactly` + `Bind_AppliesDimmedCaptionColor_ForStoreOnlyRows_AndWhiteForLiveRows`, `KeyboardConfigControllerTests.UnmappedRows_DimTheirCaption_MappedRowsStayWhite`) pin the exact dimmed set so a future consumer landing without also flipping its row's literal fails the build, not just the eye. | A reviewer comparing a byte-exact retail screenshot to acdream will see caption colors retail never has — this row exists precisely so that divergence is understood as intentional, not a bug. If a row's dim/live classification in the four cited tables ever drifts from its ACTUAL consumer state (a landed consumer whose row was never un-dimmed, or a regressed consumer whose row was never re-dimmed), the caption becomes misleading in the OPPOSITE direction it was built to prevent — treat any report of "this dimmed row visibly does something" or "this live-looking row does nothing" as a real defect, not a rendering nit (see the gate script's own note). This row retires only when acdream reaches full retail parity (zero store-only rows remaining), at which point the convention itself — not just its content — should be deleted. | None (acdream-only divergence; retail has no store-only rows to compare against) — `docs/research/2026-08-10-character-options-map.md` §7.1 (Group A/B/C/D split); `docs/research/2026-08-11-campaign-op-test-script.md` (per-tab store-only enumerations this row's dimmed set matches) |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,18 @@ Re-gate note for the previously-blank tabs: #372's viewport fix landed
|
|||
`[options] gameplay button 0x... clicked` — a silent click in that log IS
|
||||
evidence, report it per-button.
|
||||
|
||||
**Caption dimming (AD-78, user-directed, gate 2).** As of this session, every
|
||||
row this script marks as a store-only row (see each section's own "Store-only
|
||||
rows" subsection, or the register row AD-78 for the complete four-controller
|
||||
enumeration) renders its caption in a dimmed neutral grey instead of the
|
||||
normal white/DAT-authored color — the Character tab, the Config tab, and
|
||||
Configure Keyboard's unmapped rows. A dimmed caption = stored (and
|
||||
wire-synced where the row is auto-save) but drives nothing yet — this is
|
||||
CONTRACTED, not a bug; do not report it. What IS worth reporting: a row this
|
||||
script or AD-78 lists as LIVE that renders dimmed (a wiring regression), or a
|
||||
row listed as store-only/dimmed that visibly DOES something (its "no
|
||||
consumer" premise going stale without its caption catching up).
|
||||
|
||||
This document is the script the user runs against the live connected
|
||||
client (`ACDREAM_LIVE=1` against the local ACE server) to accept each
|
||||
slice. Each item states what to do and what retail-faithful behavior to
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
|
@ -82,95 +83,160 @@ public static class CharacterOptionsPageController
|
|||
|
||||
private const uint StringTableId = 0x23000003u;
|
||||
|
||||
/// <summary>One authored Character-tab row: the wire/storage id plus
|
||||
/// retail's OWN <c>PlayerOption</c> enum member name (for the
|
||||
/// <summary>One authored Character-tab row: the wire/storage id, retail's
|
||||
/// OWN <c>PlayerOption</c> enum member name (for the
|
||||
/// <c>ID_PlayerOption_<Name></c>/<c>_Help</c> string hash — see
|
||||
/// the U2 note above).</summary>
|
||||
public readonly record struct RowSpec(CharacterOptionId Id, string RetailName);
|
||||
/// the U2 note above), and <see cref="StoreOnly"/> (AD-78,
|
||||
/// user-directed, 2026-08-11, gate 2 — see the derivation table below
|
||||
/// this record).</summary>
|
||||
public readonly record struct RowSpec(CharacterOptionId Id, string RetailName, bool StoreOnly);
|
||||
|
||||
/// <summary>One authored header group: its section string key plus
|
||||
/// authored-order rows.</summary>
|
||||
public readonly record struct GroupSpec(string HeaderKey, RowSpec[] Rows);
|
||||
|
||||
/// <summary>
|
||||
/// AD-78 caption-dimming derivation (user-directed, 2026-08-11, gate 2).
|
||||
/// <see cref="RowSpec.StoreOnly"/> below is derived from
|
||||
/// <c>docs/research/2026-08-10-character-options-map.md</c> §2/§7.1's
|
||||
/// four-group split (A = wire+store only, no client consumer needed;
|
||||
/// B = needs a client consumer; C = already live, re-pointed to the
|
||||
/// server bit; D = deferred with a register row), CROSS-CHECKED against
|
||||
/// the actual shipped code (a source grep for every
|
||||
/// <c>CharacterOptionId.*</c> reference outside
|
||||
/// <c>CharacterOptionTable</c>/this controller/the wire codec) because
|
||||
/// the research doc predates OP1/OP4's implementation and Group B was
|
||||
/// only PARTIALLY wired (confirmed against
|
||||
/// <c>docs/research/2026-08-11-campaign-op-test-script.md</c> §OP4
|
||||
/// steps 13-18, written after the code landed):
|
||||
///
|
||||
/// <list type="bullet">
|
||||
/// <item><description><b>NOT dimmed (15 rows, real acdream-side
|
||||
/// consumer):</b> Group B rows actually bound —
|
||||
/// <c>ViewCombatTarget</c>/<c>AutoTarget</c>/<c>AutoRepeatAttack</c>
|
||||
/// (combat: <c>LiveCombatAttackOperations.cs</c>,
|
||||
/// <c>CombatUiController.cs</c> — Group C re-point),
|
||||
/// <c>VividTargetingIndicator</c> (<c>InteractionRetainedUiComposition.cs:696</c>
|
||||
/// — Group C), <c>CoordinatesOnRadar</c>
|
||||
/// (<c>LivePresentationComposition.cs:845</c> — Group C),
|
||||
/// <c>DisableDistanceFog</c>/<c>DisplayTimeStamps</c>/<c>ToggleRun</c>
|
||||
/// (Group B, polled at <c>GameWindow.cs:655-680</c>),
|
||||
/// <c>DragItemOnPlayerOpensSecureTrade</c> (TS-48,
|
||||
/// <c>InteractionRetainedUiComposition.cs:326</c>), and all six
|
||||
/// <c>ListenTo*Chat</c> ids (<c>TurbineChatMembershipGate.cs:105-136</c>
|
||||
/// gates every Turbine room join on the matching bit).</description></item>
|
||||
/// <item><description><b>Dimmed (35 rows, store-only):</b> every
|
||||
/// remaining Group A row (wire+store only — ACE, not acdream, is the
|
||||
/// consumer) and every remaining Group B row the OP4 gate script's own
|
||||
/// step 16 lists as "no consumer surface" (<c>ShowTooltips</c>,
|
||||
/// <c>SideBySideVitals</c>, <c>SpellDuration</c>,
|
||||
/// <c>AdvancedCombatUI</c>, <c>StayInChatMode</c>,
|
||||
/// <c>DisableMostWeatherEffects</c>, <c>PersistentAtDay</c>,
|
||||
/// <c>FilterLanguage</c>, <c>MainPackPreferred</c>), plus every Group D
|
||||
/// deferral (<c>SalvageMultiple</c>,
|
||||
/// <c>DisableHouseRestrictionEffects</c>, <c>FellowshipShareXP</c>,
|
||||
/// <c>HearPkDeathMessages</c>).</description></item>
|
||||
/// <item><description><b>Flagged ambiguity, resolved by code evidence
|
||||
/// (see final report, not re-litigated here):</b> the research doc's
|
||||
/// §7.1 lists <c>AcceptLootPermits</c> in BOTH Group A's bullet list AND
|
||||
/// Group C's bullet list. The actual consumer
|
||||
/// (<c>LiveSessionRuntimeFactory.cs:497-502</c>, the <c>/consent</c>
|
||||
/// slash command) is a second SETTER surface for the same server bit,
|
||||
/// not a behavioral reader — ACE alone decides whether to honor corpse
|
||||
/// looting (<c>Player_Death.cs:755</c>). Classified Group A / dimmed.
|
||||
/// <c>IgnoreFellowshipRequests</c>/<c>FellowshipAutoAcceptRequests</c>
|
||||
/// have a genuine coded mutual-exclusion side effect
|
||||
/// (<c>RuntimeCharacterState.TrySetOption</c>, MF-2) that the research
|
||||
/// doc's Group A classification does not capture (the doc predates
|
||||
/// OP1's review-fix round) — kept dimmed per the doc's explicit Group A
|
||||
/// listing and this task's instruction to treat the doc as the sole
|
||||
/// authority rather than infer from code comments; a future un-dim is a
|
||||
/// one-line judgment call, not a re-investigation.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
private const bool Live = false;
|
||||
private const bool StoreOnly = true;
|
||||
|
||||
/// <summary>
|
||||
/// The complete 6-group / 50-row authored table — research doc §2 /
|
||||
/// §7, id-for-id, in EXACT authored order. <c>RetailName</c> values
|
||||
/// verified against <c>acclient.h:4162-4218</c>'s literal enumerator
|
||||
/// spelling.
|
||||
/// spelling. See the derivation table above <see cref="Groups"/> for the
|
||||
/// <c>StoreOnly</c> column's sourcing.
|
||||
/// </summary>
|
||||
public static readonly GroupSpec[] Groups =
|
||||
{
|
||||
new("ID_CharacterOption_UIBehavior_Section", new RowSpec[]
|
||||
{
|
||||
new(CharacterOptionId.ViewCombatTarget, "ViewCombatTarget"),
|
||||
new(CharacterOptionId.SalvageMultiple, "SalvageMultiple"),
|
||||
new(CharacterOptionId.MainPackPreferred, "MainPackPreferred"),
|
||||
new(CharacterOptionId.ViewCombatTarget, "ViewCombatTarget", Live), // Group C
|
||||
new(CharacterOptionId.SalvageMultiple, "SalvageMultiple", StoreOnly), // Group D
|
||||
new(CharacterOptionId.MainPackPreferred, "MainPackPreferred", StoreOnly), // Group B, unbound
|
||||
}),
|
||||
new("ID_CharacterOption_UIDisplay_Section", new RowSpec[]
|
||||
{
|
||||
new(CharacterOptionId.VividTargetingIndicator, "VividTargetingIndicator"),
|
||||
new(CharacterOptionId.ShowTooltips, "ShowTooltips"),
|
||||
new(CharacterOptionId.CoordinatesOnRadar, "CoordinatesOnRadar"),
|
||||
new(CharacterOptionId.SideBySideVitals, "SideBySideVitals"),
|
||||
new(CharacterOptionId.SpellDuration, "SpellDuration"),
|
||||
new(CharacterOptionId.DisableMostWeatherEffects, "DisableMostWeatherEffects"),
|
||||
new(CharacterOptionId.DisableDistanceFog, "DisableDistanceFog"),
|
||||
new(CharacterOptionId.PersistentAtDay, "PersistentAtDay"),
|
||||
new(CharacterOptionId.DisableHouseRestrictionEffects, "DisableHouseRestrictionEffects"),
|
||||
new(CharacterOptionId.UseCraftSuccessDialog, "UseCraftSuccessDialog"),
|
||||
new(CharacterOptionId.ConfirmVolatileRareUse, "ConfirmVolatileRareUse"),
|
||||
new(CharacterOptionId.DisplayTimeStamps, "DisplayTimeStamps"),
|
||||
new(CharacterOptionId.FilterLanguage, "FilterLanguage"),
|
||||
new(CharacterOptionId.ShowHelm, "ShowHelm"),
|
||||
new(CharacterOptionId.ShowCloak, "ShowCloak"),
|
||||
new(CharacterOptionId.VividTargetingIndicator, "VividTargetingIndicator", Live), // Group C
|
||||
new(CharacterOptionId.ShowTooltips, "ShowTooltips", StoreOnly), // Group B, unbound
|
||||
new(CharacterOptionId.CoordinatesOnRadar, "CoordinatesOnRadar", Live), // Group C
|
||||
new(CharacterOptionId.SideBySideVitals, "SideBySideVitals", StoreOnly), // Group B, unbound
|
||||
new(CharacterOptionId.SpellDuration, "SpellDuration", StoreOnly), // Group B, unbound
|
||||
new(CharacterOptionId.DisableMostWeatherEffects, "DisableMostWeatherEffects", StoreOnly), // Group B, unbound
|
||||
new(CharacterOptionId.DisableDistanceFog, "DisableDistanceFog", Live), // Group B, bound (GameWindow.cs:657)
|
||||
new(CharacterOptionId.PersistentAtDay, "PersistentAtDay", StoreOnly), // Group B, unbound
|
||||
new(CharacterOptionId.DisableHouseRestrictionEffects, "DisableHouseRestrictionEffects", StoreOnly), // Group D
|
||||
new(CharacterOptionId.UseCraftSuccessDialog, "UseCraftSuccessDialog", StoreOnly), // Group A
|
||||
new(CharacterOptionId.ConfirmVolatileRareUse, "ConfirmVolatileRareUse", StoreOnly), // Group A
|
||||
new(CharacterOptionId.DisplayTimeStamps, "DisplayTimeStamps", Live), // Group B, bound (GameWindow.cs:664)
|
||||
new(CharacterOptionId.FilterLanguage, "FilterLanguage", StoreOnly), // Group B, unbound
|
||||
new(CharacterOptionId.ShowHelm, "ShowHelm", StoreOnly), // Group A
|
||||
new(CharacterOptionId.ShowCloak, "ShowCloak", StoreOnly), // Group A
|
||||
}),
|
||||
new("ID_CharacterOption_Grouping_Section", new RowSpec[]
|
||||
{
|
||||
new(CharacterOptionId.IgnoreAllegianceRequests, "IgnoreAllegianceRequests"),
|
||||
new(CharacterOptionId.IgnoreFellowshipRequests, "IgnoreFellowshipRequests"),
|
||||
new(CharacterOptionId.DisplayAllegianceLogonNotifications, "DisplayAllegianceLogonNotifications"),
|
||||
new(CharacterOptionId.FellowshipShareXP, "FellowshipShareXP"),
|
||||
new(CharacterOptionId.FellowshipShareLoot, "FellowshipShareLoot"),
|
||||
new(CharacterOptionId.FellowshipAutoAcceptRequests, "FellowshipAutoAcceptRequests"),
|
||||
new(CharacterOptionId.IgnoreAllegianceRequests, "IgnoreAllegianceRequests", StoreOnly), // Group A
|
||||
new(CharacterOptionId.IgnoreFellowshipRequests, "IgnoreFellowshipRequests", StoreOnly), // Group A (see ambiguity note)
|
||||
new(CharacterOptionId.DisplayAllegianceLogonNotifications, "DisplayAllegianceLogonNotifications", StoreOnly), // Group A
|
||||
new(CharacterOptionId.FellowshipShareXP, "FellowshipShareXP", StoreOnly), // Group D
|
||||
new(CharacterOptionId.FellowshipShareLoot, "FellowshipShareLoot", StoreOnly), // Group A
|
||||
new(CharacterOptionId.FellowshipAutoAcceptRequests, "FellowshipAutoAcceptRequests", StoreOnly), // Group A (see ambiguity note)
|
||||
}),
|
||||
new("ID_CharacterOption_OtherPlayers_Section", new RowSpec[]
|
||||
{
|
||||
new(CharacterOptionId.AcceptLootPermits, "AcceptLootPermits"),
|
||||
new(CharacterOptionId.UseDeception, "UseDeception"),
|
||||
new(CharacterOptionId.AllowGive, "AllowGive"),
|
||||
new(CharacterOptionId.IgnoreTradeRequests, "IgnoreTradeRequests"),
|
||||
new(CharacterOptionId.DragItemOnPlayerOpensSecureTrade, "DragItemOnPlayerOpensSecureTrade"),
|
||||
new(CharacterOptionId.DisplayDateOfBirth, "DisplayDateOfBirth"),
|
||||
new(CharacterOptionId.DisplayAge, "DisplayAge"),
|
||||
new(CharacterOptionId.DisplayChessRank, "DisplayChessRank"),
|
||||
new(CharacterOptionId.DisplayFishingSkill, "DisplayFishingSkill"),
|
||||
new(CharacterOptionId.DisplayNumberDeaths, "DisplayNumberDeaths"),
|
||||
new(CharacterOptionId.DisplayNumberCharacterTitles, "DisplayNumberCharacterTitles"),
|
||||
new(CharacterOptionId.AcceptLootPermits, "AcceptLootPermits", StoreOnly), // Group A (see ambiguity note)
|
||||
new(CharacterOptionId.UseDeception, "UseDeception", StoreOnly), // Group A
|
||||
new(CharacterOptionId.AllowGive, "AllowGive", StoreOnly), // Group A
|
||||
new(CharacterOptionId.IgnoreTradeRequests, "IgnoreTradeRequests", StoreOnly), // Group A
|
||||
new(CharacterOptionId.DragItemOnPlayerOpensSecureTrade, "DragItemOnPlayerOpensSecureTrade", Live), // TS-48, live gate
|
||||
new(CharacterOptionId.DisplayDateOfBirth, "DisplayDateOfBirth", StoreOnly), // Group A
|
||||
new(CharacterOptionId.DisplayAge, "DisplayAge", StoreOnly), // Group A
|
||||
new(CharacterOptionId.DisplayChessRank, "DisplayChessRank", StoreOnly), // Group A
|
||||
new(CharacterOptionId.DisplayFishingSkill, "DisplayFishingSkill", StoreOnly), // Group A
|
||||
new(CharacterOptionId.DisplayNumberDeaths, "DisplayNumberDeaths", StoreOnly), // Group A
|
||||
new(CharacterOptionId.DisplayNumberCharacterTitles, "DisplayNumberCharacterTitles", StoreOnly), // Group A
|
||||
}),
|
||||
new("ID_CharacterOption_CharacterBehavior_Section", new RowSpec[]
|
||||
{
|
||||
new(CharacterOptionId.ToggleRun, "ToggleRun"),
|
||||
new(CharacterOptionId.AdvancedCombatUI, "AdvancedCombatUI"),
|
||||
new(CharacterOptionId.AutoTarget, "AutoTarget"),
|
||||
new(CharacterOptionId.AutoRepeatAttack, "AutoRepeatAttack"),
|
||||
new(CharacterOptionId.UseChargeAttack, "UseChargeAttack"),
|
||||
new(CharacterOptionId.LeadMissileTargets, "LeadMissileTargets"),
|
||||
new(CharacterOptionId.UseFastMissiles, "UseFastMissiles"),
|
||||
new(CharacterOptionId.ToggleRun, "ToggleRun", Live), // Group B, bound (GameWindow.cs:680)
|
||||
new(CharacterOptionId.AdvancedCombatUI, "AdvancedCombatUI", StoreOnly), // Group B, unbound
|
||||
new(CharacterOptionId.AutoTarget, "AutoTarget", Live), // Group C
|
||||
new(CharacterOptionId.AutoRepeatAttack, "AutoRepeatAttack", Live), // Group C
|
||||
new(CharacterOptionId.UseChargeAttack, "UseChargeAttack", StoreOnly), // Group A
|
||||
new(CharacterOptionId.LeadMissileTargets, "LeadMissileTargets", StoreOnly), // Group A
|
||||
new(CharacterOptionId.UseFastMissiles, "UseFastMissiles", StoreOnly), // Group A
|
||||
}),
|
||||
new("ID_CharacterOption_Chat_Section", new RowSpec[]
|
||||
{
|
||||
new(CharacterOptionId.StayInChatMode, "StayInChatMode"),
|
||||
new(CharacterOptionId.ListenToAllegianceChat, "HearAllegianceChat"),
|
||||
new(CharacterOptionId.ListenToGeneralChat, "HearGeneralChat"),
|
||||
new(CharacterOptionId.ListenToTradeChat, "HearTradeChat"),
|
||||
new(CharacterOptionId.ListenToLFGChat, "HearLFGChat"),
|
||||
new(CharacterOptionId.ListenToRoleplayChat, "HearRoleplayChat"),
|
||||
new(CharacterOptionId.ListenToSocietyChat, "HearSocietyChat"),
|
||||
new(CharacterOptionId.StayInChatMode, "StayInChatMode", StoreOnly), // Group B, unbound
|
||||
new(CharacterOptionId.ListenToAllegianceChat, "HearAllegianceChat", Live), // TurbineChatMembershipGate.cs:107-110
|
||||
new(CharacterOptionId.ListenToGeneralChat, "HearGeneralChat", Live), // TurbineChatMembershipGate.cs:111-114
|
||||
new(CharacterOptionId.ListenToTradeChat, "HearTradeChat", Live), // TurbineChatMembershipGate.cs:115-118
|
||||
new(CharacterOptionId.ListenToLFGChat, "HearLFGChat", Live), // TurbineChatMembershipGate.cs:119-122
|
||||
new(CharacterOptionId.ListenToRoleplayChat, "HearRoleplayChat", Live), // TurbineChatMembershipGate.cs:123-126
|
||||
new(CharacterOptionId.ListenToSocietyChat, "HearSocietyChat", Live), // TurbineChatMembershipGate.cs:127-130
|
||||
// D3: the 50th row. Not in the 2013 build (register row
|
||||
// AP-193 covers the ACE-sourced id/mask); the DAT string
|
||||
// 0x0D16E9A3 ("Listen to PK death messages.") IS present, so
|
||||
// it renders exactly like every other row — wire+store only.
|
||||
new(CharacterOptionId.HearPkDeathMessages, "HearPKDeaths"),
|
||||
new(CharacterOptionId.HearPkDeathMessages, "HearPKDeaths", StoreOnly), // Group D
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
@ -349,6 +415,13 @@ public static class CharacterOptionsPageController
|
|||
$"[D.2b] CharacterOptionsPageController: label '{labelKey}' did not resolve — "
|
||||
+ "row renders with no caption rather than invented English.");
|
||||
|
||||
// AD-78 (user-directed, 2026-08-11, gate 2): store-only rows keep
|
||||
// full interactivity — only the caption dims, per the derivation
|
||||
// table above Groups.
|
||||
checkbox.LabelColor = spec.StoreOnly
|
||||
? UiRenderContext.StoreOnlyCaptionColor
|
||||
: Vector4.One;
|
||||
|
||||
string? tooltip = resolveString(
|
||||
StringTableId, DatStringResolver.ComputeHash(labelKey + "_Help"));
|
||||
if (tooltip is not null)
|
||||
|
|
|
|||
|
|
@ -47,6 +47,18 @@ namespace AcDream.App.UI.Layout;
|
|||
/// (Campaign CH slice CH6b). <see cref="FilterBlockSpec.CompactWindowId"/> carries the
|
||||
/// mapping so nothing downstream needs to know retail's raw numbering.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>No store-only rows (AD-78 audit, user-directed, 2026-08-11, gate 2).</b>
|
||||
/// Every row on this tab has a live acdream consumer — the two opacity
|
||||
/// sliders drive <c>RetailWindowOpacityController</c> (AP-190) through
|
||||
/// <see cref="Bindings.SetDefaultOpacity"/>/<see cref="Bindings.SetActiveOpacity"/>,
|
||||
/// and all five filter blocks write <see cref="ChatWindowState"/> directly,
|
||||
/// the SAME state CH6's floating chat windows read to decide what to show.
|
||||
/// Unlike the Character/Config tabs and Configure Keyboard, this controller
|
||||
/// has no caption-dimming to apply; verified by inspection rather than
|
||||
/// skipped.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class ChatOptionsPageController
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
|
||||
|
|
@ -130,6 +131,24 @@ namespace AcDream.App.UI.Layout;
|
|||
/// <c>0x004039ed</c>, NOT a per-machine runtime enumeration as the
|
||||
/// rejected slice's comment claimed).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Caption dimming (AD-78, user-directed, 2026-08-11, gate 2).</b> Every
|
||||
/// STORE-ONLY row above (the 21 rows named in the paragraph before this one
|
||||
/// -- AP-198's ten, AP-199's three, and TS-74's six Camera/Input rows plus
|
||||
/// AP-200's two Chat-font rows) renders its caption in
|
||||
/// <see cref="UiRenderContext.StoreOnlyCaptionColor"/> instead of the normal
|
||||
/// white/DAT-authored color. The row stays fully interactive -- it still
|
||||
/// persists -- the dim is ONLY a visual marker so the four-tab panel makes
|
||||
/// "does this actually do anything yet" legible at a glance, per the
|
||||
/// campaign's gate-2 directive. LIVE rows (Sound/Ambient trios, Resolution,
|
||||
/// Full Screen) and NEXT-LAUNCH rows (Sync To Refresh, Field of View -- they
|
||||
/// DO apply, just not until the next process start) are NOT dimmed. Threaded
|
||||
/// through every row builder below as an explicit <c>storeOnly</c> parameter
|
||||
/// at each of the 27 call sites, so wiring a future consumer means flipping
|
||||
/// that literal to <see langword="false"/> -- the matching conformance test
|
||||
/// then fails until the change is made consciously.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class ConfigOptionsPageController
|
||||
{
|
||||
|
|
@ -317,7 +336,8 @@ public static class ConfigOptionsPageController
|
|||
AudioSettings updated = bindings.LoadAudio() with { SoundFeatures = value };
|
||||
bindings.SaveAudio(updated);
|
||||
},
|
||||
defaultValue: 0);
|
||||
defaultValue: 0,
|
||||
storeOnly: true); // AP-199
|
||||
|
||||
// OP6 rework (2026-08-11, review M2): read/apply the ENABLED-sense
|
||||
// fields directly — toggleDefault stays `true` because retail's own
|
||||
|
|
@ -339,7 +359,8 @@ public static class ConfigOptionsPageController
|
|||
toggleRead: () => bindings.LoadAudio().SfxEnabled,
|
||||
toggleApply: value => bindings.SaveAudio(bindings.LoadAudio() with { SfxEnabled = value }),
|
||||
sliderRead: () => bindings.LoadAudio().Sfx,
|
||||
sliderApply: value => bindings.SaveAudio(bindings.LoadAudio() with { Sfx = value }));
|
||||
sliderApply: value => bindings.SaveAudio(bindings.LoadAudio() with { Sfx = value }),
|
||||
storeOnly: false); // LIVE
|
||||
|
||||
BuildTrioRow(
|
||||
listBox, "ID_Sound_DisableAmbientSound", sliderTooltipKey: "ID_Sound_AmbientVolume",
|
||||
|
|
@ -349,7 +370,8 @@ public static class ConfigOptionsPageController
|
|||
toggleRead: () => bindings.LoadAudio().AmbientEnabled,
|
||||
toggleApply: value => bindings.SaveAudio(bindings.LoadAudio() with { AmbientEnabled = value }),
|
||||
sliderRead: () => bindings.LoadAudio().Ambient,
|
||||
sliderApply: value => bindings.SaveAudio(bindings.LoadAudio() with { Ambient = value }));
|
||||
sliderApply: value => bindings.SaveAudio(bindings.LoadAudio() with { Ambient = value }),
|
||||
storeOnly: false); // LIVE
|
||||
|
||||
// Interface Sound: retail's own dead knob (AP-174 — "interface
|
||||
// sounds are scaled by the EFFECT knob"; registered and never
|
||||
|
|
@ -363,12 +385,14 @@ public static class ConfigOptionsPageController
|
|||
toggleRead: () => bindings.LoadAudio().InterfaceEnabled,
|
||||
toggleApply: value => bindings.SaveAudio(bindings.LoadAudio() with { InterfaceEnabled = value }),
|
||||
sliderRead: () => bindings.LoadAudio().InterfaceVolume,
|
||||
sliderApply: value => bindings.SaveAudio(bindings.LoadAudio() with { InterfaceVolume = value }));
|
||||
sliderApply: value => bindings.SaveAudio(bindings.LoadAudio() with { InterfaceVolume = value }),
|
||||
storeOnly: true); // AP-174 / AP-199
|
||||
|
||||
BuildToggleRow(
|
||||
listBox, "ID_Sound_NoFocusNoSound", defaultValue: true, page, resolveString,
|
||||
read: () => bindings.LoadAudio().PlaySoundOnlyWhenActive,
|
||||
apply: value => bindings.SaveAudio(bindings.LoadAudio() with { PlaySoundOnlyWhenActive = value }));
|
||||
apply: value => bindings.SaveAudio(bindings.LoadAudio() with { PlaySoundOnlyWhenActive = value }),
|
||||
storeOnly: true); // AP-199
|
||||
|
||||
audio = bindings.LoadAudio();
|
||||
}
|
||||
|
|
@ -393,6 +417,7 @@ public static class ConfigOptionsPageController
|
|||
min: 0.285714298f, max: 1f, defaultValue: 0.45f, page, resolveString,
|
||||
read: () => bindings.LoadCameraTurning().Stiffness,
|
||||
apply: value => bindings.SaveCameraTurning(bindings.LoadCameraTurning() with { Stiffness = value }),
|
||||
storeOnly: true, // TS-74
|
||||
rangeLowKey: "ID_Graphics_Value_Soft", rangeHighKey: "ID_Graphics_Value_Hard");
|
||||
|
||||
BuildSliderRow(
|
||||
|
|
@ -400,6 +425,7 @@ public static class ConfigOptionsPageController
|
|||
min: 5f, max: 80f, defaultValue: 40.0f, page, resolveString,
|
||||
read: () => bindings.LoadCameraTurning().AdjustmentSpeed,
|
||||
apply: value => bindings.SaveCameraTurning(bindings.LoadCameraTurning() with { AdjustmentSpeed = value }),
|
||||
storeOnly: true, // TS-74
|
||||
rangeLowKey: "ID_Graphics_Value_Slow", rangeHighKey: "ID_Graphics_Value_Fast");
|
||||
|
||||
// Field of View: NEXT-LAUNCH via DisplaySettings.FieldOfView + the
|
||||
|
|
@ -411,12 +437,14 @@ public static class ConfigOptionsPageController
|
|||
min: 10f, max: 160f, defaultValue: 90.0f, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().FieldOfView,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { FieldOfView = value }),
|
||||
storeOnly: false, // NEXT-LAUNCH, not store-only
|
||||
rangeLowKey: "ID_Graphics_Value_Narrow", rangeHighKey: "ID_Graphics_Value_Wide");
|
||||
|
||||
BuildToggleRow(
|
||||
listBox, "ID_Camera_AlignToSlope", defaultValue: true, page, resolveString,
|
||||
read: () => bindings.LoadCameraTurning().AlignToSlope,
|
||||
apply: value => bindings.SaveCameraTurning(bindings.LoadCameraTurning() with { AlignToSlope = value }));
|
||||
apply: value => bindings.SaveCameraTurning(bindings.LoadCameraTurning() with { AlignToSlope = value }),
|
||||
storeOnly: true); // TS-74
|
||||
|
||||
cameraTurning = bindings.LoadCameraTurning();
|
||||
}
|
||||
|
|
@ -455,19 +483,22 @@ public static class ConfigOptionsPageController
|
|||
DisplaySettings.AvailableResolutions, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().Resolution,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { Resolution = value }),
|
||||
defaultValue: "800x600");
|
||||
defaultValue: "800x600",
|
||||
storeOnly: false); // LIVE
|
||||
|
||||
BuildToggleRow(
|
||||
listBox, "ID_Rendering_FullScreen", defaultValue: true, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().Fullscreen,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { Fullscreen = value }));
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { Fullscreen = value }),
|
||||
storeOnly: false); // LIVE
|
||||
|
||||
// Sync To Refresh: NEXT-LAUNCH (same DisplaySettings.VSync
|
||||
// pre-existing precedent as FieldOfView above).
|
||||
BuildToggleRow(
|
||||
listBox, "ID_Rendering_SyncToDisplayRefresh", defaultValue: false, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().VSync,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { VSync = value }));
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { VSync = value }),
|
||||
storeOnly: false); // NEXT-LAUNCH, not store-only
|
||||
|
||||
// Screen Brightness: OP6 rework (2026-08-11, review S2) — its OWN
|
||||
// DisplaySettings.ScreenBrightness field ([-1,1], default 0), NOT
|
||||
|
|
@ -479,18 +510,21 @@ public static class ConfigOptionsPageController
|
|||
min: -1f, max: 1f, defaultValue: 0f, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().ScreenBrightness,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { ScreenBrightness = value }),
|
||||
storeOnly: true, // review S2
|
||||
rangeLowKey: "ID_Graphics_Value_Dark", rangeHighKey: "ID_Graphics_Value_Bright");
|
||||
|
||||
BuildToggleRow(
|
||||
listBox, "ID_Graphics_AdaptiveDegrade", defaultValue: false, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().AutomaticDegrades,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { AutomaticDegrades = value }));
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { AutomaticDegrades = value }),
|
||||
storeOnly: true); // AP-198
|
||||
|
||||
BuildSliderRow(
|
||||
listBox, RangedSliderTemplateIndex, "ID_Graphics_AdaptiveDegradeBias",
|
||||
min: -1f, max: 1f, defaultValue: 0f, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().GraphicsPerformance,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { GraphicsPerformance = value }),
|
||||
storeOnly: true, // AP-198
|
||||
rangeLowKey: "ID_Graphics_Value_Speed", rangeHighKey: "ID_Graphics_Value_Detail");
|
||||
|
||||
BuildSliderRow(
|
||||
|
|
@ -498,6 +532,7 @@ public static class ConfigOptionsPageController
|
|||
min: 0f, max: 100f, defaultValue: 50.0f, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().DegradeDistance,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { DegradeDistance = value }),
|
||||
storeOnly: true, // AP-198
|
||||
rangeLowKey: "ID_Graphics_Value_Close", rangeHighKey: "ID_Graphics_Value_Far");
|
||||
|
||||
display = bindings.LoadDisplay();
|
||||
|
|
@ -539,19 +574,22 @@ public static class ConfigOptionsPageController
|
|||
listBox, "ID_Graphics_LandscapeTextureDetail", TextureDetailChoices, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().LandscapeTextureDetail,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { LandscapeTextureDetail = value }),
|
||||
defaultValue: 2);
|
||||
defaultValue: 2,
|
||||
storeOnly: true); // AP-198
|
||||
|
||||
BuildMenuRow(
|
||||
listBox, "ID_Graphics_EnvironmentTextureDetail", TextureDetailChoices, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().EnvironmentTextureDetail,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { EnvironmentTextureDetail = value }),
|
||||
defaultValue: 1);
|
||||
defaultValue: 1,
|
||||
storeOnly: true); // AP-198
|
||||
|
||||
BuildMenuRow(
|
||||
listBox, "ID_Graphics_TextureFiltering", TextureFilteringChoices, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().TextureFiltering,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { TextureFiltering = value }),
|
||||
defaultValue: 1);
|
||||
defaultValue: 1,
|
||||
storeOnly: true); // AP-198
|
||||
|
||||
// UNRESOLVED (see class doc / register row): retail's own
|
||||
// SetDefaultValue(8) does not index this 6-entry choice array.
|
||||
|
|
@ -561,17 +599,20 @@ public static class ConfigOptionsPageController
|
|||
listBox, "ID_Graphics_LandscapeDrawDistance", LandscapeDrawDistanceChoices, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().LandscapeDrawDistance,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { LandscapeDrawDistance = value }),
|
||||
defaultValue: 8);
|
||||
defaultValue: 8,
|
||||
storeOnly: true); // AP-198
|
||||
|
||||
BuildToggleRow(
|
||||
listBox, "ID_Graphics_BuildingDetailTextures", defaultValue: true, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().BuildingDetailTextures,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { BuildingDetailTextures = value }));
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { BuildingDetailTextures = value }),
|
||||
storeOnly: true); // AP-198
|
||||
|
||||
BuildToggleRow(
|
||||
listBox, "ID_Graphics_MultiPassAlpha", defaultValue: false, page, resolveString,
|
||||
read: () => bindings.LoadDisplay().MultiPassAlpha,
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { MultiPassAlpha = value }));
|
||||
apply: value => bindings.SaveDisplay(bindings.LoadDisplay() with { MultiPassAlpha = value }),
|
||||
storeOnly: true); // AP-198
|
||||
|
||||
display = bindings.LoadDisplay();
|
||||
}
|
||||
|
|
@ -595,12 +636,14 @@ public static class ConfigOptionsPageController
|
|||
listBox, SimpleSliderTemplateIndex, "ID_Input_MouseLookSensitivity",
|
||||
min: 0.00999999978f, max: 1f, defaultValue: 0.55f, page, resolveString,
|
||||
read: () => bindings.LoadCameraTurning().MouseLookSensitivity,
|
||||
apply: value => bindings.SaveCameraTurning(bindings.LoadCameraTurning() with { MouseLookSensitivity = value }));
|
||||
apply: value => bindings.SaveCameraTurning(bindings.LoadCameraTurning() with { MouseLookSensitivity = value }),
|
||||
storeOnly: true); // TS-74
|
||||
|
||||
BuildToggleRow(
|
||||
listBox, "ID_Input_InvertMouseLookYAxis", defaultValue: false, page, resolveString,
|
||||
read: () => bindings.LoadCameraTurning().InvertMouseLookYAxis,
|
||||
apply: value => bindings.SaveCameraTurning(bindings.LoadCameraTurning() with { InvertMouseLookYAxis = value }));
|
||||
apply: value => bindings.SaveCameraTurning(bindings.LoadCameraTurning() with { InvertMouseLookYAxis = value }),
|
||||
storeOnly: true); // TS-74
|
||||
|
||||
// Input_UseMouseTurning: the Config tab's OWN client-local
|
||||
// UIPreference — DISTINCT from the Gameplay tab macro's
|
||||
|
|
@ -609,7 +652,8 @@ public static class ConfigOptionsPageController
|
|||
BuildToggleRow(
|
||||
listBox, "ID_Input_UseMouseTurning", defaultValue: false, page, resolveString,
|
||||
read: () => bindings.LoadCameraTurning().UseMouseTurning,
|
||||
apply: value => bindings.SaveCameraTurning(bindings.LoadCameraTurning() with { UseMouseTurning = value }));
|
||||
apply: value => bindings.SaveCameraTurning(bindings.LoadCameraTurning() with { UseMouseTurning = value }),
|
||||
storeOnly: true); // TS-74
|
||||
|
||||
cameraTurning = bindings.LoadCameraTurning();
|
||||
}
|
||||
|
|
@ -662,13 +706,15 @@ public static class ConfigOptionsPageController
|
|||
listBox, "ID_UI_ChatFontFace", ChatFontFaceChoices, page, resolveString,
|
||||
read: () => bindings.LoadChat().ChatFontFace,
|
||||
apply: value => bindings.SaveChat(bindings.LoadChat() with { ChatFontFace = value }),
|
||||
defaultValue: 2);
|
||||
defaultValue: 2,
|
||||
storeOnly: true); // AP-200
|
||||
|
||||
BuildMenuRow(
|
||||
listBox, "ID_UI_ChatFontSize", ChatFontSizeChoices, page, resolveString,
|
||||
read: () => bindings.LoadChat().ChatFontSizeIndex,
|
||||
apply: value => bindings.SaveChat(bindings.LoadChat() with { ChatFontSizeIndex = value }),
|
||||
defaultValue: 1);
|
||||
defaultValue: 1,
|
||||
storeOnly: true); // AP-200
|
||||
|
||||
chat = bindings.LoadChat();
|
||||
}
|
||||
|
|
@ -712,7 +758,8 @@ public static class ConfigOptionsPageController
|
|||
OptionPage page,
|
||||
Func<uint, uint, string?> resolveString,
|
||||
Func<bool> read,
|
||||
Action<bool> apply)
|
||||
Action<bool> apply,
|
||||
bool storeOnly)
|
||||
{
|
||||
UiElement? row = listBox.AddItemFromTemplateList(ToggleTemplateIndex);
|
||||
if (row is null)
|
||||
|
|
@ -732,7 +779,7 @@ public static class ConfigOptionsPageController
|
|||
return;
|
||||
}
|
||||
|
||||
ApplyLabelAndTooltip(checkbox, labelKey, resolveString);
|
||||
ApplyLabelAndTooltip(checkbox, labelKey, resolveString, storeOnly);
|
||||
|
||||
bool initial = read();
|
||||
checkbox.Selected = initial;
|
||||
|
|
@ -774,6 +821,7 @@ public static class ConfigOptionsPageController
|
|||
Func<uint, uint, string?> resolveString,
|
||||
Func<float> read,
|
||||
Action<float> apply,
|
||||
bool storeOnly,
|
||||
string? rangeLowKey = null,
|
||||
string? rangeHighKey = null)
|
||||
{
|
||||
|
|
@ -787,7 +835,7 @@ public static class ConfigOptionsPageController
|
|||
}
|
||||
|
||||
if (UiElement.FindDescendant(row, SliderLabelElementId) is UiText label)
|
||||
SetLabelText(label, labelKey, resolveString);
|
||||
SetLabelText(label, labelKey, resolveString, storeOnly);
|
||||
|
||||
if (rangeLowKey is not null)
|
||||
SetRangeLabel(row, SliderRangeMinElementId, rangeLowKey, resolveString);
|
||||
|
|
@ -870,7 +918,8 @@ public static class ConfigOptionsPageController
|
|||
Func<bool> toggleRead,
|
||||
Action<bool> toggleApply,
|
||||
Func<float> sliderRead,
|
||||
Action<float> sliderApply)
|
||||
Action<float> sliderApply,
|
||||
bool storeOnly)
|
||||
{
|
||||
if (listBox.AddItemFromTemplateList(TrioTemplateIndex) is not UiOptionToggleSlider trio)
|
||||
{
|
||||
|
|
@ -891,7 +940,7 @@ public static class ConfigOptionsPageController
|
|||
return;
|
||||
}
|
||||
|
||||
ApplyLabelAndTooltip(checkbox, toggleLabelKey, resolveString);
|
||||
ApplyLabelAndTooltip(checkbox, toggleLabelKey, resolveString, storeOnly);
|
||||
|
||||
bool toggleInitial = toggleRead();
|
||||
checkbox.Selected = toggleInitial;
|
||||
|
|
@ -940,7 +989,8 @@ public static class ConfigOptionsPageController
|
|||
Func<uint, uint, string?> resolveString,
|
||||
Func<int> read,
|
||||
Action<int> apply,
|
||||
int defaultValue)
|
||||
int defaultValue,
|
||||
bool storeOnly)
|
||||
{
|
||||
UiElement? row = listBox.AddItemFromTemplateList(MenuTemplateIndex);
|
||||
if (row is null)
|
||||
|
|
@ -952,7 +1002,7 @@ public static class ConfigOptionsPageController
|
|||
}
|
||||
|
||||
if (UiElement.FindDescendant(row, MenuLabelElementId) is UiText label)
|
||||
SetLabelText(label, labelKey, resolveString);
|
||||
SetLabelText(label, labelKey, resolveString, storeOnly);
|
||||
|
||||
if (UiElement.FindDescendant(row, MenuElementId) is not UiMenu menu)
|
||||
{
|
||||
|
|
@ -1022,7 +1072,8 @@ public static class ConfigOptionsPageController
|
|||
Func<uint, uint, string?> resolveString,
|
||||
Func<string> read,
|
||||
Action<string> apply,
|
||||
string defaultValue)
|
||||
string defaultValue,
|
||||
bool storeOnly)
|
||||
{
|
||||
UiElement? row = listBox.AddItemFromTemplateList(MenuTemplateIndex);
|
||||
if (row is null)
|
||||
|
|
@ -1034,7 +1085,7 @@ public static class ConfigOptionsPageController
|
|||
}
|
||||
|
||||
if (UiElement.FindDescendant(row, MenuLabelElementId) is UiText label)
|
||||
SetLabelText(label, labelKey, resolveString);
|
||||
SetLabelText(label, labelKey, resolveString, storeOnly);
|
||||
|
||||
if (UiElement.FindDescendant(row, MenuElementId) is not UiMenu menu)
|
||||
{
|
||||
|
|
@ -1079,7 +1130,7 @@ public static class ConfigOptionsPageController
|
|||
}
|
||||
|
||||
private static void ApplyLabelAndTooltip(
|
||||
UiButton checkbox, string labelKey, Func<uint, uint, string?> resolveString)
|
||||
UiButton checkbox, string labelKey, Func<uint, uint, string?> resolveString, bool storeOnly)
|
||||
{
|
||||
string? label = resolveString(StringTableId, DatStringResolver.ComputeHash(labelKey));
|
||||
if (label is not null)
|
||||
|
|
@ -1089,6 +1140,13 @@ public static class ConfigOptionsPageController
|
|||
$"[D.2b] ConfigOptionsPageController: label '{labelKey}' did not resolve — "
|
||||
+ "row renders with no caption rather than invented English.");
|
||||
|
||||
// AD-78: store-only rows keep their full interactivity (see class
|
||||
// doc) — only the caption color changes, matching the existing
|
||||
// disabled/ghosted convention (UiRenderContext.StoreOnlyCaptionColor).
|
||||
checkbox.LabelColor = storeOnly
|
||||
? UiRenderContext.StoreOnlyCaptionColor
|
||||
: Vector4.One;
|
||||
|
||||
string? tooltip = ResolveTooltip(labelKey, resolveString);
|
||||
if (tooltip is not null)
|
||||
checkbox.TooltipText = tooltip;
|
||||
|
|
@ -1106,7 +1164,8 @@ public static class ConfigOptionsPageController
|
|||
private static string? ResolveTooltip(string labelKey, Func<uint, uint, string?> resolveString)
|
||||
=> resolveString(StringTableId, DatStringResolver.ComputeHash(labelKey + "_Help"));
|
||||
|
||||
private static void SetLabelText(UiText label, string labelKey, Func<uint, uint, string?> resolveString)
|
||||
private static void SetLabelText(
|
||||
UiText label, string labelKey, Func<uint, uint, string?> resolveString, bool storeOnly)
|
||||
{
|
||||
string? text = resolveString(StringTableId, DatStringResolver.ComputeHash(labelKey));
|
||||
if (text is null)
|
||||
|
|
@ -1116,7 +1175,10 @@ public static class ConfigOptionsPageController
|
|||
+ "row renders with no caption rather than invented English.");
|
||||
return;
|
||||
}
|
||||
label.LinesProvider = () => new[] { new UiText.Line(text, label.DefaultColor) };
|
||||
// AD-78: store-only rows dim their caption instead of the DAT-authored
|
||||
// (or default white) color — see class doc.
|
||||
Vector4 color = storeOnly ? UiRenderContext.StoreOnlyCaptionColor : label.DefaultColor;
|
||||
label.LinesProvider = () => new[] { new UiText.Line(text, color) };
|
||||
}
|
||||
|
||||
private static UiButton? FindCheckbox(UiElement root)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.Core.Input;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
|
@ -120,6 +121,18 @@ namespace AcDream.App.UI.Layout;
|
|||
/// retail's <c>OpenOverwriteBindingDialog(&conflicts)</c> — BEFORE reassigning;
|
||||
/// only on accept are the losing rows' slots erased and the new chord applied.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Caption dimming (AD-78, user-directed, 2026-08-11, gate 2).</b> A row
|
||||
/// whose <see cref="RowView.MappedAction"/> is null (AP-203's store-only
|
||||
/// set — mostly Emotes and CharacterSettings, plus every non-user-bindable
|
||||
/// InputMap this screen renders) dims its synthesized caption via
|
||||
/// <see cref="UiRenderContext.StoreOnlyCaptionColor"/> in
|
||||
/// <see cref="BuildActionRow"/>. The row stays fully rendered, bindable,
|
||||
/// conflict-checked, and persisted (per the paragraph above) — only the
|
||||
/// caption color changes, so the dim is a visual "no live gameplay consumer
|
||||
/// yet" marker, not a functional restriction.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class KeyboardConfigController
|
||||
{
|
||||
|
|
@ -332,9 +345,16 @@ public sealed class KeyboardConfigController
|
|||
string? label = resolveString(RetailInputMapHeaders.StringTableId, row.LabelHash);
|
||||
string? tooltip = resolveString(RetailInputMapHeaders.StringTableId, row.TooltipHash);
|
||||
|
||||
bool mapped = RetailActionIdentityTable.TryResolve(row.InputMapId, row.ActionId, out InputAction action);
|
||||
InputAction? mappedAction = mapped ? action : null;
|
||||
|
||||
// The row's own caption — synthesized, composed beside the authored key
|
||||
// buttons (UiText is sealed; see class doc). Occupies the "Command" column
|
||||
// (x=0..270, matching the authored column headers).
|
||||
// (x=0..270, matching the authored column headers). AD-78 (user-directed,
|
||||
// 2026-08-11, gate 2): an unmapped row (MappedAction null — no live
|
||||
// InputDispatcher consumer, AP-203) dims its caption; the key buttons
|
||||
// themselves stay fully interactive (bindable/persisted/conflict-checked,
|
||||
// see class doc).
|
||||
var captionText = new UiText
|
||||
{
|
||||
Left = 0f,
|
||||
|
|
@ -346,14 +366,12 @@ public sealed class KeyboardConfigController
|
|||
RightAligned = false,
|
||||
Padding = 2f,
|
||||
Anchors = AnchorEdges.Left | AnchorEdges.Top,
|
||||
DefaultColor = mapped ? Vector4.One : UiRenderContext.StoreOnlyCaptionColor,
|
||||
};
|
||||
if (label is not null)
|
||||
captionText.LinesProvider = () => new[] { new UiText.Line(label, captionText.DefaultColor) };
|
||||
built.AddChild(captionText);
|
||||
|
||||
bool mapped = RetailActionIdentityTable.TryResolve(row.InputMapId, row.ActionId, out InputAction action);
|
||||
InputAction? mappedAction = mapped ? action : null;
|
||||
|
||||
// M1: capture this row's live Activation/Scope ONCE, from the first
|
||||
// existing binding for the action (every multi-chord action in
|
||||
// KeyBindings.RetailDefaults() shares one Activation/Scope pair across
|
||||
|
|
|
|||
|
|
@ -209,6 +209,23 @@ public sealed class UiRenderContext
|
|||
/// 9 elements in the whole DAT set — every other outlined element uses this).</summary>
|
||||
public static readonly Vector4 DefaultOutlineColor = new(0f, 0f, 0f, 1f);
|
||||
|
||||
/// <summary>
|
||||
/// AD-78 (user-directed, 2026-08-11, Campaign OP gate 2): the shared caption
|
||||
/// color for Options-panel / Configure-Keyboard rows that are store-only —
|
||||
/// they persist (and, where auto-save, send the wire bit) but drive nothing
|
||||
/// observable in acdream yet. Retail dims nothing here (every retail row has
|
||||
/// a live consumer by construction); this is a deliberate acdream-only
|
||||
/// convention so a store-only row is visually distinguishable from a live
|
||||
/// one without any invented marker text. Same neutral grey as the existing
|
||||
/// disabled/ghosted convention (<see cref="UiMenu.TextColorGhosted"/>,
|
||||
/// retail's disabled StateDesc grey) — reused rather than a new color so the
|
||||
/// "this doesn't do anything yet" signal reads consistently across the whole
|
||||
/// UI. See AD-78 for the full citation list; the row retires as consumers
|
||||
/// land (each landing un-dims its own rows via the owning controller's
|
||||
/// conformance test).
|
||||
/// </summary>
|
||||
public static readonly Vector4 StoreOnlyCaptionColor = new(0.5f, 0.5f, 0.5f, 1f);
|
||||
|
||||
/// <summary>
|
||||
/// Draw a single line of text with a retail dat font (<see cref="UiDatFont"/>),
|
||||
/// at <paramref name="x"/>,<paramref name="y"/> = the top-left of the
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
|
@ -693,4 +694,127 @@ public sealed class CharacterOptionsPageControllerTests
|
|||
// degrades to "no text", never a fabricated label.
|
||||
Assert.Equal(50, controller.CharacterPage.Rows.Count);
|
||||
}
|
||||
|
||||
// ── AD-78 caption dimming (user-directed, 2026-08-11, gate 2) ───────────
|
||||
|
||||
/// <summary>
|
||||
/// The exact 35 <see cref="CharacterOptionId"/> values this slice dims
|
||||
/// (<see cref="RowSpec.StoreOnly"/> == true), transcribed independently
|
||||
/// of <c>CharacterOptionsPageController.Groups</c> from the derivation
|
||||
/// table in that class's own doc comment. <b>Wiring a future consumer
|
||||
/// for any of these means removing it from this literal set AND
|
||||
/// flipping its <c>Groups</c> table entry from <c>StoreOnly</c> to
|
||||
/// <c>Live</c> consciously — leaving either one stale fails this
|
||||
/// test.</b>
|
||||
/// </summary>
|
||||
private static readonly HashSet<CharacterOptionId> ExpectedStoreOnlyIds =
|
||||
[
|
||||
// Group 1 (UI Behavior) — SalvageMultiple (D), MainPackPreferred (B, unbound)
|
||||
CharacterOptionId.SalvageMultiple,
|
||||
CharacterOptionId.MainPackPreferred,
|
||||
// Group 2 (UI Display) — 11 of 15
|
||||
CharacterOptionId.ShowTooltips,
|
||||
CharacterOptionId.SideBySideVitals,
|
||||
CharacterOptionId.SpellDuration,
|
||||
CharacterOptionId.DisableMostWeatherEffects,
|
||||
CharacterOptionId.PersistentAtDay,
|
||||
CharacterOptionId.DisableHouseRestrictionEffects,
|
||||
CharacterOptionId.UseCraftSuccessDialog,
|
||||
CharacterOptionId.ConfirmVolatileRareUse,
|
||||
CharacterOptionId.FilterLanguage,
|
||||
CharacterOptionId.ShowHelm,
|
||||
CharacterOptionId.ShowCloak,
|
||||
// Group 3 (Grouping) — all 6
|
||||
CharacterOptionId.IgnoreAllegianceRequests,
|
||||
CharacterOptionId.IgnoreFellowshipRequests,
|
||||
CharacterOptionId.DisplayAllegianceLogonNotifications,
|
||||
CharacterOptionId.FellowshipShareXP,
|
||||
CharacterOptionId.FellowshipShareLoot,
|
||||
CharacterOptionId.FellowshipAutoAcceptRequests,
|
||||
// Group 4 (Other Players) — 10 of 11
|
||||
CharacterOptionId.AcceptLootPermits,
|
||||
CharacterOptionId.UseDeception,
|
||||
CharacterOptionId.AllowGive,
|
||||
CharacterOptionId.IgnoreTradeRequests,
|
||||
CharacterOptionId.DisplayDateOfBirth,
|
||||
CharacterOptionId.DisplayAge,
|
||||
CharacterOptionId.DisplayChessRank,
|
||||
CharacterOptionId.DisplayFishingSkill,
|
||||
CharacterOptionId.DisplayNumberDeaths,
|
||||
CharacterOptionId.DisplayNumberCharacterTitles,
|
||||
// Group 5 (Character Behavior) — 4 of 7
|
||||
CharacterOptionId.AdvancedCombatUI,
|
||||
CharacterOptionId.UseChargeAttack,
|
||||
CharacterOptionId.LeadMissileTargets,
|
||||
CharacterOptionId.UseFastMissiles,
|
||||
// Group 6 (Chat) — 2 of 8
|
||||
CharacterOptionId.StayInChatMode,
|
||||
CharacterOptionId.HearPkDeathMessages,
|
||||
];
|
||||
|
||||
[Fact]
|
||||
public void StoreOnlyRows_MatchTheDerivationTableExactly()
|
||||
{
|
||||
HashSet<CharacterOptionId> actualStoreOnly = AllRows()
|
||||
.Where(static r => r.StoreOnly)
|
||||
.Select(static r => r.Id)
|
||||
.ToHashSet();
|
||||
|
||||
Assert.Equal(ExpectedStoreOnlyIds, actualStoreOnly);
|
||||
Assert.Equal(35, actualStoreOnly.Count);
|
||||
Assert.Equal(15, 50 - actualStoreOnly.Count); // the 15 live rows
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bind_AppliesDimmedCaptionColor_ForStoreOnlyRows_AndWhiteForLiveRows()
|
||||
{
|
||||
// A non-null constant resolver (unlike this file's usual BindReal()
|
||||
// default) so every checkbox actually gets a Label -- LabelColor
|
||||
// itself is set unconditionally either way, but this keeps the
|
||||
// built tree representative of a real DAT string table.
|
||||
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
|
||||
OptionsPanelController controller = OptionsPanelController.Bind(
|
||||
layout,
|
||||
new OptionsPanelController.Callbacks(
|
||||
Toggle: () => { },
|
||||
RequestExitToCharacterSelection: () => { },
|
||||
ExitGame: () => { },
|
||||
UseMouseTurningSettings: () => { },
|
||||
DisplaySystemMessage: _ => { }))!;
|
||||
var fakeBindings = new FakeBindings();
|
||||
bool bound = CharacterOptionsPageController.Bind(
|
||||
layout, controller.CharacterPage, MakeTemplateResolver(), (_, _) => "x",
|
||||
fakeBindings.ToBindings());
|
||||
Assert.True(bound);
|
||||
|
||||
var listBox = Assert.IsType<UiTemplateListBox>(
|
||||
layout.FindElement(CharacterOptionsPageController.ListBoxElementId));
|
||||
UiElement viewport = Assert.Single(listBox.Children);
|
||||
|
||||
// Filter for "contains a checkbox leaf" rather than asserting a
|
||||
// concrete non-checkbox widget type for headers/separators -- robust
|
||||
// to either template's underlying DatWidgetFactory class, exactly
|
||||
// like the controller's own FindCheckbox degrade-gracefully pattern.
|
||||
const uint ToggleCheckboxElementId = 0x10000219u;
|
||||
List<UiButton> checkboxesInOrder = viewport.Children
|
||||
.Select(item => UiElement.FindDescendant(item, ToggleCheckboxElementId) as UiButton)
|
||||
.Where(static cb => cb is not null)
|
||||
.Select(static cb => cb!)
|
||||
.ToList();
|
||||
Assert.Equal(50, checkboxesInOrder.Count);
|
||||
|
||||
List<CharacterOptionsPageController.RowSpec> specsInOrder = AllRows().ToList();
|
||||
Assert.Equal(50, specsInOrder.Count);
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
CharacterOptionsPageController.RowSpec spec = specsInOrder[i];
|
||||
Vector4 expected = spec.StoreOnly ? UiRenderContext.StoreOnlyCaptionColor : Vector4.One;
|
||||
Assert.True(
|
||||
expected == checkboxesInOrder[i].LabelColor,
|
||||
$"row {i} ({spec.RetailName}, 0x{(uint)spec.Id:X2}): expected "
|
||||
+ $"{(spec.StoreOnly ? "DIMMED" : "LIVE")} caption color {expected} but the "
|
||||
+ $"built checkbox rendered {checkboxesInOrder[i].LabelColor}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
|
|
@ -858,4 +859,108 @@ public sealed class ConfigOptionsPageControllerTests
|
|||
Assert.True(bound);
|
||||
Assert.Equal(30, controller.ConfigPage.Rows.Count);
|
||||
}
|
||||
|
||||
// ── AD-78 caption dimming (user-directed, 2026-08-11, gate 2) ───────────
|
||||
|
||||
private enum RowKind { Toggle, TrioToggle, Slider, Menu }
|
||||
|
||||
/// <summary>
|
||||
/// One literal expectation per option-row-bearing viewport item, in the
|
||||
/// SAME order <see cref="ConfigOptionsPageController.Bind"/> builds them
|
||||
/// (39 stacked items: 6 headers + 6 separators + 27 option rows — see
|
||||
/// <see cref="Bind_ListBoxStacks39Items_SixHeaders_SixSeparators_27OptionRows"/>
|
||||
/// for the index arithmetic this table shares). <b>Wiring a future
|
||||
/// consumer for any row here means flipping BOTH this literal's
|
||||
/// <c>StoreOnly</c> AND the matching <c>ConfigOptionsPageController</c>
|
||||
/// <c>BindXxxSection</c> call site's own <c>storeOnly:</c> argument
|
||||
/// consciously — leaving either one stale fails this test.</b>
|
||||
/// </summary>
|
||||
private static readonly (int ViewportIndex, RowKind Kind, bool StoreOnly, string Label)[]
|
||||
DimmingExpectations =
|
||||
{
|
||||
(1, RowKind.Menu, true, "Sound Features"), // AP-199
|
||||
(2, RowKind.TrioToggle, false, "Disable Sound Effects"), // LIVE
|
||||
(3, RowKind.TrioToggle, false, "Disable Ambient Sound"), // LIVE
|
||||
(4, RowKind.TrioToggle, true, "Disable Interface Sound"), // AP-174/AP-199
|
||||
(5, RowKind.Toggle, true, "Play Sound Only When Active"), // AP-199
|
||||
(8, RowKind.Slider, true, "Camera Stiffness"), // TS-74
|
||||
(9, RowKind.Slider, true, "Camera Adjustment Speed"), // TS-74
|
||||
(10, RowKind.Slider, false, "Field Of View"), // NEXT-LAUNCH
|
||||
(11, RowKind.Toggle, true, "Align To Slope"), // TS-74
|
||||
(14, RowKind.Menu, false, "Resolution"), // LIVE
|
||||
(15, RowKind.Toggle, false, "Full Screen"), // LIVE
|
||||
(16, RowKind.Toggle, false, "Sync To Refresh"), // NEXT-LAUNCH
|
||||
(17, RowKind.Slider, true, "Screen Brightness"), // review S2
|
||||
(18, RowKind.Toggle, true, "Automatic Degrades"), // AP-198
|
||||
(19, RowKind.Slider, true, "Graphics Performance"), // AP-198
|
||||
(20, RowKind.Slider, true, "Degrade Distance"), // AP-198
|
||||
(23, RowKind.Menu, true, "Landscape Texture Detail"), // AP-198
|
||||
(24, RowKind.Menu, true, "Environment Texture Detail"), // AP-198
|
||||
(25, RowKind.Menu, true, "Texture Filtering"), // AP-198
|
||||
(26, RowKind.Menu, true, "Landscape Draw Distance"), // AP-198
|
||||
(27, RowKind.Toggle, true, "Building Detail Textures"), // AP-198
|
||||
(28, RowKind.Toggle, true, "Multi-Pass Alpha"), // AP-198
|
||||
(31, RowKind.Slider, true, "Mouse Look Sensitivity"), // TS-74
|
||||
(32, RowKind.Toggle, true, "Invert Mouselook Y Axis"), // TS-74
|
||||
(33, RowKind.Toggle, true, "Use Mouse Turning"), // TS-74
|
||||
(36, RowKind.Menu, true, "Chat Font Face"), // AP-200
|
||||
(37, RowKind.Menu, true, "Chat Font Size"), // AP-200
|
||||
};
|
||||
|
||||
private static Vector4? FindTextLineColor(UiElement root, uint elementId)
|
||||
{
|
||||
if (UiElement.FindDescendant(root, elementId) is not UiText text)
|
||||
return null;
|
||||
IReadOnlyList<UiText.Line> lines = text.LinesProvider();
|
||||
return lines.Count == 0 ? null : lines[0].Color;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CaptionDimming_MatchesTheStoreOnlySetExactly()
|
||||
{
|
||||
// A non-null constant resolver is required here (unlike most of this
|
||||
// file's BindReal() default): SetLabelText's dim color is baked into
|
||||
// the UiText.Line closure only when a label actually resolves, so a
|
||||
// null resolver would leave every slider/menu label's LinesProvider
|
||||
// at its empty default and hide the very thing this test checks.
|
||||
(OptionsPanelController controller, _, bool bound) = BindReal(resolveString: (_, _) => "x");
|
||||
Assert.True(bound);
|
||||
|
||||
var configSlot = UiElement.FindDescendant(controller.TabPanel, ConfigPageSlotId)!;
|
||||
var listBox = Assert.IsType<UiTemplateListBox>(
|
||||
UiElement.FindDescendant(configSlot, ConfigOptionsPageController.ListBoxElementId));
|
||||
UiElement viewport = Assert.Single(listBox.Children);
|
||||
IReadOnlyList<UiElement> items = viewport.Children.ToList();
|
||||
Assert.Equal(39, items.Count);
|
||||
|
||||
const uint ToggleCheckboxElementId = 0x10000219u;
|
||||
const uint SliderLabelElementId = 0x1000021Bu;
|
||||
const uint MenuLabelElementId = 0x10000223u;
|
||||
|
||||
foreach ((int index, RowKind kind, bool storeOnly, string label) in DimmingExpectations)
|
||||
{
|
||||
Vector4 expected = storeOnly
|
||||
? UiRenderContext.StoreOnlyCaptionColor
|
||||
: Vector4.One;
|
||||
Vector4? actual = kind switch
|
||||
{
|
||||
RowKind.Toggle =>
|
||||
(UiElement.FindDescendant(items[index], ToggleCheckboxElementId) as UiButton)?.LabelColor,
|
||||
RowKind.TrioToggle =>
|
||||
Assert.IsType<UiOptionToggleSlider>(items[index]).Toggle?.LabelColor,
|
||||
RowKind.Slider => FindTextLineColor(items[index], SliderLabelElementId),
|
||||
RowKind.Menu => FindTextLineColor(items[index], MenuLabelElementId),
|
||||
_ => throw new InvalidOperationException($"unhandled row kind {kind}"),
|
||||
};
|
||||
Assert.True(
|
||||
actual.HasValue,
|
||||
$"{label} (viewport index {index}, kind {kind}): could not locate the "
|
||||
+ "caption widget/line to check its color.");
|
||||
Assert.True(
|
||||
expected == actual.Value,
|
||||
$"{label} (viewport index {index}): expected "
|
||||
+ $"{(storeOnly ? "DIMMED" : "LIVE")} caption color {expected} but the "
|
||||
+ $"built widget rendered {actual.Value}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Input;
|
||||
|
|
@ -610,4 +611,49 @@ public sealed class KeyboardConfigControllerTests
|
|||
}
|
||||
Assert.True(seen.Count > 100, $"sanity: only {seen.Count} mapped actions seen");
|
||||
}
|
||||
|
||||
// ── AD-78 caption dimming (user-directed, 2026-08-11, gate 2) ───────────
|
||||
|
||||
[Fact]
|
||||
public void UnmappedRows_DimTheirCaption_MappedRowsStayWhite()
|
||||
{
|
||||
// AP-203's store-only set: a row whose RetailActionIdentityTable
|
||||
// lookup fails (MappedAction null -- mostly Emotes/CharacterSettings)
|
||||
// never reaches the InputDispatcher, so its caption dims. Wiring a
|
||||
// future mapping for "Bow Deep" (or any other unmapped row) means
|
||||
// this assertion flips from StoreOnlyCaptionColor to Vector4.One --
|
||||
// a conscious edit, not a silent pass.
|
||||
var snapshot = new RetailActionMapSnapshot(new[]
|
||||
{
|
||||
Row(0x4, 0x29, RetailActionClass.Movement), // -> MovementForward (mapped)
|
||||
Row(0x10000006, 0x100000A0, RetailActionClass.Emote), // "Bow Deep" -> unmapped
|
||||
});
|
||||
|
||||
ImportedLayout layout = FixtureLoader.LoadKeyboardConfig();
|
||||
var fake = new FakeBindings();
|
||||
KeyboardConfigController controller = KeyboardConfigController.Bind(
|
||||
layout, snapshot, MakeTemplateResolver(), (_, _) => null, fake.ToBindings())!;
|
||||
|
||||
KeyboardConfigController.RowView forward = controller.Rows.Single(r => r.ActionId == 0x29u);
|
||||
Assert.NotNull(forward.MappedAction);
|
||||
UiText forwardCaption = RowCaption(forward);
|
||||
Assert.Equal(Vector4.One, forwardCaption.DefaultColor);
|
||||
|
||||
KeyboardConfigController.RowView bowDeep = controller.Rows.Single(r => r.ActionId == 0x100000A0u);
|
||||
Assert.Null(bowDeep.MappedAction);
|
||||
UiText bowDeepCaption = RowCaption(bowDeep);
|
||||
Assert.Equal(UiRenderContext.StoreOnlyCaptionColor, bowDeepCaption.DefaultColor);
|
||||
}
|
||||
|
||||
/// <summary>The row's synthesized caption (composed beside the authored key
|
||||
/// buttons -- see KeyboardConfigController's class doc) has no stable dat
|
||||
/// element id of its own, so it is located structurally: the ONLY
|
||||
/// <see cref="UiText"/> direct child of the key buttons' shared parent
|
||||
/// (the row container <c>BuildActionRow</c> builds).</summary>
|
||||
private static UiText RowCaption(KeyboardConfigController.RowView row)
|
||||
{
|
||||
UiElement parent = row.KeyButtons.First().Parent
|
||||
?? throw new InvalidOperationException("row's key button has no parent element");
|
||||
return parent.Children.OfType<UiText>().Single();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue