diff --git a/docs/plans/2026-04-10-plugin-architecture-design.md b/docs/plans/2026-04-10-plugin-architecture-design.md new file mode 100644 index 0000000..cf572c8 --- /dev/null +++ b/docs/plans/2026-04-10-plugin-architecture-design.md @@ -0,0 +1,207 @@ +# acdream plugin architecture design + +**Date:** 2026-04-10 +**Status:** Approved — design locked, ready for implementation planning. +**Supersedes:** original 7-phase MVP from session planning (now extended to integrate plugin scaffolding per phase). + +## Goal + +Give acdream a plugin and scripting system from day 1 so that player-authored automation (macros, overlays, combat assist, dashboard integrations) is a first-class citizen — not a retrofit. Roughly DECAL-era power, on a modern .NET stack, without DECAL compatibility. + +## Non-goals + +- DECAL plugin binary compatibility. Existing DECAL plugins (including the user's own `MosswartMassacre`) do not load unchanged. A future shim layer is possible but out of scope. +- Process isolation. Plugins run in-process, same trust level as the client. Appropriate for local test use. +- Cryptographic plugin signing, remote plugin repositories, or auto-update. Manual install only. +- Python or JavaScript scripting. Only C# (native plugins) and Lua (macro layer) are supported. + +## Architecture overview + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ acdream client │ +│ │ +│ ┌──────────────┐ ┌───────────────┐ ┌─────────────────────────┐ │ +│ │ Network │──▶│ Protocol │──▶│ World state │ │ +│ │ (UDP+ISAAC) │ │ (parse msgs) │ │ (entities, player, …) │ │ +│ └──────┬───────┘ └──────┬────────┘ └──────────┬──────────────┘ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Plugin pipeline (middleware) │ │ +│ │ [raw-in] → [parsed-in] → [state] → [parsed-out] → [raw-out] │ │ +│ │ plugins register handlers at any stage: observe, modify, │ │ +│ │ drop, or inject │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌──────────┐ ┌─────────────┐ ┌────────────────┐ │ +│ │ Rendering│ │ Plugin host │ │ UI chrome │ │ +│ │(Silk.NET)│◀─────│ (ALC) │ │ (Avalonia) │ │ +│ └──────────┘ └──────┬──────┘ └────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────┐ │ +│ │ C# plugin DLLs │ ← one is "Macros", │ +│ │ from plugins/ │ which embeds MoonSharp │ +│ └─────────────────┘ and hosts Lua scripts │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +**Key principles:** + +1. Core modules (`Network`, `Protocol`, `World`, `Render`, `UI`) expose events and actions through a stable **plugin API** surface. +2. A **plugin pipeline** sits between the core modules. Every raw packet, parsed message, state delta, and outbound action passes through a middleware chain plugins hook into. +3. **Plugins** are C# DLLs dropped into a `plugins/` directory, loaded into individual collectible `AssemblyLoadContext`s on startup. +4. **Lua/macro support** is provided by a first-party C# plugin (`AcDream.Plugins.Macros`) that embeds MoonSharp. The core has no knowledge of Lua. Users write Lua scripts that the Macros plugin sandboxes, loads, and dispatches. +5. The plugin API lives in its own assembly — `AcDream.Plugin.Abstractions` — published as a local NuGet package. The core never references plugin code; only the abstractions. Plugins never reference the core directly; only the abstractions. + +## Plugin lifecycle + +``` +discover → load → init → enable → (run) → disable → unload +``` + +- **discover** — on startup, scan `/plugins/*/plugin.json`. Each plugin lives in its own subfolder with a manifest: `id`, `displayName`, `version`, `entryDll`, `apiVersion`, optional `dependencies[]`. +- **load** — for each manifest, create a collectible `AssemblyLoadContext`, load the entry DLL, find the class implementing `IAcDreamPlugin`, instantiate via its parameterless constructor. +- **init** — call `plugin.Initialize(IPluginHost host)`. The host hands over the API surface. The plugin stores references it needs. No game state exists yet. +- **enable** — called after the client has connected to a world (or, for UI-only plugins, after the main window is up). Plugin may register hooks, allocate resources, start background work. +- **disable** — called on disconnect, shutdown, or user-initiated disable. Plugin releases hooks and frees resources. +- **unload** — the ALC is unloaded. The plugin is gone. A reload cycles through `unload → load → init → enable` with the new DLL. + +**Failure isolation:** exceptions thrown in plugin callbacks are logged, the plugin is marked `Faulted`, and its hooks are removed from all pipelines. The core continues running. A faulted plugin can be re-enabled from the UI once the author fixes and drops a new DLL. + +**Hot reload:** supported but with standard collectible-ALC gotchas. Plugins must not let host objects capture references to plugin-defined types across reload, or the ALC leaks. The API discourages this by returning host types, not plugin types. + +## Plugin API surface (`AcDream.Plugin.Abstractions`) + +```csharp +public interface IAcDreamPlugin { + void Initialize(IPluginHost host); + void Enable(); + void Disable(); +} + +public interface IPluginHost { + IGameState State { get; } // read-only view of current world state + IActions Actions { get; } // synthesize actions (move, cast, chat, use) + IEvents Events { get; } // subscribe to high-level events + IPacketPipeline Packets { get; } // hook raw+parsed messages, both directions + IOverlay Overlay { get; } // draw into a plugin overlay layer on top of the 3D view + IHttp Http { get; } // outbound HTTP (for dashboards like MosswartOverlord) + ILogger Log { get; } // structured logging + IStorage Storage { get; } // plugin-scoped key/value persistence + IPluginCatalog Catalog { get; } // enumerate other plugins, obtain interop shims +} +``` + +**Surface details:** + +- `IGameState` returns immutable snapshots. Plugins never mutate core state directly. Mutation flows only through `IActions`, which translates to outbound protocol messages. +- `IEvents` is high-level: `ChatReceived`, `HealthChanged`, `EntitySpawned`, `EntityDespawned`, `EntityDied`, `MovementCompleted`, etc. Events fire *after* the packet pipeline has run, so they reflect final world state. +- `IPacketPipeline` is the low-level hook. Plugins register middleware at one of four stages: **inbound-raw**, **inbound-parsed**, **outbound-parsed**, **outbound-raw**. Returning `Passthrough`, `Modify`, `Drop`, or `Inject` lets plugins observe, rewrite, drop, or synthesize. +- `IOverlay` gives each plugin an immediate-mode drawing handle scoped to its own Z-ordered layer. No z-fighting between plugins. +- `IHttp` is a sanctioned HTTP client for outbound calls to user-owned services (MosswartOverlord and similar). Plugins can use `HttpClient` directly too, but this one is logged and rate-limitable. +- `IStorage` persists to `/acdream/plugin-data//`. Scoped per plugin. +- `IPluginCatalog` enables interop: `MosswartMassacre` can query the catalog for `"acdream.macros"`, retrieve a typed shim, and dispatch Lua macros from inside its own C# code. + +## Packet pipeline + +``` + network socket + │ + ▼ + ┌──────────────────┐ + │ inbound-raw │ ← post-ISAAC-decrypt, pre-parse + └────────┬─────────┘ + ▼ + parser (protocol classes) + │ + ▼ + ┌──────────────────┐ + │ inbound-parsed │ ← strongly-typed GameMessage objects + └────────┬─────────┘ + ▼ + world state update + │ + ▼ + high-level event dispatch → IEvents.* + │ + ▼ + plugin callbacks → renderer → next tick + + + Actions.Cast(spellId) or similar + │ + ▼ + ┌──────────────────┐ + │ outbound-parsed │ ← typed message; plugin can modify/drop + └────────┬─────────┘ + ▼ + serializer + │ + ▼ + ┌──────────────────┐ + │ outbound-raw │ ← bytes ready for encryption + └────────┬─────────┘ + ▼ + ISAAC encrypt + │ + ▼ + network send +``` + +**Four stages are the minimum cover:** + +- Most plugins want `inbound-parsed` only. Strongly-typed, easy. +- Debugging / protocol tools want `inbound-raw`. Byte-level. +- Exploit detection, movement rewriters want one or both outbound stages. + +**Ordering:** plugins declare integer priority. Middleware runs in priority order per stage. Ties broken by load order. + +## Lua macro layer (`AcDream.Plugins.Macros`) + +`AcDream.Plugins.Macros` is a first-party C# plugin shipped in the default `plugins/` folder. It embeds MoonSharp (pure-C#, MIT, Lua 5.2) and exposes a sandboxed subset of `IPluginHost` to scripts. + +**User flow:** +1. User drops a `.lua` file into `/acdream/scripts/`. +2. Macros plugin detects the file, compiles the script into a new `MoonSharp.Script` instance. +3. Script gets injected globals: `on(eventName, fn)`, `actions`, `state`, `log`. +4. Script registers handlers. When events fire, Macros invokes the handlers from the C# side. + +```lua +-- autoloot.lua +on("EntitySpawned", function(e) + if e.kind == "item" and e.name:find("Gem") then + actions.pickup(e.id) + end +end) +``` + +**Scope:** + +- Each script gets its own `Script` instance; per-script isolation. +- A script that throws is automatically disabled by the Macros plugin, not the whole plugin. +- File-watcher hot reload. +- **No packet pipeline access from Lua.** Macros are for everyday automation. Power users who need packet hooks write a C# plugin. + +## Integration into the phased MVP + +| Phase | Core scope | Plugin work | +|---|---|---| +| 0 | Dat asset inventory. **Done.** | — | +| 1 | Silk.NET window + render one landblock (terrain). | Create `AcDream.Plugin.Abstractions` assembly with minimal types. Plugin host discovers and loads plugins but does nothing yet. | +| 2 | Free-fly camera + static meshes. | Wire `IOverlay`. Dummy plugin draws `"hello from plugin"` as smoke test. | +| 3 | Character Setup + clothing + idle animation. | `IGameState` surfaces initial read-only entity views. `IEvents` emits `EntitySpawned/Despawned` for rendered setups. | +| 4 | ISAAC handshake, character select. | Wire `IPacketPipeline` stages. First `inbound-raw` middleware hook verified end-to-end. | +| 5 | Enter world, WASD movement, position sync. | `IActions.Move()` exposed. `IEvents.HealthChanged` etc wired. **First real Lua macro runs here.** `AcDream.Plugins.Macros` plugin goes live. | +| 6 | Chat, other players, basic interaction. | `IActions.Say()`, `IEvents.ChatReceived`. Macros plugin ships with a `hello.lua` example script. | +| 7+ | Combat, spells, inventory, UI chrome. | Expand API surface incrementally. Every new game system is exposed through the plugin API in the same commit that adds it. | + +**Discipline:** every time we add a game system, we expose it through the plugin API in the same commit. No "we'll plugin-ify this later." + +## Open questions (deferred, not blocking) + +- Plugin UI: Avalonia controls inside overlay, or ImGui-style immediate mode? Defer to Phase 2 when we wire overlay. +- Per-plugin settings UI: do we give plugins a schema-driven settings surface, or is a text config file good enough? Defer to Phase 5. +- MosswartMassacre port strategy: does the user port it fresh to `IAcDreamPlugin`, or build a small DECAL-shim later? Defer until MosswartMassacre is ready to migrate.