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:
parent
8872a9286a
commit
cf6d5b94bb
1 changed files with 517 additions and 0 deletions
517
docs/research/vtank-kb/09-commands-and-interop.md
Normal file
517
docs/research/vtank-kb/09-commands-and-interop.md
Normal 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue