fix(ui): OP6 rework — six range captions, un-invert Sound enabled flags, five font faces
Fixes all three MUST-FIX findings from the OP6 REJECT review (docs/research/2026-08-11-op6-review.md) plus its SHOULD-FIXes and NOTEs. M1 — the "retail ships zero range captions" claim was a Binary Ninja constant-folding artifact (the same class the header-string globals a few lines above already worked around). The six SetSliderLabel call sites byte-decode to reads of runtime-filled ID_Graphics_Value_* globals, not immediate zeros (PE-byte-verified against the PDB-paired acclient.exe, independently re-derived in this session, not just re-asserted from the review). ConfigOptionsPageController.BuildSliderRow gained optional rangeLowKey/rangeHighKey parameters wired for all six idx6 sliders (Camera Stiffness Soft/Hard, Adjustment Speed Slow/Fast, FOV Narrow/Wide, Screen Brightness Dark/Bright, Graphics Performance Speed/Detail, Degrade Distance Close/Far) via the same SetRangeLabel mechanism OP5's Chat opacity sliders already established. Mouse Look Sensitivity (idx3) correctly stays uncaptioned — the one genuine SetSliderLabel omission. Class doc corrected; gate-script lines 535/653-equivalent corrected in place. M2 — the three Sound "Disabled" toggles were semantically inverted: SoundManager::effect_sounds_enabled/ambient_sounds_enabled/ interface_sounds_enabled are all compiled = 1 in .data, and UserPreferences::RegisterPreference binds the checkbox's boolean value DIRECTLY onto those enabled-sense statics — checked-by-default means enabled-by-default, not disabled. AudioSettings.SfxDisabled/AmbientDisabled/ InterfaceDisabled renamed to SfxEnabled/AmbientEnabled/InterfaceEnabled (fresh JSON keys — the rejected slice's keys never shipped in an accepted build); RuntimeSettingsStartupTargets.ApplyAudio now computes effective volume through the extracted, independently-unit-tested pure function ComputeEffectiveCategoryVolumes (enabled ? slider : 0f). This closes the blast radius the review flagged: a missing key in an EXISTING settings.json now falls back to AudioSettings.Default, which is enabled=true, so a fresh launch is audible, not muted. AP-199's wording and gate-script step 6 corrected; the enshrined-inversion test rewritten to assert the correct default and a new SettingsStore test pins the legacy-file fallback path. M3 — UI_ChatFontFace now ships all five of retail's authored choices (Arial, CourierNew, PalatinoLinotype, Tahoma, TimesNewRoman — a fixed compile-time array at gmClient::InitUIPreferences, PE-byte-verified present verbatim in .rdata, not a per-machine runtime enumeration as the rejected slice's comment claimed). Default index 2 (PalatinoLinotype) now indexes a real entry. S1 — Bind() now emits the sixth trailing AddSeperator retail's own InitOptions ends with (0x0049E80D), matching retail's 39-item ListBox (6 headers + 6 separators + 27 option-widget-rows) instead of 38. S2 — Screen Brightness gets its own DisplaySettings.ScreenBrightness field ([-1,1], default 0) instead of overloading Gamma, which has a different unit system (default 1.0, legacy [0.5,2.0] slider) and its own live Settings-panel consumer. S3 — UiScrollbar and UiMenu gained a settable TooltipText surfaced through GetTooltipText (UiButton's existing pattern). Every slider and menu row's own interactive widget (not just toggle/trio rows) now carries retail's "<label>_Help" tooltip, verified as a universal suffix convention across every AttachPreference site touched by this tab. S4 — "800x600" added to DisplaySettings.AvailableResolutions: a genuine retail display mode (Device::ForceDisplayResolution(1,0x320,0x258) at startup) and the Config tab's own byte-verified Resolution row default, not an invented preset. Defaults now lands on a highlighted, re-selectable dropdown entry instead of an orphaned value. S5 — four new/extended tests: ComputeEffectiveCategoryVolumes gets a dedicated pure-function value assertion (Theory + a default-profile-is- audible Fact) in RuntimeSettingsControllerTests, closing the "only event order was asserted" gap that let M2 ship; a label/choice-key conformance table in ConfigOptionsPageControllerTests enumerates every key this tab queries (traced directly from the fixed code paths, not guessed) and fails on an invented OR a dropped key; a per-row DefaultValue pin asserts every row's default against the retail literal directly, independent of the underlying settings-record defaults; and the S1 separator fix gets its own 39-item stacked-ListBox count pin. NOTEs — AP-198's row count was always ten (its own enumeration never said nine); the commit-message inconsistency N1 flagged is reconciled in both the row and the section-summary line, and its Screen Brightness sub-clause now matches S2. N2: Bind() now reads the scrollbar id from UiTemplateListBox.ScrollbarElementId (dat property 0x72) instead of a hardcoded constant. N3 (batch Defaults writes) and N4 (AfterApply on Config-tab entry, needs no action) are left as recorded — out of this rework's scope per the review's own disposition. Full Release suite: 13,125 passed / 4 skipped / 0 failed (baseline 13,117/4/0 — net +8 tests added, 0 regressions, 0 removed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
4996974cfd
commit
472525b99e
11 changed files with 774 additions and 168 deletions
|
|
@ -319,6 +319,50 @@ public sealed class RuntimeSettingsControllerTests
|
|||
Assert.Equal(resolved, controller.ResolvedQuality);
|
||||
}
|
||||
|
||||
// OP6 rework (2026-08-11, review S5 / M2): pins the EFFECTIVE volume
|
||||
// ApplyAudio actually computes, not just that some target was called.
|
||||
// The FakeRuntimeTargets-based "target-audio" assertion above (and its
|
||||
// predecessor before this rework) only ever checked that ApplyAudio
|
||||
// fired, never what it fired WITH — the exact gap that let M2's
|
||||
// Enabled/Disabled inversion mute every default profile unnoticed.
|
||||
[Theory]
|
||||
[InlineData(true, true, 0.6f, 0.9f, 0.6f, 0.9f)] // enabled: slider value passes through
|
||||
[InlineData(false, false, 0.6f, 0.9f, 0f, 0f)] // disabled: forced to zero regardless of slider
|
||||
[InlineData(true, false, 1.0f, 1.0f, 1.0f, 0f)] // independent per-category gating
|
||||
public void ComputeEffectiveCategoryVolumes_GatesSliderValueOnEnabledFlag(
|
||||
bool sfxEnabled, bool ambientEnabled,
|
||||
float sfxSlider, float ambientSlider,
|
||||
float expectedSfx, float expectedAmbient)
|
||||
{
|
||||
AudioSettings audio = AudioSettings.Default with
|
||||
{
|
||||
SfxEnabled = sfxEnabled,
|
||||
AmbientEnabled = ambientEnabled,
|
||||
Sfx = sfxSlider,
|
||||
Ambient = ambientSlider,
|
||||
};
|
||||
|
||||
(float sfx, float ambient) = RuntimeSettingsStartupTargets.ComputeEffectiveCategoryVolumes(audio);
|
||||
|
||||
Assert.Equal(expectedSfx, sfx);
|
||||
Assert.Equal(expectedAmbient, ambient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeEffectiveCategoryVolumes_DefaultProfile_IsAudible_NotMuted()
|
||||
{
|
||||
// The exact regression M2 shipped: a fresh/default AudioSettings
|
||||
// must leave sound effects and ambient audio AUDIBLE on the next
|
||||
// launch, matching retail's own byte-verified enabled-by-default
|
||||
// statics (SoundManager::effect_sounds_enabled/
|
||||
// ambient_sounds_enabled = 1) — see AudioSettings' class doc.
|
||||
(float sfx, float ambient) =
|
||||
RuntimeSettingsStartupTargets.ComputeEffectiveCategoryVolumes(AudioSettings.Default);
|
||||
|
||||
Assert.Equal(1.0f, sfx);
|
||||
Assert.Equal(1.0f, ambient);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SaveChatPublishesSetSingleCharacterOptionOnlyForChangedBits()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -41,16 +41,22 @@ public sealed class ConfigOptionsPageControllerTests
|
|||
[Fact]
|
||||
public void AudioSettings_Default_MatchesRetailByteVerifiedTrioDefaults()
|
||||
{
|
||||
// gmClient::InitUIPreferences: every Sound trio's checkbox defaults
|
||||
// to CHECKED ("Disabled"=true) — SetDefaultValue(1, 0x3f800000) in
|
||||
// gmConfigUI::InitOptions, ported faithfully (see
|
||||
// AudioSettings.Default's own doc for why this looks backwards but
|
||||
// isn't a "fix").
|
||||
// OP6 rework (2026-08-11, review M2): every Sound trio's checkbox
|
||||
// defaults to CHECKED — SetDefaultValue(1, 0x3f800000) in
|
||||
// gmConfigUI::InitOptions — AND checked means ENABLED, not
|
||||
// "Disabled" (SoundManager::effect_sounds_enabled/
|
||||
// ambient_sounds_enabled/interface_sounds_enabled are all compiled
|
||||
// = 1 in .data, and UserPreferences::RegisterPreference binds the
|
||||
// checkbox's boolean value directly onto those statics — see
|
||||
// AudioSettings' own class doc for the full byte evidence). A
|
||||
// fresh character therefore hears sound by default; this is NOT
|
||||
// the "looks backwards but isn't a fix" case the rejected slice
|
||||
// claimed — the rejected slice's inversion was the actual bug.
|
||||
AudioSettings d = AudioSettings.Default;
|
||||
Assert.Equal(0, d.SoundFeatures);
|
||||
Assert.True(d.SfxDisabled);
|
||||
Assert.True(d.AmbientDisabled);
|
||||
Assert.True(d.InterfaceDisabled);
|
||||
Assert.True(d.SfxEnabled);
|
||||
Assert.True(d.AmbientEnabled);
|
||||
Assert.True(d.InterfaceEnabled);
|
||||
Assert.Equal(1.0f, d.InterfaceVolume);
|
||||
Assert.True(d.PlaySoundOnlyWhenActive);
|
||||
Assert.Equal(1.0f, d.Sfx);
|
||||
|
|
@ -73,6 +79,8 @@ public sealed class ConfigOptionsPageControllerTests
|
|||
public void DisplaySettings_Default_MatchesRetailByteVerifiedConfigDefaults()
|
||||
{
|
||||
DisplaySettings d = DisplaySettings.Default;
|
||||
// OP6 rework (review S2): ScreenBrightness is its own field now.
|
||||
Assert.Equal(0f, d.ScreenBrightness);
|
||||
Assert.False(d.AutomaticDegrades);
|
||||
Assert.Equal(0f, d.GraphicsPerformance);
|
||||
Assert.Equal(50f, d.DegradeDistance);
|
||||
|
|
@ -102,12 +110,14 @@ public sealed class ConfigOptionsPageControllerTests
|
|||
try
|
||||
{
|
||||
var store = new SettingsStore(path);
|
||||
// OP6 rework (review M2): renamed Enabled-sense fields, fresh
|
||||
// JSON keys ("sfxEnabled" etc.) — see AudioSettings' own doc.
|
||||
var written = AudioSettings.Default with
|
||||
{
|
||||
SoundFeatures = 1,
|
||||
SfxDisabled = false,
|
||||
AmbientDisabled = false,
|
||||
InterfaceDisabled = false,
|
||||
SfxEnabled = false,
|
||||
AmbientEnabled = false,
|
||||
InterfaceEnabled = false,
|
||||
InterfaceVolume = 0.4f,
|
||||
PlaySoundOnlyWhenActive = false,
|
||||
};
|
||||
|
|
@ -122,7 +132,47 @@ public sealed class ConfigOptionsPageControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void SettingsStore_DisplayRoundTrip_PreservesTheNineOP6Fields()
|
||||
public void SettingsStore_ExistingProfileMissingTheEnabledKeys_LoadsWithSoundOn()
|
||||
{
|
||||
// OP6 rework (review M2 blast radius): an existing settings.json
|
||||
// written by a build that never had the sfxEnabled/ambientEnabled/
|
||||
// interfaceEnabled keys (e.g. only the "display" section present)
|
||||
// must fall back to AudioSettings.Default — enabled=true — NOT to
|
||||
// an inverted/missing-means-muted reading. This is the exact
|
||||
// scenario the review's blast-radius finding named: "every existing
|
||||
// settings.json... loads as disabled" under the rejected slice.
|
||||
string path = System.IO.Path.Combine(
|
||||
System.IO.Path.GetTempPath(), $"acdream-op6-audio-legacy-{Guid.NewGuid():N}.json");
|
||||
try
|
||||
{
|
||||
System.IO.File.WriteAllText(path, """
|
||||
{
|
||||
"version": 2,
|
||||
"display": { "resolution": "1920x1080" }
|
||||
}
|
||||
""");
|
||||
var store = new SettingsStore(path);
|
||||
AudioSettings read = store.LoadAudio();
|
||||
|
||||
Assert.True(read.SfxEnabled);
|
||||
Assert.True(read.AmbientEnabled);
|
||||
Assert.True(read.InterfaceEnabled);
|
||||
Assert.Equal(1.0f, read.Sfx);
|
||||
Assert.Equal(1.0f, read.Ambient);
|
||||
|
||||
(float sfx, float ambient) =
|
||||
AcDream.App.Settings.RuntimeSettingsStartupTargets.ComputeEffectiveCategoryVolumes(read);
|
||||
Assert.Equal(1.0f, sfx);
|
||||
Assert.Equal(1.0f, ambient);
|
||||
}
|
||||
finally
|
||||
{
|
||||
System.IO.File.Delete(path);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SettingsStore_DisplayRoundTrip_PreservesTheTenOP6Fields()
|
||||
{
|
||||
string path = System.IO.Path.Combine(
|
||||
System.IO.Path.GetTempPath(), $"acdream-op6-display-{Guid.NewGuid():N}.json");
|
||||
|
|
@ -131,6 +181,8 @@ public sealed class ConfigOptionsPageControllerTests
|
|||
var store = new SettingsStore(path);
|
||||
var written = DisplaySettings.Default with
|
||||
{
|
||||
// OP6 rework (review S2): ScreenBrightness's own field.
|
||||
ScreenBrightness = 0.25f,
|
||||
AutomaticDegrades = true,
|
||||
GraphicsPerformance = 0.5f,
|
||||
DegradeDistance = 75f,
|
||||
|
|
@ -327,15 +379,49 @@ public sealed class ConfigOptionsPageControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void ToggleRow_SfxDisabled_WritesThroughAudioBindings()
|
||||
public void Bind_ListBoxStacks39Items_SixHeaders_SixSeparators_27OptionRows()
|
||||
{
|
||||
// OP6 rework (review S1): retail's own InitOptions ends with a
|
||||
// SIXTH AddSeperator tailcall (0x0049e80d @Bind) — a trailing
|
||||
// separator after the LAST section, not just the five interior
|
||||
// ones. 6 headers + 6 separators + 27 option-widget-rows (the 3
|
||||
// trios are ONE widget row each despite registering TWO
|
||||
// IOptionRow instances — see Bind_Succeeds_AndRegistersExactly30Rows)
|
||||
// = 39 stacked ListBox items total, matching retail exactly. The
|
||||
// rejected slice built 38 (five interior separators only).
|
||||
ImportedLayout layout = FixtureLoader.LoadOptionsPanelHost();
|
||||
OptionsPanelController controller = OptionsPanelController.Bind(
|
||||
layout,
|
||||
new OptionsPanelController.Callbacks(
|
||||
Toggle: () => { },
|
||||
RequestExitToCharacterSelection: () => { },
|
||||
ExitGame: () => { },
|
||||
UseMouseTurningSettings: () => { },
|
||||
DisplaySystemMessage: _ => { }))!;
|
||||
var fakeBindings = new FakeBindings();
|
||||
bool bound = ConfigOptionsPageController.Bind(
|
||||
layout, controller.ConfigPage, MakeTemplateResolver(), (_, _) => null,
|
||||
fakeBindings.ToBindings());
|
||||
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);
|
||||
Assert.Equal(39, viewport.Children.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToggleRow_SfxEnabled_WritesThroughAudioBindings()
|
||||
{
|
||||
(OptionsPanelController controller, FakeBindings bindings, _) = BindReal();
|
||||
var row = (BoolOptionRow)controller.ConfigPage.Rows[1]; // Sound trio toggle
|
||||
|
||||
row.SetCurrentValue(!AudioSettings.Default.SfxDisabled);
|
||||
row.SetCurrentValue(!AudioSettings.Default.SfxEnabled);
|
||||
|
||||
Assert.Single(bindings.AudioSaves);
|
||||
Assert.Equal(!AudioSettings.Default.SfxDisabled, bindings.AudioSaves[0].SfxDisabled);
|
||||
Assert.Equal(!AudioSettings.Default.SfxEnabled, bindings.AudioSaves[0].SfxEnabled);
|
||||
// The slider half's own field must be untouched by the toggle write.
|
||||
Assert.Equal(AudioSettings.Default.Sfx, bindings.AudioSaves[0].Sfx);
|
||||
}
|
||||
|
|
@ -468,6 +554,184 @@ public sealed class ConfigOptionsPageControllerTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EveryRow_DefaultValue_MatchesRetailLiteral_NotJustSelfConsistency()
|
||||
{
|
||||
// OP6 rework (review S5): Defaults_RestoresRetailDefaultValue...
|
||||
// above only proves each row's Current becomes its OWN
|
||||
// DefaultValue — it says nothing about whether that DefaultValue
|
||||
// IS retail's byte-verified literal. This pins every row's
|
||||
// DefaultValue directly against the decompiled
|
||||
// gmConfigUI::InitOptions/gmClient::InitUIPreferences constant,
|
||||
// independent of the underlying settings-record defaults (already
|
||||
// pinned separately above) — Build*Row's own inline
|
||||
// `defaultValue:` parameters could drift from those record
|
||||
// defaults without either test alone catching it.
|
||||
(OptionsPanelController controller, _, _) = BindReal();
|
||||
IReadOnlyList<IOptionRow> rows = controller.ConfigPage.Rows;
|
||||
Assert.Equal(30, rows.Count);
|
||||
|
||||
object[] expected =
|
||||
[
|
||||
0, // 0 Sound Features menu (Stereo=0)
|
||||
true, 1.0f, // 1-2 Sound trio (Enabled, Sfx)
|
||||
true, 1.0f, // 3-4 Ambient trio
|
||||
true, 1.0f, // 5-6 Interface trio
|
||||
true, // 7 Play Sound Only When Active
|
||||
|
||||
0.45f, // 8 Camera Stiffness
|
||||
40.0f, // 9 Camera Adjustment Speed
|
||||
90.0f, // 10 Field Of View
|
||||
true, // 11 Align To Slope
|
||||
|
||||
"800x600", // 12 Resolution
|
||||
true, // 13 Full Screen
|
||||
false, // 14 Sync To Refresh
|
||||
0f, // 15 Screen Brightness
|
||||
false, // 16 Automatic Degrades
|
||||
0f, // 17 Graphics Performance
|
||||
50.0f, // 18 Degrade Distance
|
||||
|
||||
2, // 19 Landscape Texture Detail
|
||||
1, // 20 Environment Texture Detail
|
||||
1, // 21 Texture Filtering
|
||||
8, // 22 Landscape Draw Distance (opaque — AP-198 sub-note)
|
||||
true, // 23 Building Detail Textures
|
||||
false, // 24 Multi-Pass Alpha
|
||||
|
||||
0.55f, // 25 Mouse Look Sensitivity
|
||||
false, // 26 Invert Mouselook Y Axis
|
||||
false, // 27 Use Mouse Turning
|
||||
|
||||
2, // 28 Chat Font Face (PalatinoLinotype)
|
||||
1, // 29 Chat Font Size
|
||||
];
|
||||
|
||||
for (int i = 0; i < rows.Count; i++)
|
||||
{
|
||||
object actual = rows[i] switch
|
||||
{
|
||||
BoolOptionRow b => b.DefaultValue,
|
||||
FloatOptionRow f => f.DefaultValue,
|
||||
IntOptionRow n => n.DefaultValue,
|
||||
StringOptionRow s => s.DefaultValue,
|
||||
_ => throw new InvalidOperationException(
|
||||
$"row {i} has unexpected type {rows[i].GetType()}"),
|
||||
};
|
||||
Assert.True(
|
||||
Equals(expected[i], actual),
|
||||
$"row {i}: expected retail default {expected[i]} but row.DefaultValue was {actual}.");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bind_ResolvesOnlyTheAuthoredStringKeys_NoInventedOrDroppedKey()
|
||||
{
|
||||
// OP6 rework (review S5): the Character tab's registry-conformance
|
||||
// pattern ("an invented row or a dropped row fails the build")
|
||||
// applied to Config's label/tooltip/choice STRING KEYS — the exact
|
||||
// dimension M3's 1-vs-5-entry ChatFontFaceChoices bug lived in and
|
||||
// that LabelResolutionFailure... (null resolver) cannot see, since
|
||||
// a null resolver makes every key invisible. Every string this
|
||||
// page queries is transcribed here directly from
|
||||
// gmClient::InitUIPreferences/gmConfigUI::InitOptions (bare label
|
||||
// keys, their "<key>_Help" tooltip siblings, and every menu choice
|
||||
// key) — an extra query (a typo'd/invented key) fails immediately;
|
||||
// a query this test expected but never saw (a dropped key) fails
|
||||
// at the end.
|
||||
string[] expectedKeys =
|
||||
[
|
||||
// Headers
|
||||
"ID_Sound_SoundSection", "ID_Camera_CameraSection",
|
||||
"ID_Graphics_GraphicsSection", "ID_Graphics_TextureSection",
|
||||
"ID_Input_InputSection", "ID_UI_UISection",
|
||||
|
||||
// Sound section
|
||||
"ID_Sound_SoundFeatures", "ID_Sound_SoundFeatures_Help",
|
||||
"ID_Sound_Stereo", "ID_Sound_Mono",
|
||||
"ID_Sound_DisableSound", "ID_Sound_DisableSound_Help",
|
||||
"ID_Sound_EffectVolume_Help",
|
||||
"ID_Sound_DisableAmbientSound", "ID_Sound_DisableAmbientSound_Help",
|
||||
"ID_Sound_AmbientVolume_Help",
|
||||
"ID_Sound_DisableInterfaceSound", "ID_Sound_DisableInterfaceSound_Help",
|
||||
"ID_Sound_InterfaceVolume_Help",
|
||||
"ID_Sound_NoFocusNoSound", "ID_Sound_NoFocusNoSound_Help",
|
||||
|
||||
// Camera section
|
||||
"ID_Camera_Stiffness", "ID_Camera_Stiffness_Help",
|
||||
"ID_Graphics_Value_Soft", "ID_Graphics_Value_Hard",
|
||||
"ID_Camera_AdjustmentSpeed", "ID_Camera_AdjustmentSpeed_Help",
|
||||
"ID_Graphics_Value_Slow", "ID_Graphics_Value_Fast",
|
||||
"ID_Graphics_FieldOfView", "ID_Graphics_FieldOfView_Help",
|
||||
"ID_Graphics_Value_Narrow", "ID_Graphics_Value_Wide",
|
||||
"ID_Camera_AlignToSlope", "ID_Camera_AlignToSlope_Help",
|
||||
|
||||
// Graphics section
|
||||
"ID_Rendering_DisplayResolution", "ID_Rendering_DisplayResolution_Help",
|
||||
"ID_Rendering_FullScreen", "ID_Rendering_FullScreen_Help",
|
||||
"ID_Rendering_SyncToDisplayRefresh", "ID_Rendering_SyncToDisplayRefresh_Help",
|
||||
"ID_Graphics_ScreenBrightness", "ID_Graphics_ScreenBrightness_Help",
|
||||
"ID_Graphics_Value_Dark", "ID_Graphics_Value_Bright",
|
||||
"ID_Graphics_AdaptiveDegrade", "ID_Graphics_AdaptiveDegrade_Help",
|
||||
"ID_Graphics_AdaptiveDegradeBias", "ID_Graphics_AdaptiveDegradeBias_Help",
|
||||
"ID_Graphics_Value_Speed", "ID_Graphics_Value_Detail",
|
||||
"ID_Graphics_DegradeDistance", "ID_Graphics_DegradeDistance_Help",
|
||||
"ID_Graphics_Value_Close", "ID_Graphics_Value_Far",
|
||||
|
||||
// Rendering Quality section
|
||||
"ID_Graphics_LandscapeTextureDetail", "ID_Graphics_LandscapeTextureDetail_Help",
|
||||
"ID_Graphics_EnvironmentTextureDetail", "ID_Graphics_EnvironmentTextureDetail_Help",
|
||||
"ID_Graphics_Value_VeryLow", "ID_Graphics_Value_Low", "ID_Graphics_Value_Medium",
|
||||
"ID_Graphics_Value_High", "ID_Graphics_Value_VeryHigh",
|
||||
"ID_Graphics_TextureFiltering", "ID_Graphics_TextureFiltering_Help",
|
||||
"ID_Graphics_TextureFiltering_Bilinear", "ID_Graphics_TextureFiltering_Trilinear",
|
||||
"ID_Graphics_TextureFiltering_Sharp", "ID_Graphics_TextureFiltering_Anisotropic",
|
||||
"ID_Graphics_LandscapeDrawDistance", "ID_Graphics_LandscapeDrawDistance_Help",
|
||||
"ID_Graphics_Value_Extreme",
|
||||
"ID_Graphics_BuildingDetailTextures", "ID_Graphics_BuildingDetailTextures_Help",
|
||||
"ID_Graphics_MultiPassAlpha", "ID_Graphics_MultiPassAlpha_Help",
|
||||
|
||||
// Input section
|
||||
"ID_Input_MouseLookSensitivity", "ID_Input_MouseLookSensitivity_Help",
|
||||
"ID_Input_InvertMouseLookYAxis", "ID_Input_InvertMouseLookYAxis_Help",
|
||||
"ID_Input_UseMouseTurning", "ID_Input_UseMouseTurning_Help",
|
||||
|
||||
// UI section
|
||||
"ID_UI_ChatFontFace", "ID_UI_ChatFontFace_Help",
|
||||
"ID_UI_Value_Arial", "ID_UI_Value_CourierNew", "ID_UI_Value_PalatinoLinotype",
|
||||
"ID_UI_Value_Tahoma", "ID_UI_Value_TimesNewRoman",
|
||||
"ID_UI_ChatFontSize", "ID_UI_ChatFontSize_Help",
|
||||
"ID_UI_Value_Tiny", "ID_UI_Value_Small", "ID_UI_Value_Medium",
|
||||
"ID_UI_Value_Large", "ID_UI_Value_XLarge",
|
||||
];
|
||||
|
||||
var expectedByHash = new Dictionary<uint, string>();
|
||||
foreach (string key in expectedKeys)
|
||||
expectedByHash[DatStringResolver.ComputeHash(key)] = key;
|
||||
|
||||
var seen = new HashSet<string>();
|
||||
Func<uint, uint, string?> recordingResolver = (table, hash) =>
|
||||
{
|
||||
Assert.Equal(0x23000003u, table);
|
||||
Assert.True(
|
||||
expectedByHash.TryGetValue(hash, out string? key),
|
||||
$"resolveString queried hash 0x{hash:X8} — no key in the expected table hashes "
|
||||
+ "to this value. Either an invented/typo'd key was added, or this test's "
|
||||
+ "expected-key table is stale.");
|
||||
seen.Add(key!);
|
||||
return "x"; // any non-null value keeps the row-build path fully populated.
|
||||
};
|
||||
|
||||
(_, _, bool bound) = BindReal(recordingResolver);
|
||||
Assert.True(bound);
|
||||
|
||||
IEnumerable<string> missing = expectedByHash.Values.Except(seen);
|
||||
Assert.True(
|
||||
!missing.Any(),
|
||||
"Bind never queried these expected keys (a dropped row/key): "
|
||||
+ string.Join(", ", missing));
|
||||
}
|
||||
|
||||
// The tab host's own private per-page SLOT ids (OptionsPanelController's
|
||||
// ConfigPageId/ChatPageId) — the ids that actually survive base-merge in
|
||||
// the host-mounted tree (each page's own standalone-layout root id does
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue