From 642eacc13cbedfdb598cf4ec827246d4cf61435f Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 6 Sep 2026 20:15:22 +0200 Subject: [PATCH] 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 --- .../vtank-kb/07-meta-and-expressions.md | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/docs/research/vtank-kb/07-meta-and-expressions.md b/docs/research/vtank-kb/07-meta-and-expressions.md index 3c487fcf..81227994 100644 --- a/docs/research/vtank-kb/07-meta-and-expressions.md +++ b/docs/research/vtank-kb/07-meta-and-expressions.md @@ -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` (`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`, the 5-view cap, version gate | `bw.cs` | | `aq` | One created view: XML parse, size-vs-window clamp, `Dictionary` 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`) 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`) 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` 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` plus 46 registered by the +17 inherited from the base `ExpressionEvaluator` 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