Files
SEQUA-Engine/core/src/sim/replay.cpp
T
Ange-Marie MAURIN - A2M Systèmes ab219ad611 feat(core): simulation déterministe, enregistrement et rejeu
Virgule fixe Q16.16, générateur PCG32 à graine explicite, horloge à pas fixe exacte, simulation d'un micro-mode de démonstration (marqué SIMULÉ), empreinte FNV-1a de l'état, enregistrement binaire versionné des entrées, rejeu avec d'autres réglages de ressenti et détection du premier tick divergent. Empreinte de référence multiplateforme figée. 22 tests (Debug, Release, ASan).
2026-11-06 19:50:56 +01:00

148 lines
4.3 KiB
C++

// SPDX-License-Identifier: MPL-2.0
#include "sequa/core/sim/replay.hpp"
#include "sim/bytes.hpp"
#include <algorithm>
namespace sequa::core::sim {
namespace {
constexpr std::uint32_t recording_magic = 0x43525153u; // "SQRC" en petit-boutiste
constexpr std::uint32_t recording_version = 1;
constexpr std::size_t input_frame_size = 8 + 4 + 2 + 2;
} // namespace
bool Recording::is_valid() const noexcept
{
if (tick_rate_hz == 0) {
return false;
}
for (std::size_t i = 0; i < inputs.size(); ++i) {
const InputFrame& frame = inputs[i];
if (frame.tick >= tick_count || frame.is_neutral()) {
return false;
}
if (i > 0 && frame.tick <= inputs[i - 1].tick) {
return false;
}
}
return true;
}
std::vector<std::byte> serialize(const Recording& recording)
{
detail::ByteWriter w;
w.u32(recording_magic);
w.u32(recording_version);
w.u64(recording.seed);
w.u32(recording.tick_rate_hz);
w.u64(recording.tick_count);
w.u64(recording.inputs.size());
for (const InputFrame& frame : recording.inputs) {
w.u64(frame.tick);
w.u32(frame.buttons);
w.i16(frame.move_x);
w.i16(frame.move_y);
}
return w.take();
}
std::optional<Recording> deserialize_recording(std::span<const std::byte> bytes)
{
detail::ByteReader r(bytes);
if (r.u32() != recording_magic || r.u32() != recording_version) {
return std::nullopt;
}
const auto seed = r.u64();
const auto tick_rate = r.u32();
const auto tick_count = r.u64();
const auto input_count = r.u64();
if (!seed || !tick_rate || !tick_count || !input_count) {
return std::nullopt;
}
// La taille annoncée doit correspondre exactement aux octets restants : protège
// contre une allocation démesurée sur un fichier corrompu.
if (*input_count > r.remaining() / input_frame_size || r.remaining() != *input_count * input_frame_size) {
return std::nullopt;
}
Recording recording;
recording.seed = *seed;
recording.tick_rate_hz = *tick_rate;
recording.tick_count = *tick_count;
recording.inputs.reserve(static_cast<std::size_t>(*input_count));
for (std::uint64_t i = 0; i < *input_count; ++i) {
const auto tick = r.u64();
const auto buttons = r.u32();
const auto move_x = r.i16();
const auto move_y = r.i16();
if (!tick || !buttons || !move_x || !move_y) {
return std::nullopt;
}
recording.inputs.push_back(InputFrame{*tick, *buttons, *move_x, *move_y});
}
if (!recording.is_valid()) {
return std::nullopt;
}
return recording;
}
SessionRecorder::SessionRecorder(const SimConfig& config) : simulation_(config)
{
recording_.seed = config.seed;
recording_.tick_rate_hz = config.tick_rate_hz;
}
void SessionRecorder::step(InputFrame input)
{
input.tick = simulation_.tick();
if (!input.is_neutral()) {
recording_.inputs.push_back(input);
}
simulation_.step(input);
recording_.tick_count = simulation_.tick();
}
ReplayResult replay(const Recording& recording, const FeelParams& feel, const ArenaParams& arena)
{
SimConfig config;
config.seed = recording.seed;
config.tick_rate_hz = recording.tick_rate_hz;
config.feel = feel;
config.arena = arena;
Simulation simulation(config);
ReplayResult result;
result.hashes.reserve(static_cast<std::size_t>(recording.tick_count));
auto next_input = recording.inputs.begin();
for (Tick tick = 0; tick < recording.tick_count; ++tick) {
InputFrame input = neutral_input(tick);
if (next_input != recording.inputs.end() && next_input->tick == tick) {
input = *next_input++;
}
simulation.step(input);
result.hashes.push_back(simulation.state_hash());
}
result.final_world = simulation.world();
result.final_hash = simulation.state_hash();
return result;
}
std::optional<Tick> first_divergence(const ReplayResult& a, const ReplayResult& b) noexcept
{
const auto [ita, itb] = std::mismatch(a.hashes.begin(), a.hashes.end(), b.hashes.begin(), b.hashes.end());
if (ita == a.hashes.end() && itb == b.hashes.end()) {
return std::nullopt;
}
return static_cast<Tick>(ita - a.hashes.begin());
}
} // namespace sequa::core::sim