Split the simulation from presentation into an SDL-free library

Sim holds one whole game (tick, snapshots, checksum) and talks to the screen only
through Cues, which the client's Scene plays and the server ignores. All drawing
moved to scene.cpp. Match takes this screen's wishes as request flags instead of
reading the menu, input and settings. The sim library builds without SDL and the
server links it, ready to run games itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shweetandClaude Opus 5.5 committed 2026-11-08 01:46:07 -05:00
1 parent 2007fb7edc
commit 465f75f6d7
38 files changed
+2225 -1965

No files matched your search

+103
View File
@@ -0,0 +1,103 @@
#include "sim.h"
#include <algorithm>
#include <cstring>
constexpr Uint32 FNV_OFFSET = 2166136261u;
constexpr Uint32 FNV_PRIME = 16777619u;
// Sets up a game from scratch: the sandbox world, the bot graph, fighters and the seeded generator.
void Sim::init(Uint32 seed)
{
world.sandbox_load();
bot.init(world);
for (int i = 0; i < MAX_FIGHTERS; i++)
fighters[i].init(*this, i);
rng.state = seed;
}
void Sim::tick()
{
match.update(*this);
if (match.state != MATCH_COUNTDOWN)
bot.update(*this);
for (int i = 0; i < fighterCount && match.state != MATCH_COUNTDOWN; i++)
fighters[i].update(*this);
weapons.update(*this);
world.update(*this);
}
// Moves a level by a step within the levels there are, cueing whether it went up or down.
void level_step(Sim& sim, int& level, int step)
{
int next = std::clamp(level + step, 0, LEVEL_COUNT - 1);
if (next == level)
return;
sim.cues->level_step(next > level);
level = next;
}
Snapshot Sim::snapshot_save()
{
Snapshot snapshot = {.match = match,
.weapons = weapons,
.world = world,
.rng = rng,
.fighterCount = fighterCount};
std::copy(std::begin(fighters), std::end(fighters), snapshot.fighters);
std::copy(std::begin(bot.states), std::end(bot.states), snapshot.bots);
return snapshot;
}
void Sim::snapshot_load(Snapshot& snapshot)
{
std::copy(std::begin(snapshot.fighters), std::end(snapshot.fighters), fighters);
std::copy(std::begin(snapshot.bots), std::end(snapshot.bots), bot.states);
fighterCount = snapshot.fighterCount;
weapons = snapshot.weapons;
world = snapshot.world;
rng = snapshot.rng;
match = snapshot.match;
}
static void hash_add(Uint32& hash, float value)
{
Uint32 bits;
std::memcpy(&bits, &value, sizeof(bits));
hash = (hash ^ bits) * FNV_PRIME;
}
// A fingerprint of the simulation state that matters for a desync: the fighters, every attack, the dummies, the
// match and the random generator.
Uint32 Sim::checksum_get()
{
Uint32 hash = FNV_OFFSET;
hash_add(hash, (float)match.state + match.timer);
for (int i = 0; i < fighterCount; i++)
{
Fighter& fighter = fighters[i];
for (float value : {fighter.body.box.x, fighter.body.box.y, fighter.body.velocity.x, fighter.body.velocity.y,
fighter.aim.x, fighter.aim.y, fighter.charge, fighter.health, fighter.hitstop})
hash_add(hash, value);
for (float cooldown : fighter.cooldowns)
hash_add(hash, cooldown);
// What the menu's edits change: loadouts, levels, slot counts, stacking and PvP.
for (int slot = 0; slot < SLOT_MAX; slot++)
for (int value : {fighter.weaponLoadout[slot], fighter.trinketLoadout[slot], fighter.weaponSlotLevels[slot],
fighter.trinketSlotLevels[slot]})
hash_add(hash, (float)value);
for (int level : fighter.weaponItemLevels)
hash_add(hash, (float)level);
for (int level : fighter.trinketItemLevels)
hash_add(hash, (float)level);
for (int value : {fighter.weaponSlots, fighter.trinketSlots, (int)fighter.isPvp})
hash_add(hash, (float)value);
}
for (Attack& attack : weapons.attacks)
for (float value : {attack.position.x, attack.position.y, attack.velocity.x, attack.velocity.y, attack.angle,
attack.time, (float)attack.hitsLeft})
hash_add(hash, value);
for (Dummy& dummy : world.dummies)
for (float value : {dummy.flash, dummy.wobble, dummy.wobbleSpeed})
hash_add(hash, value);
return (hash ^ rng.state) * FNV_PRIME;
}