docs(vt): VT1 catalog 04 — VTank buffs, item enchantments, helpers and vitals

Buff plan and per-tick re-evaluation, the profile-item enchant pipeline
(PluginCore.cs:8327-8445), helper heals/random helper, the nine recharge
thresholds and rule order, kits/potions/food, dispel and worn-item mana.
MossTank gap ranked. Lead spot-check: fk.b() dead check, ba.cs 100-draw
loop, MySpell.HasScarabsInInventory.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-06 19:44:02 +02:00
parent cf6d5b94bb
commit 289c6706ba

View file

@ -0,0 +1,719 @@
# VTank knowledge base 04 — buffs and vitals
Research only. Oracle: `refs/vtank/decompiled/` (ILSpy decompile of
`utank2-i.dll`; obfuscated identifiers, intact strings/settings/spell
names). Builds on
`refs/vtank/notes/2026-09-06-idlepeace-fcm-trace.md` (rule-engine order,
`ForceCombatMode`, wand/caster selection) — that note's pins are not
re-derived here, only extended. All paths below are relative to
`refs/vtank/decompiled/` unless marked `acdream:`, which is relative to
the repo root. Identifiers are obfuscated single/double letters; this
doc never pastes decompiled source, only short identifiers and the
string literals VTank ships (spell/item/setting names survive
obfuscation intact).
## 0. The moving parts, one paragraph each
- **`fz.cs`** — the `BuffSelf` rule (`ILogicRule`). Registered twice in
`cLogic.cs`: once as the ordinary rebuff rule (threshold setting
`RebuffTimeRemainingSeconds`, considers consumable helper items) and
once as the idle top-off rule (threshold `IdleBuffTopoffTimeSeconds`,
does not consider consumables), see §1.2.
- **`eq.cs`** (`PluginCore.dz.k`, field `s.k` at `s.cs:29`) — the buff
*engine*: builds the self-buff candidate list (`b()`), the
item-enchantment candidate list (`g()`), runs the "is anything due"
scan (`a(out itemId, out consumableItemId)`), and owns the profile
store `m_e : MyList<c>` where `c(int itemId, int spellId)` is one
profile row (ctor `eq.cs:31-35` maps first arg → field `b` = item id,
second arg → field `a` = spell id; `-1` spell id = "no buffs" row).
- **`ga.cs`** — `ForceCombatMode` (wand/shield/caster selection,
already pinned in the trace note) plus the class→`fs` item-type
table and the `fi` object-class-9 sub-tags used by the wand/shield
eligibility checks.
- **`cr.cs`** (`RechargeSelf2`) / **`cg.cs`** — the vitals *trigger*:
compares current Health/Stam/Mana percent against the three
`Recharge-*-*` settings and calls `cRechargeManager.Recharge(vital)`.
- **`cRechargeManager.cs`** — the vitals *dispatch table*:
`RechargeHandlerSet` (a settings TABLE), tried in list order per
(vital, stance) until one's `Activate()` succeeds.
- **`m.cs`, `g4.cs`, `a5.cs`, `dg.cs`** (+ `bn`/`hc`/`c2`/`f2` :
`dg`) — the seven `IRechargeHandler`s: Regular Spell, Recharge With
Food, Kit Recharge, and the four vital-transfer spells.
- **`he.cs`** — shared recharge-cast dispatch: arms the
`RechargeLevelBoost_*` action lock (via `RechargeBoostTimeSeconds`)
when not yet in Magic mode, casts once actually in Magic mode.
Estimates a transfer spell's HP yield by name suffix (§4.4).
- **`ai.cs`** (`PluginCore.dz.l`) — helper-heal target picker: tracks
fellows (server broadcast `0xF7B1`/event 704) and non-fellows
(client/server echo dispatch), casts the fixed Adja's
Gift/Replenish/Gift of Essence family.
- **`gu.cs`** (`RechargeOther`), **`fb.cs`** (`UseHealersHeart`),
**`af.cs`** (`DispelAllies`), **`ba.cs`** (`RandomHelper`) — the
four helper/ally logic rules.
- **`c8.cs`** (`DispelSelf`), **`cx.cs`** (`UseDispelItem`) — self
dispel.
- **`MySpell.HasScarabsInInventory`** (`uTank2/MySpell.cs:398-456`) —
the real blacklisted-component gate.
- **`e7.cs`** / **`hz.cs`** — the two "Choose Extra Buffs" pickers
(same XML, different title/target list): e7 adds to the whitelist
`dz.m.i`, hz adds to the blacklist `dz.m.j`.
acdream counterparts, all under `src/AcDream.Plugins.MossTank/`:
`BuffPlan.cs`, `BuffProfile.cs`, `VitalRecharge.cs`,
`DispelController.cs`, `ItemManaRecharge.cs`, `SpellComponentPolicy.cs`,
and the driver in `MossTankPanel.cs` (`StartBuffPass`, `BuildPlan`,
`TryCast`, `TickAutomaticBuffing`, `TickRandomHelper`).
## 1. Buff plan
### 1.1 Candidate list (`eq.b()`, `eq.cs:134-236`)
Retail's self-buff candidate list is a single **hardcoded, ordered**
sequence, not a generic "everything self-targeted" scan:
| Order | Candidate(s) | Cite |
|---|---|---|
| 1 | `Creature Enchantment Mastery Self I` | `eq.cs:143` |
| 2 | `Focus Self I` | `eq.cs:144` |
| 3 | `Willpower Self I` | `eq.cs:145` |
| 4 | `Mana Conversion Mastery Self I` | `eq.cs:146` |
| 5 | `Life Magic Mastery Self I` | `eq.cs:147` |
| 6 | one self-buff per **trained skill** in `PluginCore.dz.q.x` (skill→spell-name map), only for skill classes 2 or 3 (i.e. an actual skill, not an attribute); unknown skill → warns "Unknown skill detected!" and drops it | `eq.cs:96-110,148-151` |
| 7 | `Strength/Endurance/Coordination/Quickness/Regeneration/Rejuvenation/Mana Renewal Self I` | `eq.cs:152-158` |
| 8 | `Impenetrability I` (physical bane, i.e. Armor Self's item-enchant sibling) | `eq.cs:159` |
| 9 | the elemental **prots** named by `BuffProfile-Prots`/`BuffProfile_Prots` (letter string `ALFCBPS`, or a numeric legacy mode) | `eq.cs:160-165` |
| 10 | the user's **extra self buffs** list (`PluginCore.dz.m.i`, populated by the `e7.cs` picker) | `eq.cs:166-169` |
| 11 | any **untargeted** profile row (`eq.m_e`, item id -1 entries with a spell whose `isUntargeted` is true) | `eq.cs:170-181` |
Then two filters remove entries whose *family* is already covered by
something in the **blacklist** (`PluginCore.dz.m.j`, populated by
`hz.cs`, §1.7) — `eq.cs:182-197` — and two more strip whole
**schools** once the character exceeds `BuffWithUntrained-Item` /
`-Creature` / `-Life` level while untrained in that school
(`f9.c(eGameSkillID.*)` = trained check) — `eq.cs:198-234`.
`eq.g()` (`eq.cs:238-261`) is the **item-enchantment** candidate list:
`Impenetrability I` targeted at self first, then the profile's
**banes** (`BuffProfile-Banes`/`BuffProfile_Banes`, same letter/legacy
scheme), then every **targeted** profile row (item id ≠ irrelevant,
spell not untargeted). It is gated the same way on
`BuffWithUntrained-Item` (`eq.cs:242-245`) — it returns an **empty**
list outright above that level while untrained, rather than filtering
per-row.
**acdream (`acdream:src/AcDream.Plugins.MossTank/BuffProfile.cs`)**
takes a structurally different, description-driven approach: it groups
every known self-buff by spell family and classifies each family from
retail's own spell **description** text (regexes for "Increases the
caster's X", "Reduces damage… from X", the aura sentence, the bane
sentence, the three regen-rate sentences —
`BuffProfile.cs:79-142,204-291`), rather than a hardcoded name list.
This generalizes past retail's fixed candidate set (any self-buff the
character knows gets classified and offered, not just the ~20 hardcoded
lines), and is documented as deliberate (`BuffProfile.cs:47-76`). It
does not reproduce retail's list-*order* semantics directly; instead
`acdream:BuffPlan.cs:337-383` (`CastRank`) encodes school-then-family
ordering (Creature → Item → Life, with Creature sub-order
mastery→Focus→Self→Endurance→rest, Life sub-order
prots/other→regeneration-last) as a *documented reconstruction* of why
retail's literal list is ordered the way it is (§1.6 below covers a gap
in that reconstruction).
### 1.2 The "due" test
`fz.a()` (`ILogicRule.ValidNow`, `fz.cs:70-90`):
1. `false` if the `ItemUse` action lock is held (`fz.cs:72-75`).
2. `false` if `EnableBuffing` is off (`fz.cs:76-79`).
3. `num = f3.j(m_c)` — the configured threshold in **seconds**
(`RebuffTimeRemainingSeconds` = 300 for the main rule,
`IdleBuffTopoffTimeSeconds` = 1200 for the idle rule;
`cLogic.cs:472,553`).
4. If the `BuffCastRecast` lock is held, `num += BuffCastRecast_Seconds`
(default 30) — i.e. right after a successful cast the *effective*
due-threshold is widened by 30s for the reset window
(`fz.cs:80-84`, and see §1.3).
5. Floor `num` at 10 seconds (`fz.cs:85-88`).
6. Delegates to `eq.a(num, m_d, out m_e)``m_d` is the ctor's
"consider consumables" flag: `true` for the main rule, `false` for
idle top-off (`fz.cs:21-27`, `cLogic.cs:472,553`).
`eq.a(int seconds, bool considerConsumables, out bool isConsumable)`
(`eq.cs:573-582`) sets `this.b = seconds`, `this.c = considerConsumables`,
then calls the private `a(out itemId, out consumableItemId)`
(`eq.cs:477-565`) which walks the candidate lists in the exact order of
§1.1 and returns the **first** spell that is either: not currently
tracked as active at ≥ the candidate's quality with ≥ `seconds`
remaining (self-buffs, `eq.cs:481-503`; the "remaining" bookkeeping is
`eq.i()`/`eq.b(ActiveSpellInfo)`/`eq.a(ActiveSpellInfo)` reacting to
the client's tracked active-spell events, `eq.cs:362-475`), or a
targeted item-enchant row whose remaining time (`eq.j.b(itemId,
spell).TotalSeconds`) is under `seconds` (`eq.cs:510-527`), or — only
when `considerConsumables` is true and `ItemUse` is free — a known
**consumable buff item** (`PluginCore.dz.m.g`, populated at
identify-time, `PluginCore.cs:7524-7534,7628-7654`) not on cooldown
and not a fellowship spell while out of fellowship (`eq.cs:528-561`;
sets `m_h = true` to signal "this is a consumable", `eq.cs:554`).
**acdream** (`acdream:BuffPlan.cs:104-213`) is a *snapshot* planner:
`Build()` reads skills/attributes/active enchantments **once**, decides
every wanted line, picks a tier, and returns the whole ordered queue.
`MossTankPanel.StartBuffPass` calls `BuildPlan` (→ `BuffPlan.Build`)
exactly once per pass (`acdream:MossTankPanel.cs:3607-3656,3679-3698`);
`TryCast` (`acdream:MossTankPanel.cs:4265-4303`) then walks the fixed
queue index-by-index and never re-invokes `BuildPlan` mid-pass. Retail
re-evaluates `eq.a()` **every logic tick** (~293 ms,
`uTank2/cLogic.cs:145`) against freshly-read server skill/quality data
— see §5 gap G1.
### 1.3 The `BuffCastRecast` lock, and how a pass "ends"
`fz.a(bool running)` (`ILogicRule.set_Running`, `fz.cs:111-122`) fires
once per successful pick: if the picked spell was a consumable
(`m_e`) **or** the character is already in (or can reach) Magic mode
(`ga.a((CombatState)8, 0, anyItem:true)` — a `ForceCombatMode`
probe/no-op call, not a real state change here), it arms the
`BuffCastRecast` action lock for `BuffCastRecastReset_Seconds` (default
30) and calls `eq.a()` (`eq.cs:584-619`) — which re-derives the pick
and either dispatches the spell cast (`this.m_a.h.a(spell, itemId)`) or
uses the consumable item (`f9.p(consumableItemId)` plus an `ItemUse`
lock of `PluginCore.dz.o.k`, `eq.cs:588-618`).
There is **no explicit "pass" object** in retail: `BuffSelf` is a
single `ILogicRule` that the engine re-evaluates every tick; a "pass"
is just "the rule keeps being the first `ValidNow` rule in the list
until nothing is due any more" (first-match-wins per the trace note).
Nothing marks completion; the rule simply stops returning `true` from
`ValidNow` once every candidate in the ordered list is covered or
exhausted.
**acdream** has an explicit pass object (`_queue`/`_queueIndex`,
`acdream:MossTankPanel.cs:3607-3656`) with its own progress/stall
tracking (`_castThisPass`, `_sinceProgress`) not present in retail —
a deliberate modernization, not a port; see §5.
### 1.4 Force Buff / Cancel Force Buff
`PluginCore.ForceBuff()``dz.k.i()` (`eq.cs:362-372`): for **every**
tracked active-spell entry, sets its "not due again until" timestamp
`a` to `DateTimeOffset.Now`. Since the due test compares
`(entry.a - Now).TotalSeconds >= threshold`, zeroing `a` makes every
currently active buff look like it is about to expire *right now*
so the very next `eq.a()` call treats everything as due, without
touching the real `ExpireTime` (`d`). `CancelForceBuff()``dz.k.e()`
(`eq.cs:374-384`) restores `a = d` for every entry, i.e. undoes the
force by putting the real remaining time back. Both call into
`this.m_a.j.d()`/`.h()` (the buff-list UI refresh) afterward
(`eq.cs:371,383`; `PluginCore.cs:230-239`).
**acdream**: `BuffPlan.Build(force:true, …)` (`acdream:BuffPlan.cs:104,
185-192`) skips the "already covered" `continue` outright when
`force` is set, which is functionally equivalent (recast everything at
best tier regardless of remaining duration) but is a **different
mechanism** — a build-time bypass of one `if`, not a timestamp-zeroing
trick against a live tracked-duration table. No functional gap
observed; noted for completeness since the two codebases reach the
same visible behavior by different means.
### 1.5 Fizzle / failure handling
Not modeled as retry logic anywhere in the traced files: `fz`/`eq`
issue one cast attempt (`m_a.h.a(spell, target)`, field `s.h : gj`)
and rely on the **next tick's** fresh `eq.a()` re-evaluation to notice
the spell is still not active and try again — there is no dedicated
"cast failed, retry N times" state machine in this family (contrast
with the raw drop-to-peace retry counters the trace note found
elsewhere, e.g. `ga.r`). `gj.cs:211/226` (`o.h()`) and `gj.cs:198`
(`o.e()`) are the cast-in-progress suspend counters already pinned in
the trace note; while `ga.f()` (casting-in-progress) is true the whole
logic engine bails (`cLogic.cs:214`), so a fizzle simply means the
suspend clears, the engine resumes, and `eq.a()` runs again from the
top of its candidate list.
### 1.6 Blacklisted components — the *real* mechanism
The obvious-looking gate inside `fk.c()` (`fk.cs:198-236`,
"`Fail_HasBannedComponent`") calls `b(MySpell)` — but `fk.b(MySpell)`
(`fk.cs:95-102`) is:
```
if (A_0 == null || !A_0.isValid) return false;
return false;
```
i.e. it **always** returns `false` regardless of input — the
`HasBannedComponent` diagnostic branch is dead code in this build.
The real blacklist is `MySpell.HasScarabsInInventory`
(`uTank2/MySpell.cs:398-456`): it parses `BlacklistedSpellComps` into a
name dictionary `d` (`MySpell.cs:401-410`), then for each of the
spell's own **scarab-class** components (`d10.e.Contains("Scarab")`,
`MySpell.cs:432`) — if that scarab's name is in the blacklist,
**disable the spell entirely** (`MySpell.cs:443-446`); otherwise if
the character doesn't have enough of that scarab in inventory, disable
it too and warn once (`MySpell.cs:447-452`). This gate is read by
`fk.c()`'s "can I currently cast this" check (`fk.cs:219,189`), so it
applies to **every** spell cast attempt system-wide, not just buffs.
**acdream** (`acdream:SpellComponentPolicy.cs`) checks **all** of a
spell's `FormulaComponentIds` (id or name match) against the setting
— broader in principle than retail's scarab-only filter, but in this
game era all physical spell components *are* scarabs/talismans, so
the practical difference should be negligible; flagged as
"could not fully confirm" in §6.
### 1.7 Extra buffs / blacklist pickers (`e7.cs` / `hz.cs`)
Both use the identical `SelfBuffChoiceView.xml` (list + search box,
`refs/vtank/decompiled/uTank2.ViewXML.SelfBuffChoiceView.xml:1-11`),
built from the same candidate set — every known spell, deduplicated by
family, that is `isUntargeted && !isOffensive && Duration >= 1s`
(`e7.cs:56-76`, `hz.cs:56-76`, identical). Clicking a row: `e7`
(title unset, "Choose Extra Buffs") adds the spell id to
`PluginCore.dz.m.i` — the **whitelist** consumed at `eq.b()` item 10
above (`e7.cs:123-127`); `hz` (title "Choose Blacklisted Buffs") adds
to `PluginCore.dz.m.j` — the **blacklist** consumed by family-removal
at `eq.cs:182-197` (`hz.cs:123-127`).
### 1.8 Fast-cast / movement trick
`MySpell.IsInstantCast` (`uTank2/MySpell.cs:104-129`): `Difficulty <
50` → true; else `isUntargeted && !isFellowship &&
EffectiveDurationMS >= 60000 && School.Id in {2 (LifeMagic), 4
(CreatureEnchantment)}` → true; else `RealFamily in
{243..249, 639}` → true. Gated by setting `FastCastBuffs`
("Attempt to fastcast buff spells. May move the character during
casting.", `uTank2.Resources.defaultsettings.usd:1139-1143`) at
`gj.cs:171,233,237` — movement is only attempted for non-War/non-Void
schools while an instant-cast spell is in flight.
**acdream** (`acdream:MossTankPanel.cs:4348-4360`,
`IsVtankInstantCast`) reproduces this **exactly**: same
difficulty<50 branch, same untargeted/non-fellowship/60s/school-{31
(Creature),33 (Life)} branch (AC skill ids 31/33 = VTank's internal
School.Id 4/2, consistent with the `fk.cs` school-id map), same family
range `243..249,639`. This is a faithful 1:1 port — no gap.
## 2. Item enchantments
`PluginCore.l(int itemId)` (`uTank2/PluginCore.cs:8327-8445`) is what
runs when a wand/shield/weapon/pet is **added to the profile** (the
"Add" / "Add (no buffs)" buttons, `PluginCore.cs:1625`,
`eBuffedItemAddOptions` enum at `PluginCore.cs:613-617`). It reads the
item's `LongValueKey 218103822` mastery-mask value and auto-populates
default enchant rows:
| `LongValueKey 218103822` value | Auto-added spells (StandardBuffs) | "No buffs" fallback | Cite |
|---|---|---|---|
| `1048576` or `33554432` | Aura of Defender, Blood Drinker, Swift Killer, Heart Seeker Self I | `eq.c(itemId, -1)` | `PluginCore.cs:8355-8375` |
| `4194304` (excluded if `dz.o.c(itemId) == fi.h`) | Aura of Defender, Blood Drinker, Swift Killer Self I | `eq.c(itemId, -1)` | `PluginCore.cs:8377-8397` |
| `16777216` | Aura of Defender, Hermetic Link, Spirit Drinker Self I | `eq.c(itemId, -1)` | `PluginCore.cs:8399-8419` |
| `2097152` (shield) | all 7 elemental prots (`eDamageElement 0..6`) + Physical (`Armor Self I`) | `eq.c(itemId, -1)` | `PluginCore.cs:8421-8438` |
A **pet** item (detected via `LongValueKey 36` presence,
`PluginCore.cs:8332-8334`, or `ga.e(WorldObject)`) is always added as
`eq.c(itemId, -1)` — no-buffs — with a mastery-mismatch chat warning if
`ga.f(fu)` fails (`PluginCore.cs:8340-8348`). Only items whose
`LongValueKey 218103822` is one of the five values above, or a pet,
pass the "Add" button's eligibility filter (`PluginCore.l()`'s caller
`PluginCore.b()`, `PluginCore.cs:8448-8477`).
`eq.g()` (§1.1) is the runtime consumer: `Impenetrability I` self-cast
first, then the profile's configured **banes**
(`BuffProfile-Banes`/`_Banes`), then every profile row whose item id
still exists in inventory (`f9.d(itemId)`) and whose spell id is not
`-1` (`eq.cs:510-527`) — a `-1`-spell row (from either the "no buffs"
button or a pet) never contributes a targeted cast; a warning fires if
the item itself has left inventory (`eq.cs:512-515`).
Wielding: item-enchant casting always targets the item itself
(`d(itemId)`, `eq.cs:353-360`, which substitutes the currently-wielded
weapon's id, `this.m_a.ax.g()`, whenever the profile row's item id is
`-1`) — there is no separate "must be wielded first" gate inside `eq`;
wielding for combat is handled by `ForceCombatMode` (§(c) of the trace
note) before any attack, independently of the buff pass.
**acdream**: no equivalent "auto-populate default enchant candidates
from a weapon's mastery mask when it's added to the profile" pipeline
was found among `BuffPlan.cs`/`BuffProfile.cs`/`MossTankPanel.cs`.
`BuffProfile.Build` only classifies the character's own **known
spellbook** self-buffs (attributes/skills/prots/regen/self-auras via
description parsing); it has no notion of a *profile item* or its
`LongValueKey 218103822` mastery mask at all. This is flagged as gap
G2 in §5 — either the mastery-driven default-enchant flow lives
somewhere outside the six files this doc was scoped to (a companion
"item profile" module not yet located), or it genuinely has no acdream
counterpart yet.
## 3. Helper buffs
### 3.1 `RandomHelper` (`ba.cs`)
Gate (`ba.a()`, `ba.cs:83-126`): `RandomHelperBuffs` on, `ItemUse` lock
free, `RandomHelperBuffLock` action lock free (armed for
`RandomHelperIntervalSeconds`, default 5s, on fire —
`ba.cs:154`), not mid-cast (`this.m_a.h.e()`). Target: any `Player`
object other than self within `0.075` distance units
(`f9.a(item.k, PluginCore.dn, true) < 0.075`, `ba.cs:105`; using the
trace note's ≈240 m/unit this is **18 m**). Spell: **one random draw**
from a fixed 11-name stem list (Endurance/Regeneration/
Rejuvenation/Armor/Blade·Bludgeoning·Cold·Fire·Lightning·Piercing·Acid
Protection, all " Other I", `ba.cs:29-40`), resolved to the character's
best known tier via `this.m_a.i.a(id, false)` (`fk.a(int,bool)`
`fk.b` → private `fk.a(MySpell,bool)`, which walks the spell's
`RealFamily` for the highest-quality version the character can
currently cast — `fk.cs:130-141,170-196`).
Quoting the loop verbatim in structure (not pasting code): it retries
**up to 100 times**, and **returns `true` as soon as a randomly-picked
stem resolves to no castable spell** (`g == null`) — `ba.cs:116-124`.
Only if every one of 100 random draws happens to resolve to a castable
spell does the loop exhaust and `ValidNow` return `false`
(`ba.cs:125`). `Running(true)` then only actually casts when `g !=
null` (`ba.cs:152`), so a hit on the `g == null` branch makes the rule
fire for that tick and do **nothing** — a plausible retail
quirk/bug (the loop looks like it means to search *for* a castable
spell, but structurally fires early on the *opposite* condition). This
is quoted, not paraphrased away, per project convention on decomp
predicates; flagged again in §5/§6.
**acdream** (`acdream:MossTankPanel.cs:3996-4071`,
`TickRandomHelper`): same distance constant (`< 18d`,
`MossTankPanel.cs:4019-4020`, matching retail's 0.075 unit band), same
11-name stem list verbatim (`MossTankPanel.cs:4026-4033`), same
"resolve to best known tier" (`OrderByDescending(Quality).ThenBy(Tier)`,
`MossTankPanel.cs:4044-4046`), same interval lock semantics
(`_randomHelperRemaining`, floor 0.25s, `MossTankPanel.cs:4062-4064`).
It does **not** reproduce the 100-random-draws/early-`true`-on-miss
structure: instead it round-robins **deterministically** through every
(player × stem) combination from a rolling cursor and only fires on
the first combination that is actually castable
(`MossTankPanel.cs:4034-4068`). This is a clear, intentional
divergence — arguably a fix for what §3.1 quotes as a retail
oddity — not a straight port; recorded here as a finding rather than a
gap since it is not obviously "worse" than retail, only *different*.
### 3.2 `UseHealersHeart` (`fb.cs`)
Narrow item-specific rule: requires `UseHealersHeart` setting,
inventory item named exactly `"The Healer's Heart"` or `"Legendary
Seed of Mornings"` (the latter takes priority if both present,
`fb.cs:85-96`), `Healing` skill (`CharFilterSkillType 33`) buffed ≥
245 and `Arcane Lore` (`CharFilterSkillType 14`) buffed ≥ 105
(`fb.cs:101-108`), and a fellow found under the `Recharge-Helper-HitP`
threshold via `PluginCore.dz.l.a(float)` (`ai.cs:387-395`, §3.4). Casts
by using the item on the target (`PluginCore.dz.aa.a(itemId, targetId)`)
then marks that target's health-vital cooldown (`PluginCore.dz.l.a(target,
CharFilterVitalType.Health)`, `fb.cs:141-145` — though `ai.a(int,
CharFilterVitalType)`, `ai.cs:430-432`, is itself an empty method body
in this build, so the cooldown mark is a no-op here).
**acdream**: `TryHealersHeart` referenced from
`VitalRechargePlanner.TryPlanHelper` (`acdream:VitalRecharge.cs:85-140`,
call at `~line 124-131`) is folded into the general helper-recharge
path rather than kept as a standalone rule, matching the *effect* (item
name gate + skill gate + fellow-under-threshold target) as far as
inspected; full field-by-field parity (the two skill thresholds, the
priority order between the two item names) was not re-verified against
the acdream implementation body in this pass.
### 3.3 `RechargeOther` (`gu.cs`) and the fellow tracker (`ai.cs`)
`gu.a()`/`gu.a(bool)` (`gu.cs:70-130`) build a `gh{a=HitP%, b=Stam%,
c=Mana%}` from `Recharge-Helper-HitP/Stam/Mana` and delegate to
`ai.b(gh)` (any fellow/tracked player below any threshold?) for
`ValidNow`, and `ai.a(gh)` (actually cast) for `Running`. `ai.a()`
private (`ai.cs:246-375`) computes the worst tracked percent across
**two pools**:
- **Fellowship members** (`ai.l`, keyed by id), refreshed by the
server's fellowship-vitals broadcast (message type `63408`, event
`704`, `ai.cs:145-178`) gated on the fellowship window being open
(`this.m_a`, toggled by message `63409` action `166`,
`ai.cs:122-143`) and the character actually being in a fellowship
(`j`, tracked via `ChangeFellowshipEventArgs`, `ai.cs:180-244`).
Entries older than `ai.b` seconds (10.0 default field value) are
ignored (`ai.cs:268`). Distance-gated per vital by
`HelperDistanceHitP/Stam/Mana` (`ai.cs:290,296,302`).
- **Non-fellowship tracked players** (`ai.m`), aged out after 2
**minutes** of no update (`ai.cs:314-322`), entries considered
within `ai.c` seconds (10.0 default), each with its own **per-vital
5-second (`ai.d`) re-cast cooldown** keyed by which vital was last
cast on that target (`ai.cs:354,360,366` checking `a11.i` against
the vital and `timeSpan2 < d`).
Target spell family is **fixed**, not the profile's choice: Health →
`Adja's Gift`, Stamina → `Replenish`, Mana → `Gift of Essence`
(`ai.cs:81-83`), each resolved to the caster's best current-castable
tier via `e.i.c(spell)` (`fk.c`, `ai.cs:456-465`) before casting.
**acdream** (`acdream:VitalRecharge.cs:85-200`ish,
`TryPlanHelper`/`Lowest`/`TryFindFamily`): same three fixed base spell
families (`SpellId.AdjaSGift`, `.Replenish`, `.GiftOfEssence`,
`VitalRecharge.cs:114-118`), same per-vital distance settings
(`HelperHealthDistance` etc.), same fellowship-membership gate
(`settings.HelpOthers && automation.Fellowship.IsInFellowship`,
`VitalRecharge.cs:102-106`). The non-fellowship "track any nearby
player's broadcast vitals and help them too" pool (`ai.m`) was not
located in the acdream implementation inspected here — flagged as a
possible gap (§5 G3) pending a closer read of `Fellowship`/tracked-
player plumbing outside the six named files.
### 3.4 `DispelAllies` (`af.cs`)
Requires setting `UseDispelDrum`, an inventory item named `"Awakener"`
or `"Attenuated Awakener"` (`af.cs:78-85`), and per-item skill gates:
`Awakener` needs `Item Enchantment` (`CharFilterSkillType 31`)
**Specialized** + `Healing` buffed ≥ 110; `Attenuated Awakener` accepts
Trained *or* Specialized Item Enchantment, same Healing floor
(`af.cs:87-105`). Target selection scans `ai.m` (the same tracked-player
pool as §3.3, but requiring an active *fellowship-visible* relation
via `dz.j.b(id, spell) > TimeSpan.Zero` and presence in `dz.j.b`,
`af.cs:112-116`) for the fellow whose **currently active vuln stack**
(all 7 elements' `Vuln` family, difficulty ≤ 350, quality-summed only
above 250 per element) is worst (`af.cs:117-141`), then uses the drum
item on them (`PluginCore.dz.aa.a(itemId, targetId)`).
**acdream**: not investigated beyond confirming
`DispelController.cs:93` gates on `UseDispelDrum` and calls
`TryStartAllyDispel`; the vuln-severity target-scoring logic was not
diffed line-for-line against `af.cs:117-141` in this pass.
## 4. Vitals
### 4.1 The trigger (`cr.cs` / `cg.cs`)
`cr` ("RechargeSelf2") is registered **three times** in `cLogic.cs`
(main position before `BuffSelf`, using `Recharge-Norm-*`; idle
position after target-approach, using `Recharge-NoTarg-*`;
`cLogic.cs:467,558`) — helper recharge (`gu`, §3.3) is the analogous
rule for `Recharge-Helper-*`. Gate: `ItemUse` lock free, then any of
`cg.c(healthSetting) || cg.b(stamSetting) || cg.a(manaSetting)`
(`cr.cs:75-91`) — note the deliberate **Health > Stamina > Mana**
short-circuit priority both in the gate and in dispatch
(`cr.cs:120-133`: Health checked first, Stamina second, Mana is the
`else` fallback). `cg.c/b/a` (`cg.cs:36-97`) each: read the setting as
a percent, compare against `100*current/max` for that vital, subtracting
`RechargeBoostAmount` first if the matching `RechargeLevelBoost_HP/
Stam/Mana` action lock is armed (armed for `RechargeBoostTimeSeconds`
by `he.cs:49,65` whenever a recharge cast/use hasn't yet landed —
§4.4) — i.e. **the boost temporarily lowers the effective reading** so
`cr` keeps re-firing (looking urgent) while a recharge action is still
in flight, rather than a boost that raises urgency intrinsically.
Default thresholds (`uTank2.Resources.defaultsettings.usd:203-267`):
| Setting | Default % |
|---|---|
| `Recharge-Norm-HitP` | 75 |
| `Recharge-Norm-Stam` | 50 |
| `Recharge-Norm-Mana` | 50 |
| `Recharge-NoTarg-HitP` | 1 |
| `Recharge-NoTarg-Stam` | 1 |
| `Recharge-NoTarg-Mana` | 1 |
| `Recharge-Helper-HitP` | 20 |
| `Recharge-Helper-Stam` | 1 |
| `Recharge-Helper-Mana` | 1 |
(`NoTarg` = idle recharge, effectively "only recharge idly once nearly
empty"; `Norm` = the in-combat/normal-stance thresholds, much more
eager.)
### 4.2 The handler-set table (`cRechargeManager.cs`)
Seven registered `IRechargeHandler`s (`cRechargeManager.cs:26-36`):
`m` (Regular Spell), `bn` (Health→Stamina), `hc` (Health→Mana), `c2`
(Mana→Health), `f2` (Stamina→Health), `a5` (Kit Recharge), `g4`
(Recharge With Food). Per (vital, stance) the ordered candidate rows
come from the `RechargeHandlerSet` settings TABLE
(`uTank2.Resources.defaultsettings.usd`, `RechargeHandlerSet` block),
parsed row = `(Vital 1/2/3=HitP/Stam/Mana, HandlerString, MinPercent,
MaxPercent, Stance 1=Other/2=MagicMode)`. Default table (26 rows,
decoded from the `.usd` TABLE literal):
| Vital | Stance | Ordered handlers (name, minmax %) |
|---|---|---|
| Health | Other | Stamina→Health (015), Mana→Health (015), Regular Spell (015), Recharge With Food (015), Kit Recharge (0100), Stamina→Health (0100), Mana→Health (0100), Regular Spell (0100), Recharge With Food (0100) |
| Stamina | Other | Kit Recharge (0100), Regular Spell (0100), Recharge With Food (0100) |
| Mana | Other | Kit Recharge (0100), Recharge With Food (0100), Regular Spell (0100) |
| Health | MagicMode | Recharge With Food (015), Kit Recharge (0100), Recharge With Food (0100), Stamina→Health (010), Regular Spell (0100) |
| Stamina | MagicMode | Kit Recharge (0100), Recharge With Food (0100), Regular Spell (0100) |
| Mana | MagicMode | Kit Recharge (0100), Recharge With Food (0100), Regular Spell (0100) |
Dispatch (`cRechargeManager.Recharge`, `cRechargeManager.cs:47-78`):
filter rows to the current stance, then walk in table order; a row is
attempted if percent is in `[Min,Max]` **or it is the last row in the
list** (guaranteed fallback attempt), and the first `Activate()` that
returns `true` wins. `GetCurrentStance()` (`cRechargeManager.cs:193-205`)
is simply `CombatMode == Magic ? MagicMode : Other`.
**acdream** (`acdream:VitalRecharge.cs:36-83`, `VitalRechargePlanner`)
explicitly documents itself as "VTank's default `RechargeHandlerSet`,
including its stance- and current-percentage-dependent order"
(`VitalRecharge.cs:36-40`) and takes a `VitalRechargeMethod[]` per
(vital, magic-mode-bool, percent, table) — structurally the same
design as `cRechargeManager`; the concrete default table values were
not independently re-verified cell-by-cell against the table above in
this pass (§6).
### 4.3 The seven handlers
| Handler | Cast/use logic | Key gates | Cite |
|---|---|---|---|
| `m` (Regular Spell) | Health→`Adja's Intervention`, Stamina→`Robustification`, Mana→`Meditative Trance`; picks best of the named spell vs. the caster's best current tier via `he.a(name,out,out)` | spell known only | `m.cs:47-85` |
| `bn`/`hc`/`c2`/`f2` (`dg` base) | vital-transfer spells named "X to Y Self" | non-Health→Health direction additionally requires the *other* vital's own `Recharge-Norm-*` NOT be under threshold (`cg.a/b/c`, i.e. don't drain Stamina/Mana to top off Health if Stamina/Mana is itself already low); for the Health-restoring direction, estimates whether the transfer would net-gain HP via `he.a()`/`he.a(spell,vital)` before committing | `dg.cs:54-139` |
| `a5` (Kit Recharge) | picks the highest-suitability kit item classified `fs.a/c/e` (Health/Stam/Mana Kit) meeting `MinimumHealKitSuccessChance`; if `Recharge-Norm-Stam` (Healing skill, `CharFilterSkillType 21`) not Trained/Specialized → unusable; blocked in Magic mode unless `UseKitsInMagicMode`; requires ≥15 Stamina except for Stamina kits themselves | `GoToPeaceModeToUseKits`: if set and not currently at Peace, **switches to Peace and returns (does not use the kit this call)** — the kit use happens on a later tick once Peace is confirmed | `a5.cs:49-128` |
| `g4` (Recharge With Food) | picks a non-cooling-down food/potion item classified `fs.b/d/f` (Health/Stam/Mana Food); if the item has a cooldown and the character is busy, waits; otherwise uses it | none beyond item availability | `g4.cs:48-91` |
`fs` item classification (`uTank2/PluginCore.cs:7445-7602`): built at
identify time from `WorldObject.Values`/`ObjectClass`/name; Health/
Stam/Mana **Food** (`fs.b/d/f`) comes from `LongValueKey 89 ==
2/4/6`; named Medicated/Eternal/Greater/Lesser Stamina/Mana **Kits**
map to `fs.c/e`; a generic Health Kit falls to `fs.a`;
`ObjectClass == 16` with `DoubleValueKey 137 != 1.0``fs.i` (the
worn-item mana-charge consumables, §4.5) vs. `== 1.0` with charge
fields present → `fs.g`.
**acdream** (`acdream:VitalRecharge.cs`, `VitalRechargeMethod` enum
`RegularSpell/StaminaToHealth/ManaToHealth/HealthToStamina/
HealthToMana/Kit/Food`, `VitalRecharge.cs:25-34`) mirrors the same six
handler shapes plus the constant `HealingSkill = 21` (matching
`CharFilterSkillType 21`, `VitalRecharge.cs:43`); the item
classification rules (kit/food name-or-flag matching) were not
independently re-verified line-for-line against `PluginCore.cs:
7445-7602` in this pass.
### 4.4 `GoToPeaceModeToUseKits` sequencing and vital-transfer sizing
`a5.b()` line 121-125: `if (GoToPeaceModeToUseKits && CombatMode !=
Peace) { f9.a(Peace); return true; }` — this is a **raw** peace switch
(the "Family A" open-coded drop-to-peace pattern from the trace note),
not routed through `ForceCombatMode`; it fires once per tick until
Peace lands, then the *next* `a5.b()` call actually applies the kit.
`he.a(MySpell,eRechargeVital_Single)` (`he.cs:95-155`) estimates a
transfer spell's **effective HP delivered**: base current-vital reading
(minus a flat 30 for Mana, floored at 0, `he.cs:100-108`), times a
tier-dependent multiplier/cap by name suffix (`" I"`→0.9×/cap 50,
`" II"`→1.0×/cap 100, `" III"`→1.1×/cap 150, `" IV"`→1.2×/cap 200,
`" V"`/`" VI"`/`"Incantation of "`/other→1.351.75×/uncapped). Used by
the `dg` base handler (§4.3) to decide whether a transfer nets a
positive HP gain before committing, and by the "estimate Heal Self
cost/tier" helper `he.a()` (`he.cs:74-93`, a name→flat-cost lookup for
`Heal Self I..VI`/`Adja's Intervention`/`Incantation of Heal Self`,
default 10).
**acdream**: not independently re-verified (no `he.cs`-equivalent
sizing table was located during this pass; flagged §6).
### 4.5 Mana recharge of wielded/worn items (`dy.cs` / `a0.cs`)
`a0` ("RefillWieldedMana") gate/dispatch (`a0.cs:60-112`): first tries
`dz.ac.h()`/`.i()` (the `dy` worn-item queue, below); only if that has
nothing does it fall back to `dz.ab.d()`/`.e()` (a `ManaStoneUse`
action-locked path, `ev` class — not traced in this pass).
`dy` (§0) tracks, per **wear/wield event** (`dy.b(int itemId)`,
`dy.cs:284-289`, subscribed via the client's `g6.o` wield map), a FIFO
queue `dy.c` of newly-equipped items still below full charge (dropped
once `q.a(item, actions)` reports it no longer needs charge,
`dy.cs:292-303`) and a parallel list `dy.a`/`dy.b` of qualifying
**charge consumables** classified `fs.i`
(`ObjectClass==16 && DoubleValueKey 137 != 1.0`, i.e. "not a
usable-food flagged item" — the gems/food-panel mana charges),
partitioned by whether they're stacked (`item2.g()`) — rebuilt whenever
the profile changes (`dy.b()` private, `dy.cs:50-73`) by scanning
`PluginCore.dz.q.p[worn-item-name]`, i.e. **the charge candidates are
keyed to the specific worn item's own name**, not a global pool.
`h()` = "queue non-empty AND a matching charge item exists"
(`dy.cs:309-315`); `i()` = `ApplyItem(chargeItemId, dy.c[0])` — a
direct item-on-item apply, **oldest-queued worn item first**, not
sorted by current charge percent (`dy.cs:318-322`).
**acdream** (`acdream:ItemManaRecharge.cs`): `ItemManaRechargePlanner.
Plan` (`ItemManaRecharge.cs:17-54`) instead (a) matches charge
consumables by a **global** `ItemType & ManaStoneItemType` flag plus a
configured name allowlist (`consumableNames`, not auto-derived from
`fs.i`'s per-worn-item keying), and (b) picks the **lowest current
mana-percent** equipped item under the configured threshold, not the
oldest-queued one. Flagged as gap G4 in §5: retail's charge-item
pool is scoped per *specific worn item name* (`q.p[name]`), so a charge
consumable that only works on item A is never offered for item B;
acdream's flat name-allowlist + generic item-type flag does not encode
that per-item scoping, which could mean either mismatching a charge to
an incompatible worn item or failing to find a match retail would have
found via the correct per-name bucket.
### 4.6 Dispel self / dispel items
`CastDispelSelf` (`c8.cs`): requires `q.d("Chorizite") != 0`
(`c8.cs:72-75``g6.d(string)` sums a `LongValueKey` stack-count field
over owned items whose "identified name" equals the given string,
`g6.cs:222-211`ish; **`"Chorizite"` is read here as a literal
in-inventory item name gate**, not a debuff-detection API — see §6 for
residual uncertainty on why owning an item named "Chorizite" gates a
dispel-self spell), then the known `Eradicate Life Magic Self` spell
and a difficulty-vs-active-vuln check (`c4.a(mySpell.Difficulty)`,
`c8.cs:76-85`).
`UseDispelItems` (`cx.cs`): if the worst active vuln difficulty is
≥400 (`c4.a(400)`), tries `Rune of Dispel` → `Society Gem of
Dispelling` → `Black Market Gem of Dispelling`, in that priority
(`cx.cs:74-91`); else if ≥350 (`c4.a(350)`), tries `Rune of Dispel`
`Chocolate Gromnie``Condensed Dispel Potion``Gem of Stillness`
(`cx.cs:92-114`) — i.e. pricier items are reserved for the
worse-vuln tier, cheaper ones cover the lesser tier.
**acdream** (`acdream:DispelController.cs`): `TryStartSelfDispel`
(`DispelController.cs:108-157`) gates on the exact same literal
`"Chorizite"` item-with-stack check
(`DispelController.cs:115-117`) — a faithful (if unexplained) port,
not a divergence. `TrySelectDispelItem`
(`DispelController.cs:159-173`) reproduces the two-tier
400/350-difficulty item lists via `HasVulnerabilityAtOrBelow` +
`HighDifficultyItems`/`NormalDifficultyItems`. Not independently
re-verified item-for-item against `cx.cs:74-114` in this pass.
## 5. MossTank gap ledger (ranked by likely player-visible impact)
| # | Gap | Retail cite | acdream cite | Impact |
|---|---|---|---|---|
| **G1** | `BuffPlan.Build` snapshots skills/attributes/active-enchantments **once** per pass and queues fixed spell picks; retail's `eq.a()` re-reads skill/quality **every logic tick** (~293 ms) and re-picks fresh, so a mid-pass skill raise (e.g. casting a mastery buff) immediately unlocks a higher tier for the *next* candidate in the *same* pass. acdream cannot realize that cascade — it locks in tier choices from before the pass started. | `eq.cs:477-527` (fresh per call), `uTank2/cLogic.cs:145` (293 ms tick) | `BuffPlan.cs:104-213` (one `Build` call), `MossTankPanel.cs:3607-3698` (`BuildPlan` called once at pass start, never mid-pass) | **High** for any character whose unbuffed skill sits close to a tier's difficulty threshold — retail casts a stronger buff line than acdream will, specifically for spells downstream of the Creature-Enchantment-Mastery→Focus/Willpower→Item-Enchantment/Life-Magic dependency chain the acdream code's own comment (`BuffPlan.cs:305-336`) describes but the implementation doesn't dynamically realize. |
| **G2** | Retail auto-populates default enchant-spell candidates (auras / prots / banes) for a wand/shield/pet the **moment it's added to the profile**, keyed off the item's `LongValueKey 218103822` mastery mask (§2). No equivalent "profile item → default spell rows" pipeline was found in `BuffPlan.cs`/`BuffProfile.cs`; `BuffProfile.Build` only classifies the character's **own spellbook**. | `PluginCore.cs:8327-8445` | not located in the six named files | **High** if genuinely absent — this is how a fresh VTank user gets working weapon/shield enchants with zero configuration; if it lives elsewhere in the plugin it should be cited, otherwise it's a real feature gap for item-enchantment buffing specifically (as opposed to self-buffing, which G1 covers). |
| **G3** | Retail's helper-heal target pool includes **non-fellowship tracked players** (`ai.m`, populated from general server/client vitals echo dispatch, independent 5s-per-vital re-cast cooldown, 2-minute staleness) in addition to fellowship members. acdream's `TryPlanHelper` gates entirely on `settings.HelpOthers && automation.Fellowship.IsInFellowship` and was not seen to consult a non-fellowship tracked pool. | `ai.cs:246-375` (two pools, `l` fellowship + `m` non-fellowship) | `VitalRecharge.cs:94-140` (`TryPlanHelper`, fellowship-gated only) | **Medium** — narrows "help nearby strangers" to "help fellowship only"; likely intentional scoping (`DoHelp`'s own retail description even says "The fellowship window must be open to help fellows", `uTank2.Resources.defaultsettings.usd:499-503`, suggesting the non-fellow pool may be a secondary/legacy path) but worth a deliberate decision rather than an unnoticed drop. |
| **G4** | Worn-item mana-charge matching: retail keys charge candidates to the **specific worn item's own name** (`q.p[itemName]`) and drains the **oldest-queued** worn item first; acdream uses a **global** consumable-name allowlist + generic `ManaStoneItemType` flag and picks the **lowest-mana-percent** item first. | `dy.cs:50-73,309-322` | `ItemManaRecharge.cs:17-54` | **Medium** — could either apply an incompatible charge to a worn item that doesn't accept it, or simply pick a different (lower-%) target than retail would on the same tick; low severity in practice since AC mana-charge items are broadly compatible by charge-type, but the per-item keying is a real mechanism difference. |
| **G5** | `ba.cs`'s `RandomHelper` fires `ValidNow = true` on the *first* of up to 100 random draws that resolves to an **uncastable** spell (doing nothing that tick), and only returns `false` if all 100 draws happen to be castable (§3.1). acdream instead deterministically round-robins every (player × stem) pair and only fires on an actually-castable one. | `ba.cs:116-125,152` | `MossTankPanel.cs:4034-4068` | **Low/positive** — acdream's version helps allies strictly more often and more predictably than retail's likely-buggy sampling loop; flagged as a behavioral divergence worth a conscious call (replicate the retail quirk vs. keep the fix) rather than an oversight, since the retail behavior reads as unintended. |
## 6. Could not determine
- **`g6.d("Chorizite")`'s in-game meaning.** Confirmed mechanically —
it sums a `LongValueKey`-backed count over owned items whose
identified name equals the literal string `"Chorizite"`
(`g6.cs:222-231`) — and confirmed that acdream ports the same literal
gate (`DispelController.cs:115-117`). Not resolved: *why* owning an
item named "Chorizite" is the prerequisite for casting `Eradicate
Life Magic Self` at all (no corroborating string or comment found in
the traced files; may be a VTank-internal placeholder/detection
object rather than a "real" tradeable AC item, or may key off a
specific quest/consumable whose purpose isn't documented in this
build's strings).
- **Exact retail `RechargeHandlerSet` table values vs. acdream's
ported default table** — the 26-row table was fully decoded from
`uTank2.Resources.defaultsettings.usd` (§4.2), but acdream's own
default table in `VitalRecharge.cs` was not diffed cell-by-cell
against it in this pass.
- **`he.a`'s HP-yield sizing table vs. any acdream equivalent** — no
matching sizing/estimate function was located in
`VitalRecharge.cs` during this pass; may exist under a name not
grepped, or may be a genuine gap (vital-transfer handlers casting
without an HP-net-gain pre-check).
- **`af.cs`'s vuln-severity scoring vs. acdream's `DispelController`
ally-dispel target selection**, and **`fb.cs`'s exact two-skill/
two-item-name gates vs. `TryHealersHeart`** — both confirmed present
in acdream at the call-site level (§3.2, §3.4) but not diffed
field-by-field against the retail source in this pass.
- **VTank's own `ba.cs` 100-draw loop**: whether the `g == null` /
`return true` branch (§3.1, §5 G5) is a genuine retail bug or
deliberate (e.g. "yield the tick so some other rule can act" —
though nothing downstream appears to rely on that), left unresolved;
quoted verbatim rather than guessed at.