From 0f5287666a4048463282b29f6cf3545077e7d66b Mon Sep 17 00:00:00 2001 From: is-this-c <87069698+is-this-c@users.noreply.github.com> Date: Mon, 4 May 2026 19:08:42 +1200 Subject: [PATCH 01/11] Add `IoCursor` and `PacketBuilder` --- game_patch/CMakeLists.txt | 1 + game_patch/multi/network.h | 88 +++++++++ game_patch/os/io_cursor.h | 366 +++++++++++++++++++++++++++++++++++++ 3 files changed, 455 insertions(+) create mode 100644 game_patch/os/io_cursor.h diff --git a/game_patch/CMakeLists.txt b/game_patch/CMakeLists.txt index 83bae0a6..84c082ac 100644 --- a/game_patch/CMakeLists.txt +++ b/game_patch/CMakeLists.txt @@ -207,6 +207,7 @@ set(SRCS multi/bots/bot_weapon_profiles.h os/console.cpp os/console.h + os/io_cursor.h os/commands.cpp os/autocomplete.cpp os/frametime.cpp diff --git a/game_patch/multi/network.h b/game_patch/multi/network.h index 84a3c281..03dc8406 100644 --- a/game_patch/multi/network.h +++ b/game_patch/multi/network.h @@ -2,9 +2,11 @@ #include #include +#include #include #include "server_internal.h" #include "../rf/multi.h" +#include "../os/io_cursor.h" constexpr uint32_t ALPINE_FACTION_SIGNATURE = 0x4E4C5246; constexpr uint32_t DASH_FACTION_SIGNATURE = 0xDA58FAC7; @@ -277,3 +279,89 @@ void send_queues_rel_clear_packets(int socket_id); void send_queues_rel_add_packet(int socket_id, const uint8_t* data, size_t len); void clear_rcon_profile_sessions(); void multi_disconnect_from_server(); + +template +class PacketBuilder { +public: + explicit PacketBuilder(const Storage storage) + noexcept(std::is_nothrow_move_constructible_v) + : storage{std::move(storage)} + , cursor{this->storage} { + this->cursor.write(DUMMY_HEADER); + } + + template + requires std::constructible_from + explicit PacketBuilder(Args&&... args) + noexcept(std::is_nothrow_constructible_v) + : storage{std::forward(args)...} + , cursor{this->storage} { + this->cursor.write(DUMMY_HEADER); + } + + template + bool write(this PacketBuilder& self, const T& value) noexcept { + return self.cursor.write(value); + } + + bool write_zstring( + this PacketBuilder& self, + const std::string_view text + ) noexcept { + return self.cursor.write_zstring(text); + } + + template + requires std::is_enum_v + && (sizeof(std::underlying_type_t) == 1) + void finalize(this PacketBuilder& self, const E type) noexcept { + const RF_GamePacketHeader header{ + .type = std::to_underlying(type), + .size = static_cast(self.payload_len()) + }; + self.storage.write_into( + 0, + std::span{ + reinterpret_cast(&header), + sizeof(RF_GamePacketHeader) + } + ); + } + + [[nodiscard]] + size_t capacity(this const PacketBuilder& self) noexcept { + return self.cursor.len(); + } + + [[nodiscard]] + size_t payload_len(this const PacketBuilder& self) noexcept { + return self.cursor.position() - sizeof(RF_GamePacketHeader); + } + + [[nodiscard]] + size_t len(this const PacketBuilder& self) noexcept { + return self.cursor.position(); + } + + [[nodiscard]] + bool is_poisoned(this const PacketBuilder& self) noexcept { + return self.cursor.is_poisoned(); + } + + [[nodiscard]] + const char* ptr(this const PacketBuilder& self) noexcept { + return self.storage.ptr(); + } + +private: + static constexpr RF_GamePacketHeader DUMMY_HEADER{}; + + Storage storage; + IoCursor cursor; +}; + +template +PacketBuilder(char (&)[N]) -> PacketBuilder; +PacketBuilder(std::span) -> PacketBuilder; +PacketBuilder(void*, size_t) -> PacketBuilder; +PacketBuilder(std::vector&) -> PacketBuilder; diff --git a/game_patch/os/io_cursor.h b/game_patch/os/io_cursor.h new file mode 100644 index 00000000..cedb7337 --- /dev/null +++ b/game_patch/os/io_cursor.h @@ -0,0 +1,366 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace io_cursor_detail { + +template +concept PodLike = std::is_trivially_copyable_v + && std::is_standard_layout_v; + +template +concept Read = requires( + const Storage& storage, + size_t position, + std::span out +) { + { storage.ptr() } noexcept -> std::same_as; + { storage.len() } noexcept -> std::same_as; + { storage.read_into(position, out) } noexcept -> std::same_as; +}; + +template +concept Write = requires( + Storage& storage, + size_t position, + std::span in +) { + { storage.write_into(position, in) } -> std::same_as; +}; + +class ReadOnlySpanStorage { +public: + ReadOnlySpanStorage() = default; + + explicit ReadOnlySpanStorage(const std::span buf) noexcept + : view{buf} {} + + explicit ReadOnlySpanStorage(const std::vector& buf) noexcept + : view{buf} {} + + template + explicit ReadOnlySpanStorage(const char (&buf)[N]) noexcept + : view{buf, N} {} + + explicit ReadOnlySpanStorage(const void* const buf, const size_t len) noexcept + : view{static_cast(buf), len} {} + + [[nodiscard]] + const char* ptr(this const ReadOnlySpanStorage& self) noexcept { + return self.view.data(); + } + + [[nodiscard]] + size_t len(this const ReadOnlySpanStorage& self) noexcept { + return self.view.size(); + } + + size_t read_into( + this const ReadOnlySpanStorage& self, + const size_t position, + const std::span out + ) noexcept { + if (position > self.view.size()) { + return 0; + } + const size_t num_bytes = std::min(out.size(), self.view.size() - position); + if (num_bytes > 0) { + std::memcpy(out.data(), self.view.data() + position, num_bytes); + } + return num_bytes; + } + +private: + std::span view; +}; + +class FixedSpanStorage { +public: + FixedSpanStorage() = default; + + explicit FixedSpanStorage(const std::span buf) noexcept + : view{buf} {} + + template + explicit FixedSpanStorage(char (&buf)[N]) noexcept + : view{buf, N} {} + + explicit FixedSpanStorage(void* const buf, const size_t len) noexcept + : view{static_cast(buf), len} {} + + [[nodiscard]] + const char* ptr(this const FixedSpanStorage& self) noexcept { + return self.view.data(); + } + + [[nodiscard]] + size_t len(this const FixedSpanStorage& self) noexcept { + return self.view.size(); + } + + size_t read_into( + this const FixedSpanStorage& self, + const size_t position, + const std::span out + ) noexcept { + if (position > self.view.size()) { + return 0; + } + const size_t num_bytes = std::min(out.size(), self.view.size() - position); + if (num_bytes > 0) { + std::memcpy(out.data(), self.view.data() + position, num_bytes); + } + return num_bytes; + } + + size_t write_into( + this FixedSpanStorage& self, + const size_t position, + const std::span in + ) noexcept { + if (position > self.view.size()) { + return 0; + } + const size_t num_bytes = std::min(in.size(), self.view.size() - position); + if (num_bytes > 0) { + std::memcpy(self.view.data() + position, in.data(), num_bytes); + } + return num_bytes; + } + +private: + std::span view; +}; + +class VectorGrowStorage { +public: + VectorGrowStorage() = default; + + explicit VectorGrowStorage(std::vector& buf) noexcept + : buf{buf} {} + + [[nodiscard]] + const char* ptr(this const VectorGrowStorage& self) noexcept { + return self.buf.data(); + } + + [[nodiscard]] + size_t len(this const VectorGrowStorage& self) noexcept { + return self.buf.size(); + } + + size_t read_into( + this const VectorGrowStorage& self, + const size_t position, + const std::span out + ) noexcept { + if (position > self.buf.size()) { + return 0; + } + const size_t num_bytes = std::min(out.size(), self.buf.size() - position); + if (num_bytes > 0) { + std::memcpy(out.data(), self.buf.data() + position, num_bytes); + } + return num_bytes; + } + + size_t write_into( + this VectorGrowStorage& self, + const size_t position, + const std::span in + ) { + if (in.empty()) { + return 0; + } + const size_t end = position + in.size(); + if (self.buf.size() < end) { + self.buf.resize(end); + } + std::memcpy(self.buf.data() + position, in.data(), in.size()); + return in.size(); + } + +private: + std::vector& buf; +}; + +} + +template +class IoCursor { +public: + explicit IoCursor(Storage storage) + noexcept(std::is_nothrow_move_constructible_v) + : storage{std::move(storage)} + , pos{0} + , poisoned{false} {} + + template + requires std::constructible_from + explicit IoCursor(Args&&... args) + noexcept(std::is_nothrow_constructible_v) + : storage{std::forward(args)...} + , pos{0} + , poisoned{false} {} + + template + [[nodiscard]] + bool available(this const IoCursor& self) noexcept { + return !self.poisoned + && self.pos <= self.storage.len() + && sizeof(T) <= self.storage.len() - self.pos; + } + + template + bool peek(this IoCursor& self, T& out) noexcept { + if (!self.available()) { + self.poison(); + return false; + } + const std::span bytes = std::span{ + reinterpret_cast(std::addressof(out)), + sizeof(T) + }; + if (self.storage.read_into(self.pos, bytes) != sizeof(T)) { + self.poison(); + return false; + } + return true; + } + + template + bool skip(this IoCursor& self) noexcept { + if (!self.available()) { + self.poison(); + return false; + } + self.pos += sizeof(T); + return true; + } + + template + bool read(this IoCursor& self, T& out) noexcept { + if (!self.peek(out)) { + return false; + } + self.pos += sizeof(T); + return true; + } + + bool read_zstring(this IoCursor& self, std::string_view& out) noexcept { + if (self.poisoned) { + return false; + } else if (self.pos >= self.storage.len()) { + self.poison(); + return false; + } + const char* const storage = self.storage.ptr(); + const char* const begin = storage + self.pos; + const char* const end = storage + self.storage.len(); + const char* const nul = std::find(begin, end, '\0'); + if (nul == end) { + self.poison(); + return false; + } + const size_t len = std::distance(begin, nul); + out = std::string_view{begin, len}; + self.pos += len + 1; + return true; + } + + bool read_zstring(this IoCursor& self, std::string& out) { + std::string_view view{}; + if (!self.read_zstring(view)) { + return false; + } + out.assign(view); + return true; + } + + template + requires io_cursor_detail::Write + bool write(this IoCursor& self, const T& value) { + if (self.poisoned) { + return false; + } + const std::span bytes = std::span{ + reinterpret_cast(std::addressof(value)), + sizeof(T) + }; + const size_t num_bytes = self.storage.write_into(self.pos, bytes); + if (num_bytes != sizeof(T)) { + self.poison(); + return false; + } + self.pos += num_bytes; + return true; + } + + template + requires io_cursor_detail::Write + bool write_zstring(this IoCursor& self, const std::string_view text) { + if (self.poisoned) { + return false; + } + const size_t num_bytes = + self.storage.write_into(self.pos, std::span{text}); + if (num_bytes != text.size()) { + self.poison(); + return false; + } + self.pos += num_bytes; + constexpr char NUL = '\0'; + if (self.storage.write_into(self.pos, std::span{&NUL, 1}) != 1) { + self.poison(); + return false; + } + self.pos += 1; + return true; + } + + [[nodiscard]] + bool is_poisoned(this const IoCursor& self) noexcept { + return self.poisoned; + } + + [[nodiscard]] + size_t position(this const IoCursor& self) noexcept { + return self.pos; + } + + [[nodiscard]] + size_t len(this const IoCursor& self) noexcept { + return self.storage.len(); + } + +private: + void poison(this IoCursor& self) noexcept { + self.poisoned = true; + } + + size_t pos; + Storage storage; + bool poisoned; +}; + +template +IoCursor(const char (&)[N]) -> IoCursor; +IoCursor(std::span) -> IoCursor; +IoCursor(const std::vector&) -> IoCursor; +IoCursor(const void*, size_t) -> IoCursor; + +template +IoCursor(char (&)[N]) -> IoCursor; +IoCursor(std::span) -> IoCursor; +IoCursor(void*, size_t) -> IoCursor; + +IoCursor(std::vector&) -> IoCursor; From 9db15cb6b30059fc6e9d06c95dce86eade0f4027 Mon Sep 17 00:00:00 2001 From: is-this-c <87069698+is-this-c@users.noreply.github.com> Date: Mon, 4 May 2026 19:49:27 +1200 Subject: [PATCH 02/11] Small change --- game_patch/multi/network.h | 2 +- game_patch/os/io_cursor.h | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/game_patch/multi/network.h b/game_patch/multi/network.h index 03dc8406..295ec5fa 100644 --- a/game_patch/multi/network.h +++ b/game_patch/multi/network.h @@ -319,7 +319,7 @@ class PacketBuilder { .type = std::to_underlying(type), .size = static_cast(self.payload_len()) }; - self.storage.write_into( + self.storage.write( 0, std::span{ reinterpret_cast(&header), diff --git a/game_patch/os/io_cursor.h b/game_patch/os/io_cursor.h index cedb7337..437fac07 100644 --- a/game_patch/os/io_cursor.h +++ b/game_patch/os/io_cursor.h @@ -25,7 +25,7 @@ concept Read = requires( ) { { storage.ptr() } noexcept -> std::same_as; { storage.len() } noexcept -> std::same_as; - { storage.read_into(position, out) } noexcept -> std::same_as; + { storage.read(position, out) } noexcept -> std::same_as; }; template @@ -34,7 +34,7 @@ concept Write = requires( size_t position, std::span in ) { - { storage.write_into(position, in) } -> std::same_as; + { storage.write(position, in) } -> std::same_as; }; class ReadOnlySpanStorage { @@ -64,7 +64,7 @@ class ReadOnlySpanStorage { return self.view.size(); } - size_t read_into( + size_t read( this const ReadOnlySpanStorage& self, const size_t position, const std::span out @@ -107,7 +107,7 @@ class FixedSpanStorage { return self.view.size(); } - size_t read_into( + size_t read( this const FixedSpanStorage& self, const size_t position, const std::span out @@ -122,7 +122,7 @@ class FixedSpanStorage { return num_bytes; } - size_t write_into( + size_t write( this FixedSpanStorage& self, const size_t position, const std::span in @@ -158,7 +158,7 @@ class VectorGrowStorage { return self.buf.size(); } - size_t read_into( + size_t read( this const VectorGrowStorage& self, const size_t position, const std::span out @@ -173,7 +173,7 @@ class VectorGrowStorage { return num_bytes; } - size_t write_into( + size_t write( this VectorGrowStorage& self, const size_t position, const std::span in @@ -230,7 +230,7 @@ class IoCursor { reinterpret_cast(std::addressof(out)), sizeof(T) }; - if (self.storage.read_into(self.pos, bytes) != sizeof(T)) { + if (self.storage.read(self.pos, bytes) != sizeof(T)) { self.poison(); return false; } @@ -296,7 +296,7 @@ class IoCursor { reinterpret_cast(std::addressof(value)), sizeof(T) }; - const size_t num_bytes = self.storage.write_into(self.pos, bytes); + const size_t num_bytes = self.storage.write(self.pos, bytes); if (num_bytes != sizeof(T)) { self.poison(); return false; @@ -312,14 +312,14 @@ class IoCursor { return false; } const size_t num_bytes = - self.storage.write_into(self.pos, std::span{text}); + self.storage.write(self.pos, std::span{text}); if (num_bytes != text.size()) { self.poison(); return false; } self.pos += num_bytes; constexpr char NUL = '\0'; - if (self.storage.write_into(self.pos, std::span{&NUL, 1}) != 1) { + if (self.storage.write(self.pos, std::span{&NUL, 1}) != 1) { self.poison(); return false; } From 028df3c1348339fc3d9d25b5c519df3467be35fb Mon Sep 17 00:00:00 2001 From: is-this-c <87069698+is-this-c@users.noreply.github.com> Date: Mon, 4 May 2026 20:43:41 +1200 Subject: [PATCH 03/11] Update network.h --- game_patch/multi/network.h | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/game_patch/multi/network.h b/game_patch/multi/network.h index 295ec5fa..d1ed7e6d 100644 --- a/game_patch/multi/network.h +++ b/game_patch/multi/network.h @@ -319,13 +319,11 @@ class PacketBuilder { .type = std::to_underlying(type), .size = static_cast(self.payload_len()) }; - self.storage.write( - 0, - std::span{ - reinterpret_cast(&header), - sizeof(RF_GamePacketHeader) - } - ); + const std::span bytes{ + reinterpret_cast(&header), + sizeof(RF_GamePacketHeader) + }; + self.storage.write(0, bytes); } [[nodiscard]] From d764ed81de87542b051b16c5958fb0a0bc12dac4 Mon Sep 17 00:00:00 2001 From: is-this-c <87069698+is-this-c@users.noreply.github.com> Date: Mon, 4 May 2026 20:44:49 +1200 Subject: [PATCH 04/11] Update network.h --- game_patch/multi/network.h | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/game_patch/multi/network.h b/game_patch/multi/network.h index d1ed7e6d..655775ef 100644 --- a/game_patch/multi/network.h +++ b/game_patch/multi/network.h @@ -319,11 +319,8 @@ class PacketBuilder { .type = std::to_underlying(type), .size = static_cast(self.payload_len()) }; - const std::span bytes{ - reinterpret_cast(&header), - sizeof(RF_GamePacketHeader) - }; - self.storage.write(0, bytes); + const char* const ptr = reinterpret_cast(&header); + self.storage.write(0, std::span{ptr, sizeof(RF_GamePacketHeader)}); } [[nodiscard]] From 7547b36a45a1e1f61931e8395a42070e46469018 Mon Sep 17 00:00:00 2001 From: is-this-c <87069698+is-this-c@users.noreply.github.com> Date: Tue, 5 May 2026 01:54:37 +1200 Subject: [PATCH 05/11] Fix constructor parameter type in PacketBuilder --- game_patch/multi/network.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game_patch/multi/network.h b/game_patch/multi/network.h index 655775ef..a2423dfa 100644 --- a/game_patch/multi/network.h +++ b/game_patch/multi/network.h @@ -283,7 +283,7 @@ void multi_disconnect_from_server(); template class PacketBuilder { public: - explicit PacketBuilder(const Storage storage) + explicit PacketBuilder(Storage storage) noexcept(std::is_nothrow_move_constructible_v) : storage{std::move(storage)} , cursor{this->storage} { From 61a1fc890566f8676f570a44e2f3cf711a2f0460 Mon Sep 17 00:00:00 2001 From: is-this-c <87069698+is-this-c@users.noreply.github.com> Date: Tue, 26 May 2026 19:30:35 +1200 Subject: [PATCH 06/11] Small changes --- game_patch/CMakeLists.txt | 8 +- game_patch/multi/alpine_packets.cpp | 54 +++++++------- game_patch/multi/network.h | 64 ++++++++++++---- game_patch/os/io_cursor.h | 111 ++++++++++++++++++---------- game_patch/purefaction/pf.cpp | 2 +- game_patch/rf/multi.h | 2 +- 6 files changed, 156 insertions(+), 85 deletions(-) diff --git a/game_patch/CMakeLists.txt b/game_patch/CMakeLists.txt index 84c082ac..f6313587 100644 --- a/game_patch/CMakeLists.txt +++ b/game_patch/CMakeLists.txt @@ -205,16 +205,16 @@ set(SRCS multi/bots/bot_waypoint_route.cpp multi/bots/bot_waypoint_route.h multi/bots/bot_weapon_profiles.h + os/autocomplete.cpp os/console.cpp os/console.h - os/io_cursor.h os/commands.cpp - os/autocomplete.cpp os/frametime.cpp - os/timer.cpp - os/win32_console.cpp + os/io_cursor.h os/os.cpp os/os.h + os/timer.cpp + os/win32_console.cpp object/object.h object/mover.h object/object_private.h diff --git a/game_patch/multi/alpine_packets.cpp b/game_patch/multi/alpine_packets.cpp index 21379056..14627461 100644 --- a/game_patch/multi/alpine_packets.cpp +++ b/game_patch/multi/alpine_packets.cpp @@ -49,8 +49,8 @@ void af_send_packet(rf::Player* player, const void* data, int len, bool is_relia } return; } - if (len <= 0 || len > rf::max_packet_size) { - xlog::error("af_send_packet: Packet size {} exceeds max {}", len, rf::max_packet_size); + if (len <= 0 || len > rf::MAX_PACKET_SIZE) { + xlog::error("af_send_packet: Packet size {} exceeds max {}", len, rf::MAX_PACKET_SIZE); return; } @@ -148,7 +148,7 @@ void af_send_ping_location_req_packet(rf::Vector3* pos) return; } - std::byte packet_buf[rf::max_packet_size]; + std::byte packet_buf[rf::MAX_PACKET_SIZE]; af_ping_location_req_packet ping_location_req_packet{}; ping_location_req_packet.header.type = static_cast(af_packet_type::af_ping_location_req); ping_location_req_packet.header.size = sizeof(ping_location_req_packet) - sizeof(ping_location_req_packet.header); @@ -229,7 +229,7 @@ void af_send_ping_location_packet(rf::Vector3* pos, uint8_t player_id, rf::Playe return; } - std::byte packet_buf[rf::max_packet_size]; + std::byte packet_buf[rf::MAX_PACKET_SIZE]; af_ping_location_packet ping_location_packet{}; ping_location_packet.header.type = static_cast(af_packet_type::af_ping_location); ping_location_packet.header.size = sizeof(ping_location_packet) - sizeof(ping_location_packet.header); @@ -309,7 +309,7 @@ void af_send_damage_notify_packet(uint8_t player_id, float damage, bool died, rf return; } - std::byte packet_buf[rf::max_packet_size]; + std::byte packet_buf[rf::MAX_PACKET_SIZE]; af_damage_notify_packet damage_notify_packet{}; damage_notify_packet.header.type = static_cast(af_packet_type::af_damage_notify); damage_notify_packet.header.size = sizeof(damage_notify_packet) - sizeof(damage_notify_packet.header); @@ -429,7 +429,7 @@ void af_send_obj_update_packet(rf::Player* player) size_t object_data_size = obj_updates.size() * sizeof(af_obj_update); size_t total_packet_size = sizeof(RF_GamePacketHeader) + object_data_size; - if (total_packet_size > rf::max_packet_size) { + if (total_packet_size > rf::MAX_PACKET_SIZE) { xlog::error("af_send_obj_update_packet: Packet too large! Size: {}", total_packet_size); return; } @@ -587,7 +587,7 @@ void af_send_client_req_packet(const af_client_req_packet& packet) return; } - std::byte buf[rf::max_packet_size]; + std::byte buf[rf::MAX_PACKET_SIZE]; size_t offset = 0; // Write header @@ -686,7 +686,7 @@ void af_send_server_req_packet(const af_server_req_packet& packet, rf::Player* p return; } - std::byte buf[rf::max_packet_size]; + std::byte buf[rf::MAX_PACKET_SIZE]; size_t offset = 0; std::memcpy(buf + offset, &packet.header, sizeof(packet.header)); @@ -834,7 +834,7 @@ void af_send_just_spawned_loadout(rf::Player* to_player, std::vectorsize = static_cast(1 + payload_written); const size_t total_len = sizeof(RF_GamePacketHeader) + hdr->size; - if (total_len > rf::max_packet_size) + if (total_len > rf::MAX_PACKET_SIZE) return; af_send_packet(to_player, buf, static_cast(total_len), true); @@ -958,10 +958,10 @@ void af_send_koth_hill_state_packet(rf::Player* player, const HillInfo& h, const pkt.red_score = static_cast(std::clamp(g_koth_info.red_team_score, 0, 0xFFFF)); pkt.blue_score = static_cast(std::clamp(g_koth_info.blue_team_score, 0, 0xFFFF)); - std::byte buf[rf::max_packet_size]; + std::byte buf[rf::MAX_PACKET_SIZE]; const size_t wire_sz = sizeof(pkt); - if (wire_sz > rf::max_packet_size) { - xlog::error("af_koth_state: packet too large ({}>{})", wire_sz, rf::max_packet_size); + if (wire_sz > rf::MAX_PACKET_SIZE) { + xlog::error("af_koth_state: packet too large ({}>{})", wire_sz, rf::MAX_PACKET_SIZE); return; } std::memcpy(buf, &pkt, wire_sz); @@ -1052,12 +1052,12 @@ void af_send_koth_hill_captured_packet(rf::Player* player, uint8_t hill_uid, Hil const size_t payload_size = (sizeof(af_koth_hill_captured_packet) - sizeof(RF_GamePacketHeader)) + id_count; const size_t wire_size = sizeof(RF_GamePacketHeader) + payload_size; - if (wire_size > rf::max_packet_size) { - xlog::error("af_koth_hill_captured: packet too large ({} > {})", wire_size, rf::max_packet_size); + if (wire_size > rf::MAX_PACKET_SIZE) { + xlog::error("af_koth_hill_captured: packet too large ({} > {})", wire_size, rf::MAX_PACKET_SIZE); return; } - std::byte packet_buf[rf::max_packet_size]; + std::byte packet_buf[rf::MAX_PACKET_SIZE]; af_koth_hill_captured_packet af_koth_hill_captured_packet{}; af_koth_hill_captured_packet.header.type = static_cast(af_packet_type::af_koth_hill_captured); @@ -1646,7 +1646,7 @@ void af_send_server_cfg(rf::Player* player) { } const auto send_msg = [player] (const std::string_view msg) { - constexpr size_t max_chunk_len = rf::max_packet_size - sizeof(af_server_msg_packet); + constexpr size_t max_chunk_len = rf::MAX_PACKET_SIZE - sizeof(af_server_msg_packet); const size_t len = std::clamp(msg.size(), 0uz, max_chunk_len); af_server_msg_packet server_msg_packet; @@ -1675,7 +1675,7 @@ void af_send_server_cfg(rf::Player* player) { // We cannot send multiple server configs at once. send_queues_rel_clear_packets(player->net_data->reliable_socket); - constexpr int chunk_size = rf::max_packet_size - sizeof(af_server_msg_packet); + constexpr int chunk_size = rf::MAX_PACKET_SIZE - sizeof(af_server_msg_packet); for (const auto chunk : g_alpine_server_config.printed_cfg | std::views::chunk(chunk_size)) { send_msg(std::string_view{chunk.begin(), chunk.end()}); @@ -1698,13 +1698,13 @@ void af_send_server_cfg(rf::Player* player) { union af_server_msg_packet_buf { af_server_msg_packet packet; - std::array buf; + std::array buf; }; af_server_msg_packet_buf build_automated_chat_msg_packet( const std::string_view msg ) { - constexpr size_t max_len = rf::max_packet_size - sizeof(af_server_msg_packet); + constexpr size_t max_len = rf::MAX_PACKET_SIZE - sizeof(af_server_msg_packet); const size_t len = std::clamp(msg.size(), 0uz, max_len); af_server_msg_packet_buf buf{}; @@ -1723,7 +1723,7 @@ af_server_msg_packet_buf build_automated_chat_msg_packet( af_server_msg_packet_buf build_server_console_msg_packet( const std::string_view msg ) { - constexpr size_t max_len = rf::max_packet_size - sizeof(af_server_msg_packet); + constexpr size_t max_len = rf::MAX_PACKET_SIZE - sizeof(af_server_msg_packet); const size_t len = std::clamp(msg.size(), 0uz, max_len); af_server_msg_packet_buf buf{}; @@ -1867,7 +1867,7 @@ void af_send_bot_control_simple(rf::Player* player, af_bot_control_type subtype) return; } - std::byte buf[rf::max_packet_size]; + std::byte buf[rf::MAX_PACKET_SIZE]; size_t offset = 0; RF_GamePacketHeader header{}; @@ -1940,7 +1940,7 @@ void af_send_bot_control_update_personality(rf::Player* player, const ServerBotC return; } - std::byte buf[rf::max_packet_size]; + std::byte buf[rf::MAX_PACKET_SIZE]; const size_t len = write_profile_update_payload( buf, sizeof(buf), af_bot_control_type::update_personality, @@ -1959,7 +1959,7 @@ void af_send_bot_control_update_skill(rf::Player* player, const ServerBotConfig& return; } - std::byte buf[rf::max_packet_size]; + std::byte buf[rf::MAX_PACKET_SIZE]; const size_t len = write_profile_update_payload( buf, sizeof(buf), af_bot_control_type::update_skill, @@ -1981,7 +1981,7 @@ void af_send_bot_control_update_identity(rf::Player* player, const std::string& const uint8_t name_len = static_cast( std::min(name.size(), kMaxPresetNameLen)); - std::byte buf[rf::max_packet_size]; + std::byte buf[rf::MAX_PACKET_SIZE]; size_t offset = 0; RF_GamePacketHeader header{}; @@ -2393,7 +2393,7 @@ void af_send_player_info_response(const rf::NetAddr& addr) // Compute segment boundaries (each segment fits within max_packet_size) constexpr int header_size = static_cast(sizeof(af_player_info_packet)); - constexpr int max_payload = static_cast(rf::max_packet_size) - header_size; + constexpr int max_payload = static_cast(rf::MAX_PACKET_SIZE) - header_size; int seg_boundaries[max_players + 2]; // preamble + entries + sentinel int total_segments = 0; @@ -2429,7 +2429,7 @@ void af_send_player_info_response(const rf::NetAddr& addr) } // Build and send each segment - std::byte packet_buf[rf::max_packet_size]; + std::byte packet_buf[rf::MAX_PACKET_SIZE]; for (int seg = 0; seg < total_segments; ++seg) { int first_entry = seg_boundaries[seg]; int end_entry = seg_boundaries[seg + 1]; diff --git a/game_patch/multi/network.h b/game_patch/multi/network.h index 655775ef..53763f32 100644 --- a/game_patch/multi/network.h +++ b/game_patch/multi/network.h @@ -280,13 +280,16 @@ void send_queues_rel_add_packet(int socket_id, const uint8_t* data, size_t len); void clear_rcon_profile_sessions(); void multi_disconnect_from_server(); -template +template class PacketBuilder { public: explicit PacketBuilder(const Storage storage) noexcept(std::is_nothrow_move_constructible_v) : storage{std::move(storage)} , cursor{this->storage} { + if constexpr (std::derived_from) { + this->storage.reserve(rf::MAX_PACKET_SIZE); + } this->cursor.write(DUMMY_HEADER); } @@ -296,56 +299,87 @@ class PacketBuilder { noexcept(std::is_nothrow_constructible_v) : storage{std::forward(args)...} , cursor{this->storage} { + if constexpr (std::derived_from) { + this->storage.reserve(rf::MAX_PACKET_SIZE); + } this->cursor.write(DUMMY_HEADER); } template bool write(this PacketBuilder& self, const T& value) noexcept { - return self.cursor.write(value); + const bool res = self.cursor.write(value); + if (!res || self.size() > MAX_LEN) { + self.cursor.poison(); + return false; + } else { + return true; + } } bool write_zstring( this PacketBuilder& self, const std::string_view text ) noexcept { - return self.cursor.write_zstring(text); + const bool res = self.cursor.write_zstring(text); + if (!res || self.size() > MAX_LEN) { + self.cursor.poison(); + return false; + } else { + return true; + } } template requires std::is_enum_v && (sizeof(std::underlying_type_t) == 1) - void finalize(this PacketBuilder& self, const E type) noexcept { + bool finalize(this PacketBuilder& self, const E type) noexcept { + if (self.cursor.is_poisoned() + || self.payload_size() > std::numeric_limits::max()) { + return false; + } const RF_GamePacketHeader header{ .type = std::to_underlying(type), - .size = static_cast(self.payload_len()) + .size = static_cast(self.payload_size()) }; const char* const ptr = reinterpret_cast(&header); - self.storage.write(0, std::span{ptr, sizeof(RF_GamePacketHeader)}); + + try { + const size_t num_bytes = + self.storage.write(0, std::span{ptr, sizeof(RF_GamePacketHeader)}); + if (num_bytes != sizeof(RF_GamePacketHeader) || self.size() > MAX_LEN) { + goto POISON; + } + } catch (...) { + POISON: + self.cursor.poison(); + return false; + } + return true; } [[nodiscard]] size_t capacity(this const PacketBuilder& self) noexcept { - return self.cursor.len(); + return self.cursor.size(); } [[nodiscard]] - size_t payload_len(this const PacketBuilder& self) noexcept { - return self.cursor.position() - sizeof(RF_GamePacketHeader); + size_t size(this const PacketBuilder& self) noexcept { + return self.cursor.position(); } [[nodiscard]] - size_t len(this const PacketBuilder& self) noexcept { - return self.cursor.position(); + size_t payload_size(this const PacketBuilder& self) noexcept { + return self.cursor.position() - sizeof(RF_GamePacketHeader); } [[nodiscard]] - bool is_poisoned(this const PacketBuilder& self) noexcept { - return self.cursor.is_poisoned(); + const char* data(this const PacketBuilder& self) noexcept { + return self.storage.data(); } [[nodiscard]] - const char* ptr(this const PacketBuilder& self) noexcept { - return self.storage.ptr(); + bool is_poisoned(this const PacketBuilder& self) noexcept { + return self.cursor.is_poisoned(); } private: diff --git a/game_patch/os/io_cursor.h b/game_patch/os/io_cursor.h index 437fac07..3f771720 100644 --- a/game_patch/os/io_cursor.h +++ b/game_patch/os/io_cursor.h @@ -18,25 +18,30 @@ concept PodLike = std::is_trivially_copyable_v && std::is_standard_layout_v; template -concept Read = requires( +concept ReadSeek = requires( const Storage& storage, size_t position, std::span out ) { - { storage.ptr() } noexcept -> std::same_as; - { storage.len() } noexcept -> std::same_as; + { storage.size() } noexcept -> std::same_as; + { storage.data() } noexcept -> std::same_as; { storage.read(position, out) } noexcept -> std::same_as; }; template -concept Write = requires( +concept WriteSeek = requires( Storage& storage, size_t position, std::span in ) { + { storage.size() } noexcept -> std::same_as; + { storage.data() } noexcept -> std::same_as; { storage.write(position, in) } -> std::same_as; }; +template +concept ReadWriteSeek = ReadSeek && WriteSeek; + class ReadOnlySpanStorage { public: ReadOnlySpanStorage() = default; @@ -55,12 +60,12 @@ class ReadOnlySpanStorage { : view{static_cast(buf), len} {} [[nodiscard]] - const char* ptr(this const ReadOnlySpanStorage& self) noexcept { + const char* data(this const ReadOnlySpanStorage& self) noexcept { return self.view.data(); } [[nodiscard]] - size_t len(this const ReadOnlySpanStorage& self) noexcept { + size_t size(this const ReadOnlySpanStorage& self) noexcept { return self.view.size(); } @@ -98,12 +103,12 @@ class FixedSpanStorage { : view{static_cast(buf), len} {} [[nodiscard]] - const char* ptr(this const FixedSpanStorage& self) noexcept { + const char* data(this const FixedSpanStorage& self) noexcept { return self.view.data(); } [[nodiscard]] - size_t len(this const FixedSpanStorage& self) noexcept { + size_t size(this const FixedSpanStorage& self) noexcept { return self.view.size(); } @@ -143,21 +148,25 @@ class FixedSpanStorage { class VectorGrowStorage { public: - VectorGrowStorage() = default; + VectorGrowStorage() = delete; explicit VectorGrowStorage(std::vector& buf) noexcept : buf{buf} {} [[nodiscard]] - const char* ptr(this const VectorGrowStorage& self) noexcept { + const char* data(this const VectorGrowStorage& self) noexcept { return self.buf.data(); } [[nodiscard]] - size_t len(this const VectorGrowStorage& self) noexcept { + size_t size(this const VectorGrowStorage& self) noexcept { return self.buf.size(); } + void reserve(this VectorGrowStorage& self, const size_t size) noexcept { + self.buf.reserve(size); + } + size_t read( this const VectorGrowStorage& self, const size_t position, @@ -195,7 +204,7 @@ class VectorGrowStorage { } -template +template class IoCursor { public: explicit IoCursor(Storage storage) @@ -213,14 +222,16 @@ class IoCursor { , poisoned{false} {} template + requires io_cursor_detail::ReadSeek || io_cursor_detail::WriteSeek [[nodiscard]] bool available(this const IoCursor& self) noexcept { return !self.poisoned - && self.pos <= self.storage.len() - && sizeof(T) <= self.storage.len() - self.pos; + && self.pos <= self.storage.size() + && sizeof(T) <= self.storage.size() - self.pos; } template + requires io_cursor_detail::ReadSeek bool peek(this IoCursor& self, T& out) noexcept { if (!self.available()) { self.poison(); @@ -238,6 +249,7 @@ class IoCursor { } template + requires io_cursor_detail::ReadSeek || io_cursor_detail::WriteSeek bool skip(this IoCursor& self) noexcept { if (!self.available()) { self.poison(); @@ -247,7 +259,9 @@ class IoCursor { return true; } + template + requires io_cursor_detail::ReadSeek bool read(this IoCursor& self, T& out) noexcept { if (!self.peek(out)) { return false; @@ -256,16 +270,18 @@ class IoCursor { return true; } + template + requires io_cursor_detail::ReadSeek bool read_zstring(this IoCursor& self, std::string_view& out) noexcept { if (self.poisoned) { return false; - } else if (self.pos >= self.storage.len()) { + } else if (self.pos >= self.storage.size()) { self.poison(); return false; } - const char* const storage = self.storage.ptr(); - const char* const begin = storage + self.pos; - const char* const end = storage + self.storage.len(); + const char* const data = self.storage.data(); + const char* const begin = data + self.pos; + const char* const end = data + self.storage.size(); const char* const nul = std::find(begin, end, '\0'); if (nul == end) { self.poison(); @@ -277,27 +293,38 @@ class IoCursor { return true; } - bool read_zstring(this IoCursor& self, std::string& out) { + template + requires io_cursor_detail::ReadSeek + bool read_zstring(this IoCursor& self, std::string& out) noexcept { std::string_view view{}; if (!self.read_zstring(view)) { return false; } - out.assign(view); + try { + out.assign(view); + } catch (...) { + self.poison(); + return false; + } return true; } template - requires io_cursor_detail::Write - bool write(this IoCursor& self, const T& value) { + requires io_cursor_detail::WriteSeek + bool write(this IoCursor& self, const T& value) noexcept { if (self.poisoned) { return false; } - const std::span bytes = std::span{ - reinterpret_cast(std::addressof(value)), - sizeof(T) - }; - const size_t num_bytes = self.storage.write(self.pos, bytes); + const char* const ptr = reinterpret_cast(std::addressof(value)); + const std::span bytes = std::span{ptr, sizeof(T)}; + size_t num_bytes = 0; + try { + num_bytes = self.storage.write(self.pos, bytes); + } catch (...) { + goto POISON; + } if (num_bytes != sizeof(T)) { + POISON: self.poison(); return false; } @@ -306,22 +333,30 @@ class IoCursor { } template - requires io_cursor_detail::Write - bool write_zstring(this IoCursor& self, const std::string_view text) { + requires io_cursor_detail::WriteSeek + bool write_zstring(this IoCursor& self, const std::string_view text) noexcept { if (self.poisoned) { return false; } - const size_t num_bytes = - self.storage.write(self.pos, std::span{text}); + size_t num_bytes = 0; + try { + num_bytes = self.storage.write(self.pos, std::span{text}); + } catch (...) { + goto POISON; + } if (num_bytes != text.size()) { + POISON: self.poison(); return false; } self.pos += num_bytes; constexpr char NUL = '\0'; - if (self.storage.write(self.pos, std::span{&NUL, 1}) != 1) { - self.poison(); - return false; + try { + if (self.storage.write(self.pos, std::span{&NUL, 1}) != sizeof(char)) { + goto POISON; + } + } catch (...) { + goto POISON; } self.pos += 1; return true; @@ -337,9 +372,11 @@ class IoCursor { return self.pos; } + template + requires io_cursor_detail::ReadSeek || io_cursor_detail::WriteSeek [[nodiscard]] - size_t len(this const IoCursor& self) noexcept { - return self.storage.len(); + size_t size(this const IoCursor& self) noexcept { + return self.storage.size(); } private: @@ -347,8 +384,8 @@ class IoCursor { self.poisoned = true; } - size_t pos; Storage storage; + size_t pos; bool poisoned; }; diff --git a/game_patch/purefaction/pf.cpp b/game_patch/purefaction/pf.cpp index a651c29e..bfc3dce5 100644 --- a/game_patch/purefaction/pf.cpp +++ b/game_patch/purefaction/pf.cpp @@ -36,7 +36,7 @@ void send_pf_player_stats_packet(rf::Player* player) return; } - std::byte packet_buf[rf::max_packet_size]; + std::byte packet_buf[rf::MAX_PACKET_SIZE]; pf_player_stats_packet stats_packet{}; stats_packet.hdr.type = static_cast(pf_packet_type::player_stats); stats_packet.hdr.size = sizeof(stats_packet) - sizeof(stats_packet.hdr); diff --git a/game_patch/rf/multi.h b/game_patch/rf/multi.h index 5ceb2c0c..82eaf856 100644 --- a/game_patch/rf/multi.h +++ b/game_patch/rf/multi.h @@ -230,7 +230,7 @@ namespace rf #pragma pack(pop) static_assert(sizeof(NetReliableSocket) == 0x6BF); - constexpr size_t max_packet_size = 512; + constexpr size_t MAX_PACKET_SIZE = 512; static auto& multi_get_game_type = addr_as_ref(0x00470770); static auto& multi_io_send = addr_as_ref(0x00479370); From bf338db731172ee60ace98bc64ec40d4ca7a83be Mon Sep 17 00:00:00 2001 From: is-this-c <87069698+is-this-c@users.noreply.github.com> Date: Tue, 26 May 2026 19:43:51 +1200 Subject: [PATCH 07/11] Update CHANGELOG.md --- docs/CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7f8eb506..6ee15d40 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -9,6 +9,9 @@ Version 1.4.0 (Lupin): Not yet released [@GooberRF](https://github.com/GooberRF) - Add vote-allowed levels to level autodownload list for dedicated servers +[@is-this-c](https://github.com/is-this-c) +- Add `IoCursor` and `PacketBuilder` + ### Bug fixes [@GooberRF](https://github.com/GooberRF) - Fix team balance not properly randomizing the distribution order of equal-scoring human players From a14ed7cb9fccd885c27d1c9725c048b733d68e2a Mon Sep 17 00:00:00 2001 From: is-this-c <87069698+is-this-c@users.noreply.github.com> Date: Tue, 26 May 2026 19:44:34 +1200 Subject: [PATCH 08/11] Update CHANGELOG.md --- docs/CHANGELOG.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 83664ac9..42cc5f16 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -20,6 +20,7 @@ Version 1.4.0 (Lupin): Not yet released - Make clock clutter objects correctly display the current local real world time [@is-this-c](https://github.com/is-this-c) +- Add `IoCursor` and `PacketBuilder` - Disable `Refresh Selected` in the server browser, only if `Refresh Selected` was pressed - Never disable `Add Server` in the server browser - Add `NOT IN ROUND`, `IDLE`, and `SPECTATOR` to the spectate UI @@ -28,9 +29,6 @@ Version 1.4.0 (Lupin): Not yet released - Add detection of manually loaded levels - Highlight an active level in a server's rotation via background color instead of text color -[@is-this-c](https://github.com/is-this-c) -- Add `IoCursor` and `PacketBuilder` - ### Bug fixes [@GooberRF](https://github.com/GooberRF) - Fix team balance not properly randomizing the distribution order of equal-scoring human players From 3457fac670461e375204d1e453d88a01be91d8e1 Mon Sep 17 00:00:00 2001 From: is-this-c <87069698+is-this-c@users.noreply.github.com> Date: Tue, 26 May 2026 19:49:39 +1200 Subject: [PATCH 09/11] Update network.h --- game_patch/multi/network.h | 1 - 1 file changed, 1 deletion(-) diff --git a/game_patch/multi/network.h b/game_patch/multi/network.h index cb3579f5..14a1d9d2 100644 --- a/game_patch/multi/network.h +++ b/game_patch/multi/network.h @@ -342,7 +342,6 @@ class PacketBuilder { .size = static_cast(self.payload_size()) }; const char* const ptr = reinterpret_cast(&header); - try { const size_t num_bytes = self.storage.write(0, std::span{ptr, sizeof(RF_GamePacketHeader)}); From 51351394c43a767e63a6d30a0f0c4ea82e8545f8 Mon Sep 17 00:00:00 2001 From: is-this-c <87069698+is-this-c@users.noreply.github.com> Date: Tue, 26 May 2026 19:53:58 +1200 Subject: [PATCH 10/11] Update network.h --- game_patch/multi/network.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/game_patch/multi/network.h b/game_patch/multi/network.h index 14a1d9d2..913d81ad 100644 --- a/game_patch/multi/network.h +++ b/game_patch/multi/network.h @@ -346,12 +346,12 @@ class PacketBuilder { const size_t num_bytes = self.storage.write(0, std::span{ptr, sizeof(RF_GamePacketHeader)}); if (num_bytes != sizeof(RF_GamePacketHeader) || self.size() > MAX_LEN) { - goto POISON; + POISON: + self.cursor.poison(); + return false; } } catch (...) { - POISON: - self.cursor.poison(); - return false; + goto POISON; } return true; } From 97d88c9135e70d539215127a19d1726bc355d5f9 Mon Sep 17 00:00:00 2001 From: is-this-c <87069698+is-this-c@users.noreply.github.com> Date: Tue, 26 May 2026 20:05:53 +1200 Subject: [PATCH 11/11] Small change --- game_patch/multi/network.h | 10 +++++----- game_patch/os/io_cursor.h | 8 ++------ 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/game_patch/multi/network.h b/game_patch/multi/network.h index 913d81ad..673e1981 100644 --- a/game_patch/multi/network.h +++ b/game_patch/multi/network.h @@ -341,19 +341,19 @@ class PacketBuilder { .type = std::to_underlying(type), .size = static_cast(self.payload_size()) }; - const char* const ptr = reinterpret_cast(&header); try { const size_t num_bytes = - self.storage.write(0, std::span{ptr, sizeof(RF_GamePacketHeader)}); + self.storage.write(0, std::as_bytes(std::span{&header, 1})); if (num_bytes != sizeof(RF_GamePacketHeader) || self.size() > MAX_LEN) { - POISON: - self.cursor.poison(); - return false; + goto POISON; } } catch (...) { goto POISON; } return true; + POISON: + self.cursor.poison(); + return false; } [[nodiscard]] diff --git a/game_patch/os/io_cursor.h b/game_patch/os/io_cursor.h index 3f771720..bfaac6e7 100644 --- a/game_patch/os/io_cursor.h +++ b/game_patch/os/io_cursor.h @@ -237,10 +237,7 @@ class IoCursor { self.poison(); return false; } - const std::span bytes = std::span{ - reinterpret_cast(std::addressof(out)), - sizeof(T) - }; + const std::span bytes = std::as_bytes(std::span{std::addressof(out), 1}); if (self.storage.read(self.pos, bytes) != sizeof(T)) { self.poison(); return false; @@ -315,8 +312,7 @@ class IoCursor { if (self.poisoned) { return false; } - const char* const ptr = reinterpret_cast(std::addressof(value)); - const std::span bytes = std::span{ptr, sizeof(T)}; + const std::span bytes = std::as_bytes(std::span{std::addressof(value), 1}); size_t num_bytes = 0; try { num_bytes = self.storage.write(self.pos, bytes);