From 887242cd151aaef87e7d0fdb1e4dda48733e78eb Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 6 Sep 2026 20:02:39 +0200 Subject: [PATCH] docs(vt): fold the citation-pass corrections into KB 02 (ga.a line map, gj/gs identity, fd.cs IdlePeace gate scope, VitalPlan.Threshold branch, entry counts) Co-Authored-By: Claude Fable 5.1 --- .../vtank-kb/02-scheduler-and-actions.md | 64 +++++++++---------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/docs/research/vtank-kb/02-scheduler-and-actions.md b/docs/research/vtank-kb/02-scheduler-and-actions.md index dc0ad538..a2652eae 100644 --- a/docs/research/vtank-kb/02-scheduler-and-actions.md +++ b/docs/research/vtank-kb/02-scheduler-and-actions.md @@ -66,8 +66,8 @@ ey.b()`) — i.e. it coalesces multiple timer/event firings within the same render frame into one pass. `SchedulePoke()` (`cLogic.cs:154-161`) is the fast-wake path: two Decal -game-state trackers, `gj` (spell-cast completion, `gj.cs`) and `gs` -(attack/missile completion, `gs.cs`), each fire a `b`-enum "completed/idle" +game-state trackers, `gj` (the SpellCaster cast-completion tracker, `gj.cs:269,326,442`) and `gs` +(the WandCaster cast-completion tracker, `gs.cs:178,194,233,442` — NOT attack/missile completion; citation pass 2026-09-06), each fire a `b`-enum "completed/idle" event that `cLogic` subscribes to (`cLogic.cs:52-59, 125-139`); on that event `SchedulePoke()` stamps the dedup counter to the *current* frame and re-arms `f` for **1 ms**, forcing a near-immediate re-poke on the very @@ -121,7 +121,7 @@ is the actual pass, called as `a(l, n)` from `TryPokeMacro` and as (in the second case) immediately calls `StopMacro()` on itself. - Sets `dz.o.c = true` (the single "is the macro running" flag almost every `ValidNow`/`Running` body reads), clears the loot-wait table - (`dz.o.j`), resets the equip-retry counter (`dz.o.r = 0`), clears every + (`dz.o.l()` and `dz.o.k()`, `cLogic.cs:319,321` — the field letter `j` in the first draft was wrong), resets the equip-retry counter (`dz.o.r = 0`), clears every `ActionLockType` cooldown (`dz.o.n.b()`), resets Meta, arms the 293 ms timer and starts it, disables every item in the macro-disabled Always Rules list `m` (they're only meaningful while stopped — see §1.1), and @@ -155,7 +155,7 @@ lock-target-requested, corpse-open-attempt, recharge-level-boost per vital, random-helper-buff-lock, buff-cast-recast) — see the "Suppressors" column in §2. `ga.a(CombatState,...)` (the shared wield/mode gate, §3) also returns `false` outright whenever `Actions.BusyState != 0` -(`ga.cs:1454-1458`), which starves every rule that routes through it +(`ga.cs:1448-1451`), which starves every rule that routes through it (BuffSelf, DispelSelf, DispelAllies, RechargeOther, UseHealersHeart, RandomHelper, Attack's own wield step) for that tick without any explicit "paused" bookkeeping. @@ -190,7 +190,7 @@ VTank schedules rules on two separate tracks: ### 2.2 Main-track priority order -All 39 entries of `l`, in the exact order `InitializeDefaultLogicRules` +All 66 entries of `l`, in the exact order `InitializeDefaultLogicRules` (`cLogic.cs:433-578`) adds them. Sentinel rows are markers only (their `ValidNow` is hard-coded `false`, `LogicRuleSentinel.cs:11`); they never win, but their names are the section headers VTank's own debug UI/log @@ -487,38 +487,38 @@ near-identical body to `cr`, including its own copy of the stuck-combat- state handling for level-boost spells. No `new be(` appears anywhere in the decompiled tree — it is superseded, unreachable code, not a rule any current VTank build ever runs. Flagged here so it is not mistaken for a -27th live rule. +25th live rule. --- ## 3. Combat-mode and equipment sequencing ### 3.1 The shared gate: `ga.a(CombatState, int, bool, eDamageElement, -ePrismaticDamageBehavior, int)` (`ga.cs:1421-1568`) +ePrismaticDamageBehavior, int)` (`ga.cs:1433-1573`) Every rule that needs to cast a buff/heal/recharge/dispel spell — BuffSelf, DispelSelf, DispelAllies, RechargeOther, UseHealersHeart, RandomHelper — calls through this **one** subroutine (via an overload chain, -`ga.cs:1417-1447`) instead of switching mode itself. Per call: +`ga.cs:1415-1431`) instead of switching mode itself. Per call: 1. **Busy check.** If `Actions.BusyState != 0`, return `false` - immediately — `ga.cs:1454-1458`. + immediately — `ga.cs:1448-1451`. 2. **Resolve the primary item to wield.** - Start from the explicit override `A_1` (an item id), if the caller passed one, and validate it's owned by the player and currently wieldable (`b(WorldFilter[num])`); if invalid, fall back to `0` and - log a warning — `ga.cs:1459-1470`. + log a warning — `ga.cs:1452-1461`. - **Prefer whatever is already wielded**: if auto-select is on (`A_2`) or no override was given, *and* something is already wielded whose weapon-class already maps to the requested `CombatState` (via `a(ObjectClass)`, `ga.cs:1573-1607`), keep using that item instead of - switching — `ga.cs:1471-1475`. + switching — `ga.cs:1462-1465`. - Otherwise, call the private `a()` helper (`ga.cs:1398-1411`): scan `dz.k.j()` (a tracked item-id list — see §0; **its population order is not established from this file alone**, see §6) for the first owned item classified `fi.b` ("is a wand"); if none, post **"You must add at least one wand to your profile."**, `StopMacro()`, return - `false` — `ga.cs:1477-1481`. + `false` — `ga.cs:1466-1475` (message at 1471). 3. **Resolve a secondary/held item** the same way (`A_5`), and whether a spell/element re-selection is needed (`bv.a(...)`, an element/prismatic-compatibility check) — `ga.cs:1482-1499`. @@ -536,8 +536,8 @@ calls through this **one** subroutine (via an overload chain, - Once in Peace mode (`r` reset to 0, `ga.cs:1527`): if the target item is a wand type (`fi.h`) *and* a secondary must also change, unequip the current primary first if one is worn, else equip the secondary - — `ga.cs:1528-1547`; else if the primary itself needs to change, - equip it — `ga.cs:1548-1551`; else if only the spell/element + — `ga.cs:1530-1539`; else if the primary itself needs to change, + equip it — `ga.cs:1540-1544`; else if only the spell/element selection needs updating, do that (`bv.a()`) — `ga.cs:1552-1554`. - Any of the above branches returns `false` for this tick — the wield step always costs at least one extra tick before it can report ready. @@ -550,7 +550,7 @@ calls through this **one** subroutine (via an overload chain, cast/use inside their own `if (ga.a(...)) { ... }` branch — see e.g. `c8.cs:114-121`, `af.cs:185-189`, `ba.cs:152-156`. -`ga.a(ObjectClass)` (`ga.cs:1573-1607`) is the class→mode map: +`ga.a(ObjectClass)` (`ga.cs:1581-1613`) is the class→mode map: `MeleeWeapon → Melee(2)`, class `9` (missile launcher) `→ Missile(4)`, class `31` and everything else `→ Magic(8)` (the default). @@ -565,12 +565,10 @@ back to the first `fi.b`-classified item found while scanning `dz.k.j()` verification 2026-09-06): `eq.j()` (`eq.cs:83-94`) walks `m_e` in list order and emits each distinct item id (skipping `-1` and self); `m_e` is appended by `eq.a(c)` (`eq.cs:54-57`, a plain `List.Add`), whose only -producers are the Items-tab Add buttons — `PluginCore.cs:8422-8434` +producers are the Items-tab Add buttons — `PluginCore.cs:8348-8434` (five switch cases) (`dz.k.a(new eq.c(item, spell))`, one row per element for "Add", a single -`-1`-spell row for "Add (no buffs)") — and the profile loader. So the -first wand a player added is the fallback caster. (`ga.cs:1462-1464` is -the wielded-first branch; the line numbers `1471-1475` above refer to the -same function's error path.) +`-1`-spell row for "Add (no buffs)") — and the profile loader (`da.cs:628`). So the +first wand a player added is the fallback caster. ### 3.2 (b) Fighting with the rule weapon @@ -652,9 +650,10 @@ carefully: `fd.cs:103-110`, the `nav minimum distance` referenced in the campaign brief). - If that "stop" condition is true **and** `Actions.CombatMode == Peace` - (`fd.cs:129`): if `IdlePeaceMode` is on, log **"Warning: Idle peace + (`fd.cs:129`): if `IdlePeaceMode` is on (`fd.cs:131`) log **"Warning: Idle peace selected with low waypoint minimum distance. Will switch to magic - mode."** (`fd.cs:133`), then attempt to force Magic mode via + mode."** (`fd.cs:133`) — the warning is the ONLY thing that setting gates — + then, regardless of the setting, attempt to force Magic mode via `dz.o.a((CombatState)8, 0, true)` (the shared gate, §3.1) — and if that fails, cancel the "stop" decision for this tick, i.e. keep walking instead (`fd.cs:135-138`). @@ -687,7 +686,7 @@ Files read: `MossTankPanel.cs` (`OnTick`, `TickAutomaticBuffing`, VTank's `l` is a declarative list of independent `ILogicRule` objects; one pass picks the first whose `ValidNow` is true and marks it `Running`, and every other rule is explicitly told `Running = false` (§1.2). MossTank's -`OnTick` (`MossTankPanel.cs:3732-3936`) is an imperative chain of ~16 +`OnTick` (`MossTankPanel.cs:3732-4022`) is an imperative chain of ~16 named controllers, each called unconditionally every tick with a `canAct` argument that is the logical AND of "not owned by any higher controller so far." The net *suppression* behavior is similar (a higher controller @@ -732,7 +731,7 @@ observably different: retail VTank always buffs allies with the weakest version of Tier-I self-buffs, cycling randomly; MossTank always buffs with the strongest known tier, cycling in a fixed order. The 18 m range constant (VTank's hardcoded `0.075` landblock units) is correctly ported -(`< 18d`, line 4071, with an explicit comment tying it to the conversion). +(`< 18d`, line 4071; no comment ties it to the conversion). ### 5.4 BuffCasterPreparer vs. `ga.a(...)`: same shape, one real difference @@ -769,11 +768,12 @@ VTank re-checks self-vital recharge at two separate priority tiers (`cr` "Recharge-Norm-*" before Attack, "Recharge-NoTarg-*" after target- approach fails, §2.2 rows 4 and 59) — two independent settings, gated purely by *list position* relative to Attack. MossTank's `VitalPlan. -Threshold` (`VitalPlan.cs:79-88`) keeps both `Normal*` and `NoTarget*` -settings but combines them as `Math.Max(Normal, NoTarget)` regardless of -whether a target is present, then only the `noTarget` bool -(`VitalRechargeController.Tick`'s third parameter, -`VitalRecharge.cs:887, 933-939`) selects which pair applies. This +Threshold` (`VitalPlan.cs:79-91`) keeps both `Normal*` and `NoTarget*` +settings and evaluates `noTarget ? Math.Max(Normal, NoTarget) : Normal` — +with a target present the NoTarget setting is ignored entirely, and only +in the no-target branch does the Max apply (the `noTarget` bool is +`VitalRechargeController.Tick`'s third parameter, `VitalRecharge.cs:887, +933-939`). This converges on similar behavior in the common case (both thresholds usually agree in intent — top off more eagerly while idle) but is not a literal port: VTank's two rules can disagree in *which* vitals they check (a @@ -848,9 +848,9 @@ fight. likely covered by a combat-math KB doc. - **`gj`/`gs` (the two Decal state trackers that drive `SchedulePoke`'s fast-wake path)** were read only enough to confirm what they represent - in shape (spell-cast completion and attack/missile completion, - `gj.cs`/`gs.cs` headers); their own internal state machines were not - traced. + in shape (resolved by the citation pass: `gj` = SpellCaster, `gs` = + WandCaster, both cast-completion trackers); their own internal state + machines were not traced. - **Meta FSM** (`dz.at.h()`, `a7.e()`, `cLogic.cs:191-196`) is explicitly out of scope — it is evaluated once per pass alongside the main rule list but is its own subsystem (VTank's expression-language meta layer),