From 8fa971ceae95a06e813940e89a1ec58552671546 Mon Sep 17 00:00:00 2001 From: WackyHcky Date: Mon, 5 Oct 2026 06:22:24 +1100 Subject: [PATCH 1/3] Raise physics world body pool for large custom maps Stock worlds allocate 8,200 bodies (+1 internal) from FBPhysicsWorldSize. Studio maps give every collision part its own static body, so large maps (e.g. a 41k-part GTA IV export) hit the FatalAssert "bodyIndex < m_freeBodyList.capacity()" in FBPhysicsWorld::asyncAllocateBody and crash on load. Hook fb::FBPhysicsWorld creation (RVA 0x02606ff0) and raise the size descriptor before it is copied into the job arguments, so every free list and per-body array is sized consistently. BodyCount becomes 65,000 (kept below 16 bits); the pools a static mesh body also draws from (geometry, owner, hull/mesh, shape, aggregate/fixed shape, fixed partition, persistent spatial index) scale by the same factor, capped at 2^20. Contacts, joints, queries and culling tables keep their native sizes. Both native callers build the descriptor on their own stack, so raising it in place only affects that world. Contract bytes for the entry, the descriptor copy, the internal-body increment and both call sites are validated before the hook is enabled. Co-Authored-By: Claude Opus 5.5 --- .../Game/Build/20260929/physics_world_size.h | 72 +++++++++ Extension/World/physics_world_size.cpp | 145 ++++++++++++++++++ Extension/World/physics_world_size.h | 18 +++ Runtime/bootstrap.cpp | 3 + cmake/Runtime.cmake | 1 + 5 files changed, 239 insertions(+) create mode 100644 Engine/Game/Build/20260929/physics_world_size.h create mode 100644 Extension/World/physics_world_size.cpp create mode 100644 Extension/World/physics_world_size.h diff --git a/Engine/Game/Build/20260929/physics_world_size.h b/Engine/Game/Build/20260929/physics_world_size.h new file mode 100644 index 0000000..90ef97a --- /dev/null +++ b/Engine/Game/Build/20260929/physics_world_size.h @@ -0,0 +1,72 @@ +// Reviewed native contract for supported_build::game_sha256 (September 29 2026). +// fb::FBPhysicsWorld creation copies the 0xa8-byte FBPhysicsWorldSize at the +// front of its descriptor into the job arguments (+0x2428) before any pool, +// free list or per-body array is sized from it. Both native callers build that +// descriptor on their own stack immediately before the call. +#pragma once +#include +#include +#include +#include + +namespace dingosdk::game::build::v20260929::physics_world_size { +template consteval auto hex_bytes(const char (&hex)[N]) { + static_assert(N % 2 == 1); + std::array result{}; + const auto nibble = [](char c) -> unsigned { + if (c >= '0' && c <= '9') return static_cast(c - '0'); + if (c >= 'a' && c <= 'f') return static_cast(c - 'a' + 10); + throw "Invalid physics world size contract hex"; + }; + for (std::size_t i = 0; i < result.size(); ++i) + result[i] = static_cast((nibble(hex[i * 2]) << 4) | nibble(hex[i * 2 + 1])); + return result; +} + +// World creation entry, through the stack cookie. +inline constexpr auto create_entry = hex_bytes( + "48895c241848897424205741544155415641574881eca0030000c5f829b42490030000488b05a6d3bb044833c4" + "4889842480030000"); +// rbx = this->m_jobArguments; copy descriptor[0x00..0xa8) to rbx+0x2428. +inline constexpr auto size_copy = hex_bytes( + "488b5e28c4c17c1006c5fc118328240000c4c17c104e20c5fc118b48240000c4c17c104640c5fc118368240000" + "c4c17c104e60c5fc118b88240000c4c17c108680000000c5fc1183a8240000c4c17b108ea0000000c5fb118bc8" + "240000"); +// The world reserves one internal body on top of BodyCount (data 8200 -> 8201 slots). +inline constexpr auto internal_body = hex_bytes("ff833024000083832824000003"); +// Both callers: lea rdx,[stack descriptor]; mov rcx,rax; call create. +inline constexpr auto caller_a_call = hex_bytes("e851195200"); +inline constexpr auto caller_b_call = hex_bytes("e8306f8dfe"); + +struct Contract { + std::uintptr_t rva; + std::span bytes; +}; +inline constexpr Contract contracts[]{ + {0x02606ff0, create_entry}, + {0x02607fec, size_copy}, + {0x026080b0, internal_body}, + {0x020e569a, caller_a_call}, + {0x03d300bb, caller_b_call}, +}; +inline constexpr std::size_t max_contract_size = [] { + std::size_t size = 0; + for (const auto& contract : contracts) size = contract.bytes.size() > size ? contract.bytes.size() : size; + return size; +}(); + +// unsigned-int fields of FBPhysicsWorldSize in reflected field order (index * 4). +inline constexpr std::size_t size_bytes = 0xa8; +inline constexpr std::size_t geometry_count = 0x00; +inline constexpr std::size_t owner_count = 0x04; +inline constexpr std::size_t body_count = 0x08; +inline constexpr std::size_t hull_and_mesh_count = 0x14; +inline constexpr std::size_t shape_count = 0x18; +inline constexpr std::size_t aggregate_shape_count = 0x1c; +inline constexpr std::size_t fixed_shape_count = 0x20; +inline constexpr std::size_t fixed_partition_count = 0x3c; +inline constexpr std::size_t persistent_spatial_index_count = 0xa0; + +// fb::FBPhysicsWorld creation: (void* world, Descriptor* descriptor). +inline constexpr std::uintptr_t create_rva = 0x02606ff0; +} // namespace dingosdk::game::build::v20260929::physics_world_size diff --git a/Extension/World/physics_world_size.cpp b/Extension/World/physics_world_size.cpp new file mode 100644 index 0000000..517f81c --- /dev/null +++ b/Extension/World/physics_world_size.cpp @@ -0,0 +1,145 @@ +#include "physics_world_size.h" +#include "Engine/Game/Build/supported_build.h" +#include "Engine/Game/Build/addresses.h" +#include "Engine/Game/Build/20260929/physics_world_size.h" +#include "Engine/Core/Log/logging.h" + +#include +#include "Engine/Core/Hooks/hooks.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace dingosdk { +namespace { +namespace world_size = addr::physics_world_size; +using WorldCreate = void* (*)(void*, void*); +std::atomic original_create{}; +std::uintptr_t installed_base{}; +std::mutex install_mutex; + +// Every pool a static triangle-mesh body is carved from. Contacts, joints, +// queries and culling tables track dynamic interactions and stay native. +constexpr std::size_t scaled_fields[]{ + world_size::geometry_count, + world_size::owner_count, + world_size::body_count, + world_size::hull_and_mesh_count, + world_size::shape_count, + world_size::aggregate_shape_count, + world_size::fixed_shape_count, + world_size::fixed_partition_count, + world_size::persistent_spatial_index_count, +}; + +bool read_memory(const void* source, void* destination, std::size_t size) noexcept { + SIZE_T count{}; + return ReadProcessMemory(GetCurrentProcess(), source, destination, size, &count) && count == size; +} + +bool validate_contract(std::uintptr_t base) noexcept { + if (!base || base > std::numeric_limits::max() - supported_build::game_image_size) + return false; + IMAGE_DOS_HEADER dos{}; + IMAGE_NT_HEADERS64 nt{}; + if (!read_memory(reinterpret_cast(base), &dos, sizeof(dos)) || + dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew < static_cast(sizeof(dos)) || + dos.e_lfanew > 0x1000 || + !read_memory(reinterpret_cast(base + dos.e_lfanew), &nt, sizeof(nt)) || + nt.Signature != IMAGE_NT_SIGNATURE || nt.FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64 || + nt.OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC || + nt.OptionalHeader.SizeOfImage != supported_build::game_image_size) return false; + for (const auto& contract : world_size::contracts) { + std::array actual{}; + if (!read_memory(reinterpret_cast(base + contract.rva), actual.data(), contract.bytes.size()) || + std::memcmp(actual.data(), contract.bytes.data(), contract.bytes.size()) != 0) return false; + } + return true; +} + +std::uint32_t field(const std::uint8_t* size, std::size_t offset) noexcept { + std::uint32_t value{}; + std::memcpy(&value, size + offset, sizeof(value)); + return value; +} + +void* create_world(void* world, void* descriptor) { + const auto incoming_error = GetLastError(); + const auto original = original_create.load(std::memory_order_acquire); + alignas(16) std::array size{}; + // The descriptor is the caller's stack local, rebuilt for every creation, so + // raising it in place affects only this world. Absolute targets keep it idempotent. + if (descriptor && read_memory(descriptor, size.data(), size.size())) { + const auto native_size = size; + const auto bodies = field(size.data(), world_size::body_count); + if (bodies > 0 && bodies < physics_world_body_target) { + const auto scale = (physics_world_body_target + bodies - 1) / bodies; + for (const auto offset : scaled_fields) { + const auto native = field(size.data(), offset); + auto raised = offset == world_size::body_count + ? physics_world_body_target + : static_cast(std::min( + std::uint64_t{native} * scale, physics_world_pool_ceiling)); + raised = std::max(raised, native); + std::memcpy(size.data() + offset, &raised, sizeof(raised)); + } + std::memcpy(descriptor, size.data(), size.size()); + const auto pair = [&](std::size_t offset) { + return std::format("{} -> {}", field(native_size.data(), offset), field(size.data(), offset)); + }; + logging::log(logging::Level::info, logging::Channel::world, + "Physics world pools raised: bodies {}, shapes {}, fixed shapes {}, owners {}, " + "geometries {}, hull/mesh {}, partitions {}, spatial indices {}", + pair(world_size::body_count), pair(world_size::shape_count), + pair(world_size::fixed_shape_count), pair(world_size::owner_count), + pair(world_size::geometry_count), pair(world_size::hull_and_mesh_count), + pair(world_size::fixed_partition_count), pair(world_size::persistent_spatial_index_count)); + } + } + SetLastError(incoming_error); + return original(world, descriptor); +} +} // namespace + +bool start_physics_world_size(std::uintptr_t base, std::string& error) { + std::lock_guard lock(install_mutex); + error.clear(); + if (installed_base) { + if (installed_base == base) return true; + error = "Physics world size support is already installed on another image"; + return false; + } + if (!validate_contract(base)) { + error = "Physics world creation contract does not match the supported game"; + return false; + } + auto* target = reinterpret_cast(base + world_size::create_rva); + WorldCreate original{}; + const auto created = hook_prepare(target, reinterpret_cast(&create_world), + reinterpret_cast(&original)); + if (created != HookOk) { + error = "Cannot create physics world hook (Detours hook service status " + std::to_string(created) + ")"; + return false; + } + if (!original) { + hook_remove(target); + error = "Physics world trampoline is unavailable"; + return false; + } + original_create.store(original, std::memory_order_release); + const auto enabled = hook_enable(target); + if (enabled != HookOk) { + hook_remove(target); + original_create.store(nullptr, std::memory_order_release); + error = "Cannot enable physics world hook (Detours hook service status " + std::to_string(enabled) + ")"; + return false; + } + installed_base = base; + return true; +} +} // namespace dingosdk diff --git a/Extension/World/physics_world_size.h b/Extension/World/physics_world_size.h new file mode 100644 index 0000000..eb89df8 --- /dev/null +++ b/Extension/World/physics_world_size.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +namespace dingosdk { +// Static collision slots per physics world. Stock worlds hold 8,200 bodies +// (+1 internal); a large custom map gives every collision part its own body. +// Kept below 65,536 so no 16-bit index elsewhere in the engine can wrap. +inline constexpr std::uint32_t physics_world_body_target = 65000; +// Pools that each static mesh body also draws from scale with BodyCount, up to this. +inline constexpr std::uint32_t physics_world_pool_ceiling = 1u << 20; + +// Install after Detours hook service init, before any level loads. Raises the +// body pool and its companion static-collision pools at world creation only; +// queries, contacts, joints and per-query result limits keep their native size. +bool start_physics_world_size(std::uintptr_t base, std::string& error); +} // namespace dingosdk diff --git a/Runtime/bootstrap.cpp b/Runtime/bootstrap.cpp index 898142a..d04e538 100644 --- a/Runtime/bootstrap.cpp +++ b/Runtime/bootstrap.cpp @@ -15,6 +15,7 @@ #include "Extension/Rendering/display_startup.h" #include "Extension/World/native_route_lookahead.h" #include "Extension/World/native_entity_pages.h" +#include "Extension/World/physics_world_size.h" #include namespace dingosdk::runtime { @@ -51,6 +52,8 @@ bool initialize_bootstrap(std::uintptr_t base) { if (!stage(Channel::assets, "Native render-resource pools", ready, error)) return false; error.clear(); ready = start_native_entity_pages(base, error); if (!stage(Channel::world, "Native entity pages (1048576 entity/update/render slots, 16384 spatial blocks/bucket, 128 MiB arena minimum)", ready, error)) return false; + error.clear(); ready = start_physics_world_size(base, error); + if (!stage(Channel::world, "Physics world pools (65000 static bodies)", ready, error)) return false; error.clear(); ready = start_native_route_lookahead(base, error); if (!stage(Channel::world, "NPC route cycle and endpoint guards", ready, error)) return false; if (!stage(Channel::graphics, "Display startup settings", start_display_settings(base), {})) return false; diff --git a/cmake/Runtime.cmake b/cmake/Runtime.cmake index 2ac9edc..c8ced38 100644 --- a/cmake/Runtime.cmake +++ b/cmake/Runtime.cmake @@ -87,6 +87,7 @@ add_library(dingosdk_runtime SHARED Extension/Assets/morph_memory_pool.cpp Extension/Assets/native_render_resource_pool.cpp Extension/World/native_entity_pages.cpp + Extension/World/physics_world_size.cpp Extension/Rendering/display_startup.cpp Extension/Rendering/replay_export.cpp Engine/Game/World/world_model.cpp From 5e557d184e427254ad4e012bad843dfd983a7aeb Mon Sep 17 00:00:00 2001 From: xThrasherrr Date: Sun, 4 Oct 2026 13:27:55 -0600 Subject: [PATCH 2/3] Keep a non-ASCII last letter in the server browser's tag name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tag_text dropped a trailing lead byte even when its character was whole, so "Skate Café" was listed as "Skate Caf". The browser shows the tag name until the server answers a query, so hosts without a forwarded query port kept it. --- Extension/Multiplayer/Test/protocol_tests.cpp | 11 +++++++++++ Server/steam_server.cpp | 15 +++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Extension/Multiplayer/Test/protocol_tests.cpp b/Extension/Multiplayer/Test/protocol_tests.cpp index f8b93e1..fb7bf45 100644 --- a/Extension/Multiplayer/Test/protocol_tests.cpp +++ b/Extension/Multiplayer/Test/protocol_tests.cpp @@ -856,6 +856,17 @@ void dedicated_server_codec() { check(row && row->dedicated && row->name == "Big friendly server" && row->map == ad.map && row->players == 3 && row->capacity == 16 && row->password_required && row->code == format_invite({server, ad.secret}), "Server tags did not round trip"); + // A whole last character is kept, and one the byte limit cuts through is dropped whole. + const auto accented = read_server_tags(dingosdk::server::server_tags({"Skate Caf\xC3\xA9", "Caf\xC3\xA9", 1, 8, false, true, 1}), server); + check(accented && accented->name == "Skate Caf\xC3\xA9" && accented->map == "Caf\xC3\xA9", + "A server name lost its non-ASCII last letter"); + for (std::size_t lead = 0; lead < 3; ++lead) { + std::string euros(lead, 'a'); + for (int i = 0; i < 50; ++i) euros += "\xE2\x82\xAC"; + const auto cut = dingosdk::server::server_tags({euros, "Caf\xC3\xA9", 1, 8, false, true, 1}); + const auto name = cut.substr(cut.find(",n") + 2); + check(cut.size() < 128 && name.size() > lead && (name.size() - lead) % 3 == 0, "A server name was cut inside a character"); + } check(!read_server_tags(tags, player), "A player's ID read as a server"); check(!read_server_tags("reskate,v1,k1,c4", server), "An incompatible server version listed"); } diff --git a/Server/steam_server.cpp b/Server/steam_server.cpp index e2a3fa8..6692169 100644 --- a/Server/steam_server.cpp +++ b/Server/steam_server.cpp @@ -1,4 +1,5 @@ #include "steam_server.h" +#include "server_text.h" #include "Extension/Multiplayer/Net/protocol.h" #ifdef _WIN32 #include @@ -12,6 +13,7 @@ #include #include #endif +#include #include #include #include @@ -45,14 +47,11 @@ struct SteamIP { // Tags are comma separated, so a name loses its commas; the whole list must // stay under Steam's 128 byte limit. std::string tag_text(std::string_view text, std::size_t limit) { - std::string result; - for (const char c : text) { - if (result.size() >= limit) break; - result += c == ',' ? ' ' : c; - } - // Never end inside a UTF-8 sequence. - while (!result.empty() && (static_cast(result.back()) & 0xC0) == 0x80) result.pop_back(); - if (!result.empty() && (static_cast(result.back()) & 0xC0) == 0xC0) result.pop_back(); + std::string result(text); + std::replace(result.begin(), result.end(), ',', ' '); + // Cut at the limit only, and never through a character: a whole last character + // is kept ("Café" stays "Café"), one the limit splits is dropped. + cut_text(result, limit); return result; } // Steam's library prints its own crash-reporting notes ("Setting breakpad From b20406897cd56798d25d24950522cc4ac2d83575 Mon Sep 17 00:00:00 2001 From: xThrasherrr Date: Sun, 4 Oct 2026 13:43:32 -0600 Subject: [PATCH 3/3] Refuse server ports outside 1-65535 instead of wrapping them A port of 70000 was narrowed to 4464 without a word, and the next saved setting wrote 4464 over the owner's typo. The check sits in load_config because config_error only ever sees the narrowed value. --- Server/Test/server_config_tests.cpp | 16 ++++++++++++++++ Server/server_config.cpp | 11 +++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Server/Test/server_config_tests.cpp b/Server/Test/server_config_tests.cpp index c6a6256..b1c41fa 100644 --- a/Server/Test/server_config_tests.cpp +++ b/Server/Test/server_config_tests.cpp @@ -68,6 +68,22 @@ int run() { const auto odd = load_config(file); check(odd.score_check == "warn" && odd.score_allow == std::vector{0xabc}, "Bad scoring settings not cleaned"); + // A port outside 1-65535 is refused, not wrapped to another port, and the file keeps the typo. + const auto refused = [&](const char *json) { + std::ofstream(file, std::ios::binary) << json; + try { + static_cast(load_config(file)); + } catch (const std::exception &) { + return text(file) == json; + } + return false; + }; + check(refused(R"({"query_port": 70000})") && refused(R"({"port": 65536})") && refused(R"({"port": 0})"), + "An out-of-range port was accepted or the file rewritten"); + std::ofstream(file, std::ios::binary) << R"({"port": 65535, "query_port": 1})"; + const auto edges = load_config(file); + check(edges.port == 65535 && edges.query_port == 1, "Ports at the ends of the range refused"); + std::filesystem::remove_all(folder); if (failures) return 1; std::cout << "server config: ok\n"; diff --git a/Server/server_config.cpp b/Server/server_config.cpp index 8be3b34..3053042 100644 --- a/Server/server_config.cpp +++ b/Server/server_config.cpp @@ -128,8 +128,15 @@ ServerConfig load_config(const std::filesystem::path &file, std::vector(root.value("port", static_cast(c.port))); - c.query_port = static_cast(root.value("query_port", static_cast(c.query_port))); + // Checked here, not in config_error: once narrowed, 70000 is just port 4464, and the + // next save would write that over the owner's typo. + const auto read_port = [&](const char *key, std::uint16_t fallback) { + const auto value = root.value(key, static_cast(fallback)); + if (value < 1 || value > 65535) throw std::runtime_error(std::string(key) + " must be 1 to 65535."); + return static_cast(value); + }; + c.port = read_port("port", c.port); + c.query_port = read_port("query_port", c.query_port); c.tps = root.value("tps", c.tps); c.voice_chat = root.value("voice_chat", c.voice_chat); c.voice_range = root.value("voice_range", c.voice_range);