Skip to documentation
SLOP

tiny.game.session

Reference tiny.game session

Defined in tiny.game.

Recording and replay of the input each step of a run received, and a reader that gives the input for any step from a short list of input changes.

API (2)

Actions

Public operations.

No direct callersNo direct callstiny.gamesession
Static calls · unresolved targets: unknown · external targets: unknown.

Source

Called byCallsprivate; no linkfun.game.src.sessionfoldtest; no linkfun.game.src.sessiontest: recorded 60 Hz input replays wi...inputSnapshotsessionSession
Static calls · unresolved targets: 1 · external targets: 1.

Source: fun/game/src/root.zig:116

zig
pub const session = @import("session.zig");

Source: fun/game/src/session.zig

zig
//! Recording and replay of the input each step of a run received, and a reader that gives the input//! for any step from a short list of input changes. A developer wants to record the input each step//! of a run received and replay it later, so the same run happens again step for step. A scripted//! test wants to state its input only at the steps at which the input changes, and to read the//! input for any step from that list.//!//! A replay repeats a run only when every step receives the exact input it received live, in the//! same order. The memory for a recording is fixed ahead of the run, so a long run can fill it. A//! value held from one change to the next would repeat a one-time event, such as a reset, on every//! step until the next change.//!//! A recorder (`Session`) stores each step beside the step's input as a fixed-size array of bytes//! (a *snapshot*), in storage the caller provides. The recorder calls no allocator, and its//! capacity is the length of the caller's storage. `append` returns `error.SessionCapacityExceeded`//! once the storage is full. Recording starts at step 0 and takes the steps in order, so `append`//! returns `error.NonSequentialStep` for a step out of order. `Session.replay` calls the caller's//! update with each recorded step and snapshot, in order, hands it only the context the caller//! passes and those two values, and reads no clock of its own. A script lists its input as entries//! that each hold from the step in their `tick` field until the next entry (*spans*), sorted by//! step, and `inputAt` gives the input for any one step from that list. The fields a caller lists//! in `pulse_fields`, such as a reset, keep their value only at the step their span starts, and//! read false at every later step of that span.const std = @import("std");const clock = @import("clock.zig");const input = @import("input.zig");/// Returns the input for step `index` from `spans`: the last span whose `tick` is at or before/// `index`, with its `tick` set to `index`. A scripted run calls it once per step to turn a short/// list of input changes into the input for every step. `spans` is sorted by `tick`, ascending,/// because the scan stops at the first span past `index`. Each span holds its values until the next/// span starts. Before the first span, every field has its default value. Each field named in/// `pulse_fields` keeps its span's value only at the span's own `tick`, and reads false at every/// later step of that span. `Input` gives every field a default value, has a `tick` field that/// compares with a `u32`, and holds a boolean for each name in `pulse_fields`. The call scans/// `spans` from the start on every call.pub fn inputAt(comptime Input: type, spans: []const Input, index: u32, comptime pulse_fields: []const []const u8) Input {    var active = Input{};    for (spans) |span| {        if (span.tick > index) break;        active = span;    }    inline for (pulse_fields) |name| {        @field(active, name) = @field(active, name) and active.tick == index;    }    active.tick = index;    return active;}/// Returns a recorder type that stores steps with their snapshots of `input_bytes` bytes, in/// storage the caller provides. A test or a host records a run with it and replays the run later,/// step for step. The recorder calls no allocator, and its capacity is the length of the slice/// passed to `init`.pub fn Session(comptime input_bytes: usize) type {    return struct {        const Self = @This();        /// The snapshot type of this recorder, `input.Snapshot(input_bytes)`. A caller builds each        /// step's input as this type before it appends the step.        pub const Input = input.Snapshot(input_bytes);        /// One recorded step: the `clock.Step` and its snapshot. A caller declares the storage as        /// an array of these and reads them back from `records`.        pub const Record = struct { step: clock.Step, input: Input };        storage: []Record,        count: usize = 0,        /// Returns an empty recorder that writes into `storage`. A caller passes storage sized for        /// the longest run it records. The recorder borrows `storage`, so the slice has to outlive        /// it.        pub fn init(storage: []Record) Self {            return .{ .storage = storage };        }        /// Stores `step` and `snapshot` after the last recorded step. A host calls it once per        /// step, after the clock hands out the step and the game encodes that step's input. The        /// call returns `error.SessionCapacityExceeded` when the storage is full. The call returns        /// `error.NonSequentialStep` when `step.index` differs from the number of steps already        /// recorded, so recording starts at step 0. A full recorder returns the capacity error even        /// for a step out of order.        pub fn append(self: *Self, step: clock.Step, snapshot: Input) !void {            if (self.count == self.storage.len) return error.SessionCapacityExceeded;            if (step.index != @as(u64, @intCast(self.count))) return error.NonSequentialStep;            self.storage[self.count] = .{ .step = step, .input = snapshot };            self.count += 1;        }        /// Returns the recorded steps, in order. A caller reads the stored run back, for example        /// the `phase` of its last step. The slice points into the caller's storage and covers the        /// steps recorded when the call returns.        pub fn records(self: *const Self) []const Record {            return self.storage[0..self.count];        }        /// Calls `update(context, step, snapshot)` for each recorded step, in order. A test replays        /// a recording into the update it ran live and checks that both runs end in the same state.        /// The replay hands `update` only `context` and the recorded step and snapshot, and it        /// reads no clock of its own. `update` is known at compile time.        pub fn replay(self: *const Self, context: anytype, comptime update: anytype) void {            for (self.records()) |record| update(context, record.step, record.input);        }    };}test "recorded 60 Hz input replays with identical state hash" {    const Log = Session(2);    var storage: [12]Log.Record = undefined;    var log = Log.init(&storage);    var timer = try clock.Clock.init(.{ .steps_per_second = 60, .source_frames_per_second = 20 });    var live_hash: u64 = 14695981039346656037;    for (0..12) |i| {        const step = timer.commandStep();        const snapshot = Log.Input{ .bytes = .{ @intCast(i), @intCast(i * 3) } };        try log.append(step, snapshot);        fold(&live_hash, step, snapshot);    }    var replay_hash: u64 = 14695981039346656037;    log.replay(&replay_hash, fold);    try std.testing.expectEqual(@as(u64, 0x786dd5e860a2d561), live_hash);    try std.testing.expectEqual(live_hash, replay_hash);    try std.testing.expectEqual(@as(u32, 2), log.records()[11].step.phase);}fn fold(hash: *u64, step: clock.Step, snapshot: Session(2).Input) void {    const bytes = [_]u8{ @intCast(step.phase), snapshot.bytes[0], snapshot.bytes[1] };    for (bytes) |byte| {        hash.* = (hash.* ^ byte) *% 1099511628211;    }}

Audit

Definitions3
Public names3
Members0
Version26.7.0
Revisiondaab053ee433