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
| Actor | On this page |
|---|---|
schema-author | declares entities, aspects, state machines, presets |
every actor | queries, subscribes, calls entity RPCs, reads state |
At a glance
[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()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();
};[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.blockedassociates withopenon its own, so theclose_requestedtransition declared onopenapplies inside it without being repeated. While the machine sits inopen.blockedit is inopen— a state check foropenis true, andOnEntered("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
openingdrops itsAfterSeconds, 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 declaration | What it means |
|---|---|
caller in entity.room | any 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 |
Actor | the caller's identity, the same object whoami returns |
caller.Inventory | Inventory'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:
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"))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.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.
| Declares | What it is |
|---|---|
aspect | a named field group declared whole, with its own sync policy and access mask. An entity carries several, and no field is in two |
state machine | states nestable one level, transitions and guards; several per entity |
trigger | what fires a transition — the four sources are below |
entity RPC | a verb on the entity, declared inside the view with the right atom it needs |
entity event | a signal the entity emits, delivered to whoever subscribes to that instance |
hook | pre 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 track | whether the view keeps the instant window at all |
ref | a 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.
| Source | How it fires |
|---|---|
client event or RPC | any declared one — RequestOpen above fires open_requested |
collision | a contact or a trigger-volume entry — traps, pressure plates — through the aspect Collision binds to |
data threshold | declared on a stat, 0 HP → death, enforced by hook order rather than by code in a room |
time | AfterSeconds 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.
| Axis | What is admissible |
|---|---|
filter and sort | declared fields only — there is no table handle, and a selection is entity-addressed, scoped to a room or to the project |
include | a declared ref, pulled in with the page |
paging | by opaque cursor: not an offset, not a row id, and its meaning does not survive a version change. Pass it back, never parse it |
access | predicates apply before paging, so a page never carries holes where hidden rows would be |
live | subscribing to a selection keeps it live, with members entering and leaving as their data changes |
// 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()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.// 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.
| Always | What it is |
|---|---|
a selection | is 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 |
history | is 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 implementation | is 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 declared | is 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 call | does 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 instance | carries 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:
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})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.// 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.
| Limit | At the boundary |
|---|---|
| aspects per view · machines per view · nesting depth inside an aspect | the declaration is rejected at playserv push, never silently truncated |
| stored instance size | the write is refused as a conflict, naming the field and the measured size; approaching the limit is observable before the refusal |
| selection page size | the page is cut to the cap and "there is more" stays true — you never get a short page that looks final |
| instant-history window | a read outside the window is refused, not answered with the nearest value |
| change rate on one instance | a rate-limit refusal carrying how long to wait |