style
esc
  • Loading…
Engineering standardsRev. 2026.10 · Changelog

AxiomStyle Guide

How to write, test, package and ship plugins, game providers and language providers for the Axiom universal modding framework.

22 chapters · templates and checklistsC ABIWASM · .NETProviders

You are reading revision 2026.10, released 2026-10-19. Its text never changes. It is the newest revision. What changed

Chapter 01

Purpose and how to read this guide

Axiom is a universal modding framework: one core that loads plugins into many games, with plugins written in many languages. This guide is for the people who extend Axiom:

  • Plugin authors write the gameplay changes, tools and overlays that players install.
  • Game provider authors teach Axiom how to load into one game and expose that game to plugins.
  • Language provider authors let plugins be written in another language (a WASM engine, a script VM, a native loader).

It covers how to structure, write, test, package and ship each of them so that every plugin and provider looks and behaves the same, and so that a broken one never takes the game down. Code examples use a fictional game, mygame, and a fictional author, acme.

Plugins and providers written in C or C++ also follow the C++ Style Guide. This guide only adds what is specific to Axiom.

Rule levels

Level Meaning
MUST / MUST NOT Required. The registry, CI or review rejects the plugin or provider.
SHOULD / SHOULD NOT The default. Deviate only with a short comment explaining why.
MAY Allowed when it helps; no justification needed.
AVOID Legal but discouraged; expect a review question.

Principles

  • Plugins are guests. They borrow the game's process and frame time. Leave everything as you found it.
  • Go through the API. A plugin talks to the game only through the Axiom API and game API modules; a provider is the only code that touches game internals.
  • Fail small. An error disables one plugin, never the game, the core or another plugin.
  • The lowest tier that works. Write a WASM plugin before a script plugin, and a script plugin before a native one.
  • Version everything. Plugins, providers, game API modules and the ABI each carry a version and declare what they need.
Chapter 02

How Axiom fits together

Every plugin reaches the game only through the C ABI and the core

+-----------------------+  +-----------------------+  +-----------------------+  +-----------------------+
|      WASM plugin      |  |     Script plugin     |  |    Hosted C# plugin   |  |     Native plugin     |
| Rust, C, C++, Zig, C# |  |          Lua          |  |   Full .NET, trusted  |  |  C or C++ via the ABI |
+-----------------------+  +-----------------------+  +-----------------------+  +-----------------------+
            |                          |                          |                          |
            v                          v                          v                          v
+-----------------------+  +-----------------------+  +-----------------------+  +-----------------------+
|     WASM provider     |  |      Lua provider     |  |     .NET provider     |  |    Native provider    |
|   Sandboxed, metered  |  |   One VM per plugin   |  | One load context each |  |  Signed plugins only  |
+-----------------------+  +-----------------------+  +-----------------------+  +-----------------------+
            |                          |                          |                          |
            v                          v                          v                          v
+--------------------------------------------------------------------------------------------------------+
|                                         Axiom C ABI (axiom.h)                                          |
|         Versioned, C only, frozen once released: the single source every SDK is generated from         |
+--------------------------------------------------------------------------------------------------------+
                                                     |
                                                     v
+--------------------------------------------------------------------------------------------------------+
| Axiom Core (C++23)                                                                                     |
|   +----------------+  +----------------+  +----------------+  +----------------+  +----------------+   |
|   | Plugin manager |  |  Hook manager  |  |   Event bus    |  |    Sandbox     |  |  Diagnostics   |   |
|   +----------------+  +----------------+  +----------------+  +----------------+  +----------------+   |
+--------------------------------------------------------------------------------------------------------+
                 |                                   |                                   |
                 v                                   v                                   v
+--------------------------------+  +--------------------------------+  +--------------------------------+
|      Native game provider      |  |     Managed game provider      |  |     Emulated game provider     |
|   Injected, hooks game code    |  |  Bridges .NET or JVM runtimes  |  |    Hooks inside an emulator    |
+--------------------------------+  +--------------------------------+  +--------------------------------+
                 |                                   |                                   |
                 v                                   v                                   v
+--------------------------------+  +--------------------------------+  +--------------------------------+
|        Native PC games         |  |     Managed-runtime games      |  |     Emulated console games     |
+--------------------------------+  +--------------------------------+  +--------------------------------+

Axiom layers: plugins, language providers, the C ABI, the core and game providers.

A plugin never touches a game directly. It calls the C ABI through its language provider; the core checks and applies the call; a game provider turns it into hooks and reads inside one specific game. The launcher (not drawn) starts the game, loads the core and the right game provider, and manages plugin profiles.

Piece Who writes it Owns May touch game memory?
Plugin Plugin authors Gameplay changes, tools, UI No — only through game API modules
Language provider Provider authors Running plugin code in one language, safely No
C ABI (axiom.h) Axiom The contract every plugin and provider builds against —
Core Axiom Loading, ordering, hooks, events, capabilities, diagnostics No
Game provider Provider authors Loading into one game; its hooks, symbols and game API module Yes, for exactly one game

Rules

  • MUSTAXIOM-02.1 keep dependencies pointing down the diagram: plugins → language providers → ABI → core → game providers.
  • MUSTAXIOM-02.2 reach the game from a plugin only through the Axiom API and a game API module. There is no back door for "trusted" native plugins.
  • MUSTAXIOM-02.3 add support for a game by writing a game provider, and for a language by writing a language provider. Neither requires changes to the core.
Chapter 03

Choosing what to build

You want to… Build a… Tier
Change gameplay, add UI, add commands, record stats Plugin WASM by default
Share code between your plugins Library plugin (a plugin that exports services, no gameplay) Same as its users
Make Axiom work with a new game Game provider + its game API module Native (C++)
Expose more of a supported game to plugins A new version of that game's game API module Native (C++)
Let plugins be written in a new language Language provider + its SDK Native (C++)

Plugin tiers

Plugins are tiered by the language provider they run on. The tier decides how much Axiom trusts the code.

Tier Language provider Trust Isolation Pick it when
1 WASM (Rust, C, C++, Zig, AssemblyScript, C#) Untrusted Own linear memory, fuel limits, capability-gated imports Almost always; the default template
2 Script (Lua) Untrusted Own VM state, stripped standard library, instruction and memory quotas Small scripts, config-heavy plugins, fast iteration with live reload
3 Native (C or C++ via the C ABI) or hosted .NET (C#) Trusted None; runs in the game process Measured performance needs or OS access WASM cannot offer; must be signed

C# has two paths. Untrusted C# plugins compile to WASM with .NET's WASI target and run on the WASM language provider like any other tier 1 plugin. C# plugins that need full .NET (reflection-heavy libraries, large existing codebases, fast startup) run on the hosted .NET language provider. .NET cannot sandbox code inside a process, so hosted .NET plugins are tier 3: trusted and signed, under the same rules as native plugins.

  • MUSTAXIOM-03.1 start every new plugin on tier 1 and move to a higher tier only for a reason written in the plugin's README.
  • MUST NOTAXIOM-03.2 write a game provider to work around a missing API; ask for (or contribute) the API in the game's API module instead.
  • SHOULDAXIOM-03.3 split a large plugin into a library plugin and thin feature plugins when other authors could reuse the library.
Chapter 04

Toolchain and project layout

Start every plugin and provider from the official template (axiom new plugin --lang rust, axiom new game-provider, axiom new language-provider). The templates already follow this guide.

Toolchain

Area Plugins Providers
Language Any with an Axiom SDK (Rust, C, C++, Zig, AssemblyScript, C#, Lua) C++23, with a C11 public surface
Build The language's own build tool (cargo, zig build, npm, dotnet) wrapped by axiom build Premake5; MUST build with MSVC on Windows; Clang/GCC for Linux and macOS
Formatting The language's standard formatter (rustfmt, clang-format, dotnet format, stylua) clang-format, 2-space indent
Packaging axiom pack → .axmod axiom pack --provider → .axprov
Testing axiom test with the mock host Unit tests + the provider conformance suite

A plugin project

better-minimap/
  axiom.toml              # Manifest (§6)
  src/
    lib.rs                # Entry point: on_load / on_enable / on_disable / on_unload
    minimap.rs
    minimap.test.rs       # Tests beside the code
  assets/                 # Textures, sounds, localisation (lang/en.json)
  config/default.toml     # Default user settings
  README.md
  CHANGELOG.md

A game provider project

provider-mygame/
  axiom.toml              # Provider manifest: game IDs, supported builds
  premake5.lua
  include/axiom/mygame.h  # Game API module (generated from spec/)
  spec/                   # Game API definitions (IDL)
  src/
    Provider.cpp          # ax_game_provider_t implementation
    Hooks/                # One file per hooked subsystem
    Signatures/           # Patterns and offsets per game build
    Api/                  # Game API module implementation
    Provider.test.cpp
  tests/conformance/      # Provider conformance suite config

Rules

  • MUSTAXIOM-04.1 keep one plugin or provider per repository (or per top-level folder in a monorepo), with its manifest at the root.
  • MUSTAXIOM-04.2 keep tests beside the code they test (<file>.test.<ext>), and shared fixtures in tests/.
  • MUSTAXIOM-04.3 commit the lock file of the language's package manager, and pin the Axiom SDK version.
  • MUST NOTAXIOM-04.4 vendor or modify Axiom headers; depend on the SDK package.
  • SHOULDAXIOM-04.5 keep game-build-specific data (signatures, offsets) in data files under Signatures/, not inline in code, so a game update is a data change.
Chapter 05

Naming

Axiom's short code is AX: it prefixes every Axiom macro, and in lower case every C ABI symbol. Your own code uses your own prefix.

Thing Convention Example
Plugin ID Lower-case author.plugin-name acme.better-minimap
Game provider ID provider.<game> (official) or author.provider-<game> provider.mygame
Language provider ID lang.<language> or author.lang-<language> lang.wasm, lang.lua, lang.dotnet
Game ID Lower-case, stable, chosen by the game provider mygame
Package file <id>-<version>.axmod / .axprov acme.better-minimap-1.2.0.axmod
Event name <area>.<noun>.<verb-past> player.health.changed
Plugin-defined event Your plugin ID + / + event acme.better-minimap/zoom.changed
Capability <area>:<access> fs:read, net:connect, game:write
Config key snake_case zoom_level
C ABI symbols (Axiom) ax_<area>_<verb>, types ax_..._t, constants AX_... ax_event_subscribe, AX_RESULT_OK
Game API symbols ax_<game>_<area>_<verb> ax_mygame_player_get_health
Your C/C++ macros Your own 2–3 letter prefix, never AX_ BMM_ASSERT

Rules

  • MUSTAXIOM-05.1 choose a plugin ID once; it is how saves, configs, dependencies and the registry find the plugin. MUST NOT rename it after the first release.
  • MUST NOTAXIOM-05.2 use the ax_, AX_, axiom. or provider. prefixes for your own symbols or IDs; they are reserved for Axiom and official providers.
  • MUSTAXIOM-05.3 namespace every event, config key and storage key a plugin defines under its plugin ID.
  • MUSTAXIOM-05.4 keep IDs, event names and capability names lower-case ASCII so they are stable across file systems and languages.
  • SHOULDAXIOM-05.5 use Axiom's nouns consistently in code, docs and UI: plugin, game provider, language provider, game API module, event, hook, capability.
Chapter 06

The plugin manifest

Every plugin has an axiom.toml at its root. The core reads it before loading any code, so it decides load order, dependencies and permissions.

[plugin]
id = "acme.better-minimap"
name = "Better Minimap"
version = "1.2.0"                  # Semantic version
description = "A zoomable minimap with waypoints."
authors = ["Acme Mods <mods@acme.example>"]
license = "MIT"
language = "wasm"                  # Language provider: wasm | lua | dotnet | native
entry = "better_minimap.wasm"

[requires]
axiom = ">=1.4, <2"                # Core ABI version range
"game:mygame" = { api = ">=2.1, <3", builds = ["*"] }

[dependencies]
"acme.ui-kit" = ">=0.9, <1"

[optional-dependencies]
"other.waypoints-sync" = ">=1"

[load]
after = ["acme.ui-kit"]
phase = "game"                     # early | game | late

[capabilities]
required = ["game:read", "ui:draw", "storage:plugin"]
optional = ["net:connect"]          # Asked for at runtime with a reason

[capabilities.reasons]
"net:connect" = "Sync waypoints with your party."

Rules

  • MUSTAXIOM-06.1 declare every capability the plugin uses, with a one-sentence reason for each optional one. Undeclared calls fail with AX_RESULT_DENIED.
  • MUSTAXIOM-06.2 declare version ranges, never exact versions, for axiom, game API modules and dependencies; the upper bound is the next major.
  • MUSTAXIOM-06.3 declare load order with after / before and phase, never by relying on file names or install order.
  • MUSTAXIOM-06.4 bump version for every published change, following semantic versioning (§21).
  • MUST NOTAXIOM-06.5 put secrets, tokens or machine-specific paths in the manifest.
  • SHOULDAXIOM-06.6 keep description under 120 characters; it is shown in the launcher and registry.
Chapter 07

Plugin entry point and lifecycle

States

State Callback Do here Never do here
Loaded on_load Read config, register events, hooks and services; allocate long-lived state Touch game state; the game may not be ready
Enabled on_enable Start gameplay effects, show UI, start timers Long blocking work (move it to a task)
Disabled on_disable Undo everything on_enable did: remove effects, hide UI, stop timers Fail; disabling must always succeed
Unloaded on_unload Free everything allocated in on_load; save state if hot-reloading Call any Axiom API other than logging
  • MUSTAXIOM-07.1 keep every callback idempotent and safe to call in order load → enable → disable → enable → disable → unload. Players toggle plugins at runtime.
  • MUSTAXIOM-07.2 leave the game exactly as it was found after on_disable: every hook, event subscription, spawned entity and UI element the plugin added is gone.
  • SHOULDAXIOM-07.3 register subscriptions and hooks through scoped handles the SDK releases automatically on disable, rather than tracking them by hand.
  • MUSTAXIOM-07.4 return from each callback within the lifecycle budget (§18); slow initialisation goes into an async task.
  • MAYAXIOM-07.5 implement on_save_state / on_restore_state to keep state across hot reloads during development.

Entry point (C)

#include <axiom/axiom.h>
#include <axiom/mygame.h>

typedef struct bmm_state { ax_subscription_t on_health; int32_t zoom; } bmm_state;

static ax_result_t AX_CALL bmm_on_load(ax_plugin_ctx_t* ctx) {
  bmm_state* s = ax_alloc(ctx, sizeof(bmm_state));   /* owned by the plugin */
  if (s == NULL) return AX_RESULT_OUT_OF_MEMORY;
  s->zoom = ax_config_get_i32(ctx, AX_STR("zoom_level"), 3);
  ax_plugin_set_user_data(ctx, s);
  return AX_RESULT_OK;
}

static ax_result_t AX_CALL bmm_on_disable(ax_plugin_ctx_t* ctx) {
  bmm_state* s = ax_plugin_get_user_data(ctx);
  ax_event_unsubscribe(ctx, s->on_health);
  return AX_RESULT_OK;
}

AX_PLUGIN_EXPORT ax_result_t AX_CALL ax_plugin_main(const ax_api_t* api, ax_plugin_desc_t* out) {
  if (api->version_major != AX_ABI_VERSION_MAJOR) return AX_RESULT_UNSUPPORTED_VERSION;
  out->struct_size = sizeof(*out);
  out->on_load = bmm_on_load;
  out->on_enable = bmm_on_enable;
  out->on_disable = bmm_on_disable;
  out->on_unload = bmm_on_unload;
  return AX_RESULT_OK;
}

Entry point (Rust SDK)

use axiom::prelude::*;
use axiom_mygame::player;

#[axiom::plugin]
struct BetterMinimap {
    zoom: i32,
}

impl Plugin for BetterMinimap {
    fn on_load(ctx: &mut Ctx) -> Result<Self> {
        Ok(Self { zoom: ctx.config().get("zoom_level").unwrap_or(3) })
    }

    fn on_enable(&mut self, ctx: &mut Ctx) -> Result<()> {
        // Scoped: removed automatically on disable.
        ctx.events().on(player::HealthChanged, |ev, _ctx| {
            log::debug!("health {} -> {}", ev.old, ev.new);
        })?;
        Ok(())
    }
}
Chapter 08

Calling the C ABI correctly

Most authors use an SDK, which handles these rules. Native plugins, providers and SDK authors work with axiom.h directly and MUST follow them.

  • MUSTAXIOM-08.1 check the ax_result_t of every call that returns one. AX_RESULT_DENIED means a capability is missing; AX_RESULT_NOT_READY means the game is not in a state where the call is valid.
  • MUSTAXIOM-08.2 set struct_size = sizeof(...) on every struct passed to Axiom. This is how newer cores stay compatible with your plugin; a zero struct_size is rejected.
  • MUSTAXIOM-08.3 zero-initialise structs before filling them (ax_event_desc_t d = {0};) so fields added in later minor versions start at their defaults.
  • MUSTAXIOM-08.4 pass strings as ax_str_t (UTF-8 pointer + length; use AX_STR("literal")), never assume Axiom returns null-terminated strings.
  • MUSTAXIOM-08.5 use only fixed-width types at the boundary (int32_t, uint64_t, float, ax_bool_t); never bool, long, wchar_t or a C++ type.
  • MUSTAXIOM-08.6 treat every handle (ax_entity_t, ax_subscription_t) as opaque and possibly stale: check results when using a handle you stored across frames.
  • MUSTAXIOM-08.7 mark callbacks AX_CALL and keep them non-throwing (C++: noexcept), and pass a user_data pointer instead of using globals.
  • MUSTAXIOM-08.8 check the API version you were given at the entry point and call newer functions only when api->version_minor says they exist (the SDK's ax_has(api, fn) macro does this).
  • MUST NOTAXIOM-08.9 call Axiom from threads Axiom did not give you unless the function is documented as thread-safe (§10).
ax_event_desc_t desc = {0};
desc.struct_size = sizeof(desc);
desc.name = AX_STR("player.health.changed");
desc.priority = 0;
desc.callback = bmm_on_health;
desc.user_data = state;

ax_result_t r = api->event_subscribe(ctx, &desc, &state->on_health);
if (r != AX_RESULT_OK) {
  ax_log(ctx, AX_LOG_ERROR, "subscribe player.health.changed failed: %s", ax_result_name(r));
  return r;
}
Chapter 09

Events, hooks and talking to other plugins

Events

  • SHOULDAXIOM-09.1 react to events instead of polling game state every frame.
  • MUSTAXIOM-09.2 keep event handlers short and non-blocking; heavy work is scheduled as a task (§10).
  • MUSTAXIOM-09.3 treat event payloads as borrowed: valid only during the callback. Copy what you need.
  • MUSTAXIOM-09.4 use priorities only to express a real ordering need, and document it. Lower numbers run first; the default is 0.
  • MUSTAXIOM-09.5 cancel a cancellable event (ax_event_cancel) only when the plugin's purpose is to block that action, and log why at debug level.

Hooks

Hooks are the plugin-facing way to change game behaviour at a point the game API module exposes (mygame.damage.apply). Plugins never patch game code themselves.

Kind Runs Can
pre Before the game's code Read and change arguments, or skip the original
post After the game's code Read and change the result
observe After, read-only Read arguments and result; cheapest
  • MUSTAXIOM-09.6 use the weakest hook kind that works: observe over post, post over pre.
  • MUSTAXIOM-09.7 skip the original in a pre hook only when the plugin replaces that behaviour completely; two plugins skipping the same original is a conflict the core reports.
  • MUST NOTAXIOM-09.8 keep a hook's argument pointers after the hook returns.

Services between plugins

  • MUSTAXIOM-09.9 talk to other plugins through services (a versioned interface a plugin registers under its ID) or plugin-defined events, never by reaching into another plugin's memory or files.
  • MUSTAXIOM-09.10 version every service interface with semantic versioning, and declare the dependency in the manifest.
  • MUSTAXIOM-09.11 handle an optional dependency being absent or disabled at any time; check ax_service_get every time you need it, or listen for plugin.disabled.
  • SHOULDAXIOM-09.12 pass plain data (structs, strings, byte buffers) through services, not handles that only make sense inside one language provider.
Chapter 10

Threading and the game loop

Axiom runs plugin callbacks on the game thread during defined phases of each frame, because most game state is only safe to touch there.

Phase When Typical work
frame.begin Start of the game's update Read input, start per-frame work
game events and hooks During the game's own update React to gameplay
frame.end After the game's update, before rendering Apply queued changes
render.ui During UI rendering Draw overlays and windows
Task pool Worker threads, any time File I/O, networking, heavy computation
  • MUSTAXIOM-10.1 touch game state and call game API functions only from the game thread, inside a callback or hook, unless the function is documented as thread-safe.
  • MUST NOTAXIOM-10.2 block the game thread: no sleeping, no synchronous network or disk I/O, no waiting on locks held by worker threads.
  • MUSTAXIOM-10.3 move slow work to the task pool with ax_task_spawn and bring results back with ax_task_post_to_game (or the SDK's ctx.spawn(...).then_on_game(...)).
  • MUST NOTAXIOM-10.4 create raw OS threads in a plugin. Native plugins that need a dedicated thread request one through ax_thread_create so the core can name, track and stop it on disable.
  • MUSTAXIOM-10.5 cancel and wait for every task a plugin started before on_disable returns; the SDK's scoped tasks do this.
  • MUSTAXIOM-10.6 keep state shared between a task and the game thread behind the SDK's channels or a mutex, never as unsynchronised globals.

WASM and Lua plugins are single-threaded per plugin: their callbacks never run concurrently, and tasks are run as async calls on the plugin's own queue.

Chapter 11

Memory and ownership across the boundary

Every pointer and handle that crosses the C ABI has one of four ownership rules, and the header says which.

Marked Meaning Your job
/* borrowed */ Axiom owns it; valid only during this call or callback Copy what you keep
/* valid until <event> */ Axiom owns it; valid until a named point (end of frame, unload) Do not use it after that point
/* caller owns */ You allocated it and pass it in; Axiom does not keep it Free it when you are done
/* transfers ownership */ Axiom allocated it for you Release it with the matching ax_*_release function
  • MUSTAXIOM-11.1 free memory with the allocator that allocated it. Memory from ax_alloc goes back through ax_free; never mix with free, delete or another language's allocator.
  • MUST NOTAXIOM-11.2 pass ownership of a C++ object, a std:: type, a Rust Box or a Lua table across the ABI; pass plain data or a handle.
  • MUSTAXIOM-11.3 release every handle and buffer a plugin owns by on_unload. The core reports leaks per plugin in development builds.
  • MUSTAXIOM-11.4 treat handles to game objects (ax_entity_t) as weak: the object can be destroyed between frames. Re-validate with ax_<game>_entity_is_valid or listen for the destroy event.
  • SHOULDAXIOM-11.5 use the SDK's RAII or scoped wrappers (ax::Handle<T> in C++, Drop types in Rust) instead of manual release calls.
  • MUSTAXIOM-11.6 in providers: validate every pointer and length that comes from a plugin before reading it, and copy guest data out of WASM linear memory before using it.
Chapter 12

Errors and crash safety

In plugins

  • MUSTAXIOM-12.1 handle every error result. Expected failures (missing config, denied capability, entity gone) are logged at warn or debug and the plugin carries on.
  • MUSTAXIOM-12.2 return a failure from on_load or on_enable when the plugin cannot work, with a message that tells the player what to do ("requires acme.ui-kit 0.9 or newer").
  • MUST NOTAXIOM-12.3 let an exception, Rust panic or Lua error escape a callback. SDKs catch them at the boundary, log them with a stack trace and disable the plugin; native plugins MUST do the same (noexcept callbacks with a try / catch at the edge).
  • MUST NOTAXIOM-12.4 call abort, exit, std::terminate or anything that ends the process.

In providers

  • MUSTAXIOM-12.5 isolate every call into a plugin: catch C++ exceptions, and on Windows use structured exception handling around native plugin calls, so a crash in a plugin disables that plugin instead of the game.
  • MUSTAXIOM-12.6 disable a plugin that exceeds its limits (instruction budget, memory quota, frame-time budget repeated across frames) and tell the player which plugin and why.
  • MUSTAXIOM-12.7 write a crash report on a real crash (minidump on Windows) that names the plugins and providers loaded and which one was running, and MUST NOT upload it without the player's consent.
  • MUSTAXIOM-12.8 fail closed when a game build is unknown: refuse to install hooks and report "unsupported game version" rather than patching memory that may have moved.

Messages

  • MUSTAXIOM-12.9 write player-facing errors as: what happened, which plugin, what to do. "Better Minimap was disabled: it needs the ui:draw permission. Allow it in the launcher."
  • SHOULDAXIOM-12.10 give every error a stable code (BMM-0012) so players can search for it and you can find it in logs.
Chapter 13

Writing a game provider

A game provider is the only code in Axiom that knows about one specific game. It implements the provider contract, installs the low-level hooks, and backs the game API module that plugins call.

The provider contract

Function Called Must
detect By the launcher, before starting the game Identify the game and its exact build from files only (hashes, version resources); no side effects
attach Once, early in the game process Resolve signatures, verify the build, prepare hooks; fail closed on unknown builds
start When the game is ready for plugins Install hooks, register the game API module, emit game.ready
frame Each frame, from the game thread Drive the frame phases (§10)
stop Before unload or on shutdown Remove every hook, release every resource
detach Last Leave the process as it was before attach
namespace MyGame {

class Provider final : public Axiom::GameProvider {
public:
  ax_result_t Detect(const ax_game_files_t& files, ax_game_match_t& out) noexcept override;
  ax_result_t Attach(const ax_api_t& api) noexcept override;
  ax_result_t Start() noexcept override;
  void Frame(ax_frame_phase_t phase) noexcept override;
  void Stop() noexcept override;
  void Detach() noexcept override;

private:
  Signatures m_Signatures;
  HookSet m_Hooks;
  Api::Module m_Api;
};

}  // namespace MyGame

AX_GAME_PROVIDER(MyGame::Provider)

Living inside the game's process

  • MUST NOTAXIOM-13.1 do work in DllMain or a shared-library constructor beyond recording the module handle: no allocation, threads, locks, logging or library loads under the loader lock.
  • MUST NOTAXIOM-13.2 rely on static initialisation order or non-trivial global constructors; create and destroy state in attach / detach.
  • MUSTAXIOM-13.3 link the C runtime statically on Windows (/MT) so the provider never conflicts with the game's CRT.
  • MUST NOTAXIOM-13.4 change process-wide state the game depends on (FPU control word, locale, working directory, signal or exception handlers, COM apartment) without restoring it on the same thread.
  • MUSTAXIOM-13.5 be able to stop and detach cleanly at runtime: every hook removed, every thread joined, every allocation returned.
  • MUSTAXIOM-13.6 use std::expected<T, Axiom::Error> for recoverable failures and keep every function the core or a hook can call noexcept.

Managed and emulated games

  • SHOULDAXIOM-13.7 hook managed games (.NET, JVM) at the runtime level through the runtime's supported profiling or agent interfaces before resorting to native patches.
  • MUSTAXIOM-13.8 in emulated targets, hook the emulator's guest-facing layer (CPU, memory, HLE calls), not the emulator's UI or host code, and translate guest addresses through the emulator's memory map every time.
Chapter 14

Game providers: hooking, signatures and game updates

Hooking

  • MUSTAXIOM-14.1 install every hook through Axiom's hook manager (Axiom::Hooks), never with a private detour library, so hooks from different providers are ordered, tracked and removable.
  • MUSTAXIOM-14.2 suspend or synchronise game threads while patching code, and verify the bytes being replaced match what the signature expected.
  • MUSTAXIOM-14.3 keep each native hook as thin as possible: capture arguments, call into the core's dispatch, return. Game logic lives in the game API module, not in the trampoline.
  • MUSTAXIOM-14.4 preserve the hooked function's calling convention, registers and stack alignment exactly; write a test for each hook that calls the original through it.
  • SHOULDAXIOM-14.5 hook the narrowest stable function that gives plugins what they need, and prefer one hook that feeds many events over many hooks.

Signatures and offsets

  • MUSTAXIOM-14.6 locate game code and data by byte-pattern signatures, exported symbols, RTTI or metadata — never by hard-coded absolute addresses.
  • MUSTAXIOM-14.7 keep signatures and offsets in data files per game build (Signatures/<build>.toml), each with a name, pattern, expected match count and the build it was verified on.
  • MUSTAXIOM-14.8 fail closed: if any required signature has zero or more than one match, refuse to start and report which one.
  • SHOULDAXIOM-14.9 resolve all signatures once in attach, in parallel, and cache the result per build hash.

Game updates

  • MUSTAXIOM-14.10 detect the game build exactly (file hash or version resource) and list supported builds in the provider manifest.
  • MUSTAXIOM-14.11 treat a new game build as unsupported until the signatures are verified on it; the launcher tells players to wait for a provider update.
  • SHOULDAXIOM-14.12 run the provider conformance suite against every supported build in CI, and keep at least the last two builds of a live-service game supported while players update.
# Signatures/1.18.4-0b7c2e.toml
[build]
version = "1.18.4"
sha256 = "0b7c2e…"

[[signature]]
name = "Player::ApplyDamage"
pattern = "48 89 5C 24 ?? 57 48 83 EC 30 8B FA"
matches = 1
Chapter 15

Game API modules: exposing a game to plugins

The game API module is the public face of a game provider: the functions, types, events and hook points plugins use for that game. It is a public API, held to the same standard as axiom.h.

One definition

  • MUSTAXIOM-15.1 define the module in spec/ (IDL). AxiomGen generates the C header (axiom/mygame.h), the size asserts, every SDK binding and the reference pages from it. Generated files MUST NOT be edited by hand.
  • MUSTAXIOM-15.2 land a spec change, its generated output and an example plugin that uses it in the same pull request.

Design

  • MUSTAXIOM-15.3 expose game concepts, not game internals: ax_mygame_player_get_health(player) rather than a pointer to the game's Player struct or an offset.
  • MUSTAXIOM-15.4 use stable identifiers that survive game updates (names, hashed names, provider-assigned IDs), never raw addresses.
  • MUSTAXIOM-15.5 separate queries (no side effects) from commands (change state), and state which phases each may be called in.
  • SHOULDAXIOM-15.6 publish events for state changes plugins would otherwise poll (player.health.changed), and hook points for behaviour they may change (mygame.damage.apply).
  • SHOULDAXIOM-15.7 offer batched calls for things plugins do in loops (ax_mygame_entities_query filling a caller buffer), because each boundary crossing from WASM or Lua has a fixed cost.
  • MUSTAXIOM-15.8 gate writes and anything that affects multiplayer or other players behind capabilities (game:write, game:network).
  • MUSTAXIOM-15.9 keep the module the same across supported game builds; differences between builds are the provider's problem, hidden behind the module.

Versioning

Change Bump Existing plugins
New function, event, hook point or optional trailing struct field Minor Keep working
Bug fix that keeps the documented behaviour Patch Keep working
Remove, rename, reorder or change the meaning of anything released Major Must be updated; keep the previous major available for at least one year
  • MUSTAXIOM-15.10 version the game API module independently of the provider binary and of Axiom; a game update that only needs new signatures does not change the module version.
  • MUSTAXIOM-15.11 deprecate before removing: mark it in the spec, warn once per plugin at load, and keep it working until the next major.
Chapter 16

Writing a language provider

A language provider lets plugins be written in one language. It loads plugin code, runs it safely, and bridges every Axiom and game API call.

The contract

Function Must
load Validate and load the plugin's code; no plugin code runs yet
call Invoke a plugin callback with arguments marshalled from the C ABI
limits Apply the plugin's memory, instruction and time quotas
usage Report memory, CPU time and call counts per plugin
save_state / restore_state Support hot reload where the language allows it
unload Free every resource the plugin had, even if it misbehaved

Rules

  • MUSTAXIOM-16.1 expose exactly the API the C ABI defines, generated by AxiomGen from the same spec. A function available to native plugins but missing in your language is a bug.
  • MUSTAXIOM-16.2 give every plugin its own isolated instance (a WASM instance, a Lua state); plugins never share globals.
  • MUSTAXIOM-16.3 enforce quotas: memory caps, and fuel, epoch or instruction-count interruption so an infinite loop is stopped and the plugin disabled, not the game frozen.
  • MUSTAXIOM-16.4 remove ambient authority from the language: no WASI file, clock, random or network imports and no Lua os, io, debug, package.loadlib or FFI, except as granted by capabilities.
  • MUSTAXIOM-16.5 bounds-check every (pointer, length) pair a guest passes, and copy data out of guest memory before using it.
  • MUSTAXIOM-16.6 convert language-level failures (WASM traps, Lua errors, panics) into an error result plus a log entry with the plugin's own stack trace.
  • MUSTAXIOM-16.7 ship an SDK for the language with idiomatic wrappers: scoped handles, typed events, the language's normal error type and async model.
  • SHOULDAXIOM-16.8 support hot reload in development builds: save state, unload, load the new code, restore.
  • MUSTAXIOM-16.9 embed the engine (WASM runtime, Lua VM) behind your provider's own interface and pin its version, so it can be upgraded without changing plugins.

The hosted .NET language provider

  • MUSTAXIOM-16.10 host the .NET runtime once per game process (through hostfxr), pinned to one supported .NET LTS version that the provider ships with; plugins never bring their own runtime.
  • MUSTAXIOM-16.11 load each plugin into its own collectible AssemblyLoadContext so it can be unloaded and hot-reloaded, and MUST release every GC handle and delegate pointing into a plugin before unloading it, or the context will never collect.
  • MUSTAXIOM-16.12 marshal across the C ABI with [UnmanagedCallersOnly] entry points and blittable structs generated by AxiomGen; no Marshal.GetDelegateForFunctionPointer or COM interop at the boundary.
  • MUSTAXIOM-16.13 catch every exception at the boundary and never let one unwind into native frames.
  • MUSTAXIOM-16.14 treat hosted .NET plugins as trusted (tier 3): signed packages only, process:native capability required.
  • SHOULDAXIOM-16.15 run plugin callbacks on the game thread without allocating, keeping per-frame paths free of LINQ, closures and boxing so the .NET GC does not cause frame spikes.

C# SDK

  • MUSTAXIOM-16.16 target the same API surface for both C# paths: one Axiom NuGet package whose code builds for the WASI target and for hosted .NET.
  • MUSTAXIOM-16.17 follow the standard .NET conventions in C# plugins: PascalCase public members, async / await for tasks (mapped onto Axiom's task pool), nullable reference types enabled, warnings as errors.
  • SHOULDAXIOM-16.18 expose events as C# events or IObservable<T>, and scoped resources as IDisposable, so using releases them automatically.
Chapter 17

Capabilities and security

Players install plugins from strangers. Capabilities let them see and limit what each plugin can do.

Capability Grants
game:read Read game state through game API modules
game:write Change game state and use pre / post hooks
game:network Send or alter multiplayer traffic (denied on servers that forbid it)
ui:draw Draw overlays and windows
input:read Read keyboard, mouse and controller input
storage:plugin Read and write the plugin's own data folder
fs:read / fs:write Read or write files outside the plugin's folder, through a picker the player approves
net:connect Open outbound connections to the hosts the manifest lists
process:native Run as a native (tier 3) plugin

For plugin authors

  • MUSTAXIOM-17.1 request the fewest capabilities that work, and explain every optional one in the manifest.
  • MUSTAXIOM-17.2 handle AX_RESULT_DENIED gracefully: turn the feature off and tell the player, never retry in a loop.
  • MUSTAXIOM-17.3 list the exact hosts a plugin connects to ([network] hosts = ["api.acme.example"]) and use TLS.
  • MUST NOTAXIOM-17.4 collect personal data, telemetry or crash reports without the player's opt-in, and MUST say what is sent in the README.
  • MUST NOTAXIOM-17.5 include obfuscated code, downloaders that fetch new code at runtime, or anything that bypasses a game's anti-cheat or multiplayer rules.

For provider authors

  • MUSTAXIOM-17.6 enforce capabilities in the provider for every call that needs one; never trust the plugin's SDK to have checked.
  • MUSTAXIOM-17.7 load native (tier 3) plugins only when signed by a trusted key or explicitly allowed by the player in developer mode.
  • MUSTAXIOM-17.8 verify package signatures and hashes before loading any plugin or provider.
  • MUSTAXIOM-17.9 refuse to run plugins with game:write or game:network in online modes the game provider marks as protected.
Chapter 18

Performance budgets

A plugin shares the frame with the game and every other plugin. Budgets keep one plugin from costing everyone their frame rate. The numbers below are starting defaults; a game provider may tighten them for its game.

Where Budget per plugin What happens when exceeded
Work on the game thread per frame 0.5 ms average, 2 ms peak Warning in the overlay; disabled after sustained overruns
A single event handler or hook 0.1 ms Logged with the handler name in development builds
on_load / on_enable 50 ms Warning; move work to a task
Memory (WASM and Lua) 64 MiB default, raised in the manifest up to 512 MiB Allocation fails; plugin disabled if it cannot recover
Hooks installed by a game provider No measurable cost when no plugin uses the hook point Provider bug
  • MUSTAXIOM-18.1 stay within budget on the reference machine the game's minimum specs describe.
  • MUSTAXIOM-18.2 measure before optimising, with axiom profile or the in-game overlay, and include before and after numbers in performance pull requests.
  • SHOULDAXIOM-18.3 cache results that do not change every frame, and spread periodic work across frames instead of doing it all on one.
  • SHOULDAXIOM-18.4 use batched game API calls in WASM and Lua plugins; each boundary crossing has a fixed cost.
  • MUST NOTAXIOM-18.5 allocate on hot paths in native plugins and providers (per-frame handlers, hook trampolines); reuse buffers.
  • MUSTAXIOM-18.6 in providers: make a hook point cost nothing when no plugin subscribes to it, by checking a subscriber count before dispatching.
Chapter 19

Logging and diagnostics

  • MUSTAXIOM-19.1 log through Axiom's logger (ax_log, or the SDK's log macros); never printf, std::cout, print or the game's own console. Axiom tags each line with the plugin or provider ID.
  • MUSTAXIOM-19.2 use levels consistently: trace and debug for development detail, info for lifecycle and rare player-relevant events, warn for handled problems, error for failures that disabled a feature.
  • MUST NOTAXIOM-19.3 log every frame at info or above. Rate-limit repeated messages (ax_log_once, log::warn_every!(5s, ...)).
  • MUST NOTAXIOM-19.4 log secrets, tokens, or personal data such as player names or chat in non-debug levels.
  • SHOULDAXIOM-19.5 add structured fields instead of formatting them into text: log::info!(zoom = self.zoom; "minimap enabled").
  • SHOULDAXIOM-19.6 expose a debug panel in the in-game overlay (ax_debug_panel_add) for plugin state, instead of verbose logging.
  • MUSTAXIOM-19.7 in providers: report per-plugin frame time, memory and hook counts to Axiom's diagnostics so the overlay and axiom profile can show them, and include the provider version and game build in every crash report.
Chapter 20

Testing plugins and providers

Plugins

  • MUSTAXIOM-20.1 unit-test plugin logic with the language's normal test runner, tests beside the code (minimap.test.rs, minimap.test.lua).
  • MUSTAXIOM-20.2 test behaviour against the mock host (axiom test): a fake core and fake game API module that let tests fire events, call hooks, advance frames and assert on what the plugin did.
  • MUSTAXIOM-20.3 test the full lifecycle at least once: load → enable → disable → enable → disable → unload, asserting nothing leaks and nothing stays hooked.
  • MUSTAXIOM-20.4 test the denied-capability and missing-optional-dependency paths.
  • SHOULDAXIOM-20.5 run an in-game smoke test (axiom run --game mygame --headless --script smoke.toml) before each release when the game supports it.
#[axiom::test]
fn zoom_resets_on_disable(host: &mut MockHost) -> Result<()> {
    let plugin = host.load::<BetterMinimap>()?;
    host.enable(&plugin)?;
    host.emit(player::HealthChanged { old: 100, new: 40 });
    host.advance_frames(3);
    host.disable(&plugin)?;
    assert!(host.ui().windows_of(&plugin).is_empty());
    assert_eq!(host.hooks_of(&plugin).len(), 0);
    Ok(())
}

Game providers

  • MUSTAXIOM-20.6 pass the provider conformance suite: lifecycle, attach and detach, hook install and removal, event delivery, capability checks and crash isolation.
  • MUSTAXIOM-20.7 verify every signature file against its game build in CI (match counts and expected bytes) and fail on any drift.
  • MUSTAXIOM-20.8 test each hook by calling the original through it with known arguments and comparing the result to an unhooked call.
  • SHOULDAXIOM-20.9 keep a set of real community plugins as a compatibility corpus and run it against every provider release.

Language providers

  • MUSTAXIOM-20.10 run the shared API conformance tests through your language's SDK, proving every ABI function is reachable and behaves the same as from C.
  • MUSTAXIOM-20.11 test quota enforcement: an infinite loop, a memory bomb and a deep recursion are each stopped and the plugin disabled, with the game still running.
  • MUSTAXIOM-20.12 fuzz the marshalling layer with malformed guest pointers and lengths.

CI

  • MUSTAXIOM-20.13 build and test on every supported platform, and run providers' tests under ASan and UBSan builds as well as the release build.
Chapter 21

Packaging, versioning and publishing

Versioning

Change to a plugin Bump
Bug fix, no change to config, saves or services Patch
New feature, new config key with a default, new service method Minor
Removed feature, renamed config key, changed save format or service contract Major
  • MUSTAXIOM-21.1 follow semantic versioning for plugins, providers, game API modules and services.
  • MUSTAXIOM-21.2 migrate a plugin's saved data and config forward automatically on upgrade, and keep a schema version in every saved file.
  • MUSTAXIOM-21.3 keep a CHANGELOG.md generated from Conventional Commits, with player-facing notes at the top of each release.

Packages

  • MUSTAXIOM-21.4 build packages with axiom pack only. A .axmod contains the manifest, the compiled plugin (.wasm, Lua sources, or one native library per platform), assets, licences and a file hash list.
  • MUSTAXIOM-21.5 sign every published package with the author's registry key; native plugins and all providers MUST be signed.
  • MUSTAXIOM-21.6 make builds reproducible: pinned toolchains, lock files, no timestamps or absolute paths in the output.
  • MUST NOTAXIOM-21.7 include debug symbols, source maps or test data in the published package; publish symbols separately for crash reports.

Publishing

  • MUSTAXIOM-21.8 fill in the registry listing: description, screenshots, supported games and builds, capabilities with reasons, source link and licence.
  • MUSTAXIOM-21.9 publish open-source plugins with an OSI licence file at the root; closed-source plugins are allowed but cannot request process:native.
  • SHOULDAXIOM-21.10 release to a beta channel first and promote to stable after it has run without new crash reports.
Chapter 22

Git, releases and review

Commits and branches

  • MUSTAXIOM-22.1 follow Conventional Commits: type(scope): summary, imperative, lower case, no trailing period, under 72 characters. Scope is the subsystem: feat(waypoints): add party sync, fix(signatures): update for 1.18.4.
  • MUSTAXIOM-22.2 mark breaking changes with ! and a BREAKING CHANGE: footer; these require a major version bump.
  • MUSTAXIOM-22.3 keep commits small and focused, each building and passing tests.
  • MUSTAXIOM-22.4 branch from main with a typed name (feat/party-sync, fix/1.18.4-signatures), and tag releases v<semver>.

Review checklist: plugins

  • Does on_disable undo everything on_enable did, with no leaks or hooks left behind?
  • Are capabilities minimal, explained, and is denial handled?
  • Does every handler stay within its budget and keep heavy work off the game thread?
  • Are events, config keys and services namespaced under the plugin ID?
  • Are errors handled, and are player-facing messages clear?

Review checklist: providers

  • Does it fail closed on unknown game builds and ambiguous signatures?
  • Are all hooks installed through the hook manager and removed on stop?
  • Is every plugin call isolated, and are capabilities enforced in the provider?
  • Are game API changes made in the spec, generated, versioned and covered by an example plugin?
  • Does the conformance suite pass on every supported build and platform?
Appendix

Appendix

A. Minimal WASM plugin (Rust)

// src/lib.rs
use axiom::prelude::*;

#[axiom::plugin]
struct HelloAxiom;

impl Plugin for HelloAxiom {
    fn on_load(_ctx: &mut Ctx) -> Result<Self> {
        Ok(Self)
    }

    fn on_enable(&mut self, ctx: &mut Ctx) -> Result<()> {
        log::info!("hello from {}", ctx.plugin_id());
        Ok(())
    }
}
# axiom.toml
[plugin]
id = "acme.hello-axiom"
name = "Hello Axiom"
version = "0.1.0"
language = "wasm"
entry = "hello_axiom.wasm"

[requires]
axiom = ">=1.4, <2"

[capabilities]
required = []

B. Minimal Lua plugin

-- main.lua
local plugin = axiom.plugin()

function plugin:on_enable(ctx)
  ctx.events:on("player.health.changed", function(ev)
    if ev.new < 20 then
      ctx.ui:toast("Low health!")
    end
  end)
end

return plugin

B2. Minimal C# plugin

// HelloAxiom.cs
using Axiom;
using Axiom.MyGame;

[Plugin("acme.hello-axiom")]
public sealed class HelloAxiom : Plugin
{
    private IDisposable? _onHealth;

    public override void OnEnable(PluginContext ctx)
    {
        // Removed automatically on disable; disposing early is fine too.
        _onHealth = ctx.Events.On<Player.HealthChanged>(ev =>
        {
            if (ev.New < 20)
            {
                ctx.Ui.Toast("Low health!");
            }
        });
    }

    public override void OnDisable(PluginContext ctx) => _onHealth?.Dispose();
}

C. New plugin checklist

  • Started from axiom new plugin; WASM unless the README says why not.
  • Plugin ID chosen and final; events, config and services namespaced under it.
  • Manifest declares version ranges, load order and minimal capabilities with reasons.
  • Lifecycle test passes: enable/disable twice, no leaks, no hooks left.
  • Within the frame budget on minimum-spec hardware.
  • README, CHANGELOG and licence present; package signed.

D. New game provider checklist

  • Detects the game and exact build from files only.
  • Signatures in data files per build, verified in CI, failing closed.
  • All hooks through the hook manager, removed on stop; process state restored on detach.
  • Game API module defined in spec/, generated, versioned, with an example plugin.
  • Capabilities enforced for writes and network; protected online modes respected.
  • Conformance suite passing on every supported build and platform.

E. Sources