docs(vt): fold the citation-pass corrections into KB 07 (hn.cs line map, dh/q hierarchy, 74-function count, ReturnFromCall guard, resolved fm/a shapes)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-06 20:15:22 +02:00
parent b7ed65bc64
commit 642eacc13c

View file

@ -13,7 +13,7 @@ Cross-referenced against `docs/research/2026-07-29-vtank-plugin-automation-requi
`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
exact `[Expr_FunctionName]` attribute count is **74** — 17 base + 57 `ch`, §3.7; both prior figures were low, so this is "both undercounted", not a 67-vs-66 conflict); 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*
@ -45,8 +45,8 @@ at the tree checked out in worktree `eloquent-hugle-42119e`.
| `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` |
| `dh` | Abstract `bx` base providing the parent-link/refresh plumbing; declares `i()`/`h(gy)` **abstract**, so it imposes no on-disk shape. Subclasses span every shape: zero-arg (`ct`,`do`,`gk`,`cy`,`i`,`co`,`ao`), K/V table (`ea`), `f6` blob (`ff`), scalar int (`ge`,`hy`,`gw`), and the `q``dj` form bases (citation pass 2026-09-06) | `dh.cs:5-14` |
| `q` | Shared N-row UI-form base (`q : dh`); **`dj` derives from it**. Direct subclasses: `c1`, `gc`, `e3` (bare-int scalar on disk) and `fm` (hand-rolled `bd("k","v")`). `ge`/`hy` extend `dh` directly, not `q` | `q.cs:8`, `dj.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` |
@ -70,18 +70,18 @@ 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",
— confirmed by `hn.k()` (save, `hn.cs:339-362`): `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]))`
Load is the mirror in `hn.a()` (`hn.cs:364-409`): `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`).
logging `"Meta file load: unsupported cond/act type."` (`hn.cs:382-387`).
A DB-format failure (missing `"CondAct"` key) does the same
(`hn.cs:379-382` load path/`hn.cs:398`).
(`hn.cs:371-376`).
### 1.2 Worked decode — one real rule from a live `.met`
@ -157,11 +157,11 @@ exactly 1 (`ea.cs:55`, rejects/ignores anything else on load).
| 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`) |
| 23 | x | Time Left On Spell >= | `fm` | `bd("k","v")` table, 2 rows (hand-rolled — `fm : q`, not `dj`-derived, same wart as `cp`) | `sid`=int spell id, `sec`=int seconds | `fm.cs:61-86` |
| 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) |
| 27 | **ab** | **— unregistered —** | — | n/a | n/a | `d9.cs:14-57` (the gap itself at `d9.cs:40-41`, `h.aa``h.ac`)`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
@ -195,8 +195,9 @@ suggests.
| 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
conflated:** the bare-cell zero-arg types (`ic`/`a6`/`ct`/`do`/`gk`/`cy`/`i`/`co`/
`ao`, plus the confirmed `bp.a`="None" action, `a.cs:42-44`; note `ic`, `a6`
and `a` implement `az`/`ho` directly and each spells out `gy.a(0)` itself) 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
@ -242,12 +243,12 @@ 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`)
### 2.2 One pass, `hn.h()`, in full (`hn.cs:41-92`)
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
`"Disabling Meta."` and set `EnableMeta` false (`hn.cs:51-56`); 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
@ -298,7 +299,7 @@ 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
on `hn.b(string)` — a state transition (`hn.cs:115-124`, `e.Clear()` at `:117`) — 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
@ -316,8 +317,9 @@ string (`m_b`, defaulting to `"Default"` if left blank in the UI — not the
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
10000-entry overflow guard as the watchdog-expiry path (§2.2 step 1,
`cp.cs:144-150`) — `CallMetaState` only; `ReturnFromCall` has NO depth guard,
just an empty-stack underflow check (`ao.cs:75-81`). **Both 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.
@ -325,8 +327,8 @@ 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
transition (`hn.b(string)`, `hn.cs:115-124`) **and** on Meta's constructor/
full reset (`hn.c()`, `hn.cs:32-39` — not `hn.g()`, which touches neither field) — 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
@ -454,7 +456,7 @@ 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`
`0`=none/uninitialized, `1`=Number, `3`=String, `7`=Object (`ExpressionEvaluator.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`/
@ -496,23 +498,26 @@ finding describe the same fact from two angles, not a disagreement.
`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
74-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
17 inherited from the base `ExpressionEvaluator<W>` plus 57 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
`ExpressionEvaluator.cs:306-318`) — **74 total** (plus the two pre-registered *variables* `true`/`false`,
`ExpressionEvaluator.cs:341-342`, for 76 resolvable names; exact
`[Expr_FunctionName]` attribute counts: 17 in `ExpressionEvaluator.cs`, 57 in
`ch.cs`). 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):**
**Base engine (17 functions + 2 variables):**
| Name | Arity | Semantics | Cite |
|---|---|---|---|
@ -535,9 +540,8 @@ CLR type (`typeof(d)`/`typeof(fu)`/subclass-of `c0`).
| `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):**
**`ch` subclass (57):**
| Name | Arity | Semantics | Cite |
|---|---|---|---|
@ -762,16 +766,10 @@ line counts). This is an unusually precise reverse-engineering result — the
## 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`).
- RESOLVED (citation pass 2026-09-06): `fm.cs:61-86` hand-rolls `bd("k","v")`
with rows `sid`/`sec` (UI labels "SpellID" / "Time Left (Seconds)",
`fm.cs:43-44`); `a.cs:42-44` writes bare int `0`, `a.cs:32-34` loads nothing,
`a.cs:86-89` returns `true` — the inferred shapes were right.
- **`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