CombatSettings.Rules lives only in MossTank's JSON side-car; VtankSettingsProfileSerializer preserves the real .usd MyMonsters table byte-for-byte but never parses it into MonsterRules or regenerates it from them (VtankSettingsProfileSerializer.cs:24-30). MonsterRules.cs/ MonsterExpression.cs (the rule-grammar evaluator itself) is a faithful, well-cited port with no material gap — only the real-file round trip is missing. Filed TS-86 (temporary stopgap; slice 3 ports the table) and added it as gap item 6 in docs/research/vtank-kb/03-combat.md section 8 (previously absent — the existing five gaps are about rule-grammar/priority fidelity, not about whether the real table round-trips at all). Corrected the TS section header's stale active-row count (was undercounting by one before this row) to the actual count. Documentation-only; the parse is deliberately NOT implemented this round, per the task's explicit scope. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
34 KiB
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):
- No cached creature-info record for the guid (
CreatureInfoMissing). - The creature-info record is itself invalid, i.e.
hj.a()true (CIInvalid). - Its matched Monster Rule priority is negative (
NegativePriority— see §3). - Distance from the player exceeds the scan's max (
DistanceTooFar— normallyAttackDistance, but the pet-pick and "target lock follow" call sites pass their own max,dz.cs:952,971,1092-1093). - Distance is below the scan's min (
DistanceTooNear— normallyAttackMinimumDistance). - It needs a debuff but the monster's rule forbids attacking without one first (
DebuffPassWithNoAttack— the two-argdz.a(int,f7)/dz.a(int,bool,d1.a)gate atdz.cs:561-607; only reached when the rule's own "needs a specific debuff and it's not up" test atdz.cs:561-585is 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:
- Priority (the matched Monster Rule's priority, §3) always wins
first — unless
DebuffEachFirstis"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). - Within a priority tie: if
DebuffEachFirstis"All"or"Priority", a candidate needing a debuff outright beats one that doesn't (dz.cs:770-781,flaggate).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. - Debuff-urgency score (
f7.g, fromdz.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 ofDebuffEachFirst, and only checks these two/three specific debuffs, not the full monster-rule debuff set. - Target lock (
f7.j): if one candidate is the in-game selection underTargetLockand the other isn't, it wins (dz.cs:788-793). - Wield-match (only when both candidates are within
TargetSelectAngleRangedistance 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). - Sticky last target (
f7.l, guid equals the previously-attacked guidga.e): prefer the target already being attacked over a new one (dz.cs:825-830). - 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. DebuffEachFirst IS implemented as a debuff scope filter
(CombatController.DebuffScope(), CombatController.cs:1618-1658) but is
never consulted during target selection; only the debuff-urgency score
(f7.g) has no equivalent anywhere (citation pass 2026-09-06). 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:1766-1767 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; members Auto, AutoShield, AutoWeapon, None, LISTEDTYPES_END (uTank2/eSecondaryEquipTypeOrObjectID.cs:3-10) |
| 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-304precedence table,cl.cs:306-446evaluator). - Built-in identifiers, each a function of the candidate monster
(
cl.cs:87-182):true,false,name,typeid(PropertyIntbc.cp),species(species-table name viadz.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 six-value enum l (l.a–l.f,
declared at l.cs:1-8) attached to debuff items and spells (dz.cs:250,264,296;
f7.cs:122-146); the member names are obfuscated, so the semantic mapping
is unrecovered (§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 withinRingDistance" —dz.cs:735-739) meetsMinimumRingTargets, 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-Qualityspell wins outright —UseArcsis consulted only when both spells tie in Quality, where1= prefer bolt,2= prefer arc only iff7.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):
- Magic Yield Other I (
hi.cs:123-129) - Weakening Curse I (
130-136) - Festering Curse I (
137-143) - Corruption I (
144-150, zero-tolerance) - Destructive Curse I (
151-157, zero-tolerance) - Corrosion I (
158-164, zero-tolerance) - Imperil Other I (
165-171) - Vuln matching the current attack element (
172-178) - Vuln matching the rule's
SecondaryVuln/"Ex. Vuln" column, viaf7.h(179-185) - Gravity Well (
186-192) - Broadside of a Barn (
193-199) - 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):
DebuffScheduler.cs (class DebuffSpellCatalog)'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
(DebuffScheduler.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):
- Pick a scan range:
PetCustomRangeifPetRangeMode == 1, elseAttackDistance(ga.cs:1081-1085). - Build
f7candidates for every monster in range (bypassing the min-distance/target-lock/priority-reject gates used for normal attack scanning — only the base validity checkf7.ois used); among those whose matched Monster Rule has aPetDamageTypeother thanNone, track the count and the single best one under a combined "nearer distance AND higher rule priority" predicate (ga.cs:1090-1110). - Refuse to summon (return 0) if no eligible monster exists, or if
the eligible count is below
PetMonsterDensity(ga.cs:1111-1118). - 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'sPetDamageTypescores 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 highera12.mstat (not identified further) (ga.cs:1119-1206). - 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
exists — PetAutomation.cs (PetAutomationChoice: device/target/element pick)
and PetDeviceCatalog.cs — but was not compared step-by-step against
ga.j(); that comparison is owed (§9).
8. MossTank gap ranking (highest player impact first)
- Debuff-kind ordering and selection model is structurally
different, not just re-ordered.
DebuffSpellCatalog.OrderedFlags(DebuffScheduler.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 (DebuffScheduler.cs:93-118vs.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. - Target selection omits
DebuffEachFirstand the debuff-urgency score entirely, and inverts the priority ofTargetLock/sticky-target. Retail interleaves debuff-need into priority/tie-break resolution (dz.cs:740-824) withTargetLockand the sticky-last-target as low-priority tiebreaks near the bottom of the chain; MossTank (CombatController.cs:1707-1798) givesTargetLockand the sticky-last-target unconditional first refusal within the top priority tier;DebuffEachFirstexists only as a scope filter (DebuffScope(), :1618-1658) and is never consulted here, and there is no 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. - Arc vs. Bolt is chosen by
UseArcs/range before spell quality, not after. Retail always prefers the higher-Qualityknown spell and only falls back to theUseArcsrule on an exact tie (hi.cs:501-540); MossTank'sPreference()buckets byUseArcs/ArcRangefirst 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 theUseArcs/range rule says so. - 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
MonsterDamageOverridesnorSpeciesDamages(e0.cs:327-349), meaning no auto-Vuln is cast for it; MossTank falls back to a fixedPierce > Bludgeon > Slash > Acid > Electric > Cold > Fireguess (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. - 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 inCombatController.cs. Effect: minor DPS/time loss from unnecessary re-wields when several adjacent monsters need different weapons, lower impact than 1–3 above. - The real
.usdMyMonsterstable is preserved but never read or written — a persistence gap, not a rule-grammar gap. Round 3 item 11:CombatSettings.Rules(the liveMonsterRulelistMonsterRules.cs/MonsterExpression.csevaluate against, item 5's own faithful port) lives ONLY in MossTank's JSON side-car (MossTankProfileStore.SideCarDocument.CombatRules); the real 21-columnMyMonsterstableVtankSettingsProfileSerializerround-trips inside the.usdfile (VtankSettingsProfileSerializer.cs:24-30: "every table other than Settings … is preserved byte-for-byte") is never parsed intoMonsterRules on load and never regenerated from them on save. Effect: a drop-in.usdfrom real VTank (or a hand-edited one) keeps itsMyMonstersrows completely inert in acdream — MossTank always uses whatever the side-car separately holds instead, and a real VTank opening an acdream-saved.usdwould see stale/absentMyMonstersrows regardless of what MossTank's own rule editor currently shows. Deferred to a future slice (slice 3) that ports the table; NOT implemented this round (seedocs/architecture/retail-divergence-register.md).
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.) inuTank2.Resources.defaultsettings.usd— the file appears to hold at least two differently-typed tables under the same setting names (one using small0.02083...-style doubles, a second using plain integers like16/48), and the exacty/bddeserialization 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. - The
lenum (l.cs:1-8, six membersa–f) is declared but obfuscated; which member is which launcher/projectile shape is unrecovered. CombatState's enum declaration and full name — only inferred from the numeric casts(CombatState)2/4/8at every call site (ga.cs:1596-1615and callers); no explicitenum 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-32555cooldown check gating theh1SummonPet rule — not traced beyond their call site.- The four cast-result regex families referenced as
l.g.a/b/c/dingj.csare theMyList<Regex> a/b/c/dfields ofd3.cs:5-13(the only class holding four regex lists); thelholder itself was not located, so the type link is strong but not proven. - The step-by-step comparison of MossTank
sPetAutomation.csagainst retailsga.j()(§7) is owed. VtankAmmunitionDatabase.csand the fullga.a/ga.bammo-availability pair (ga.csbeyond line ~1245) were not compared against retail in detail.