mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-23 04:35:54 +02:00
crash fix
This commit is contained in:
1 parent
8f92b41a78
commit
290089e16d
9 files changed
+170
-9
No files matched your search
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
#include "Engine/Game/Build/fingerprint.h"
|
||||
#include <cstdint>
|
||||
|
||||
// A mesh's render settings (Extension/World/mesh_settings_guard.cpp).
|
||||
namespace dingosdk::game::build::v20260929::mesh_settings {
|
||||
// apply_settings(mesh, settings): as a level's meshes load (0x3f59610, for each mesh whose flag
|
||||
// at + 0x3b0 is clear), it builds the settings object of the mesh's asset in place at mesh + 0x340
|
||||
// (0x70 bytes, a reflected instance of 0x76f26f0) when the asset has one; settings is that
|
||||
// object's source, null when it has none. Its caller sets the flag either way.
|
||||
inline constexpr Fingerprint apply_settings{0x3f5beb0, {
|
||||
0x48,0x89,0x5c,0x24,0x10,0x57,0x48,0x83,0xec,0x60,0x80,0xb9,0xb0,0x03,0x00,0x00,
|
||||
0x00,0x48,0x8b,0xfa,0x48,0x8b,0xd9,0x74,0x20,0x48,0x81,0xc1,0x40,0x03,0x00,0x00}};
|
||||
// The mesh (0x3c0 bytes, made at 0x3f33770) leaves + 0x340 to + 0x3af as the allocation had it.
|
||||
inline constexpr std::ptrdiff_t settings_offset = 0x340;
|
||||
// The settings object's string the mesh frees as it unloads (0x3f5fa20, through 0x1584c50,
|
||||
// whenever the flag is set): with no settings object it is whatever the allocation left there.
|
||||
inline constexpr std::ptrdiff_t unload_string_offset = 0x360;
|
||||
inline constexpr std::ptrdiff_t applied_flag_offset = 0x3b0;
|
||||
// The game's empty string: a string that holds it frees nothing (0x1584c50 compares with it).
|
||||
inline constexpr std::uintptr_t empty_string = 0x72fe704;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#include "mesh_settings_guard.h"
|
||||
#include "Engine/Core/Hooks/hooks.h"
|
||||
#include "Engine/Core/Log/logging.h"
|
||||
#include "Engine/Core/Platform/memory.h"
|
||||
#include "Engine/Game/Build/addresses.h"
|
||||
#include "Engine/Game/Build/20260929/mesh_settings.h"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <cstring>
|
||||
|
||||
namespace dingosdk::mesh_settings_guard {
|
||||
namespace {
|
||||
namespace ms = addr::mesh_settings;
|
||||
using ApplySettings = void (*)(std::byte*, void*);
|
||||
ApplySettings original{};
|
||||
std::uintptr_t empty{};
|
||||
std::atomic<bool> installed{};
|
||||
std::atomic<std::uint64_t> missing{}, stale{};
|
||||
|
||||
// (A count worth a line in the log: the first few, then each power of two.)
|
||||
__declspec(noinline) void note(std::uint64_t count, std::uint64_t held, std::uintptr_t value) noexcept {
|
||||
if (count > 4 && !std::has_single_bit(count)) return;
|
||||
try {
|
||||
logging::log(logging::Level::info, logging::Channel::runtime,
|
||||
"Mesh settings: {} mesh(es) have none ({} held something the game would have freed on unload; this one {:#x}); they are "
|
||||
"given an empty string.", count, held, value);
|
||||
} catch (...) {}
|
||||
}
|
||||
|
||||
void guarded(std::byte* mesh, void* settings) {
|
||||
// Only a mesh about to be marked as having settings it will not get: the string its unload
|
||||
// frees is made the empty one. A mesh that gets its settings has the string made with them.
|
||||
if (mesh && !settings && mesh[ms::applied_flag_offset] == std::byte{0}) {
|
||||
std::uintptr_t held{};
|
||||
std::memcpy(&held, mesh + ms::unload_string_offset, sizeof(held));
|
||||
std::memcpy(mesh + ms::unload_string_offset, &empty, sizeof(empty));
|
||||
const bool would_free = held != 0 && held != empty;
|
||||
const auto count = missing.fetch_add(1, std::memory_order_relaxed) + 1;
|
||||
const auto freed = stale.fetch_add(would_free ? 1 : 0, std::memory_order_relaxed) + (would_free ? 1 : 0);
|
||||
note(count, freed, held);
|
||||
}
|
||||
original(mesh, settings);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool install(std::uintptr_t base) noexcept {
|
||||
if (installed.exchange(true)) return original != nullptr;
|
||||
if (!base) return false;
|
||||
std::array<unsigned char, 32> head{};
|
||||
if (!memory::peek(base + ms::apply_settings.rva, head) || head != ms::apply_settings.bytes) {
|
||||
logging::log(logging::Level::warning, logging::Channel::runtime,
|
||||
"Mesh settings guard: this build's mesh settings code differs; it is left as it is.");
|
||||
return false;
|
||||
}
|
||||
empty = base + ms::empty_string;
|
||||
const auto target = reinterpret_cast<void*>(base + ms::apply_settings.rva);
|
||||
if (hook_prepare(target, reinterpret_cast<void*>(&guarded), reinterpret_cast<void**>(&original)) != HookOk ||
|
||||
hook_enable(target) != HookOk) {
|
||||
original = nullptr;
|
||||
logging::log(logging::Level::warning, logging::Channel::runtime, "Mesh settings guard: the game's function could not be hooked.");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::uint64_t without_settings() noexcept { return missing.load(std::memory_order_relaxed); }
|
||||
std::uint64_t would_have_freed() noexcept { return stale.load(std::memory_order_relaxed); }
|
||||
} // namespace dingosdk::mesh_settings_guard
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
// Leaving a level crashed the game in its allocator (Skate.exe+0x3d9ed9d, +0x3d9ee43, +0x3d9a856)
|
||||
// as it freed a string of each unloading mesh (Engine/Game/Build/<build>/mesh_settings.h). The
|
||||
// string belongs to the settings object the game builds inside the mesh; a mesh whose asset has no
|
||||
// settings object is marked as having one anyway, and its string is whatever that memory held
|
||||
// before (often a pointer freed long ago). The guard gives such a mesh the game's empty string
|
||||
// first, so its unload frees nothing.
|
||||
namespace dingosdk::mesh_settings_guard {
|
||||
// Checks this build's function and hooks it; once. False when the build differs (no guard).
|
||||
bool install(std::uintptr_t base) noexcept;
|
||||
// Meshes without a settings object since the game started, and how many of them held something
|
||||
// the game would have freed.
|
||||
std::uint64_t without_settings() noexcept;
|
||||
std::uint64_t would_have_freed() noexcept;
|
||||
}
|
||||
+40
-9
@@ -1,10 +1,13 @@
|
||||
#include "gui_internal.h"
|
||||
#include "gui_renderer.h"
|
||||
|
||||
#include "mod_manager.h"
|
||||
|
||||
#include "problem.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <format>
|
||||
|
||||
// The main screen: background, name plate, tiles and the STATUS tile.
|
||||
@@ -120,21 +123,47 @@ void progress_bar(ImDrawList* draw, ImVec2 a, ImVec2 b, float fill, float time)
|
||||
skate_theme::striped_bar(draw, a, b, fill, time, g_scale);
|
||||
}
|
||||
|
||||
// Loads the signed-in account's Steam picture for the plate, or drops it when Steam is closed or
|
||||
// signed out. Only re-decoded when the cached file changes.
|
||||
void refresh_steam_avatar(Ui& ui) {
|
||||
const auto path = launcher_app::steam_avatar_path();
|
||||
std::error_code error;
|
||||
const auto time = path.empty() ? fs::file_time_type{} : fs::last_write_time(path, error);
|
||||
if (path == ui.steam_avatar_path && time == ui.steam_avatar_time) return;
|
||||
if (ui.steam_avatar && g_renderer) g_renderer->release_texture(ui.steam_avatar);
|
||||
ui.steam_avatar = {};
|
||||
ui.steam_avatar_path = path;
|
||||
ui.steam_avatar_time = time;
|
||||
if (path.empty() || !g_renderer) return;
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
const std::vector<unsigned char> bytes{std::istreambuf_iterator<char>(input), std::istreambuf_iterator<char>()};
|
||||
std::vector<unsigned char> pixels;
|
||||
UINT width{}, height{};
|
||||
if (!bytes.empty() && decode_image(bytes, ImVec2(S(46), S(46)), pixels, width, height))
|
||||
ui.steam_avatar = g_renderer->upload_texture(pixels, width, height);
|
||||
}
|
||||
|
||||
// Name plate in the top-left corner, like the HUB's player card.
|
||||
void draw_plate(ImDrawList* draw, const Fonts& fonts, ImVec2 position, const std::string& title, const std::string& version) {
|
||||
void draw_plate(ImDrawList* draw, const Fonts& fonts, ImVec2 position, const std::string& title, const std::string& version,
|
||||
ImTextureID avatar) {
|
||||
const float h = S(46);
|
||||
const float text = std::max(fonts.bold->CalcTextSizeA(fonts.bold->FontSize, FLT_MAX, 0, title.c_str()).x,
|
||||
fonts.body->CalcTextSizeA(fonts.body->FontSize, FLT_MAX, 0, version.c_str()).x);
|
||||
const float width = std::max(S(300), h + S(28) + text);
|
||||
draw->AddRectFilled(ImVec2(position.x + h, position.y + S(3)), ImVec2(position.x + width, position.y + h - S(3)),
|
||||
rgba(44, 47, 49, 0.94f));
|
||||
draw->AddRectFilled(position, ImVec2(position.x + h, position.y + h), color::avatar);
|
||||
// A small deck on the avatar tile.
|
||||
const ImVec2 c(position.x + h * 0.5f, position.y + h * 0.5f);
|
||||
const int start = draw->VtxBuffer.Size;
|
||||
draw->AddRectFilled(ImVec2(c.x - S(15), c.y - S(5.5f)), ImVec2(c.x + S(15), c.y + S(5.5f)), color::text, S(5.5f));
|
||||
for (const float x : {-9.0f, 9.0f}) draw->AddCircleFilled(ImVec2(c.x + S(x), c.y), S(1.8f), color::avatar);
|
||||
rotate_since(draw, start, -40.0f, c);
|
||||
if (avatar) {
|
||||
// The player's Steam picture in the tile.
|
||||
draw->AddImage(avatar, position, ImVec2(position.x + h, position.y + h));
|
||||
} else {
|
||||
draw->AddRectFilled(position, ImVec2(position.x + h, position.y + h), color::avatar);
|
||||
// A small deck on the avatar tile.
|
||||
const ImVec2 c(position.x + h * 0.5f, position.y + h * 0.5f);
|
||||
const int start = draw->VtxBuffer.Size;
|
||||
draw->AddRectFilled(ImVec2(c.x - S(15), c.y - S(5.5f)), ImVec2(c.x + S(15), c.y + S(5.5f)), color::text, S(5.5f));
|
||||
for (const float x : {-9.0f, 9.0f}) draw->AddCircleFilled(ImVec2(c.x + S(x), c.y), S(1.8f), color::avatar);
|
||||
rotate_since(draw, start, -40.0f, c);
|
||||
}
|
||||
draw->AddCircleFilled(ImVec2(position.x + S(3), position.y + S(3)), S(5), color::good);
|
||||
draw->AddText(fonts.bold, fonts.bold->FontSize, ImVec2(position.x + h + S(12), position.y + S(5)), color::text, title.c_str());
|
||||
draw->AddText(fonts.body, fonts.body->FontSize, ImVec2(position.x + h + S(12), position.y + S(24)), color::muted, version.c_str());
|
||||
@@ -393,6 +422,7 @@ void frame(Launcher& launcher, const Fonts& fonts, HWND window, Ui& ui, ModsPane
|
||||
// from the first Steam check; steam_offline_seen keeps it to one decision.
|
||||
if (time - ui.steam_checked > 5) {
|
||||
ui.steam_name = launcher_app::steam_persona_name();
|
||||
refresh_steam_avatar(ui);
|
||||
ui.steam_checked = time;
|
||||
if (!ui.steam_offline_seen) {
|
||||
ui.steam_offline_seen = true;
|
||||
@@ -417,7 +447,8 @@ void frame(Launcher& launcher, const Fonts& fonts, HWND window, Ui& ui, ModsPane
|
||||
if (update::binary_updates_enabled() && state.config && !state.config->launcher.version.empty())
|
||||
version = "Launcher " + state.config->launcher.version;
|
||||
const bool greet = !launcher.settings().offline && !ui.steam_name.empty();
|
||||
draw_plate(draw, fonts, ImVec2(S(50), S(40)), greet ? "Welcome, " + ui.steam_name : std::string("ReSkate"), version);
|
||||
draw_plate(draw, fonts, ImVec2(S(50), S(40)), greet ? "Welcome, " + ui.steam_name : std::string("ReSkate"), version,
|
||||
launcher.settings().offline ? ImTextureID{} : ui.steam_avatar);
|
||||
page_title(draw, fonts, ImVec2(S(52), S(88)), "RESKATE");
|
||||
|
||||
window_buttons(draw, window, size);
|
||||
|
||||
@@ -377,6 +377,11 @@ struct Ui {
|
||||
// Steam display name for the name plate, re-read every few seconds.
|
||||
std::string steam_name;
|
||||
double steam_checked{-100};
|
||||
// The account's Steam picture on the plate in place of the drawn logo; reloaded
|
||||
// when Steam caches a new one or another account signs in.
|
||||
ImTextureID steam_avatar{};
|
||||
fs::path steam_avatar_path;
|
||||
fs::file_time_type steam_avatar_time{};
|
||||
// One-time notice that Skate will start offline because Steam is not
|
||||
// running or not signed in. Decided on the first Steam check.
|
||||
bool steam_offline{};
|
||||
|
||||
@@ -635,6 +635,17 @@ std::string steam_persona_name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
std::filesystem::path steam_avatar_path() {
|
||||
if (!steam_signed_in()) return {};
|
||||
const auto user = active_steam_id();
|
||||
const auto steam = steam_directory();
|
||||
if (!user || steam.empty()) return {};
|
||||
// Steam keeps each account's picture it has shown as config/avatarcache/<SteamID64>.png.
|
||||
auto path = steam / L"config" / L"avatarcache" / (std::to_wstring(*user) + L".png");
|
||||
std::error_code error;
|
||||
return fs::is_regular_file(path, error) ? path : std::filesystem::path{};
|
||||
}
|
||||
|
||||
// Skate.exe hard-imports MiniDumpWriteDump from the Windows API set
|
||||
// api-ms-win-core-debug-minidump-l1-1-0.dll. Windows resolves that set through
|
||||
// its API set schema, so no such file exists on disk; Wine/Proton does not map
|
||||
|
||||
@@ -41,6 +41,9 @@ std::wstring steam_offline_reason();
|
||||
// Display name of the signed-in Steam account (UTF-8), or empty when Steam is
|
||||
// not running, signed out, or the name cannot be read.
|
||||
std::string steam_persona_name();
|
||||
// The signed-in account's profile picture in Steam's avatar cache, or empty
|
||||
// when Steam is not running, signed out, or has not cached it.
|
||||
std::filesystem::path steam_avatar_path();
|
||||
// Validates, starts, injects and resumes Skate; returns its process id.
|
||||
// `created` receives the id as soon as the process exists (ReSkate's startup
|
||||
// splash can open before this returns). Throws a user-facing message.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#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/light_table_guard.h"
|
||||
#include "Extension/World/stack_check_report.h"
|
||||
@@ -329,6 +330,7 @@ extern "C" __declspec(dllexport) BOOL WINAPI DingoSDKDebugInitialize() {
|
||||
(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::light_table_guard::install(r.base);
|
||||
(void)dingosdk::load_error_guard::install(r.base);
|
||||
|
||||
@@ -88,6 +88,7 @@ add_library(dingosdk_runtime SHARED
|
||||
Extension/Music/app_audio_capture.cpp
|
||||
Extension/Music/music_inject.cpp
|
||||
Extension/World/mesh_unload_guard.cpp
|
||||
Extension/World/mesh_settings_guard.cpp
|
||||
Extension/World/shader_permutation_guard.cpp
|
||||
Extension/World/light_table_guard.cpp
|
||||
Extension/World/stack_check_report.cpp
|
||||
|
||||
Reference in new issue
Block a user