docs: plugin architecture design
Captures the day-1 plugin + scripting contract for acdream: C# plugins via collectible AssemblyLoadContext with a stable IPluginHost API, Lua macros via a first-party Macros plugin that embeds MoonSharp, and a four-stage packet pipeline for raw and parsed traffic in both directions. Extends the phased MVP so every core system is exposed through the plugin API in the same commit that adds it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
020ec2a35d
commit
c016ab54fd
1 changed files with 207 additions and 0 deletions
207
docs/plans/2026-04-10-plugin-architecture-design.md
Normal file
207
docs/plans/2026-04-10-plugin-architecture-design.md
Normal file
|
|
@ -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 `<exe dir>/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 `<appdata>/acdream/plugin-data/<pluginId>/`. 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 `<appdata>/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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue