mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-28 04:36:43 +01:00
DingoSDKDebugInitialize answers the launcher with a bare TRUE or FALSE. On FALSE the launcher threw "DingoSDKDebugInitialize returned FALSE", which the STATUS tile turned into "ReSkate could not attach to Skate" with anti-virus advice, although the attach had worked and the runtime had logged its reason. ReSkate.dll now exports DingoSDKStartupFailure, a 1 KB buffer that every FALSE path fills with its reason (the first one stays). The launcher reads it out of Skate before closing it. With an older ReSkate.dll it reads the ERROR lines that Skate process wrote to ReSkate.log during the call instead, and with neither it says it was not told why. problem.h explains these messages before the attach rules: a save in use or a folder another game holds says Skate is already running, hook and overlay failures keep the attach advice, and anything else leads with the runtime's own reason. Bootstrap and the local profile hand their reason back, and the profile checks that used to return false without a word now log one. Fixes #90, fixes #346. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
606 lines
37 KiB
C++
606 lines
37 KiB
C++
#include "runtime_internal.h"
|
|
#include "Extension/Customization/item_browser.h"
|
|
#include "Extension/Rendering/Capture/capture_runtime.h"
|
|
#include "bootstrap.h"
|
|
#include "Engine/Core/Hooks/hooks.h"
|
|
#include "Engine/Core/Log/logging.h"
|
|
#include "Engine/Core/Platform/game_instance.h"
|
|
#include "Engine/Core/Profiling/profiler.h"
|
|
#include "Extension/Settings/engine_tweaks.h"
|
|
#include "Extension/Settings/replay_guard.h"
|
|
#include "Extension/World/load_error_guard.h"
|
|
#include "Extension/World/mesh_unload_guard.h"
|
|
#include "Extension/World/mesh_settings_guard.h"
|
|
#include "Extension/World/shader_permutation_guard.h"
|
|
#include "Extension/World/shader_registry_guard.h"
|
|
#include "Extension/World/light_table_guard.h"
|
|
#include "Extension/World/guid_table_guard.h"
|
|
#include "Extension/World/texture_handle_guard.h"
|
|
#include "Extension/World/physics_wake_guard.h"
|
|
#include "Extension/World/streaming_copy_guard.h"
|
|
#include "Extension/World/stack_check_report.h"
|
|
#include "Extension/World/world_entities.h"
|
|
#include "Extension/Music/music_inject.h"
|
|
#include "Extension/Music/spotify_web.h"
|
|
#include "Extension/Music/plex_web.h"
|
|
#include "Extension/Music/spotify_device.h"
|
|
#include "Extension/Music/spotify_dashboard.h"
|
|
#include "Extension/Music/youtube_music.h"
|
|
#include "Extension/Music/spotify_rows.h"
|
|
#include "Extension/World/texture_handle_limit.h"
|
|
#include "Extension/Console/commands.h"
|
|
#include "Extension/UI/ui_pointer_skip.h"
|
|
#include "Engine/Game/Build/addresses.h"
|
|
#include "Engine/Game/Build/20260929/profiler_labels.h"
|
|
#include "Engine/Game/Build/supported_build.h"
|
|
#include "Engine/Vfs/mod_catalog.h"
|
|
#include "Extension/Assets/map_download.h"
|
|
#include "Extension/Boot/offline_boot.h"
|
|
#include "Extension/Skater/skater_physics.h"
|
|
#include "Extension/Skater/xray.h"
|
|
#include "Extension/UI/hud_corner.h"
|
|
#include "Extension/UI/Overlay/game_images.h"
|
|
#include "Extension/Plugins/plugin_runtime.h"
|
|
#include "Extension/Skater/launches.h"
|
|
#include "Extension/Skater/trick_rewrite.h"
|
|
#include "Extension/Skater/camera_observer.h"
|
|
#include "Extension/Progression/entitlement_request_hook.h"
|
|
#include "Extension/Skater/skater_observer.h"
|
|
#include "Extension/UI/NativeMenu/native_menu.h"
|
|
#include "Extension/UI/NativeMenu/ui_sound.h"
|
|
#include "Extension/Throwdowns/throwdown_lab.h"
|
|
#include "Extension/Multiplayer/Session/session.h"
|
|
#include "Extension/Throwdowns/throwdown_debug_text.h"
|
|
#include "Extension/Multiplayer/Hud/custom_nametags.h"
|
|
#include "Extension/Profile/local_profile.h"
|
|
#include "Extension/Profile/local_profile_runtime.h"
|
|
#include "Extension/Progression/fixed_stop_entitlement_provider.h"
|
|
#include "Extension/Progression/mission_progression_override.h"
|
|
#include "Extension/Progression/neighborhood_unlock_override.h"
|
|
#include "Extension/Progression/progression_service_guard.h"
|
|
#include "Extension/Rendering/graphics_labels.h"
|
|
#include "Extension/Settings/gameplay_settings_override.h"
|
|
#include "Extension/Settings/named_settings.h"
|
|
#include "Extension/Skater/client_source_spawn.h"
|
|
#include "Extension/Skater/no_bail.h"
|
|
#include "Extension/Skater/skater_slot_override.h"
|
|
#include "Extension/UI/Startup/startup_window.h"
|
|
#include "Extension/World/level_loading.h"
|
|
#include "Extension/World/loading_screen.h"
|
|
#include "Extension/Customization/preset_lookup_guard.h"
|
|
#include "Extension/Rendering/replay_export.h"
|
|
#include "Extension/ReplayKeyframes/replay_keyframes.h"
|
|
#include "Engine/Vfs/content_catalogs.h"
|
|
#include "Engine/Vfs/world_layer_scan.h"
|
|
#include "Engine/Game/World/world_layer_catalog.h"
|
|
#include "Engine/Core/Platform/startup_failure.h"
|
|
#include <array>
|
|
#include <cstring>
|
|
#include <filesystem>
|
|
|
|
// Why startup stopped, for the launcher to show (startup_failure.h).
|
|
extern "C" __declspec(dllexport) char DingoSDKStartupFailure[dingosdk::startup_failure::capacity]{};
|
|
|
|
namespace dingosdk::runtime::detail {
|
|
namespace {
|
|
// Keeps the first reason startup stopped. The launcher reads it only after
|
|
// DingoSDKDebugInitialize has returned, so nothing reads it while it is written.
|
|
BOOL startup_failed(std::string_view reason) noexcept {
|
|
if (DingoSDKStartupFailure[0]) return FALSE;
|
|
const auto length = dingosdk::startup_failure::utf8_prefix(reason, sizeof(DingoSDKStartupFailure) - 1);
|
|
std::memcpy(DingoSDKStartupFailure, reason.data(), length);
|
|
DingoSDKStartupFailure[length] = '\0';
|
|
return FALSE;
|
|
}
|
|
BOOL stop_startup(dingosdk::logging::Level level, dingosdk::logging::Channel channel,
|
|
std::string_view reason) noexcept {
|
|
dingosdk::logging::write(level, channel, reason);
|
|
return startup_failed(reason);
|
|
}
|
|
bool exact_environment_one(const wchar_t* name) noexcept {
|
|
const auto incoming_error = GetLastError();
|
|
std::array<wchar_t, 2> value{};
|
|
const auto length = GetEnvironmentVariableW(name, value.data(),
|
|
static_cast<DWORD>(value.size()));
|
|
SetLastError(incoming_error);
|
|
return length == 1 && value[0] == L'1';
|
|
}
|
|
bool validate_image(std::uintptr_t base) {
|
|
IMAGE_DOS_HEADER dos{}; IMAGE_NT_HEADERS64 nt{};
|
|
if (!read(base, dos) || dos.e_magic != IMAGE_DOS_SIGNATURE || dos.e_lfanew <= 0 || dos.e_lfanew > 0x100000 ||
|
|
!read(base + dos.e_lfanew, nt) || nt.Signature != IMAGE_NT_SIGNATURE ||
|
|
nt.FileHeader.Machine != IMAGE_FILE_MACHINE_AMD64 || nt.OptionalHeader.SizeOfImage != dingosdk::supported_build::game_image_size) return false;
|
|
if (!matches(base + rt::client_tick, rt::client_tick_prefix)) return false;
|
|
for (const auto& entry : rt::image_checks) if (!matches(base + entry.rva, entry.bytes)) return false;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
extern "C" __declspec(dllexport) BOOL WINAPI DingoSDKDebugInitialize() {
|
|
try {
|
|
using namespace dingosdk::runtime::detail;
|
|
auto& r = runtime();
|
|
if (r.initialized) return TRUE;
|
|
r.base = reinterpret_cast<std::uintptr_t>(GetModuleHandleW(nullptr));
|
|
const auto logging_options = dingosdk::logging::options_from_environment();
|
|
const bool files_ready = dingosdk::logging::initialize(logging_options);
|
|
dingosdk::logging::startup_banner();
|
|
if (!files_ready) dingosdk::logging::write(dingosdk::logging::Level::warning,
|
|
dingosdk::logging::Channel::runtime, "File logging is unavailable; console and debugger output remain active.");
|
|
if (!validate_image(r.base))
|
|
return stop_startup(dingosdk::logging::Level::critical, dingosdk::logging::Channel::runtime, "Game image validation failed.");
|
|
// Before anything is merged or opened in the folder: one game in it at a time
|
|
// (game_instance.h). The launcher checks first; this is two started at the same moment.
|
|
{
|
|
std::wstring game(32768, L'\0');
|
|
game.resize(GetModuleFileNameW(nullptr, game.data(), static_cast<DWORD>(game.size())));
|
|
if (!game.empty() && !dingosdk::game_instance::claim(std::filesystem::path(game).parent_path()))
|
|
return stop_startup(dingosdk::logging::Level::critical, dingosdk::logging::Channel::runtime,
|
|
"Another Skate is already running from this folder; this one does not start.");
|
|
}
|
|
if (const auto& content = dingosdk::content_cache::catalogs(); content.available) {
|
|
dingosdk::logging::log(dingosdk::logging::Level::info, dingosdk::logging::Channel::assets,
|
|
"Game content cache: {} items, {} challenges, {} entitlements.",
|
|
content.items.size(), content.challenges.size(), content.entitlements.size());
|
|
} else {
|
|
dingosdk::logging::write(dingosdk::logging::Level::warning, dingosdk::logging::Channel::assets,
|
|
"Game content cache is not installed: item names, challenges and default entitlements are unavailable. "
|
|
"Start ReSkate from ReSkateLauncher to install it.");
|
|
}
|
|
// World layers come from the installed level data. The launcher builds
|
|
// the per-build cache; without it this scans the level TOCs once here.
|
|
try {
|
|
std::wstring executable(32768, L'\0');
|
|
executable.resize(GetModuleFileNameW(nullptr, executable.data(), static_cast<DWORD>(executable.size())));
|
|
const auto started = GetTickCount64();
|
|
auto layers = dingosdk::world_layer_scan::load_or_scan(
|
|
std::filesystem::path(executable).parent_path(), dingosdk::world_layer_scan::cache_file());
|
|
dingosdk::logging::log(dingosdk::logging::Level::info, dingosdk::logging::Channel::world,
|
|
"World layers: {} layers across {} maps from the level data ({} ms).",
|
|
layers.layers.size(), layers.anchors.size(), GetTickCount64() - started);
|
|
dingosdk::install_world_layer_catalog(std::move(layers));
|
|
} catch (const std::exception& error) {
|
|
dingosdk::logging::log(dingosdk::logging::Level::warning, dingosdk::logging::Channel::world,
|
|
"World layers are unavailable: {}", error.what());
|
|
}
|
|
// The game shows nothing for several seconds yet; the startup window
|
|
// covers that, and goes by itself once the game's window appears. A
|
|
// startup that fails takes it down on the way out.
|
|
dingosdk::startup::open();
|
|
struct StartupWindow {
|
|
bool handed_over{};
|
|
~StartupWindow() { if (!handed_over) dingosdk::startup::close(); }
|
|
void hand_over() {
|
|
handed_over = true;
|
|
dingosdk::startup::status("Starting Skate\xE2\x80\xA6");
|
|
}
|
|
} startup_window;
|
|
dingosdk::initialize_client_source_spawn(r.base);
|
|
std::string bootstrap_failure;
|
|
if (!dingosdk::runtime::initialize_bootstrap(r.base, bootstrap_failure)) return startup_failed(bootstrap_failure);
|
|
|
|
// Skate imports its graphics factory from the sibling Streamline
|
|
// interposer. Load that ordinary dependency while the game is still
|
|
// suspended so the overlay can hook the provider before first use.
|
|
HMODULE self{};
|
|
#pragma warning(push)
|
|
#pragma warning(disable: 4191)
|
|
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
|
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
|
reinterpret_cast<LPCWSTR>(&DingoSDKDebugInitialize), &self)) {
|
|
const auto error = GetLastError();
|
|
return stop_startup(dingosdk::logging::Level::error, dingosdk::logging::Channel::runtime,
|
|
std::format("Cannot locate the runtime module (Windows error {}).", error));
|
|
}
|
|
#pragma warning(pop)
|
|
wchar_t self_path[32768]{};
|
|
const auto self_length = GetModuleFileNameW(self, self_path, 32768);
|
|
if (!self_length || self_length >= 32768) {
|
|
const auto error = GetLastError();
|
|
return stop_startup(dingosdk::logging::Level::error, dingosdk::logging::Channel::runtime,
|
|
std::format("Cannot resolve the runtime path (Windows error {}).", error));
|
|
}
|
|
const auto interposer_path = std::filesystem::path(
|
|
std::wstring(self_path, self_length)).parent_path() / L"sl.interposer.dll";
|
|
// Every mod folder may declare destinations, highest priority first,
|
|
// and the Patch folder keeps working for a single Studio-built map.
|
|
const auto& mod_catalog = dingosdk::mods::catalog();
|
|
const auto patch_level_path = std::filesystem::path(
|
|
std::wstring(self_path, self_length)).parent_path() / L"Patch" / L"reskate-levels.json";
|
|
r.patch_level_manifests = {patch_level_path};
|
|
if (!mod_catalog.data_root.empty()) {
|
|
const auto data_level_path = mod_catalog.data_root / L"Patch" / L"reskate-levels.json";
|
|
std::error_code level_path_error;
|
|
if (!std::filesystem::equivalent(patch_level_path, data_level_path, level_path_error) ||
|
|
level_path_error) r.patch_level_manifests.push_back(data_level_path);
|
|
}
|
|
r.custom_levels = read_custom_levels(dingosdk::mods::level_manifest_paths(mod_catalog));
|
|
if (r.custom_levels.present)
|
|
dingosdk::logging::log(dingosdk::logging::Level::info, dingosdk::logging::Channel::level,
|
|
"Custom-level destinations after merging: {}.", r.custom_levels.levels.size());
|
|
// Names and Quick Drop families mods give their own objects, before anything lists objects.
|
|
{
|
|
std::vector<std::pair<std::string, std::filesystem::path>> object_files;
|
|
for (const auto& mod : mod_catalog.mods)
|
|
object_files.emplace_back(mod.name, mod.directory / dingosdk::content_cache::mod_objects_file);
|
|
const auto declared = dingosdk::content_cache::install_mod_objects(object_files);
|
|
for (const auto& note : declared.notes)
|
|
dingosdk::logging::log(dingosdk::logging::Level::warning, dingosdk::logging::Channel::objects,
|
|
"Mod objects: {}.", note);
|
|
if (declared.mods)
|
|
dingosdk::logging::log(dingosdk::logging::Level::info, dingosdk::logging::Channel::objects,
|
|
"Mod objects: {} named by {} mod(s).", declared.objects, declared.mods);
|
|
}
|
|
auto interposer = GetModuleHandleW(L"sl.interposer.dll");
|
|
if (!interposer) interposer = LoadLibraryExW(interposer_path.c_str(), nullptr,
|
|
LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_SYSTEM32);
|
|
if (!interposer) {
|
|
const auto error = GetLastError();
|
|
return stop_startup(dingosdk::logging::Level::error, dingosdk::logging::Channel::graphics,
|
|
std::format("Cannot load the game's sl.interposer.dll (Windows error {}).", error));
|
|
}
|
|
wchar_t interposer_loaded_path[32768]{};
|
|
const auto interposer_length = GetModuleFileNameW(
|
|
interposer, interposer_loaded_path, 32768);
|
|
std::error_code interposer_error;
|
|
if (!interposer_length || interposer_length >= 32768 ||
|
|
!std::filesystem::equivalent(interposer_path,
|
|
std::filesystem::path(std::wstring(interposer_loaded_path, interposer_length)),
|
|
interposer_error) || interposer_error)
|
|
return stop_startup(dingosdk::logging::Level::error, dingosdk::logging::Channel::graphics,
|
|
"Loaded graphics provider does not match the game's sibling sl.interposer.dll.");
|
|
dingosdk::logging::write(dingosdk::logging::Level::info, dingosdk::logging::Channel::graphics,
|
|
"Game graphics provider loaded: sl.interposer.dll.");
|
|
const dingosdk::overlay::CallbacksV3 callbacks{
|
|
nullptr, read_model, queue_load, queue_debug, queue_offline_feature,
|
|
queue_console_command};
|
|
auto native_callbacks = callbacks;
|
|
native_callbacks.read_model = read_native_menu_model;
|
|
dingosdk::multiplayer::set_native_menu_callbacks(native_callbacks);
|
|
dingosdk::overlay::set_multiplayer_queue(queue_multiplayer_command);
|
|
dingosdk::overlay::set_chat_feed(dingosdk::multiplayer::chat);
|
|
// A map a session needs: hosts say which Thunderstore package theirs is from, and a
|
|
// guest without it is offered it on a card (Extension/Assets/map_download.h).
|
|
dingosdk::multiplayer::set_map_package_lookup(dingosdk::map_download::package_of);
|
|
// And a dedicated server's required plugins, offered on the same card.
|
|
dingosdk::multiplayer::set_plugin_check(dingosdk::map_download::missing_plugins, dingosdk::map_download::has_level);
|
|
dingosdk::overlay::set_map_download_feed([]() {
|
|
const auto view = dingosdk::map_download::view();
|
|
using Stage = dingosdk::map_download::Stage;
|
|
dingosdk::overlay::MapDownloadCard card;
|
|
card.stage = view.stage == Stage::asking ? 1 : view.stage == Stage::downloading ? 2 : view.stage == Stage::installing ? 3
|
|
: view.stage == Stage::applying ? 4 : view.stage == Stage::joining ? 5 : 0;
|
|
card.map = view.map;
|
|
card.package = view.package;
|
|
card.author = view.author;
|
|
card.version = view.version;
|
|
card.description = view.description;
|
|
card.step = view.step;
|
|
card.steps = view.steps;
|
|
card.step_name = view.step_name;
|
|
card.server = view.server;
|
|
card.moved = view.moved;
|
|
card.installed = view.installed;
|
|
card.have = view.have;
|
|
card.bytes = view.bytes;
|
|
card.choice = view.choice;
|
|
card.received = view.received;
|
|
card.total = view.total;
|
|
card.yes_bind = view.yes_bind;
|
|
card.no_bind = view.no_bind;
|
|
for (const auto& item : view.items)
|
|
card.items.push_back({item.name, item.author, item.version, item.have, item.plugin, item.installed, item.bytes});
|
|
card.current = view.current;
|
|
return card;
|
|
});
|
|
dingosdk::overlay::set_map_download_answer(dingosdk::map_download::answer);
|
|
dingosdk::overlay::set_hub_page_feed(dingosdk::multiplayer::native_menu_page);
|
|
dingosdk::overlay::set_hub_callbacks(native_callbacks);
|
|
dingosdk::overlay::set_ui_sound(dingosdk::multiplayer::queue_ui_sound);
|
|
dingosdk::overlay::set_item_browser(dingosdk::item_browser::overlay_host());
|
|
dingosdk::overlay::set_frame_grab(dingosdk::capture::frame_grab());
|
|
dingosdk::overlay::set_game_text_feed(dingosdk::multiplayer::skate_debug_text);
|
|
dingosdk::overlay::set_skate_hud_feed(dingosdk::multiplayer::skate_hud);
|
|
dingosdk::overlay::set_nametag_feed(dingosdk::multiplayer::custom_nametags);
|
|
// Engine functions the profiler's stack sampler names in its reports.
|
|
static constexpr dingosdk::profiler::Label engine_labels[]{
|
|
{dingosdk::addr::profiler_labels::client_update, "Client game update (ReSkate tick hook)"},
|
|
{dingosdk::addr::profiler_labels::client_frame_job.rva, "Client frame job"},
|
|
{dingosdk::addr::profiler_labels::expression_vm, "Expression VM"},
|
|
{dingosdk::addr::profiler_labels::expression_vm_profiled, "Expression VM (profiled)"},
|
|
{dingosdk::addr::profiler_labels::menu_expression, "Menu expression"},
|
|
{dingosdk::addr::profiler_labels::job_run_until_done, "Job system: run jobs until done"},
|
|
{dingosdk::addr::profiler_labels::job_execute, "Job system: execute job"},
|
|
{dingosdk::addr::profiler_labels::job_idle_wait, "Job system: idle wait"},
|
|
{dingosdk::addr::profiler_labels::simulation_loop_thread, "GameSimulationLoop thread"},
|
|
{dingosdk::addr::profiler_labels::job_worker_loop, "Job system: worker loop"},
|
|
{dingosdk::addr::profiler_labels::job_pop, "Job system: look for work (spins when idle)"},
|
|
{dingosdk::addr::profiler_labels::job_queue_pop, "Job system: queue pop"},
|
|
{dingosdk::addr::profiler_labels::expression_shader_model_build, "Job: expressionShaderModelBuild"},
|
|
{dingosdk::addr::profiler_labels::render_bundle_job, "Job: renderBundleJob"},
|
|
{dingosdk::addr::profiler_labels::mesh_cull, "Job: meshCull"},
|
|
{dingosdk::addr::profiler_labels::render_dispatch_end, "Job: renderDispatchEnd"},
|
|
{dingosdk::addr::profiler_labels::input_dispatch, "Input dispatch"},
|
|
{dingosdk::addr::profiler_labels::ui_pointer_update, "UI pointer update (mouse hit-test)"},
|
|
{dingosdk::addr::profiler_labels::ui_view_pointer, "UI view pointer hit-test"}};
|
|
dingosdk::profiler::set_engine_labels(engine_labels);
|
|
dingosdk::overlay::set_park_surface_queue(dingosdk::queue_local_park_surface);
|
|
dingosdk::overlay::set_park_preview_queue(dingosdk::queue_local_park_preview);
|
|
dingosdk::overlay::set_park_selection_queue(dingosdk::queue_local_park_selection);
|
|
dingosdk::overlay::set_park_paste_queue(dingosdk::queue_local_park_paste);
|
|
dingosdk::overlay::set_park_object_set_queue(dingosdk::queue_local_park_object_set);
|
|
dingosdk::overlay::set_park_delete_queue(dingosdk::queue_local_park_delete);
|
|
if (!DingoSDKOverlayStartV3(&callbacks))
|
|
return stop_startup(dingosdk::logging::Level::error, dingosdk::logging::Channel::graphics, "Overlay startup failed.");
|
|
// Install the offline boot hooks before the passive observers and other
|
|
// native hooks, while Skate's primary thread remains suspended. The
|
|
// launcher always sets the global-offline variable; install the dependent
|
|
// guard first in forwarding-only mode. Launches without it do not
|
|
// install this hook.
|
|
const bool global_offline_requested = exact_environment_one(
|
|
L"RESKATE_DEBUG_TEST_GLOBAL_OFFLINE");
|
|
const bool progression_guard_prepared = global_offline_requested &&
|
|
dingosdk::prepare_progression_service_guard(r.base);
|
|
record("{\"event\":\"progression_service_guard_prepared\",\"requested\":" +
|
|
std::string(global_offline_requested ? "true" : "false") +
|
|
",\"prepared\":" +
|
|
std::string(progression_guard_prepared ? "true" : "false") + "}");
|
|
const bool global_offline_hook = global_offline_requested &&
|
|
progression_guard_prepared && dingosdk::start_offline_boot(r.base);
|
|
record("{\"event\":\"global_offline_experiment_initialized\",\"active\":" +
|
|
std::string(global_offline_hook ? "true" : "false") +
|
|
",\"requested\":" +
|
|
std::string(global_offline_requested ? "true" : "false") +
|
|
",\"progression_guard_prepared\":" +
|
|
std::string(progression_guard_prepared ? "true" : "false") + "}");
|
|
const auto global_offline = dingosdk::offline_boot_observation();
|
|
record(global_offline.json);
|
|
const bool progression_guard_armed = global_offline_hook &&
|
|
dingosdk::arm_progression_service_guard(r.base);
|
|
record("{\"event\":\"progression_service_guard_initialized\",\"prepared\":" +
|
|
std::string(progression_guard_prepared ? "true" : "false") +
|
|
",\"armed\":" + std::string(progression_guard_armed ? "true" : "false") +
|
|
",\"global_offline_prerequisite\":" +
|
|
std::string(global_offline_hook ? "true" : "false") + "}");
|
|
const auto progression_guard = dingosdk::progression_service_guard_observation();
|
|
record(progression_guard.json);
|
|
if (global_offline_requested && (!global_offline_hook || !progression_guard_armed)) {
|
|
record("{\"event\":\"launcher_readiness\",\"ready\":false,"
|
|
"\"reason\":\"authored_offline_hooks\"}");
|
|
return stop_startup(dingosdk::logging::Level::error, dingosdk::logging::Channel::runtime,
|
|
std::format("The offline hooks could not be installed (offline boot {}, progression guard {}).",
|
|
global_offline_hook ? "ready" : "failed", progression_guard_armed ? "armed" : "not armed"));
|
|
}
|
|
const bool graphics_labels = dingosdk::initialize_graphics_labels(r.base,
|
|
global_offline_hook && progression_guard_armed);
|
|
record("{\"event\":\"graphics_labels_initialized\",\"active\":" +
|
|
std::string(graphics_labels ? "true" : "false") + "}");
|
|
std::string profile_failure;
|
|
if (global_offline_hook && progression_guard_armed &&
|
|
!dingosdk::initialize_local_profile(r.base, true, dingosdk::profile::default_path(), profile_failure)) {
|
|
record("{\"event\":\"launcher_readiness\",\"ready\":false,\"reason\":\"local_profile\"}");
|
|
return startup_failed(profile_failure);
|
|
}
|
|
if (global_offline_hook && progression_guard_armed) {
|
|
dingosdk::set_local_location_travel_queue(&queue_location_travel);
|
|
start_menu_entry();
|
|
}
|
|
start_frame_timing();
|
|
(void)dingosdk::ui_pointer::install(r.base);
|
|
(void)dingosdk::engine_tweaks::install(r.base);
|
|
(void)dingosdk::replay_guard::install(r.base);
|
|
(void)dingosdk::mesh_unload_guard::install(r.base);
|
|
(void)dingosdk::mesh_settings_guard::install(r.base);
|
|
(void)dingosdk::shader_permutation_guard::install(r.base);
|
|
(void)dingosdk::shader_registry_guard::install(r.base);
|
|
(void)dingosdk::light_table_guard::install(r.base);
|
|
(void)dingosdk::guid_table_guard::install(r.base);
|
|
(void)dingosdk::texture_handle_guard::install(r.base);
|
|
(void)dingosdk::physics_wake_guard::install(r.base);
|
|
(void)dingosdk::streaming_copy_guard::install(r.base);
|
|
(void)dingosdk::load_error_guard::install(r.base);
|
|
(void)dingosdk::texture_handle_limit::install(r.base);
|
|
#if !defined(DINGOSDK_LEAN_PLAYTEST)
|
|
(void)dingosdk::stack_check_report::install(r.base);
|
|
dingosdk::console::start_memory_trace_from_environment();
|
|
#endif
|
|
(void)dingosdk::music_inject::install(r.base);
|
|
(void)dingosdk::spotify_rows::install(r.base);
|
|
dingosdk::spotify_device::start(); // (the game as a device in the player's Spotify app)
|
|
dingosdk::spotify_web::start();
|
|
dingosdk::plex_web::start();
|
|
dingosdk::youtube_music::start(); // (the player's YouTube playlists, read before the music screen is built)
|
|
dingosdk::spotify_dashboard::start(); // (the page in the browser that sets Spotify up) // (the player's playlists, fetched before the music screen is built)
|
|
const bool fixed_stop_provider = (global_offline_hook && progression_guard_armed) &&
|
|
dingosdk::prepare_fixed_stop_entitlement_provider(r.base);
|
|
record("{\"event\":\"fixed_stop_entitlement_provider_initialized\",\"prepared\":" +
|
|
std::string(fixed_stop_provider ? "true" : "false") +
|
|
",\"requested\":" +
|
|
std::string(global_offline_requested ? "true" : "false") + "}");
|
|
const auto fixed_stop_provider_observation =
|
|
dingosdk::fixed_stop_entitlement_provider_observation();
|
|
record(fixed_stop_provider_observation.json);
|
|
r.last_fixed_stop_entitlement_provider_observation =
|
|
fixed_stop_provider_observation.json;
|
|
const bool entitlement_trace = (global_offline_hook && progression_guard_armed) &&
|
|
dingosdk::prepare_entitlement_request_trace(r.base);
|
|
record("{\"event\":\"entitlement_request_trace_initialized\",\"active\":" +
|
|
std::string(entitlement_trace ? "true" : "false") +
|
|
",\"requested\":" +
|
|
std::string(global_offline_requested ? "true" : "false") + "}");
|
|
const auto entitlement_trace_observation =
|
|
dingosdk::entitlement_request_trace_observation();
|
|
record(entitlement_trace_observation.json);
|
|
r.fixed_stop_entitlement_route_ready = fixed_stop_provider && entitlement_trace;
|
|
dingosdk::set_fixed_stop_entitlement_ownership(dingosdk::local_profile_owns_entitlement);
|
|
dingosdk::set_fixed_stop_entitlement_provider_enabled(
|
|
r.fixed_stop_entitlement_route_ready && dingosdk::local_profile_access().bus_stops);
|
|
record("{\"event\":\"fixed_stop_entitlement_route_initialized\",\"ready\":" +
|
|
std::string(r.fixed_stop_entitlement_route_ready ? "true" : "false") +
|
|
",\"provider_prepared\":" +
|
|
std::string(fixed_stop_provider ? "true" : "false") +
|
|
",\"trace_prepared\":" +
|
|
std::string(entitlement_trace ? "true" : "false") + "}");
|
|
const bool neighborhood_unlock_provider =
|
|
(global_offline_hook && progression_guard_armed) &&
|
|
dingosdk::prepare_neighborhood_unlock_override(r.base);
|
|
if (neighborhood_unlock_provider)
|
|
dingosdk::set_expression_binder_observer(&observe_player_card_expression);
|
|
if (neighborhood_unlock_provider)
|
|
(void)dingosdk::set_neighborhood_unlock_override_enabled(dingosdk::local_profile_access().neighborhoods);
|
|
record("{\"event\":\"neighborhood_unlock_provider_initialized\",\"prepared\":" +
|
|
std::string(neighborhood_unlock_provider ? "true" : "false") +
|
|
",\"requested\":" +
|
|
std::string(global_offline_requested ? "true" : "false") + "}");
|
|
const auto neighborhood_unlock_provider_observation =
|
|
dingosdk::neighborhood_unlock_override_observation();
|
|
const bool named_settings = dingosdk::initialize_named_settings(r.base);
|
|
record("{\"event\":\"named_settings_initialized\",\"ready\":" + std::string(named_settings ? "true" : "false") + "}");
|
|
const bool gameplay_settings = dingosdk::initialize_gameplay_settings_override(
|
|
r.base, global_offline_hook && progression_guard_armed);
|
|
const bool skater_slots = dingosdk::initialize_skater_slot_override(
|
|
r.base, global_offline_hook && progression_guard_armed);
|
|
const bool main_missions = (global_offline_hook && progression_guard_armed) &&
|
|
dingosdk::prepare_main_mission_override(r.base);
|
|
const auto gameplay_settings_observation =
|
|
dingosdk::gameplay_settings_override_observation();
|
|
const auto skater_slot_observation =
|
|
dingosdk::skater_slot_override_observation();
|
|
const auto main_mission_observation =
|
|
dingosdk::main_mission_override_observation();
|
|
record(gameplay_settings_observation.json);
|
|
record(skater_slot_observation.json);
|
|
record(main_mission_observation.json);
|
|
r.last_gameplay_settings_override_observation =
|
|
gameplay_settings_observation.json;
|
|
r.last_skater_slot_override_observation = skater_slot_observation.json;
|
|
r.last_main_mission_override_observation = main_mission_observation.json;
|
|
{
|
|
std::lock_guard lock(r.mutex);
|
|
r.offline_model = gameplay_settings_observation.model;
|
|
merge_skater_slot_observation(r.offline_model, skater_slot_observation);
|
|
merge_main_mission_observation(r.offline_model, main_mission_observation);
|
|
merge_progression_provider_observations(r.offline_model,
|
|
fixed_stop_provider_observation,
|
|
neighborhood_unlock_provider_observation,
|
|
r.fixed_stop_entitlement_route_ready);
|
|
++r.offline_revision;
|
|
}
|
|
record("{\"event\":\"gameplay_settings_override_initialized\",\"active\":" +
|
|
std::string(gameplay_settings ? "true" : "false") +
|
|
",\"authored_offline_prerequisite\":" +
|
|
std::string(global_offline_hook && progression_guard_armed ? "true" : "false") + "}");
|
|
record("{\"event\":\"skater_slot_override_initialized\",\"active\":" +
|
|
std::string(skater_slots ? "true" : "false") +
|
|
",\"authored_offline_prerequisite\":" +
|
|
std::string(global_offline_hook && progression_guard_armed ? "true" : "false") + "}");
|
|
record("{\"event\":\"main_mission_override_initialized\",\"prepared\":" +
|
|
std::string(main_missions ? "true" : "false") +
|
|
",\"authored_offline_prerequisite\":" +
|
|
std::string(global_offline_hook && progression_guard_armed ? "true" : "false") + "}");
|
|
const bool skater_hooks = dingosdk::start_skater_observer(r.base);
|
|
record("{\"event\":\"skater_hooks_initialized\",\"active\":" + std::string(skater_hooks ? "true" : "false") + "}");
|
|
const bool camera_hook = dingosdk::start_camera_observer(r.base);
|
|
record("{\"event\":\"camera_observer_initialized\",\"active\":" + std::string(camera_hook ? "true" : "false") + "}");
|
|
(void)dingosdk::start_no_bail(r.base);
|
|
const bool plugins = dingosdk::plugins::start(r.base);
|
|
record("{\"event\":\"plugins_initialized\",\"active\":" + std::string(plugins ? "true" : "false") + "}");
|
|
// What plugins reach of the skater's physics, its x-ray and skate.'s HUD corner; the game's
|
|
// pictures are read now, as the overlay builds its atlas once.
|
|
const bool physics = dingosdk::skater_physics::start(r.base);
|
|
const bool xray = dingosdk::xray::start(r.base);
|
|
dingosdk::hud_corner::start_hud(r.base);
|
|
dingosdk::overlay::prepare_game_images();
|
|
record("{\"event\":\"skater_physics_initialized\",\"active\":" + std::string(physics ? "true" : "false") + ",\"xray\":" +
|
|
std::string(xray ? "true" : "false") + "}");
|
|
const bool launch_hooks = dingosdk::launches::start(r.base);
|
|
record("{\"event\":\"launches_initialized\",\"active\":" + std::string(launch_hooks ? "true" : "false") + "}");
|
|
const bool trick_rewrites = dingosdk::trick_rewrite::start(r.base);
|
|
record("{\"event\":\"trick_rewrite_initialized\",\"active\":" + std::string(trick_rewrites ? "true" : "false") + "}");
|
|
const bool noclip_velocity = dingosdk::start_client_noclip_velocity(r.base);
|
|
record("{\"event\":\"noclip_velocity_initialized\",\"active\":" + std::string(noclip_velocity ? "true" : "false") + "}");
|
|
const bool loading_screens = dingosdk::loading_screen::start(r.base);
|
|
record("{\"event\":\"map_loading_screens_initialized\",\"active\":" + std::string(loading_screens ? "true" : "false") + "}");
|
|
const bool preset_guard = dingosdk::preset_lookup_guard::start(r.base);
|
|
record("{\"event\":\"preset_lookup_guard_initialized\",\"active\":" + std::string(preset_guard ? "true" : "false") + "}");
|
|
const bool replay_export = dingosdk::replay_export::start(r.base);
|
|
record("{\"event\":\"replay_export_initialized\",\"active\":" + std::string(replay_export ? "true" : "false") + "}");
|
|
const bool replay_keyframes = dingosdk::replay_keyframes::start(r.base);
|
|
record("{\"event\":\"replay_keyframes_initialized\",\"active\":" + std::string(replay_keyframes ? "true" : "false") + "}");
|
|
void* original{};
|
|
auto* address = reinterpret_cast<void*>(r.base + rt::client_tick);
|
|
auto hook_status = dingosdk::hook_prepare(address, reinterpret_cast<void*>(&tick), &original);
|
|
if (hook_status == dingosdk::HookOk) {
|
|
r.original_tick = reinterpret_cast<Tick>(original);
|
|
hook_status = dingosdk::hook_enable(address);
|
|
if (hook_status != dingosdk::HookOk) dingosdk::hook_remove(address);
|
|
}
|
|
if (hook_status != dingosdk::HookOk) {
|
|
// The renderer is already installed. Keep it available for diagnostics
|
|
// and report successful optional loading without claiming native control.
|
|
{
|
|
std::lock_guard lock(r.mutex);
|
|
r.model.state = "Native controller unavailable";
|
|
r.model.load_block_reason = "The engine update hook could not be initialized.";
|
|
r.offline_model.available = false;
|
|
r.offline_model.status =
|
|
"Gameplay settings require the native game update hook.";
|
|
++r.model_revision;
|
|
++r.offline_revision;
|
|
r.initialized = !global_offline_requested;
|
|
}
|
|
record("{\"event\":\"native_hook_failed\",\"status\":" + std::to_string(hook_status) + "}");
|
|
if (global_offline_requested) {
|
|
record("{\"event\":\"launcher_readiness\",\"ready\":false,"
|
|
"\"reason\":\"native_tick_hook\"}");
|
|
return stop_startup(dingosdk::logging::Level::error, dingosdk::logging::Channel::runtime,
|
|
std::format("The engine update hook could not be installed: {} ({}).",
|
|
dingosdk::hook_status_string(hook_status), static_cast<long>(hook_status)));
|
|
}
|
|
startup_window.hand_over();
|
|
return TRUE;
|
|
}
|
|
{
|
|
std::lock_guard lock(r.mutex);
|
|
r.native_tick_ready = true;
|
|
}
|
|
r.native_loading_logging = dingosdk::start_level_loading_logging(r.base,
|
|
[](std::uintptr_t base, unsigned next) noexcept {
|
|
dingosdk::local_profile_before_level_transition(next);
|
|
dingosdk::world_entities::before_level_transition(base, next);
|
|
dingosdk::multiplayer::native_menu_before_level_transition(base, next);
|
|
dingosdk::multiplayer::throwdown_lab_before_level_transition(next);
|
|
});
|
|
if (!r.native_loading_logging) dingosdk::logging::write(dingosdk::logging::Level::warning,
|
|
dingosdk::logging::Channel::ui, "Native ReSkate menu disabled: shutdown cleanup hook is unavailable.");
|
|
r.initialized = true;
|
|
if (global_offline_requested)
|
|
record("{\"event\":\"launcher_readiness\",\"ready\":true}");
|
|
record("{\"event\":\"debug_initialized\",\"overlay\":true,\"native_load\":\"guarded_local_only\"}");
|
|
activity_line(dingosdk::ConsoleSource::runtime, "Offline runtime initialized. Waiting for the game.");
|
|
startup_window.hand_over();
|
|
return TRUE;
|
|
} catch (const std::exception& error) {
|
|
// Nothing may escape the launcher's remote thread: a reason that cannot
|
|
// be built falls back to a fixed one.
|
|
using namespace dingosdk::runtime::detail;
|
|
try {
|
|
return stop_startup(dingosdk::logging::Level::critical, dingosdk::logging::Channel::runtime,
|
|
std::string("Runtime initialization failed: ") + error.what());
|
|
} catch (...) {
|
|
return stop_startup(dingosdk::logging::Level::critical, dingosdk::logging::Channel::runtime,
|
|
"Runtime initialization failed.");
|
|
}
|
|
} catch (...) {
|
|
using namespace dingosdk::runtime::detail;
|
|
return stop_startup(dingosdk::logging::Level::critical, dingosdk::logging::Channel::runtime,
|
|
"Runtime initialization failed with an unknown exception.");
|
|
}
|
|
}
|
|
|
|
BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID) {
|
|
if (reason == DLL_PROCESS_ATTACH) DisableThreadLibraryCalls(instance);
|
|
return TRUE;
|
|
}
|