diff --git a/server/server.cpp b/server/server.cpp index 7aaa09d..492d689 100644 --- a/server/server.cpp +++ b/server/server.cpp @@ -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 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 socket; + std::shared_ptr connection; + std::shared_ptr channel; + std::shared_ptr inputs; int ack = -1; int lead = 0; int room = 0; @@ -78,11 +92,13 @@ struct Lobby std::vector 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 socket; + std::shared_ptr channel; int id = 0; bool isBinary = false; bool isClosed = false; @@ -97,27 +113,99 @@ 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; -// 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(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(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) + "}"); @@ -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 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; } diff --git a/src/menu.cpp b/src/menu.cpp index 1d97fcf..13f70c3 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -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; } diff --git a/src/net.cpp b/src/net.cpp index f413cc0..67dfa41 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -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 connection; @@ -35,7 +33,7 @@ struct PeerLink struct Net::Impl { std::shared_ptr socket; - std::map 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(data)) + { shared->post([this, text = std::get(data)]() { message_handle(text); }); + return; + } + rtc::binary& bytes = std::get(data); + std::string packet(reinterpret_cast(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(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 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(data)) { rtc::binary& bytes = std::get(data); std::string packet(reinterpret_cast(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(data)]() { - auto link = impl->links.find(id); - if (link == impl->links.end() || !link->second.channel || !link->second.channel->isOpen()) - 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; + std::string& text = std::get(data); + if (!text.starts_with("pong:")) + return; + 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 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(bytes.data()), bytes.size()); + auto data = reinterpret_cast(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) diff --git a/src/net.h b/src/net.h index 1b85fbe..c430830 100644 --- a/src/net.h +++ b/src/net.h @@ -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 members; - // Binary messages from other members, oldest first, for the session to take. - std::vector packets; + // Binary messages from the server, oldest first, for the session to take. + std::vector packets; // Lines for the chat log from the server (like this screen being kicked), oldest first, for the chat to take. std::vector notices; // Members the server said were kicked, with the reason (empty for none given). @@ -73,12 +65,10 @@ private: std::unique_ptr 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); }; diff --git a/src/session.cpp b/src/session.cpp index 9c673c2..597bcd7 100644 --- a/src/session.cpp +++ b/src/session.cpp @@ -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 packets; + std::vector 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; }