Replay run start¶
The native game has one shared reset/startup path and a quest-specific second prelude. Replays record the exact state needed at our chosen run boundary instead of trying to reconstruct earlier menu history.
The session's most recent crt_srand argument is not sufficient. Menus and
earlier startup work may already have consumed draws before gameplay begins, so
the run seed is the CRT RNG state entering gameplay_reset_state().
Native startup shape¶
Entering gameplay calls gameplay_reset_state() before branching by mode. That
shared path resets gameplay structures, consumes RNG, and calls
terrain_generate_random().
Survival and rush continue from that generic terrain. Quest mode then calls
quest_start_selected(), which performs additional resets and RNG work,
generates quest terrain, equips the start weapon, and builds the quest spawn
script.
The architecture is therefore:
flowchart TD
A["gameplay_reset_state()"] --> B["Shared reset and RNG work"]
B --> C["terrain_generate_random()"]
C --> D{"mode"}
D -->|"survival / rush"| E["Run continues"]
D -->|"quest"| F["quest_start_selected()"]
F --> G["Quest reset, RNG, terrain, and spawn setup"]
G --> E
Relevant native evidence is address-keyed:
gameplay_reset_stateat0x00412dc0terrain_generate_randomat0x004181b0quest_start_selectedat0x0043a790game_state_setat0x004461c0
The run boundary¶
Replaying the whole process from a stale session seed would require recording and reproducing unrelated menu and startup draws. It would also hide the real question when a run diverges: whether the same run-setup state produces the same gameplay behavior.
Every mode's run starts in game_state_set with gameplay_reset_state(), so
the seed is the RNG state entering it. Run setup then replays native exactly:
- the reset's draws: the score tag, one
anim_phaseper creature slot, the score tag again terrain_generate_random()- the mode's own setup (Quests:
quest_start_selected()) - the frame's end:
game_frame_updateends every frame with a discardedcrt_rand(), and run setup happens inside a frame
Each tick is one native gameplay frame: the gameplay update, then that same end-of-frame draw. Native frames outside gameplay (pause, the perk menu and its transitions) draw it too, but how many there are depends on wall-clock time, so the port fixes them at zero; see settings that steer the RNG.
Native creature slots can still contain residue once the reset has run; port replays carry none and start from a fresh pool.
Native effect_spawn_detail_skip_counter is residue of the same kind: at detail
presets 1 and 2 effect_spawn drops every other effect, and the counter lives
for the whole process (effect_defaults_reset leaves it alone), so a run's first
low-detail effect is kept or dropped depending on every earlier run in the
session. The port's counter starts at zero with each world. Carrying it across
runs would make a replay's effects, and the decals they leave on the ground,
depend on the session it plays in, so replays would have to record it. Only
visuals differ: effect_spawn returns nothing, so its callers draw the same RNG
either way.
Settings during a run¶
Simulation detail and violence settings stay fixed at the values in the
starting RunSpec. Changing effect density in Options applies
to the next game: changing these values mid-run would change RNG consumption
without a corresponding replay operation. Visual-only flags, audio volume,
and input preferences can still apply live.
Current replay contract¶
The replay layout, and which older formats readers still accept, is specified
in Replays; traces support only their
current formats. A replay's RunSpec holds the run seed, mode, player count, the run-relevant
status fields, quest and presentation settings, and the Typ-o state a run
inherits from earlier runs of the process (typo_carry); port runs always start from
a fresh creature pool. Checkpoint sidecars (<replay>.crd.chk, written with
--replay-checkpoints) use the replay's single-frame zstd envelope with their
own format version and size caps (src/crimson/replay/checkpoints.py). Each
checkpoint pins the RNG state after its tick, which a reorder of draws within the
tick leaves unchanged, so it also carries rng_callers_crc32: a CRC32 of the
tick's RNG call-site tags in draw order. A reorder fails at its tick.
Every tick runs at the fixed float32 1/60 s delta and carries one f32-quantized packed input row per player plus an ordered command list; where in the tick each command applies is in the replay format. Live play and playback share one command handler.
There is no independent replay-input stream or inferred movement input.
replay_step is the single authority for what drove the tick.
Startup and tick evidence¶
The channels intentionally separate cause from effect:
replay_steprecords the time step, input intent, ordered prelude and postlude, and commandscheckpointprovides a compact deterministic state hash/input to fast localizationsim_staterecords player movement state includingheading,move_speed,move_phase,aim, andaim_headingentity_samplesrecords stable-UID pool staterng_streamrecords draw values and state transitionstiming_samplesties the native update boundary toreplay_step.dt
When movement diverges, compare replay_step first. Matching inputs with a
different sim_state point at integration or state-reset behavior; different
inputs point at replay-driving data.
For same-build port-to-port regression tests, tests/support/state_digest.py
provides session_state_bytes and session_digest. These include complete
player, mode, pool, allocator, RNG, and terrain-queue state, including inactive
slot residue. Compact checkpoints remain useful for native comparisons and
readable diagnostics, but do not prove that all deterministic state agrees.
The inspection encoding excludes file paths, dirty flags, RNG trace sinks,
and profiling samples. It is neither a persisted replay format nor a
recoverable session snapshot.
Latest-only policy¶
- Trace readers require the current version matrix; replay readers accept only the formats the replay format lists.
- Unknown fields and incomplete lifecycle rows are rejected.
- Older throwaway artifacts are regenerated, not migrated.
This keeps startup semantics in one current implementation and prevents compatibility code from masking a parity difference.
Shared port startup¶
sim.run_spec.RunSpec describes the pre-start inputs; a replay adds only
recording metadata and its result. sim.run_init.initialize_run constructs the world and mode
session for all five gameplay modes and playback. It consumes generic terrain,
then the quest score tag, quest terrain and spawn draws when applicable, before
assigning starting weapons. The returned terrain setup is consumed separately by
the renderer.
Live play binds the actual save object; replay binds a detached copy of the same pre-start snapshot. Starting weapon usage and quest play counters are applied once on both paths. Snapshotting after weapon assignment would count it twice on replay. Port runs always allocate fresh pools. The reset types live in the simulation layer without importing the replay codec.
tests/replay/test_live_run_start.py compares complete session state at startup
and after input ticks through actual mode open/start and recorder/playback paths,
including multiplayer-sized local runs, preserved quirks, and non-default visual
settings. Typ-o commands retain their inside-tick phase, before the frame's simulation;
perk picks run before timing is derived.
Terrain RNG and rendering¶
src/crimson/sim/terrain_generate.py owns terrain RNG. terrain_generate(rng, slots) and
terrain_generate_random(rng, unlock_index) mirror the native functions: both draw every stamp from the
supplied RNG before returning a TerrainSetup of texture slots and stamp layers. The random generator
draws three overwritten texture selectors, then the unlock rolls (>= 40, >= 30, >= 20, each drawn only
when its threshold passes). A successful roll delegates to terrain_generate with quest 4.2's, 3.2's or
2.2's slots, (6, 7, 6), (4, 5, 4) or (2, 3, 2), so its stamp draws carry the explicit generator's
callers. Only the fallthrough stamps with the random generator's own callers and slots (0, 1, 0).
The renderer draws the setup's stamps and never regenerates them, so drawing or re-applying a setup consumes
no RNG. PreparedRun.terrain is derived during initialization, not stored in the
CRD header or checkpoints. The checkpoints' call-order digests cover ticks, not initialization, so startup
ordering is pinned by the tests named below instead.
Every run's reset ends with terrain_generate_random. Quests then draw the score tag and generate the
quest terrain with terrain_generate: two complete generations, of which only the second is shown. The
first one's draws stay; only its stamps are discarded. The menu ground uses terrain_generate_random on
the application RNG, which stands in for native startup's generation outside the recorded run.
See tests/native_oracle/test_terrain_generate.py, tests/render/test_ground_stamp_cases.py and
tests/render/test_terrain_runtime_boundaries.py for the boundary tests.