Ledger v3 · C# · Godot 4.7 Get started
The author's guide

The host: real time, turns, a space of the game's own, and a recording across a reload

ledger new GAME writes a real-time game on the kernel's plane. Two flags change the host, and each changes what the game's code must do around the world. Write the flags when the game is made: they choose lines across the rules, the Godot side and the content, and are not a setting to turn on later.

--turns: no tick while the player thinks

A turn-based world needs no time while a player decides, and a recording that ticked through the thinking would hold every idle tick. The host is paused whenever the game waits on the player, and runs only while an order resolves. The game's frame pauses it as the turn comes back, inside the tick, so no later physics step of the same frame runs:

game/Main.cs
public void BeforeTick() => _g!.BeforeTick();

public void AfterTick()
{
    _g!.AfterTick();
    // the turn's order has resolved and the content passed the turn to the player: no tick until the player orders
    if (_g.Waiting) _host.Paused = true;
    if (_quitAt >= 0 && (_g.W.Tick >= _quitAt || _g.Over != 0))
    {
        GD.Print("the end: tick " + _g.W.Tick + ", over " + _g.Over + "; health " + _g.Health(0) + " to " + _g.Health(1));
        _quitAt = -1;
        GetTree().Quit();
    }
}

and an order the player gives sets it running, the walk here and each ability the kit's bar uses (below) alike:

game/Main.cs
// a walk is the game's order, not an ability: the ability bar lists the content's abilities, and W the walk
private void Walk()
{
    if (!_g!.Give(0, "walk", out string words)) Note(words);
    // the order given, the world runs until it resolves and the turn comes back; AfterTick pauses it again
    else _host.Paused = false;
}

Every other place that changes whose turn it is sets Paused from Waiting again: the start, a quickload, the autopilot turned on or off. A place that forgets leaves the game frozen, or ticking while the player thinks. The turn is content's. Whose turn it is is a pool on the match's own actor (Turn), and a rule the match holds passes it as a side's order has done what it does: after Resolved of anyone, an activation once all it set off has settled. A strike resolves at once; a walk, the content's Walk, a channel of a second, resolves as it ends; and an ability added and granted takes its turn the same, with no C#. The game keeps no turn of its own: it reads Turn, and Waiting is read from the world, the player's turn with no order in hand (no activation of its fighter running). The autopilot gives its side's order on its side's turn, and the game walks a fighter while its Walk runs.

A round chosen first. A game where each side chooses its moves before any goes, a battler's round, gives every order as the player chooses it, and content holds each until its holder's turn: an effect that says holds Move until turn keeps the order, answered done and marked Held, and sets it off as the turn comes to its holder, in the order content sorts the turn by (ordered(holder, Priority) reads a held order's property). The host is paused while the player chooses, as above, and runs once the round's orders are all given; it reads what is chosen with HeldOrders, and a choice taken back is a Cancel of its id. Before the clause, such content refused every move off its turn Countered and kept the move in an effect's vars to give it again by a rule.

--space own: a space the game answers

The game's own space answers what the content asks of places beyond distance: here a fact, Seen, which the content reads as requires fact(Seen, holder, target) > 0. It keeps the kernel's plane inside it, for who is within reach, and tells where its actors stand from that plane, so the quicksave writes it as it writes a plane:

game/Sim/GameSpace.cs
using Ledger;
using Ledger.Host;

namespace GAME.Sim;

/// <summary>
/// The game's own space: the plane inside it answers who is within reach and how far, and the game answers the facts
/// the content declares, here Seen, whether the first actor's side sees the second. The host's space over the plane
/// passes the plane every other question and command, and tells where its actors stand from it, so a quicksave writes
/// this space as it writes a plane.
/// </summary>
public sealed class GameSpace : SpaceOver
{
    /// <summary>How far a side sees. The space's own rule: the content asks it by name and never sees the number.</summary>
    public readonly Num Sight = Num.Of(8);

    public GameSpace(Num size) : base(new PlaneSpace(size, Num.Of(4))) { }

    // a question is pure: it reads only the places, which change between orders, so a recording can keep its answer
    public override Num Fact(string fact, long a, long b)
    {
        if (fact != "Seen") return Num.Zero;
        Num d = Inner.Distance(a, b);
        return d >= Num.Zero && d <= Sight ? Num.One : Num.Zero;
    }

    // the same of a point: whether the side sees a spot (fact(Seen, holder, target) where target holds a point)
    public override Num Fact(string fact, long a, in Place b)
    {
        if (fact != "Seen") return Num.Zero;
        Num d = Inner.Distance(Place.OfActor(a), b);
        return d >= Num.Zero && d <= Sight ? Num.One : Num.Zero;
    }
}

A space's question is pure: it reads only what changes between orders (here the places), so a recording keeps its answers and a replay gives them back. A play says what the space answers with fact Seen a b, or of a point with fact Seen a 5 0. Its one write beside the commands is Stand: the world asks it to stand an actor content makes at a place (create Cloud at target) as the actor's Create is raised, and it is put back as a preview ends, as a push is. A space without it answers no, and the made actor stands nowhere, a fault saying so.

The session over it is LedgerSession<GameSpace>. The world host takes it as it takes the plane's: WorldHost.Host takes any session (ILedgerSession), so the inspector, the running game's preview and the dock's pause and Reload reach this game as they reach one over the plane, and WorldHost.Begin watches the content and takes reloads.

Put, tell, step. A game moves its bodies in its space, names each as a mover (Mover), tells them in one Placed (TellMovers), and then steps the world (Tick), moving nothing untold between the two. Where a frame keeps that order, the world's tick wakes an auto only for a body its clause can take, which is most of what a crowd pays for; a frame that steps without telling keeps the old wake, as before, and so does the step after a save taken between the two. Telling a put a frame late, or putting a body after the Placed and before the step, isn't an error the world can see: an auto may then act a tick late. A space whose distances move with no body moving, as a rank's line closes up at a death, tells no Placed: there a body comes into reach of another without either moving, and only the old wake looks for it. The world can't tell such a space from a plane, so the rule is the game's to keep.

The host's helpers for a game's own space code. None is the world's, and a game that calls none is as before:

  • SpaceOver, which the space above derives from: it holds a PlaneSpace (Inner, and its Plane) and passes it every question and command, each virtual, so a game's space overrides only what it adds (a fact, both of which answer 0 unless overridden; a meter around a question; a command of its own).
  • plane.Put(S, actor, x, y) on a PlaneSpace: the actor put at the place clamped to the arena and named as a mover, as the template's walk does; plane.Put(S, actor, x, y, fromX, fromY) names the path it came along, so an area it passed through is heard crossed. It says whether the actor moved, and names it either way. A quickload puts a quicksave's places back with loaded.PutBack(plane.Plane).
  • Steer.Toward(x, y, tx, ty, step, keep, out nx, out ny): a step toward a point, stopping keep short, by the plane's own distance and one Num.MulDiv an axis, so every game steps alike; Steer.Diagonal is a diagonal's share of a unit step on each axis. A walk of a game's own that divided first, or took its root through a double, lands a millionth elsewhere here.
  • Trig.Cos(degrees), Trig.Sin(degrees) and Trig.Direction(degrees) of any whole number of degrees, from one table: never Math.Cos, whose last digit differs between machines, where a place is the world's.
  • Found, a collector of a question's answers (Plane.Within(..., found)), its arrays kept between questions: Clear it before one, and SortByNumber before reading what it found in an order that matters.
  • On the plane itself, Plane.Nearest(x, y, radius, filter, out number, out distance) and Nearest(actor, radius, filter, ...): the nearest placed actor within a radius, the lower number on a tie, among those the filter admits, a struct of the game's own (IPlaneFilter.Admits(number)), allocating nothing. The world never asks it; a game's targeting and steering do.

What a host calls on the world

The game's C# reaches the world through its session (LedgerSession, S in the template) and the world it holds (World, W). Here is what a host calls, by what it does, and each member's comment gives the rest; which file holds each is said after the list.

  • Reading a value. W.Get(actor, "Might") is a stat worked out now, or a pool's amount; W.MaxOf(actor, "Health") is a pool's maximum. W.Get(hero, "Offers") on a pile's name is how many items it holds, the count content reads as holder.Offers. W.Pile(keeper, "Offers", list) gives the items too, top first, and W.PileOf(item, out keeper) the pile an item is in. W.Has(actor, "Stunned") asks for an effect held or a tag carried, W.Stacks(actor, "Rage") for an effect's stacks, and W.Exists(actor) whether an actor still stands. Ask Exists before a read: a read of an actor that is gone is refused in words. long Remaining(ActorId actor, string effect) is an effect's time left, in ticks, on the actor's longest-lasting instance of it: 0 where it holds none, long.MaxValue where it never ends. So a power-up's clock on the screen is W.Remaining(hero, "Star") over the rate. An effect that lasts a count of events (lasts 5 RoundEnded, lasts 3 turns) never ends by time, so Remaining answers long.MaxValue for it: its count left is HeldRow.CountLeft, and what it counts HeldRow.Counts ("turns" or the event's name), each row of W.Holds(actor, rows, vars) one instance the actor holds. So a reversal's rounds left on the screen are its row's CountLeft, with no pool kept beside the lifetime to say it again. An ability's cooldown is an effect its caster holds, named ABILITY.cooldown (ledger forms cooldown): W.Remaining(hero, "Dash.cooldown") is what is left of the Dash's, and a host that writes one, a client told its server's, puts it on with W.Apply(hero, "Dash.cooldown", 1, default, Lasting.Ticks(left)) and takes it off with W.Remove(hero, "Dash.cooldown").
  • Finding actors. W.Find(number) is the live actor with a number, or none (IsNone): the game keeps numbers, as the plane and a save do, and finds each when it asks. W.NumberOf(actor) goes the other way. ActorId LinkOf(ActorId actor, string link) is the actor a link points at (W.LinkOf(actor, "team"), a body's side here), and W.Global(link) a global link's. int Having(string effectOrTag, List<ActorId> into) fills the list with every live actor carrying a tag or holding an effect, as Has asks it, in the order of their numbers, and answers how many, so a host lists the bodies content made, a fight's or a wave's, with no walk of its own (W.Having("Body", list)); asked into the same list each frame, it allocates nothing. W.ForEachActor(list) gives every live actor. string ArchetypeOf(ActorId id) is the archetype an actor was made from, empty for one gone, so a game draws a pickup by its kind as the world holds it.
  • Reading a definition's times. catalog.Times(name, list) fills the list with an ability's or an effect's times as content gives them, once its template is filled in, each a DefTime (string Clause, string Effect, long? Ticks, string Clock; Effect is empty for a clause of the definition's own, and Ticks has no value where it is none; Clock is the clock the time runs on, as content writes clock: a stat's name, read on what holds the time, the caster for a phase or a cooldown, the projectile for a flight, the target for an apply, the holder for an effect's lasts and every, or world; a channel's every gives its beats' clock, so Blizzard's channel 6s clock world with every 1s clock CastSpeed gives channel 360 world and every 60 CastSpeed): an ability's charge up, windup, channel or flies, a channel's every blocks, and its cooldown, an effect's lasts and each every, then each effect its steps and rules apply, in the order written however deep in an if, with the apply's time, or its effect's lasts where it says none. Ticks is none where the time reads the world, a stat or a value (read the world for it), or where none is written: a channel's every 1s / (1 + 0.1 * self.beats) gives none, its beats read from the cast bar's TicksToBeat. So a training room's frames table reads a move's numbers, never the content's text: for ability EmberStandLight from Normal(0, 4, 2, 7, EmberSL), windup 4, apply EmberSL 2 and apply Recovering 9; for its strike, apply Blockstun 11, apply Combo with none, apply Hitstun 14 and apply CancelWindow 8.
  • Ordering. S.Give(order) gives an order and answers its outcome; Outcome Check(Order order) asks whether it would be taken without taking it (W.Check(order)), and its shorthand Outcome Check(ActorId caster, string ability, ActorId target = default, Num x = default) asks it of an ability used at an actor. An order is filled from an Order the game keeps (new Order(catalog), made again at a reload): o.Activate(caster, "Strike").At(foe). ActorId Create(string archetype, params (string Link, ActorId To)[] links) makes an actor, as the game's own rules do (W.Create("Wall", ("team", side))).
  • Reading the orders held. An order content holds for its holder's turn (holds, ledger forms holds) answers done with Outcome.Held set, and its Activation the id it will start under. int HeldOrders(List<HeldOrderRow> rows) fills the list with every order held, oldest holding instance first, and answers how many: each row's Holder, Ability (its name), first Target, Id, the holding Effect's name and the Until event's. HeldOrdersAs(sees, side, rows) lists them as a side sees the world: a row for each holder sees names, its ability, target and id only where the holder stands on side, so a foe's is seen as held and no more. Num Ordered(ActorId actor, string? prop = null) reads what content's ordered(ACTOR [, PROP]) reads, by the property's name: 1 while the actor holds an order and 0 if not, or the held order's property, its default where it carries none (W.Ordered(knight, "Priority") for a move's card, read in a preview's body too). Num? OrderedAs(sees, side, actor, prop) reads it as a side sees the world: a holder sees doesn't name holds none, and a foe's order reads 1 but its properties null, its side's. A held order is cancelled as a running one is, o.Cancel(id); it is dropped when a new order replaces it, its holder falls or its hold ends, each a Dropped refusal in the journal. Holds and a side's view carry none of it.
  • Telling the world what the game saw, and setting a global link. Each is an order, given as any order is, and a shorthand on the world that gives it:
    • Order Raise(string eventName, ActorId subject, ActorId source = default), on the world Outcome Raise(...) the same: raises one of the content's events on a subject, from a source or none, and its rules run as for a raise step; a box the game's engine saw meet a fighter is told so (W.Raise("Hit", foe, striker)). An activation's own events (a Commit, a Stopped, a Pulse, a Finished, a Contact) and a Resolved are refused;
    • Order SetGlobal(string link, ActorId to), on the world Outcome SetGlobal(...): points a global link at an actor, or at none (ActorId.None), as content's set link does;
    • Outcome Give(Order order) gives any of these, and S.Give(order) through the session, recording it.
  • An order's builders. Each starts an order of its kind in the Order the game keeps, or adds a part to the one started, and gives the order back, so they chain: S.Give(o.Activate(caster, "Bolt").At(foe).X(Num.Of(3))). A name the catalog lacks throws as it is filled, but for a property's tag, refused BadProperty as the order is given. Each builder that starts an order has a shorthand on the world of the same name that gives it at once (W.Activate(caster, "Bolt", foe) for o.Activate(caster, "Bolt").At(foe) given); a builder that adds a part to one started (At, X, With, Tagged and the like) has none, its part an argument of the shorthand where it takes one. Filled again each frame, an order allocates nothing once its buffers have grown. By what each does:
    • using an ability: Order Activate(ActorId caster, string ability) starts one. Order At(ActorId target) names one more actor it is used at, the target or a list's next; Order At(Num x, Num y) a point, for an ability whose clause says target point; Order Toward(Num dx, Num dy) a direction, for one that says target direction, one of no length refused as the order is given. Order X(Num x) is its X; Order Grant(long instance) the grant it is used through, by its instance's number (AbilityRow.Grant), where two effects grant the ability; Order Answers(long choice) the open choice it answers, by its number as W.Choices gives it; Order Id(long count) the id its activation will take, its caster's count, refused IdTaken unless past the caster's last;
    • a running activation: Order Cancel(ActivationId activation) ends it Cancelled; Order Release(ActivationId activation) ends a held one's hold, and Order Release(ActorId holder, string ability) the hold of that ability its holder holds, found by the world, so a guard or a channel is let go with no handle kept;
    • a flight's report: Order Report(ActivationId activation) starts one, and Order Met(ActorId actor) names the actor it met, none (as it starts) for a miss; the world's shorthand is below;
    • an order's numbers and tags: Order With(string name, Num value) is an Activate's or a Report's number property (X and the grant are named so too), an Apply's var or a Draw's argument; Order With(string property, string tag) a tag property, an Activate's or a Report's; and Order With(string link, ActorId to) a Create's link;
    • actors and links: Order Create(string archetype) makes an actor, its links named with With; Order Destroy(ActorId actor) ends one; Order SetLink(ActorId actor, string link, ActorId to) points an actor's link at another, or at none (ActorId.None); SetGlobal, a global link's, is below;
    • effects: Order Apply(ActorId actor, string effect, int stacks = 1, ActorId source = default, Lasting duration = default) puts one on, its vars named with With; a Lasting given (Lasting.Ticks(n), Lasting.Turns(n), Lasting.Of(n, "Event")) counts that in place of what its lifetime says. Order Remove(ActorId actor, string effect, int stacks = 0) takes off every instance, or that many stacks, oldest first; Order Remove(InstanceId instance, int stacks = 0) one instance, whole or that many stacks; Order Reschedule(InstanceId instance, Lasting left) sets what one instance has left. A game that keeps what its engine knew as effects tells it so (below, "When the engine moves the bodies");
    • pools: Order Deal(ActorId source, ActorId target, Num amount) deals to the target's damage pool, from a source or none, each of its tags named with Order Tagged(string tag); Order Set(ActorId actor, string pool, Num amount) sets a pool to an amount, a scenario's given;
    • places: Order Placed(ActorId actor) tells the world the space placed or moved one actor; Order Placed() starts one naming none yet, Order Mover(ActorId actor) names one more, and Order From(Num x, Num y) the path the last mover named came along since it was last told, so an area it passed through is heard crossed. The session's Mover and TellMovers fill one for a frame (below);
    • the clock: Order Advance(long ticks) moves it on by that many ticks, Order AdvanceTo(long tick) to a tick;
    • tables and streams: Order Draw(string stream, string table, int n) draws up to n rows from a table on a stream, Order For(ActorId actor) naming the actor its conditions read and Order Into(List<TableRow> rows) the game's list its rows go into; Order Reseed(string stream, long number) starts a stream again from the world's seed, its name and the number;
    • piles: Order Move(ActorId item, ActorId keeper, string pile, bool top = false) puts an item into a pile an actor keeps, at its bottom or its top; Order IntoPlay(ActorId item) takes it out of every pile into play.
  • A flight's report. Outcome Report(ActivationId activation, ActorId met, params (string Name, Num Value)[] props) tells the world what a missile in flight met: activation is the flight (W.FlightOf(projectile)), met the actor it hit, or ActorId.None where it met nothing, a wall or the ground, which ends it Stopped Stop.Missed; props are its report's numbers, held to what its flies takes. A tag property goes in the order's form, o.Report(flight).Met(met).With("Element", "Element.Fire").
  • The seat's reads and writes: W.KeyRow, W.WriteNum, W.ReadNum and the rest, each with its signature, are in state.md, "The seat's reads and writes".
  • Numbers. A Num is a whole count of millionths: Num.Scale is 1,000,000, and Raw the count, so a sum is exact where a float's isn't. Num.Of(3) is a whole, Num.Ratio(1, 3) a fraction rounded to the millionth, and Num.FromRaw(raw) one from its count. Its operators are C#'s: + and - (and a unary -) exact, an overflow thrown; * and / each rounded once to the millionth, half to even, and a division by zero thrown; % the remainder of the counts; ==, !=, <, >, <= and >=; and an int is a Num where one is wanted. Num Abs() is its absolute value and Num Sqrt() its square root, rounded down to the millionth (of a negative one, thrown); Num.Min, Num.Max and Num.Clamp(v, lo, hi) say what they do, Num.MulDiv(a, b, c) is a × b / c rounded once, Num Round(Rounding mode) is it made whole (Rounding.Floor, Ceiling, Nearest or TowardZero), and long ToWhole(Rounding mode) that whole as a long. A host rounds an engine's float once, as it tells the world, with Num Nums.Quantize(double v) (in Ledger.Host), to the nearest millionth, half to even; Nums.ToFloat(n) and Nums.ToDouble(n) go back, for drawing only.
  • An order through an item's grant. A card or an item in a granting pile gives its keeper its abilities, one grant each, and an order names the grant it is used through, as a play's act p Sell using k does. W.Abilities(keeper, rows, prices, lines) lists one AbilityRow a grant: its Ability, its Source (the card or item it comes from; for an actor's own kit, the actor) and its Grant, the instance the order names. So a sale of a card is the row whose Ability is "Sell" and whose Source is the card, given as S.Give(o.Activate(player, "Sell").Grant(row.Grant)). An order naming no grant is used through one the world finds for it, so where two cards grant Sell, the order names the one meant. The kit's ability bar does this for a game (its rows narrowed by Source, each order handed with its row through LedgerKit.Hand: "The presentation kit", below). W.Running(actor, out running) is what an actor is casting now, its TicksToBeat the next beat's tick whether its channel's every is a constant or a formula, or beats on a clock of its own (every 1s clock CastSpeed under channel 6s clock world): Elapsed, Length, TicksLeft and Rate are then its length's clock's, the world's for that Blizzard, so a haste moves its TicksToBeat and none of them; content reads the same cast with remaining(ACTOR) and elapsed(ACTOR), in its clock's ticks where the bar gives the world's, and ActivationId FlightOf(ActorId projectile) the flight a missile is (W.FlightOf(projectile)); W.Report tells the world a flight's hit. W.Running(actor, ...) reads an actor's slot, which a projectile never holds, so it answers false of a projectile: a flight is read by its id, W.Running(W.FlightOf(projectile), out RunningActivation run), its Target what it flies at (after a reflect, the side it flies back at), its Side whose it is, and its Elapsed over its Length how far along it is. A flight at a speed (flies ARCHETYPE at F) is timed by the world as it is loosed and lands as it arrives, with no report: the game draws it from its caster toward its Target at that share and reports only what meets it before, a body in its way. A piercing flight keeps whom it has flown on past, so report every actor its body overlaps: one it has passed is refused AlreadyMet, nothing run. W.Met(flight, list) gives them, in the order met, as W.Pile gives a pile's items: a game whose engine keeps collision exceptions sets them again from it after a quickload. A running cast's tags are read by its id, as an event's are by its number: bool HasTag(ActivationId activation, string tag) and int Tags(ActivationId activation, List<string> into), its ability's tags, then the carried tags its chain gave it (an arrow an echo casts tagged Echo has Echo), a tag had as an event has it, itself or one under it. False and none where nothing runs by that id or the tag isn't declared, and nothing allocated once the list is warm, so an AI asks it each frame and cuts the casts its designer tags: W.HasTag(run.Id, "Interruptible"), not the abilities its C# names.
  • Telling moves. S.Mover(actor) names a body the game moved, S.TellMovers() tells them in one Placed, and S.Tick() steps the world, in that order (above).
  • Saving and restoring. W.Save() is the world's save; World.Restore(catalog, save, ...) makes a world of its own from one, and S.Replace(restored) puts it in the session. Restore over S.RestoreSpace, the space a restore runs over (the recording's keeper while one is on), so a recording goes on across the load. The template's Quicksave writes the world, the places and the game's part as one file, and its Quickload reads and checks all of it over S.RestoreSpace before anything live is replaced.
  • Previews. W.CheckAs, W.PriceAs and W.ForecastAs ask as a side sees the world; W.PreviewAs runs any question in a world put back as it ends.
  • The journal. W.Cursor() marks where a read begins, and W.Read(ref cursor, out entry) gives each row since.
  • A sort's receipt. A world made with WorldOptions { Sorts = true } keeps what each sorted for each read as it sorted (W.Sorts, one tick's, a SortRead each: its rule, tick and cause, each actor found in its order with its keys' readings, and each place its set left as it was), never saved, hashed, journaled or told, and plays as a world made without it. Ledger.Checks' SortReceipt, read after each tick and order (receipt.Read(W)), names why each place fell where it did, changed or kept (receipt.Lines(number, "Place")), as ledger play --why prints it.
  • The state hash. W.StateHash() is the world's state as one number (below). A batch that compares worlds across a content edit compares their state hashes, since a save carries the catalog's fingerprint and so moves with any edit. The hash holds each definition by its place in the catalog, so an edit that adds or takes one away moves it too: across such an edit, compare the saves without their content line.

Where each is, by file: src\Ledger\Api.cs holds the types a host passes and is answered in (ActorId, InstanceId, ActivationId, Outcome and its Refusal, Lasting, Place, Shape, WorldOptions) and the space's interfaces, not the world's members. World's are in src\Ledger\Runtime\World.cs and the parts beside it, each World.NAME.cs holding what its name says (its effects' in World.Effects.cs, the seat's reads and writes in World.Seat.cs); an order's builders are in src\Ledger\Order.cs, the seat's declaration in src\Ledger\Seat.cs, Num in src\Ledger\Num.cs, DefTime in src\Ledger\Content\Catalog.cs, and the session's members and Nums in src\Ledger.Host\ (LedgerSession.cs, Nums.cs).

The plane's push. The kernel's plane carries out one command, Push: its target is driven straight away from whoever requested it, by the amount, and held inside the arena. Content declares it as it declares any command, with command Push, then requests it with request Push target by 1. A request is its rule's actor's, and in an effect that is the effect's holder's. So a push away from the hero is requested in a rule the hero holds, not in the effect on the foe (traps.md). Anything else content requests of the plane moves nothing; a game's own space answers what it declares (--space own, above).

What the state hash holds. StateHash holds two worlds alike that were started alike and fed alike, as twins and lockstep peers are: hashed each tick, the two agree until one is fed otherwise. A world restored from a save lays its rows out anew, so it hashes apart from its source though their saves agree; two worlds restored from the same save hash alike. So to hold a quickload to the live world tick by tick, compare their saves (W.Save()), or lay the live world anew too, restoring it from its own save, and hash the two. A world rolled back in place by its snapshots is no restore: it hashes as the frame it came from (below, "A rollback host"), so rollback peers compare hashes. The template's run prints the hash for a run held to itself: the same seed run twice gives the same hash.

When the engine moves the bodies

A game whose engine moves its bodies, Godot's physics moving a CharacterBody2D by numbers it reads from the world (W.Get(hero, "JumpSpeed")), keeps the world as the record of what the engine did, so a save, a recording and a replay hold the game with no physics. Four things keep it so.

The engine's edges, told as events and kept as effects. What only the engine knows, the floor under the feet or a wall at the side, the game tells the world as the engine's answer changes, on an edge, by Raise. The platformer's host, after each move, raises Landed where body.IsOnFloor() and the hero holds no OnFloor, and LeftFloor where the floor has gone and it still does: if (floor && !W.Has(hero, "OnFloor")) S.Give(o.Raise("Landed", hero)). Content keeps each as an effect: its Feet hears after Landed of holder and does apply OnFloor, and hears after LeftFloor of holder when has(holder, OnFloor) and does remove OnFloor and apply Coyote. So a jump asks the world (requires has(holder, OnFloor) or has(holder, Coyote)), never the engine; the world's save and its recording hold what the engine knew; and a replay with no engine gives every rule the same answers. Raise on the edge, not each frame: an edge is one order, where a state told each frame is an order every tick of the recording.

A position rounded once to Num. The engine's float becomes a Num once, as it is told: the place the game gives its space is Nums.Quantize(body.GlobalPosition.X), and nothing the world decides goes back through a float. The engine keeps its float, which the game's part of the quicksave writes exactly, beside the world's rounded place.

What a quickload makes again. A quickload puts back the world and, in the game's part, every body's floats. The engine's next move reads more than those: its own memory of the last move, which no save holds. The platformer's loads parted from its run with no load three times, each on such a memory, and each was closed on the game's side:

  • a floor across two tiles is reported in the engine's own order, its broadphase's, which a load doesn't keep, so the loaded hero stood on the other tile: the game finds the floor from the bodies' places, the nearest to the hero's middle;
  • a moving platform's carry is read from the physics server, worked out at a step the loaded run never had: the game carries the hero itself, by the platform's own step between two ticks, a function of the tick;
  • a body set in place isn't in the engine's broadphase until a physics step, so the first move after a load missed the floor by 0.55 px: the game puts the platforms where the last frame left them, steps the physics once with the world held (the world host's Paused), then snaps each body to its floor.

To find one, write a quicksave after every tick from the load on, in the run with no load and in the loaded run, and compare them: the first tick whose saves differ, and their difference, say what the engine remembered. To hold them, quicksave at a few hard places, a jump, a dash, on a moving platform, a hurt; load each in a new process, play it to the end, and compare its final quicksave with the run's own, byte for byte.

The headless verbs that need the engine. The template's headless tool steps the world with no engine. Its check and replay serve such a game as they serve any: content's plays, and a recording holding all the world was told. Its run, record, roundtrip and perf step the template's Game.Step, which moves no body the engine moves, so for this game they move nothing. They go to the game's own arguments under Godot's --headless, godot --headless --fixed-fps 60 -- --autopilot --record=FILE, as the platformer's do; the tool's usage, and the game's CLAUDE.md in its line on the tool, say so.

Threads: a batch of worlds

A catalog is read and never written once it loads, so one catalog serves worlds on many threads at once: a batch of a thousand fights, each its own world, ends at the same saves on 32 threads as on one. A world is one thread's: step it, give it orders and read it from the thread that made it, and never share one between two.

Each thread a world runs on needs the stack its bounds were measured for: about 768 KB at the default depth budget, more at a deeper one (LedgerSession.StackNeeded(options)). A session asks it as it is made (ThreadFits) and is refused in words on a thread with less. A thread the host makes for a batch is given more as its size, new Thread(Fight, maxStackSize: 4 << 20), and the session made on it asks ThreadFits there.

A batch's speedup stops where a world's making allocates more than its ticks do: a fight whose setup allocates 1.2 MB against its ticks' 0.8 MB went near 2.8 times as fast on 8 threads as on one with the workstation collector, and 3.7 times with the server's, and little faster on more. The server collector is the project's setting (<ServerGarbageCollector>true</ServerGarbageCollector> in the batch's .csproj), not the world's.

The presentation kit: what a side is shown

The adapter's kit (using Ledger.Godot.Kit;, its design in the kernel's godot\notes\kit.md) shows the world to one side, and only what that side may be told. Content says rules, never a widget or a look: no content word names one. The game made by ledger new uses it in game\Main.cs (its kit region):

  • LedgerKit, one per side shown, with the world host and a viewer: Viewer.All(side) in a game that hides nothing, Viewer.Of(side, sees) with the host's sees in one that does (here, with --space own, the space's Seen). Its own actors, those whose allegiance link points at the side, it sees whole.
  • PoolBar and CastBar for an actor, by its number: nothing of an actor the side doesn't see. A pool's text is whole, its reading exact. A stat the game draws itself, an offer's armour, is read through KitStat, gated alike.
  • AbilityBar for one of the side's own actors: every ability the content grants it, each with its key (1 to 9), its price and cooldown, and whether it can be used now, asked as the side sees the world (CheckAs, PriceAs). A key or a click uses one; one that takes a target is used at the next click on an actor, or at the first in reach when its key is pressed again. Its AbilityTooltip previews what it would do (ForecastAs). The bar gives its orders through LedgerKit.Give, here the game's Use, so the game's own rules (whose turn it is) hold for them.
  • What the bar decides, a game may set on its Reading (a KitAbilities); each left unset, the bar does as above:
    • its rows: Abilities (only these, in this order), Source (only the rows from one card or disc, by number) and Only (the game's own test of a row). A row carries its Source, the card or item its grant comes from (for an own kit, the actor), by number where the side sees it, 0 where it doesn't;
    • its keys: Keys = row => "Q", a key's name or null for none;
    • its aim: Aim = row => KitAim.At(ally) or KitAim.Place(x, y) gives a row its target from the game's own aim (a crosshair, an aimed ally, a drag) in place of a click; usable now is then asked there, and a press uses it at once;
    • what an order takes: Fill = (row, property) => power fills each property the ability takes (the catalog lists them: Catalog.Takes(ability, list)) before the order is checked, priced, forecast and given. A row whose takes the game leaves unfilled says "takes Power, not given", and gives nothing.
  • The order whole: LedgerKit.Hand = (order, row) => ..., asked before Give, hands the game each order the bar gives with its row (Ability, Grant, Source). The bar makes each order new, so the game may keep it, stamp it and send it, or hold it to its own rules and give it later.
  • A bar without its buttons: Buttons = false draws nothing, and the game presses a row where it draws it (bar.Press(i)), a shop's slot or a pad's button; the rows, their keys and their give are the bar's still.
  • Cues: a tag to a scene, bound in C# (Bind("Attack", scene, kind: "Activate", at: CueAt.Target)) or a CueMap resource. It plays for the journal's rows the side is told: an ability's tags are on its Activate (and down its chain only where declared carried), and a cue at an actor the side doesn't see doesn't play. A cue may also name the row's ability and a tag its actor carries (Bind(tag, scene, kind, at, ability, having); a Cue's Ability and Having), each a condition as the kind is: weapons sharing their tags are a cue each, and the hero's hurt is Damage having Hero. The having is read as the side sees the world when the kit reads the row, so an actor unseen or gone carries nothing. A cue not met leaves its tag's dotted ancestor to be tried. Over a rollback it keeps each cue it played by its frame, tag, actor and order, as far back as the session's ring reaches (Reach, set where the game rolls back by a ring of its own): one the frames played again find again isn't played again, one they don't is freed at once, and one they find first is played told its age, the frames since its own, so a scene that implements ICueScene.Cued(row, tag, at, age) starts from its own frame. A world that never rolls back plays as before.
  • Counted headless: the readings have no Godot type and are a library of their own, which a game's rules library or headless tool references (<ProjectReference Include="$(LedgerRoot)godot\Ledger.Godot.Kit\Ledger.Godot.Kit.csproj" />), so it reads the journal (KitJournal, its Cut where a rollback made one), binds cues (KitCueRules) and keeps them (KitCues: Begun, each with its Age, TakenBack and Kept) as Godot's Cues do. The game's Godot build takes it through the targets.

So an ability added in content, and granted, is on the bar, priced and used with no C#. A widget never asks the world plainly: what it shows is what the side may be told (design\24-previews.md, "Who may see what").

A recording across a reload

Either host keeps one recording from the start to the end of a run, across reloads of the content. Where a run watches its content (run from the editor, or with --watch, the host's own argument after --), WorldHost.Begin reloads an edited file into the running world between two ticks, and the reload opens a new segment of the recording before the game rebinds. Each segment keeps the files its catalog was loaded from (Catalog.Sources), the save it starts from, and its orders; Recording.Segments lists them, and SaveOf, ContentOf and FirstOf read one.

A replay loads each segment's catalog from the recording, not from the files on disk, so a recording made before an edit replays whatever the files hold since, and each reload in it is held byte for byte. A segment whose kept content doesn't load is refused (SegmentContent), naming the segment by its number, with the loader's words. A recording written before segments reads as one, keeping no content of its own: it replays against the catalog it is given only where that catalog is the content its save was made on, and is refused (SegmentContent) where not.

A quickload keeps the recording: read the file over S.RestoreSpace, and S.Replace opens the recording's next segment at the restored world's save, marked as a restore's (segment K restored, its content content same where it is the segment before's, and the text format 4, which a kernel reading only 2 and 3 refuses). The stretch the load abandoned stays in the segment before, as it was played, and the replay plays it, then restores where the game did and holds that save byte for byte; F8 writes the one file. A world restored over another space than S.RestoreSpace is refused by Replace before anything moves, since it would ask what the recording never keeps. A read refused, or checked and dropped, leaves the recording as it was: a restore asks its space nothing. S.StartRecording() after a quickload refuses, the recording being begun once; a session made to record whose recording the game hasn't begun yet begins it on the restored world when the game starts it.

A rollback host: snapshots and a rollback in place

A game played online over rollback plays ahead on a guess of the other side's input, and when the guess proves wrong puts its world back to the frame it guessed at and plays the frames since again. The session keeps that past for it, in place, allocating nothing a frame once its buffers are sized (design\23-save.md, "Rolling back"):

  • S.KeepSnapshots(capacity) makes the session's ring of snapshots over its world: as many as the rollbacks reach back, plus one. A world is kept by one ring at a time.
  • S.Take(), a frame's first call, between ticks: a snapshot of the world as it stands, with the recording's place beside it. Once the ring is full, the oldest goes.
  • S.RollBack(tick) puts the same world back as it stood at that frame's snapshot: its values, its seat, its streams, its journal cut back to that frame, and its state hash that frame's, so two peers compare one number. The movers named and not yet told are dropped, and the recording is cut back, so the frames played again are recorded once. That snapshot is spent: Take again before playing the frame again.
  • What stays the game's: its space, and what it keeps outside the seat. The kernel's plane gives a spare of its own: Plane.CopyTo(spare) with each take, Plane.CopyFrom(spare) with the rollback. Input buffers and the like are the game's to copy beside its snapshots.
  • A cursor on the journal read past the cut is moved back at its next read, to where the frames played again began, and reads them as journaled anew: drop what it read from its Position on. A read that begins before where the last stopped met a cut. The kit's reader says so itself (KitJournal.Cut), and its Cues keep, take back and start late the cues of the frames played again (above, "The presentation kit").
  • Refused: a take or a rollback inside a preview (from the space's PreviewBegins too), while a moment resolves or an order is given, or of a tick the ring doesn't hold; a take of a stopped world, whose rollback clears the stop; and the session's rollback to a snapshot taken before its recording began. A quickload or a reload ends the session's snapshots, since they were of the world it replaced, though the recording goes on; make them again with KeepSnapshots, and a rollback cuts the recording back no further than the load's segment.

A host over a world of its own, with no session, makes the ring itself (new Snapshots(world, capacity)), and takes and rolls back on it the same way, its Take(mark) keeping a mark of its own that RollBack gives back.

A predicting client: a side's view, written

A game played under server authority keeps the one world on the server and gives each client a world of its own, which predicts the client's own orders at once and is corrected from what the server sends. The kernel reads what a side may be told, encodes it, and writes a client's world to hold it (design\24-previews.md, "A side's view"); the sight, the places, the wire and the loop stay the game's. The netshooter's game\Sim\Net.cs and View.cs are a whole one.

  • Both worlds are made from the catalog with WorldOptions { Views = true } and set up alike, so the actors every world makes at its setup (the roster, the sides, the match) are made in the same order. A world made so keeps who made each actor an order or an activation made (its tie) and a written pool's told maximum; its save carries a made line under each made actor, and a recording of it replays with the same option (read.Replay(catalog, new WorldOptions { Views = true })), or the ties aren't made again.
  • On the server, a side's view a side: SideView(World world, ActorId side). Before the first read, void Name(World world, ActorId actor, int label) labels each setup actor, the same label on every client, and void Withhold(string name) names a pool or an effect told of the side's own actors alone (an enemy's magazine, a cooldown); a label held, an actor labelled, or a name the catalog lacks is refused.
  • Each tick, between moments: View Read(World world, Func<ActorId, bool> sees, PlaceOf? places = null). sees is the game's sight, asked once of each actor alive, the public actors among those it names; places is delegate bool PlaceOf(long number, out Num x, out Num y), the space's TryPlace. The view (View: its long Tick and its List<ViewRow> Rows) holds, by the side's labels, never a number: each actor seen, each pool's amount and maximum, each hold, each link, what runs, each place, the side's own copy of each side's part, and the answers to its orders.
  • Its answers: after giving an order of the side's own, void Answer(long tag, in Outcome outcome); the next read tells the tag, the outcome's kind and refusal, the count of the activation it began, and the actor it made where the side sees it. What the order met (a ray's) the game names by int LabelOf(long number), the label where the last read saw it, else 0, and sends beside.
  • The bytes are the host package's (ViewBytes, in Ledger.Host, each on a View): int Encode(this View view, ref byte[] into) writes a view whole, int EncodeDiff(this View view, View? basis, ref byte[] into) only what differs from the view the client holds, and the client's void Decode(this View view, byte[] from, int length) reads either onto the view it holds, a diff from another tick than its own refused before anything moves.
  • The client's loop, each its own: keep snapshots (S.KeepSnapshots, S.Take() each tick, above); predict the client's own orders at once, each naming its id (Order Id(long count)) so a barrier or a needle it makes is tied to the server's; and on a view of tick s, S.RollBack(s), then void Write(World world, View view) on its own SideView of its own world (its side, the same names and withholds), then put each place the view tells (a Place row's label is long NumberOf(int label)'s actor; its A and B are the raw x and y) and take off a hero's it doesn't, then play its own orders since again. int Hold(World world, View view) counts the rows the world doesn't hold as it reads back: 0 once written, so a client held to its last view is one whose hold is 0.
  • What the write does: each actor the view names is found by its label, or its tie, or made; one it no longer names is destroyed, but those the game named (it can't tell gone from out of sight: the game takes the place off), and, before the links and again before the values, every actor no label holds: a prediction the view doesn't tie to, and what the write's own orders made (a hold's start rule makes its own, and the view names the server's); then links, holds and values, each pool's maximum held as told; then the side's seat. An activation is never written: what runs on another's actor is told to be drawn. A server quickloaded or rolled back to an earlier point gives numbers out again, so it makes its sides' views anew, and each client its own.
  • Refused: a read or a write while a moment resolves, and a write to a world made without Views.

From ledger-v3's guide/ledger-author/host.md, last changed 2026-11-06.