diff --git a/docs/research/vtank-kb/05-looting-and-utl.md b/docs/research/vtank-kb/05-looting-and-utl.md index d7b26995..ae0cdbf9 100644 --- a/docs/research/vtank-kb/05-looting-and-utl.md +++ b/docs/research/vtank-kb/05-looting-and-utl.md @@ -77,7 +77,7 @@ order (feature-gated): | 2 (v1 only) | `CustomExpression` | free text; **VTClassic writes it but its `Match`/`Classify` never read it** — editor-only round-trip field, not executed (`cLootItemRule.cs:34-56` has no expression evaluator) | | 3 | `pri;act;type0;type1;…` | `;`-split: `pri` (int, `Priority()`), `act` (int cast to `eLootAction`), then one int per requirement (`eLootRuleType`) | | conditional | `LootActionData` | only if `act == KeepUpTo`: one line, the keep-count (`cLootItemRule.cs:101-104`) | -| per requirement | length + payload (v1) / fixed lines (v0) | v1: one line = char count, then that many raw characters read via `inf.Read(char[],0,num)` (`cLootItemRule.cs:110-124`); v0: no length prefix — `iLootRule.Read` consumes its own fixed line count directly (`cLootItemRule.cs:125-129`) | +| per requirement | length + payload (v1) / fixed lines (v0) | v1: one line = char count; for a RECOGNIZED type the count is read and discarded and `iLootRule.Read` consumes its own lines (`cLootItemRule.cs:112-118`); only an UNRECOGNIZED type consumes that many raw characters via `inf.Read(char[],0,num)` (`cLootItemRule.cs:119-123`); v0: no length prefix — `iLootRule.Read` consumes its own fixed line count directly (`cLootItemRule.cs:125-129`) | `LootRuleCreator.CreateLootRule` (`VTClassic/LootRuleCreator.cs:5-43`) maps each `eLootRuleType` int to its class; an unrecognized type under v1 becomes @@ -131,7 +131,7 @@ ID-independent type (character-state and color rules decide immediately; `hasdecision=true`); every ID-dependent type (`GameInfo.IsIDProperty` true, plus the hardcoded-`true` buffed family, plus `SpellCountGE`/`SpellMatch`/ `SpellNameMatch`, which gate on the item's own "identified" flag -`IntValueKey 218103824` bit `1`, e.g. `SpellCountGE.cs:31-43`) reports +`IntValueKey 218103824` bit `1` — when that bit is SET the rule reports undecided, i.e. the bit marks "spell data not yet revealed", not "identified"; e.g. `SpellCountGE.cs:31-43`) reports `hasdecision=false` until ID data exists. ### 1.4 The extra-block manager @@ -148,7 +148,7 @@ type is skipped by raw character count (`UTLFileExtraBlockManager.cs:83-87`) `SalvageCombine` block exists even for a profile written before the block existed, seeded with VTClassic's built-in defaults (`UTLBlock_SalvageCombine.cs:30-59`: `DefaultCombineString = "1-6, 7-8, 9, -10"`, plus a fixed override table of 24 named gem/leather/ivory materials → +10"`, plus a fixed override table of 25 named gem/leather/ivory materials → `"1-10"`, resolved through `GameInfo.GetMaterialID`). `UTLBlock_SalvageCombine.Read/Write` (`UTLBlockHandlers/UTLBlock_SalvageCombine.cs:194-236`): @@ -238,12 +238,19 @@ position in the file/list. `ComputedItemInfo.cs:205,234` — if the item has no base value for that key at all, the buffed value is just the caller's default, spell bonuses are **not** added). If the key exists, the raw value is summed - (int) or changed (double: additive unless the static table's `Change==1`, - in which case multiplicative, `ComputedItemInfo.cs:244`) with a bonus from - a hardcoded spell-id → (key, bonus) table seeded in the static - constructor (`ComputedItemInfo.cs:88-139` — Impen/Blood Drinker/Heart - Seeker/etc. tiers by spell id, e.g. spell `1616`→`+20` to - `IntValueKey 218103842` = Damage). + (int) or changed (double: additive unless `(int)Change == 1` — i.e. `Change` + anywhere in [1.0, 2.0) truncates to 1 — in which case multiplicative; in + BOTH branches the operand applied is `Bonus`, never `Change`, + `ComputedItemInfo.cs:244`) with a bonus from a hardcoded spell-id → + `SpellInfo{Key,Change,Bonus}` table seeded in the static constructor + (`ComputedItemInfo.cs:88-139`). **Only the three-argument entries are + live**: the two-argument ctor (`ComputedItemInfo.cs:17-20`) sets + `Bonus = 0.0`, and both getters skip any entry with `Bonus == 0.0` (`:213`, + `:242`). So spell `1616` (`SpellInfo(218103842, 20.0)`, Bonus 0) + contributes **nothing**; a live example is spell `2598` → `+2` to + `IntValueKey 218103842` = Damage (`:96`). MossTank's tables at + `VtankLootRequirementEvaluator.cs:16-39` correctly carry only the live + entries (citation pass 2026-09-06). - **`BuffedAverageDamage`** (`ComputedItemInfo.cs:36-45`): median of the buffed max damage and its variance-adjusted min (`max - variance*max`, averaged with max — same formula VTClassic exposes @@ -307,15 +314,20 @@ short-circuit on a definite non-match" logic per-rule that `Match` does. `LootPluginBase.GetLootDecision` (abstract, `uTank2.LootPlugins/LootPluginBase.cs:27`) → `LootCore.GetLootDecision` (`VTClassic/LootCore.cs:53-91`): calls `cLootRules.Classify`, maps -`eLootAction` (VTClassic's own internal enum) to the public -`uTank2.LootPlugins.LootAction` (a separate, parallel enum — same ordinal -values 0-10, `LootAction.cs:1-81`), stamping the matched rule's name onto +`VTClassic.eLootAction` (11 members, 0-10) onto the host's +`uTank2.eLootAction` (13 members, 0-12 — adds `ManaStone`/`ManaTank`, +`uTank2/eLootAction.cs:3-18`) wrapped in the public class +`uTank2.LootPlugins.LootAction` (`LootAction.cs:1-81`), stamping the matched rule's name onto `LootAction.RuleName`. `LootAction` itself is a closed factory type (ctors `internal`) exposing static singletons (`NoLoot`,`Keep`,`Salvage`,`Sell`, `User1..5`) plus `GetKeepUpTo(maxcount)`; `IsRead`/`LootAction.Read` are -`internal` — **a third-party loot plugin cannot construct a `Read` action -at all**, only VTClassic itself (in the same assembly) can; every other -plugin's "read" support would have to reuse `User1..5`. `GameItemInfo` +`internal` to the **uTank2 assembly** — **no loot plugin can construct a +`Read` action, VTClassic included** (VTClassic ships as its own assembly and +the tree carries no `InternalsVisibleTo`). Confirmed by the mapping switch +itself (`LootCore.cs:65-82`), which has no `Read` case **and no `User1..5` +cases** — both fall through to the `NoLoot` initializer, so VTClassic can only +ever return NoLoot/Keep/Salvage/Sell/KeepUpTo; `Read` and `User1..5` reach +`hv` only from host-internal paths. `GameItemInfo` (`uTank2.LootPlugins/GameItemInfo.cs:7-279`) is the read side of the contract: `ObjectClass`, `HasIDData`, `Id`, `Spells`/`ItemSpell` (resolved through the plugin core's spell cache, not the raw wire spell-id list), @@ -373,8 +385,9 @@ for a possessive form (a combat pet's name reads as `"Owner's Petname"`); if it matches and the possessive owner is the local character, the killer is rewritten to the local character's own name; else, if fellowship data is available (`dz.aj.b()`), the fellow roster is scanned for a member matching -that owner name and the killer is rewritten to the matched member's full -name — kills by your own or a fellow's **combat pet** are attributed to the +that owner name and the killer is rewritten to that member's own name field (`fo.cs:233` +assigns the same `item.Value.b` it matched case-insensitively — a case +normalization, not a fuller name) — kills by your own or a fellow's **combat pet** are attributed to the owner. If the long description has **no** `"Killed by "` match at all (non-monster corpse, or one killed by nothing recognizable), `f=""` and **`e` (rare) is forced `true`** (`fo.cs:243-245`) — a permissive default so @@ -388,7 +401,7 @@ the currently-targeted corpse `i`; the eligibility scan (below) skips any corpse denied within the last 10 seconds. **MossTank has no equivalent chat-text listener** — see §4. -**Cache eviction** (`fo.cs:107-155`, on a `StartupComplete`/timer-poked +**Cache eviction** (`fo.cs:133-155`, on a `StartupComplete`/timer-poked event with a bound `ey` rate-limiter fired every 30841 ms, `fo.cs:57,84-93`): an entry is only removed once **both** `a` (released from view) is true **and** @@ -571,12 +584,13 @@ picks the **highest-priority entry** in `m_d`, marks it in-progress, increments its attempt counter, drops it from the queue once attempts exceed `CorpseLootItemMaxAttempts` (`hv.cs:379-382`), else issues `f9.p(itemId)` — the SAME raw-UseItem helper used to open/close the -corpse. **VTank does have a genuine per-item pickup-priority mechanism** -(`hv.a.e`) — but this pass did not confirm what populates that score (it -was not traced back to `cLootItemRule.pri`, and §2.1 already established -that VTClassic's own `Classify()` never reads `pri`; whether `hv.a.e` is -sourced from `pri` via some other path, or is a purely host-computed value -unrelated to it, remains open — see §5). +corpse. **The priority field `hv.a.e` is dead.** It is declared +(`hv.cs:19`), read at `hv.cs:371,374`, and **never written anywhere** — the +ctor leaves it `0` (`hv.cs:25-31`) and neither construction site sets it +(`hv.cs:256,289`); `hv.a` is a `private class` so no external writer is +possible. The "highest-priority" scan therefore always keeps the FIRST key +`MyDictionary` enumerates (strict `>` against `int.MinValue`). VTank has no +effective per-item pickup priority (citation pass 2026-09-06). **Readiness**, `hv.f()` (`hv.cs:315-350`) — "corpse fully processed": `false` while items are still expected (`CorpseItemAppearanceTimeoutSeconds`, @@ -732,18 +746,20 @@ Ranked semantic differences (highest impact first): |---|---|---|---|---| | 1 | **No ID-avoidance optimization.** | `NeedsID`/`EarlyMatch` (§2.3) skip appraisal when the classification is already decidable, or when a later same-action rule makes an earlier undecidable rule moot. | `LootController.ContinueCurrentCorpse` (`Looting.cs:708-721`) calls `loot.Identify(item.ObjectId)` for **every** corpse item before any decision is made — there is no `DoesPotentialItemNeedID` analog anywhere in `Looting.cs`, and no test exercises one (`LootingTests.cs` has no `NeedsID`/`DoesPotentialItemNeedID` case). | High — changes appraisal/identify pacing and network chatter for every loot pass; a rule set that could skip IDing (e.g. "NoLoot everything except ObjectClass==Money") gets fully IDed anyway on live retail/ACE. | | 2 | **Rare corpses are not prioritized in selection.** | `fo.a` (§3.2) strictly prefers ANY rare corpse over ANY non-rare corpse, regardless of distance, once any exists in the known set. | `LootController.Tick`'s candidate loop (`Looting.cs:557-561`) orders strictly by `.Distance` then `.ObjectId` — `CanLoot` gates eligibility per §4's fellow/all-corpse rules (see #4 below, this part IS faithful) but never re-orders for rarity. | High for players who loot mixed rare/mundane fields — a farther rare corpse can be skipped in favor of a nearer mundane one until the mundane one is done. | -| 3 | **No chat-text ownership-denial listener.** | `fo.cs:71-73,269-323,393` treats a "already in use by someone else!" / "you do not yet have the right to loot" chat line as an immediate 10-second skip for that corpse. | `Looting.cs` has no chat-message handling anywhere in the loot path; a denied corpse is only ever backed off via the generic `BlacklistCorpseOpenAttemptCount`/`BlacklistCorpseOpenTimeoutSeconds` retry-then-blacklist mechanism (`Looting.cs:397,1397-1417`), which the defaults confirm are faithfully ported (30 attempts / 200 s, matching `defaultsettings.usd:1059-1071` exactly). | Medium — same eventual outcome (corpse gets skipped) but far slower: retail's explicit refusal is immediate, MossTank's fallback needs up to 30 failed open attempts first. | -| 4 | **`BuffedInt`/`BuffedDouble` omit the base-key-exists gate.** | `ComputedItemInfo.GetBuffedLogValueKey`/`GetBuffedDoubleValueKey` (`ComputedItemInfo.cs:198-248`) only add a spell bonus if the item **already has** that base key (`KeyExistsInt`/`KeyExistsDouble`); otherwise the buffed value stays at the caller's default. | `VtankLootRequirementEvaluator.BuffedInt`/`BuffedDouble` (`VtankLootRequirementEvaluator.cs:415-448`) compute the base value through a default-returning lookup (no existence check) and then unconditionally add any matching spell bonus. | Medium-low — for the common case (a weapon rule reading `Damage`, which virtually every weapon carries) this never differs; it only diverges for an item that lacks the base key entirely but is affected by a matching buff spell, an edge case not covered by any current test. | +| 3 | **No chat-text ownership-denial listener.** | `fo.cs:71-73,269-323,393` treats a "already in use by someone else!" / "you do not yet have the right to loot" chat line as an immediate 10-second skip for that corpse. | `Looting.cs` has no chat-message handling anywhere in the loot path; a denied corpse is only ever backed off via the generic `BlacklistCorpseOpenAttemptCount`/`BlacklistCorpseOpenTimeoutSeconds` retry-then-blacklist mechanism (`Looting.cs:386-387,1397-1417`), which the defaults confirm are faithfully ported (30 attempts / 200 s, matching `defaultsettings.usd:1059-1071` exactly). | Medium — same eventual outcome (corpse gets skipped) but far slower: retail's explicit refusal is immediate, MossTank's fallback needs up to 30 failed open attempts first. | +| 4 | **`BuffedInt`/`BuffedDouble` omit the base-key-exists gate.** | `ComputedItemInfo.GetBuffedLogValueKey`/`GetBuffedDoubleValueKey` (`ComputedItemInfo.cs:198-248`) only add a spell bonus if the item **already has** that base key (`KeyExistsInt`/`KeyExistsDouble`); otherwise the buffed value stays at the caller's default. | `VtankLootRequirementEvaluator.BuffedInt`/`BuffedDouble` (`VtankLootRequirementEvaluator.cs:415-448`) compute the base value through a default-returning lookup (no existence check) and then unconditionally add any matching spell bonus. It also mis-ports the branch selector: `(int)bonus.Bonus == 1` (`VtankLootRequirementEvaluator.cs:445`) tests the bonus where VTClassic tests the separate `Change` field (`ComputedItemInfo.cs:244`) — harmless today only because MossTank's table holds only entries where `Change == Bonus`. | Medium-low — for the common case (a weapon rule reading `Damage`, which virtually every weapon carries) this never differs; it only diverges for an item that lacks the base key entirely but is affected by a matching buff spell, an edge case not covered by any current test. | | 5 | **The 100-second public/fellow-corpse "age" clock starts at a different moment.** | `fo.a.c` (§3.1) is stamped once at `WorldFilter.CreateObject` — i.e. as soon as the corpse object streams into the client's known-object set, which is typically a much larger radius than the loot-approach range. | MossTank's `_corpseFirstSeen` (`Looting.cs:394,530`) is only populated inside `CaptureCorpses(CorpseApproachRange)` (`Looting.cs:526-530`) — the clock cannot start until the corpse is already within the (much smaller) loot approach range. | Low-medium — makes MossTank's 100-second public-corpse and fellow-non-share timers start later than retail VTank's for a corpse seen from far away before the player walks up to it; converges to the same behavior once the player is in loot range for 100+ seconds regardless. | | 6 | **VTank explicitly re-closes a finished corpse; MossTank does not.** | `a1`'s `fo.b()` (`fo.cs:353-363`) issues a second raw UseItem on the corpse id specifically to close the container view once looting completes. | `LootController.ContinueCurrentCorpse`'s completion path (`Looting.cs:750-760`) just clears local state and moves on — it never issues an explicit close action; whether the container view auto-closes depends on the host's `ILootAutomation`/retail behavior rather than an explicit port of `fo.b()`'s second UseItem. | Low — cosmetic/UI-state difference (an open corpse window lingering) rather than a loot-decision difference, unconfirmed whether retail's own container-close behavior makes this moot. | | 7 | **`el.cs`'s 80-attempt stuck-item abandon threshold vs MossTank's 40-attempt salvage-combine abandon.** | VTank's `AutoStack`/`AutoCram` mover (`el.cs:182-198`) gives up after 80 consecutive stuck ticks. | MossTank's `ContinueSalvageBagCombine` gives up after 40 (`Looting.cs:1138`). | Low — different subsystem (stack/cram vs salvage-bag-combine) and an unconfirmed-as-intentional magic number on both sides; flagged only because the pattern shape is otherwise an exact match. | | 8 | **Reading a scroll is an immediate continuation in MossTank; VTank defers it to a separate idle-loop rule.** | `hv`'s `Read` handling (§3.7) only enqueues `spellId → itemId` into `dz.o.i`; the actual read fires later, whenever `er` ("ReadScroll") next becomes `ValidNow` in the (priority- or idle-)loot stage — it can be delayed behind combat/idle-status rules ahead of it in the list (§3.6). | `LootController`'s `_postUseItem` continuation (`Looting.cs:825-831,857-903`) issues the read as the very next action after the scroll's pickup completes. | Low — same eventual outcome (scroll gets read once known), different latency/interleaving; a VTank session with `LootPriorityBoost` off and a full combat queue could sit on a picked-up scroll far longer than MossTank ever would. | -| — | **`cLootItemRule.pri`/`Priority` has no effect in VTClassic's own classifier — but VTank's item-tracker DOES have a real per-item pickup-priority field.** | `cLootRules.Classify` never reads `Priority()` (§2.1) — dead weight from VTClassic's own read/write perspective. VTank's item-tracker (`hv`) separately carries a genuine priority score per queued item (`hv.a.e`, an `int`) and its pickup mover explicitly picks the **highest-priority** queued item first (`hv.a()`, §3.5). | `LootController.ContinueCurrentCorpse`'s pickup-order selection (`Looting.cs:764-769`) sorts already-decided candidates by `Decision.Priority` (descending) then rule index — the SAME shape as `hv.a.e`-based selection. | Not ranked as a gap — MossTank's behavior has a genuine VTank-side counterpart in shape. **Unconfirmed**: what populates `hv.a.e` was not traced to its source in this pass (not confirmed to be `cLootItemRule.pri`, and not confirmed to be anything else either) — see §5. | +| — | **`cLootItemRule.pri`/`Priority` has no effect in VTClassic's own classifier — but VTank's item-tracker DOES have a real per-item pickup-priority field.** | `cLootRules.Classify` never reads `Priority()` (§2.1) — dead weight from VTClassic's own read/write perspective. VTank's item-tracker declares a priority field (`hv.a.e`) but **never writes it** (§3.5) — the "highest-priority" scan degenerates to dictionary-enumeration order, so VTank has no effective pickup priority. | `LootController.ContinueCurrentCorpse`'s pickup-order selection (`Looting.cs:764-769`) sorts already-decided candidates by `Decision.Priority` (descending) then rule index — the SAME shape as `hv.a.e`-based selection. | Low — MossTank's `Decision.Priority` ordering is a MossTank-side addition with no live VTank counterpart. Resolved: `hv.a.e` is dead (§3.5). | Not gaps (verified faithful, listed so a future pass doesn't re-litigate them): the entire `LootOnlyRareCorpses`/`LootFellowCorpses`/`LootAllCorpses` ownership-gate cascade (`CanLoot`, `Looting.cs:1191-1230`) matches -`fo.a`'s ownership branch (`fo.cs:403-434`) line-for-line, including the +`fo.a`'s ownership branch (`fo.cs:403-434`) line-for-line (one benign +difference: VTank compares killer and fellow names case-SENSITIVELY, +`fo.cs:407,417`; MossTank uses `OrdinalIgnoreCase`), including the 100-second thresholds and the "share loot" fellow-member flag; the `BlacklistCorpseOpenAttemptCount`/`TimeoutSeconds` defaults (30/200) and the `CorpseItemAppearanceTimeoutSeconds`/`CorpseItemIdentifyTimeoutSeconds` @@ -759,12 +775,6 @@ constants). ## 5. Could not determine -- **What populates `hv.a.e`** (the per-queued-item pickup-priority score, - §3.5) — confirmed to exist and to drive pickup order, but not traced - back to a source; not confirmed to be `cLootItemRule.pri` (which §2.1 - established VTClassic's own `Classify()` never reads) or to be anything - else. This is the one open question behind the "not ranked as a gap" - row in §4's table. - **`fo.a`'s dual meaning of the `e` (rare) field** (`fo.cs:12`, set both at `fo.cs:209` for a genuine "Killed by X...Generated..." match and at `fo.cs:244` for NO "Killed by X." match at all) — whether these two @@ -777,14 +787,11 @@ constants). §4 item 2 (VTank preferring "rare" corpses) — the practical rare-corpse population it actually prefers may be broader than "wear the retail rare drop message" alone. -- **`id.a.j`** (`id.cs:29`, the fellow-member record field checked at - `fo.cs:420`, gating IMMEDIATE-vs-100-second-delayed fellow-corpse - looting) — sits among several plain `int`/unnamed fields with no naming - information surviving obfuscation; its precise semantic (e.g. "member - currently in range", "member online", "member's own share-loot - preference") was not confirmed. MossTank's `member.ShareLoot` - (`Looting.cs:1224`) is presented as a direct port of this field but that - mapping itself is unverified against a confirmed semantic. +- **`id.a.j` — RESOLVED** (citation pass 2026-09-06): `id.cs:221` populates it + directly from the fellowship message's named field + (`a10.j = A_0.Value("shareLoot") != 0;`, alongside `name`/`level`/ + `maxHealth`, `id.cs:212-221`; field declared at `id.cs:30`). MossTank's + `member.ShareLoot` mapping (`Looting.cs:1224`) is confirmed correct. - **`f0.c` enum members `a` and `d`** (`f0.cs:11-17`) — only `b` (item-needs-ID) and `c` (corpse-needs-ID) were observed at call sites in this pass; the other two members' purposes are unknown. diff --git a/docs/research/vtank-kb/09-commands-and-interop.md b/docs/research/vtank-kb/09-commands-and-interop.md index 2e1e288e..50b4eacb 100644 --- a/docs/research/vtank-kb/09-commands-and-interop.md +++ b/docs/research/vtank-kb/09-commands-and-interop.md @@ -119,7 +119,7 @@ These exist in the parser but are absent from the four `/vt help` lines `waterdepth`, `physobj`, `testcollision`, `clearshapes`, `zoneid`, `activespells`, `burdentest`, `explain [idqueue]` (`k.cs:1-56`), `debugon`, `debugdump`, `skillvalues`, `throwex` (throws `Exception("Testing 123")`), -`spheredist`, `obtest` — `PluginCore.cs:4771-4901, 5152-5230, 5943-6034`. +`spheredist` — `PluginCore.cs:4771-4901, 5152-5230, 5943-6034`; `obtest` — `PluginCore.cs:5628-5633`. **No `/vt pause`.** Only `start` and `stop` exist in the parser; there is no separate pause verb in this decompiled build. @@ -138,7 +138,7 @@ directly; other call sites use `d5.a` with an explicit type). At (`d5.cs:27-36`, category name / description / color index / target-window array), then every emitted line is prefixed `"[VTank] "` (`d5.m_b`, `d5.cs:29,84`) and routed through `fh.b(name, text, color, targets)` -(`fh.cs:70-101`), which either forwards to VVS's `Presets.FilterOutputPreset` +(`fh.cs:75-97`), which either forwards to VVS's `Presets.FilterOutputPreset` (if VCS5 is loaded and running) or falls back to `PluginHost.Actions.AddChatText(text, color, target)` per target window (`fh.cs:78-89`): @@ -160,13 +160,19 @@ array), then every emitted line is prefixed `"[VTank] "` (`d5.m_b`, `ah` (`ah.cs`) wraps `d5.a(eChatType.Warnings, ...)` behind a `Dictionary` of messages already shown -(`ah.cs:6,13-30`). `ah.a(string)` and the `params object[]`-formatted -overload both check `ContainsKey` first and only emit (and record) a -message once per session; `ah.a()` (no args) clears the cache. This is -how VTank avoids spamming the same warning every tick (e.g. missing loot -profile, invalid regex in a Meta condition, monster added to the species -DB with no auto-damage entry — see call sites in `en.cs:23,32`, `c5.cs:35,64`, -`hl.cs:120`, `e0.cs:168`). +(`ah.cs:6,13-30`). `ah.a(string)` checks `ContainsKey` first and emits (and records) a +message once per session. The `params object[]` overload is **broken**: it +tests `ContainsKey(A_0)` against the *format* string but records the +*formatted* result (`ah.cs:24-28`), so any message with a placeholder is never +deduped and re-emits every call. `ah.a()` (no args) clears the cache. This is +how VTank avoids spamming the same warning every tick (69 call sites, e.g. +`ar.cs:73` no salvage ust, `b7.cs:146,151,161` unpickable doors, +`cl.cs:265,271` monster-spec parse errors, `d7.cs:31` invalid Meta-condition +regex). Note VTank also emits near-identical warning text through the +**un**-deduped `d5.a(eChatType.Warnings, …)` path — missing loot profile +(`en.cs:23,32`), invalid Meta regex (`c5.cs:35`, `hl.cs:120`), species with no +auto-damage entry (`e0.cs:168`) — so the same warning class can spam or not +depending on which sink the call site chose (citation pass 2026-09-06). ### Input-box text injection (distinct mechanism) @@ -262,11 +268,11 @@ relative to its `LogicObject`-gated sibling `Decision_GlobalBusyFlag`). MosswartMassacre — a real published third-party Decal plugin — never goes through the signed handshake. `vTank.cs:33-58` (`Enable()`): -1. Reflects the **private** `cExternalInterfaceTrustedRelay(eExternalsPermissionLevel)` +1. Reflects the **internal** `cExternalInterfaceTrustedRelay(eExternalsPermissionLevel)` constructor via `GetConstructors(BindingFlags.Instance|BindingFlags.NonPublic)[0]` and invokes it with `eExternalsPermissionLevel.None` — legal because the ctor's own internal check (`Assembly.GetCallingAssembly() == - Assembly.GetExecutingAssembly()`, `PluginCore.cs:165-172`) only compares + Assembly.GetExecutingAssembly()`, `PluginCore.cs:163-173`) only compares assemblies, and reflection's calling-assembly is the *invoking* code, not VTank itself, so this actually **fails** that check and would set `m_a = None`... except step 2 overwrites it directly. @@ -297,7 +303,7 @@ acquired) `vTank.Instance`: is the same trick as calling `/vt` yourself, done programmatically. - `VtGetMetaState`, `VtGetSetting`, `VtSetSetting`, `VtMacroEnabled` (`VtankControl.cs:30-106`) call the relay properties directly - (`CurrentMetaState`, `GetSetting`, `SetSettingType`/`SetSetting`, + (`CurrentMetaState`, `GetSetting`, `GetSettingType`/`SetSetting`, `MacroEnabled`) — these work because of the forced `15` permission mask. - `VtAdvanceWaypoint` (`VtankControl.cs:114-226`) is the clearest evidence of an API gap: there is **no relay method to advance the current @@ -307,8 +313,10 @@ acquired) `vTank.Instance`: reflecting `uTank2.PluginCore`'s public static `PC` field (tier 1, no permission needed) and invoking the private method `i(object, MVControlEventArgs)` — the exact handler wired to the in-UI "next - waypoint" button (`PluginCore.cs:3289-3309`, matches the `o` waypoint - handler naming pattern) — and (b), if that reflection fails, walking + waypoint" button (`PluginCore.cs:3621-3639`, subscribed at + `PluginCore.cs:1819` as `cd.Click += i;` — it increments `dz.o.l`, clamps to + `dz.m.k.b.Count - 1`, and no-ops for `eNavType.Target`/`Once`, exactly what + MosswartMassacre's own fallback re-implements) — and (b), if that reflection fails, walking `dz.o.l` (the raw current-index field) directly and incrementing it by hand. Both are reflection into implementation-private state because the public surface simply doesn't expose the operation. @@ -408,8 +416,9 @@ No installer artifact for the registry-key registration itself documented in `/vt help`. - **The loot-plugin SPI** has a modern equivalent: `IPluginLootClassifier`/`IPluginLootClassifierRegistry` - (`LootClassifierPlugins.cs:40-93`) mirrors `LootPluginBase`'s - `Classify`/`OnLooted`/`OnItemRemoved` shape with VTank's own action + (`LootClassifierPlugins.cs:40-93`) collapses `LootPluginBase`'s `GetLootDecision` into `Classify` and adds + two post-decision notifications retail never had (`OnLooted`/ + `OnItemRemoved`), with VTank's own action vocabulary reproduced verbatim as `PluginLootAction` (`NoLoot, Keep, Salvage, Sell, Read, User1..User5, KeepUpTo`, `LootClassifierPlugins.cs:4-17`) — but registration is **in-process, @@ -462,7 +471,10 @@ No installer artifact for the registry-key registration itself `VtAdvanceWaypoint` both exist *because* the legitimate API was incomplete, forcing either chat-command re-injection (`Decal_DispatchOnChatCommand`) or reflection into private fields. - acdream's `IPluginCommandRegistry.TryHandle` is host-internal (chat + acdream's `TryHandle` (`PluginCommandRegistry.cs:40`) exists only on the + concrete host registry and is absent from the `IPluginCommandRegistry` + abstraction plugins see (`PluginCommands.cs:15-23`, `Register` only) — it is + host-internal (chat text typed by the human player), not something a second plugin can invoke to simulate a `/vt` command against a first plugin — there is no equivalent of Decal's `DispatchOnChatCommand` P/Invoke for plugin code @@ -505,13 +517,28 @@ No installer artifact for the registry-key registration itself time.** No `.reg` file, installer project, or registration code is present in `refs/vtank-classiclooter/decompiled/` — this is presumably handled by an external installer (NSIS/MSI or similar) not vendored - here. -- **UtilityBelt/VTank integration.** `C:\Users\erikn\source\repos\ - utilitybelt.service` was checked directly; its available source (a - `UBService.cs`-rooted project plus `Lib`/`Views`/`scripts`) contains - **no** reference to `uTank2`, `VTank`, or `PluginCore` outside one - incidental binary-file match inside a vendored `.dll` under `deps/`. - Either UtilityBelt has no VTank integration in this checkout, or it - lives in a part of the tree/DLL not searchable as C# source; this - report does not claim UtilityBelt has zero VTank awareness in general, - only that none is visible in the given path. + here. Narrowed by the citation pass: VTank itself never writes the key — + the only `Software\Decal\LootPlugins` access in the whole decomp is the + read-only `OpenSubKey` at `cu.cs:231` — and VTank's own expression help + cites `http://www.virindi.net/repos/virindi_public/trunk/VirindiTankLootPlugins/VTClassic Shared/Constants.cs` + (`ch.cs:260,287,314,342,370,1074,1270`), placing Classic Looter inside the + VTank author's own SVN tree, so it almost certainly shipped in the Virindi + Plugins installer rather than self-registering. +- **UtilityBelt/VTank integration — RESOLVED** (citation pass 2026-09-06; the + first draft looked in the wrong tree). The integration lives in the full + UtilityBelt source (`C:\Users\erikn\source\repos\utilitybelt.gitlab.io`, and + a vendored copy at `MosswartMassacre/Unused/utilitybelt.gitlab.io/UtilityBelt/`): + `Tools/VTankControl.cs` (2,279 lines), `Tools/VTankExtensions.cs`, + `Tools/VTankFellowHeals.cs`, `Lib/VTNav/VTNavRoute.cs`, plus + `UBHelper.vTank.Decision_Lock(...)` calls across `AutoVendor.cs`, + `AutoTrade.cs`, `AutoSalvage.cs`, `InventoryManager.cs`, + `EquipmentManager.cs`. It is a THIRD bypass route beyond the two §3 tiers: + it never uses the signed handshake and **Harmony-patches VTank's internals** + (`HarmonyLib`, `harmonyClassic`/`harmonyExpressions`, + `VTankExtensions.cs:25-27`), resolving obfuscated types by name off + `typeof(uTank2.PluginCore).Assembly` (`hi`, `eq`, `fk`, `c5`, `dv`, `b3`, + `dt`, `fl`, `hl`, `f9`, `bo`, `gj`, `aw`) and reading the private static `dz` + through tier-1 `PluginCore.PC` (`VTankExtensions.cs:419,524`). + MosswartMassacre's `vTank.cs` is a copy of UtilityBelt's `UBHelper.vTank` + helper. Takeaway for acdream: **every** real-world VTank consumer examined + bypassed the permission system — the RSA gate gated nothing in practice.