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.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}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue