diff --git a/Engine/Game/Build/20260929/mesh_settings.h b/Engine/Game/Build/20260929/mesh_settings.h new file mode 100644 index 0000000..f2ba12a --- /dev/null +++ b/Engine/Game/Build/20260929/mesh_settings.h @@ -0,0 +1,22 @@ +#pragma once +#include "Engine/Game/Build/fingerprint.h" +#include + +// 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; +} diff --git a/Extension/World/mesh_settings_guard.cpp b/Extension/World/mesh_settings_guard.cpp new file mode 100644 index 0000000..941e36e --- /dev/null +++ b/Extension/World/mesh_settings_guard.cpp @@ -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 +#include +#include +#include + +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 installed{}; +std::atomic 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 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(base + ms::apply_settings.rva); + if (hook_prepare(target, reinterpret_cast(&guarded), reinterpret_cast(&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 diff --git a/Extension/World/mesh_settings_guard.h b/Extension/World/mesh_settings_guard.h new file mode 100644 index 0000000..8ab2f47 --- /dev/null +++ b/Extension/World/mesh_settings_guard.h @@ -0,0 +1,17 @@ +#pragma once +#include + +// 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//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; +} diff --git a/Launcher/gui_home.cpp b/Launcher/gui_home.cpp index 9e2b2c4..5b869be 100644 --- a/Launcher/gui_home.cpp +++ b/Launcher/gui_home.cpp @@ -1,10 +1,13 @@ #include "gui_internal.h" +#include "gui_renderer.h" #include "mod_manager.h" #include "problem.h" #include +#include +#include #include // 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 bytes{std::istreambuf_iterator(input), std::istreambuf_iterator()}; + std::vector 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); diff --git a/Launcher/gui_internal.h b/Launcher/gui_internal.h index b11ae8f..d1baeb0 100644 --- a/Launcher/gui_internal.h +++ b/Launcher/gui_internal.h @@ -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{}; diff --git a/Launcher/launch.cpp b/Launcher/launch.cpp index 40f330b..e4a28cc 100644 --- a/Launcher/launch.cpp +++ b/Launcher/launch.cpp @@ -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/.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 diff --git a/Launcher/launch.h b/Launcher/launch.h index a0d206d..bb103d2 100644 --- a/Launcher/launch.h +++ b/Launcher/launch.h @@ -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. diff --git a/Runtime/runtime_init.cpp b/Runtime/runtime_init.cpp index 841c8aa..45cc317 100644 --- a/Runtime/runtime_init.cpp +++ b/Runtime/runtime_init.cpp @@ -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); diff --git a/cmake/Runtime.cmake b/cmake/Runtime.cmake index 7afb9fe..3f4ef9d 100644 --- a/cmake/Runtime.cmake +++ b/cmake/Runtime.cmake @@ -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