mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 12:57:01 +02:00
Guard a missing shader preset, end a stuck close, unload guard off
Costume crash: the skater scripts' item:addEsShaderPreset uses the preset it is handed without checking that it is loaded, and the game crashed in the material's construction shortly after arriving on a server's map while wearing a modded costume. The call is now checked first: an item whose preset is not there is shown without it, and the log says so. Stuck close: once the player closes the game, a watch starts. If the window has gone or stopped answering and the process is still there 8 seconds later, the process is ended. A window that still answers (the close was turned down) is left alone, and the watch stands down after two minutes. Level unload guard: switched off. Skipping the entry that crashed left its reference registered, so leaving the level never finished and the game hung with the world unloaded. The crash it covered is back until the reference is released properly.
This commit is contained in:
1 parent
8452f8871f
commit
978c2a2430
8 files changed
+124
-6
No files matched your search
@@ -35,4 +35,18 @@ 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};
|
||||
// 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<unsigned char, 16> 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<unsigned char, 8> script_preset_reference_prefix{0x4c,0x8d,0x05,0x81,0xa2,0x0d,0x03,0xe9};
|
||||
inline constexpr std::uintptr_t referenced_preset = 0x16fe9c0;
|
||||
inline constexpr std::array<unsigned char, 17> referenced_preset_prefix{
|
||||
0x48,0x8b,0x09,0x48,0x85,0xc9,0x74,0x07,0x48,0x8b,0x01,0x48,0xff,0x60,0x18,0x33,0xc0};
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#include "exit_watch.h"
|
||||
#include "Engine/Core/Log/logging.h"
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
|
||||
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<bool> watching{};
|
||||
std::atomic<ULONGLONG> 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
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <Windows.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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<ScriptFunction> original_add_preset{};
|
||||
std::uintptr_t image{};
|
||||
int add_preset(void *state) {
|
||||
const auto reference = reinterpret_cast<void *(*)(void *, int)>(image + native::script_preset_reference)(state, 3);
|
||||
if (reference && reinterpret_cast<void *(*)(void *, int)>(image + native::referenced_preset)(reference, 0))
|
||||
return original_add_preset.load()(state);
|
||||
static std::atomic<unsigned> 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<std::size_t Size> bool matches(std::uintptr_t address, const std::array<unsigned char, Size> &prefix) {
|
||||
std::array<unsigned char, Size> 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<void *>(base + native::add_shader_preset);
|
||||
void *previous{};
|
||||
if (hook_prepare(target, reinterpret_cast<void *>(&add_preset), &previous) != HookOk) return false;
|
||||
original_add_preset = reinterpret_cast<ScriptFunction>(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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 <string>
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user