Write once. Run where the capabilities match.
One capability API across injected native games, managed runtimes and emulated targets. Mods compile to WebAssembly by default, so a build made today still loads after the game patches.
Pick a tier before you pick a language.
The tier decides what the mod can be trusted with, how it is distributed, and whether it survives a game update.
Sandboxed and portable. One artifact runs on every backend and architecture, hot-reloads in under a second, and cannot crash the game.
No boundary and no sandbox. For renderers, physics and anything that must run inside the game's own frame budget. Signed, and prompted once.
One file, no build step, instant reload. For config tweaks, event handlers and the thing you want working before dinner.
The surface is generated, not written.
Every binding, every tier and this reference come from one definition, so none of them can drift apart.
capability entity.transform { version: 1 fn position(e: EntityId) -> Vec3 bulk fn positions(e: []EntityId) -> []Vec3 event moved { entity, from, to } }
Anything you would call in a loop has a bulk form. The linter warns when a sandboxed mod uses the scalar one inside a frame callback.
The same mod, three ways.
A HUD mod that hooks one function, reads every live entity once per frame and submits a panel. The bindings differ, the surface does not.
#include <axiom/surface.hpp> #include <vector> struct BetterHud final : axiom::Mod { void on_load(axiom::Context& ctx) override { ctx.hooks().install("CG_DrawHud", 100); ctx.config().bind("scale", &scale_); } void on_frame(axiom::Frame& f) override { auto ids = f.entities().alive(); markers_.clear(); // bulk form: one boundary crossing for the whole frame for (auto const& p : f.entities().positions(ids)) markers_.push_back(f.camera().project(p)); } void on_draw(axiom::ui::Surface& ui) override { auto panel = ui.panel("Better HUD", axiom::ui::Anchor::BottomLeft); panel.scale(scale_); panel.text("{} contacts", markers_.size()); for (auto const& m : markers_) panel.marker(m); ui.submit(std::move(panel)); // retained tree, only the diff crosses } float scale_ = 1.1f; std::vector<axiom::ui::Point> markers_; }; AXIOM_MOD(BetterHud);
local axiom = require("axiom") local markers = {} axiom.hooks.install("CG_DrawHud", 100) axiom.on_frame(function(f) local ids = f.entities.alive() markers = f.entities.positions(ids) -- bulk end) axiom.on_draw(function(ui) ui.panel("Better HUD", { anchor = "bottom-left" }) :text("%d contacts", #markers) :markers(markers) :submit() end) -- save the file and it reloads in place
[config.scale] label = "Scale" type = "range" min = 50 max = 150 default = 110 unit = "%" [config.anchor] label = "Anchor" type = "enum" options = ["bottom-left", "bottom-centre", "top-right"] default = "bottom-left"
class NativeBackend final : public axiom::Backend { public: Caps capabilities() const override { return Caps::Memory | Caps::Symbols | Caps::Hooks | Caps::Frame | Caps::Surface | Caps::Input; } Result<Addr> resolve(std::string_view name) override; Result<Hook> install(Addr at, Detour d) override; Result<void> read(Addr at, std::span<std::byte>) override; }; // the managed backend returns the same Caps minus // Memory::Protect, and the loader does the rest
Start here.
Backends refuse a process with a known kernel anticheat driver loaded, and the registry marks those titles as blocked. Single-player, private servers, dedicated servers and emulated titles are the target. Read the policy.