docs(vt): fold the citation-pass corrections into KB 05 and 09
05: recognized-vs-unrecognized rule payloads, 25 materials, only 3-arg SpellInfo entries are live, Read/User1..5 unreachable from any loot plugin, hv.a.e is a dead field, id.a.j = shareLoot, the Change-vs-Bonus branch selector mis-port in the evaluator. 09: obtest location, the broken params dedup overload, the real ah call sites, internal relay ctor, the next-waypoint handler at 3621-3639, TryHandle is host-only, Classic Looter registry narrowed, UtilityBelt's Harmony bypass route resolved. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
642eacc13c
commit
ee901b7f26
2 changed files with 103 additions and 69 deletions
|
|
@ -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<int>("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.
|
||||
|
|
|
|||
|
|
@ -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<string, byte>` 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.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue