docs(vt): land KB doc 07, VT2 gap audit draft, slice-1 contract; metas/navs move to metaf .af

Owner amendment 2026-09-06: MossTank implements the human-readable metaf
.af format for metas and nav routes instead of VTank's binary .met/.nav;
the reference converter lives in the owner's metas repo. Docs 06 §1/07 §1
stay as the binary record; their semantics sections remain the oracle.

Doc 07 spot-checked by the lead: ExpressionEvaluator.cs:787-790 (';'
returns the first operand), hn.cs:41-80 (the pass loop), bw.cs:25 (the
'> 5' six-view cap), d6.cs:7-8 (Button/Layout only).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-06 19:56:45 +02:00
parent b320cbfbc7
commit 986bd9668a
5 changed files with 962 additions and 1 deletions

View file

@ -0,0 +1,107 @@
# Campaign VT — VT3 slice 1: VTank file compatibility + multi-column list markup
Date: 2026-09-06
Status: PLANNED (implementation starts after VT2 is FINAL)
Contract for two implementers in isolated worktrees, merged back into
`claude/latest-main-sync-497549`. Nothing merges to main; nothing is pushed.
## Part A — VTank files, drop-in (plugin side, `src/AcDream.Plugins.MossTank`)
Oracle docs: `docs/research/vtank-kb/01-settings-and-profiles.md` (§1 grammar,
§3 files/naming/versioning), `05-looting-and-utl.md` §1-2, and for metas/navs
the **metaf** reference: `C:\Users\erikn\source\repos\metas\metaf_monolithic.py`
(GPLv3 port of `metaf.cs`; read it, cite `metaf_monolithic.py:line`, never
paste it) — `06-navigation-and-nav.md` §1 and `07-meta-and-expressions.md` §1
describe the binaries metaf converts from and are the field-semantics oracle
(what each node/condition/action field means), not the file format we write.
Real fixtures: `.usd`/`.utl`/`.ast` from `C:\Games\VirindiPlugins\VirindiTank\`
(read-only), `.af` from `C:\Users\erikn\source\repos\metas\af\` (148 files;
`nav_*.af` are nav-only). Copy the ones used as fixtures into
`tests/AcDream.Plugins.MossTank.Tests/Fixtures/vtank/` — they are the owner's
own files, small, and the tests must run on Linux CI without those dirs.
A1. `.usd` reader/writer: the self-describing recursive table grammar exactly
as VTank reads and writes it (encoding, table headers, nested tables such as
`RechargeHandlerSet`, unknown-key preservation, version). Round-trip a real
profile byte-for-byte where VTank itself would (document any normalization
VTank applies on save). Map every one of the 137 settings onto the existing
`CombatSettings`/`BuffSettings`/`VitalSettings`/`InventorySettings` fields
by VTank name (the catalog table gives the mapping); the ten fields
`BuffProfileDocument` drops must round-trip; `RechargeHandlerSet` becomes a
real parsed table consumed by `VitalRechargePlanner` (replace the hardcoded
default replica with the parsed default from `defaultsettings.usd`, which is
committed as a fixture).
A2. Profile directory: a configured VTank-profiles path (default on Windows
`C:\Games\VirindiPlugins\VirindiTank`, on Linux `$XDG_DATA_HOME/acdream/vtank`
— through `ApplicationPathSet`/the plugin storage API, never a hard-coded
Windows path in the plugin), VTank's file naming and selection rules (`--`
shared prefix, per-character names, `[By char]`, `.ast`), listing, load,
save, and the Profiles tab bound to real files instead of JSON documents.
JSON stays as the storage of MossTank-only state; profile state moves to
`.usd`.
A3. **metaf `.af` reader/writer** for metas and nav routes (owner decision
2026-09-06: MossTank does not implement `.met` and does not author `.nav`).
Grammar from `metaf_monolithic.py`: `~~` comments; `STATE: {name}` /
`IF: <Cond> args` / `DO: <Action> args` / `NAV: <name> <circular|linear|once|follow>`;
`{...}` strings with doubled braces for literals; the 28 condition and 16
action keywords (`CTypeID`/`ATypeID`, py:15-63) with the per-type argument
lists in each `C*`/`A*`/`N*` class's `ImportFromMetAF`/`ExportToMetAF` pair
(py:6727-11821); nav node lines `<flw|pnt|prt|rcl|pau|cht|vnd|ptl|tlk|chk|jmp> x y z args`
(py:10728-11821); nested `All`/`Any`/`Not`/`DoAll` by indentation exactly as
metaf emits and accepts them; embedded navs inside `EmbedNav`. Load into the
existing `Meta`/`Navigation` models (replacing `VtankMetaProfileSerializer`
and `VtankNavRouteSerializer` as the profile-store format; keep the binary
readers only if a test still needs them — delete otherwise, with their
tests), save back as `.af` that metaf itself accepts. Proof: (1) every
`.af` under `metas/af/` parses; (2) parse → write → parse is identical;
(3) for the `met/` and `nav/` samples, our model loaded from metaf's own
`.af` conversion equals our model loaded from the matching `.af` in `af/`
where both exist; (4) the writer's output for at least five fixtures is
byte-identical to metaf's canonical emission after comment stripping. Also:
the `.utl` `BuffedInt/Double` base-key-exists gate; the jump-charge clamp to
2000 ms applied to `jmp` nodes at load. Each with a real-file fixture and a
round-trip test. Line-numbered parse errors (file:line, what was expected).
Tests: one round-trip test per format on every committed fixture; a
137-setting mapping test (every catalog name maps to exactly one field,
defaults equal `defaultsettings.usd`); Linux-path test; each new test shown
to fail first.
## Part B — multi-column `<list>` (App side, `src/AcDream.App/UI`)
Oracle: `docs/research/vtank-kb/08-ui-views.md` §2-3 (VVS `HudList`
semantics and the proposed extension), `docs/plugin-ui-markup.md`.
B1. Markup: `<list>` accepts child `<column type="text|check|icon"
header="…" width="…" [name] />` elements. Without columns the element is
unchanged (single text column, existing bindings). With columns: `rows`
binds an `IEnumerable<PluginListRow>`-shaped contract in
`AcDream.Plugin.Abstractions` (BCL-only: a row = ordered cells; cell = text
string, bool check, or uint icon id with `iconkind` per column), `selected`
/`onchange` as today, plus `oncell="{Action<int,int>}"` (row, column) fired
on a click inside a cell, and check columns toggling through
`oncheck="{Action<int,int,bool>}"`. Header row optional (`headers="true"`),
per-column widths in px with the last column absorbing the remainder,
horizontal clipping per cell, scrolling as today.
B2. Widget: extend `UiMarkupList` (or a sibling) — draw header, per-cell
text/check/icon, hit-test to (row, column), keyboard-free. Icons through the
existing `IMarkupIconResolver` with the column's `iconkind`.
B3. Docs: `docs/plugin-ui-markup.md` section for columns; remove the
"single text column" limitation note.
Tests: parse + binding-type tests, draw-level tests with the recording
renderer (column x-offsets, header, check glyph, icon per row), hit-test
tests for cell clicks, and a MossTank markup contract test proving the
existing single-column lists still bind.
## Reviews
Two Opus lenses per part (architecture; VTank/format fidelity against the
catalog and the fixtures), fix round, narrow re-review. Then merge both
worktree branches into the campaign branch and run the full App + MossTank
suites and the Ubuntu portable closure (`dotnet test` on the MossTank tests
under WSL if available).
## Out of scope
Rebuilding the tabs on the new columns (slice 7, visual gate), any behavior
change in combat/buff/loot/nav (slices 2-6).

View file

@ -0,0 +1,56 @@
# Campaign VT — VT2 gap audit and implementation order
Date: 2026-09-06
Status: DRAFT until the VT1 citation pass and doc 07 land; then FINAL.
Inputs: the nine "MossTank gap" sections in `docs/research/vtank-kb/`,
the review of `c406942ef` recorded in
`docs/plans/2026-09-06-mosstank-mode-arbitration.md`, and the owner decisions
in `docs/plans/2026-09-06-campaign-vt-vtank-oracle.md`.
## Headline
MossTank already carries VTank's 137 setting names, all documented `/vt`
verbs, working `.nav`/`.utl`/`.met` readers (the meta/nav ones now superseded
by the `.af` decision), the exact loot-requirement
vocabulary, faithful ownership/blacklist/stop-range numbers, and a shell with
the nine tabs. The gaps are concentrated in five places: **(1) no `.usd`
profile file at all** and a lossy JSON persistence record, **(2) the
scheduler model** (an imperative owns-action chain instead of VTank's
single-winner priority list, which is where every ordering bug lives), **(3)
combat policy details** (debuff order, target selection, arc/bolt), **(4) the
UI tables** (no multi-column list, so four tabs are select-then-edit
approximations), and **(5) the plugin-to-plugin API** (no sibling access at
all).
## Ordered slices
Sizes: S ≈ one implementer-day, M ≈ two to three, L ≈ a week. "Gate" says who
accepts: T = tests only, O = owner connected/visual gate.
| # | Slice | Contents (doc §) | Depends on | Size | Gate |
|---|---|---|---|---|---|
| 1 | **File compatibility + multi-column list** (VT3 slice 1, in the current goal) | 1a `.usd` reader/writer with the self-describing table grammar, profile directory discovery and selection (`--`/`[By char]` naming), `RechargeHandlerSet` table parse, and the ten fields `BuffProfileDocument` drops (01 §1,§3,§5). 1b **metaf `.af` reader/writer for metas and nav routes** (owner amendment 2026-09-06: no `.met`, no authored `.nav`) proven against the 148 real files in the owner's metas repo and against the metaf tool's own conversions of `met/` and `nav/`; the `.utl` `BuffedInt/Double` base-key gate (05 gap 4); the jump-charge clamp to 2000 ms at load (06 gap 5) applied to `.af` `jmp` nodes. Loading real files from a configured directory (Linux-clean). 1c `<list>` gains `<column type="text\|check\|icon">` children with header, widths, per-cell click and per-row check state (08 §2-3). | — | M+M (parallel: plugin vs App) | T |
| 2 | **Scheduler parity** | Replace the owns-action chain with a declarative single-winner rule list in VTank's order incl. sentinels, `Running=false` teardown signal, 293 ms heartbeat + event poke, IdlePeace as terminal rule AND pre-chain on idle loot/approach/nav, the `fd` tight-waypoint Magic override; fold in every deferred finding on `c406942ef` (no-caster stops the macro; retry budget in ticks with wand-use recovery; "buff due" suppresses idle; Items-page order for the fallback wand; equip cadence/budget; `Unknown` mode hands-off; single caster predicate; notice text). (02 §1-5) | 1 (profile order, settings) | L | O (macro start from peace; idle peace) |
| 3 | **Combat policy corrections** | Hardcoded 12-step debuff order and first-due-wins; target selection with `DebuffEachFirst` + urgency score, `TargetLock`/sticky as low tiebreaks; quality before `UseArcs`; unknown-monster → no auto-element; wield-match tiebreak. (03 §8) | 2 | M | O (live fight) |
| 4 | **Buffs and vitals** | Per-tick plan re-evaluation (mid-pass tier cascade); profile-item → default enchant rows on Add; helper pool incl. non-fellow tracked players (decide); worn-item mana keyed per item, oldest first; keep the RandomHelper improvement (decide, document). (04 §5) | 2 | M | O (buff pass) |
| 5 | **Looting** | `EarlyMatch`/`NeedsID` identify-avoidance; rare-first corpse selection; ownership-denial chat listener (10 s skip); corpse age clock from first sighting; explicit re-close. (05 §4) | 1 | M | O (loot field) |
| 6 | **Navigation** | Creep band + `FaceHeading` snap-turn inside 1/160 with the forced-Magic case; OpenVendor fire-and-forget vs wait (decide, document as a divergence if kept); lockpick strategy (decide); recall stationary gate; Portal2/UseNPC candidate filter; chat-color gate on UseNPC. (06 §6) | 2 | M | O (route) |
| 7 | **UI transcription** | Monsters/Items/Consumables/Buffs/Route/Meta tabs rebuilt as VTank's grids on the multi-column list; window geometry from `mainView.xml`; Advanced Options and Loot Editor stay in-panel by decision. (08 §1,§5) | 1c | M | O (visual) |
| 8 | **Meta and expressions** | Doc 07's five semantic gaps: `CreateView` markup translation (VTank's two-tag Button/Layout dialect → our panel markup; 6-view cap), nested `All` actions must run every child (no short-circuit), case-sensitive monster-name regex, the `;` operator decision (retail returns the FIRST operand, UtilityBelt/MossTank the last — owner decides, document as a divergence either way), watchdog ring pre-fill with the far sentinel; plus the per-state-entry firing rule, the two "seconds in state" clocks, and the call/return stack semantics of 07 §2. Unregistered retail condition id 27 stays unregistered. | 1b (`.af`) | M | T + O (a live meta run) |
| 9 | **Plugin interop API** | Expand `AcDream.Plugin.Abstractions`: sibling plugin discovery/handle, a public event surface (macro state, profile changed, cast complete), settings-by-name get/set, command injection into another plugin's verb table, a permission tier for trusted partners. Needed by a ported MosswartMassacre. (09 §5) | — | M | T |
Slices 37 all need the owner's eyes and are outside the current goal; the
goal ends after slice 1's review. Slice 9 is host API work and can run in
parallel with 2 later.
## `c406942ef` re-judged against doc 02
Keep the two-owner shape (it matches VTank's `ga.a` + terminal `cm` rule)
but its ten findings belong to slice 2, where the scheduler rewrite makes
most of them fall out naturally (priority-ordered rules give "buff due
suppresses idle" and "no-caster stops the macro" for free). No fix lands
before slice 2; the commit stays as is on the branch.
## Ledger
- 2026-09-06 draft written from docs 01-06, 08, 09; 07 and the citation pass pending.
- 2026-09-06 doc 07 landed and its gaps placed in slice 8; owner amendment: metas and navs are metaf `.af` (slice 1b rewritten). Citation pass still pending before FINAL.

View file

@ -17,7 +17,7 @@ catalog.
|---|---|
| Decompile set | VTank (`utank2-i.dll`), VirindiViewService, VTank Classic Looter (`VTClassic.dll`). UtilityBelt and Mag-Tools are read from their open source, not decompiled. No Decal core. |
| UI | Our own native retained UI (retail chrome, DAT fonts). VVS is NOT ported. VTank's own view XML is the layout truth (control inventory, grouping, density per tab); pixel-matching the VVS theme is not required. |
| Files | Drop-in compatibility: `.usd` profiles, `.met` metas, `.nav` routes, `.utl` loot profiles load unchanged; settings keep VTank's names. |
| Files | Drop-in compatibility for `.usd` profiles and `.utl` loot profiles (load unchanged; settings keep VTank's names). **Metas and navs use metaf `.af` (owner, 2026-09-06 amendment):** MossTank reads and writes the human-readable metaf format for both metas and nav routes and does NOT implement binary `.met` or author `.nav`; conversion to/from VTank's binaries is the metaf tool's job. Reference parser: `C:\Users\erikn\source\repos\metas\metaf_monolithic.py` (port of `metaf.cs`, GPLv3 — read, never paste), with 148 real `.af`, 25 `.met`, 130 `.nav` files beside it. Memory: `claude-memory/reference_metaf_af_format.md`. |
| Knowledge base | Full catalog with decompile citations BEFORE implementation. |
| Platform | Linux is a first-class target: no `System.Drawing`, no GDI+/Windows fonts, no Windows-only paths or registry, no COM. The plugin stays BCL-only per `AcDream.Plugin.Abstractions`. |
@ -34,6 +34,7 @@ PowerShell reflection.
| `refs/vtank/uTank2.Resources.defaultsettings.usd` (+ `defaultsettingstemplate.usd`, `defaultinfodb.ugd`, `defaultitemagedb.ugd`, `CustomIcons.RES`, `dc.xml`) | Default profile (the 137 options), templates, databases. |
| `refs/vvs/decompiled/` + `refs/vvs/resources/` | VirindiViewService: control semantics (`HudList`, `HudCombo`, `HudTabView`…), themes (`Float_Theme.cs` is the default when the registry names none; `Decal_Theme.cs` is the retail-textured one), theme images. Reference only. |
| `refs/vtank-classiclooter/decompiled/` | 53 `.cs`, real names: `LootCore`, `cLootItemRule`, `UTLBlock_*`, `UTLFileExtraBlockManager`, `ComputedItemInfo`, `GameInfo`. |
| metaf (owner-supplied 2026-09-06) | `C:\Users\erikn\source\repos\metas` (`ssh://git@git.snakedesert.se/SawatoMosswartsEnjoyersClub/metas.git`): `metaf_monolithic.py` (the `.met/.nav``.af` converter), `af/` (148 real metas and `nav_*.af` nav-only files), `met/`, `nav/`, `profiles/`. The `.af` grammar oracle and the fixture source for slice 1b. |
| Open source (owner-supplied references, 2026-09-06) | UtilityBelt: `git@gitlab.com:utilitybelt/utilitybelt.gitlab.io.git` — holds the full plugin source (`UtilityBelt.sln`, `UtilityBelt/Lib/Expressions/` incl. the `MetaExpressions.g4` grammar); cloned to `C:\Users\erikn\source\repos\utilitybelt.gitlab.io` (older clone `utilitybelt.service`). Mag-Tools: `https://github.com/Mag-nus/Mag-Plugins/tree/master/Mag-Tools`, local `C:\Users\erikn\source\repos\Mag-Plugins`. Read as source, never decompiled. |
Rules for using it: cite `file:line` in the catalog and in code comments; never
@ -81,3 +82,5 @@ Linux-clean by construction; the Ubuntu CI closure runs the MossTank tests.
## Ledger
- 2026-09-06 VT0 complete. VT1 docs 01/02/03/08 dispatched first, 0407/09 next.
- 2026-09-06 VT1: all nine catalog docs landed; index written; Opus citation pass in flight.
- 2026-09-06 Files decision amended by the owner: metas and navs are metaf `.af`, not `.met`/`.nav` (see Decisions). Docs 06 §1 and 07 §1 stay as the binary-format record (they describe what the metaf tool converts from); the semantics sections (06 §2-5, 07 §2-4) remain the behavior oracle. VT2 doc: `2026-09-06-campaign-vt-vt2-gap-audit.md`; slice-1 contract: `2026-09-06-campaign-vt-slice1-files-and-columns.md`.

View file

@ -0,0 +1,788 @@
# VTank KB 07 — Meta and Expressions
Oracle: `refs/vtank/decompiled/` (obfuscated VTank 2.x decompile via ILSpy;
identifiers are single/double-letter, strings and method *shapes* are
intact). All citations are `file:line` against that tree. No decompiled
source is pasted verbatim below — every claim is paraphrased/tabulated and
cited so it can be checked against the file directly. Real sample `.met`
files under `C:\Games\VirindiPlugins\VirindiTank\*.met` were read (read-only)
to confirm the on-disk shape described here.
Cross-referenced against `docs/research/2026-07-29-vtank-plugin-automation-requirements.md`
(prior `.met`-structure pass) and
`docs/research/2026-08-26-mosstank-vtank-utilitybelt-research.md` (UtilityBelt's
expression dialect). Disagreements are called out inline as **DISAGREES**. Two
found: the 2026-07-29 doc's "67 functions" count for retail VTank (this doc's
direct reflection-attribute count from `ch.cs` is **66** — §3.7); and the
2026-08-26 doc's UtilityBelt grammar audit states `;` "return[s] the final
result" (§3.2 there) where this doc's direct read of retail's own
`ExpressionEvaluator.cs:787-790` shows retail **VTank** returns the *first*
operand and discards the rest (§3.3) — i.e. UtilityBelt itself already
diverges from retail here, and MossTank correctly followed UtilityBelt's
(different) convention rather than retail's; see §5.2 gap 4.
Comparison target: `src/AcDream.Plugins.MossTank/` (Meta.cs, MetaViewManager.cs,
MossTankMetaProfileStore.cs, VtankMetaProfileSerializer.cs, Expressions/*.cs)
at the tree checked out in worktree `eloquent-hugle-42119e`.
---
## 0. Class-name key
| Obfuscated | Role | Notes |
|---|---|---|
| `hn` (field `dz.at`) | `MetaMgr` — the Meta subsystem owner: state→rule-list map, per-pass fired-set, state-entry clocks, call stack, expression evaluator instance, `.met` load/save | `hn.cs` (whole file) |
| `ch` (field `hn.k`, i.e. `dz.at.k`) | `MetaExpressionEvaluator` — the concrete `ExpressionEvaluator<byte>` subclass registering every VTank-specific function/variable | `ch.cs` (1743 lines) |
| `ExpressionEvaluator<W>` | The generic tokenizer/shunting-yard/eval engine (kept its class name after decompile) | `MyClasses/ExpressionEvaluator.cs` |
| `a7` | The per-pass chat-message buffer + three one-shot edge flags (death/portal-enter/portal-exit) | `a7.cs` |
| `h7` | The watchdog timer (10-sample position ring + expiry test) | `h7.cs` |
| `h` (enum) | `CondType` — 29 members `a..z,aa,ab,ac`; **`ab` is never registered** (a real gap in retail's own factory table) | `h.cs` |
| `bp` (enum) | `ActionType` — 16 members `a..p`; all 16 registered | `bp.cs` |
| `d9` | The condition/action factory: `Dictionary<h,az>` and `Dictionary<bp,ho>` mapping type id → prototype instance, `Create<T>(int)` news up a fresh one by id | `d9.cs` |
| `az` (interface) | Base condition contract: `h a()` (its own type id), `bool b()` (ValidNow) | `az.cs` |
| `ho` (interface) | Base action contract: `bp f()` (its own type id), `bool h()` (Execute — return meaning is **continue-pass** semantics, see §2) | `ho.cs` |
| `bx` (interface) | Shared base of `az`/`ho`: `g()` display type-name, `h(gy)`/`i()` deserialize/serialize, `j(object)` UI-build, `c()` referenced-state-name list (for the state-usage counter), `d()`/`a(bx)` parent link, `e()` UI-refresh cascade, `k()` display string | `bx.cs` |
| `ax` | One compiled rule: `{ az Condition, ho Action, string State }` | `ax.cs` |
| `MultipleBase<T>` (`uTank2.Meta` namespace) | Shared container base for compound condition/action lists (`All`/`Any` conditions, `All` action) | `uTank2.Meta/MultipleBase.cs` |
| `dj` | Shared base for the common "N named typed parameters" leaf condition/action shape | `dj.cs` |
| `dh` | Shared base for the "zero parameters" leaf shape (serializes as a bare int `0`, not an empty table — see §1.4) | referenced by `ic.cs`, `do.cs`, `ct.cs`, `gk.cs`, `cy.cs`, `i.cs`, `co.cs`, `ao.cs` |
| `q` | Shared UI-form base used by some scalar-int leaf types (`c1`, `gc`, `e3`, `ge`, `hy`) — same on-disk shape as a raw `gy.a(int)` scalar, distinct from `dj` | `q.cs` |
| `bw` | The Meta-view registry: `Dictionary<string,aq>`, the 5-view cap, version gate | `bw.cs` |
| `aq` | One created view: XML parse, size-vs-window clamp, `Dictionary<string,c0>` named-control index | `aq.cs` |
| `c0` | Base view control (left/top/width/height/name attrs) | `c0.cs` |
| `c9` | `Button` view control (`text`, `actionexpr`, `setstate` attrs) | `c9.cs` |
| `au` | `Layout` view control (recursive `<control>` children container) | `au.cs` |
| `d6` | Control-type factory: `"BUTTON"→c9`, `"LAYOUT"→au`, anything else `→null` | `d6.cs` |
| `gy`/`bd`/`cw`/`y` | The generic line-text database primitives (self-typing cell / table / row / whole database) — **format fully documented in doc 01 §1**; this doc only adds the Meta-specific record shapes built on top of it | `gy.cs`, `bd.cs`, `cw.cs`, `y.cs` |
| `f6` | The length-prefixed raw-text blob cell type (tag `"ba"`) — used for embedded nav routes and CreateView XML payloads | `f6.cs` |
---
## 1. The `.met` format exactly as read/written
### 1.1 Container: reuses doc 01's `y`/`bd`/`cw`/`gy` grammar verbatim
A `.met` file **is** a `y`-format database (plain text, CRLF, no compression,
no checksum — doc 01 §1/§3) holding exactly **one table named `CondAct`**
with **5 columns**, all non-indexed (`"n"` flags):
```
CType AType CData AData State
```
— confirmed by `hn.k()` (save, `hn.cs:379-393`): `new bd("CType","AType",
"CData","AData","State")`, one row per rule, `cw2[0]=gy.a((int)Condition
type)`, `cw2[1]=gy.a((int)Action type)`, `cw2[2]=Condition.i()` (the
condition's own serialized `gy`), `cw2[3]=Action.i()`, `cw2[4]=gy.a(stateName)`.
Load is the mirror in `hn.a()` (`hn.cs:397-431`): `d9.a((h)gy.e(cell[0]))`
instantiates the condition by type id, `.h(cell[2])` deserializes it;
same for the action from cell[1]/cell[3]; cell[4] is the state name string.
**Any unsupported/unknown type id (including the real `h.ab` gap) fails the
whole load** — `hn.a()` clears the entire rule table and returns false,
logging `"Meta file load: unsupported cond/act type."` (`hn.cs:401-405`).
A DB-format failure (missing `"CondAct"` key) does the same
(`hn.cs:379-382` load path/`hn.cs:398`).
### 1.2 Worked decode — one real rule from a live `.met`
A representative row from a sample `.met` (`Chat` state, condition
`ChatMessage` matching `"^You have been slain"`, action `SetMetaState` to
`"HandleDeath"`) decodes as, in `gy` line order inside the `CondAct` table:
```
i <- CType tag
4 <- CType value = h.e = ChatMessage
i <- AType tag
1 <- AType value = bp.b = SetMetaState
s <- CData tag (ChatMessage condition = bare string, hl.cs:59-62)
^You have been slain
s <- AData tag (SetMetaState action = bare string, a3.cs:42-45)
HandleDeath
s <- State tag
Chat
```
A compound rule (`Any` condition wrapping two `ChatMessage` children,
`CallMetaState` action) instead nests a **`bd("K","V")` table** for CData
(uppercase key names — the `MultipleBase<T>` shape, `MultipleBase.cs:52-63`):
```
i / 3 <- CType = h.d = Any
i / 5 <- AType = bp.f = CallMetaState
TABLE / 2 / K / V / n / n <- nested bd header: 2 cols, "K","V", no index
2 <- row count = 2 children
i / 4 <- child K = h.e = ChatMessage
s / <pattern1> <- child V = the child condition's own gy (bare string)
i / 4
s / <pattern2>
TABLE / 2 / k / v / n / n <- AData: the CallMetaState action's OWN table (lowercase k/v dj shape, dj.cs:86-95, NOT the MultipleBase shape)
2
s / st / s / <target state>
s / ret / s / <return-to state>
s / <State name>
```
The **case of the K/V column names is load-bearing to reproduce byte-for-byte**
(not merely cosmetic): `MultipleBase<T>` always writes `"K","V"`
(`MultipleBase.cs:54`); every `dj`-based leaf (including the hand-rolled
`cp`/CallMetaState, which is not `dj`-derived but manually builds the same
shape) writes lowercase `"k","v"` (`dj.cs:86`, `cp.cs:61`). `Not` (`ea.cs`)
also uses uppercase `"K","V"` (`ea.cs:72`) with the row count pinned to
exactly 1 (`ea.cs:55`, rejects/ignores anything else on load).
### 1.3 Per-condition record layout (29 declared `h` ids; 28 wired, `h.ab`=27 is a real gap)
| # | `h` | Name (retail UI string) | Class | On-disk `CData` shape | Fields (name→gy type) | Cite |
|---|---|---|---|---|---|---|
| 0 | a | Never | `ic` | bare `gy.a(0)` (tag `i`, value `0`) | — | `ic.cs:9-45` |
| 1 | b | Always | `a6` | bare int 0 | — | (mirror of `ic`) |
| 2 | c | All | `gq` | `bd("K","V")`, N rows | per-child `K`=type id int, `V`=nested gy | `gq.cs:4-34`, `MultipleBase.cs:52-63` |
| 3 | d | Any | `e4` | same as All | — | `e4.cs` |
| 4 | e | Chat Message | `hl` | bare string | the regex pattern | `hl.cs:59-73` |
| 5 | f | Pack Slots <= | `c` | bare int | threshold | `c.cs:51-64` |
| 6 | g | Seconds in state >= | `c1` | bare int | seconds | `c1.cs:32-46` |
| 7 | h | Navroute empty | `ct` | bare int 0 | — | `ct.cs` |
| 8 | i | Character Death | `do` | bare int 0 | — | `do.cs` |
| 9 | j | Any Vendor Open | `gk` | bare int 0 | — | `gk.cs` |
| 10 | k | Vendor Closed | `cy` | bare int 0 | — | `cy.cs` |
| 11 | l | Inventory Item Count <= | `cd` | `dj` table, 2 fields | `n`=string item name (exact match), `c`=int count | `cd.cs:25-31` |
| 12 | m | Inventory Item Count >= | `hs` | same shape as 11 | `n`, `c` | mirror of `cd` |
| 13 | n | Monster Name Count Within Distance | `d7` | `dj` table, 3 fields | `n`=string regex, `c`=int count, `r`=double range (meters) | `d7.cs:57-64` |
| 14 | o | Monster Priority Count Within Distance | `bt` | `dj` table, 3 fields | `p`=int priority, `c`=int count, `r`=double range | `bt.cs:44-51` |
| 15 | p | Need to Buff | `gw` | bare int 0 | — | `gw.cs` |
| 16 | q | No Monsters Within Distance | `h4` | `dj` table, 1 field | `r`=double range (meters) | `h4.cs:9-14` |
| 17 | r | Landblock == | `ge` | bare int | landblock id (masked `&0xFFFF0000` at eval, not at storage) | `ge.cs:54-68` |
| 18 | s | Landcell == | `hy` | bare int | full cell id (no mask) | `hy.cs` (mirror of `ge` minus the mask) |
| 19 | t | Portalspace Entered | `i` | bare int 0 | — | `i.cs` |
| 20 | u | Portalspace Exited | `co` | bare int 0 | — | `co.cs` |
| 21 | v | Not | `ea` | `bd("K","V")`, **exactly 1 row** | `K`=child type id, `V`=nested gy | `ea.cs:53-62` |
| 22 | w | Seconds in state (P) >= | `gc` | bare int | seconds, compared against a **separate, non-macro-reset clock** — see §2.5 | `gc.cs:32-46` |
| 23 | x | Time Left On Spell >= | `fm` | `dj` table, 2 fields (inferred from serializer parity — not independently re-read; see `sid`/`sec` in §1.5) | `sid`=int spell id, `sec`=int seconds | `fm.cs` (name string only, `fm.cs:52`) |
| 24 | y | Burden Percentage (eg. 100) | `e3` | bare int | percent threshold | `e3.cs:51-65` |
| 25 | z | Dist any route pt >= | `gf` | `dj` table, 1 field | `dist`=double meters | `gf.cs:7-12` |
| 26 | aa | Expression | `b3` | `dj` table, 1 field | `e`=string expression text | `b3.cs:42-47` |
| 27 | **ab** | **— unregistered —** | — | n/a | n/a | `d9.cs:9-35``h.ab` is never passed to `a(h,az)`; loading this id fails the whole file (§1.1) |
| 28 | ac | Chat Message Capture | `c5` | `dj` table, 2 fields | `p`=string regex pattern, `c`=string semicolon-joined color-id list (empty = any color) | `c5.cs:16-22` |
**Retail UI-name vs. semantic mismatch (a real VTank wart, not a decompiler
artifact):** `gf`/"Dist any route pt >=" is actually a **universal**, not
existential, test — `c()` returns `true` only when **every** route point is
farther than `dist` (it returns `false` early the instant it finds ANY point
within `(epsilon, dist]`, `gf.cs:44-62`). The display name reads as "exists a
point at distance >= X"; the real semantics is "no point is within X" (i.e.
"far from the whole route"), the opposite quantifier from what the name
suggests.
### 1.4 Per-action record layout (16 declared `bp` ids; all 16 wired)
| # | `bp` | Name | Class | On-disk `AData` shape | Fields | Cite |
|---|---|---|---|---|---|---|
| 0 | a | None | `a` | bare int 0 | — | (not independently re-read; parity confirmed via serializer cross-check §5) |
| 1 | b | Set Meta State | `a3` | bare string | target state name | `a3.cs:42-56` |
| 2 | c | Chat Command | `h2` | bare string | verbatim chat text (no expression eval) | `h2.cs:40-58` |
| 3 | d | All | `f` | `bd("K","V")`, N rows | per-child `K`=type id, `V`=nested gy | `f.cs:4-37` |
| 4 | e | Load Embedded Nav Route | `ff` | `f6` blob (tag `ba`): line1=source-name, line2=point-count, remainder=raw serialized nav-route text | — | `ff.cs:31-61` |
| 5 | f | Call Meta State | `cp` | `bd("k","v")`, 2 rows, keyed by string tag not type id | `st`=string state to call, `ret`=string state to return to | `cp.cs:46-71` |
| 6 | g | Return From Call | `ao` | bare int 0 | — | `ao.cs` |
| 7 | h | Expression Action | `dv` | `dj` table, 1 field | `e`=string expression (result discarded) | `dv.cs:20-48` |
| 8 | i | Chat Expression | `n` | `dj` table, 1 field | `e`=string expression (result sent to chatbox) | `n.cs:20-56` |
| 9 | j | Set Watchdog | `f5` | `dj` table, 3 fields | `s`=string state to call, `r`=double range **meters** (stored raw; `/240` conversion happens only at execute time, `f5.cs:53`), `t`=double seconds | `f5.cs:6-13` |
| 10 | k | Clear Watchdog | `ht` | `dj` table, **0 fields** (an empty table, distinct byte shape from the bare-int "no data" encoding — see below) | — | `ht.cs:6-9` |
| 11 | l | Get VT Option | `fl` | `dj` table, 2 fields | `o`=string option name, `v`=string destination variable name | `fl.cs:8-13` |
| 12 | m | Set VT Option | `dt` | `dj` table, 2 fields | `o`=string option name, `v`=string expression to evaluate | `dt.cs:8-13` |
| 13 | n | Create View | `p` | `dj` table, 2 fields | `n`=string view name, `x`=`f6` blob = raw view XML | `p.cs:6-11` |
| 14 | o | Destroy View | `fr` | `dj` table, 1 field | `n`=string view name | `fr.cs:6-10` |
| 15 | p | Destroy All Views | `ag` | `dj` table, **0 fields** | — | `ag.cs:4-8` |
**Two distinct "no parameters" byte encodings exist and must not be
conflated:** `dh`-based zero-arg types (`ic`/`a6`/`ct`/`do`/`gk`/`cy`/`i`/`co`/
`ao`, and by analogy the `bp.a`="None" action) serialize as a **bare typed
int cell** — two lines, `"i"` then `"0"` (`ic.cs:42-44``gy.a(0)` resolves
to the `int` overload, not a true "void" `gy`). `dj`-based zero-arg types
(`ht`/`ClearWatchdog`, `ag`/`DestroyAllViews`) instead serialize as a
**`bd("k","v")` table header with a row count of `0`** — a `TABLE` tag,
column spec, and `"0"` — a different byte sequence with the same practical
meaning. A `.met` reader that treats "no data" as one canonical shape will
mis-parse one of these two families.
### 1.5 The embedded-nav-route blob (`ff`, action type 4) in detail
`ff.e()` (save, `ff.cs:52-61`) writes an `f6` blob whose *contents* are three
lines — source nav-profile name (or `"[None]"` if none was selected at
capture time), the route's `.c()` point count, then the **entire raw
serialized nav-route text** written by the nav-route object's own `.a
(TextWriter)` (`ff.cs:105-120` — this is the *same* format as a standalone
`.nav` file, embedded verbatim rather than referenced by path). Loading
(`ff.a(gy)`, `ff.cs:31-44`) reverses this via `StringReader`. `ff.b()`
(Execute, `ff.cs:149-165`) feeds the embedded text back into the active
nav-route object and always returns `true` (does not end the pass by itself;
see §2.2).
---
## 2. FSM semantics
### 2.1 What drives an evaluation pass
Meta is **not** polled on its own timer. It is evaluated once per scheduler
pass (`cLogic.a(MyList<ILogicRule>,MyList<ILogicRule>)`, the same pass that
drives the action-list scheduler documented in KB doc 02), gated on
`PluginCore.dz.o.c` (macro running) and the `EnableMeta` setting:
```
if (PluginCore.dz.o.c && f3.k("EnableMeta")) {
PluginCore.dz.at.h(); // one Meta FSM step
a7.e(); // clear the chat buffer + one-shot edge flags
}
```
`uTank2/cLogic.cs:188-196`. This means Meta's effective cadence is doc
02's scheduler cadence: the 293 ms heartbeat while running, plus the
fast-wake path on spell-cast/attack completion (doc 02 §1.1) — **not** a
fixed per-frame tick, and **not** independent of the main action-list
scheduler.
### 2.2 One pass, `hn.h()`, in full (`hn.cs:38-80`)
1. If the watchdog has expired (`h7.b() == h7.a.a`, §2.6): if the call stack
(`hn.j`, a `Stack<string>`) already holds >= 10000 entries, post
`"Meta Error: Call stack overflow (recursive call loop?)."` +
`"Disabling Meta."` and set `EnableMeta` false (`hn.cs:47-51`); otherwise
post `"Meta watchdog expired!"`, **push the current state** onto the call
stack, and transition to the watchdog's configured target state
(`hn.b(h7.d())`) — this reuses the SAME call/return stack as
`CallMetaState`/`ReturnFromCall` (§2.4), so a state entered via watchdog
expiry can later `ReturnFromCall` back to whatever state was active when
it tripped. This branch is **mutually exclusive** with step 2 below — no
rules are evaluated on a watchdog-triggering pass.
2. Otherwise, walk `hn.d[CurrentState]` (a `List<ax>`, insertion/authoring
order) **top to bottom**. For each rule not yet fired this state-entry
(`!hn.e.ContainsKey(rule)`) whose condition is true:
- Mark it fired (`hn.e[rule] = true`) — a rule fires **at most once per
state entry**, not once per pass; see §2.3.
- Log `"Meta executing action: " + action.k()`.
- Execute the action: `bool flag2 = !action.b.h()`.
- If a meta-profile reload happened *during* that action's execution
(`hn.m_c`, a flag only `hn.d(string)`/profile-load sets true), reset the
flag and **break** immediately regardless of `flag2`.
- Otherwise, **break only if** the current state changed (`text !=
hn.m_g`, where `text` was cached at the top of the pass) **or**
`flag2` is true (the action's own `.h()` returned `false`).
- **If neither**, continue the `foreach` — a **second (and third, …)
rule in the same state CAN fire within the same `hn.h()` call**, as
long as each fired action both keeps `CurrentState` unchanged and
returns `true`.
This is a genuinely different shape from "first rule wins, stop": it is
"walk in order, fire every eligible not-yet-fired rule whose action reports
*not done yet, and no state change*, then stop." **Every state-changing
action in the catalog (`SetMetaState`, `CallMetaState`, `ReturnFromCall`)
already returns `false`** (a3.cs:118, cp.cs:153, ao.cs:84), so in the current
action set the `flag2` check alone would suffice for those three — **except
inside a compound `All` action** (`bp.d`, class `f`), which unconditionally
executes every child via `item.h()` **ignoring each child's return value**,
and itself always returns `true` (`f.cs:30-37`). A `SetMetaState` nested
inside an `All` action still transitions state, but the `All` wrapper keeps
running every subsequent sibling child in the same call — the pass only
actually stops afterward because the *outer* `hn.h()` loop's `text !=
hn.m_g` check catches the state change once the whole `All` action returns.
**Every action listed with "returns true" in §1.4's execute semantics
(`ff`/EmbedNav, `f5`/SetWatchdog, `ht`/ClearWatchdog, `fl`/GetOpt,
`dt`/SetOpt=true-on-success, `p`/CreateView, `fr`/DestroyView, `ag`
/DestroyAllViews, `dv`/ExprAct, `n`/ChatExpression, `h2`/ChatCommand) permits
the pass to keep evaluating later rules in the same state during the same
call** — this is the mechanism that lets one Meta pass, e.g., clear a
watchdog *and* send a chat message *and* still fall through to whichever
rule comes next in list order, all in one `hn.h()` invocation.
### 2.3 Per-state-entry firing, not per-pass firing
`hn.e` (`Dictionary<ax,bool>`) is the fired-set, and it is **only** cleared
on `hn.b(string)` — a state transition (`hn.cs:104-112`) — never cleared
mid-state. So a rule that fires once in a state stays "spent" for every
subsequent pass until the FSM leaves and re-enters that state (even the same
state name via `SetMetaState "SameState"` counts as a fresh entry, since
`hn.b` unconditionally clears `hn.e`). This is why a state commonly needs an
explicit `Always`-gated rule near the top if it must re-fire every pass
(`Always`'s own condition just returns `true` every time it's re-checked,
but once it has fired once this state-entry it will not be re-evaluated
again — the *entry*, not the condition, is what the fired-set gates).
### 2.4 Call/return stack
`CallMetaState` (`cp.f()`, `cp.cs:142-154`) pushes the **return-to** state
string (`m_b`, defaulting to `"Default"` if left blank in the UI — not the
*current* state unless explicitly set to it) onto `hn.j`, then transitions to
the **call** state (`m_a`). `ReturnFromCall` (`ao.b()`, `ao.cs:73-85`) pops
`hn.j` and transitions there; popping an empty stack posts `"Meta Error: Call
stack underflow, cannot return."` and disables Meta. Both share the same
10000-entry overflow guard as the watchdog-expiry path (§2.2 step 1) — **all
three producers of stack growth (CallMetaState, watchdog expiry) and the one
consumer (ReturnFromCall) operate on the literal same `Stack<string>`
instance** (`hn.j`), so a watchdog-triggered call can be closed by an
ordinary `ReturnFromCall` rule and vice versa.
### 2.5 Two independent "seconds in state" clocks
`hn.h` and `hn.i` (both `DateTimeOffset`) are reset together on every state
transition (`hn.b(string)`, `hn.cs:104-112`) **and** on Meta's constructor/
full reset (`hn.g()`) — but only **`hn.h`** is additionally reset every time
the macro is (re)started (`cLogic.StartMacro()`, `uTank2/cLogic.cs:324`,
confirmed as the only other write site to either field via a repo-wide
grep). `c1`/"Seconds in state >=" reads `hn.h` (`c1.cs:17`); `gc`/"Seconds in
state (P) >=" reads `hn.i` (`gc.cs:17`) — the UI's own description string
for `gc` spells this out verbatim: `"Seconds in state (start/stop
persistent):"` (`gc.cs:93`). **Practical effect**: stopping and restarting
the macro while parked in the same state resets the plain "Seconds in
state >=" clock to zero, but the "(P)" persistent variant keeps counting as
though the macro was never stopped.
### 2.6 Watchdog (`h7`) in full
`SetWatchdog` (`f5.c()`, `f5.cs:51-55`) arms `h7` with `(stateToCall,
rangeMeters/240.0, timeSpanSeconds)`. `h7.a(state,range,timeSpan)`
(`h7.cs:37-51`) then: sets the armed flag, stores the target state and
range, sets the sample interval `i = timeSpan/10.0`, resets the ring index to
0, and — **critically** — pre-fills all **10** position-sample slots with a
sentinel `(1000,1000,1000)` far away from any real in-game coordinate
(`global::d.a(1000.0,1000.0,1000.0)`, `h7.cs:45-49`), not the player's actual
position at arm time. `h7.b()` (`h7.cs:58-84`), called once per Meta pass
(§2.2 step 1): if `>= sampleInterval` seconds have elapsed since the last
sample, advance the ring index (mod 10) and overwrite that slot with the
current position; then, regardless, compare the **current** position against
**all 10** stored samples — if **any** one of them is farther than the
configured range, report **not expired**. Only once every one of the 10
samples (which, right after arming, are all the far sentinel and therefore
guaranteed to trip "farther than range") has been overwritten by a real,
close-together sample does expiry become reachable — i.e. the earliest a
freshly-armed watchdog can possibly expire is one full `timeSpan` after
arming, and only if the player genuinely didn't move more than `range`
meters throughout that whole window. `ClearWatchdog` (`ht.c()`, `ht.cs:47-50`)
is a documented **no-op**: a repo-wide grep for `h7.c(` (the disarm method)
finds exactly four call sites, all inside `hn.cs` (constructor, full reset,
state-transition, profile-load) — **never** from `ht.cs`. The "Clear
Watchdog" meta action does nothing to `h7`'s armed state; only an actual
state transition (which also happens to call `h7.c()` as a side effect of
`hn.b(string)`) disarms a running watchdog.
### 2.7 Embedded nav and chat capture — see §1.5 and §3.6/§4
---
## 3. The expression language
### 3.1 Tokenizer (`MyClasses/ExpressionEvaluator.cs:933-1116`)
Character classes (`ExpressionEvaluator<W>.a()`, `:754-783`): digits and `.`
are numeric-candidate chars; `+-*/%#<>=!^;&|` are all **operator** chars
(single-char-classified; multi-char operators like `&&`/`==`/`>=` are
recognized later by string content, `sExpressionToken.d()`, `:166-188`); `(`
and `)` are their own types; **everything else defaults to a "string"
char**. There is **no conventional `"quoted string"` literal** at all:
- A bareword — any run of non-operator, non-paren, non-whitespace,
non-backtick, non-backslash characters — becomes a **String** token
automatically (the tokenizer has no separate "identifier" class; a bare
`foo` and a function-name `foo` are only disambiguated by whether a `[`
immediately follows).
- **Backtick `` ` `` is the only quoting mechanism**: entering backtick mode
(`:958-964`) suspends normal char-classification until a matching closing
backtick; a doubled backtick `` `` `` inside a backtick run is an escaped
literal backtick (`:1057-1072`); an *empty* `` `` `` (opened and
immediately closed with nothing between, and no prior escaped content) is
a parse error, `"Empty \`\` not allowed"` (`:1067`). Backtick-quoted text
is never re-interpreted as a variable name or re-parsed as a number
(`item.e = true` marks it "already resolved", `:1083-1086, 1120-1121`).
- **Backslash `\` is a single-character escape** outside backtick mode
(`:966-970`, consumes the backslash and passes the next character through
literally) — this is how a bareword string embeds an otherwise-special
character (a space, an operator char, a bracket) without needing
backticks for the whole token.
- `true`/`false` are **not** literal grammar — they are two pre-registered
*variables* (`b()`, `:339-343`) returning `1.0`/`0.0`, looked up through
the ordinary bareword-as-string→variable-resolution path (`:1124-1130`) —
a bareword `true` that happens to also be a registered variable name
resolves to the variable's value, not literally to the string `"true"`.
- Function-call syntax is `name[arg1,arg2,...]` — square brackets, not
parens; parens are pure grouping. Nested `[...]` inside an argument
(another function call) is tracked via a bracket-depth counter so commas
inside a nested call don't split the outer argument list (`:986-1041`).
### 3.2 Grammar / precedence (`sExpressionToken.d()`, `:166-188`)
Shunting-yard over the tokenized/pre-resolved queue (`:1147-1226`), operators
compared by this integer precedence table (higher binds tighter; equal
precedence pops left-to-right since the comparison is `<=`, `:1175`):
| Precedence | Operators | Notes |
|---|---|---|
| 9,999,999 (lowest) | `;` | Statement/expression separator — see §3.3 for its unusual evaluation semantics |
| 1 | `&&` `\|\|` `^` | Boolean AND/OR/XOR **all at the same precedence** — no AND-binds-tighter-than-OR rule (unlike C-family languages) |
| 0 | `==` `<` `>` `>=` `<=` `!=` | Comparison |
| 1 | `#` | Regex-match operator (String `#` String → bool) — sits **between** comparison and additive, an unusual placement |
| 2 | `+` `-` | Additive |
| 3 (highest) | `*` `/` `%` | Multiplicative |
Any operator string not in this table throws `"Invalid operator '...'"`
(`:186`).
### 3.3 The `;` operator's actual semantics — returns the LEFT operand
`a(sExpressionToken A_0, A_1, A_2)` (the binary-op evaluator,
`:785-931`): `if (A_2.b == ";") return A_0;` (`:787-790`) — given `a ; b`,
the result is **`a`, discarding `b`'s value entirely**, even though `b` was
still fully evaluated (for side effects) to get there. This is the opposite
of the "last statement's value wins" convention common to most
statement-separator designs. Since `;` participates in the *same*
shunting-yard as every other operator (not a special top-level-only
construct), it can appear **nested anywhere an operator can**, including
inside parens or (per the tokenizer's bracket-depth counter) inside a
function argument slot, as long as normal operator-precedence composition
allows it — e.g. `(a; b) + 1` legally parses and evaluates to `a + 1`.
### 3.4 Types and coercions
`eTokenCharType` values a leaf token can settle into: `b`=Number (double),
`d`=String, `h`=opaque Object (a CLR object riding along in `.d`, with `.b`
holding a display string — used for `Stopwatch`, `Coordinates` (class `d`),
`WorldObject` (class `fu`), and view/control references (`aq`/`c0`
subtypes)). Binary operators are defined **only** for Number/Number and
String/String pairs (`:791-929`); mixing types, or using either operand
Object-typed, throws `"Attempted to operate on two disparate types"` (`:930`)
— Object tokens have **no** operators at all, not even `==`; they can only be
produced/consumed by dedicated functions. A numeric literal is parsed with
`double.TryParse(..., NumberStyles.Any, InvariantCulture)` (`:1135`) — a
parse failure throws a bare `TokenParseError("")` (no message text).
`getobjectinternaltype[]` exposes the raw type-id numbering used internally:
`0`=none/uninitialized, `1`=Number, `3`=String, `7`=Object (`ch.cs:482`
description string — note the enum's actual C# ordinals are `a=0,b=1,c=2,
d=3,...,h=7`, so the description's "3"/"7" line up with `eTokenCharType.d`
and `.h` respectively, skipping the intermediate parser-only states `c`/`e`/
`f`/`g`).
### 3.5 Variables (`ch.cs:131-256`)
Six functions, all operating on a single `Dictionary<string,sExpressionToken>
a` field private to the `ch` subclass (a session-scoped store that lives for
the whole VTank process run, cleared only by `clearallvars[]` or an explicit
`clearvar[]`):
| Function | Arity | Semantics | Cite |
|---|---|---|---|
| `testvar[name]` | 1 | `true` iff a variable of that name is currently defined | `ch.cs:131-151` |
| `getvar[name]` | 1 | Returns the stored value, or `false` (a Number 0) if undefined — **never throws for a missing variable** | `ch.cs:153-173` |
| `setvar[name,value]` | 2 | Stores `value` under `name` (any type), returns `value` | `ch.cs:175-193` |
| `touchvar[name]` | 1 | If undefined, defines it as `false` and returns `false`; if already defined, returns `true` **without changing the existing value** | `ch.cs:195-217` |
| `clearvar[name]` | 1 | Removes the variable if present; returns whether it had existed | `ch.cs:233-255` |
| `clearallvars[]` | 0 | Clears every variable | `ch.cs:219-231` |
There is exactly **one** scope — no persistent (disk-backed) or global
(cross-character) variable tier in retail VTank's expression engine itself
(persistence across sessions is a *profile* concept — `.usd`/`.met` files —
not an expression-variable concept). The backing `Dictionary` (`ch.a`) is
never cleared by anything in this doc's oracle except `clearvar`/
`clearallvars` themselves or a fresh `ch`/`hn` construction
(`s.cs:216`, the **only** `new hn()` call site found by a repo-wide grep) —
since `hn` is constructed once per plugin load, not once per character
login, variables in practice **do survive a character relog** within the
same running Decal/VTank process, matching the earlier research doc's
"Variables persist to relog" note (`2026-07-29-vtank-plugin-automation-
requirements.md:205`) — that note and this doc's "no disk-backed tier"
finding describe the same fact from two angles, not a disagreement.
### 3.6 Lists / dicts
**Retail VTank's expression engine has no list or dictionary type at all.**
`eTokenCharType` has exactly the four settled kinds (Number/String/Object/
uninitialized) enumerated in §3.4; there is no array/collection literal
syntax, no `listcreate[]`/`dictcreate[]`-style function anywhere in `ch.cs`'s
66-function catalog (§3.7), and no collection case in the binary-operator
dispatch (`:785-931`). Any list/dict-shaped function names encountered
belong to **UtilityBelt's** dialect, not retail VTank's — see §5.
### 3.7 Complete built-in function table
20 inherited from the base `ExpressionEvaluator<W>` plus 46 registered by the
`ch` subclass (all discovered via one-time reflection over every
`[Expr_FunctionName]`-attributed method across the class hierarchy,
`ExpressionEvaluator.cs:306-318`) — **66 total**. Every function validates
its own arity by comparing `A_0.Count` against the hardcoded literal in its
body (the `[Expr_ParamCount]` attribute is metadata for the in-game function
browser, not itself enforced at call time). "Coordinates"/"WorldObject"/
"ViewControl" params are runtime type-checked against the boxed `.d` field's
CLR type (`typeof(d)`/`typeof(fu)`/subclass-of `c0`).
**Base engine (20):**
| Name | Arity | Semantics | Cite |
|---|---|---|---|
| `true` (variable) | 0 | `1.0` | `ExpressionEvaluator.cs:345-348` |
| `false` (variable) | 0 | `0.0` | `:350-353` |
| `isfalse[x]` | 1 | `true` iff `x` is Number `0` | `:355-374` |
| `istrue[x]` | 1 | `true` iff `x` is Number and nonzero | `:376-395` |
| `iif[cond,a,b]` | 3 | `a` if `cond` is a nonzero Number, else `b` | `:397-423` |
| `randint[min,max]` | 2 | Random int in `[min,max)` | `:425-446` |
| `cstr[n]` | 1 | Number→string, `ToString()` (current-thread culture) | `:448-463` |
| `strlen[s]` | 1 | String length | `:465-480` |
| `getobjectinternaltype[x]` | 1 | Raw type-id number (§3.4) | `:482-493` |
| `cstrf[n,fmt]` | 2 | Number→string with a .NET format string | `:495-515` |
| `stopwatchcreate[]` | 0 | New Stopwatch object (not started) | `:517-534` |
| `stopwatchstart[sw]` | 1 | Starts it, returns it | `:536-561` |
| `stopwatchstop[sw]` | 1 | Stops it, returns it | `:563-588` |
| `stopwatchelapsedseconds[sw]` | 1 | Elapsed ms / 1000 | `:590-615` |
| `cnumber[s]` | 1 | String→double via `TryParse`, `0` on failure | `:617-633` |
| `floor[n]` | 1 | `Math.Floor` | `:635-652` |
| `ceiling[n]` | 1 | `Math.Ceiling` | `:654-671` |
| `round[n]` | 1 | `Math.Round` | `:673-690` |
| `abs[n]` | 1 | `Math.Abs` | `:692-709` |
| (internal `setvar` shadow) | — | Overridden by `ch`'s own `setvar` (§3.5) | — |
**`ch` subclass (46):**
| Name | Arity | Semantics | Cite |
|---|---|---|---|
| `testvar` `getvar` `setvar` `touchvar` `clearvar` `clearallvars` | 1/1/2/1/1/0 | §3.5 | `ch.cs:131-256` |
| `getcharintprop[key]` | 1 | Character `IntValueKey` property, `false` if unset | `ch.cs:257-283` |
| `getcharquadprop[key]` | 1 | Character `QuadValueKey` (precision loss above 2^531, doubles) | `ch.cs:285-311` |
| `getchardoubleprop[key]` | 1 | `DoubleValueKey` | `ch.cs:313-339` |
| `getcharboolprop[key]` | 1 | `BoolValueKey` | `ch.cs:341-367` |
| `getcharstringprop[key]` | 1 | `StringValueKey` | `ch.cs:369-395` |
| `getisspellknown[spellid]` | 1 | Spell present in spellbook | `ch.cs:397-418` |
| `getcancastspell_hunt[spellid]` | 1 | Castable per hunt-tier scarab/skill check (`SpellDiffExcessThreshold-Hunt`) | `ch.cs:420-446` |
| `getcancastspell_buff[spellid]` | 1 | Same, buff-tier (`SpellDiffExcessThreshold-Buff`) | `ch.cs:448-474` |
| `getcharvital_base[1/2/3]` | 1 | Base Health/Stamina/Mana (1=H,2=S,3=M; clamps result to a minimum of 1) | `ch.cs:476-496` |
| `getcharvital_current[1/2/3]` | 1 | Current value | `ch.cs:498-...` |
| `getcharvital_buffedmax[1/2/3]` | 1 | Buffed max | `ch.cs:520-...` |
| `getcharskill_traininglevel[skill]` | 1 | 0=Unusable,1=Untrained,2=Trained,3=Specialized | `ch.cs:543-559` |
| `getcharskill_base[skill]` | 1 | Base skill value | `ch.cs:560-576` |
| `getcharskill_buffed[skill]` | 1 | Buffed skill value | `ch.cs:577-591` |
| `getplayerlandcell[]` | 0 | Current landcell id (incl. landblock portion) | `ch.cs:592-608` |
| `getplayercoordinates[]` | 0 | Coordinates object, physics-predicted position | `ch.cs:610-632` |
| `coordinategetns[c]` `coordinategetwe[c]` `coordinategetz[c]` | 1 each | N/S, W/E, Z components of a Coordinates object | `ch.cs:634-713` |
| `coordinatetostring[c]` | 1 | String form (`d.ToString()`) | `ch.cs:715-740` |
| `coordinateparse[s]` | 1 | Parses `"00.0N, 00.0W"` (no Z); `false` on failure | `ch.cs:742-767` |
| `coordinatedistancewithz[c1,c2]` | 2 | 3D distance in **meters** (raw units × 240) | `ch.cs:769-808` |
| `coordinatedistanceflat[c1,c2]` | 2 | 2D distance in meters (Z ignored) | `ch.cs:810-849` |
| `wobjectgetphysicscoordinates[obj]` | 1 | Coordinates object for a WorldObject | `ch.cs:851-881` |
| `wobjectgetname[obj]` | 1 | Object's display name | `ch.cs:883-907` |
| `wobjectgetobjectclass[obj]` | 1 | ObjectClass number | `ch.cs:909-...` |
| `wobjectgettemplatetype[obj]` | 1 | Template-type number | `ch.cs:936-960` |
| `wobjectgetisdooropen[obj]` | 1 | Bool | `ch.cs:961-...` |
| `wobjectfindnearestmonster[]` | 0 | Nearest **non-blacklisted** monster or `false` | `ch.cs:991-1032` |
| `wobjectfindnearestdoor[]` | 0 | Nearest door or `false` | `ch.cs:1034-1072` |
| `wobjectfindnearestbyobjectclass[class]` | 1 | Nearest object of a given ObjectClass (excludes self) | `ch.cs:1075-1120` |
| `wobjectfindininventorybytemplatetype[type]` | 1 | First inventory match | `ch.cs:1122-1157` |
| `wobjectfindininventorybyname[name]` | 1 | First inventory match, **exact** name | `ch.cs:1159-1185` |
| `wobjectfindininventorybynamerx[pattern]` | 1 | First inventory match, regex (default case-sensitivity — new `Regex(pattern)` with no options) | `ch.cs:1187-1222` |
| `wobjectgetselection[]` | 0 | Currently-selected object or `false` | `ch.cs:1224-1245` |
| `wobjectgetplayer[]` | 0 | The player's own WorldObject | `ch.cs:1247-1268` |
| `wobjectfindnearestbynameandobjectclass[class,pattern]` | 2 | Nearest match on both criteria | `ch.cs:1270-1322` |
| `actiontryselect[obj]` | 1 | Selects the object; always returns `false` | `ch.cs:1324-1349` |
| `actiontryuseitem[obj]` | 1 | Uses it if owned; bool success | `ch.cs:1351-1381` |
| `actiontryapplyitem[a,b]` | 2 | Applies `a` to `b` (select `b`, use `a`, restore prior selection) | `ch.cs:1383-1436` |
| `actiontrygiveitem[item,target]` | 2 | Gives an item to a player/NPC | `ch.cs:1438-1488` |
| `actiontryequipanywand[]` | 0 | One step toward equipping any profile wand; `true` if already equipped | `ch.cs:1490-1505` |
| `actiontrycastbyid[spellid]` | 1 | Untargeted cast attempt; `0`/`1`/`2` (not-yet/begun/impossible) | `ch.cs:1507-1539` |
| `actiontrycastbyidontarget[spellid,obj]` | 2 | Targeted cast attempt, same return convention | `ch.cs:1541-1575` |
| `chatbox[s]` | 1 | Sends `s` verbatim to chat | `ch.cs:1577-1591` |
| `chatboxpaste[s]` | 1 | Pastes into the chat input box (strips control chars) without sending | `ch.cs:1593-1624` |
| `statushud[key,value]` | 2 | Updates a Virindi HUD Status-HUD row under the fixed group name `"VTank Meta"` | `ch.cs:1626-1647` |
| `statushudcolored[key,value,rgb]` | 3 | Same, with an explicit RGB color (forced fully opaque via `\| 0xFF000000`) | `ch.cs:1649-1672` |
| `uigetcontrol[viewname,controlname]` | 2 | Opaque ViewControl reference into a Meta-created view, or `false` | `ch.cs:1674-1700` |
| `uisetlabel[control,text]` | 2 | Sets a Button's label; **throws for any control type other than Button** | `ch.cs:1702-1720` |
| `uisetvisible[control,bool]` | 2 | Sets any control's visibility | `ch.cs:1722-1742` |
### 3.8 Error behavior
Every function/tokenizer failure throws `TokenParseError` (a plain
`Exception` subclass, `ExpressionEvaluator.cs:263-271`); the **top-level**
entry point `ch.a(string,W,out bool,out bool)` (invoked as `dz.at.k.a(text,
0, out consumed, out isError)` everywhere in the codebase) catches only
`TokenParseError` and converts it into a String-typed result token holding
the error message, setting the `isError` out-param — **no other exception
type is caught** (`ExpressionEvaluator.cs:734-752`). Callers (every
condition/action's own execute method) uniformly just log the error string
to chat (`ah.a("Error in ...: " + text + " (" + result.b + ")")`) and
otherwise degrade to a safe default (a condition treats an error as `false`;
most actions still return `true`, i.e. still let the pass continue). There
is no exception propagation up to the scheduler — a malformed expression
never crashes Meta, it just silently no-ops that one condition/action for
that pass (with a chat warning).
---
## 4. Views from metas (`CreateView`)
### 4.1 The 5-view cap — actually a 6-view cap (off-by-one)
`bw.a(string,string)` (`bw.cs:23-45`) refuses a new `CreateView` only when
`bw.b.Count > 5`**strictly greater than**, not `>=`. Since this check
runs *before* the new view is added, a 6th distinct view name is still
permitted (the check only blocks the 7th). `bw.b(name,xml)` also requires
the host's Decal version to be `>= 1.0.0.45` (`bw.cs:14-21`) — below that,
`CreateView` silently no-ops. Creating a view under a name that already
exists **replaces** it (destroys the old one first, `bw.cs:29-32`) rather
than stacking a duplicate.
### 4.2 The markup dialect (`aq.b(string)`, `aq.cs:41-106`)
- The XML **root element's tag name is never checked** — only its `title`
(string, `aq.cs:59`), `width`, `height` (ints, `aq.cs:60-61`) attributes
are read. Any root tag name works.
- Requested `width`/`height` are validated against `RegionWindow.{Width,
Height} - 100` in each dimension (`aq.cs:62-73`) — an **oversized request
is rejected outright** (view creation fails with a chat error naming the
current max), never silently clamped.
- The window's **only required content** is its root's `FirstChild`, which
must literally be an element named `"control"` (case-insensitive,
`aq.cs:108-118`); anything else there yields a window with no content.
- Exactly **two** `type=` values are recognized by the control factory
(`d6.a(string)`, `d6.cs:3-11`, case-insensitive): `"BUTTON"` → class `c9`,
`"LAYOUT"` → class `au`. Any other `type` value returns `null` (that
`<control>` and everything under it is silently dropped).
- Base control attributes (`c0.a(XmlNode,...)`, `c0.cs:20-28`): `left`,
`top`, `width`, `height` (ints), `name` (string — if non-empty, the
control is registered by that name into the view's flat
`Dictionary<string,c0>`, **regardless of nesting depth**, for later
`uigetcontrol[viewname,controlname]` lookup).
- `Button` (`c9.a`, `c9.cs:22-32`) adds `text` (label), `actionexpr` (an
arbitrary meta expression string, evaluated on click via the *same*
`dz.at.k` evaluator instance used everywhere else — errors chat-logged,
never crash the click handler), and `setstate` (a state name — if
non-empty, transitions Meta on click, **independently of and in addition
to** `actionexpr`; both fire on the same click if both are present,
`c9.cs:34-49`).
- `Layout` (`au.a`, `au.cs:18-32`) is a pure container: it recursively
parses every **direct** child XML node via the same `aq.a(node,...)`
dispatcher (so `Layout`-inside-`Layout` nesting is unlimited), rendering
each child at its own declared `left/top/width/height` inside the parent's
`HudFixedLayout`.
- There is **no** list/combo/slider/edit/checkbox/tab control type in this
dialect — it is a small, deliberately minimal 2-tag subset, unrelated to
the ~9-tab, list-heavy `mainView.xml` dialect documented in KB doc 08 for
VTank's *own* main window (that one goes through
`VirindiViewService.XMLParsers.Decal3XMLParser`, a completely different,
much larger parser).
### 4.3 Control binding to variables/expressions
Bindings are all **one-directional and event-driven**, not a live/continuous
data-binding model:
- `uigetcontrol[view,name]` → an opaque ViewControl token, consumed later by
`uisetlabel`/`uisetvisible`.
- A Button's `actionexpr` runs **once per click**, through the shared
session-scoped expression-variable store (§3.5) — there is no
automatic re-evaluation on a timer or on variable change; a view that
needs to reflect changing state must be driven explicitly (e.g. a
`Chat Expression`/`Expression Action` meta rule calling `uisetlabel[...]`
each pass).
- `DestroyView[name]`/`DestroyAllViews[]` are the only teardown paths besides
the implicit "recreate under the same name" replace-on-CreateView
behavior (§4.1).
---
## 5. The MossTank gap
Files read: `Meta.cs`, `MetaViewManager.cs`, `MossTankMetaProfileStore.cs`,
`VtankMetaProfileSerializer.cs` (+ `VtankMetaProfileSerializerTests.cs`),
`Expressions/ExpressionEngine.cs`, `Expressions/ExpressionRuntime.cs`,
`Expressions/ExpressionValue.cs`, `Expressions/CoreExpressionFunctions.cs`,
`Expressions/HostExpressionFunctions.cs`, `Expressions/
MossTankExpressionRuntime.cs`.
### 5.1 Can a real `.met` load today? — Yes, at the byte/structural level
`MossTankMetaProfileStore.Import` (`MossTankMetaProfileStore.cs:119-157`)
reads a file named `<profile>.met` out of the plugin's `imports/`/`exports``
storage and calls `VtankMetaProfileSerializer.TryLoad`
(`VtankMetaProfileSerializer.cs:23-63`). Cross-checking that serializer's
condition/action type-id tables (`ConditionType`/`ConditionKind`,
`VtankMetaProfileSerializer.cs:463-527`; `ActionType`/`ActionKind`,
`:529-569`) against the retail `h`/`bp` enum ordinals derived independently
in §1.3/§1.4 (via `d9.cs`'s registration order) shows a **byte-for-byte
match on every single type id, including the real retail gap at `h.ab`=27**
(the serializer has no `case 27`, matching retail's own unregistered id).
Every per-type field layout checked (§1.3/§1.4's key names: `n`/`c`/`r`/`p`
for the monster/inventory conditions, `st`/`ret` for CallMetaState, `s`/`r`/
`t` for SetWatchdog, `o`/`v` for Get/SetOption, `n`/`x` for CreateView, `p`/`c`
for ChatMessageCapture, the uppercase `K`/`V` vs. lowercase `k`/`v` compound-
vs-leaf table distinction, and the two distinct "zero fields" encodings from
§1.4) matches the decompiled source **exactly**, including the embedded
nav-route's own sub-format (mode 1/2/3/4 dispatch, node-type-dependent extra
line counts). This is an unusually precise reverse-engineering result — the
`.met` **byte format itself is not where the gaps are**.
### 5.2 Ranked semantic gaps
| # | Gap | Retail behavior | MossTank behavior | Impact |
|---|---|---|---|---|
| 1 | **`CreateView` markup is not translated at all** | Root tag name irrelevant; requires a `<control type="Button"\|"Layout">` first child (§4.2) | `MetaViewManager.Create` (`MetaViewManager.cs:37-40`) requires the XML root to be literally named `"panel"` (acdream's own native panel-markup convention) and hands the raw string straight to `RegisterPanelContent` — no VTank Button/Layout/`actionexpr`/`setstate` parser exists anywhere in the plugin (confirmed absent by grep) | **Highest.** A real `.met`'s `CreateView` action (root tag anything, first child `<control type="Layout">`) will be rejected outright by the `"panel"` check and never render. The 5/6-view off-by-one quirk itself *is* faithfully preserved (`MetaViewManager.cs:15,32`, with an explicit code comment citing `bw.a`'s `Count > 5`) — only the control dialect is missing. |
| 2 | **Nested `All` action short-circuits instead of unconditionally running every child** | `f.b()` (`f.cs:30-37`) calls every child's `.h()` **ignoring its return value**, and the `All` action itself always returns `true` — only the *outer* pass loop's separate "did the state change" check can end the pass once `All` finishes | `Meta.cs:374-380`: `foreach (child) { if (!ExecuteAction(child)) return false; }` — stops at the **first** child that reports "don't continue" | **High.** A `.met` rule using `All` to sequence e.g. `SetMetaState` + `ChatCommand` + `ClearWatchdog` will, in MossTank, run only `SetMetaState` and silently drop the remaining siblings (since `SetMetaState` always reports `false`); retail runs every sibling regardless, and only the whole *pass* stops afterward. |
| 3 | **`MonsterNameCountWithinDistance`'s name pattern is case-insensitive in MossTank, case-sensitive in retail** | `d7.c()` compiles with `RegexOptions.Compiled` only (`d7.cs:26`) — case-sensitive | `Meta.cs`'s `MonsterCount` helper (`Meta.cs:536-552`) compiles with `RegexOptions.IgnoreCase \| RegexOptions.CultureInvariant` | **Medium-high.** A pattern authored against retail's case-sensitive matching (e.g. deliberately excluding a differently-cased variant name) will over-match in MossTank. Note `ChatMessage`/`ChatMessageCapture` do **not** have this divergence — MossTank's `ChatMatch` (`Meta.cs:463-513`) is correctly case-sensitive (`RegexOptions.CultureInvariant` only), matching `hl.cs`/`c5.cs`. |
| 4 | **`;` sequence-operator value convention is inverted vs. retail (but matches UtilityBelt, and was chosen deliberately)** | Retail VTank: `a;b` evaluates to **`a`** (the left/first operand), discarding `b`'s value, while still executing `b` for side effects (`ExpressionEvaluator.cs:787-790`); `;` is an ordinary operator that can nest anywhere via normal precedence | `ExpressionProgram.Evaluate` (`ExpressionEngine.cs:20-27`) treats top-level `;`-separated statements as a `Node[]` program and returns the value of the **last** statement — this matches UtilityBelt's own audited grammar ("multiple `;`-separated statements, returning the final result", `2026-08-26-mosstank-vtank-utilitybelt-research.md:237`), which the campaign explicitly chose as MossTank's baseline dialect (§3.1 there) | **Medium, and by design, not an oversight.** Retail VTank and UtilityBelt already disagree with each other on `;`'s return value; MossTank correctly implements UtilityBelt's convention. The compat risk is narrower than a plain bug: only a `.met` authored *against retail's own* semantics (chaining `sideeffect[]; realcheck[]` and relying on the *first* value) evaluates to the opposite result once imported. |
| 5 | **Watchdog ring pre-fill differs (far sentinel vs. arm-time position)** | All 10 position-history slots start as a sentinel far from any real coordinate (`(1000,1000,1000)`, `h7.cs:45-49`), guaranteeing expiry cannot even be *reachable* until one full `timeSpan` window has elapsed and every slot has been overwritten with a real sample | `SetWatchdog` (`Meta.cs:585-596`) pre-fills all 10 slots with the **current** position at arm time | **Lower-medium.** Both converge on "earliest possible expiry is ~one `TimeSpanSeconds` after arming" in the common case, but they diverge for a watchdog that is armed, then the player leaves and returns to very near the arm point before the window closes — retail's sentinel-seeded ring cannot spuriously read the arm-time position as one of its 10 samples, MossTank's can. |
### 5.3 Confirmed non-gaps (the format/engine is otherwise unusually faithful)
- The full expression-variable family (`getvar`/`setvar`/`testvar`/
`touchvar`/`clearvar`/`clearallvars`) is present under identical names and
identical semantics (`CoreExpressionFunctions.cs:43-81`) — MossTank
additionally exposes **persistent** (`pgetvar`/…) and **global**
(`ggetvar`/…) scoped variants layered on top via the same helper (a
superset addition, not a compat break, since the retail names still work
unqualified).
- The "Seconds in state" vs. "(P)ersistent" reset-on-macro-start-vs-not
distinction (§2.5) is reproduced exactly: `_stateSeconds` resets in both
`Transition()` and `SetEnabled(true)` (`Meta.cs:172-188, 222-230`);
`_persistentStateSeconds` resets only in `Transition()`.
`ChatMessageCapture`'s malformed-color-list failure mode (retail: the
whole filter list is discarded and the condition then matches nothing;
MossTank: `ParseKinds` returns an empty-but-non-null set on any bad token,
`Meta.cs:515-527`, which likewise matches nothing) is behaviorally
equivalent despite a different internal representation.
- The tokenizer is a strict **superset** of retail's grammar: it accepts
backtick, single-, and double-quoted strings plus retail's bareword
fallback (`ExpressionEngine.cs:374-399`) — every legal retail `.met`
expression string still tokenizes the same way.
- `floor`/`ceiling`/`round`/`abs`/`getobjectinternaltype` and the whole
"nearest object" function family
(`wobjectfindnearestbyobjectclass`/`…bynameandobjectclass`/`…door`/
`…monster`) are all present, just registered through small helper
functions rather than one-line literal string registrations (initially
looked like gaps under a naive grep; verified present by direct read,
`HostExpressionFunctions.cs:567-576`, `CoreExpressionFunctions.cs:87-95`).
- Retail VTank's expression engine has **no** list/dict type at all (§3.6);
MossTank's extensive `list*`/`dict*` function family
(`CoreExpressionFunctions.cs`) is a pure **UtilityBelt-dialect addition**
with no retail counterpart to diverge from.
---
## 6. Could not determine
- **`fm.cs` (Time Left On Spell >=, `h.x`=23)** was cited by name/type-id
only (`fm.cs:52`, the display-name string); its field layout in §1.3 (`sid`
int spell id, `sec` int seconds) is inferred from
`VtankMetaProfileSerializer`'s parity with every *other* independently
verified type in this doc, not from an independent read of `fm.cs`'s own
`a(gy)`/`e()` methods.
- **Action type `bp.a`="None" (`a.cs`)** was not independently re-read in
this pass (its "bare int 0, always returns true, no-op" shape is inferred
from its retail UI name and from `VtankMetaProfileSerializer`'s case-0
handling, which groups it with the confirmed-`dh`-shaped `ReturnFromCall`).
- **`ChatMessageCapture`'s wiki-referenced color-id numbering** ("Chat
colorid list", `c5.cs:19-20`) — the exact mapping from small integers to
named chat colors was not traced here; it lives in KB's chat-color
reference material (`memory/reference_retail_chat_colors.md`), out of
scope for this expression/meta-focused doc.
- **Whether a `.met` rule's `State` field can itself contain a `;`- or
bracket-bearing string** that would confuse the flat-line `y`/`bd` reader
— not tested against a real hand-crafted adversarial file; the worked
decode in §1.2 uses only well-formed sample data actually observed on
disk.
- **The exact behavior of `MetaViewManager`'s hashed window-id collision
handling** (`MetaViewManager.cs:78-82`, SHA-256-derived id) under two
*different* view names that happen to collide in the truncated 8-byte
hash — astronomically unlikely, not pursued.

View file

@ -26,6 +26,13 @@ implementation order.
| 08 | [UI views](08-ui-views.md) | Every control of the nine tabs and three secondary views (type, geometry, binding), VVS control semantics our markup must offer, the markup extension needed (multi-column lists) | `uTank2.ViewXML.*.xml`, VVS `HudList/HudCombo/HudTabView`, `Decal3XMLParser.cs` |
| 09 | [commands and interop](09-commands-and-interop.md) | The `/vt` verb table (~48 documented + 15 parser-only), chat sinks and dedup, the three-tier export API, Classic Looter SPI, MosswartMassacre's real usage | `uTank2/PluginCore.cs`, `d5.cs`, `ah.cs`, `eExternalsPermissionLevel.cs` |
**Owner amendment 2026-09-06 — metas and navs use metaf `.af`.** Docs 06 §1
and 07 §1 document VTank's binary `.nav`/`.met` layouts; MossTank does not
implement them. It reads and writes the human-readable metaf format (the
reference converter is `C:\Users\erikn\source\repos\metas\metaf_monolithic.py`,
see `claude-memory/reference_metaf_af_format.md`). The semantics sections of
06/07 (execution, FSM, expressions, views) remain the behavior oracle.
Local companion note (not committed): `refs/vtank/notes/2026-09-06-idlepeace-fcm-trace.md`
— the line-level trace of the combat-mode choke point, the eight
drop-to-peace sites and the wield sequencing.