package sim // Sim_State is the authoritative gameplay state. If a value affects replay, // combat, collision, death, loot, or echo behavior, it belongs here or below here. Sim_State :: struct { tick: u64, rng: Rng_State, initial: Initial_State_Descriptor, world: World_State, } // Initialization must be explicit and seed-driven so tests and replays can create // identical worlds without opening a window or touching platform APIs. sim_init :: proc( state: ^Sim_State, seed := DEFAULT_WORLD_SEED, initial := DEFAULT_INITIAL_STATE, ) { state^ = Sim_State{ rng = rng_from_seed(seed), initial = initial, world = World_State{ seed = seed, arena = DEFAULT_ARENA, }, } world_spawn_player(&state.world, initial.player_position) } // sim_tick is the only place gameplay time advances. It intentionally takes an // already-canonical command so hardware, frame time, and replay never enter sim. sim_tick :: proc(state: ^Sim_State, command: Input_Command) { sim_tick_with_archive(state, command, nil) } sim_tick_with_archive :: proc( state: ^Sim_State, live_command: Input_Command, archive: ^Run_Archive, ) { player := world_player(&state.world) if player != nil { apply_actor_command(player, live_command) } for index in 0..= command_count { actor.echo.mode = .Timeline_Ended continue } echo_command, found := run_archive_command_at(archive, actor.echo.source_run_id, actor.echo.replay_tick) if !found { actor.echo.mode = .Timeline_Ended continue } apply_actor_command(actor, echo_command) actor.echo.replay_tick += 1 if actor.echo.replay_tick >= command_count { actor.echo.mode = .Timeline_Ended } } state.tick += 1 } apply_actor_command :: proc(actor: ^Actor_State, command: Input_Command) { delta := move_delta(command.move) actor.position.x += delta.x actor.position.y += delta.y } sim_digest :: proc(state: ^Sim_State) -> u64 { hash := u64(14695981039346656037) hash = digest_mix(hash, u64(SIM_VERSION)) hash = digest_mix(hash, state.tick) hash = digest_mix(hash, state.rng.value) hash = digest_mix(hash, state.world.seed) hash = digest_mix(hash, u64(state.world.actor_count)) hash = digest_mix_fixed(hash, state.world.arena.min.x) hash = digest_mix_fixed(hash, state.world.arena.min.y) hash = digest_mix_fixed(hash, state.world.arena.max.x) hash = digest_mix_fixed(hash, state.world.arena.max.y) hash = digest_mix_fixed(hash, state.initial.player_position.x) hash = digest_mix_fixed(hash, state.initial.player_position.y) hash = digest_mix(hash, u64(state.world.player_id.index)) hash = digest_mix(hash, u64(state.world.player_id.generation)) for index in 0.. u64 { return (hash ~ value) * 1099511628211 } digest_mix_fixed :: proc(hash: u64, value: Fixed) -> u64 { bits := u32(i32(value)) return digest_mix(hash, u64(bits)) }