fix(vtank): slice 7 round D item 2 — VTank's Advanced Options click model

Owner live report 2026-09-07, screenshots 2/3 (relayed VTank click
model — this worktree has no refs/vtank/ checkout to read
db.cs:118-165 directly): clicking a VALUE cell used to always just
select the row, no matter the setting's type. VTank's own model
dispatches by declared type: a tBool value flips in place, a tEnum
value cycles to the next label, and only int/double/single/string
select the row and load the current value into the edit field below
for typing. Apply/Back buttons are gone entirely (retail's own
AdvancedOptionsView has neither; Enter on the field already applies
via its existing onsubmit) — the popup closes from its own title bar
or the Options tab's toggle, both already independent of this content.

Changes:
- VtankDefaultSettingsDatabase.SettingEnumValues parses VTank's own
  SettingsEnumInfo table (3 columns, 33 rows: Setting/Value/EnumValue)
  from the embedded .usd — the real per-setting enum code->label table
  (UseArcs: 1=No, 2=At Range, 3=Yes), not a hand-typed guess.
- MossTankPanel.DisplayAdvancedOptionValue shows the enum LABEL for
  Enum-typed settings in the value column instead of the raw stored
  integer.
- MossTankPanel.ClickAdvancedOptionValue dispatches to
  FlipAdvancedOptionBool / CycleAdvancedOptionEnum (wraps past the
  last entry) / SelectAdvancedOption by VtankOptionCatalog.DeclaredType.
- mosstank-advanced.xml: the value column's onclick is now
  {ClickAdvancedOptionValue}; Apply/Back buttons removed; the notice
  label and the whole "MossTank Extras" section below it moved up 26px
  to reclaim the space; panel height 476->450.
- Removed the now-unreferenced ApplyAdvancedOption wrapper property
  (ApplyAdvancedOptionCore is still used by the field's own onsubmit).

Mutations shown to fail: (1) reducing ClickAdvancedOptionValue to a
bare SelectAdvancedOption(index) call failed both the bool-flip test
(expected False, got True — the click never flipped it) and the
enum-cycle test (expected "At Range", got "No" — the click never
advanced); the numeric-select test correctly stayed green since
select-only is still its own expected behavior. Restored and
confirmed green.

Verified: dotnet build AcDream.slnx -c Release green; MossTank suite
682/682 (679 -> 682, three new interaction tests); App markup/plugin
filter 242/242 (unchanged, mosstank-advanced.xml's new footprint
392x450 updated in ExpectedPopupBounds).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 16:30:10 +02:00
parent ebe670adf5
commit 435ced86f7
5 changed files with 288 additions and 41 deletions

View file

@ -578,7 +578,7 @@ internal sealed partial class MossTankPanel
: _advancedOptionNames[
Math.Clamp(_selectedAdvancedOption, 0, _advancedOptionNames.Count - 1)];
_advancedOptionValueColumn = _advancedOptionNames
.Select(name => GetMetaOption(name).ToDisplayString())
.Select(DisplayAdvancedOptionValue)
.ToArray();
_advancedOptionSelectedDescription =
VtankDefaultSettingsDatabase.SettingDescriptions.TryGetValue(
@ -587,6 +587,29 @@ internal sealed partial class MossTankPanel
: _advancedOptionSelectedName;
}
/// <summary>
/// Round D item 2 (owner's VTank interaction model, screenshots 2/3):
/// an enum-typed setting's value column shows the LABEL
/// (VtankDefaultSettingsDatabase.SettingEnumValues, VTank's own
/// SettingsEnumInfo table — e.g. "No"/"At Range"/"Yes" for UseArcs),
/// never the raw stored integer. Every other type keeps its plain
/// ToDisplayString() text.
/// </summary>
private string DisplayAdvancedOptionValue(string name)
{
ExpressionValue value = GetMetaOption(name);
if (VtankOptionCatalog.DeclaredType(name) == VtankSettingValueType.Enum
&& VtankDefaultSettingsDatabase.SettingEnumValues.TryGetValue(
name, out IReadOnlyList<VtankEnumValue>? entries))
{
int current = value.AsInt32();
foreach (VtankEnumValue entry in entries)
if (entry.Value == current)
return entry.Label;
}
return value.ToDisplayString();
}
public Action ShowAdvancedOptions => () =>
{
_advancedOptionsVisible = true;
@ -613,6 +636,36 @@ internal sealed partial class MossTankPanel
RefreshAdvancedOptions();
LoadAdvancedOptionDraft();
};
/// <summary>
/// Round D item 2: VTank's own Advanced Options click model
/// (owner-relayed <c>db.cs:118-165</c> behavior, screenshots 2/3 — this
/// worktree has no refs/vtank/ checkout to read the decompile directly).
/// Clicking the VALUE cell dispatches by the setting's declared type:
/// a <c>tBool</c> flips in place, a <c>tEnum</c> cycles to the next
/// label, and everything else (int/double/single/string/the one
/// Custom row) just selects the row and loads its current value into
/// the edit field at the bottom — the same behavior every click used
/// to have. Only numbers (and strings) are actually typed/submitted
/// through that field; booleans and enums never need it.
/// </summary>
public Action<int> ClickAdvancedOptionValue => index =>
{
if ((uint)index >= (uint)_advancedOptionNames.Count)
return;
string name = _advancedOptionNames[index];
switch (VtankOptionCatalog.DeclaredType(name))
{
case VtankSettingValueType.Bool:
FlipAdvancedOptionBool(index, name);
break;
case VtankSettingValueType.Enum:
CycleAdvancedOptionEnum(index, name);
break;
default:
SelectAdvancedOption(index);
break;
}
};
public Action<string> SetAdvancedOptionValueDraft => value =>
_advancedOptionValueDraft = value;
public Action<string> SubmitAdvancedOption => value =>
@ -620,7 +673,6 @@ internal sealed partial class MossTankPanel
_advancedOptionValueDraft = value;
ApplyAdvancedOptionCore();
};
public Action ApplyAdvancedOption => ApplyAdvancedOptionCore;
/// <summary>The button is Force Buff; while a pass runs it cancels.</summary>
public string BuffButtonText => _running ? "Stop buffing" : "Force buff";
@ -2622,6 +2674,57 @@ internal sealed partial class MossTankPanel
}
}
/// <summary>
/// Round D item 2: clicking a <c>tBool</c> row's VALUE cell flips it in
/// place — no edit field, no Apply click, matching the owner's relayed
/// VTank click model.
/// </summary>
private void FlipAdvancedOptionBool(int index, string name)
{
_selectedAdvancedOption = index;
ExpressionValue next = ExpressionValue.Boolean(!GetMetaOption(name).IsTruthy);
_advancedOptionNotice = SetMetaOption(name, next)
? $"Set {name} = {next.ToDisplayString()}."
: $"{name} is unavailable.";
RefreshAdvancedOptions();
LoadAdvancedOptionDraft();
}
/// <summary>
/// Round D item 2: clicking a <c>tEnum</c> row's VALUE cell advances to
/// the NEXT row of VTank's own <c>SettingsEnumInfo</c> table for that
/// setting (wrapping past the last entry back to the first). A current
/// value with no matching entry (should not happen against the real
/// data) starts the cycle at the first entry rather than throwing.
/// </summary>
private void CycleAdvancedOptionEnum(int index, string name)
{
_selectedAdvancedOption = index;
if (!VtankDefaultSettingsDatabase.SettingEnumValues.TryGetValue(
name, out IReadOnlyList<VtankEnumValue>? entries)
|| entries.Count == 0)
{
SelectAdvancedOption(index);
return;
}
int current = GetMetaOption(name).AsInt32();
int currentPosition = -1;
for (int i = 0; i < entries.Count; i++)
{
if (entries[i].Value == current)
{
currentPosition = i;
break;
}
}
VtankEnumValue nextEntry = entries[(currentPosition + 1) % entries.Count];
_advancedOptionNotice = SetMetaOption(name, ExpressionValue.Number(nextEntry.Value))
? $"Set {name} = {nextEntry.Label}."
: $"{name} is unavailable.";
RefreshAdvancedOptions();
LoadAdvancedOptionDraft();
}
/// <summary>VTank: row 0 is always DEFAULT and can never be empty.</summary>
private void EnsureDefaultMonsterRule()
{

View file

@ -26,6 +26,8 @@ internal static class VtankDefaultSettingsDatabase
new(LoadCategoryBitmasks);
private static readonly Lazy<IReadOnlyDictionary<string, string>> DescriptionsByName =
new(LoadDescriptions);
private static readonly Lazy<IReadOnlyDictionary<string, IReadOnlyList<VtankEnumValue>>>
EnumValuesByName = new(LoadEnumValues);
/// <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);
@ -54,6 +56,54 @@ internal static class VtankDefaultSettingsDatabase
/// </summary>
public static IReadOnlyDictionary<string, string> SettingDescriptions => DescriptionsByName.Value;
/// <summary>
/// VTank's own <c>SettingsEnumInfo</c> table (3 columns, 33 rows:
/// <c>Setting</c> name, integer <c>Value</c>, <c>EnumValue</c> label —
/// e.g. <c>UseArcs</c> = 1 "No", 2 "At Range", 3 "Yes"). Owner-driven
/// Advanced Options interaction model (2026-09-07, VTank's own
/// <c>db.cs:118-165</c> click behavior): clicking a <c>tEnum</c> row's
/// VALUE cell cycles to the NEXT row here (wrapping), and the value
/// column shows the label, not the raw stored integer. Rows are
/// returned in the file's own order, which is already ascending by
/// <c>Value</c> within each setting's group.
/// </summary>
public static IReadOnlyDictionary<string, IReadOnlyList<VtankEnumValue>> SettingEnumValues =>
EnumValuesByName.Value;
private static IReadOnlyDictionary<string, IReadOnlyList<VtankEnumValue>> LoadEnumValues()
{
VtankDatabase database = VtankDatabase.Parse(RawText.Value);
VtankTable? enumInfo = database.Find("SettingsEnumInfo");
var map = new Dictionary<string, List<VtankEnumValue>>(StringComparer.OrdinalIgnoreCase);
if (enumInfo is null)
return map.ToDictionary(
static pair => pair.Key,
static pair => (IReadOnlyList<VtankEnumValue>)pair.Value,
StringComparer.OrdinalIgnoreCase);
int nameColumn = enumInfo.ColumnIndex("Setting");
int valueColumn = enumInfo.ColumnIndex("Value");
int labelColumn = enumInfo.ColumnIndex("EnumValue");
if (nameColumn < 0 || valueColumn < 0 || labelColumn < 0)
return map.ToDictionary(
static pair => pair.Key,
static pair => (IReadOnlyList<VtankEnumValue>)pair.Value,
StringComparer.OrdinalIgnoreCase);
foreach (VtankRow row in enumInfo.Rows)
{
string name = row.Cells[nameColumn].AsString();
var entry = new VtankEnumValue(
row.Cells[valueColumn].AsInt(),
row.Cells[labelColumn].AsString());
if (!map.TryGetValue(name, out List<VtankEnumValue>? list))
map[name] = list = [];
list.Add(entry);
}
return map.ToDictionary(
static pair => pair.Key,
static pair => (IReadOnlyList<VtankEnumValue>)pair.Value,
StringComparer.OrdinalIgnoreCase);
}
private static IReadOnlyDictionary<string, string> LoadDescriptions()
{
VtankDatabase database = VtankDatabase.Parse(RawText.Value);
@ -126,3 +176,6 @@ internal static class VtankDefaultSettingsDatabase
return [];
}
}
/// <summary>One row of VTank's <c>SettingsEnumInfo</c> table: an enum-typed setting's integer code and its display label.</summary>
internal readonly record struct VtankEnumValue(int Value, string Label);

View file

@ -55,31 +55,56 @@
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"
straight from the embedded .usd, 9 distinct bits). Round D item 1
replaced lFilterList's raw bit-value labels ("0x04" etc.) with VTank's
own category names (Misc, Recharge, MeleeCombat, SpellCombat, Ranges,
Navigation, Buffing, Crafting, Looting — VtankOptionCatalog.
CategoryNamesByBit). The description readout (AdvancedOptionDescription)
fills VTank's exact 384x80 txtInfo and 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.
Round D item 2 (owner live report 2026-09-07, screenshots 2/3 —
relayed VTank click model, this worktree has no refs/vtank/ checkout to
read db.cs:118-165 directly): clicking a VALUE cell now dispatches by
the setting's declared type instead of always just selecting the row.
A tBool value flips in place; a tEnum value cycles to the next label
(VtankDefaultSettingsDatabase.SettingEnumValues, VTank's own
SettingsEnumInfo table); everything else (int/double/single/string)
selects the row and loads its current value into the field below for
typing — the SAME behavior every click used to have, now narrowed to
only the types that actually need an edit box. Apply/Back are REMOVED
entirely: retail's own AdvancedOptionsView has neither (VTank edits are
destructive-live), and acdream's own draft-then-submit flow no longer
needs a button once Enter (the field's own onsubmit) is the only path
a number/string value ever takes — the popup closes from its own
title bar or the Options tab's "Advanced Options" toggle, both already
wired independently of this content (see the top-of-file note on
AdvancedOptionsVisible/PluginWindowVisibilityController). Removing the
23px button row plus its 3px gap to the notice label reclaims 26px:
the notice label and every "MossTank Extras" control below it moved up
by that amount, and the panel's own height shrinks 476->450 to match
(438px of real content + 12px bottom padding, the same margin the
file used before this round).
Fix round B item 8's own "MossTank Extras" section (controls with no
VTank-tab counterpart, moved here per the owner's silhouette rule)
is otherwise unchanged: Checkpoint/Jump (AddRouteCheckpoint/
AddRouteJump), Remove (RemoveRouteWaypoint), Set Follow Target + its
status label, Follow Corners/Open Doors (navigation toggles with no
Route-tab VTank equivalent), Nav Priority (a real second copy of
Options' own "Boost Nav. Priority"), and the Follow/Nav Min Distance
+/- stepper (a real duplicate of Options' own editable
FollowNavMinimumValueText field). 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="450" title="MossTank Advanced Options"
visible="{AdvancedOptionsVisible}" resize="none">
<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}" />
<column type="text" width="69" items="{AdvancedOptionValueColumn}" onclick="{ClickAdvancedOptionValue}" />
</list>
<!-- Fix round B item 15's own build-over-real-files test caught a real
bug here: <list> markup requires a "selected" int binding even for a
@ -94,29 +119,25 @@
<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." />
tooltip="Edit the selected advanced-option value and press Enter to apply." />
<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="272" w="384" h="16" text="{AdvancedOptionNotice}" color="#FFC7B98F" />
<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"
<label x="4" y="294" w="200" h="16" text="MossTank Extras" color="#FFE8DEC3" />
<button x="4" y="312" w="90" h="18" text="Checkpoint" onclick="{AddRouteCheckpoint}" />
<button x="100" y="312" w="90" h="18" text="Jump" onclick="{AddRouteJump}" />
<button x="4" y="334" w="90" h="18" text="Remove" onclick="{RemoveRouteWaypoint}" />
<button x="100" y="334" w="140" h="18" text="Set Follow Target" onclick="{SetFollowTarget}" />
<label x="4" y="356" w="376" h="16" text="{RouteFollowTargetText}" color="#FFC7B98F" />
<toggle x="4" y="376" w="140" h="18" text="Follow Corners"
checked="{FollowAroundCornersEnabled}" onclick="{ToggleFollowAroundCorners}" />
<toggle x="200" y="402" w="140" h="18" text="Open Doors"
<toggle x="200" y="376" w="140" h="18" text="Open Doors"
checked="{OpenDoorsEnabled}" onclick="{ToggleOpenDoors}" />
<toggle x="4" y="424" w="140" h="18" text="Nav Priority"
<toggle x="4" y="398" w="140" h="18" text="Nav Priority"
checked="{NavigationPriorityEnabled}" onclick="{ToggleNavigationPriority}" />
<label x="4" y="446" w="180" h="16" text="{RouteMinimumDistanceText}" color="#FFC7B98F" />
<button x="190" y="446" w="24" h="18" text="-"
<label x="4" y="420" w="180" h="16" text="{RouteMinimumDistanceText}" color="#FFC7B98F" />
<button x="190" y="420" w="24" h="18" text="-"
onclick="{RouteMinimumDistanceDown}" tooltip="Decrease the route arrival distance." />
<button x="218" y="446" w="24" h="18" text="+"
<button x="218" y="420" w="24" h="18" text="+"
onclick="{RouteMinimumDistanceUp}" tooltip="Increase the route arrival distance." />
</panel>