feat(vtank): slice 7 fix round B item 9 — Advanced Options gets clVal, lFilterList, and a real description readout

VTank's own AdvancedOptionsView (docs/research/vtank-kb/08-ui-views.md §1's
secondary-view table): lOptionList is clOpt(180)+clVal(62) two columns, not
a name-only list; lFilterList (268,4,120,180) is a category checklist
(check+text columns) filtering it; txtInfo (4,188,384,80) is a description
readout.

- lOptionList is now a real 2-column <list><column> grid: name (clOpt,
  PITCH 187) + live value (clVal, PITCH 69, AdvancedOptionValueColumn via
  the existing GetMetaOption/.ToDisplayString() pipeline).
- lFilterList genuinely filters lOptionList: VtankDefaultSettingsDatabase
  now parses VTank's own SettingsCategories table (136 rows straight from
  the embedded .usd, 2 columns: Setting name + Categories bitmask) into
  VtankOptionCatalog.CategoryBits (9 distinct bits, 0x01-0x100). A setting
  with no recorded bitmask always shows regardless of filter state. This
  worktree has no refs/vtank/ checkout, so VTank's own category NAME
  strings for those 9 bits aren't available anywhere in this repo — the
  checklist honestly labels each by its raw bit value ("0x04" etc.)
  instead of a guessed name.
- AdvancedOptionDescription surfaces VTank's own real Settings.Description
  column (93 of 137 rows non-empty, genuine retail help text — e.g.
  DoHelp's own wording about fellowship healing) prefixed with the option
  name, filling the exact 384x80 readout and replacing the old bare
  name-only label.
- SelectAdvancedOption/AdvancedOptionName now resolve through the FILTERED
  list (not the raw 137-row catalog) so selection stays correct as the
  filter narrows/widens it.
- Apply/Back move beside each other below the two lists to make room;
  popup height grows 392 -> 476 for the taller lists + description block,
  pushing item 8's "MossTank Extras" section further down (unchanged
  internally).

New tests: AdvancedOptionCategoryFilterHidesNonMatchingSettings (unchecking
every category but EnableLooting's own 0x100 hides EnableNav but keeps
EnableLooting; re-checking restores the full list),
AdvancedOptionDescriptionSurfacesRealRetailHelpText,
AdvancedOptionValueColumnMirrorsTheLiveSettingValue. All three mutation-
checked: disabling FilteredAdvancedOptionNames's filter predicate, the
description's name-prefix, and the value column's GetMetaOption call each
turned their test red; restoring each turns it green.

tests/AcDream.Plugins.MossTank.Tests: 671/671 (was 668/668, +3).
tests/AcDream.App.Tests --filter Markup|Plugin|UiMenu|Slider: 276/3 skipped/279 (unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 12:54:49 +02:00
parent 95c93858a1
commit 328faea995
6 changed files with 266 additions and 42 deletions

View file

@ -188,6 +188,11 @@ internal sealed partial class MossTankPanel
private bool _lootEditorVisible;
private bool _advancedOptionsVisible;
private int _selectedAdvancedOption;
// Fix round B item 9: one enabled flag per VtankOptionCatalog.CategoryBits
// entry, all true by default (no filter narrows the list until the user
// unchecks something).
private readonly bool[] _advancedOptionCategoryEnabled =
Enumerable.Repeat(true, VtankOptionCatalog.CategoryBits.Length).ToArray();
private string _advancedOptionValueDraft = string.Empty;
private string _advancedOptionNotice =
"All VTank settings are available here.";
@ -398,15 +403,65 @@ internal sealed partial class MossTankPanel
public bool MetaVisible => MetaSelected;
public bool LootEditorVisible => _lootEditorVisible;
public bool AdvancedOptionsVisible => _advancedOptionsVisible;
public IReadOnlyList<string> AdvancedOptionNames => VtankOptionCatalog.Names;
// Fix round B item 9: VTank's own AdvancedOptionsView (docs/research/
// vtank-kb/08-ui-views.md §1) pairs lOptionList's clOpt name column
// with a clVal VALUE column, and filters both through lFilterList — a
// category checklist keyed by SettingsCategories' own per-setting
// bitmask (VtankDefaultSettingsDatabase.SettingCategoryBitmasks, 136
// rows straight from the embedded .usd). This worktree has no
// refs/vtank/ checkout, so VTank's own category NAME strings aren't
// available anywhere in this repo — the filter groups are labeled by
// 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> AdvancedOptionCategoryNames { get; } =
VtankOptionCatalog.CategoryBits.Select(bit => $"0x{bit:X}").ToArray();
public IReadOnlyList<bool> AdvancedOptionCategoryEnabled => _advancedOptionCategoryEnabled;
public Action<int> ToggleAdvancedOptionCategoryAt => index =>
{
if ((uint)index >= (uint)_advancedOptionCategoryEnabled.Length)
return;
_advancedOptionCategoryEnabled[index] = !_advancedOptionCategoryEnabled[index];
_selectedAdvancedOption = ClampRow(_selectedAdvancedOption, AdvancedOptionNames.Count);
LoadAdvancedOptionDraft();
};
public int SelectedAdvancedOptionIndex => _selectedAdvancedOption;
public string AdvancedOptionName => VtankOptionCatalog.Names[
Math.Clamp(
_selectedAdvancedOption,
0,
VtankOptionCatalog.Names.Length - 1)];
public string AdvancedOptionName
{
get
{
IReadOnlyList<string> names = FilteredAdvancedOptionNames();
return names.Count == 0
? string.Empty
: names[Math.Clamp(_selectedAdvancedOption, 0, names.Count - 1)];
}
}
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 AdvancedOptionNotice => _advancedOptionNotice;
private IReadOnlyList<string> FilteredAdvancedOptionNames()
{
int enabledMask = 0;
for (int i = 0; i < VtankOptionCatalog.CategoryBits.Length; i++)
if (_advancedOptionCategoryEnabled[i])
enabledMask |= VtankOptionCatalog.CategoryBits[i];
return VtankOptionCatalog.Names.Where(name =>
!VtankDefaultSettingsDatabase.SettingCategoryBitmasks.TryGetValue(name, out int mask)
|| mask == 0
|| (mask & enabledMask) != 0).ToArray();
}
public Action ShowAdvancedOptions => () =>
{
_advancedOptionsVisible = true;
@ -429,10 +484,7 @@ internal sealed partial class MossTankPanel
};
public Action<int> SelectAdvancedOption => index =>
{
_selectedAdvancedOption = Math.Clamp(
index,
0,
VtankOptionCatalog.Names.Length - 1);
_selectedAdvancedOption = ClampRow(index, FilteredAdvancedOptionNames().Count);
LoadAdvancedOptionDraft();
};
public Action<string> SetAdvancedOptionValueDraft => value =>

View file

@ -22,6 +22,10 @@ internal static class VtankDefaultSettingsDatabase
private const string ResourceSuffix = ".VtankDefaultSettings.usd";
private static readonly Lazy<string> RawText = new(LoadText);
private static readonly Lazy<RechargeHandlerRow[]> DefaultRows = new(LoadDefaultRows);
private static readonly Lazy<IReadOnlyDictionary<string, int>> CategoryBitmasksByName =
new(LoadCategoryBitmasks);
private static readonly Lazy<IReadOnlyDictionary<string, string>> DescriptionsByName =
new(LoadDescriptions);
/// <summary>A fresh, independently mutable parse of the embedded document — every call gets its own object graph.</summary>
public static VtankDatabase Parse() => VtankDatabase.Parse(RawText.Value);
@ -29,6 +33,63 @@ internal static class VtankDefaultSettingsDatabase
/// <summary>VTank's own shipped <c>RechargeHandlerSet</c> rows (26, in file order).</summary>
public static IReadOnlyList<RechargeHandlerRow> DefaultRechargeHandlerRows => DefaultRows.Value;
/// <summary>
/// VTank's own <c>SettingsCategories</c> table (2 columns, 136 rows:
/// <c>Setting</c> name, <c>Categories</c> bitmask) — the data
/// <c>lFilterList</c> (docs/research/vtank-kb/08-ui-views.md §1's
/// AdvancedOptionsView table) filters <c>lOptionList</c> by. This
/// worktree has no access to VTank's own category NAME strings (no
/// <c>refs/vtank/</c> checkout here — see fix round B item 9's own
/// binding-site comment); only the raw per-setting bitmask is real
/// data we can port from the embedded .usd.
/// </summary>
public static IReadOnlyDictionary<string, int> SettingCategoryBitmasks => CategoryBitmasksByName.Value;
/// <summary>
/// VTank's own <c>Settings</c> table <c>Description</c> column (93 of
/// 137 rows non-empty) — real retail help text, e.g. "DoHelp" =>
/// "If true, allies are healed/restamed/given mana. The fellowship
/// window must be open to help fellows." Powers fix round B item 9's
/// <c>txtInfo</c> readout.
/// </summary>
public static IReadOnlyDictionary<string, string> SettingDescriptions => DescriptionsByName.Value;
private static IReadOnlyDictionary<string, string> LoadDescriptions()
{
VtankDatabase database = VtankDatabase.Parse(RawText.Value);
VtankTable? settings = database.Find("Settings");
var map = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (settings is null)
return map;
int nameColumn = settings.ColumnIndex("Setting");
int descriptionColumn = settings.ColumnIndex("Description");
if (nameColumn < 0 || descriptionColumn < 0)
return map;
foreach (VtankRow row in settings.Rows)
{
string description = row.Cells[descriptionColumn].AsString();
if (!string.IsNullOrWhiteSpace(description))
map[row.Cells[nameColumn].AsString()] = description;
}
return map;
}
private static IReadOnlyDictionary<string, int> LoadCategoryBitmasks()
{
VtankDatabase database = VtankDatabase.Parse(RawText.Value);
VtankTable? categories = database.Find("SettingsCategories");
var map = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
if (categories is null)
return map;
int nameColumn = categories.ColumnIndex("Setting");
int bitsColumn = categories.ColumnIndex("Categories");
if (nameColumn < 0 || bitsColumn < 0)
return map;
foreach (VtankRow row in categories.Rows)
map[row.Cells[nameColumn].AsString()] = row.Cells[bitsColumn].AsInt();
return map;
}
private static string LoadText()
{
Assembly assembly = typeof(VtankDefaultSettingsDatabase).Assembly;

View file

@ -389,4 +389,25 @@ internal static class VtankOptionCatalog
DeclaredTypes.TryGetValue(name, out VtankSettingValueType type)
? type
: VtankSettingValueType.Double;
/// <summary>
/// Every individual bit (power of two) actually used across
/// <see cref="VtankDefaultSettingsDatabase.SettingCategoryBitmasks"/>'s
/// values, ascending — the real filter groups fix round B item 9's
/// <c>lFilterList</c> checklist offers, since a row's own mask can be a
/// sum of several category bits (e.g. 12 = 4+8).
/// </summary>
internal static readonly int[] CategoryBits = VtankDefaultSettingsDatabase
.SettingCategoryBitmasks.Values
.SelectMany(DecomposeBits)
.Distinct()
.Order()
.ToArray();
private static IEnumerable<int> DecomposeBits(int mask)
{
for (int bit = 1; bit != 0 && bit <= mask; bit <<= 1)
if ((mask & bit) != 0)
yield return bit;
}
}

View file

@ -45,45 +45,73 @@
stepper (a real duplicate of Options' own editable
FollowNavMinimumValueText field — kept for symmetry with the other
extras, not deleted, since both write the same
NavigationSettings.MinimumDistanceMeters). Panel height grows 300->392
to fit this section without touching the retail editor above it; item 9
is expected to reshuffle the retail portion further (clVal column,
lFilterList checklist, description readout) without moving this section.
Bold text isn't representable in the plain retail UI font (0x40000000
has no bold face) — the section header uses the same bright caption
color other headers use instead. -->
<panel x="253" y="405" w="392" h="392" title="MossTank Advanced Options"
NavigationSettings.MinimumDistanceMeters).
Fix round B item 9 reshuffled the retail portion to match
AdvancedOptionsView's own geometry (docs/research/vtank-kb/
08-ui-views.md §1's secondary-view table) instead of the single-column
name-only list item 8 left in place: lOptionList is now a real 2-column
grid (clOpt name + clVal live value, PITCH 180+7=187 / 62+7=69) sitting
beside lFilterList (268,4,120,180), a real category checklist
(check + text columns) that filters lOptionList by VTank's own
SettingsCategories bitmask (VtankDefaultSettingsDatabase — 136 rows
straight from the embedded .usd, 9 distinct bits). This worktree has NO
refs/vtank/ checkout, so VTank's own category NAME strings for those 9
bits aren't available anywhere in this repo; the checklist labels each
by its raw bit value ("0x04" etc.) rather than a guessed name — a
documented, honest gap, not silently dropped. The description readout
(AdvancedOptionDescription) fills VTank's exact 384x80 txtInfo and now
surfaces VTank's own real per-setting help text (Settings.Description,
93 of 137 rows non-empty, e.g. DoHelp's own retail wording) prefixed
with the option's name — replacing the old bare name-only label, which
is dropped as redundant. Apply/Back move beside each other under the
two lists (VTank's own popup has neither; acdream keeps them for the
draft-then-submit editing flow the plain key-value grammar needs).
Panel height grows 392->476 for the taller two-list row plus the
description block; the "MossTank Extras" section (item 8) shifts down
to keep sitting below everything the retail portion now needs. Bold
text isn't representable in the plain retail UI font (0x40000000 has no
bold face) — the section header uses the same bright caption color
other headers use instead. -->
<panel x="253" y="405" w="392" h="476" title="MossTank Advanced Options"
visible="{AdvancedOptionsVisible}" resize="none">
<list x="4" y="22" w="260" h="160" rowheight="17"
items="{AdvancedOptionNames}" selected="{SelectedAdvancedOptionIndex}"
onchange="{SelectAdvancedOption}"
tooltip="Select one of Virindi Tank's 137 advanced options." />
<label x="272" y="22" w="116" h="16" text="{AdvancedOptionName}" color="#FFE8DEC3" />
<button x="272" y="42" w="116" h="23" text="Apply"
onclick="{ApplyAdvancedOption}" />
<button x="272" y="70" w="116" h="23" text="Back"
onclick="{HideAdvancedOptions}" />
<field x="4" y="188" w="260" h="23" text="{AdvancedOptionValueDraft}"
<list x="4" y="4" w="256" h="160" rowheight="17"
selected="{SelectedAdvancedOptionIndex}" onchange="{SelectAdvancedOption}"
tooltip="Select one of Virindi Tank's 137 advanced options.">
<column type="text" width="187" items="{AdvancedOptionNames}" onclick="{SelectAdvancedOption}" />
<column type="text" width="69" items="{AdvancedOptionValueColumn}" onclick="{SelectAdvancedOption}" />
</list>
<list x="268" y="4" w="120" h="180" rowheight="18"
tooltip="Uncheck a category to hide its settings from the list on the left.">
<column type="check" width="20" values="{AdvancedOptionCategoryEnabled}" onchange="{ToggleAdvancedOptionCategoryAt}" />
<column type="text" width="*" items="{AdvancedOptionCategoryNames}" onclick="{ToggleAdvancedOptionCategoryAt}" />
</list>
<field x="4" y="168" w="260" h="16" text="{AdvancedOptionValueDraft}"
onchange="{SetAdvancedOptionValueDraft}" onsubmit="{SubmitAdvancedOption}"
maxlength="160" clearonsubmit="false" background="#E6000000"
tooltip="Edit the selected advanced-option value and press Enter or Apply." />
<label x="4" y="216" w="384" h="16" text="{AdvancedOptionNotice}" color="#FFC7B98F" />
<label x="4" y="188" w="384" h="80" text="{AdvancedOptionDescription}" color="#FFE8DEC3" />
<button x="4" y="272" w="116" h="23" text="Apply"
onclick="{ApplyAdvancedOption}" />
<button x="128" y="272" w="116" h="23" text="Back"
onclick="{HideAdvancedOptions}" />
<label x="4" y="298" w="384" h="16" text="{AdvancedOptionNotice}" color="#FFC7B98F" />
<label x="4" y="236" w="200" h="16" text="MossTank Extras" color="#FFE8DEC3" />
<button x="4" y="254" w="90" h="18" text="Checkpoint" onclick="{AddRouteCheckpoint}" />
<button x="100" y="254" w="90" h="18" text="Jump" onclick="{AddRouteJump}" />
<button x="4" y="276" w="90" h="18" text="Remove" onclick="{RemoveRouteWaypoint}" />
<button x="100" y="276" w="140" h="18" text="Set Follow Target" onclick="{SetFollowTarget}" />
<label x="4" y="298" w="376" h="16" text="{RouteFollowTargetText}" color="#FFC7B98F" />
<toggle x="4" y="318" w="140" h="18" text="Follow Corners"
<label x="4" y="320" w="200" h="16" text="MossTank Extras" color="#FFE8DEC3" />
<button x="4" y="338" w="90" h="18" text="Checkpoint" onclick="{AddRouteCheckpoint}" />
<button x="100" y="338" w="90" h="18" text="Jump" onclick="{AddRouteJump}" />
<button x="4" y="360" w="90" h="18" text="Remove" onclick="{RemoveRouteWaypoint}" />
<button x="100" y="360" w="140" h="18" text="Set Follow Target" onclick="{SetFollowTarget}" />
<label x="4" y="382" w="376" h="16" text="{RouteFollowTargetText}" color="#FFC7B98F" />
<toggle x="4" y="402" w="140" h="18" text="Follow Corners"
checked="{FollowAroundCornersEnabled}" onclick="{ToggleFollowAroundCorners}" />
<toggle x="200" y="318" w="140" h="18" text="Open Doors"
<toggle x="200" y="402" w="140" h="18" text="Open Doors"
checked="{OpenDoorsEnabled}" onclick="{ToggleOpenDoors}" />
<toggle x="4" y="340" w="140" h="18" text="Nav Priority"
<toggle x="4" y="424" w="140" h="18" text="Nav Priority"
checked="{NavigationPriorityEnabled}" onclick="{ToggleNavigationPriority}" />
<label x="4" y="362" w="180" h="16" text="{RouteMinimumDistanceText}" color="#FFC7B98F" />
<button x="190" y="362" w="24" h="18" text="-"
<label x="4" y="446" w="180" h="16" text="{RouteMinimumDistanceText}" color="#FFC7B98F" />
<button x="190" y="446" w="24" h="18" text="-"
onclick="{RouteMinimumDistanceDown}" tooltip="Decrease the route arrival distance." />
<button x="218" y="362" w="24" h="18" text="+"
<button x="218" y="446" w="24" h="18" text="+"
onclick="{RouteMinimumDistanceUp}" tooltip="Increase the route arrival distance." />
</panel>