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
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue