Skip to main content
Your game's model

Not translated yet — showing English.

Entity

An entity is a schema declaration with live aspects: data 0..*, states 0..*, RPC 0..*, events 0..*, hooks, and change history. Maps bind obstacles to entities, collision binds a transform aspect, stats is a preset, world objects are a preset plus a state machine.

Entity mounts at the root, so room.Entity<Door>(id) and playserv.Entities<KeyDef>() sit directly on the root rather than behind a namespace. It builds on three primitives — events, RPC and data — and on nothing else. Collision, locomotion and prediction sit above it: each binds to one aspect, not to the whole entity, which is why a contact can fire a transition without the collision module knowing anything about permissions.

When to use it

  • A world object needs behaviour, not just fields: state machines, permissioned RPCs and events on one declaration.
  • Doors, traps, pickups: transitions must fire from client events, collision contacts, or stat thresholds without room code.
  • You want game objects as one-line creations: apply or derive presets like world-objects.
  • A dispute needs the exact world state at the shot: read an instance at a past sim_time, inside the declared window.
  • Skip it when the thing has no identity. A value that only ever lives inside something else, like the text on a door's plaque, is a field in an aspect, not an entity of its own. Everything that is addressed is an entity: Data is the mechanics underneath, and no table path bypasses them.

Who does what

ActorOn this page
schema-authordeclares entities, aspects, state machines, presets
every actorqueries, subscribes, calls entity RPCs, reads state

At a glance

The dungeon door: two aspects with their own policy, a guarded machine, a declared event, and an RPC that names the right it needs
[Aspect("info", Read = "any")]                            // rarely changes, everyone reads it
public class Info { public string Name; }

[Aspect("motion", Hz = 20, Read = "any", Write = "fn")]   // 20 updates a second while it swings
public class Motion { public float OpenRatio; public bool Jammed; }

[Machine("gate")]
public class Gate
{
    [State(Initial = true), Transition("open_requested", to: "opening")] public State Closed;
    [State, AfterSeconds(1.2f, to: "open")]                              public State Opening;
    [State, Transition("close_requested", to: "closed")]                 public State Open;
    [State("open.blocked"), Transition("cleared", to: "open", Guard = "!motion.jammed")] public State Blocked;
}

[Entity("key-def", Persistence = Persistence.Persistent)]  // authored content: key.bronze, key.gold
public class KeyDef
{
    [Key]    public string Key;
    [Aspect] public Info Info;
}

[Entity("door", Persistence = Persistence.Runtime)]
public class Door
{
    [Aspect]  public Info Info;
    [Aspect]  public Motion Motion;
    [Machine] public Gate Gate;
    [Ref]     public Ref<KeyDef> Needs;                            // holds the id, never the key
    [Event("locked", Clock = Clock.SimTime)] public Event Locked;  // reaches whoever sees the door

    [EntityRpc(Requires = Entity.Permissions.Execute, Rows = "caller in entity.room")]
    public void RequestOpen(Actor caller)
    {
        if (caller.Inventory.Has(Needs)) Gate.Fire("open_requested");
        else Locked.Send();
    }
}
@Aspect('info', { read: 'any' })                            // rarely changes, everyone reads it
export class Info { name = ''; }

@Aspect('motion', { hz: 20, read: 'any', write: 'fn' })     // 20 updates a second while it swings
export class Motion { openRatio = 0; jammed = false; }

@Machine('gate')
export class Gate {
  @State({ initial: true }) @Transition('open_requested', { to: 'opening' }) closed: State;
  @State() @AfterSeconds(1.2, { to: 'open' }) opening: State;
  @State() @Transition('close_requested', { to: 'closed' }) open: State;
  @State('open.blocked') @Transition('cleared', { to: 'open', guard: '!motion.jammed' }) blocked: State;
}

@Entity('key-def', { persistence: Persistence.Persistent })  // authored content: key.bronze, key.gold
export class KeyDef {
  @Key() key = '';
  @Aspect() info: Info;
}

@Entity('door', { persistence: Persistence.Runtime })
export class Door {
  @Aspect() info: Info;
  @Aspect() motion: Motion;
  @Machine() gate: Gate;
  @Ref() needs: Ref<KeyDef>;                                 // holds the id, never the key
  @Event('locked', { clock: Clock.SimTime }) locked: Event;  // reaches whoever sees the door

  @EntityRpc({ requires: Entity.permissions.execute, rows: 'caller in entity.room' })
  requestOpen(caller: Actor) {
    if (caller.inventory.has(this.needs)) this.gate.fire('open_requested');
    else this.locked.send();
  }
}
@aspect("info", read="any")                                  # rarely changes, everyone reads it
class Info:
    name: str = ""

@aspect("motion", hz=20, read="any", write="fn")             # 20 updates a second while it swings
class Motion:
    open_ratio: float = 0.0
    jammed: bool = False

@machine("gate")
class Gate:
    closed  = state(initial=True, on="open_requested", to="opening")
    opening = state(after_seconds=1.2, to="open")
    open    = state(on="close_requested", to="closed")
    blocked = state("open.blocked", on="cleared", to="open", guard="!motion.jammed")

@entity("key-def", persistence=Persistence.PERSISTENT)       # authored content: key.bronze, key.gold
class KeyDef:
    key: str = key()
    info: Info = aspect()

@entity("door", persistence=Persistence.RUNTIME)
class Door:
    info: Info = aspect()
    motion: Motion = aspect()
    gate: Gate = machine()
    needs: Ref[KeyDef] = ref()                               # holds the id, never the key
    locked = event("locked", clock=Clock.SIM_TIME)           # reaches whoever sees the door

    @entity_rpc(requires=entity.permissions.execute, rows="caller in entity.room")
    def request_open(self, caller: Actor):
        if caller.inventory.has(self.needs):
            self.gate.fire("open_requested")
        else:
            self.locked.send()
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

// declarations compile into the same pushed model — playserv push from the UE project or CI
USTRUCT()
struct FInfo { GENERATED_BODY() UPROPERTY() FString Name; };

USTRUCT()
struct FMotion { GENERATED_BODY() UPROPERTY() float OpenRatio; UPROPERTY() bool bJammed; };

USTRUCT(PSMachine = (Name = "gate"))
struct FGate
{
    GENERATED_BODY()

    UPROPERTY(PSState = (Name = "closed", Initial = "true"),
              PSTransition = (On = "open_requested", To = "opening"))               FPSState Closed;
    UPROPERTY(PSState = "opening", PSAfterSeconds = (Seconds = "1.2", To = "open")) FPSState Opening;
    UPROPERTY(PSState = "open", PSTransition = (On = "close_requested", To = "closed")) FPSState Open;
    UPROPERTY(PSState = (Name = "open.blocked"),
              PSTransition = (On = "cleared", To = "open", Guard = "!motion.jammed")) FPSState Blocked;
};

USTRUCT(PSEvent = (Name = "locked", Clock = "SimTime"))
struct FLocked { GENERATED_BODY() };                       // reaches whoever sees the door

UCLASS(PSEntity = (Name = "key-def", Persistence = "Persistent"))
class UKeyDef : public UObject
{
    GENERATED_BODY()

    UPROPERTY(PSKey)                                    FString Key;
    UPROPERTY(PSAspect = (Name = "info", Read = "any")) FInfo Info;
};

UCLASS(PSEntity = (Name = "door", Persistence = "Runtime"))
class UDoor : public UObject
{
    GENERATED_BODY()

    UPROPERTY(PSAspect = (Name = "info", Read = "any"))                          FInfo Info;
    UPROPERTY(PSAspect = (Name = "motion", Hz = 20, Read = "any", Write = "fn")) FMotion Motion;

    UPROPERTY(PSMachine = "gate")
    FGate Gate;

    UPROPERTY(PSRef = "key-def") TPSRef<UKeyDef> Needs;    // holds the id, never the key

    UFUNCTION(PSRpc = (Requires = "Entity.Execute", Rows = "caller in entity.room"))
    void RequestOpen();
};
Unity C# is the same C# API here — the same C# attributes compile in Unity (2021.3 baseline, no newer C# required) — declarations push into the same model
[Aspect("info", Read = "any")]                            // rarely changes, everyone reads it
public class Info { public string Name; }

[Aspect("motion", Hz = 20, Read = "any", Write = "fn")]   // 20 updates a second while it swings
public class Motion { public float OpenRatio; public bool Jammed; }

[Machine("gate")]
public class Gate
{
    [State(Initial = true), Transition("open_requested", to: "opening")] public State Closed;
    [State, AfterSeconds(1.2f, to: "open")]                              public State Opening;
    [State, Transition("close_requested", to: "closed")]                 public State Open;
    [State("open.blocked"), Transition("cleared", to: "open", Guard = "!motion.jammed")] public State Blocked;
}

[Entity("key-def", Persistence = Persistence.Persistent)]  // authored content: key.bronze, key.gold
public class KeyDef
{
    [Key]    public string Key;
    [Aspect] public Info Info;
}

[Entity("door", Persistence = Persistence.Runtime)]
public class Door
{
    [Aspect]  public Info Info;
    [Aspect]  public Motion Motion;
    [Machine] public Gate Gate;
    [Ref]     public Ref<KeyDef> Needs;                            // holds the id, never the key
    [Event("locked", Clock = Clock.SimTime)] public Event Locked;  // reaches whoever sees the door

    [EntityRpc(Requires = Entity.Permissions.Execute, Rows = "caller in entity.room")]
    public void RequestOpen(Actor caller)
    {
        if (caller.Inventory.Has(Needs)) Gate.Fire("open_requested");
        else Locked.Send();
    }
}

An aspect is the declared unit, not the field: motion carries its own cadence and its own mask, info carries different ones, and a field belongs to exactly one of them. That is what lets a preset attach a whole group at once, and what lets collision bind to the one aspect that carries a transform without seeing anything else on the entity.

Three rules govern the machine in that block:

  • A dotted name nests one level. open.blocked associates with open on its own, so the close_requested transition declared on open applies inside it without being repeated. While the machine sits in open.blocked it is in open — a state check for open is true, and OnEntered("open") fired on the way in and does not fire again for the substate.
  • A timer declared on a state runs only while that state is current. Leaving opening drops its AfterSeconds, and entering it again starts a fresh one.
  • A guard is a declared predicate over the entity's own fields, in the same language as an access row predicate. Logic that needs code is a hook, not a guard.

Field paths are the pushed model's spelling. Predicates and query paths name fields as the declaration pushed them — motion.jammed, gate.state, info.name — whatever each binding spells them locally.

Who may call the RPC. An entity RPC names the right it needs the way every operation does: a right atom (entity × execute) plus a row predicate saying which instances it covers (Access owns both).

In the declarationWhat it means
caller in entity.roomany actor in the room the door is in, whatever build they run. Proximity is not part of it: how near you must stand to receive the door's deltas is a visibility rule on the aspect's sync policy — bandwidth, not permission, and widening a view never widens a right
Actorthe caller's identity, the same object whoami returns
caller.InventoryInventory's handle for that player, available wherever that module is mounted

playserv push is what makes a declaration real. Schema as Code owns the step: it diffs your declarations against the deployed model, carries the revision it was diffed against, and refuses instead of overwriting if the deployed schema has moved. A re-push that would break instances already live goes through propose → plan → apply, so the plan is readable before anything changes.

On the client, the entity is the API:

The door as the API: connect, join, call the RPC, take both outcomes — the chime and the locked signal
var playserv = await PlayServ.Connect(projectKey);
var room = await playserv.Rooms.Join(seat);   // a seat from Matchmaking, or a room you found

var door = room.Entity<Door>(doorId);         // a typed Ref — passable to any RPC as-is
await door.RequestOpen();
door.Gate.OnEntered("open", () => PlayChime());
door.Locked.On(() => Hud.Flash("Locked — the bronze key opens it"));
const playserv = await PlayServ.connect(projectKey);
const room = await playserv.rooms.join(seat);  // a seat from Matchmaking, or a room you found

const door = room.entity<Door>(doorId);        // a typed Ref — passable to any RPC as-is
await door.requestOpen();
door.gate.onEntered('open', () => playChime());
door.locked.on(() => hud.flash('Locked — the bronze key opens it'));
playserv = await PlayServ.connect(project_key)
room = await playserv.rooms.join(seat)     # a seat from Matchmaking, or a room you found

door = room.entity(Door, door_id)          # a typed Ref — passable to any RPC as-is
await door.request_open()
door.gate.on_entered("open", lambda: play_chime())
door.locked.on(lambda: hud.flash("Locked — the bronze key opens it"))
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

FPlayServClient::Connect(ProjectKey,
    TPSOnResult<FPlayServClient*>::CreateWeakLambda(this, [this](const TPSResult<FPlayServClient*>& ConnectResult)
    {
        if (!ConnectResult.HasValue()) { return; }
        // a seat from Matchmaking, or a room you found
        ConnectResult.Value()->Rooms->Join(Seat,
            TPSOnResult<FPSRoom*>::CreateWeakLambda(this, [this](const TPSResult<FPSRoom*>& JoinResult)
            {
                if (!JoinResult.HasValue()) { return; }
                OnJoined(JoinResult.Value());
            }));
    }));

// in OnJoined(FPSRoom* Room): a typed handle — every declared member generated, passable to any RPC
Room->Entities->Of<UDoor>()->Get(DoorId,
    TPSOnResult<UDoor*>::CreateWeakLambda(this, [this](const TPSResult<UDoor*>& DoorResult)
    {
        if (!DoorResult.HasValue()) { return; }
        UDoor* Door = DoorResult.Value();
        Door->Call->RequestOpen();
        TPSSubscription OpenChime  = Door->Gate->Subscribe->Entered(PSKeys::States::Open, [this]() { PlayChime(); });
        TPSSubscription LockAlerts = Door->Subscribe->Locked([this]() { Hud->Flash(TEXT("Locked — the bronze key opens it")); });
    }));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
Unity C# is the same C# API here — runs as-is in Unity against the generated types
var playserv = await PlayServ.Connect(projectKey);
var room = await playserv.Rooms.Join(seat);   // a seat from Matchmaking, or a room you found

var door = room.Entity<Door>(doorId);         // a typed Ref — passable to any RPC as-is
await door.RequestOpen();
door.Gate.OnEntered("open", () => PlayChime());
door.Locked.On(() => Hud.Flash("Locked — the bronze key opens it"));

The locked signal is the door's own event: its target is the declaration's — this instance — so everyone subscribed to the door hears it and no recipient list travels with the send.

The model

A tank, a door, a stat bar and a quest are all entities. They differ in which aspects they carry and in nothing else, which is what lets every other module build on this one.

What an entity declares.

DeclaresWhat it is
aspecta named field group declared whole, with its own sync policy and access mask. An entity carries several, and no field is in two
state machinestates nestable one level, transitions and guards; several per entity
triggerwhat fires a transition — the four sources are below
entity RPCa verb on the entity, declared inside the view with the right atom it needs
entity eventa signal the entity emits, delivered to whoever subscribes to that instance
hookpre and post, on data operations and on transitions, deployed as cloud functions. Extensibility declares the order, the verdict shape and what a failure does
history trackwhether the view keeps the instant window at all
refa link to another entity holding its id and never its key, so renaming a key never breaks a link. Include pulls it in with the page

What fires a transition. None of the four sources is code running in a room.

SourceHow it fires
client event or RPCany declared one — RequestOpen above fires open_requested
collisiona contact or a trigger-volume entry — traps, pressure plates — through the aspect Collision binds to
data thresholddeclared on a stat, 0 HP → death, enforced by hook order rather than by code in a room
timeAfterSeconds on a state is a declared trigger, not a coroutine: it runs on the room's simulation clock, advances with sim_time, stops while the room is not simulating, and deleting the instance ends its machines and their pending timers with it

What a selection may do.

AxisWhat is admissible
filter and sortdeclared fields only — there is no table handle, and a selection is entity-addressed, scoped to a room or to the project
includea declared ref, pulled in with the page
pagingby opaque cursor: not an offset, not a row id, and its meaning does not survive a version change. Pass it back, never parse it
accesspredicates apply before paging, so a page never carries holes where hidden rows would be
livesubscribing to a selection keeps it live, with members entering and leaving as their data changes
Query, filter, sort, page by cursor — and subscribe to the selection itself
// in this room: doors still shut, by name, first page of 20 — with the key each one needs
var shut = await room.Entities<Door>()
    .Where(d => d.Gate.State == "closed")
    .Include(d => d.Needs)
    .OrderBy(d => d.Info.Name)
    .Page(20)
    .Query();

// live selection: fires as doors swing open and shut
room.Entities<Door>().Where(d => d.Gate.State == "open").Subscribe(open => Minimap.Mark(open));

// project-wide, outside any room: the key catalogue, page by page
var keys = await playserv.Entities<KeyDef>().OrderBy(k => k.Info.Name).Page(50).Query();
var more = await playserv.Entities<KeyDef>().OrderBy(k => k.Info.Name).Page(50, after: keys.Cursor).Query();
// in this room: doors still shut, by name, first page of 20 — with the key each one needs
const shut = await room.entities<Door>()
  .where((d) => d.gate.state === 'closed')
  .include((d) => d.needs)
  .orderBy((d) => d.info.name)
  .page(20)
  .query();

// live selection: fires as doors swing open and shut
room.entities<Door>().where((d) => d.gate.state === 'open').subscribe((open) => minimap.mark(open));

// project-wide, outside any room: the key catalogue, page by page
const keys = await playserv.entities<KeyDef>().orderBy((k) => k.info.name).page(50).query();
const more = await playserv.entities<KeyDef>().orderBy((k) => k.info.name)
  .page(50, { after: keys.cursor }).query();
# in this room: doors still shut, by name, first page of 20 — with the key each one needs
shut = await (room.entities(Door)
    .where("gate.state", "closed")
    .include("needs")
    .order_by("info.name")
    .page(20)
    .query())

# live selection: fires as doors swing open and shut
room.entities(Door).where("gate.state", "open").subscribe(lambda open: minimap.mark(open))

# project-wide, outside any room: the key catalogue, page by page
keys = await playserv.entities(KeyDef).order_by("info.name").page(50).query()
more = await playserv.entities(KeyDef).order_by("info.name").page(50, after=keys.cursor).query()
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

// in this room: doors still shut, by name, first page of 20 — with the key each one needs
Room->Entities->Of<UDoor>()->Select()
    .Where(PSFields::Door::Gate::State == PSKeys::States::Closed)
    .Include(PSFields::Door::Needs)
    .OrderBy(PSFields::Door::Info::Name)
    .Page(20)
    .Then(TPSOnResult<TPSPage<UDoor>>::CreateWeakLambda(this, [this](const TPSResult<TPSPage<UDoor>>& Result)
    {
        if (!Result.HasValue()) { return; }
        const TPSPage<UDoor>& ShutDoors = Result.Value();
        Minimap->MarkShut(ShutDoors.Rows);

        // the next page rides the cursor this one returned
        Room->Entities->Of<UDoor>()->Select()
            .Where(PSFields::Door::Gate::State == PSKeys::States::Closed)
            .Page(20, ShutDoors.Cursor)
            .Then(OnMoreShutDoors);
    }));

// live selection: fires as doors swing open and shut
TPSSubscription OpenDoors = Room->Entities->Of<UDoor>()->Select()
    .Where(PSFields::Door::Gate::State == PSKeys::States::Open)
    .Subscribe([this](const TArray<UDoor*>& Open) { Minimap->Mark(Open); });

// project-wide, outside any room: the key catalogue
Client->Entities->Of<UKeyDef>()->Select()
    .OrderBy(PSFields::KeyDef::Info::Name)
    .Page(50)
    .Then(TPSOnResult<TPSPage<UKeyDef>>::CreateWeakLambda(this, [this](const TPSResult<TPSPage<UKeyDef>>& KeyPage)
    {
        if (!KeyPage.HasValue()) { return; }
        Catalogue->Show(KeyPage.Value().Rows);
    }));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
Unity C# is the same C# API here — runs as-is in Unity against the generated types
// in this room: doors still shut, by name, first page of 20 — with the key each one needs
var shut = await room.Entities<Door>()
    .Where(d => d.Gate.State == "closed")
    .Include(d => d.Needs)
    .OrderBy(d => d.Info.Name)
    .Page(20)
    .Query();

// live selection: fires as doors swing open and shut
room.Entities<Door>().Where(d => d.Gate.State == "open").Subscribe(open => Minimap.Mark(open));

// project-wide, outside any room: the key catalogue, page by page
var keys = await playserv.Entities<KeyDef>().OrderBy(k => k.Info.Name).Page(50).Query();
var more = await playserv.Entities<KeyDef>().OrderBy(k => k.Info.Name).Page(50, after: keys.Cursor).Query();

What is true of every entity.

AlwaysWhat it is
a selectionis a set of entities, not a group: a group's members are actors and exist so one signal reaches all of them, while a selection is a read that happens to stay live
historyis the instant window only: recent states indexed by sim_time, bounded by a declared depth. Branching history — alternate timelines, undo, replay of a whole match — is out of scope, because it would have to promise reproducible float values and the type rules do not
a declared method with no implementationis a finished state, not a half-configured one. Calling it answers with a verdict carrying the machine-readable reason "no implementation" : not a refusal, and not a success holding an empty result
a name never declaredis a different outcome from a name declared without an implementation: the first is a validation refusal, the second a verdict, and code can tell them apart
an unimplemented calldoes not vanish — that someone invoked it is observable to the studio. Which form the observation takes is deliberately not part of the contract, so build on the fact that it is observable, not on a log line
creating an instancecarries the entity × write atom: a cloud function, a dedicated server and a master-client hold it by default, and a plain client only where a role grants it — in every binding, not only in Unreal

A change is atomic only within one entity

Two entities changed by one caller, such as debiting a wallet and adding the item, may be observed half-applied. A pair that must appear together belongs in one instance. A hook can't stand in for that: it runs around one change, not across two.

Presets. A preset is a named bundle of aspects, machines, hooks and limits applied to a view. It adds no new concepts: everything a preset brings you could declare by hand. Five ship: stats, abilities, projectiles, drops, world-objects, and Entity presets declares each in full. A studio derives its own from them, in code or in the panel — Crate is world-objects plus stats:

Derive Crate from two shipped presets, then create one per line and tune it to 250 HP
// derived once, in the schema
[Entity("crate", Persistence = Persistence.Runtime, Presets = new[] { Preset.WorldObjects, Preset.Stats })]
public class Crate { [Stat(Max = 100, AtMin = "broken")] public Stat Hp; }

// then one line per crate, on the room host
var crate = await room.Create<Crate>(at: pos, tune: c => c.Hp.Max = 250);
// derived once, in the schema
@Entity('crate', { persistence: Persistence.Runtime, presets: [Preset.WorldObjects, Preset.Stats] })
export class Crate { @Stat({ max: 100, atMin: 'broken' }) hp: Stat; }

// then one line per crate, on the room host
const crate = await room.create<Crate>({ at: pos, tune: (c) => { c.hp.max = 250; } });
# derived once, in the schema
@entity("crate", persistence=Persistence.RUNTIME, presets=[Preset.WORLD_OBJECTS, Preset.STATS])
class Crate:
    hp = stat(max=100, at_min="broken")

# then one line per crate, on the room host
crate = await room.create(Crate, at=pos, tune={"hp.max": 250})
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

// declarations compile into the same pushed model — playserv push from the UE project or CI
UCLASS(PSEntity = (Name = "crate", Persistence = "Runtime", Presets = "world-objects, stats"))
class UCrate : public UObject
{
    GENERATED_BODY()

    UPROPERTY(PSStat = (Max = 100, AtMin = "broken")) FPSStat Hp;
};

// then one line per crate, on the room host
Room->Entities->Of<UCrate>()->Create(FPSIdempotencyKey(CrateId),
    [SpawnPosition](UCrate& Crate) { Crate.Position = SpawnPosition; });   // Position — from the world-objects preset
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.
Unity C# is the same C# API here — the same C# declaration; creating is a room-host surface, and a Unity client sees the crate arrive
// derived once, in the schema
[Entity("crate", Persistence = Persistence.Runtime, Presets = new[] { Preset.WorldObjects, Preset.Stats })]
public class Crate { [Stat(Max = 100, AtMin = "broken")] public Stat Hp; }

// then one line per crate, on the room host
var crate = await room.Create<Crate>(at: pos, tune: c => c.Hp.Max = 250);

Errors

  • An instance that does not exist, or one a predicate hides, both answer not found — so a refusal never tells a caller that something exists but is not theirs.
  • An undeclared field, nested ones included, and an obligatory field with no value, are validation refusals naming the field.
  • A transition the machine does not declare is refused as invalid_state_transition, never quietly ignored. Pushing a machine past its guards is a different operation with a different atom — administer, which no client key holds by default.
  • A version mismatch is a precondition failure: re-read and decide again.
  • A taken key is a conflict.
  • A write on a player's behalf that does not name the player is a validation refusal rather than a write attributed to nobody.
  • No permission answers forbidden, with read and write distinguished.

Limits

The numbers are set per project; what happens at each boundary is fixed now.

LimitAt the boundary
aspects per view · machines per view · nesting depth inside an aspectthe declaration is rejected at playserv push, never silently truncated
stored instance sizethe write is refused as a conflict, naming the field and the measured size; approaching the limit is observable before the refusal
selection page sizethe page is cut to the cap and "there is more" stays true — you never get a short page that looks final
instant-history windowa read outside the window is refused, not answered with the nearest value
change rate on one instancea rate-limit refusal carrying how long to wait