Files
smack-a-brother/server/server.cpp
T
shweetandClaude Opus 5.5 dadfff189d Play over WebRTC to the server
The server answers each player's WebRTC offer (signaled over the socket) with a
connection of its own: frames go on a lossy channel, everything else on a
reliable one, and the socket carries them until both open or where UDP is
blocked. The client's Net now holds one link to the server instead of one per
player. WebRTC takes UDP 47810-47899 by default, set on the command line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 02:09:21 -05:00

764 lines
29 KiB
C++

// The server for Smack a Brother. Players connect over a WebSocket (behind nginx, which handles TLS), make a lobby or
// join one by its code, then open a WebRTC connection to the server through it (signaling over the socket), and play
// over that: the server runs every lobby's sandbox and the games queued in it, each
// a room, stepping them all at the game's tick rate on one thread. Players send their controls ahead of time; each
// tick takes whatever has come (repeating a player's last controls when theirs are late) and sends every player in
// the room the controls it played, which their game replays to confirm its own. See src/protocol.h for the messages.
//
// The WebRTC connection has two data channels: a reliable one for everything that mustn't go missing, and a lossy one
// for frames, which are sent again until confirmed. Until both are open (or if they never open, where UDP is blocked),
// the binary messages go over the WebSocket instead.
//
// smack-a-brother-server [port] [first UDP port] [last UDP port]
// (listens on 127.0.0.1, port 47800 by default, and takes WebRTC on UDP 47810 to 47899, which the firewall must let
// through)
#include "protocol.h"
#include "sim.h"
#include "state.h"
#include <rtc/rtc.hpp>
#include <simdjson.h>
#include <algorithm>
#include <chrono>
#include <csignal>
#include <cstdio>
#include <map>
#include <mutex>
#include <random>
#include <thread>
#include <vector>
constexpr uint16_t PORT_DEFAULT = 47800;
constexpr uint16_t UDP_FIRST_DEFAULT = 47810;
constexpr uint16_t UDP_LAST_DEFAULT = 47899;
constexpr const char STUN_SERVER[] = "stun:stun.l.google.com:19302";
constexpr const char INPUTS_LABEL[] = "inputs";
constexpr size_t MESSAGE_MAX = 65536;
constexpr int CODE_TRIES = 100;
constexpr int FRAME_RING = 256;
constexpr int FRAMES_PER_PACKET = 30;
constexpr int INPUT_REACH = 120;
constexpr int CHECKSUM_INTERVAL = 60;
constexpr int CATCH_UP_TICKS = 5;
constexpr Uint32 ROOM_SEED_MIX = 2654435761u;
constexpr auto TICK_TIME = std::chrono::duration<double>(TICK);
// The controls every fighter played on one tick of a room, and how many of its events were in by then.
struct Frame
{
Controls controls[MAX_FIGHTERS];
int tick = -1;
int events = 0;
int count = 0;
};
// One online game: a lobby's sandbox or a game queued in it. Inputs holds each member's controls by tick as they come
// (until played), and held what each played last, repeated when the next are late. Again counts the ticks a finished
// game's players have all been set to play again.
struct Room
{
Sim sim;
std::vector<RoomEvent> events;
std::map<int, std::map<int, Controls>> inputs;
std::map<int, Controls> held;
Frame frames[FRAME_RING];
std::string code;
int again = 0;
int tick = 0;
};
// Someone connected: their name, socket and WebRTC connection (with its reliable channel and lossy one), their lobby (empty until they make or join one), the room they play in
// (0 for none), the last tick of it they've confirmed, how early their controls last came, and the fighter they
// brought, kept while they're away in a game so they come back to the sandbox as they left.
struct Member
{
Profile profile;
std::string name;
std::string code;
std::shared_ptr<rtc::WebSocket> socket;
std::shared_ptr<rtc::PeerConnection> connection;
std::shared_ptr<rtc::DataChannel> channel;
std::shared_ptr<rtc::DataChannel> inputs;
int ack = -1;
int lead = 0;
int room = 0;
};
// A lobby: its members in the order they joined (the first hosting), and its rooms, the sandbox first.
struct Lobby
{
std::vector<int> members;
std::vector<int> rooms;
};
// What the network's threads hand the game loop: a member's connection opening or closing, one of their data channels
// arriving, or a message from them.
struct Arrival
{
std::string bytes;
std::shared_ptr<rtc::WebSocket> socket;
std::shared_ptr<rtc::DataChannel> channel;
int id = 0;
bool isBinary = false;
bool isClosed = false;
};
// Everything below belongs to the game loop's thread, except the arrivals, which the network's threads add to under
// the lock.
static std::mutex lock;
static std::vector<Arrival> arrivals;
static std::map<int, Member> members;
static std::map<std::string, Lobby> lobbies;
static std::map<int, Room> rooms;
static simdjson::dom::parser parser;
static std::mt19937 rng(std::random_device{}());
static rtc::Configuration peerConfig;
static int nextRoom = 1;
// Sends a member a lobby message (text) or a game one (binary): binary goes over their data channels once both are
// open (the lossy one for frames), and the socket until then. A connection can drop between checking it and sending,
// which throws. It's let go: its closing comes in later.
static void send(int id, const std::string& message, bool isBinary = false, bool isLossy = false)
{
auto found = members.find(id);
if (found == members.end())
return;
Member& member = found->second;
auto data = reinterpret_cast<const std::byte*>(message.data());
bool isLinked = member.channel && member.channel->isOpen() && member.inputs && member.inputs->isOpen();
try
{
if (isBinary && isLinked)
(isLossy ? member.inputs : member.channel)->send(data, message.size());
else if (isBinary && member.socket->isOpen())
member.socket->send(rtc::binary(data, data + message.size()));
else if (member.socket->isOpen())
member.socket->send(message);
}
catch (const std::exception&)
{
}
}
// Answers a member's WebRTC offer with a connection of the server's own, passing its description and candidates back
// over the socket. Their data channels come to the game loop as they arrive, and their messages like the socket's.
static void peer_open(int id)
{
Member& member = members[id];
if (member.connection)
member.connection->close();
member.connection = std::make_shared<rtc::PeerConnection>(peerConfig);
std::weak_ptr<rtc::WebSocket> socket = member.socket;
auto signal_send = [socket](const std::string& payload) {
auto open = socket.lock();
if (open && open->isOpen())
open->send("{\"type\":\"signal\",\"payload\":" + payload + "}");
};
member.connection->onLocalDescription([signal_send](rtc::Description description) {
signal_send("{\"sdp\":" + json_quote(std::string(description)) +
",\"kind\":" + json_quote(description.typeString()) + "}");
});
member.connection->onLocalCandidate([signal_send](rtc::Candidate candidate) {
signal_send("{\"candidate\":" + json_quote(candidate.candidate()) + ",\"mid\":" + json_quote(candidate.mid()) +
"}");
});
member.connection->onDataChannel([id](std::shared_ptr<rtc::DataChannel> channel) {
std::weak_ptr<rtc::DataChannel> weak = channel;
channel->onMessage([id, weak](rtc::message_variant data) {
// Pings are answered at once, so they time the connection alone.
if (std::holds_alternative<std::string>(data))
{
std::string& text = std::get<std::string>(data);
auto open = weak.lock();
if (open && text.starts_with("ping:"))
open->send("pong:" + text.substr(5));
return;
}
rtc::binary& binary = std::get<rtc::binary>(data);
std::lock_guard guard(lock);
arrivals.push_back({.bytes = std::string(reinterpret_cast<const char*>(binary.data()), binary.size()),
.id = id,
.isBinary = true});
});
std::lock_guard guard(lock);
arrivals.push_back({.channel = channel, .id = id});
});
}
// A description or candidate for a member's connection, from their game.
static void signal_handle(int id, simdjson::dom::element payload)
{
std::string_view sdp;
std::string_view kind;
std::string_view candidate;
std::string_view mid;
Member& member = members[id];
if (!payload["sdp"].get(sdp) && !payload["kind"].get(kind))
{
if (kind == "offer")
peer_open(id);
if (member.connection)
member.connection->setRemoteDescription(rtc::Description(std::string(sdp), std::string(kind)));
return;
}
if (!payload["candidate"].get(candidate) && !payload["mid"].get(mid) && member.connection)
member.connection->addRemoteCandidate(rtc::Candidate(std::string(candidate), std::string(mid)));
}
static void error_send(int id, const char* message)
{
send(id, "{\"type\":\"error\",\"message\":" + json_quote(message) + "}");
}
static void lobby_send(const std::string& code, const std::string& message, bool isBinary = false)
{
for (int id : lobbies[code].members)
send(id, message, isBinary);
}
// A name as the lobby shows it: printable characters only, cut to length, or "Player" when nothing's left.
static std::string name_clean(std::string_view raw)
{
std::string name = text_clean(raw, PLAYER_NAME_MAX);
return name.empty() ? "Player" : name;
}
static std::string code_make()
{
std::uniform_int_distribution<int> letter(0, (int)sizeof(LOBBY_CODE_LETTERS) - 2);
for (int attempt = 0; attempt < CODE_TRIES; attempt++)
{
std::string code;
for (int i = 0; i < LOBBY_CODE_LENGTH; i++)
code += LOBBY_CODE_LETTERS[letter(rng)];
if (!lobbies.contains(code))
return code;
}
return "";
}
// A fresh room in a lobby, numbered.
static int room_open(const std::string& code)
{
int serial = nextRoom++;
Room& room = rooms[serial];
room.code = code;
lobbies[code].rooms.push_back(serial);
return serial;
}
// Sets a change in a room for the tick it's about to play, and tells everyone in it.
static void event_add(int serial, RoomEvent event)
{
Room& room = rooms[serial];
event.tick = room.tick;
room.events.push_back(event);
PacketWriter packet;
packet.put(PACKET_EVENT).put(serial).put((int)room.events.size() - 1).put(event);
for (auto& [id, member] : members)
if (member.room == serial)
send(id, packet.bytes, true);
}
// Puts a member in a room as its next tick starts: their fighter joins (from their profile, or their seat in a game
// already set up for them), and they get the room as it is then, with every event it's had.
static void room_enter(int id, int serial, bool isJoining)
{
Member& member = members[id];
Room& room = rooms[serial];
member.room = serial;
member.ack = room.tick - 1;
room.held[id] = {};
PacketWriter packet;
packet.put(PACKET_STATE).put(serial).put(room.tick);
packet.bytes += state_save(room.sim);
send(id, packet.bytes, true);
for (size_t i = 0; i < room.events.size(); i++)
send(id, PacketWriter().put(PACKET_EVENT).put(serial).put((int)i).put(room.events[i]).bytes, true);
if (isJoining)
event_add(serial, {.type = ROOM_JOIN, .member = id, .profile = member.profile});
}
// Takes a member out of their room: the sandbox drops their fighter, and a game counts them as having left it.
static void room_exit(int id)
{
Member& member = members[id];
auto room = rooms.find(member.room);
member.room = 0;
if (room == rooms.end())
return;
room->second.inputs.erase(id);
room->second.held.erase(id);
if (lobbies[member.code].rooms.front() == room->first)
event_add(room->first, {.type = ROOM_LEAVE, .member = id});
}
// Puts a member into a lobby and tells everyone: the newcomer gets the whole lobby, the rest get the newcomer. Their
// fighter joins the sandbox once their profile comes.
static void lobby_enter(int id, const std::string& code)
{
Lobby& lobby = lobbies[code];
if (lobby.rooms.empty())
{
Room& sandbox = rooms[room_open(code)];
sandbox.sim.init(rng() | 1);
sandbox.sim.fighterCount = 0;
sandbox.sim.match.isOnline = true;
std::fill(std::begin(sandbox.sim.match.owners), std::end(sandbox.sim.match.owners), -1);
}
lobby.members.push_back(id);
members[id].code = code;
std::string list;
for (int other : lobby.members)
list += (list.empty() ? "" : ",") + std::string("{\"id\":") + std::to_string(other) +
",\"name\":" + json_quote(members[other].name) + "}";
send(id, "{\"type\":\"welcome\",\"id\":" + std::to_string(id) + ",\"code\":" + json_quote(code) +
",\"host\":" + std::to_string(lobby.members.front()) + ",\"members\":[" + list + "]}");
for (int other : lobby.members)
if (other != id)
send(other, "{\"type\":\"member_join\",\"id\":" + std::to_string(id) +
",\"name\":" + json_quote(members[id].name) + "}");
}
// Takes a member out of its lobby, telling the rest (and who hosts now, and why if they were kicked). An empty lobby is
// gone, with its rooms.
static void lobby_exit(int id, const std::string& kick = "", bool isKicked = false)
{
std::string code = members[id].code;
if (code.empty())
return;
room_exit(id);
members[id].code.clear();
Lobby& lobby = lobbies[code];
std::erase(lobby.members, id);
if (lobby.members.empty())
{
for (int serial : lobby.rooms)
rooms.erase(serial);
lobbies.erase(code);
std::printf("Lobby %s closed (%zu open)\n", code.c_str(), lobbies.size());
return;
}
std::string reason = isKicked ? ",\"kicked\":true,\"reason\":" + json_quote(kick) : "";
lobby_send(code, "{\"type\":\"member_leave\",\"id\":" + std::to_string(id) +
",\"host\":" + std::to_string(lobby.members.front()) + reason + "}");
}
// The lobby messages, in JSON: making, joining and leaving lobbies, kicking, and keeping the connection busy.
static void text_handle(int id, const std::string& text)
{
simdjson::dom::element message;
std::string_view type;
if (parser.parse(text).get(message) || message["type"].get(type))
return error_send(id, "Bad message");
// Answered, so proxies see the connection busy both ways.
if (type == "ping")
return send(id, "{\"type\":\"pong\"}");
if (type == "create" || type == "join")
{
int64_t version = 0;
std::string_view name;
if (message["version"].get(version) || version != PROTOCOL_VERSION)
return error_send(id, "Different game version");
if (!members[id].code.empty())
return error_send(id, "Already in a lobby");
members[id].name = name_clean(message["name"].get(name) ? "" : name);
if (type == "create")
{
std::string code = code_make();
if (code.empty())
return error_send(id, "No free lobby codes");
std::printf("Lobby %s opened (%zu open)\n", code.c_str(), lobbies.size() + 1);
return lobby_enter(id, code);
}
std::string_view raw;
if (message["code"].get(raw))
return error_send(id, "No code");
std::string code;
for (char c : raw)
code += (char)std::toupper((unsigned char)c);
auto lobby = lobbies.find(code);
if (lobby == lobbies.end())
return error_send(id, "No lobby with that code");
if ((int)lobby->second.members.size() >= LOBBY_MEMBERS_MAX)
return error_send(id, "That lobby is full");
return lobby_enter(id, code);
}
if (type == "leave")
return lobby_exit(id);
if (type == "signal")
{
simdjson::dom::element payload;
if (message["payload"].get(payload) || members[id].code.empty())
return error_send(id, "Bad signal");
return signal_handle(id, payload);
}
if (type == "kick")
{
// Only a lobby's host can kick, and only someone else in their lobby. The kicked member is told why, then is
// out (their connection to the server stays; they can join a lobby again).
int64_t target = 0;
std::string_view raw;
std::string& code = members[id].code;
if (message["id"].get(target) || code.empty() || lobbies[code].members.front() != id || target == id)
return error_send(id, "Only the host can kick");
auto kicked = members.find((int)target);
if (kicked == members.end() || kicked->second.code != code)
return error_send(id, "No such player in the lobby");
std::string reason = text_clean(message["reason"].get(raw) ? "" : raw, KICK_REASON_MAX);
send((int)target, "{\"type\":\"kicked\",\"reason\":" + json_quote(reason) + "}");
std::printf("Lobby %s: %d kicked %d\n", code.c_str(), id, (int)target);
return lobby_exit((int)target, reason, true);
}
error_send(id, "Unknown message");
}
// Text a member sent for the lobby (a chat line or their new name), cleaned, passed to everyone with who it's from.
static void text_relay(int id, PacketType type, PacketReader& reader, size_t max)
{
Uint16 length = 0;
if (!reader.get(length) || reader.at + length > reader.bytes.size())
return;
std::string text = text_clean(std::string_view(reader.bytes.data() + reader.at, length), max);
if (text.empty())
return;
if (type == PACKET_RENAME)
members[id].name = text;
PacketWriter packet;
packet.put(type).put(id).put((Uint16)text.size());
packet.bytes += text;
lobby_send(members[id].code, packet.bytes, true);
}
// The game messages, in binary (see protocol.h), from a member in a lobby.
static void packet_handle(int id, const std::string& bytes)
{
Member& member = members[id];
if (member.code.empty())
return;
PacketReader reader = {bytes};
PacketType type;
if (!reader.get(type))
return;
if (type == PACKET_PROFILE)
{
// A member's first profile brings their fighter into the sandbox.
if (!reader.get(member.profile))
return;
member.profile.color = std::clamp(member.profile.color, -1, COLOR_COUNT - 1);
std::memcpy(member.profile.name, member.name.c_str(), member.name.size() + 1);
if (member.room == 0)
room_enter(id, lobbies[member.code].rooms.front(), true);
return;
}
if (type == PACKET_INPUTS)
{
// Controls for ticks already played are too late, and dropped. How early the latest came steers the player's
// game to keep its lead.
int serial = 0;
int ack = 0;
int first = 0;
Uint16 count = 0;
if (!reader.get(serial) || !reader.get(ack) || !reader.get(first) || !reader.get(count) ||
serial != member.room)
return;
Room& room = rooms[serial];
member.ack = std::max(member.ack, std::min(ack, room.tick - 1));
member.lead = first + count - 1 - room.tick;
for (int i = 0; i < count; i++)
{
Controls controls;
if (!reader.get(controls))
return;
int at = first + i;
if (at >= room.tick && at < room.tick + INPUT_REACH)
room.inputs[id][at] = controls;
}
return;
}
if (type == PACKET_QUEUE)
{
Uint8 isOpen = 0;
Rules rules;
int sandbox = lobbies[member.code].rooms.front();
if (reader.get(isOpen) && reader.get(rules) && member.room == sandbox)
event_add(sandbox, {.type = isOpen ? ROOM_QUEUE : ROOM_UNQUEUE, .member = id, .rules = rules_clean(rules)});
return;
}
if (type == PACKET_CHAT || type == PACKET_RENAME)
return text_relay(id, type, reader, type == PACKET_CHAT ? CHAT_MESSAGE_MAX : PLAYER_NAME_MAX);
if (type == PACKET_TYPING)
{
Uint8 isTyping = 0;
if (reader.get(isTyping))
lobby_send(member.code, PacketWriter().put(PACKET_TYPING).put(id).put(isTyping).bytes, true);
}
}
// The controls a room's fighter plays this tick: its member's for the tick if they came, or their last again (without
// a one-off edit or weapon switch). A member gone from the lobby leaves a game, and bots and empty seats send nothing.
static Controls controls_take(Room& room, int owner)
{
if (owner < 0)
return {};
auto member = members.find(owner);
if (member == members.end() || member->second.code != room.code)
return {.edit = edit_encode({.type = EDIT_LEAVE})};
std::map<int, Controls>& inputs = room.inputs[owner];
auto found = inputs.find(room.tick);
Controls& held = room.held[owner];
if (found != inputs.end())
held = found->second;
else
{
held.edit = -1;
held.slot = -1;
}
inputs.erase(inputs.begin(), inputs.upper_bound(room.tick));
return held;
}
// Plays one tick of a room: its events due, then every fighter's controls, kept as the tick's frame.
static void room_step(Room& room)
{
for (RoomEvent& event : room.events)
if (event.tick == room.tick)
room.sim.event_apply(event);
Frame& frame = room.frames[room.tick % FRAME_RING];
frame.tick = room.tick;
frame.events = room.events.size();
frame.count = room.sim.fighterCount;
for (int i = 0; i < room.sim.fighterCount; i++)
frame.controls[i] = room.sim.fighters[i].controls = controls_take(room, room.sim.match.owners[i]);
room.sim.tick();
room.tick++;
}
// A sandbox's queued game that's ready starts in a room of its own: its players leave the sandbox (bringing nothing
// along but their name and colour) and go into it.
static void game_open(int sandboxSerial, int queuer)
{
Room& sandbox = rooms[sandboxSerial];
Match& lobby = sandbox.sim.match;
int seats[MAX_FIGHTERS];
int count = lobby.queue_members_get(sandbox.sim, queuer, seats);
int players[MAX_FIGHTERS];
for (int k = 0; k < count; k++)
{
players[k] = lobby.owners[seats[k]];
members[players[k]].profile = profile_get(sandbox.sim.fighters[seats[k]]);
std::memcpy(members[players[k]].profile.name, lobby.names[seats[k]], sizeof(lobby.names[seats[k]]));
members[players[k]].profile.color = lobby.colors[seats[k]];
}
int serial = room_open(sandbox.code);
Room& game = rooms[serial];
game.sim.game_start(lobby, seats, count, lobby.queues[queuer].rules,
sandbox.sim.rng.state ^ (Uint32)sandbox.tick * ROOM_SEED_MIX);
for (int k = 0; k < count; k++)
{
room_exit(players[k]);
room_enter(players[k], serial, false);
}
}
// After a game's tick: anyone who's left it goes back to the sandbox, and once a finished match's players have all
// been set to play again for a moment, they go on to a fresh game in a room of its own. A game no one plays in any more
// is over.
static void game_follow(int serial)
{
Room& game = rooms[serial];
Match& match = game.sim.match;
int sandbox = lobbies[game.code].rooms.front();
int seats[MAX_FIGHTERS];
int humans = 0;
bool isAgain = match.state == MATCH_OVER;
for (int k = 0; k < game.sim.fighterCount; k++)
{
int owner = match.owners[k];
auto member = members.find(owner);
if (owner < 0 || member == members.end() || member->second.room != serial)
continue;
if (match.isGone[k])
{
room_exit(owner);
room_enter(owner, sandbox, true);
continue;
}
isAgain &= match.isReady[k];
seats[humans++] = k;
}
bool isSet = isAgain && (humans >= PLAYERS_MIN || match.rules.isBotFill);
game.again = isSet ? game.again + 1 : 0;
if (humans > 0 && game.again < QUEUE_START_TICKS)
return;
if (humans > 0)
{
Rules rules = match.rules;
if (!rules.isBotFill)
rules.players = humans;
int next = room_open(game.code);
rooms[next].sim.game_start(match, seats, humans, rules, game.sim.rng.state ^ (Uint32)game.tick * ROOM_SEED_MIX);
for (int k = 0; k < humans; k++)
{
int owner = match.owners[seats[k]];
room_exit(owner);
room_enter(owner, next, false);
}
}
std::erase(lobbies[game.code].rooms, serial);
rooms.erase(serial);
}
// Sends everyone in a room the frames they haven't confirmed (up to a packet's worth), and every so often the state's
// checksum.
static void frames_send(int serial)
{
Room& room = rooms[serial];
for (auto& [id, member] : members)
{
if (member.room != serial)
continue;
int first = std::max(member.ack + 1, room.tick - FRAME_RING + 1);
int count = std::min(room.tick - first, FRAMES_PER_PACKET);
if (count <= 0)
continue;
PacketWriter packet;
packet.put(PACKET_FRAMES).put(serial).put(member.lead).put(first).put((Uint16)count);
for (int at = first; at < first + count; at++)
{
Frame& frame = room.frames[at % FRAME_RING];
packet.put((Uint16)frame.events).put((Uint8)frame.count);
for (int i = 0; i < frame.count; i++)
packet.put(frame.controls[i]);
}
send(id, packet.bytes, true, true);
}
if (room.tick % CHECKSUM_INTERVAL == 0)
for (auto& [id, member] : members)
if (member.room == serial)
send(id, PacketWriter().put(PACKET_CHECKSUM).put(serial).put(room.tick).put(room.sim.checksum_get()).bytes,
true);
}
// One tick of every room, in every lobby: the sandbox, then its games, then the frames out.
static void rooms_step()
{
std::vector<int> serials;
for (auto& [serial, room] : rooms)
serials.push_back(serial);
for (int serial : serials)
{
if (!rooms.contains(serial))
continue;
Room& room = rooms[serial];
room_step(room);
if (lobbies[room.code].rooms.front() == serial)
{
int queuer = room.sim.match.queue_startable_get(room.sim);
if (queuer >= 0)
game_open(serial, queuer);
}
else
game_follow(serial);
}
for (auto& [serial, room] : rooms)
frames_send(serial);
}
// Takes in what the network's threads have handed over since the last tick.
static void arrivals_take()
{
std::vector<Arrival> taken;
{
std::lock_guard guard(lock);
taken.swap(arrivals);
}
for (Arrival& arrival : taken)
{
if (arrival.socket)
members[arrival.id] = {.socket = arrival.socket};
else if (arrival.channel && members.contains(arrival.id))
(arrival.channel->label() == INPUTS_LABEL ? members[arrival.id].inputs : members[arrival.id].channel) =
arrival.channel;
else if (arrival.isClosed)
{
lobby_exit(arrival.id);
if (members[arrival.id].connection)
members[arrival.id].connection->close();
members.erase(arrival.id);
}
else if (members.contains(arrival.id))
arrival.isBinary ? packet_handle(arrival.id, arrival.bytes) : text_handle(arrival.id, arrival.bytes);
}
}
static volatile std::sig_atomic_t isStopping = 0;
int main(int argc, char** argv)
{
std::setvbuf(stdout, nullptr, _IOLBF, 0);
rtc::InitLogger(rtc::LogLevel::Warning);
rtc::WebSocketServerConfiguration config;
config.port = argc > 1 ? (uint16_t)std::atoi(argv[1]) : PORT_DEFAULT;
config.bindAddress = "127.0.0.1";
config.maxMessageSize = MESSAGE_MAX;
rtc::WebSocketServer server(config);
peerConfig.iceServers.emplace_back(STUN_SERVER);
peerConfig.portRangeBegin = argc > 2 ? (uint16_t)std::atoi(argv[2]) : UDP_FIRST_DEFAULT;
peerConfig.portRangeEnd = argc > 3 ? (uint16_t)std::atoi(argv[3]) : UDP_LAST_DEFAULT;
peerConfig.maxMessageSize = MESSAGE_MAX;
int nextId = 1;
server.onClient([&nextId](std::shared_ptr<rtc::WebSocket> socket) {
std::lock_guard guard(lock);
int id = nextId++;
arrivals.push_back({.socket = socket, .id = id});
socket->onMessage([id](rtc::message_variant data) {
std::lock_guard guard(lock);
if (std::holds_alternative<std::string>(data))
arrivals.push_back({.bytes = std::get<std::string>(data), .id = id});
else
{
rtc::binary& binary = std::get<rtc::binary>(data);
arrivals.push_back({.bytes = std::string(reinterpret_cast<const char*>(binary.data()), binary.size()),
.id = id,
.isBinary = true});
}
});
socket->onClosed([id]() {
std::lock_guard guard(lock);
arrivals.push_back({.id = id, .isClosed = true});
});
});
std::printf("Server listening on 127.0.0.1:%d, WebRTC on UDP %d to %d\n", server.port(), peerConfig.portRangeBegin,
peerConfig.portRangeEnd);
std::signal(SIGINT, [](int) { isStopping = 1; });
std::signal(SIGTERM, [](int) { isStopping = 1; });
// The game loop: a tick of every room each TICK, catching up a few at most when it falls behind.
auto next = std::chrono::steady_clock::now();
while (!isStopping)
{
arrivals_take();
for (int i = 0; i < CATCH_UP_TICKS && std::chrono::steady_clock::now() >= next; i++)
{
rooms_step();
next += std::chrono::duration_cast<std::chrono::steady_clock::duration>(TICK_TIME);
}
if (std::chrono::steady_clock::now() > next)
next = std::chrono::steady_clock::now();
std::this_thread::sleep_until(next);
}
std::printf("Server stopping\n");
for (auto& [id, member] : members)
{
if (member.connection)
member.connection->close();
member.socket->close();
}
return 0;
}