Axiom/SDK
SDK / Entities

Entities

The most used part of the surface. Enumerating the world, holding handles safely across frames, and reading or writing transforms.

entities@1

Enumerating the world and checking whether a handle still refers to something.

Answered by: a pack

EntityId

The index says where, the generation says when. When an entity despawns and the slot is reused, the generation increments, so a stale handle compares unequal to the new occupant instead of silently pointing at it. This is why holding an id across frames is safe.

TYPE
struct EntityId { index: u32, generation: u32 };  // packed into one u64
Hold ids, never anything derived from them

An EntityId is safe across frames because of the generation. A transform, a count, a resolved address or a pointer is not.

Enumerating

bulk alive() -> [EntityId]BULKCACHED

Every live entity this frame. One pool read host-side, cached for the frame, so six mods calling it cost one crossing between them.

Returns
[EntityId]
Empty on an empty world, not an error
Errors
Denied
No pack answers entities on this game
bulk by_kind(kind: str) -> [EntityId]BULK

Entities of one kind. Kind names come from the pack.

Parameters
kind
A name the pack defines, such as npc or item
bulk within(centre: Vec3, radius: f32) -> [EntityId]BULK

Entities inside a sphere. Filtered host-side, so only survivors cross the boundary. A pack with a spatial index answers this natively; otherwise the core filters a snapshot.

Parameters
centre
World position, metres
radius
Metres
is_alive(id: EntityId) -> bool

Whether that exact handle still resolves. A reused slot returns false because the generation moved.

count() -> u32

Live entity count. A pack with a live counter answers without building a snapshot.

Filters compose host-side

The whole chain is one crossing. Filtering happens where the data already is, and only surviving ids come back.

CHAINone crossing
auto targets = f.entities()
  .alive()
  .of_kind("npc")
  .within(me_pos, 80.0f)
  .excluding(me);
transform@1

Where things are, and moving them where the pack allows it.

Answered by: a pack
transform(id: EntityId) -> Result<Transform>

Position, rotation and scale for one entity.

Returns
Transform
{ pos: Vec3, rot: Quat, scale: Vec3 }, metres and radians
Errors
Failed{StaleHandle}
The generation moved; the entity is gone
bulk transforms(ids: [EntityId]) -> [Result<Transform>]BULK

The same for many. One crossing whatever the length, and per-entity failures come back in place rather than failing the batch.

auto ids = f.entities().alive();
auto xs  = f.entities().transforms(ids);  // 1 crossing, not ids.size()
set_transform(id: EntityId, t: Transform) -> Result<>WRITE

Write it back.

Errors
Denied
The pack answers transform read-only. Check writable at init rather than discovering it here.
writable() -> bool

Whether writes are available at all on this game.

components@1

For games with component systems. Frequently unanswered, and that is fine.

Answered by: a pack
has(id: EntityId, comp: str) -> bool

Whether an entity carries a component.

bulk with(comp: str) -> [EntityId]BULK

Everything carrying one. On an ECS game this maps onto archetypes and is cheap.

get(id: EntityId, comp: str) -> Result<Blob>

The component’s bytes, shaped by the pack’s layouts.

Unanswered beats misleading

On a game without components a pack either synthesises a few sensible names or leaves this unanswered, so has() returns Denied rather than a confident false.