diff --git a/docs/research/vtank-kb/04-buffs-and-vitals.md b/docs/research/vtank-kb/04-buffs-and-vitals.md index 568c0e6c..c97f25f1 100644 --- a/docs/research/vtank-kb/04-buffs-and-vitals.md +++ b/docs/research/vtank-kb/04-buffs-and-vitals.md @@ -80,10 +80,10 @@ sequence, not a generic "everything self-targeted" scan: | 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` | +| 8 | `Armor Self I` (the physical **prot**, `fk.b`, `fk.cs:424`; `Impenetrability I` is `fk.a` and belongs only to `eq.g()` at `eq.cs:246` — citation pass 2026-09-06) | `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` | +| 11 | any **untargeted** profile row (`eq.m_e` rows whose spell has `isUntargeted` true — the code filters on `isUntargeted` alone) | `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 @@ -159,8 +159,8 @@ sets `m_h = true` to signal "this is a consumable", `eq.cs:554`). `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 +exactly once per pass (`acdream:MossTankPanel.cs:3614-3656,3687-3705`); +`TryCast` (`acdream:MossTankPanel.cs:4317-4354`) 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 @@ -188,7 +188,7 @@ Nothing marks completion; the rule simply stops returning `true` from exhausted. **acdream** has an explicit pass object (`_queue`/`_queueIndex`, -`acdream:MossTankPanel.cs:3607-3656`) with its own progress/stall +`acdream:MossTankPanel.cs:3614-3656`) with its own progress/stall tracking (`_castThisPass`, `_sinceProgress`) not present in retail — a deliberate modernization, not a port; see §5. @@ -287,7 +287,7 @@ 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`, +**acdream** (`acdream:MossTankPanel.cs:4400-4412`, `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 @@ -305,7 +305,7 @@ 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` | +| `1048576` or `33554432` | Aura of Defender Self I, Aura of Blood Drinker Self I, Aura of Swift Killer Self I, Aura of 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` | @@ -377,13 +377,13 @@ 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`, +**acdream** (`acdream:MossTankPanel.cs:4047-4120`, `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`). +`MossTankPanel.cs:4071`, matching retail's 0.075 unit band), same +11-name stem list verbatim (`MossTankPanel.cs:4079-4087`), same +"resolve to best known tier" (`OrderByDescending(Quality).ThenByDescending(Tier)`, +`MossTankPanel.cs:4094-4096`), same interval lock semantics +(`_randomHelperRemaining`, floor 0.25s, `MossTankPanel.cs:4112-4114`). 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 @@ -483,10 +483,10 @@ diffed line-for-line against `af.cs:117-141` in this pass. ### 4.1 The trigger (`cr.cs` / `cg.cs`) -`cr` ("RechargeSelf2") is registered **three times** in `cLogic.cs` +`cr` ("RechargeSelf2") is registered **twice** 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 +`cLogic.cs:470,566`) — 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** @@ -528,17 +528,17 @@ Seven registered `IRechargeHandler`s (`cRechargeManager.cs:26-36`): 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, +MaxPercent, Stance 1=MagicMode/2=Other — `uTank2/eRechargeStance.cs`: `MagicMode = 1, Other`)`. 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) | +| Health | MagicMode | 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 | MagicMode | Kit Recharge (0–100), Regular Spell (0–100), Recharge With Food (0–100) | | Mana | MagicMode | Kit Recharge (0–100), Recharge With Food (0–100), Regular Spell (0–100) | +| Health | Other | Recharge With Food (0–15), Kit Recharge (0–100), Recharge With Food (0–100), Stamina→Health (0–10), Regular Spell (0–100) | +| Stamina | Other | Kit Recharge (0–100), Recharge With Food (0–100), Regular Spell (0–100) | +| Mana | Other | 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 @@ -576,7 +576,7 @@ 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 +HealthToMana/Kit/Food`, `VitalRecharge.cs:25-34`) mirrors the same seven 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 @@ -623,8 +623,11 @@ once `q.a(item, actions)` reports it no longer needs charge, 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. +`PluginCore.dz.q.p[name]` for every name that `dz.m.h` (item-name → +classification, `da.cs:29`) classifies `fs.i` (`dz.q.p` is name → inventory +instances, `g6.cs:58`), i.e. **a global pool of charge consumables keyed by +the consumable's own name** — NOT scoped per worn item (corrected by the +citation pass 2026-09-06). `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 @@ -634,15 +637,11 @@ sorted by current charge percent (`dy.cs:318-322`). 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 +`fs.i`'s classification), 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. +oldest-queued one. Flagged as gap G4 in §5: since retail's pool is +global too, the surviving difference is selection order only (retail +applies to `c[0]`, the oldest-queued worn item; acdream the lowest-%). ### 4.6 Dispel self / dispel items @@ -681,7 +680,7 @@ re-verified item-for-item against `cx.cs:74-114` in this pass. | **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. | +| **G4** | Worn-item mana-charge ordering: both retail and acdream draw charge consumables from a **global** pool (retail: every `fs.i`-classified name via `dz.m.h`/`dz.q.p`; acdream: `ManaStoneItemType` + name allowlist); retail drains the **oldest-queued** worn item first (`c[0]`), acdream the **lowest-mana-percent** one. | `dy.cs:50-73,309-322` | `ItemManaRecharge.cs:17-54` | **Low** — a different target on the same tick, never an incompatible apply (the per-worn-item keying claimed in the first draft was wrong; citation pass 2026-09-06). | | **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 @@ -697,16 +696,16 @@ re-verified item-for-item against `cx.cs:74-114` in this pass. 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). +- RESOLVED (citation pass 2026-09-06): acdream's default handler table + (`VitalRechargePlanner.Handlers`, `VitalRecharge.cs:208-288`, selected by + `magicMode: mode == PluginCombatMode.Magic`) reproduces the 26 retail rows + cell-for-cell — the `magicMode` arm is stance 1 (MagicMode), the `else` arm + stance 2 (Other). And `he.a`'s HP-yield sizing has an exact equivalent: + `VitalRechargePlanner.EstimatedTransfer` (`VitalRecharge.cs:725-746`; + I 0.9/cap 50 … VI 1.5/uncapped, else 1.75), the Mana −30 floor at + `VitalRecharge.cs:699-701`, `EstimatedOrdinaryHeal` (`:712-723`, + 17/25/32/45/67/87/115/135, default 10) and the net-gain pre-check at + `:708-709`. - **`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