Axiom/Docs
Adding a game / How answers work

How answers work

What ANSWER compiles to, how the core calls it, and the optional members that let a pack beat the generic default.

Skippable. Nothing here changes what you write; it is what ANSWER compiles to, and it matters once you want a pack to be fast.

Every answer is an interface

ANSWER(entities) generates an implementation of the interface behind entity.query@1. The core never assumes a layout: it asks for the frame's entity set and your code gets there however the game demands.

GENERATEDentity.query@1
// ---- required for CORRECTNESS
struct EntityProvider {
  Status snapshot(const Query& q, SnapshotSink& sink);
  bool   alive(EntityId id);
  Status resolve(EntityId id, EntityRef& out);

// ---- required for PERFORMANCE above ~10k entities
virtual u32 count() { return UseDefault; }
virtual Status within(Vec3 c, f32 r, Sink& s) { return UseDefault; }
virtual Status by_kind(Str kind, Sink& s) { return UseDefault; }

// ---- tells the core how to cache you
virtual Hints hints() { return {}; }
};

Where the per-game reality goes

snapshot is the whole trick. Seven games, seven different bodies, one API on the mod's side.

Game shapeWhat snapshot does
Flat pool, fixed strideOne bulk read, stride the buffer
Chunked pool, free listWalk chunk headers, skip free slots by bitmap
ECS archetype tablesIterate archetypes matching the component mask
Scene graphDepth-first from the root, flatten as it goes
Handle table behind a vtableRead the table, call the virtual getter per live slot
Managed heap (JVM, CLR)Enumerate through the backend object API, no pointer math
Linked list of actorsChase next until null, bounded by a sanity cap

From the mod's side all seven are f.entities().alive(). That is the only thing the surface has to guarantee.

A worked example

A chunked pool with a free-list bitmap, which no declarative binding could express. Two crossings regardless of entity count.

EXPANDEDfrom ANSWER(entities)
Status snapshot(SnapshotSink& sink) override {
  auto base = sym("entity_pool");
  if (base.failed()) return base.status();

auto hdr = read<PoolHeader>(\*base);
if (hdr.failed()) return hdr.status();

// one crossing for every chunk
std::vector<Span> spans;
for (u32 c = 0; c < hdr->chunk_count; ++c)
spans.push_back({ hdr->chunks[c], hdr->chunk_bytes });

auto blobs = read_many(spans);

for (auto& blob : blobs) {
if (blob.failed()) continue;
auto bits = bitmap_of(blob);
for (u32 i = 0; i < hdr->per_chunk; ++i) {
if (!bits.test(i)) continue; // free slot
auto\* e = entity_at(blob, i);
sink.add({ e->index, e->generation });
}
}
return Status::Ok;
}
A pack never caches

snapshot is called at most once per frame and the result is shared, so alive() stays one crossing however many mods call it.

Three functions is true only for small pools

snapshot is both the required primitive and the most expensive operation, while the operations that are cheap per shape are the optional ones. That inversion has a cost that scales.

EntitiesFull transform snapshotEffect
400~26 KBNegligible
10,000~640 KBApproaching the frame budget
100,000~6.4 MBAt or over the whole budget, before any mod runs
500,000~32 MBNot viable at any frame rate
Above ~10k entities the optionals are mandatory

The real interface is six functions, not three. count() on a free-list pool is a popcount; by_kind() on an archetype game is one table walk. Falling back to a filtered snapshot for either pays O(total) for an O(matching) answer.

snapshot takes a Query so the core can push the filter down to you. Linked lists and scene graphs cannot narrow cheaply and will walk anyway; they declare that through hints() rather than silently costing more. Conformance records which members you implement natively, so the registry can show that a pack is correct but slow rather than letting a user find out in a stutter.

Going faster

The optionals exist so a pack can beat the core's generic default. UseDefault is not an error, it means "use your default", so you implement only what your game can genuinely do better.

OptionalDefaultOverride when
count()Size of a snapshotThe game keeps a live count you can read
within()Filter the snapshot host-sideThe game has a spatial index, octree or grid
by_kind()Filter the snapshot host-sideKinds are separate pools or archetypes
hints()Conservative assumptionsThe snapshot is cheap, unstable, or bounded
The rule that keeps this honest

Adding a required member is a major bump of the question. Adding an optional with a core default is a minor, and every existing pack keeps working untouched. The interface grows without invalidating packs.

Sandboxing

An answer usually needs memory.read, symbols and sometimes memory.scan, which is short enough to compile to sandboxed WebAssembly: capability-gated, architecture-portable, and a bad answer traps instead of corrupting the host. That is the default, so most packs install with no prompt despite being code.

$ axiom pack build wasm, no install prompt
$ axiom pack build --native prompts on install, names the signer

Native is for the rare case that must be inside the frame holding a game lock or calling a game function directly.

A wrong answer is a wrong answer, not a compromise

The worst a sandboxed pack does is hand back garbage entities or a Failed. That is exactly why conformance testing is not optional.