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).
148 lines
4.3 KiB
C++
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
|