17 return std::string(name.data(), strnlen(name.data(), name.size()));
21 const std::size_t size = std::min(name.size(), output.size() - 1U);
22 std::memcpy(output.data(), name.data(), size);
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)];
41 if (!socket.setblocking(
false)) {
45 port_mapping = std::make_unique<PortMapping>();
46 port_mapping->open(port);
47 session_active =
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();
61 if (!socket.connect(address, port)) {
65 if (!socket.setblocking(
false)) {
69 session_active =
true;
71 local_player_name = player_name;
81 session_active =
false;
83 send_accumulator = 0.0f;
85 assigned_player_id = 0xFFU;
86 local_player_name.clear();
87 session_join_code.clear();
90 connected_players = {};
95 if (!session_active) {
100 peers[assigned_player_id].state = local_state;
106 peers[player].idle_seconds += delta_time;
107 if (peers[player].idle_seconds > 5.0f) {
109 connected_players[player] =
false;
110 names[player].clear();
114 peers[0].idle_seconds += delta_time;
115 if (peers[0].idle_seconds > 5.0f) {
117 if (player != assigned_player_id) {
118 connected_players[player] =
false;
127 const ssize_t received = host_role ? socket.recvfrom(&packet,
sizeof(packet), sender) : socket.recvfrom(&packet,
sizeof(packet));
131 if (received !=
static_cast<ssize_t
>(
sizeof(packet)) || packet.magic != Packet{}.magic ||
132 packet.version != Packet{}.version || packet.size !=
sizeof(Packet)) {
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) {
141 const std::optional<std::uint8_t> player = find_or_assign_peer(sender);
142 if (!player.has_value()) {
145 Peer &peer = peers[*player];
146 if (packet.sequence <= peer.receive_sequence && peer.receive_sequence != 0) {
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});
157 peers[0].idle_seconds = 0.0f;
159 connected_players[player] = (packet.connected_mask & (1U << player)) != 0U;
162 assigned_player_id = packet.assigned_player_id;
163 names[assigned_player_id] = local_player_name;
164 connected_players[assigned_player_id] =
true;
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});
174 send_accumulator += delta_time;
175 if (send_accumulator >= (1.0f / 30.0f)) {
176 send_accumulator = 0.0f;
184 send_host_relay(source, destination);
189 send_client_state(local_state);
191 }
else if (!host_role && assigned_player_id == 0xFFU) {
192 send_client_state(local_state);
203 return static_cast<std::size_t
>(std::count(connected_players.begin(), connected_players.end(),
true));
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;
215 void MultiplayerSession::send_client_state(
const NetworkState &state) {
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));
227 void MultiplayerSession::send_host_relay(std::uint8_t source_player, std::uint8_t destination_player) {
229 packet.sequence = ++send_sequence;
230 packet.player_id = source_player;
231 packet.assigned_player_id = destination_player;
233 if (connected_players[player])
234 packet.connected_mask |=
static_cast<std::uint8_t
>(1U << player);
236 packet.state = peers[source_player].state;
238 socket.sendto(&packet,
sizeof(packet), peers[destination_player].address);
241 std::optional<std::uint8_t> MultiplayerSession::find_or_assign_peer(
const mxnetwork::SocketAddress &address) {
243 if (peers[player].
connected && peers[player].address == address) {
249 peers[player].connected =
true;
250 peers[player].address = address;
251 connected_players[player] =
true;
Lightweight exception wrapper for MXNetwork failures.
C++ wrapper around the MXNetwork socket API.
ssize_t sendto(const void *buf, size_t bytes)
Send a datagram using the configured socket type.
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.
void set_packet_name(std::array< char, 32 > &output, const std::string &name)
std::string packet_name(const std::array< char, 32 > &name)
std::string random_join_code()
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.