diff --git a/Engine/Game/Build/20260929/native_cosmetics.h b/Engine/Game/Build/20260929/native_cosmetics.h index 97f06930..c169c63b 100644 --- a/Engine/Game/Build/20260929/native_cosmetics.h +++ b/Engine/Game/Build/20260929/native_cosmetics.h @@ -35,4 +35,18 @@ inline constexpr std::uintptr_t named_lookup = 0x16fc900; inline constexpr std::array 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}; +// The skater scripts' item:addEsShaderPreset(material, preset): a Lua function (state) -> count +// of results. Its third argument is a reference to the loaded preset, which it uses without +// checking that the preset is there: an item whose preset has gone (or never loaded) by the +// time its script runs crashes the game in the material's construction. +inline constexpr std::uintptr_t add_shader_preset = 0x52cb70; +inline constexpr std::array add_shader_preset_prefix{ + 0x40,0x53,0x55,0x56,0x57,0x41,0x56,0x41,0x57,0x48,0x83,0xec,0x28,0x48,0x8b,0xd9}; +// The two calls it makes to read that argument: (state, argument number) -> the reference, and +// (reference, 0) -> the preset it holds, or null. +inline constexpr std::uintptr_t script_preset_reference = 0x336f060; +inline constexpr std::array script_preset_reference_prefix{0x4c,0x8d,0x05,0x81,0xa2,0x0d,0x03,0xe9}; +inline constexpr std::uintptr_t referenced_preset = 0x16fe9c0; +inline constexpr std::array referenced_preset_prefix{ + 0x48,0x8b,0x09,0x48,0x85,0xc9,0x74,0x07,0x48,0x8b,0x01,0x48,0xff,0x60,0x18,0x33,0xc0}; } diff --git a/Extension/Boot/exit_watch.cpp b/Extension/Boot/exit_watch.cpp new file mode 100644 index 00000000..b329111e --- /dev/null +++ b/Extension/Boot/exit_watch.cpp @@ -0,0 +1,52 @@ +#include "exit_watch.h" +#include "Engine/Core/Log/logging.h" +#include +#include + +namespace dingosdk::exit_watch { +namespace { +// How long a close may take once the window is gone or has stopped answering, and how long a +// window that still answers is watched (the game may ask before it closes, and be told no). +constexpr ULONGLONG grace_ms = 8000, watch_ms = 120000; +std::atomic watching{}; +std::atomic destroyed_at{}; + +void watch(HWND window) noexcept { + const auto asked = GetTickCount64(); + ULONGLONG stuck_since{}; + for (;;) { + Sleep(500); + const auto now = GetTickCount64(); + DWORD_PTR result{}; + // Gone, or not taking messages: the close is under way, or stuck. + const bool answers = !destroyed_at.load() && IsWindow(window) && + SendMessageTimeoutW(window, WM_NULL, 0, 0, SMTO_ABORTIFHUNG, 1000, &result); + if (answers) { + stuck_since = 0; + // Still the game as it was: the close was turned down. Watch the next one afresh. + if (now - asked >= watch_ms) { watching = false; return; } + continue; + } + if (!stuck_since) stuck_since = now; + if (now - stuck_since < grace_ms) continue; + logging::log(logging::Level::warning, logging::Channel::runtime, + "The game did not finish closing {} s after it was asked to; ending it.", (now - asked) / 1000); + logging::flush(); + TerminateProcess(GetCurrentProcess(), 0); + } +} +} // namespace + +void note_window_message(HWND window, UINT message, WPARAM wp) noexcept { + const bool closing = message == WM_CLOSE || message == WM_DESTROY || (message == WM_SYSCOMMAND && (wp & 0xfff0) == SC_CLOSE) || + (message == WM_ENDSESSION && wp); + if (!closing) return; + if (message == WM_DESTROY) destroyed_at = GetTickCount64(); + if (watching.exchange(true)) return; + try { + std::thread(watch, window).detach(); + } catch (...) { + watching = false; + } +} +} // namespace dingosdk::exit_watch diff --git a/Extension/Boot/exit_watch.h b/Extension/Boot/exit_watch.h new file mode 100644 index 00000000..94d7c433 --- /dev/null +++ b/Extension/Boot/exit_watch.h @@ -0,0 +1,10 @@ +#pragma once +#include + +namespace dingosdk::exit_watch { +// The game sometimes never finishes closing: its window goes (or stops answering) and the +// process stays, holding the game's files and Steam's "running" state. Once the player has +// asked the game to close, this gives it a few seconds and then ends the process. +// Called from the game window's procedure with each message of that window. +void note_window_message(HWND window, UINT message, WPARAM wp) noexcept; +} diff --git a/Extension/Customization/preset_lookup_guard.cpp b/Extension/Customization/preset_lookup_guard.cpp index 50aa2f03..51eb239e 100644 --- a/Extension/Customization/preset_lookup_guard.cpp +++ b/Extension/Customization/preset_lookup_guard.cpp @@ -34,6 +34,43 @@ void **lookup(void **out, const char *name, void *table, std::uint8_t flag, void text.empty() ? "unnamed" : text); return out; } +// item:addEsShaderPreset(material, preset) with a preset that is not there: nothing is added, +// and the script goes on as it does after any other call. +using ScriptFunction = int (*)(void *state); +std::atomic original_add_preset{}; +std::uintptr_t image{}; +int add_preset(void *state) { + const auto reference = reinterpret_cast(image + native::script_preset_reference)(state, 3); + if (reference && reinterpret_cast(image + native::referenced_preset)(reference, 0)) + return original_add_preset.load()(state); + static std::atomic seen{}; + const auto count = seen.fetch_add(1, std::memory_order_relaxed) + 1; + if (count <= 5 || count % 500 == 0) + logging::log(logging::Level::warning, logging::Channel::customization, + "An item's shader preset was not loaded when its script used it ({} so far); the item is shown without it.", + count); + return 0; +} +template bool matches(std::uintptr_t address, const std::array &prefix) { + std::array bytes{}; + return memory::read_bytes(address, bytes.data(), bytes.size()) && bytes == prefix; +} +bool start_add_preset(std::uintptr_t base) { + if (!matches(base + native::add_shader_preset, native::add_shader_preset_prefix) || + !matches(base + native::script_preset_reference, native::script_preset_reference_prefix) || + !matches(base + native::referenced_preset, native::referenced_preset_prefix)) + return false; + image = base; + auto *target = reinterpret_cast(base + native::add_shader_preset); + void *previous{}; + if (hook_prepare(target, reinterpret_cast(&add_preset), &previous) != HookOk) return false; + original_add_preset = reinterpret_cast(previous); + if (hook_enable(target) != HookOk) { + hook_remove(target); + return false; + } + return true; +} } // namespace bool start(std::uintptr_t base) noexcept { @@ -50,7 +87,7 @@ bool start(std::uintptr_t base) noexcept { hook_remove(target); return false; } - return true; + return start_add_preset(base); } catch (...) { return false; } diff --git a/Extension/Customization/preset_lookup_guard.h b/Extension/Customization/preset_lookup_guard.h index 803e80e5..c9f62e91 100644 --- a/Extension/Customization/preset_lookup_guard.h +++ b/Extension/Customization/preset_lookup_guard.h @@ -4,6 +4,7 @@ namespace dingosdk::preset_lookup_guard { // A preset whose bundle cannot be found (a cosmetic mod's registration missing // from the character bundle-reference table) is treated as not found instead -// of crashing the game in the named lookup. +// of crashing the game in the named lookup. So is an item's shader preset that is +// not loaded when the item's script comes to use it: the item goes without it. bool start(std::uintptr_t base) noexcept; } diff --git a/Extension/UI/Overlay/overlay_input.cpp b/Extension/UI/Overlay/overlay_input.cpp index 95ee5c3f..b703e847 100644 --- a/Extension/UI/Overlay/overlay_input.cpp +++ b/Extension/UI/Overlay/overlay_input.cpp @@ -1,5 +1,6 @@ #include "Engine/Core/Platform/launcher_support.h" #include "Engine/Core/Log/logging.h" +#include "Extension/Boot/exit_watch.h" #include "overlay_internal.h" #include "input_capture.h" #include "playstation_input.h" @@ -120,6 +121,8 @@ LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM wp, LPARAM lp) { if (window != s.window.load()) return previous ? CallWindowProcW(previous, window, message, wp, lp) : DefWindowProcW(window, message, wp, lp); + // The player closing the game: it is ended if it does not finish by itself. + dingosdk::exit_watch::note_window_message(window, message, wp); struct ReleaseOnOpen { HWND window; WNDPROC previous; bool was_visible; ~ReleaseOnOpen() { diff --git a/Runtime/bootstrap.cpp b/Runtime/bootstrap.cpp index 92a310f3..b62b5a9e 100644 --- a/Runtime/bootstrap.cpp +++ b/Runtime/bootstrap.cpp @@ -15,7 +15,6 @@ #include "Extension/Assets/native_render_resource_pool.h" #include "Extension/Rendering/display_startup.h" #include "Extension/World/native_route_lookahead.h" -#include "Extension/World/unload_guard.h" #include "Extension/World/native_entity_pages.h" #include "Extension/World/physics_world_size.h" #include @@ -58,9 +57,10 @@ bool initialize_bootstrap(std::uintptr_t base) { if (!stage(Channel::world, "Physics world pools (65000 static bodies)", ready, error)) return false; error.clear(); ready = start_native_route_lookahead(base, error); if (!stage(Channel::world, "NPC route cycle and endpoint guards", ready, error)) return false; - // A guard against one of the game's own crashes: without it the game runs as shipped. - error.clear(); ready = start_unload_guard(base, error); - (void)stage(Channel::world, "Level unload crash guard", ready, error); + // The level unload guard (unload_guard.h) is not installed: stepping over an entry with no + // asset stopped the crash, but that entry had been registered when the level loaded, and + // left registered it kept the level from ever finishing its unload (a map change that never + // ends, which is worse than the crash). It goes back in once it can release what it skips. if (!stage(Channel::graphics, "Display startup settings", start_display_settings(base), {})) return false; error.clear(); ready = start_user_data_redirect(error); if (!stage(Channel::runtime, "Separate game user data", ready, error)) return false; diff --git a/cmake/Runtime.cmake b/cmake/Runtime.cmake index 482299fc..563c1056 100644 --- a/cmake/Runtime.cmake +++ b/cmake/Runtime.cmake @@ -97,6 +97,7 @@ add_library(dingosdk_runtime SHARED Extension/World/level_loading.cpp Extension/World/loading_screen.cpp Extension/Customization/preset_lookup_guard.cpp + Extension/Boot/exit_watch.cpp Extension/Customization/developer_hoodie.cpp Extension/Customization/developer_board.cpp Extension/Skater/skater_model.cpp