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>
This commit is contained in:
1 parent
655332778c
commit
dadfff189d
5 files changed
+227
-194
No files matched your search
+123
-14
@@ -1,10 +1,17 @@
|
||||
// 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, and play in it here: the server runs every lobby's sandbox and the games queued in it, each
|
||||
// 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.
|
||||
//
|
||||
// smack-a-brother-server [port] (listens on 127.0.0.1, port 47800 by default)
|
||||
// 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"
|
||||
@@ -23,6 +30,10 @@
|
||||
#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;
|
||||
@@ -57,7 +68,7 @@ struct Room
|
||||
int tick = 0;
|
||||
};
|
||||
|
||||
// Someone connected: their name and socket, their lobby (empty until they make or join one), the room they play in
|
||||
// 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
|
||||
@@ -66,6 +77,9 @@ struct Member
|
||||
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;
|
||||
@@ -78,11 +92,13 @@ struct Lobby
|
||||
std::vector<int> rooms;
|
||||
};
|
||||
|
||||
// What the network's threads hand the game loop: a member's connection opening or closing, or a message from them.
|
||||
// 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;
|
||||
@@ -97,27 +113,99 @@ 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;
|
||||
|
||||
// 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)
|
||||
// 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 member = members.find(id);
|
||||
if (member == members.end() || !member->second.socket->isOpen())
|
||||
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)
|
||||
member->second.socket->send(rtc::binary(data, data + message.size()));
|
||||
else
|
||||
member->second.socket->send(message);
|
||||
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) + "}");
|
||||
@@ -300,6 +388,13 @@ static void text_handle(int id, const std::string& text)
|
||||
}
|
||||
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
|
||||
@@ -539,7 +634,7 @@ static void frames_send(int serial)
|
||||
for (int i = 0; i < frame.count; i++)
|
||||
packet.put(frame.controls[i]);
|
||||
}
|
||||
send(id, packet.bytes, true);
|
||||
send(id, packet.bytes, true, true);
|
||||
}
|
||||
if (room.tick % CHECKSUM_INTERVAL == 0)
|
||||
for (auto& [id, member] : members)
|
||||
@@ -585,9 +680,14 @@ static void arrivals_take()
|
||||
{
|
||||
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))
|
||||
@@ -606,6 +706,10 @@ int main(int argc, char** argv)
|
||||
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) {
|
||||
@@ -629,7 +733,8 @@ int main(int argc, char** argv)
|
||||
arrivals.push_back({.id = id, .isClosed = true});
|
||||
});
|
||||
});
|
||||
std::printf("Server listening on 127.0.0.1:%d\n", server.port());
|
||||
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; });
|
||||
@@ -649,6 +754,10 @@ int main(int argc, char** argv)
|
||||
}
|
||||
std::printf("Server stopping\n");
|
||||
for (auto& [id, member] : members)
|
||||
{
|
||||
if (member.connection)
|
||||
member.connection->close();
|
||||
member.socket->close();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
+1
-3
@@ -696,9 +696,7 @@ void Menu::online_draw(Game& game, Rect area)
|
||||
Color color = fighter >= 0 && game.sim.match.state == MATCH_PRACTICE ? game.color_get(fighter, true) : ui.tint;
|
||||
text.draw(game.renderer, line, {.color = color, .position = {area.x, y + TEXT_ROW_INSET}});
|
||||
float x = area.x + text.size_get(line).x + PADDING * 2;
|
||||
if (!isSelf && !member.isConnected)
|
||||
text.draw(game.renderer, "Connecting...", {.color = ui.shade, .position = {x, y + TEXT_ROW_INSET}});
|
||||
else if (!isSelf)
|
||||
if (member.ping >= 0)
|
||||
ui.ping_draw(game, {x, y + TEXT_ROW_INSET}, member.ping, 1);
|
||||
y += ROW_HEIGHT;
|
||||
}
|
||||
|
||||
+75
-139
@@ -17,9 +17,7 @@ constexpr Uint64 PING_INTERVAL = 1000;
|
||||
constexpr Uint64 HEARTBEAT_INTERVAL = 20000;
|
||||
constexpr float NS_PER_MS = 1e6f;
|
||||
|
||||
// A direct connection to one other member: the connection and, once they're there, its two data channels: a reliable,
|
||||
// ordered one for everything that mustn't go missing, and one for inputs, which are sent again in every packet until
|
||||
// they're acknowledged, so it neither waits on nor resends a lost one.
|
||||
// The WebRTC connection to the server and, once they're there, its two data channels (see Net).
|
||||
struct PeerLink
|
||||
{
|
||||
std::shared_ptr<rtc::PeerConnection> connection;
|
||||
@@ -35,7 +33,7 @@ struct PeerLink
|
||||
struct Net::Impl
|
||||
{
|
||||
std::shared_ptr<rtc::WebSocket> socket;
|
||||
std::map<int, PeerLink> links;
|
||||
PeerLink link;
|
||||
std::string pending;
|
||||
simdjson::dom::parser parser;
|
||||
Uint64 nextPing = 0;
|
||||
@@ -44,7 +42,6 @@ struct Net::Impl
|
||||
struct Delayed
|
||||
{
|
||||
Uint64 due;
|
||||
int to;
|
||||
bool isReliable;
|
||||
std::string bytes;
|
||||
bool isText = false;
|
||||
@@ -101,10 +98,9 @@ Net::~Net()
|
||||
// Closes every connection, waiting for nothing; anything still queued is dropped.
|
||||
void Net::shutdown()
|
||||
{
|
||||
for (auto& [id, link] : impl->links)
|
||||
if (link.connection)
|
||||
link.connection->close();
|
||||
impl->links.clear();
|
||||
if (impl->link.connection)
|
||||
impl->link.connection->close();
|
||||
impl->link = {};
|
||||
if (impl->socket)
|
||||
impl->socket->close();
|
||||
impl->socket.reset();
|
||||
@@ -124,12 +120,12 @@ void Net::lobby_join(Game& game, const char* lobby)
|
||||
json_quote(game.settings.name) + ",\"version\":" + std::to_string(PROTOCOL_VERSION) + "}");
|
||||
}
|
||||
|
||||
// Leaves the lobby, closing every direct connection but keeping the one to the server.
|
||||
// Leaves the lobby, closing the WebRTC connection but keeping the socket to the server.
|
||||
void Net::lobby_leave()
|
||||
{
|
||||
for (auto& [id, link] : impl->links)
|
||||
link.connection->close();
|
||||
impl->links.clear();
|
||||
if (impl->link.connection)
|
||||
impl->link.connection->close();
|
||||
impl->link = {};
|
||||
if (impl->socket && impl->socket->isOpen() && state == NET_LOBBY)
|
||||
impl->socket->send(std::string("{\"type\":\"leave\"}"));
|
||||
members.clear();
|
||||
@@ -174,7 +170,13 @@ void Net::request_send(Game& game, const std::string& request)
|
||||
});
|
||||
impl->socket->onMessage([this, shared](rtc::message_variant data) {
|
||||
if (std::holds_alternative<std::string>(data))
|
||||
{
|
||||
shared->post([this, text = std::get<std::string>(data)]() { message_handle(text); });
|
||||
return;
|
||||
}
|
||||
rtc::binary& bytes = std::get<rtc::binary>(data);
|
||||
std::string packet(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||||
shared->post([this, packet]() { packets.push_back(packet); });
|
||||
});
|
||||
impl->socket->onClosed([this, shared]() {
|
||||
shared->post([this]() {
|
||||
@@ -230,14 +232,10 @@ void Net::message_handle(const std::string& text)
|
||||
NetMember member;
|
||||
member.id = entry["id"].get(number) ? -1 : (int)number;
|
||||
member.name = entry["name"].get(field) ? "" : std::string(field);
|
||||
member.isConnected = member.id == selfId;
|
||||
members.push_back(member);
|
||||
}
|
||||
state = NET_LOBBY;
|
||||
// The newcomer reaches out to everyone already here.
|
||||
for (NetMember& member : members)
|
||||
if (member.id != selfId)
|
||||
peer_open(member.id, true);
|
||||
link_open();
|
||||
return;
|
||||
}
|
||||
if (type == "member_join")
|
||||
@@ -266,137 +264,85 @@ void Net::message_handle(const std::string& text)
|
||||
if (!message["kicked"].get(isKicked) && isKicked)
|
||||
kicks[id] = text_clean(message["reason"].get(field) ? "" : field, KICK_REASON_MAX);
|
||||
hostId = message["host"].get(number) ? hostId : (int)number;
|
||||
peer_close(id);
|
||||
std::erase_if(members, [&](NetMember& member) { return member.id == id; });
|
||||
return;
|
||||
}
|
||||
if (type == "signal")
|
||||
{
|
||||
simdjson::dom::element payload;
|
||||
if (!message["from"].get(number) && !message["payload"].get(payload))
|
||||
signal_handle((int)number, simdjson::minify(payload));
|
||||
if (!message["payload"].get(payload))
|
||||
signal_handle(simdjson::minify(payload));
|
||||
}
|
||||
}
|
||||
|
||||
// Starts a direct connection to another member. The one offering makes the data channel, which starts the
|
||||
// negotiation; the other gets it once the offer is answered. Either way, descriptions and candidates go through the
|
||||
// server.
|
||||
void Net::peer_open(int id, bool isOfferer)
|
||||
// Offers the server a WebRTC connection with both data channels, which starts the negotiation. Descriptions and
|
||||
// candidates go back and forth over the socket. Pongs on the reliable channel time the round trip.
|
||||
void Net::link_open()
|
||||
{
|
||||
rtc::Configuration config;
|
||||
config.iceServers.emplace_back(STUN_SERVER);
|
||||
PeerLink& link = impl->links[id];
|
||||
PeerLink& link = impl->link;
|
||||
link.connection = std::make_shared<rtc::PeerConnection>(config);
|
||||
Impl* shared = impl.get();
|
||||
auto signal_send = [this](int to, const std::string& payload) {
|
||||
auto signal_send = [this](const std::string& payload) {
|
||||
if (impl->socket && impl->socket->isOpen())
|
||||
impl->socket->send("{\"type\":\"signal\",\"to\":" + std::to_string(to) + ",\"payload\":" + payload + "}");
|
||||
impl->socket->send("{\"type\":\"signal\",\"payload\":" + payload + "}");
|
||||
};
|
||||
link.connection->onLocalDescription([shared, signal_send, id](rtc::Description description) {
|
||||
link.connection->onLocalDescription([shared, signal_send](rtc::Description description) {
|
||||
std::string payload = "{\"sdp\":" + json_quote(std::string(description)) +
|
||||
",\"kind\":" + json_quote(description.typeString()) + "}";
|
||||
shared->post([signal_send, id, payload]() { signal_send(id, payload); });
|
||||
shared->post([signal_send, payload]() { signal_send(payload); });
|
||||
});
|
||||
link.connection->onLocalCandidate([shared, signal_send, id](rtc::Candidate candidate) {
|
||||
link.connection->onLocalCandidate([shared, signal_send](rtc::Candidate candidate) {
|
||||
std::string payload =
|
||||
"{\"candidate\":" + json_quote(candidate.candidate()) + ",\"mid\":" + json_quote(candidate.mid()) + "}";
|
||||
shared->post([signal_send, id, payload]() { signal_send(id, payload); });
|
||||
shared->post([signal_send, payload]() { signal_send(payload); });
|
||||
});
|
||||
|
||||
// A member is connected once both its channels are open. The reliable one also answers pings and times pongs.
|
||||
// Binary messages on either are queued for the session.
|
||||
auto channel_watch = [this, shared, id](std::shared_ptr<rtc::DataChannel> channel) {
|
||||
auto watched = impl->links.find(id);
|
||||
if (watched == impl->links.end())
|
||||
{
|
||||
channel->close();
|
||||
return;
|
||||
}
|
||||
bool isReliable = channel->label() != INPUTS_LABEL;
|
||||
(isReliable ? watched->second.channel : watched->second.inputs) = channel;
|
||||
channel->onOpen([this, shared, id]() {
|
||||
shared->post([this, id]() {
|
||||
auto link = impl->links.find(id);
|
||||
NetMember* member = member_get(id);
|
||||
if (member && link != impl->links.end())
|
||||
member->isConnected = link->second.is_open();
|
||||
});
|
||||
});
|
||||
channel->onClosed([this, shared, id]() {
|
||||
shared->post([this, id]() {
|
||||
if (NetMember* member = member_get(id))
|
||||
member->isConnected = false;
|
||||
});
|
||||
});
|
||||
channel->onMessage([this, shared, id, isReliable](rtc::message_variant data) {
|
||||
rtc::DataChannelInit lossy;
|
||||
lossy.reliability.unordered = true;
|
||||
lossy.reliability.maxRetransmits = 0;
|
||||
link.channel = link.connection->createDataChannel(CHANNEL_LABEL);
|
||||
link.inputs = link.connection->createDataChannel(INPUTS_LABEL, lossy);
|
||||
for (auto& channel : {link.channel, link.inputs})
|
||||
channel->onMessage([this, shared](rtc::message_variant data) {
|
||||
if (std::holds_alternative<rtc::binary>(data))
|
||||
{
|
||||
rtc::binary& bytes = std::get<rtc::binary>(data);
|
||||
std::string packet(reinterpret_cast<const char*>(bytes.data()), bytes.size());
|
||||
shared->post([this, id, isReliable, packet]() {
|
||||
packets.push_back({.from = id, .isReliable = isReliable, .bytes = packet});
|
||||
});
|
||||
shared->post([this, packet]() { packets.push_back(packet); });
|
||||
return;
|
||||
}
|
||||
shared->post([this, id, text = std::get<std::string>(data)]() {
|
||||
auto link = impl->links.find(id);
|
||||
if (link == impl->links.end() || !link->second.channel || !link->second.channel->isOpen())
|
||||
std::string& text = std::get<std::string>(data);
|
||||
if (!text.starts_with("pong:"))
|
||||
return;
|
||||
if (text.starts_with("ping:"))
|
||||
text_send(id, "pong:" + text.substr(5));
|
||||
else if (text.starts_with("pong:"))
|
||||
if (NetMember* member = member_get(id))
|
||||
member->ping = (SDL_GetTicksNS() - SDL_strtoull(text.c_str() + 5, nullptr, 10)) / NS_PER_MS;
|
||||
float ping = (SDL_GetTicksNS() - SDL_strtoull(text.c_str() + 5, nullptr, 10)) / NS_PER_MS;
|
||||
shared->post([this, ping]() {
|
||||
if (NetMember* member = member_get(selfId))
|
||||
member->ping = ping;
|
||||
});
|
||||
});
|
||||
};
|
||||
if (isOfferer)
|
||||
{
|
||||
rtc::DataChannelInit unreliable;
|
||||
unreliable.reliability.unordered = true;
|
||||
unreliable.reliability.maxRetransmits = 0;
|
||||
channel_watch(link.connection->createDataChannel(CHANNEL_LABEL));
|
||||
channel_watch(link.connection->createDataChannel(INPUTS_LABEL, unreliable));
|
||||
}
|
||||
else
|
||||
link.connection->onDataChannel([shared, channel_watch](std::shared_ptr<rtc::DataChannel> channel) {
|
||||
shared->post([channel_watch, channel]() { channel_watch(channel); });
|
||||
});
|
||||
}
|
||||
|
||||
void Net::peer_close(int id)
|
||||
{
|
||||
auto link = impl->links.find(id);
|
||||
if (link == impl->links.end())
|
||||
return;
|
||||
link->second.connection->close();
|
||||
impl->links.erase(link);
|
||||
}
|
||||
|
||||
// A description or candidate from another member. An offer from someone not yet connected starts the connection on
|
||||
// this side.
|
||||
void Net::signal_handle(int from, const std::string& text)
|
||||
// A description or candidate from the server for this screen's connection.
|
||||
void Net::signal_handle(const std::string& text)
|
||||
{
|
||||
simdjson::dom::element payload;
|
||||
std::string_view sdp;
|
||||
std::string_view kind;
|
||||
std::string_view candidate;
|
||||
std::string_view mid;
|
||||
if (impl->parser.parse(text).get(payload) || !member_get(from))
|
||||
PeerLink& link = impl->link;
|
||||
if (impl->parser.parse(text).get(payload) || !link.connection)
|
||||
return;
|
||||
if (!payload["sdp"].get(sdp) && !payload["kind"].get(kind))
|
||||
{
|
||||
if (!impl->links.contains(from))
|
||||
peer_open(from, false);
|
||||
impl->links[from].connection->setRemoteDescription(rtc::Description(std::string(sdp), std::string(kind)));
|
||||
return;
|
||||
}
|
||||
if (!payload["candidate"].get(candidate) && !payload["mid"].get(mid) && impl->links.contains(from))
|
||||
impl->links[from].connection->addRemoteCandidate(rtc::Candidate(std::string(candidate), std::string(mid)));
|
||||
link.connection->setRemoteDescription(rtc::Description(std::string(sdp), std::string(kind)));
|
||||
else if (!payload["candidate"].get(candidate) && !payload["mid"].get(mid))
|
||||
link.connection->addRemoteCandidate(rtc::Candidate(std::string(candidate), std::string(mid)));
|
||||
}
|
||||
|
||||
// Sends a binary message to another member, if they're connected.
|
||||
void Net::send(int to, const std::string& bytes, bool isReliable)
|
||||
// Sends the server a game message, held back or dropped first by the network simulator if it's on.
|
||||
void Net::send(const std::string& bytes, bool isReliable)
|
||||
{
|
||||
if (impl->lag > 0 || impl->loss > 0)
|
||||
{
|
||||
@@ -405,36 +351,31 @@ void Net::send(int to, const std::string& bytes, bool isReliable)
|
||||
return;
|
||||
if (impl->lag > 0)
|
||||
{
|
||||
impl->delayed.push_back({SDL_GetTicks() + impl->lag, to, isReliable, bytes});
|
||||
impl->delayed.push_back({SDL_GetTicks() + impl->lag, isReliable, bytes});
|
||||
return;
|
||||
}
|
||||
}
|
||||
send_now(to, bytes, isReliable);
|
||||
send_now(bytes, isReliable);
|
||||
}
|
||||
|
||||
void Net::send_now(int to, const std::string& bytes, bool isReliable)
|
||||
// Over the data channels once they're open, or the socket until then.
|
||||
void Net::send_now(const std::string& bytes, bool isReliable)
|
||||
{
|
||||
auto link = impl->links.find(to);
|
||||
if (link == impl->links.end() || !link->second.is_open())
|
||||
return;
|
||||
auto& channel = isReliable ? link->second.channel : link->second.inputs;
|
||||
channel->send(reinterpret_cast<const rtc::byte*>(bytes.data()), bytes.size());
|
||||
auto data = reinterpret_cast<const rtc::byte*>(bytes.data());
|
||||
if (is_linked())
|
||||
(isReliable ? impl->link.channel : impl->link.inputs)->send(data, bytes.size());
|
||||
else if (impl->socket && impl->socket->isOpen() && state == NET_LOBBY)
|
||||
impl->socket->send(rtc::binary(data, data + bytes.size()));
|
||||
}
|
||||
|
||||
// A text message (pings and pongs) on the reliable channel, held back by the network simulator like everything else.
|
||||
void Net::text_send(int to, const std::string& text)
|
||||
// Times the round trip to the server over the reliable channel (the network simulator holds it back too).
|
||||
void Net::ping_send()
|
||||
{
|
||||
std::string text = "ping:" + std::to_string(SDL_GetTicksNS());
|
||||
if (impl->lag > 0)
|
||||
impl->delayed.push_back({SDL_GetTicks() + impl->lag, to, true, text, true});
|
||||
else
|
||||
text_send_now(to, text);
|
||||
}
|
||||
|
||||
void Net::text_send_now(int to, const std::string& text)
|
||||
{
|
||||
auto link = impl->links.find(to);
|
||||
if (link != impl->links.end() && link->second.channel && link->second.channel->isOpen())
|
||||
link->second.channel->send(text);
|
||||
impl->delayed.push_back({SDL_GetTicks() + impl->lag, true, text, true});
|
||||
else if (is_linked())
|
||||
impl->link.channel->send(text);
|
||||
}
|
||||
|
||||
// Asks the server to put a member out of the lobby (it only does so for the host).
|
||||
@@ -445,16 +386,13 @@ void Net::kick_send(int member, const std::string& reason)
|
||||
"}");
|
||||
}
|
||||
|
||||
// Whether a direct connection to every other member is open.
|
||||
bool Net::is_connected_all()
|
||||
// Whether the WebRTC connection to the server is up, with both its channels open.
|
||||
bool Net::is_linked()
|
||||
{
|
||||
for (NetMember& member : members)
|
||||
if (!member.isConnected)
|
||||
return false;
|
||||
return state == NET_LOBBY;
|
||||
return impl->link.is_open();
|
||||
}
|
||||
|
||||
// Once a frame: handles what the network reported (on desktop), pings every connected member, and on desktop keeps the
|
||||
// Once a frame: handles what the network reported (on desktop), pings the server over the link, and on desktop keeps the
|
||||
// server connection alive.
|
||||
void Net::update()
|
||||
{
|
||||
@@ -471,17 +409,15 @@ void Net::update()
|
||||
{
|
||||
Impl::Delayed due = impl->delayed.front();
|
||||
impl->delayed.erase(impl->delayed.begin());
|
||||
if (due.isText)
|
||||
text_send_now(due.to, due.bytes);
|
||||
else
|
||||
send_now(due.to, due.bytes, due.isReliable);
|
||||
if (due.isText && is_linked())
|
||||
impl->link.channel->send(due.bytes);
|
||||
else if (!due.isText)
|
||||
send_now(due.bytes, due.isReliable);
|
||||
}
|
||||
if (state == NET_LOBBY && now >= impl->nextPing)
|
||||
{
|
||||
impl->nextPing = now + PING_INTERVAL;
|
||||
for (auto& [id, link] : impl->links)
|
||||
if (link.channel && link.channel->isOpen())
|
||||
text_send(id, "ping:" + std::to_string(SDL_GetTicksNS()));
|
||||
ping_send();
|
||||
}
|
||||
#ifndef SDL_PLATFORM_EMSCRIPTEN
|
||||
if (now >= impl->nextHeartbeat)
|
||||
|
||||
@@ -15,27 +15,19 @@ enum NetState
|
||||
NET_LOBBY
|
||||
};
|
||||
|
||||
// A binary message from another member, on the reliable channel (in order, never lost) or the inputs one (either may
|
||||
// fail).
|
||||
struct NetPacket
|
||||
{
|
||||
int from = -1;
|
||||
bool isReliable = true;
|
||||
std::string bytes;
|
||||
};
|
||||
|
||||
// Someone in the lobby (this screen's player too): their server id and name, whether a direct connection to them is
|
||||
// open, and the round trip over it in milliseconds (-1 until measured). This screen's own member is always connected.
|
||||
// Someone in the lobby (this screen's player too): their server id and name, and their connection's round trip to the
|
||||
// server in milliseconds (-1 while it isn't known: this screen only measures its own).
|
||||
struct NetMember
|
||||
{
|
||||
int id = -1;
|
||||
std::string name;
|
||||
bool isConnected = false;
|
||||
float ping = -1;
|
||||
};
|
||||
|
||||
// Online play: a WebSocket to the server (see protocol.h) to make or join a lobby and pass signaling, and a
|
||||
// WebRTC data channel to each other member, connected directly. A newcomer connects out to everyone already there.
|
||||
// Online play: a WebSocket to the server (see protocol.h) to make or join a lobby, and through it a WebRTC connection
|
||||
// to the server to play over, with two data channels: a reliable, ordered one for everything that mustn't go missing,
|
||||
// and a lossy one for controls and frames, which are sent again until they're confirmed, so a lost one is never waited
|
||||
// on. Until both are open (or where they never open, with UDP blocked), the game's messages go over the socket.
|
||||
// The network calls back on its own threads (on desktop), so everything it reports is queued and handled in update,
|
||||
// once a frame on the main thread. Members are in the order they joined, so the first hosts. Error holds why the last
|
||||
// request failed or the connection was lost, until the next request.
|
||||
@@ -51,9 +43,9 @@ public:
|
||||
void update();
|
||||
void shutdown();
|
||||
void heartbeat();
|
||||
void send(int to, const std::string& bytes, bool isReliable);
|
||||
void send(const std::string& bytes, bool isReliable);
|
||||
void kick_send(int member, const std::string& reason);
|
||||
bool is_connected_all();
|
||||
bool is_linked();
|
||||
|
||||
NetState state = NET_OFFLINE;
|
||||
std::string code;
|
||||
@@ -61,8 +53,8 @@ public:
|
||||
int selfId = -1;
|
||||
int hostId = -1;
|
||||
std::vector<NetMember> members;
|
||||
// Binary messages from other members, oldest first, for the session to take.
|
||||
std::vector<NetPacket> packets;
|
||||
// Binary messages from the server, oldest first, for the session to take.
|
||||
std::vector<std::string> packets;
|
||||
// Lines for the chat log from the server (like this screen being kicked), oldest first, for the chat to take.
|
||||
std::vector<std::string> notices;
|
||||
// Members the server said were kicked, with the reason (empty for none given).
|
||||
@@ -73,12 +65,10 @@ private:
|
||||
std::unique_ptr<Impl> impl;
|
||||
|
||||
void request_send(Game& game, const std::string& request);
|
||||
void send_now(int to, const std::string& bytes, bool isReliable);
|
||||
void text_send(int to, const std::string& text);
|
||||
void text_send_now(int to, const std::string& text);
|
||||
void send_now(const std::string& bytes, bool isReliable);
|
||||
void ping_send();
|
||||
void message_handle(const std::string& text);
|
||||
void peer_open(int id, bool isOfferer);
|
||||
void peer_close(int id);
|
||||
void signal_handle(int from, const std::string& payload);
|
||||
void link_open();
|
||||
void signal_handle(const std::string& payload);
|
||||
NetMember* member_get(int id);
|
||||
};
|
||||
+14
-14
@@ -437,10 +437,10 @@ void Session::members_watch(Game& game)
|
||||
{
|
||||
Net& net = game.net;
|
||||
int self = net.selfId;
|
||||
if (!isJoined && !isRequested && net.is_connected_all() && net.hostId != self)
|
||||
if (!isJoined && !isRequested && net.is_linked() && net.hostId != self)
|
||||
{
|
||||
Profile profile = profile_get(game);
|
||||
net.send(net.hostId, PacketWriter().put(PEER_JOIN_REQUEST).put(profile).bytes, true);
|
||||
net.send(PacketWriter().put(PEER_JOIN_REQUEST).put(profile).bytes, true);
|
||||
isRequested = true;
|
||||
}
|
||||
if (!isJoined || net.hostId != self)
|
||||
@@ -460,10 +460,10 @@ void Session::members_watch(Game& game)
|
||||
|
||||
void Session::packets_take(Game& game)
|
||||
{
|
||||
std::vector<NetPacket> packets;
|
||||
std::vector<std::string> packets;
|
||||
packets.swap(game.net.packets);
|
||||
for (NetPacket& packet : packets)
|
||||
packet_handle(game, packet.from, packet.bytes, packet.isReliable);
|
||||
for (std::string& packet : packets)
|
||||
packet_handle(game, -1, packet, true);
|
||||
}
|
||||
|
||||
void Session::packet_handle(Game& game, int from, const std::string& bytes, bool isReliable)
|
||||
@@ -638,7 +638,7 @@ void Session::join_grant(Game& game, int member, const Profile& profile)
|
||||
PacketWriter packet;
|
||||
packet.put(PEER_STATE).put(from);
|
||||
packet.bytes += state;
|
||||
game.net.send(member, packet.bytes, true);
|
||||
game.net.send(packet.bytes, true);
|
||||
SDL_Log("Session: %d joins at tick %d, from tick %d (%zu bytes)", member, event.tick, from, state.size());
|
||||
}
|
||||
|
||||
@@ -668,7 +668,7 @@ void Session::event_send(Game& game, int to, const SessionEvent& event, int inpu
|
||||
packet.put(PEER_EVENT).put(event).put(inputsFrom).put((Uint16)inputs.size());
|
||||
for (Controls& controls : inputs)
|
||||
packet.put(controls);
|
||||
game.net.send(to, packet.bytes, true);
|
||||
game.net.send(packet.bytes, true);
|
||||
}
|
||||
|
||||
// Takes an event in: a join says when the newcomer's controls start counting and that they need this screen's from
|
||||
@@ -740,7 +740,7 @@ void Session::inputs_send(Game& game)
|
||||
packet.put(PEER_INPUTS).put(tick).put(logs[member].contiguous).put(first).put(count);
|
||||
for (int at = first; at < first + count; at++)
|
||||
packet.put(own.controls[at % INPUT_RING]);
|
||||
game.net.send(member, packet.bytes, false);
|
||||
game.net.send(packet.bytes, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -758,7 +758,7 @@ void Session::checksums_exchange(Game& game)
|
||||
packet.put(PEER_CHECKSUM).put(at).put(checksums[at % SNAPSHOT_RING]);
|
||||
for (auto& [member, peer] : peers)
|
||||
if (peer.isSending)
|
||||
game.net.send(member, packet.bytes, true);
|
||||
game.net.send(packet.bytes, true);
|
||||
}
|
||||
for (auto entry = remoteChecksums.begin(); entry != remoteChecksums.end();)
|
||||
{
|
||||
@@ -828,7 +828,7 @@ void Session::broadcast(Game& game, const std::string& bytes)
|
||||
{
|
||||
for (NetMember& member : game.net.members)
|
||||
if (member.id != game.net.selfId)
|
||||
game.net.send(member.id, bytes, true);
|
||||
game.net.send(bytes, true);
|
||||
}
|
||||
|
||||
// A chat message from this screen: to everyone (when there's anyone), and into this screen's own log.
|
||||
@@ -860,7 +860,7 @@ void Session::rename_send(Game& game, int to, bool isQuiet)
|
||||
if (to < 0)
|
||||
broadcast(game, packet.bytes);
|
||||
else
|
||||
game.net.send(to, packet.bytes, true);
|
||||
game.net.send(packet.bytes, true);
|
||||
}
|
||||
|
||||
// Notices this screen's name or colour changing (in Customize, or by a chat command). A new name goes to everyone once
|
||||
@@ -941,7 +941,7 @@ void Session::queue_send(Game& game, const Rules& rules)
|
||||
return;
|
||||
if (game.net.hostId == game.net.selfId)
|
||||
return queue_decide(game, game.net.selfId, true, rules);
|
||||
game.net.send(game.net.hostId, PacketWriter().put(PEER_QUEUE).put((Uint8)1).put(rules).bytes, true);
|
||||
game.net.send(PacketWriter().put(PEER_QUEUE).put((Uint8)1).put(rules).bytes, true);
|
||||
}
|
||||
|
||||
void Session::unqueue_send(Game& game)
|
||||
@@ -950,7 +950,7 @@ void Session::unqueue_send(Game& game)
|
||||
return;
|
||||
if (game.net.hostId == game.net.selfId)
|
||||
return queue_decide(game, game.net.selfId, false, {});
|
||||
game.net.send(game.net.hostId, PacketWriter().put(PEER_QUEUE).put((Uint8)0).put(Rules{}).bytes, true);
|
||||
game.net.send(PacketWriter().put(PEER_QUEUE).put((Uint8)0).put(Rules{}).bytes, true);
|
||||
}
|
||||
|
||||
// The host sets a queue change for a moment from now and tells everyone.
|
||||
@@ -1327,6 +1327,6 @@ float Session::ping_get(Game& game, int member)
|
||||
int to = member == net.selfId ? net.hostId : member;
|
||||
for (NetMember& netMember : net.members)
|
||||
if (netMember.id == to)
|
||||
return netMember.isConnected ? netMember.ping : -1;
|
||||
return netMember.ping;
|
||||
return PING_NONE;
|
||||
}
|
||||
Reference in new issue
Block a user