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.
Session: Returns a recorder type that stores steps with their snapshots ofinput_bytesbytes, in storage the caller provides.inputAt: Returns the input for stepindexfromspans: the last span whosetickis at or beforeindex, with itstickset toindex.
Source
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
| Definitions | 3 |
|---|---|
| Public names | 3 |
| Members | 0 |
| Version | 26.7.0 |
| Revision | daab053ee433 |