Files
smack-a-brother/src/protocol.h
T
shweetandClaude Opus 5.5 9a41474069 Tomatoes, match tabs, shared pings, and a round of fixes
- On the results, everyone but the winner splats tomatoes where they
  click (bots aim at the winner), with a new splat sound.
- During a match the menu has the item tabs (choosing when the
  countdown allows, and otherwise levelling the sandbox's levels for
  when you're back), Play (the game's rules) and Settings.
- Pings are shared through the server (protocol 4), so other players'
  connection bars show over their heads and on their windows.
- PvP comes into lobbies with your profile and is saved.
- The choice menu opens by itself however the countdown began.
- Knocked-out fighters keep their buttons current, so Ready on the
  results toggles once per press.
- The weapon wheel wraps both ways. Readying with empty slots plays
  the equip sound for your own fighter.
- Shock Absorber: up to 50% less weapon movement and 33% less
  knockback. Only the catalog previews the next level.

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

150 lines
6.0 KiB
C++

#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 <cstring>
#include <string>
#include <string_view>
#include <type_traits>
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 <class T> PacketWriter& put(const T& value)
{
static_assert(std::is_trivially_copyable_v<T>);
bytes.append(reinterpret_cast<const char*>(&value), sizeof(T));
return *this;
}
};
struct PacketReader
{
const std::string& bytes;
size_t at = 0;
template <class T> bool get(T& value)
{
static_assert(std::is_trivially_copyable_v<T>);
if (at + sizeof(T) > bytes.size())
return false;
std::memcpy(static_cast<void*>(&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 + "\"";
}