Lobbies hold 16 players and the sandbox 16 fighters, games up to 8. The arena gets spawns 5-8 (outer ledges and middle platforms) and the race start eight places. The sandbox adds eight more spawns, each the open spot farthest from the rest. Rival cards wrap into rows of four, the lobby's player list goes to two tiny columns when crowded, and the server keeps frame packets to one datagram. Formatted with clang-format. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
165 lines
5.8 KiB
C++
165 lines
5.8 KiB
C++
#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);
|
|
}
|
|
|
|
// 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;
|
|
}
|