From cf6d5b94bb7eede44ea71b9a0c5ace7b7294b7d1 Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 6 Sep 2026 19:42:51 +0200 Subject: [PATCH] =?UTF-8?q?docs(vt):=20VT1=20catalog=2009=20=E2=80=94=20/v?= =?UTF-8?q?t=20commands,=20chat=20sinks,=20and=20VTank's=20plugin=20intero?= =?UTF-8?q?p=20surface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ~48 documented subcommands plus 15 parser-only debug verbs (no /vt pause), the d5/ah chat sinks, the three-tier export model (public static PC, permission-gated relay, LootPluginBase SPI) annotated against MosswartMassacre's real usage, and the interop gaps in Plugin.Abstractions. Lead spot-check: PC field, eExternalsPermissionLevel, start/stop parser. Co-Authored-By: Claude Fable 5.1 --- .../vtank-kb/09-commands-and-interop.md | 517 ++++++++++++++++++ 1 file changed, 517 insertions(+) create mode 100644 docs/research/vtank-kb/09-commands-and-interop.md diff --git a/docs/research/vtank-kb/09-commands-and-interop.md b/docs/research/vtank-kb/09-commands-and-interop.md new file mode 100644 index 000000000..2e1e288ea --- /dev/null +++ b/docs/research/vtank-kb/09-commands-and-interop.md @@ -0,0 +1,517 @@ +# VTank KB 09 — Commands and Interop + +Oracle: decompiled retail VTank at `refs/vtank/decompiled/` (namespace root +`uTank2`; obfuscated single/double-letter file names throughout — `dz` is +VTank's central logic-object aggregate, `PC` is the plugin-core self +reference). Loot-plugin cross-check: `refs/vtank-classiclooter/decompiled/` +(`VTClassic.LootCore`). Consumer cross-check: a real third-party Decal +plugin, `MosswartMassacre/vTank.cs` + `VtankControl.cs`. acdream side: +`src/AcDream.Plugins.MossTank/MossTankCommands.cs`, +`src/AcDream.Plugin.Abstractions/PluginCommands.cs`, +`src/AcDream.Plugin.Abstractions/LootClassifierPlugins.cs`, +`src/AcDream.App/Plugins/LocalPluginPeerRegistry.cs`, +`src/AcDream.App/Plugins/AppAutomationSurface.cs`, +`src/AcDream.Core/Plugins/PluginCommandRegistry.cs`. All decompiled facts +are cited `file:line` against `refs/vtank/decompiled/...` unless another +root is named. `utilitybelt.service` at +`C:\Users\erikn\source\repos\utilitybelt.service` was checked and has **no** +VTank/uTank2 integration in its available source (see §6). + +## How `/vt` reaches the parser + +VTank subscribes to Decal's chat-parser interception event; the handler is +a thin adapter that hands the raw line to the real parser and marks it +"eaten" (swallowed, never reaches the AC server or later plugins in the +interception chain) when the parser recognized it: + +``` +private void a(object A_0, ChatParserInterceptEventArgs A_1) +{ + ((EatableEventArgs)A_1).Eat = l(A_1.Text); +} +``` +`PluginCore.cs:3970-3980`. `l(string)` is the actual command parser +(`PluginCore.cs:4732-6249`, ~1500 lines, a single cascading `if/else` +chain keyed on `text.CompareTo(...)`/`text.StartsWith(...)` after +stripping a leading `/` or `@` and the `vt ` prefix, `PluginCore.cs:4756-4761`). +VTank's own Decal plugin registers under GUID +`{642F1F48-16BE-48BF-B1D4-286652C4533E}` (`PluginCore.cs:3990`). + +## 1. The `/vt` command table + +`/vt help` prints exactly this four-line catalog (`PluginCore.cs:4765-4768`): +profiles, actions, game info, debug. The table below is every verb the +parser actually recognizes, grouped the same way, with syntax/effect/output +drawn from the parser body. + +### Profiles + +| Verb | Syntax | Effect | Output / error | `file:line` | +|---|---|---|---|---| +| `settings` | `/vt settings [save\|load\|savechar\|loadchar] [filename]` | save/load `.usd` settings profile; `savechar`/`loadchar` prefix the filename with the character-scoped folder (`dw`) | usage text on bad args | `PluginCore.cs:5671-5725` | +| `nav` | `/vt nav [save\|load] [filename]` | save/load `.nav` route file (`dz.m.n`, `dz.m.r/o`) | usage text on bad args | `PluginCore.cs:5634-5670` | +| `loot` | `/vt loot [load\|new] [filename]` | load or create a loot profile; validates the filename has no `\`, and that its extension matches an **active loot plugin's** registered extension (`dz.ah.a(ext)`) — else lists active plugins via `dz.ah.c()` | "Filename contains invalid characters.", "Invalid file extension (.xxx). Active loot plugins are:" + list, or usage text | `PluginCore.cs:5726-5775` | +| `meta` | `/vt meta [save\|load] [filename]` | save/load `.met` Meta (state-machine) profile | usage text on bad args | `PluginCore.cs:5776-5813` | +| `opt` | `/vt opt list` / `get [name]` / `set [name] [value]` / `setinall [name] [value]` | list/read/write a VTank setting by name via the reflection-typed `cw`/`gy` value wrappers (double/int/float/string/bool); `setinall` additionally pushes the value into **all** monster-rule profiles | "Usage: /vt opt [list/get/set/setinall]"; "Option set: Invalid option specified."; "Option set: Invalid value specified. Proper type of X is Y." | `PluginCore.cs:5446-5622` | +| `testitem` | `/vt testitem` | classifies the currently selected item against loot rules (forces an ID request first if unidentified) | "TestItem: No item selected." or the rule match / classify text (see `b(fu)` helper) | `PluginCore.cs:5129-5144` | +| `propertydump` | `/vt propertydump` | dumps every retail Property enum value (Int/Int64/Bool/Float/String/DataId/InstanceId + link/resource values + palette entries) known for the selected object | "Propertydump: Either no object selected or current selection object is invalid." | `PluginCore.cs:5981-5996`, dump body `PluginCore.cs:4600-4730` | +| `addnavpt` | `/vt addnavpt [coords]` or bare | appends a route waypoint at the given `sCoord` or the player's current position; refused while nav type is `Target` | "Usage: /vt addnavpt [coords] OR /vt addnavpt" | `PluginCore.cs:6111-6158` | +| `refresh` | `/vt refresh` | re-reads settings (`f3.a()`), refreshes editor pages | "Refreshed settings pages." | `PluginCore.cs:5145-5151` | +| `getdb` | `/vt getdb` | triggers `dz.y.c()` (VTank's bundled game-info DB re-fetch/reload) | none printed by the command itself | `PluginCore.cs:5623-5627` | +| `addnavjump` | `/vt addnavjump [heading] [shift] [ms] [strafeleft\|straferight\|forward]` | appends a jump waypoint to the route (heading in degrees, `shift`=walk/run bool, `ms`=charge time, optional strafe direction, default forward) | two-line usage text on bad args | `PluginCore.cs:4928-4952`, `6231-6246` | +| `addnavcheckpoint` | `/vt addnavcheckpoint [coords]` or bare | same as `addnavpt` but inserts a `gr` (checkpoint) waypoint, not a `fn` (point) | "Usage: /vt addnavcheckpoint [coords] OR /vt addnavcheckpoint" | `PluginCore.cs:6159-6206` | + +### Actions + +| Verb | Syntax | Effect | Output / error | `file:line` | +|---|---|---|---|---| +| `start` | `/vt start` | `dz.d.StartMacro()` | none | `PluginCore.cs:6035-6039` | +| `stop` | `/vt stop` | `dz.d.StopMacro()` | none | `PluginCore.cs:6040-6044` | +| `forcebuff` | `/vt forcebuff` | `dz.k.i()` (starts the self-buff-only decision cycle) | "Force buff enabled." | `PluginCore.cs:6045-6050` | +| `cancelforcebuff` | `/vt cancelforcebuff` | `dz.k.e()` | "Force buff canceled." | `PluginCore.cs:6051-6056` | +| `setmetastate` | `/vt setmetastate [state]` | transitions the Meta state machine to `state` (case-sensitive); falls back to `"Default"` if the name is unknown | "Usage: /vt setmetastate [somestate]" + case-sensitivity note; "Warning: Attempted to set an unused state. Setting to default instead." | `PluginCore.cs:6057-6080` | +| `fakedeath` | `/vt fakedeath` | fires the internal death-event handler with null args (`a((object)null,(DeathEventArgs)null)`) | none | `PluginCore.cs:5997-6001` | +| `deletemonster` | `/vt deletemonster` | forces the client to purge the selected monster object (`f9.f(guid)`) — only if a monster is selected | "Forcing the client to delete NAME (guid)!!" or "Select a monster, then do /vt deletemonster" | `PluginCore.cs:5427-5445` | +| `reverseroute` | `/vt reverseroute` | toggles `dz.o.m` (navigate the route backwards) | "Setting nav backwards to: True/False" | `PluginCore.cs:5213-5219` | +| `reverseroutequery` | `/vt reverseroutequery` | reads `dz.o.m` | "Nav backwards is: True/False" | `PluginCore.cs:5220-5225` | +| `equipitemsfor` | `/vt equipitemsfor [monster name]` | drives one equip-decision step (peace mode, wield weapon, etc.) toward the named monster; explicitly documented as multi-call | "Usage: ..." + two-line note; "Changing items for monster \"X\", ready: {bool}" | `PluginCore.cs:5169-5192` | +| `mexec` | `/vt mexec [expression]` | evaluates a Meta expression via `dz.at.k.a` and prints the token or error | "MExec evaluating expression: \"X\""; "Result: X" or "Expression error: X" | `PluginCore.cs:5014-5032` | +| `echo` | `/vt echo [text]` | posts `text` verbatim through `eChatType.Echo` | the text itself | `PluginCore.cs:5033-5040` | +| `tapjump` | `/vt tapjump` | `dz.aw.a(100.0)` — a fixed 100 ms jump tap | none | `PluginCore.cs:4896-4900` | +| `jump` | `/vt jump [heading] [shift] [ms] [strafeleft\|straferight\|forward]` | performs one jump immediately (does not touch the route) — same arg grammar as `addnavjump` | two-line usage text | `PluginCore.cs:4901-4926`, `6212-6230` | +| `setattackbar` | `/vt setattackbar [0..1]` | sets the retained attack-power slider (`f9.a(value)`) | "Usage: /vt setattackbar [0 to 1]" if out of `[0,1]` or unparsable | `PluginCore.cs:6086-6110` | + +### Game info + +| Verb | Effect | Output | `file:line` | +|---|---|---|---| +| `dumpspells` | writes every `MySpell` (id, name, family/override, saying, duration, difficulty, fellowship/offensive/untargeted/instant flags, school, turn-to requirement) to `c:\spelldump.txt` | "Spelltable dump complete. (c:\spelldump.txt)" | `PluginCore.cs:5231-5312` | +| `dumpspecies` | writes the species id/name table to `c:\speciesdump.txt` | "Species dump complete. (...)" | `PluginCore.cs:5313-5334` | +| `dumpmats` | writes the material id/name table to `c:\matdump.txt` | "Material dump complete. (...)" | `PluginCore.cs:5335-5356` | +| `dumpskills` | writes id/name/credits-to-train/specialize/description/attributes to `c:\skilldump.txt` | "Skill dump complete. (...)" | `PluginCore.cs:5357-5402` | + +All four are **hardcoded to the `C:\` drive root** — no filename argument, no +configurable directory. + +### Debug + +| Verb | Effect | Output | `file:line` | +|---|---|---|---| +| `log` | bare: report active `eLogState` flags; `[type] [on\|off]`: toggle one flag (`ActiveRule SalvageList SpellCast RuleInfo Timers CastInfo DebuffChoice Loot CharProps Misc BusyState`, or `All`) | state summary / "Set X" / "Reset X" | `PluginCore.cs:5814-5942` | +| `testmonster` | evaluates monster-rule attack-distance targeting for the selected monster | "TestMonster: No monster selected." or evaluation result + `allowedtarget` | `PluginCore.cs:4864-4880` | +| `lockdump` | dumps `dz.o.f()` (unstoppably-busy flag) + every held `ActionLockType` with expiry and expired-yet? bool | multi-line dump | `PluginCore.cs:5109-5118` | +| `dumptracker` | writes every tracked world object plus a full property dump for each to `c:\trackerdump.txt` | none in chat | `PluginCore.cs:5403-5426` | +| `clearlocks` | `dz.o.n.a().Clear()` | none | `PluginCore.cs:5119-5123` | +| `clearbusy` | `dz.o.e()` | none | `PluginCore.cs:5124-5128` | +| `listmonstervariables` | lists supported monster-rule expression variable names (`cl.b()`) | comma-joined list | `PluginCore.cs:5193-5212` | +| `dumpmetavars` | lists every assigned Meta expression variable | `name = value` per line | `PluginCore.cs:5041-5049` | +| `listmetafunctions` | lists builtin Meta expression functions, wrapped at ~250 chars/line | comma-joined, chunked | `PluginCore.cs:5075-5108` | +| `metafunchelp [name]` | prints one function's description, parameter count, and per-parameter name/type/description | "Function not defined \"X\"" or the multi-line block | `PluginCore.cs:5050-5074` | +| `fakeimp` | casts a hardcoded fake "Gossamer Flesh" imperil (`dz.f.b("Gossamer Flesh")`, `PC.d(id, target, 3000000)`) at the current selection | "Fake cast complete." | `PluginCore.cs:6002-6012` | +| `pscount` | reports the portal-space toggle counter | "Portal space toggle count: N" | `PluginCore.cs:6081-6085` | +| `testspell [id]` | resolves a `MySpell` by id and reports range, school, computed/raw skill, castability under both hunting and buffing thresholds | "Usage: /vt testspell [spellid]"; "Invalid spellid."; multi-line report | `PluginCore.cs:4953-5013` | +| `testpet` | times `bm.a()` (can-pet-spawn probe) | "Pet can spawn: X, test time: Yms" | `PluginCore.cs:4818-4826` | + +### Undocumented (not in the `/vt help` catalog, still parsed) + +These exist in the parser but are absent from the four `/vt help` lines +(`PluginCore.cs:4765-4768`) — pure engineering scaffolding: `test`, +`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`. + +**No `/vt pause`.** Only `start` and `stop` exist in the parser; there is +no separate pause verb in this decompiled build. + +## 2. Chat output conventions + +### `eChatType` and the `d5` sink + +`eChatType` (`uTank2/eChatType.cs:3-11`): `CommandLine, Logging, Warnings, +Errors, MetaUpdates, Echo`. + +`d5` (`d5.cs`) is the single sink every command handler calls through +(`a(...)` at `PluginCore.cs:4298` wraps `d5.a(eChatType.CommandLine, text)` +directly; other call sites use `d5.a` with an explicit type). At +`PluginInitComplete` it registers a Decal "output preset" per type +(`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` +(if VCS5 is loaded and running) or falls back to +`PluginHost.Actions.AddChatText(text, color, target)` per target window +(`fh.cs:78-89`): + +| `eChatType` | color idx | target window(s) | description string | +|---|---|---|---| +| `CommandLine` | 7 | 1 | "Generic plugin text" | +| `Logging` | 2 | 5 | "/vt log messages" | +| `Warnings` | 6 | 1 | "Non-fatal warnings" | +| `Errors` | 6 | 1,2,3,4,5 | "Errors which stop operation" | +| `MetaUpdates` | 7 | 1 | "Meta status updates" | +| `Echo` | 14 | 1,2,3,4,5 | "Messages from /vt echo" | + +(`d5.cs:30-35`). If VTank is running inside `Direct3D9_Container` or +`DecalContainer` (`d5.b()`, `d5.cs:48-52`) every line is also mirrored to +`Console.WriteLine`/`Debug.Print` for the developer console (`d5.cs:85-89`). + +### `ah` — the deduped-warning sink + +`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`). + +### Input-box text injection (distinct mechanism) + +Separate from `d5.a` (which posts to chat *output*), `ab.a(int, string)` +(`ab.cs:67-...`) drives synthetic keyboard input (`GetKeyboardState` + +an internal virtual-key queue) to **type text into the chat entry box** +without submitting it — used for "click here" style prompts, e.g. +`/vt loot new ` is typed into the box (cursor left after the trailing +space) so the player only has to supply a filename +(`PluginCore.cs:6261,6335,6339,6357,6968`). This is keystroke simulation, +not a chat message. + +## 3. Exported plugin API + +VTank's cross-plugin surface has three tiers, from least to most gated. + +### Tier 1 — ungated public members directly on `PluginCore` + +`PluginCore` keeps a public static self-reference +(`public static PluginCore PC;`, `PluginCore.cs:937`). Because classic +Decal/Virindi plugins share one process/AppDomain, **any** other plugin +assembly can reflect over `AppDomain.CurrentDomain.GetAssemblies()`, +find `uTank2.PluginCore`, read the static `PC` field, and call any public +member with no permission check at all. This tier includes: + +- Public **events**: `AuthorizationComplete`, `NavWaypointChanged`, + `NavRouteChanged`, `MacroStateChanged`, `ProfileChanged`, + `LootProfileChanged`, `SpellCastComplete`, `SpellCastAttempting`, + `AllSpellsExpired`, `RequestSpreadLock`, `RequestTargetSelection`, + `SetInvalidTarget` (`PluginCore.cs:1040-1360`). +- Public **methods**, notably the `F*`-prefixed family meant for other + Decal plugins to query VTank's live world/loot state: + `FLootPluginClassifyImmediate`, `FLootPluginQueryNeedsID`, + `FLootPluginClassifyCallback` (async callback list keyed by object id, + `PluginCore.cs:3082-3148`), `FWorldTracker_GetAllInInventoryWithName`, + `FWorldTracker_GetInventory`, `FWorldTracker_GetInContainer`, + `FWorldTracker_GetWithName`, `FWorldTracker_GetWithObjectClass`, + `FWorldTracker_GetWithID`, `FWorldTracker_GetWithVendorObjectTemplateID`, + `FWorldTracker_CountStackedInventoryObjectsWithName`, + `FGameInfo_QueryAutoDamageElementList`, `FMonsterList_QueryFinalDamageType` + (`PluginCore.cs:3150-3268`), plus `PushStackCramSettings`/ + `PopStackCramSettings` (a save/restore stack for the `AutoStack`/`AutoCram` + settings, `PluginCore.cs:3269-3287`). + +### Tier 2 — the RSA-gated trusted relay + +`GetExternalInterface()` (no args) always returns `null` +(`PluginCore.cs:2351-2354`) — it is a dead stub. The real entry point is: + +``` +public cExternalInterfaceTrustedRelay GetExternalInterface( + string CallerAssemblySignature, string AssemblyKey) +``` +(`PluginCore.cs:2537-2553`). It looks `AssemblyKey` up in `c8` +(`MyDictionary`, `PluginCore.cs:885`), +loaded at startup from `signkeys.txt` next to `signkeys.txt.signature.txt` +in VTank's data directory (`cu.g` analog — actual loader is +`PluginCore.g()`, `PluginCore.cs:2567-2598`): the signature file is itself +RSA-verified against a hardcoded public key +(`PluginCore.cs:2582`), and each remaining line pair is +`AssemblyKey` / `int permissionFlags`. `AssemblyKey` doubles as a +compact RSA public-key blob (`Modulus~Exponent`, url-safe-base64, +`PluginCore.cs:2405-2535`): the caller must supply +`CallerAssemblySignature` = the SHA1 hash of its own calling assembly, +signed with the **private** half of that same key +(`PluginCore.cs:2375-2387`). Only a partner whose public key VTank's +author has hand-added to `signkeys.txt` — and RSA-signed with VTank's own +master key — can obtain a non-`None` relay. +`RequestKeyVerificationDownload(string)` (`PluginCore.cs:2555-2557`) is an +**empty stub in this build** — see §6. + +`cExternalInterfaceTrustedRelay` (`PluginCore.cs:34-586`) is a nested +class holding one `eExternalsPermissionLevel` flags field +(`ReadSettings=1, WriteSettings=2, FullUnderlying=4, LogicObject=8`, +`uTank2/eExternalsPermissionLevel.cs`); every member calls a private +guard `a(eExternalsPermissionLevel required)` that throws +`"Permission denied..."` if the flag isn't set (`PluginCore.cs:579-585`). +Members, by required flag: + +| Flag | Members | +|---|---| +| `LogicObject` | `LogicObject` property (returns `dz.d`, the macro-engine object); `SpellSystem_GetSpellById/ByName`, `SpellSystem_QueryCombatSpellForElement`, `SpellSystem_QueryBestSpellAvailableInSameFamilyAsSpellNamed`, `SpellSystem_CastNormalSpell`, `SpellSystem_CastEquippedWandSpell`, `SpellSystem_CanUseWandSpell`; `Decision_Lock/UnLock/IsLocked`; `Equipment_TryEquipAnyWand`, `Equipment_TryEquipWandWeapon`, `Equipment_TryEquipWandWeaponWithArrows`; `Decision_GlobalBusyFlag` getter | +| `ReadSettings` | `GetSetting`, `GetSettingType` | +| `WriteSettings` | `SetSetting` (5 overloads), `ResetPanels` | +| `FullUnderlying` | `Version`, `CurrentMetaState`, `CurrentRulePriority`, `NavCurrent`, `NavNumPoints`, `NavType`, `MacroEnabled`, `HelperBonusTimeoutSeconds`, `HelperBonusActionWaitSeconds`, `ForceBuff`, `CancelForceBuff`, `ShowMainUI`, `IncrementBusyFlag`, `DecrementBusyFlag`, `AcceptSpreadLockTarget`, `SetLockedByOther`, `GetByAction`, `SetSpreadFire`, the `Nav*` waypoint CRUD family (`NavGetPoints/Point`, `NavBeginChanges/EndChanges`, `NavSetPoint`, `NavSetPointPortal2`, `NavSetUseNPC`, `NavSetOpenVendor`, `NavRouteClear`, `NavSetFollowTarget`, `NavGetFollowTargetInt/String`, `NavInsertPoint`, `NavDeletePoint`), the profile get/load family (`Get/LoadSettingsProfile`, `Get/LoadNavProfile`, `Get/LoadLootProfile`, `Get/LoadMetaProfile`), `HelperPlayerUpdate/SetInvalid`, `LogSpellCast`, `LogAllSpellsExpired`, `SetAnInvalidTarget`, `LogCastAttempt`, `SetMacroSetting`, `NeedToBuffInNext`, `GetCustomLootActionItems`, `Equipment_TryEquipWeaponsForMonster` | + +`PermissionLevel` itself is always readable, and `Decision_GlobalBusyCount` +has **no guard at all** (`PluginCore.cs:161`, a getter-only oversight +relative to its `LogicObject`-gated sibling `Decision_GlobalBusyFlag`). + +### MosswartMassacre's real usage (call-by-call) + +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)` + 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 + 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. +2. Immediately reflects the private field `m_a` (aliased `"a"` in the + decompile) and force-sets it to `15` — i.e. `ReadSettings|WriteSettings| + FullUnderlying|LogicObject`, every flag at once — completely bypassing + the RSA gate. +3. Separately reflects VTank's internal chat-message class (assembly-private + type `"a7"`, its static field `"a"`) to obtain the **live outbound chat + queue** (`IList`) VTank itself drains, so `Tell(message, color, target)` + (`vTank.cs:99-113`) can enqueue a chat line that VTank's own Meta + condition matcher will see as if it came from the game. +4. `Decision_Lock`/`Decision_UnLock` (`vTank.cs:75-90`) call straight through + to the relay's `LogicObject`-gated members — this is the one path that + *would* have worked legitimately if the permission flag were honestly + granted. + +`VtankControl.cs` layers a small helper API on top of the (illegitimately +acquired) `vTank.Instance`: + +- `VtSetMetaState` (`VtankControl.cs:17-22`) does **not** call + `CurrentMetaState`'s setter — it instead calls + `PluginCore.Decal_DispatchOnChatCommand("/vt setmetastate {state}")`, + MosswartMassacre's own P/Invoke wrapper around Decal's + `DispatchOnChatCommand` export (`MosswartMassacre/PluginCore.cs:1289-1305`) + — i.e. it re-injects a **fabricated chat command string** for VTank's own + chat-parser hook to consume, rather than touching the object graph. This + 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`, + `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 + waypoint index**. The comment block documents the author's own search + through the decompile (`"From decompiled code: external interface uses + PC.NavCurrent which references dz.o.l"`) before falling back to (a) + 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 + `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. + +## 4. Classic Looter ↔ VTank boundary + +### The contract (`uTank2.LootPlugins`) + +`LootPluginBase` (`uTank2.LootPlugins/LootPluginBase.cs:3-28`) is the +abstract SPI a loot plugin implements: + +| Member | Purpose | +|---|---| +| `LootPluginInfo Startup()` | one-time init; return value declares the profile file extension (and optional extra search directories) | +| `void Shutdown()` | teardown | +| `void LoadProfile(string filename, bool newprofile)` | load an existing profile or create a blank one | +| `void UnloadProfile()` | clear active profile | +| `void OpenEditorForProfile()` / `CloseEditorForProfile()` | editor lifecycle hooks (VTank shows/hides its own "Loot" checkbox based on these, `cu.cs:105-134`) | +| `bool DoesPotentialItemNeedID(GameItemInfo item)` | VTank asks this **before** classifying an unidentified item, to decide whether to send an Inquiry first | +| `LootAction GetLootDecision(GameItemInfo item)` | the actual keep/salvage/sell/etc. decision | + +`LootPluginInfo(string ProfileFileExtension, params string[] ExtraDirectories)` +(`uTank2.LootPlugins/LootPluginInfo.cs:9-20`) normalizes the extension +(strip leading dot, lowercase). Optional capability interfaces extend the +contract, e.g. `ILootPluginCapability_SalvageCombineDecision2` (adds +`ChooseBagsToCombine(List)`, used by Classic Looter's own +salvage-combine block handler) and `ILootPluginCapability_GetExtraOptions` +(lets a plugin hide VTank's editor checkbox, `cu.cs:96-108`). + +### Registration (discovery, load, wiring) + +VTank discovers loot plugins from the **Windows registry**, not from a +plugin folder scan: `cu.i()` (`cu.cs:228-312`) opens +`HKLM\Software\Decal\LootPlugins`, iterates subkeys, and for each reads +string values `""` (display name), `Assembly` (dll filename), `Object` +(fully-qualified type name), `Path` (containing directory). It loads the +assembly (`Assembly.Load(new AssemblyName{CodeBase=...})`), resolves the +type, checks `type.IsSubclassOf(typeof(LootPluginBase))`, and +`Activator.CreateInstance`s it. On success it wires the instance's +internal `Host`/`ViewSystem` fields to a fresh `VTHost`/ +`FlexibleViewSystem` and calls `Startup()`; a null/throwing `Startup()` +unregisters the plugin (`cu.cs:290-310`). The result is stored as an `e8` +record (`e8.cs:6-16`: `LootPluginBase a; LootPluginInfo b; Assembly c; +string d,e; eLootPluginExtraOption f;`) in a `List` (`cu.c`). + +Only **one** loot plugin can be active at a time (`cu.m_a`, singular); +`/vt loot load` / `/vt loot new` (`PluginCore.cs:5726-5775`) match the +requested filename's extension against the registered `e8.b.a` values, +select that plugin, and call its `LoadProfile`. Classifying an item routes +through `cu.a(int objectId)` → `this.m_a.a.GetLootDecision(new +GameItemInfo(objectId))` (`cu.cs:176-195`), and needs-ID checks through +`cu.b(int objectId)` → `DoesPotentialItemNeedID` (`cu.cs:155-174`). + +### Classic Looter's implementation (`VTClassic.LootCore`) + +`LootCore : LootPluginBase, ILootPluginCapability_SalvageCombineDecision2` +(`refs/vtank-classiclooter/decompiled/VTClassic/LootCore.cs:9-199`): +`Startup()` returns `new LootPluginInfo("utl", new string[0])` +(`LootCore.cs:167-180` — extension `.utl`, no extra directories) and +records the static singleton `Instance`. `GetLootDecision` delegates to +`cLootRules.Classify(item, out matchedrulename, out data)`, which returns +VTank's internal `eLootAction` enum (`Keep, NoLoot, Salvage, KeepUpTo, +Sell`), translated 1:1 to the public `LootAction` type +(`LootCore.cs:53-91`, with `KeepUpTo` carrying the count via +`LootAction.GetKeepUpTo(data)` and the matched rule name attached to +`val.RuleName`). `LoadProfile`/`UnloadProfile` own a `cLootRules` instance +directly from/to a `.utl` file with a `CountedStreamWriter` +(`LootCore.cs:93-143`). This is the whole boundary: VTank never sees rule +internals, only the four-verb `LootAction` result plus a display name for +the matched rule. + +No installer artifact for the registry-key registration itself +(`HKLM\Software\Decal\LootPlugins\`) is present in the vendored +`refs/vtank-classiclooter` tree — see §6. + +## 5. "MossTank gap" vs `src/AcDream.Plugins.MossTank` + +### What acdream already ports faithfully + +- **`/vt` chat surface.** `MossTankCommands.cs` (`MossTankPanel`, partial + class) ports essentially every retail production verb from §1's + Profiles/Actions/Game-info tables — `help/start/stop/forcebuff/ + cancelforcebuff/settings/nav/loot/meta/opt/setmetastate/mexec/echo/ + setattackbar/tapjump/jump/addnavjump/addnavpt/addnavcheckpoint/ + reverseroute/reverseroutequery/deletemonster/equipitemsfor/testitem/ + propertydump/testmonster/testspell/testpet/listmonstervariables/ + dumpmetavars/listmetafunctions/metafunchelp/fakedeath/pscount/refresh/ + getdb/log/lockdump/dumptracker/clearlocks/clearbusy/fakeimp/dumpspells/ + dumpspecies/dumpmats/dumpskills` (`MossTankCommands.cs:48-223`) — with + the same usage strings, same case-sensitivity notes, and the same + argument grammar (e.g. jump's `heading shift ms [direction]`, + `MossTankCommands.cs:491-524` vs `PluginCore.cs:4901-4952`). The + retail-only pure-debug verbs (`test, waterdepth, physobj, testcollision, + clearshapes, zoneid, activespells, burdentest, explain, debugon, + debugdump, skillvalues, throwex, spheredist, obtest`) are correctly + **not** ported — they were VTank's own engineering scaffolding, never + 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 + vocabulary reproduced verbatim as `PluginLootAction` (`NoLoot, Keep, + Salvage, Sell, Read, User1..User5, KeepUpTo`, + `LootClassifierPlugins.cs:4-17`) — but registration is **in-process, + machine-local, and lifetime-scoped to the owning plugin** + (`LootClassifierPlugins.cs:54-58`), not a signed-DLL/registry-key/ + `Assembly.Load` discovery pass. This is a deliberate simplification: no + loot-plugin author needs a Windows registry entry or a separate + assembly to extend acdream's classifier. +- **Chat-command routing.** `IPluginCommandRegistry`/ + `PluginCommandRegistry` (`PluginCommands.cs:15-23`, + `PluginCommandRegistry.cs:10-142`) replace Decal's + `ChatParserInterceptEventArgs.Eat` chain-of-responsibility with an + **exclusive verb ownership** model: `Register(verb, handler)` throws if + the verb is already claimed (`PluginCommandRegistry.cs:29-33`) instead of + letting multiple plugins race to "eat" the same text. This is simpler + and safer but means acdream cannot reproduce VTank's own precedent of + silently falling through to a second handler for an unclaimed verb — by + design, every verb has exactly one owner. +- **Cross-instance telemetry**, not cross-instance control: + `LocalPluginPeerRegistry` (`LocalPluginPeerRegistry.cs:12-225`) is + acdream's analogue of VTank's `uTank2.P2P` folder — but it is + explicitly **data-only** ("The files carry data only—never commands.", + `LocalPluginPeerRegistry.cs:9-11`), publishing bounded heartbeat JSON + documents to a shared directory and reading back other live clients' + position/vitals/tags (`INetworkAutomation.CaptureClients()`, + `NetworkAutomation.cs:21-25`, explicitly "Read-only discovery"). + +### The gap — five most important interop pieces a ported MosswartMassacre would need and does not have + +1. **No cross-plugin object reference at all.** VTank's tier-1 surface + (public events + `F*` methods reachable via the public static `PC` + field, §3) and tier-2 relay both assume one plugin can obtain a live + reference to *another loaded plugin's* running instance in the same + process. acdream's `IPluginHost`/`IAcDreamPlugin` surface + (`IPluginHost.cs`, `IAcDreamPlugin.cs`) has no enumeration of other + loaded plugins, no "get plugin by name/id" call, and no shared + interface a second plugin could implement to be discovered — there is + currently no way for one acdream plugin to reach into another's state + the way MosswartMassacre reaches into VTank's `PC` field or relay. +2. **No signed-partner / permission-tier concept.** VTank's + `eExternalsPermissionLevel` + RSA `signkeys.txt` handshake (§3, Tier 2) + has no analogue — acdream's plugin command/loot-classifier registries + are unauthenticated by design (any loaded plugin can call `Register`), + which is *simpler* but means there is no way to expose a **privileged** + subset of one plugin's API to only a trusted second plugin, the way + VTank could grant `LogicObject`-level spell-casting control to one + named partner assembly and nothing to everyone else. +3. **No fabricated-chat-command injection path, and no "advance waypoint"- + style gap-filler.** MosswartMassacre's `VtSetMetaState` and + `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 + 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 + to programmatically submit a command into *another* plugin's verb + table. +4. **No public event surface on `MossTankPlugin`/`MossTankPanel` at all** + (no `SpellCastComplete`, `ProfileChanged`, `MacroStateChanged`, + `NavWaypointChanged`/`NavRouteChanged`, `AllSpellsExpired`, + `RequestSpreadLock`/`RequestTargetSelection` equivalents). A ported + MosswartMassacre that wants to react to MossTank's macro + starting/stopping, its Meta state changing, or a spell cast completing + would need new events added to the plugin (or to + `AcDream.Plugin.Abstractions`) — none exist today. +5. **No settings-by-name get/set surface for cross-plugin config.** + VTank's relay exposes `GetSetting`/`SetSetting`/`GetSettingType` by + string name, reflecting over its own typed setting store; MossTank's + equivalent state (`_combatSettings`, `_inventorySettings`, etc., + referenced throughout `MossTankCommands.cs`) is private to the plugin + with no by-name accessor exposed through + `AcDream.Plugin.Abstractions` for a second plugin to read or write — + only the plugin's own `/vt opt` chat verb can touch it, and only a + human (or a plugin willing to fabricate chat text through the *host's* + own submit path, not another plugin's) can drive that verb. + +## 6. Could not determine + +- **`RequestKeyVerificationDownload`'s real implementation.** The method + body is empty in this decompiled build (`PluginCore.cs:2555-2557`) — it + is unclear whether this build ever shipped a live network-backed key + request/download flow, or whether trusted-partner keys were always + distributed out-of-band (the author manually appending a line to + `signkeys.txt` for a named partner). No network call, URL, or server + contract for this flow exists anywhere in `refs/vtank/decompiled/`. +- **The actual current contents of `signkeys.txt`** (which third-party + assemblies, if any, hold a real signed key and at what permission level) + are not part of the decompiled source; the file ships alongside the + installed plugin, not in this repository's `refs/`. +- **How Classic Looter's own registry key + (`HKLM\Software\Decal\LootPlugins\`) gets written at install + 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.