1.0.0 Release!

This commit is contained in:
AyeZeeBB committed 2026-10-02 14:40:41 -05:00
1 parent 706e191fbd
commit ca1d82f636
1051 files changed
+607895

No files matched your search

+16
View File
@@ -0,0 +1,16 @@
# Vendor hashes cover exact upstream bytes, including nested headers.
External/rapidjson/** -text
External/rapidjson/** whitespace=-blank-at-eol
External/detours/** -text
External/detours/** whitespace=-blank-at-eol
External/imgui/** -text
External/imgui/** whitespace=-blank-at-eol
External/spdlog/** -text
External/steam_networking/** -text
External/steam_networking/** whitespace=-blank-at-eol
External/miniz/** -text
+76
View File
@@ -0,0 +1,76 @@
name: Build ReSkate
on:
workflow_dispatch:
inputs:
version:
description: Version stamped into the binaries (e.g. 0.2.0)
required: true
default: 0.0.0-dev
permissions:
contents: read
jobs:
build:
name: Windows x64 Release
runs-on: windows-2022
timeout-minutes: 30
defaults:
run:
shell: pwsh
steps:
- name: Check out source
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Configure
env:
RESKATE_BACKTRACE_URL: ${{ secrets.RESKATE_BACKTRACE_URL }}
run: cmake --preset vs2022-x64 -DDINGOSDK_LAUNCHER_AUTO_UPDATE=ON '-DDINGOSDK_VERSION=${{ inputs.version }}'
- name: Build launcher and runtime
run: cmake --build --preset release --parallel 4 --target dingosdk_launcher dingosdk_runtime
- name: Stage package
run: |
$ErrorActionPreference = 'Stop'
$package = 'build/package'
New-Item -ItemType Directory -Path "$package/licenses" -Force | Out-Null
Copy-Item -LiteralPath 'build/vs2022-x64/Release/ReSkateLauncher.exe' -Destination $package
Copy-Item -LiteralPath 'build/vs2022-x64/Release/ReSkate.dll' -Destination $package
$symbols = 'build/symbols'
New-Item -ItemType Directory -Path $symbols -Force | Out-Null
foreach ($name in @('ReSkate', 'ReSkateLauncher')) {
Copy-Item -LiteralPath "build/vs2022-x64/Release/$name.pdb" -Destination $symbols
}
Copy-Item -LiteralPath 'build/vs2022-x64/Release/ReSkate.dll' -Destination $symbols
Copy-Item -LiteralPath 'build/vs2022-x64/Release/ReSkateLauncher.exe' -Destination $symbols
Copy-Item -LiteralPath 'LICENSE' -Destination "$package/LICENSE.txt"
foreach ($library in @('imgui', 'detours', 'rapidjson', 'sqlite', 'miniz', 'bcdec')) {
Copy-Item -LiteralPath "External/$library/LICENSE.txt" -Destination "$package/licenses/$library-LICENSE.txt"
}
foreach ($library in @('spdlog', 'lz4', 'zstd', 'steam_networking')) {
Copy-Item -LiteralPath "External/$library/LICENSE" -Destination "$package/licenses/$library-LICENSE.txt"
}
Copy-Item -Path 'External/fonts/*-LICENSE.txt' -Destination "$package/licenses"
- name: Upload ReSkate ZIP
uses: actions/upload-artifact@v7
with:
name: ReSkate-windows-x64-${{ github.sha }}
path: build/package/
archive: true
if-no-files-found: error
retention-days: 30
- name: Upload matching debug symbols
uses: actions/upload-artifact@v7
with:
name: ReSkate-symbols-${{ github.sha }}
path: build/symbols/
archive: true
if-no-files-found: error
retention-days: 30
+29
View File
@@ -0,0 +1,29 @@
/build/
/out/
/artifacts/
/logs/
/.vs/
__pycache__/
*.log
*.dll
*.exe
*.pdb
*.ilk
*.lib
*.exp
*.keys
*.pcap
*.pcapng
*.pdml
*.mitm
analysis/raw/**/*.ebx
analysis/raw/**/*.res
# Optional developer utilities stay local. CMake is self-contained.
/tools/
/tests/
# Local research and extended documentation are not shipped.
/analysis/
/docs/
/release
+165
View File
@@ -0,0 +1,165 @@
cmake_minimum_required(VERSION 3.24)
project(DingoSDK LANGUAGES CXX)
if(NOT WIN32 OR NOT MSVC OR NOT CMAKE_SIZEOF_VOID_P EQUAL 8)
message(FATAL_ERROR "DingoSDK requires Windows, MSVC and the x64 platform.")
endif()
include(cmake/ProjectOptions.cmake)
include(cmake/Dependencies.cmake)
include(cmake/CustomScriptData.cmake)
include(cmake/Core.cmake)
include(cmake/Features.cmake)
include(cmake/UI.cmake)
include(cmake/Runtime.cmake)
include(cmake/Apps.cmake)
include(cmake/Backtrace.cmake)
option(DINGOSDK_BUILD_TESTS "Build logging regression tests" OFF)
option(DINGOSDK_BUILD_PARK_EDITOR_TESTS "Build park editor regression tests" OFF)
if(DINGOSDK_BUILD_PARK_EDITOR_TESTS)
enable_testing()
add_executable(dingosdk_park_editor_tests Extension/Objects/ParkEditor/Test/park_editor_tests.cpp)
target_link_libraries(dingosdk_park_editor_tests PRIVATE dingosdk_local_profile)
add_test(NAME park_editor COMMAND dingosdk_park_editor_tests "${CMAKE_CURRENT_BINARY_DIR}/editor-fixtures")
add_executable(dingosdk_park_editor_ui_tests Extension/UI/Overlay/Test/park_editor_tests.cpp
Extension/UI/Overlay/park_editor.cpp Extension/UI/Overlay/park_editor_actions.cpp
Extension/UI/Overlay/park_editor_panels.cpp Extension/UI/Overlay/park_editor_viewport.cpp Extension/UI/Overlay/park_previews.cpp)
target_link_libraries(dingosdk_park_editor_ui_tests PRIVATE dingosdk_imgui dingosdk_json dingosdk_game_archives dingosdk_content_cache dingosdk_logging)
add_test(NAME park_editor_ui COMMAND dingosdk_park_editor_ui_tests "${CMAKE_CURRENT_BINARY_DIR}/editor-preview" "${DINGOSDK_TEST_GAME_ROOT}")
add_executable(dingosdk_park_editor_transaction_tests Extension/Objects/ParkEditor/Test/park_editor_transaction_tests.cpp)
target_link_libraries(dingosdk_park_editor_transaction_tests PRIVATE dingosdk_local_profile dingosdk_logging)
add_test(NAME park_editor_transactions COMMAND dingosdk_park_editor_transaction_tests)
add_executable(dingosdk_profile_migration_tests Extension/Profile/Test/profile_migration_tests.cpp)
target_link_libraries(dingosdk_profile_migration_tests PRIVATE dingosdk_local_profile)
add_test(NAME profile_migration COMMAND dingosdk_profile_migration_tests)
add_executable(dingosdk_object_categories_tests Extension/Objects/Test/object_categories_tests.cpp)
target_link_libraries(dingosdk_object_categories_tests PRIVATE dingosdk_local_profile)
add_test(NAME object_categories COMMAND dingosdk_object_categories_tests)
endif()
option(DINGOSDK_BUILD_MULTIPLAYER_TESTS "Build multiplayer protocol regression tests" OFF)
if(DINGOSDK_BUILD_MULTIPLAYER_TESTS)
enable_testing()
add_executable(dingosdk_voice_tests Extension/Multiplayer/Test/voice_chat_tests.cpp Extension/Multiplayer/Net/protocol.cpp
Extension/Multiplayer/Net/delta_codec.cpp Extension/Multiplayer/Net/wire_codec.cpp)
target_link_libraries(dingosdk_voice_tests PRIVATE dingosdk_lz4 dingosdk_zstd)
add_test(NAME multiplayer_voice COMMAND dingosdk_voice_tests)
if(EXISTS "${PROJECT_SOURCE_DIR}/tests/first_person_spring_tests.cpp")
add_executable(dingosdk_first_person_spring_tests tests/first_person_spring_tests.cpp)
target_include_directories(dingosdk_first_person_spring_tests PRIVATE "${PROJECT_SOURCE_DIR}")
add_test(NAME first_person_spring COMMAND dingosdk_first_person_spring_tests)
endif()
add_executable(dingosdk_playstation_report_tests Engine/Game/Input/Test/playstation_report_tests.cpp)
target_include_directories(dingosdk_playstation_report_tests PRIVATE "${PROJECT_SOURCE_DIR}")
add_test(NAME playstation_report COMMAND dingosdk_playstation_report_tests)
add_executable(dingosdk_physics_tuning_tests Extension/Skater/Test/physics_tuning_tests.cpp
Extension/Skater/physics_tuning_model.cpp)
target_include_directories(dingosdk_physics_tuning_tests PRIVATE "${PROJECT_SOURCE_DIR}")
target_link_libraries(dingosdk_physics_tuning_tests PRIVATE dingosdk_game_archives)
add_test(NAME physics_tuning COMMAND dingosdk_physics_tuning_tests "${DINGOSDK_TEST_GAME_ROOT}")
add_executable(dingosdk_camera_observer_tests Extension/Skater/Test/camera_observer_tests.cpp)
add_test(NAME camera_observer_recovery COMMAND dingosdk_camera_observer_tests)
add_executable(dingosdk_native_menu_view_tests Extension/UI/NativeMenu/Test/native_menu_view_tests.cpp)
add_executable(dingosdk_loading_screen_tests Extension/World/Test/loading_screen_tests.cpp)
target_include_directories(dingosdk_loading_screen_tests PRIVATE "${PROJECT_SOURCE_DIR}")
add_test(NAME loading_screen_selection COMMAND dingosdk_loading_screen_tests)
add_test(NAME native_menu_browser COMMAND dingosdk_native_menu_view_tests)
add_executable(dingosdk_native_tools_view_tests Extension/UI/NativeMenu/Test/native_tools_view_tests.cpp)
target_include_directories(dingosdk_native_tools_view_tests PRIVATE "${PROJECT_SOURCE_DIR}")
add_test(NAME native_menu_tools COMMAND dingosdk_native_tools_view_tests)
add_executable(dingosdk_native_menu_lifetime_tests Extension/UI/NativeMenu/Test/native_menu_lifetime_tests.cpp)
add_test(NAME native_menu_lifetime COMMAND dingosdk_native_menu_lifetime_tests)
add_executable(dingosdk_native_callback_tests Engine/Game/Abi/Test/native_callback_queue_tests.cpp)
target_include_directories(dingosdk_native_callback_tests PRIVATE "${PROJECT_SOURCE_DIR}")
add_test(NAME native_callback_lifetime COMMAND dingosdk_native_callback_tests)
add_executable(dingosdk_steam_friend_join_tests Extension/Multiplayer/Test/steam_friend_join_tests.cpp)
add_test(NAME steam_friend_join COMMAND dingosdk_steam_friend_join_tests)
add_executable(dingosdk_multiplayer_tests
Extension/Multiplayer/Test/protocol_tests.cpp Extension/Multiplayer/Net/protocol.cpp Extension/Multiplayer/Remote/playback_buffers.cpp
Extension/Multiplayer/Net/wire_codec.cpp Extension/Multiplayer/Steam/steam_server_browser.cpp Server/steam_server.cpp
Extension/Throwdowns/throwdown_wire.cpp)
target_link_libraries(dingosdk_multiplayer_tests PRIVATE dingosdk_lz4 dingosdk_zstd dingosdk_logging dingosdk_word_filter)
add_test(NAME multiplayer_protocol COMMAND dingosdk_multiplayer_tests)
add_executable(dingosdk_party_tests Extension/Multiplayer/Test/party_tests.cpp Extension/Multiplayer/Session/party_book.cpp)
target_include_directories(dingosdk_party_tests PRIVATE "${PROJECT_SOURCE_DIR}")
add_test(NAME multiplayer_parties COMMAND dingosdk_party_tests)
add_executable(dingosdk_throwdown_relay_tests Extension/Throwdowns/Test/throwdown_relay_tests.cpp
Extension/Throwdowns/throwdown_relay.cpp Extension/Throwdowns/throwdown_wire.cpp)
target_include_directories(dingosdk_throwdown_relay_tests PRIVATE "${PROJECT_SOURCE_DIR}")
target_link_libraries(dingosdk_throwdown_relay_tests PRIVATE dingosdk_logging)
add_test(NAME throwdown_relay COMMAND dingosdk_throwdown_relay_tests)
add_executable(dingosdk_server_activity_tests Server/Test/server_activity_tests.cpp Server/server_activity.cpp
Extension/Throwdowns/throwdown_wire.cpp)
target_include_directories(dingosdk_server_activity_tests PRIVATE "${PROJECT_SOURCE_DIR}")
add_test(NAME server_activity COMMAND dingosdk_server_activity_tests)
add_executable(dingosdk_speed_check_tests Server/Test/speed_check_tests.cpp)
target_include_directories(dingosdk_speed_check_tests PRIVATE "${PROJECT_SOURCE_DIR}")
add_test(NAME server_speed_check COMMAND dingosdk_speed_check_tests)
add_executable(dingosdk_server_config_tests Server/Test/server_config_tests.cpp Server/server_config.cpp
Extension/Multiplayer/Net/protocol.cpp)
target_include_directories(dingosdk_server_config_tests PRIVATE "${PROJECT_SOURCE_DIR}")
target_link_libraries(dingosdk_server_config_tests PRIVATE dingosdk_launcher_support dingosdk_world_layer_scan dingosdk_json
dingosdk_lz4 dingosdk_zstd dingosdk_logging)
add_test(NAME server_config COMMAND dingosdk_server_config_tests)
add_executable(dingosdk_multiplayer_native_tests
Extension/Multiplayer/Test/native_layout_tests.cpp Extension/Multiplayer/Net/protocol.cpp)
add_test(NAME multiplayer_native_layout COMMAND dingosdk_multiplayer_native_tests)
add_executable(dingosdk_multiplayer_lobby_tests
Extension/Multiplayer/Test/lobby_tests.cpp Extension/Multiplayer/Steam/steam_lobbies.cpp
Extension/Multiplayer/Net/protocol.cpp)
add_test(NAME multiplayer_lobbies COMMAND dingosdk_multiplayer_lobby_tests)
add_executable(dingosdk_multiplayer_mesh_tests
Extension/Multiplayer/Test/mesh_tests.cpp Extension/Multiplayer/Net/protocol.cpp Extension/Multiplayer/Remote/playback_buffers.cpp
Extension/Multiplayer/Net/wire_codec.cpp Extension/Multiplayer/Net/delta_codec.cpp
Extension/Multiplayer/Session/password.cpp Extension/Multiplayer/Steam/steam_lobbies.cpp
Extension/Multiplayer/Steam/steam_server_browser.cpp)
target_link_libraries(dingosdk_multiplayer_mesh_tests PRIVATE dingosdk_logging dingosdk_launcher_support dingosdk_lz4 dingosdk_zstd bcrypt
dingosdk_word_filter)
add_test(NAME multiplayer_mesh COMMAND dingosdk_multiplayer_mesh_tests)
add_executable(dingosdk_word_filter_tests Engine/Core/Text/Test/word_filter_tests.cpp)
target_link_libraries(dingosdk_word_filter_tests PRIVATE dingosdk_word_filter)
add_test(NAME word_filter COMMAND dingosdk_word_filter_tests)
add_executable(dingosdk_settings_lock_tests Engine/Game/Settings/Test/multiplayer_settings_lock_tests.cpp)
target_include_directories(dingosdk_settings_lock_tests PRIVATE "${PROJECT_SOURCE_DIR}")
add_test(NAME multiplayer_settings_lock COMMAND dingosdk_settings_lock_tests)
add_executable(dingosdk_held_input_tests Extension/UI/Overlay/Test/held_input_tests.cpp)
add_test(NAME overlay_held_input COMMAND dingosdk_held_input_tests)
add_executable(dingosdk_multiplayer_bandwidth_tests
Extension/Multiplayer/Test/bandwidth_tests.cpp Extension/Multiplayer/Net/protocol.cpp Extension/Multiplayer/Remote/playback_buffers.cpp
Extension/Multiplayer/Net/wire_codec.cpp Extension/Multiplayer/Net/delta_codec.cpp)
target_link_libraries(dingosdk_multiplayer_bandwidth_tests PRIVATE dingosdk_lz4 dingosdk_zstd)
add_test(NAME multiplayer_bandwidth COMMAND dingosdk_multiplayer_bandwidth_tests)
add_executable(dingosdk_multiplayer_lane_tests Extension/Multiplayer/Test/steam_lanes_tests.cpp)
target_include_directories(dingosdk_multiplayer_lane_tests SYSTEM PRIVATE "${PROJECT_SOURCE_DIR}/External/steam_networking")
add_test(NAME multiplayer_lanes COMMAND dingosdk_multiplayer_lane_tests)
add_executable(dingosdk_multiplayer_prediction_tests
Extension/Multiplayer/Test/prediction_tests.cpp Extension/Multiplayer/Net/protocol.cpp Extension/Multiplayer/Remote/playback_buffers.cpp)
add_test(NAME multiplayer_prediction COMMAND dingosdk_multiplayer_prediction_tests)
endif()
# The route and logging tests live in the local, untracked tests/ folder.
option(DINGOSDK_BUILD_ROUTE_TESTS "Build native route cycle regression tests" OFF)
if(DINGOSDK_BUILD_ROUTE_TESTS AND EXISTS "${PROJECT_SOURCE_DIR}/tests/route_cycle_tests.cpp")
enable_testing()
add_executable(dingosdk_route_cycle_tests tests/route_cycle_tests.cpp)
target_include_directories(dingosdk_route_cycle_tests PRIVATE "${PROJECT_SOURCE_DIR}")
add_test(NAME route_cycle_guard COMMAND dingosdk_route_cycle_tests)
add_executable(dingosdk_route_endpoint_tests tests/route_endpoint_tests.cpp)
target_link_libraries(dingosdk_route_endpoint_tests PRIVATE dingosdk_hooks)
add_test(NAME route_endpoint_guard COMMAND dingosdk_route_endpoint_tests)
endif()
if(DINGOSDK_BUILD_TESTS AND EXISTS "${PROJECT_SOURCE_DIR}/tests/logging_tests.cpp")
enable_testing()
add_executable(dingosdk_logging_tests tests/logging_tests.cpp)
target_link_libraries(dingosdk_logging_tests PRIVATE dingosdk_logging)
add_test(NAME logging_regressions COMMAND dingosdk_logging_tests)
endif()
# Header-only game contracts belong to the runtime's subsystem tree.
file(GLOB_RECURSE game_headers CONFIGURE_DEPENDS "${PROJECT_SOURCE_DIR}/Engine/Game/*.h")
target_sources(dingosdk_runtime PRIVATE ${game_headers})
get_property(project_targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS)
foreach(target IN LISTS project_targets)
if(NOT target MATCHES "^(dingosdk_imgui|dingosdk_detours|dingosdk_sqlite|dingosdk_lz4|dingosdk_zstd|dingosdk_miniz)$")
dingosdk_configure_target(${target})
endif()
endforeach()
+23
View File
@@ -0,0 +1,23 @@
{
"version": 3,
"configurePresets": [
{
"name": "vs2022-x64",
"displayName": "Visual Studio 2022 x64",
"generator": "Visual Studio 17 2022",
"architecture": "x64",
"binaryDir": "${sourceDir}/build/vs2022-x64",
"environment": {
"VCLibPackagePath": "${sourceDir}/build/no-vcpkg/vcpkg.user"
},
"toolset": "v143"
}
],
"buildPresets": [
{
"name": "release",
"configurePreset": "vs2022-x64",
"configuration": "Release"
}
]
}
+403
View File
@@ -0,0 +1,403 @@
// ReSkateEmotePacker: builds the chat emote pack ReSkate.dll bakes in (assets/emotes) from a
// folder of pictures and GIFs. Every file becomes one emote named after the file; animated
// GIFs keep their frames and timing. Output: emotes.json + emotes.png in Better Chat's format
// (Extension/UI/Overlay/chat_emotes.h), within the limits ReSkate's loader accepts.
//
// ReSkateEmotePacker <folder> [output folder] [--size <px>] [--max-frames <n>]
// [--max-width <px>] [--name <pack name>]
//
// Dropping a folder on the exe writes the pack next to it, in "<folder>_pack".
#include "Engine/Core/Json/json.h"
#include <Windows.h>
#include <wincodec.h>
#include <wrl/client.h>
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
#include <filesystem>
#include <map>
#include <optional>
#include <set>
#include <stdexcept>
#include <string>
#include <vector>
using Microsoft::WRL::ComPtr;
namespace fs = std::filesystem;
using dingosdk::Json;
namespace {
// ReSkate's loader limits (chat_emotes.cpp): frames at most 256 x 256, names of letters,
// digits and _ up to 32 characters, 4096 frames in all, a texture at most 8192 x 8192.
constexpr int max_side = 256, max_texture = 8192;
constexpr std::size_t max_name = 32, max_total_frames = 4096;
struct Options {
fs::path input, output;
int size = 56; // emote height in pixels (wide emotes may be shorter)
int max_width = 2048; // atlas width
std::size_t max_frames = 64; // per animated emote; longer GIFs are thinned evenly
std::string name = "ReSkate";
};
struct Image {
int width{}, height{};
std::vector<unsigned char> rgba; // straight alpha
};
struct Frame {
Image image;
unsigned time{}; // ms
int x{}, y{}; // in the atlas
};
struct Emote {
std::string name, file;
std::vector<Frame> frames;
};
void check(HRESULT result, const char *what) {
if (FAILED(result)) {
char text[160];
std::snprintf(text, sizeof text, "%s failed (0x%08lx)", what, static_cast<unsigned long>(result));
throw std::runtime_error(text);
}
}
// Kept for the life of the process: releasing it after COM has shut down would crash on exit.
IWICImagingFactory &factory() {
static IWICImagingFactory *value = [] {
IWICImagingFactory *f{};
check(CoCreateInstance(CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&f)), "Starting Windows Imaging");
return f;
}();
return *value;
}
Image to_rgba(IWICBitmapSource *source) {
ComPtr<IWICFormatConverter> converter;
check(factory().CreateFormatConverter(&converter), "Creating a format converter");
check(converter->Initialize(source, GUID_WICPixelFormat32bppRGBA, WICBitmapDitherTypeNone, nullptr, 0,
WICBitmapPaletteTypeCustom), "Converting to RGBA");
UINT w{}, h{};
check(converter->GetSize(&w, &h), "Reading the size");
Image image{static_cast<int>(w), static_cast<int>(h)};
image.rgba.resize(static_cast<std::size_t>(w) * h * 4);
check(converter->CopyPixels(nullptr, w * 4, static_cast<UINT>(image.rgba.size()), image.rgba.data()), "Reading pixels");
return image;
}
std::optional<unsigned> metadata_number(IWICMetadataQueryReader *reader, const wchar_t *path) {
PROPVARIANT value;
PropVariantInit(&value);
std::optional<unsigned> result;
if (reader && SUCCEEDED(reader->GetMetadataByName(path, &value))) {
if (value.vt == VT_UI1) result = value.bVal;
else if (value.vt == VT_UI2) result = value.uiVal;
else if (value.vt == VT_UI4) result = value.ulVal;
}
PropVariantClear(&value);
return result;
}
// Every frame of a picture as it shows: a GIF's frames are composited onto its canvas with
// their offsets, transparency and disposal, as a browser plays them. Other formats: frame 1.
std::vector<Frame> decode(const fs::path &file) {
ComPtr<IWICBitmapDecoder> decoder;
check(factory().CreateDecoderFromFilename(file.c_str(), nullptr, GENERIC_READ, WICDecodeMetadataCacheOnDemand, &decoder),
"Opening the picture");
GUID container{};
decoder->GetContainerFormat(&container);
UINT count{};
check(decoder->GetFrameCount(&count), "Counting frames");
std::vector<Frame> frames;
if (container != GUID_ContainerFormatGif || count <= 1) {
ComPtr<IWICBitmapFrameDecode> frame;
check(decoder->GetFrame(0, &frame), "Reading the picture");
frames.push_back({to_rgba(frame.Get()), 0});
return frames;
}
ComPtr<IWICMetadataQueryReader> global;
decoder->GetMetadataQueryReader(&global);
const int width = static_cast<int>(metadata_number(global.Get(), L"/logscrdesc/Width").value_or(0));
const int height = static_cast<int>(metadata_number(global.Get(), L"/logscrdesc/Height").value_or(0));
if (width <= 0 || height <= 0 || width > 4096 || height > 4096) throw std::runtime_error("the GIF has no usable canvas size");
std::vector<unsigned char> canvas(static_cast<std::size_t>(width) * height * 4, 0), saved;
struct Rect { int x{}, y{}, w{}, h{}; } previous;
unsigned previous_disposal{};
for (UINT i = 0; i < count; ++i) {
ComPtr<IWICBitmapFrameDecode> frame;
check(decoder->GetFrame(i, &frame), "Reading a GIF frame");
ComPtr<IWICMetadataQueryReader> reader;
frame->GetMetadataQueryReader(&reader);
const auto piece = to_rgba(frame.Get());
const Rect rect{static_cast<int>(metadata_number(reader.Get(), L"/imgdesc/Left").value_or(0)),
static_cast<int>(metadata_number(reader.Get(), L"/imgdesc/Top").value_or(0)), piece.width, piece.height};
const auto delay = metadata_number(reader.Get(), L"/grctlext/Delay").value_or(10);
const auto disposal = metadata_number(reader.Get(), L"/grctlext/Disposal").value_or(0);
// The previous frame leaves the canvas as its disposal says.
if (i > 0 && previous_disposal == 2)
for (int y = std::max(previous.y, 0); y < std::min(previous.y + previous.h, height); ++y)
for (int x = std::max(previous.x, 0); x < std::min(previous.x + previous.w, width); ++x)
std::fill_n(canvas.begin() + (static_cast<std::size_t>(y) * width + x) * 4, 4, static_cast<unsigned char>(0));
else if (i > 0 && previous_disposal == 3 && !saved.empty())
canvas = saved;
if (disposal == 3) saved = canvas;
for (int y = 0; y < piece.height; ++y)
for (int x = 0; x < piece.width; ++x) {
const int cx = rect.x + x, cy = rect.y + y;
if (cx < 0 || cy < 0 || cx >= width || cy >= height) continue;
const auto *src = piece.rgba.data() + (static_cast<std::size_t>(y) * piece.width + x) * 4;
if (src[3] == 0) continue; // transparent pixels leave what is underneath
std::copy_n(src, 4, canvas.begin() + (static_cast<std::size_t>(cy) * width + cx) * 4);
}
// Browsers show delays under 20 ms as 100 ms.
frames.push_back({{width, height, canvas}, delay < 2 ? 100U : delay * 10U});
previous = rect;
previous_disposal = disposal;
}
return frames;
}
// Scaled to `height` (wide pictures to at most 256 wide), in premultiplied alpha so edges do
// not darken.
Image scale(const Image &image, int height) {
int h = height, w = std::max(1, static_cast<int>(std::lround(static_cast<double>(image.width) * height / image.height)));
if (w > max_side) {
h = std::max(1, static_cast<int>(std::lround(static_cast<double>(h) * max_side / w)));
w = max_side;
}
if (w == image.width && h == image.height) return image;
ComPtr<IWICBitmap> bitmap;
check(factory().CreateBitmapFromMemory(image.width, image.height, GUID_WICPixelFormat32bppRGBA, image.width * 4,
static_cast<UINT>(image.rgba.size()), const_cast<BYTE *>(image.rgba.data()), &bitmap),
"Loading pixels");
ComPtr<IWICFormatConverter> premultiplied;
check(factory().CreateFormatConverter(&premultiplied), "Creating a format converter");
check(premultiplied->Initialize(bitmap.Get(), GUID_WICPixelFormat32bppPRGBA, WICBitmapDitherTypeNone, nullptr, 0,
WICBitmapPaletteTypeCustom), "Premultiplying");
ComPtr<IWICBitmapScaler> scaler;
check(factory().CreateBitmapScaler(&scaler), "Creating a scaler");
check(scaler->Initialize(premultiplied.Get(), w, h,
w < image.width ? WICBitmapInterpolationModeFant : WICBitmapInterpolationModeHighQualityCubic),
"Scaling");
return to_rgba(scaler.Get());
}
// Consecutive identical frames become one, showing for their combined time; more than
// `limit` frames are thinned evenly, each kept frame showing for the ones it replaces.
std::vector<Frame> tidy(std::vector<Frame> frames, std::size_t limit) {
std::vector<Frame> merged;
for (auto &frame : frames) {
if (!merged.empty() && merged.back().image.rgba == frame.image.rgba) merged.back().time += frame.time;
else merged.push_back(std::move(frame));
}
if (merged.size() <= limit) return merged;
std::vector<Frame> thinned;
for (std::size_t i = 0; i < limit; ++i) {
const auto from = i * merged.size() / limit, to = (i + 1) * merged.size() / limit;
Frame kept = std::move(merged[from]);
for (auto j = from + 1; j < to; ++j) kept.time += merged[j].time;
thinned.push_back(std::move(kept));
}
return thinned;
}
std::string emote_name(const fs::path &file) {
std::string name;
for (const char c : file.stem().string())
name += std::isalnum(static_cast<unsigned char>(c)) ? c : '_';
if (name.size() > max_name) name.resize(max_name);
return name.empty() ? std::string("emote") : name;
}
std::string lowered(std::string text) {
for (auto &c : text) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return text;
}
// Rows of frames across the atlas, tallest rows first.
int pack(std::vector<Emote> &emotes, int width) {
std::vector<Frame *> frames;
for (auto &e : emotes)
for (auto &f : e.frames) frames.push_back(&f);
std::stable_sort(frames.begin(), frames.end(), [](const Frame *a, const Frame *b) { return a->image.height > b->image.height; });
constexpr int gap = 1;
int x = 0, y = 0, row = 0;
for (auto *f : frames) {
if (x > 0 && x + f->image.width > width) {
x = 0;
y += row + gap;
row = 0;
}
f->x = x;
f->y = y;
x += f->image.width + gap;
row = std::max(row, f->image.height);
}
return y + row;
}
void write_png(const fs::path &file, int width, int height, const std::vector<unsigned char> &rgba) {
// The PNG encoder takes BGRA everywhere.
std::vector<unsigned char> bgra(rgba);
for (std::size_t i = 0; i < bgra.size(); i += 4) std::swap(bgra[i], bgra[i + 2]);
ComPtr<IWICStream> stream;
check(factory().CreateStream(&stream), "Creating the output file");
check(stream->InitializeFromFilename(file.c_str(), GENERIC_WRITE), "Opening the output file");
ComPtr<IWICBitmapEncoder> encoder;
check(factory().CreateEncoder(GUID_ContainerFormatPng, nullptr, &encoder), "Creating the PNG encoder");
check(encoder->Initialize(stream.Get(), WICBitmapEncoderNoCache), "Starting the PNG");
ComPtr<IWICBitmapFrameEncode> frame;
check(encoder->CreateNewFrame(&frame, nullptr), "Adding the PNG image");
check(frame->Initialize(nullptr), "Starting the PNG image");
check(frame->SetSize(width, height), "Sizing the PNG");
WICPixelFormatGUID format = GUID_WICPixelFormat32bppBGRA;
check(frame->SetPixelFormat(&format), "Choosing the PNG format");
if (format != GUID_WICPixelFormat32bppBGRA) throw std::runtime_error("the PNG encoder does not take 32-bit BGRA");
check(frame->WritePixels(height, width * 4, static_cast<UINT>(bgra.size()), bgra.data()), "Writing the PNG pixels");
check(frame->Commit(), "Finishing the PNG image");
check(encoder->Commit(), "Finishing the PNG");
}
Options parse(int argc, wchar_t **argv) {
Options o;
std::vector<fs::path> positional;
for (int i = 1; i < argc; ++i) {
const std::wstring arg = argv[i];
const auto value = [&]() -> std::wstring {
if (i + 1 >= argc) throw std::runtime_error("missing a value after an option");
return argv[++i];
};
if (arg == L"--size") o.size = std::stoi(value());
else if (arg == L"--max-frames") o.max_frames = static_cast<std::size_t>(std::stoul(value()));
else if (arg == L"--max-width") o.max_width = std::stoi(value());
else if (arg == L"--name") o.name = fs::path(value()).string();
else if (arg == L"--help" || arg == L"-h" || arg == L"/?") throw std::invalid_argument("");
else positional.emplace_back(arg);
}
if (positional.empty() || positional.size() > 2) throw std::invalid_argument("");
o.input = fs::absolute(positional[0]);
o.output = positional.size() == 2 ? fs::absolute(positional[1])
: o.input.parent_path() / (o.input.filename().wstring() + L"_pack");
if (o.size < 8 || o.size > max_side) throw std::runtime_error("--size must be 8 to 256");
if (o.max_width < max_side || o.max_width > max_texture) throw std::runtime_error("--max-width must be 256 to 8192");
if (o.max_frames < 1 || o.max_frames > 512) throw std::runtime_error("--max-frames must be 1 to 512");
return o;
}
int run(const Options &o) {
if (!fs::is_directory(o.input)) throw std::runtime_error("not a folder: " + o.input.string());
std::vector<fs::path> files;
for (const auto &entry : fs::directory_iterator(o.input)) {
const auto extension = lowered(entry.path().extension().string());
if (entry.is_regular_file() && (extension == ".png" || extension == ".gif" || extension == ".jpg" ||
extension == ".jpeg" || extension == ".bmp" || extension == ".webp"))
files.push_back(entry.path());
}
std::sort(files.begin(), files.end());
if (files.empty()) throw std::runtime_error("no .png, .gif, .jpg, .bmp or .webp files in " + o.input.string());
std::vector<Emote> emotes;
std::set<std::string> names, folded;
std::size_t total{}, failed{};
for (const auto &file : files) {
try {
auto name = emote_name(file);
for (int n = 2; names.contains(name); ++n) {
const auto suffix = "_" + std::to_string(n);
name = emote_name(file).substr(0, max_name - suffix.size()) + suffix;
}
auto frames = decode(file);
const bool animated = frames.size() > 1;
for (auto &f : frames) f.image = scale(f.image, o.size);
frames = tidy(std::move(frames), o.max_frames);
if (!animated || frames.size() == 1) frames.front().time = 0;
if (total + frames.size() > max_total_frames)
throw std::runtime_error("the pack would pass ReSkate's " + std::to_string(max_total_frames) + " frames");
total += frames.size();
if (!folded.insert(lowered(name)).second)
std::printf(" note: %s differs from another emote only in case; typing picks the first.\n", name.c_str());
names.insert(name);
std::printf(" :%s: %dx%d%s\n", name.c_str(), frames.front().image.width, frames.front().image.height,
frames.size() > 1 ? (", " + std::to_string(frames.size()) + " frames").c_str() : "");
emotes.push_back({name, file.filename().string(), std::move(frames)});
} catch (const std::exception &e) {
++failed;
std::printf(" skipped %s: %s\n", file.filename().string().c_str(), e.what());
}
}
if (emotes.empty()) throw std::runtime_error("no pictures could be read");
const int height = pack(emotes, o.max_width);
if (height > max_texture)
throw std::runtime_error("the atlas would be " + std::to_string(height) +
" pixels tall (ReSkate takes 8192): use a smaller --size or --max-frames");
std::vector<unsigned char> atlas(static_cast<std::size_t>(o.max_width) * height * 4, 0);
auto emote_list = Json::object();
for (const auto &e : emotes) {
for (const auto &f : e.frames)
for (int y = 0; y < f.image.height; ++y)
std::copy_n(f.image.rgba.data() + static_cast<std::size_t>(y) * f.image.width * 4, f.image.width * 4,
atlas.begin() + (static_cast<std::size_t>(f.y + y) * o.max_width + f.x) * 4);
auto entry = Json::object();
entry["size"] = Json::array({e.frames.front().image.width, e.frames.front().image.height});
if (e.frames.size() == 1) {
entry["pos"] = Json::array({e.frames.front().x, e.frames.front().y});
} else {
auto list = Json::array();
for (const auto &f : e.frames) {
auto frame = Json::object();
frame["time"] = f.time;
frame["pos"] = Json::array({f.x, f.y});
list.push_back(std::move(frame));
}
entry["frames"] = std::move(list);
}
emote_list[e.name] = std::move(entry);
}
fs::create_directories(o.output);
write_png(o.output / "emotes.png", o.max_width, height, atlas);
auto document = Json::object();
document["name"] = o.name;
document["texture"] = "emotes.png";
document["emotes"] = std::move(emote_list);
{
const auto text = document.dump(2);
FILE *out{};
if (_wfopen_s(&out, (o.output / "emotes.json").c_str(), L"wb") != 0 || !out)
throw std::runtime_error("cannot write emotes.json");
std::fwrite(text.data(), 1, text.size(), out);
std::fclose(out);
}
std::printf("\n%zu emotes (%zu frames) in a %dx%d atlas%s.\nWrote %s\\emotes.json and emotes.png.\n"
"Copy both into the repository's assets\\emotes and rebuild ReSkate.dll.\n",
emotes.size(), total, o.max_width, height, failed ? (", " + std::to_string(failed) + " skipped").c_str() : "",
o.output.string().c_str());
return failed ? 2 : 0;
}
} // namespace
int wmain(int argc, wchar_t **argv) {
std::setvbuf(stdout, nullptr, _IONBF, 0); // progress shows as it happens, even through a pipe
const bool own_console = [] {
DWORD processes[2];
return GetConsoleProcessList(processes, 2) == 1; // started by double-click or drag-and-drop
}();
int code = 1;
if (FAILED(CoInitializeEx(nullptr, COINIT_MULTITHREADED))) {
std::printf("Windows Imaging could not start.\n");
} else {
try {
code = run(parse(argc, argv));
} catch (const std::invalid_argument &) {
std::printf("ReSkateEmotePacker: builds ReSkate's chat emote pack from a folder of pictures and GIFs.\n\n"
" ReSkateEmotePacker <folder> [output folder] [--size 56] [--max-frames 64]\n"
" [--max-width 2048] [--name ReSkate]\n\n"
"Each file becomes :FileName: (letters, digits and _). Animated GIFs keep their frames.\n"
"Writes emotes.json and emotes.png (default: <folder>_pack next to the folder).\n");
} catch (const std::exception &e) {
std::printf("Error: %s\n", e.what());
}
CoUninitialize();
}
if (own_console) {
std::printf("\nPress Enter to close.");
std::getchar();
}
return code;
}
+113
View File
@@ -0,0 +1,113 @@
#include "command_registry.h"
#include <array>
namespace dingosdk::console {
std::vector<CompletionToken> completion_tokens(std::string_view input, int cursor) {
const int size = static_cast<int>(input.size());
const int limit = std::clamp(cursor, 0, size);
std::vector<CompletionToken> tokens;
int position = 0;
const auto separator = [](char c) { return c == ' ' || c == '\t'; };
while (position <= limit) {
while (position < limit && separator(input[position]))
++position;
CompletionToken token;
token.begin = position;
char quote = 0;
bool escaped = false;
// Scan the whole token, but decode only its prefix before the cursor.
// This matches execution's escapes and single/double/concatenated quotes.
for (; position < size; ++position) {
const char c = input[position];
if (!escaped && !quote && separator(c))
break;
const bool before_cursor = position < limit;
if (escaped) {
if (before_cursor)
token.text += c;
escaped = false;
} else if (c == '\\')
escaped = true;
else if (quote) {
if (c == quote)
quote = 0;
else if (before_cursor)
token.text += c;
} else if (c == '\'' || c == '"') {
quote = c;
token.quoted = true;
} else if (before_cursor)
token.text += c;
}
token.end = position;
tokens.push_back(std::move(token));
if (position >= limit)
break;
++position;
}
return tokens;
}
std::string lower(std::string_view text) {
std::string result(text);
for (auto &c : result)
if (c >= 'A' && c <= 'Z')
c = static_cast<char>(c + ('a' - 'A'));
return result;
}
bool equal(std::string_view a, std::string_view b) {
return a.size() == b.size() && ascii_case_insensitive_starts_with(a, b);
}
const char *group_name(Group group) noexcept {
constexpr std::array names{"Console", "Movement", "Gameplay", "World",
"Graphics", "Progression", "Objects", "Engine"};
const auto i = static_cast<std::size_t>(group);
return i < names.size() ? names[i] : "Console";
}
std::string value_text(const Value &value) {
return std::visit(
[](const auto &v) -> std::string {
using T = std::decay_t<decltype(v)>;
if constexpr (std::is_same_v<T, std::string>)
return v;
else if constexpr (std::is_same_v<T, bool>)
return v ? "1" : "0";
else {
std::array<char, 64> text{};
const auto result = std::to_chars(text.data(), text.data() + text.size(), v);
return result.ec == std::errc{} ? std::string(text.data(), result.ptr) : "?";
}
},
value);
}
std::optional<Value> parse_value(Type type, std::string_view text) {
if (type == Type::text)
return std::string(text);
if (type == Type::boolean) {
if (text == "1" || equal(text, "true") || equal(text, "on"))
return true;
if (text == "0" || equal(text, "false") || equal(text, "off"))
return false;
return std::nullopt;
}
const auto parse = [&]<class T>() -> std::optional<Value> {
T value{};
const auto result = std::from_chars(text.data(), text.data() + text.size(), value);
if (result.ec != std::errc{} || result.ptr != text.data() + text.size())
return std::nullopt;
if constexpr (std::is_floating_point_v<T>)
if (!std::isfinite(value))
return std::nullopt;
return value;
};
switch (type) {
case Type::integer:
return parse.operator()<std::int64_t>();
case Type::unsigned_integer:
return parse.operator()<std::uint64_t>();
case Type::number:
return parse.operator()<double>();
default:
return std::nullopt;
}
}
} // namespace dingosdk::console
+354
View File
@@ -0,0 +1,354 @@
#pragma once
#include "console_core.h"
#include <algorithm>
#include <charconv>
#include <cmath>
#include <functional>
#include <map>
#include <optional>
#include <set>
#include <stdexcept>
#include <string>
#include <variant>
#include <type_traits>
namespace dingosdk::console {
enum class Group {
console,
movement,
gameplay,
world,
graphics,
progression,
objects,
engine
};
enum class Kind {
action,
variable
};
enum class Execution {
local,
game_thread
};
enum class Type {
text,
boolean,
integer,
unsigned_integer,
number
};
using Value = std::variant<std::string, bool, std::int64_t, std::uint64_t, double>;
using Values = std::vector<Value>;
struct Output {
std::function<void(const std::string &)> write;
std::function<void()> clear;
std::function<void()> history;
void operator()(const std::string &text) const {
if (write)
write(text);
}
};
struct State {
bool available = true;
std::optional<std::string> value;
std::string reason;
std::string detail;
bool overridden = false;
};
std::string lower(std::string_view text);
bool equal(std::string_view a, std::string_view b);
const char *group_name(Group group) noexcept;
std::string value_text(const Value &value);
std::optional<Value> parse_value(Type type, std::string_view text);
struct CompletionToken {
std::string text;
int begin = 0, end = 0;
bool quoted = false;
};
std::vector<CompletionToken> completion_tokens(std::string_view input, int cursor);
struct Suggestion {
std::string text, name, description, usage;
Group group = Group::console;
Kind kind = Kind::action;
State state;
bool argument = false;
};
template <class Context> class Registry {
public:
struct Argument {
std::string name;
Type type = Type::text;
bool optional = false, rest = false;
std::vector<std::string> choices;
std::optional<double> minimum, maximum;
std::function<std::vector<std::string>(const Context &, std::span<const std::string>)> complete;
};
struct Entry {
std::string name, description;
Group group = Group::console;
Kind kind = Kind::action;
Execution execution = Execution::game_thread;
bool echo_input = true;
std::vector<Argument> arguments;
std::vector<std::string> aliases;
std::function<State(const Context &)> inspect;
std::function<void(const Context &, const Values &, const Output &)> run;
std::function<void(const Context &, const Output &)> reset;
};
Registry() = default;
Registry(const Registry &) = delete;
Registry &operator=(const Registry &) = delete;
void add(Entry entry) {
const auto parsed = parse_console_command(entry.name);
if (!parsed || parsed.arguments.empty())
throw std::logic_error("Invalid command name");
bool optional = false;
for (std::size_t i = 0; i < entry.arguments.size(); ++i) {
const auto &arg = entry.arguments[i];
if (arg.name.empty() || (optional && !arg.optional) ||
(arg.rest && (i + 1 != entry.arguments.size() || arg.type != Type::text)))
throw std::logic_error("Invalid command argument schema: " + entry.name);
optional = arg.optional;
}
if (entry.kind == Kind::variable && (entry.arguments.size() != 1 || !entry.inspect))
throw std::logic_error("Variables require one typed argument and a state reader");
std::vector<std::string> names{entry.name};
names.insert(names.end(), entry.aliases.begin(), entry.aliases.end());
std::map<std::string, bool> unique;
for (const auto &name : names) {
const auto key = lower(name);
if (!parse_console_command(name) || name.empty() || lookup_.contains(key) ||
!unique.emplace(key, true).second)
throw std::logic_error("Duplicate/invalid command: " + name);
}
const auto index = entries_.size();
entries_.push_back(std::move(entry));
for (const auto &name : names)
lookup_.emplace(lower(name), index);
}
const std::vector<Entry> &entries() const noexcept {
return entries_;
}
const Entry *find(std::string_view name) const {
const auto it = lookup_.find(lower(name));
return it == lookup_.end() ? nullptr : &entries_[it->second];
}
static std::string usage(const Entry &entry) {
std::string result = entry.name;
for (const auto &arg : entry.arguments) {
const bool optional = arg.optional || entry.kind == Kind::variable;
result += optional ? " [" : " <";
result += arg.name;
if (arg.rest)
result += "...";
result += optional ? "]" : ">";
}
return result;
}
State state(const Entry &entry, const Context &context) const {
return entry.inspect ? entry.inspect(context) : State{};
}
std::string describe(const Entry &entry, const Context &context) const {
const auto current = state(entry, context);
std::string result = entry.name;
if (current.value)
result += " [" + *current.value + "]";
if (!entry.description.empty())
result += " - " + entry.description;
if (!current.available)
result += " | unavailable: " + current.reason;
if (!current.detail.empty())
result += " | " + current.detail;
return result;
}
bool echoes(std::span<const std::string> words) const {
const auto [entry, consumed] = resolve(words);
return !entry || entry->echo_input || consumed != words.size();
}
bool local(std::span<const std::string> words, const Context &context) const {
const auto [entry, consumed] = resolve(words);
return !entry || entry->execution == Execution::local ||
(entry->kind == Kind::variable && consumed == words.size()) || !state(*entry, context).available;
}
bool execute(std::string_view text, const Context &context, const Output &output) const {
const auto parsed = parse_console_command(text);
if (!parsed || parsed.arguments.empty()) {
output("error: Invalid command text; check quoting and length.");
return false;
}
return execute(parsed.arguments, context, output);
}
bool execute(std::span<const std::string> words, const Context &context, const Output &output) const {
const auto [entry, consumed] = resolve(words);
if (!entry) {
output("error: Unknown command. Use help or Tab completion.");
return false;
}
const auto current = state(*entry, context);
if (entry->kind == Kind::variable && consumed == words.size()) {
output(describe(*entry, context));
return true;
}
if (!current.available) {
output("error: " + entry->name + " is unavailable: " + current.reason);
return false;
}
const auto args = words.subspan(consumed);
Values values;
std::size_t index = 0;
for (const auto &arg : entry->arguments) {
if (index == args.size()) {
if (arg.optional)
break;
output("error: Missing " + arg.name + ". Usage: " + usage(*entry));
return false;
}
std::string token = args[index++];
if (arg.rest)
while (index < args.size()) {
token += ' ';
token += args[index++];
}
auto value = parse_value(arg.type, token);
if (!value) {
output("error: Invalid " + arg.name + ": '" + token + "'. Usage: " + usage(*entry));
return false;
}
if (!arg.choices.empty()) {
const auto match = std::find_if(arg.choices.begin(), arg.choices.end(), [&](const auto &choice) {
if (arg.type == Type::text)
return equal(choice, token);
const auto parsed_choice = parse_value(arg.type, choice);
return parsed_choice && *parsed_choice == *value;
});
if (match == arg.choices.end()) {
std::string choices;
for (const auto &choice : arg.choices) {
if (!choices.empty())
choices += ", ";
choices += choice;
}
output("error: " + arg.name + " must be one of: " + choices);
return false;
}
value = parse_value(arg.type, *match);
}
if (arg.minimum || arg.maximum) {
const auto numeric = std::visit(
[](const auto &v) -> double {
using T = std::decay_t<decltype(v)>;
if constexpr (std::is_arithmetic_v<T>)
return static_cast<double>(v);
else
return 0;
},
*value);
if ((arg.minimum && numeric < *arg.minimum) || (arg.maximum && numeric > *arg.maximum)) {
output("error: " + arg.name + " is outside the allowed range. Use help " + entry->name);
return false;
}
}
values.push_back(std::move(*value));
}
if (index != args.size()) {
output("error: Too many arguments. Usage: " + usage(*entry));
return false;
}
if (!entry->run) {
output("error: " + entry->name + " is read-only.");
return false;
}
entry->run(context, values, output);
return true;
}
// words includes the active, possibly empty token; completion only reads Context.
std::vector<Suggestion> complete(std::span<const std::string> words, const Context &context) const {
std::vector<Suggestion> result;
std::set<std::string> emitted;
if (words.empty())
return result;
const auto prefix = words.back();
const auto prior = words.first(words.size() - 1);
auto add = [&](std::string text, const Entry &entry, bool argument) {
if (!ascii_case_insensitive_starts_with(text, prefix))
return;
if (!emitted.insert(lower(text)).second)
return;
result.push_back({std::move(text), entry.name, entry.description, usage(entry), entry.group, entry.kind,
state(entry, context), argument});
};
for (const auto &item : entries_) {
std::vector<std::string> routes{item.name};
// Multiword aliases also supply the menu command routes.
for (const auto &alias : item.aliases)
if (alias.find(' ') != std::string::npos)
routes.push_back(alias);
for (const auto &route : routes) {
const auto path = parse_console_command(route).arguments;
if (prior.size() >= path.size())
continue;
bool matches = true;
for (std::size_t i = 0; i < prior.size(); ++i)
matches = matches && equal(prior[i], path[i]);
if (!matches)
continue;
const auto before = result.size();
add(path[prior.size()], item, false);
if (result.size() == before)
continue;
auto &suggestion = result.back();
if (prior.size() + 1 < path.size()) {
suggestion.name = suggestion.text;
suggestion.description = "Browse " + suggestion.text + " commands";
suggestion.usage = route;
suggestion.kind = Kind::action;
suggestion.state = {};
} else
suggestion.name = route;
}
}
const auto [entry, consumed] = resolve(prior);
if (entry) {
const auto arg_index = prior.size() - consumed;
if (arg_index < entry->arguments.size()) {
const auto &arg = entry->arguments[arg_index];
auto candidates = arg.complete ? arg.complete(context, prior.subspan(consumed)) : arg.choices;
if (candidates.empty() && arg.type == Type::boolean)
candidates = {"0", "1"};
for (const auto &candidate : candidates)
add(candidate, *entry, true);
}
}
std::sort(result.begin(), result.end(),
[](const auto &a, const auto &b) { return lower(a.text) < lower(b.text); });
if (result.size() > console_max_autocomplete_matches)
result.resize(console_max_autocomplete_matches);
return result;
}
private:
std::pair<const Entry *, std::size_t> resolve(std::span<const std::string> words) const {
const Entry *entry = nullptr;
std::size_t consumed = 0;
std::string path;
for (std::size_t i = 0; i < words.size(); ++i) {
if (i)
path += ' ';
path += words[i];
if (const auto *found = find(path)) {
entry = found;
consumed = i + 1;
}
}
return {entry, consumed};
}
std::vector<Entry> entries_;
std::map<std::string, std::size_t> lookup_;
};
} // namespace dingosdk::console
+95
View File
@@ -0,0 +1,95 @@
#include "console_activity.h"
#include <format>
namespace dingosdk {
namespace {
enum class Phase { assets, profile, disconnected, unloading, loading, active, connecting,
sublevel_loading, sublevel_active, shutdown, idle };
Phase phase(unsigned state) {
switch (state) {
case 0: return Phase::assets;
case 1: return Phase::profile;
case 2: return Phase::disconnected;
case 3: case 14: case 22: return Phase::unloading;
case 4: case 5: case 6: case 7: case 8: case 9: case 10: case 11: case 12: return Phase::loading;
case 13: return Phase::active;
case 15: case 23: return Phase::connecting;
case 16: case 17: case 18: case 19: case 20: return Phase::sublevel_loading;
case 21: return Phase::sublevel_active;
case 24: case 25: return Phase::shutdown;
default: return Phase::idle;
}
}
}
void ConsoleActivityFeed::update(const ConsoleActivity& a, Emit sink, std::uint64_t now_ms) {
const ConsoleActivity empty;
const auto& before = previous_ ? *previous_ : empty;
const auto emit = [&](ConsoleSource source, std::string text,
ConsoleSeverity severity = ConsoleSeverity::info) {
if (sink) sink(source, text, severity);
};
const auto current = phase(a.state);
const bool loading = current == Phase::loading || current == Phase::sublevel_loading;
const bool sublevel = current == Phase::sublevel_loading || current == Phase::sublevel_active;
const auto& destination = sublevel ? a.sublevel : a.level;
const bool changed = !previous_ || current != phase(before.state) || a.level != before.level ||
(sublevel && a.sublevel != before.sublevel);
if (changed && !a.native_loading_logging) {
const auto suffix = destination.empty() ? "." : ": " + destination;
if (loading) {
if (!load_started_ || current != phase(before.state)) load_started_ = now_ms;
waiting_reported_ = false;
preparation_reported_ = false;
emit(ConsoleSource::level, std::string(sublevel ? "Loading sublevel" : "Loading world") + suffix);
} else if (current == Phase::active || current == Phase::sublevel_active) {
const auto duration = load_started_ ? std::format(" (load took {:.1f}s)",
static_cast<double>(now_ms - *load_started_) / 1000.0) : "";
emit(ConsoleSource::level, std::string(sublevel ? "Sublevel active" : "World active") + suffix + duration,
ConsoleSeverity::success);
load_started_.reset();
} else {
load_started_.reset();
switch (current) {
case Phase::assets: emit(ConsoleSource::assets, "Loading shared game assets."); break;
case Phase::profile: emit(ConsoleSource::profile, "Applying profile options."); break;
case Phase::disconnected: emit(ConsoleSource::network, "Game reported a lost connection.", ConsoleSeverity::warning); break;
case Phase::unloading: emit(ConsoleSource::level, "Unloading world content" + suffix); break;
case Phase::connecting: emit(ConsoleSource::level, "Connecting to local world" + suffix); break;
case Phase::shutdown: emit(ConsoleSource::runtime, "Game is shutting down."); break;
default: break;
}
}
}
if (!a.native_loading_logging && loading && load_started_ && !waiting_reported_ && now_ms - *load_started_ >= 30000) {
emit(ConsoleSource::level, "Still loading after 30 seconds: " + a.state_name +
(destination.empty() ? "." : " - " + destination), ConsoleSeverity::warning);
waiting_reported_ = true;
}
if (!a.sublevel.empty() && a.sublevel_active && a.client_sublevel_present &&
current != Phase::sublevel_active && (a.sublevel != before.sublevel ||
!before.sublevel_active || !before.client_sublevel_present))
emit(ConsoleSource::level, "Sublevel content ready: " + a.sublevel, ConsoleSeverity::success);
if (a.players_available && (!before.players_available || a.players != before.players) &&
(a.players || before.players)) {
emit(ConsoleSource::player, a.players ? "Local player joined (" + std::to_string(a.players) + ")."
: "Local player left.");
}
if (a.players_available && a.controllables != before.controllables) {
emit(ConsoleSource::player, a.controllables ? "Controllable entity assigned to local player."
: "Local player released its controllable entity.");
}
if (a.setup_attempts > before.setup_attempts && !preparation_reported_) {
emit(ConsoleSource::skater, "Skater initialization started.");
preparation_reported_ = true;
}
if (a.skater != before.skater) {
if (a.skater) emit(ConsoleSource::skater,
std::format("Local skater entity available at ({:.1f}, {:.1f}, {:.1f}).",
a.position[0], a.position[1], a.position[2]), ConsoleSeverity::success);
else emit(ConsoleSource::skater, "Local skater entity no longer available.");
}
if (!a.flow.empty() && a.flow != before.flow)
emit(ConsoleSource::flow, a.flow, a.flow == "InGame" ? ConsoleSeverity::success : ConsoleSeverity::info);
previous_ = a;
}
}
+29
View File
@@ -0,0 +1,29 @@
#pragma once
#include "console_core.h"
#include <array>
namespace dingosdk {
// Values sampled by the existing game-thread observers; no counters that change
// every frame belong in the console. Positions are logged only on entity arrival.
struct ConsoleActivity {
unsigned state{26};
bool native_loading_logging{}; // Detailed native stages replace coarse level messages when available.
std::string state_name, level, sublevel, flow;
bool sublevel_active{}, client_sublevel_present{}, players_available{};
unsigned players{}, controllables{};
std::uint64_t setup_attempts{};
std::uintptr_t skater{};
std::array<float, 3> position{};
};
class ConsoleActivityFeed {
public:
using Emit = void (*)(ConsoleSource, std::string_view, ConsoleSeverity);
void update(const ConsoleActivity& activity, Emit emit, std::uint64_t now_ms = console_timestamp_ms());
private:
std::optional<ConsoleActivity> previous_;
bool preparation_reported_{}, waiting_reported_{};
std::optional<std::uint64_t> load_started_;
};
}
+214
View File
@@ -0,0 +1,214 @@
#include "console_core.h"
#include <chrono>
#include <iomanip>
#include <limits>
#include <sstream>
#include <stdexcept>
namespace dingosdk {
namespace {
bool is_separator(char value) noexcept {
return value == ' ' || value == '\t';
}
bool is_allowed_input_character(unsigned char value) noexcept {
return value == static_cast<unsigned char>('\t') ||
(value >= static_cast<unsigned char>(' ') &&
value <= static_cast<unsigned char>('~'));
}
char ascii_lower(char value) noexcept {
if (value >= 'A' && value <= 'Z') {
return static_cast<char>(value + ('a' - 'A'));
}
return value;
}
} // namespace
CommandParseResult parse_console_command(std::string_view input) {
CommandParseResult result;
if (input.size() > console_max_command_bytes) {
result.error = CommandParseError::input_too_long;
result.error_offset = console_max_command_bytes;
return result;
}
for (std::size_t index = 0; index < input.size(); ++index) {
if (!is_allowed_input_character(static_cast<unsigned char>(input[index]))) {
result.error = CommandParseError::invalid_character;
result.error_offset = index;
return result;
}
}
std::string argument;
argument.reserve(input.size());
bool argument_started = false;
bool escaped = false;
char quote = '\0';
const auto finish_argument = [&]() -> bool {
if (!argument_started) return true;
if (result.arguments.size() == console_max_arguments) {
result.error = CommandParseError::too_many_arguments;
return false;
}
result.arguments.push_back(argument);
argument.clear();
argument_started = false;
return true;
};
for (std::size_t index = 0; index < input.size(); ++index) {
const char value = input[index];
if (escaped) {
argument.push_back(value);
argument_started = true;
escaped = false;
continue;
}
if (value == '\\') {
escaped = true;
argument_started = true;
continue;
}
if (quote != '\0') {
if (value == quote) {
quote = '\0';
} else {
argument.push_back(value);
}
argument_started = true;
continue;
}
if (value == '\'' || value == '"') {
quote = value;
argument_started = true;
continue;
}
if (is_separator(value)) {
if (!finish_argument()) {
result.error_offset = index;
result.arguments.clear();
return result;
}
continue;
}
argument.push_back(value);
argument_started = true;
}
if (escaped) {
result.error = CommandParseError::dangling_escape;
result.error_offset = input.empty() ? 0 : input.size() - 1;
result.arguments.clear();
return result;
}
if (quote != '\0') {
result.error = CommandParseError::unterminated_quote;
result.error_offset = input.size();
result.arguments.clear();
return result;
}
if (!finish_argument()) {
result.error_offset = input.size();
result.arguments.clear();
}
return result;
}
bool ascii_case_insensitive_starts_with(std::string_view value,
std::string_view prefix) noexcept {
if (prefix.size() > value.size()) return false;
for (std::size_t index = 0; index < prefix.size(); ++index) {
if (ascii_lower(value[index]) != ascii_lower(prefix[index])) return false;
}
return true;
}
ConsoleLogBuffer::ConsoleLogBuffer(ConsoleLogLimits limits)
: limits_(limits) {
if (limits_.max_lines == 0 || limits_.max_bytes == 0 ||
limits_.max_line_bytes == 0 || limits_.max_line_bytes > limits_.max_bytes) {
throw std::invalid_argument("Invalid console log limits");
}
}
std::uint64_t console_timestamp_ms() noexcept {
static const auto start = std::chrono::steady_clock::now();
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count());
}
const char* console_source_name(ConsoleSource source) noexcept {
return logging::name(source).data();
}
std::string format_console_line(const ConsoleLogLine& line, bool timestamp) {
std::ostringstream output;
if (timestamp) output << '[' << std::fixed << std::setprecision(3)
<< std::setw(9) << static_cast<double>(line.elapsed_ms) / 1000.0 << "] ";
output << logging::name(line.context) << ": [" << console_source_name(line.source)
<< "] [" << logging::name(line.severity) << "] ";
output << line.text;
return output.str();
}
std::uint64_t ConsoleLogBuffer::append(std::string_view text,
ConsoleSeverity severity, ConsoleSource source, logging::Context context) {
std::lock_guard lock(mutex_);
if (text.empty()) return latest_sequence_;
const auto elapsed_ms = console_timestamp_ms();
std::size_t start = 0;
while (start < text.size()) {
const auto newline = text.find('\n', start);
if (newline == std::string_view::npos) {
append_line_locked(text.substr(start), elapsed_ms, severity, source, context);
break;
}
auto line = text.substr(start, newline - start);
if (!line.empty() && line.back() == '\r') line.remove_suffix(1);
append_line_locked(line, elapsed_ms, severity, source, context);
start = newline + 1;
}
return latest_sequence_;
}
std::vector<ConsoleLogLine> ConsoleLogBuffer::snapshot_after(
std::uint64_t after_sequence) const {
std::lock_guard lock(mutex_);
std::vector<ConsoleLogLine> snapshot;
for (const auto& line : lines_) {
if (line.sequence > after_sequence) snapshot.push_back(line);
}
return snapshot;
}
std::uint64_t ConsoleLogBuffer::latest_sequence() const noexcept {
std::lock_guard lock(mutex_);
return latest_sequence_;
}
void ConsoleLogBuffer::append_line_locked(std::string_view text, std::uint64_t elapsed_ms,
ConsoleSeverity severity, ConsoleSource source, logging::Context context) {
if (latest_sequence_ == (std::numeric_limits<std::uint64_t>::max)()) {
throw std::overflow_error("Console log sequence exhausted");
}
const auto bounded = text.substr(0, limits_.max_line_bytes);
lines_.push_back({++latest_sequence_, std::string(bounded), elapsed_ms, severity, source, context});
stored_bytes_ += bounded.size();
enforce_limits_locked();
}
void ConsoleLogBuffer::enforce_limits_locked() {
while (lines_.size() > limits_.max_lines || stored_bytes_ > limits_.max_bytes) {
stored_bytes_ -= lines_.front().text.size();
lines_.pop_front();
}
}
} // namespace dingosdk
+83
View File
@@ -0,0 +1,83 @@
#pragma once
#include "console_entry.h"
#include <cstddef>
#include <cstdint>
#include <deque>
#include <mutex>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <vector>
namespace dingosdk {
inline constexpr std::size_t console_max_command_bytes = 4096;
inline constexpr std::size_t console_max_arguments = 64;
inline constexpr std::size_t console_max_autocomplete_matches = 256;
enum class CommandParseError {
none,
input_too_long,
invalid_character,
too_many_arguments,
unterminated_quote,
dangling_escape,
};
struct CommandParseResult {
std::vector<std::string> arguments;
CommandParseError error{CommandParseError::none};
std::size_t error_offset{};
explicit operator bool() const noexcept { return error == CommandParseError::none; }
};
// Parses printable ASCII command text. Space and horizontal tab separate
// arguments. Single and double quotes preserve separators, and a backslash
// escapes the following printable character both inside and outside quotes.
CommandParseResult parse_console_command(std::string_view input);
bool ascii_case_insensitive_starts_with(std::string_view value,
std::string_view prefix) noexcept;
struct ConsoleLogLimits {
std::size_t max_lines{2048};
std::size_t max_bytes{1024 * 1024};
std::size_t max_line_bytes{4096};
};
class ConsoleLogBuffer {
public:
explicit ConsoleLogBuffer(ConsoleLogLimits limits = {});
ConsoleLogBuffer(const ConsoleLogBuffer&) = delete;
ConsoleLogBuffer& operator=(const ConsoleLogBuffer&) = delete;
// Splits LF and CRLF input into logical lines. A terminal newline does not
// create an extra line, while explicit adjacent newlines preserve blanks.
// Returns the latest sequence after the append, or the prior sequence for
// empty input.
std::uint64_t append(std::string_view text, ConsoleSeverity severity,
ConsoleSource source, logging::Context context);
// Returns currently retained lines whose sequence is strictly greater than
// after_sequence.
std::vector<ConsoleLogLine> snapshot_after(std::uint64_t after_sequence) const;
std::uint64_t latest_sequence() const noexcept;
private:
void append_line_locked(std::string_view text, std::uint64_t elapsed_ms,
ConsoleSeverity severity, ConsoleSource source, logging::Context context);
void enforce_limits_locked();
const ConsoleLogLimits limits_;
mutable std::mutex mutex_;
std::deque<ConsoleLogLine> lines_;
std::size_t stored_bytes_{};
std::uint64_t latest_sequence_{};
};
} // namespace dingosdk
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include "Engine/Core/Log/types.h"
#include <cstdint>
#include <string>
namespace dingosdk {
using ConsoleSeverity = logging::Level;
using ConsoleSource = logging::Channel;
struct ConsoleLogLine {
std::uint64_t sequence{};
std::string text;
std::uint64_t elapsed_ms{};
ConsoleSeverity severity{ConsoleSeverity::info};
ConsoleSource source{ConsoleSource::runtime};
logging::Context context{logging::default_context(source)};
};
std::uint64_t console_timestamp_ms() noexcept;
const char* console_source_name(ConsoleSource source) noexcept;
std::string format_console_line(const ConsoleLogLine& line, bool timestamp = true);
}
+273
View File
@@ -0,0 +1,273 @@
#include <Windows.h>
#include <DbgHelp.h>
#include "Engine/Core/Debug/backtrace.h"
#include "Engine/Core/Debug/native_dump.h"
#include "Engine/Core/Debug/upload.h"
#include "Engine/Core/Debug/multipart.h"
#include "Engine/Core/Debug/protocol.h"
#include "backtrace_config.h"
#include "Engine/Core/Log/logging.h"
#include <fstream>
#include <format>
#include <iostream>
#include <thread>
using namespace dingosdk;
using namespace dingosdk::backtrace;
namespace {
constexpr DWORD test_exception = 0xe0424242;
void check(bool value, const char* message) { if (!value) throw std::runtime_error(message); }
LONG WINAPI finish_child(EXCEPTION_POINTERS*) { ExitProcess(42); }
std::filesystem::path native_fixture_directory;
LONG WINAPI native_child_handler(EXCEPTION_POINTERS* pointers) {
const auto path = native_fixture_directory / L"game-crash.mdmp";
Handle file(CreateFileW(path.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr));
MINIDUMP_EXCEPTION_INFORMATION exception{GetCurrentThreadId(), pointers, FALSE};
if (file.value == INVALID_HANDLE_VALUE || !MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(),
file.value, MiniDumpNormal, &exception, nullptr, nullptr)) ExitProcess(43);
CloseHandle(file.release());
CopyFileW(path.c_str(), (native_fixture_directory / L"duplicate.dmp").c_str(), TRUE);
ExitProcess(42);
}
std::string read(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary);
return {std::istreambuf_iterator<char>(file), std::istreambuf_iterator<char>()};
}
void endpoint_checks() {
Endpoint endpoint;
check(parse_endpoint(L"https://submit.backtrace.io/team/token/minidump", endpoint), "Hosted endpoint rejected");
check(endpoint.host == L"submit.backtrace.io" && endpoint.port == 443 && endpoint.resource == L"/team/token/minidump", "Hosted URL components incorrect");
check(parse_endpoint(L"https://team.sp.backtrace.io:6098/post?format=minidump&token=test", endpoint), "Listener endpoint rejected");
check(endpoint.port == 6098 && endpoint.resource == L"/post?format=minidump&token=test", "Listener query lost");
for (const auto* value : {L"", L"http://example.com/post", L"https://user:password@example.com/post",
L"https://example.com/post#fragment", L"https://example.com/post\r\nheader", L"https:///post", L"https://example.com"})
check(!parse_endpoint(value, endpoint), "Unsafe endpoint accepted");
std::string id;
check(accepted_response(200, R"({"response":"ok","_rxid":"1234-abcd"})", id) && id == "1234-abcd", "Valid receipt rejected");
for (const auto* response : {R"({"response":"error","_rxid":"1234"})", R"({"response":"ok"})", "<html>OK</html>", R"({"response":"ok","_rxid":"bad\nvalue"})"})
check(!accepted_response(200, response, id), "Unconfirmed submission accepted");
check(!accepted_response(429, R"({"response":"ok","_rxid":"1234"})", id), "Rate limited submission accepted");
check(!accepted_response(500, R"({"response":"ok","_rxid":"1234"})", id), "Failed submission accepted");
}
void attachment_checks(const std::filesystem::path& root) {
const auto directory = root / L"attachments";
std::filesystem::create_directories(directory);
const auto log = directory / L"ReSkate.log", dump = directory / L"fixture.dmp";
const std::string dump_bytes("MDMP\0binary\xff", 12);
const std::string original_log = "session start\r\nlast crash message\r\n";
std::ofstream(dump, std::ios::binary).write(dump_bytes.data(), dump_bytes.size());
std::ofstream(log, std::ios::binary) << original_log;
check(snapshot_log(log, dump) == LogSnapshot::complete, "Log snapshot failed");
std::ofstream(log) << "a later game session";
check(read(log_attachment_path(dump)) == original_log, "Queued attachment changed with live log");
auto attributes = Json::object(); attributes["version"] = "fixture-build"; attributes["attachment.log"] = "complete";
{
MultipartReport body(dump, attributes, "fixture-boundary");
check(body.error() == 0, "Cannot prepare attached report");
std::string wire;
check(body.write([&](const void* data, DWORD bytes) { wire.append(static_cast<const char*>(data), bytes); return true; }), "Cannot stream attached report");
check(wire.size() == body.size(), "Multipart Content-Length does not match transmitted bytes");
// Parse the emitted MIME parts independently to check names, boundaries and raw payloads.
std::map<std::string, std::pair<std::string, std::string>> parts;
const std::string delimiter = "--fixture-boundary";
std::size_t cursor{};
for (;;) {
check(wire.compare(cursor, delimiter.size(), delimiter) == 0, "Malformed MIME delimiter");
cursor += delimiter.size();
if (wire.compare(cursor, 4, "--\r\n") == 0) { check(cursor + 4 == wire.size(), "Trailing MIME data"); break; }
check(wire.compare(cursor, 2, "\r\n") == 0, "Missing MIME header separator"); cursor += 2;
const auto end = wire.find("\r\n\r\n", cursor), next = wire.find("\r\n" + delimiter, end + 4);
check(end != std::string::npos && next != std::string::npos, "Unterminated MIME part");
const auto header = wire.substr(cursor, end - cursor);
const auto name = header.find("name=\"");
check(name != std::string::npos, "Missing MIME field name");
const auto name_end = header.find('"', name + 6);
check(name_end != std::string::npos, "Invalid MIME field name");
check(parts.emplace(header.substr(name + 6, name_end - name - 6),
std::make_pair(header, wire.substr(end + 4, next - end - 4))).second, "Duplicate MIME field");
cursor = next + 2;
}
check(parts.size() == 5 && parts.at("version").second == "fixture-build", "Attributes lost during multipart upload");
check(parts.at("upload_file_minidump").second == dump_bytes, "Dump bytes corrupted");
check(parts.at("attachment_ReSkate.log").second == original_log &&
parts.at("attachment_ReSkate.log").first.find("filename=\"ReSkate.log\"") != std::string::npos, "Log is not a named file attachment");
check(Json::parse(parts.at("attachment_report.json").second).at("version").string() == "fixture-build", "Report details attachment missing");
}
{
MultipartReport body(dump, attributes, "fixture-boundary");
check(!body.write([](const void*, DWORD) { return false; }), "Stream failure ignored");
}
std::filesystem::remove(log_attachment_path(dump));
check(MultipartReport(dump, attributes, "fixture-boundary").error() != 0, "Missing queued log silently dropped");
attributes.erase("attachment.log");
check(MultipartReport(dump, attributes, "fixture-boundary").error() == 0, "Legacy report without logs blocked");
const std::string tail(max_log_bytes, 'x');
std::ofstream(log, std::ios::binary) << "discard this old prefix" << tail;
check(snapshot_log(log, dump) == LogSnapshot::tail && read(log_attachment_path(dump)) == tail, "Large log did not retain bounded tail");
check(std::filesystem::file_size(log) > max_log_bytes, "Original log was truncated");
check(snapshot_log(directory / L"missing.log", directory / L"missing.dmp") == LogSnapshot::unavailable, "Missing source log reported as attached");
}
void queue_checks(const std::filesystem::path& root) {
const auto queue = root / L"queue";
std::filesystem::create_directories(queue);
const auto dump = queue / L"fixture.dmp";
const auto metadata = dump.wstring() + L".json";
std::ofstream(dump) << "synthetic test data";
std::ofstream(metadata) << R"({"version":"original-build","attachment.log":"complete"})";
std::ofstream(log_attachment_path(dump)) << "original-session-log";
const Sender failure = [](const auto&, const auto&, const auto&) { return UploadResult{false, 503, 0, {}}; };
check(upload_pending(L"test", queue, failure) == 0 && std::filesystem::exists(dump), "Failed upload did not retain dump");
check(read(log_attachment_path(dump)) == "original-session-log", "Failed upload lost log attachment");
const Sender success = [](const auto&, const auto& queued_dump, const Json& attributes) {
check(attributes.at("version").string() == "original-build", "Queued report lost original build attributes");
check(read(log_attachment_path(queued_dump)) == "original-session-log", "Retry lost original session log");
return UploadResult{true, 200, 0, "1234-abcd"};
};
Handle lock(CreateFileW((queue / L"queue.lock").c_str(), GENERIC_WRITE, 0, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr));
check(upload_pending(L"test", queue, success) == 0 && std::filesystem::exists(dump), "Queue lock failed");
CloseHandle(lock.release());
check(upload_pending(L"test", queue, success) == 1, "Confirmed upload not counted");
check(!std::filesystem::exists(dump) && !std::filesystem::exists(metadata) && !std::filesystem::exists(log_attachment_path(dump)), "Confirmed report or attachment not removed");
check(queue_directory(root, L"https://a/token/minidump") != queue_directory(root, L"https://b/token/minidump"), "Different destinations share queue");
for (std::size_t i = 0; i < max_pending_reports + 2; ++i) {
const auto path = queue / std::format(L"{}.dmp", i);
std::ofstream(path) << "synthetic test data";
std::ofstream(path.wstring() + L".json") << R"({"version":"original-build"})";
std::ofstream(log_attachment_path(path)) << "retention-fixture";
}
upload_pending(L"test", queue, failure);
std::size_t count{};
for (const auto& entry : std::filesystem::directory_iterator(queue)) {
if (entry.path().extension() == L".dmp") ++count;
if (entry.path().extension() == L".log") {
auto owner = entry.path(); owner.replace_extension();
check(std::filesystem::exists(owner), "Retention orphaned a log attachment");
}
}
check(count == max_pending_reports, "Queue retention limit not enforced");
const auto expired = queue / L"expired.dmp";
std::ofstream(expired) << "expired";
std::ofstream(expired.wstring() + L".json") << "{}";
std::ofstream(log_attachment_path(expired)) << "expired-session-log";
std::filesystem::last_write_time(expired, std::filesystem::file_time_type::clock::now() - std::chrono::hours(24 * 15));
upload_pending(L"test", queue, failure);
check(!std::filesystem::exists(expired) && !std::filesystem::exists(log_attachment_path(expired)), "Expired report or attachment retained");
}
void crash_check(const std::filesystem::path& root, bool native) {
std::filesystem::create_directories(root);
std::array<wchar_t, 32768> executable{};
GetModuleFileNameW(nullptr, executable.data(), static_cast<DWORD>(executable.size()));
std::filesystem::path child_path(executable.data());
if (native) {
child_path = root / L"Skate.exe";
std::filesystem::copy_file(executable.data(), child_path);
std::filesystem::copy_file(std::filesystem::path(executable.data()).parent_path() / L"ReSkateLauncher.exe", root / L"ReSkateLauncher.exe");
}
auto command = std::format(L"\"{}\" {} \"{}\"", child_path.wstring(), native ? L"--native-crash" : L"--crash", root.wstring());
STARTUPINFOW startup{sizeof(startup)};
PROCESS_INFORMATION process{};
check(CreateProcessW(child_path.c_str(), command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr, &startup, &process) != FALSE, "Cannot start crash fixture");
Handle child(process.hProcess), thread(process.hThread);
check(WaitForSingleObject(child.value, 30000) == WAIT_OBJECT_0, "Crash fixture timed out");
DWORD code{}; GetExitCodeProcess(child.value, &code);
check(code == 42, "Previous exception handler was not preserved");
const auto log = read(root / L"ReSkate.log");
check(log.find("Backtrace crash reporting enabled") != std::string::npos, "Helper did not start");
check((log.find("ReSkate unhandled exception") != std::string::npos) != native, "Fixture did not exercise the expected exception handler");
const auto queue = queue_directory(root / L"crashes", L"https://127.0.0.1:1/test/minidump");
std::filesystem::path dump;
for (int attempt = 0; attempt < 100 && dump.empty(); ++attempt) {
for (const auto& entry : std::filesystem::directory_iterator(queue)) if (entry.path().extension() == L".dmp") dump = entry.path();
if (dump.empty()) std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
check(!dump.empty(), "No out-of-process minidump captured");
auto bytes = read(dump);
MINIDUMP_DIRECTORY* stream{}; void* data{}; ULONG size{};
check(MiniDumpReadDumpStream(bytes.data(), ExceptionStream, &stream, &data, &size) != FALSE, "Minidump exception stream missing");
const auto* exception = static_cast<const MINIDUMP_EXCEPTION_STREAM*>(data);
check(size >= sizeof(*exception) && exception->ExceptionRecord.ExceptionCode == test_exception, "Wrong exception captured");
check(MiniDumpReadDumpStream(bytes.data(), ModuleListStream, &stream, &data, &size) != FALSE, "Minidump module list missing");
check(Json::parse(read(dump.wstring() + L".json")).at("application").string() == "ReSkate", "Crash attributes missing");
check(Json::parse(read(dump.wstring() + L".json")).at("capture.source").string() ==
(native ? "game-native" : "unhandled-exception"), "Wrong capture source");
check(Json::parse(read(dump.wstring() + L".json")).at("attachment.log").string() == "complete" &&
read(log_attachment_path(dump)) == log, "Crash did not snapshot its session log");
if (native) {
const auto source = root / L"local" / L"ReSkate" / L"Game" / L"Skate" / L"CrashDumps";
const auto pid = GetProcessId(child.value), created = process_creation_seconds(child.value);
const auto selected = find_native_dump(source, pid, created);
check(selected && selected->path.extension() == L".mdmp", "Native dump selection failed");
check(!find_native_dump(source, pid + 1, created), "Imported a different process's dump");
check(!find_native_dump(source, pid, created - 1), "Imported a different lifetime of the same PID");
check(std::filesystem::exists(source / L"game-crash.mdmp"), "Original game dump was removed");
std::filesystem::create_directories(root / L"invalid");
std::ofstream(root / L"invalid" / L"bad.mdmp") << "MDMP";
check(!find_native_dump(root / L"invalid", pid, created), "Truncated native dump accepted");
}
// Allow the helper's deliberately refused loopback upload to finish before fixture cleanup.
for (int i = 0; i < 100 && !std::filesystem::exists(queue / L"status.txt"); ++i) std::this_thread::sleep_for(std::chrono::milliseconds(100));
check(std::filesystem::exists(dump), "Offline dump was lost");
}
}
int wmain(int argc, wchar_t** argv) {
SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX);
try {
if (argc == 5 && std::wstring_view(argv[1]) == L"--inspect-native") {
const auto dump = find_native_dump(argv[2], static_cast<DWORD>(std::stoul(argv[3])), static_cast<DWORD>(std::stoul(argv[4])));
check(dump.has_value(), "No native dump matched this process lifetime");
std::cout << "Matched native crash: process " << dump->process_id << "; exception 0x" << std::hex
<< dump->exception_code << "; address 0x" << dump->exception_address << '\n';
return 0;
}
if (argc == 3 && std::wstring_view(argv[1]) == L"--submit-fixture") {
const std::filesystem::path dump(argv[2]);
auto bytes = read(dump);
check(bytes.size() >= sizeof(MINIDUMP_HEADER), "Not a minidump fixture");
MINIDUMP_DIRECTORY* stream{}; void* data{}; ULONG size{};
check(MiniDumpReadDumpStream(bytes.data(), ExceptionStream, &stream, &data, &size) != FALSE &&
size >= sizeof(MINIDUMP_EXCEPTION_STREAM) &&
static_cast<const MINIDUMP_EXCEPTION_STREAM*>(data)->ExceptionRecord.ExceptionCode == test_exception,
"Only synthetic regression-test crashes may be submitted by this command");
auto attributes = Json::parse(read(dump.wstring() + L".json"));
attributes["integration.test"] = "true";
attributes["error.message"] = "Synthetic ReSkate Backtrace integration test; no game crash";
auto url = environment(L"RESKATE_BACKTRACE_URL");
if (url.empty()) url = configured_url;
const auto result = upload_report(url, dump, attributes);
std::cout << "HTTP " << result.http_status << "; Windows error " << result.error << "; report " << result.report_id << '\n';
check(result.accepted, "Backtrace did not confirm test report");
return 0;
}
if (argc == 3 && (std::wstring_view(argv[1]) == L"--crash" || std::wstring_view(argv[1]) == L"--native-crash")) {
const bool native = std::wstring_view(argv[1]) == L"--native-crash";
if (native) {
const auto local = std::filesystem::path(argv[2]) / L"local";
SetEnvironmentVariableW(L"LOCALAPPDATA", local.c_str());
native_fixture_directory = local / L"ReSkate" / L"Game" / L"Skate" / L"CrashDumps";
std::filesystem::create_directories(native_fixture_directory);
}
SetEnvironmentVariableW(L"RESKATE_CRASH_REPORTING", L"1");
SetEnvironmentVariableW(L"RESKATE_BACKTRACE_URL", L"https://127.0.0.1:1/test/minidump");
SetUnhandledExceptionFilter(finish_child);
logging::Options options; options.directory = argv[2]; options.truncate_file = true;
check(logging::initialize(options), "Fixture logging failed");
if (native) SetUnhandledExceptionFilter(native_child_handler); // Reproduce the game's later handler registration.
RaiseException(test_exception, EXCEPTION_NONCONTINUABLE, 0, nullptr);
return 3;
}
check(argc == 2, "Pass a fixture root");
const auto root = std::filesystem::path(argv[1]) / std::format(L"run-{}-{}", GetCurrentProcessId(), GetTickCount64());
std::filesystem::create_directories(root);
SetEnvironmentVariableW(L"RESKATE_CRASH_REPORTING", L"0");
check(start(root) == StartResult::disabled, "Explicit opt-out ignored");
SetEnvironmentVariableW(L"RESKATE_CRASH_REPORTING", L"1");
SetEnvironmentVariableW(L"RESKATE_BACKTRACE_URL", L"http://invalid/post");
check(start(root) == StartResult::failed, "Invalid endpoint enabled reporter");
endpoint_checks();
attachment_checks(root);
queue_checks(root);
crash_check(root / L"direct", false);
crash_check(root / L"native", true);
std::cout << "Backtrace endpoint, receipts, multipart attachments, log snapshots, retries, retention, opt-out, exception chaining, direct capture and native-handler fallback checks passed.\n";
} catch (const std::exception& error) { std::cerr << error.what() << '\n'; return 1; }
}
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <Windows.h>
#include <filesystem>
namespace dingosdk::backtrace {
enum class StartResult { disabled, ready, failed };
// Called outside DllMain. A missing endpoint or RESKATE_CRASH_REPORTING=0 disables reporting.
StartResult start(const std::filesystem::path& log_directory) noexcept;
void stop() noexcept;
// Last-chance path: only signals the prestarted helper and waits at most 15 seconds.
void capture(EXCEPTION_POINTERS* exception) noexcept;
}
+123
View File
@@ -0,0 +1,123 @@
#include "backtrace.h"
#include "protocol.h"
#include "backtrace_config.h"
#include <algorithm>
#include <format>
#include <vector>
namespace dingosdk::backtrace {
namespace {
std::array<HANDLE, handle_count> handles{};
HANDLE helper{};
SharedReport* report{};
// 0 = ready, 1 = capture owns the IPC, 2 = stopped/initializing.
volatile LONG capturing{2};
struct View {
void* value{};
~View() { if (value) UnmapViewOfFile(value); }
};
template<std::size_t N> bool copy(std::array<wchar_t, N>& target, const std::wstring& value) {
if (value.size() >= N) return false;
std::copy(value.begin(), value.end(), target.begin());
target[value.size()] = 0;
return true;
}
}
StartResult start(const std::filesystem::path& log_directory) noexcept {
try {
if (report) return StartResult::ready;
if (environment(L"RESKATE_CRASH_REPORTING") == L"0") return StartResult::disabled;
auto url = environment(L"RESKATE_BACKTRACE_URL");
if (url.empty()) url = configured_url;
if (url.empty()) return StartResult::disabled;
if (!url.starts_with(L"https://") || url.size() >= SharedReport{}.url.size()) return StartResult::failed;
HMODULE module{};
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(&handles), &module)) return StartResult::failed;
std::array<wchar_t, 32768> module_path{}, process_path{};
const auto module_size = GetModuleFileNameW(module, module_path.data(), static_cast<DWORD>(module_path.size()));
const auto process_size = GetModuleFileNameW(nullptr, process_path.data(), static_cast<DWORD>(process_path.size()));
if (!module_size || module_size >= module_path.size() || !process_size || process_size >= process_path.size())
return StartResult::failed;
const auto executable = std::filesystem::path(module_path.data()).parent_path() / L"ReSkateLauncher.exe";
SECURITY_ATTRIBUTES security{sizeof(security), nullptr, TRUE};
std::array<Handle, handle_count> owned;
owned[mapping].value = CreateFileMappingW(INVALID_HANDLE_VALUE, &security, PAGE_READWRITE, 0, sizeof(SharedReport), nullptr);
for (std::size_t i = requested; i <= ready; ++i) owned[i].value = CreateEventW(&security, TRUE, FALSE, nullptr);
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(), GetCurrentProcess(), &owned[parent].value,
PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | SYNCHRONIZE, TRUE, 0)) return StartResult::failed;
std::array<HANDLE, handle_count> inherited{};
for (std::size_t i = 0; i < handle_count; ++i) {
if (!owned[i].value) return StartResult::failed;
inherited[i] = owned[i].value;
}
View view{MapViewOfFile(owned[mapping].value, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(SharedReport))};
if (!view.value) return StartResult::failed;
auto* shared = new (view.value) SharedReport{};
if (!copy(shared->url, url) || !copy(shared->directory, std::filesystem::absolute(log_directory / L"crashes").wstring()) ||
!copy(shared->application, std::filesystem::path(process_path.data()).filename().wstring())) return StartResult::failed;
// Frostbite handles some fatal exceptions itself and writes a native dump
// before exiting, so those faults never reach our last-chance filter. The
// game's LocalAppData is redirected to ReSkate\Game (Extension/Boot/user_data_redirect.cpp).
if (_wcsicmp(shared->application.data(), L"Skate.exe") == 0) {
const auto local = environment(L"LOCALAPPDATA");
if (!local.empty() && !copy(shared->native_directory,
(std::filesystem::path(local) / L"ReSkate" / L"Game" / L"Skate" / L"CrashDumps").wstring()))
return StartResult::failed;
}
SIZE_T size{};
InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
std::vector<unsigned char> storage(size);
auto* attributes = reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(storage.data());
if (!InitializeProcThreadAttributeList(attributes, 1, 0, &size)) return StartResult::failed;
struct Attributes { LPPROC_THREAD_ATTRIBUTE_LIST value; ~Attributes() { DeleteProcThreadAttributeList(value); } } cleanup{attributes};
if (!UpdateProcThreadAttribute(attributes, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
inherited.data(), sizeof(inherited), nullptr, nullptr)) return StartResult::failed;
auto command = std::format(L"\"{}\" --reskate-crash-helper", executable.wstring());
for (const auto handle : inherited) command += std::format(L" {}", reinterpret_cast<ULONG_PTR>(handle));
STARTUPINFOEXW startup{};
startup.StartupInfo.cb = sizeof(startup);
startup.lpAttributeList = attributes;
PROCESS_INFORMATION process{};
if (!CreateProcessW(executable.c_str(), command.data(), nullptr, nullptr, TRUE,
CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT, nullptr, executable.parent_path().c_str(),
&startup.StartupInfo, &process)) return StartResult::failed;
Handle child(process.hProcess), thread(process.hThread);
// Prevent later child processes from inheriting our IPC and parent handles.
for (const auto handle : inherited) SetHandleInformation(handle, HANDLE_FLAG_INHERIT, 0);
const HANDLE wait_for[]{owned[ready].value, child.value};
if (WaitForMultipleObjects(2, wait_for, FALSE, 5000) != WAIT_OBJECT_0) {
SetEvent(owned[stopping].value);
return StartResult::failed;
}
for (std::size_t i = 0; i < handle_count; ++i) handles[i] = owned[i].release();
helper = child.release();
report = shared;
view.value = nullptr;
InterlockedExchange(&capturing, 0);
return StartResult::ready;
} catch (...) { return StartResult::failed; }
}
void capture(EXCEPTION_POINTERS* exception) noexcept {
if (!exception || InterlockedCompareExchange(&capturing, 1, 0) != 0) return;
report->thread_id = GetCurrentThreadId();
report->exception = exception;
MemoryBarrier();
if (SetEvent(handles[requested])) {
const HANDLE wait_for[]{handles[captured], helper};
WaitForMultipleObjects(2, wait_for, FALSE, capture_timeout_ms);
}
}
void stop() noexcept {
// An exception already using these handles owns them until process exit.
// Never unmap its report or close its events from a concurrent shutdown.
if (InterlockedCompareExchange(&capturing, 2, 0) != 0) return;
SetEvent(handles[stopping]);
UnmapViewOfFile(report);
report = nullptr;
for (auto& handle : handles) { CloseHandle(handle); handle = nullptr; }
CloseHandle(helper);
helper = nullptr;
}
}
+88
View File
@@ -0,0 +1,88 @@
#include "multipart.h"
#include "upload.h"
#include <algorithm>
#include <format>
#include <limits>
namespace dingosdk::backtrace {
namespace {
bool stream_file(HANDLE file, DWORD bytes, const std::function<bool(const void*, DWORD)>& sink) {
std::array<char, 65536> buffer{};
while (bytes) {
DWORD read{};
const auto wanted = std::min<DWORD>(bytes, static_cast<DWORD>(buffer.size()));
if (!ReadFile(file, buffer.data(), wanted, &read, nullptr) || read != wanted || !sink(buffer.data(), read)) return false;
bytes -= read;
}
return true;
}
bool file_length(HANDLE file, std::uintmax_t maximum, DWORD& bytes) {
LARGE_INTEGER length{};
if (file == INVALID_HANDLE_VALUE || !GetFileSizeEx(file, &length) || length.QuadPart < 0 ||
static_cast<std::uintmax_t>(length.QuadPart) > maximum) return false;
bytes = static_cast<DWORD>(length.QuadPart);
return true;
}
}
std::filesystem::path log_attachment_path(const std::filesystem::path& dump) { return dump.wstring() + L".log"; }
LogSnapshot snapshot_log(const std::filesystem::path& source, const std::filesystem::path& dump) noexcept {
try {
Handle input(CreateFileW(source.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
LARGE_INTEGER length{};
if (input.value == INVALID_HANDLE_VALUE || !GetFileSizeEx(input.value, &length) || length.QuadPart < 0)
return LogSnapshot::unavailable;
const bool truncated = length.QuadPart > max_log_bytes;
const auto bytes = static_cast<DWORD>(std::min<LONGLONG>(length.QuadPart, max_log_bytes));
LARGE_INTEGER start{}; start.QuadPart = length.QuadPart - bytes;
if (!SetFilePointerEx(input.value, start, nullptr, FILE_BEGIN)) return LogSnapshot::unavailable;
const auto destination = log_attachment_path(dump);
Handle output(CreateFileW(destination.c_str(), GENERIC_WRITE, 0, nullptr, CREATE_NEW, FILE_ATTRIBUTE_NORMAL, nullptr));
if (output.value == INVALID_HANDLE_VALUE) return LogSnapshot::unavailable;
const auto saved = stream_file(input.value, bytes, [&](const void* data, DWORD count) {
DWORD written{};
return WriteFile(output.value, data, count, &written, nullptr) && written == count;
}) && FlushFileBuffers(output.value);
CloseHandle(output.release());
if (!saved) { DeleteFileW(destination.c_str()); return LogSnapshot::unavailable; }
return truncated ? LogSnapshot::tail : LogSnapshot::complete;
} catch (...) { return LogSnapshot::unavailable; }
}
MultipartReport::MultipartReport(const std::filesystem::path& dump, const Json& attributes, const std::string& boundary) {
if (!attributes.is_object() || boundary.empty() || boundary.size() > 70 ||
boundary.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-") != std::string::npos) {
error_ = ERROR_INVALID_PARAMETER; return;
}
dump_.value = CreateFileW(dump.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (!file_length(dump_.value, max_dump_bytes, dump_bytes_) || !dump_bytes_) { error_ = ERROR_FILE_INVALID; return; }
for (const auto& [key, value] : attributes.items()) {
if (key.empty() || key.size() > 128 || key.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-") != std::string::npos ||
!value.is_string() || value.string().size() > 4096 || value.string().find(boundary) != std::string::npos) {
error_ = ERROR_INVALID_DATA; return;
}
prefix_ += std::format("--{}\r\nContent-Disposition: form-data; name=\"{}\"\r\n\r\n{}\r\n", boundary, key, value.string());
if (prefix_.size() > 65536) { error_ = ERROR_INVALID_DATA; return; }
}
prefix_ += std::format("--{}\r\nContent-Disposition: form-data; name=\"upload_file_minidump\"; filename=\"crash.dmp\"\r\nContent-Type: application/octet-stream\r\n\r\n", boundary);
const auto metadata = attributes.dump();
if (metadata.size() > 65536) { error_ = ERROR_INVALID_DATA; return; }
attachments_ = std::format("\r\n--{}\r\nContent-Disposition: form-data; name=\"attachment_report.json\"; filename=\"report.json\"\r\nContent-Type: application/json\r\n\r\n{}", boundary, metadata);
const auto log_status = attributes.value("attachment.log", "unavailable");
if (log_status == "complete" || log_status == "tail") {
log_.value = CreateFileW(log_attachment_path(dump).c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
// If a queued attachment becomes unreadable, retain the report for retry
// instead of confirming an upload that silently omitted its log.
if (!file_length(log_.value, max_log_bytes, log_bytes_)) { error_ = ERROR_FILE_INVALID; return; }
attachments_ += std::format("\r\n--{}\r\nContent-Disposition: form-data; name=\"attachment_ReSkate.log\"; filename=\"ReSkate.log\"\r\nContent-Type: text/plain\r\n\r\n", boundary);
}
suffix_ = std::format("\r\n--{}--\r\n", boundary);
const auto total = prefix_.size() + static_cast<std::uint64_t>(dump_bytes_) + attachments_.size() + log_bytes_ + suffix_.size();
if (total > std::numeric_limits<DWORD>::max()) { error_ = ERROR_FILE_TOO_LARGE; return; }
size_ = static_cast<DWORD>(total);
}
bool MultipartReport::write(const std::function<bool(const void*, DWORD)>& sink) {
return !error_ && sink(prefix_.data(), static_cast<DWORD>(prefix_.size())) && stream_file(dump_.value, dump_bytes_, sink) &&
sink(attachments_.data(), static_cast<DWORD>(attachments_.size())) && stream_file(log_.value, log_bytes_, sink) &&
sink(suffix_.data(), static_cast<DWORD>(suffix_.size()));
}
}
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include "protocol.h"
#include "Engine/Core/Json/json.h"
#include <filesystem>
#include <functional>
namespace dingosdk::backtrace {
inline constexpr DWORD max_log_bytes = 8 * 1024 * 1024;
enum class LogSnapshot { unavailable, complete, tail };
std::filesystem::path log_attachment_path(const std::filesystem::path& dump);
// Snapshot before publishing the dump; retrying must never read a later session's log.
LogSnapshot snapshot_log(const std::filesystem::path& source, const std::filesystem::path& dump) noexcept;
// Keeps the queued files open and unchanged until the complete request is sent.
class MultipartReport {
public:
MultipartReport(const std::filesystem::path& dump, const Json& attributes, const std::string& boundary);
DWORD error() const { return error_; }
DWORD size() const { return size_; }
bool write(const std::function<bool(const void*, DWORD)>& sink);
private:
Handle dump_, log_;
DWORD dump_bytes_{}, log_bytes_{}, size_{}, error_{};
std::string prefix_, attachments_, suffix_;
};
}
+75
View File
@@ -0,0 +1,75 @@
#include "native_dump.h"
#include "protocol.h"
#include "upload.h"
#include <DbgHelp.h>
#include <cstring>
#include <span>
namespace dingosdk::backtrace {
namespace {
template<class T> bool read(std::span<const std::byte> bytes, std::size_t offset, T& value) {
if (offset > bytes.size() || sizeof(T) > bytes.size() - offset) return false;
std::memcpy(&value, bytes.data() + offset, sizeof(T));
return true;
}
std::optional<NativeDump> inspect(const std::filesystem::path& path, DWORD process_id, DWORD process_created) {
Handle file(CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr));
LARGE_INTEGER size{};
if (file.value == INVALID_HANDLE_VALUE || !GetFileSizeEx(file.value, &size) ||
size.QuadPart < sizeof(MINIDUMP_HEADER) || static_cast<std::uintmax_t>(size.QuadPart) > max_dump_bytes) return {};
Handle mapping(CreateFileMappingW(file.value, nullptr, PAGE_READONLY, 0, 0, nullptr));
if (!mapping.value) return {};
auto* data = static_cast<const std::byte*>(MapViewOfFile(mapping.value, FILE_MAP_READ, 0, 0, 0));
if (!data) return {};
struct View { const void* data; ~View() { UnmapViewOfFile(data); } } view{data};
const std::span bytes(data, static_cast<std::size_t>(size.QuadPart));
MINIDUMP_HEADER header{};
if (!read(bytes, 0, header) || header.Signature != MINIDUMP_SIGNATURE ||
(header.Version & 0xffff) != MINIDUMP_VERSION || header.NumberOfStreams > 128 ||
header.TimeDateStamp < process_created) return {};
MINIDUMP_MISC_INFO misc{};
MINIDUMP_EXCEPTION_STREAM exception{};
bool have_misc{}, have_exception{};
for (DWORD i = 0; i < header.NumberOfStreams; ++i) {
MINIDUMP_DIRECTORY entry{};
if (!read(bytes, static_cast<std::size_t>(header.StreamDirectoryRva) + i * sizeof(entry), entry) ||
entry.Location.Rva > bytes.size() || entry.Location.DataSize > bytes.size() - entry.Location.Rva) return {};
if (entry.StreamType == MiscInfoStream && entry.Location.DataSize >= sizeof(misc))
have_misc = read(bytes, entry.Location.Rva, misc);
if (entry.StreamType == ExceptionStream && entry.Location.DataSize >= sizeof(exception))
have_exception = read(bytes, entry.Location.Rva, exception);
}
constexpr DWORD required = MINIDUMP_MISC1_PROCESS_ID | MINIDUMP_MISC1_PROCESS_TIMES;
if (!have_misc || !have_exception || (misc.Flags1 & required) != required || misc.ProcessId != process_id ||
misc.ProcessCreateTime != process_created || !exception.ThreadId || !exception.ExceptionRecord.ExceptionCode) return {};
return NativeDump{path, misc.ProcessId, misc.ProcessCreateTime, exception.ThreadId,
exception.ExceptionRecord.ExceptionCode, header.TimeDateStamp, exception.ExceptionRecord.ExceptionAddress};
}
}
DWORD process_creation_seconds(HANDLE process) noexcept {
FILETIME created{}, exited{}, kernel{}, user{};
if (!GetProcessTimes(process, &created, &exited, &kernel, &user)) return 0;
ULARGE_INTEGER time{};
time.LowPart = created.dwLowDateTime; time.HighPart = created.dwHighDateTime;
constexpr auto epoch = 11644473600ull;
const auto seconds = time.QuadPart / 10000000ull;
return seconds >= epoch ? static_cast<DWORD>(seconds - epoch) : 0;
}
std::optional<NativeDump> find_native_dump(const std::filesystem::path& directory,
DWORD process_id, DWORD process_created) noexcept {
try {
if (directory.empty() || !process_id || !process_created) return {};
std::optional<NativeDump> selected;
for (const auto& entry : std::filesystem::directory_iterator(directory)) {
const auto extension = entry.path().extension();
if ((extension != L".mdmp" && extension != L".dmp") || !entry.is_regular_file()) continue;
auto candidate = inspect(entry.path(), process_id, process_created);
if (!candidate) continue;
if (!selected || candidate->timestamp < selected->timestamp ||
(candidate->timestamp == selected->timestamp && extension == L".mdmp" && selected->path.extension() != L".mdmp"))
selected = std::move(candidate);
}
return selected;
} catch (...) { return {}; }
}
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <Windows.h>
#include <filesystem>
#include <optional>
namespace dingosdk::backtrace {
struct NativeDump {
std::filesystem::path path;
DWORD process_id{}, process_created{}, thread_id{}, exception_code{}, timestamp{};
ULONG64 exception_address{};
};
// Match the exact process lifetime, not filenames or modification times. The game
// can emit several dumps for one fault; choose the first, preferring its rich mdmp.
std::optional<NativeDump> find_native_dump(const std::filesystem::path& directory,
DWORD process_id, DWORD process_created) noexcept;
DWORD process_creation_seconds(HANDLE process) noexcept;
}
+34
View File
@@ -0,0 +1,34 @@
#pragma once
#include <Windows.h>
#include <array>
#include <string>
namespace dingosdk::backtrace {
inline constexpr DWORD protocol_version = 2;
inline constexpr DWORD capture_timeout_ms = 15000;
struct SharedReport {
DWORD version{protocol_version};
DWORD thread_id{};
EXCEPTION_POINTERS* exception{}; // Address in the parent; MiniDumpWriteDump uses ClientPointers=TRUE.
std::array<wchar_t, 4096> url{};
std::array<wchar_t, 32768> directory{};
std::array<wchar_t, 260> application{};
std::array<wchar_t, 32768> native_directory{};
};
// Explicitly inherited unnamed handles; no token or paths on the command line.
enum HandleIndex : std::size_t { mapping, requested, captured, stopping, ready, parent, handle_count };
struct Handle {
HANDLE value{};
Handle() = default;
explicit Handle(HANDLE handle) : value(handle) {}
~Handle() { if (value && value != INVALID_HANDLE_VALUE) CloseHandle(value); }
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
HANDLE release() { const auto result = value; value = nullptr; return result; }
};
inline std::wstring environment(const wchar_t* name) {
std::array<wchar_t, 32768> buffer{};
const auto size = GetEnvironmentVariableW(name, buffer.data(), static_cast<DWORD>(buffer.size()));
return size && size < buffer.size() ? std::wstring(buffer.data(), size) : std::wstring{};
}
}
+165
View File
@@ -0,0 +1,165 @@
#include <Windows.h>
#include <winhttp.h>
#include <bcrypt.h>
#include "upload.h"
#include "protocol.h"
#include "multipart.h"
#include <algorithm>
#include <chrono>
#include <fstream>
#include <format>
#include <vector>
namespace dingosdk::backtrace {
namespace {
struct Internet {
HINTERNET value{};
~Internet() { if (value) WinHttpCloseHandle(value); }
};
std::string utf8(std::wstring_view value) {
const auto size = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value.data(), static_cast<int>(value.size()), nullptr, 0, nullptr, nullptr);
if (!size) throw std::runtime_error("Invalid text");
std::string result(static_cast<std::size_t>(size), '\0');
WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, value.data(), static_cast<int>(value.size()), result.data(), size, nullptr, nullptr);
return result;
}
void remove_report(const std::filesystem::path& dump) {
std::error_code error;
std::filesystem::remove(dump, error);
if (!error) {
std::filesystem::remove(dump.wstring() + L".json", error);
std::filesystem::remove(log_attachment_path(dump), error);
}
}
void status_file(const std::filesystem::path& directory, const std::string& text) {
// Overwrite a bounded status file. Never include the endpoint, token, response body, or local paths.
std::ofstream(directory / L"status.txt", std::ios::trunc) << text << '\n';
}
}
bool parse_endpoint(const std::wstring& url, Endpoint& endpoint) {
if (url.empty() || url.size() >= 4096 || url.find_first_of(L"\r\n\t #") != std::wstring::npos) return false;
URL_COMPONENTS parts{};
parts.dwStructSize = sizeof(parts);
parts.dwHostNameLength = parts.dwUrlPathLength = parts.dwExtraInfoLength =
parts.dwUserNameLength = parts.dwPasswordLength = static_cast<DWORD>(-1);
if (!WinHttpCrackUrl(url.c_str(), static_cast<DWORD>(url.size()), 0, &parts) ||
parts.nScheme != INTERNET_SCHEME_HTTPS || !parts.dwHostNameLength || parts.dwUserNameLength || parts.dwPasswordLength ||
!parts.dwUrlPathLength) return false;
endpoint.host.assign(parts.lpszHostName, parts.dwHostNameLength);
endpoint.resource.assign(parts.lpszUrlPath, parts.dwUrlPathLength);
if (parts.dwExtraInfoLength) endpoint.resource.append(parts.lpszExtraInfo, parts.dwExtraInfoLength);
endpoint.port = parts.nPort;
return true;
}
bool accepted_response(DWORD status, std::string_view response, std::string& report_id) {
report_id.clear();
if (status < 200 || status >= 300) return false;
try {
const auto json = Json::parse(response, {65536, 8, 256});
if (json.value("response", "") != "ok") return false;
const auto id = json.value("_rxid", "");
if (id.empty() || id.size() > 128 || id.find_first_not_of("0123456789abcdefABCDEF-") != std::string::npos) return false;
report_id = id;
return true;
} catch (...) { return false; }
}
std::filesystem::path queue_directory(const std::filesystem::path& root, const std::wstring& url) {
// Hash the destination so changing projects cannot send an old queue to a new account.
const auto encoded = utf8(url);
std::array<unsigned char, 32> hash{};
if (BCryptHash(BCRYPT_SHA256_ALG_HANDLE, nullptr, 0,
reinterpret_cast<PUCHAR>(const_cast<char*>(encoded.data())), static_cast<ULONG>(encoded.size()),
hash.data(), static_cast<ULONG>(hash.size())) < 0) throw std::runtime_error("Cannot identify report queue");
std::wstring name;
for (const auto byte : hash) name += std::format(L"{:02x}", byte);
return root / name;
}
UploadResult upload_report(const std::wstring& url, const std::filesystem::path& dump, const Json& attributes) {
UploadResult result;
Endpoint endpoint;
if (!parse_endpoint(url, endpoint) || !attributes.is_object()) { result.error = ERROR_INVALID_PARAMETER; return result; }
const auto boundary = std::format("ReSkate-{:x}-{:x}", GetCurrentProcessId(), GetTickCount64());
MultipartReport body(dump, attributes, boundary);
if (body.error()) { result.error = body.error(); return result; }
Internet session{WinHttpOpen(L"ReSkate-CrashReporter/1", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, nullptr, nullptr, 0)};
if (!session.value) { result.error = GetLastError(); return result; }
if (!WinHttpSetTimeouts(session.value, 5000, 5000, 10000, 10000)) { result.error = GetLastError(); return result; }
Internet connection{WinHttpConnect(session.value, endpoint.host.c_str(), endpoint.port, 0)};
if (!connection.value) { result.error = GetLastError(); return result; }
Internet request{WinHttpOpenRequest(connection.value, L"POST", endpoint.resource.c_str(), nullptr, nullptr,
WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE)};
if (!request.value) { result.error = GetLastError(); return result; }
DWORD redirect = WINHTTP_OPTION_REDIRECT_POLICY_NEVER, logon = WINHTTP_AUTOLOGON_SECURITY_LEVEL_HIGH;
if (!WinHttpSetOption(request.value, WINHTTP_OPTION_REDIRECT_POLICY, &redirect, sizeof(redirect)) ||
!WinHttpSetOption(request.value, WINHTTP_OPTION_AUTOLOGON_POLICY, &logon, sizeof(logon))) {
result.error = GetLastError(); return result;
}
const std::wstring wide_boundary(boundary.begin(), boundary.end());
const auto headers = L"Content-Type: multipart/form-data; boundary=" + wide_boundary + L"\r\n";
if (!WinHttpSendRequest(request.value, headers.c_str(), static_cast<DWORD>(headers.size()), nullptr, 0, body.size(), 0)) {
result.error = GetLastError(); return result;
}
const auto started = GetTickCount64();
const auto write = [&](const void* data, DWORD bytes) {
DWORD written{};
return GetTickCount64() - started < 60000 && WinHttpWriteData(request.value, data, bytes, &written) && written == bytes;
};
if (!body.write(write)) { result.error = ERROR_WRITE_FAULT; return result; }
if (!WinHttpReceiveResponse(request.value, nullptr)) {
result.error = GetLastError(); return result;
}
DWORD status_size = sizeof(result.http_status);
if (!WinHttpQueryHeaders(request.value, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, nullptr,
&result.http_status, &status_size, nullptr)) { result.error = GetLastError(); return result; }
std::string response;
std::array<char, 65536> buffer{};
for (;;) {
DWORD read{};
if (GetTickCount64() - started >= 60000 || !WinHttpReadData(request.value, buffer.data(), static_cast<DWORD>(buffer.size()), &read)) {
result.error = ERROR_TIMEOUT; return result;
}
if (!read) break;
if (response.size() + read > 65536) { result.error = ERROR_INVALID_DATA; return result; }
response.append(buffer.data(), read);
}
result.accepted = accepted_response(result.http_status, response, result.report_id);
return result;
}
std::size_t upload_pending(const std::wstring& url, const std::filesystem::path& directory, const Sender& sender) noexcept {
try {
Handle lock(CreateFileW((directory / L"queue.lock").c_str(), GENERIC_WRITE, 0, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr));
if (lock.value == INVALID_HANDLE_VALUE) return 0;
std::vector<std::filesystem::directory_entry> pending;
const auto cutoff = std::filesystem::file_time_type::clock::now() - std::chrono::hours(24 * 14);
for (const auto& entry : std::filesystem::directory_iterator(directory)) {
if (!entry.is_regular_file() || entry.path().extension() != L".dmp") continue;
if (entry.last_write_time() < cutoff || entry.file_size() > max_dump_bytes || !entry.file_size()) remove_report(entry.path());
else pending.push_back(entry);
}
std::sort(pending.begin(), pending.end(), [](const auto& a, const auto& b) { return a.last_write_time() < b.last_write_time(); });
while (pending.size() > max_pending_reports) { remove_report(pending.front().path()); pending.erase(pending.begin()); }
std::size_t uploaded{};
for (const auto& entry : pending) {
const auto metadata = entry.path().wstring() + L".json";
std::error_code error;
const auto size = std::filesystem::file_size(metadata, error);
if (error || size > 65536) continue;
std::ifstream input(metadata, std::ios::binary);
std::string contents(static_cast<std::size_t>(size), '\0');
if (!input.read(contents.data(), static_cast<std::streamsize>(contents.size()))) continue;
input.close(); // Windows cannot remove an open metadata file after confirmation.
Json attributes;
try { attributes = Json::parse(contents, {65536, 8, 256}); } catch (...) { continue; }
const auto result = sender(url, entry.path(), attributes);
if (!result.accepted) {
status_file(directory, std::format("Upload pending; HTTP {}; Windows error {}. Retrying on next launch.", result.http_status, result.error));
break;
}
status_file(directory, "Accepted by Backtrace: " + result.report_id);
remove_report(entry.path());
if (++uploaded == 4) break; // Bound work for a single launch.
}
return uploaded;
} catch (...) { return 0; }
}
}
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "Engine/Core/Json/json.h"
#include <Windows.h>
#include <filesystem>
#include <functional>
#include <string>
namespace dingosdk::backtrace {
inline constexpr std::uintmax_t max_dump_bytes = 128ull * 1024 * 1024;
inline constexpr std::size_t max_pending_reports = 20;
struct Endpoint { std::wstring host, resource; USHORT port{}; };
struct UploadResult { bool accepted{}; DWORD http_status{}, error{}; std::string report_id; };
bool parse_endpoint(const std::wstring& url, Endpoint& endpoint);
bool accepted_response(DWORD status, std::string_view response, std::string& report_id);
std::filesystem::path queue_directory(const std::filesystem::path& root, const std::wstring& url);
UploadResult upload_report(const std::wstring& url, const std::filesystem::path& dump, const Json& attributes);
using Sender = std::function<UploadResult(const std::wstring&, const std::filesystem::path&, const Json&)>;
// Cross-process queue lock prevents the launcher and game helpers submitting the same dump.
std::size_t upload_pending(const std::wstring& url, const std::filesystem::path& directory,
const Sender& sender = upload_report) noexcept;
}
+280
View File
@@ -0,0 +1,280 @@
#include "hooks.h"
#include <detours.h>
#include <TlHelp32.h>
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <map>
#include <memory>
#include <mutex>
#include <vector>
namespace dingosdk {
namespace {
thread_local HookFailure last_failure;
LONG failure(const char* operation, LONG error, DWORD thread = 0) {
last_failure = {operation, error, thread}; return error;
}
struct Hook {
void* target{};
void* replacement{};
void* relay{};
void** original{};
bool attached{}, queued{}, retired{};
};
struct Registry {
std::mutex mutex;
bool initialized{};
std::map<void*, Hook*> targets;
std::vector<std::unique_ptr<Hook>> lifetime;
};
Registry& registry() {
// Native callbacks can retain originals until process teardown. Deliberately
// retain their tiny relays; freeing them on remove/shutdown would invalidate
// function pointers that feature code already published.
static auto* value = new Registry;
return *value;
}
HookStatus status(LONG error) { return static_cast<HookStatus>(error); }
bool executable(void* address) {
MEMORY_BASIC_INFORMATION page{};
if (!address || !VirtualQuery(address, &page, sizeof(page)) || page.State != MEM_COMMIT ||
(page.Protect & (PAGE_GUARD | PAGE_NOACCESS))) return false;
const auto protection = page.Protect & 0xff;
return protection == PAGE_EXECUTE || protection == PAGE_EXECUTE_READ ||
protection == PAGE_EXECUTE_READWRITE || protection == PAGE_EXECUTE_WRITECOPY;
}
HookStatus create_relay(Hook& hook) {
SYSTEM_INFO system{}; GetSystemInfo(&system);
const auto page = static_cast<std::size_t>(system.dwPageSize);
auto* allocation = static_cast<std::byte*>(VirtualAlloc(nullptr, page * 2,
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE));
if (!allocation) return HookOutOfMemory;
// jmp qword ptr [rip+disp32]. A separate writable page holds the pointer
// managed by Detours. No register/stack changes, and no RWX code page.
std::array<std::byte, 6> jump{std::byte{0xff}, std::byte{0x25}};
const auto displacement = static_cast<std::int32_t>(page - jump.size());
std::memcpy(jump.data() + 2, &displacement, sizeof(displacement));
std::memcpy(allocation, jump.data(), jump.size());
auto** pointer = reinterpret_cast<void**>(allocation + page);
*pointer = hook.target;
DWORD old{};
if (!VirtualProtect(allocation, page, PAGE_EXECUTE_READ, &old) ||
!FlushInstructionCache(GetCurrentProcess(), allocation, jump.size())) {
const auto error = GetLastError(); VirtualFree(allocation, 0, MEM_RELEASE); return status(error);
}
hook.relay = allocation; hook.original = pointer;
return HookOk;
}
struct Threads {
std::vector<HANDLE> handles;
~Threads() { for (const auto thread : handles) CloseHandle(thread); }
LONG collect() {
const auto snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (snapshot == INVALID_HANDLE_VALUE) return failure("enumerate threads", static_cast<LONG>(GetLastError()));
THREADENTRY32 entry{}; entry.dwSize = sizeof(entry);
LONG error = NO_ERROR;
if (!Thread32First(snapshot, &entry)) error = static_cast<LONG>(GetLastError());
else do {
if (entry.th32OwnerProcessID != GetCurrentProcessId() || entry.th32ThreadID == GetCurrentThreadId()) continue;
const auto thread = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_QUERY_LIMITED_INFORMATION | SYNCHRONIZE,
FALSE, entry.th32ThreadID);
if (!thread) {
const auto failure = GetLastError();
if (failure == ERROR_INVALID_PARAMETER) continue; // Thread exited during enumeration.
// A terminating thread can reject full access while its object
// is still in the snapshot. Skip only an independently verified
// signaled thread; a live inaccessible thread must fail safely.
const auto check = OpenThread(SYNCHRONIZE, FALSE, entry.th32ThreadID);
const bool exited = check && WaitForSingleObject(check, 0) == WAIT_OBJECT_0;
if (check) CloseHandle(check);
if (exited) continue;
error = dingosdk::failure("open thread", static_cast<LONG>(failure), entry.th32ThreadID); break;
}
const auto owner = GetProcessIdOfThread(thread);
if (!owner) {
const auto query_error = GetLastError();
const bool exited = WaitForSingleObject(thread, 0) == WAIT_OBJECT_0;
CloseHandle(thread);
if (exited) continue;
error = failure("query thread owner", static_cast<LONG>(query_error), entry.th32ThreadID); break;
}
if (owner != GetCurrentProcessId()) { CloseHandle(thread); continue; }
try { handles.push_back(thread); }
catch (...) { CloseHandle(thread); CloseHandle(snapshot); throw; }
} while (Thread32Next(snapshot, &entry));
if (error == NO_ERROR && GetLastError() != ERROR_NO_MORE_FILES)
error = static_cast<LONG>(GetLastError());
CloseHandle(snapshot);
if (error != NO_ERROR && !last_failure.error) failure("enumerate threads", error);
return error;
}
};
struct Change { Hook* hook; bool attach; };
HookStatus transact(const std::vector<Change>& changes) {
if (changes.empty()) return HookOk;
// Enumerate/open handles and allocate all project-owned storage before any
// thread is suspended. The declared Detours vendor patch keeps its transaction
// records off the shared heap for the same reason, including commit/abort.
for (unsigned attempt = 0; attempt < 4; ++attempt) {
Threads threads;
auto error = threads.collect();
if (error != NO_ERROR) return status(error);
error = DetourTransactionBegin();
if (error != NO_ERROR) return status(failure("begin transaction", error));
for (const auto& change : changes) {
error = change.attach ? DetourAttach(change.hook->original, change.hook->replacement)
: DetourDetach(change.hook->original, change.hook->replacement);
if (error != NO_ERROR) {
DetourTransactionAbort();
return status(failure(change.attach ? "attach target" : "detach target", error));
}
}
bool retry = false;
for (const auto thread : threads.handles) {
if (WaitForSingleObject(thread, 0) == WAIT_OBJECT_0) continue;
error = DetourUpdateThread(thread);
if (error == NO_ERROR) continue;
const auto thread_id = GetThreadId(thread);
const bool exited = WaitForSingleObject(thread, 0) == WAIT_OBJECT_0;
// UpdateThread poisons the transaction on failure. Abort/resume all
// threads, then recollect; never commit after a failed enlistment.
DetourTransactionAbort();
if (!exited || attempt == 3) return status(failure("enlist thread", error, thread_id));
retry = true;
break;
}
if (retry) continue;
error = DetourTransactionCommit();
if (error != NO_ERROR) return status(failure("commit transaction", error));
for (const auto& change : changes) change.hook->attached = change.attach;
return HookOk;
}
return HookProtectionFailed;
}
template<class F> HookStatus locked(F action) {
try {
auto& r = registry(); std::lock_guard lock(r.mutex);
last_failure = {};
return action(r);
} catch (const std::bad_alloc&) { return HookOutOfMemory; }
}
Hook* find(Registry& r, void* target) {
const auto found = r.targets.find(target);
return found == r.targets.end() ? nullptr : found->second;
}
}
HookStatus WINAPI hook_initialize() {
return locked([](Registry& r) {
if (r.initialized) return HookAlreadyInitialized;
r.initialized = true; return HookOk;
});
}
HookStatus WINAPI hook_prepare(void* target, void* replacement, void** original) {
if (original) *original = nullptr;
return locked([&](Registry& r) {
if (!r.initialized) return HookNotInitialized;
if (!executable(target) || !executable(replacement) || target == replacement) return HookNotExecutable;
if (r.targets.contains(target)) return HookAlreadyPrepared;
auto hook = std::make_unique<Hook>();
hook->target = target; hook->replacement = replacement;
// Reserve containers before allocating the executable relay, so failure
// cannot strand an unpublished allocation or a half-registered target.
if (r.lifetime.size() == r.lifetime.capacity())
r.lifetime.reserve(std::max<std::size_t>(16, r.lifetime.capacity() * 2));
const auto [entry, inserted] = r.targets.emplace(target, hook.get());
(void)inserted;
const auto result = create_relay(*hook);
if (result != HookOk) { r.targets.erase(entry); return result; }
if (original) *original = hook->relay;
r.lifetime.push_back(std::move(hook));
return HookOk;
});
}
HookStatus WINAPI hook_enable(void* target) {
return locked([&](Registry& r) {
if (!r.initialized) return HookNotInitialized;
auto* hook = find(r, target); if (!hook) return HookNotFound;
if (hook->attached) return HookOk;
return transact({{hook, true}});
});
}
HookStatus WINAPI hook_disable(void* target) {
return locked([&](Registry& r) {
if (!r.initialized) return HookNotInitialized;
auto* hook = find(r, target); if (!hook) return HookNotFound;
if (!hook->attached) return HookDisabled;
return transact({{hook, false}});
});
}
HookStatus WINAPI hook_remove(void* target) {
return locked([&](Registry& r) {
if (!r.initialized) return HookNotInitialized;
auto* hook = find(r, target); if (!hook) return HookNotFound;
if (hook->attached) {
const auto result = transact({{hook, false}});
if (result != HookOk) return result;
}
hook->retired = true; hook->queued = false; r.targets.erase(target);
return HookOk;
});
}
HookStatus WINAPI hook_queue_enable(void* target) {
return locked([&](Registry& r) {
if (!r.initialized) return HookNotInitialized;
auto* hook = find(r, target); if (!hook) return HookNotFound;
hook->queued = true; return HookOk;
});
}
HookStatus WINAPI hook_apply_queued() {
return locked([](Registry& r) {
if (!r.initialized) return HookNotInitialized;
std::vector<Change> changes;
for (const auto& [target, hook] : r.targets) {
(void)target;
if (hook->queued && !hook->attached) changes.push_back({hook, true});
}
const auto result = transact(changes);
if (result == HookOk) for (const auto& [target, hook] : r.targets) { (void)target; hook->queued = false; }
return result;
});
}
HookStatus WINAPI hook_shutdown() {
return locked([](Registry& r) {
if (!r.initialized) return HookNotInitialized;
std::vector<Change> changes;
for (const auto& [target, hook] : r.targets) { (void)target; if (hook->attached) changes.push_back({hook, false}); }
const auto result = transact(changes);
if (result != HookOk) return result;
for (const auto& [target, hook] : r.targets) { (void)target; hook->retired = true; hook->queued = false; }
r.targets.clear(); r.initialized = false; return HookOk;
});
}
HookFailure hook_last_failure() noexcept { return last_failure; }
const char* WINAPI hook_status_string(HookStatus value) {
switch (value) {
case HookOk: return "success";
case HookNotInitialized: return "hook service not initialized";
case HookAlreadyInitialized: return "hook service already initialized";
case HookAlreadyPrepared: return "target already registered";
case HookDisabled: return "hook already detached";
case HookNotFound: return "target not registered";
case HookNotExecutable: return "target or replacement is not executable";
case HookOutOfMemory: return "out of memory";
case HookProtectionFailed: return "memory protection or thread access denied";
case HookUnsupportedFunction: return "Detours cannot relocate target";
default: return "Detours transaction failed (Win32 status)";
}
}
HookStatistics hook_statistics() {
auto& r = registry(); std::lock_guard lock(r.mutex);
HookStatistics result;
for (const auto& hook : r.lifetime) {
if (hook->retired) ++result.retired;
else { ++result.prepared; if (hook->attached) ++result.attached; }
}
return result;
}
}
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include <Windows.h>
#include <cstddef>
namespace dingosdk {
// Win32 failures from Detours are preserved; negative values describe the
// SDK-owned lifecycle. No vendor status codes escape through feature code.
enum HookStatus : LONG {
HookOk = NO_ERROR,
HookNotInitialized = -1,
HookAlreadyInitialized = -2,
HookAlreadyPrepared = -3,
HookDisabled = -4,
HookNotFound = -5,
HookNotExecutable = -6,
HookOutOfMemory = ERROR_NOT_ENOUGH_MEMORY,
HookProtectionFailed = ERROR_ACCESS_DENIED,
HookUnsupportedFunction = ERROR_INVALID_BLOCK,
};
// A prepared original is a permanent x64 relay. Detours changes its pointer
// transactionally, so originals published into atomics or retained by native
// callbacks stay callable before attach and after detach. Only Detours performs
// instruction relocation and target patching. Preparation never patches target.
HookStatus WINAPI hook_initialize();
// original may be null when the replacement never forwards to the target.
HookStatus WINAPI hook_prepare(void* target, void* replacement, void** original = nullptr);
HookStatus WINAPI hook_enable(void* target);
HookStatus WINAPI hook_disable(void* target);
HookStatus WINAPI hook_remove(void* target);
HookStatus WINAPI hook_queue_enable(void* target);
HookStatus WINAPI hook_apply_queued();
HookStatus WINAPI hook_shutdown();
const char* WINAPI hook_status_string(HookStatus status);
// Captured on the calling thread; formatting/logging happens only after Detours
// has resumed enlisted threads. Read before issuing another hook operation.
struct HookFailure { const char* operation{"none"}; LONG error{}; DWORD thread_id{}; };
HookFailure hook_last_failure() noexcept;
struct HookStatistics { std::size_t prepared{}, attached{}, retired{}; };
HookStatistics hook_statistics();
}
+148
View File
@@ -0,0 +1,148 @@
#include "json.h"
#include <cmath>
#include <rapidjson/error/en.h>
#include <rapidjson/memorystream.h>
#include <rapidjson/prettywriter.h>
#include <rapidjson/reader.h>
#include <rapidjson/stringbuffer.h>
namespace dingosdk {
Json::Json(std::initializer_list<Json> values) {
const bool fields = std::all_of(values.begin(), values.end(), [](const Json& entry) {
return entry.is_array() && entry.size() == 2 && entry[0].is_string();
});
if (fields) {
data_ = Object{};
for (const auto& entry : values) items().emplace(entry[0].string(), entry[1]);
} else data_ = Array(values);
}
bool Json::operator==(const Json& other) const {
if (data_.index() == other.data_.index()) return data_ == other.data_;
if (!is_number() || !other.is_number()) return false;
if (is_number_float() || other.is_number_float()) return get<double>() == other.get<double>();
if (is_number_unsigned()) return std::cmp_equal(get<std::uint64_t>(), other.get<std::int64_t>());
return std::cmp_equal(get<std::int64_t>(), other.get<std::uint64_t>());
}
bool Json::operator<(const Json& other) const {
if (!is_number() || !other.is_number()) throw std::runtime_error("JSON ordering requires numbers");
if (is_number_float() || other.is_number_float()) return get<double>() < other.get<double>();
if (is_number_unsigned()) {
if (other.is_number_unsigned()) return get<std::uint64_t>() < other.get<std::uint64_t>();
return std::cmp_less(get<std::uint64_t>(), other.get<std::int64_t>());
}
if (other.is_number_unsigned()) return std::cmp_less(get<std::int64_t>(), other.get<std::uint64_t>());
return get<std::int64_t>() < other.get<std::int64_t>();
}
namespace {
struct ProfileHandler : rapidjson::BaseReaderHandler<rapidjson::UTF8<>, ProfileHandler> {
struct Frame { Json* value; std::string key; };
Json root;
std::vector<Frame> stack;
JsonLimits limits;
std::size_t events{};
explicit ProfileHandler(JsonLimits bounds) : limits(bounds) {}
void event(std::size_t depth) {
if (depth > limits.depth || ++events > limits.events)
throw std::runtime_error("Local profile JSON is too complex");
}
Json& add(Json value) {
if (stack.empty()) { root = std::move(value); return root; }
auto& frame = stack.back();
if (frame.value->is_object()) {
auto& slot = (*frame.value)[frame.key]; slot = std::move(value); return slot;
}
frame.value->push_back(std::move(value));
return frame.value->at(frame.value->size() - 1);
}
bool scalar(Json value) { event(stack.size()); add(std::move(value)); return true; }
bool Null() { return scalar(nullptr); }
bool Bool(bool value) { return scalar(value); }
bool Int(int value) { return scalar(value); }
bool Uint(unsigned value) { return scalar(value); }
bool Int64(std::int64_t value) { return scalar(value); }
bool Uint64(std::uint64_t value) { return scalar(value); }
bool Double(double value) { return scalar(value); }
bool String(const char* text, rapidjson::SizeType length, bool) {
return scalar(std::string(text, length));
}
bool Key(const char* text, rapidjson::SizeType length, bool) {
event(stack.size());
auto& frame = stack.back();
frame.key.assign(text, length);
if (frame.value->contains(frame.key)) throw std::runtime_error("Duplicate local profile JSON key");
return true;
}
bool start(Json value) {
event(stack.size());
auto& node = add(std::move(value)); stack.push_back({&node, {}}); return true;
}
bool StartObject() { return start(Json::object()); }
bool StartArray() { return start(Json::array()); }
bool end() { stack.pop_back(); event(stack.size()); return true; }
bool EndObject(rapidjson::SizeType) { return end(); }
bool EndArray(rapidjson::SizeType) { return end(); }
};
template<class Writer> bool write_json(Writer& writer, const Json& value) {
if (value.is_null()) return writer.Null();
if (value.is_boolean()) return writer.Bool(value.get<bool>());
if (value.is_number_unsigned()) return writer.Uint64(value.get<std::uint64_t>());
if (value.is_number_integer()) return writer.Int64(value.get<std::int64_t>());
if (value.is_number_float()) {
const auto number = value.get<double>();
return std::isfinite(number) ? writer.Double(number) : writer.Null();
}
if (value.is_string()) {
const auto& text = value.string();
return writer.String(text.data(), static_cast<rapidjson::SizeType>(text.size()));
}
if (value.is_object()) {
if (!writer.StartObject()) return false;
for (const auto& [key, field] : value.items()) {
if (!writer.Key(key.data(), static_cast<rapidjson::SizeType>(key.size())) || !write_json(writer, field)) return false;
}
return writer.EndObject();
}
if (!writer.StartArray()) return false;
for (const auto& item : value) if (!write_json(writer, item)) return false;
return writer.EndArray();
}
}
Json Json::parse(std::string_view text, JsonLimits limits) {
if (text.size() > limits.bytes) throw std::runtime_error("Local profile exceeds size limit");
if (text.starts_with("\xef\xbb\xbf")) text.remove_prefix(3);
rapidjson::MemoryStream stream(text.empty() ? "" : text.data(), text.size());
rapidjson::Reader reader;
ProfileHandler handler(limits);
constexpr unsigned flags = rapidjson::kParseValidateEncodingFlag | rapidjson::kParseFullPrecisionFlag |
rapidjson::kParseIterativeFlag;
const auto result = reader.Parse<flags>(stream, handler);
if (!result) throw std::runtime_error("Invalid JSON at byte " + std::to_string(result.Offset()) +
": " + rapidjson::GetParseError_En(result.Code()));
// MemoryStream uses NUL as its sentinel. A literal NUL in a file must not
// hide a second document or trailing garbage after an otherwise valid root.
if (stream.Tell() != text.size()) throw std::runtime_error("Unexpected data after JSON root");
return std::move(handler.root);
}
std::string Json::dump(int indent) const {
rapidjson::StringBuffer buffer;
bool ok{};
if (indent < 0) {
rapidjson::Writer<rapidjson::StringBuffer, rapidjson::UTF8<>, rapidjson::UTF8<>,
rapidjson::CrtAllocator, rapidjson::kWriteValidateEncodingFlag> writer(buffer);
ok = write_json(writer, *this);
} else {
rapidjson::PrettyWriter<rapidjson::StringBuffer, rapidjson::UTF8<>, rapidjson::UTF8<>,
rapidjson::CrtAllocator, rapidjson::kWriteValidateEncodingFlag> writer(buffer);
writer.SetIndent(' ', static_cast<unsigned>(indent));
ok = write_json(writer, *this);
}
if (!ok) throw std::runtime_error("Cannot encode JSON string as UTF-8");
return {buffer.GetString(), buffer.GetSize()};
}
}
+173
View File
@@ -0,0 +1,173 @@
#pragma once
#include <algorithm>
#include <concepts>
#include <cstdint>
#include <initializer_list>
#include <iterator>
#include <map>
#include <ranges>
#include <stdexcept>
#include <string>
#include <string_view>
#include <type_traits>
#include <utility>
#include <variant>
#include <vector>
namespace dingosdk {
struct JsonLimits {
std::size_t bytes = 4 * 1024 * 1024;
std::size_t depth = 32;
std::size_t events = 16384 * 16;
};
// Owning profile values with deep copies and stable object-member references.
// RapidJSON's SAX reader/writers handle JSON syntax in json.cpp; this deliberately
// small value model keeps allocator lifetimes out of snapshots and runtime code.
class Json {
public:
using Object = std::map<std::string, Json, std::less<>>;
using Array = std::vector<Json>;
Json() = default;
Json(const Json&) = default;
Json(Json&&) noexcept = default;
// Copy before replacing storage, including assignment from a child value.
Json& operator=(Json value) noexcept { data_.swap(value.data_); return *this; }
Json(std::nullptr_t) {}
Json(bool value) : data_(value) {}
template<std::integral T> requires (!std::same_as<T, bool>)
Json(T value) {
if constexpr (std::is_signed_v<T>) data_ = static_cast<std::int64_t>(value);
else data_ = static_cast<std::uint64_t>(value);
}
template<std::floating_point T> Json(T value) : data_(static_cast<double>(value)) {}
Json(const char* value) : data_(std::string(value)) {}
Json(std::string value) : data_(std::move(value)) {}
Json(std::string_view value) : data_(std::string(value)) {}
Json(std::initializer_list<Json> values);
template<std::ranges::input_range R>
requires (!std::convertible_to<R, std::string_view> && !std::same_as<R, Json>)
Json(const R& values) : data_(Array{}) {
for (const auto& value : values) std::get<Array>(data_).emplace_back(value);
}
static Json object() { Json value; value.data_ = Object{}; return value; }
static Json array(std::initializer_list<Json> values = {}) {
Json value; value.data_ = Array(values); return value;
}
static Json parse(std::string_view text, JsonLimits limits = {});
template<std::input_iterator It> static Json parse(It first, It last) {
return parse(std::string(first, last));
}
std::string dump(int indent = -1) const;
bool is_null() const { return std::holds_alternative<std::monostate>(data_); }
bool is_boolean() const { return std::holds_alternative<bool>(data_); }
bool is_number_unsigned() const { return std::holds_alternative<std::uint64_t>(data_); }
bool is_number_integer() const { return is_number_unsigned() || std::holds_alternative<std::int64_t>(data_); }
bool is_number_float() const { return std::holds_alternative<double>(data_); }
bool is_number() const { return is_number_integer() || is_number_float(); }
bool is_string() const { return std::holds_alternative<std::string>(data_); }
bool is_array() const { return std::holds_alternative<Array>(data_); }
bool is_object() const { return std::holds_alternative<Object>(data_); }
std::size_t size() const {
if (is_object()) return std::get<Object>(data_).size();
if (is_array()) return std::get<Array>(data_).size();
return is_null() ? 0 : 1;
}
bool empty() const { return size() == 0; }
const std::string& string() const { return std::get<std::string>(data_); }
template<class T> T get() const {
if constexpr (std::same_as<T, Json>) return *this;
else if constexpr (std::same_as<T, std::string>) return string();
else if constexpr (std::same_as<T, bool>) return std::get<bool>(data_);
else if constexpr (std::is_arithmetic_v<T>) {
if (is_number_unsigned()) return static_cast<T>(std::get<std::uint64_t>(data_));
if (std::holds_alternative<std::int64_t>(data_)) return static_cast<T>(std::get<std::int64_t>(data_));
if (is_number_float()) return static_cast<T>(std::get<double>(data_));
throw std::runtime_error("JSON value must be numeric");
} else static_assert(sizeof(T) == 0, "Unsupported JSON conversion");
}
bool contains(std::string_view key) const {
return is_object() && std::get<Object>(data_).contains(key);
}
const Json& at(std::string_view key) const {
const auto& fields = std::get<Object>(data_);
const auto found = fields.find(key);
if (found == fields.end()) throw std::out_of_range("Missing JSON field: " + std::string(key));
return found->second;
}
Json& at(std::string_view key) { return const_cast<Json&>(std::as_const(*this).at(key)); }
const Json& at(std::size_t index) const { return std::get<Array>(data_).at(index); }
Json& at(std::size_t index) { return std::get<Array>(data_).at(index); }
Json& operator[](std::string_view key) {
if (is_null()) data_ = Object{};
return std::get<Object>(data_)[std::string(key)];
}
const Json& operator[](std::string_view key) const { return at(key); }
Json& operator[](std::size_t index) { return at(index); }
const Json& operator[](std::size_t index) const { return at(index); }
template<class T> T value(std::string_view key, const T& fallback) const {
if (!is_object()) throw std::runtime_error("JSON value must be an object");
return contains(key) ? at(key).get<T>() : fallback;
}
std::string value(std::string_view key, const char* fallback) const {
return value(key, std::string(fallback));
}
Object& items() { return std::get<Object>(data_); }
const Object& items() const { return std::get<Object>(data_); }
std::size_t erase(std::string_view key) { return items().erase(std::string(key)); }
void push_back(Json value) {
if (is_null()) data_ = Array{};
std::get<Array>(data_).push_back(std::move(value));
}
template<bool Const> class Iterator {
using Owner = std::conditional_t<Const, const Json, Json>;
using Member = std::conditional_t<Const, Object::const_iterator, Object::iterator>;
Owner* owner_{};
Member member_{};
std::size_t index_{};
friend class Json;
Iterator(Owner* owner, bool end) : owner_(owner), index_(end ? owner->size() : 0) {
if (owner->is_object()) member_ = end ? owner->items().end() : owner->items().begin();
}
public:
using value_type = Json;
using difference_type = std::ptrdiff_t;
using reference = Owner&;
using pointer = Owner*;
using iterator_category = std::forward_iterator_tag;
Iterator() = default;
reference operator*() const { return owner_->is_object() ? member_->second : owner_->at(index_); }
pointer operator->() const { return &**this; }
Iterator& operator++() { if (owner_->is_object()) ++member_; else ++index_; return *this; }
Iterator operator++(int) { auto old = *this; ++*this; return old; }
bool operator==(const Iterator& other) const {
if (owner_ != other.owner_) return false;
return owner_ && owner_->is_object() ? member_ == other.member_ : index_ == other.index_;
}
};
using iterator = Iterator<false>;
using const_iterator = Iterator<true>;
iterator begin() { return iterator(this, false); }
iterator end() { return iterator(this, true); }
const_iterator begin() const { return const_iterator(this, false); }
const_iterator end() const { return const_iterator(this, true); }
iterator find(std::string_view key) {
auto result = end(); if (is_object()) result.member_ = items().find(key); return result;
}
const_iterator find(std::string_view key) const {
auto result = end(); if (is_object()) result.member_ = items().find(key); return result;
}
bool operator==(const Json& other) const;
bool operator<(const Json& other) const;
bool operator>(const Json& other) const { return other < *this; }
bool operator<=(const Json& other) const { return *this == other || *this < other; }
bool operator>=(const Json& other) const { return *this == other || other < *this; }
private:
std::variant<std::monostate, bool, std::int64_t, std::uint64_t, double, std::string, Array, Object> data_;
};
}
+26
View File
@@ -0,0 +1,26 @@
#include "logging_internal.h"
#include <atomic>
namespace dingosdk::logging {
void startup_banner() noexcept {
detail::PreserveError preserve;
static std::atomic_flag shown = ATOMIC_FLAG_INIT;
if (shown.test_and_set()) return;
write(Level::info, Channel::runtime, R"banner( ____ ____ _ _
| _ \ ___ / ___|| | ____ _| |_ ___
| |_) / _ \\___ \| |/ / _` | __/ _ \
| _ < __/ ___) | < (_| | || __/
|_| \_\___||____/|_|\_\__,_|\__\___|
o================o)banner");
log(Level::info, Channel::runtime, "Skate SDK | development build | Windows x64 | MSVC {} | {} {}",
_MSC_VER, __DATE__, __TIME__);
try {
const auto config = status();
write(Level::info, Channel::runtime, L"Log: " + (config.directory / L"ReSkate.log").wstring());
log(Level::info, Channel::runtime, "Log level: {} | Windows console: {}",
name(config.level), config.external_console ? "on" : "off");
write(Level::info, Channel::runtime,
"Sources: C=client, S=server, U=UI, E=engine, F=filesystem, M=materials/rendering, A=audio; SDK=tools/runtime.");
} catch (...) {}
}
}
+61
View File
@@ -0,0 +1,61 @@
#include "logging_internal.h"
#include "Engine/Core/Debug/backtrace.h"
#include <array>
namespace dingosdk::logging::detail {
namespace {
std::array<wchar_t, 32768> report_path{};
LPTOP_LEVEL_EXCEPTION_FILTER previous{};
volatile LONG reporting{};
bool installed{};
// Last-chance reporting deliberately avoids the logger, heap, CRT formatting,
// symbol loader and their locks. Leave Windows/game exception handling intact.
LONG WINAPI unhandled(EXCEPTION_POINTERS* exception) {
if (InterlockedCompareExchange(&reporting, 1, 0) == 0 && exception && exception->ExceptionRecord && exception->ContextRecord) {
char text[2048]{};
std::size_t used = 0;
const auto append = [&](const char* value) { while (*value && used + 1 < sizeof(text)) text[used++] = *value++; };
const auto hex = [&](const char* label, unsigned long long value) {
append(label); append("=0x");
constexpr char digits[] = "0123456789abcdef";
for (int shift = 60; shift >= 0; shift -= 4) if (used + 1 < sizeof(text)) text[used++] = digits[(value >> shift) & 15];
append("\r\n");
};
append("\r\n[CRITICAL] ReSkate unhandled exception\r\n");
hex("process", GetCurrentProcessId()); hex("thread", GetCurrentThreadId());
hex("exception", exception->ExceptionRecord->ExceptionCode);
hex("address", reinterpret_cast<ULONG_PTR>(exception->ExceptionRecord->ExceptionAddress));
hex("rip", exception->ContextRecord->Rip); hex("rsp", exception->ContextRecord->Rsp);
hex("rbp", exception->ContextRecord->Rbp);
if (exception->ExceptionRecord->NumberParameters >= 2 && exception->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
hex("access_type", exception->ExceptionRecord->ExceptionInformation[0]);
hex("access_address", exception->ExceptionRecord->ExceptionInformation[1]);
}
const auto file = CreateFileW(report_path.data(), FILE_APPEND_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file != INVALID_HANDLE_VALUE) {
DWORD written{}; WriteFile(file, text, static_cast<DWORD>(used), &written, nullptr);
FlushFileBuffers(file); CloseHandle(file);
}
}
backtrace::capture(exception);
return previous ? previous(exception) : EXCEPTION_CONTINUE_SEARCH;
}
}
void install_crash_report(const std::filesystem::path& directory) noexcept {
try {
if (installed) return;
const auto path = (directory / L"ReSkate.log").wstring();
if (path.size() >= report_path.size()) return;
std::copy(path.begin(), path.end(), report_path.begin()); report_path[path.size()] = 0;
reporting = 0;
previous = SetUnhandledExceptionFilter(unhandled); installed = true;
} catch (...) {}
}
void remove_crash_report() noexcept {
if (!installed) return;
const auto current = SetUnhandledExceptionFilter(previous);
if (current != unhandled) SetUnhandledExceptionFilter(current);
installed = false;
backtrace::stop();
}
}
+86
View File
@@ -0,0 +1,86 @@
#include "logging_internal.h"
#include <rapidjson/document.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include <algorithm>
#include <atomic>
namespace dingosdk::logging {
void event(Channel channel, std::string_view json, Level level) noexcept {
event(default_context(channel), channel, json, level);
}
void event(Context context, Channel channel, std::string_view json, Level level) noexcept {
detail::PreserveError preserve;
try {
if (json.empty()) return;
if (json.size() > 256 * 1024) {
write(Level::warning, context, channel, "Diagnostic event exceeded the 256 KiB limit."); return;
}
rapidjson::Document document;
document.Parse<rapidjson::kParseValidateEncodingFlag | rapidjson::kParseIterativeFlag>(json.data(), json.size());
if (document.HasParseError() || !document.IsObject()) {
write(Level::warning, context, channel, "Malformed diagnostic event was discarded."); return;
}
const auto event = document.FindMember("event");
const std::string_view id = event != document.MemberEnd() && event->value.IsString()
? std::string_view(event->value.GetString(), event->value.GetStringLength()) : "observation";
// Legacy native observers use structured events. Preserve their severity
// during migration, including errors produced through generic callbacks.
if (id.ends_with("_failed") || id == "bootstrap_failed") level = std::max(level, Level::error);
else if (id.ends_with("_rejected") || id.ends_with("_mismatch")) level = std::max(level, Level::warning);
const std::pair<std::string_view, std::string_view> activities[]{
{"local_profile_initialized", "Local profile services initialized"},
{"local_profile_hydrated", "Saved profile applied to the game"},
{"local_profile_mission_saved", "Mission progress saved"},
{"local_location_travel_request", "Travel requested"},
{"local_object_browser_ready", "Object browser opened"},
{"local_object_categories_published", "Object browser categories populated"},
{"controller_binding_saved", "Controller binding saved"},
{"local_player_card_saved", "Player card saved"}
};
const auto activity = std::find_if(std::begin(activities), std::end(activities),
[&](const auto& entry) { return entry.first == id; });
if (activity != std::end(activities)) level = std::max(level, Level::info);
if (id == "launcher_readiness") {
const auto ready = document.FindMember("ready");
if (ready != document.MemberEnd() && ready->value.IsBool() && !ready->value.GetBool())
level = std::max(level, Level::error);
}
if (id == "local_profile_event_saved") {
// Aggregate automatic saves: first success, then at most one update
// per 30 seconds when saves continue. No timer or background thread.
static std::atomic<ULONGLONG> next{};
static std::atomic<std::uint64_t> saves{};
++saves;
const auto now = GetTickCount64();
auto due = next.load();
if (now >= due && next.compare_exchange_strong(due, now + 30000))
log(Level::info, context, Channel::profile, "Profile changes saved ({} updates).", saves.exchange(0));
}
const auto error = document.FindMember("error");
if (error != document.MemberEnd() && ((error->value.IsBool() && error->value.GetBool()) ||
(error->value.IsString() && error->value.GetStringLength()))) level = std::max(level, Level::error);
if (!enabled(level)) return;
std::string message(activity != std::end(activities) ? activity->second : id);
std::replace(message.begin(), message.end(), '_', ' ');
if (!message.empty() && message[0] >= 'a' && message[0] <= 'z') message[0] -= 'a' - 'A';
bool first = true;
for (auto member = document.MemberBegin(); member != document.MemberEnd(); ++member) {
if (std::string_view(member->name.GetString(), member->name.GetStringLength()) == "event") continue;
message += first ? ": " : ", "; first = false;
message.append(member->name.GetString(), member->name.GetStringLength()); message += '=';
const auto& value = member->value;
if (value.IsString()) message.append(value.GetString(), std::min<rapidjson::SizeType>(value.GetStringLength(), 512));
else if (value.IsArray()) message += std::format("[{} items]", value.Size());
else if (value.IsObject()) message += std::format("[{} fields]", value.MemberCount());
else {
rapidjson::StringBuffer buffer; rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
value.Accept(writer); message.append(buffer.GetString(), buffer.GetSize());
}
if (message.size() > 8000) { message += "... [truncated]"; break; }
}
write(level, context, channel, message);
log(Level::trace, context, channel, "Native event: {}", json);
} catch (...) { write(Level::error, context, channel, "Could not record a diagnostic event."); }
}
}
+294
View File
@@ -0,0 +1,294 @@
#include "logging_internal.h"
#include "Engine/Core/Debug/backtrace.h"
#include "Engine/Core/Console/console_core.h"
#include <spdlog/logger.h>
#include <spdlog/sinks/base_sink.h>
#include <spdlog/sinks/msvc_sink.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <cstdio>
#include <memory>
#include <mutex>
namespace dingosdk::logging {
namespace {
spdlog::level::level_enum native_level(Level level) noexcept {
switch (level) {
case Level::trace: return spdlog::level::trace;
case Level::debug: return spdlog::level::debug;
case Level::warning: return spdlog::level::warn;
case Level::error: return spdlog::level::err;
case Level::critical: return spdlog::level::critical;
case Level::off: return spdlog::level::off;
default: return spdlog::level::info;
}
}
// FILE_APPEND_DATA chooses EOF for each WriteFile, including writes from the
// launcher while the runtime is initializing. No independent buffered offsets.
class SharedFile final : public spdlog::sinks::base_sink<std::mutex> {
public:
SharedFile(const std::filesystem::path& path, bool truncate) {
constexpr DWORD sharing = FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE;
if (truncate) {
const auto reset = CreateFileW(path.c_str(), GENERIC_WRITE, sharing, nullptr,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (reset == INVALID_HANDLE_VALUE) spdlog::throw_spdlog_ex("Cannot reset ReSkate.log", GetLastError());
CloseHandle(reset);
}
file_ = CreateFileW(path.c_str(), FILE_APPEND_DATA, sharing, nullptr,
OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file_ == INVALID_HANDLE_VALUE) spdlog::throw_spdlog_ex("Cannot open ReSkate.log", GetLastError());
}
~SharedFile() override { if (file_ != INVALID_HANDLE_VALUE) CloseHandle(file_); }
private:
void sink_it_(const spdlog::details::log_msg& message) override {
spdlog::memory_buf_t formatted;
formatter_->format(message, formatted);
DWORD written{};
if (!WriteFile(file_, formatted.data(), static_cast<DWORD>(formatted.size()), &written, nullptr))
spdlog::throw_spdlog_ex("Cannot write ReSkate.log", GetLastError());
if (written != formatted.size()) spdlog::throw_spdlog_ex("Incomplete write to ReSkate.log");
}
// WriteFile already handed each message to Windows. Avoid durable disk
// flushes in game hooks; crashes perform a final best-effort flush directly.
void flush_() override {}
HANDLE file_{INVALID_HANDLE_VALUE};
};
class WindowConsole final : public spdlog::sinks::base_sink<std::mutex> {
public:
WindowConsole() {
handle_ = CreateFileW(L"CONOUT$", GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
if (handle_ == INVALID_HANDLE_VALUE && AllocConsole()) {
owned_ = true;
SetConsoleTitleW(L"ReSkate Console");
// Text selection in a classic console must not pause game hooks.
const auto input = GetStdHandle(STD_INPUT_HANDLE);
DWORD mode{};
if (GetConsoleMode(input, &mode))
SetConsoleMode(input, (mode | ENABLE_EXTENDED_FLAGS) & ~ENABLE_QUICK_EDIT_MODE);
handle_ = CreateFileW(L"CONOUT$", GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
nullptr, OPEN_EXISTING, 0, nullptr);
}
CONSOLE_SCREEN_BUFFER_INFO info{};
if (handle_ != INVALID_HANDLE_VALUE && GetConsoleScreenBufferInfo(handle_, &info)) original_ = info.wAttributes;
}
~WindowConsole() override {
if (handle_ != INVALID_HANDLE_VALUE) CloseHandle(handle_);
if (owned_) FreeConsole();
}
bool available() const noexcept { return handle_ != INVALID_HANDLE_VALUE; }
private:
void sink_it_(const spdlog::details::log_msg& message) override {
if (!available()) return;
spdlog::memory_buf_t formatted;
formatter_->format(message, formatted);
const auto count = MultiByteToWideChar(CP_UTF8, 0, formatted.data(), static_cast<int>(formatted.size()), nullptr, 0);
if (count <= 0) return;
std::wstring wide(static_cast<std::size_t>(count), L'\0');
MultiByteToWideChar(CP_UTF8, 0, formatted.data(), static_cast<int>(formatted.size()), wide.data(), count);
const WORD color = message.level >= spdlog::level::err ? FOREGROUND_RED | FOREGROUND_INTENSITY
: message.level == spdlog::level::warn ? FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY
: message.logger_name == "Native(S)" ? FOREGROUND_BLUE | FOREGROUND_INTENSITY
: message.logger_name == "Native(M)" || message.logger_name == "Native(U)" ? FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_INTENSITY
: message.logger_name == "Native(F)" ? FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY
: message.logger_name == "Native(C)" ? FOREGROUND_GREEN | FOREGROUND_INTENSITY
: message.logger_name == "Native(A)" ? FOREGROUND_RED | FOREGROUND_GREEN
: original_;
SetConsoleTextAttribute(handle_, color);
DWORD written{}; WriteConsoleW(handle_, wide.data(), static_cast<DWORD>(wide.size()), &written, nullptr);
SetConsoleTextAttribute(handle_, original_);
}
void flush_() override {}
HANDLE handle_{INVALID_HANDLE_VALUE};
WORD original_{FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE};
bool owned_{};
};
struct RepeatedMessage {
Level level{};
Channel channel{};
Context context{};
std::string text;
ULONGLONG last{};
std::uint64_t suppressed{};
};
struct State {
std::mutex mutex;
std::atomic<Level> level{Level::info};
std::atomic<bool> file_output{};
bool initialized{}, console{};
Options options;
std::vector<RepeatedMessage> repeats;
std::array<std::shared_ptr<spdlog::logger>, context_count> loggers;
ConsoleLogBuffer history{{2048, 2 * 1024 * 1024, 8300}};
};
State& state() { static auto* value = new State; return *value; }
void sink_error(const std::string& error) noexcept {
state().file_output.store(false);
OutputDebugStringA("ReSkate logging sink failed: ");
OutputDebugStringA(error.c_str());
OutputDebugStringA("\n");
}
// State::mutex serializes history and all sinks so every view has the same order.
void emit_locked(State& s, Level level, Context context, Channel channel, std::string_view text) {
const auto index = static_cast<std::size_t>(context);
s.history.append(text, level, channel, context);
std::size_t start = 0;
while (start < text.size()) {
const auto end = text.find('\n', start);
const auto line = text.substr(start, end == std::string::npos ? text.size() - start : end - start);
const auto payload = std::format("[{}] [{}] {}", name(channel), name(level), line);
if (s.loggers[index]) s.loggers[index]->log(native_level(level), payload);
else OutputDebugStringA(std::format("{}: {}\n", name(context), payload).c_str());
if (end == std::string::npos) break;
start = end + 1;
}
}
void report_repeats(State& s, RepeatedMessage& repeat) {
if (!repeat.suppressed) return;
if (enabled(repeat.level)) emit_locked(s, repeat.level, repeat.context, repeat.channel,
std::format("Repeated {} more times: {}", repeat.suppressed, repeat.text));
repeat.suppressed = 0;
}
}
bool initialize(const Options& options) noexcept {
detail::PreserveError preserve;
try {
auto& s = state();
std::lock_guard lock(s.mutex);
if (s.initialized) return s.file_output.load();
s.options = options;
s.level.store(options.level);
std::vector<spdlog::sink_ptr> sinks;
auto debugger = std::make_shared<spdlog::sinks::msvc_sink_mt>();
sinks.push_back(debugger);
if (options.external_console) {
auto console = std::make_shared<WindowConsole>();
s.console = console->available();
if (s.console) sinks.push_back(console);
}
try {
std::filesystem::create_directories(options.directory);
sinks.push_back(std::make_shared<SharedFile>(options.directory / L"ReSkate.log", options.truncate_file));
s.file_output.store(true);
} catch (const std::exception& error) { sink_error(error.what()); }
for (std::size_t i = 0; i < context_count; ++i) {
auto logger = std::make_shared<spdlog::logger>(std::string(context_names[i]), sinks.begin(), sinks.end());
logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%P:%t] %n: %v");
logger->set_level(spdlog::level::trace);
// Sinks write synchronously without a logging worker. Startup messages
// remain readable while diagnosing a hang.
logger->flush_on(spdlog::level::trace);
logger->set_error_handler(sink_error);
s.loggers[i] = std::move(logger);
}
s.initialized = true;
detail::install_crash_report(options.directory);
const auto reporter = backtrace::start(options.directory);
emit_locked(s, reporter == backtrace::StartResult::failed ? Level::warning : Level::info,
Context::sdk, Channel::runtime,
reporter == backtrace::StartResult::ready ? "Backtrace crash reporting enabled."
: reporter == backtrace::StartResult::disabled ? "Backtrace crash reporting disabled."
: "Backtrace crash reporter could not start; local crash logging remains active.");
return s.file_output.load();
} catch (...) {
OutputDebugStringA("ReSkate logging initialization failed; debugger output remains available.\n");
return false;
}
}
bool enabled(Level level) noexcept {
detail::PreserveError preserve;
try { const auto minimum = state().level.load(); return level != Level::off && minimum != Level::off && level >= minimum; }
catch (...) { return level >= Level::warning && level != Level::off; }
}
void set_level(Level level) noexcept { detail::PreserveError preserve; try { state().level.store(level); } catch (...) {} }
void write(Level level, Channel channel, std::string_view message) noexcept {
write(level, default_context(channel), channel, message);
}
void write(Level level, Context context, Channel channel, std::string_view message) noexcept {
detail::PreserveError preserve;
try {
if (level == Level::off || (!enabled(level) && channel != Channel::command) || message.empty()) return;
auto text = detail::clean(message);
if (text.empty()) return;
auto& s = state();
std::lock_guard lock(s.mutex);
const auto index = static_cast<std::size_t>(channel);
if (index >= channel_count) channel = Channel::runtime;
if (static_cast<std::size_t>(context) >= context_count) context = Context::sdk;
const auto now = GetTickCount64();
for (auto& repeat : s.repeats)
if (now - repeat.last >= 5000) report_repeats(s, repeat);
if (channel != Channel::command && level != Level::critical) {
auto found = std::find_if(s.repeats.begin(), s.repeats.end(), [&](const auto& entry) {
return entry.level == level && entry.channel == channel && entry.context == context && entry.text == text;
});
if (found != s.repeats.end()) {
if (now - found->last < 5000) { ++found->suppressed; return; }
found->last = now;
} else {
if (s.repeats.size() == 128) {
auto oldest = std::min_element(s.repeats.begin(), s.repeats.end(),
[](const auto& left, const auto& right) { return left.last < right.last; });
report_repeats(s, *oldest);
*oldest = {level, channel, context, text, now, 0};
} else s.repeats.push_back({level, channel, context, text, now, 0});
}
}
emit_locked(s, level, context, channel, text);
} catch (...) { OutputDebugStringA("ReSkate: a log message could not be recorded.\n"); }
}
void write(Level level, Channel channel, std::wstring_view message) noexcept {
write(level, default_context(channel), channel, message);
}
void write(Level level, Context context, Channel channel, std::wstring_view message) noexcept {
detail::PreserveError preserve;
try { write(level, context, channel, detail::utf8(message)); } catch (...) {}
}
void vprintf(Level level, Channel channel, const char* format, va_list args) noexcept {
vprintf(level, default_context(channel), channel, format, args);
}
void vprintf(Level level, Context context, Channel channel, const char* format, va_list args) noexcept {
detail::PreserveError preserve;
if (!format || level == Level::off || (!enabled(level) && channel != Channel::command)) return;
std::array<char, 8192> message{};
_vsnprintf_s(message.data(), message.size(), _TRUNCATE, format, args);
write(level, context, channel, message.data());
}
void printf(Level level, Channel channel, const char* format, ...) noexcept {
detail::PreserveError preserve;
va_list args; va_start(args, format); vprintf(level, channel, format, args); va_end(args);
}
void printf(Level level, Context context, Channel channel, const char* format, ...) noexcept {
detail::PreserveError preserve;
va_list args; va_start(args, format); vprintf(level, context, channel, format, args); va_end(args);
}
Status status() {
detail::PreserveError preserve;
auto& s = state(); std::lock_guard lock(s.mutex);
return {s.options.directory, s.level.load(), s.initialized, s.file_output.load(), s.console};
}
std::vector<ConsoleLogLine> snapshot_after(std::uint64_t sequence) { return state().history.snapshot_after(sequence); }
std::uint64_t latest_sequence() noexcept { try { return state().history.latest_sequence(); } catch (...) { return 0; } }
void flush() noexcept {
detail::PreserveError preserve;
try {
auto& s = state(); std::lock_guard lock(s.mutex);
for (auto& repeat : s.repeats) report_repeats(s, repeat);
for (const auto& logger : s.loggers) if (logger) logger->flush();
} catch (...) {}
}
void shutdown() noexcept {
detail::PreserveError preserve;
flush();
try {
auto& s = state(); std::lock_guard lock(s.mutex);
detail::remove_crash_report();
for (auto& logger : s.loggers) logger.reset();
s.repeats.clear();
s.initialized = false; s.file_output = false; s.console = false;
} catch (...) {}
}
}
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include "types.h"
#include "Engine/Core/Console/console_entry.h"
#include <Windows.h>
#include <cstdarg>
#include <filesystem>
#include <format>
#include <string>
#include <vector>
#include <utility>
namespace dingosdk::logging {
struct Options {
std::filesystem::path directory;
Level level{Level::info};
bool external_console{};
// The launcher resets the file before starting the child; the runtime appends.
bool truncate_file{};
};
struct Status {
std::filesystem::path directory;
Level level{Level::info};
bool initialized{}, file_output{}, external_console{};
};
// Initialize outside DllMain and before native hooks. File failures are nonfatal;
// the bounded in-game feed and attached debugger remain usable.
bool initialize(const Options& options) noexcept;
Options options_from_environment();
std::filesystem::path log_directory(const std::filesystem::path& game_directory);
Status status();
bool enabled(Level level) noexcept;
void set_level(Level level) noexcept;
void startup_banner() noexcept;
void flush() noexcept;
// Executables call shutdown at normal exit. The pinned runtime lives until
// process teardown; do not take logging locks or join threads from DllMain.
void shutdown() noexcept;
void write(Level level, Context context, Channel channel, std::string_view message) noexcept;
void write(Level level, Context context, Channel channel, std::wstring_view message) noexcept;
void vprintf(Level level, Context context, Channel channel, const char* format, va_list args) noexcept;
void printf(Level level, Context context, Channel channel, const char* format, ...) noexcept;
void write(Level level, Channel channel, std::string_view message) noexcept;
void write(Level level, Channel channel, std::wstring_view message) noexcept;
void vprintf(Level level, Channel channel, const char* format, va_list args) noexcept;
void printf(Level level, Channel channel, const char* format, ...) noexcept;
template<class... Args>
void log(Level level, Context context, Channel channel, std::format_string<Args...> format, Args&&... args) noexcept {
const auto saved = GetLastError();
try {
if (enabled(level) || (channel == Channel::command && level != Level::off)) write(level, context, channel, std::format(format, std::forward<Args>(args)...));
} catch (...) { write(Level::error, Channel::runtime, "Could not format a log message."); }
SetLastError(saved);
}
template<class... Args>
void log(Level level, Channel channel, std::format_string<Args...> format, Args&&... args) noexcept {
log(level, default_context(channel), channel, format, std::forward<Args>(args)...);
}
// Structured native observations retain their fields. Human-readable output goes
// through the same sinks as ordinary messages; trace level includes raw payloads.
void event(Channel channel, std::string_view json, Level level = Level::debug) noexcept;
void event(Context context, Channel channel, std::string_view json, Level level = Level::debug) noexcept;
std::vector<ConsoleLogLine> snapshot_after(std::uint64_t sequence);
std::uint64_t latest_sequence() noexcept;
}
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include "logging.h"
namespace dingosdk::logging::detail {
struct PreserveError {
DWORD saved{GetLastError()};
~PreserveError() { SetLastError(saved); }
};
std::string utf8(std::wstring_view text);
std::string clean(std::string_view text);
void install_crash_report(const std::filesystem::path& directory) noexcept;
void remove_crash_report() noexcept;
}
+83
View File
@@ -0,0 +1,83 @@
#include "logging_internal.h"
#include <ShlObj.h>
#include <array>
#include <algorithm>
#include <stdexcept>
namespace dingosdk::logging {
namespace {
std::wstring environment(const wchar_t* name) {
std::array<wchar_t, 32768> value{};
const auto count = GetEnvironmentVariableW(name, value.data(), static_cast<DWORD>(value.size()));
return count && count < value.size() ? std::wstring(value.data(), count) : std::wstring{};
}
}
std::filesystem::path log_directory(const std::filesystem::path& game_directory) {
detail::PreserveError preserve;
const auto attempt = [&](const std::filesystem::path& path) -> std::filesystem::path {
std::error_code error;
std::filesystem::create_directories(path, error);
if (error) return {};
const auto probe = CreateFileW((path / std::format(L".write-check-{}", GetCurrentProcessId())).c_str(), GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, CREATE_NEW,
FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE, nullptr);
if (probe == INVALID_HANDLE_VALUE) return {};
CloseHandle(probe);
return path;
};
if (auto path = attempt(game_directory / L"logs"); !path.empty()) return path;
PWSTR local{};
if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_LocalAppData, KF_FLAG_DONT_VERIFY, nullptr, &local))) {
const auto root = std::filesystem::path(local) / L"ReSkate" / L"logs";
CoTaskMemFree(local);
if (auto path = attempt(root); !path.empty()) return path;
}
throw std::runtime_error("Cannot create a log directory beside the game or in Local AppData");
}
Options options_from_environment() {
detail::PreserveError preserve;
Options options;
options.directory = environment(L"RESKATE_LOG_DIRECTORY");
if (options.directory.empty()) {
options.truncate_file = true;
std::array<wchar_t, 32768> module{};
const auto count = GetModuleFileNameW(nullptr, module.data(), static_cast<DWORD>(module.size()));
if (!count || count >= module.size()) throw std::runtime_error("Cannot resolve log directory");
options.directory = log_directory(std::filesystem::path(module.data()).parent_path());
}
if (const auto level = parse_level(detail::utf8(environment(L"RESKATE_LOG_LEVEL")))) options.level = *level;
options.external_console = environment(L"RESKATE_LOG_CONSOLE") == L"1";
return options;
}
namespace detail {
std::string utf8(std::wstring_view text) {
if (text.empty()) return {};
const auto size = WideCharToMultiByte(CP_UTF8, 0, text.data(), static_cast<int>(text.size()), nullptr, 0, nullptr, nullptr);
std::string result(static_cast<std::size_t>(size), '\0');
if (size) WideCharToMultiByte(CP_UTF8, 0, text.data(), static_cast<int>(text.size()), result.data(), size, nullptr, nullptr);
return result;
}
std::string clean(std::string_view text) {
constexpr std::size_t limit = 8192;
std::string result; result.reserve(std::min(text.size(), limit));
for (std::size_t i = 0; i < text.size() && result.size() < limit; ++i) {
const auto ch = static_cast<unsigned char>(text[i]);
if (ch == 0x1b) {
if (i + 1 < text.size() && text[i + 1] == '[') {
i += 2;
while (i < text.size() && !(text[i] >= '@' && text[i] <= '~')) ++i;
}
continue;
}
if (ch == '\r') continue;
result += ch >= 0x20 || ch == '\n' || ch == '\t' ? static_cast<char>(ch) : '?';
}
if (text.size() > limit && result.size() == limit) {
while (!result.empty() && (static_cast<unsigned char>(result.back()) & 0xc0) == 0x80) result.pop_back();
if (!result.empty() && static_cast<unsigned char>(result.back()) >= 0xc0) result.pop_back();
result += "... [truncated]";
}
return result;
}
}
}
+71
View File
@@ -0,0 +1,71 @@
#pragma once
#include <array>
#include <cstddef>
#include <optional>
#include <string_view>
namespace dingosdk::logging {
enum class Level { trace, debug, info, success, warning, error, critical, off };
// Native execution context is independent of the feature/channel. For example,
// both client and server can report Player or Level messages.
enum class Context { sdk, client, server, ui, engine, filesystem, material, audio, count };
inline constexpr std::size_t context_count = static_cast<std::size_t>(Context::count);
inline constexpr std::array<std::string_view, context_count> context_names{
"SDK", "Native(C)", "Native(S)", "Native(U)", "Native(E)", "Native(F)", "Native(M)", "Native(A)"};
inline constexpr std::string_view name(Context context) noexcept {
const auto index = static_cast<std::size_t>(context);
return index < context_names.size() ? context_names[index] : "SDK";
}
enum class Channel {
runtime, level, player, skater, flow, command, launcher, hooks, graphics, input,
ui, profile, progression, customization, world, objects, settings, music, news,
assets, network, diagnostics, park, subworld, count
};
inline constexpr std::size_t channel_count = static_cast<std::size_t>(Channel::count);
inline constexpr std::array<std::string_view, channel_count> channel_names{
"Runtime", "Level", "Player", "Skater", "Flow", "Console", "Launcher", "Hooks",
"Graphics", "Input", "UI", "Profile", "Progression", "Customization", "World",
"Objects", "Settings", "Music", "News", "Assets", "Network", "Diagnostics", "Park", "Subworld"};
// Defaults for existing subsystem callsites; hooks with a different native owner
// must pass their context explicitly instead of deriving it from message text.
inline constexpr Context default_context(Channel channel) noexcept {
switch (channel) {
case Channel::level: case Channel::player: case Channel::skater:
case Channel::input: case Channel::profile: case Channel::progression:
case Channel::customization: case Channel::settings: return Context::client;
case Channel::ui: case Channel::flow: case Channel::news: return Context::ui;
case Channel::graphics: return Context::material;
case Channel::world: case Channel::objects: case Channel::network:
case Channel::park: case Channel::subworld: return Context::engine;
case Channel::assets: return Context::filesystem;
case Channel::music: return Context::audio;
default: return Context::sdk;
}
}
inline constexpr std::string_view name(Channel channel) noexcept {
const auto index = static_cast<std::size_t>(channel);
return index < channel_names.size() ? channel_names[index] : "Runtime";
}
inline constexpr std::string_view name(Level level) noexcept {
switch (level) {
case Level::trace: return "TRACE";
case Level::debug: return "DEBUG";
case Level::info: return "INFO";
case Level::success: return "SUCCESS";
case Level::warning: return "WARNING";
case Level::error: return "ERROR";
case Level::critical: return "CRITICAL";
default: return "OFF";
}
}
inline constexpr std::optional<Level> parse_level(std::string_view value) noexcept {
if (value == "trace") return Level::trace;
if (value == "debug") return Level::debug;
if (value == "info") return Level::info;
if (value == "warning" || value == "warn") return Level::warning;
if (value == "error") return Level::error;
if (value == "critical") return Level::critical;
if (value == "off") return Level::off;
return std::nullopt;
}
}
+477
View File
@@ -0,0 +1,477 @@
#include "launcher_support.h"
#include "launcher_support_internal.h"
#include <Windows.h>
#include <TlHelp32.h>
#include <algorithm>
#include <array>
#include <cstddef>
#include <cstring>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <vector>
namespace fs = std::filesystem;
namespace dingosdk::launcher {
namespace {
using detail::fail;
using detail::fail_windows;
using detail::Handle;
bool executable(DWORD protection) {
if (protection & (PAGE_GUARD | PAGE_NOACCESS)) return false;
switch (protection & 0xff) {
case PAGE_EXECUTE:
case PAGE_EXECUTE_READ:
case PAGE_EXECUTE_READWRITE:
case PAGE_EXECUTE_WRITECOPY:
return true;
default:
return false;
}
}
class RemoteEntrypointPatch {
public:
RemoteEntrypointPatch(HANDLE process, std::uintptr_t address)
: process_(process), address_(reinterpret_cast<void*>(address)) {
MEMORY_BASIC_INFORMATION memory{};
if (!VirtualQueryEx(process_, address_, &memory, sizeof(memory)))
fail_windows("Cannot inspect the executable entrypoint");
const auto region_end = reinterpret_cast<std::uintptr_t>(memory.BaseAddress) +
memory.RegionSize;
if (memory.State != MEM_COMMIT || memory.Type != MEM_IMAGE ||
!executable(memory.Protect) || address > region_end ||
original_.size() > region_end - address)
fail("Executable entrypoint is not in a writable image range");
SIZE_T count{};
if (!ReadProcessMemory(process_, address_, original_.data(), original_.size(), &count) ||
count != original_.size())
fail_windows("Cannot read the executable entrypoint");
constexpr std::array<unsigned char, 2> spin{0xeb, 0xfe};
if (original_ == spin) fail("Executable entrypoint is already patched");
DWORD protection{};
if (!VirtualProtectEx(process_, address_, spin.size(), PAGE_EXECUTE_READWRITE, &protection))
fail_windows("Cannot make the executable entrypoint writable");
original_protection_ = protection;
const auto wrote = WriteProcessMemory(process_, address_, spin.data(), spin.size(), &count) &&
count == spin.size();
if (wrote) active_ = true;
DWORD ignored{};
const auto protected_again = VirtualProtectEx(
process_, address_, spin.size(), original_protection_, &ignored) != FALSE;
if (!wrote) fail_windows("Cannot install the executable entrypoint gate");
if (!FlushInstructionCache(process_, address_, spin.size()))
fail_windows("Cannot flush the executable entrypoint gate");
if (!protected_again)
fail_windows("Cannot restore executable entrypoint protection");
}
~RemoteEntrypointPatch() { restore_noexcept(); }
RemoteEntrypointPatch(const RemoteEntrypointPatch&) = delete;
RemoteEntrypointPatch& operator=(const RemoteEntrypointPatch&) = delete;
void restore() {
if (!active_) return;
DWORD protection{};
if (!VirtualProtectEx(process_, address_, original_.size(), PAGE_EXECUTE_READWRITE,
&protection))
fail_windows("Cannot make the gated entrypoint writable for restoration");
SIZE_T count{};
const auto wrote = WriteProcessMemory(process_, address_, original_.data(),
original_.size(), &count) && count == original_.size();
const auto flushed = wrote && FlushInstructionCache(
process_, address_, original_.size()) != FALSE;
DWORD ignored{};
const auto protected_again = VirtualProtectEx(process_, address_, original_.size(),
original_protection_, &ignored) != FALSE;
if (!wrote) fail_windows("Cannot restore the executable entrypoint");
if (!flushed) fail_windows("Cannot flush the restored executable entrypoint");
if (!protected_again) fail_windows("Cannot restore executable entrypoint protection");
active_ = false;
}
private:
void restore_noexcept() noexcept {
if (!active_) return;
DWORD protection{};
if (!VirtualProtectEx(process_, address_, original_.size(), PAGE_EXECUTE_READWRITE,
&protection))
return;
SIZE_T count{};
if (WriteProcessMemory(process_, address_, original_.data(), original_.size(), &count) &&
count == original_.size())
FlushInstructionCache(process_, address_, original_.size());
DWORD ignored{};
VirtualProtectEx(process_, address_, original_.size(), original_protection_, &ignored);
active_ = false;
}
HANDLE process_{};
void* address_{};
std::array<unsigned char, 2> original_{};
DWORD original_protection_{};
bool active_{};
};
// `size` bytes at `rva` of a PE image as the loader maps it at `base`, read from
// the file on disk and relocated. Overlays and security tools hook LoadLibraryW
// in running processes, so the file is the reference that no one has patched.
std::vector<unsigned char> image_file_bytes(const fs::path& file, std::uint32_t rva, std::size_t size, std::uintptr_t base) {
std::vector<unsigned char> image;
{
HANDLE handle = CreateFileW(file.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (handle == INVALID_HANDLE_VALUE) return {};
LARGE_INTEGER length{};
if (GetFileSizeEx(handle, &length) && length.QuadPart > 0 && length.QuadPart < 64ll * 1024 * 1024) {
image.resize(static_cast<std::size_t>(length.QuadPart));
DWORD read{};
if (!ReadFile(handle, image.data(), static_cast<DWORD>(image.size()), &read, nullptr) || read != image.size())
image.clear();
}
CloseHandle(handle);
}
const auto in_file = [&](std::size_t offset, std::size_t count) { return offset <= image.size() && count <= image.size() - offset; };
if (!in_file(0, sizeof(IMAGE_DOS_HEADER))) return {};
IMAGE_DOS_HEADER dos{};
std::memcpy(&dos, image.data(), sizeof(dos));
if (dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew <= 0 || !in_file(static_cast<std::size_t>(dos.e_lfanew), sizeof(IMAGE_NT_HEADERS64)))
return {};
IMAGE_NT_HEADERS64 nt{};
std::memcpy(&nt, image.data() + dos.e_lfanew, sizeof(nt));
if (nt.Signature != IMAGE_NT_SIGNATURE || nt.OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC) return {};
const auto sections_at = static_cast<std::size_t>(dos.e_lfanew) + offsetof(IMAGE_NT_HEADERS64, OptionalHeader) +
nt.FileHeader.SizeOfOptionalHeader;
const auto file_offset = [&](std::uint32_t address, std::size_t count) -> std::optional<std::size_t> {
for (unsigned i = 0; i < nt.FileHeader.NumberOfSections; ++i) {
const auto at = sections_at + i * sizeof(IMAGE_SECTION_HEADER);
if (!in_file(at, sizeof(IMAGE_SECTION_HEADER))) return std::nullopt;
IMAGE_SECTION_HEADER section{};
std::memcpy(&section, image.data() + at, sizeof(section));
if (address >= section.VirtualAddress && address - section.VirtualAddress + count <= section.SizeOfRawData) {
const auto offset = static_cast<std::size_t>(section.PointerToRawData) + (address - section.VirtualAddress);
return in_file(offset, count) ? std::optional(offset) : std::nullopt;
}
}
return std::nullopt;
};
// Read with 8 bytes either side so a relocation straddling an edge is still applied whole.
constexpr std::uint32_t pad = 8;
if (rva < pad) return {};
const auto window = file_offset(rva - pad, size + pad * 2);
if (!window) return {};
std::vector<unsigned char> bytes(image.begin() + static_cast<std::ptrdiff_t>(*window),
image.begin() + static_cast<std::ptrdiff_t>(*window + size + pad * 2));
const auto delta = base - static_cast<std::uintptr_t>(nt.OptionalHeader.ImageBase);
const auto& relocations = nt.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
if (delta && relocations.Size) {
const auto table = file_offset(relocations.VirtualAddress, relocations.Size);
if (!table) return {};
for (std::size_t at = 0; at + sizeof(IMAGE_BASE_RELOCATION) <= relocations.Size;) {
IMAGE_BASE_RELOCATION block{};
std::memcpy(&block, image.data() + *table + at, sizeof(block));
if (block.SizeOfBlock < sizeof(block) || block.SizeOfBlock > relocations.Size - at) return {};
for (std::size_t entry = sizeof(block); entry + 2 <= block.SizeOfBlock; entry += 2) {
std::uint16_t value{};
std::memcpy(&value, image.data() + *table + at + entry, sizeof(value));
if ((value >> 12) != IMAGE_REL_BASED_DIR64) continue;
const std::uint64_t target = static_cast<std::uint64_t>(block.VirtualAddress) + (value & 0xfff);
if (target < rva - pad || target + 8 > static_cast<std::uint64_t>(rva) + size + pad) continue;
std::uint64_t pointer{};
std::memcpy(&pointer, bytes.data() + (target - (rva - pad)), sizeof(pointer));
pointer += delta;
std::memcpy(bytes.data() + (target - (rva - pad)), &pointer, sizeof(pointer));
}
at += block.SizeOfBlock;
}
}
return std::vector<unsigned char>(bytes.begin() + pad, bytes.begin() + pad + static_cast<std::ptrdiff_t>(size));
}
} // namespace
LoaderGate::~LoaderGate() {
for (const auto& thread : threads_) if (thread.handle) CloseHandle(thread.handle);
}
LoaderGate::LoaderGate(LoaderGate&& other) noexcept
: threads_(std::move(other.threads_)), entrypoint_(other.entrypoint_),
exiting_threads_(other.exiting_threads_), resumed_(other.resumed_) {
other.threads_.clear();
other.resumed_ = true;
}
LoaderGate& LoaderGate::operator=(LoaderGate&& other) noexcept {
if (this == &other) return *this;
for (const auto& thread : threads_) if (thread.handle) CloseHandle(thread.handle);
threads_ = std::move(other.threads_);
entrypoint_ = other.entrypoint_;
exiting_threads_ = other.exiting_threads_;
resumed_ = other.resumed_;
other.threads_.clear();
other.resumed_ = true;
return *this;
}
void LoaderGate::resume() {
if (resumed_) fail("Loader-gated threads were already resumed");
const auto resume_group = [this](bool primary) {
for (const auto& thread : threads_) {
if (thread.primary != primary) continue;
const auto previous = ResumeThread(thread.handle);
if (previous == static_cast<DWORD>(-1) && !thread.primary &&
WaitForSingleObject(thread.handle, 0) == WAIT_OBJECT_0)
continue;
if (previous == static_cast<DWORD>(-1))
fail_windows("Cannot restore a loader-gated thread");
if (previous != thread.previous_suspend_count + 1)
fail("A loader-gated thread has an unexpected suspend count");
}
};
// Keep the executable entrypoint gated until all pre-existing worker
// threads have had their original suspension counts restored.
resume_group(false);
resume_group(true);
resumed_ = true;
}
LoaderGate prepare_loader_for_injection(HANDLE process, HANDLE primary_thread,
DWORD process_id, std::uintptr_t image_base) {
if (!process || !primary_thread || !image_base || GetProcessId(process) != process_id)
fail("Invalid suspended process handles");
const auto primary_id = GetThreadId(primary_thread);
if (!primary_id) fail_windows("Cannot identify the suspended primary thread");
IMAGE_DOS_HEADER dos{};
SIZE_T count{};
if (!ReadProcessMemory(process, reinterpret_cast<const void*>(image_base), &dos,
sizeof(dos), &count) || count != sizeof(dos) ||
dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew <= 0 || dos.e_lfanew > 0x100000)
fail("Cannot validate the gated executable DOS header");
IMAGE_NT_HEADERS64 nt{};
if (!ReadProcessMemory(process,
reinterpret_cast<const void*>(image_base + static_cast<std::uintptr_t>(dos.e_lfanew)),
&nt, sizeof(nt), &count) || count != sizeof(nt) ||
nt.Signature != IMAGE_NT_SIGNATURE ||
nt.FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64 ||
nt.OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC ||
!nt.OptionalHeader.AddressOfEntryPoint ||
nt.OptionalHeader.AddressOfEntryPoint >= nt.OptionalHeader.SizeOfImage)
fail("Cannot validate the gated executable PE header");
const auto entrypoint = image_base + nt.OptionalHeader.AddressOfEntryPoint;
if (entrypoint < image_base) fail("Executable entrypoint address overflowed");
RemoteEntrypointPatch patch(process, entrypoint);
bool primary_running = false;
try {
const auto initial_suspend_count = ResumeThread(primary_thread);
if (initial_suspend_count == static_cast<DWORD>(-1))
fail_windows("Cannot begin Windows loader initialization");
if (initial_suspend_count != 1) {
SuspendThread(primary_thread);
fail("Primary thread did not have the expected creation suspension");
}
primary_running = true;
LoaderGate gate;
gate.entrypoint_ = entrypoint;
const auto deadline = GetTickCount64() + 30000;
for (;;) {
if (GetTickCount64() >= deadline)
fail("Windows loader initialization timed out before the executable entrypoint");
const auto previous = SuspendThread(primary_thread);
if (previous == static_cast<DWORD>(-1))
fail_windows("Cannot sample the executable primary thread");
primary_running = false;
if (previous != 0)
fail("Executable primary thread acquired an unexpected suspension");
CONTEXT context{};
context.ContextFlags = CONTEXT_CONTROL;
if (!GetThreadContext(primary_thread, &context))
fail_windows("Cannot read the executable primary-thread context");
if (context.Rip == entrypoint) {
HANDLE owned_primary{};
if (!DuplicateHandle(GetCurrentProcess(), primary_thread, GetCurrentProcess(),
&owned_primary, 0, FALSE, DUPLICATE_SAME_ACCESS))
fail_windows("Cannot retain the gated primary thread");
gate.threads_.push_back({owned_primary, primary_id, previous, true});
break;
}
const auto resumed = ResumeThread(primary_thread);
if (resumed == static_cast<DWORD>(-1))
fail_windows("Cannot continue Windows loader initialization");
if (resumed != 1)
fail("Executable primary thread changed suspension while sampled");
primary_running = true;
Sleep(1);
}
// Once the primary thread is held in the entrypoint loop, iteratively
// capture loader-created workers. Repeating until a stable snapshot
// closes the creation race: after every observed thread is suspended,
// no target thread remains able to create another one.
const auto suspend_thread = reinterpret_cast<LONG(NTAPI*)(HANDLE, PULONG)>(
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtSuspendThread"));
if (!suspend_thread) fail_windows("Cannot locate NtSuspendThread");
constexpr LONG status_invalid_handle = static_cast<LONG>(0xC0000008);
constexpr LONG status_thread_is_terminating = static_cast<LONG>(0xC000004B);
std::vector<DWORD> exiting;
bool stable = false;
for (unsigned pass = 0; pass < 16 && !stable; ++pass) {
stable = true;
Handle snapshot(CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0));
if (snapshot.get() == INVALID_HANDLE_VALUE)
fail_windows("Snapshot loader-initialized threads");
THREADENTRY32 thread{sizeof(thread)};
if (!Thread32First(snapshot.get(), &thread))
fail_windows("Enumerate loader-initialized threads");
do {
if (thread.th32OwnerProcessID != process_id || thread.th32ThreadID == primary_id) {
thread.dwSize = sizeof(thread);
continue;
}
const auto known = std::any_of(gate.threads_.begin(), gate.threads_.end(),
[&thread](const LoaderGate::Thread& captured) {
return captured.id == thread.th32ThreadID;
});
if (known) {
thread.dwSize = sizeof(thread);
continue;
}
const auto handle = OpenThread(THREAD_SUSPEND_RESUME | SYNCHRONIZE |
THREAD_QUERY_LIMITED_INFORMATION, FALSE, thread.th32ThreadID);
if (!handle) {
if (GetLastError() == ERROR_INVALID_PARAMETER) {
thread.dwSize = sizeof(thread);
continue;
}
fail_windows("Open loader-initialized thread");
}
ULONG previous{};
const auto status = suspend_thread(handle, &previous);
if (status < 0) {
CloseHandle(handle);
// A thread that is already exiting (loader-time helper threads of
// injected mods such as OptiScaler) can never run code again, so it
// needs no gate. SuspendThread reports it only as access denied.
if (status == status_thread_is_terminating) {
if (std::find(exiting.begin(), exiting.end(), thread.th32ThreadID) == exiting.end())
exiting.push_back(thread.th32ThreadID);
thread.dwSize = sizeof(thread);
continue;
}
if (status == status_invalid_handle) {
thread.dwSize = sizeof(thread);
continue;
}
std::ostringstream detail;
detail << "Suspend loader-initialized thread " << thread.th32ThreadID
<< " failed (NTSTATUS 0x" << std::hex << static_cast<ULONG>(status) << ')';
throw std::runtime_error(detail.str());
}
gate.threads_.push_back({handle, thread.th32ThreadID, previous, false});
stable = false;
thread.dwSize = sizeof(thread);
} while (Thread32Next(snapshot.get(), &thread));
if (GetLastError() != ERROR_NO_MORE_FILES)
fail_windows("Enumerate loader-initialized threads");
}
if (!stable) fail("Loader-created threads did not reach a stable gated set");
gate.exiting_threads_ = exiting.size();
patch.restore();
return gate;
} catch (...) {
if (primary_running) SuspendThread(primary_thread);
throw;
}
}
std::uintptr_t validated_remote_load_library(HANDLE process) {
const auto kernel32 = GetModuleHandleW(L"kernel32.dll");
if (!kernel32) fail_windows("Cannot locate local kernel32.dll");
const auto symbol = GetProcAddress(kernel32, "LoadLibraryW");
if (!symbol) fail_windows("Cannot locate local LoadLibraryW");
const auto address = reinterpret_cast<std::uintptr_t>(symbol);
MEMORY_BASIC_INFORMATION local{}, remote{};
if (!VirtualQuery(reinterpret_cast<const void*>(address), &local, sizeof(local)))
fail_windows("Cannot inspect local LoadLibraryW");
if (!VirtualQueryEx(process, reinterpret_cast<const void*>(address), &remote, sizeof(remote)))
fail_windows("Cannot inspect child LoadLibraryW");
if (local.State != MEM_COMMIT || remote.State != MEM_COMMIT ||
local.Type != MEM_IMAGE || remote.Type != MEM_IMAGE ||
!executable(local.Protect) || !executable(remote.Protect) ||
local.AllocationBase != remote.AllocationBase) {
std::ostringstream detail;
detail << "The child does not share the validated LoadLibraryW KnownDLL mapping"
<< " (local base=" << local.AllocationBase << " state=" << std::hex << local.State
<< " type=" << local.Type << " protect=" << local.Protect
<< ", remote base=" << remote.AllocationBase << " state=" << remote.State
<< " type=" << remote.Type << " protect=" << remote.Protect << ')';
throw std::runtime_error(detail.str());
}
wchar_t provider_path[32768]{};
const auto provider_length = GetModuleFileNameW(
reinterpret_cast<HMODULE>(local.AllocationBase), provider_path, 32768);
if (!provider_length || provider_length >= 32768)
fail_windows("Cannot identify the LoadLibraryW provider");
const auto provider_name = fs::path(std::wstring(provider_path, provider_length)).filename();
if (_wcsicmp(provider_name.c_str(), L"kernel32.dll") != 0 &&
_wcsicmp(provider_name.c_str(), L"kernelbase.dll") != 0)
fail("LoadLibraryW is not owned by an expected Windows KnownDLL");
const auto local_base = reinterpret_cast<std::uintptr_t>(local.AllocationBase);
const auto remote_base = reinterpret_cast<std::uintptr_t>(remote.AllocationBase);
const auto dos = *reinterpret_cast<const IMAGE_DOS_HEADER*>(local_base);
if (dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew <= 0 || dos.e_lfanew > 0x100000)
fail("Local LoadLibraryW provider has an invalid image header");
const auto local_nt = *reinterpret_cast<const IMAGE_NT_HEADERS64*>(
local_base + static_cast<std::uintptr_t>(dos.e_lfanew));
IMAGE_DOS_HEADER remote_dos{};
SIZE_T read{};
if (!ReadProcessMemory(process, reinterpret_cast<const void*>(remote_base), &remote_dos,
sizeof(remote_dos), &read) || read != sizeof(remote_dos) ||
remote_dos.e_magic != dos.e_magic || remote_dos.e_lfanew != dos.e_lfanew)
fail_windows("Cannot validate the child LoadLibraryW provider header");
IMAGE_NT_HEADERS64 remote_nt{};
if (!ReadProcessMemory(process,
reinterpret_cast<const void*>(remote_base + static_cast<std::uintptr_t>(remote_dos.e_lfanew)),
&remote_nt, sizeof(remote_nt), &read) || read != sizeof(remote_nt) ||
remote_nt.Signature != IMAGE_NT_SIGNATURE ||
remote_nt.FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64 ||
remote_nt.FileHeader.TimeDateStamp != local_nt.FileHeader.TimeDateStamp ||
remote_nt.OptionalHeader.SizeOfImage != local_nt.OptionalHeader.SizeOfImage ||
remote_nt.OptionalHeader.CheckSum != local_nt.OptionalHeader.CheckSum)
fail("The child LoadLibraryW provider image does not match this process");
if (address < remote_base || address - remote_base >= remote_nt.OptionalHeader.SizeOfImage)
fail("Child LoadLibraryW is outside its provider image");
constexpr std::size_t fingerprint_size = 32;
const auto local_region_end = reinterpret_cast<std::uintptr_t>(local.BaseAddress) + local.RegionSize;
const auto remote_region_end = reinterpret_cast<std::uintptr_t>(remote.BaseAddress) + remote.RegionSize;
if (address > local_region_end || fingerprint_size > local_region_end - address ||
address > remote_region_end || fingerprint_size > remote_region_end - address)
fail("LoadLibraryW entry bytes cross an unexpected memory region");
std::array<unsigned char, fingerprint_size> local_bytes{}, actual{};
std::memcpy(local_bytes.data(), reinterpret_cast<const void*>(address), local_bytes.size());
if (!ReadProcessMemory(process, reinterpret_cast<const void*>(address), actual.data(),
actual.size(), &read) || read != actual.size())
fail_windows("Cannot read the child LoadLibraryW entry");
// The reference is the provider file on disk: overlays (MSI Afterburner/RTSS,
// Discord, OBS, Overwolf) and security tools often hook LoadLibraryW in the
// launcher's own window process, while the suspended child is still pristine.
const auto clean = image_file_bytes(fs::path(std::wstring(provider_path, provider_length)),
static_cast<std::uint32_t>(address - local_base), fingerprint_size, remote_base);
const bool matches_file = clean.size() == actual.size() && std::equal(clean.begin(), clean.end(), actual.begin());
if (!matches_file && actual != local_bytes)
fail("Child LoadLibraryW entry bytes match neither Windows' kernel32 file nor this process. "
"Security software may be hooking new processes; try allowing ReSkateLauncher.exe and Skate.exe.");
return address;
}
} // namespace dingosdk::launcher
+232
View File
@@ -0,0 +1,232 @@
#include "launcher_support.h"
#include "launcher_support_internal.h"
#include <Windows.h>
#include <bcrypt.h>
#include <algorithm>
#include <array>
#include <cstring>
#include <limits>
#include <vector>
namespace fs = std::filesystem;
namespace dingosdk::launcher {
namespace {
using detail::fail;
using detail::Handle;
class Algorithm {
public:
~Algorithm() { if (value) BCryptCloseAlgorithmProvider(value, 0); }
BCRYPT_ALG_HANDLE value{};
};
class Hash {
public:
~Hash() { if (value) BCryptDestroyHash(value); }
BCRYPT_HASH_HANDLE value{};
};
template<class T>
T object_at(const std::byte* data, std::size_t size, std::size_t offset) {
if (offset > size || sizeof(T) > size - offset) fail("Truncated PE image");
T output{};
std::memcpy(&output, data + offset, sizeof(output));
return output;
}
class MappedFile {
public:
explicit MappedFile(const fs::path& path) {
file_.reset(CreateFileW(path.c_str(), GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, nullptr));
if (file_.get() == INVALID_HANDLE_VALUE) fail("Cannot open PE image");
LARGE_INTEGER length{};
if (!GetFileSizeEx(file_.get(), &length) || length.QuadPart <= 0 ||
static_cast<unsigned long long>(length.QuadPart) >
static_cast<unsigned long long>(std::numeric_limits<std::size_t>::max()))
fail("Invalid PE image size");
size_ = static_cast<std::size_t>(length.QuadPart);
mapping_.reset(CreateFileMappingW(file_.get(), nullptr, PAGE_READONLY, 0, 0, nullptr));
if (!mapping_.get()) fail("Cannot map PE image");
data_ = static_cast<const std::byte*>(MapViewOfFile(mapping_.get(), FILE_MAP_READ, 0, 0, 0));
if (!data_) fail("Cannot read mapped PE image");
}
~MappedFile() { if (data_) UnmapViewOfFile(data_); }
const std::byte* data() const noexcept { return data_; }
std::size_t size() const noexcept { return size_; }
private:
Handle file_;
Handle mapping_;
const std::byte* data_{};
std::size_t size_{};
};
struct ParsedPe {
PeFileInfo info;
IMAGE_NT_HEADERS64 nt{};
std::vector<IMAGE_SECTION_HEADER> sections;
};
ParsedPe parse_pe(const std::byte* data, std::size_t size) {
const auto dos = object_at<IMAGE_DOS_HEADER>(data, size, 0);
if (dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew < 0)
fail("File is not a PE image");
const auto nt_offset = static_cast<std::size_t>(dos.e_lfanew);
const auto nt = object_at<IMAGE_NT_HEADERS64>(data, size, nt_offset);
if (nt.Signature != IMAGE_NT_SIGNATURE ||
nt.FileHeader.SizeOfOptionalHeader != sizeof(IMAGE_OPTIONAL_HEADER64) ||
nt.OptionalHeader.Magic != IMAGE_NT_OPTIONAL_HDR64_MAGIC)
fail("File is not a PE32+ image");
if (!nt.FileHeader.NumberOfSections || nt.FileHeader.NumberOfSections > 96)
fail("Invalid PE section table");
const auto sections_offset = nt_offset + sizeof(DWORD) + sizeof(IMAGE_FILE_HEADER) +
nt.FileHeader.SizeOfOptionalHeader;
std::vector<IMAGE_SECTION_HEADER> sections;
sections.reserve(nt.FileHeader.NumberOfSections);
for (std::size_t index = 0; index < nt.FileHeader.NumberOfSections; ++index)
sections.push_back(object_at<IMAGE_SECTION_HEADER>(data, size,
sections_offset + index * sizeof(IMAGE_SECTION_HEADER)));
return {{nt.FileHeader.Machine, nt.FileHeader.Characteristics,
nt.OptionalHeader.SizeOfImage, nt.OptionalHeader.SizeOfHeaders, true},
nt, std::move(sections)};
}
std::size_t rva_offset(const ParsedPe& pe, std::size_t file_size, std::uint32_t rva,
std::size_t required) {
if (rva < pe.info.headers_size) {
if (rva > file_size || required > file_size - rva) fail("PE RVA is outside the file");
return rva;
}
for (const auto& section : pe.sections) {
const std::uint64_t start = section.VirtualAddress;
const std::uint64_t end = start + std::max(section.Misc.VirtualSize, section.SizeOfRawData);
if (rva < start || rva >= end) continue;
const auto delta = static_cast<std::uint64_t>(rva) - start;
if (delta > section.SizeOfRawData || required > section.SizeOfRawData - delta)
fail("PE RVA has no file data");
const auto offset = static_cast<std::uint64_t>(section.PointerToRawData) + delta;
if (offset > file_size || required > file_size - offset) fail("PE RVA is outside the file");
return static_cast<std::size_t>(offset);
}
fail("PE RVA does not belong to a section");
}
template<class T>
T rva_object(const MappedFile& file, const ParsedPe& pe, std::uint32_t rva) {
return object_at<T>(file.data(), file.size(), rva_offset(pe, file.size(), rva, sizeof(T)));
}
std::string rva_string(const MappedFile& file, const ParsedPe& pe, std::uint32_t rva) {
const auto offset = rva_offset(pe, file.size(), rva, 1);
std::string output;
for (std::size_t index = offset; index < file.size() && output.size() <= 4096; ++index) {
const auto value = static_cast<char>(file.data()[index]);
if (!value) return output;
output.push_back(value);
}
fail("Unterminated PE export name");
}
} // namespace
std::string sha256_file(const fs::path& path) {
Handle file(CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_DELETE,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, nullptr));
if (file.get() == INVALID_HANDLE_VALUE) fail("Cannot open file for SHA-256");
Algorithm algorithm;
if (BCryptOpenAlgorithmProvider(&algorithm.value, BCRYPT_SHA256_ALGORITHM, nullptr, 0) < 0)
fail("Cannot initialize SHA-256");
DWORD object_size{}, result_size{};
if (BCryptGetProperty(algorithm.value, BCRYPT_OBJECT_LENGTH,
reinterpret_cast<PUCHAR>(&object_size), sizeof(object_size), &result_size, 0) < 0 ||
result_size != sizeof(object_size)) fail("Cannot query SHA-256 state size");
std::vector<UCHAR> object(object_size);
Hash hash;
if (BCryptCreateHash(algorithm.value, &hash.value, object.data(), object_size,
nullptr, 0, 0) < 0) fail("Cannot create SHA-256 state");
std::vector<UCHAR> bytes(1024 * 1024);
for (;;) {
DWORD count{};
if (!ReadFile(file.get(), bytes.data(), static_cast<DWORD>(bytes.size()), &count, nullptr))
fail("Cannot read file for SHA-256");
if (!count) break;
if (BCryptHashData(hash.value, bytes.data(), count, 0) < 0)
fail("Cannot update SHA-256");
}
std::array<UCHAR, 32> digest{};
if (BCryptFinishHash(hash.value, digest.data(), static_cast<ULONG>(digest.size()), 0) < 0)
fail("Cannot finish SHA-256");
constexpr char digits[] = "0123456789abcdef";
std::string output;
output.reserve(digest.size() * 2);
for (const auto byte : digest) {
output.push_back(digits[byte >> 4]);
output.push_back(digits[byte & 15]);
}
return output;
}
PeFileInfo inspect_pe_file(const fs::path& path) {
const MappedFile file(path);
return parse_pe(file.data(), file.size()).info;
}
std::uint32_t exported_function_rva(const fs::path& path, std::string_view export_name) {
const MappedFile file(path);
const auto pe = parse_pe(file.data(), file.size());
const auto& directory = pe.nt.OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (!directory.VirtualAddress || directory.Size < sizeof(IMAGE_EXPORT_DIRECTORY))
fail("DLL has no export directory");
const auto exports = rva_object<IMAGE_EXPORT_DIRECTORY>(file, pe, directory.VirtualAddress);
if (!exports.NumberOfFunctions || exports.NumberOfNames > exports.NumberOfFunctions ||
exports.NumberOfFunctions > 65536) fail("Invalid PE export table");
for (std::uint32_t index = 0; index < exports.NumberOfNames; ++index) {
const auto name_rva = rva_object<std::uint32_t>(file, pe,
exports.AddressOfNames + index * sizeof(std::uint32_t));
if (rva_string(file, pe, name_rva) != export_name) continue;
const auto ordinal = rva_object<std::uint16_t>(file, pe,
exports.AddressOfNameOrdinals + index * sizeof(std::uint16_t));
if (ordinal >= exports.NumberOfFunctions) fail("Invalid PE export ordinal");
const auto function = rva_object<std::uint32_t>(file, pe,
exports.AddressOfFunctions + ordinal * sizeof(std::uint32_t));
const std::uint64_t export_end = static_cast<std::uint64_t>(directory.VirtualAddress) + directory.Size;
if (!function || (function >= directory.VirtualAddress && function < export_end))
fail("Initializer export is missing or forwarded");
return function;
}
fail("Required initializer export is missing");
}
void validate_game_file(const fs::path& path) {
std::error_code error;
const auto size = fs::file_size(path, error);
if (error || size != expected_game_file_size) fail("Skate.exe has the wrong file size");
const auto image = inspect_pe_file(path);
if (!image.pe64 || image.machine != IMAGE_FILE_MACHINE_AMD64 ||
!(image.characteristics & IMAGE_FILE_EXECUTABLE_IMAGE) ||
(image.characteristics & IMAGE_FILE_DLL) || image.image_size != expected_game_image_size)
fail("Skate.exe has the wrong PE image identity");
if (sha256_file(path) != expected_game_sha256)
fail("Skate.exe SHA-256 does not match the supported build");
}
void validate_steam_api_file(const fs::path& path) {
std::error_code error;
const auto size = fs::file_size(path, error);
if (error || size != expected_steam_api_file_size)
fail("steam_api64.dll is not the supported original Steam library (wrong size)");
const auto image = inspect_pe_file(path);
if (!image.pe64 || image.machine != IMAGE_FILE_MACHINE_AMD64 ||
!(image.characteristics & IMAGE_FILE_EXECUTABLE_IMAGE) ||
!(image.characteristics & IMAGE_FILE_DLL))
fail("steam_api64.dll is not an x64 Windows DLL");
if (sha256_file(path) != expected_steam_api_sha256)
fail("steam_api64.dll is not the supported original Steam library (SHA-256 mismatch)");
}
} // namespace dingosdk::launcher
+223
View File
@@ -0,0 +1,223 @@
#include "Engine/Core/Log/types.h"
#include "launcher_support.h"
#include "launcher_support_internal.h"
#include <Windows.h>
#include <array>
#include <cstdlib>
#include <string>
#include <vector>
namespace fs = std::filesystem;
namespace dingosdk::launcher {
namespace {
using detail::fail;
} // namespace
bool bindable_key(unsigned key) noexcept {
if (key == 0 || key > 0xFE) return false;
switch (key) {
case VK_LBUTTON: case VK_RBUTTON: case VK_MBUTTON: case VK_XBUTTON1: case VK_XBUTTON2: case VK_CANCEL:
case VK_SHIFT: case VK_CONTROL: case VK_MENU: case VK_LSHIFT: case VK_RSHIFT: case VK_LCONTROL:
case VK_RCONTROL: case VK_LMENU: case VK_RMENU: case VK_LWIN: case VK_RWIN: case VK_APPS:
case VK_CAPITAL: case VK_NUMLOCK: case VK_ESCAPE: case VK_RETURN: case VK_TAB: case VK_SPACE:
case VK_BACK: case VK_PACKET: case VK_PROCESSKEY:
return false;
default:
return true;
}
}
std::string key_name(unsigned key) {
switch (key) {
case VK_INSERT: return "Insert";
case VK_DELETE: return "Delete";
case VK_HOME: return "Home";
case VK_END: return "End";
case VK_PRIOR: return "Page Up";
case VK_NEXT: return "Page Down";
case VK_UP: return "Up";
case VK_DOWN: return "Down";
case VK_LEFT: return "Left";
case VK_RIGHT: return "Right";
case VK_PAUSE: return "Pause";
case VK_SCROLL: return "Scroll Lock";
case VK_SNAPSHOT: return "Print Screen";
case VK_DIVIDE: return "Num /";
case VK_OEM_3: return "` / ~";
default: break;
}
if (key >= VK_F1 && key <= VK_F24) return "F" + std::to_string(key - VK_F1 + 1);
if (key >= VK_NUMPAD0 && key <= VK_NUMPAD9) return "Num " + std::to_string(key - VK_NUMPAD0);
const auto scan = MapVirtualKeyW(key, MAPVK_VK_TO_VSC);
std::array<wchar_t, 64> text{};
const auto length = scan ? GetKeyNameTextW(static_cast<LONG>(scan << 16), text.data(), static_cast<int>(text.size())) : 0;
if (length <= 0) return "Key " + std::to_string(key);
const auto size = WideCharToMultiByte(CP_UTF8, 0, text.data(), length, nullptr, 0, nullptr, nullptr);
std::string result(static_cast<std::size_t>(size > 0 ? size : 0), '\0');
WideCharToMultiByte(CP_UTF8, 0, text.data(), length, result.data(), size, nullptr, nullptr);
return result;
}
std::string key_cap(unsigned key) {
switch (key) {
case VK_INSERT: return "INS";
case VK_DELETE: return "DEL";
case VK_PRIOR: return "PGUP";
case VK_NEXT: return "PGDN";
default: break;
}
auto name = key_name(key);
for (auto& ch : name) if (ch >= 'a' && ch <= 'z') ch = static_cast<char>(ch - ('a' - 'A'));
return name;
}
OverlayKeys overlay_keys() noexcept {
static const OverlayKeys keys = [] {
const auto incoming_error = GetLastError();
const auto read = [](const wchar_t* name, unsigned fallback) {
wchar_t value[16]{};
const auto length = GetEnvironmentVariableW(name, value, 16);
if (!length || length >= 16) return fallback;
wchar_t* end{};
const auto parsed = std::wcstoul(value, &end, 0);
return end && *end == L'\0' && bindable_key(parsed) ? static_cast<unsigned>(parsed) : fallback;
};
OverlayKeys result{read(L"RESKATE_MENU_KEY", default_menu_key), read(L"RESKATE_CONSOLE_KEY", default_console_key)};
if (result.menu == result.console) result = {};
SetLastError(incoming_error);
return result;
}();
return keys;
}
bool offline_mode() noexcept {
static const bool offline = [] {
const auto incoming_error = GetLastError();
wchar_t value[2]{};
const auto length = GetEnvironmentVariableW(L"RESKATE_OFFLINE", value, 2);
SetLastError(incoming_error);
return length == 1 && value[0] == L'1';
}();
return offline;
}
std::uint64_t offline_steam_id() noexcept {
static const std::uint64_t id = [] {
constexpr std::uint64_t fallback = 0x0110000100000001ull;
const auto incoming_error = GetLastError();
char value[32]{};
const auto length = GetEnvironmentVariableA("RESKATE_OFFLINE_STEAM_ID", value, sizeof(value));
SetLastError(incoming_error);
if (!length || length >= sizeof(value)) return fallback;
char* end{};
const auto parsed = std::strtoull(value, &end, 10);
// Individual accounts in the public universe only.
return end && *end == '\0' && (parsed >> 52) == 0x011 ? parsed : fallback;
}();
return id;
}
LaunchOptions parse_launch_options(const std::vector<std::wstring>& arguments) {
LaunchOptions options;
bool log_level_explicit = false;
const auto dimension = [](std::wstring_view value, unsigned minimum) {
unsigned result = 0;
if (value.empty()) fail("Resolution requires a decimal pixel count");
for (const auto character : value) {
if (character < L'0' || character > L'9' || result > 16384)
fail("Resolution requires a decimal pixel count, at most 16384");
result = result * 10 + static_cast<unsigned>(character - L'0');
}
if (result < minimum || result > 16384)
fail("Resolution must be at least 320 x 200 and at most 16384 x 16384");
return result;
};
for (std::size_t index = 0; index < arguments.size(); ++index) {
const std::wstring_view argument(arguments[index]);
if (argument == L"--no-loose-files") { options.loose_files = false; continue; }
if (argument == L"-offline" || argument == L"--offline") { options.offline = true; continue; }
if (argument == L"-wconsole") { options.window_console = true; continue; }
if (argument == L"--log-trace") { options.log_trace = true; continue; }
if (argument == L"--log-level" || argument.starts_with(L"--log-level=")) {
const auto equals = argument.find(L'=');
std::wstring_view value;
if (equals != std::wstring_view::npos) value = argument.substr(equals + 1);
else {
if (++index == arguments.size()) fail("--log-level requires a severity");
value = arguments[index];
}
std::string level;
for (auto ch : value) {
if (ch < L'a' || ch > L'z') fail("Invalid --log-level");
level += static_cast<char>(ch);
}
if (!logging::parse_level(level)) fail("--log-level requires trace, debug, info, warning, error, critical, or off");
options.log_level = std::move(level); log_level_explicit = true; continue;
}
if (argument == L"--gpu-diagnostics") { options.gpu_diagnostics = true; continue; }
const bool menu_key = argument.starts_with(L"--menu-key=");
if (menu_key || argument.starts_with(L"--console-key=")) {
const std::wstring value(argument.substr(argument.find(L'=') + 1));
wchar_t* end{};
const auto key = std::wcstoul(value.c_str(), &end, 0);
if (value.empty() || !end || *end != L'\0' || !bindable_key(key))
fail("--menu-key and --console-key take a virtual-key code such as 0x2D (Insert)");
(menu_key ? options.menu_key : options.console_key) = static_cast<unsigned>(key);
continue;
}
// Translate the friendly flag into the inspected engine settings below.
if (argument == L"-windowed" || argument == L"--windowed") {
options.force_windowed = true;
continue;
}
const bool width = argument == L"--width" || argument.starts_with(L"--width=");
const bool height = argument == L"--height" || argument.starts_with(L"--height=");
if (width || height) {
options.force_windowed = true;
const auto equals = argument.find(L'=');
std::wstring_view value;
if (equals != std::wstring_view::npos) value = argument.substr(equals + 1);
else {
if (++index == arguments.size()) fail("--width and --height require a pixel count");
value = arguments[index];
}
if (width) options.width = dimension(value, 320);
else options.height = dimension(value, 200);
continue;
}
options.game_arguments.emplace_back(argument);
}
if (options.log_trace && !log_level_explicit) options.log_level = "trace";
return options;
}
std::wstring windowed_arguments(const LaunchOptions& options) {
if (!options.force_windowed) return {};
return L" -Window.Width " + std::to_wstring(options.width) +
L" -Window.Height " + std::to_wstring(options.height) +
L" -Window.AutoSize 0 -Window.FullscreenAutoSize 0"
L" -RenderDevice.FullscreenModeEnable 0 -RenderDevice.WindowedBorderless 0";
}
std::wstring mod_data_arguments(const fs::path& game_directory,
const std::vector<std::wstring>& game_arguments) {
for (const auto& argument : game_arguments) {
const std::wstring_view key(argument.data(), argument.find(L'=') == std::wstring::npos
? argument.size() : argument.find(L'='));
if (key.size() == 9 && CompareStringOrdinal(key.data(), 9, L"-dataPath", 9, TRUE) == CSTR_EQUAL)
return {};
}
return fs::is_directory(game_directory / L"ModData")
? L" -dataPath \"ModData/Default\"" : L"";
}
SiblingPaths sibling_paths(const fs::path& launcher) {
const auto directory = launcher.parent_path();
return {directory, directory / L"Skate.exe", directory / L"ReSkate.dll",
directory / L"steam_api64.dll", directory / L"logs"};
}
} // namespace dingosdk::launcher
+133
View File
@@ -0,0 +1,133 @@
#pragma once
#include <Windows.h>
#include "Engine/Game/Build/supported_build.h"
#include <cstdint>
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>
namespace dingosdk::launcher {
inline constexpr std::uint64_t expected_game_file_size = supported_build::game_file_size;
inline constexpr std::uint32_t expected_game_image_size = supported_build::game_image_size;
inline constexpr std::string_view expected_game_sha256 = supported_build::game_sha256;
inline constexpr std::uint64_t expected_steam_api_file_size = supported_build::steam_api_file_size;
inline constexpr std::string_view expected_steam_api_sha256 = supported_build::steam_api_sha256;
struct SiblingPaths {
std::filesystem::path directory;
std::filesystem::path game;
std::filesystem::path dll;
std::filesystem::path steam_api;
std::filesystem::path logs;
};
struct PeFileInfo {
std::uint16_t machine{};
std::uint16_t characteristics{};
std::uint32_t image_size{};
std::uint32_t headers_size{};
bool pe64{};
};
struct LaunchOptions {
unsigned width{1280};
unsigned height{720};
bool force_windowed{};
bool gpu_diagnostics{};
bool window_console{};
bool log_trace{};
bool loose_files{true};
bool offline{};
// Virtual-key codes that open ReSkate's menu and console (--menu-key /
// --console-key); passed to the game as RESKATE_MENU_KEY / RESKATE_CONSOLE_KEY.
unsigned menu_key{0x2D}; // VK_INSERT
unsigned console_key{0xC0}; // VK_OEM_3, the key left of 1
std::string log_level{"info"};
std::vector<std::wstring> game_arguments;
};
// Arguments exclude argv[0]. Saved game display settings apply by default.
// --windowed or an explicit width/height requests the windowed override.
LaunchOptions parse_launch_options(const std::vector<std::wstring>& arguments);
std::wstring windowed_arguments(const LaunchOptions& options);
// Detect mods beside Skate.exe. Explicit game data paths take precedence.
std::wstring mod_data_arguments(const std::filesystem::path& game_directory,
const std::vector<std::wstring>& game_arguments);
class LoaderGate {
public:
LoaderGate() = default;
~LoaderGate();
LoaderGate(const LoaderGate&) = delete;
LoaderGate& operator=(const LoaderGate&) = delete;
LoaderGate(LoaderGate&& other) noexcept;
LoaderGate& operator=(LoaderGate&& other) noexcept;
std::uintptr_t entrypoint() const noexcept { return entrypoint_; }
std::size_t thread_count() const noexcept { return threads_.size(); }
// Threads that were already exiting when the gate closed; they need no gate.
std::size_t exiting_thread_count() const noexcept { return exiting_threads_; }
void resume();
private:
friend LoaderGate prepare_loader_for_injection(HANDLE, HANDLE, DWORD, std::uintptr_t);
struct Thread {
HANDLE handle{};
DWORD id{};
DWORD previous_suspend_count{};
bool primary{};
};
std::vector<Thread> threads_;
std::uintptr_t entrypoint_{};
std::size_t exiting_threads_{};
bool resumed_{};
};
SiblingPaths sibling_paths(const std::filesystem::path& launcher);
std::string sha256_file(const std::filesystem::path& path);
PeFileInfo inspect_pe_file(const std::filesystem::path& path);
std::uint32_t exported_function_rva(const std::filesystem::path& path,
std::string_view export_name);
void validate_game_file(const std::filesystem::path& path);
void validate_steam_api_file(const std::filesystem::path& path);
// -offline: the launcher sets RESKATE_OFFLINE=1 so the game runs without Steam,
// as "Unknown Player", with multiplayer hidden. Read once per process.
inline constexpr const char* offline_player_name = "Unknown Player";
bool offline_mode() noexcept;
// The SteamID offline mode reports: RESKATE_OFFLINE_STEAM_ID, the account
// Steam was last signed in to, so the game keeps the same settings save.
std::uint64_t offline_steam_id() noexcept;
// Keys that open ReSkate's menu and console in game.
inline constexpr unsigned default_menu_key = 0x2D; // VK_INSERT
inline constexpr unsigned default_console_key = 0xC0; // VK_OEM_3
struct OverlayKeys { unsigned menu{default_menu_key}, console{default_console_key}; };
// The keys the launcher chose (RESKATE_MENU_KEY / RESKATE_CONSOLE_KEY), or
// the defaults. Read once per process.
OverlayKeys overlay_keys() noexcept;
// Whether a virtual key can open the menu or console: not a mouse button,
// modifier, Escape, Enter, Tab, Space or Backspace.
bool bindable_key(unsigned key) noexcept;
// "Insert", "F8", "`"... for settings and hints; `short_name` gives a keycap
// label such as "INS".
std::string key_name(unsigned key);
std::string key_cap(unsigned key);
// Temporarily gates the validated executable entrypoint of a process created
// with CREATE_SUSPENDED. This lets the Windows loader finish while preventing
// executable startup, then suspends every existing thread and restores the
// exact entrypoint bytes before returning.
LoaderGate prepare_loader_for_injection(HANDLE process, HANDLE primary_thread,
DWORD process_id, std::uintptr_t image_base);
// Returns the ordinary LoadLibraryW address for a same-architecture suspended
// child only after verifying the matching KnownDLL mapping and entry bytes.
std::uintptr_t validated_remote_load_library(HANDLE process);
} // namespace dingosdk::launcher
@@ -0,0 +1,39 @@
#pragma once
#include <Windows.h>
#include <stdexcept>
#include <string>
// Shared by launcher_support.cpp, launcher_pe.cpp and launcher_injection.cpp.
namespace dingosdk::launcher::detail {
class Handle {
public:
explicit Handle(HANDLE value = nullptr) noexcept : value_(value) {}
~Handle() { reset(); }
Handle(const Handle&) = delete;
Handle& operator=(const Handle&) = delete;
Handle(Handle&& other) noexcept : value_(other.release()) {}
Handle& operator=(Handle&& other) noexcept {
if (this != &other) reset(other.release());
return *this;
}
HANDLE get() const noexcept { return value_; }
HANDLE release() noexcept { const auto value = value_; value_ = nullptr; return value; }
void reset(HANDLE value = nullptr) noexcept {
if (value_ && value_ != INVALID_HANDLE_VALUE) CloseHandle(value_);
value_ = value;
}
private:
HANDLE value_{};
};
[[noreturn]] inline void fail(const char* message) { throw std::runtime_error(message); }
[[noreturn]] inline void fail_windows(const char* message, DWORD error = GetLastError()) {
throw std::runtime_error(std::string(message) + " (Windows error " +
std::to_string(error) + ")");
}
} // namespace dingosdk::launcher::detail
+50
View File
@@ -0,0 +1,50 @@
#pragma once
#include <Windows.h>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <type_traits>
namespace dingosdk::memory {
inline constexpr std::uintptr_t highest_user_address = 0x00007fffffffffffULL;
inline bool read_bytes(std::uintptr_t address, void* destination, std::size_t size) noexcept {
if (!destination || !size || size > highest_user_address || address < 0x10000 ||
address > highest_user_address - size) return false;
SIZE_T count{};
return ReadProcessMemory(GetCurrentProcess(), reinterpret_cast<const void*>(address), destination, size, &count) && count == size;
}
template<class T> requires std::is_trivially_copyable_v<T>
bool read(std::uintptr_t address, T& value) noexcept { return read_bytes(address, &value, sizeof(value)); }
// The same read without a kernel transition: a plain copy that stops at an access violation.
// ReadProcessMemory above costs a system call (about 1 us) per read, which adds up on paths
// that run every game frame. Use peek there, for game objects expected to be readable; a
// failed peek costs an exception, so keep read for probing addresses that are often invalid.
inline bool peek_bytes(std::uintptr_t address, void* destination, std::size_t size) noexcept {
if (!destination || !size || size > highest_user_address || address < 0x10000 ||
address > highest_user_address - size) return false;
__try {
std::memcpy(destination, reinterpret_cast<const void*>(address), size);
return true;
} __except (EXCEPTION_EXECUTE_HANDLER) {
return false;
}
}
template<class T> requires std::is_trivially_copyable_v<T>
bool peek(std::uintptr_t address, T& value) noexcept { return peek_bytes(address, &value, sizeof(value)); }
// A NUL-terminated string copied the same way: the length (terminator written at destination[length]),
// or -1 if it is unreadable or has no terminator within capacity bytes. Reads byte by byte, so it never
// touches memory past the terminator.
inline std::ptrdiff_t peek_cstring(std::uintptr_t address, char* destination, std::size_t capacity) noexcept {
if (!destination || !capacity || address < 0x10000 || address > highest_user_address - capacity) return -1;
__try {
const auto* source = reinterpret_cast<const char*>(address);
for (std::size_t i = 0; i < capacity; ++i)
if (!(destination[i] = source[i])) return static_cast<std::ptrdiff_t>(i);
return -1;
} __except (EXCEPTION_EXECUTE_HANDLER) {
return -1;
}
}
}
+322
View File
@@ -0,0 +1,322 @@
#include "profiler_internal.h"
#include <TlHelp32.h>
#include <psapi.h>
#include <algorithm>
#include <cctype>
#include <array>
#include <map>
#include <mutex>
#include <thread>
namespace dingosdk::profiler {
std::atomic<bool> zones_enabled{false};
namespace detail {
std::atomic<std::uint32_t> client_thread{0}, present_thread{0};
std::atomic<bool> sampling{false};
}
namespace {
std::atomic<Site*> sites{nullptr};
std::atomic<bool> hud_visible{false}, window_visible{false}, monitor_running{false};
// Recent values for the HUD's graphs. One writer each (the client update thread, the present
// thread); the overlay reads them racily, which at worst shows one value twice.
constexpr std::size_t history_size = 512;
struct Ring {
std::array<std::atomic<float>, history_size> values{};
std::atomic<std::uint64_t> written{0};
void push(float value) noexcept {
const auto at = written.load(std::memory_order_relaxed);
values[at % history_size].store(value, std::memory_order_relaxed);
written.store(at + 1, std::memory_order_release);
}
};
std::array<Ring, 4> rings;
// Totals since the monitor's last summary, taken with exchange().
struct Totals {
std::atomic<std::uint64_t> count{0}, sum{0}, longest{0};
void add(std::uint64_t value) noexcept {
count.fetch_add(1, std::memory_order_relaxed);
sum.fetch_add(value, std::memory_order_relaxed);
auto previous = longest.load(std::memory_order_relaxed);
while (value > previous && !longest.compare_exchange_weak(previous, value, std::memory_order_relaxed)) {}
}
struct Taken { std::uint64_t count, sum, longest; };
Taken take() noexcept {
return {count.exchange(0, std::memory_order_relaxed), sum.exchange(0, std::memory_order_relaxed),
longest.exchange(0, std::memory_order_relaxed)};
}
};
Totals client_interval, client_frame, client_own, present_interval;
std::mutex summary_mutex;
std::shared_ptr<const Summary> latest_summary = std::make_shared<Summary>();
bool wanted() noexcept {
return hud_visible.load(std::memory_order_acquire) || window_visible.load(std::memory_order_acquire) ||
detail::sampling.load(std::memory_order_acquire);
}
std::uint64_t filetime(const FILETIME& value) noexcept {
return (static_cast<std::uint64_t>(value.dwHighDateTime) << 32) | value.dwLowDateTime;
}
// Per-thread CPU use, from GetThreadTimes deltas. The thread list is refreshed every few
// seconds: a Toolhelp snapshot walks every thread on the system.
class ThreadMonitor {
struct Thread {
HANDLE handle{};
std::uint64_t last{};
std::string label;
bool seen{};
};
std::map<DWORD, Thread> threads_;
std::uint64_t last_process_{}, last_wall_{};
std::chrono::steady_clock::time_point next_refresh_{};
static std::string module_of(std::uintptr_t address) {
HMODULE module{};
if (!address || !GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(address), &module)) return {};
wchar_t name[MAX_PATH]{};
if (!GetModuleBaseNameW(GetCurrentProcess(), module, name, MAX_PATH)) return {};
std::string result;
for (const wchar_t* c = name; *c; ++c) result.push_back(*c < 128 ? static_cast<char>(*c) : '?');
return result;
}
static std::string label_of(HANDLE handle) {
PWSTR description{};
std::string result;
if (SUCCEEDED(GetThreadDescription(handle, &description)) && description) {
for (const wchar_t* c = description; *c; ++c) result.push_back(*c < 128 ? static_cast<char>(*c) : '?');
LocalFree(description);
}
if (!result.empty()) return result;
using Query = LONG(NTAPI*)(HANDLE, int, PVOID, ULONG, PULONG);
static const auto query = reinterpret_cast<Query>(
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "NtQueryInformationThread"));
void* start{};
if (query) query(handle, 9 /* ThreadQuerySetWin32StartAddress */, &start, sizeof(start), nullptr);
auto module = module_of(reinterpret_cast<std::uintptr_t>(start));
std::string lower = module;
for (auto& c : lower) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
// Frostbite starts its threads through the CRT, so they all begin in ucrtbase.
if (lower == "reskate.dll") return "ReSkate worker";
if (lower == "ucrtbase.dll" || lower == "skate.exe") return "Skate";
if (lower.starts_with("steam") || lower.starts_with("tier0") || lower.starts_with("gameoverlay")) return "Steam";
if (lower.starts_with("amd") || lower.starts_with("nv") || lower.starts_with("igd") || lower.starts_with("d3d")) return "Graphics driver";
return module.empty() ? "Thread" : module;
}
void refresh() {
for (auto& [id, thread] : threads_) thread.seen = false;
const HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (snapshot == INVALID_HANDLE_VALUE) return;
const auto self = GetCurrentProcessId();
THREADENTRY32 entry{sizeof(entry)};
for (BOOL ok = Thread32First(snapshot, &entry); ok; ok = Thread32Next(snapshot, &entry)) {
if (entry.th32OwnerProcessID != self) continue;
auto [it, fresh] = threads_.try_emplace(entry.th32ThreadID);
it->second.seen = true;
if (fresh) {
// Full query rights read the start address (for the label); limited ones still time it.
it->second.handle = OpenThread(THREAD_QUERY_INFORMATION, FALSE, entry.th32ThreadID);
if (!it->second.handle)
it->second.handle = OpenThread(THREAD_QUERY_LIMITED_INFORMATION, FALSE, entry.th32ThreadID);
if (it->second.handle) {
FILETIME created, exited, kernel, user;
if (GetThreadTimes(it->second.handle, &created, &exited, &kernel, &user))
it->second.last = filetime(kernel) + filetime(user);
it->second.label = label_of(it->second.handle);
}
}
}
CloseHandle(snapshot);
for (auto it = threads_.begin(); it != threads_.end();) {
if (it->second.seen) { ++it; continue; }
if (it->second.handle) CloseHandle(it->second.handle);
it = threads_.erase(it);
}
}
public:
static std::string label(HANDLE handle) { return label_of(handle); }
void sample(Summary& summary) {
const auto now = std::chrono::steady_clock::now();
if (now >= next_refresh_) { refresh(); next_refresh_ = now + std::chrono::seconds(3); }
FILETIME wall_time;
GetSystemTimePreciseAsFileTime(&wall_time);
const auto wall = filetime(wall_time);
const double elapsed = last_wall_ ? static_cast<double>(wall - last_wall_) : 0.0;
FILETIME created, exited, kernel, user;
if (GetProcessTimes(GetCurrentProcess(), &created, &exited, &kernel, &user)) {
const auto process = filetime(kernel) + filetime(user);
if (elapsed > 0 && last_process_) summary.process_cpu_percent = 100.0 * static_cast<double>(process - last_process_) / elapsed;
last_process_ = process;
}
const auto client = detail::client_thread.load(std::memory_order_relaxed);
const auto present = detail::present_thread.load(std::memory_order_relaxed);
for (auto& [id, thread] : threads_) {
if (!thread.handle || !GetThreadTimes(thread.handle, &created, &exited, &kernel, &user)) continue;
const auto total = filetime(kernel) + filetime(user);
const double percent = elapsed > 0 ? 100.0 * static_cast<double>(total - thread.last) / elapsed : 0.0;
thread.last = total;
if (percent < 0.5 && id != client && id != present) continue;
std::string label = id == client ? "Client update" : id == present ? "Present" : thread.label;
summary.threads.push_back({static_cast<std::uint32_t>(id), std::move(label), percent});
}
last_wall_ = wall;
std::ranges::sort(summary.threads, [](const ThreadRow& a, const ThreadRow& b) { return a.cpu_percent > b.cpu_percent; });
}
~ThreadMonitor() {
for (auto& [id, thread] : threads_) if (thread.handle) CloseHandle(thread.handle);
}
};
void monitor() {
SetThreadDescription(GetCurrentThread(), L"ReSkate profiler");
ThreadMonitor threads;
auto last = std::chrono::steady_clock::now();
// Zone ticks to nanoseconds: the timestamp counter measured against the steady clock over
// each summary's second.
auto last_ticks = __rdtsc();
while (wanted()) {
zones_enabled.store(true, std::memory_order_relaxed);
Sleep(1000);
const auto now = std::chrono::steady_clock::now();
const double seconds = std::max(1e-3, std::chrono::duration<double>(now - last).count());
last = now;
const auto now_ticks = __rdtsc();
const double ms_per_tick = seconds * 1e3 / static_cast<double>(std::max<std::uint64_t>(1, now_ticks - last_ticks));
last_ticks = now_ticks;
auto next = std::make_shared<Summary>();
auto& s = *next;
s.seconds = seconds;
s.cores = GetActiveProcessorCount(ALL_PROCESSOR_GROUPS);
s.client_thread = detail::client_thread.load(std::memory_order_relaxed);
s.present_thread = detail::present_thread.load(std::memory_order_relaxed);
const auto ms = [](std::uint64_t ns) { return static_cast<double>(ns) / 1e6; };
const auto interval = client_interval.take(), frame = client_frame.take(), own = client_own.take();
s.client_updates_per_second = static_cast<double>(own.count) / seconds;
if (interval.count) {
s.client_interval_average_ms = ms(interval.sum) / static_cast<double>(interval.count);
s.client_interval_longest_ms = ms(interval.longest);
}
if (frame.count) {
s.client_frame_average_ms = ms(frame.sum) / static_cast<double>(frame.count);
s.client_frame_longest_ms = ms(frame.longest);
}
if (own.count) {
s.client_own_average_ms = ms(own.sum) / static_cast<double>(own.count);
s.client_own_longest_ms = ms(own.longest);
}
const auto presents = present_interval.take();
s.presents_per_second = static_cast<double>(presents.count) / seconds;
if (presents.count) {
s.present_interval_average_ms = ms(presents.sum) / static_cast<double>(presents.count);
s.present_interval_longest_ms = ms(presents.longest);
}
double hooks_ms{};
for (auto* site = sites.load(std::memory_order_acquire); site; site = site->next) {
const auto calls = site->calls.exchange(0, std::memory_order_relaxed);
const auto total = static_cast<double>(site->ticks.exchange(0, std::memory_order_relaxed)) * ms_per_tick;
const auto longest = static_cast<double>(site->longest.exchange(0, std::memory_order_relaxed)) * ms_per_tick;
if (!calls) continue;
ZoneRow row{site->name, static_cast<double>(calls) / seconds, total / seconds,
total * 1e3 / static_cast<double>(calls), longest};
if (std::string_view(site->name).starts_with("hooks/")) hooks_ms += total;
// Sites sharing a name (the same work in two hooks) show as one row.
const auto same = std::ranges::find(s.zones, row.name, &ZoneRow::name);
if (same == s.zones.end()) { s.zones.push_back(std::move(row)); continue; }
const auto calls_total = same->calls_per_second + row.calls_per_second;
same->average_microseconds = calls_total > 0 ? (same->average_microseconds * same->calls_per_second +
row.average_microseconds * row.calls_per_second) / calls_total : 0.0;
same->calls_per_second = calls_total;
same->milliseconds_per_second += row.milliseconds_per_second;
same->longest_milliseconds = std::max(same->longest_milliseconds, row.longest_milliseconds);
}
if (own.count) s.hooks_milliseconds_per_update = hooks_ms / static_cast<double>(own.count);
std::ranges::sort(s.zones, [](const ZoneRow& a, const ZoneRow& b) {
return a.milliseconds_per_second > b.milliseconds_per_second;
});
threads.sample(s);
std::lock_guard lock(summary_mutex);
latest_summary = std::move(next);
}
zones_enabled.store(false, std::memory_order_relaxed);
monitor_running.store(false, std::memory_order_release);
// Something may have asked again between the last check and the flag being cleared.
if (wanted()) detail::wake_monitor();
}
} // namespace
namespace detail {
std::string thread_label(HANDLE thread, std::uint32_t id) {
if (id == client_thread.load(std::memory_order_relaxed)) return "Client update";
if (id == present_thread.load(std::memory_order_relaxed)) return "Present";
return ThreadMonitor::label(thread);
}
void wake_monitor() noexcept {
if (!wanted() || monitor_running.exchange(true, std::memory_order_acq_rel)) return;
zones_enabled.store(true, std::memory_order_relaxed);
try { std::thread(monitor).detach(); }
catch (...) { monitor_running.store(false, std::memory_order_release); }
}
}
Site::Site(const char* site_name) noexcept : name(site_name) {
auto* head = sites.load(std::memory_order_relaxed);
do { next = head; } while (!sites.compare_exchange_weak(head, this, std::memory_order_release, std::memory_order_relaxed));
}
void record_client_update(std::uint64_t own_ns) noexcept {
// Known even while off, so a sample can target it straight away.
detail::client_thread.store(GetCurrentThreadId(), std::memory_order_relaxed);
if (!zones_enabled.load(std::memory_order_relaxed)) return;
static std::uint64_t last{};
const auto now = now_ns();
if (last && now > last) {
client_interval.add(now - last);
rings[static_cast<std::size_t>(Series::client_interval)].push(static_cast<float>(now - last) / 1e6f);
}
last = now;
client_own.add(own_ns);
rings[static_cast<std::size_t>(Series::client_own)].push(static_cast<float>(own_ns) / 1e6f);
}
void record_client_frame(std::uint64_t frame_ns) noexcept {
if (!zones_enabled.load(std::memory_order_relaxed)) return;
client_frame.add(frame_ns);
rings[static_cast<std::size_t>(Series::client_frame)].push(static_cast<float>(frame_ns) / 1e6f);
}
void record_present() noexcept {
detail::present_thread.store(GetCurrentThreadId(), std::memory_order_relaxed);
if (!zones_enabled.load(std::memory_order_relaxed)) return;
static std::uint64_t last{};
const auto now = now_ns();
if (last && now > last) {
present_interval.add(now - last);
rings[static_cast<std::size_t>(Series::present_interval)].push(static_cast<float>(now - last) / 1e6f);
}
last = now;
}
std::size_t history(Series series, std::span<float> out) noexcept {
auto& ring = rings[static_cast<std::size_t>(series)];
const auto written = ring.written.load(std::memory_order_acquire);
const auto count = static_cast<std::size_t>(std::min<std::uint64_t>({written, history_size, out.size()}));
for (std::size_t i = 0; i < count; ++i)
out[i] = ring.values[(written - count + i) % history_size].load(std::memory_order_relaxed);
return count;
}
void set_hud(bool visible) noexcept { hud_visible.store(visible, std::memory_order_release); detail::wake_monitor(); }
bool hud() noexcept { return hud_visible.load(std::memory_order_acquire); }
void set_window(bool visible) noexcept { window_visible.store(visible, std::memory_order_release); detail::wake_monitor(); }
bool window() noexcept { return window_visible.load(std::memory_order_acquire); }
bool active() noexcept { return wanted(); }
std::shared_ptr<const Summary> summary() noexcept {
std::lock_guard lock(summary_mutex);
return latest_summary;
}
} // namespace dingosdk::profiler
+147
View File
@@ -0,0 +1,147 @@
#pragma once
#include <atomic>
#include <chrono>
#include <intrin.h>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <span>
#include <string>
#include <vector>
// ReSkate's performance profiler (Extension/UI/Overlay/perf_overlay.cpp draws it, `perf` console
// commands drive it):
// * zones: scoped timers on ReSkate's own work, so its cost on the game's threads is visible;
// * the client update (the hooked native client tick) and presented frames, timed;
// * CPU use per thread of the game, which shows a runaway worker at a glance;
// * an in-process stack sampler for one thread, with a report the overlay lists and a file of
// collapsed stacks (load it in speedscope.app or flamegraph.pl for a flame graph).
// Everything is off until something asks for it: a zone then costs one relaxed atomic load.
namespace dingosdk::profiler {
// ---- zones ----------------------------------------------------------------------------------
// A place in ReSkate's code whose time is measured. Declared once (DINGO_PROFILE_ZONE makes a
// function-local static), timed on any thread. "a/b" names nest under "a" in the window.
// Zones count CPU timestamp ticks (__rdtsc, a few nanoseconds; invariant on every CPU the game
// runs on) rather than steady_clock (~25 ns through QueryPerformanceCounter): the hook zones run
// ~80,000 times a second, where the clock had cost more than the work it timed.
struct Site {
explicit Site(const char* name) noexcept;
const char* name;
std::atomic<std::uint64_t> ticks{0}, calls{0}, longest{0};
Site* next{};
void add(std::uint64_t elapsed) noexcept {
ticks.fetch_add(elapsed, std::memory_order_relaxed);
calls.fetch_add(1, std::memory_order_relaxed);
auto previous = longest.load(std::memory_order_relaxed);
while (elapsed > previous && !longest.compare_exchange_weak(previous, elapsed, std::memory_order_relaxed)) {}
}
};
extern std::atomic<bool> zones_enabled;
inline std::uint64_t now_ns() noexcept {
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
}
class Scope {
Site* site_;
std::uint64_t start_;
public:
explicit Scope(Site& site) noexcept
: site_(zones_enabled.load(std::memory_order_relaxed) ? &site : nullptr), start_(site_ ? __rdtsc() : 0) {}
~Scope() { if (site_) site_->add(__rdtsc() - start_); }
Scope(const Scope&) = delete;
Scope& operator=(const Scope&) = delete;
};
#define DINGO_PROFILE_CAT2(a, b) a##b
#define DINGO_PROFILE_CAT(a, b) DINGO_PROFILE_CAT2(a, b)
#define DINGO_PROFILE_ZONE(name) \
static ::dingosdk::profiler::Site DINGO_PROFILE_CAT(dingo_profile_site_, __LINE__){name}; \
const ::dingosdk::profiler::Scope DINGO_PROFILE_CAT(dingo_profile_scope_, __LINE__){ \
DINGO_PROFILE_CAT(dingo_profile_site_, __LINE__)}
// ---- frame timing ---------------------------------------------------------------------------
// ReSkate's client tick (once per client update): its own work, timed.
void record_client_update(std::uint64_t own_ns) noexcept;
// The game's whole client frame job, which the tick runs inside: the number that has to stay
// within the sim budget (16.7 ms at 60 Hz) or the game runs in slow motion.
void record_client_frame(std::uint64_t frame_ns) noexcept;
// Each presented frame (the overlay's Present hook).
void record_present() noexcept;
enum class Series { client_interval, client_frame, client_own, present_interval };
// The most recent values in milliseconds, oldest first; returns how many were written.
std::size_t history(Series series, std::span<float> out) noexcept;
// ---- what is on ------------------------------------------------------------------------------
// The HUD and window are the overlay's (both start off each launch); turning either on starts
// the timing and thread monitor.
void set_hud(bool visible) noexcept;
bool hud() noexcept;
void set_window(bool visible) noexcept;
bool window() noexcept;
// Whether anything wants the profiler running (HUD, window or a sample).
bool active() noexcept;
// ---- once-a-second summary ---------------------------------------------------------------------
struct ZoneRow {
std::string name;
double calls_per_second{}, milliseconds_per_second{}, average_microseconds{}, longest_milliseconds{};
};
struct ThreadRow {
std::uint32_t id{};
std::string label;
double cpu_percent{};
};
struct Summary {
double seconds{}; // the window the rates cover
double client_updates_per_second{}, client_interval_average_ms{}, client_interval_longest_ms{};
double client_frame_average_ms{}, client_frame_longest_ms{}; // the game's client frame job
double client_own_average_ms{}, client_own_longest_ms{};
double hooks_milliseconds_per_update{}; // zones named "hooks/..." per client update
double presents_per_second{}, present_interval_average_ms{}, present_interval_longest_ms{};
double process_cpu_percent{}; // 100 = one whole core
unsigned cores{};
std::uint32_t client_thread{}, present_thread{};
std::vector<ZoneRow> zones; // busiest first
std::vector<ThreadRow> threads; // busiest first
};
std::shared_ptr<const Summary> summary() noexcept;
// ---- the stack sampler -------------------------------------------------------------------------
// `all` samples every thread that was busy (3% of a core or more) when the sample started, and
// reports where the process's CPU work went; time spent waiting in the kernel is left out.
enum class Target { client, present, thread, all };
struct SampleRequest {
Target target = Target::client;
std::uint32_t thread_id{}; // for Target::thread
double seconds = 10;
unsigned rate = 1000; // samples per second
};
struct SampledFunction {
std::string name; // "Skate 0x14xxxxxxx [label]" or "module!symbol"
std::uint64_t address{}; // function start; Skate.exe ones as Ghidra addresses
std::uint32_t self{}, total{}; // samples with it on top / anywhere on the stack
};
struct SampleReport {
bool running{};
double progress{}; // 0..1 while running
std::string target;
std::uint32_t thread_id{};
std::uint32_t samples{}, rate{};
double seconds{};
double busy_percent{}; // samples not waiting in the kernel
std::vector<SampledFunction> functions; // by self, then total
std::vector<ThreadRow> threads; // Target::all: each thread's share of the busy samples
std::filesystem::path report, folded; // written next to ReSkate.log
std::string error;
};
// Starts a sample on its own thread; false (with the reason) if one is running or the target
// thread is unknown.
bool start_sampling(const SampleRequest& request, std::string& error);
void stop_sampling() noexcept;
std::shared_ptr<const SampleReport> sample_report() noexcept;
// Engine function names for the sampler's report, by RVA (Engine/Game/Build/<build>/profiler.h,
// plus any "<rva or 0x14 address> <name>" lines in ReSkate.labels.tsv next to Skate.exe).
struct Label { std::uintptr_t rva; const char* name; };
void set_engine_labels(std::span<const Label> labels) noexcept;
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include "profiler.h"
#include <Windows.h>
namespace dingosdk::profiler::detail {
// Threads the summary and the sampler name: the one running the client update, and the one
// presenting frames.
extern std::atomic<std::uint32_t> client_thread, present_thread;
// True while a sample runs; keeps the monitor (and so the zones) alive.
extern std::atomic<bool> sampling;
// Starts the once-a-second monitor if anything wants it.
void wake_monitor() noexcept;
// What a thread is, for reports: its description, or what its start address belongs to.
std::string thread_label(HANDLE thread, std::uint32_t id);
}
+548
View File
@@ -0,0 +1,548 @@
#include "profiler_internal.h"
#include "Engine/Core/Log/logging.h"
#include <DbgHelp.h>
#include <TlHelp32.h>
#include <psapi.h>
#include <algorithm>
#include <chrono>
#include <format>
#include <fstream>
#include <mutex>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <functional>
// The stack sampler. A sample suspends the target thread, takes its context and walks its stack,
// then resumes it. While the target is suspended this thread must not take any lock the target
// might hold -- the heap, the loader, DbgHelp -- or both stop for good. So the walk uses only
// memory prepared beforehand: each module's own unwind table (found by binary search, not
// RtlLookupFunctionEntry, which locks) and RtlVirtualUnwind, which only reads. Names are looked
// up after sampling ends.
namespace dingosdk::profiler {
namespace {
constexpr std::uint32_t max_depth = 96;
constexpr std::uintptr_t ghidra_image_base = 0x140000000;
struct Module {
std::uintptr_t base{}, end{};
const RUNTIME_FUNCTION* functions{};
std::uint32_t count{};
std::wstring path;
std::string name;
bool exe{};
};
struct Modules {
std::vector<Module> list; // sorted by base
const Module* find(std::uintptr_t pc) const noexcept {
std::size_t lo = 0, hi = list.size();
while (lo < hi) {
const auto mid = (lo + hi) / 2;
if (list[mid].end <= pc) lo = mid + 1; else hi = mid;
}
return lo < list.size() && list[lo].base <= pc && pc < list[lo].end ? &list[lo] : nullptr;
}
};
std::string narrow(std::wstring_view text) {
std::string result;
for (const auto c : text) result.push_back(c < 128 ? static_cast<char>(c) : '?');
return result;
}
Modules snapshot_modules() {
Modules result;
std::vector<HMODULE> handles(1024);
DWORD needed{};
if (!EnumProcessModulesEx(GetCurrentProcess(), handles.data(), static_cast<DWORD>(handles.size() * sizeof(HMODULE)),
&needed, LIST_MODULES_ALL)) return result;
handles.resize(std::min<std::size_t>(handles.size(), needed / sizeof(HMODULE)));
const auto exe = GetModuleHandleW(nullptr);
for (const auto handle : handles) {
MODULEINFO info{};
if (!GetModuleInformation(GetCurrentProcess(), handle, &info, sizeof(info))) continue;
Module module;
module.base = reinterpret_cast<std::uintptr_t>(info.lpBaseOfDll);
module.end = module.base + info.SizeOfImage;
module.exe = handle == exe;
wchar_t path[MAX_PATH]{};
if (GetModuleFileNameW(handle, path, MAX_PATH)) {
module.path = path;
const auto slash = module.path.find_last_of(L"\\/");
module.name = narrow(slash == std::wstring::npos ? module.path : module.path.substr(slash + 1));
}
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(module.base);
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS64*>(module.base + dos->e_lfanew);
if (dos->e_magic == IMAGE_DOS_SIGNATURE && nt->Signature == IMAGE_NT_SIGNATURE &&
nt->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC &&
nt->OptionalHeader.NumberOfRvaAndSizes > IMAGE_DIRECTORY_ENTRY_EXCEPTION) {
const auto& directory = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];
if (directory.VirtualAddress && directory.Size && directory.VirtualAddress + directory.Size <= info.SizeOfImage) {
module.functions = reinterpret_cast<const RUNTIME_FUNCTION*>(module.base + directory.VirtualAddress);
module.count = directory.Size / sizeof(RUNTIME_FUNCTION);
}
}
result.list.push_back(std::move(module));
}
std::ranges::sort(result.list, {}, &Module::base);
return result;
}
const RUNTIME_FUNCTION* lookup(const Module& module, std::uintptr_t pc) noexcept {
if (!module.functions) return nullptr;
const auto rva = static_cast<std::uint32_t>(pc - module.base);
std::uint32_t lo = 0, hi = module.count;
while (lo < hi) {
const auto mid = (lo + hi) / 2;
if (module.functions[mid].EndAddress <= rva) lo = mid + 1; else hi = mid;
}
return lo < module.count && module.functions[lo].BeginAddress <= rva ? &module.functions[lo] : nullptr;
}
// Unwinds a suspended thread's context into frames. No allocation, no locks; a fault on a
// corrupt stack just ends the walk.
std::uint32_t walk(const Modules& modules, CONTEXT& context, std::uint64_t* frames) noexcept {
std::uint32_t depth = 0;
__try {
while (depth < max_depth && context.Rip) {
const auto pc = context.Rip, sp = context.Rsp;
frames[depth++] = pc;
const auto* module = modules.find(pc);
if (!module) break;
// Past the first frame the PC is a return address, which can sit just after the end of
// its function: look it up one byte back.
const auto* entry = lookup(*module, depth > 1 ? pc - 1 : pc);
if (!entry) {
// A leaf function: no frame, the return address is on top of the stack.
context.Rip = *reinterpret_cast<const DWORD64*>(context.Rsp);
context.Rsp += 8;
} else {
void* handler_data{};
DWORD64 establisher{};
RtlVirtualUnwind(UNW_FLAG_NHANDLER, module->base, pc, const_cast<PRUNTIME_FUNCTION>(entry), &context,
&handler_data, &establisher, nullptr);
}
if (context.Rsp <= sp) break; // a stack only unwinds upwards
}
} __except (EXCEPTION_EXECUTE_HANDLER) {}
return depth;
}
// The function a frame belongs to: its primary unwind entry (chained entries describe parts of
// the same function) -- the start Ghidra shows too.
std::uintptr_t function_start(const Modules& modules, std::uintptr_t pc, const Module** owner) noexcept {
const auto* module = modules.find(pc);
if (owner) *owner = module;
if (!module) return pc;
const auto* entry = lookup(*module, pc);
for (int guard = 0; entry && guard < 32; ++guard) {
const auto* info = reinterpret_cast<const std::uint8_t*>(module->base + entry->UnwindData);
if (!(info[0] >> 3 & UNW_FLAG_CHAININFO)) break;
const auto codes = info[2];
entry = reinterpret_cast<const RUNTIME_FUNCTION*>(info + 4 + ((codes + 1) & ~1) * 2);
}
return entry ? module->base + entry->BeginAddress : pc;
}
// ---- names ----
std::mutex labels_mutex;
std::vector<Label> engine_labels;
std::unordered_map<std::uintptr_t, std::string> load_labels(const Modules& modules) {
std::unordered_map<std::uintptr_t, std::string> result;
{
std::lock_guard lock(labels_mutex);
for (const auto& label : engine_labels) result[label.rva] = label.name;
}
// ReSkate.labels.tsv next to Skate.exe: "<RVA or Ghidra address> <name>" per line, so
// functions named while reading a profile show up by name in the next one.
for (const auto& module : modules.list) {
if (!module.exe) continue;
const auto folder = std::filesystem::path(module.path).parent_path();
std::ifstream file(folder / L"ReSkate.labels.tsv");
std::string line;
while (std::getline(file, line)) {
const auto first = line.find_first_not_of(" \t");
if (first == std::string::npos || line[first] == '#') continue;
const auto gap = line.find_first_of(" \t", first);
if (gap == std::string::npos) continue;
const auto rest = line.find_first_not_of(" \t", gap);
if (rest == std::string::npos) continue;
std::uintptr_t address{};
try { address = std::stoull(line.substr(first, gap - first), nullptr, 16); } catch (...) { continue; }
if (address >= ghidra_image_base) address -= ghidra_image_base;
auto name = line.substr(rest);
while (!name.empty() && (name.back() == '\r' || name.back() == ' ' || name.back() == '\t')) name.pop_back();
if (!name.empty()) result[address] = name;
}
}
return result;
}
// DbgHelp is single-threaded and the process may use it elsewhere (a crash handler): a private
// session under a handle of our own, used only from the sampler thread, under this lock.
std::mutex symbols_mutex;
const HANDLE symbol_session = reinterpret_cast<HANDLE>(static_cast<std::uintptr_t>(0x5245534b)); // "RESK"
bool symbols_ready{};
std::unordered_set<std::uintptr_t> symbols_loaded;
std::string symbol_name(const Module& module, std::uintptr_t address) {
std::lock_guard lock(symbols_mutex);
if (!symbols_ready) {
SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_NO_PROMPTS | SYMOPT_FAIL_CRITICAL_ERRORS);
symbols_ready = SymInitializeW(symbol_session, nullptr, FALSE) != FALSE;
}
if (symbols_ready && symbols_loaded.insert(module.base).second)
SymLoadModuleExW(symbol_session, nullptr, module.path.c_str(), nullptr, module.base,
static_cast<DWORD>(module.end - module.base), nullptr, 0);
alignas(SYMBOL_INFO) char buffer[sizeof(SYMBOL_INFO) + 512]{};
auto* info = reinterpret_cast<SYMBOL_INFO*>(buffer);
info->SizeOfStruct = sizeof(SYMBOL_INFO);
info->MaxNameLen = 511;
DWORD64 displacement{};
if (symbols_ready && SymFromAddr(symbol_session, address, &displacement, info) && info->Name[0])
return module.name + "!" + info->Name;
return std::format("{}+0x{:x}", module.name, address - module.base);
}
bool waiting(std::string_view name) {
constexpr std::string_view waits[] = {"WaitFor", "DelayExecution", "RemoveIoCompletion", "YieldExecution",
"SignalAndWait", "WaitForAlertByThreadId", "WaitForWorkViaWorkerFactory", "ReplyWaitReceivePort"};
if (!name.starts_with("ntdll.dll!") && !name.starts_with("win32u.dll!")) return false;
return std::ranges::any_of(waits, [&](std::string_view wait) { return name.find(wait) != std::string_view::npos; });
}
// ---- state ----
std::mutex report_mutex;
std::shared_ptr<const SampleReport> latest_report = std::make_shared<SampleReport>();
std::atomic<bool> stop_requested{false};
void publish(SampleReport report) {
std::lock_guard lock(report_mutex);
latest_report = std::make_shared<SampleReport>(std::move(report));
}
std::string target_name(Target target, std::uint32_t id) {
switch (target) {
case Target::client: return "client update";
case Target::present: return "present";
case Target::all: return "all busy threads";
default: return std::format("thread {}", id);
}
}
std::string sanitize(std::string text) {
for (auto& c : text) if (c == ';' || c == '\n' || c == '\r') c = ' ';
return text;
}
struct Sampled { HANDLE handle{}; std::uint32_t id{}; std::string label; };
// One sample: where its frames start in the pool, how many, and which thread it came from.
struct Record { std::uint32_t offset{}; std::uint16_t thread{}; std::uint8_t depth{}; };
// The pool holds at most this many frames (64 MiB); a sample that no longer fits ends the run.
constexpr std::size_t pool_limit = 8u << 20;
void run(SampleRequest request, std::vector<Sampled> threads) {
SetThreadDescription(GetCurrentThread(), L"ReSkate sampler");
SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_HIGHEST);
const bool all = request.target == Target::all;
SampleReport report;
report.running = true;
report.target = target_name(request.target, threads.front().id);
report.thread_id = all ? 0 : threads.front().id;
report.rate = request.rate;
const auto modules = snapshot_modules();
const auto wanted = static_cast<std::size_t>(request.seconds * request.rate) * threads.size() + 16;
const auto capacity = std::min<std::size_t>(wanted, pool_limit / 16);
std::vector<std::uint64_t> pool;
std::vector<Record> records;
const auto close = [&] { for (const auto& t : threads) CloseHandle(t.handle); };
try {
pool.resize(std::min(pool_limit, capacity * 48 + max_depth));
records.reserve(capacity);
} catch (...) {
report.running = false;
report.error = "Not enough memory for the sample buffer.";
publish(std::move(report));
close();
detail::sampling.store(false, std::memory_order_release);
return;
}
publish(report);
const HANDLE timer = CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS);
const auto period = std::chrono::nanoseconds(1'000'000'000 / request.rate);
const auto start = std::chrono::steady_clock::now();
const auto finish = start + std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::duration<double>(request.seconds));
auto next = start;
auto next_progress = start;
std::size_t used = 0;
std::vector<char> ended(threads.size());
bool full = false;
while (!full && !stop_requested.load(std::memory_order_acquire)) {
const auto now = std::chrono::steady_clock::now();
if (now >= finish) break;
bool any = false;
for (std::size_t t = 0; t < threads.size(); ++t) {
if (ended[t]) continue;
if (records.size() == capacity || pool.size() - used < max_depth) { full = true; break; }
// Nothing between Suspend and Resume may allocate or lock: the frames go straight
// into the pool, and the record is added after the thread runs again.
if (SuspendThread(threads[t].handle) == static_cast<DWORD>(-1)) { ended[t] = 1; continue; }
any = true;
alignas(16) CONTEXT context{};
context.ContextFlags = CONTEXT_FULL;
std::uint32_t depth = 0;
if (GetThreadContext(threads[t].handle, &context)) depth = walk(modules, context, pool.data() + used);
ResumeThread(threads[t].handle);
if (!depth) continue;
records.push_back({static_cast<std::uint32_t>(used), static_cast<std::uint16_t>(t), static_cast<std::uint8_t>(depth)});
used += depth;
}
if (!any && !full) { report.error = "The thread has ended."; break; }
if (now >= next_progress) {
next_progress = now + std::chrono::milliseconds(250);
report.progress = std::chrono::duration<double>(now - start).count() / request.seconds;
report.samples = static_cast<std::uint32_t>(records.size());
publish(report);
}
next += period;
const auto wait = next - std::chrono::steady_clock::now();
if (wait > std::chrono::microseconds(50)) {
if (timer) {
LARGE_INTEGER due{};
due.QuadPart = -std::max<long long>(1, std::chrono::duration_cast<std::chrono::nanoseconds>(wait).count() / 100);
if (SetWaitableTimer(timer, &due, 0, nullptr, nullptr, FALSE)) WaitForSingleObject(timer, 100);
} else {
Sleep(1);
}
} else if (wait < -period * 4) {
next = std::chrono::steady_clock::now(); // fell behind: don't burst
}
}
if (timer) CloseHandle(timer);
close();
report.seconds = std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count();
report.progress = 1;
// ---- aggregate: by function, and whole stacks for the flame graph ----
try {
struct Counts { std::uint32_t self{}, total{}; const Module* module{}; };
std::unordered_map<std::uintptr_t, Counts> functions;
std::vector<std::uintptr_t> stack;
std::unordered_map<std::string, std::uint32_t> folded;
const auto labels = load_labels(modules);
std::unordered_map<std::uintptr_t, std::string> names;
std::unordered_map<std::uintptr_t, const Module*> owners;
const auto name_of = [&](std::uintptr_t key, const Module* module) -> const std::string& {
auto it = names.find(key);
if (it != names.end()) return it->second;
std::string name;
if (!module) name = std::format("0x{:x}", key);
else if (module->exe) {
const auto rva = key - module->base;
const auto label = labels.find(rva);
name = std::format("Skate 0x{:x}", ghidra_image_base + rva);
if (label != labels.end()) name += " " + label->second;
} else name = symbol_name(*module, key);
return names.emplace(key, std::move(name)).first->second;
};
std::vector<std::uint32_t> busy_by_thread(threads.size());
std::uint32_t idle{}, counted{};
for (const auto& record : records) {
const auto* sample = pool.data() + record.offset;
stack.clear();
for (std::uint32_t f = 0; f < record.depth; ++f) {
const Module* module{};
stack.push_back(function_start(modules, f ? sample[f] - 1 : sample[f], &module));
owners[stack.back()] = module;
}
const bool waits = !stack.empty() && waiting(name_of(stack.front(), owners[stack.front()]));
if (waits) ++idle; else ++busy_by_thread[record.thread];
// Across threads, a profile of the work: waiting threads are not where CPU went.
if (all && waits) continue;
++counted;
for (std::size_t f = 0; f < stack.size(); ++f) {
auto& counts = functions[stack[f]];
counts.module = owners[stack[f]];
if (!f) ++counts.self;
const auto here = stack.begin() + static_cast<std::ptrdiff_t>(f);
if (std::find(stack.begin(), here, stack[f]) == here) ++counts.total;
}
std::string line = all ? sanitize(std::format("thread {} {}", threads[record.thread].id, threads[record.thread].label))
: std::string{};
for (auto it = stack.rbegin(); it != stack.rend(); ++it) {
if (!line.empty()) line += ';';
line += sanitize(name_of(*it, owners[*it]));
}
++folded[line];
}
const auto total = records.size();
report.samples = counted;
report.busy_percent = total ? 100.0 * static_cast<double>(total - idle) / static_cast<double>(total) : 0.0;
if (all) {
const auto busy_total = std::max<std::uint32_t>(1, counted);
for (std::size_t t = 0; t < threads.size(); ++t)
if (busy_by_thread[t])
report.threads.push_back({threads[t].id, threads[t].label, 100.0 * busy_by_thread[t] / busy_total});
std::ranges::sort(report.threads, [](const ThreadRow& a, const ThreadRow& b) { return a.cpu_percent > b.cpu_percent; });
}
for (const auto& [key, counts] : functions)
report.functions.push_back({name_of(key, counts.module),
counts.module && counts.module->exe ? ghidra_image_base + (key - counts.module->base) : key,
counts.self, counts.total});
std::ranges::sort(report.functions, [](const SampledFunction& a, const SampledFunction& b) {
return a.self != b.self ? a.self > b.self : a.total > b.total;
});
// ---- files next to ReSkate.log ----
const auto directory = logging::status().directory;
SYSTEMTIME now{};
GetLocalTime(&now);
const auto stem = std::format("profile-{:04}{:02}{:02}-{:02}{:02}{:02}-{}", now.wYear, now.wMonth, now.wDay,
now.wHour, now.wMinute, now.wSecond, request.target == Target::client ? "client" :
request.target == Target::present ? "present" : all ? "all" : std::to_string(threads.front().id));
report.report = directory / (stem + ".txt");
report.folded = directory / (stem + ".folded");
const double per = static_cast<double>(std::max<std::uint32_t>(1, counted));
{
std::ofstream out(report.report);
if (all) {
out << std::format("ReSkate profile: all busy threads ({}), {:.1f} s, {} samples at {} Hz per thread\n",
threads.size(), report.seconds, total, request.rate);
out << std::format("Busy {:.1f}% of samples; the {} busy ones are profiled below (waiting left out).\n",
report.busy_percent, counted);
out << "\n Share Thread\n";
for (const auto& t : report.threads) out << std::format("{:5.1f}% {} {}\n", t.cpu_percent, t.id, t.label);
out << "\n";
} else {
out << std::format("ReSkate profile: {} (thread {}), {:.1f} s, {} samples at {} Hz\n", report.target,
threads.front().id, report.seconds, total, request.rate);
out << std::format("Busy {:.1f}% (the rest waiting in the kernel)\n", report.busy_percent);
}
out << "Skate.exe functions are Ghidra addresses (image base 0x140000000). Name them in\n"
"ReSkate.labels.tsv next to Skate.exe (\"<address> <name>\" per line) for the next report.\n"
"Flame graph: open the .folded file at https://www.speedscope.app\n\n";
out << " Self Total Function\n";
for (const auto& f : report.functions) {
if (f.total * 1000.0 < per) continue; // under 0.1%
out << std::format("{:5.1f}% {:5.1f}% {}\n", 100.0 * f.self / per, 100.0 * f.total / per, f.name);
}
}
{
std::ofstream out(report.folded);
for (const auto& [line, n] : folded) out << line << ' ' << n << '\n';
}
if (report.functions.size() > 400) report.functions.resize(400);
logging::log(logging::Level::info, logging::Channel::diagnostics, "Profiler: sampled {} for {:.1f} s ({} samples); report {}",
report.target, report.seconds, total, narrow(report.report.wstring()));
} catch (const std::exception& e) {
report.error = std::string("Report failed: ") + e.what();
}
report.running = false;
publish(std::move(report));
detail::sampling.store(false, std::memory_order_release);
}
std::uint64_t cpu_time(HANDLE thread) {
FILETIME created, exited, kernel, user;
if (!GetThreadTimes(thread, &created, &exited, &kernel, &user)) return 0;
const auto value = [](const FILETIME& f) { return (static_cast<std::uint64_t>(f.dwHighDateTime) << 32) | f.dwLowDateTime; };
return value(kernel) + value(user);
}
// The process's threads that used 3% of a core or more over a quarter second (at most 48),
// other than ReSkate's own profiler threads.
std::vector<Sampled> busy_threads() {
std::vector<Sampled> candidates;
const HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0);
if (snapshot == INVALID_HANDLE_VALUE) return {};
THREADENTRY32 entry{sizeof(entry)};
const auto self = GetCurrentProcessId(), me = GetCurrentThreadId();
for (BOOL ok = Thread32First(snapshot, &entry); ok; ok = Thread32Next(snapshot, &entry)) {
if (entry.th32OwnerProcessID != self || entry.th32ThreadID == me) continue;
const HANDLE handle = OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION, FALSE,
entry.th32ThreadID);
if (handle) candidates.push_back({handle, entry.th32ThreadID, {}});
}
CloseHandle(snapshot);
std::vector<std::uint64_t> before(candidates.size());
for (std::size_t i = 0; i < candidates.size(); ++i) before[i] = cpu_time(candidates[i].handle);
Sleep(250);
std::vector<std::pair<std::uint64_t, std::size_t>> used;
for (std::size_t i = 0; i < candidates.size(); ++i) used.push_back({cpu_time(candidates[i].handle) - before[i], i});
std::ranges::sort(used, std::greater<>{});
std::vector<Sampled> result;
for (const auto& [time, i] : used) {
auto& c = candidates[i];
PWSTR description{};
bool ours = false;
if (SUCCEEDED(GetThreadDescription(c.handle, &description)) && description) {
ours = std::wstring_view(description).starts_with(L"ReSkate ");
LocalFree(description);
}
// 3% of a core over 250 ms, in 100 ns units.
if (ours || time < 75'000 || result.size() == 48) { CloseHandle(c.handle); continue; }
c.label = detail::thread_label(c.handle, c.id);
result.push_back(std::move(c));
}
return result;
}
} // namespace
bool start_sampling(const SampleRequest& request, std::string& error) {
if (detail::sampling.exchange(true, std::memory_order_acq_rel)) { error = "A sample is already running."; return false; }
auto bounded = request;
bounded.seconds = std::clamp(bounded.seconds, 0.5, 60.0);
bounded.rate = std::clamp(bounded.rate, 50u, 4000u);
stop_requested.store(false, std::memory_order_release);
detail::wake_monitor();
try {
// Choosing the busy threads takes a quarter second: done on the sampler's own thread.
std::thread([bounded] {
std::vector<Sampled> threads;
std::string failure;
if (bounded.target == Target::all) {
threads = busy_threads();
if (threads.empty()) failure = "No busy threads found.";
} else {
const std::uint32_t id = bounded.target == Target::client ? detail::client_thread.load()
: bounded.target == Target::present ? detail::present_thread.load() : bounded.thread_id;
const HANDLE handle = id ? OpenThread(THREAD_SUSPEND_RESUME | THREAD_GET_CONTEXT | THREAD_QUERY_INFORMATION,
FALSE, id) : nullptr;
if (!id) failure = bounded.target == Target::thread ? "No thread id given." :
"That thread has not run yet; try again once the game is up.";
else if (!handle) failure = std::format("Cannot open thread {} (error {}).", id, GetLastError());
else threads.push_back({handle, id, detail::thread_label(handle, id)});
}
if (!failure.empty()) {
SampleReport report;
report.error = failure;
publish(std::move(report));
detail::sampling.store(false, std::memory_order_release);
return;
}
run(bounded, std::move(threads));
}).detach();
} catch (...) {
detail::sampling.store(false, std::memory_order_release);
error = "Cannot start the sampler thread.";
return false;
}
return true;
}
void stop_sampling() noexcept { stop_requested.store(true, std::memory_order_release); }
std::shared_ptr<const SampleReport> sample_report() noexcept {
std::lock_guard lock(report_mutex);
return latest_report;
}
void set_engine_labels(std::span<const Label> labels) noexcept {
try {
std::lock_guard lock(labels_mutex);
engine_labels.assign(labels.begin(), labels.end());
} catch (...) {}
}
}
+387
View File
@@ -0,0 +1,387 @@
#include "save_database.h"
#include <sqlite3.h>
#include <Windows.h>
#include <algorithm>
#include <cmath>
#include <optional>
#include <stdexcept>
namespace dingosdk::storage {
namespace {
constexpr int application_id = 0x52534b54; // RSKT
constexpr std::size_t max_bytes = 64 * 1024 * 1024, max_record = 4 * 1024 * 1024, max_nodes = 262144;
void check(bool ok, const char* message) { if (!ok) throw std::runtime_error(message); }
void sql_check(int code, sqlite3* db) {
if (code != SQLITE_OK) throw std::runtime_error(std::string("Save database: ") + sqlite3_errmsg(db));
}
void execute(sqlite3* db, const char* sql) { sql_check(sqlite3_exec(db, sql, nullptr, nullptr, nullptr), db); }
class Connection {
public:
sqlite3* db{};
Connection(const std::filesystem::path& path, int flags) {
const auto name = path.u8string();
const auto code = sqlite3_open_v2(reinterpret_cast<const char*>(name.c_str()), &db, flags | SQLITE_OPEN_FULLMUTEX, nullptr);
if (code != SQLITE_OK) {
const std::string error = db ? sqlite3_errmsg(db) : "Cannot allocate SQLite connection";
if (db) sqlite3_close(db);
db = nullptr; throw std::runtime_error("Save database: " + error);
}
sqlite3_extended_result_codes(db, 1);
sqlite3_limit(db, SQLITE_LIMIT_LENGTH, static_cast<int>(max_record + 8192));
sqlite3_limit(db, SQLITE_LIMIT_SQL_LENGTH, 16384);
sqlite3_db_config(db, SQLITE_DBCONFIG_DEFENSIVE, 1, nullptr);
sqlite3_db_config(db, SQLITE_DBCONFIG_TRUSTED_SCHEMA, 0, nullptr);
// ReSkate owns its writers. External contention fails promptly without
// pausing a native callback for a long database busy timeout.
sqlite3_busy_timeout(db, 0);
}
~Connection() { if (db) sqlite3_close(db); }
Connection(const Connection&) = delete;
Connection& operator=(const Connection&) = delete;
};
class Statement {
public:
sqlite3_stmt* statement{};
sqlite3* db{};
Statement(sqlite3* connection, const char* sql) : db(connection) {
sql_check(sqlite3_prepare_v2(db, sql, -1, &statement, nullptr), db);
}
~Statement() { sqlite3_finalize(statement); }
Statement(const Statement&) = delete;
Statement& operator=(const Statement&) = delete;
void bind(int index, std::string_view value) {
check(value.size() <= max_record, "Save database record exceeds limit");
sql_check(sqlite3_bind_text(statement, index, value.data(), static_cast<int>(value.size()), SQLITE_TRANSIENT), db);
}
void bind(int index, const Json& value) {
if (value.is_string()) bind(index, std::string_view(value.string()));
else if (value.is_null()) sql_check(sqlite3_bind_null(statement, index), db);
else if (value.is_boolean()) sql_check(sqlite3_bind_int(statement, index, value.get<bool>() ? 1 : 0), db);
else if (value.is_number_integer()) {
check(!value.is_number_unsigned() || value.get<std::uint64_t>() <= INT64_MAX, "SQL integer overflow");
sql_check(sqlite3_bind_int64(statement, index, value.get<std::int64_t>()), db);
} else if (value.is_number_float()) sql_check(sqlite3_bind_double(statement, index, value.get<double>()), db);
else throw std::runtime_error("SQL columns require scalar values");
}
Json value(int column) const {
switch (sqlite3_column_type(statement, column)) {
case SQLITE_TEXT: return text(column);
case SQLITE_INTEGER: {
const auto n = sqlite3_column_int64(statement, column);
return n >= 0 ? Json(static_cast<std::uint64_t>(n)) : Json(n);
}
case SQLITE_FLOAT: return sqlite3_column_double(statement, column);
case SQLITE_NULL: return nullptr;
default: throw std::runtime_error("Unexpected SQL column type");
}
}
bool row() {
const auto code = sqlite3_step(statement);
if (code == SQLITE_ROW) return true;
if (code == SQLITE_DONE) return false;
sql_check(code, db); return false;
}
std::string text(int column) const {
check(sqlite3_column_type(statement, column) == SQLITE_TEXT, "Invalid save database record type");
const auto* data = sqlite3_column_text(statement, column);
const auto size = sqlite3_column_bytes(statement, column);
check(data && size >= 0 && static_cast<std::size_t>(size) <= max_record, "Invalid save database text");
return {reinterpret_cast<const char*>(data), static_cast<std::size_t>(size)};
}
void run() {
check(!row(), "Unexpected save database result");
sql_check(sqlite3_reset(statement), db); sql_check(sqlite3_clear_bindings(statement), db);
}
};
class Transaction {
sqlite3* db_;
bool committed_{};
public:
explicit Transaction(sqlite3* db, bool write) : db_(db) { execute(db_, write ? "BEGIN IMMEDIATE" : "BEGIN"); }
~Transaction() { if (!committed_) sqlite3_exec(db_, "ROLLBACK", nullptr, nullptr, nullptr); }
void commit() { execute(db_, "COMMIT"); committed_ = true; }
};
int integer(sqlite3* db, const char* query) {
Statement s(db, query); check(s.row(), "Missing save database metadata");
const auto value = sqlite3_column_int(s.statement, 0);
check(!s.row(), "Duplicate save database metadata"); return value;
}
void verify(sqlite3* db, std::string_view kind) {
check(integer(db, "PRAGMA application_id") == application_id && integer(db, "PRAGMA user_version") == 1,
"Unsupported ReSkate save database; original file retained");
Statement type(db, "SELECT value FROM metadata WHERE key='kind'");
check(type.row() && type.text(0) == kind && !type.row(), "Wrong ReSkate save database kind");
Statement integrity(db, "PRAGMA quick_check(1)");
check(integrity.row() && integrity.text(0) == "ok" && !integrity.row(), "Save database integrity check failed");
}
// Check complete trees without serializing the entire profile on each save.
// Bounds also cover arrays stored as one record and future extension members.
void bounds(const Json& value, std::size_t& nodes, std::size_t& bytes, unsigned depth = 0) {
check(depth <= 32 && ++nodes <= max_nodes, "Save database tree exceeds complexity limit");
bytes += 32;
if (value.is_string()) bytes += value.string().size();
else if (value.is_number_float()) check(std::isfinite(value.get<double>()), "Non-finite save value");
else if (value.is_object()) for (const auto& [key, child] : value.items()) {
bytes += key.size(); bounds(child, nodes, bytes, depth + 1);
}
else if (value.is_array()) for (const auto& child : value) bounds(child, nodes, bytes, depth + 1);
check(bytes <= max_bytes, "Save database tree exceeds size limit");
}
void validate_tree(const Json& value) {
check(value.is_object(), "Save database requires an object root");
std::size_t nodes{}, bytes{}; bounds(value, nodes, bytes);
}
std::vector<std::string> columns(const SaveTable& table) {
std::vector<std::string> result;
std::string_view names = table.columns;
while (!names.empty()) {
const auto comma = names.find(','); result.emplace_back(names.substr(0, comma));
if (comma == std::string_view::npos) break; names.remove_prefix(comma + 1);
}
check(!result.empty() && table.key_columns > 0 && table.key_columns <= result.size(), "Invalid save schema");
return result;
}
void validate_tables(const Json& value, SaveSchema schema) {
validate_tree(value);
check(value.size() == schema.tables.size(), "Unexpected save tables");
for (const auto& table : schema.tables) {
check(value.contains(table.name) && value.at(table.name).is_object(), "Missing save table");
const auto width = columns(table).size();
for (const auto& [key, row] : value.at(table.name).items()) {
check(row.is_array() && row.size() == width && row_key(row, table.key_columns) == key, "Invalid save row");
for (const auto& cell : row) check(!cell.is_object() && !cell.is_array(), "Invalid SQL cell");
}
}
}
Json load(sqlite3* db, SaveSchema schema) {
Json root = Json::object(); std::size_t count{}, bytes{};
for (const auto& table : schema.tables) {
const auto names = columns(table);
auto& rows = root[table.name]; rows = Json::object();
const auto sql = "SELECT " + std::string(table.columns) + " FROM " + std::string(table.name);
Statement records(db, sql.c_str());
while (records.row()) {
check(++count <= max_nodes, "Too many save rows");
Json row = Json::array();
for (std::size_t i = 0; i < names.size(); ++i) {
auto value = records.value(static_cast<int>(i));
bytes += value.is_string() ? value.string().size() : 16;
check(bytes <= max_bytes, "Save database exceeds limit"); row.push_back(std::move(value));
}
const auto key = row_key(row, table.key_columns);
check(!rows.contains(key), "Duplicate save database key"); rows[key] = std::move(row);
}
}
validate_tables(root, schema); return root;
}
void apply_changes(sqlite3* db, SaveSchema schema, const Json* before, const Json& after) {
for (const auto& table : schema.tables) {
const auto& next = after.at(table.name);
const Json* old = before ? &before->at(table.name) : nullptr;
if (old && *old == next) continue;
const auto names = columns(table);
std::string insert = "INSERT INTO " + std::string(table.name) + '(' + std::string(table.columns) + ") VALUES(";
std::string remove = "DELETE FROM " + std::string(table.name) + " WHERE ";
for (std::size_t i = 0; i < names.size(); ++i) { if (i) insert += ','; insert += '?'; }
insert += ") ON CONFLICT(";
for (unsigned i = 0; i < table.key_columns; ++i) {
if (i) { insert += ','; remove += " AND "; }
insert += names[i]; remove += names[i] + "=?";
}
insert += ')';
if (table.key_columns == names.size()) insert += " DO NOTHING";
else {
insert += " DO UPDATE SET ";
for (std::size_t i = table.key_columns; i < names.size(); ++i) {
if (i != table.key_columns) insert += ',';
insert += names[i] + "=excluded." + names[i];
}
}
Statement erase(db, remove.c_str()), upsert(db, insert.c_str());
if (old) for (const auto& [key, row] : old->items()) if (!next.contains(key)) {
for (unsigned i = 0; i < table.key_columns; ++i) erase.bind(static_cast<int>(i + 1), row[i]);
erase.run();
}
for (const auto& [key, row] : next.items()) {
if (old && old->contains(key) && old->at(key) == row) continue;
for (std::size_t i = 0; i < names.size(); ++i) upsert.bind(static_cast<int>(i + 1), row[i]);
upsert.run();
}
}
}
void configure_writer(sqlite3* db) {
Statement mode(db, "PRAGMA journal_mode=WAL");
check(mode.row() && mode.text(0) == "wal" && !mode.row(), "Cannot enable save database WAL");
execute(db, "PRAGMA foreign_keys=ON; PRAGMA synchronous=FULL; PRAGMA wal_autocheckpoint=256; PRAGMA journal_size_limit=1048576; PRAGMA max_page_count=16384;");
}
void check_size(const std::filesystem::path& path) { check(std::filesystem::file_size(path) <= max_bytes, "Save database exceeds size limit"); }
void clear_staging(const std::filesystem::path& path) {
// These are unpublished files owned by the locked writer. Remove journals
// too, so a retry cannot recover an abandoned transaction into a new file.
for (const auto* suffix : {L"", L"-journal", L"-wal", L"-shm"}) {
const auto file = path.wstring() + suffix;
if (std::filesystem::exists(file)) check(!!DeleteFileW(file.c_str()), "Cannot clear interrupted save staging");
}
}
}
std::string row_key(const Json& row, unsigned key_columns) {
check(row.is_array() && row.size() >= key_columns, "Invalid save key");
Json key = Json::array(); for (unsigned i = 0; i < key_columns; ++i) key.push_back(row[i]);
return key.dump();
}
std::filesystem::path database_path(std::filesystem::path path) {
if (path.extension() == L".json") path.replace_extension(L".sqlite3"); return path;
}
std::filesystem::path json_source_path(std::filesystem::path path) { return path.replace_extension(L".json"); }
struct SaveDatabase::Impl {
std::filesystem::path path;
SaveSchema schema;
std::size_t nodes{}, bytes{};
std::vector<HANDLE> locks;
std::unique_ptr<Connection> connection;
Json value;
int version{};
~Impl() { connection.reset(); for (const auto handle : locks) CloseHandle(handle); }
void lock(const std::filesystem::path& target) {
const auto handle = CreateFileW((target.wstring() + L".lock").c_str(), GENERIC_READ | GENERIC_WRITE,
0, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
check(handle != INVALID_HANDLE_VALUE, "Save is already in use or inaccessible; close the game before editing");
try { locks.push_back(handle); } catch (...) { CloseHandle(handle); throw; }
}
};
SaveDatabase::SaveDatabase(std::filesystem::path path, SaveSchema schema) : impl_(std::make_unique<Impl>()) {
auto& s = *impl_; s.path = database_path(std::move(path)); s.schema = schema;
if (!s.path.parent_path().empty()) std::filesystem::create_directories(s.path.parent_path());
s.lock(s.path); s.lock(json_source_path(s.path)); // Excludes older JSON writers too.
const auto legacy = s.path.parent_path() / L"profile.rsp";
if (schema.name == "profile" && std::filesystem::exists(legacy)) s.lock(legacy);
if (!std::filesystem::exists(s.path)) {
check(!std::filesystem::exists(s.path.wstring() + L".bak") && !std::filesystem::exists(s.path.wstring() + L"-wal") &&
!std::filesystem::exists(s.path.wstring() + L"-shm") && !std::filesystem::exists(s.path.wstring() + L"-journal"), "Save database missing while recovery files exist"); return;
}
check_size(s.path);
s.connection = std::make_unique<Connection>(s.path, SQLITE_OPEN_READWRITE);
Transaction read(s.connection->db, false); verify(s.connection->db, schema.name); s.value = load(s.connection->db, schema);
s.version = integer(s.connection->db, "PRAGMA data_version"); read.commit();
bounds(s.value, s.nodes, s.bytes);
}
SaveDatabase::~SaveDatabase() = default;
bool SaveDatabase::exists() const { return !!impl_->connection; }
const Json& SaveDatabase::document() const { check(exists(), "Save database is not initialized"); return impl_->value; }
void SaveDatabase::initialize(Json value) {
check(!exists(), "Save database is already initialized"); validate_tables(value, impl_->schema);
auto& s = *impl_;
const auto temporary = s.path.wstring() + L".migrating";
// Unpublished staging belongs to this writer. Retrying a crashed migration
// never replaces the active database or the retained original JSON.
clear_staging(temporary);
{
Connection initial(temporary, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
execute(initial.db, "PRAGMA foreign_keys=ON; PRAGMA journal_mode=DELETE; PRAGMA synchronous=EXTRA; PRAGMA max_page_count=16384;");
Transaction transaction(initial.db, true);
execute(initial.db, "PRAGMA application_id=1381190484; PRAGMA user_version=1;"
"CREATE TABLE metadata(key TEXT PRIMARY KEY NOT NULL,value TEXT NOT NULL) STRICT, WITHOUT ROWID;");
for (const auto& table : s.schema.tables) execute(initial.db, std::string(table.create_sql).c_str());
Statement type(initial.db, "INSERT INTO metadata VALUES('kind',?1)"); type.bind(1, s.schema.name); type.run();
apply_changes(initial.db, s.schema, nullptr, value); transaction.commit();
}
check(!!MoveFileExW(temporary.c_str(), s.path.c_str(), MOVEFILE_WRITE_THROUGH), "Cannot publish migrated save database");
s.connection = std::make_unique<Connection>(s.path, SQLITE_OPEN_READWRITE);
verify(s.connection->db, s.schema.name); configure_writer(s.connection->db);
s.version = integer(s.connection->db, "PRAGMA data_version"); s.value = std::move(value);
s.nodes = s.bytes = 0; bounds(s.value, s.nodes, s.bytes);
}
void SaveDatabase::commit(Json value) {
auto& s = *impl_; check(exists(), "Save database is not initialized"); validate_tables(value, s.schema);
if (s.value == value) return;
Transaction transaction(s.connection->db, true);
check(integer(s.connection->db, "PRAGMA data_version") == s.version, "Save database edited externally; restart to load changes");
apply_changes(s.connection->db, s.schema, &s.value, value); transaction.commit();
s.value = std::move(value); // Memory is published only after durable COMMIT.
s.nodes = s.bytes = 0; bounds(s.value, s.nodes, s.bytes);
}
void SaveDatabase::patch(Json patch) {
auto& s = *impl_;
check(exists() && patch.is_object(), "Invalid save patch");
Json before = Json::object(), after = Json::object();
for (const auto& table : s.schema.tables) {
before[table.name] = Json::object(); after[table.name] = Json::object();
}
auto nodes = s.nodes, bytes = s.bytes;
struct Publication {
Json::Object* rows;
Json::Object::iterator old;
Json::Object::node_type replacement;
};
std::vector<Publication> publications;
for (auto& [table_name, changes] : patch.items()) {
const auto table = std::find_if(s.schema.tables.begin(), s.schema.tables.end(),
[&](const auto& item) { return item.name == table_name; });
check(table != s.schema.tables.end() && changes.is_object(), "Unknown save patch table");
const auto width = columns(*table).size();
auto& rows = s.value.at(table_name).items();
for (auto it = changes.items().begin(); it != changes.items().end();) {
auto current = it++;
const auto& key = current->first;
const auto& row = current->second;
auto old = rows.find(key);
if ((old == rows.end() && row.is_null()) ||
(old != rows.end() && old->second == row)) continue;
if (old != rows.end()) {
std::size_t old_nodes{}, old_bytes{};
bounds(old->second, old_nodes, old_bytes, 2);
nodes -= old_nodes; bytes -= old_bytes + key.size();
before[table_name][key] = old->second;
}
if (!row.is_null()) {
check(row.is_array() && row.size() == width &&
row_key(row, table->key_columns) == key, "Invalid save patch row");
for (const auto& cell : row) check(!cell.is_object() && !cell.is_array(), "Invalid SQL cell");
std::size_t new_nodes{}, new_bytes{};
bounds(row, new_nodes, new_bytes, 2);
nodes += new_nodes; bytes += new_bytes + key.size();
after[table_name][key] = row;
}
publications.push_back({&rows, old, {}});
if (!row.is_null()) publications.back().replacement = changes.items().extract(current);
}
}
if (publications.empty()) return;
check(nodes <= max_nodes && bytes <= max_bytes, "Save patch exceeds database limits");
Transaction transaction(s.connection->db, true);
check(integer(s.connection->db, "PRAGMA data_version") == s.version,
"Save database edited externally; restart to load changes");
const auto changed_before = sqlite3_total_changes64(s.connection->db);
apply_changes(s.connection->db, s.schema, &before, after);
check(sqlite3_total_changes64(s.connection->db) - changed_before == static_cast<sqlite3_int64>(publications.size()),
"Save patch changed rows outside its declared records");
transaction.commit();
// Every replacement map node was allocated before COMMIT. Publishing the
// cache uses only erasure/node transfer, so allocation failures cannot leave
// a successfully saved transaction absent from this session's read model.
for (auto& item : publications) {
if (item.old != item.rows->end()) item.rows->erase(item.old);
if (!item.replacement.empty()) item.rows->insert(std::move(item.replacement));
}
s.nodes = nodes; s.bytes = bytes;
}
void SaveDatabase::backup() {
auto& s = *impl_; check(exists(), "Save database is not initialized");
const auto temporary = s.path.wstring() + L".bak.pending";
clear_staging(temporary);
{
Transaction read(s.connection->db, false);
check(integer(s.connection->db, "PRAGMA data_version") == s.version, "Save database changed during load");
Connection destination(temporary, SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE);
auto* backup = sqlite3_backup_init(destination.db, "main", s.connection->db, "main");
check(backup != nullptr, "Cannot initialize save backup");
const auto result = sqlite3_backup_step(backup, -1); const auto finish = sqlite3_backup_finish(backup);
check(result == SQLITE_DONE && finish == SQLITE_OK, "Cannot complete save backup");
execute(destination.db, "PRAGMA journal_mode=DELETE; PRAGMA synchronous=EXTRA;"); read.commit();
}
check(!!MoveFileExW(temporary.c_str(), (s.path.wstring() + L".bak").c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH),
"Cannot publish save backup");
configure_writer(s.connection->db);
check(integer(s.connection->db, "PRAGMA data_version") == s.version, "Save database changed during backup");
}
}
+44
View File
@@ -0,0 +1,44 @@
#pragma once
#include "Engine/Core/Json/json.h"
#include <filesystem>
#include <memory>
#include <span>
#include <string_view>
namespace dingosdk::storage {
std::filesystem::path database_path(std::filesystem::path);
std::filesystem::path json_source_path(std::filesystem::path);
// Each row is an array matching the named SQL columns. The first key_columns
// fields form its primary key; table names and SQL come from static schemas.
struct SaveTable {
std::string_view name, columns;
unsigned key_columns;
std::string_view create_sql;
};
struct SaveSchema {
std::string_view name;
std::span<const SaveTable> tables;
};
// A document maps table names to rows keyed by row_key(). Only changed rows
// enter each transaction; reads use the cached document.
std::string row_key(const Json& row, unsigned key_columns);
class SaveDatabase {
public:
SaveDatabase(std::filesystem::path, SaveSchema schema);
~SaveDatabase();
SaveDatabase(const SaveDatabase&) = delete;
SaveDatabase& operator=(const SaveDatabase&) = delete;
bool exists() const;
const Json& document() const;
void initialize(Json);
void commit(Json);
// Sparse table rows; null deletes a row. Atomic cache/disk publication.
void patch(Json);
// After domain validation, before the first write of an existing session.
void backup();
private:
struct Impl;
std::unique_ptr<Impl> impl_;
};
}
@@ -0,0 +1,70 @@
#include "Engine/Core/Text/word_filter.h"
#include <cstdio>
#include <string>
#include <string_view>
namespace {
int failures = 0;
void expect(bool condition, std::string_view what) {
if (condition) return;
++failures;
std::printf("FAIL: %.*s\n", static_cast<int>(what.size()), what.data());
}
void bad(std::string_view text) { expect(dingosdk::text::contains_bad_words(text), std::string("bad: ") + std::string(text)); }
void clean(std::string_view text) { expect(!dingosdk::text::contains_bad_words(text), std::string("clean: ") + std::string(text)); }
void masks(std::string_view text, std::string_view expected) {
const auto masked = dingosdk::text::mask_bad_words(text);
expect(masked == expected, std::string("mask: \"") + std::string(text) + "\" -> \"" + masked + "\", wanted \"" +
std::string(expected) + "\"");
}
} // namespace
int main() {
// Listed words, any case, whole or inside longer words.
bad("fuck");
bad("FUCK this server");
bad("Fuckface Skaters");
bad("motherfuckers only");
bad("Shithead's park");
bad("big ass ramps");
bad("Bitch Please");
// Look-alikes and spelled-out letters.
bad("sh1t");
bad("@$$ hats");
bad("f u c k");
bad("F.U.C.K. yeah");
bad("b17ch");
// Ordinary words that contain a listed word, and server names people use.
clean("Hello World");
clean("Shell Shock Skatepark");
clean("Classic Skate Session");
clean("Pass the Grass");
clean("Scunthorpe Skaters");
clean("Cocktail Hour");
clean("Peacock Plaza");
clean("Dickies Team Session");
clean("Arsenal FC fans");
clean("Sparse Parsec");
clean("Saturday Night Session");
clean("Reputation Skate");
clean("San Vansterdam 24/7");
clean("Titanic Ledges");
clean("Cumulus Bowl");
clean("Essex Street League");
clean("Pakistan Plaza");
clean("Scrapyard DIY");
clean("ReSkate server");
clean("");
// Masking keeps everything else and the length.
masks("what the fuck dude", "what the **** dude");
masks("shit happens", "**** happens");
masks("sh1t", "****");
masks("f u c k", "* * * *");
masks("Hello friends", "Hello friends");
masks("fuckface", "****face");
if (failures == 0) std::printf("word filter: all checks passed\n");
return failures == 0 ? 0 : 1;
}
+418
View File
@@ -0,0 +1,418 @@
@$$
AssMonkey
Assface
Biatch
BlowJob
CarpetMuncher
Clit
Cock
CockSucker
Ekrem
Ekto
Felcher
Flikker
Fotze
Fu
FudgePacker
Fukah
Fuken
Fukin
Fukk
Fukkah
Fukken
Fukker
Fukkin
Goddamned
Huevon
Kurac
Lesbian
Lezzian
Lipshits
Lipshitz
MothaFucker
MothaFuker
MothaFukkah
MothaFukker
MotherFucker
MotherFukah
MotherFuker
MotherFukkah
MotherFukker
MuthaFucker
MuthaFukah
MuthaFuker
MuthaFukkah
MuthaFukker
Phuc
Phuck
Phuk
Phuker
Phukker
Poonani
Shitty
Shity
Sht
Shyt
Shyte
Shytty
Skanky
Slutty
ahole
amcik
andskota
anus
arschloch
arse
ash0le
ash0les
asholes
ass
assh0le
assh0lez
asshole
assholes
assholz
assrammer
asswipe
ayir
azzhole
b00bs
b17ch
b1tch
bassterds
bastard
bastards
bastardz
basterds
basterdz
bch
bi7ch
bich
bitch
bitches
blowjob
boffing
boiolas
bollock
boobs
breasts
btch
buceta
bullshit
butthole
buttpirate
buttwipe
c0ck
c0cks
c0k
cabron
cawk
cawks
cazzo
chink
chraa
chuj
cipa
clit
clits
cnts
cntz
cock
cockhead
cocks
cocksucker
crap
cum
cunt
cunts
cuntz
d4mn
damn
daygo
dego
dick
dike
dild0
dild0s
dildo
dildos
dilld0
dilld0s
dirsa
dominatricks
dominatrics
dominatrix
dupa
dyke
dziwka
ejackulate
ejakulate
enculer
enema
faen
fag
fag1t
faget
fagg1t
faggit
faggot
fagit
fags
fagz
faig
faigs
fanculo
fanny
fart
fatass
fcuk
feces
feg
ficken
fitt
flipping
foreskin
fuchah
fuck
fucka
fucker
fuckin
fucking
fucks
fuk
fukah
fuker
fukka
fukkah
fukker
futkretzn
fux0r
g00k
gay
gaybor
gayboy
gaygirl
gays
gayz
gook
guiena
h00r
h0ar
h0r
h0re
h4x0r
hell
hells
helvete
hoar
hoer
honkey
hoor
hoore
hore
hui
injun
jackoff
jap
japs
jerkoff
jisim
jism
jiss
jizm
jizz
kanker
kawk
kike
klootzak
knob
knobs
knobz
knulle
kraut
kuk
kuksuger
kunt
kunts
kuntz
kurwa
kusi
kyrpa
l3i+ch
l3itch
lesbian
lesbo
mamhoon
masochist
masokist
massterbait
masstrbait
masstrbate
masterbaiter
masterbat
masterbat3
masterbate
masterbates
masturbat
masturbate
merd
mibun
mofo
monkleigh
motha
motherfucker
mouliewop
muie
mulkku
muschi
mutha
n1gr
nastt
nasty
nazi
nazis
nepesaurio
nigga
niggas
nigger
nigur
niiger
niigr
nutsack
orafis
orgasim
orgasm
orgasum
oriface
orifice
orifiss
orospu
p0rn
packi
packie
packy
paki
pakie
paky
paska
pecker
peeenus
peeenusss
peenus
peinus
pen1s
penas
penis
penisbreath
penus
penuus
perse
phuck
picka
pierdol
pillu
pimmel
pimpis
piss
pizda
polac
polack
polak
poontsee
poop
porn
pr0n
pr1c
pr1ck
pr1k
preteen
pula
pule
pusse
pussee
pussy
puta
puto
puuke
puuker
qahbeh
queef
queer
queers
queerz
qweers
qweerz
qweir
rautenberg
recktum
rectum
retard
s.o.b.
sadist
scank
schaffer
scheiss
schlampe
schlong
schmuck
screw
screwing
scrotum
semen
sex
sexx
sexxx
sexy
sh1t
sh1ter
sh1ts
sh1tter
sh1tz
sharmuta
sharmute
shemale
shi+
shipal
shit
shits
shitt
shitter
shitz
shiz
skanck
skank
skankee
skankey
skanks
skrib
slut
sluts
slutz
smut
sonofabitch
sx
teets
teez
testical
testicle
tit
tits
titt
turd
va1jina
vag1na
vagiina
vagina
vaj1na
vajina
vullva
vulva
w00se
w0p
wank
wh00r
wh0re
whoar
whore
xrated
xxx
+172
View File
@@ -0,0 +1,172 @@
#include "word_filter.h"
#include "embedded_bad_words.h"
#include <algorithm>
#include <array>
#include <cstddef>
#include <functional>
#include <unordered_set>
#include <vector>
namespace dingosdk::text {
namespace {
// Letters fold to lower case and look-alikes to the letter they stand for; anything else
// separates words.
char fold(char c) {
if (c >= 'a' && c <= 'z') return c;
if (c >= 'A' && c <= 'Z') return static_cast<char>(c - 'A' + 'a');
switch (c) {
case '0': return 'o';
case '1': return 'i';
case '3': return 'e';
case '4': return 'a';
case '5': return 's';
case '7': return 't';
case '8': return 'b';
case '@': return 'a';
case '$': return 's';
case '+': return 't';
default: return 0;
}
}
// Longer list words that are common inside ordinary ones ("hell" in hello, "arse" in parse,
// "puta" in reputation, "turd" in saturday): these only count as a whole word.
constexpr std::string_view whole_word_only[]{
"anus", "arse", "ayir", "bich", "breasts", "cawk", "cawks", "chuj", "cipa", "crap",
"dego", "dike", "dupa", "ekto", "faen", "faig", "faigs", "fanny", "fart", "fitt",
"flipping", "gays", "gayz", "gook", "hell", "hells", "hoar", "hoer", "hoor", "hore",
"injun", "jiss", "kawk", "knob", "knobs", "knobz", "kraut", "kunt", "kunts", "kuntz",
"kusi", "merd", "muie", "nasty", "nastt", "packi", "packie", "packy", "paki", "pakie",
"paky", "paska", "perse", "picka", "pillu", "polac", "polak", "poop", "pric", "prik",
"pron", "pula", "pule", "pusse", "puta", "puto", "rautenberg", "schaffer", "screw", "screwing",
"semen", "shiz", "smut", "teets", "teez", "tits", "titt", "turd", "woose"};
// Ordinary words with a bad word inside; a match that one of these covers does not count.
constexpr std::string_view allowed_words[]{
"scunthorpe", "dickens", "dickinson", "dickson", "dickies", "cocktail", "cockpit", "peacock",
"cockroach", "cockatoo", "cockatiel", "cockney", "cockle", "cockerel", "cockburn", "cocky",
"hancock", "hitchcock", "woodcock", "babcock", "shuttlecock", "ashkenazi", "shiitake", "shitake",
"matsushita", "yamashita", "kinoshita", "morishita", "takeshita", "swank", "swanky", "woodpecker",
"clitheroe", "snigger", "pussycat", "pissarro", "retardant"};
struct Hash {
using is_transparent = void;
std::size_t operator()(std::string_view text) const { return std::hash<std::string_view>{}(text); }
};
using WordSet = std::unordered_set<std::string, Hash, std::equal_to<>>;
struct Lists {
WordSet whole; // every listed word
WordSet inside; // the ones that also match inside longer words
std::size_t shortest_inside{~std::size_t{}}, longest_inside{};
};
const Lists& lists() {
static const Lists built = [] {
Lists l;
std::string_view text = embedded::bad_words_list;
while (!text.empty()) {
const auto end = text.find('\n');
const auto line = text.substr(0, end);
text = end == std::string_view::npos ? std::string_view{} : text.substr(end + 1);
std::string word;
for (const char c : line)
if (const char letter = fold(c)) word += letter;
if (word.empty()) continue;
l.whole.insert(word);
if (word.size() > 3 && std::ranges::find(whole_word_only, word) == std::ranges::end(whole_word_only)) {
l.shortest_inside = std::min(l.shortest_inside, word.size());
l.longest_inside = std::max(l.longest_inside, word.size());
l.inside.insert(std::move(word));
}
}
return l;
}();
return built;
}
// A word as folded letters, with where each letter sits in the text.
struct Word {
std::string letters;
std::vector<std::size_t> at;
};
std::vector<Word> words(std::string_view text) {
std::vector<Word> out;
Word current;
for (std::size_t i = 0; i < text.size(); ++i) {
if (const char letter = fold(text[i])) {
current.letters += letter;
current.at.push_back(i);
} else if (!current.letters.empty()) {
out.push_back(std::move(current));
current = {};
}
}
if (!current.letters.empty()) out.push_back(std::move(current));
// Three or more letters spelled out one at a time ("f u c k", "s.o.b.") also read as a word.
std::vector<Word> spelled;
for (std::size_t i = 0; i < out.size();) {
auto end = i;
while (end < out.size() && out[end].letters.size() == 1) ++end;
if (end - i >= 3) {
Word joined;
for (auto k = i; k < end; ++k) {
joined.letters += out[k].letters;
joined.at.push_back(out[k].at.front());
}
spelled.push_back(std::move(joined));
}
i = end == i ? i + 1 : end;
}
out.insert(out.end(), std::make_move_iterator(spelled.begin()), std::make_move_iterator(spelled.end()));
return out;
}
bool covered(std::string_view letters, std::size_t start, std::size_t length) {
for (const auto allowed : allowed_words)
for (auto at = letters.find(allowed); at != std::string_view::npos; at = letters.find(allowed, at + 1))
if (at <= start && start + length <= at + allowed.size()) return true;
return false;
}
// Whether `word` has a bad word in it; with `marked`, flags each letter that belongs to one.
bool match(const Word& word, std::vector<bool>* marked) {
const auto& l = lists();
const std::string_view letters = word.letters;
if (l.whole.contains(letters)) {
if (marked) marked->assign(letters.size(), true);
return true;
}
bool found{};
for (std::size_t start = 0; start + l.shortest_inside <= letters.size(); ++start)
for (auto length = l.shortest_inside; length <= l.longest_inside && start + length <= letters.size(); ++length) {
if (!l.inside.contains(letters.substr(start, length)) || covered(letters, start, length)) continue;
if (!marked) return true;
found = true;
std::fill(marked->begin() + static_cast<std::ptrdiff_t>(start),
marked->begin() + static_cast<std::ptrdiff_t>(start + length), true);
}
return found;
}
} // namespace
bool contains_bad_words(std::string_view text) {
for (const auto& word : words(text))
if (match(word, nullptr)) return true;
return false;
}
std::string mask_bad_words(std::string_view text) {
std::string out(text);
for (const auto& word : words(text)) {
std::vector<bool> marked(word.letters.size());
if (!match(word, &marked)) continue;
for (std::size_t i = 0; i < marked.size(); ++i)
if (marked[i]) out[word.at[i]] = '*';
}
return out;
}
} // namespace dingosdk::text
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <string>
#include <string_view>
// The bad-word filter: the list in bad_words.txt (built in), matched against text after
// folding case and look-alike characters, so "Sh1t", "@$$" and "f u c k" count too.
//
// - Words of three letters or fewer, and a few longer ones that are common inside ordinary
// words ("hell" in hello, "arse" in parse), only match a whole word.
// - Every other word also matches inside a longer one ("fuckface"), unless an ordinary word
// covers it there ("Scunthorpe", "cocktail").
//
// Used for dedicated server names (the server refuses to list one, the client hides it) and,
// as a player option, to mask chat.
namespace dingosdk::text {
bool contains_bad_words(std::string_view text);
// `text` with each letter of every bad word replaced by '*'; same length, other text untouched.
std::string mask_bad_words(std::string_view text);
} // namespace dingosdk::text
@@ -0,0 +1,60 @@
#include "Engine/Game/Abi/native_callback_queue.h"
#include <array>
#include <cstdio>
namespace {
struct Request { std::uintptr_t callback; };
std::array<unsigned, 16> destroyed{};
void destroy(void* wrapper) {
const auto value = *static_cast<std::uintptr_t*>(wrapper);
if (value < destroyed.size()) ++destroyed[value];
}
void check(bool result, const char* message) { if (!result) throw std::runtime_error(message); }
void regression() {
using dingosdk::game::NativeCallbackQueue;
dingosdk::game::native_data().values.destroy_delegate = destroy;
NativeCallbackQueue<Request> queue;
unsigned invoked{};
bool assets_alive = true;
const auto invoke = [&](const Request&, std::uintptr_t) {
check(assets_alive, "A callback reached an unloaded script descriptor");
++invoked;
};
// A Build Kit request can wait for inventory across many client ticks.
queue.push({1}); queue.push({2});
queue.before_transition(14);
assets_alive = false;
queue.deliver(invoke);
queue.before_transition(3);
check(invoked == 0 && destroyed[1] == 1 && destroyed[2] == 1 && !queue.available(),
"Native unload must cancel retained callbacks once, even with inventory still unavailable");
// The splash screen subscribes during construction, before state 13.
queue.before_transition(4); assets_alive = true;
check(queue.available(), "Fresh loading-world subscriptions must be accepted");
queue.push({3}); queue.deliver(invoke);
check(invoked == 1 && destroyed[3] == 1, "Fresh subscriptions must still complete normally");
// A callback may rebuild/unload the world, invalidating the rest of its batch.
queue.push({4}); queue.push({5});
queue.deliver([&](const Request&, std::uintptr_t callback) {
check(callback == 4, "Reentrant unload left a stale callback in the active batch");
++invoked;
queue.push({6});
queue.before_transition(22);
assets_alive = false;
});
check(invoked == 2 && destroyed[4] == 1 && destroyed[5] == 1 && destroyed[6] == 1,
"Cancel pending and in-flight batches without invoking or leaking old callbacks");
queue.before_transition(16); assets_alive = true;
queue.push({7}); queue.push({8});
try { queue.deliver([](const Request&, std::uintptr_t) { throw std::runtime_error("delivery failed"); }); }
catch (const std::runtime_error&) {}
check(destroyed[7] == 1 && destroyed[8] == 1, "Failed delivery must release the entire retained batch");
queue.push({9}); queue.before_transition(24); queue.before_transition(4); queue.before_transition(13);
queue.deliver(invoke);
check(!queue.available() && destroyed[9] == 1 && invoked == 2, "Shutdown cannot resume callback delivery");
}
}
int main() {
try { regression(); std::puts("Native callback lifetime regressions passed."); return 0; }
catch (const std::exception& error) { std::fprintf(stderr, "%s\n", error.what()); return 1; }
}
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include "native_data.h"
#include <utility>
#include <vector>
namespace dingosdk::game {
// Copying a native delegate owns its optional binding, but not the script's
// asset-backed function descriptor. Cancel deferred work before that world
// unloads, including work already moved into a reentrant delivery batch.
template<class Request> class NativeCallbackQueue {
std::vector<Request> pending_;
std::uint64_t generation_{};
bool blocked_{}, shutdown_{};
public:
bool available() const { return !blocked_ && pending_.size() < 32; }
bool empty() const { return pending_.empty(); }
void push(Request request) { pending_.push_back(std::move(request)); }
template<class Deliver> void deliver(Deliver&& invoke) {
if (blocked_) return;
const auto generation = generation_;
struct Batch {
std::vector<Request> requests;
~Batch() {
for (auto& request : requests) {
NativeDelegateGuard owned;
owned.value = request.callback;
}
}
} pending{std::exchange(pending_, {})};
for (auto& request : pending.requests) {
NativeDelegateGuard owned;
owned.value = std::exchange(request.callback, 0);
if (!blocked_ && generation == generation_) invoke(request, owned.value);
}
}
void before_transition(unsigned next) {
if (next == 3 || next == 14 || next == 22 || next == 24 || next == 25) {
blocked_ = true;
shutdown_ |= next == 24 || next == 25;
++generation_;
auto pending = std::exchange(pending_, {});
for (auto& request : pending) {
NativeDelegateGuard owned;
owned.value = request.callback;
}
} else if (!shutdown_ && (next == 4 || next == 8 || next == 13 || next == 16 || next == 21)) {
// Constructors in the next load can subscribe before gameplay starts.
blocked_ = false;
}
}
};
}
+38
View File
@@ -0,0 +1,38 @@
#include "native_data.h"
#include "Engine/Game/Build/addresses.h"
#include "Engine/Game/Build/20260929/profile.h"
namespace dingosdk::game {
bool initialize_native_data(std::uintptr_t base) {
using namespace addr::profile;
if (!base) return false;
for (const auto& contract : {model_manager_contract, model_create_contract, model_destroy_contract, model_value_contract,
model_find_contract, card_model_field_contract, neighborhood_publish_contract, model_lock_contract, model_unlock_contract,
news_assign_contract, news_delegate_copy_contract, news_delegate_destroy_contract,
news_invoke_contract, news_reference_contract, asset_find_contract}) {
std::array<unsigned char, 32> bytes{};
if (base > UINTPTR_MAX - contract.rva ||
!memory::read(base + contract.rva, bytes) || bytes != contract.bytes) return false;
}
NativeData next;
next.find_asset = reinterpret_cast<decltype(next.find_asset)>(base + asset_find_contract.rva);
auto& m = next.models;
auto& v = next.values;
m.get_model = reinterpret_cast<decltype(m.get_model)>(base + model_manager_contract.rva);
m.create = reinterpret_cast<decltype(m.create)>(base + model_create_contract.rva);
m.destroy = reinterpret_cast<decltype(m.destroy)>(base + model_destroy_contract.rva);
m.value = reinterpret_cast<decltype(m.value)>(base + model_value_contract.rva);
m.find = reinterpret_cast<decltype(m.find)>(base + model_find_contract.rva);
m.field = reinterpret_cast<decltype(m.field)>(base + card_model_field_contract.rva);
m.publish = reinterpret_cast<decltype(m.publish)>(base + neighborhood_publish_contract.rva);
m.lock = reinterpret_cast<decltype(m.lock)>(base + model_lock_contract.rva);
m.unlock = reinterpret_cast<decltype(m.unlock)>(base + model_unlock_contract.rva);
v.assign = reinterpret_cast<decltype(v.assign)>(base + news_assign_contract.rva);
v.copy_delegate = reinterpret_cast<decltype(v.copy_delegate)>(base + news_delegate_copy_contract.rva);
v.destroy_delegate = reinterpret_cast<decltype(v.destroy_delegate)>(base + news_delegate_destroy_contract.rva);
v.invoke = reinterpret_cast<decltype(v.invoke)>(base + news_invoke_contract.rva);
v.null_reference = reinterpret_cast<decltype(v.null_reference)>(base + news_reference_contract.rva);
native_data() = next;
return true;
}
}
+95
View File
@@ -0,0 +1,95 @@
#pragma once
#include "Engine/Core/Platform/memory.h"
#include <array>
#include <cstdint>
#include <limits>
#include <stdexcept>
#include <string_view>
#include <type_traits>
namespace dingosdk::game {
// All pointers refer to the validated build. Model values are borrowed; callers
// hold ModelWriteLock across reads and synchronous publication.
struct NativeModelFunctions {
using Lock = void (*)(std::uintptr_t);
std::uintptr_t (*get_model)(std::uint32_t){};
std::uint64_t (*create)(std::uintptr_t, std::uintptr_t, std::uint64_t, std::uint32_t, bool, std::uint8_t){};
std::uintptr_t (*value)(std::uintptr_t, std::uint64_t, std::uint64_t, std::uint64_t){};
std::uint64_t (*find)(std::uintptr_t, std::uintptr_t, std::uint64_t, std::uint32_t, std::uint8_t){};
std::uint64_t (*field)(std::uintptr_t, std::uint64_t, unsigned, std::uint32_t, bool){};
bool (*publish)(std::uintptr_t, std::uint64_t, std::uintptr_t, const void*){};
Lock lock{}, unlock{};
void* (*destroy)(std::uintptr_t, void*, std::uint64_t, std::uint8_t, bool, bool){};
};
struct NativeValueFunctions {
void* (*assign)(void*, const char*, std::uint32_t){};
void (*copy_delegate)(void*, const void*){};
void (*destroy_delegate)(void*){};
void (*invoke)(const void*, const void*){};
void* (*null_reference)(void*, std::uintptr_t){};
};
struct NativeData {
NativeModelFunctions models;
NativeValueFunctions values;
// Loaded named assets only; native lookup locks the asset registries.
std::uintptr_t (*find_asset)(std::uint16_t, const char*){};
};
inline NativeData& native_data() { static NativeData value; return value; }
// Runs before publishing active providers or enabling their hooks.
bool initialize_native_data(std::uintptr_t base);
inline std::uint32_t native_name_hash(std::string_view name) noexcept {
std::uint32_t hash = 5381;
for (const unsigned char c : name) hash = hash * 33 ^ c;
return hash;
}
class ModelWriteLock {
NativeModelFunctions::Lock unlock_;
std::uintptr_t address_;
public:
explicit ModelWriteLock(std::uintptr_t manager, const NativeModelFunctions& functions = native_data().models)
: unlock_(functions.unlock), address_(manager + 0xf20) {
if (!manager || manager > UINTPTR_MAX - 0xf20 || !functions.lock || !unlock_)
throw std::runtime_error("Native model lock is unavailable");
functions.lock(address_);
}
~ModelWriteLock() { unlock_(address_); }
ModelWriteLock(const ModelWriteLock&) = delete;
ModelWriteLock& operator=(const ModelWriteLock&) = delete;
};
// Holds one owned native callback. Raw value transfers are explicit at the
// asynchronous queue boundary; this guard must never be copied.
class NativeDelegateGuard {
void (*destroy_)(void*) = native_data().values.destroy_delegate;
public:
std::uintptr_t value{};
NativeDelegateGuard() = default;
~NativeDelegateGuard() { if (value && destroy_) destroy_(&value); }
NativeDelegateGuard(const NativeDelegateGuard&) = delete;
NativeDelegateGuard& operator=(const NativeDelegateGuard&) = delete;
std::uintptr_t release() noexcept { const auto result = value; value = 0; return result; }
};
// Frostbite's counted-array ABI, distinct from EASTL vectors and strings.
// This storage is borrowed only for calls that synchronously copy its values.
template<class T, std::size_t N> struct NativeArrayStorage {
static_assert(N <= UINT32_MAX && alignof(T) <= 8);
std::uint32_t capacity{static_cast<std::uint32_t>(N)}, count{};
std::array<T, N> values{};
};
template<class T> bool native_array(const void* wrapper, std::uintptr_t& data,
std::uint32_t& count, std::uint32_t maximum) {
static_assert(std::is_trivially_copyable_v<T>);
std::uintptr_t pointer{}; std::uint32_t size{};
data = 0; count = 0;
if (!memory::read(reinterpret_cast<std::uintptr_t>(wrapper), pointer) || pointer < 4 ||
!memory::read(pointer - 4, size)) return false;
size &= 0x7fffffff;
if (size > maximum || pointer > UINTPTR_MAX - static_cast<std::uintptr_t>(size) * sizeof(T))
return false;
data = pointer; count = size;
return true;
}
} // namespace dingosdk::game
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <string>
namespace dingosdk::game {
// Native eastl string: 16 bytes of inline data/heap metadata and allocator.
// These are borrowed only during Cache(), which copies both strings itself.
struct NativeStringView {
std::array<std::byte, 16> storage{};
std::uintptr_t allocator{};
explicit NativeStringView(const std::string& value) {
if (value.size() < 16) {
std::memcpy(storage.data(), value.data(), value.size());
storage[15] = static_cast<std::byte>(15 - value.size());
} else {
const auto pointer = value.c_str();
const auto size = static_cast<std::uint32_t>(value.size());
const auto capacity = size | 0x80000000;
std::memcpy(storage.data(), &pointer, 8);
std::memcpy(storage.data() + 8, &size, 4);
std::memcpy(storage.data() + 12, &capacity, 4);
}
}
};
static_assert(sizeof(NativeStringView) == 0x18);
static_assert(offsetof(NativeStringView, allocator) == 0x10);
}
+36
View File
@@ -0,0 +1,36 @@
#pragma once
#include "Engine/Core/Platform/memory.h"
#include <array>
#include <cstring>
#include <string>
namespace dingosdk::game {
// Copy a bounded ASCII identifier from the supported build's EASTL string.
// Empty names are valid (the park notification uses them to clear a lot).
// Call only while the native callback owns the borrowed string's lifetime.
inline bool read_native_identifier(std::uintptr_t address, std::string& result) {
result.clear();
std::array<unsigned char, 24> data{}, checked{};
if (!memory::read(address, data)) return false;
std::array<char, 256> text{};
std::uint32_t length{};
if (data[15] & 0x80) {
std::uintptr_t pointer{};
std::uint32_t capacity{};
std::memcpy(&pointer, data.data(), sizeof(pointer));
std::memcpy(&length, data.data() + 8, sizeof(length));
std::memcpy(&capacity, data.data() + 12, sizeof(capacity));
if (length >= text.size() || length > (capacity & 0x7fffffff) ||
!memory::read_bytes(pointer, text.data(), static_cast<std::size_t>(length) + 1)) return false;
} else {
if (data[15] > 15) return false;
length = 15 - data[15];
std::memcpy(text.data(), data.data(), length);
}
if (text[length] || !memory::read(address, checked) || checked != data) return false;
for (std::size_t i = 0; i < length; ++i)
if (static_cast<unsigned char>(text[i]) < 32 || static_cast<unsigned char>(text[i]) > 126) return false;
result.assign(text.data(), length);
return true;
}
}
+60
View File
@@ -0,0 +1,60 @@
#pragma once
#include <array>
#include <cstdint>
// AI skaters spawned from the player blueprint.
namespace dingosdk::game::build::v20260929::ai_skaters {
// Entity bus: create_bus(owner, parent, 0, 0, 0, 0) creates a child bus and acquire_bus adds
// a reference (released with skater_entities::release_reference).
inline constexpr std::uintptr_t create_bus = 0x2b7eb40;
inline constexpr std::array<unsigned char, 19> create_bus_prefix{
0x40,0x53,0x55,0x56,0x57,0x48,0x83,0xec,0x48,0x48,0x8b,0xe9,0x41,0x8b,0xd9,0x48,
0x8b,0x49,0x48};
inline constexpr std::uintptr_t acquire_bus = 0x2b7e530;
inline constexpr std::array<unsigned char, 13> acquire_bus_prefix{
0xb8,0x01,0x00,0x00,0x00,0xf0,0x0f,0xc1,0x41,0x40,0xff,0xc0,0xc3};
// Writes the source data+0xe1 flag into the entity's +0x578 property binding.
inline constexpr std::uintptr_t set_property_binding = 0x544020;
inline constexpr std::array<unsigned char, 15> set_property_binding_prefix{
0x88,0x54,0x24,0x10,0x48,0x83,0xec,0x28,0x48,0x81,0xc1,0x78,0x05,0x00,0x00};
// Skater entity vtable slot +0x458. Reads the +0x578 binding unchecked once the skater's physics exists.
inline constexpr std::uintptr_t binding_reader = 0x53eba0;
// The +0x578 load inside binding_reader.
inline constexpr std::uintptr_t binding_read_site = 0x53ebbf;
inline constexpr std::array<unsigned char, 7> binding_read_site_prefix{
0x48,0x8d,0x8b,0x78,0x05,0x00,0x00};
// Skater component physics setter fed from source data+0xe0 after placement.
inline constexpr std::uintptr_t physics_source_setter = 0xf9cf40;
inline constexpr std::array<unsigned char, 17> physics_source_setter_prefix{
0x48,0x89,0x5c,0x24,0x10,0x57,0x48,0x83,0xec,0x20,0x48,0x8b,0x81,0xa0,0x00,0x00,
0x00};
// Expected code of skater_entities::physics_cb_setter.
inline constexpr std::array<unsigned char, 7> physics_cb_setter_prefix{
0x88,0x91,0xcb,0x00,0x00,0x00,0xc3};
// Native named-asset lookup that searches a domain and its parents. Reference only.
inline constexpr std::uintptr_t asset_domain_lookup = 0x41b5780;
// EATBrainTree component (collection slot 12). brain_tree_factory creates the bt_default
// instance (+0x60) through holder +0x40 (brain_tree_holder_vtable); brain_tree_update (vtable
// +0x138) ticks the tree only while run flag +0x70 is set.
inline constexpr std::uintptr_t brain_tree_vtable = 0x6252b20;
inline constexpr std::uintptr_t brain_tree_update = 0x16f5560;
inline constexpr std::uintptr_t brain_tree_holder_vtable = 0x6252d60;
// Reference only.
inline constexpr std::uintptr_t brain_tree_factory = 0x16f23a0;
// SkaterAIComponent (collection slot 14, created by skater_ai_factory, 0x50 bytes). Its update
// (vtable +0x138) creates the 0x2c90-byte AI skater controller at +0x40 (ai_controller_create,
// bound to the skater's physics body) and runs it (ai_controller_run) only while +0x48 is set.
inline constexpr std::uintptr_t skater_ai_vtable = 0x6170180;
inline constexpr std::uintptr_t skater_ai_update = 0xf97630;
inline constexpr std::array<unsigned char, 18> skater_ai_update_prefix{
0x40,0x53,0x57,0x48,0x83,0xec,0x38,0x80,0x79,0x48,0x00,0x48,0x8b,0xf9,0x48,0x8b,
0x59,0x40};
// Reference only.
inline constexpr std::uintptr_t skater_ai_factory = 0xf881d0;
// Reference only.
inline constexpr std::uintptr_t ai_controller_create = 0x10373a0;
// Reference only.
inline constexpr std::uintptr_t ai_controller_run = 0x10461e0;
}
+216
View File
@@ -0,0 +1,216 @@
#pragma once
#include "Engine/Game/Build/fingerprint.h"
#include <array>
#include <cstdint>
namespace dingosdk::game::build::v20260929::atmosphere {
struct AtmosphereBinding {
unsigned kind; std::uint32_t hash; unsigned cache, size, mask_offset;
std::uint32_t mask; unsigned group, asset_offset; bool texture;
};
// Reference assignment, void(slot*, value): retains value, releases the old one.
inline constexpr std::uintptr_t reference_assign = 0x3dd1d70;
inline constexpr std::array<Fingerprint,4> atmosphere_sites{{
{0x5fcc6f0,{0x41,0x81,0xf8,0xa5,0xef,0xc0,0x0d,0x0f,0x8f,0xb3,0x0a,0x00,0x00,0x0f,0x84,0x76,0x0a,0x00,0x00,0x41,0x81,0xf8,0xaf,0xd5,0x21,0xd4,0x0f,0x8f,0x9c,0x05,0x00,0x00}},
{0x5fd29b0,{0x48,0x83,0xec,0x28,0x41,0x81,0xf8,0xb8,0x9b,0xb1,0xfa,0x0f,0x8f,0xdd,0x0e,0x00,0x00,0x0f,0x84,0x99,0x0e,0x00,0x00,0x41,0x81,0xf8,0x6f,0xe1,0x67,0xb5,0x0f,0x8f}},
{0x5fd74d0,{0x41,0x81,0xf8,0xd4,0x1a,0xa0,0xe0,0x0f,0x8f,0x41,0x01,0x00,0x00,0x0f,0x84,0x0f,0x01,0x00,0x00,0x41,0x81,0xf8,0xfe,0xcd,0x89,0x8b,0x0f,0x84,0xd3,0x00,0x00,0x00}},
{reference_assign,{0x4c,0x8b,0x01,0x4c,0x8b,0xc9,0x49,0x8b,0xc8,0x48,0x8b,0xc2,0x48,0x83,0xe1,0xfb,0x48,0x83,0xe0,0xfb,0x48,0x3b,0xc8,0x74,0x31,0xb8,0x00,0x20,0x00,0x00,0x48,0x85}},
}};
// Verified native property dispatch + PropertyOverrides hash dispatch.
// Live component offsets, not serialized EBX reflection offsets.
inline constexpr std::array<AtmosphereBinding, 194> atmosphere_bindings{{
{0,0x8ba423e4,0x26b,1,0x198,0x1,0,0x29e,false}, // Fog.Enable
{0,0x79cf6d43,0x1f4,4,0x198,0x2,0,0x228,false}, // Fog.FogDistanceMultiplier
{0,0x27e5432c,0x267,1,0x198,0x4,0,0x29b,false}, // Fog.FogGradientEnable
{0,0xdc0efa5,0x1bc,4,0x198,0x8,0,0x1d0,false}, // Fog.Start
{0,0xb87a32a,0x1d0,4,0x198,0x10,0,0x234,false}, // Fog.End
{0,0xcb01792,0x130,16,0x198,0x20,0,0x170,false}, // Fog.Curve
{0,0xfc54d655,0x266,1,0x198,0x40,0,0x29d,false}, // Fog.FogGradientHeightFadeEnable
{0,0xbd8ad483,0x1d8,4,0x198,0x80,0,0x1e0,false}, // Fog.FadeStart
{0,0x54cd410c,0x190,4,0x198,0x100,0,0x1cc,false}, // Fog.FadeEnd
{0,0x59dd7757,0x264,1,0x198,0x200,0,0x2a5,false}, // Fog.FogColorEnable
{0,0x15926076,0x110,16,0x198,0x400,0,0x180,false}, // Fog.FogColor
{0,0x66de58b6,0x258,4,0x198,0x800,0,0x290,false}, // Fog.FogColorStart
{0,0x39381539,0x19c,4,0x198,0x1000,0,0x280,false}, // Fog.FogColorEnd
{0,0x67eaf0c1,0x140,16,0x198,0x2000,0,0x160,false}, // Fog.FogColorCurve
{0,0x39b0a478,0x200,4,0x198,0x4000,0,0x230,false}, // Fog.TransparencyFadeClamp
{0,0x3acdca8b,0x22c,4,0x198,0x8000,0,0x25c,false}, // Fog.TransparencyFadeStart
{0,0x3b4b1304,0x250,4,0x198,0x10000,0,0x28c,false}, // Fog.TransparencyFadeEnd
{0,0x39bd237c,0x150,16,0x198,0x20000,0,0x140,false}, // Fog.TransparencyFadeCurve
{0,0x1edbc483,0x26e,1,0x198,0x40000,0,0x298,false}, // Fog.ForwardLightScatteringEnabled
{0,0x2ecc6f6e,0x1f0,4,0x198,0x80000,0,0x1fc,false}, // Fog.ForwardLightScatteringPhaseG
{0,0x2411ef23,0x1a4,4,0x198,0x100000,0,0x250,false}, // Fog.ForwardLightScatteringStrength
{0,0x2ee34dbb,0x120,16,0x198,0x200000,0,0x190,false}, // Fog.ForwardLightScatteringColor
{0,0x6650e9bf,0x25c,4,0x198,0x400000,0,0x1e8,false}, // Fog.ForwardLightScatteringPresence
{0,0xad856027,0x204,4,0x198,0x800000,0,0x1f0,false}, // Fog.ForwardLightScatteringMaxBlurLength
{0,0x1f83add7,0x1c8,4,0x198,0x1000000,0,0x1e4,false}, // Fog.ForwardLightScatteringExtinction
{0,0xe3d2aa6f,0x1b4,4,0x198,0x2000000,0,0x260,false}, // Fog.ForwardLightScatteringSmoothness
{0,0xd89cb4bc,0x230,4,0x198,0x4000000,0,0x294,false}, // Fog.ForwardLightScatteringAttenuationType
{0,0x577b1355,0x276,1,0x198,0x8000000,0,0x2a6,false}, // Fog.HeightFogEnable
{0,0x9497b25c,0x214,4,0x198,0x10000000,0,0x214,false}, // Fog.HeightFogFollowCamera
{0,0x88bb3da4,0x1f8,4,0x198,0x20000000,0,0x26c,false}, // Fog.HeightFogAltitude
{0,0x9dd60e99,0x23c,4,0x198,0x40000000,0,0x20c,false}, // Fog.HeightFogDepth
{0,0x7f3a9c6d,0x1d4,4,0x198,0x80000000,0,0x218,false}, // Fog.HeightFogVisibilityRange
{0,0x9d5b5d74,0x188,4,0x1dc,0x1,1,0x22c,false}, // Fog.HeightFogStart
{0,0x87c8f1fb,0x18c,4,0x1dc,0x2,1,0x200,false}, // Fog.HeightFogEnd
{0,0x9e6c2583,0xe0,16,0x1dc,0x4,1,0x130,false}, // Fog.HeightFogCurve
{0,0xdb5f6bd1,0x26d,1,0x1dc,0x8,1,0x2a8,false}, // Fog.ParticipatingMediaEnable
{0,0xf513e058,0x268,1,0x1dc,0x10,1,0x29a,false}, // Fog.ParticipatingMediaSun
{0,0xde58529a,0x240,4,0x1dc,0x20,1,0x268,false}, // Fog.SunLightScatteringIntensity
{0,0x4375eef0,0x272,1,0x1dc,0x40,1,0x29f,false}, // Fog.ParticipatingMediaLocalLights
{0,0xcb29387f,0x238,4,0x1dc,0x80,1,0x21c,false}, // Fog.LocalLightScatteringIntensity
{0,0x8a3eea60,0x265,1,0x1dc,0x100,1,0x2aa,false}, // Fog.ParticipatingMediaExtinctionEnable
{0,0x8cba4079,0x275,1,0x1dc,0x200,1,0x2a1,false}, // Fog.ParticipatingMediaEmissiveAndAmbientEnable
{0,0xf5cac355,0x271,1,0x1dc,0x400,1,0x2a9,false}, // Fog.LocalLightVolumetricShadowEnable
{0,0x77d5c80e,0x26f,1,0x1dc,0x800,1,0x29c,false}, // Fog.SunVolumetricShadowEnable
{0,0x16aeb8b3,0x274,1,0x1dc,0x1000,1,0x2a4,false}, // Fog.CloudShadowEnable
{0,0x36c7a2,0x180,8,0x1dc,0x2000,1,0x1c0,false}, // Fog.CloudShadowIntensityRange
{0,0xfc4c6567,0x270,1,0x1dc,0x4000,1,0x2a0,false}, // Fog.LocalParticipatingMediaVolumesEnable
{0,0x95105574,0x1e4,4,0x1dc,0x8000,1,0x288,false}, // Fog.ParticipatingMediaDefaultPhase
{0,0x502dc356,0x1c0,4,0x1dc,0x10000,1,0x220,false}, // Fog.ParticipatingMediaFadeStart
{0,0xe5467fd9,0x24c,4,0x1dc,0x20000,1,0x270,false}, // Fog.ParticipatingMediaFadeEnd
{0,0x513e7b61,0x100,16,0x1dc,0x40000,1,0x120,false}, // Fog.ParticipatingMediaFadeCurve
{0,0x6fe1efa3,0x273,1,0x1dc,0x80000,1,0x299,false}, // Fog.ParticipatingMediaFromGlobalFog
{0,0x28d795d3,0x40,80,0x1dc,0x100000,1,0x80,false}, // Fog.DepthFogParticipatingMedia
{0,0x9c044441,0x90,80,0x1dc,0x200000,1,0xd0,false}, // Fog.HeightFogParticipatingMedia
{0,0x44729613,0x1cc,4,0x1dc,0x400000,1,0x24c,false}, // Fog.ParticipatingMediaTemporalMethod
{0,0xbc5798e9,0x1e0,4,0x1dc,0x800000,1,0x1c8,false}, // Fog.ParticipatingMediaTemporalFilterStrength
{0,0xd770d72e,0x1ac,4,0x1dc,0x2000000,1,0x1d8,false}, // Fog.LocalVolumeViewDistance
{0,0x205b48c9,0x1ec,4,0x1dc,0x4000000,1,0x1f8,false}, // Fog.LocalVolumeReflectionViewDistance
{0,0xd500e4a5,0x269,1,0x1dc,0x8000000,1,0x2a7,false}, // Fog.DepthSliceUseLogDistribution
{0,0x2af7565a,0x20c,4,0x1dc,0x10000000,1,0x210,false}, // Fog.DepthSliceLogGradient
{0,0xadf24311,0x194,4,0x1dc,0x20000000,1,0x238,false}, // Fog.DepthSliceDistributionExponent
{0,0xfe32028f,0x1b0,4,0x1dc,0x40000000,1,0x204,false}, // Fog.LocalVolumeFarFadeLength
{0,0x753332a0,0xf0,16,0x1dc,0x80000000,1,0x150,false}, // Fog.LocalVolumeMediaFarFadeCurve
{0,0x45b60c5e,0x1fc,4,0x254,0x1,2,0x254,false}, // Fog.ExtinctionCascadeBaseVoxelSize
{0,0xd589e34,0x1a8,4,0x254,0x2,2,0x264,false}, // Fog.ExtinctionCascadeVoxelSizeCascadeFactor
{0,0x786b1ac8,0x224,4,0x254,0x4,2,0x27c,false}, // Fog.ExtinctionCascadeResolution
{0,0x7c16fbb5,0x220,4,0x254,0x8,2,0x248,false}, // Fog.VolumetricShadowmapResolution
{0,0x68c3a84c,0x228,4,0x254,0x10,2,0x1dc,false}, // Fog.VolumetricShadowmapMaxCount
{0,0xd4590ca,0x248,4,0x254,0x20,2,0x258,false}, // Fog.SunVolumetricShadowSampleCount
{0,0xdb330072,0x21c,4,0x254,0x40,2,0x274,false}, // Fog.SunVolumetricShadowSampleStrech
{0,0x1346a541,0x26a,1,0x254,0x80,2,0x2a2,false}, // Fog.SunExponentialShadowMapEnable
{0,0x3b8714b3,0x160,16,0x254,0x100,2,0x1a0,false}, // Fog.ResolutionScale
{0,0xc78b3f8b,0x170,16,0x254,0x200,2,0x1b0,false}, // Fog.DepthSlices
{0,0xda640834,0x1c4,4,0x254,0x400,2,0x244,false}, // Fog.ReflectionResolutionScale
{0,0x6481fa8c,0x260,4,0x254,0x800,2,0x224,false}, // Fog.ReflectionDepthSlices
{0,0x156b4de7,0x26c,1,0x254,0x1000,2,0x2a3,false}, // Fog.GlobalVolumeEnable
{0,0xbf43aa08,0x210,4,0x254,0x2000,2,0x284,false}, // Fog.GlobalVolumeViewDistance
{0,0xa571ca6f,0x1b8,4,0x254,0x4000,2,0x1ec,false}, // Fog.GlobalVolumeReflectionViewDistance
{0,0x19a9741a,0x1e8,4,0x254,0x8000,2,0x240,false}, // Fog.DepthSliceDistributionExponentGlobal
{0,0xe6199207,0x208,4,0x254,0x10000,2,0x1d4,false}, // Fog.DepthSliceDensityRatioLocalGlobal
{1,0x8ba423e4,0x2dc,1,0x1f0,0x1,0,0x2d6,false}, // Sky.Enable
{1,0x8bfc33e9,0x2df,1,0x1f0,0x2,0,0x2d3,false}, // Sky.DrawSkyGeo
{1,0xb930a227,0x2e0,1,0x1f0,0x4,0,0x2da,false}, // Sky.CaptureForceLowerColorEnabled
{1,0xdefff3c2,0x50,16,0x1f0,0x8,0,0x80,false}, // Sky.CaptureForceLowerColor
{1,0xb18176bc,0x1e8,4,0x1f0,0x10,0,0x1b8,false}, // Sky.SkyType
{1,0x9aff5c27,0x190,4,0x1f0,0x40,0,0x1ac,false}, // Sky.LuminanceScale
{1,0x3bb33fbd,0x188,8,0x1f0,0x80,0,0x198,true}, // Sky.SkyGradientTexture
{1,0x28fb890e,0x2ac,4,0x1f0,0x100,0,0x228,false}, // Sky.AlphaOutput
{1,0x82e1c220,0x2e1,1,0x1f0,0x200,0,0x2d4,false}, // Sky.AllowEnergyIncreaseInNormalization
{1,0xd0a1ba6,0x224,4,0x1f0,0x400,0,0x224,false}, // Sky.HdriRotation
{1,0x239bd7cd,0x120,8,0x1f0,0x800,0,0x160,true}, // Sky.HdriTexture
{1,0x801c6248,0x208,4,0x1f0,0x1000,0,0x230,false}, // Sky.SunSize
{1,0x83ae3355,0x220,4,0x1f0,0x2000,0,0x1d0,false}, // Sky.SunScale
{1,0x876feef0,0x1c4,4,0x1f0,0x4000,0,0x2c4,false}, // Sky.PanoramicUVMinX
{1,0x8770112e,0x228,4,0x1f0,0x8000,0,0x2bc,false}, // Sky.PanoramicUVMaxX
{1,0x876feef1,0x2cc,4,0x1f0,0x10000,0,0x20c,false}, // Sky.PanoramicUVMinY
{1,0x8770112f,0x274,4,0x1f0,0x20000,0,0x2b0,false}, // Sky.PanoramicUVMaxY
{1,0xfab19bb8,0x1ac,4,0x1f0,0x40000,0,0x274,false}, // Sky.PanoramicTileFactor
{1,0x3a72c6f5,0x2b8,4,0x1f0,0x80000,0,0x1b0,false}, // Sky.PanoramicRotation
{1,0x9ffb073e,0x150,8,0x1f0,0x100000,0,0x188,true}, // Sky.PanoramicTexture
{1,0x86cd3d0a,0x170,8,0x1f0,0x200000,0,0x1a0,true}, // Sky.PanoramicAlphaTexture
{1,0xe11089cd,0x204,4,0x1f0,0x400000,0,0x2b4,false}, // Sky.PanoramicAlphaScale
{1,0x6de9a4c,0x1dc,4,0x1f0,0x800000,0,0x264,false}, // Sky.PanoramicAlphaBias
{1,0xaa727174,0x2e3,1,0x1f0,0x1000000,0,0x2d7,false}, // Sky.SkyGradientFollowsPanoramicUVs
{1,0x2fe80f92,0x2bc,4,0x1f0,0x2000000,0,0x240,false}, // Sky.FlowPeriod
{1,0x2d112cd4,0x198,4,0x1f0,0x4000000,0,0x22c,false}, // Sky.FlowDistance
{1,0xf0115992,0x26c,4,0x1f0,0x8000000,0,0x1bc,false}, // Sky.FlowDirection
{1,0x9f392004,0x258,4,0x1f0,0x10000000,0,0x2cc,false}, // Sky.FlowHeightMaskScale
{1,0x907b0fa5,0x238,4,0x1f0,0x20000000,0,0x2a4,false}, // Sky.FlowHeightMaskBias
{1,0xe1fcc07c,0x138,8,0x1f0,0x40000000,0,0x178,true}, // Sky.FlowMaskTexture
{1,0x7c0164a2,0x110,16,0x1f0,0x80000000,0,0xe0,false}, // Sky.CloudLayerSunColor
{1,0x5084247c,0x128,8,0x290,0x1,1,0x170,true}, // Sky.CloudLayerMaskTexture
{1,0x7ad9af56,0x2b0,4,0x290,0x2,1,0x25c,false}, // Sky.CloudLayer1Altitude
{1,0x4412351f,0x248,4,0x290,0x4,1,0x2c8,false}, // Sky.CloudLayer1TileFactor
{1,0x68e78212,0x24c,4,0x290,0x8,1,0x284,false}, // Sky.CloudLayer1Rotation
{1,0xb661eb21,0x1f4,4,0x290,0x10,1,0x1a8,false}, // Sky.CloudLayer1Speed
{1,0x6c30401f,0x1d8,4,0x290,0x20,1,0x1c4,false}, // Sky.CloudLayer1SunLightIntensity
{1,0x26c0fa8f,0x218,4,0x290,0x40,1,0x1b4,false}, // Sky.CloudLayer1SunLightPower
{1,0x543001af,0x2d0,4,0x290,0x80,1,0x294,false}, // Sky.CloudLayer1AmbientLightIntensity
{1,0xb54ad2db,0xf0,16,0x290,0x100,1,0xb0,false}, // Sky.CloudLayer1Color
{1,0x717edc66,0x200,4,0x290,0x200,1,0x1d4,false}, // Sky.CloudLayer1AlphaMul
{1,0xdc02bbf9,0x158,8,0x290,0x400,1,0x190,true}, // Sky.CloudLayer1Texture
{1,0x3c039f87,0x1a0,4,0x290,0x800,1,0x210,false}, // Sky.CloudLayer1Absorption
{1,0xc0c08be0,0x1cc,4,0x290,0x1000,1,0x24c,false}, // Sky.CloudLayer1Scattering
{1,0xb6ea9429,0x2a0,4,0x290,0x2000,1,0x23c,false}, // Sky.CloudLayer1Phase
{1,0x81b98690,0x27c,4,0x290,0x4000,1,0x21c,false}, // Sky.CloudLayer1Thickness
{1,0x775fd355,0x214,4,0x290,0x8000,1,0x1f0,false}, // Sky.CloudLayer2Altitude
{1,0x9dfca59c,0x1f8,4,0x290,0x10000,1,0x298,false}, // Sky.CloudLayer2TileFactor
{1,0x7d862991,0x280,4,0x290,0x20000,1,0x204,false}, // Sky.CloudLayer2Rotation
{1,0xab23bbc2,0x1c8,4,0x290,0x40000,1,0x220,false}, // Sky.CloudLayer2Speed
{1,0x2eebddbc,0x1b0,4,0x290,0x80000,1,0x290,false}, // Sky.CloudLayer2SunLightIntensity
{1,0x5742212c,0x254,4,0x290,0x100000,1,0x28c,false}, // Sky.CloudLayer2SunLightPower
{1,0x3a9a798c,0x20c,4,0x290,0x200000,1,0x2c0,false}, // Sky.CloudLayer2AmbientLightIntensity
{1,0xaa0d9238,0x80,16,0x290,0x400000,1,0x110,false}, // Sky.CloudLayer2Color
{1,0x6e2608e5,0x2a4,4,0x290,0x800000,1,0x1c0,false}, // Sky.CloudLayer2AlphaMul
{1,0x47a4ed1a,0x130,8,0x290,0x1000000,1,0x168,true}, // Sky.CloudLayer2Texture
{1,0x6e341204,0x1a4,4,0x290,0x2000000,1,0x2ac,false}, // Sky.CloudLayer2Absorption
{1,0x9c4e05a3,0x29c,4,0x290,0x4000000,1,0x1ec,false}, // Sky.CloudLayer2Scattering
{1,0xab41e2ca,0x2c8,4,0x290,0x8000000,1,0x234,false}, // Sky.CloudLayer2Phase
{1,0x57d9b473,0x28c,4,0x290,0x10000000,1,0x270,false}, // Sky.CloudLayer2Thickness
{1,0x97d7f483,0x140,8,0x290,0x20000000,1,0x180,true}, // Sky.StaticEnvmapTexture
{1,0x3ed079c4,0x284,4,0x290,0x40000000,1,0x1c8,false}, // Sky.StaticEnvmapScale
{1,0x20dc0c97,0x250,4,0x290,0x80000000,1,0x2b8,false}, // Sky.EarthRadius
{1,0x63f98f93,0x1d0,4,0x1e0,0x1,2,0x254,false}, // Sky.AtmosphereRadius
{1,0xe5491897,0x2b4,4,0x1e0,0x2,2,0x208,false}, // Sky.MieScatteringCoefficient
{1,0x7c801b03,0x1b8,4,0x1e0,0x4,2,0x29c,false}, // Sky.MieG
{1,0xf628cee6,0x1fc,4,0x1e0,0x8,2,0x1f8,false}, // Sky.MieExtinctionCoefficientRelation
{1,0x2e9d1ae3,0x1b4,4,0x1e0,0x10,2,0x214,false}, // Sky.ScaleHeightMie
{1,0xeb66fed3,0xd0,16,0x1e0,0x20,2,0x90,false}, // Sky.RayleighScatteringCoefficient
{1,0x689fea8b,0x278,4,0x1e0,0x40,2,0x1e4,false}, // Sky.RayleighScatteringCoefficientScale
{1,0xd47a7722,0x2c0,4,0x1e0,0x80,2,0x250,false}, // Sky.RayleighExtinctionCoefficientRelation
{1,0x92d3847,0x22c,4,0x1e0,0x100,2,0x260,false}, // Sky.ScaleHeightRayleigh
{1,0xf48e117,0x2d9,1,0x1e0,0x200,2,0x2d0,false}, // Sky.UseOzone
{1,0x831c0e0c,0x1e4,4,0x1e0,0x400,2,0x288,false}, // Sky.OzonePercentage
{1,0x40d17298,0x2d8,1,0x1e0,0x800,2,0x2d5,false}, // Sky.UseAerialPerspective
{1,0xcd0471a3,0x2a8,4,0x1e0,0x1000,2,0x1cc,false}, // Sky.AerialPerspectiveScale
{1,0xcb1a4494,0x25c,4,0x1e0,0x2000,2,0x27c,false}, // Sky.AerialPerspectiveIntensity
{1,0x504cf03d,0x270,4,0x1e0,0x4000,2,0x26c,false}, // Sky.AerialPerspectiveDithering
{1,0x95a82c6c,0x23c,4,0x1e0,0x8000,2,0x1f4,false}, // Sky.AerialPerspectiveMaxDistance
{1,0x51914cd7,0xc0,16,0x1e0,0x10000,2,0xa0,false}, // Sky.Light1Color
{1,0xe5a3a4c5,0x210,4,0x1e0,0x20000,2,0x1e8,false}, // Sky.Light1Intensity
{1,0x2335c4fd,0x2dd,1,0x1e0,0x40000,2,0x2d2,false}, // Sky.Light1FollowOutdoorLight
{1,0xb567e16f,0x2da,1,0x1e0,0x80000,2,0x2d8,false}, // Sky.Light1TakesColorFromOutdoorLight
{1,0x7ea12c1b,0x234,4,0x1e0,0x100000,2,0x248,false}, // Sky.Light1RotX
{1,0x7ea12c1a,0x294,4,0x1e0,0x200000,2,0x258,false}, // Sky.Light1RotY
{1,0x608d0ff7,0x2db,1,0x1e0,0x400000,2,0x2d1,false}, // Sky.UseLightSource2
{1,0x46540c34,0x40,16,0x1e0,0x800000,2,0xc0,false}, // Sky.Light2Color
{1,0x3004c626,0x1a8,4,0x1e0,0x1000000,2,0x1dc,false}, // Sky.Light2Intensity
{1,0x7e4d0358,0x288,4,0x1e0,0x2000000,2,0x2a8,false}, // Sky.Light2RotX
{1,0x7e4d0359,0x19c,4,0x1e0,0x4000000,2,0x278,false}, // Sky.Light2RotY
{1,0xf52bb58,0x2e2,1,0x1e0,0x8000000,2,0x2db,false}, // Sky.UseNoise
{1,0xccf636a8,0x194,4,0x1e0,0x10000000,2,0x1e0,false}, // Sky.FogStartDistance
{1,0xd1b50b0e,0x60,16,0x1e0,0x20000000,2,0x100,false}, // Sky.RayleighPolarization
{1,0x4fe711ca,0xa0,16,0x1e0,0x40000000,2,0x130,false}, // Sky.MiePolarization
{1,0xea7beb3b,0x90,16,0x1e0,0x80000000,2,0x150,false}, // Sky.OutdoorLightScale
{1,0xda190bd0,0x2de,1,0x298,0x1,3,0x2d9,false}, // Sky.DrawSunDisc
{1,0xa71db073,0x70,16,0x298,0x2,3,0x120,false}, // Sky.ForwardScatteringDepthVisibility
{1,0x77148335,0x1ec,4,0x298,0x4,3,0x244,false}, // Sky.ForwardScatteringStartDepth
{1,0xec82bf5a,0x260,4,0x298,0x8,3,0x200,false}, // Sky.ForwardScatteringEndDepth
{1,0x4a1d65bd,0x21c,4,0x298,0x10,3,0x238,false}, // Sky.ForwardScatteringTakesColorFromOutdoorLight
{1,0xf92d1999,0x100,16,0x298,0x20,3,0xf0,false}, // Sky.ForwardScatteringOutdoorLightTint
{1,0x7ec79f08,0xe0,16,0x298,0x40,3,0x140,false}, // Sky.HeightFogColorAdd
{1,0x57b98fe9,0xb0,16,0x298,0x80,3,0xd0,false}, // Sky.HeightFogColorMult
{1,0x63c1b09,0x1d4,4,0x298,0x100,3,0x1fc,false}, // Sky.MinHeightFogTransmittance
{2,0xbb9fa0d4,0x40,4,0x62,0x1,0,0x94,false}, // Wind.WindDirection
{2,0x1995362e,0x54,4,0x62,0x2,0,0x9c,false}, // Wind.WindDirectionVariationMultiplier
{2,0xe0a01ad4,0x5c,4,0x62,0x4,0,0xa0,false}, // Wind.WindStrength
{2,0x5543330b,0x58,4,0x62,0x8,0,0x98,false}, // Wind.WindVariationMultiplier
{2,0x9993e529,0x50,4,0x62,0x10,0,0x80,false}, // Wind.WindVariationRateMultiplier
{2,0x463725d1,0x38,4,0x62,0x20,0,0x88,false}, // Wind.WindMicroVariationMultiplier
{2,0x62d41de3,0x48,4,0x62,0x40,0,0x84,false}, // Wind.WindOscillationAmplitude
{2,0x8b89cdfe,0x3c,4,0x62,0x80,0,0x90,false}, // Wind.WindOscillationFrequency
{2,0xbff3190b,0x4c,4,0x62,0x100,0,0x8c,false}, // Wind.TurbulenceMultiplier
{2,0x44bbbf8,0x44,4,0x62,0x200,0,0xa4,false}, // Wind.TurbulenceScale
}};
}
@@ -0,0 +1,9 @@
#pragma once
#include <cstdint>
namespace dingosdk::game::build::v20260929::buildkit_limits {
// DingoObjectPersistenceSettings vtable (Build Kit object budget and cursor radius).
inline constexpr std::uintptr_t object_persistence_settings_vtable = 0x61259a0;
// DingoObjectPersistenceSettings TypeInfo; also its settings-manager lookup key.
inline constexpr std::uintptr_t object_persistence_settings_type = 0x723a510;
}
@@ -0,0 +1,22 @@
#pragma once
#include <array>
#include <cstdint>
namespace dingosdk::game::build::v20260929::camera_observer {
// Camera callback observed by the camera diagnostics.
inline constexpr std::uintptr_t camera_callback = 0x178b690;
inline constexpr std::array<unsigned char, 24> camera_callback_prefix{
0x40,0x53,0x48,0x83,0xec,0x20,0x48,0x8b,0x59,0x18,0x48,0x8b,
0x41,0x20,0xc5,0xfa,0x10,0x02,0x48,0x89,0x53,0x60,0x48,0x8d};
// Return address of the verified native caller of the camera callback.
inline constexpr std::uintptr_t native_caller_return = 0x1728bd5;
// Camera callback object.
inline constexpr std::uintptr_t callback_vtable = 0x625b768;
// The two native camera entry variants. 14178fb00 and 14178fba0 create them
// through the same 1417224a0 base constructor; the live stuck-camera capture
// selected the alternate one after a camera/editor transition.
inline constexpr std::uintptr_t entry_vtable = 0x625c3d8;
inline constexpr std::uintptr_t alternate_entry_vtable = 0x625c270;
// Callback helper.
inline constexpr std::uintptr_t helper_vtable = 0x625b7b0;
}
@@ -0,0 +1,58 @@
#pragma once
#include <array>
#include <cstdint>
// Native camera, game UI and noclip functions used by the debug controls and party spectate.
namespace dingosdk::game::build::v20260929::client_source_spawn {
// ClientSkaterSource creator (source). Creates source+0x68 from data+0xd0. Reference only
// (ai_skaters mirrors it).
inline constexpr std::uintptr_t source_create = 0x51fef0;
// uint32 offset of the owning client in a game context.
inline constexpr std::uintptr_t context_client_offset = 0x6fc25f0;
// Free camera.
// uint32 native camera local id.
inline constexpr std::uintptr_t camera_local_id = 0x73c7ab8;
inline constexpr std::uintptr_t camera_controller_vtable = 0x63efab8;
// Camera controller vtable slot +0xb8.
inline constexpr std::uintptr_t camera_mode_switch = 0x2f2c360;
inline constexpr std::array<unsigned char, 24> camera_mode_prefix{
0x48,0x89,0x5c,0x24,0x08,0x57,0x48,0x83,0xec,0x20,0x8b,0xda,0x48,0x8b,0xf9,0x39,
0x51,0x08,0x74,0x14,0x83,0xfa,0x01,0x77};
inline constexpr std::uintptr_t free_camera_vtable = 0x63fc220;
// FreeCamera vtable slot +0xd0.
inline constexpr std::uintptr_t camera_transform_setter = 0x2f8fa90;
inline constexpr std::array<unsigned char, 24> camera_transform_prefix{
0xc5,0xfc,0x10,0x02,0xc5,0xfc,0x11,0x81,0x80,0x01,0x00,0x00,0xc5,0xfc,0x10,0x4a,
0x20,0xc5,0xfc,0x11,0x89,0xa0,0x01,0x00};
// Initializes the camera's degree FOV at +0xac. Reference only.
inline constexpr std::uintptr_t camera_fov_init = 0x2f8bb70;
// FreeCamera input helpers at +0x1d0 and +0x1d8.
inline constexpr std::array<std::uintptr_t, 2> free_camera_helper_vtables{0x63fc1e0, 0x63fc200};
// Pointer to the camera view manager.
inline constexpr std::uintptr_t view_manager = 0x75cbc48;
// Pointer: the registered camera view manager.
inline constexpr std::uintptr_t view_manager_registration = 0x75dd288;
inline constexpr std::uintptr_t view_manager_vtable = 0x63ece68;
// view_manager_vtable slots 0, +8, +0x28 and +0x98.
inline constexpr std::array<std::uintptr_t, 4> view_manager_methods{0x2f11470, 0x2f0f700, 0x2f0d7c0, 0x2f0d0b0};
inline constexpr std::uintptr_t camera_view_vtable = 0x63ed0a0;
// Debug UI and noclip.
// Pointer to the game UI settings; draw flag at +0x4e.
inline constexpr std::uintptr_t ui_settings = 0x7761aa0;
inline constexpr std::uintptr_t ui_settings_vtable = 0x65ae378;
inline constexpr std::uintptr_t ui_settings_type = 0x77674d0;
inline constexpr std::uintptr_t board_physics_vtable = 0x65e67a0;
inline constexpr std::uintptr_t rig_physics_vtable = 0x65dabc0;
// Skater physics core update; no_bail::bail_core_vtable slot +0x58. Hooked for noclip.
inline constexpr std::uintptr_t physics_update = 0x47ddca0;
inline constexpr std::array<unsigned char, 32> physics_update_prefix{
0x40,0x53,0x48,0x83,0xec,0x20,0x48,0x8b,0xd9,0x48,0x8b,0x89,0xb0,0x03,0x00,0x00,
0x48,0x8b,0x01,0xff,0x50,0x18,0x48,0x8b,0x8b,0xf8,0x03,0x00,0x00,0xe8,0xfe,0xd3};
// Skater motion target update. Hooked for noclip.
inline constexpr std::uintptr_t skater_motion = 0x4776a80;
inline constexpr std::array<unsigned char, 32> skater_motion_prefix{
0x48,0x89,0x5c,0x24,0x08,0x48,0x89,0x74,0x24,0x10,0x57,0x48,0x83,0xec,0x50,0x49,
0x8d,0x70,0x10,0x48,0x8b,0xd9,0x49,0x8d,0x78,0x20,0xc4,0xc1,0x7c,0x10,0x00,0xc5};
}
@@ -0,0 +1,8 @@
#pragma once
#include <array>
namespace dingosdk::game::build::v20260929::debug_settings_access {
// Expected first bytes of the settings manager lookup (engine::settings_lookup).
inline constexpr std::array<unsigned char, 24> settings_lookup_prefix{
0x48,0x89,0x5c,0x24,0x08,0x48,0x89,0x74,0x24,0x10,0x57,0x48,0x83,0xec,0x20,0x48,0x8d,0x59,0x70,0x48,0x8b,0xf9,0x48,0x8b};
}
+143
View File
@@ -0,0 +1,143 @@
#pragma once
#include <cstdint>
// Engine addresses shared by several features. Addresses only one feature uses
// belong in that feature's own header.
namespace dingosdk::game::build::v20260929::engine {
// Functions
// UI control destructor shared by the cosmetic and music list vtables.
inline constexpr std::uintptr_t ui_control_destroy = 0x214930;
// Audio manager vtable slot +0x20.
inline constexpr std::uintptr_t audio_manager_update = 0x43c2f0;
// Entitlement check request.
inline constexpr std::uintptr_t check_entitlements = 0x774340;
// Constructs an engine error value.
inline constexpr std::uintptr_t construct_error = 0x83de30;
// Animation definition count getter (definition+0x1a0 -> +0xc).
inline constexpr std::uintptr_t animation_count_getter = 0x11e5040;
// Releases an engine-owned text handle.
inline constexpr std::uintptr_t native_text_release = 0x1586050;
inline constexpr std::uintptr_t string_assign = 0x1587620;
// Returns the calling thread's game context.
inline constexpr std::uintptr_t current_context = 0x18b6ae0;
// Settings manager lookup by descriptor.
inline constexpr std::uintptr_t settings_lookup = 0x18d6ef0;
// UI data model lookup.
inline constexpr std::uintptr_t model_find = 0x19193b0;
// Game query context constructor.
inline constexpr std::uintptr_t query_context_constructor = 0x276f3e0;
// Skateboard render pose publisher.
inline constexpr std::uintptr_t board_render_pose = 0x2e48610;
// Assigns a future from a promise state.
inline constexpr std::uintptr_t assign_future = 0x3dc5bd0;
// Engine array allocation.
inline constexpr std::uintptr_t array_allocate = 0x3dc7540;
inline constexpr std::uintptr_t delegate_copy = 0x3dd5540;
inline constexpr std::uintptr_t delegate_destroy = 0x3dd5780;
// Sets a flag on a customization/indicator property.
inline constexpr std::uintptr_t set_customization_flag = 0x4654e10;
// Table functions graphs bind PostEventDelayedFrames (0x9967fc07) and
// PostEventDelayedSeconds (0x5e3aee8e) calls to.
inline constexpr std::uintptr_t post_event_delayed_frames = 0x4664e80;
inline constexpr std::uintptr_t post_event_delayed_seconds = 0x4665040;
// Completes a promise with a result or error.
inline constexpr std::uintptr_t complete_promise = 0x58b1cf0;
inline constexpr std::uintptr_t delegate_invoke = 0x58b1d60;
// Player card info constructor.
inline constexpr std::uintptr_t card_info_construct = 0x59dde00;
// Vtables
// Connection controller.
inline constexpr std::uintptr_t controller_vtable = 0x606dc60;
inline constexpr std::uintptr_t allocator_adapter_vtable = 0x606df70;
inline constexpr std::uintptr_t source_manager_vtable = 0x6079940;
inline constexpr std::uintptr_t skater_audio_component_vtable = 0x607a500;
inline constexpr std::uintptr_t audio_manager_vtable = 0x607b520;
// Client game context; state at +0xc4.
inline constexpr std::uintptr_t client_vtable = 0x60842c0;
inline constexpr std::uintptr_t client_skater_source_vtable = 0x6084fa8;
inline constexpr std::uintptr_t skater_entity_vtable = 0x6087908;
inline constexpr std::uintptr_t cosmetics_manager_vtable = 0x6089cc8;
// Current game server.
inline constexpr std::uintptr_t server_vtable = 0x60e0890;
// DelMarGameSettings.
inline constexpr std::uintptr_t game_settings_vtable = 0x6145be0;
inline constexpr std::uintptr_t skater_component_vtable = 0x6170410;
inline constexpr std::uintptr_t board_component_vtable = 0x6170e38;
inline constexpr std::uintptr_t board_entity_vtable = 0x6171808;
inline constexpr std::uintptr_t board_data_vtable = 0x6186fa0;
// Blueprint objects, including the board and UI widget blueprints.
inline constexpr std::uintptr_t blueprint_vtable = 0x6187008;
// Skater appearance entity.
inline constexpr std::uintptr_t skater_appearance_vtable = 0x61c9ec0;
inline constexpr std::uintptr_t camera_vtable = 0x6255870;
inline constexpr std::uintptr_t board_holder_vtable = 0x63db690;
inline constexpr std::uintptr_t local_player_vtable = 0x63e2d30;
inline constexpr std::uintptr_t local_player_manager_vtable = 0x63e30a0;
inline constexpr std::uintptr_t server_player_vtable = 0x63e4108;
inline constexpr std::uintptr_t server_player_manager_vtable = 0x63e41d0;
// Type information
inline constexpr std::uintptr_t cosmetic_slot_type = 0x72290b8;
inline constexpr std::uintptr_t persona_id_type = 0x723bcf8;
inline constexpr std::uintptr_t player_info_type = 0x72591d8;
inline constexpr std::uintptr_t board_blueprint_type = 0x7288f80;
inline constexpr std::uintptr_t reference_type = 0x7493030;
inline constexpr std::uintptr_t bool_type = 0x765d8e8;
inline constexpr std::uintptr_t uint32_type = 0x765da28;
inline constexpr std::uintptr_t int32_type = 0x765da68;
inline constexpr std::uintptr_t uint64_type = 0x765daa8;
inline constexpr std::uintptr_t int64_type = 0x765dae8;
inline constexpr std::uintptr_t float_type = 0x765db28;
// CString.
inline constexpr std::uintptr_t string_type = 0x765dcc0;
// Globals
// uint32 offset of the partition record in a game context.
inline constexpr std::uintptr_t context_partition_offset = 0x6fc2430;
// uint32 offset of the type/realm record in a game context.
inline constexpr std::uintptr_t context_type_offset = 0x6fc24b8;
// uint32 offset of the client connection in a game context.
inline constexpr std::uintptr_t context_client_connection_offset = 0x715d5e8;
// uint32 offset of the player manager in a game context.
inline constexpr std::uintptr_t context_player_manager_offset = 0x7161720;
// Pointer.
inline constexpr std::uintptr_t backend_services = 0x71e17e0;
// Pointer.
inline constexpr std::uintptr_t audio_manager = 0x71e3978;
// Pointer.
inline constexpr std::uintptr_t appearance_manager = 0x71eaba0;
// Pointer array indexed by partition * 7 + kind.
inline constexpr std::uintptr_t source_managers = 0x71ec560;
// Pointer.
inline constexpr std::uintptr_t loadout_manager = 0x71ec8c8;
// Pointer.
inline constexpr std::uintptr_t cosmetics_manager = 0x71ee610;
// Pointer.
inline constexpr std::uintptr_t client_game_manager = 0x71ef898;
// Pointer.
inline constexpr std::uintptr_t location_owner = 0x71f4288;
// Pointer.
inline constexpr std::uintptr_t ui_manager = 0x71f5ef0;
// Pointer.
inline constexpr std::uintptr_t game_server = 0x71f8ad0;
// DelMarGameSettings handle.
inline constexpr std::uintptr_t game_settings_handle = 0x725b018;
// Pointer.
inline constexpr std::uintptr_t default_arena = 0x72f6e78;
// The engine's empty CString.
inline constexpr std::uintptr_t empty_cstring = 0x72fe704;
// Pointer.
inline constexpr std::uintptr_t settings_manager = 0x7482ce0;
// Pointer.
inline constexpr std::uintptr_t ui_allocator = 0x74889e8;
// uint8.
inline constexpr std::uintptr_t entity_creation_ready = 0x75c26f0;
// uint32 registration offset between player base classes.
inline constexpr std::uintptr_t player_parent_offset = 0x75c44d8;
// Pointer array indexed by domain.
inline constexpr std::uintptr_t domain_owners = 0x7726d10;
// uint32 engine TLS slot.
inline constexpr std::uintptr_t tls_index = 0x78d40e0;
}
@@ -0,0 +1,88 @@
#pragma once
#include "Engine/Game/Build/fingerprint.h"
#include <cstdint>
// Engine work the developers left on the table (Extension/Rendering/engine_tweaks.cpp, `perf`
// toggles; analysis/sim-performance.md "Engine code").
namespace dingosdk::game::build::v20260929::engine_tweaks {
// 1. Enlighten GI's per-frame update(manager, mode): runs on the render thread every rendered
// frame and dispatches the probe-set solves (SolveEntireProbeSetTask 0x54d5090, SolveIrradianceTask
// 0x54c7350). Its own skip path (manager + 0x18 bits 0/1) returns at once with nothing done.
// The GI frame (0x5196270) calls it once a frame (gi_update_frame_return), or, while a setting
// (+0x13d of the object its +0x250 getter returns) is on, settings + 0x78 times in a row
// (gi_update_loop_return).
inline constexpr Fingerprint gi_update{0x518c4c0, {
0x40,0x55,0x41,0x55,0x41,0x56,0x48,0x8d,0xac,0x24,0x90,0xf6,0xff,0xff,0x48,0x81,
0xec,0x70,0x0a,0x00,0x00,0x48,0x8b,0x05,0xe4,0x7e,0x03,0x02,0x48,0x33,0xc4,0x48}};
inline constexpr std::uintptr_t gi_update_frame_return = 0x51966f4;
inline constexpr std::uintptr_t gi_update_loop_return = 0x519660c;
// 2. meshCull's scheduler, run once per frame before the cull jobs; 11 arguments (rcx, rdx, r9
// and stack slots 5, 6 and 11 are read; no floating-point ones). It and the job (0x4eecaa0)
// each read WorldRenderSettings.MeshCullTreeEnabled (+0x95d): 0 = flat path testing every chunk
// against every view, 1 = octree pre-cull (meshCullPrepare 0x4eb2b70). The settings come from
// settings_lookup(*settings_manager, world_render_settings_type).
inline constexpr Fingerprint mesh_cull_schedule{0x4eb6620, {
0x48,0x89,0x5c,0x24,0x18,0x55,0x56,0x57,0x41,0x54,0x41,0x55,0x41,0x56,0x41,0x57,
0x48,0x8d,0xac,0x24,0xe0,0xfd,0xff,0xff,0x48,0x81,0xec,0x20,0x03,0x00,0x00,0x48}};
inline constexpr std::uintptr_t settings_lookup = 0x18d6ef0;
inline constexpr std::uintptr_t settings_manager = 0x7482ce0;
inline constexpr std::uintptr_t world_render_settings_type = 0x78adf88;
inline constexpr std::uintptr_t mesh_cull_tree_enabled = 0x95d;
// 3. Job wake-ups. push(scheduler, job): if scheduler + 0x271 runs the job at once (execute
// 0x321ed00 on scheduler + 0x2c0); else pushes it (lockfree_push(scheduler + 0x10, job, job + 0x40))
// and wakes the first worker, in bit order, of the waiter mask (scheduler + 0x2a8) that wake()
// accepts. Worker states (worker_table + i * worker_stride + worker_state): 3 running, 4 entering
// idle (wake = one CAS 4 -> 3), 5 asleep (wake = CAS 5 -> 3 and a semaphore release). The idle
// wait (0x3228220) sets 3 -> 4, calls register_idle (no arguments), which puts the worker's bit in
// its schedulers' and queues' masks and returns non-zero if work was already there; on 0 it sets
// 4 -> 5 and sleeps, or, when a pusher got there first (state 3), unregisters and runs again.
// The worker is read from the thread's TLS block (gs:[0x58][*tls_index] + tls_worker).
inline constexpr Fingerprint job_push{0x321d710, {
0x40,0x53,0x48,0x83,0xec,0x20,0x80,0xb9,0x71,0x02,0x00,0x00,0x00,0x48,0x8b,0xd9,
0x75,0x6a,0x4c,0x8d,0x42,0x40,0x48,0x89,0x74,0x24,0x38,0x48,0x83,0xc1,0x10,0xe8}};
inline constexpr Fingerprint job_register_idle{0x3227290, {
0x40,0x53,0x55,0x56,0x57,0x48,0x83,0xec,0x28,0x8b,0x0d,0x41,0xce,0x6a,0x04,0x65,
0x48,0x8b,0x04,0x25,0x58,0x00,0x00,0x00,0xbd,0x19,0x0b,0x00,0x00,0x48,0x8b,0x1c}};
inline constexpr std::uintptr_t job_wake = 0x3228400;
inline constexpr std::uintptr_t job_lockfree_push = 0x11eb850;
inline constexpr std::uintptr_t job_execute = 0x321ed00;
inline constexpr std::uintptr_t job_scheduler_immediate = 0x271;
inline constexpr std::uintptr_t job_scheduler_queue = 0x10;
inline constexpr std::uintptr_t job_scheduler_inline = 0x2c0;
inline constexpr std::uintptr_t job_scheduler_waiters = 0x2a8;
inline constexpr std::uintptr_t job_link = 0x40;
inline constexpr std::uintptr_t worker_table = 0x7607770;
inline constexpr std::uintptr_t worker_stride = 0x178;
inline constexpr std::uintptr_t worker_state = 0x5c;
inline constexpr std::uintptr_t tls_index = 0x78d40e0;
inline constexpr std::uintptr_t tls_worker = 0x6c8;
// 4. Precise sleep(timer, nanoseconds): an auto-reset, non-high-resolution waitable timer (timer +
// 8, made by 0x18b0220) for all but the last 1 ms (0x73c7ba0, from timeGetDevCaps), then a spin,
// at thread priority 15 while it waits; clears and obeys an abort flag (timer + 0x10) on every
// pass. The abort (0x18bd9d0) sets the flag and fires the timer at once. The main loop (0x4645080)
// calls it with what is left of its 10 ms budget; main_sleep_return is that call's return address.
inline constexpr Fingerprint precise_sleep{0x18bcc30, {
0x40,0x55,0x53,0x56,0x57,0x41,0x56,0x48,0x8d,0x6c,0x24,0xc9,0x48,0x81,0xec,0xa0,
0x00,0x00,0x00,0x4c,0x8b,0x35,0x26,0xb1,0xb0,0x05,0x48,0x8b,0xda,0x48,0x8b,0xf1}};
inline constexpr std::uintptr_t main_sleep_return = 0x4645328;
inline constexpr std::uintptr_t sleep_timer_handle = 0x8;
inline constexpr std::uintptr_t sleep_abort = 0x10;
// 5. Expression shader section dirty-bit clear(section), every frame for every section (only
// caller 0x505a9a0): takes a byte spinlock (+0x16) to clear the dirty bits -- bits 0-17 of the
// dword at +0x18 when the list at +0x128..+0x130 is empty, else bits 0-5 of the word at +0x1c --
// and only does more (sets bit 18, notifies) when some were set. With none set it writes back
// the same values: no effect but the lock.
inline constexpr Fingerprint section_dirty_clear{0x506d620, {
0x40,0x53,0x48,0x83,0xec,0x20,0x48,0x8b,0x81,0x30,0x01,0x00,0x00,0x48,0x8b,0xd9,
0x48,0x39,0x81,0x28,0x01,0x00,0x00,0xb9,0x01,0x00,0x00,0x00,0x74,0x32,0x66,0x90}};
inline constexpr std::uintptr_t section_list_begin = 0x128;
inline constexpr std::uintptr_t section_list_end = 0x130;
inline constexpr std::uintptr_t section_dirty = 0x18; // dword
inline constexpr std::uint32_t section_dirty_mask = 0x3ffff;
inline constexpr std::uintptr_t section_list_dirty = 0x1c; // word
inline constexpr std::uint16_t section_list_dirty_mask = 0x3f;
}
@@ -0,0 +1,49 @@
#pragma once
#include "engine.h"
#include <array>
#include <cstdint>
namespace dingosdk::game::build::v20260929::entitlement_trace {
// CheckUsersEntitlements entry.
inline constexpr std::uintptr_t check_entitlements = engine::check_entitlements;
inline constexpr std::array<unsigned char, 40> check_entitlements_prefix{
0x40,0x55,0x57,0x41,0x56,0x41,0x57,0x48,0x8d,0x6c,0x24,0xd8,0x48,0x81,0xec,0x28,
0x01,0x00,0x00,0x48,0x8b,0x05,0x66,0x00,0xa5,0x06,0x48,0x33,0xc4,0x48,0x89,0x45,
0x08,0x4c,0x8b,0xf1,0x4c,0x89,0x44,0x24};
// Progression request builder.
inline constexpr std::uintptr_t request_builder = 0x3912d0;
inline constexpr std::array<unsigned char, 40> request_builder_prefix{
0x48,0x89,0x5c,0x24,0x20,0x55,0x56,0x57,0x41,0x54,0x41,0x55,0x41,0x56,0x41,0x57,
0x48,0x8d,0xac,0x24,0x40,0xff,0xff,0xff,0x48,0x81,0xec,0xc0,0x01,0x00,0x00,0x48,
0x8b,0x05,0xca,0x30,0xe3,0x06,0x48,0x33};
// Entitlement result reducer.
inline constexpr std::uintptr_t result_reducer = 0x76d900;
inline constexpr std::array<unsigned char, 40> result_reducer_prefix{
0x48,0x8b,0xc4,0x53,0x57,0x48,0x81,0xec,0xa8,0x00,0x00,0x00,0x4c,0x89,0x60,0xe0,
0x48,0x8b,0xda,0x4c,0x89,0x70,0xd0,0x4c,0x8b,0xe1,0x48,0x89,0x4c,0x24,0x78,0x48,
0x8d,0x4c,0x24,0x28,0x4c,0x89,0x78,0xc8};
// Entitlement request continuation callback.
inline constexpr std::uintptr_t continuation_callback = 0x76f7f0;
inline constexpr std::array<unsigned char, 40> continuation_callback_prefix{
0x4c,0x8b,0xdc,0x49,0x89,0x5b,0x20,0x57,0x48,0x83,0xec,0x60,0x48,0x8b,0x02,0x4d,
0x8b,0xc8,0x49,0x89,0x43,0xb8,0x4d,0x8d,0x43,0xb8,0x48,0x8b,0x42,0x08,0x48,0x8b,
0xfa,0x49,0x89,0x43,0xc0,0x48,0x8b,0xd9};
// Promise capture inside check_entitlements.
inline constexpr std::uintptr_t entry_promise_capture = 0x774665;
inline constexpr std::array<unsigned char, 48> entry_promise_capture_prefix{
0x48,0x8b,0x54,0x24,0x58,0x48,0x8d,0x4c,0x24,0x38,0xe8,0xcc,0x0e,0x66,0x03,0x48,
0x8d,0x05,0x25,0xb3,0xff,0xff,0x48,0x89,0x45,0xf0,0x48,0x8d,0x54,0x24,0x38,0x48,
0x8d,0x05,0x65,0xb1,0xff,0xff,0x48,0x8d,0x4d,0xe0,0x48,0x89,0x45,0xf8,0xe8,0xa8};
// check_entitlements' call to the request builder and its return address.
inline constexpr std::uintptr_t entry_builder_call = 0x7746fe;
inline constexpr std::uintptr_t entry_builder_return = 0x77470f;
inline constexpr std::array<unsigned char, 17> entry_builder_call_prefix{
0x4c,0x8d,0x45,0xa0,0x49,0x8b,0xcd,0x48,0x8d,0x54,0x24,0x60,0xe8,0xc1,0xcb,0xc1,
0xff};
// The continuation callback's call to the result reducer and its return address.
inline constexpr std::uintptr_t callback_reducer_call = 0x76f846;
inline constexpr std::uintptr_t callback_reducer_return = 0x76f858;
inline constexpr std::array<unsigned char, 18> callback_reducer_call_prefix{
0x49,0x8b,0xc9,0xc5,0xf8,0x11,0x44,0x24,0x40,0x89,0x44,0x24,0x50,0xe8,0xa8,0xe0,
0xff,0xff};
}
+369
View File
@@ -0,0 +1,369 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
namespace dingosdk::game::build::v20260929::entity_pages {
// EntityBus TransformSpace and property-space snapshots have 128 page pointers.
// Slot 131072 formerly indexed pointer 128, overwriting snapshot bookkeeping.
// Enlarge the leaf pages without changing owners, snapshots, IDs or their ABI.
inline constexpr std::size_t page_count = 128;
inline constexpr unsigned page_shift = 13;
inline constexpr std::size_t page_entries = std::size_t{1} << page_shift;
inline constexpr std::size_t page_bytes = page_entries * sizeof(std::uint64_t);
inline constexpr std::size_t capacity = page_count * page_entries;
// The native render-instance pool keeps 512 page pointers and stable direct
// object addresses. Grow its 464-byte-record pages without changing that ABI.
inline constexpr unsigned render_instance_page_shift = 11;
inline constexpr std::size_t render_instance_page_bytes = (std::size_t{1} << render_instance_page_shift) * 0x1d0;
static_assert((512u << render_instance_page_shift) == capacity);
static_assert(render_instance_page_bytes == 0xe8000);
// The cull-ID list and this separate monotonic bounds arena must both fit the
// city. Retail config can override GridSize after Game.cfg has been processed.
// Preserve native allocation/reset ownership and any larger configured value.
inline constexpr std::size_t spatial_arena_min_bytes = 128 * 1024 * 1024;
static_assert(capacity == 1'048'576 && page_bytes == 65'536);
enum class Kind {
index_shift, page_bytes, page_entries, spatial_arena_bytes,
spatial_bucket_layout, spatial_bucket_stride, pending_update_pages,
render_instance_pages
};
struct Patch {
std::uintptr_t rva;
Kind kind;
std::size_t size;
std::array<std::uint8_t, 15> expected{};
std::array<std::uint8_t, 15> replacement{};
};
template<std::size_t N>
consteval Patch patch(std::uintptr_t rva, Kind kind,
const char (&before)[N], const char (&after)[N]) {
static_assert(N % 2 == 1 && N > 1 && N <= 31);
Patch result{rva, kind, (N - 1) / 2};
const auto nibble = [](char c) -> unsigned {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
throw "Invalid entity-page instruction contract";
};
for (std::size_t i = 0; i < result.size; ++i) {
result.expected[i] = static_cast<std::uint8_t>((nibble(before[i*2]) << 4) | nibble(before[i*2+1]));
result.replacement[i] = static_cast<std::uint8_t>((nibble(after[i*2]) << 4) | nibble(after[i*2+1]));
}
return result;
}
// Reviewed complete instructions for the supported executable. In addition to
// insertion/lookup, include iteration skips, copy-on-write and snapshot repair.
// The separate 24-byte update queues are covered by pending_updates.h.
inline constexpr Patch patches[]{
// Sized destruction, insertion, allocation and the losing CAS allocation.
// The native page-index assertion remains enabled.
patch(0x050cdf6c, Kind::render_instance_pages, "41b800a00300", "41b800800e00"),
patch(0x050e23f6, Kind::render_instance_pages, "c1eb09", "c1eb0b"),
patch(0x050e2422, Kind::render_instance_pages, "ba00a00300", "ba00800e00"),
patch(0x050e244d, Kind::render_instance_pages, "41b800a00300", "41b800800e00"),
patch(0x050e245b, Kind::render_instance_pages, "c1e309", "c1e30b"),
patch(0x0489ee29, Kind::spatial_arena_bytes, "bb00001000", "bb00000008"),
patch(0x048a5e2c, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048a5e31, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x048a5fec, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048a6015, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x048b2713, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x048b271c, Kind::index_shift, "41c1e00a", "41c1e00d"),
patch(0x048b27b6, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048b27c4, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x048b292a, Kind::index_shift, "41c1ea0a", "41c1ea0d"),
patch(0x048b2935, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x048b3816, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x048b3823, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x048b5112, Kind::index_shift, "41c1e80a", "41c1e80d"),
patch(0x048b5122, Kind::index_shift, "c1e00a", "c1e00d"),
patch(0x048b51a2, Kind::index_shift, "41c1e80a", "41c1e80d"),
patch(0x048b51b2, Kind::index_shift, "c1e00a", "c1e00d"),
patch(0x048b52d6, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048b52dd, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x048b53cc, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048b53d3, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x048b5692, Kind::index_shift, "41c1e80a", "41c1e80d"),
patch(0x048b56a2, Kind::index_shift, "c1e00a", "c1e00d"),
patch(0x048b57a9, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048b57b0, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x048b58b6, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048b58bd, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x048b7ed4, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x048b7ef5, Kind::index_shift, "41c1e00a", "41c1e00d"),
patch(0x048b8530, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048b8556, Kind::index_shift, "41c1e20a", "41c1e20d"),
patch(0x048b9879, Kind::page_entries, "81c600040000", "81c600200000"),
patch(0x048b9881, Kind::page_bytes, "488daf00200000", "488daf00000100"),
patch(0x048b99d8, Kind::page_entries, "4181c400040000", "4181c400200000"),
patch(0x048b99e4, Kind::page_bytes, "498dbe00200000", "498dbe00000100"),
patch(0x048bb417, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x048bb41f, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x048bb76a, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048bb771, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x048bb83a, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048bb841, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x048bb8fa, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048bb901, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x048bb9b9, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048bb9c0, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x048bba89, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048bba90, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x048bbb89, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048bbb90, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x048bbc4b, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048bbc52, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x048bbd39, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048bbd40, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x048bbf26, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048bbf2d, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x048bd05e, Kind::page_bytes, "b900200000", "b900000100"),
patch(0x048bd06d, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x048bd42b, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048bd432, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x048bd516, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048bd51d, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x048c019e, Kind::page_entries, "4181c600040000", "4181c600200000"),
patch(0x048c01a7, Kind::page_bytes, "4c8da500200000", "4c8da500000100"),
patch(0x048c024e, Kind::page_entries, "4181c400040000", "4181c400200000"),
patch(0x048c025a, Kind::page_bytes, "4c8dad00200000", "4c8dad00000100"),
patch(0x048c069c, Kind::page_entries, "81c500040000", "81c500200000"),
patch(0x048c06a4, Kind::page_bytes, "4c8db600200000", "4c8db600000100"),
patch(0x048c07f8, Kind::page_entries, "4181c400040000", "4181c400200000"),
patch(0x048c0804, Kind::page_bytes, "498dbe00200000", "498dbe00000100"),
patch(0x048c3942, Kind::page_bytes, "b900200000", "b900000100"),
patch(0x048c394f, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x048c39bb, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x048c39c0, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x048c4fbd, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x048c4fc5, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x048c50cf, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x048c50d6, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x048c5d01, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x048c5d06, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x048c5e1c, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x048c5e53, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x048c5e8d, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x048c5ec7, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x048c5f01, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x048c5f3b, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x048c5f75, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x048c5faf, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x048c703c, Kind::index_shift, "c1ea0a", "c1ea0d"),
patch(0x048c7049, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x04ea3670, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04ea3692, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x04ea3b1d, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04ea3b42, Kind::index_shift, "c1e00a", "c1e00d"),
patch(0x04ea3bd0, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04ea3bf8, Kind::index_shift, "c1e00a", "c1e00d"),
patch(0x04ec12e3, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04ec12eb, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x04ec13c7, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04ec13d5, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04ec2b13, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04ec2b1b, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x04ec2bf7, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04ec2c05, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04ec2dc6, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04ec2dce, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x04ec2eb7, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04ec2ec5, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04ed56e9, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04ed56ef, Kind::index_shift, "41c1e20a", "41c1e20d"),
patch(0x04ed7296, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04ed72bb, Kind::index_shift, "41c1e20a", "41c1e20d"),
patch(0x04ed78d3, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04ed78f8, Kind::index_shift, "41c1e20a", "41c1e20d"),
patch(0x04edde6d, Kind::page_entries, "81c500040000", "81c500200000"),
patch(0x04edde78, Kind::page_bytes, "4c8dbf00200000", "4c8dbf00000100"),
patch(0x04eddff8, Kind::page_entries, "4181c400040000", "4181c400200000"),
patch(0x04ede004, Kind::page_bytes, "498dbe00200000", "498dbe00000100"),
patch(0x04edf33d, Kind::page_entries, "81c500040000", "81c500200000"),
patch(0x04edf348, Kind::page_bytes, "4c8dbf00200000", "4c8dbf00000100"),
patch(0x04edf508, Kind::page_entries, "4181c400040000", "4181c400200000"),
patch(0x04edf514, Kind::page_bytes, "498dbe00200000", "498dbe00000100"),
patch(0x04edf719, Kind::page_entries, "81c600040000", "81c600200000"),
patch(0x04edf724, Kind::page_bytes, "488daf00200000", "488daf00000100"),
patch(0x04edf888, Kind::page_entries, "4181c400040000", "4181c400200000"),
patch(0x04edf894, Kind::page_bytes, "498dbe00200000", "498dbe00000100"),
patch(0x04ee3b47, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04ee3b4f, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x04ee4c77, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04ee4c7f, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x04ee4f47, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04ee4f4f, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x04ee7daa, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04ee7db9, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x04eeb19e, Kind::page_bytes, "b900200000", "b900000100"),
patch(0x04eeb1ad, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04eeba26, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04eeba2d, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04eec74e, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04eec755, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04eec8ac, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04eec8b3, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04eecd45, Kind::index_shift, "41c1e80a", "41c1e80d"),
patch(0x04eecd58, Kind::index_shift, "c1e00a", "c1e00d"),
patch(0x04eece4d, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04eece6e, Kind::index_shift, "41c1e00a", "41c1e00d"),
patch(0x04eecf6d, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04eecf74, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04eed0a0, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04eed0a7, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04eed221, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04eed228, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04eed31c, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04eed323, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04eed468, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04eed474, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04eed6a0, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04eed6a7, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04eed869, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04eed870, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04eed9f4, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04eed9fb, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04eee208, Kind::index_shift, "41c1e80a", "41c1e80d"),
patch(0x04eee21b, Kind::index_shift, "c1e00a", "c1e00d"),
patch(0x04eee292, Kind::index_shift, "41c1e80a", "41c1e80d"),
patch(0x04eee2a5, Kind::index_shift, "c1e00a", "c1e00d"),
patch(0x04eee328, Kind::index_shift, "41c1e80a", "41c1e80d"),
patch(0x04eee33b, Kind::index_shift, "c1e00a", "c1e00d"),
patch(0x04eee3b2, Kind::index_shift, "41c1e80a", "41c1e80d"),
patch(0x04eee3c5, Kind::index_shift, "c1e00a", "c1e00d"),
patch(0x04eee432, Kind::index_shift, "41c1e80a", "41c1e80d"),
patch(0x04eee445, Kind::index_shift, "c1e00a", "c1e00d"),
patch(0x04eee4a2, Kind::index_shift, "41c1e80a", "41c1e80d"),
patch(0x04eee4b5, Kind::index_shift, "c1e00a", "c1e00d"),
patch(0x04eee60a, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04eee62b, Kind::index_shift, "41c1e00a", "41c1e00d"),
patch(0x04eee72f, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04eee736, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04eee89f, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04eee8a6, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04eeea0a, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04eeea11, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04eeecbe, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04eeecc5, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04eeede9, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04eeedf0, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04eef06f, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04eef076, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04eef15d, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04eef164, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04eef214, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04eef21b, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04ef0113, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04ef011a, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04ef5deb, Kind::page_entries, "4181c700040000", "4181c700200000"),
patch(0x04ef5df7, Kind::page_bytes, "498db600200000", "498db600000100"),
patch(0x04ef5f89, Kind::page_entries, "4181c700040000", "4181c700200000"),
patch(0x04ef5f95, Kind::page_bytes, "498db600200000", "498db600000100"),
patch(0x04ef60ae, Kind::page_entries, "4181c500040000", "4181c500200000"),
patch(0x04ef60ba, Kind::page_bytes, "4d8dbe00200000", "4d8dbe00000100"),
patch(0x04efc57b, Kind::page_entries, "4181c700040000", "4181c700200000"),
patch(0x04efc587, Kind::page_bytes, "498db600200000", "498db600000100"),
patch(0x04efc729, Kind::page_entries, "4181c700040000", "4181c700200000"),
patch(0x04efc735, Kind::page_bytes, "498db600200000", "498db600000100"),
patch(0x04efc858, Kind::page_entries, "4181c500040000", "4181c500200000"),
patch(0x04efc864, Kind::page_bytes, "4d8dbe00200000", "4d8dbe00000100"),
patch(0x04f097dc, Kind::page_entries, "4181c600040000", "4181c600200000"),
patch(0x04f097e5, Kind::page_bytes, "4c8da500200000", "4c8da500000100"),
patch(0x04f0988f, Kind::page_entries, "4181c400040000", "4181c400200000"),
patch(0x04f0989b, Kind::page_bytes, "4c8dad00200000", "4c8dad00000100"),
patch(0x04f0b69c, Kind::page_entries, "81c500040000", "81c500200000"),
patch(0x04f0b6a7, Kind::page_bytes, "4c8db600200000", "4c8db600000100"),
patch(0x04f0b828, Kind::page_entries, "4181c400040000", "4181c400200000"),
patch(0x04f0b834, Kind::page_bytes, "498dbe00200000", "498dbe00000100"),
patch(0x04f16e2d, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04f16e34, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04f16ee4, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04f16eeb, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04f1700c, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04f17013, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04f172d4, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04f172db, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04f17618, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04f1761f, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04f19462, Kind::page_bytes, "b900200000", "b900000100"),
patch(0x04f1946f, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f194db, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04f194e0, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x04f19ca2, Kind::page_bytes, "b900200000", "b900000100"),
patch(0x04f19caf, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f19d1b, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04f19d20, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x04f19f22, Kind::page_bytes, "b900200000", "b900000100"),
patch(0x04f19f2f, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f19f9b, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04f19fa0, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x04f1e856, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04f1e863, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x04f1e8f0, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04f1e8f8, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x04f1ea31, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04f1ea39, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04f216fd, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04f2170a, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x04f21796, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04f2179e, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x04f218d6, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04f218de, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04f21f09, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04f21f16, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x04f21fa2, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04f21faa, Kind::index_shift, "c1e20a", "c1e20d"),
patch(0x04f220e8, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04f220f0, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04f2b166, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04f2b16b, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04f2b2e4, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04f2b2ec, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04f2b38c, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2b3c3, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2b3fd, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2b437, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2b471, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2b4ab, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2b4e5, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2b51f, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2e6e6, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04f2e6eb, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04f2e877, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04f2e87f, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04f2e92c, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2e963, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2e99d, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2e9d7, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2ea11, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2ea4b, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2ea85, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2eabf, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2ec86, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04f2ec8b, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04f2ee27, Kind::index_shift, "c1e90a", "c1e90d"),
patch(0x04f2ee2f, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04f2eedc, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2ef13, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2ef4d, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2ef87, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2efc1, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2effb, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2f035, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f2f06f, Kind::page_bytes, "41b800200000", "41b800000100"),
patch(0x04f34fea, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04f34ff1, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04f35b60, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04f35b66, Kind::index_shift, "41c1e00a", "41c1e00d"),
patch(0x04f38418, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04f3841f, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04f38958, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04f3895f, Kind::index_shift, "c1e10a", "c1e10d"),
patch(0x04f38a78, Kind::index_shift, "c1e80a", "c1e80d"),
patch(0x04f38a7f, Kind::index_shift, "c1e10a", "c1e10d"),
};
} // namespace dingosdk::game::build::v20260929::entity_pages
@@ -0,0 +1,91 @@
#pragma once
#include "engine.h"
#include <array>
#include <cstdint>
// Fixed-stop entitlement provider (CheckEntitlements replacement).
namespace dingosdk::game::build::v20260929::fixed_stop_entitlement {
// CheckEntitlements entry.
inline constexpr std::uintptr_t check_entitlements = engine::check_entitlements;
inline constexpr std::array<unsigned char, 40> check_entitlements_prefix{
0x40,0x55,0x57,0x41,0x56,0x41,0x57,0x48,0x8d,0x6c,0x24,0xd8,0x48,0x81,0xec,0x28,
0x01,0x00,0x00,0x48,0x8b,0x05,0x66,0x00,0xa5,0x06,0x48,0x33,0xc4,0x48,0x89,0x45,
0x08,0x4c,0x8b,0xf1,0x4c,0x89,0x44,0x24};
// Group enumeration inside CheckEntitlements: the exact entry sequence that
// reserves the eID list, reads group +0x88/+0x90, skips null members, obtains
// the native eID key, and calls the member lookup.
inline constexpr std::uintptr_t group_enumeration = 0x77458f;
inline constexpr std::array<unsigned char, 76> group_enumeration_prefix{
0x48,0x8d,0x4c,0x24,0x60,0x48,0x8b,0x93,0x90,0x00,0x00,0x00,0x48,0x2b,0x93,0x88,
0x00,0x00,0x00,0x48,0xc1,0xfa,0x04,0xe8,0xd5,0x62,0xae,0xff,0x4c,0x8b,0xbb,0x90,
0x00,0x00,0x00,0x48,0x8b,0xb3,0x88,0x00,0x00,0x00,0x49,0x3b,0xf7,0x74,0x7c,0x66,
0x90,0x48,0x8b,0x5e,0x08,0x48,0x85,0xdb,0x74,0x68,0x33,0xc9,0xe8,0x70,0x50,0xfd,
0xff,0x48,0x8b,0xd0,0x48,0x8b,0xcb,0xe8,0xf5,0x05,0xf5,0x00};
// Result vector constructor.
inline constexpr std::uintptr_t result_vector_init = 0x58b1cb0;
inline constexpr std::array<unsigned char, 37> result_vector_init_prefix{
0x40,0x53,0x48,0x83,0xec,0x20,0x48,0x8b,0xd9,0xe8,0x42,0x9e,0x51,0xfe,0x48,0x89,
0x03,0x48,0x8b,0xc3,0xc7,0x43,0x08,0x00,0x00,0x00,0x00,0xc6,0x43,0x0c,0x00,0x48,
0x83,0xc4,0x20,0x5b,0xc3};
// Native array allocation.
inline constexpr std::uintptr_t array_allocate = engine::array_allocate;
inline constexpr std::array<unsigned char, 61> array_allocate_prefix{
0x48,0x89,0x5c,0x24,0x08,0x57,0x48,0x83,0xec,0x20,0x49,0x8b,0xc1,0x49,0x8b,0xd8,
0x48,0x8b,0xfa,0x4d,0x85,0xc9,0x75,0x07,0xb2,0x01,0xe8,0xe1,0x26,0x7d,0xfd,0x44,
0x0f,0xb6,0x4c,0x24,0x48,0x4c,0x8b,0xc3,0x48,0x8b,0xd7,0x48,0x8b,0xc8,0x48,0x8b,
0x5c,0x24,0x30,0x48,0x83,0xc4,0x20,0x5f,0xe9,0xc3,0x7a,0x7b,0xfd};
// Result element constructor.
inline constexpr std::uintptr_t result_element_init = 0x58b1c70;
inline constexpr std::array<unsigned char, 50> result_element_init_prefix{
0x40,0x53,0x48,0x83,0xec,0x20,0x48,0x8b,0xd9,0xe8,0x82,0x9e,0x51,0xfe,0x48,0x89,
0x03,0x48,0x8d,0x05,0x7c,0xca,0xa4,0x01,0x48,0x89,0x43,0x08,0xe8,0x6f,0x9e,0x51,
0xfe,0x48,0x89,0x43,0x10,0x48,0x8b,0xc3,0xc6,0x43,0x18,0x00,0x48,0x83,0xc4,0x20,
0x5b,0xc3};
// Expected leading bytes of engine::string_assign.
inline constexpr std::array<unsigned char, 40> assign_string_prefix{
0x48,0x89,0x5c,0x24,0x10,0x48,0x89,0x6c,0x24,0x18,0x56,0x57,0x41,0x56,0x48,0x83,
0xec,0x20,0x48,0x8b,0x19,0x48,0x8d,0x2d,0xc8,0x70,0xd7,0x05,0x41,0x8b,0xf0,0x4c,
0x8b,0xf2,0x48,0x8b,0xf9,0x48,0x85,0xdb};
// Error constructor.
inline constexpr std::uintptr_t construct_error = engine::construct_error;
inline constexpr std::array<unsigned char, 40> construct_error_prefix{
0x48,0x89,0x5c,0x24,0x08,0x48,0x89,0x74,0x24,0x10,0x57,0x48,0x83,0xec,0x20,0x48,
0x8b,0xf1,0x49,0x8b,0xd8,0x49,0x8b,0xc8,0x8b,0xfa,0xe8,0xcd,0xfb,0xc2,0x04,0x44,
0x8b,0xc0,0x48,0x8b,0xd3,0x48,0x8b,0xce};
// Promise completion.
inline constexpr std::uintptr_t complete_promise = engine::complete_promise;
inline constexpr std::array<unsigned char, 40> complete_promise_prefix{
0x48,0x83,0xec,0x48,0x4c,0x8b,0xd2,0x48,0x8b,0x11,0x48,0x8b,0x02,0x4c,0x8b,0x48,
0x40,0x4d,0x85,0xc9,0x74,0x14,0x80,0x78,0x68,0x00,0x75,0x0e,0x49,0x8b,0xd0,0x49,
0x8b,0xca,0x41,0xff,0xd1,0x48,0x83,0xc4};
// Result vector destructor.
inline constexpr std::uintptr_t destroy_vector = 0x76c370;
inline constexpr std::array<unsigned char, 40> destroy_vector_prefix{
0x48,0x89,0x74,0x24,0x18,0x41,0x56,0x48,0x83,0xec,0x20,0x48,0x8b,0x31,0x4c,0x8b,
0xf1,0xe8,0x7a,0xf7,0x65,0x03,0x48,0x3b,0xf0,0x74,0x79,0x49,0x8b,0x06,0x48,0x89,
0x5c,0x24,0x30,0x48,0x8b,0xde,0x48,0x89};
// Expected leading bytes of engine::native_text_release.
inline constexpr std::array<unsigned char, 40> destroy_string_prefix{
0x48,0x8b,0x09,0x48,0x8d,0x05,0xaa,0x86,0xd7,0x05,0x48,0x3b,0xc8,0x0f,0x85,0xbd,
0x9b,0xff,0xff,0xc3,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,
0x48,0x89,0x5c,0x24,0x08,0x57,0x48,0x83};
// Future assignment.
inline constexpr std::uintptr_t assign_future = engine::assign_future;
inline constexpr std::array<unsigned char, 34> assign_future_prefix{
0x48,0x85,0xd2,0x74,0x16,0xb8,0x00,0x20,0x00,0x00,0x66,0x85,0x42,0x14,0x48,0x8b,
0xc1,0x75,0x0b,0xf0,0xff,0x42,0x10,0x48,0x89,0x11,0xc3,0x48,0x8b,0xc1,0x48,0x89,
0x11,0xc3};
// Native eID key getter.
inline constexpr std::uintptr_t eid_key = 0x749640;
inline constexpr std::array<unsigned char, 21> eid_key_prefix{
0x48,0x8d,0x05,0x65,0xfd,0x98,0x05,0x48,0x89,0x44,0x24,0x10,0x48,0x63,0xc1,0x48,
0x8b,0x44,0xc4,0x10,0xc3};
// Group member eID lookup.
inline constexpr std::uintptr_t find_member_eid = 0x16c4bd0;
inline constexpr std::array<unsigned char, 40> find_member_eid_prefix{
0x40,0x53,0x55,0x56,0x57,0x48,0x83,0xec,0x48,0x48,0x8d,0x71,0x60,0xc6,0x44,0x24,
0x20,0x00,0x48,0x8b,0xca,0xc6,0x44,0x24,0x2f,0x0f,0x48,0x8b,0xea,0x48,0xc7,0x44,
0x24,0x30,0x00,0x00,0x00,0x00,0xe8,0x21};
// Pointer to the client game manager.
inline constexpr std::uintptr_t client_game_manager = engine::client_game_manager;
}
@@ -0,0 +1,56 @@
#pragma once
#include <array>
#include <cstdint>
namespace dingosdk::game::build::v20260929::gameplay_settings {
// Dedicated fast-travel getter: independent evidence for the
// FastTravelPointsEnabled byte, and its expected first bytes.
inline constexpr std::uintptr_t fast_travel_getter = 0x87b640;
inline constexpr std::array<unsigned char, 32> fast_travel_getter_prefix{0x48,0x83,0xec,0x28,0x48,0x8b,0x0d,0x95,0x76,0xc0,0x06,0x48,0x8d,0x15,0xae,0x23,0x9e,0x06,0xe8,0x99,0xb8,0x05,0x01,0x48,0x85,0xc0,0x74,0x0d,0x80,0x78,0x29,0x00};
// Settings sections: TypeInfo handle (settings lookup key) and vtable.
// DelMarGameSettings is engine::game_settings_handle / engine::game_settings_vtable.
inline constexpr std::uintptr_t activity_settings_handle = 0x725cc18;
inline constexpr std::uintptr_t activity_settings_vtable = 0x6146340;
inline constexpr std::uintptr_t advance_settings_handle = 0x725da00;
inline constexpr std::uintptr_t advance_settings_vtable = 0x61464a0;
inline constexpr std::uintptr_t player_progression_settings_handle = 0x7243cb0;
inline constexpr std::uintptr_t player_progression_settings_vtable = 0x612ebb0;
inline constexpr std::uintptr_t ui_settings_handle = 0x7257fe8;
inline constexpr std::uintptr_t ui_settings_vtable = 0x6141688;
inline constexpr std::uintptr_t progression_settings_handle = 0x725d5f0;
inline constexpr std::uintptr_t progression_settings_vtable = 0x61463e8;
// Reflection member records ({name hash, offset, type}) of the overridden
// boolean fields, named after the field.
// DingoActivitySettings
inline constexpr std::uintptr_t force_all_activities_enabled_member = 0x82ff060;
inline constexpr std::uintptr_t use_dev_enabled_activities_member = 0x82ff090;
inline constexpr std::uintptr_t enable_quest_system_member = 0x82ff120;
inline constexpr std::uintptr_t enable_action_system_member = 0x82ff240;
inline constexpr std::uintptr_t enable_narrative_member = 0x82ff318;
inline constexpr std::uintptr_t enable_activity_spawned_waypoints_member = 0x82ff4e0;
inline constexpr std::uintptr_t enable_quest_system_debug_member = 0x82ff138;
inline constexpr std::uintptr_t force_use_mock_quest_member = 0x82ff1f8;
inline constexpr std::uintptr_t show_local_quests_member = 0x82ff288;
inline constexpr std::uintptr_t auto_claim_quests_member = 0x82ff2d0;
inline constexpr std::uintptr_t disable_quest_order_filtering_member = 0x82ff330;
inline constexpr std::uintptr_t dev_workflows_enabled_member = 0x82ff498;
// DingoAdvanceSettings
inline constexpr std::uintptr_t fast_travel_points_enabled_member = 0x82fea00;
inline constexpr std::uintptr_t board_wear_enabled_member = 0x82fe9b8;
inline constexpr std::uintptr_t player_vs_player_collision_enabled_member = 0x82fe9d0;
// PlayerProgressionSystemSettings
inline constexpr std::uintptr_t enable_player_progression_member = 0x82ed7e8;
inline constexpr std::uintptr_t enable_graphs_member = 0x82ed800;
// DelMarGameSettings
// DelMarUISettings
inline constexpr std::uintptr_t enable_dev_only_menu_member = 0x82fa6f8;
inline constexpr std::uintptr_t enable_content_browser_ui_member = 0x82fa710;
inline constexpr std::uintptr_t enable_legacy_progression_menu_member = 0x82fa728;
inline constexpr std::uintptr_t enable_new_map_member = 0x82fa8f0;
inline constexpr std::uintptr_t enable_old_buildkit_object_browser_member = 0x82faa10;
// DingoProgressionSettings
inline constexpr std::uintptr_t enable_neighborhood_rank_member = 0x82fe0d0;
inline constexpr std::uintptr_t enable_competitive_rank_member = 0x82fe0e8;
}
@@ -0,0 +1,20 @@
#pragma once
#include <array>
#include <cstdint>
namespace dingosdk::game::build::v20260929::global_offline {
// Shared OnlineEnabled leaf hooked by the global offline experiment.
inline constexpr std::uintptr_t online_enabled = 0x3b8830;
// MOV AL,1; RET followed by the inspected INT3 padding.
inline constexpr std::array<unsigned char, 16> online_enabled_prefix{
0xb0,0x01,0xc3,0xcc,0xcc,0xcc,0xcc,0xcc,
0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc,0xcc};
// Return addresses of the direct OnlineEnabled calls the experiment answers.
inline constexpr std::uintptr_t backend_predicate_return = 0x2830c4;
inline constexpr std::uintptr_t async_prepare_character_return = 0x7fe2f2;
inline constexpr std::uintptr_t character_readiness_return = 0x8070d5;
// Return address inside the reflected Flow getIsOnlineEnabled invocation adapter.
inline constexpr std::uintptr_t flow_adapter_return = 0x58e2abe;
// Post-spawn progression guard.
inline constexpr std::uintptr_t post_spawn_progression_return = 0x82ab99;
}
@@ -0,0 +1,10 @@
#pragma once
#include "engine.h"
#include <cstdint>
namespace dingosdk::game::build::v20260929::graph_throttle {
// What a loaded graph's delayed re-post slots are bound to before the throttle
// takes them over.
inline constexpr std::uintptr_t post_seconds_fast = engine::post_event_delayed_seconds;
inline constexpr std::uintptr_t post_frames_fast = engine::post_event_delayed_frames;
}
@@ -0,0 +1,29 @@
#pragma once
#include "Engine/Game/Build/fingerprint.h"
#include "engine.h"
namespace dingosdk::game::build::v20260929::graphics_controls {
// Filmic component constructor, destructor and property setter; the settings
// manager lookup; the video filter pass setup.
inline constexpr std::array<Fingerprint, 5> graphics_sites{{
{0x52bcc20, {0x40,0x53,0x55,0x56,0x57,0x41,0x56,0x48,0x81,0xec,0x00,0x01,0x00,0x00,0x44,0x0f,0xb7,0x05,0x1a,0x04,0x60,0x02,0x48,0x8b,0xd9,0x48,0x8d,0x4c,0x24,0x40,0x48,0x8b}},
{0x52b94a0, {0x48,0x89,0x5c,0x24,0x08,0x57,0x48,0x83,0xec,0x20,0x48,0x8d,0x05,0x77,0xc2,0x51,0x01,0x48,0x8b,0xf9,0x48,0x89,0x01,0x8b,0xda,0x48,0x83,0xc1,0x28,0xe8,0x4e,0x34}},
{0x52b9390, {0x48,0x89,0x74,0x24,0x18,0x57,0x48,0x83,0xec,0x20,0x4c,0x8b,0x4a,0x08,0x48,0x8b,0xf1,0x44,0x8b,0x02,0x48,0x8d,0x51,0x40,0x48,0x8b,0x49,0x08,0xe8,0x1f,0x2e,0xd1}},
{engine::settings_lookup, {0x48,0x89,0x5c,0x24,0x08,0x48,0x89,0x74,0x24,0x10,0x57,0x48,0x83,0xec,0x20,0x48,0x8d,0x59,0x70,0x48,0x8b,0xf9,0x48,0x8b,0xcb,0x48,0x8b,0xf2,0xe8,0x6f,0x8e,0xcb}},
{0x5e2d60, {0x48,0x89,0x5c,0x24,0x18,0x48,0x89,0x74,0x24,0x20,0x55,0x57,0x41,0x56,0x48,0x8d,0x6c,0x24,0xb9,0x48,0x81,0xec,0xd0,0x00,0x00,0x00,0x48,0x8b,0x01,0x48,0x8b,0xf1}},
}};
// DingoVideoFilterRenderPass and its module; the pass caches the video effect mask.
inline constexpr std::uintptr_t video_filter_pass_vtable = 0x60ca0d8;
inline constexpr std::uintptr_t video_filter_module_vtable = 0x60ca020;
// Filmic component vtable (destructor at +8, property setter at +0x18) and its data TypeInfo.
inline constexpr std::uintptr_t filmic_vtable = 0x67d57f8;
inline constexpr std::uintptr_t filmic_data_type = 0x78c6968;
// GlobalPostProcessSettings TypeInfo. The native renderer settings service
// (3bc72f0) caches PostProcess copies by its DdcDirtyVersion.
inline constexpr std::uintptr_t post_process_settings_type = 0x76f54e8;
// GlobalPostProcessSettings reflected field records.
inline constexpr std::uintptr_t film_grain_enable_field = 0x8407cb8;
inline constexpr std::uintptr_t vignette_enable_field = 0x8407b80;
inline constexpr std::uintptr_t ddc_dirty_version_field = 0x8408720;
}
@@ -0,0 +1,17 @@
#pragma once
#include <array>
#include <cstdint>
// Fallback labels for the TierManager graphics settings cache.
namespace dingosdk::game::build::v20260929::graphics_labels {
// TierManager settings cache rebuild, void(manager).
inline constexpr std::uintptr_t tier_rebuild = 0x5fed70;
inline constexpr std::array<unsigned char, 32> tier_rebuild_prefix{
0x40,0x55,0x53,0x56,0x57,0x41,0x54,0x41,0x55,0x41,0x56,0x41,0x57,0x48,0x8d,0x6c,
0x24,0xd8,0x48,0x81,0xec,0x28,0x01,0x00,0x00,0x48,0x8b,0x91,0xd0,0x01,0x00,0x00};
// TierManager vtable.
inline constexpr std::uintptr_t tier_manager_vtable = 0x6092548;
// Expected first bytes of engine::string_assign.
inline constexpr std::array<unsigned char, 18> string_assign_prefix{
0x48,0x89,0x5c,0x24,0x10,0x48,0x89,0x6c,0x24,0x18,0x56,0x57,0x41,0x56,0x48,0x83,0xec,0x20};
}
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <array>
#include <cstdint>
namespace dingosdk::game::build::v20260929::initfs {
// 16-byte InitFS AES key in the executable image.
inline constexpr std::uintptr_t key = 0x71901c8;
// Code in the InitFS decryptor that references the key.
inline constexpr std::uintptr_t decrypt_reference = 0x41bd5d9;
inline constexpr std::array<unsigned char, 18> decrypt_reference_prefix{
0xc5,0xf8,0x10,0x05,0xe7,0x2b,0xfd,0x02,0x8b,0x45,0x18,
0x48,0x8d,0x0d,0xdd,0x2b,0xfd,0x02};
}
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#include <cstdint>
// The engine's job system (worker loop 0x3221db0, run-until-done 0x3222210, job pop 0x3226a30).
// See analysis/sim-performance.md.
namespace dingosdk::game::build::v20260929::job_system {
// How long an idle worker keeps polling its schedulers and queues before it sleeps, in timestamp
// counter ticks. Computed once at job-system start (0x3227660) from spin_milliseconds; both
// loops read it live on every pass, so a new value applies at once.
inline constexpr std::uintptr_t spin_ticks = 0x760a670;
// The configured spin time in milliseconds (float, 0.25 in this build; job config + 0x1a0).
inline constexpr std::uintptr_t spin_milliseconds = 0x760aa10;
// The number of job threads started (int).
inline constexpr std::uintptr_t thread_count = 0x7607724;
}
@@ -0,0 +1,26 @@
#pragma once
#include "Engine/Game/Build/fingerprint.h"
namespace dingosdk::game::build::v20260929::level_loading {
// Supported SHA-256 fbce74d5e28ef525dbba2cb4adbebc13405bdbd88f31bc940bca45e4ae88b8f9.
// Client update writes client+0xc4 then calls void(client, next:u32, previous:u32).
// Check both the callback entry and the caller's argument/state-store sequence.
inline constexpr Fingerprint level_transition_contract{0x2e6bf40, {
0x48,0x89,0x5c,0x24,0x20,0x55,0x56,0x57,0x41,0x56,0x41,0x57,0x48,0x8d,0x6c,0x24,
0xf0,0x48,0x81,0xec,0x10,0x01,0x00,0x00,0x48,0x8b,0x05,0x61,0x84,0x35,0x04,0x48}};
inline constexpr Fingerprint level_transition_caller_contract{0x2e773ce, {
0x44,0x8b,0xc7,0x89,0xab,0xc4,0x00,0x00,0x00,0x8b,0xd5,0x48,0x8b,0xcb,0xe8,0x5f,
0x4b,0xff,0xff,0x48,0x8b,0x15,0x00,0x16,0x61,0x04,0xb9,0x20,0x00,0x00,0x00,0xe8}};
inline constexpr Fingerprint level_request_contract{0x2e722b0, {
0x89,0x54,0x24,0x10,0x48,0x83,0xec,0x28,0x4c,0x8b,0xc1,0x48,0x81,0xc1,0xe0,0x00,
0x00,0x00,0x83,0x79,0x40,0x00,0x74,0x11,0x49,0x8b,0x80,0xe8,0x00,0x00,0x00,0x39}};
// Native connection closes independently of the ReSkate Steam session.
inline constexpr Fingerprint connection_disconnect_contract{0x2e9f090, {
0x81,0xfa,0xcd,0xbe,0x2c,0x10,0x74,0x12,0x81,0xb9,0x24,0x01,0x00,0x00,0xcd,0xbe,
0x2c,0x10,0x75,0x06,0x89,0x91,0x24,0x01,0x00,0x00,0x8b,0x81,0x20,0x01,0x00,0x00}};
// RimeLoadScreen::selectInfo(this, boot): called before the next description is
// delivered during local root reloads. Return an authored RimeLoadScreenInfo.
inline constexpr Fingerprint loading_screen_select_contract{0x43d3d00, {
0x48,0x89,0x5c,0x24,0x08,0x57,0x48,0x83,0xec,0x20,0x48,0x83,0x79,0x40,0x00,0x48,
0x8b,0xd9,0x0f,0xb6,0xfa,0x75,0x0d,0x33,0xc0,0x48,0x8b,0x5c,0x24,0x30,0x48,0x83}};
}
+66
View File
@@ -0,0 +1,66 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
namespace dingosdk::game::build::v20260929::live_mods {
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 ch) -> unsigned {
if (ch >= '0' && ch <= '9') return static_cast<unsigned>(ch - '0');
if (ch >= 'a' && ch <= 'f') return static_cast<unsigned>(ch - 'a' + 10);
throw "Invalid native contract hex";
};
for (std::size_t index = 0; index < result.size(); ++index)
result[index] = static_cast<std::uint8_t>((nibble(hex[index * 2]) << 4) | nibble(hex[index * 2 + 1]));
return result;
}
// Superbundle registry (the layer set's +0x58). register(registry, name)
// creates and indexes a superbundle the way layout.toc's superBundles list
// does at launch; find(registry, name, prefixed) looks one up by name; attach
// (superbundle, 0, install chunk, 1) lists it under an install chunk, which is
// what lets its bundles be located. All run under the layout manager's lock.
inline constexpr std::uintptr_t register_superbundle = 0x41de0e0;
inline constexpr auto register_superbundle_prefix = hex_bytes("48895c241048897424185557415441564157488bec4883ec");
inline constexpr std::uintptr_t find_superbundle = 0x41e5cb0;
inline constexpr auto find_superbundle_prefix = hex_bytes("48895c24184889742420574881ecd0000000");
inline constexpr std::uintptr_t attach_superbundle = 0x41dfb70;
inline constexpr auto attach_superbundle_prefix = hex_bytes("48895c242056574155415641574883ec30488b4120488bf1");
// Object layout.
inline constexpr std::uintptr_t manager_lock = 0x130; // CRITICAL_SECTION on the layout manager
inline constexpr std::uintptr_t layers_registry = 0x58; // layer set -> superbundle registry
inline constexpr std::uintptr_t registry_manifest = 0x28; // registry -> install manifest
inline constexpr std::uintptr_t manifest_chunks = 0x28; // install manifest -> chunk pointer vector (begin, end)
inline constexpr std::uintptr_t chunk_name = 0x100; // install chunk -> name string
// A registered superbundle's mounts (vector of pointers at +0x30), each holding
// the TOC every data layer supplied for it (vector of 0x28-byte entries at
// +0x50, top layer first). An entry keeps the TOC file object at +0x10 and the
// start of its data (past the 0x22c-byte file header) at +0x18 and +0x20; the
// file object owns the whole file at +0x28, its size at +0x30 and +0x38.
// Bundles are looked up in that data when they load, so a TOC rewritten on
// disk can be handed to the game by swapping these pointers.
inline constexpr std::uintptr_t superbundle_mounts = 0x30;
inline constexpr std::uintptr_t mount_tocs = 0x50;
inline constexpr std::size_t toc_entry_size = 0x28;
inline constexpr std::uintptr_t toc_entry_file = 0x10;
inline constexpr std::uintptr_t toc_entry_data = 0x18;
inline constexpr std::uintptr_t toc_file_bytes = 0x28;
inline constexpr std::uintptr_t toc_file_size = 0x30;
inline constexpr std::size_t toc_header = 0x22c;
// Converts a TOC's data from the file's big-endian words to native order, in
// place (data = file + toc_header); the loader runs it on every TOC it mounts.
inline constexpr std::uintptr_t prepare_toc = 0x41f8ed0;
inline constexpr auto prepare_toc_prefix = hex_bytes("4883ec28c5fa6f01c5fa6f15");
// The root level's LevelDescriptionAsset, loaded from globals at launch only. Its
// OnDemandBundles array ({char* SuperBundle; u32 BundleHash; pad} per entry,
// element count in the u32 before the first element) is how the root finds a
// map's bundle; a map added while the game runs is appended to it in memory.
inline constexpr char root_description[] = "levels/game/dingolevel_root/dingolevel_root/description";
inline constexpr std::uintptr_t description_on_demand = 0x40;
inline constexpr std::size_t on_demand_entry_size = 0x10;
}
@@ -0,0 +1,12 @@
#pragma once
#include <cstdint>
// The select hook itself is level_loading::loading_screen_select_contract.
namespace dingosdk::game::build::v20260929::loading_screen {
// RimeLoadScreen vtable.
inline constexpr std::uintptr_t screen_vtable = 0x65a75f0;
// RimeLoadScreenConfig TypeInfo.
inline constexpr std::uintptr_t config_type = 0x7767300;
// RimeLoadScreenInfo TypeInfo.
inline constexpr std::uintptr_t info_type = 0x77671e8;
}
@@ -0,0 +1,29 @@
#pragma once
#include <cstdint>
// Local challenge catalog and completion delivery. Related native code: the
// real activity catalog's anonymous model allocation 1417e18c0; progress
// hydration 140735900 (uses GetModelField); OnAmpActivityEndRecieved_Global
// sender 1406de170 (rejects an empty EID); 1415b4d90 places a graph's page
// table after its aligned frame size; the criterion value copy constructor
// 14591b5e0 treats its array fields as reference-counted ValueRefs.
namespace dingosdk::game::build::v20260929::local_challenge {
// Challenge (activity) owner system.
inline constexpr std::uintptr_t challenge_owner_vtable = 0x6089158;
// Pointer to the game context that owns the challenge system.
inline constexpr std::uintptr_t challenge_context = 0x73c7cb0;
// uint32 registered offset of the challenge owner in that context.
inline constexpr std::uintptr_t challenge_owner_offset = 0x7201838;
// uint16 reflected sizes of ChallengeCounts and ActivityData.
inline constexpr std::uintptr_t challenge_counts_size = 0x7f15396;
inline constexpr std::uintptr_t activity_data_size = 0x7f16196;
// Reflected ActivityData criterion-group field record (hash 0x571954a7, +0x30, UInt64).
inline constexpr std::uintptr_t activity_criteria_field = 0x82dd3c0;
// Model TypeInfos of the challenge catalog.
inline constexpr std::uintptr_t activity_data_type = 0x7223880;
inline constexpr std::uintptr_t activity_kind_type = 0x72238e8;
inline constexpr std::uintptr_t criterion_group_type = 0x7223940;
inline constexpr std::uintptr_t criterion_type = 0x72239f0;
// ActivityListUpdated event type.
inline constexpr std::uintptr_t activity_list_updated_type = 0x7223fe8;
}
@@ -0,0 +1,21 @@
#pragma once
#include <cstdint>
// Retail OwnableData catalog bridge. Related native code: OwnableData decoder
// 141e2e350 (tag 0xba, field 23 -> rarityId +160); protobuf reader constructor
// 1420c8450; trivial CoreAllocator adapter 1401f4230; inventory manager
// constructor 14094dc10 with its normal subscriptions 1406bc560; 140695b00
// stores the borrowed OwnableData pointer at +0x1d0; default-owned
// subscription callback 1406a0c30 -> 1406b6870.
namespace dingosdk::game::build::v20260929::local_cosmetic_catalog {
// Shared control block vtable for a native OwnableData message.
inline constexpr std::uintptr_t message_control_vtable = 0x60736a8;
// OwnableData (cosmetic UI) manager.
inline constexpr std::uintptr_t ownable_manager_vtable = 0x60d2600;
// Ownable inventory manager.
inline constexpr std::uintptr_t inventory_manager_vtable = 0x60d20e8;
// Pointer to the OwnableData manager.
inline constexpr std::uintptr_t ownable_manager = 0x71f5ee0;
// Pointer to the ownable inventory manager.
inline constexpr std::uintptr_t inventory_manager = 0x71f4278;
}
@@ -0,0 +1,7 @@
#pragma once
#include <cstdint>
namespace dingosdk::game::build::v20260929::local_customization {
// RecipeTemplate TypeInfo (starter character recipe templates).
inline constexpr std::uintptr_t recipe_template_type = 0x7229390;
}
@@ -0,0 +1,20 @@
#pragma once
#include <array>
#include <cstdint>
// Offline location travel catalog. The owner pointer is engine::location_owner;
// the native hooks are in location_travel.
namespace dingosdk::game::build::v20260929::local_location_travel {
// Location travel owner vtable.
inline constexpr std::uintptr_t owner_vtable = 0x60d1e48;
// Data model TypeInfos of a travel location and an access point.
inline constexpr std::uintptr_t location_type = 0x7251610;
inline constexpr std::uintptr_t access_point_type = 0x7251828;
// Event types dispatched once the catalogs are published.
inline constexpr std::array<std::uintptr_t, 2> notify_event_types{0x72519e8, 0x7251a40};
// Type records (uint32 hash at +0, uint16 size at +6) of the 0x28-byte access
// point, the 0xc0-byte location and two 1-byte types.
inline constexpr std::uintptr_t access_point_record = 0x7f67560;
inline constexpr std::uintptr_t location_record = 0x7f676b0;
inline constexpr std::array<std::uintptr_t, 2> byte_records{0x7f67330, 0x7f672c0};
}
+52
View File
@@ -0,0 +1,52 @@
#pragma once
#include "Engine/Game/Build/fingerprint.h"
#include "engine.h"
#include <array>
#include <cstdint>
namespace dingosdk::game::build::v20260929::local_music {
// Registered music assets
// Pointer to the music asset manager. Its CRITICAL_SECTION at +0x20 is taken
// through the game's IAT thunks (0x158fd80/0x1590860) by registration (0x43e260,
// keyed by "artist - title") and unregistration (0x449e70).
inline constexpr std::uintptr_t asset_manager = 0x71e39a0;
// MusicGraphAsset vtable.
inline constexpr std::uintptr_t graph_asset_vtable = 0x6204638;
// Music metadata (artist/title) vtable.
inline constexpr std::uintptr_t metadata_vtable = 0x60f72a8;
// Music TagRef vtable (enumeration + selection).
inline constexpr std::uintptr_t tag_vtable = 0x60f56a8;
// Tag selection (enum value) vtable.
inline constexpr std::uintptr_t tag_selection_vtable = 0x61ec7f0;
// Music UI
// Pointer to the music UI manager.
inline constexpr std::uintptr_t ui_manager = 0x71f5ed0;
inline constexpr std::uintptr_t ui_manager_vtable = 0x60d2588;
// Shared control block vtables of the playlist and song DTOs, and their expected slots.
inline constexpr std::uintptr_t playlist_control_vtable = 0x6070f48;
inline constexpr std::uintptr_t song_control_vtable = 0x6070f68;
inline constexpr std::array<std::uintptr_t, 4> playlist_control_slots{0x2106d0, engine::ui_control_destroy, 0x2143c0, 0x1fe380};
inline constexpr std::array<std::uintptr_t, 4> song_control_slots{0x25f1f0, engine::ui_control_destroy, 0x274ea0, 0x1fe380};
// Playlist and song DTO constructors.
inline constexpr std::uintptr_t construct_playlist = 0x1d47820;
inline constexpr std::uintptr_t construct_song = 0x1d47a40;
// Publish the playlist and song DTO lists into the UI manager.
inline constexpr std::uintptr_t publish_playlists = 0x6e88d0;
inline constexpr std::uintptr_t publish_songs = 0x6e8e30;
// Inserts a context into the UI manager's context map.
inline constexpr std::uintptr_t context_insert = 0x6c6120;
// Invokes the initialization completion delegate.
inline constexpr std::uintptr_t complete_delegate = 0x58b1f00;
// Music UI initializer (hook target).
inline constexpr Fingerprint music_ui_initialize_contract{0x868760, {0x48,0x89,0x6c,0x24,0x18,0x48,0x89,0x74,0x24,0x20,0x41,0x54,0x41,0x56,0x41,0x57,0x48,0x81,0xec,0x80,0x00,0x00,0x00,0x49,0x8b,0xe9,0x4d,0x8b,0xf0,0x4c,0x8b,0xfa}};
inline constexpr std::array<Fingerprint, 7> music_ui_contracts{{
music_ui_initialize_contract,
{construct_playlist, {0x48,0x89,0x5c,0x24,0x08,0x57,0x48,0x83,0xec,0x20,0x48,0x8b,0xda,0x48,0x8b,0xf9,0x48,0x8d,0x15,0x21,0x95,0x32,0x04,0xe8,0x94,0x30,0x5e,0x00,0x33,0xc9,0x48,0x8d}},
{construct_song, {0x48,0x89,0x5c,0x24,0x08,0x57,0x48,0x83,0xec,0x20,0x48,0x8b,0xda,0x48,0x8b,0xf9,0x48,0x8d,0x15,0x29,0x93,0x32,0x04,0xe8,0x74,0x2e,0x5e,0x00,0x33,0xc9,0x48,0x8d}},
{publish_playlists, {0x48,0x8b,0xc4,0x48,0x89,0x50,0x10,0x55,0x53,0x57,0x48,0x8d,0x68,0xa1,0x48,0x81,0xec,0xa0,0x00,0x00,0x00,0x48,0x89,0x70,0xe0,0x48,0x8d,0xb9,0x98,0x01,0x00,0x00}},
{publish_songs, {0x48,0x8b,0xc4,0x48,0x89,0x50,0x10,0x55,0x53,0x41,0x54,0x48,0x8d,0x68,0xa1,0x48,0x81,0xec,0x90,0x00,0x00,0x00,0x48,0x89,0x70,0xe0,0x48,0x8b,0xda,0x48,0x89,0x78}},
{context_insert, {0x40,0x53,0x55,0x56,0x57,0x41,0x54,0x41,0x55,0x41,0x56,0x41,0x57,0x48,0x83,0xec,0x38,0x49,0x8b,0xf9,0x49,0x8b,0xd8,0x48,0x8b,0xf2,0x4c,0x8b,0xf9,0x33,0xed,0x48}},
{complete_delegate, {0x48,0x83,0xec,0x28,0x48,0x8b,0x11,0x48,0x8b,0x0a,0x48,0x8b,0x41,0x40,0x48,0x85,0xc0,0x74,0x0d,0x80,0x79,0x68,0x00,0x75,0x07,0xff,0xd0,0x48,0x83,0xc4,0x28,0xc3}}
}};
}
@@ -0,0 +1,8 @@
#pragma once
#include <cstdint>
namespace dingosdk::game::build::v20260929::local_native_settings {
// Pointer to DingoProfileSettingsManager; its first member is a vector of the
// three option groups (LocalPerDevice, then the two cloud groups).
inline constexpr std::uintptr_t profile_settings_manager = 0x71eff00;
}
@@ -0,0 +1,17 @@
#pragma once
#include <cstdint>
// Neighborhood (district) progression records. The native response conversion
// 140346f80 hashes ProgressionId, then 14065c280 copies that hash into the base
// record (+0x38).
namespace dingosdk::game::build::v20260929::local_neighborhood {
// Neighborhood record model TypeInfo.
inline constexpr std::uintptr_t neighborhood_type = 0x72517d0;
// Reflected neighborhood record type: uint32 name hash (0xdcf37866) and uint16 size.
inline constexpr std::uintptr_t neighborhood_record_hash = 0x7f675d0;
inline constexpr std::uintptr_t neighborhood_record_size = 0x7f675d6;
// Reflected field records: Bool at +0xbd (hash 0x124511ee) and the UInt32
// progression hash at +0x38 (hash 0x9444f3fc).
inline constexpr std::uintptr_t neighborhood_flag_field = 0x82f7df8;
inline constexpr std::uintptr_t neighborhood_progression_field = 0x82d0028;
}
@@ -0,0 +1,8 @@
#pragma once
#include <cstdint>
namespace dingosdk::game::build::v20260929::local_object {
// TypeInfo registered on the Build Kit category result before the native parser
// (profile::object_parse_contract, 0x7b7840) fills it.
inline constexpr std::uintptr_t category_result_type = 0x7210018;
}
@@ -0,0 +1,18 @@
#pragma once
#include <cstdint>
namespace dingosdk::game::build::v20260929::local_placements {
// Placed-object manager vtable; its id -> entity hash table starts at +0x10.
inline constexpr std::uintptr_t placement_manager_vtable = 0x60e0380;
// uint32 registration offset of the server player's component holding the native
// simulation owner (+0x98). 0x7b3470/0x7befd0 activate rendering and collision
// only for that owner.
inline constexpr std::uintptr_t player_component_offset = 0x6f7cf60;
// uint32 offset of the client context record whose +0x100 byte is 1 once ready.
inline constexpr std::uintptr_t client_ready_offset = 0x7215038;
// Pointer to the teleport manager; its first uint32 is 0 while idle.
inline constexpr std::uintptr_t teleport_manager = 0x71ee790;
// Reference only (PlacementCreateRecipe mirrors these, nothing calls them): the
// create-message handler 0x87da20; the free-roam drop 0x7af5b0, whose park
// context byte (127) comes from 0x6f7c750; the source enum codec 0x58f96c0/0x58f8c50.
}
@@ -0,0 +1,14 @@
#pragma once
#include <cstdint>
namespace dingosdk::game::build::v20260929::local_player_card {
// Appearance (RIP Card recipe) manager, read through engine::appearance_manager.
inline constexpr std::uintptr_t appearance_manager_vtable = 0x6084c28;
// TypeInfo of the UIPlayerInfo presence connection-state value (field 7).
inline constexpr std::uintptr_t connection_state_type = 0x7258ef8;
// TypeInfo of the UIPlayerInfo player network (field 10, record +0xf4): the platform
// logo beside the name follows it. Values as EadpPlayerNetwork: 0 unknown (shown as EA),
// 1 EA, 2 PSN, 3 Xbox, 4 Steam, 5 Nintendo, 6 Apple, 7 Google.
inline constexpr std::uintptr_t player_network_type = 0x723b8f8;
inline constexpr std::uint32_t steam_network = 4;
}
@@ -0,0 +1,12 @@
#pragma once
#include <cstdint>
namespace dingosdk::game::build::v20260929::local_rip_score {
// Collection-score progression model TypeInfo.
inline constexpr std::uintptr_t rip_score_type = 0x7253080;
// Reflected record type: uint32 name hash (0x3b7ce311) and uint16 size (0x70).
inline constexpr std::uintptr_t rip_score_record_hash = 0x7f66ed0;
inline constexpr std::uintptr_t rip_score_record_size = 0x7f66ed6;
// Reflected Int64 total field record (hash 0x2891685a, +0x10).
inline constexpr std::uintptr_t rip_score_total_field = 0x82f8070;
}
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <array>
#include <cstdint>
namespace dingosdk::game::build::v20260929::localization {
// The string-override store every localized-text lookup checks before the shipped string
// database (hasString 0x3320c70 and getString 0x3322fd0 both do). Retail fills its layer 0
// from the backend's client_strings_v1 service (handler 0x268a00, lambda 0x2696d0: clear
// the layer with 0x331d060, then set each record). An id found nowhere is shown as written.
// Ids are hashed h = h * 33 + byte from 0xffffffff (0x3325670).
// Address store(): the store (a static at +0x761b410).
inline constexpr std::uintptr_t override_store = 0x331d5e0;
inline constexpr std::array<unsigned char, 19> override_store_prefix{
0x48,0x83,0xec,0x38,0x8b,0x0d,0xf6,0x6a,0x5b,0x04,0x65,0x48,0x8b,0x04,0x25,0x58,0x00,0x00,0x00};
// bool set(Address store, const char* id, const char* utf8, int layer): takes the store's
// lock, converts the text to UTF-16 and registers its glyphs. Engine allocator: game thread.
inline constexpr std::uintptr_t override_set = 0x331cc20;
inline constexpr std::array<unsigned char, 19> override_set_prefix{
0x40,0x55,0x53,0x56,0x57,0x41,0x54,0x41,0x56,0x41,0x57,0x48,0x8d,0xac,0x24,0x70,0xfc,0xff,0xff};
// bool has(uint32 hash): the id is in the store or the database.
inline constexpr std::uintptr_t string_exists = 0x3320c70;
inline constexpr std::array<unsigned char, 19> string_exists_prefix{
0x40,0x53,0x48,0x83,0xec,0x20,0x8b,0xd9,0xe8,0x63,0xc9,0xff,0xff,0x8b,0xd3,0x48,0x8b,0xc8,0xe8};
// set() encodes the text against the loaded string database (0x3325800: UTF-8 to UTF-16,
// each character registered with the database 0x3320730, then 0x3323d90 writes the stored
// form). With no database loaded yet it stores an empty string, which shows as blank.
// The database is found as that function finds it: the qword at database_loaded must be
// set; then the first of the {u32 in use (0 = this one), pad, entry*} rows at *databases,
// indices 0..*database_last, whose entry +0x10 is the database.
inline constexpr std::uintptr_t database_loaded = 0x761b4a0;
inline constexpr std::uintptr_t databases = 0x761b4a8;
inline constexpr std::uintptr_t database_last = 0x716d524;
} // namespace dingosdk::game::build::v20260929::localization
@@ -0,0 +1,11 @@
#pragma once
#include "Engine/Game/Build/fingerprint.h"
namespace dingosdk::game::build::v20260929::location_travel {
inline constexpr Fingerprint travel_location_ctor_contract{0x59c4740, {0x48,0x89,0x5c,0x24,0x08,0x48,0x89,0x74,0x24,0x10,0x57,0x48,0x83,0xec,0x20,0x48,0x8d,0x3d,0xae,0x9f,0x93,0x01,0x33,0xf6,0x48,0x89,0x31,0x48,0x8b,0xd9,0x48,0x89}};
inline constexpr Fingerprint travel_access_ctor_contract{0x59c44d0, {0x48,0x89,0x5c,0x24,0x08,0x57,0x48,0x83,0xec,0x20,0x48,0x8d,0x3d,0x23,0xa2,0x93,0x01,0x48,0x8b,0xd9,0x48,0x89,0x39,0x48,0x89,0x79,0x08,0x48,0x89,0x79,0x10,0xe8}};
inline constexpr Fingerprint travel_request_contract{0x79c9e0, {0x48,0x8b,0xc1,0x48,0x8b,0xd,0x4e,0x28,0xa5,0x6,0x48,0x85,0xc9,0x74,0xf,0xc6,0x41,0x1a,0x1,0x4c,0x8b,0x2,0x48,0x8b,0x10,0xe9,0xc2,0x6e,0xde,0xff,0xc3,0xcc}};
inline constexpr Fingerprint travel_prepare_contract{0x786cd0, {0x48,0x89,0x5c,0x24,0x08,0x57,0x48,0x83,0xec,0x20,0x48,0x8b,0xfa,0x48,0x8b,0xd9,0xba,0x89,0x97,0x0f,0xbf,0x48,0x8d,0x4c,0x24,0x40,0xe8,0x51,0x01,0x13,0x01,0x48}};
inline constexpr Fingerprint travel_attribute_request_contract{0x7c5590, {0x40,0x55,0x53,0x41,0x56,0x48,0x8d,0xac,0x24,0x40,0xfe,0xff,0xff,0x48,0x81,0xec,0xc0,0x02,0x00,0x00,0x48,0x8b,0x05,0x15,0xee,0x9f,0x06,0x48,0x33,0xc4,0x48,0x89}};
}
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
namespace dingosdk::game::build::v20260929::loose_files {
template<std::size_t N> consteval auto hex_bytes(const char (&hex)[N]) {
static_assert(N % 2 == 1);
std::array<unsigned char, (N - 1) / 2> result{};
const auto nibble = [](char ch) -> unsigned {
if (ch >= '0' && ch <= '9') return static_cast<unsigned>(ch - '0');
if (ch >= 'a' && ch <= 'f') return static_cast<unsigned>(ch - 'a' + 10);
throw "Invalid native contract hex";
};
for (std::size_t index = 0; index < result.size(); ++index)
result[index] = static_cast<unsigned char>((nibble(hex[index * 2]) << 4) | nibble(hex[index * 2 + 1]));
return result;
}
// VFS open. The Lua vfs.open binding and the engine's read-file wrapper both
// reach this open ABI.
inline constexpr std::uintptr_t vfs_open = 0x4b51130;
inline constexpr auto vfs_open_prefix = hex_bytes("4c8bdc5557498dabc8f9ffff4881ec28070000488b05763267024833c4488985");
// VFS exists.
inline constexpr std::uintptr_t vfs_exists = 0x4b52660;
inline constexpr auto vfs_exists_prefix = hex_bytes("40574881ec80050000488b05501d67024833c448898424700500004c8bc2c744");
// Directory file-system factory.
inline constexpr std::uintptr_t create_directory = 0x4b4c220;
inline constexpr auto create_directory_prefix = hex_bytes("48895c24184889742420574883ec40488b154aac7a02488bd9b9a0000000e8ad");
// VFS mount.
inline constexpr std::uintptr_t vfs_mount = 0x4b53f90;
inline constexpr auto vfs_mount_prefix = hex_bytes("40535556574883ec28488b5910488be9488bcb498bf8488bf2e8d2bda3fc488b");
// Returns the global VFS.
inline constexpr std::uintptr_t global_vfs = 0x4b4d410;
inline constexpr auto global_vfs_prefix = hex_bytes("488b058919c902c3");
// Reference taken inside mount: the factory initializes its intrusive
// reference count at +8 and Mount takes a ref.
inline constexpr std::uintptr_t mount_reference = 0x4b53e62;
inline constexpr auto mount_reference_prefix = hex_bytes("4d85ff7405f041ff4708");
}
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include "Engine/Game/Build/fingerprint.h"
namespace dingosdk::game::build::v20260929::lua_startup {
// Native post-startup routine hooked to run the Lua startup callbacks.
inline constexpr Fingerprint startup_contract{0x4641d40, {0x48,0x89,0x5c,0x24,0x08,0x57,0x48,0x83,0xec,0x50,0x48,0x8b,0xf9,0xe8,0x3e,0x52,0x28,0xfd,0x33,0xd2,0x48,0x8b,0xc8,0x48,0x8b,0xd8,0xe8,0x61,0x56,0x28,0xfd,0x48}};
// Executes a Lua source buffer in a context.
inline constexpr Fingerprint execute_contract{0x18c7640, {0x48,0x83,0xec,0x38,0x48,0x8d,0x05,0xd5,0x32,0x9b,0x04,0xc6,0x44,0x24,0x28,0x00,0x48,0x89,0x44,0x24,0x20,0xe8,0xf6,0x31,0x00,0x00,0x48,0x83,0xc4,0x38,0xc3,0xcc}};
// Returns the Lua context.
inline constexpr Fingerprint get_context_contract{0x18c6f90, {0x48,0x83,0xec,0x28,0x48,0x8b,0x05,0x15,0xbd,0xbb,0x05,0x48,0x85,0xc0,0x0f,0x85,0xb8,0x00,0x00,0x00,0x44,0x0f,0xb6,0x44,0x24,0x30,0xb9,0x10,0x01,0x00,0x00,0x48}};
// Reloads settings on the settings manager.
inline constexpr Fingerprint reload_settings_contract{0x18d3360, {0x4c,0x8b,0xdc,0x55,0x53,0x49,0x8d,0xab,0x08,0xfe,0xff,0xff,0x48,0x81,0xec,0xe8,0x02,0x00,0x00,0x48,0x8b,0x05,0x46,0x10,0x8f,0x05,0x48,0x33,0xc4,0x48,0x89,0x85}};
}
@@ -0,0 +1,55 @@
#pragma once
#include "engine.h"
#include <array>
#include <cstdint>
// Main mission / quest progression override.
namespace dingosdk::game::build::v20260929::mission_progression {
// Quest lookup by QuestId.
inline constexpr std::uintptr_t quest_lookup = 0x825750;
inline constexpr std::array<unsigned char, 32> quest_lookup_prefix{
0x48,0x89,0x6c,0x24,0x18,0x48,0x89,0x74,0x24,0x20,0x41,0x56,0x48,0x83,0xec,0x20,
0x4c,0x8b,0xf1,0x49,0x8b,0xe8,0x48,0x8b,0x0d,0x73,0xd5,0xc5,0x06,0x48,0x8b,0xf2};
// Generic ECS component query.
inline constexpr std::uintptr_t component_query = 0x276c4f0;
inline constexpr std::array<unsigned char, 31> component_query_prefix{
0x40,0x53,0x55,0x56,0x57,0x48,0x83,0xec,0x28,0x4c,0x8b,0x51,0x10,0x48,0x8b,0xd9,
0x48,0x83,0xc1,0x08,0x41,0x8b,0xe9,0x49,0x8b,0xf0,0x4d,0x85,0xd2,0x74,0x0c};
// Quest status component query call site: LEA the exact Quest status component
// descriptor, initialize the 24-byte query result, and call the generic ECS
// component query. It ends at quest_status_query_return.
inline constexpr std::uintptr_t quest_status_query_call = 0x82cbb9;
inline constexpr std::array<unsigned char, 29> quest_status_query_call_prefix{
0x48,0x8d,0x15,0xb0,0xe7,0x9f,0x06,0x48,0x89,0x5d,0x0f,0x48,0x8d,0x4d,0x17,0x48,
0x89,0x7d,0xef,0xc5,0xfa,0x7f,0x45,0xf7,0xe8,0x1a,0xf9,0xf3,0x01};
// Return address of that query: validate both result pointers and read the
// QuestState dword at component offset +0x18 before the caller's terminal-state test.
inline constexpr std::uintptr_t quest_status_query_return = 0x82cbd6;
inline constexpr std::array<unsigned char, 36> quest_state_consumer_prefix{
0x48,0x83,0x7d,0xf7,0x00,0x0f,0x84,0xd7,0x01,0x00,0x00,0x48,0x8b,0x4d,0xff,0x48,
0x85,0xc9,0x0f,0x84,0xca,0x01,0x00,0x00,0x8b,0x41,0x18,0x48,0x8b,0x16,0x89,0x45,
0xcb,0x83,0xf8,0x01};
// Quest status component descriptor.
inline constexpr std::uintptr_t quest_status_component_descriptor = 0x722b370;
// Expression quest lookup call site and return: checks readiness, resolves
// QuestId, then returns that exact handle to the compiled mission/presentation graph.
inline constexpr std::uintptr_t quest_expression_call = 0x83508c;
inline constexpr std::uintptr_t quest_expression_return = 0x8350a5;
inline constexpr std::array<unsigned char, 49> quest_expression_call_prefix{
0x80,0xb8,0xd0,0x01,0x00,0x00,0x00,0x74,0x38,0x4c,0x8b,0xc6,0x48,0x8d,0x54,0x24,
0x20,0x48,0x8b,0xc8,0xe8,0xab,0x06,0xff,0xff,0x48,0x8b,0x08,0x48,0xb8,0x00,0x00,
0x00,0x00,0xff,0xff,0xff,0xff,0x48,0x85,0xc8,0x48,0x89,0x0f,0x0f,0x95,0xc0,0x88,0x03};
// ECS query context constructor and initializer.
inline constexpr std::uintptr_t query_context_constructor = engine::query_context_constructor;
inline constexpr std::array<unsigned char, 26> query_context_constructor_prefix{
0x40,0x53,0x48,0x83,0xec,0x20,0x45,0x33,0xc0,0x48,0x8b,0xd9,0xe8,0xdf,0xa3,0xfe,
0xff,0x48,0x8b,0xc3,0x48,0x83,0xc4,0x20,0x5b,0xc3};
inline constexpr std::uintptr_t query_context_initializer = 0x27597d0;
inline constexpr std::array<unsigned char, 32> query_context_initializer_prefix{
0x48,0x89,0x5c,0x24,0x08,0x57,0x48,0x83,0xec,0x20,0x48,0x89,0x11,0x48,0x8b,0xda,
0x48,0xc7,0x41,0x08,0x00,0x00,0x00,0x00,0x48,0x8b,0xf9,0x48,0xc7,0x41,0x10,0x00};
// Query-result vtable and quest status type field, as built by 0x14082cab0
// (its LEAs at 14082c71e/14082c72d feed the query at 14082c7c1).
inline constexpr std::uintptr_t query_result_vtable = 0x60de9c8;
inline constexpr std::uintptr_t quest_status_type = 0x722b210;
}
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
namespace dingosdk::game::build::v20260929::mod_layers {
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 ch) -> unsigned {
if (ch >= '0' && ch <= '9') return static_cast<unsigned>(ch - '0');
if (ch >= 'a' && ch <= 'f') return static_cast<unsigned>(ch - 'a' + 10);
throw "Invalid native contract hex";
};
for (std::size_t index = 0; index < result.size(); ++index)
result[index] = static_cast<std::uint8_t>(
(nibble(hex[index * 2]) << 4) | nibble(hex[index * 2 + 1]));
return result;
}
// `classify` derives a layer kind from the layout.toc path.
inline constexpr std::uintptr_t classify = 0x41ded80;
inline constexpr auto classify_prefix = hex_bytes("4055535657488d6c24884881ec78010000488b052856fe024833c44889456048");
// `load` adds one layer to the layout manager.
inline constexpr std::uintptr_t load_layer = 0x4203e60;
inline constexpr auto load_layer_prefix = hex_bytes("48895c242055565741544157488dac2460feffff4881eca0020000488b053e05");
// Slot 0x60 of the layout manager's vtable, which must still be `load`.
inline constexpr std::uintptr_t load_layer_slot = 0x65924f8;
// layout_bootstrap, which performs the LCU, Patch and Data loads in that order.
inline constexpr std::uintptr_t layout_bootstrap = 0x41bc220;
inline constexpr auto layout_bootstrap_prefix = hex_bytes("48895c2418488974242055574156488dac2430fdffff4881ecd0030000488b05");
}
@@ -0,0 +1,184 @@
// Reviewed native contract for supported_build::game_sha256 (September 29 2026).
// RVA-only code, pdata and unwind bytes are ASLR invariant. This file is a
// production compatibility guard, not an extracted asset or runtime dump.
#pragma once
#include <array>
#include <cstddef>
#include <cstdint>
#include <span>
namespace dingosdk::game::build::v20260929::morph_memory_pool {
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 morph 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;
}
// SHA256 9d5371d7d80da2418e82d14fdc28bf77e0d1c26c03ea34a9249e8b4759f2710d
inline constexpr auto constructor = hex_bytes(
"48895c2408574883ec30488d05670c7804488bf9488901488d415848c7410800000000c5fc1002c5fc11411048894150"
"ba0100000048894138448bc2488941304883c0404889414048c74148000000004881c198000000c5f877e8616527024c"
"8bcf48c7442420000000004c8d05ee810000488d15c7810000488d8f98000000e85ba427024533c0488d8f98000000ba"
"01000000e827c927024c8b4718488d8f98000000ba07000000e812c92702488b5c2440488bc74883c4305fc3");
// SHA256 255be1a04afcfe7c13c7a2f7e327246e06d702a914c8255c889b101daf7dd580
inline constexpr auto constructor_pdata_0 = hex_bytes(
"8015b2013c16b20160b1ab06");
// SHA256 95a10c6d2d751622a4535e1f8c8bdca6a784f9865cf0e1039457f8b7b56c3dae
inline constexpr auto constructor_unwind_0 = hex_bytes(
"010a04000a3408000a520670");
// SHA256 607fea135c302789e7f2deae98ebdd6d31f38e5a44c945694896bc8ed3d300c8
inline constexpr auto morph_initializer = hex_bytes(
"48895c2408574881eca0000000488b1d74a59705488d05cd2b790448890546589a05488d05d72b790448890548589a05"
"488d05e12b79044889054a589a0548890553589a05e8d6106ffe33ff84c00f8599010000488b054d5e9a054885c0751a"
"488b0d194897054c8d053a5e9a05488d1553539a05e8c68adcff486348284c8d15f7f45504488d15d42a790448890d3d"
"589a054889150e589a054533c94c89151c589a054533c0893d03589a05c705fd579a050200000048893dfa579a054889"
"3d03589a054863402048891508589a05498bd248894c2420488d4c2450893dfd579a05c705f7579a050200000048893d"
"f4579a054c8915f5579a0548893df6579a05488905f7579a05e8028ea8ff488b05bb579a05b920150000488b15f7887e"
"05488b1d60a497054889442468c644247001e859f4a6ff4885c074194c8bc3488d542450488bc8e8248da8ff48890575"
"579a05eb0748893d6c579a0548393d95579a057571488b0594579a05488d4c2450488b1578579a054533c94533c04889"
"442420e8888da8ff488b0571579a05b920150000488b157d887e05488b1de6a397054889442468c644247001e8dff3a6"
"ff4885c074194c8bc3488d542450488bc8e8aa8ca8ff4889052b579a05eb0748893d22579a05c605c3569a0501488d1d"
"f4569a05488d05bd569a05eb03488bc348891d69569a0548891d82569a05488b1da35c9a0548890564569a054889057d"
"569a054885db751d488b0d614697054c8d05825c9a05488d159b519a05e80e89dcff488bd8488b0d34569a05488b151d"
"569a0548894c243048c74424380000010048634b3448894c2440b9700c0000c644244801e827f3a6ff4885c0740f488d"
"542430488bc8e8a52e0100eb03488bc7488b15d9559a05b9700c000048890595569a05488b05d6559a05488944243048"
"63432c4889442440c644244801e8def2a6ff4885c07425488d542430488bc8e85c2e010048890555569a05488b9c24b0"
"0000004881c4a00000005fc3488b9c24b000000048893d35569a054881c4a00000005fc3");
// SHA256 33d299717f4325f4df48939844e7c9a58b871e085d43ee6c8e515846d718f903
inline constexpr auto morph_initializer_pdata_0 = hex_bytes(
"60e4b00154e7b001fcc5ac06");
// SHA256 0805341631737f6e131b8ceadb7b0046158c75a5a77488732dd00253aff37e9f
inline constexpr auto morph_initializer_unwind_0 = hex_bytes(
"010d05000d3416000d01140006700000");
// SHA256 b0d0e53c6708473c5c8aaaef4e3d8ed0a497990f86a70011d80d572beb784cf9
inline constexpr auto pool_allocate = hex_bytes(
"40535556574154415641574883ec304d8be14d8bf84c8bf2488be90f1f440000488b4508a83f751b0f1f840000000000"
"488bd04883ca3ff0480fb155087418a83f74ed41b801000000488d4d08488bd0e87b38a6ffebc94c8b4518488d8d9800"
"00004533c9c744242004000000498bd7e88b852702488bf84885c07525488bcde81bfbffff4c8b4518488d8d98000000"
"4533c9897c2420498bd7e861852702488bf848c7c2c1fffffff0480fc155084883c2c148f7c280ffffff7409488d4d08"
"e84b73a6ff4d8bcf4c8bc7488bd5498bcee8eab53102498bd4498bcee8af5c6dfe498bc64883c430415f415e415c5f5e"
"5d5bc3");
// SHA256 bec574e7df6885128b3496a7f3996084aae42ba123a546d9026f0e411eee822d
inline constexpr auto pool_allocate_pdata_0 = hex_bytes(
"a038b2019339b201c4daab06");
// SHA256 eb3d1b62e666c6777ab6187669de7911588d02064dc5bd980db5d82be559a7f5
inline constexpr auto pool_allocate_unwind_0 = hex_bytes(
"010f08000f520bf009e007c00570046003500230");
// SHA256 82090f22003a26a5baa782117422d2b7d01692808a90ccb4ed3a3a6533f856f7
inline constexpr auto pool_grow = hex_bytes(
"4053555741554883ec68488b4120488bf948894424384533ed488b411848894424400fb64128488b4938488be9884424"
"48482b6f30488d05b4ed770448c1fd03c74424303000000066c744244a01004c896c2458c6442449014889442450483b"
"4f407310488d4108488947384c8929e9130100004889b424980000004c89a424a00000004c89b424a80000004c897c24"
"6085ed741f81fdffffff7f73108d346d0000000085f675114d8bfdeb2cbeffffffffeb05be010000008bd6488d4f4848"
"c1e2034533c941b80800000044896c2420e80a1ebf004c8bf88bcd4d8bc74d892ccf4c8b4f38488b4f30493bc9741790"
"488b114c89294883c1084989104983c008493bc975ea4c8b77384d8d6008488b5f30493bde7417488b0b4885c9740648"
"8b01ff50084883c308493bde75e9488b57304c8bb424a80000004885d27421483b5750488d4f4874174c8b47404c2bc2"
"49c1f803458bc049c1e003e8c01dbf008bc6488bb424980000004c897f304c8967384c8ba424a0000000498d0cc74c8b"
"7c246048894f40488b0d4258bc05488b5f38488b01ff50604c8d43f8488bc8488d5424304c8b0841ff91880000004c8b"
"4720488d8f9800000089ac24900000004533c94489ac2494000000488b84249000000048c1e8208bd548c1e220480bd0"
"4c896c242048b80000000000000080480bd0e8894b27024883c468415d5f5d5bc3");
// SHA256 1c0c2e6a97b2eaa9f4277e4648cb8abaae51541b07b177b6476cd4c8ad80bcb3
inline constexpr auto pool_grow_pdata_0 = hex_bytes(
"4034b201b434b201b855c606");
// SHA256 38605cfc6c0a2eae2275b9f0d12567636ffb9468d1da3e3d3f7489b0cc42b396
inline constexpr auto pool_grow_unwind_0 = hex_bytes(
"010a05000ac206d00470035002300000");
// SHA256 c9e7811d183bf2c809fd4bda01f0ed59e8aa1ea13f8e25bf08700b9c43f85111
inline constexpr auto pool_grow_pdata_1 = hex_bytes(
"b434b201c434b201c855c606");
// SHA256 bf698f75148e8b8d93cbee934ca4e804af2b8975b04e04bf4a1428d6f35f49d2
inline constexpr auto pool_grow_unwind_1 = hex_bytes(
"2110040010c41400086413004034b201b434b201b855c606");
// SHA256 3e4df33dc716181ae3129bc7911ccd84ce6015430f4a3904ed2ed0c4fdb45f90
inline constexpr auto pool_grow_pdata_2 = hex_bytes(
"c434b2017f35b201e055c606");
// SHA256 e16c924644dc1dc90eb3c68bc8a8e63ad65b1205c98cef8ea1cc7c91993ba4ac
inline constexpr auto pool_grow_unwind_2 = hex_bytes(
"210d04000df40c0008e41500b434b201c434b201c855c606");
// SHA256 640c455ecd23d4077cab2e987610fe1300b6bb2fb999527e2f673b48a6b632f2
inline constexpr auto pool_grow_pdata_3 = hex_bytes(
"7f35b201c735b201f855c606");
// SHA256 ec4f597441944f311d301422061c2555e58e09b218f06ebc005204950a947e43
inline constexpr auto pool_grow_unwind_3 = hex_bytes(
"2100020000f40c00b434b201c434b201c855c606");
// SHA256 15353f67825a571676015f7940054e1c25ce0aeae9ecfb06285cc1eb34d380ea
inline constexpr auto pool_grow_pdata_4 = hex_bytes(
"c735b2014136b2010c56c606");
// SHA256 18eed2b26ab659cfce6633d49c6818a9fba7bfc7c56d340924c23e259b1bc9e1
inline constexpr auto pool_grow_unwind_4 = hex_bytes(
"210000004034b201b434b201b855c606");
// SHA256 bac5adde31da1384fc6b409a92d261a7112fb4c46d0824eb78b078cc54c08129
inline constexpr auto pool_map_heap_offset = hex_bytes(
"40535556574883ec28498be8488bfa488bf1488b460848a980ffffff75660fb6d080e23f80fa3e735b488d4801f0480f"
"b14e0875e1488b4630488bcd48c1e92048c7c2ffffffff0fbaf11f488b0cc88bc54889470848890ff0480fc1560848ff"
"caf6c23f751248f7c280ffffff7409488d4e08e8483aa6ff488bc74883c4285f5e5d5bc34533c0488d4e08488bd0e8ed"
"fea5ffe97affffff");
// SHA256 a45a521d57a2ff5732f925598412e0ab324d4e138c224bfecce8b955ce76acbf
inline constexpr auto pool_map_heap_offset_pdata_0 = hex_bytes(
"f071b2018872b20120b6ab06");
// SHA256 f28ccee9e54971753b66d1ea62e19a750e5b147360135aca2e5cffe13c1c86eb
inline constexpr auto pool_map_heap_offset_unwind_0 = hex_bytes(
"01090500094205700460035002300000");
// SHA256 4896d783b760dae884dfc92ca9df2a19f00420d05a6fd73b6e44d0ca1a2d5b90
inline constexpr auto temporary_field_name = hex_bytes(
"54656d7052656e6465724d656d6f7279506f6f6c457874656e6453697a6500");
// SHA256 6705479ca5b564ada2ee65fc0f3fa22b5a5e0ce0014c88ccb697eca29df61f15
inline constexpr auto pool_debug_name = hex_bytes(
"44696e676f4d6f7270684d656d6f7279506f6f6c00");
// SHA256 73eb8b851694db818e5f16ae4ce8afedba597e738d78c82e7cdb5d3923e71dfd
inline constexpr auto settings_constructor = hex_bytes(
"40534883ec20488bd9e8528f0004488d05fbef7a04c7432000008002488903488bc3c7432400008000c7432800008002"
"c7432c00000001c7433000008000c743340000a0004883c4205bc3");
struct Contract { std::uintptr_t rva; std::span<const std::uint8_t> bytes; };
inline constexpr Contract contracts[]{
{0x01b21580, constructor},
{0x08d926c4, constructor_pdata_0},
{0x06abb160, constructor_unwind_0},
{0x01b0e460, morph_initializer},
{0x08d91260, morph_initializer_pdata_0},
{0x06acc5fc, morph_initializer_unwind_0},
{0x01b238a0, pool_allocate},
{0x08d929dc, pool_allocate_pdata_0},
{0x06abdac4, pool_allocate_unwind_0},
{0x01b23440, pool_grow},
{0x08d92970, pool_grow_pdata_0},
{0x06c655b8, pool_grow_unwind_0},
{0x08d9297c, pool_grow_pdata_1},
{0x06c655c8, pool_grow_unwind_1},
{0x08d92988, pool_grow_pdata_2},
{0x06c655e0, pool_grow_unwind_2},
{0x08d92994, pool_grow_pdata_3},
{0x06c655f8, pool_grow_unwind_3},
{0x08d929a0, pool_grow_pdata_4},
{0x06c6560c, pool_grow_unwind_4},
{0x01b271f0, pool_map_heap_offset},
{0x08d92cc4, pool_map_heap_offset_pdata_0},
{0x06abb620, pool_map_heap_offset_unwind_0},
{0x0629fcc0, temporary_field_name},
{0x062a2230, pool_debug_name},
{0x01af1dc0, settings_constructor},
};
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;
}();
// Morph heap constructor hooked to raise the render heap capacity.
inline constexpr std::uintptr_t constructor_rva = 0x01b21580;
// Return addresses of the temporary and permanent morph heap constructions.
inline constexpr std::uintptr_t temporary_return = 0x01b0e724;
inline constexpr std::uintptr_t permanent_return = 0x01b0e6db;
// Globals holding the temporary and permanent morph pools (null until built).
inline constexpr std::uintptr_t temporary_pool = 0x074b3d80;
inline constexpr std::uintptr_t permanent_pool = 0x074b3d88;
} // namespace dingosdk::game::build::v20260929::morph_memory_pool
@@ -0,0 +1,47 @@
#pragma once
#include "engine.h"
#include "Engine/Game/Build/fingerprint.h"
namespace dingosdk::game::build::v20260929::named_settings {
// Named getter/setter callers at 0x2d6dfe0..0x2d6e277 and the preset
// parser at 0x5f77ef independently establish these ABIs. The typed setter
// increments DdcDirtyVersion and calls registered setting listeners.
inline constexpr std::uintptr_t named_settings_manager = engine::settings_manager;
inline constexpr std::uintptr_t named_settings_get = 0x18d95e0;
inline constexpr std::uintptr_t named_settings_set = 0x18d9c50;
inline constexpr std::uintptr_t native_bool = engine::bool_type;
inline constexpr std::uintptr_t native_int32 = engine::int32_type;
inline constexpr std::uintptr_t native_uint32 = engine::uint32_type;
inline constexpr std::uintptr_t native_float32 = engine::float_type;
inline constexpr std::uintptr_t native_cstring = engine::string_type;
// Pointer to the reflection symbol registry (count at +0xd0); populated before
// named lookups are attempted.
inline constexpr std::uintptr_t symbol_registry = 0x765d318;
// Enumeration (reads only). The manager's group map (FUN_1418d2630 registers,
// FUN_1418d2d30 walks it): buckets +0xa0, bucket count u32 +0xa8, element count
// u32 +0xac; node {char* name, object*, ..., next +0x40}; object +8 = class
// TypeInfo. A second map of raw structs (FUN_1418d2930): buckets +0xf0, count
// +0xf8, elements +0xfc; node {char* name, TypeInfo*, data*, ..., next +0x48}.
// The manager lock is the critical section at +0x70.
inline constexpr std::size_t group_buckets = 0xa0, group_bucket_count = 0xa8, group_count = 0xac, group_next = 0x40;
inline constexpr std::size_t raw_buckets = 0xf0, raw_bucket_count = 0xf8, raw_count = 0xfc, raw_next = 0x48;
inline constexpr std::size_t manager_lock = 0x70;
// Registry name map (FUN_143dabc00): buckets +0x78, count u32 +0x80; node
// {u64 key, char* name, next +0x10}, keyed by field-data or descriptor address.
inline constexpr std::size_t name_buckets = 0x78, name_bucket_count = 0x80;
// TypeInfo: +0 descriptor, +0x20 super TypeInfo (FUN_143dc4960). Descriptor:
// +4 u16 flags (kind = bits 5..9; 3 class, 2 value type), +0x2a u16 own field
// count, fields at +0x38 (class) or +0x60 (value type, FUN_143dd70a0); 0x18-byte
// field data {u32 name hash, u16 offset @8, TypeInfo* @0x10} (FUN_143db7ac0).
inline constexpr std::size_t type_super = 0x20, descriptor_field_count = 0x2a,
class_fields = 0x38, value_fields = 0x60, field_size = 0x18, field_type = 0x10;
// SystemSettings: every group inherits its "Platform" field; not a setting.
inline constexpr std::uintptr_t system_settings_type = 0x7490460;
inline constexpr std::array<Fingerprint, 6> named_settings_sites{{
{named_settings_get, {0x48,0x83,0xec,0x48,0x44,0x88,0x4c,0x24,0x20,0x4d,0x8b,0xc8,0x4c,0x8b,0xc2,0x48,0x8d,0x54,0x24,0x30,0xe8,0x87,0xda,0xff,0xff,0x48,0x8b,0x00,0x48,0x83,0xc4,0x48}},
{0x18d7080, {0x40,0x53,0x55,0x56,0x57,0x41,0x56,0x48,0x81,0xec,0x60,0x01,0x00,0x00,0x48,0x8b,0x05,0x2b,0xd3,0x8e,0x05,0x48,0x33,0xc4,0x48,0x89,0x84,0x24,0x58,0x01,0x00,0x00}},
{named_settings_set, {0x40,0x55,0x53,0x56,0x57,0x41,0x56,0x41,0x57,0x48,0x8d,0xac,0x24,0x48,0xff,0xff,0xff,0x48,0x81,0xec,0xb8,0x01,0x00,0x00,0x48,0x8b,0x05,0x51,0xa7,0x8e,0x05,0x48}},
{0x18d5850, {0x4c,0x8b,0xdc,0x55,0x57,0x41,0x54,0x49,0x8d,0xab,0xc8,0xfb,0xff,0xff,0x48,0x81,0xec,0x20,0x05,0x00,0x00,0x48,0x8b,0x05,0x54,0xeb,0x8e,0x05,0x48,0x33,0xc4,0x48}},
{0x3db5290, {0x48,0x8b,0x01,0x4d,0x8b,0xd0,0x44,0x0f,0xb7,0x40,0x04,0x41,0xc1,0xe8,0x05,0x41,0x83,0xe0,0x1f,0x41,0x83,0xf8,0x1c,0x0f,0x87,0xe8,0x00,0x00,0x00,0x49,0x63,0xc0}},
{0x3daa880, {0x48,0x89,0x5c,0x24,0x08,0x48,0x89,0x74,0x24,0x10,0x57,0x48,0x83,0xec,0x20,0x49,0x8b,0x18,0x49,0x8b,0xf9,0x48,0x8b,0xcb,0x48,0x8b,0xf2,0xe8,0x7c,0x31,0x6c,0x01}},
}};
} // namespace dingosdk::game::build::v20260929::named_settings
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <array>
#include <cstdint>
// Remote skater sound.
namespace dingosdk::game::build::v20260929::native_audio {
// Skater sound component submit (handle, frame). Hooked to capture local sound.
inline constexpr std::uintptr_t submit = 0x44a370;
inline constexpr std::array<unsigned char, 17> submit_prefix{
0x4c,0x8b,0xc9,0x4c,0x8b,0xc2,0x48,0x8b,0x0d,0xfb,0x95,0xd9,0x06,0x41,0x0f,0xb7,
0x11};
// Merges a partial sound frame into owned storage (dst, src, masks). Primitive fields only.
inline constexpr std::uintptr_t frame_copy = 0x58d6540;
inline constexpr std::array<unsigned char, 15> frame_copy_prefix{
0x40,0x53,0x56,0x57,0x41,0xf6,0x00,0x01,0x4c,0x8d,0x91,0x64,0x01,0x00,0x00};
// Native sound frame difference function (audited with frame_copy). Reference only.
inline constexpr std::uintptr_t frame_difference = 0x58d53a0;
// Resets a sound frame to its defaults.
inline constexpr std::uintptr_t frame_reset = 0x58ced60;
inline constexpr std::array<unsigned char, 14> frame_reset_prefix{
0xc5,0xfc,0x10,0x05,0x68,0x49,0xb0,0x00,0x33,0xc0,0xc5,0xfc,0x11,0x01};
// Component stop operation; clears its handle (component + 0x74).
inline constexpr std::uintptr_t stop_sound = 0x43e540;
inline constexpr std::array<unsigned char, 15> stop_sound_prefix{
0x40,0x53,0x48,0x83,0xec,0x20,0x0f,0xb7,0x11,0x48,0x8b,0xd9,0x66,0x85,0xd2};
}
@@ -0,0 +1,23 @@
#pragma once
#include <array>
#include <cstdint>
// Remote skater and skateboard cosmetics.
namespace dingosdk::game::build::v20260929::native_cosmetics {
// Customization component that owns the recipe at +0x150.
inline constexpr std::uintptr_t cosmetic_component_vtable = 0x6084c88;
// Deep-copies a recipe value into a component (component, recipe).
inline constexpr std::uintptr_t recipe_copy = 0x5230b0;
inline constexpr std::array<unsigned char, 24> recipe_copy_prefix{
0x48,0x89,0x5c,0x24,0x08,0x57,0x48,0x83,0xec,0x20,0x48,0x8b,0x41,0x08,0x48,0x8b,
0xf9,0x48,0x8b,0x98,0x90,0x00,0x00,0x00};
// Finds a named entry in a loaded table by the FNV-1a hash of its name:
// (out, name, table, flag, context) -> out, the fifth passed on the stack.
// The skater loader's preset lookups reach it through a Lua binding that
// passes a nil table on when a preset's bundle was not found, and the function
// reads the table without checking it.
inline constexpr std::uintptr_t named_lookup = 0x16fc900;
inline constexpr std::array<unsigned char, 24> named_lookup_prefix{
0x40,0x55,0x56,0x57,0x41,0x56,0x48,0x81,0xec,0xd8,0x02,0x00,0x00,0x48,0x8b,0x05,
0xac,0x7a,0xac,0x05,0x48,0x33,0xc4,0x48};
}
@@ -0,0 +1,65 @@
#pragma once
#include <array>
#include <cstdint>
namespace dingosdk::game::build::v20260929::native_indicators {
// Hooked HUD indicator functions.
// HUD indicator presenter tick.
inline constexpr std::uintptr_t hud_tick = 0x1aec120;
inline constexpr std::array<unsigned char, 16> hud_tick_prefix{
0x48, 0x8b, 0xc4, 0x48, 0x89, 0x48, 0x08, 0x41, 0x54, 0x48, 0x81, 0xec, 0xe0, 0x04, 0, 0};
// Removes a provider from the indicator registry.
inline constexpr std::uintptr_t provider_remove = 0x1aebd90;
inline constexpr std::array<unsigned char, 16> provider_remove_prefix{
0x48, 0x89, 0x54, 0x24, 0x10, 0x48, 0x89, 0x4c, 0x24, 0x08, 0x41, 0x54, 0x48, 0x83, 0xec, 0x70};
// Creates an indicator widget through the presenter's widget cache.
inline constexpr std::uintptr_t widget_create = 0x4379f80;
inline constexpr std::array<unsigned char, 16> widget_create_prefix{
0x48, 0x89, 0x5c, 0x24, 0x10, 0x48, 0x89, 0x6c, 0x24, 0x20, 0x44, 0x89, 0x44, 0x24, 0x18, 0x56};
// Called indicator functions.
// Registers a provider with the indicator registry.
inline constexpr std::uintptr_t indicator_register = 0x1ae76a0;
inline constexpr std::array<unsigned char, 16> indicator_register_prefix{
0x40, 0x53, 0x41, 0x57, 0x48, 0x83, 0xec, 0x38, 0x48, 0x8b, 0x02, 0x48, 0x8b, 0xd9, 0x48, 0x8b};
// Presenter pending-removal pass.
inline constexpr std::uintptr_t presenter_flush = 0x1ae7920;
inline constexpr std::array<unsigned char, 16> presenter_flush_prefix{
0x48, 0x8b, 0xc4, 0x48, 0x89, 0x48, 0x08, 0x41, 0x56, 0x48, 0x81, 0xec, 0x90, 0, 0, 0};
// Initializes a widget's output property binding.
inline constexpr std::uintptr_t property_bind = 0x2b7e930;
inline constexpr std::array<unsigned char, 16> property_bind_prefix{
0x48, 0x89, 0x5c, 0x24, 0x08, 0x48, 0x89, 0x74, 0x24, 0x10, 0x57, 0x48, 0x83, 0xec, 0x60, 0x49};
// Native class-reference getter: *(out) = *(storage) & ~4. The property
// setters at 0x4651af0 / 0x4657040 pass class values as the referenced asset.
inline constexpr std::uintptr_t class_reference_getter = 0x3dd1890;
inline constexpr std::array<unsigned char, 11> class_reference_getter_prefix{
0x48, 0x8b, 0x01, 0x48, 0x83, 0xe0, 0xfb, 0x48, 0x89, 0x02, 0xc3};
// Expected code of shared engine/model helpers this adapter calls.
inline constexpr std::array<unsigned char, 8> set_customization_flag_prefix{0x48, 0x8b, 0x09, 0xe9, 0xb8, 0x1b, 0, 0};
inline constexpr std::array<unsigned char, 16> model_record_prefix{
0x40, 0x53, 0x55, 0x56, 0x57, 0x41, 0x56, 0x48, 0x83, 0xec, 0x30, 0x41, 0x0f, 0xb6, 0xf0, 0x48};
inline constexpr std::array<unsigned char, 16> model_type_prefix{
0x41, 0x0f, 0xb7, 0x40, 0x04, 0x41, 0xb8, 0x00, 0x06, 0x00, 0x00, 0x25, 0xff, 0x7f, 0x00, 0x00};
inline constexpr std::array<unsigned char, 16> card_info_construct_prefix{
0x48, 0x89, 0x5c, 0x24, 0x08, 0x48, 0x89, 0x74, 0x24, 0x10, 0x57, 0x48, 0x83, 0xec, 0x20, 0x48};
inline constexpr std::array<unsigned char, 16> native_text_release_prefix{
0x48, 0x8b, 0x09, 0x48, 0x8d, 0x05, 0xaa, 0x86, 0xd7, 0x05, 0x48, 0x3b, 0xc8, 0x0f, 0x85, 0xbd};
// Globals and vtables
// Indicator provider registry object.
inline constexpr std::uintptr_t presenter_registry = 0x71267e0;
inline constexpr std::uintptr_t hud_presenter_vtable = 0x629ea70;
// Indicator template source.
inline constexpr std::uintptr_t indicator_source_vtable = 0x629f0e0;
// Parent of a created indicator widget.
inline constexpr std::uintptr_t widget_parent_vtable = 0x6388c90;
// Type information
// Compass icon asset property.
inline constexpr std::uintptr_t compass_icon_type = 0x76e2850;
inline constexpr std::uintptr_t vec3_type = 0x765d350;
// UIPlayerInfo user-info field.
inline constexpr std::uintptr_t user_info_type = 0x7258fb0;
}
Loaded 100 of 1051 files, more files were not shown because too many files have changed in this diff. Show more