Packs
A pack is how Axiom knows how to talk to one game: where the entities are, which matrix is the camera, what that field means. It is the only thing you publish to add a game.
The split worth holding onto: the backend knows how to read the process, the pack knows what the bytes mean in that game. Reading memory, resolving symbols, hooking functions, drawing the overlay and taking input are the same on every Unity game and every JVM game, so a backend ships with Axiom once per runtime family. Which region is the entity pool is not a property of x64 or the JVM, so no backend can ever supply it.
That is why a pack is both the whole third-party story and a small thing to write. You are not implementing the SDK. You are telling it where to look in one game.
Two verbs
| Verb | Meaning |
|---|---|
ANSWER | Answer a question Axiom asks about a game |
ASK | Ask your own question, when the game has something no other game has |
That is the whole vocabulary. There is no second artifact type, no interface to implement, no tier to choose.
The questions Axiom asks
Answer the ones your game lets you. Stay silent on the rest. A pack does not have to be complete to be useful.
| Question | Without an answer |
|---|---|
entities | No mod can list or find anything in the world |
transform | Nothing knows where anything is |
camera | No world-to-screen, so no overlays drawn in the scene |
types | No access to the game's own structs by name |
components | No component filters, on games that have them |
assets | No texture or model replacement |
world | No line of sight checks |
Drawing the overlay, reading input, hooking functions, reading memory and resolving symbols come from the backend. Backends ship with Axiom, one per runtime family, and you will almost certainly never write one.
Writing one
AXIOM_PACK("alice.mygame") // Q: which entities are alive right now? ANSWER(entities) { auto pool = sym("entity_pool"); auto hdr = read<PoolHeader>(pool); for (u32 i = 0; i < hdr.count; ++i) { auto e = read<Entity>(pool + 0x10 + i \* 0x2C0); if (e.flags & LIVE) found(e.index, e.generation); } } // Q: where is this entity? ANSWER(transform) { auto e = read<Entity>(handle); gives(e.pos, e.rot, e.scale); }
That is the whole surface area. If a game happens to store things in a plain flat array, the same answer is four lines of TOML instead, and you never find out they compile to the same thing.
Knowing where you stand
Ship when you are happy with that. A pack answering two questions is worth publishing.
Packs never ship inside mods
Publish rejects one that does. This is not a style rule; bundling would undo everything on this page.
| If a mod bundled a pack | What breaks |
|---|---|
| The game patches | The bundled copy is a private fork only that mod's author can update |
| Two mods on one game | Two copies of the same answers, disagreeing silently |
| The user wants a better pack | Nothing to switch, stack or roll back; it was never installed |
So they resolve separately. A mod declares questions; the manager finds packs that answer them.
A mod whose required question nothing answers does not install for that game, and the manager names the missing question rather than blaming the mod. A slashed name is a hard dependency on one specific pack, which is the cost the slash exists to advertise.
Packs stack
No pack has to be complete, so several can be active for one game at once. Above, Alice answers most
of it and Bob answers camera, which she never did. Bob did not fork her pack; he published twelve
lines.
Forking becomes the last resort instead of the only move. When a maintainer disappears, somebody patches one answer rather than adopting everything.
Stacking is priority, never merge
Packs stack in the sense that different packs answer different questions. They never combine answers to the same one.
Merging sounds better and cannot be made correct. Two packs reading different substrates both hand back an index 5 that means different entities, and fixing that inside a 64-bit id means shaving the generation down until it wraps in minutes, which reintroduces the exact aliasing bug generations exist to prevent.
So the order decides, the user controls it, and a second pack filling a gap the first left works exactly as above.
Substrate is declared and checked
One answer per question is not enough on its own. Alice can answer entities from the actor pool
while Bob answers transform from the physics-body array on different indexing. Both pass
conformance. The stack hands mods wrong positions.
[substrate] entities = "actor_pool" transform = "actor_pool" camera = "render_view"
A substrate name is scoped to the game rather than global. Two packs claiming actor_pool are
asserting they index the same thing, and a wrong assertion becomes a pack bug with a name instead of a
mystery.
Handles are minted by the core
A pack's internal index and generation never reach a mod. The core mints an opaque 64-bit handle and keeps a per-session indirection table, rebuilt whenever the active pack set changes.
| Cost | What it buys | |
|---|---|---|
| Opaque handle | One lookup per resolve() | Pack-internal ids can be any width or shape |
| Per-session table | Rebuilt on stack change | Changing packs cannot alias old handles onto new entities |
| Core-owned generation | A u32 the core controls | Works on games with no generation counter of their own |
On a game with no generation concept a pack used to have no honest option:
synthesising one is impossible and shipping a constant made the safety
guarantee silently untrue. Now the core owns it, and a pack declares whether
slot reuse is reuse = "observable" or "opaque". An opaque game gets a
conservative policy and a visible marker rather than a promise nobody is
keeping.
Three events the surface was missing
handoff(question, from_pack, to_pack) // a pack update changed who answers invalidate_all(reason) // pool compaction; every handle is dead stack_changed(questions) // packs installed, removed or reordered
A compacting pool preserves neither index nor generation relationships, so mass invalidation is the only correct outcome and a mod has to be told. And a pack update that quietly starts answering a question nobody answered before used to change mod behavior with no error raised anywhere.
Builds are declared, and skew is refused
A pack refuses to attach outside the builds it declares, and the manager refuses a stack whose members
disagree about the build. Without that, a game auto-updates, one pack's offsets move, and its snapshot
reads moved memory as pool data: garbage entities, a lying alive(), or a stride walk into unmapped
memory.
Loud refusal converts silent corruption into visible support load, and on a weekly-patching game it will strand packs constantly. That is the correct direction to fail, and it is why the registry shows build coverage per pack rather than a version number alone.
Asking your own questions
Some games hold things no portable question covers: a spellbook, a karma meter, a build grid, a faction table. Ask for it in the same file.
ASK(spellbook) { Spell { id: u32, name: str, cooldown_ms: u32 } bulk known() -> [Spell] cooldown(spell: u32) -> Result<u32> } ANSWER(spellbook) { ... }
It gets the same treatment as a built-in question: host dispatch, bindings in every language, bulk calls, typed errors, versioning, docs.
For a mod, the only signal is a slash
require = ["entities", "transform", "overlay", "alice.mygame/spellbook"]| Name | Means |
|---|---|
entities | Portable. Runs on any game whose packs answer it. |
alice.mygame/spellbook | Came from one pack. The mod is game-specific. |
One list. The registry derives the game-specific label itself rather than trusting the author to declare it.
First-party packs are just packs
The games we ship support for are packs: same ANSWER and ASK, same format,
same conformance runner, same registry, same install path, published under our
key.
This is a rule, not a habit, because the alternative has a predictable failure mode.
| If first-party took a private path | What happens |
|---|---|
| The internal API is nicer | The public one rots, and nobody inside notices because nobody inside uses it |
| Our packs skip conformance | Ours become the ones with silent unit and handle bugs |
| Our packs cannot be replaced | A better community pack has nowhere to go |
| A first-party pack does not get | Meaning |
|---|---|
| Resolution priority | A community pack can answer over ours; the user controls order |
| Extra questions | The same set as everyone else |
| A conformance exemption | Scores published, failures and all, next to everyone else's |
| Un-removability | Uninstall or replace it like any pack |
The single difference is that the official signing key is pre-trusted, so a native-tier first-party
pack installs without a prompt. That is a trust decision about a key, revocable in axiom trust,
rather than a privilege in the architecture. Packs bundled with the installer are the same artifacts
pinned to a version, updated and removed independently of Axiom itself.
What a pack cannot do
| Limit | Why |
|---|---|
| Add a new runtime | A game on an engine no backend covers needs a backend, which is different work against the nine primitives. A pack answers questions on a runtime that already works. |
| Unblock a protected title | A pack for a game with kernel anticheat is refused at publish and at attach. |
| Redefine a built-in question | You cannot change what entities means. Your own questions live under your pack's name, where the slash makes the trade visible. |