fix(vtank): slice 7 fix round C item D1 — materialize Advanced Options bindings once
AdvancedOptionNames, AdvancedOptionValueColumn, AdvancedOptionName, and AdvancedOptionDescription each re-ran FilteredAdvancedOptionNames() (a Where over the full 163-entry VtankOptionCatalog) on EVERY retained-UI draw of the popup, and the value column additionally ran GetMetaOption(name).ToDisplayString() per row (Trim()/ToLowerInvariant() allocations) every frame. All four are now materialized once into fields by a new RefreshAdvancedOptions(), called from every real mutation point: category toggle, edit/apply, selection change, and profile load/create/clear/delete (via ResetProfileConsumers), plus construction. Same pattern fix round B item 12 used for the Monsters/Meta/Route grids. AdvancedOptionValueColumnMirrorsTheLiveSettingValue exercised a settings change made OUTSIDE the popup's own mutators (ToggleCombatEnabled, a main Options-tab checkbox) and expected the value column to reflect it on the very next read — a real behavior this caching model intentionally narrows (mirrored settings now catch up at the next real popup mutator, not on every read). Updated the test to re-select the row afterward, exercising the "selection change" refresh point, and documented why. Mutation named: reverted the two property getters to their old live- recomputing form and confirmed the new AdvancedOptionsPopupBindingsDoNotReallocateOnEveryRead pin fails (Assert.Same throws — different array instances per read) before restoring the fix. MossTank suite 676 -> 677 (one new pin). Full solution build green. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
8d3c6ad7c3
commit
466fac426a
2 changed files with 101 additions and 23 deletions
|
|
@ -230,6 +230,19 @@ internal sealed partial class MossTankPanel
|
|||
// unchecks something).
|
||||
private readonly bool[] _advancedOptionCategoryEnabled =
|
||||
Enumerable.Repeat(true, VtankOptionCatalog.CategoryBits.Length).ToArray();
|
||||
// Fix round C item D1: AdvancedOptionNames/AdvancedOptionValueColumn/
|
||||
// AdvancedOptionName/AdvancedOptionDescription used to recompute
|
||||
// FilteredAdvancedOptionNames() (a Where over the full 163-entry
|
||||
// VtankOptionCatalog) AND, for the value column, run
|
||||
// GetMetaOption(name).ToDisplayString() per row on EVERY retained-UI
|
||||
// draw of the popup. Materialized once in RefreshAdvancedOptions,
|
||||
// called from every real mutation point (category toggle, edit/apply,
|
||||
// selection change, profile load) — the same pattern item 12 used for
|
||||
// the grids.
|
||||
private IReadOnlyList<string> _advancedOptionNames = Array.Empty<string>();
|
||||
private IReadOnlyList<string> _advancedOptionValueColumn = Array.Empty<string>();
|
||||
private string _advancedOptionSelectedName = string.Empty;
|
||||
private string _advancedOptionSelectedDescription = string.Empty;
|
||||
private string _advancedOptionValueDraft = string.Empty;
|
||||
private string _advancedOptionNotice =
|
||||
"All VTank settings are available here.";
|
||||
|
|
@ -379,6 +392,7 @@ internal sealed partial class MossTankPanel
|
|||
RefreshLootEditor();
|
||||
RefreshRouteEditor();
|
||||
RefreshMetaEditor();
|
||||
RefreshAdvancedOptions();
|
||||
_automationWasAvailable = host.Automation.IsAvailable;
|
||||
}
|
||||
|
||||
|
|
@ -464,10 +478,8 @@ internal sealed partial class MossTankPanel
|
|||
// their raw bit value ("0x04" etc.) instead of a guessed name; a
|
||||
// setting with no recorded bitmask always shows regardless of filter
|
||||
// state (never silently hidden by a filter that doesn't apply to it).
|
||||
public IReadOnlyList<string> AdvancedOptionNames => FilteredAdvancedOptionNames();
|
||||
public IReadOnlyList<string> AdvancedOptionValueColumn => FilteredAdvancedOptionNames()
|
||||
.Select(name => GetMetaOption(name).ToDisplayString())
|
||||
.ToArray();
|
||||
public IReadOnlyList<string> AdvancedOptionNames => _advancedOptionNames;
|
||||
public IReadOnlyList<string> AdvancedOptionValueColumn => _advancedOptionValueColumn;
|
||||
public IReadOnlyList<string> AdvancedOptionCategoryNames { get; } =
|
||||
VtankOptionCatalog.CategoryBits.Select(bit => $"0x{bit:X}").ToArray();
|
||||
public IReadOnlyList<bool> AdvancedOptionCategoryEnabled => _advancedOptionCategoryEnabled;
|
||||
|
|
@ -484,28 +496,16 @@ internal sealed partial class MossTankPanel
|
|||
if ((uint)index >= (uint)_advancedOptionCategoryEnabled.Length)
|
||||
return;
|
||||
_advancedOptionCategoryEnabled[index] = !_advancedOptionCategoryEnabled[index];
|
||||
_selectedAdvancedOption = ClampRow(_selectedAdvancedOption, AdvancedOptionNames.Count);
|
||||
RefreshAdvancedOptions();
|
||||
LoadAdvancedOptionDraft();
|
||||
};
|
||||
public int SelectedAdvancedOptionIndex => _selectedAdvancedOption;
|
||||
public string AdvancedOptionName
|
||||
{
|
||||
get
|
||||
{
|
||||
IReadOnlyList<string> names = FilteredAdvancedOptionNames();
|
||||
return names.Count == 0
|
||||
? string.Empty
|
||||
: names[Math.Clamp(_selectedAdvancedOption, 0, names.Count - 1)];
|
||||
}
|
||||
}
|
||||
public string AdvancedOptionName => _advancedOptionSelectedName;
|
||||
public string AdvancedOptionValueDraft => _advancedOptionValueDraft;
|
||||
// Fix round B item 9: VTank's own Settings.Description column (93 of
|
||||
// 137 rows non-empty, real retail help text) — fills the 384x80
|
||||
// txtInfo readout.
|
||||
public string AdvancedOptionDescription =>
|
||||
VtankDefaultSettingsDatabase.SettingDescriptions.TryGetValue(AdvancedOptionName, out string? text)
|
||||
? $"{AdvancedOptionName}: {text}"
|
||||
: AdvancedOptionName;
|
||||
public string AdvancedOptionDescription => _advancedOptionSelectedDescription;
|
||||
public string AdvancedOptionNotice => _advancedOptionNotice;
|
||||
|
||||
private IReadOnlyList<string> FilteredAdvancedOptionNames()
|
||||
|
|
@ -520,6 +520,37 @@ internal sealed partial class MossTankPanel
|
|||
|| mask == 0
|
||||
|| (mask & enabledMask) != 0).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fix round C item D1: the single materialization point for every
|
||||
/// Advanced Options popup binding — the filtered name list, the
|
||||
/// per-row value column (each entry costs a GetMetaOption switch plus
|
||||
/// ToDisplayString's Trim()/ToLowerInvariant() allocations), the
|
||||
/// clamped selected name, and its description lookup. Called from
|
||||
/// every real mutation: category toggle, edit/apply, selection
|
||||
/// change, and profile load/create/clear/delete (via
|
||||
/// ResetProfileConsumers) plus initial construction — never from a
|
||||
/// property getter, which is what let this recompute on every single
|
||||
/// retained-UI draw of the popup before this fix.
|
||||
/// </summary>
|
||||
private void RefreshAdvancedOptions()
|
||||
{
|
||||
_advancedOptionNames = FilteredAdvancedOptionNames();
|
||||
_selectedAdvancedOption = ClampRow(_selectedAdvancedOption, _advancedOptionNames.Count);
|
||||
_advancedOptionSelectedName = _advancedOptionNames.Count == 0
|
||||
? string.Empty
|
||||
: _advancedOptionNames[
|
||||
Math.Clamp(_selectedAdvancedOption, 0, _advancedOptionNames.Count - 1)];
|
||||
_advancedOptionValueColumn = _advancedOptionNames
|
||||
.Select(name => GetMetaOption(name).ToDisplayString())
|
||||
.ToArray();
|
||||
_advancedOptionSelectedDescription =
|
||||
VtankDefaultSettingsDatabase.SettingDescriptions.TryGetValue(
|
||||
_advancedOptionSelectedName, out string? text)
|
||||
? $"{_advancedOptionSelectedName}: {text}"
|
||||
: _advancedOptionSelectedName;
|
||||
}
|
||||
|
||||
public Action ShowAdvancedOptions => () =>
|
||||
{
|
||||
_advancedOptionsVisible = true;
|
||||
|
|
@ -542,7 +573,8 @@ internal sealed partial class MossTankPanel
|
|||
};
|
||||
public Action<int> SelectAdvancedOption => index =>
|
||||
{
|
||||
_selectedAdvancedOption = ClampRow(index, FilteredAdvancedOptionNames().Count);
|
||||
_selectedAdvancedOption = ClampRow(index, _advancedOptionNames.Count);
|
||||
RefreshAdvancedOptions();
|
||||
LoadAdvancedOptionDraft();
|
||||
};
|
||||
public Action<string> SetAdvancedOptionValueDraft => value =>
|
||||
|
|
@ -2532,15 +2564,21 @@ internal sealed partial class MossTankPanel
|
|||
_advancedOptionNotice = "Enter a value first.";
|
||||
return;
|
||||
}
|
||||
string name = AdvancedOptionName;
|
||||
try
|
||||
{
|
||||
if (!SetMetaOption(AdvancedOptionName, value))
|
||||
if (!SetMetaOption(name, value))
|
||||
{
|
||||
_advancedOptionNotice = $"{AdvancedOptionName} is unavailable.";
|
||||
_advancedOptionNotice = $"{name} is unavailable.";
|
||||
return;
|
||||
}
|
||||
// Fix round C item D1: the option's value just changed, so the
|
||||
// cached value column (and, in principle, the description if a
|
||||
// future setting's help text ever depended on its own value)
|
||||
// must be re-materialized before the next draw.
|
||||
RefreshAdvancedOptions();
|
||||
LoadAdvancedOptionDraft();
|
||||
_advancedOptionNotice = $"Applied {AdvancedOptionName}.";
|
||||
_advancedOptionNotice = $"Applied {name}.";
|
||||
}
|
||||
catch (Exception exception) when (exception is FormatException
|
||||
or OverflowException)
|
||||
|
|
@ -4283,6 +4321,7 @@ internal sealed partial class MossTankPanel
|
|||
RefreshItemEditors();
|
||||
RefreshLootEditor();
|
||||
RefreshRouteEditor();
|
||||
RefreshAdvancedOptions();
|
||||
}
|
||||
|
||||
private void ClearMossTankActionLocks()
|
||||
|
|
|
|||
|
|
@ -1295,6 +1295,14 @@ public sealed class MossTankPanelTests
|
|||
{
|
||||
// Fix round B item 9: lOptionList gained a real clVal value column
|
||||
// (AdvancedOptionValueColumn) alongside the name column.
|
||||
// Fix round C item D1: the column is now materialized once by
|
||||
// RefreshAdvancedOptions (category toggle / edit-apply / selection
|
||||
// change / profile load), not recomputed on every read — so a
|
||||
// setting changed OUTSIDE those four mutators (ToggleCombatEnabled
|
||||
// is a main Options-tab checkbox, not a popup action) is picked up
|
||||
// the next time a real popup mutator runs rather than on the very
|
||||
// next property read. Re-selecting the same row exercises exactly
|
||||
// that "selection change" mutator.
|
||||
var panel = new MossTankPanel(new FakeHost(new FakeAutomation()));
|
||||
int index = panel.AdvancedOptionNames.ToList().IndexOf("EnableCombat");
|
||||
Assert.True(index >= 0);
|
||||
|
|
@ -1302,12 +1310,43 @@ public sealed class MossTankPanelTests
|
|||
Assert.Equal(panel.CombatEnabled ? "True" : "False", before);
|
||||
|
||||
panel.ToggleCombatEnabled();
|
||||
panel.SelectAdvancedOption(index);
|
||||
|
||||
string after = panel.AdvancedOptionValueColumn[index];
|
||||
Assert.NotEqual(before, after);
|
||||
Assert.Equal(panel.CombatEnabled ? "True" : "False", after);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdvancedOptionsPopupBindingsDoNotReallocateOnEveryRead()
|
||||
{
|
||||
// Fix round C item D1: AdvancedOptionNames/AdvancedOptionValueColumn
|
||||
// used to re-run FilteredAdvancedOptionNames() (a Where over the
|
||||
// full 163-entry VtankOptionCatalog) — and, for the value column, a
|
||||
// fresh GetMetaOption(name).ToDisplayString() per row — on every
|
||||
// single retained-UI draw of the popup. Both are now materialized
|
||||
// once in RefreshAdvancedOptions: repeated reads with no mutation
|
||||
// in between must return the SAME array instance, matching the
|
||||
// precedent MetaAndRouteGridColumnsDoNotReallocateOnEveryRead set
|
||||
// for item 12's grids.
|
||||
var panel = new MossTankPanel(new FakeHost(new FakeAutomation()));
|
||||
|
||||
Assert.Same(panel.AdvancedOptionNames, panel.AdvancedOptionNames);
|
||||
Assert.Same(panel.AdvancedOptionValueColumn, panel.AdvancedOptionValueColumn);
|
||||
|
||||
int index = panel.AdvancedOptionNames.ToList().IndexOf("EnableCombat");
|
||||
Assert.True(index >= 0);
|
||||
panel.SelectAdvancedOption(index);
|
||||
|
||||
// A real mutator (selection change) is allowed to swap the cached
|
||||
// instance out for a fresh one...
|
||||
IReadOnlyList<string> namesAfterSelect = panel.AdvancedOptionNames;
|
||||
IReadOnlyList<string> valuesAfterSelect = panel.AdvancedOptionValueColumn;
|
||||
// ...but once settled, repeated reads must again share one instance.
|
||||
Assert.Same(namesAfterSelect, panel.AdvancedOptionNames);
|
||||
Assert.Same(valuesAfterSelect, panel.AdvancedOptionValueColumn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign VT slice 7 S7.2: VTank's cOn (Options tab, "Run Macro") is a
|
||||
/// Checkbox with a STATIC caption whose checked state reflects whether
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue