Skip to content

Commit

Permalink
feat: transfer player names between clients
Browse files Browse the repository at this point in the history
  • Loading branch information
mgerhold committed Nov 14, 2024
1 parent c19f2a3 commit 634eeb0
Show file tree
Hide file tree
Showing 8 changed files with 308 additions and 44 deletions.
3 changes: 2 additions & 1 deletion src/network/include/network/message_types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
#include <cstdint>

enum class MessageType : std::uint8_t {
Heartbeat = 0,
Connect,
Heartbeat,
GridState,
GameStart,
StateBroadcast,
Expand Down
57 changes: 49 additions & 8 deletions src/network/include/network/messages.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,25 @@ struct AbstractMessage {
[[nodiscard]] virtual bool equals(AbstractMessage const& other) const = 0;
};

static constexpr auto player_name_buffer_size = usize{ 32 };

struct Connect final : AbstractMessage {
std::string player_name;

explicit Connect(std::string_view player_name);

[[nodiscard]] static constexpr decltype(MessageHeader::payload_size) max_payload_size() {
return static_cast<decltype(MessageHeader::payload_size)>(player_name_buffer_size);
}

[[nodiscard]] MessageType type() const override;
[[nodiscard]] decltype(MessageHeader::payload_size) payload_size() const override;
[[nodiscard]] c2k::MessageBuffer serialize() const override;
[[nodiscard]] static Connect deserialize(c2k::MessageBuffer& buffer);

[[nodiscard]] bool equals(AbstractMessage const& other) const override;
};

struct Heartbeat final : AbstractMessage {
public:
std::uint64_t frame;
Expand Down Expand Up @@ -107,44 +126,66 @@ struct GridState final : AbstractMessage {
}
};

struct ClientIdentity final {
u8 client_id;
std::string player_name;

ClientIdentity(u8 const client_id, std::string player_name)
: client_id{ client_id }, player_name{ std::move(player_name) } {}

[[nodiscard]] bool operator==(ClientIdentity const& other) const = default;
};

struct GameStart final : AbstractMessage {
std::uint8_t client_id;
std::uint64_t start_frame;
std::uint64_t random_seed;
std::uint8_t num_players;
std::vector<ClientIdentity> client_identities;

GameStart(
std::uint8_t const client_id,
std::uint64_t const start_frame,
std::uint64_t const random_seed,
std::uint8_t const num_players
std::vector<ClientIdentity> client_identities
)
: client_id{ client_id }, start_frame{ start_frame }, random_seed{ random_seed }, num_players{ num_players } {}
: client_id{ client_id },
start_frame{ start_frame },
random_seed{ random_seed },
client_identities{ std::move(client_identities) } {
if (this->client_identities.size() > std::numeric_limits<u8>::max()) {
throw std::invalid_argument{ "Number of clients is too high." };
}
}

[[nodiscard]] MessageType type() const override;
[[nodiscard]] decltype(MessageHeader::payload_size) payload_size() const override;
[[nodiscard]] c2k::MessageBuffer serialize() const override;
[[nodiscard]] static GameStart deserialize(c2k::MessageBuffer& buffer);

[[nodiscard]] static constexpr decltype(MessageHeader::payload_size) max_payload_size() {
return calculate_payload_size();
return calculate_payload_size(std::numeric_limits<u8>::max());
}

[[nodiscard]] u8 num_players() const {
return gsl::narrow<u8>(client_identities.size());
}

private:
[[nodiscard]] static constexpr decltype(MessageHeader::payload_size) calculate_payload_size() {
[[nodiscard]] static constexpr decltype(MessageHeader::payload_size) calculate_payload_size(u8 const num_players) {
return static_cast<decltype(MessageHeader::payload_size)>(
sizeof(client_id) + sizeof(start_frame) + sizeof(random_seed) + sizeof(num_players)
sizeof(client_id) + sizeof(start_frame) + sizeof(random_seed) + sizeof(u8) /* num players */
+ num_players * (sizeof(ClientIdentity::client_id) + player_name_buffer_size)
);
}

[[nodiscard]] bool equals(AbstractMessage const& other) const override {
auto const& other_game_start = static_cast<decltype(*this)&>(other);
return std::tie(client_id, start_frame, random_seed, num_players)
return std::tie(client_id, start_frame, random_seed, client_identities)
== std::tie(
other_game_start.client_id,
other_game_start.start_frame,
other_game_start.random_seed,
other_game_start.num_players
other_game_start.client_identities
);
}
};
Expand Down
124 changes: 114 additions & 10 deletions src/network/messages.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#include <cassert>
#include <cctype>
#include <chrono>
#include <limits>
#include <network/messages.hpp>
Expand All @@ -25,6 +26,8 @@

auto const message_max_payload_size = [message_type] {
switch (message_type) {
case MessageType::Connect:
return Connect::max_payload_size();
case MessageType::Heartbeat:
return Heartbeat::max_payload_size();
case MessageType::GridState:
Expand Down Expand Up @@ -70,6 +73,8 @@

try {
switch (message_type) {
case MessageType::Connect:
return std::make_unique<Connect>(Connect::deserialize(buffer));
case MessageType::Heartbeat:
return std::make_unique<Heartbeat>(Heartbeat::deserialize(buffer));
case MessageType::GridState:
Expand All @@ -87,6 +92,70 @@
std::unreachable();
}

[[nodiscard]] static std::string sanitize(std::string_view const player_name) {
auto sanitized = std::string{};
auto const max_length = std::min(player_name_buffer_size - 1, player_name.length());
for (auto i = usize{ 0 }; i < max_length; ++i) {
auto const c = player_name.at(i);
if (not std::isprint(static_cast<unsigned char>(c))) {
sanitized += '?';
} else {
sanitized += c;
}
}
assert(sanitized.length() < player_name_buffer_size - 1);
return sanitized;
}

Connect::Connect(std::string_view const player_name)
: player_name{ sanitize(player_name) } {}

[[nodiscard]] MessageType Connect::type() const {
return MessageType::Connect;
}

decltype(MessageHeader::payload_size) Connect::payload_size() const {
return max_payload_size();
}

[[nodiscard]] c2k::MessageBuffer Connect::serialize() const {
auto buffer = c2k::MessageBuffer{};
buffer << static_cast<u8>(MessageType::Connect) << payload_size();
auto const expected_message_size = buffer.size() + player_name_buffer_size;
for (auto const c : player_name) {
buffer << c;
}
while (buffer.size() < expected_message_size) {
buffer << '\0';
}
assert(buffer.size() == expected_message_size);
return buffer;
}

[[nodiscard]] Connect Connect::deserialize(c2k::MessageBuffer& buffer) {
static constexpr auto required_num_bytes = max_payload_size(); // Message has a fixed size.
if (buffer.size() < required_num_bytes) {
throw MessageDeserializationError{ std::format(
"too few bytes to deserialize Connect message ({} needed, {} received)",
required_num_bytes,
buffer.size()
) };
}
auto player_name = std::string{};
while (not buffer.size() == 0) {
auto const c = buffer.try_extract<char>().value();
if (c == '\0') {
break;
}
player_name += c;
}
return Connect{ sanitize(std::move(player_name)) };
}

[[nodiscard]] bool Connect::equals(AbstractMessage const& other) const {
return other.type() == type() and dynamic_cast<Connect const&>(other).player_name == player_name;
}

[[nodiscard]] MessageType Heartbeat::type() const {
return MessageType::Heartbeat;
}
Expand Down Expand Up @@ -160,19 +229,31 @@
}

[[nodiscard]] decltype(MessageHeader::payload_size) GameStart::payload_size() const {
return calculate_payload_size();
return calculate_payload_size(gsl::narrow<u8>(client_identities.size()));
}

[[nodiscard]] c2k::MessageBuffer GameStart::serialize() const {
auto buffer = c2k::MessageBuffer{};
// clang-format off
buffer << static_cast<std::uint8_t>(MessageType::GameStart)
<< payload_size()
<< client_id
<< start_frame
<< random_seed
<< num_players;
buffer << static_cast<std::uint8_t>(MessageType::GameStart)
<< payload_size()
<< client_id
<< start_frame
<< random_seed
<< gsl::narrow<u8>(client_identities.size());
// clang-format on
for (auto const& [other_client_id, player_name] : client_identities) {
buffer << other_client_id;
auto num_bytes = usize{ 0 };
for (auto const c : player_name) {
buffer << c;
++num_bytes;
}
while (num_bytes < player_name_buffer_size) {
buffer << '\0';
++num_bytes;
}
}
assert(buffer.size() == payload_size() + header_size);
return buffer;
}
Expand All @@ -183,7 +264,7 @@
decltype(client_id),
decltype(start_frame),
decltype(random_seed),
decltype(num_players)
u8
>();
// clang-format on
if (buffer.size() < required_num_bytes) {
Expand All @@ -203,12 +284,35 @@
decltype(GameStart::client_id),
decltype(GameStart::start_frame),
decltype(GameStart::random_seed),
decltype(GameStart::num_players)
u8
>()
.value();
// clang-format on

auto const num_remaining_bytes = num_players * (sizeof(u8) + player_name_buffer_size);
if (buffer.size() < num_remaining_bytes) {
throw MessageDeserializationError{ std::format(
"too few bytes to deserialize client identities within GameStart message ({} needed, {} received)",
num_remaining_bytes,
buffer.size()
) };
}
auto client_identities = std::vector<ClientIdentity>{};
client_identities.reserve(num_players);
for (auto i = decltype(num_players){ 0 }; i < num_players; ++i) {
auto const other_client_id = buffer.try_extract<u8>().value();
auto player_name = std::string{};
for (auto j = usize{ 0 }; j < player_name_buffer_size; ++j) {
auto const c = buffer.try_extract<char>().value();
if (c != '\0') {
player_name += c;
}
}
client_identities.emplace_back(other_client_id, std::move(player_name));
}
assert(buffer.size() == 0);
return GameStart{ client_id, start_frame, random_seed, num_players };

return GameStart{ client_id, start_frame, random_seed, std::move(client_identities) };
}

StateBroadcast::StateBroadcast(std::uint64_t const frame, std::vector<ClientStates> states_per_client)
Expand Down
Loading

0 comments on commit 634eeb0

Please sign in to comment.