docs(vt): VT1 catalog 02 scheduler/actions and 03 combat from the VTank decompile
02: the single-winner priority list (24 rule classes, 45 instantiations), the 293 ms heartbeat + event poke, the force-combat-mode gate and its stuck-state recovery, IdlePeace in full, MossTank gap. Two draft errors corrected by the lead against the source: GoToPeaceModeToUseKits exists (a5.cs:121, defaultsettings.usd:931) and the fallback-wand list is Items-page insertion order (eq.cs:83-94, PluginCore.cs:8422-8434). 03: target acquisition/selection, monster rules, weapon/damage/ammo, attack execution, debuffs, pets, MossTank gap. Spot-checked: the hardcoded debuff-kind order (hi.cs:123-168) and quality-before-UseArcs (hi.cs:509-535). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
68abeffb37
commit
b4cb516fac
2 changed files with 1436 additions and 0 deletions
857
docs/research/vtank-kb/02-scheduler-and-actions.md
Normal file
857
docs/research/vtank-kb/02-scheduler-and-actions.md
Normal file
|
|
@ -0,0 +1,857 @@
|
|||
# VTank KB 02 — Scheduler and Actions
|
||||
|
||||
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 and cited so
|
||||
it can be checked against the file directly.
|
||||
|
||||
Comparison target: `src/AcDream.Plugins.MossTank/` at the tree checked out
|
||||
in worktree `eloquent-hugle-42119e` (commit `c406942ef` and prior).
|
||||
|
||||
---
|
||||
|
||||
## 0. Class-name key (obfuscated → role)
|
||||
|
||||
Established once here; used by name for the rest of the doc.
|
||||
|
||||
| Obfuscated | Role | Notes |
|
||||
|---|---|---|
|
||||
| `PluginCore.dz` (type `s`, `s.cs`) | The "god object" — one field per subsystem manager | `s.cs:9-90` lists ~50 fields `a`..`ay` |
|
||||
| `dz.d` | `cLogic` — the scheduler itself | field `d`, `s.cs:14` |
|
||||
| `dz.o` | `ga` — combat-mode/wield/equip manager, action-lock table, log sink | field `o`, `s.cs:34`; `ga.cs` |
|
||||
| `dz.p` | `dz` (yes, class `dz.cs` reused as a field type) — target lock / attack-plan selector | field `p`, `s.cs:40`; `dz.cs` |
|
||||
| `dz.q` | `g6` — WorldFilter/object-classification cache | field `q`, `s.cs:42` |
|
||||
| `dz.k` | `eq` — a tracked-item-id list (populated elsewhere; consumed by wand selection) | field `k`, `s.cs:22` |
|
||||
| `dz.m` | `da` — profile data (buff list, craft list, nav route) | field `m`, `s.cs:24` |
|
||||
| `dz.r` | `fo` — corpse/loot cursor | field `r`, `s.cs:44` |
|
||||
| `dz.s` | `hv` — loot-item picker | field `s`, `s.cs:46` |
|
||||
| `dz.l` | `ai` — helper-buff/recharge-other spell cache | field `l`, `s.cs:26` |
|
||||
| `dz.al` | `cRechargeManager` — self mana/stam/health-stone recharge executor | field `al`, `s.cs:64` |
|
||||
| `dz.ak` | `bf` — fellowship auto-follow manager | field `ak`, `s.cs:66` |
|
||||
| `dz.an` (via `h1`) | `fp` | pet-count / summon helper |
|
||||
| `dz.v` | `f0` — waiting-lock table keyed by object (corpse/door) | field `v`, `s.cs:52` |
|
||||
| `dz.n` | `hr` — key-press (forward/turn) driver | field `n`, `s.cs:30` |
|
||||
|
||||
`o.n` (an `ActionLockType`-keyed timer table inside `ga`) is the shared
|
||||
per-action cooldown/mutex mechanism — almost every rule's `ValidNow` checks
|
||||
`o.n.b(ActionLockType.X)` (locked → not valid) and every rule's `Running`
|
||||
body that fires a request calls `o.n.a(ActionLockType.X, TimeSpan...)` to
|
||||
arm a cooldown. This is VTank's equivalent of MossTank's per-controller
|
||||
`canAct`/cooldown fields.
|
||||
|
||||
---
|
||||
|
||||
## 1. The macro loop
|
||||
|
||||
### 1.1 What drives a tick
|
||||
|
||||
The scheduler `cLogic` (`uTank2/cLogic.cs`) is **not** driven by a fixed
|
||||
per-frame `Tick()` call from the host. It is driven by two independent
|
||||
`System.Windows.Forms.Timer` wrappers (class `ey`, `ey.cs`), whose common
|
||||
tick source is Decal's `RenderPreUI` hook incrementing a static frame
|
||||
counter (`ey.c()`/`ey.b()`, `ey.cs:70-79`); each `ey` instance also
|
||||
de-duplicates so its own handler fires **at most once per distinct render
|
||||
frame** (`ey.cs:172-177`).
|
||||
|
||||
| Timer | Interval | Armed | Handler | Purpose |
|
||||
|---|---|---|---|---|
|
||||
| `f` | 293 ms, re-armed every fire | Only while macro running (`StartMacro`, `cLogic.cs:327-328`) | `b()` → `f.a(293); TryPokeMacro();` (`cLogic.cs:141-152`) | Heartbeat: re-evaluate rules |
|
||||
| `g` | 3203 ms, fixed | Always (constructed once, `cLogic.cs:61-63`) | `a(object,EventArgs)` → if macro **not** running, evaluate the `m` list (`cLogic.cs:175-181`) | Background pass while macro is stopped |
|
||||
|
||||
`TryPokeMacro()` (`cLogic.cs:163-173`) only actually re-runs the rule pass
|
||||
if the macro is running (`dz.o.c`), a start is not already in progress
|
||||
(`!k`), and the frame-counter has advanced since the last run (`j !=
|
||||
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"
|
||||
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
|
||||
next frame rather than waiting up to 293 ms. In effect: state changes
|
||||
(a cast finishes, an attack resolves) wake the scheduler immediately; the
|
||||
293 ms timer is the fallback heartbeat for everything else (vitals
|
||||
ticking down, a target coming into range, a corpse timing out).
|
||||
|
||||
### 1.2 The rule pass itself
|
||||
|
||||
`a(MyList<ILogicRule> A_0, MyList<ILogicRule> A_1)` (`cLogic.cs:183-279`)
|
||||
is the actual pass, called as `a(l, n)` from `TryPokeMacro` and as
|
||||
`a(m, null)` from the 3203 ms background timer. Per pass:
|
||||
|
||||
1. Clear two per-tick flags (`dz.o.s`, `dz.o.t` — a "did-nav-already-decide-
|
||||
not-to-move" flag and the corpse-wait latch) — `cLogic.cs:289-293`.
|
||||
2. If Meta is enabled and the macro isn't stopped (`!dz.o.f()`), evaluate
|
||||
the Meta FSM (`dz.at.h()`, `a7.e()`) — `cLogic.cs:188-196`. (Meta is a
|
||||
separate subsystem from the action list below; not covered by this
|
||||
doc.)
|
||||
3. If `A_1` (the **independent** list, normally `n` = `{ h1 }`, see §2.1)
|
||||
is non-null, set `Running = ValidNow` on **every** item in it,
|
||||
unconditionally — `cLogic.cs:198-213`. These do **not** compete for the
|
||||
single winner slot below; they run in parallel with whatever the main
|
||||
loop picks.
|
||||
4. If the macro is stopped, return (the log line, the priority-list scan,
|
||||
and the "clear rules"/"activate rule" steps below are all skipped) —
|
||||
`cLogic.cs:214-217`.
|
||||
5. Walk `A_0` (the main list `l`) top to bottom in **insertion order**
|
||||
(the `int Priority` on each rule is a monotonically increasing debug id,
|
||||
never sorted on) and take the **first** rule whose `ValidNow` is true —
|
||||
`cLogic.cs:218-239`. This is a strict "exactly one winner" scan: no
|
||||
further items are even evaluated once one is picked (`goto IL_0323`).
|
||||
6. Every rule in `A_0` that is not the winner gets `Running = false`; the
|
||||
winner gets `Running = true` — `cLogic.cs:243-257`. Setting `Running`
|
||||
is how a rule actually *does* its work (see §2's `Running` column);
|
||||
`ValidNow` is pure precondition-checking with no side effects other than
|
||||
caching the choice it made for `Running` to re-use (most classes stash
|
||||
the choice — a target id, an item id, a spell — in a private field
|
||||
during `ValidNow` and consume it in `Running`).
|
||||
7. Every 30 s (wall clock, guarded by a `DateTimeOffset` stamp), force a
|
||||
GC — `cLogic.cs:268-277`. Not scheduler logic, but explains an
|
||||
occasional frame hitch on a live VTank session.
|
||||
|
||||
### 1.3 Run macro / Stop macro / death
|
||||
|
||||
`StartMacro()` (`cLogic.cs:295-338`):
|
||||
- Refuses if the extension failed to initialize (`PluginCore.ds` false),
|
||||
or the character isn't fully logged in (`PluginCore.dn == 0` or
|
||||
`!dz.a0.dt`) — posts "Please wait until you are fully logged in." and
|
||||
(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
|
||||
`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
|
||||
does one immediate `TryPokeMacro()`.
|
||||
|
||||
`StopMacro()` (`cLogic.cs:340-365`), wrapped in a bare `try/catch` that
|
||||
**silently swallows any exception** — a stop can partially fail with no
|
||||
visible error:
|
||||
- Sets every rule in `l` to `Running = false`, clears `dz.o.c`, stops the
|
||||
293 ms timer, unchecks the UI "Run Macro" box, and if a spread-lock
|
||||
target list was active, clears it and re-sends an empty target list to
|
||||
the host (`PluginCore.PC.a(new MyList<int>(100))`).
|
||||
- Does **not** reset the equip-retry counter or Meta state itself (those
|
||||
are handled by `StartMacro` on the next run) and does **not** stop or
|
||||
reset navigation, crafting, dispel, or loot — the rule instances are
|
||||
simply never evaluated again until `StartMacro` re-enables `dz.o.c`.
|
||||
|
||||
Death: `StopMacroOnDeath` is read elsewhere in `PluginCore.cs` (not in
|
||||
`cLogic`) — `PluginCore.cs:4155` gates a `StopMacro()` call on
|
||||
`dz.o.c && f3.k("StopMacroOnDeath")`. This doc's oracle is the scheduler;
|
||||
the death-detection trigger itself lives in the main plugin class, not
|
||||
`cLogic`.
|
||||
|
||||
### 1.4 Pause conditions
|
||||
|
||||
There is no single "pause" flag distinct from "stopped." The closest
|
||||
equivalent is the per-`ActionLockType` cooldown table (`dz.o.n`), which
|
||||
individual rules use to make themselves temporarily invalid (busy-state,
|
||||
navigation lock, item-use lock, door-opening lock, salvage lock, spread-
|
||||
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
|
||||
(BuffSelf, DispelSelf, DispelAllies, RechargeOther, UseHealersHeart,
|
||||
RandomHelper, Attack's own wield step) for that tick without any explicit
|
||||
"paused" bookkeeping.
|
||||
|
||||
---
|
||||
|
||||
## 2. The complete action list
|
||||
|
||||
### 2.1 Two tracks
|
||||
|
||||
VTank schedules rules on two separate tracks:
|
||||
|
||||
- **Main track (`l`)** — the priority list below; exactly one rule's
|
||||
`Running` fires per tick (§1.2 step 5-6).
|
||||
- **Independent track (`n`)** — evaluated every tick *in addition to*
|
||||
whichever main-track rule wins, with no competition at all
|
||||
(`cLogic.cs:198-213`). By default `n` holds exactly one rule:
|
||||
**SummonPet** (`h1`, `h1.cs`) — valid when combat and pet-summoning are
|
||||
both enabled, the Summoning skill is trained, the class is not on its
|
||||
own internal 32555ms cooldown gate (`an.a(-32555)`, `h1.cs:44-47`), a
|
||||
summonable pet item is present (`dz.o.j()`), and the pet-count check
|
||||
`bm.a()` passes; `Running(true)` uses the found item
|
||||
(`f9.p(this.m_a)`). SummonPet requires **no target at all** — it can
|
||||
fire purely because combat is enabled, independent of whatever the main
|
||||
track is doing that tick.
|
||||
- There is also a **macro-disabled Always Rules list (`m`)**, evaluated
|
||||
only by the slow 3203 ms timer while the macro is **stopped**
|
||||
(`cLogic.cs:175-181`, §1.1). By default it holds one gated rule:
|
||||
`RefillWieldedMana` (`a0`, §2.3) behind the `ManaChargesWhenOff` setting
|
||||
— VTank keeps a worn mana stone charging on its own cadence even with
|
||||
the macro off.
|
||||
|
||||
### 2.2 Main-track priority order
|
||||
|
||||
All 39 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
|
||||
uses and are reproduced here for that reason. "Gate" is the
|
||||
`ISettingDelegate[]` requirement wrapper (`LogicRulePreChain`) that must
|
||||
*all* be true before the wrapped rule's own `ValidNow` is even consulted
|
||||
(`LogicRulePreChain.cs:18-32`); "Fallback" is a `cm(0)` (IdlePeace) or
|
||||
similar rule wired as the `LogicRulePreChain`'s own sub-action, which
|
||||
`Running(true)` on the wrapper will start automatically if the wrapped
|
||||
rule/its own reqs fail but the fallback's reqs pass
|
||||
(`LogicRulePreChain.cs:34-75` — a wrapper's `Running=true` setter tries
|
||||
each fallback in order first, then the primary).
|
||||
|
||||
| # | Rule (log name) | Class | Gate | Fallback | Cite |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | Sentinel START | — | — | — | `cLogic.cs:459` |
|
||||
| 2 | SplitPeas (SpellCompMin-Critical) | `as` | — | — | `cLogic.cs:460` |
|
||||
| 3 | CraftFood (kit/food counts, non-idle) | `a9` | — | — | `cLogic.cs:461-469` |
|
||||
| 4 | RechargeSelf2 (Recharge-Norm-*) | `cr` | — | — | `cLogic.cs:470` |
|
||||
| 5 | RefillWieldedMana | `a0` | — | — | `cLogic.cs:471` |
|
||||
| 6 | BuffSelf (RebuffTimeRemainingSeconds) | `fz` | — | — | `cLogic.cs:472` |
|
||||
| 7 | SplitPeas (SpellCompMin-Normal) | `as` | — | — | `cLogic.cs:473` |
|
||||
| 8 | Sentinel POSTBUFF | — | — | — | `cLogic.cs:474` |
|
||||
| 9 | DispelSelf | `c8` | — | — | `cLogic.cs:475` |
|
||||
| 10 | UseDispelItem | `cx` | — | — | `cLogic.cs:476` |
|
||||
| 11 | UseHealersHeart | `fb` | — | — | `cLogic.cs:477` |
|
||||
| 12 | RechargeOther | `gu` | — | — | `cLogic.cs:478` |
|
||||
| 13 | DispelAllies | `af` | — | — | `cLogic.cs:479` |
|
||||
| 14 | CraftFood (all recipes/spells, count=1) | `a9` | — | — | `cLogic.cs:480` |
|
||||
| 15 | RefillPetCharges (PetRefillCount-Normal) | `dq` | — | — | `cLogic.cs:481` |
|
||||
| 16 | Sentinel POSTHELPER | — | — | — | `cLogic.cs:482` |
|
||||
| 17 | FellowshipManager | `g5` | — | — | `cLogic.cs:483` |
|
||||
| 18 | Sentinel POSTAUTOFELLOW | — | — | — | `cLogic.cs:484` |
|
||||
| 19 | OpenDoor | `b7` | — | — | `cLogic.cs:485` |
|
||||
| 20 | Sentinel PREPRIORITYLOOTACTIONS | — | — | — | `cLogic.cs:486` |
|
||||
| 21 | ReadScroll (priority) | `er` | EnableLooting, LootPriorityBoost | — | `cLogic.cs:487` |
|
||||
| 22 | StackCram (priority) | `aj` | same | — | `cLogic.cs:488` |
|
||||
| 23 | SalvageItems (priority) | `ar` | same | — | `cLogic.cs:489` |
|
||||
| 24 | Sentinel POSTPRIORITYLOOTACTIONS | — | — | — | `cLogic.cs:490` |
|
||||
| 25 | Sentinel PREPRIORITYLOOT | — | — | — | `cLogic.cs:491` |
|
||||
| 26 | Navigate → corpse (priority) | `g8`+`fg` | EnableLooting, LootPriorityBoost, SetWaitingOnCorpseId | — | `cLogic.cs:492-497` |
|
||||
| 27 | OpenCorpse (priority) | `bj` | LootPriorityBoost, SetWaitingOnCorpseId | — | `cLogic.cs:498-502` |
|
||||
| 28 | LootCorpse (priority) | `d0` | EnableLooting, LootPriorityBoost | — | `cLogic.cs:503` |
|
||||
| 29 | CorpseWait (priority) | `a1` | same | — | `cLogic.cs:504` |
|
||||
| 30 | Sentinel POSTPRIORITYLOOT | — | — | — | `cLogic.cs:505` |
|
||||
| 31 | Sentinel PREPRIORITYNAV | — | — | — | `cLogic.cs:506` |
|
||||
| 32 | Navigate → nav route (priority) | `g8`+`ca` | NavPriorityBoost, SetWaitingOnCorpseId | — | `cLogic.cs:507-512` |
|
||||
| 33 | Sentinel POSTPRIORITYNAV | — | — | — | `cLogic.cs:513` |
|
||||
| 34 | Sentinel PREATTACK | — | — | — | `cLogic.cs:514` |
|
||||
| 35 | **Attack** | `b4` | — | — | `cLogic.cs:515` |
|
||||
| 36 | Sentinel POSTATTACK | — | — | — | `cLogic.cs:516` |
|
||||
| 37 | Sentinel PREIDLESTATUS | — | — | — | `cLogic.cs:517` |
|
||||
| 38 | SplitPeas (SpellCompMin-Idle) | `as` | — | — | `cLogic.cs:518` |
|
||||
| 39 | CraftFood (idle counts) | `a9` | — | — | `cLogic.cs:519-527` |
|
||||
| 40 | RefillPetCharges (PetRefillCount-Idle) | `dq` | — | — | `cLogic.cs:528` |
|
||||
| 41 | Sentinel PREIDLELOOTACTIONS | — | — | — | `cLogic.cs:529` |
|
||||
| 42 | ReadScroll (idle) | `er` | (none) | **IdlePeace** | `cLogic.cs:530` |
|
||||
| 43 | StackCram (idle) | `aj` | (none) | **IdlePeace** | `cLogic.cs:531` |
|
||||
| 44 | SalvageItems (idle) | `ar` | (none) | **IdlePeace** | `cLogic.cs:532` |
|
||||
| 45 | Sentinel POSTIDLELOOTACTIONS | — | — | — | `cLogic.cs:533` |
|
||||
| 46 | Sentinel PREIDLELOOT | — | — | — | `cLogic.cs:534` |
|
||||
| 47 | Navigate → corpse (idle) | `g8`+`fg` | EnableLooting, SetWaitingOnCorpseId | **IdlePeace** (gated: not yet close) | `cLogic.cs:535-543` |
|
||||
| 48 | OpenCorpse (idle) | `bj` | EnableLooting, SetWaitingOnCorpseId | **IdlePeace** | `cLogic.cs:544-548` |
|
||||
| 49 | LootCorpse (idle) | `d0` | (none — own `EnableLooting` check) | — | `cLogic.cs:549` |
|
||||
| 50 | CorpseWait (idle) | `a1` | (none) | — | `cLogic.cs:550` |
|
||||
| 51 | Sentinel POSTIDLELOOT | — | — | — | `cLogic.cs:551` |
|
||||
| 52 | Sentinel PREIDLEBUFF | — | — | — | `cLogic.cs:552` |
|
||||
| 53 | BuffSelf (IdleBuffTopoffTimeSeconds) | `fz` | IdleBuffTopoff | — | `cLogic.cs:553-556` |
|
||||
| 54 | Sentinel POSTIDLEBUFF | — | — | — | `cLogic.cs:557` |
|
||||
| 55 | Sentinel PRETARGETAPPROACH | — | — | — | `cLogic.cs:558` |
|
||||
| 56 | Navigate → nearest monster | `g8`+`eb` | EnableCombat | **IdlePeace** (gated: not yet close) | `cLogic.cs:559-563` |
|
||||
| 57 | Sentinel POSTTARGETAPPROACH | — | — | — | `cLogic.cs:564` |
|
||||
| 58 | Sentinel PREIDLERECHARGE | — | — | — | `cLogic.cs:565` |
|
||||
| 59 | RechargeSelf2 (Recharge-NoTarg-*) | `cr` | — | — | `cLogic.cs:566` |
|
||||
| 60 | Sentinel POSTIDLERECHARGE | — | — | — | `cLogic.cs:567` |
|
||||
| 61 | Sentinel PRENAVROUTE | — | — | — | `cLogic.cs:568` |
|
||||
| 62 | Navigate → nav route (idle) | `g8`+`ca` | (none) | **IdlePeace** (gated: not close+not recalling) | `cLogic.cs:569-573` |
|
||||
| 63 | Sentinel POSTNAVROUTE | — | — | — | `cLogic.cs:574` |
|
||||
| 64 | RandomHelper | `ba` | — | — | `cLogic.cs:575` |
|
||||
| 65 | Sentinel END | — | — | — | `cLogic.cs:576` |
|
||||
| 66 | **IdlePeace** (terminal catch-all) | `cm` | — | — | `cLogic.cs:577` |
|
||||
|
||||
`IdlePeace` (`cm`) is instantiated **8 times total** — the terminal
|
||||
catch-all plus 7 fallback children wired directly into other rules'
|
||||
`LogicRulePreChain` — see §4.
|
||||
|
||||
### 2.3 Per-action detail
|
||||
|
||||
Each entry: precondition (`ValidNow`), what `Running(true)` does, and
|
||||
timeouts/retries/suppressors. "Suppressor" = an `ActionLockType` (or
|
||||
equivalent) whose lock, once armed, makes `ValidNow` false until it
|
||||
expires.
|
||||
|
||||
**SplitPeas — `as` (`as.cs`)**
|
||||
Precondition: not `ItemUse`-locked, in Peace mode already
|
||||
(`dz.z.d() == o.b.a`, `as.cs:72`), `SplitPeas` setting on, and a
|
||||
craftable split found under the given component-minimum setting
|
||||
(`SpellCompMin-Critical`/`-Normal`/`-Idle`) either for the special
|
||||
`[All Peas]` group or a specific reagent — `as.cs:66-131`. `Running(true)`:
|
||||
if not already in Peace mode, request Peace (`f9.a((CombatState)1)`);
|
||||
else perform the split — `as.cs:153-168`. No explicit timeout; re-attempts
|
||||
every tick it's the winner. Warns once per scan if no splitting tool is
|
||||
present (`as.cs:94, :120`).
|
||||
|
||||
**CraftFood — `a9` (`a9.cs`)**
|
||||
Precondition: not `ItemUse`-locked, in Peace mode, `AutoCraftItems` on,
|
||||
and a recipe/spell in the profile craft list (`dz.m.h`/`dz.m.g`) resolves
|
||||
to an executable craft — `a9.cs:101-140`. The idle-tier instance passes an
|
||||
explicit `fs[6]` type filter and `A_2:true`, which switches the per-type
|
||||
count from a flat `1` to the matching `IdleCraftCount_*` setting
|
||||
(kits/food × health/stamina/mana) — `a9.cs:82-98`. `Running(true)`: same
|
||||
Peace-mode-first gate as SplitPeas, then execute the craft —
|
||||
`a9.cs:160-176`.
|
||||
|
||||
**RechargeSelf2 — `cr` (`cr.cs`)**
|
||||
Precondition: not `ItemUse`-locked; `cg.c/b/a` (external helper, not this
|
||||
file) report a vital below its Norm-tier or NoTarg-tier threshold setting
|
||||
— `cr.cs:72-92`. On an exception in that check, sets an internal
|
||||
error-state flag and still reports valid (`cr.cs:86-90`). `Running(true)`:
|
||||
logs `(RechargeSelf2) Running (errorstate ...)`, then calls
|
||||
`dz.al.Recharge(...)` for whichever vital tripped — `cr.cs:112-135`. **No
|
||||
target requirement of its own** — the Norm/NoTarg split is entirely a
|
||||
function of *where in the list* it's placed (before vs. after the Attack
|
||||
rule), not a parameter difference beyond which three setting keys it
|
||||
reads.
|
||||
|
||||
**RefillWieldedMana — `a0` (`a0.cs`)**
|
||||
Precondition: not `ItemUse`-locked; delegates to `dz.ac` (an item-mana
|
||||
tracker) — `a0.cs:63-77`. `Running(true)`: recharges the wielded item if
|
||||
`dz.ac.h()`, else (if not `ManaStoneUse`-locked) uses a mana stone via
|
||||
`dz.ab.e()` — `a0.cs:97-111`.
|
||||
|
||||
**BuffSelf — `fz` (`fz.cs`)**
|
||||
Precondition: not `ItemUse`-locked, `EnableBuffing` on, and
|
||||
`dz.k.a(seconds, isIdleTopoff, out shouldForce)` (the buff-plan builder)
|
||||
reports work — `fz.cs:69-90`. The `RebuffTimeRemainingSeconds` normal-tier
|
||||
call passes `A_2:true`("idle" flag inverted per-arg — see cite) while the
|
||||
idle-tier call (`IdleBuffTopoffTimeSeconds`) passes `A_2:false`; both add
|
||||
`BuffCastRecast_Seconds` on top of the raw threshold if the
|
||||
`BuffCastRecast` lock is armed — `fz.cs:80-89`. `Running(true)`: routes
|
||||
through `ga.a((CombatState)8, 0, true)` (the shared wield/mode gate, §3)
|
||||
and, once ready, arms `BuffCastRecast` and asks the buff-plan builder to
|
||||
actually cast (`dz.k.a()`) — `fz.cs:113-121`.
|
||||
|
||||
**DispelSelf — `c8` (`c8.cs`)**
|
||||
Precondition: not `ItemUse`-locked, `CastDispelSelf` on, "Eradicate Life
|
||||
Magic Self" known and its difficulty passes `c4.a(...)` — `c8.cs:64-85`.
|
||||
`Running(true)`: through the shared wield/mode gate, cast the spell on
|
||||
self — `c8.cs:113-121`.
|
||||
|
||||
**UseDispelItem — `cx` (`cx.cs`)**
|
||||
Precondition: `UseDispelItems` on, not `ItemUse`-locked, and — gated by
|
||||
two difficulty tiers (`c4.a(400)`, `c4.a(350)`) — one of a fixed item list
|
||||
is in inventory: Rune of Dispel, Society/Black Market Gem of Dispelling
|
||||
(tier 400), or Rune of Dispel, Chocolate Gromnie, Condensed Dispel
|
||||
Potion, Gem of Stillness (tier 350) — `cx.cs:65-116`. `Running(true)`:
|
||||
uses the found item and arms `ItemUse` for `dz.o.k` (a configured
|
||||
duration) — `cx.cs:139-145`.
|
||||
|
||||
**UseHealersHeart — `fb` (`fb.cs`)**
|
||||
Precondition: `UseHealersHeart` on, not `ItemUse`-locked, "The Healer's
|
||||
Heart" or "Legendary Seed of Mornings" present and off cooldown
|
||||
(`dz.aa.a`), Healing/Arcane-Lore-equivalent skills above fixed thresholds
|
||||
(245/105), and a helper-buff spell resolves — `fb.cs:69-115`.
|
||||
`Running(true)`: through the wield/mode gate (target = the item's owner,
|
||||
not self — `A_2:false`), use the item then apply the resolved buff —
|
||||
`fb.cs:140-146`.
|
||||
|
||||
**RechargeOther — `gu` (`gu.cs`)**
|
||||
Precondition: not `ItemUse`-locked; a helper-recharge request (`gh`, built
|
||||
from three `Recharge-Helper-*` settings clamped to 100) resolves via
|
||||
`dz.l.b(...)` — `gu.cs:71-82`. `Running(true)`: through the wield/mode
|
||||
gate, execute via `dz.l.a(...)` — `gu.cs:109-129`.
|
||||
|
||||
**DispelAllies — `af` (`af.cs`)**
|
||||
Precondition: `UseDispelDrum` on, not `ItemUse`-locked, an Awakener or
|
||||
Attenuated Awakener drum present and off cooldown with matching
|
||||
skill/training thresholds, and a fellow member is found carrying
|
||||
qualifying debuffs of quality > 250 and difficulty ≤ 350 within a 5-hour
|
||||
window — `af.cs:75-158`. `Running(true)`: through the wield/mode gate
|
||||
(target = the drum's owner), use the drum on the chosen ally —
|
||||
`af.cs:184-189`.
|
||||
|
||||
**CraftFood (default) — `a9` again**, same class as above, constructed
|
||||
with no type filter (`m_c == null`), which additionally lets it craft
|
||||
*any* known helper-buff spell in the profile if no recipe matched
|
||||
(`a9.cs:127-138`) — this instance runs *after* DispelAllies, before
|
||||
RefillPetCharges (Normal).
|
||||
|
||||
**RefillPetCharges — `dq` (`dq.cs`)**
|
||||
Precondition: not `ItemUse`-locked, `EnableCombat` on, a wielded-pet-
|
||||
device candidate under the given `PetRefillCount-*` threshold found via
|
||||
`ga.e(...)` classification, and "Encapsulated Spirit" present —
|
||||
`dq.cs:38-75`. `Running(true)`: if not in Peace mode, request Peace
|
||||
first; else select the device, use the spirit item on it, restore
|
||||
selection — `dq.cs:99-113`.
|
||||
|
||||
**FellowshipManager — `g5` (`g5.cs`)**
|
||||
Precondition/Running: entirely delegated — `ValidNow` is `dz.ak.h()`,
|
||||
`Running(true)` calls `dz.ak.g()` — `g5.cs:28-58`. `dz.ak` (`bf.cs`) is
|
||||
out of scope for this doc (fellowship auto-follow subsystem).
|
||||
|
||||
**OpenDoor — `b7` (`b7.cs`)**
|
||||
Precondition: `OpenDoors` and `EnableNav` on; scans all doors, adds
|
||||
in-range ones to a navigation wait-set (`dz.v`), defers if navigation is
|
||||
already pending; then, among doors within `DoorOpenRange`, requires (for
|
||||
a locked door) `Lockpick` known and skill above the difficulty +
|
||||
`DoorLockpickDiffExcessThreshold`, and a usable lockpick found via a
|
||||
by-`ObjectClass.Door`-filtered inventory scan (`a()`, `b7.cs:52-81`) —
|
||||
`b7.cs:83-172`. `Running(true)`: for a locked door, use the lockpick on it
|
||||
(`ItemUse`/`Navigation`/`DoorOpening` locks armed for 0.5s/5s/5s); for an
|
||||
unlocked door, walk up (`f9.p`) then, once adjacent, `ApplyItem` (the
|
||||
door itself) — `b7.cs:206-226`.
|
||||
|
||||
**ReadScroll / StackCram / SalvageItems — `er`, `aj`, `ar`**
|
||||
`ReadScroll` (`er.cs:63-90`): not `ItemUse`-locked; scans a cached
|
||||
scroll-item dictionary (`dz.o.i`), drops entries no longer in the
|
||||
worldfilter classification set (`dz.q.y`), and picks the first remaining
|
||||
one the loot-item picker (`dz.s`) accepts. `Running(true)` re-uses the
|
||||
scroll (`f9.p`) and arms `ItemUse` for `dz.o.k` — `er.cs:118-137`.
|
||||
`StackCram` (`aj.cs`): delegates entirely to `dz.x` (`el.cs`, out of
|
||||
scope) — `ValidNow = dz.x.c()`, `Running(true) → dz.x.d()`. `SalvageItems`
|
||||
(`ar.cs`): `EnableLooting` on; if `Salvage` lock already armed, valid
|
||||
immediately (continuing a multi-tick salvage); else requires a "Ust"
|
||||
(salvage tool) present and `dz.u.j()` (has salvageable items) —
|
||||
`ar.cs:63-81`; `Running(true)` re-uses the Ust and calls `dz.u.i()` —
|
||||
`ar.cs:105-111`.
|
||||
|
||||
**Navigate (generic) — `g8` (`g8.cs`) + a goal provider (`bz` interface)**
|
||||
`g8` is a thin wrapper around `fd` (`fd.cs`), parameterized with two
|
||||
setting-key names (a close-stop and a far-stop range) and a goal object.
|
||||
Precondition: not already decided-no-move this tick (`dz.o.s`), `EnableNav`
|
||||
on, not `Navigation`/`SpreadLockTargetRequested`/`DoorOpening`-locked, and
|
||||
the wrapped `fd.e()` (goal non-null and not "reached", `fd.cs:274-281`)
|
||||
— `g8.cs:80-104`. `Running`: delegates start/stop to `fd.c(bool)`
|
||||
(`g8.cs:124-138`); the real per-tick walking logic (face-heading, forward-
|
||||
press via key-driver `dz.n`, autorun toggle, a periodic ~0.7s repress) is
|
||||
in `fd.a()` (`fd.cs:311-386`) — described fully in §4 because it is also
|
||||
where the IdlePeace / nav-min-distance interaction lives. Five goal
|
||||
providers are wired in: `fg` (`CorpseApproach`, corpse-priority and
|
||||
corpse-idle tiers), `eb` (`MonsterApproach`, target-approach tier), and
|
||||
`ca` (nav-route follow, nav-priority and nav-route tiers — its own
|
||||
`FriendlyName` is literally the string `"?????"`,
|
||||
`ca.cs:415-417` — a cosmetic bug shipped in retail VTank, not a decompiler
|
||||
artifact).
|
||||
|
||||
**LootCorpse / CorpseWait / OpenCorpse — `d0`, `a1`, `bj`**
|
||||
`OpenCorpse` (`bj.cs`): `EnableLooting` on; valid immediately if
|
||||
`ItemUse`-locked (so the pass "continues" while an open is in flight);
|
||||
else requires the corpse cursor (`dz.r`) to accept the given max-distance
|
||||
and not already be a "waiting" corpse (`dz.r.e()`) — `bj.cs:63-79`.
|
||||
`Running(true)`: opens the corpse and arms `ItemUse`/`Navigation`/
|
||||
`CorpseOpenAttempt` for `CorpseOpenTimeoutSeconds` — `bj.cs:104-113`.
|
||||
`LootCorpse` (`d0.cs`): `EnableLooting` on, corpse cursor's item index at
|
||||
its item count (`m == j` — fully enumerated), corpse marked "waiting"
|
||||
(`dz.r.e()`), and the loot-item picker (`dz.s`) has a decision
|
||||
(`dz.s.e()`) — `d0.cs:63-79`. `Running(true)`: executes the loot decision,
|
||||
arms `ItemUse`/`Navigation` for 0.75s — `d0.cs:105-111`. `CorpseWait`
|
||||
(`a1.cs`): `EnableLooting` on, not `ItemUse`-locked, corpse cursor
|
||||
"waiting" (`dz.r.e()`) — `a1.cs:63-71`; `Running(true)`: if the loot
|
||||
picker's own wait-check (`dz.s.f()`) also agrees, closes the corpse
|
||||
(`dz.r.b()`) — `a1.cs:95-101`.
|
||||
|
||||
**Attack — `b4` (`b4.cs`)**
|
||||
Precondition: not `ItemUse`-locked, `EnableCombat` on, and the target-lock
|
||||
class `dz.p.c()` finds an in-range target and picks a spell/attack for it
|
||||
(§3 describes `dz.p.c()`/`dz.p.a()` at a summary level — the target-
|
||||
selection and damage-formula internals are out of scope for this
|
||||
scheduler doc) — `b4.cs:63-76`. `Running(true)`: if neither casting
|
||||
(`dz.h.e()`) nor busy on an item (`dz.aa.e()`), execute the chosen attack
|
||||
via `dz.p.a()` — `b4.cs:99-105`; on `Running(false)` (losing the winner
|
||||
slot, e.g. to a higher-priority rule), it explicitly tears down door/
|
||||
salvage cursors (`dz.ar.d()`, `dz.w.c()`) and clears any spread-lock
|
||||
target list — `b4.cs:108-114`.
|
||||
|
||||
**RandomHelper — `ba` (`ba.cs`)**
|
||||
Precondition: `RandomHelperBuffs` on, not `ItemUse`-locked, not
|
||||
`RandomHelperBuffLock`-locked, not currently casting (`dz.h.e()`), a
|
||||
fellow player within **0.075 landblock units (≈18 m)**, and — trying up
|
||||
to 100 random `(target, spell)` draws from a fixed 11-spell Tier-I list
|
||||
(Endurance/Regeneration/Rejuvenation/Armor/five elemental protections/
|
||||
Acid Protection, all literally "... Other I") — an uncast combination
|
||||
found — `ba.cs:25-40, 82-125`. `Running(true)`: through the wield/mode
|
||||
gate, cast the chosen spell on the chosen target, arm
|
||||
`RandomHelperBuffLock` for `RandomHelperIntervalSeconds` — `ba.cs:146-157`.
|
||||
Both target and spell are chosen with `System.Random`, not a
|
||||
deterministic scan — VTank's RandomHelper is genuinely random by design.
|
||||
|
||||
**SummonPet — `h1`** — see §2.1 (independent track).
|
||||
|
||||
**Dead code — `be` (`be.cs`)**: an unreferenced class named
|
||||
`"RechargeSelf"` (not `"RechargeSelf2"`), same constructor shape and
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 3. Combat-mode and equipment sequencing
|
||||
|
||||
### 3.1 The shared gate: `ga.a(CombatState, int, bool, eDamageElement,
|
||||
ePrismaticDamageBehavior, int)` (`ga.cs:1421-1568`)
|
||||
|
||||
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:
|
||||
|
||||
1. **Busy check.** If `Actions.BusyState != 0`, return `false`
|
||||
immediately — `ga.cs:1454-1458`.
|
||||
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`.
|
||||
- **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`.
|
||||
- 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`.
|
||||
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`.
|
||||
4. **If anything needs to change** (wrong item wielded, wrong secondary
|
||||
held, or a spell re-selection is due):
|
||||
- Refuse if a shared busy-lock (`dz.w.c()`) fails — `ga.cs:1502-1505`.
|
||||
- **If not already in Peace mode**, this is where the stuck-state
|
||||
recovery lives: increment a retry counter `r`; once `r` reaches
|
||||
`DropToPeaceModeRetryCount`, lock `ItemUse`, re-resolve *any* wand
|
||||
from the profile, force-equip it, log **"Warning: Macro detected
|
||||
bugged combat state. Attempting to wield an item to clear it."**,
|
||||
reset `r`, and return `false` — `ga.cs:1506-1523`. Otherwise, just
|
||||
request Peace mode (`f9.a((CombatState)1)`) and return `false`,
|
||||
incrementing nothing further this tick — `ga.cs:1524-1526`.
|
||||
- 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
|
||||
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.
|
||||
5. **Once everything matches**, recompute the `CombatState` implied by
|
||||
whatever is now wielded and, if it differs from `Actions.CombatMode`,
|
||||
request that mode (again gated by the same busy-lock) and return
|
||||
`false` — `ga.cs:1556-1564`.
|
||||
6. **Only when wield + secondary + spell + mode all already match** does
|
||||
the gate return `true` — `ga.cs:1565`. Callers only execute the actual
|
||||
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:
|
||||
`MeleeWeapon → Melee(2)`, class `9` (missile launcher) `→ Missile(4)`,
|
||||
class `31` and everything else `→ Magic(8)` (the default).
|
||||
|
||||
### 3.2 (a) Buffing with a wand — which wand?
|
||||
|
||||
**Wielded first.** If auto-select is on (or no explicit item was
|
||||
requested) and the currently-wielded item is already the right *class*
|
||||
for Magic mode, VTank keeps using it rather than re-equipping every tick
|
||||
(`ga.cs:1471-1475`). Only when nothing suitable is wielded does it fall
|
||||
back to the first `fi.b`-classified item found while scanning `dz.k.j()`
|
||||
(`ga.cs:1398-1411`). **`dz.k.j()` is Items-page insertion order** (lead
|
||||
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`
|
||||
(`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.)
|
||||
|
||||
### 3.2 (b) Fighting with the rule weapon
|
||||
|
||||
Handled separately from the buff/heal gate above: `Attack` (`b4`)
|
||||
delegates target selection **and** weapon/mode resolution together to
|
||||
`dz.p` (`dz.cs`) — `dz.p.c()` (`ValidNow`, picks a target + attack plan
|
||||
within `AttackDistance`, `dz.cs:664-667`) and `dz.p.a()` (`Running`,
|
||||
executes the chosen plan, `dz.cs:201-206`). The internal weapon-choice/
|
||||
damage-formula logic inside `dz.cs`'s target-sort and `hi` attack-plan
|
||||
classes is out of scope for this scheduler doc.
|
||||
|
||||
### 3.2 (c) Kits/food — `GoToPeaceModeToUseKits`
|
||||
|
||||
`GoToPeaceModeToUseKits` **is a real VTank setting** (lead verification
|
||||
2026-09-06; an earlier draft of this section wrongly reported it absent):
|
||||
`a5.cs:121` reads `f3.k("GoToPeaceModeToUseKits")` and, when it is on and
|
||||
the character is not in Peace, the kit-use rule (`a5`) requests Peace at
|
||||
`a5.cs:123` and does nothing else that tick; the setting is declared in
|
||||
`refs/vtank/uTank2.Resources.defaultsettings.usd:931` (and repeated at
|
||||
`:1913`). It is the same class-local "Peace first, then act" pattern that
|
||||
CraftFood inlines at `a9.cs:168-171` and SplitPeas at `as.cs:160-164`,
|
||||
not a shared subroutine — eight such open-coded drop-to-peace sites exist
|
||||
(`cm.cs:101`, `a5.cs:123`, `a9.cs:170`, `as.cs:162`, `dq.cs:106`,
|
||||
`bv.cs:196`, `dz.cs:514`, `ga.cs:1526`); the only call into the client's
|
||||
`SetCombatMode` is `f9.cs:378`. Full trace:
|
||||
`refs/vtank/notes/2026-09-06-idlepeace-fcm-trace.md` (local).
|
||||
|
||||
### 3.2 (d) IdlePeace
|
||||
|
||||
See §4 in full.
|
||||
|
||||
### 3.3 Stuck-combat-state recovery
|
||||
|
||||
One mechanism, `ga.cs:1506-1523` (§3.1 step 4), reused by every rule that
|
||||
routes through `ga.a(...)`: after `DropToPeaceModeRetryCount` consecutive
|
||||
ticks stuck outside Peace mode while trying to wield something, VTank
|
||||
stops trying the *specific* requested item and instead force-equips
|
||||
*any* valid wand from the profile, with the exact chat line **"Warning:
|
||||
Macro detected bugged combat state. Attempting to wield an item to clear
|
||||
it."** This is the only explicit "timeout" in the wield/mode path — there
|
||||
is no separate wall-clock timeout, only a per-`ga.a(...)`-call tick
|
||||
counter (`r`, reset to 0 the moment Peace mode is reached).
|
||||
|
||||
---
|
||||
|
||||
## 4. IdlePeace (`cm`, `cm.cs`) in full
|
||||
|
||||
**Precondition** (`cm.cs:62-75`): `IdlePeaceMode` setting is on, **and**
|
||||
`Actions.CombatMode != Peace`. That's the entire `ValidNow` — no busy
|
||||
check, no cast check, no target check of its own.
|
||||
|
||||
**Running(true)** (`cm.cs:96-103`): logs `(IdlePeace) Running` and calls
|
||||
`f9.a((CombatState)1)` directly — **not** through the shared `ga.a(...)`
|
||||
gate. This means IdlePeace does not go through the wand-preference or
|
||||
stuck-state-recovery logic in §3.1 at all; it just asks for Peace mode
|
||||
every tick it wins.
|
||||
|
||||
**Position in the list.** IdlePeace is instantiated 8 times
|
||||
(`cLogic.cs:530-577`): once as the true terminal catch-all (last item in
|
||||
`l`, after even RandomHelper), and 7 more times as the `LogicRulePreChain`
|
||||
*fallback* wired onto other rules — ReadScroll/StackCram/SalvageItems
|
||||
(idle tier), OpenCorpse (idle tier), the corpse-approach Navigate (idle
|
||||
tier), the target-approach Navigate, and the nav-route Navigate. Because
|
||||
`LogicRulePreChain.Running=true` tries its fallback children *before* its
|
||||
primary action (`LogicRulePreChain.cs:53-67`), this means: while
|
||||
approaching a corpse, a monster, or a nav waypoint, if the *approach
|
||||
itself* isn't valid this tick but IdlePeace's own gated sub-condition is
|
||||
(see below), the character drops to Peace mode as part of that specific
|
||||
rule's turn — long before the terminal catch-all at the very end of the
|
||||
list is ever reached.
|
||||
|
||||
**Interaction with nav minimum distance — the actual mechanism lives
|
||||
outside `cm.cs`, in `fd.a(bool, double)` (`fd.cs:112-177`)**, which is the
|
||||
per-tick movement driver for every `g8`-wrapped Navigate rule (§2.3). Read
|
||||
carefully:
|
||||
|
||||
- `fd` computes whether the character is "close enough" to stop pressing
|
||||
forward (`a(double)` — true when distance < `1.0/160.0` landblock units,
|
||||
`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
|
||||
selected with low waypoint minimum distance. Will switch to magic
|
||||
mode."** (`fd.cs:133`), then 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`).
|
||||
- In other words: **`fd`, not `cm`, is what refuses to let the character
|
||||
sit in Peace mode with an extremely tight waypoint-stop distance** — it
|
||||
actively pushes into Magic mode instead, once per approach, with a
|
||||
one-line warning. `cm`'s own `ValidNow`/`Running` never reference nav
|
||||
distance at all; this override only fires while a `g8`/`fd` Navigate
|
||||
rule is actually driving movement, and IdlePeace being wired as that
|
||||
same rule's fallback (§ above) is *not* the same code path — the
|
||||
fallback IdlePeace still just asks for Peace via `f9.a((CombatState)1)`,
|
||||
unaware of `fd`'s own override, which runs on a completely separate
|
||||
tick of `fd.a()` inside the wrapping `g8`'s own `Running` handling.
|
||||
|
||||
**Cadence.** No dedicated cooldown of its own; it re-requests Peace mode
|
||||
every tick it wins (bounded only by the scheduler's own 293 ms heartbeat /
|
||||
event-driven fast-poke, §1.1).
|
||||
|
||||
---
|
||||
|
||||
## 5. The MossTank gap
|
||||
|
||||
Files read: `MossTankPanel.cs` (`OnTick`, `TickAutomaticBuffing`,
|
||||
`TickRandomHelper`), `CombatController.cs` (`OnTick`, `TickEquipment`,
|
||||
`TryEquipIfNeeded`), `BuffCasterPreparer.cs`, `MacroIdleModeArbiter.cs`,
|
||||
`DispelController.cs`, `VitalRecharge.cs`/`VitalPlan.cs`, `Navigation.cs`.
|
||||
|
||||
### 5.1 Structural model difference
|
||||
|
||||
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
|
||||
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
|
||||
owning the tick prevents a lower one from acting), but it is a different
|
||||
mechanism: VTank rules that lose the pick are actively silenced
|
||||
(`Running=false`, which several rules use to do cleanup —
|
||||
e.g. `b4.cs:108-114` tears down cursors on losing the slot); MossTank
|
||||
controllers that are not granted `canAct` simply never got permission to
|
||||
begin with; there is no equivalent "you just lost the turn, clean up"
|
||||
signal threaded through the chain.
|
||||
|
||||
### 5.2 IdlePeace / MacroIdleModeArbiter: covered, but not the nav-distance
|
||||
override
|
||||
|
||||
`MacroIdleModeArbiter` (full file read) is a faithful, deliberately
|
||||
improved single-owner port of the *terminal* IdlePeace idea — its own doc
|
||||
comment explains it fixed a real gap where `CombatController`'s no-target
|
||||
branch could never reach idle-peace once combat policy was disabled
|
||||
(`MacroIdleModeArbiter.cs:5-13`, `CombatController.cs:270-280`). But
|
||||
**there is no port of `fd.cs:129-138`'s nav-minimum-distance override**:
|
||||
`grep` across `Navigation.cs`, `MacroIdleModeArbiter.cs`, and
|
||||
`CombatController.cs` for "IdlePeace"/"low waypoint"/"minimum distance"/
|
||||
"NavMinDistance" finds nothing outside the arbiter's own setting name.
|
||||
A MossTank user with `IdlePeaceMode` on and a very tight waypoint stop
|
||||
distance would, on retail VTank, see a one-time warning and an automatic
|
||||
push into Magic mode near that waypoint; MossTank has no equivalent — it
|
||||
would presumably just alternate between Peace (arbiter) and whatever mode
|
||||
navigation itself wants, with no warning and no forced-Magic override.
|
||||
|
||||
### 5.3 RandomHelper: round-robin + best-known-tier vs. random + fixed
|
||||
Tier-I
|
||||
|
||||
VTank's `ba` (`ba.cs:114-125`) makes a **random** `(target, spell)` draw
|
||||
(`System.Random`, up to 100 tries) from a **hardcoded Tier-I** spell list
|
||||
(literally "... Other I" for all 11 buffs — it never casts a higher known
|
||||
tier even if the caster knows one). MossTank's `TickRandomHelper`
|
||||
(`MossTankPanel.cs:4047-4122`) instead does a **deterministic round-robin
|
||||
scan** (`_randomHelperCursor`) and picks the caster's **best known tier**
|
||||
of each buff family (`.OrderByDescending(Quality).ThenByDescending(Tier)`,
|
||||
line 4095-4097). Both are defensible design choices, but they are
|
||||
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).
|
||||
|
||||
### 5.4 BuffCasterPreparer vs. `ga.a(...)`: same shape, one real
|
||||
difference
|
||||
|
||||
`BuffCasterPreparer` (full file read) is a close, well-documented port of
|
||||
§3.1's shared gate: prefer the already-wielded caster
|
||||
(`TryResolveCaster`, `BuffCasterPreparer.cs:158-169`, matching
|
||||
`ga.cs:1471-1475`), else the first profiled caster by a **deterministic
|
||||
name-then-object-id order** (`BuffCasterPreparer.cs:171-188`) rather than
|
||||
VTank's `dz.k.j()` scan order (whose actual ordering semantics are
|
||||
unestablished, §6 — so this may or may not be a behavioral difference;
|
||||
it is at minimum a *documented, reproducible* order where VTank's is not).
|
||||
It reproduces the exact chat line "You must add at least one wand to
|
||||
your \[Items\] profile." (`BuffCasterPreparer.cs:201`, cf.
|
||||
`ga.cs:1471, 1481, 1517` verbatim except for the bracketed word), and
|
||||
`CombatController.TryEquipIfNeeded` (`CombatController.cs:820-847`)
|
||||
reproduces the stuck-combat-state recovery with the same
|
||||
`DropToPeaceModeRetryCount` semantics and an equivalent warning message
|
||||
(`CombatController.cs:841-843`). The one structural difference: VTank's
|
||||
gate is retried every scheduler tick (bounded only by the 293 ms
|
||||
heartbeat, §1.1); `BuffCasterPreparer`'s mode-request retry is explicitly
|
||||
throttled to a fixed 2-second wall-clock cadence
|
||||
(`BuffCasterPreparer.cs:33, 229-255`) with its own doc comment explaining
|
||||
why ("a buff pass is not scanning for targets every 0.25 s"). This is a
|
||||
deliberate, documented pacing change, not an oversight — flagged here
|
||||
because it means the *retry-count* semantics (`DropToPeaceModeRetryCount`)
|
||||
are shared, but the *wall-clock time* to exhaust them is not: VTank can
|
||||
exhaust its retry budget in under a second at a 293 ms cadence, while
|
||||
`BuffCasterPreparer` takes `DropToPeaceModeRetryCount × 2` seconds.
|
||||
|
||||
### 5.5 Two-tier vital recharge: present, but merged differently
|
||||
|
||||
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
|
||||
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
|
||||
Norm-tier check only fires while attack is even reachable in the list;
|
||||
the NoTarg-tier check is a wholly separate, later evaluation with its
|
||||
own three setting keys) whereas MossTank always evaluates one combined
|
||||
threshold per vital per tick.
|
||||
|
||||
### 5.6 SummonPet's independent scheduling: not reproduced
|
||||
|
||||
VTank's `n` list (§2.1) runs SummonPet (`h1`) on every tick regardless of
|
||||
what the main list is doing, and its own precondition needs no target —
|
||||
only `EnableCombat` + `SummonPets` + skill + item + its own cooldown
|
||||
gate. MossTank's pet automation (`PetAutomation.Tick`, called from
|
||||
`CombatController.OnTick` only after `_targetId != 0`,
|
||||
`CombatController.cs:270-280, 298-308`) is reachable **only once a
|
||||
hostile target is already selected**. A player relying on VTank's
|
||||
"summon before you need it" behavior (summon while wandering, combat
|
||||
enabled, no target yet) would not see the same behavior in MossTank —
|
||||
the pet is only summoned once combat has already found something to
|
||||
fight.
|
||||
|
||||
### 5.7 Order differences that are cosmetic, not behavioral gaps
|
||||
|
||||
- OpenDoor **is** ported (`Navigation.cs:412+`, `TickDoor`), including the
|
||||
lockpick-vs-open branch and range/threshold settings — not a gap.
|
||||
- The stuck-combat-state recovery message and mechanism **are** faithfully
|
||||
ported (§5.4) — not a gap.
|
||||
- The overall high-level ordering (critical-craft-ish → vital → buff →
|
||||
dispel → mana-recharge → crafting/idle-crafting → loot → inventory →
|
||||
navigation → random-helper → combat, with idle-peace last) tracks
|
||||
VTank's real priority reasonably closely; the one clear ordering
|
||||
inversion found is RefillWieldedMana (VTank: before BuffSelf,
|
||||
`cLogic.cs:471-472`) vs. mana-recharge (MossTank: after
|
||||
`TickAutomaticBuffing`, `MossTankPanel.cs:3792-3814`) — a player would
|
||||
notice this only in the edge case where both a buff and a wielded-item
|
||||
mana-recharge are simultaneously due.
|
||||
|
||||
### Five most important differences a player would notice
|
||||
|
||||
1. **No nav-minimum-distance → forced-Magic-mode override** (§5.2): VTank
|
||||
warns and forces Magic mode near a very tight waypoint stop distance
|
||||
when Idle Peace is on; MossTank has no such override.
|
||||
2. **RandomHelper is deterministic + best-known-tier, not random +
|
||||
fixed Tier-I** (§5.3): visibly different buff choices and cadence
|
||||
pattern on fellow players over a long session.
|
||||
3. **SummonPet requires a target in MossTank; VTank does not** (§5.6): a
|
||||
MossTank character will not pre-summon a pet while wandering with no
|
||||
enemy yet found.
|
||||
4. **BuffCasterPreparer's mode-retry cadence is a fixed 2 s wall-clock
|
||||
pace vs. VTank's ~293 ms scheduler-tick pace** (§5.4): the *number* of
|
||||
retries before the stuck-state recovery fires is the same setting, but
|
||||
MossTank takes several times longer to reach it.
|
||||
5. **RefillWieldedMana runs after buffing in MossTank, before it in
|
||||
VTank** (§5.7): only visible when both are simultaneously due, but is
|
||||
a genuine, checkable ordering inversion.
|
||||
|
||||
---
|
||||
|
||||
## 6. Could not determine
|
||||
|
||||
- ~~`dz.k.j()` ordering~~ — RESOLVED (lead, 2026-09-06): Items-page
|
||||
insertion order; producers are `PluginCore.cs:8422-8434` and the profile
|
||||
loader. See §3.2(a).
|
||||
- ~~`GoToPeaceModeToUseKits` absent~~ — RESOLVED: it exists at `a5.cs:121`
|
||||
and `defaultsettings.usd:931`. See §3.2(c).
|
||||
- **`dz.p` / `hi` (the Attack rule's target-selection and attack-plan
|
||||
execution internals)** were read only at the level needed to describe
|
||||
scheduling (`dz.cs:664-667, 201-206`); the damage-formula, spell-choice,
|
||||
and melee/missile-vs-magic decision logic inside `hi`/`dz.cs`'s target
|
||||
sort were not traced — out of scope for a scheduler/action-list doc,
|
||||
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.
|
||||
- **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),
|
||||
not one of the 24 action classes catalogued here.
|
||||
579
docs/research/vtank-kb/03-combat.md
Normal file
579
docs/research/vtank-kb/03-combat.md
Normal file
|
|
@ -0,0 +1,579 @@
|
|||
# VTank knowledge-base 03 — Combat
|
||||
|
||||
Research-only. Oracle: `refs/vtank/decompiled/` (obfuscated VTank 2.x
|
||||
source; class/field names below are the decompiler's raw identifiers —
|
||||
see the class map). Cross-checked against
|
||||
`docs/research/2026-08-26-mosstank-vtank-utilitybelt-research.md` §2.1
|
||||
(marked disagreements are called out explicitly; everywhere else the
|
||||
two sources agree). No code was changed to produce this document.
|
||||
|
||||
## Class map (for anyone grepping the decompiled tree later)
|
||||
|
||||
VTank's god object is `PluginCore.dz`, an instance of class `s`
|
||||
(`refs/vtank/decompiled/s.cs:7-114`). Its combat-relevant children:
|
||||
|
||||
| Field (`dz.X`) | Type / file | Role |
|
||||
|---|---|---|
|
||||
| `p` | `dz` (`dz.cs`) | Target scan + candidate ranking, debuff-spell/item resolution |
|
||||
| `e` | `d1` (`d1.cs`) | Monster Rules table (`MyMonsters`) + per-creature rule cache |
|
||||
| `o` | `ga` (`ga.cs`) | Creature-info dict, current/last target id, blacklist list, wield-item info, pet pick, `CombatState` classifier |
|
||||
| `w` | `bo` (`bo.cs`) | Melee/missile attack executor (swing timing, hit/miss chat parsing) |
|
||||
| `h` | `gj` (`gj.cs`) | Spell-cast state machine ("SpellCaster": gesture echo → result text) |
|
||||
| `y` | `e0` (`e0.cs`) | GameInfoDB client: monster/species auto-damage tables, heal-kit/grenade dbs |
|
||||
| `an` | `fp` (`fp.cs`) | Blacklist-attempt-count manager (temporary skip) |
|
||||
| `am` | `b8` (`b8.cs`) | Ghost-monster detector (permanent client-side deletion) |
|
||||
| `f` | `ak` | Spell name → `MySpell` lookup |
|
||||
| `i` | `fk` | Wand/spell-equivalent + element→spell-name resolution (War/Arc/Ring/Streak/Vuln) |
|
||||
| `j` | `dm` | Per-target recast-timer tracker |
|
||||
| `d` | `cLogic` (`cLogic.cs`) | Primary rule scheduler (293 ms tick) |
|
||||
| `m` | `da` (`da.cs`) | Character profile loader; owns the settings DB (`.c["MyMonsters"]`) |
|
||||
|
||||
`f7` (`f7.cs`) is not a `dz` field — it is the transient
|
||||
"attack candidate" record built fresh for every monster on every scan.
|
||||
`hi` (`hi.cs`) is the transient "what to do this tick" decision object,
|
||||
recreated each attack tick inside `dz.b(double)`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Target acquisition
|
||||
|
||||
**Scan cadence.** The primary macro loop (`cLogic`) arms a 293 ms
|
||||
repeating timer (`m_a = 293`) at construction and starts it from
|
||||
`StartMacro` (`refs/vtank/decompiled/uTank2/cLogic.cs:11,52-65,327-328`).
|
||||
Every tick, if not already mid-poke, `TryPokeMacro` walks the ordered
|
||||
rule list and re-evaluates `ValidNow` on each rule top to bottom,
|
||||
running the first one whose `ValidNow` is true
|
||||
(`cLogic.cs:163-173,183-260`). The "Attack" rule (`b4`) is one of
|
||||
those rules: its `ValidNow` calls `dz.p.c()` — the full target scan —
|
||||
on **every** 293 ms tick, gated only by `EnableCombat` and the
|
||||
`ItemUse` action lock (`refs/vtank/decompiled/b4.cs:62-76`). There is
|
||||
no separate, slower "target scan" timer; scanning and rule evaluation
|
||||
share the same 293 ms cadence. A second `cLogic` timer at 3203 ms
|
||||
(`m_b`) exists but only forces a GC pass and is unrelated to combat
|
||||
(`cLogic.cs:37,61-70,266-277`).
|
||||
|
||||
**What counts as a target.** `dz.p.c()` calls `dz.p.b(AttackDistance)`,
|
||||
which enumerates every object of `ObjectClass.Monster` known to the
|
||||
world filter (`dz.q.a(ObjectClass.Monster)`) and builds one `f7`
|
||||
candidate per object via `f7.a(fu, maxDist, minDist, targetLockArg)`
|
||||
(`refs/vtank/decompiled/dz.cs:664-704,706-730`; `refs/vtank/decompiled/f7.cs:247-297`).
|
||||
A candidate is rejected (`f7.n = false`) at the first failing gate, in
|
||||
this order (`f7.cs:254-296`):
|
||||
|
||||
1. No cached creature-info record for the guid (`CreatureInfoMissing`).
|
||||
2. The creature-info record is itself invalid, i.e. `hj.a()` true (`CIInvalid`).
|
||||
3. Its matched Monster Rule priority is negative (`NegativePriority` — see §3).
|
||||
4. Distance from the player exceeds the scan's max (`DistanceTooFar` — normally `AttackDistance`, but the pet-pick and "target lock follow" call sites pass their own max, `dz.cs:952,971,1092-1093`).
|
||||
5. Distance is below the scan's min (`DistanceTooNear` — normally `AttackMinimumDistance`).
|
||||
6. It needs a debuff but the monster's rule forbids attacking without one first (`DebuffPassWithNoAttack` — the two-arg `dz.a(int,f7)`/`dz.a(int,bool,d1.a)` gate at `dz.cs:561-607`; only reached when the rule's own "needs a specific debuff and it's not up" test at `dz.cs:561-585` is true **and** the rule is a pure-debuff row with no independent Attack/Ring flag).
|
||||
|
||||
A candidate that passes fills in distance (`f7.e`), heading delta to
|
||||
the target (`f7.f`), the monster-rule debuff-urgency score (`f7.g`,
|
||||
see §2), whether it *is* the last-attacked target (`f7.l`, guid equals
|
||||
`ga.e`), whether it *is* the currently-attacked target (`f7.k`, guid
|
||||
equals `ga.d`), whether it is the in-game selection **and**
|
||||
`TargetLock` is on (`f7.j`), and whether it is blacklisted
|
||||
(`f7.m`, guid is in `ga.f`) (`f7.cs:283-296`; `ga.cs:56-64` for the
|
||||
field types).
|
||||
|
||||
**Ranges.** All distance-shaped settings (`AttackDistance`,
|
||||
`AttackMinimumDistance`, `RingDistance`, `ApproachDistance`,
|
||||
`ArcRange`, `TargetSelectAngleRange`, `PetCustomRange`) are compared
|
||||
directly, unconverted, against `f7.e`
|
||||
(`dz.cs:666,716-726`; `ga.cs:1081-1086`). The only place a `*240`
|
||||
conversion appears is the monster-rule expression keyword `range`,
|
||||
which exists purely for user-facing text
|
||||
(`refs/vtank/decompiled/cl.cs:159-163`) — this confirms the raw
|
||||
setting values and `f7.e` share one internal unit where 240 units ≈ 1
|
||||
meter, but exact numeric defaults could not be reliably decoded from
|
||||
`uTank2.Resources.defaultsettings.usd` (see §9).
|
||||
|
||||
**Ghost detection — two independent, differently-scoped mechanisms.**
|
||||
VTank does not have one "ghost" concept; it has two counters fed by
|
||||
different signals with different consequences:
|
||||
|
||||
| | Blacklist (`dz.an` = `fp`) | Ghost delete (`dz.am` = `b8`) |
|
||||
|---|---|---|
|
||||
| Increment trigger | A physical **missile** attack reports "hit the environment" (`refs/vtank/decompiled/bo.cs:110-116`) | (a) repeated retries of the cast gesture before the "You say" echo ever arrives (`refs/vtank/decompiled/gj.cs:319-334`); (b) a cast enters its result-wait state and **times out with no fail/success/kill text at all**, single-target spells only (`gj.cs:468,477-485`) |
|
||||
| Force-trip (bypasses count) | An explicit "permanent fail" cast-result text (spell inapplicable to the creature) calls `fp.a` directly (`gj.cs:401-413`) | — |
|
||||
| Reset | Any confirmed melee/missile hit-message (`bo.cs:117-120`); a spell "kill" or "success" result text (`gj.cs:391-398,442-452`); entering the cast-result-wait state resets the ghost counter unconditionally (`gj.cs:222-223`) | separately, an HP-tracker path (below) |
|
||||
| Threshold | `BlacklistMonsterAttemptCount` (`fp.cs:79`) | `GhostMonsterSpellAttemptCount` (`b8.cs:111`) |
|
||||
| Consequence | Marks the creature-info record blacklisted for `BlacklistMonsterTimeoutSeconds` — a **temporary** skip in future scans; the object is not touched (`fp.cs:86-100`) | Deletes the client-side world object outright (`f9.f(guid)`), only if it is still `ObjectClass.Monster` and `DeleteGhostMonsters` is on — **permanent** for that object (`b8.cs:111-120`) |
|
||||
|
||||
A **second, independent** ghost path exists purely from HP-bar
|
||||
staleness: if `DeleteGhostMonstersByHPTracker` is on, the macro is
|
||||
running, and there is a currently-HP-tracked monster
|
||||
(`dz.ao.b() != 0`) whose species is recognized in the damage db, then
|
||||
once **both** "time since the tracker last updated" and a second
|
||||
tracker timestamp exceed `GhostDeleteHPTrackerSeconds`, that object is
|
||||
deleted the same way, independent of any spell attempts
|
||||
(`b8.cs:77-99`). This ticks on its own ~6.3 s timer (`b8.cs:16-19`),
|
||||
not the 293 ms combat tick.
|
||||
|
||||
**Target lock.** `TargetLock` only changes whether "is the in-game
|
||||
selection" (`f7.j`) counts as true when filling a candidate
|
||||
(`f7.cs:293`, third argument threaded from `dz.cs:717,730`). It is
|
||||
**not** a first-refusal filter at the candidate-building stage — its
|
||||
effect on final target choice is a low-priority tie-breaker inside
|
||||
selection (§2).
|
||||
|
||||
## 2. Target selection
|
||||
|
||||
After building candidates, `dz.p.b` picks one target via a single
|
||||
linear pass that keeps a running "best so far" (`this.a`), never a
|
||||
full sort (`dz.cs:706-730` header, comparison chain `dz.cs:740-920`).
|
||||
The comparison chain, evaluated **in this exact order** for every
|
||||
candidate against the current best:
|
||||
|
||||
1. **Priority** (the matched Monster Rule's priority, §3) always wins
|
||||
first — *unless* `DebuffEachFirst` is `"All"`, in which case a
|
||||
candidate that still needs a debuff outranks one that doesn't
|
||||
**even across a priority difference**, and priority is only
|
||||
compared once both share the same debuff-need state
|
||||
(`dz.cs:740-751`, flag2 gate).
|
||||
2. Within a priority tie: if `DebuffEachFirst` is `"All"` **or**
|
||||
`"Priority"`, a candidate needing a debuff outright beats one that
|
||||
doesn't (`dz.cs:770-781`, `flag` gate). `DebuffEachFirst == "One"`
|
||||
skips this step entirely — VTank's own §2.1 phrasing ("one target,
|
||||
priority group, or all targets before attack") maps to these three
|
||||
values.
|
||||
3. **Debuff-urgency score** (`f7.g`, from `dz.p.b(guid,f7)` —
|
||||
`dz.cs:163-199`): a small 0–3 integer — +1 if a Vuln debuff
|
||||
matching the *currently chosen attack element* is not yet applied
|
||||
(recast timer still running), **and** +2 if either Imperil (when
|
||||
fighting with a melee/missile weapon) or Magic Yield (when fighting
|
||||
bare-handed/with magic, i.e. `CombatState == Magic`) is not yet
|
||||
applied. Higher score wins outright; this is evaluated for **every**
|
||||
candidate regardless of `DebuffEachFirst`, and only checks these
|
||||
two/three specific debuffs, not the full monster-rule debuff set.
|
||||
4. **Target lock** (`f7.j`): if one candidate is the in-game selection
|
||||
under `TargetLock` and the other isn't, it wins (`dz.cs:788-793`).
|
||||
5. **Wield-match** (only when *both* candidates are within
|
||||
`TargetSelectAngleRange` distance of the player): counts how many
|
||||
of {weapon, offhand} would need to change to engage each candidate
|
||||
and prefers fewer changes — i.e. avoid re-wielding between two
|
||||
nearby targets (`dz.cs:794-824`).
|
||||
6. **Sticky last target** (`f7.l`, guid equals the previously-attacked
|
||||
guid `ga.e`): prefer the target already being attacked over a new
|
||||
one (`dz.cs:825-830`).
|
||||
7. Only now does `TargetSelectMethod` (§ below) decide.
|
||||
|
||||
**MossTank disagreement:** `CombatController.cs:1707-1798` clusters
|
||||
candidates by max priority, then gives `TargetLock` (line 1740) and
|
||||
the previously-attacked target (line 1757, explicitly commented as
|
||||
modelling `ga.e`) **unconditional first refusal** ahead of any
|
||||
ranking — i.e. steps 4 and 6 above are promoted ahead of steps 2–3
|
||||
instead of following them. Steps 2 (`DebuffEachFirst`) and 3
|
||||
(debuff-urgency score) have no equivalent in `CombatController.cs` at
|
||||
all. See §8 gap #1.
|
||||
|
||||
**The three `TargetSelectMethod` values**, read once per scan as
|
||||
`f3.f("TargetSelectMethod")` (`dz.cs:722`), decide only the final tie
|
||||
(step 7) — for the vast majority of contested scans with more than one
|
||||
same-priority monster this is reached only after every prior step
|
||||
above ties:
|
||||
|
||||
| Value | Name (VTank UI) | Rule (`dz.cs:831-915`) |
|
||||
|---|---|---|
|
||||
| 1 | Distance | Sort by distance (`f7.e`) first, heading delta (`f7.f`) as the tiebreak |
|
||||
| 2 | Angle | Sort by heading delta first, distance as the tiebreak |
|
||||
| 3 | Both (hybrid) | If **both** compared candidates are within `TargetSelectAngleRange` distance of the player, sort by angle first (distance tiebreak); if **both** are beyond it, sort by distance first (angle tiebreak); a candidate within the cutoff always beats one beyond it, regardless of angle/distance values |
|
||||
|
||||
Despite its name, `TargetSelectAngleRange` is compared directly
|
||||
against the **distance** field `f7.e`, never against `f7.f` (the
|
||||
angle) — this is a real quirk of the retail setting, not a
|
||||
misreading; MossTank's `CombatController.cs:1774` reproduces this
|
||||
correctly (`candidate.Target.Distance <= _settings.TargetSelectAngleRange`).
|
||||
|
||||
## 3. Monster rules
|
||||
|
||||
The `MyMonsters` table backs `d1` (`d1.cs`). Its 21 columns, recovered
|
||||
from the schema listed in
|
||||
`refs/vtank/decompiled/uTank2.Resources.defaultsettings.usd:48-70` and
|
||||
cross-checked against the column→field wiring in
|
||||
`d1.a.a(cw)`/`a(a)` (`d1.cs:58-119`) and the incremental-migration
|
||||
names in `refs/vtank/decompiled/da.cs:280-322`:
|
||||
|
||||
| # | Column name | `d1.a` field | Type | Meaning |
|
||||
|---|---|---|---|---|
|
||||
| 0 | MonsterName | `u` | string | Rule name; `"<DEFAULT>"` is the fallback row |
|
||||
| 1 | AttackPriority | `a` | int | -1 (never attack/never targeted) .. 4 |
|
||||
| 2 | DamageType | `b` | `eDamageElement` | Primary attack element, or `Auto`/`Harm` |
|
||||
| 3 | WeaponToUse | `f` | int | Wielded-item object id override (0 = auto) |
|
||||
| 4 | Imperil | `g` | bool | Cast Imperil Other I |
|
||||
| 5 | Vuln | `h` | bool | Cast Vuln matching the *attack* element |
|
||||
| 6 | Yield | `i` | bool | Cast Magic Yield Other I |
|
||||
| 7 | GravityW | `k` | bool | Cast Gravity Well |
|
||||
| 8 | Attack | `!t` | bool (stored inverted) | Attack the monster at all |
|
||||
| 9 | Ring | `j` | bool | Use ring/void-curse attack magic |
|
||||
| 10 | Broadside | `l` | bool | Cast Broadside of a Barn |
|
||||
| 11 | Fester | `m` | bool | Cast Fester Other I |
|
||||
| 12 | WeakeningCurse | `n` | bool | Cast Weakening Curse I |
|
||||
| 13 | FesteringCurse | `o` | bool | Cast Festering Curse I |
|
||||
| 14 | Corruption | `p` | bool | Cast Corruption I |
|
||||
| 15 | DestructiveCurse | `q` | bool | Cast Destructive Curse I |
|
||||
| 16 | Corrosion | `r` | bool | Cast Corrosion I |
|
||||
| 17 | Streak | `s` | bool | Prefer streak-shape attack spells |
|
||||
| 18 | SecondaryVuln ("Ex. Vuln") | `c` | `eDamageElement` | Extra Vuln element beyond the natural one |
|
||||
| 19 | SecondaryEquip ("Offhand") | `e` | `eSecondaryEquipTypeOrObjectID` | Offhand item selection mode/id (exact enum values not recovered, §9) |
|
||||
| 20 | PetDamageType | `d` | `eDamageElement` (default `PAuto`) | Preferred pet damage element for this monster |
|
||||
|
||||
**Matching semantics.** `d1.a(fu)` walks the table top-to-bottom,
|
||||
skipping the `"<DEFAULT>"` row, and returns the **first** row whose
|
||||
expression matches; the default row is the fallback only when nothing
|
||||
else matched (`d1.cs:415-448`). A per-row/per-target match result is
|
||||
cached for the session unless the expression used a *volatile* token
|
||||
(see below), in which case it's re-evaluated every call
|
||||
(`d1.cs:389-405`; the cacheability flag comes from `cl.a`'s `out bool`).
|
||||
|
||||
**Expression grammar** (`cl.cs`), a small infix language with a
|
||||
shunting-yard evaluator over doubles and strings:
|
||||
|
||||
- Literals: numbers, and quoted/bare strings.
|
||||
- Operators: `&& || == < > >= <= != #` (regex match, string only)
|
||||
`+ - * / %`, with `(` `)` grouping (`cl.cs:279-304` precedence table,
|
||||
`cl.cs:306-446` evaluator).
|
||||
- Built-in identifiers, each a function of the candidate monster
|
||||
(`cl.cs:87-182`): `true`, `false`, `name`, `typeid`
|
||||
(`PropertyInt` `bc.cp`), `species` (species-table name via
|
||||
`dz.y.b`), `maxhp` (`dz.y.c`, from the damage/species db), `range`
|
||||
(distance × 240, **volatile**), `hasshield` (any armor-class item on
|
||||
the target, **volatile**), `metastate` (**volatile**).
|
||||
- `setting_<Name>` reads any VTank setting by name at evaluation time
|
||||
(**volatile**) (`cl.cs:184-216`).
|
||||
- If the final expression value is numeric, non-zero means match; if
|
||||
it resolves to a string, VTank instead compares that string
|
||||
case-insensitively to the monster's own name (`cl.cs:247-262`) — so
|
||||
a bare string literal like `"Drudge"` is itself a valid "expression."
|
||||
- Parse or evaluation errors are caught, logged, and treated as
|
||||
cacheable non-matches (`cl.cs:263-274`).
|
||||
|
||||
**MossTank comparison:** `MonsterExpression.cs` and `MonsterRules.cs`
|
||||
are a faithful, well-cited re-derivation of this exact grammar,
|
||||
identifier set, and volatility/caching model (`MonsterExpression.cs:69-117,151,214-229`;
|
||||
`MonsterRules.cs:104-150`) — no material gap found here.
|
||||
|
||||
## 4. Weapon and damage choice
|
||||
|
||||
**Auto damage database.** `dz.y` (`e0`) wraps a community
|
||||
`gameinfodb.ugd` file (falling back to an embedded
|
||||
`defaultinfodb.ugd`), auto-updated from
|
||||
`auth.virindi.net/plugins/gamedb/get2.php`
|
||||
(`e0.cs:53-79,435-441`). `e0.d(monsterName)` resolves the auto-damage
|
||||
element preference list with a two-step fallback: an explicit
|
||||
per-monster row in `MonsterDamageOverrides`, else the monster's
|
||||
species row in `SpeciesDamages` (via `SpeciesMembers` for the
|
||||
name→species id lookup); if neither exists it returns an **empty**
|
||||
list, not a guessed default (`e0.cs:327-349`). When a Monster Rule's
|
||||
`DamageType` is `Auto`, `f7.a()`'s private element resolver takes the
|
||||
list's **first** entry as the debuff/attack element, or leaves it
|
||||
`None` if the list is empty (`f7.cs:200-227`).
|
||||
|
||||
**MossTank disagreement:** `VtankDamageDatabase.cs:12-21,28-44` falls
|
||||
back to a hardcoded 7-element guess order
|
||||
(`Pierce, Bludgeon, Slash, Acid, Electric, Cold, Fire`) when a monster
|
||||
is in neither local table, where retail simply has no auto-element
|
||||
for that monster (no Vuln cast, `f7.h == None`). See §8 gap #4.
|
||||
|
||||
**Ammo/prismatic.** For bow-class weapons, `ga.a(fi,eDamageElement)`
|
||||
checks whether ammunition of the requested element is actually in
|
||||
inventory via `bv.b(fi, element, 1, ePrismaticDamageBehavior.Any)`
|
||||
(a small per-tick cache keyed by launcher-shape) before allowing that
|
||||
element to be used, logging a warning and refusing otherwise
|
||||
(`ga.cs:1211-1241`); a mirrored `b(fi,eDamageElement)` exists
|
||||
immediately after (only partially read; not confirmed identical).
|
||||
Launcher/projectile shape is carried as a 4-value enum `l`
|
||||
(`l.a/b/c/d`) attached to debuff items and spells (`dz.cs:250,264,296`;
|
||||
`f7.cs:122-146`) but its four case names could not be recovered from
|
||||
the available source (§9).
|
||||
|
||||
**Offhand / re-wield.** The Monster Rule's `SecondaryEquip` column
|
||||
(`eSecondaryEquipTypeOrObjectID`) selects the offhand item; the target
|
||||
selector's wield-match tie-break (§2 step 5) actively tries to avoid
|
||||
re-wielding weapon or offhand between two nearby targets, but does not
|
||||
prevent it outright — a genuine priority/urgency difference always
|
||||
forces a re-wield.
|
||||
|
||||
**MossTank comparison:** `VtankAmmunitionDatabase.cs` was not read in
|
||||
detail for this pass; flagged for a follow-up doc rather than guessed
|
||||
here.
|
||||
|
||||
## 5. Attack execution
|
||||
|
||||
**Melee/missile timing (`bo`, `refs/vtank/decompiled/bo.cs`).**
|
||||
`bo.a(guid,power,spell)` (called only for physical attacks — `spell`
|
||||
is always `null` here) selects the target in-game if not already
|
||||
selected and arms the attack (`bo.cs:326-348`). A 263 ms timer
|
||||
(`bo.cs:22,46-48`) drives the swing loop: while waiting to confirm the
|
||||
in-game selection actually changed, it re-issues `SelectItem`; once
|
||||
selected, the private swing method fires (`bo.cs:238-268`). That
|
||||
method reads `DefaultMeleeAttackHeight`, and if `AutoAttackPower` is
|
||||
on, applies the computed power (§ below) via `f9.a`
|
||||
(`bo.cs:296-324`); it then sends the height-mapped key down+up pair
|
||||
(`ha.a/b/c → br.aq/af/ae`, `bo.cs:172-181`) and locks
|
||||
`ActionLockType.MeleeAttackShot` for 0.75 s (`bo.cs:322`). Hit/miss is
|
||||
read back out of chat: a missed missile shot ("hit the environment")
|
||||
feeds the blacklist counter, a matching damage-report line
|
||||
(`^(Critical hit!)? ... for ... point(s) of ...!$`) resets it
|
||||
(`bo.cs:110-120`, §1 table).
|
||||
|
||||
**Power/height and Recklessness (`hi.c`, called only for
|
||||
`CombatState.Melee`/`Missile`, i.e. `val == 2`/`4`;
|
||||
`hi.cs:650-680`).** Missile attacks always use power `1f`. Melee power
|
||||
is a fixed decision table over: whether the chosen weapon is a
|
||||
single-hand slash/pierce hybrid without an offhand melee weapon,
|
||||
whether the weapon has a triple-slash attack type, and whether the
|
||||
offhand is another melee weapon or a shield — producing one of
|
||||
`{0f, 0.2f, 0.49f, 0.5f, 1f}` (`hi.cs:657-661`). If `UseRecklessness`
|
||||
is on and the Recklessness skill is trained, the result is clamped to
|
||||
`[0.11, 0.9]` (`hi.cs:663-673`; `bo.cs` applies the same clamp to the
|
||||
`AutoAttackPower`-computed value). **MossTank's `AutoAttackPower.cs`
|
||||
is a faithful, explicitly-cited port of this exact table and clamp**
|
||||
(`AutoAttackPower.cs:55-83`, header comment names `hi.cs` directly) —
|
||||
no material gap found.
|
||||
|
||||
**Magic attack selection (`hi.a`, the per-tick decision object,
|
||||
`hi.cs:66-327`).** `CombatState` is derived from the *player's chosen
|
||||
weapon's* `ObjectClass`: `MeleeWeapon → Melee(2)`,
|
||||
`MissileWeapon → Missile(4)`, anything else (including bare hands and
|
||||
wands/orbs) → `Magic(8)` (`ga.cs:1581-1616`; the numeric tags are
|
||||
inferred from the `(CombatState)N` casts used at every call site, not
|
||||
from an explicit enum declaration — see §9). When `CombatState ==
|
||||
Magic`, `hi` first runs the fixed 12-step debuff-priority chain (§6),
|
||||
then, only if none is due, the attack-spell branch:
|
||||
|
||||
- **Ring** is used when the monster rule's Ring flag is set **and**
|
||||
either the nearby-monster count (`dz.p.c`, tallied during the scan
|
||||
as "candidates within `RingDistance`" — `dz.cs:735-739`) meets
|
||||
`MinimumRingTargets`, or the rule has no independent Attack flag at
|
||||
all (`hi.cs:220-240`). If the ring spell needs scarab components and
|
||||
they're in inventory, casts it directly; otherwise falls through to
|
||||
bolt/arc.
|
||||
- **Streak** is tried next when the rule's Streak flag is set and a
|
||||
usable streak spell of the requested element exists; if none is
|
||||
usable it logs a warning and falls back to bolt/arc
|
||||
(`hi.cs:258-307`).
|
||||
- **Bolt vs Arc** (`hi.a(eDamageElement,f7)`, `hi.cs:471-542`): looks
|
||||
up a War-school bolt spell and an Arc spell for the element; if only
|
||||
one exists, use it; **otherwise the higher-`Quality` spell wins
|
||||
outright** — `UseArcs` is consulted **only when both spells tie in
|
||||
Quality**, where `1` = prefer bolt, `2` = prefer arc only if
|
||||
`f7.e >= ArcRange`, `3` = always prefer arc, default = prefer bolt.
|
||||
|
||||
**MossTank disagreement:** `AttackSpellCatalog.cs`'s `Preference()`
|
||||
(lines 140-204) buckets candidates by `UseArcs`/`ArcRange`/streak
|
||||
**before** ever comparing spell quality — Tier/Difficulty only break
|
||||
ties *within* a bucket (`Compare`, lines 93-138). This means MossTank
|
||||
will follow the `UseArcs`/range rule even when the character actually
|
||||
knows a strictly higher-tier spell of the other shape, where retail
|
||||
picks the higher-tier spell outright and only falls back to
|
||||
`UseArcs` on an exact tie. See §8 gap #2.
|
||||
|
||||
**Spell fizzle / result handling (`gj`, the cast state machine,
|
||||
`gj.cs:1-149,341-466`).** States: idle → waiting for the "You say ..."
|
||||
gesture echo → waiting for a result chat line. Result-line
|
||||
classification against four regex families (`refs/vtank/decompiled` —
|
||||
list source not fully traced, referenced as `l.g.{a,b,c,d}`):
|
||||
"kill" (`d`, ends the target and clears the blacklist counter),
|
||||
"permanent fail" (`b`, e.g. immune — force-trips the blacklist
|
||||
immediately), "fail"/resist (`a`, plain reset, no penalty), "success"
|
||||
(`c`, matched by spell name + optional target name — clears the
|
||||
blacklist counter). A silent timeout with **no** result line at all
|
||||
increments the ghost counter instead (§1).
|
||||
|
||||
## 6. Debuffs
|
||||
|
||||
**Fixed check order.** Debuff *choice* is not a sorted/scored list —
|
||||
`hi`'s private decision method (`hi.cs:66-327`) tests exactly twelve
|
||||
debuffs in this **hardcoded** order and dispatches the first one whose
|
||||
recast timer has elapsed (within `DebuffPrecastSeconds` of expiring,
|
||||
except Corruption/DestructiveCurse/Corrosion which require the timer
|
||||
to have fully reached zero):
|
||||
|
||||
1. Magic Yield Other I (`hi.cs:123-129`)
|
||||
2. Weakening Curse I (`130-136`)
|
||||
3. Festering Curse I (`137-143`)
|
||||
4. Corruption I (`144-150`, zero-tolerance)
|
||||
5. Destructive Curse I (`151-157`, zero-tolerance)
|
||||
6. Corrosion I (`158-164`, zero-tolerance)
|
||||
7. Imperil Other I (`165-171`)
|
||||
8. Vuln matching the **current attack element** (`172-178`)
|
||||
9. Vuln matching the rule's **`SecondaryVuln`/"Ex. Vuln"** column, via
|
||||
`f7.h` (`179-185`)
|
||||
10. Gravity Well (`186-192`)
|
||||
11. Broadside of a Barn (`193-199`)
|
||||
12. Fester Other I (`200-206`)
|
||||
|
||||
Once a debuff is chosen, the actual spell/wand/item used to cast it is
|
||||
resolved separately by `dz.a(MySpell,f7)` (`dz.cs:219-393`), which
|
||||
picks among: the equivalent spell known by the character, a wielded
|
||||
wand of matching family/quality, or a thrown "grenade" item — ranked
|
||||
by the `dz.b` comparer using `DebuffSelectionMethod` (`"SpellLevel"`
|
||||
compares item Quality first then item count/priority, `"Skill"` swaps
|
||||
that order — `dz.cs:11-91`), with wand fallback and
|
||||
`AllowDebuffFallback` gating whether a mismatched projectile-type item
|
||||
may substitute at all (`dz.cs:233-393`). This per-debuff *item* choice
|
||||
comparer is a completely separate mechanism from the fixed
|
||||
*debuff-kind* order above.
|
||||
|
||||
**`DebuffEachFirst` reaches into target selection**, not just
|
||||
scheduling — see §2 steps 1–2. `"One"` leaves target choice alone;
|
||||
`"Priority"` makes debuff-need a tiebreak within a priority tier;
|
||||
`"All"` makes it override priority itself until the need is resolved.
|
||||
|
||||
**Wand switching.** When the chosen debuff must be cast via a wand and
|
||||
`SwitchWandsToDebuff` is on, VTank actually re-wields to a
|
||||
matching-element wand for the cast (comparing the *target's* current
|
||||
wielded-item CombatState against the *player's own* prospective
|
||||
change) before casting, then restores afterward
|
||||
(`dz.cs:486-508`).
|
||||
|
||||
**MossTank disagreement (highest-impact finding in this document):**
|
||||
`DebuffSpellCatalog.cs`'s `OrderedFlags` (lines 21-34) declares the
|
||||
order `Fester, Broadside, GravityWell, Imperil, Yield, Vulnerability,
|
||||
WeakeningCurse, FesteringCurse, Corruption, DestructiveCurse,
|
||||
Corrosion` — almost the **reverse** of retail's real order above
|
||||
(retail's *last*-checked debuff, Fester, is MossTank's *first*).
|
||||
Worse, MossTank does not implement retail's "check exactly one fixed
|
||||
kind per tick, first due wins" model at all: it gathers **every** due
|
||||
debuff into a candidate set and sorts it by `DebuffSelectionMethod`
|
||||
(Skill/SpellLevel) then spell Tier/Difficulty, with `ActionOrder`
|
||||
(the wrong-order array above) only as the final tiebreak
|
||||
(`DebuffSpellCatalog.cs:93-118`). Retail's `DebuffSelectionMethod`
|
||||
comparer (`dz.b`, §6 above) is a *per-kind item/spell choice*
|
||||
mechanism in the real client, never a *cross-kind debuff-choice*
|
||||
ranking — MossTank has repurposed it for a role retail never gives it.
|
||||
Also, retail's natural-element Vuln (step 8) and the rule's own
|
||||
`SecondaryVuln` Vuln (step 9) are two sequential, separately-ordered
|
||||
checks; MossTank's `Required()` (lines 125-146) does add both as
|
||||
distinct `DebuffIdentity` values, but assigns them the *same*
|
||||
`ActionOrder` (5), so their relative order falls to
|
||||
Tier/Difficulty/SpellId instead of retail's guaranteed
|
||||
natural-before-extra sequence. See §8 gap #1.
|
||||
|
||||
## 7. Pets
|
||||
|
||||
`ga.j()` (`ga.cs:1076-1209`) is the pet-selection algorithm, run from
|
||||
the always-on `h1` "SummonPet" logic rule
|
||||
(`refs/vtank/decompiled/h1.cs:30-53`, gated on `EnableCombat`,
|
||||
`SummonPets`, the Summoning skill being trained, and a
|
||||
spell-cooldown/readiness check via `an.a(-32555)`/`bm.a()` whose exact
|
||||
semantics were not traced further):
|
||||
|
||||
1. Pick a scan range: `PetCustomRange` if `PetRangeMode == 1`, else
|
||||
`AttackDistance` (`ga.cs:1081-1085`).
|
||||
2. Build `f7` candidates for every monster in range (bypassing the
|
||||
min-distance/target-lock/priority-reject gates used for normal
|
||||
attack scanning — only the base validity check `f7.o` is used);
|
||||
among those whose matched Monster Rule has a `PetDamageType` other
|
||||
than `None`, track the count and the single best one under a
|
||||
**combined** "nearer distance AND higher rule priority" predicate
|
||||
(`ga.cs:1090-1110`).
|
||||
3. Refuse to summon (return 0) if no eligible monster exists, or if
|
||||
the eligible count is below `PetMonsterDensity`
|
||||
(`ga.cs:1111-1118`).
|
||||
4. Among the player's own pet-capable items (`PluginCore.PC.ec`,
|
||||
filtered to ones actually owned/wieldable with a known damage
|
||||
element), score each by how well its element matches the target:
|
||||
exact match to the rule's `PetDamageType` scores best, then a match
|
||||
to the target's *actual chosen attack element*, then a match
|
||||
anywhere in the target's auto-damage preference list (indexed, so
|
||||
earlier list entries score better); ties prefer the pet with the
|
||||
higher `a12.m` stat (not identified further) (`ga.cs:1119-1206`).
|
||||
5. Return the winning pet's object id, or 0 for none.
|
||||
|
||||
The `h1` rule's `Running(true)` handler simply calls `f9.p(petId)` —
|
||||
use/summon that item (`h1.cs:74-81`). No MossTank pet-selection code
|
||||
was located in this pass to compare against (`CombatController.cs`
|
||||
was not searched exhaustively for a pet feature — flagged in §9).
|
||||
|
||||
## 8. MossTank gap ranking (highest player impact first)
|
||||
|
||||
1. **Debuff-kind ordering and selection model is structurally
|
||||
different, not just re-ordered.** `DebuffSpellCatalog.OrderedFlags`
|
||||
(`DebuffSpellCatalog.cs:21-34`) is close to the reverse of retail's
|
||||
real fixed 12-step order (`hi.cs:123-206`), and MossTank scores
|
||||
across debuff *kinds* using a comparer retail only ever uses to
|
||||
choose *within* one kind (`DebuffSpellCatalog.cs:93-118` vs.
|
||||
`dz.b`, `dz.cs:11-91`). Effect: a MossTank character debuffs
|
||||
targets in a different sequence than retail VTank ever would,
|
||||
which changes which debuff is up when an attack lands and can
|
||||
waste casts on lower-value debuffs first.
|
||||
2. **Target selection omits `DebuffEachFirst` and the debuff-urgency
|
||||
score entirely, and inverts the priority of `TargetLock`/sticky-target.**
|
||||
Retail interleaves debuff-need into priority/tie-break resolution
|
||||
(`dz.cs:740-824`) with `TargetLock` and the sticky-last-target as
|
||||
*low*-priority tiebreaks near the bottom of the chain; MossTank
|
||||
(`CombatController.cs:1707-1798`) gives `TargetLock` and the
|
||||
sticky-last-target unconditional first refusal within the top
|
||||
priority tier and has no debuff-need or debuff-urgency signal at
|
||||
all. Effect: MossTank can get "stuck" defending a locked/sticky
|
||||
target far more rigidly than retail, and never re-prioritizes a
|
||||
same-priority target that urgently needs a re-debuff.
|
||||
3. **Arc vs. Bolt is chosen by `UseArcs`/range before spell quality,
|
||||
not after.** Retail always prefers the higher-`Quality` known spell
|
||||
and only falls back to the `UseArcs` rule on an exact tie
|
||||
(`hi.cs:501-540`); MossTank's `Preference()` buckets by
|
||||
`UseArcs`/`ArcRange` first and only uses Tier/Difficulty to break
|
||||
ties inside a bucket (`AttackSpellCatalog.cs:140-204`). Effect: a
|
||||
character who knows a much stronger bolt (or arc) than their
|
||||
counterpart shape will still be forced into the weaker one whenever
|
||||
the `UseArcs`/range rule says so.
|
||||
4. **Unknown-monster auto-damage falls back to a guessed element
|
||||
order instead of no auto-element.** Retail returns an empty
|
||||
preference list when a monster is in neither `MonsterDamageOverrides`
|
||||
nor `SpeciesDamages` (`e0.cs:327-349`), meaning no auto-Vuln is cast
|
||||
for it; MossTank falls back to a fixed
|
||||
`Pierce > Bludgeon > Slash > Acid > Electric > Cold > Fire` guess
|
||||
(`VtankDamageDatabase.cs:12-21`). Effect: only matters for monsters
|
||||
missing from MossTank's bundled tables, but produces a
|
||||
confidently-wrong element choice rather than retail's "skip it"
|
||||
behavior.
|
||||
5. **No wield-match (weapon/offhand-thrash avoidance) tiebreak.**
|
||||
Retail actively avoids re-wielding between two nearby same-priority
|
||||
targets (`dz.cs:794-824`); no equivalent logic was found in
|
||||
`CombatController.cs`. Effect: minor DPS/time loss from unnecessary
|
||||
re-wields when several adjacent monsters need different weapons,
|
||||
lower impact than 1–3 above.
|
||||
|
||||
`AutoAttackPower.cs` (melee power table) and `MonsterRules.cs` /
|
||||
`MonsterExpression.cs` (rule expression grammar) were both checked in
|
||||
detail and found to be faithful, well-cited ports with no material gap.
|
||||
|
||||
## 9. Could not determine
|
||||
|
||||
- Exact numeric default values for distance-shaped settings
|
||||
(`AttackDistance`, `RingDistance`, `ArcRange`,
|
||||
`TargetSelectAngleRange`, etc.) in
|
||||
`uTank2.Resources.defaultsettings.usd` — the file appears to hold at
|
||||
least two differently-typed tables under the same setting names
|
||||
(one using small `0.02083...`-style doubles, a second using plain
|
||||
integers like `16`/`48`), and the exact `y`/`bd` deserialization
|
||||
schema needed to tell them apart was not available in this pass. The
|
||||
*names* and *semantic meaning* of every setting cited above are
|
||||
independently confirmed via call sites, not via this file.
|
||||
- `eSecondaryEquipTypeOrObjectID`'s enum members (offhand selection
|
||||
modes) — only its column position and type name were recovered.
|
||||
- The `l` enum's four case names (`l.a/b/c/d`, launcher/projectile
|
||||
shape attached to debuff items and ammo checks) — used but never
|
||||
declared in the files read.
|
||||
- `CombatState`'s enum declaration and full name — only inferred
|
||||
from the numeric casts `(CombatState)2/4/8` at every call site
|
||||
(`ga.cs:1596-1615` and callers); no explicit `enum CombatState { ... }`
|
||||
was found in the files searched.
|
||||
- `ga.a`'s exact per-field semantics beyond what call sites imply
|
||||
(`j`, `e`, `g`, `l`, `m` — weapon-type flag, slash/pierce-hybrid
|
||||
flag, triple-slash flag, an unidentified "l" counter used for
|
||||
critical-hit chat correlation, and an unidentified "m" stat used as
|
||||
a pet tiebreak).
|
||||
- `bm.a()` and the `-32555` cooldown check gating the `h1` SummonPet
|
||||
rule — not traced beyond their call site.
|
||||
- The four cast-result regex families referenced as `l.g.a/b/c/d` in
|
||||
`gj.cs` — their defining file was not located in this pass.
|
||||
- Whether MossTank has any pet-selection logic at all;
|
||||
`CombatController.cs` (2059 lines) was searched for combat-loop,
|
||||
target-selection, and attack-execution code but not exhaustively for
|
||||
a pet feature.
|
||||
- `VtankAmmunitionDatabase.cs` and the full `ga.a`/`ga.b`
|
||||
ammo-availability pair (`ga.cs` beyond line ~1245) were not compared
|
||||
against retail in detail.
|
||||
Loading…
Add table
Add a link
Reference in a new issue