acdream/docs/research/vtank-kb/03-combat.md
Erik 55010f92c7 docs(vt): round 3 item 11 — record the MyMonsters persistence gap
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>
2026-09-07 02:00:19 +02:00

34 KiB
Raw Blame History

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 03 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 23 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-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 six-value enum l (l.al.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 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 outrightUseArcs 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 12. "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):

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

  1. 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-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; DebuffEachFirst exists 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.
  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 13 above.
  6. The real .usd MyMonsters table is preserved but never read or written — a persistence gap, not a rule-grammar gap. Round 3 item 11: CombatSettings.Rules (the live MonsterRule list MonsterRules.cs/ MonsterExpression.cs evaluate against, item 5's own faithful port) lives ONLY in MossTank's JSON side-car (MossTankProfileStore.SideCarDocument.CombatRules); the real 21-column MyMonsters table VtankSettingsProfileSerializer round-trips inside the .usd file (VtankSettingsProfileSerializer.cs:24-30: "every table other than Settings … is preserved byte-for-byte") is never parsed into MonsterRules on load and never regenerated from them on save. Effect: a drop-in .usd from real VTank (or a hand-edited one) keeps its MyMonsters rows completely inert in acdream — MossTank always uses whatever the side-car separately holds instead, and a real VTank opening an acdream-saved .usd would see stale/absent MyMonsters rows 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 (see docs/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.) 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.
  • The l enum (l.cs:1-8, six members af) 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/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 are the MyList<Regex> a/b/c/d fields of d3.cs:5-13 (the only class holding four regex lists); the l holder itself was not located, so the type link is strong but not proven.
  • The step-by-step comparison of MossTanks PetAutomation.cs against retails ga.j() (§7) is owed.
  • VtankAmmunitionDatabase.cs and the full ga.a/ga.b ammo-availability pair (ga.cs beyond line ~1245) were not compared against retail in detail.