MXVK Vulkan Framework 0.35.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
multiplayer.cpp
Go to the documentation of this file.
1#include "multiplayer.hpp"
2#include "port_mapping.hpp"
3
5
6#include <algorithm>
7#include <cstring>
8#include <random>
9
10namespace space {
11
14
15 namespace {
16 std::string packet_name(const std::array<char, 32> &name) { return std::string(name.data(), strnlen(name.data(), name.size())); }
17
18 void set_packet_name(std::array<char, 32> &output, const std::string &name) {
19 const std::size_t size = std::min(name.size(), output.size() - 1U);
20 std::memcpy(output.data(), name.data(), size);
21 output[size] = '\0';
22 }
23
24 std::string random_join_code() {
25 static constexpr char ALPHABET[] = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
26 std::random_device random_device;
27 std::uniform_int_distribution<std::size_t> distribution(0, sizeof(ALPHABET) - 2U);
28 std::string code(8, 'A');
29 for (char &character : code)
30 character = ALPHABET[distribution(random_device)];
31 return code;
32 }
33 } // namespace
34
35 void MultiplayerSession::host(const std::string &port, const std::string &player_name) {
36 stop();
38 socket.bind(port);
39 if (!socket.setblocking(false)) {
40 stop();
41 throw mxnetwork::Exception("Could not make the host UDP socket nonblocking.");
42 }
43 port_mapping = std::make_unique<PortMapping>();
44 port_mapping->open(port);
45 session_active = true;
46 host_role = true;
47 assigned_player_id = 0;
48 local_player_name = player_name;
49 names[0] = player_name;
50 connected_players[0] = true;
51 peers[0].connected = true;
52 peers[0].name = player_name;
53 session_join_code = random_join_code();
54 }
55
56 void MultiplayerSession::join(const std::string &address, const std::string &port, const std::string &player_name, const std::string &join_code) {
57 stop();
59 if (!socket.connect(address, port)) {
60 stop();
61 throw mxnetwork::Exception("Could not connect the UDP socket to the host.");
62 }
63 if (!socket.setblocking(false)) {
64 stop();
65 throw mxnetwork::Exception("Could not make the client UDP socket nonblocking.");
66 }
67 session_active = true;
68 host_role = false;
69 local_player_name = player_name;
70 session_join_code = join_code;
71 }
72
74 port_mapping.reset();
75 if (socket.valid()) {
76 socket.close();
77 }
78 socket = mxnetwork::Socket{};
79 session_active = false;
80 host_role = false;
81 send_accumulator = 0.0f;
82 send_sequence = 0;
83 assigned_player_id = 0xFFU;
84 local_player_name.clear();
85 session_join_code.clear();
86 peers = {};
87 names = {};
88 connected_players = {};
89 }
90
91 NetworkExchange MultiplayerSession::exchange(const NetworkState &local_state, float delta_time) {
92 NetworkExchange result{};
93 if (!session_active) {
94 return result;
95 }
96
97 if (assigned_player_id < NETWORK_PLAYER_COUNT) {
98 peers[assigned_player_id].state = local_state;
99 }
100 if (host_role) {
101 for (std::uint8_t player = 1; player < NETWORK_PLAYER_COUNT; ++player) {
102 if (!peers[player].connected)
103 continue;
104 peers[player].idle_seconds += delta_time;
105 if (peers[player].idle_seconds > 5.0f) {
106 peers[player] = {};
107 connected_players[player] = false;
108 names[player].clear();
109 }
110 }
111 } else if (assigned_player_id < NETWORK_PLAYER_COUNT && connected_players[0]) {
112 peers[0].idle_seconds += delta_time;
113 if (peers[0].idle_seconds > 5.0f) {
114 for (std::uint8_t player = 0; player < NETWORK_PLAYER_COUNT; ++player) {
115 if (player != assigned_player_id) {
116 connected_players[player] = false;
117 }
118 }
119 }
120 }
121
122 while (1) {
123 Packet packet{};
125 const ssize_t received = host_role ? socket.recvfrom(&packet, sizeof(packet), sender) : socket.recvfrom(&packet, sizeof(packet));
126 if (received < 0) {
127 break;
128 }
129 if (received != static_cast<ssize_t>(sizeof(packet)) || packet.magic != Packet{}.magic || packet.version != Packet{}.version || packet.size != sizeof(Packet)) {
130 continue;
131 }
132
133 if (host_role) {
134 const std::string supplied_code(packet.join_code.data(), strnlen(packet.join_code.data(), packet.join_code.size()));
135 if (supplied_code != session_join_code) {
136 continue;
137 }
138 const std::optional<std::uint8_t> player = find_or_assign_peer(sender);
139 if (!player.has_value()) {
140 continue;
141 }
142 Peer &peer = peers[*player];
143 if (packet.sequence <= peer.receive_sequence && peer.receive_sequence != 0) {
144 continue;
145 }
146 peer.receive_sequence = packet.sequence;
147 peer.idle_seconds = 0.0f;
148 peer.state = packet.state;
149 peer.name = packet_name(packet.player_name);
150 names[*player] = peer.name;
151 connected_players[*player] = true;
152 result.players.push_back({*player, packet.state});
153 } else {
154 peers[0].idle_seconds = 0.0f;
155 for (std::uint8_t player = 0; player < NETWORK_PLAYER_COUNT; ++player) {
156 connected_players[player] = (packet.connected_mask & (1U << player)) != 0U;
157 }
158 if (packet.assigned_player_id < NETWORK_PLAYER_COUNT) {
159 assigned_player_id = packet.assigned_player_id;
160 names[assigned_player_id] = local_player_name;
161 connected_players[assigned_player_id] = true;
162 }
163 if (packet.player_id < NETWORK_PLAYER_COUNT) {
164 names[packet.player_id] = packet_name(packet.player_name);
165 connected_players[packet.player_id] = true;
166 result.players.push_back({packet.player_id, packet.state});
167 }
168 }
169 }
170
171 send_accumulator += delta_time;
172 if (send_accumulator >= (1.0f / 30.0f)) {
173 send_accumulator = 0.0f;
174 if (host_role) {
175 for (std::uint8_t destination = 1; destination < NETWORK_PLAYER_COUNT; ++destination) {
176 if (!peers[destination].connected) {
177 continue;
178 }
179 for (std::uint8_t source = 0; source < NETWORK_PLAYER_COUNT; ++source) {
180 if (peers[source].connected) {
181 send_host_relay(source, destination);
182 }
183 }
184 }
185 } else {
186 send_client_state(local_state);
187 }
188 } else if (!host_role && assigned_player_id == 0xFFU) {
189 send_client_state(local_state);
190 }
191 return result;
192 }
193
194 bool MultiplayerSession::active() const { return session_active; }
195 bool MultiplayerSession::connected() const { return player_count() > 1; }
196 bool MultiplayerSession::is_host() const { return host_role; }
197 std::uint8_t MultiplayerSession::local_player_id() const { return assigned_player_id; }
198
199 std::size_t MultiplayerSession::player_count() const { return static_cast<std::size_t>(std::count(connected_players.begin(), connected_players.end(), true)); }
200
201 const std::array<std::string, NETWORK_PLAYER_COUNT> &MultiplayerSession::player_names() const { return names; }
202 const std::array<bool, NETWORK_PLAYER_COUNT> &MultiplayerSession::player_connected() const { return connected_players; }
203 const std::string &MultiplayerSession::join_code() const { return session_join_code; }
204
205 const std::string &MultiplayerSession::port_mapping_status() const {
206 static const std::string NO_MAPPING = "Automatic router mapping unavailable; forward the UDP port manually.";
207 return port_mapping ? port_mapping->status() : NO_MAPPING;
208 }
209
210 void MultiplayerSession::send_client_state(const NetworkState &state) {
211 Packet packet{};
212 packet.sequence = ++send_sequence;
213 packet.player_id = assigned_player_id;
214 packet.state = state;
215 set_packet_name(packet.player_name, local_player_name);
216 const std::size_t code_size = std::min(session_join_code.size(), packet.join_code.size() - 1U);
217 std::memcpy(packet.join_code.data(), session_join_code.data(), code_size);
218 packet.join_code[code_size] = '\0';
219 socket.sendto(&packet, sizeof(packet));
220 }
221
222 void MultiplayerSession::send_host_relay(std::uint8_t source_player, std::uint8_t destination_player) {
223 Packet packet{};
224 packet.sequence = ++send_sequence;
225 packet.player_id = source_player;
226 packet.assigned_player_id = destination_player;
227 for (std::uint8_t player = 0; player < NETWORK_PLAYER_COUNT; ++player) {
228 if (connected_players[player])
229 packet.connected_mask |= static_cast<std::uint8_t>(1U << player);
230 }
231 packet.state = peers[source_player].state;
232 set_packet_name(packet.player_name, names[source_player]);
233 socket.sendto(&packet, sizeof(packet), peers[destination_player].address);
234 }
235
236 std::optional<std::uint8_t> MultiplayerSession::find_or_assign_peer(const mxnetwork::SocketAddress &address) {
237 for (std::uint8_t player = 1; player < NETWORK_PLAYER_COUNT; ++player) {
238 if (peers[player].connected && peers[player].address == address) {
239 return player;
240 }
241 }
242 for (std::uint8_t player = 1; player < NETWORK_PLAYER_COUNT; ++player) {
243 if (!peers[player].connected) {
244 peers[player].connected = true;
245 peers[player].address = address;
246 connected_players[player] = true;
247 return player;
248 }
249 }
250 return std::nullopt;
251 }
252
253} // namespace space
Lightweight exception wrapper for MXNetwork failures.
Definition exception.hpp:11
C++ wrapper around the MXNetwork socket API.
Definition socket.hpp:57
ssize_t sendto(const void *buf, size_t bytes)
Send a datagram using the configured socket type.
Definition socket.cpp:207
MultiplayerSession()
Initializes the networking subsystem.
void host(const std::string &port, const std::string &player_name)
Starts hosting a session.
~MultiplayerSession()
Stops the session and releases networking resources.
void join(const std::string &address, const std::string &port, const std::string &player_name, const std::string &join_code)
Joins a hosted session.
bool is_host() const
Returns whether this session is the host.
NetworkExchange exchange(const NetworkState &local_state, float delta_time)
Sends local state and receives remote updates.
bool connected() const
Returns whether the local player has an established peer connection.
const std::array< bool, NETWORK_PLAYER_COUNT > & player_connected() const
Returns connection flags indexed by player slot.
void stop()
Stops the active host or client session.
bool active() const
Returns whether a host or client session is active.
std::uint8_t local_player_id() const
Returns the assigned local player slot.
std::size_t player_count() const
Returns the number of connected players.
const std::string & port_mapping_status() const
Returns the automatic router port-mapping status.
const std::array< std::string, NETWORK_PLAYER_COUNT > & player_names() const
Returns display names indexed by player slot.
const std::string & join_code() const
Returns the session join code.
UDP multiplayer state exchange for Asteroids.
@ TYPE_INET_DGRAM
IPv4 datagram socket.
Definition socket.hpp:47
void set_packet_name(std::array< char, 32 > &output, const std::string &name)
std::string packet_name(const std::array< char, 32 > &name)
constexpr std::size_t NETWORK_PLAYER_COUNT
Maximum number of players in a session.
Automatic UDP port mapping for hosted multiplayer sessions.
Collection of remote player updates produced by one exchange tick.
std::vector< NetworkPlayerUpdate > players
Updates received during the tick.
Complete replicated gameplay state published by one player.