acdream/docs/plans/2026-04-10-plugin-architecture-design.md
Erik c016ab54fd 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>
2026-04-10 09:11:52 +02:00

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:

  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 AssemblyLoadContexts 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)

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.
-- 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.