#include "sim.h" #include #include 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); } // A fresh online game for some of a lobby's fighters (by index) to play by the rules, bots filling the rest when the // rules say to. Their members, names and colours come along, and it starts from the seed, so everyone builds the same // one. The bots keep their difficulty. void Sim::game_start(Match& lobby, const int* members, int count, const Rules& rules, Uint32 seed) { int owners[MAX_FIGHTERS]; int colors[MAX_FIGHTERS]; char names[MAX_FIGHTERS][PLAYER_NAME_MAX + 1]; for (int k = 0; k < count; k++) { owners[k] = lobby.owners[members[k]]; colors[k] = lobby.colors[members[k]]; std::memcpy(names[k], lobby.names[members[k]], sizeof(names[k])); } int players = rules.isBotFill ? std::max(rules.players, count) : count; int level = bot.level; bot = Bot{}; bot.level = level; weapons = Weapons{}; world = World{}; match = Match{}; match.rules = rules; match.rules.players = players; match.isOnline = true; rng.state = seed ? seed : RNG_SEED; fighterCount = players; bool isTaken[COLOR_COUNT] = {}; for (int k = 0; k < count; k++) isTaken[colors[k] % COLOR_COUNT] = true; for (int k = 0; k < players; k++) { fighters[k] = Fighter{}; fighters[k].init(*this, k); match.owners[k] = k < count ? owners[k] : -1; if (k < count) { match.colors[k] = colors[k]; std::memcpy(match.names[k], names[k], sizeof(match.names[k])); continue; } match.colors[k] = std::find(isTaken, isTaken + COLOR_COUNT, false) - isTaken; isTaken[match.colors[k]] = true; } match.start(*this); } void Sim::event_apply(const RoomEvent& event) { if (event.type == ROOM_JOIN) match.member_join(*this, event.member, event.profile); if (event.type == ROOM_LEAVE) match.member_leave(*this, event.member); int index = match.fighter_find(*this, event.member); if (index < 0) return; if (event.type == ROOM_QUEUE) match.queue_open(*this, index, event.rules); if (event.type == ROOM_UNQUEUE) { match.queues[index].isActive = false; match.isQueueReady[index] = false; } } // 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; }