MXVK Vulkan Framework 0.24.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) {
17 return std::string(name.data(), strnlen(name.data(), name.size()));
18 }
19
20 void set_packet_name(std::array<char, 32> &output, const std::string &name) {
21 const std::size_t size = std::min(name.size(), output.size() - 1U);
22 std::memcpy(output.data(), name.data(), size);
23 output[size] = '\0';
24 }
25
26 std::string random_join_code() {
27 static constexpr char ALPHABET[] = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
28 std::random_device random_device;
29 std::uniform_int_distribution<std::size_t> distribution(0, sizeof(ALPHABET) - 2U);
30 std::string code(8, 'A');
31 for (char &character : code)
32 character = ALPHABET[distribution(random_device)];
33 return code;
34 }
35 } // namespace
36
37 void MultiplayerSession::host(const std::string &port, const std::string &player_name) {
38 stop();
40 socket.bind(port);
41 if (!socket.setblocking(false)) {
42 stop();
43 throw mxnetwork::Exception("Could not make the host UDP socket nonblocking.");
44 }
45 port_mapping = std::make_unique<PortMapping>();
46 port_mapping->open(port);
47 session_active = true;
48 host_role = true;
49 assigned_player_id = 0;
50 local_player_name = player_name;
51 names[0] = player_name;
52 connected_players[0] = true;
53 peers[0].connected = true;
54 peers[0].name = player_name;
55 session_join_code = random_join_code();
56 }
57
58 void MultiplayerSession::join(const std::string &address, const std::string &port, const std::string &player_name, const std::string &join_code) {
59 stop();
61 if (!socket.connect(address, port)) {
62 stop();
63 throw mxnetwork::Exception("Could not connect the UDP socket to the host.");
64 }
65 if (!socket.setblocking(false)) {
66 stop();
67 throw mxnetwork::Exception("Could not make the client UDP socket nonblocking.");
68 }
69 session_active = true;
70 host_role = false;
71 local_player_name = player_name;
72 session_join_code = join_code;
73 }
74
76 port_mapping.reset();
77 if (socket.valid()) {
78 socket.close();
79 }
80 socket = mxnetwork::Socket{};
81 session_active = false;
82 host_role = false;
83 send_accumulator = 0.0f;
84 send_sequence = 0;
85 assigned_player_id = 0xFFU;
86 local_player_name.clear();
87 session_join_code.clear();
88 peers = {};
89 names = {};
90 connected_players = {};
91 }
92
93 NetworkExchange MultiplayerSession::exchange(const NetworkState &local_state, float delta_time) {
94 NetworkExchange result{};
95 if (!session_active) {
96 return result;
97 }
98
99 if (assigned_player_id < NETWORK_PLAYER_COUNT) {
100 peers[assigned_player_id].state = local_state;
101 }
102 if (host_role) {
103 for (std::uint8_t player = 1; player < NETWORK_PLAYER_COUNT; ++player) {
104 if (!peers[player].connected)
105 continue;
106 peers[player].idle_seconds += delta_time;
107 if (peers[player].idle_seconds > 5.0f) {
108 peers[player] = {};
109 connected_players[player] = false;
110 names[player].clear();
111 }
112 }
113 } else if (assigned_player_id < NETWORK_PLAYER_COUNT && connected_players[0]) {
114 peers[0].idle_seconds += delta_time;
115 if (peers[0].idle_seconds > 5.0f) {
116 for (std::uint8_t player = 0; player < NETWORK_PLAYER_COUNT; ++player) {
117 if (player != assigned_player_id) {
118 connected_players[player] = false;
119 }
120 }
121 }
122 }
123
124 while (1) {
125 Packet packet{};
127 const ssize_t received = host_role ? socket.recvfrom(&packet, sizeof(packet), sender) : socket.recvfrom(&packet, sizeof(packet));
128 if (received < 0) {
129 break;
130 }
131 if (received != static_cast<ssize_t>(sizeof(packet)) || packet.magic != Packet{}.magic ||
132 packet.version != Packet{}.version || packet.size != sizeof(Packet)) {
133 continue;
134 }
135
136 if (host_role) {
137 const std::string supplied_code(packet.join_code.data(), strnlen(packet.join_code.data(), packet.join_code.size()));
138 if (supplied_code != session_join_code) {
139 continue;
140 }
141 const std::optional<std::uint8_t> player = find_or_assign_peer(sender);
142 if (!player.has_value()) {
143 continue;
144 }
145 Peer &peer = peers[*player];
146 if (packet.sequence <= peer.receive_sequence && peer.receive_sequence != 0) {
147 continue;
148 }
149 peer.receive_sequence = packet.sequence;
150 peer.idle_seconds = 0.0f;
151 peer.state = packet.state;
152 peer.name = packet_name(packet.player_name);
153 names[*player] = peer.name;
154 connected_players[*player] = true;
155 result.players.push_back({*player, packet.state});
156 } else {
157 peers[0].idle_seconds = 0.0f;
158 for (std::uint8_t player = 0; player < NETWORK_PLAYER_COUNT; ++player) {
159 connected_players[player] = (packet.connected_mask & (1U << player)) != 0U;
160 }
161 if (packet.assigned_player_id < NETWORK_PLAYER_COUNT) {
162 assigned_player_id = packet.assigned_player_id;
163 names[assigned_player_id] = local_player_name;
164 connected_players[assigned_player_id] = true;
165 }
166 if (packet.player_id < NETWORK_PLAYER_COUNT) {
167 names[packet.player_id] = packet_name(packet.player_name);
168 connected_players[packet.player_id] = true;
169 result.players.push_back({packet.player_id, packet.state});
170 }
171 }
172 }
173
174 send_accumulator += delta_time;
175 if (send_accumulator >= (1.0f / 30.0f)) {
176 send_accumulator = 0.0f;
177 if (host_role) {
178 for (std::uint8_t destination = 1; destination < NETWORK_PLAYER_COUNT; ++destination) {
179 if (!peers[destination].connected) {
180 continue;
181 }
182 for (std::uint8_t source = 0; source < NETWORK_PLAYER_COUNT; ++source) {
183 if (peers[source].connected) {
184 send_host_relay(source, destination);
185 }
186 }
187 }
188 } else {
189 send_client_state(local_state);
190 }
191 } else if (!host_role && assigned_player_id == 0xFFU) {
192 send_client_state(local_state);
193 }
194 return result;
195 }
196
197 bool MultiplayerSession::active() const { return session_active; }
198 bool MultiplayerSession::connected() const { return player_count() > 1; }
199 bool MultiplayerSession::is_host() const { return host_role; }
200 std::uint8_t MultiplayerSession::local_player_id() const { return assigned_player_id; }
201
203 return static_cast<std::size_t>(std::count(connected_players.begin(), connected_players.end(), true));
204 }
205
206 const std::array<std::string, NETWORK_PLAYER_COUNT> &MultiplayerSession::player_names() const { return names; }
207 const std::array<bool, NETWORK_PLAYER_COUNT> &MultiplayerSession::player_connected() const { return connected_players; }
208 const std::string &MultiplayerSession::join_code() const { return session_join_code; }
209
210 const std::string &MultiplayerSession::port_mapping_status() const {
211 static const std::string NO_MAPPING = "Automatic router mapping unavailable; forward the UDP port manually.";
212 return port_mapping ? port_mapping->status() : NO_MAPPING;
213 }
214
215 void MultiplayerSession::send_client_state(const NetworkState &state) {
216 Packet packet{};
217 packet.sequence = ++send_sequence;
218 packet.player_id = assigned_player_id;
219 packet.state = state;
220 set_packet_name(packet.player_name, local_player_name);
221 const std::size_t code_size = std::min(session_join_code.size(), packet.join_code.size() - 1U);
222 std::memcpy(packet.join_code.data(), session_join_code.data(), code_size);
223 packet.join_code[code_size] = '\0';
224 socket.sendto(&packet, sizeof(packet));
225 }
226
227 void MultiplayerSession::send_host_relay(std::uint8_t source_player, std::uint8_t destination_player) {
228 Packet packet{};
229 packet.sequence = ++send_sequence;
230 packet.player_id = source_player;
231 packet.assigned_player_id = destination_player;
232 for (std::uint8_t player = 0; player < NETWORK_PLAYER_COUNT; ++player) {
233 if (connected_players[player])
234 packet.connected_mask |= static_cast<std::uint8_t>(1U << player);
235 }
236 packet.state = peers[source_player].state;
237 set_packet_name(packet.player_name, names[source_player]);
238 socket.sendto(&packet, sizeof(packet), peers[destination_player].address);
239 }
240
241 std::optional<std::uint8_t> MultiplayerSession::find_or_assign_peer(const mxnetwork::SocketAddress &address) {
242 for (std::uint8_t player = 1; player < NETWORK_PLAYER_COUNT; ++player) {
243 if (peers[player].connected && peers[player].address == address) {
244 return player;
245 }
246 }
247 for (std::uint8_t player = 1; player < NETWORK_PLAYER_COUNT; ++player) {
248 if (!peers[player].connected) {
249 peers[player].connected = true;
250 peers[player].address = address;
251 connected_players[player] = true;
252 return player;
253 }
254 }
255 return std::nullopt;
256 }
257
258} // 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.