Rewrite the online session to play the server's rooms

Session now runs only the room this screen is in. It plays ahead of the server
with its own controls, guesses everyone else's, confirms ticks against the
server's frames and rewinds on a wrong guess. It keeps its lead by playing a
little faster or slower as the server says how early its controls arrive,
resends an edit the server dropped, and checks the server's checksums.

Gone with the peer-to-peer design: the host's decisions, the second arena game,
the view swapping and spectating (to come back through the server).
SAB_NET_STATS logs how it's keeping up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
shweetandClaude Opus 5.5 committed 2026-11-08 02:18:53 -05:00
1 parent dadfff189d
commit 3b65cb575f
4 files changed
+406 -1296

No files matched your search

+56 -157
View File
@@ -1,124 +1,58 @@
#pragma once
#include "bot.h"
#include "input.h"
#include "match.h"
#include "rollback.h"
#include "trinket.h"
#include "weapon.h"
#include "sim.h"
#include <map>
#include <set>
#include <string>
#include <vector>
struct Game;
constexpr int INPUT_RING = 1024;
constexpr int SNAPSHOT_RING = 64;
// A member's connection has no ping to show (it's this screen's own, hosting).
constexpr int SESSION_RING = 64;
// A member's connection has no ping to show.
constexpr float PING_NONE = -2;
enum SessionEventType
{
EVENT_JOIN,
EVENT_LEAVE,
// A member queues a game in the lobby's sandbox, with its rules, or stops queueing one.
EVENT_QUEUE,
EVENT_UNQUEUE
};
// A change to who's playing, at a set tick, so every machine makes it at the same point in the game (and again when
// rolling back over it). From is, for a join, the tick the newcomer's copy of the game starts at.
struct SessionEvent
{
int tick = 0;
int type = EVENT_JOIN;
int member = -1;
int from = 0;
Profile profile;
Rules rules;
};
// The queued game being played, a second game beside the lobby's sandbox (see Session): its state between ticks
// (while it isn't swapped into the game), its bots' map of it, which fighter is this screen's in it and the devices
// that go with that, and the tick it started (each play of it, a fresh one).
struct Arena
// One tick of the room as this screen played it: the state as it started (and its checksum), and once the tick's
// events were in, who played each fighter and the controls each was given.
struct SessionTick
{
Snapshot state;
Bot bot;
int devices[MAX_FIGHTERS] = {};
int local = 0;
int serial = -1;
int graphSerial = -1;
bool isActive = false;
Controls controls[MAX_FIGHTERS];
int owners[MAX_FIGHTERS];
Uint32 checksum = 0;
int tick = -1;
};
// One member's controls by tick, in a ring that keeps the last INPUT_RING ticks. They're needed from start (when the
// member's fighter appears) until until (when it goes). Contiguous is the last tick up to which every tick from start
// on is known, so up to which the member's play is certain. Used is what a tick was
// simulated with when the member's controls had to be guessed, to spot a wrong guess when the real ones come.
struct InputLog
// The server's frame for a tick: every fighter's controls as it played them, after the events before them.
struct SessionFrame
{
Controls controls[INPUT_RING];
int ticks[INPUT_RING];
Controls used[INPUT_RING];
int usedTicks[INPUT_RING];
int start = 0;
int until = INT32_MAX;
int contiguous = -1;
int latest = -1;
InputLog();
void restart(int from);
bool has(int tick) const;
bool put(int tick, const Controls& controls);
Controls guess(int tick) const;
Controls controls[MAX_FIGHTERS];
int count = 0;
int events = 0;
};
// What this screen knows of another member: the last tick of this screen's inputs they've confirmed having, the first
// one they need, whether to send them inputs at all yet, and the tick they last said they were on.
struct SessionPeer
{
int ack = -1;
int sendFrom = 0;
bool isSending = false;
int remoteTick = 0;
};
// Online play as one game on every machine: rollback netcode. Each member's controls go to everyone, a few ticks
// ahead of when they're used (the input delay); a member whose controls for a tick haven't arrived yet is guessed to
// keep doing what they last did, and when the real ones turn out different, the game rewinds to that tick and plays
// forward again (silently). A machine that gets too far ahead of what it knows, or of the others, waits.
//
// The lobby's host decides who comes and goes, and when: a newcomer gets the game's state as of a tick everyone's
// controls are certain for, fast-forwards from there, and appears at a tick a moment ahead, on every machine at once.
// Someone leaving is taken out at the tick after their last known controls. Checksums of certain ticks are compared,
// to catch machines drifting apart.
//
// Lobby members can queue a game in the sandbox (see Match::queue_open). Once it's ready, it starts as a second game,
// the arena, beside the sandbox: both step every tick on every machine, each with their own fighters, the players
// in the arena standing faded in the sandbox meanwhile. This screen shows one of the two (the arena while it plays in
// it, or watches it): that one is swapped into the game around this screen's own work, which otherwise always sees
// the sandbox.
// Online play: this screen runs the room it's in (the lobby's sandbox, or a game queued in it), which the server runs
// too (see server/server.cpp). It plays ahead of the server, its own controls sent for the ticks it plays and everyone
// else's guessed to keep doing what they last did. The server's frames say what everyone really did: when one differs
// from what was played, the game rewinds to that tick and plays forward again (silently). It keeps its lead over the
// server by playing a tick more, or one less, now and then, as the server says how early its controls come.
class Session
{
public:
bool isActive = false;
bool isJoined = false;
bool isDesynced = false;
// Set when what this screen shows changes between the sandbox and the arena, for the camera to jump.
// Set when the room changes, for the camera to jump.
bool isViewChanged = false;
int tick = 0;
int delay = 2;
int rollbacks = 0;
int stalls = 0;
void host_start(Game& game);
void join_start(Game& game);
void stop(Game& game);
void update(Game& game, Controls local);
void stop(Game& game);
bool is_wanted(Game& game);
Profile profile_get(Game& game);
bool is_watching();
bool is_playing_arena(Game& game);
bool is_queued(Game& game);
void arena_leave(Game& game);
int fighter_get(Game& game, int member);
std::string base_name_get(Game& game, int member);
std::string name_get(Game& game, int member);
@@ -126,83 +60,48 @@ public:
void typing_send(Game& game, bool isTyping);
void kick(Game& game, const std::string& name, const std::string& reason);
int rename_wait_get();
void queue_send(Game& game, const Rules& rules);
void unqueue_send(Game& game);
bool is_queued(Game& game);
bool view_enter(Game& game);
void view_leave(Game& game);
bool is_arena_on();
bool is_playing_arena(Game& game);
bool is_watching();
void watch_start(Game& game);
void watch_stop(Game& game);
void arena_leave(Game& game);
const Arena& arena_get();
std::string arena_names_get(Game& game);
void queue_send(Game& game, const Rules& rules, bool isOpen = true);
float ping_get(Game& game, int member);
private:
std::map<int, InputLog> logs;
std::map<int, SessionPeer> peers;
std::vector<SessionEvent> events;
std::vector<int> departed;
Snapshot snapshots[SNAPSHOT_RING];
Snapshot arenaSnapshots[SNAPSHOT_RING];
int arenaSerials[SNAPSHOT_RING];
Arena arena;
// Whether this screen watches the arena (not being in it), whether the arena's swapped into the game now, which
// one it showed last (to start afresh on a change), and the chat lines already said for things in the game (which
// replays would otherwise say again).
bool isWatching = false;
bool isSwapped = false;
int shownSerial = -1;
std::set<std::string> announced;
int snapshotTicks[SNAPSHOT_RING];
Uint32 checksums[SNAPSHOT_RING];
std::map<int, Uint32> remoteChecksums;
int checksumSent = -1;
int ownNext = 0;
int ownStart = 0;
int rollbackTo = -1;
bool isRequested = false;
SessionTick ticks[SESSION_RING];
Controls own[SESSION_RING];
std::map<int, SessionFrame> frames;
// Each room's events by number, kept from before its state comes in case they come first.
std::map<int, std::map<int, RoomEvent>> events;
std::map<int, Uint32> checksums;
std::map<int, Controls> held;
// Every member's chosen name (cleaned), kept current by their renames, and the colour this screen last asked for.
std::map<int, std::string> names;
int room = 0;
// The next tick to play, the first not yet confirmed, and the earliest that has to be played again.
int tick = 0;
int confirmed = 0;
int rewindTo = -1;
// How early (in ticks) this screen's controls last reached the server, and when its pace last changed.
int lead = 0;
int paced = 0;
int sentColor = -1;
// When a newcomer gives up waiting for the WebRTC link, and whether its fighter has been brought in.
Uint64 linkDeadline = 0;
bool isProfileSent = false;
// With SAB_NET_STATS set, how it's keeping up goes to the log every few seconds.
bool isStatsLogged = false;
// Names can change only every RENAME_WAIT, so a player can't dodge being kicked by renaming: when this screen's can
// next, whether it's told its pending one is waiting, and when each other member's last rename came.
// next, and whether it's told its pending one is waiting.
Uint64 nextRename = 0;
bool isRenameWaiting = false;
std::map<int, Uint64> renamedAt;
void start(Game& game);
void packets_take(Game& game);
void packet_handle(Game& game, int from, const std::string& bytes, bool isReliable);
void members_watch(Game& game);
void join_grant(Game& game, int member, const Profile& profile);
void leave_decide(Game& game, int member);
void event_add(Game& game, const SessionEvent& event);
void event_send(Game& game, int to, const SessionEvent& event, int inputsFrom, int inputsTo);
void inputs_send(Game& game);
void packet_handle(Game& game, const std::string& bytes);
void state_load(Game& game, PacketReader& reader, int serial, int at);
void frames_take(PacketReader& reader);
void confirm(Game& game);
void rewind(Game& game);
void step(Game& game, bool isLive);
void events_apply(Game& game, int at);
bool is_needed(int member, int at);
int confirmed_get(Game& game);
void checksums_exchange(Game& game);
void inputs_send(Game& game);
void local_find(Game& game);
void broadcast(Game& game, const std::string& bytes);
void rename_send(Game& game, int to, bool isQuiet);
void profile_watch(Game& game);
void controls_give(Game& game, bool isArena);
void arena_swap(Game& game);
void arena_start(Game& game, int queuer);
void arena_build(Game& game, const Rules& rules, const int* members, int count);
void arena_after(Game& game);
void arena_end(Game& game);
void arena_locals_find(Game& game);
void arena_save(Game& game, int slot);
void arena_restore(Game& game, int slot);
Uint32 checksum_full(Game& game);
std::string state_full_save(Game& game);
bool state_full_load(Game& game, const std::string& bytes);
void announce(Game& game, const std::string& key, const std::string& text);
void queue_decide(Game& game, int member, bool isOpen, const Rules& rules);
void text_send(Game& game, PacketType type, const std::string& text);
};