From 522a7514012da86f7b9643179f0763746f3b232e Mon Sep 17 00:00:00 2001 From: untodesu Date: Fri, 12 Sep 2025 16:15:32 +0500 Subject: Protocol and versioning changes --- game/client/gui/bother.cc | 8 +++- game/client/gui/bother.hh | 4 +- game/client/gui/play_menu.cc | 92 +++++++++++++++++++++++++++-------------- game/client/gui/window_title.cc | 11 +---- game/client/session.cc | 6 ++- game/server/sessions.cc | 24 ++++++++++- game/server/status.cc | 6 ++- game/shared/protocol.cc | 20 ++++++--- game/shared/protocol.hh | 11 +++-- 9 files changed, 124 insertions(+), 58 deletions(-) (limited to 'game') diff --git a/game/client/gui/bother.cc b/game/client/gui/bother.cc index 1bb7097..e87b9ff 100644 --- a/game/client/gui/bother.cc +++ b/game/client/gui/bother.cc @@ -2,6 +2,8 @@ #include "client/gui/bother.hh" +#include "core/version.hh" + #include "shared/protocol.hh" #include "client/globals.hh" @@ -29,10 +31,12 @@ static void on_status_response_packet(const protocol::StatusResponse& packet) gui::BotherResponseEvent event; event.identity = identity; event.is_server_unreachable = false; - event.protocol_version = packet.version; event.num_players = packet.num_players; event.max_players = packet.max_players; event.motd = packet.motd; + event.game_version_major = packet.game_version_major; + event.game_version_minor = packet.game_version_minor; + event.game_version_patch = packet.game_version_patch; globals::dispatcher.trigger(event); enet_peer_disconnect(packet.peer, protocol::CHANNEL); @@ -89,7 +93,7 @@ void gui::bother::update_late(void) if(0 < enet_host_service(bother_host, &enet_event, 0)) { if(enet_event.type == ENET_EVENT_TYPE_CONNECT) { protocol::StatusRequest packet; - packet.version = protocol::VERSION; + packet.game_version_major = version::major; protocol::send(enet_event.peer, protocol::encode(packet)); return; } diff --git a/game/client/gui/bother.hh b/game/client/gui/bother.hh index fc5bab4..75e56d1 100644 --- a/game/client/gui/bother.hh +++ b/game/client/gui/bother.hh @@ -5,9 +5,11 @@ namespace gui struct BotherResponseEvent final { unsigned int identity; bool is_server_unreachable; - std::uint32_t protocol_version; std::uint16_t num_players; std::uint16_t max_players; + std::uint32_t game_version_major; + std::uint32_t game_version_minor; + std::uint32_t game_version_patch; std::string motd; }; } // namespace gui diff --git a/game/client/gui/play_menu.cc b/game/client/gui/play_menu.cc index 5b9887e..ad85141 100644 --- a/game/client/gui/play_menu.cc +++ b/game/client/gui/play_menu.cc @@ -10,6 +10,8 @@ #include "core/utils/string.hh" +#include "core/version.hh" + #include "shared/protocol.hh" #include "client/gui/bother.hh" @@ -25,9 +27,8 @@ constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; constexpr static std::string_view DEFAULT_SERVER_NAME = "Voxelius Server"; constexpr static std::string_view SERVERS_TXT = "servers.txt"; -constexpr static std::string_view WARNING_TOAST = "[!]"; -constexpr static std::size_t MAX_SERVER_ITEM_NAME = 24; +constexpr static std::size_t MAX_SERVER_ITEM_NAME = 18; enum class item_status : unsigned int { UNKNOWN = 0x0000U, @@ -43,10 +44,12 @@ struct ServerStatusItem final { std::uint16_t port; // Things pulled from bother events - std::uint32_t protocol_version; std::uint16_t num_players; std::uint16_t max_players; std::string motd; + std::uint16_t game_version_major; + std::uint16_t game_version_minor; + std::uint16_t game_version_patch; // Unique identifier that monotonically // grows with each new server added and @@ -69,9 +72,6 @@ static std::string str_status_init; static std::string str_status_ping; static std::string str_status_fail; -static std::string str_outdated_client; -static std::string str_outdated_server; - static std::string input_itemname; static std::string input_hostname; static std::string input_password; @@ -106,11 +106,13 @@ static void add_new_server(void) { auto item = new ServerStatusItem(); item->port = protocol::PORT; - item->protocol_version = protocol::VERSION; item->max_players = UINT16_MAX; item->num_players = UINT16_MAX; item->identity = next_identity; item->status = item_status::UNKNOWN; + item->game_version_major = 0U; + item->game_version_minor = 0U; + item->game_version_patch = 0U; next_identity += 1U; @@ -202,9 +204,6 @@ static void on_language_set(const gui::LanguageSetEvent& event) str_status_init = gui::language::resolve("play_menu.status.init"); str_status_ping = gui::language::resolve("play_menu.status.ping"); str_status_fail = gui::language::resolve("play_menu.status.fail"); - - str_outdated_client = gui::language::resolve("play_menu.outdated_client"); - str_outdated_server = gui::language::resolve("play_menu.outdated_server"); } static void on_bother_response(const gui::BotherResponseEvent& event) @@ -212,18 +211,22 @@ static void on_bother_response(const gui::BotherResponseEvent& event) for(auto item : servers_deque) { if(item->identity == event.identity) { if(event.is_server_unreachable) { - item->protocol_version = 0U; item->num_players = UINT16_MAX; item->max_players = UINT16_MAX; item->motd = str_status_fail; item->status = item_status::FAILURE; + item->game_version_major = 0U; + item->game_version_minor = 0U; + item->game_version_patch = 0U; } else { - item->protocol_version = event.protocol_version; item->num_players = event.num_players; item->max_players = event.max_players; item->motd = event.motd; item->status = item_status::REACHED; + item->game_version_major = event.game_version_major; + item->game_version_minor = event.game_version_minor; + item->game_version_patch = event.game_version_patch; } break; @@ -264,30 +267,50 @@ static void layout_server_item(ServerStatusItem* item) if(item->status == item_status::REACHED) { auto stats = std::format("{}/{}", item->num_players, item->max_players); - auto stats_width = ImGui::CalcTextSize(stats.c_str(), stats.c_str() + stats.size()).x; - auto stats_pos = ImVec2(cursor.x + item_width - stats_width - padding.x, cursor.y + padding.y); + auto stats_size = ImGui::CalcTextSize(stats.c_str(), stats.c_str() + stats.size()); + auto stats_pos = ImVec2(cursor.x + item_width - stats_size.x - padding.x, cursor.y + padding.y); draw_list->AddText(stats_pos, ImGui::GetColorU32(ImGuiCol_TextDisabled), stats.c_str(), stats.c_str() + stats.size()); - if(item->protocol_version != protocol::VERSION) { - auto warning_size = ImGui::CalcTextSize(WARNING_TOAST.data(), WARNING_TOAST.data() + WARNING_TOAST.size()); - auto warning_pos = ImVec2(stats_pos.x - warning_size.x - padding.x - 4.0f * globals::gui_scale, cursor.y + padding.y); - auto warning_end = ImVec2(warning_pos.x + warning_size.x, warning_pos.y + warning_size.y); - draw_list->AddText(warning_pos, ImGui::GetColorU32(ImGuiCol_DragDropTarget), WARNING_TOAST.data(), - WARNING_TOAST.data() + WARNING_TOAST.size()); + auto major_version_mismatch = item->game_version_major != version::major; + auto minor_version_mismatch = item->game_version_minor != version::minor; + auto patch_version_mismatch = item->game_version_patch != version::patch; - if(ImGui::IsMouseHoveringRect(warning_pos, warning_end)) { - ImGui::BeginTooltip(); + ImU32 version_color; - if(item->protocol_version < protocol::VERSION) { - ImGui::TextUnformatted(str_outdated_server.c_str(), str_outdated_server.c_str() + str_outdated_server.size()); - } - else { - ImGui::TextUnformatted(str_outdated_client.c_str(), str_outdated_client.c_str() + str_outdated_client.size()); - } + if(major_version_mismatch || minor_version_mismatch || patch_version_mismatch) { + version_color = ImGui::GetColorU32(major_version_mismatch ? ImGuiCol_PlotLinesHovered : ImGuiCol_DragDropTarget); + } + else { + version_color = ImGui::GetColorU32(ImGuiCol_PlotHistogram); + } - ImGui::EndTooltip(); - } + ImGui::PushFont(globals::font_unscii8, 4.0f); + + std::string version_toast; + + if(item->game_version_major < 16U) { + // Pre v16.x.x servers didn't send minor and patch versions + // and also used a different versioning scheme; post v16 the + // major version became the protocol version and the semver lost the tweak part + version_toast = std::string("0.0.1"); + } + else { + version_toast = std::format("{}.{}.{}", item->game_version_major, item->game_version_minor, item->game_version_patch); } + + auto version_size = ImGui::CalcTextSize(version_toast.c_str(), version_toast.c_str() + version_toast.size()); + auto version_pos = ImVec2(stats_pos.x - version_size.x - padding.x - 4.0f * globals::gui_scale, + cursor.y + padding.y + 0.5f * (stats_size.y - version_size.y)); + auto version_end = ImVec2(version_pos.x + version_size.x, version_pos.y + version_size.y); + + auto outline_pos = ImVec2(version_pos.x - 2U * globals::gui_scale, version_pos.y - 2U * globals::gui_scale); + auto outline_end = ImVec2(version_end.x + 2U * globals::gui_scale, version_end.y + 2U * globals::gui_scale); + auto outline_thickness = math::max(1.0f, 0.5f * static_cast(globals::gui_scale)); + + draw_list->AddRect(outline_pos, outline_end, version_color, 0.0f, 0, outline_thickness); + draw_list->AddText(version_pos, version_color, version_toast.c_str(), version_toast.c_str() + version_toast.size()); + + ImGui::PopFont(); } ImU32 motd_color = {}; @@ -460,11 +483,13 @@ void gui::play_menu::init(void) auto item = new ServerStatusItem(); item->port = protocol::PORT; - item->protocol_version = protocol::VERSION; item->max_players = UINT16_MAX; item->num_players = UINT16_MAX; item->identity = next_identity; item->status = item_status::UNKNOWN; + item->game_version_major = version::major; + item->game_version_minor = version::minor; + item->game_version_patch = version::patch; next_identity += 1U; @@ -543,6 +568,11 @@ void gui::play_menu::layout(void) ImGui::EndTabItem(); } + if(ImGui::BeginTabItem("debug###play_menu.debug.child")) { + ImGui::ShowStyleEditor(); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); } diff --git a/game/client/gui/window_title.cc b/game/client/gui/window_title.cc index 6e2387c..2f96205 100644 --- a/game/client/gui/window_title.cc +++ b/game/client/gui/window_title.cc @@ -10,14 +10,5 @@ void gui::window_title::update(void) { - std::string title; - - if(globals::sound_ctx && globals::sound_dev) { - title = std::format("Voxelius {}: {}", version::semver, splash::get()); - } - else { - title = std::format("Voxelius {}: {} [NOSOUND]", version::semver, splash::get()); - } - - glfwSetWindowTitle(globals::window, title.c_str()); + glfwSetWindowTitle(globals::window, std::format("Voxelius {}: {}", version::semver, splash::get()).c_str()); } diff --git a/game/client/session.cc b/game/client/session.cc index 907b789..ce3d616 100644 --- a/game/client/session.cc +++ b/game/client/session.cc @@ -6,6 +6,8 @@ #include "core/math/crc64.hh" +#include "core/version.hh" + #include "shared/entity/head.hh" #include "shared/entity/player.hh" #include "shared/entity/transform.hh" @@ -284,11 +286,13 @@ void session::disconnect(std::string_view reason) void session::send_login_request(void) { protocol::LoginRequest packet; - packet.version = protocol::VERSION; + packet.game_version_major = version::major; packet.voxel_registry_checksum = world::voxel_registry::get_checksum(); packet.item_registry_checksum = world::item_registry::get_checksum(); packet.password_hash = server_password_hash; packet.username = client_game::username.get(); + packet.game_version_minor = version::minor; + packet.game_version_patch = version::patch; protocol::send(session::peer, protocol::encode(packet)); diff --git a/game/server/sessions.cc b/game/server/sessions.cc index 2ab9d74..6758648 100644 --- a/game/server/sessions.cc +++ b/game/server/sessions.cc @@ -12,6 +12,8 @@ #include "core/utils/string.hh" +#include "core/version.hh" + #include "shared/entity/factory.hh" #include "shared/entity/head.hh" #include "shared/entity/player.hh" @@ -42,6 +44,8 @@ private: config::Unsigned sessions::max_players(8U, 1U, 128U); unsigned int sessions::num_players = 0U; +static config::Boolean strict_version_matching(true); + static emhash8::HashMap username_map; static emhash8::HashMap identity_map; static std::vector dimension_listeners; @@ -49,20 +53,36 @@ static std::vector sessions_vector; static void on_login_request_packet(const protocol::LoginRequest& packet) { - if(packet.version > protocol::VERSION) { + if(packet.game_version_major > version::major) { protocol::Disconnect response; response.reason = "protocol.outdated_server"; protocol::send(packet.peer, protocol::encode(response)); return; } - if(packet.version < protocol::VERSION) { + if(packet.game_version_minor < version::minor) { protocol::Disconnect response; response.reason = "protocol.outdated_client"; protocol::send(packet.peer, protocol::encode(response)); return; } + if(strict_version_matching.get_value()) { + if(packet.game_version_minor > version::minor || packet.game_version_patch > version::patch) { + protocol::Disconnect response; + response.reason = "protocol.outdated_server"; + protocol::send(packet.peer, protocol::encode(response)); + return; + } + + if(packet.game_version_minor < version::minor || packet.game_version_patch < version::patch) { + protocol::Disconnect response; + response.reason = "protocol.outdated_client"; + protocol::send(packet.peer, protocol::encode(response)); + return; + } + } + // FIXME: calculate voxel registry checksum ahead of time // instead of figuring it out every time a new player connects if(packet.voxel_registry_checksum != world::voxel_registry::get_checksum()) { diff --git a/game/server/status.cc b/game/server/status.cc index ba1d59d..0edd0a0 100644 --- a/game/server/status.cc +++ b/game/server/status.cc @@ -4,6 +4,8 @@ #include "core/config/number.hh" +#include "core/version.hh" + #include "shared/protocol.hh" #include "shared/splash.hh" @@ -13,10 +15,12 @@ static void on_status_request_packet(const protocol::StatusRequest& packet) { protocol::StatusResponse response; - response.version = protocol::VERSION; + response.game_version_major = version::major; response.max_players = sessions::max_players.get_value(); response.num_players = sessions::num_players; response.motd = splash::get(); + response.game_version_minor = version::minor; + response.game_version_patch = version::patch; protocol::send(packet.peer, protocol::encode(response)); } diff --git a/game/shared/protocol.cc b/game/shared/protocol.cc index 576502e..7115807 100644 --- a/game/shared/protocol.cc +++ b/game/shared/protocol.cc @@ -21,7 +21,7 @@ ENetPacket* protocol::encode(const protocol::StatusRequest& packet, enet_uint32 { write_buffer.reset(); write_buffer.write(protocol::StatusRequest::ID); - write_buffer.write(packet.version); + write_buffer.write(packet.game_version_major); return write_buffer.to_packet(flags); } @@ -29,10 +29,12 @@ ENetPacket* protocol::encode(const protocol::StatusResponse& packet, enet_uint32 { write_buffer.reset(); write_buffer.write(protocol::StatusResponse::ID); - write_buffer.write(packet.version); + write_buffer.write(packet.game_version_major); write_buffer.write(packet.max_players); write_buffer.write(packet.num_players); write_buffer.write(packet.motd); + write_buffer.write(packet.game_version_minor); + write_buffer.write(packet.game_version_patch); return write_buffer.to_packet(flags); } @@ -40,11 +42,13 @@ ENetPacket* protocol::encode(const protocol::LoginRequest& packet, enet_uint32 f { write_buffer.reset(); write_buffer.write(protocol::LoginRequest::ID); - write_buffer.write(packet.version); + write_buffer.write(packet.game_version_major); write_buffer.write(packet.voxel_registry_checksum); write_buffer.write(packet.item_registry_checksum); write_buffer.write(packet.password_hash); write_buffer.write(packet.username.substr(0, protocol::MAX_USERNAME)); + write_buffer.write(packet.game_version_minor); + write_buffer.write(packet.game_version_patch); return write_buffer.to_packet(flags); } @@ -268,26 +272,30 @@ void protocol::decode(entt::dispatcher& dispatcher, const ENetPacket* packet, EN switch(id) { case protocol::StatusRequest::ID: status_request.peer = peer; - status_request.version = read_buffer.read(); + status_request.game_version_major = read_buffer.read(); dispatcher.trigger(status_request); break; case protocol::StatusResponse::ID: status_response.peer = peer; - status_response.version = read_buffer.read(); + status_response.game_version_major = read_buffer.read(); status_response.max_players = read_buffer.read(); status_response.num_players = read_buffer.read(); status_response.motd = read_buffer.read(); + status_response.game_version_minor = read_buffer.read(); + status_response.game_version_patch = read_buffer.read(); dispatcher.trigger(status_response); break; case protocol::LoginRequest::ID: login_request.peer = peer; - login_request.version = read_buffer.read(); + login_request.game_version_major = read_buffer.read(); login_request.voxel_registry_checksum = read_buffer.read(); login_request.item_registry_checksum = read_buffer.read(); login_request.password_hash = read_buffer.read(); login_request.username = read_buffer.read(); + login_request.game_version_minor = read_buffer.read(); + login_request.game_version_patch = read_buffer.read(); dispatcher.trigger(login_request); break; diff --git a/game/shared/protocol.hh b/game/shared/protocol.hh index 3133275..f0bdff6 100644 --- a/game/shared/protocol.hh +++ b/game/shared/protocol.hh @@ -14,7 +14,6 @@ constexpr static std::size_t MAX_USERNAME = 64; constexpr static std::size_t MAX_SOUNDNAME = 1024; constexpr static std::uint16_t TICKRATE = 60; constexpr static std::uint16_t PORT = 43103; -constexpr static std::uint32_t VERSION = 15; constexpr static std::uint8_t CHANNEL = 0; } // namespace protocol @@ -107,22 +106,26 @@ ENetPacket* make_dimension_info(const world::Dimension* dimension); } // namespace protocol::utils struct protocol::StatusRequest final : public protocol::Base<0x0000> { - std::uint32_t version; + std::uint32_t game_version_major; // renamed from 'version' in v16.x.x }; struct protocol::StatusResponse final : public protocol::Base<0x0001> { - std::uint32_t version; + std::uint32_t game_version_major; // renamed from 'version' in v16.x.x std::uint16_t max_players; std::uint16_t num_players; std::string motd; + std::uint32_t game_version_minor { UINT32_MAX }; // added in v16.x.x + std::uint32_t game_version_patch { UINT32_MAX }; }; struct protocol::LoginRequest final : public protocol::Base<0x0002> { - std::uint32_t version; + std::uint32_t game_version_major; // renamed from 'version' in v16.x.x std::uint64_t voxel_registry_checksum; std::uint64_t item_registry_checksum; std::uint64_t password_hash; std::string username; + std::uint32_t game_version_minor; // added in v16.x.x + std::uint32_t game_version_patch; }; struct protocol::LoginResponse final : public protocol::Base<0x0003> { -- cgit