feat(vtank): slice 7 S7.6 — Meta tab's real 6-column rules grid

VTank's own lstMetaRules grid (delete/move-up/move-down icon-ish cells +
State/Condition/Action text cells, docs/research/vtank-kb/08-ui-views.md
§1 "Tab: Meta"; PluginCore.cs:2218-2247) replaces the padded
single-column rules list: any State/Condition/Action cell click opens
the rule editor below (reusing the existing SelectMetaRule), the
up/down cells reorder using the same 0x060028FC/FD DAT icons the
Monsters and Route grids already established, and the delete cell is a
plain text "X" — no retail DAT delete-glyph id is confirmed anywhere in
this codebase (unlike the already-established move icons), and "X" is
ASCII the default retail font renders without the unrenderable-glyph
risk NoButtonAnywhereUsesTheUnrenderableArrowGlyphs guards against.

The new pin (contract control count 180->186) and the new panel test
were shown to fail against a targeted mutation before being confirmed
green. MossTank suite 659 -> 660; App markup/plugin filter holds 192/192.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-07 10:37:52 +02:00
parent 6118062a7a
commit cc323f6a59
4 changed files with 91 additions and 4 deletions

View file

@ -921,6 +921,44 @@ internal sealed partial class MossTankPanel
public string MetaStatus => _meta.Status;
public IReadOnlyList<string> MetaRows => _metaRows;
public int SelectedMetaRuleIndex => _selectedMetaRule;
// ── Meta grid (Campaign VT S7.6: VTank's own lstMetaRules 6-column
// grid, docs/research/vtank-kb/08-ui-views.md §1 "Tab: Meta";
// PluginCore.cs:2218-2247 — col 0 delete, col 1 move up, col 2 move
// down, cols 3-5 (State/Condition/Action text) open the rule editor).
// Delete uses a text "X" cell rather than an icon: no retail DAT
// delete-glyph id is confirmed anywhere in this codebase (unlike the
// move-up/move-down 0x060028FC/FD pair, already established by the
// Monsters/Route grids), and "X" is plain ASCII the default retail
// font already renders — see NoButtonAnywhereUsesTheUnrenderableArrowGlyphs
// for why an unconfirmed glyph id is worse than a text fallback. ──
public IReadOnlyList<string> MetaDeleteColumn =>
Enumerable.Repeat("X", _metaProfile.Rules.Count).ToArray();
public IReadOnlyList<uint> MetaMoveUpIcons =>
Enumerable.Repeat(0x060028FCu, _metaProfile.Rules.Count).ToArray();
public IReadOnlyList<uint> MetaMoveDownIcons =>
Enumerable.Repeat(0x060028FDu, _metaProfile.Rules.Count).ToArray();
public IReadOnlyList<string> MetaStateColumn =>
_metaProfile.Rules.Select(static rule => rule.State).ToArray();
public IReadOnlyList<string> MetaConditionColumn =>
_metaProfile.Rules.Select(static rule => DescribeMetaCondition(rule.Condition)).ToArray();
public IReadOnlyList<string> MetaActionColumn =>
_metaProfile.Rules.Select(static rule => DescribeMetaAction(rule.Action)).ToArray();
public Action<int> DeleteMetaRuleAt => row =>
{
SelectMetaRuleCore(row);
RemoveMetaRuleCore();
};
public Action<int> MoveMetaRuleUpAt => row =>
{
SelectMetaRuleCore(row);
MoveMetaRule(-1);
};
public Action<int> MoveMetaRuleDownAt => row =>
{
SelectMetaRuleCore(row);
MoveMetaRule(1);
};
public IReadOnlyList<string> MetaConditionNames =>
Enum.GetNames<MetaConditionKind>();
public IReadOnlyList<string> MetaActionNames => Enum.GetNames<MetaActionKind>();

View file

@ -609,7 +609,13 @@
</group>
<!-- Meta: ordered VTank state-machine rules. Rules fire once per state
entry; transitions/call/return use the live MetaEngine. -->
entry; transitions/call/return use the live MetaEngine. The rules
list is VTank's own 6-column lstMetaRules grid (Campaign VT S7.6,
docs/research/vtank-kb/08-ui-views.md §1 "Tab: Meta";
PluginCore.cs:2218-2247 — col 0 delete, cols 1/2 move up/down, cols
3-5 State/Condition/Action open the rule editor below). PITCH:
delete/move 16+7=23 each, State 150+7=157, Condition/Action share
the remainder ("*"). -->
<group x="8" y="42" w="848" h="194" visible="{MetaVisible}">
<menu x="4" y="0" w="132" h="21" items="{MetaProfileNames}"
selected="{SelectedMetaProfile}" onchange="{SelectMetaProfile}"
@ -633,8 +639,15 @@
<label x="620" y="4" text="{MetaStateText}" color="#FFE8DEC3" />
<list x="4" y="26" w="776" h="68" rowheight="17"
items="{MetaRows}" selected="{SelectedMetaRuleIndex}"
onchange="{SelectMetaRule}" tooltip="Select an ordered meta rule to edit." />
selected="{SelectedMetaRuleIndex}" onchange="{SelectMetaRule}"
tooltip="Click State/Condition/Action to edit; delete/move-up/move-down cells act immediately.">
<column type="text" width="23" items="{MetaDeleteColumn}" onclick="{DeleteMetaRuleAt}" />
<column type="icon" width="23" iconkind="did" values="{MetaMoveUpIcons}" onclick="{MoveMetaRuleUpAt}" />
<column type="icon" width="23" iconkind="did" values="{MetaMoveDownIcons}" onclick="{MoveMetaRuleDownAt}" />
<column type="text" width="157" items="{MetaStateColumn}" onclick="{SelectMetaRule}" />
<column type="text" width="*" items="{MetaConditionColumn}" onclick="{SelectMetaRule}" />
<column type="text" width="*" items="{MetaActionColumn}" onclick="{SelectMetaRule}" />
</list>
<field x="4" y="100" w="118" h="21" text="{MetaStateDraft}"
onchange="{SetMetaStateDraft}" onsubmit="{SetMetaStateDraft}"

View file

@ -205,7 +205,10 @@ public sealed class MossTankMarkupContractTests
// S7.5 replaced the Route tab's single-column waypoint list with
// VTank's own clWP/clWPc 2-column grid (+2) and added the
// "Select Nearest Point" button (+1) — net 177 -> 180.
Assert.Equal(180, controls.Length);
// S7.6 replaced the Meta tab's single-column rules list with
// VTank's own 6-column lstMetaRules grid (+6: the list itself was
// already counted, now 6 <column> children) — net 180 -> 186.
Assert.Equal(186, controls.Length);
foreach (XElement control in controls)
{

View file

@ -2049,6 +2049,39 @@ public sealed class MossTankPanelTests
Assert.Equal("Hunt", panel.MetaState);
}
[Fact]
public void MetaGridColumnsMatchTheRulesAndDeleteMoveActOnTheClickedRow()
{
// Campaign VT S7.6: VTank's own 6-column lstMetaRules grid
// (PluginCore.cs:2218-2247) — col 0 delete, cols 1/2 move up/down,
// cols 3-5 open the editor for that row (SelectMetaRule, already
// covered by MetaTabEditsAndExecutesTheLiveStateMachine).
var panel = new MossTankPanel(new FakeHost(new FakeAutomation()));
panel.SetMetaStateDraft("Idle");
panel.SelectMetaAction(nameof(MetaActionKind.ChatCommand));
panel.SetMetaActionTextDraft("/mt one");
panel.AddMetaRule();
panel.SetMetaStateDraft("Hunt");
panel.SetMetaActionTextDraft("/mt two");
panel.AddMetaRule();
Assert.Equal(["Idle", "Hunt"], panel.MetaStateColumn);
Assert.Contains("/mt one", panel.MetaActionColumn[0], StringComparison.Ordinal);
Assert.Contains("/mt two", panel.MetaActionColumn[1], StringComparison.Ordinal);
Assert.Equal(["X", "X"], panel.MetaDeleteColumn);
Assert.All(panel.MetaMoveUpIcons, id => Assert.Equal(0x060028FCu, id));
Assert.All(panel.MetaMoveDownIcons, id => Assert.Equal(0x060028FDu, id));
panel.MoveMetaRuleDownAt(0); // "Idle" <-> "Hunt"
Assert.Equal(["Hunt", "Idle"], panel.MetaStateColumn);
panel.MoveMetaRuleUpAt(1); // back
Assert.Equal(["Idle", "Hunt"], panel.MetaStateColumn);
panel.DeleteMetaRuleAt(0); // "Idle"
Assert.Equal(["Hunt"], panel.MetaStateColumn);
}
[Fact]
public void MetaProfilesAreIndependentDurableDocuments()
{