- 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>
150 lines
6.0 KiB
C++
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 + "\"";
|
|
}
|