#pragma once // What the game and the server agree on. The server hands out lobby codes, keeps each lobby's members and passes // WebRTC signaling between them; matches themselves run peer to peer. Messages are JSON objects with a "type": // // Game to server: // create {name, version} make a lobby and join it as host // join {code, name, version} join a lobby by its code // leave {} leave the lobby // kick {id, reason} (the host) put someone out of the lobby // signal {to, payload} pass a WebRTC description or candidate to another member // ping {} keeps the connection alive through proxies (answered with pong) // // Server to game: // welcome {id, code, host, members: [{id, name}]} in a lobby (members includes this one) // member_join {id, name} // member_leave {id, host, kicked?, reason?} host is who hosts now; kicked says they were kicked // kicked {reason} this member was kicked out of the lobby // signal {from, payload} // error {message} the request failed; the connection stays open // pong {} // // A signal's payload is either a description {sdp, kind} or a candidate {candidate, mid}. #include #include #include #include constexpr int PROTOCOL_VERSION = 4; constexpr int LOBBY_MEMBERS_MAX = 16; constexpr int LOBBY_CODE_LENGTH = 4; // Letters only, without I and O, so codes are easy to read out and type. constexpr const char LOBBY_CODE_LETTERS[] = "ABCDEFGHJKLMNPQRSTUVWXYZ"; constexpr int PLAYER_NAME_MAX = 24; constexpr int CHAT_MESSAGE_MAX = 100; constexpr int KICK_REASON_MAX = 60; constexpr const char SERVER_URL_DEFAULT[] = "wss://shweetz.net/smack-a-brother/ws"; constexpr int SERVER_URL_LENGTH = 128; // Binary messages between a game and the server once it's in a lobby, each starting with its type. A room is one // online game (a lobby's sandbox, or a game queued in it), numbered by the server. // // Game to server: // PACKET_PROFILE Profile the fighter this player brings into the lobby // PACKET_INPUTS room, ack, first, Uint16 count, Controls* this player's controls from a tick on, and the last // tick it has confirmed // PACKET_QUEUE Uint8 isOpen, Rules queue a game in the sandbox, or stop queueing it // PACKET_CHAT Uint16 length, text // PACKET_TYPING Uint8 isTyping // PACKET_RENAME Uint16 length, text // PACKET_PING Uint16 milliseconds this player's round trip to the server // // Server to game: // PACKET_STATE room, tick, state the game this player is in now, as a tick starts // PACKET_EVENT room, serial, RoomEvent the room's events, numbered from 0, in order // PACKET_FRAMES room, lead, first, Uint16 count, then for each tick: Uint16 events, Uint8 fighters, Controls* // every fighter's controls as the server played them, // once the events before them are in, and how early // (in ticks) this player's last controls came // PACKET_CHECKSUM room, tick, Uint32 the room's state as that tick starts // PACKET_CHAT from, Uint16 length, text and the same for typing, renames and pings enum PacketType : unsigned char { PACKET_PROFILE = 1, PACKET_INPUTS, PACKET_QUEUE, PACKET_CHAT, PACKET_TYPING, PACKET_RENAME, PACKET_STATE, PACKET_EVENT, PACKET_FRAMES, PACKET_CHECKSUM, PACKET_PING }; // Packets are plain values one after another, as they sit in memory (every platform agrees on these layouts). struct PacketWriter { std::string bytes; template PacketWriter& put(const T& value) { static_assert(std::is_trivially_copyable_v); bytes.append(reinterpret_cast(&value), sizeof(T)); return *this; } }; struct PacketReader { const std::string& bytes; size_t at = 0; template bool get(T& value) { static_assert(std::is_trivially_copyable_v); if (at + sizeof(T) > bytes.size()) return false; std::memcpy(static_cast(&value), bytes.data() + at, sizeof(T)); at += sizeof(T); return true; } }; // Text from elsewhere (another player, or the server) made safe to show: only printable ASCII (what the game's font // has; anything else, emoji included, is dropped), cut to max characters, with spaces at its ends trimmed. inline std::string text_clean(std::string_view raw, size_t max) { std::string text; for (char c : raw) if (c >= ' ' && c <= '~' && text.size() < max) text += c; size_t first = text.find_first_not_of(' '); size_t last = text.find_last_not_of(' '); return first == std::string::npos ? "" : text.substr(first, last - first + 1); } // A count of seconds in words: "1 second", "29 seconds". inline std::string seconds_say(int seconds) { return std::to_string(seconds) + (seconds == 1 ? " second" : " seconds"); } // A string as a JSON string literal, quotes included. inline std::string json_quote(const std::string& text) { std::string quoted = "\""; for (unsigned char c : text) if (c == '"' || c == '\\') quoted += '\\', quoted += (char)c; else if (c == '\n') quoted += "\\n"; else if (c == '\r') quoted += "\\r"; else if (c == '\t') quoted += "\\t"; else if (c < 0x20) { constexpr char HEX[] = "0123456789abcdef"; quoted += "\\u00"; quoted += HEX[c >> 4]; quoted += HEX[c & 15]; } else quoted += (char)c; return quoted + "\""; }