// 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 #include #include #include #include #include #include #include #include #include #include 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; // Frames go out a packet of at most this many bytes at a time, so a lossy one fits one datagram and is never split. constexpr size_t FRAME_BYTES_MAX = 1100; 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(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. struct Room { Sim sim; std::vector events; std::map> inputs; std::map held; Frame frames[FRAME_RING]; std::string code; 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 socket; std::shared_ptr connection; std::shared_ptr channel; std::shared_ptr 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 members; std::vector 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 socket; std::shared_ptr 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 arrivals; static std::map members; static std::map lobbies; static std::map 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(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(peerConfig); std::weak_ptr 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 channel) { std::weak_ptr 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(data)) { std::string& text = std::get(data); auto open = weak.lock(); if (open && text.starts_with("ping:")) open->send("pong:" + text.substr(5)); return; } rtc::binary& binary = std::get(data); std::lock_guard guard(lock); arrivals.push_back({.bytes = std::string(reinterpret_cast(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 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& 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 (see Match::againTicks), 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; 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; } seats[humans++] = k; } if (humans > 0 && match.againTicks < 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 (as many as fit a packet), 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 = 0; for (size_t bytes = 0; first + count < room.tick; count++) { bytes += sizeof(Uint16) + sizeof(Uint8) + room.frames[(first + count) % FRAME_RING].count * sizeof(Controls); if (bytes > FRAME_BYTES_MAX && count > 0) break; } 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 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 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 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(data)) arrivals.push_back({.bytes = std::get(data), .id = id}); else { rtc::binary& binary = std::get(data); arrivals.push_back({.bytes = std::string(reinterpret_cast(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(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; }