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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-06 20:02:39 +02:00
parent 1b81836747
commit 887242cd15

View file

@ -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),