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>
14 KiB
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:
- Core modules (
Network,Protocol,World,Render,UI) expose events and actions through a stable plugin API surface. - 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.
- Plugins are C# DLLs dropped into a
plugins/directory, loaded into individual collectibleAssemblyLoadContexts on startup. - 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. - 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, optionaldependencies[]. - load — for each manifest, create a collectible
AssemblyLoadContext, load the entry DLL, find the class implementingIAcDreamPlugin, 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 → enablewith 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)
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:
IGameStatereturns immutable snapshots. Plugins never mutate core state directly. Mutation flows only throughIActions, which translates to outbound protocol messages.IEventsis high-level:ChatReceived,HealthChanged,EntitySpawned,EntityDespawned,EntityDied,MovementCompleted, etc. Events fire after the packet pipeline has run, so they reflect final world state.IPacketPipelineis the low-level hook. Plugins register middleware at one of four stages: inbound-raw, inbound-parsed, outbound-parsed, outbound-raw. ReturningPassthrough,Modify,Drop, orInjectlets plugins observe, rewrite, drop, or synthesize.IOverlaygives each plugin an immediate-mode drawing handle scoped to its own Z-ordered layer. No z-fighting between plugins.IHttpis a sanctioned HTTP client for outbound calls to user-owned services (MosswartOverlord and similar). Plugins can useHttpClientdirectly too, but this one is logged and rate-limitable.IStoragepersists to<appdata>/acdream/plugin-data/<pluginId>/. Scoped per plugin.IPluginCatalogenables interop:MosswartMassacrecan 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-parsedonly. 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:
- User drops a
.luafile into<appdata>/acdream/scripts/. - Macros plugin detects the file, compiles the script into a new
MoonSharp.Scriptinstance. - Script gets injected globals:
on(eventName, fn),actions,state,log. - Script registers handlers. When events fire, Macros invokes the handlers from the C# side.
-- 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
Scriptinstance; 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.