docs(vt): VT1 catalog 09 — /vt commands, chat sinks, and VTank's plugin interop surface

~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 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-06 19:42:51 +02:00
parent 8872a9286a
commit cf6d5b94bb

View file

@ -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<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`).
### 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<string, eExternalsPermissionLevel>`, `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<GameItemInfo>)`, 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<e8>` (`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\<name>`) 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\<name>`) 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.