mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 13:57:05 +02:00
Merge branch 'main' of https://github.com/Dingo-Shenanigans/ReSkate
This commit is contained in:
commit
30f3f84120
9 files changed
+282
-10
No files matched your search
@@ -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 <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <span>
|
||||
|
||||
namespace dingosdk::game::build::v20260929::physics_world_size {
|
||||
template<std::size_t N> consteval auto hex_bytes(const char (&hex)[N]) {
|
||||
static_assert(N % 2 == 1);
|
||||
std::array<std::uint8_t, (N - 1) / 2> result{};
|
||||
const auto nibble = [](char c) -> unsigned {
|
||||
if (c >= '0' && c <= '9') return static_cast<unsigned>(c - '0');
|
||||
if (c >= 'a' && c <= 'f') return static_cast<unsigned>(c - 'a' + 10);
|
||||
throw "Invalid physics world size contract hex";
|
||||
};
|
||||
for (std::size_t i = 0; i < result.size(); ++i)
|
||||
result[i] = static_cast<std::uint8_t>((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<const std::uint8_t> 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
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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 <Windows.h>
|
||||
#include "Engine/Core/Hooks/hooks.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
|
||||
namespace dingosdk {
|
||||
namespace {
|
||||
namespace world_size = addr::physics_world_size;
|
||||
using WorldCreate = void* (*)(void*, void*);
|
||||
std::atomic<WorldCreate> 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<std::uintptr_t>::max() - supported_build::game_image_size)
|
||||
return false;
|
||||
IMAGE_DOS_HEADER dos{};
|
||||
IMAGE_NT_HEADERS64 nt{};
|
||||
if (!read_memory(reinterpret_cast<const void*>(base), &dos, sizeof(dos)) ||
|
||||
dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew < static_cast<LONG>(sizeof(dos)) ||
|
||||
dos.e_lfanew > 0x1000 ||
|
||||
!read_memory(reinterpret_cast<const void*>(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<std::uint8_t, world_size::max_contract_size> actual{};
|
||||
if (!read_memory(reinterpret_cast<const void*>(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<std::uint8_t, world_size::size_bytes> 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::uint32_t>(std::min<std::uint64_t>(
|
||||
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<void*>(base + world_size::create_rva);
|
||||
WorldCreate original{};
|
||||
const auto created = hook_prepare(target, reinterpret_cast<void*>(&create_world),
|
||||
reinterpret_cast<void**>(&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
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
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
|
||||
@@ -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 <string>
|
||||
|
||||
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;
|
||||
|
||||
@@ -68,6 +68,22 @@ int run() {
|
||||
const auto odd = load_config(file);
|
||||
check(odd.score_check == "warn" && odd.score_allow == std::vector<std::uint64_t>{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<void>(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";
|
||||
|
||||
@@ -128,8 +128,15 @@ ServerConfig load_config(const std::filesystem::path &file, std::vector<std::str
|
||||
if (value.is_string())
|
||||
if (const auto fingerprint = parse_scoring(value.string()))
|
||||
c.score_allow.push_back(*fingerprint);
|
||||
c.port = static_cast<std::uint16_t>(root.value("port", static_cast<unsigned>(c.port)));
|
||||
c.query_port = static_cast<std::uint16_t>(root.value("query_port", static_cast<unsigned>(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<unsigned>(fallback));
|
||||
if (value < 1 || value > 65535) throw std::runtime_error(std::string(key) + " must be 1 to 65535.");
|
||||
return static_cast<std::uint16_t>(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);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "steam_server.h"
|
||||
#include "server_text.h"
|
||||
#include "Extension/Multiplayer/Net/protocol.h"
|
||||
#ifdef _WIN32
|
||||
#include <Windows.h>
|
||||
@@ -12,6 +13,7 @@
|
||||
#include <thread>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <optional>
|
||||
@@ -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<unsigned char>(result.back()) & 0xC0) == 0x80) result.pop_back();
|
||||
if (!result.empty() && (static_cast<unsigned char>(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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user