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/Multiplayer/Test/protocol_tests.cpp b/Extension/Multiplayer/Test/protocol_tests.cpp index efc3b58..6921a41 100644 --- a/Extension/Multiplayer/Test/protocol_tests.cpp +++ b/Extension/Multiplayer/Test/protocol_tests.cpp @@ -869,6 +869,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/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/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); 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 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