The state a game keeps beside the world
The world holds every actor, value, effect and activation, and its save holds them all. What the game keeps of its own is state too, and a quickload that forgets one piece of it plays on differently. So the game keeps it in the world's seat: parts it declares, typed, with the world's options (WorldOptions.Seat), which every world the game makes, restores, rewinds, reloads or replays is given, and which the world saves, restores, hashes, records and rolls back with its own state. What the game keeps outside the seat is in none of these (visibility.md). Each of these is the game's, and each goes into a part:
- bodies by number: which actor is each side, each hero, each unit the game draws or moves. An
ActorIdis one world's; after a quickload or a reload it names nothing. An actor field holds the number, and a read finds the actor in the world it is asked of: none where it is gone; - whose turn, and the order in progress, where the content doesn't keep them: a turn content passes, as the template's
--turnsgame does on its match'sTurn, is the world's own, and an order in progress is an activation running; - the walk in progress: a path, a goal, a step a unit is taking, which the game moves each tick;
- open choices the game offered and waits on (a pick of three, a target to name), by the choice's number;
- anything the game decided from the seed or a count: who acts first, the next wave's number, a stream's state.
A run's settings (autopilot, the window, a debug view) are not state and stay out.
// what the game keeps beside the world, in the world's seat, which every world the game makes or restores is given
// with its options: the world saves, restores, rewinds, hashes and records it with its own state, so a quickload
// has it back checked with nothing of the game's to parse. Each side and its fighter are held by number (an ActorId
// is one world's, and a number is the save's), then the end and the seed
// and the match, the actor whose Turn the content keeps: whose turn it is is the world's, not the seat's
public static readonly Seat Kept = new();
private static readonly SeatPart Sides = Kept.List("Sides", PartOwner.Game, 2);
private static readonly SeatField SideAt = Sides.Actor("Side"), FighterAt = Sides.Actor("Fighter");
private static readonly SeatPart Match = Kept.One("Match");
private static readonly SeatField OverAt = Match.Whole("Over", 0, 3), SeedAt = Match.Stream("Seed");
private static readonly SeatField MatchAt = Match.Actor("Match");
/// <summary>The run's seed: the world's, and the game's own choice from it (<see cref="First"/>), so it is in the seat.</summary>
public ulong Seed => W.ReadStream(SeedAt);
/// <summary>0 while the game goes on; else 1 + the side left standing, or 3 where neither is.</summary>
public int Over => (int)W.ReadWhole(OverAt);
/// <summary>Whose turn it is, 0 or 1: the match's Turn, which the content passes, read here.</summary>
public int Turn => (int)W.Get(W.ReadActor(MatchAt, 0), "Turn").ToWhole(Rounding.TowardZero);
/// <summary>Whether a side's order is in hand: its fighter's activation, a walk or an ability's windup, still running.</summary>
public bool InHand(int side) => W.Exists(Fighter(side)) && W.Running(Fighter(side), out _);
/// <summary>Whether a side's order is a walk, which the game moves each tick while it runs.</summary>
private bool Walking(int side) => W.Exists(Fighter(side)) && W.Running(Fighter(side), out RunningActivation run) && run.Ability == "Walk";A part is One record (there from the world's making, each field 0 until written), a List of records in order, or Keyed by its first field (a whole or an actor), each key once; each holds up to its cap, and has a field at least. A field is a whole in the range the game declares (one holding 0, which a record holds until it is written; a key's may leave it out), a Num, a flag, an actor, a place (two Nums) or a stream's state. A part is the game's, or each side's: a side's copy is told to that side alone (PartsAs), and a question asked as a side reads the game's parts and its own, never another side's, nor any stream's state. A part holds no content name: where the game's code names a kind, the game keeps its own number for it, and names what its code reads in Needs. A need is one of eight kinds: an archetype, an ability, an effect, a table, a pool, a link, an event its code raises (("raises", "event", "Moved")) and a stat it reads (("reads", "stat", "Speed")). A reload whose content drops one is refused by name, the game going on with the content it had; another kind is the game's code wrong, and throws.
The game's code writes a part between moments, before or after a tick (Write, WriteNum, WritePlace, WriteStream, AddRow, KeyRow, RemoveRow, ClearRows): a write while an order is given or a question asked is refused in words. A write that changes nothing is no write. A recording keeps each write, so a replay's saves hold the parts as the game's do. A rewind puts a part back as the save held it; a count that must outlive a rewind is a kept pool on an actor.
The seat's reads and writes
The game declares its parts once on a Seat (src\Ledger\Seat.cs), and reads and writes them on the world (src\Ledger\Runtime\World.Seat.cs); neither is in Api.cs. A row is a record's place in its part, 0 for a One. side names a side's part's copy, and is left out for the game's. The declaration:
seat.One(name, owner = PartOwner.Game),seat.List(name, owner, cap)andseat.Keyed(name, owner, cap)give aSeatPart; on it,part.Whole(name, lowest, highest),part.Num(name),part.Flag(name),part.Actor(name),part.Place(name)andpart.Stream(name)give aSeatField. A keyed part's key is its first field.
The reads, at any time:
int Rows(SeatPart part, ActorId side = default): how many records it holds;long ReadWhole(SeatField field, int row = 0, ActorId side = default), a whole, an actor's number or a flag as 0 or 1;Num ReadNum(...),bool ReadFlag(...),(Num X, Num Y) ReadPlace(...)andActorId ReadActor(...)the same way, an actor read as none where it is gone;ulong ReadStream(SeatField field, int row = 0);int RowOf(SeatPart part, long key, ActorId side = default), and by anActorIdkey: a keyed part's row of the key, or -1.
The writes, between moments:
void Write(SeatField field, int row, long value, ActorId side = default), and the same taking aboolor anActorId;void WriteNum(SeatField field, int row, Num value, ActorId side = default);void WritePlace(SeatField field, int row, Num x, Num y, ActorId side = default);void WriteStream(SeatField field, int row, ulong value);int AddRow(SeatPart part, ActorId side = default): a list's next record, its fields 0, and its row;int KeyRow(SeatPart part, long key, ActorId side = default), and by anActorIdkey: the key's record, added where it has none, and its row;void RemoveRow(SeatPart part, int row, ActorId side = default), those after it moving up a row, andvoid ClearRows(SeatPart part, ActorId side = default).
A body's speed and aim, kept by its actor: the part is Kept.Keyed("Bodies", PartOwner.Game, 64), its key Bodies.Actor("Body") and its fields Bodies.Num("Speed") and Bodies.Place("Aim"). Between ticks, int row = W.KeyRow(Bodies, body) is the body's record, made the first time, and W.WriteNum(Speed, row, Num.Of(3)) and W.WritePlace(Aim, row, x, y) write it. A read asks int at = W.RowOf(Bodies, body), -1 where the body has no record, then W.ReadNum(Speed, at) and W.ReadPlace(Aim, at).
The quicksave
Its head names the game (GAME quicksave 2), so another game's file is refused by name. A load reads and checks the whole file before anything live is replaced: the world restored into a world of its own, each part held to the game's declaration (a save of another declaration is refused, naming the part) and each value to its kind's range. A file broken anywhere replaces nothing, and says why in words. The game puts back only what the world doesn't hold: here, the places its plane keeps.
/// <summary>The quicksave: its head naming the game, the world with the seat's parts in it, and the places its space tells.</summary>
public string Quicksave() => Ledger.Host.Quicksave.Write(Name, W, S.Space, "");
/// <summary>
/// A quickload: the file read and checked whole before anything live is replaced, the world's restore holding the
/// seat's parts to the game's declaration and each value to its range, so a file broken anywhere replaces nothing and
/// says why (a FormatException, in words); then the world replaced, the places put back, and the game rebound to it
/// as a reload rebinds it. The file is read over the session's restore space, so a recording goes on: its next
/// segment opens at the restored world, and the stretch the quickload abandoned stays in it, replayed as played.
/// </summary>
public void Quickload(string text)
{
Quicksave.Loaded loaded = Ledger.Host.Quicksave.Read(Name, text, Catalog, S.Options, S.RestoreSpace);
S.Replace(loaded.World);
loaded.PutBack(Plane.Plane);
Rebind(W, new ReloadReport());
Log.Add("quickloaded at tick " + W.Tick);
}The rebind
After a quickload, and after a reload of the content into the running world, the game's state is rebound to the new world: its orders made again from the new catalog, its bodies found again by number and their places told, and the place of each actor the reload dropped freed. One method does both, so a reload can't miss what a quickload rebinds. A reload that drops an actor a part holds leaves its number there, which reads as none, and names the records in its report.
/// <summary>Each name the game's code reads, which a reload is refused without.</summary>
public IEnumerable<(string Use, string Kind, string Name)> Needs => new[]
{
("makes", "archetype", "Side"),
("makes", "archetype", "Fighter"),
("orders", "ability", "Strike"),
};
/// <summary>
/// The game's state rebound to a world now in the session's place, after a quickload or a reload: its orders made
/// again from the world's catalog, its fighters found again by number and their places told, and the place of each
/// actor the reload dropped freed.
/// </summary>
public void Rebind(World world, ReloadReport report)
{
_o = new Order(world.Catalog);
_rate = world.TicksFor(Num.One);
foreach (long n in report.Dropped) Plane.Plane.Remove(n);
for (int i = 0; i < 2; i++)
{
ActorId id = world.ReadActor(FighterAt, i);
if (!id.IsNone && Plane.Plane.Has(world.NumberOf(id))) S.Mover(id);
}
}The headless runner's roundtrip holds the quickload to the game: a quicksave loaded into a new game and saved again at once is the same file, and the two then play on to the same save. State the game keeps outside the seat, in a field of its own, is in no file: the first passes and the second fails, if the save is made where it matters. Save mid-order, not on a turn's edge, where an order in progress is nothing to lose.
From ledger-v3's guide/ledger-author/state.md, last changed 2026-11-06.