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:
parent
cf6d5b94bb
commit
289c6706ba
1 changed files with 719 additions and 0 deletions
719
docs/research/vtank-kb/04-buffs-and-vitals.md
Normal file
719
docs/research/vtank-kb/04-buffs-and-vitals.md
Normal 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, min–max %) |
|
||||
|---|---|---|
|
||||
| Health | Other | Stamina→Health (0–15), Mana→Health (0–15), Regular Spell (0–15), Recharge With Food (0–15), Kit Recharge (0–100), Stamina→Health (0–100), Mana→Health (0–100), Regular Spell (0–100), Recharge With Food (0–100) |
|
||||
| Stamina | Other | Kit Recharge (0–100), Regular Spell (0–100), Recharge With Food (0–100) |
|
||||
| Mana | Other | Kit Recharge (0–100), Recharge With Food (0–100), Regular Spell (0–100) |
|
||||
| Health | MagicMode | Recharge With Food (0–15), Kit Recharge (0–100), Recharge With Food (0–100), Stamina→Health (0–10), Regular Spell (0–100) |
|
||||
| Stamina | MagicMode | Kit Recharge (0–100), Recharge With Food (0–100), Regular Spell (0–100) |
|
||||
| Mana | MagicMode | Kit Recharge (0–100), Recharge With Food (0–100), Regular Spell (0–100) |
|
||||
|
||||
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.35–1.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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue