#include "skate3_app_common.h" #include "skate3_demo_path.h" #include "skate3_fov.h" #include "skate3_iso_installer.h" #include "skate3_native_render.h" #include "skate3_native_scene.h" #include "skate3_screenshot.h" #include "skate3_shader_disasm.h" #include "skate3_win_icon.h" #include "skate3_title_update_installer.h" #include "skate3_user_settings.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(_WIN32) #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #ifndef NOMINMAX #define NOMINMAX #endif #include #elif defined(__linux__) || defined(__APPLE__) #include #if defined(__APPLE__) #include #endif #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(__linux__) || defined(__APPLE__) extern char** environ; #endif extern const rex::PPCImageInfo eawebkit_PPCImageConfig; // Register multi-entry-function alternate entries that rex's analyzer can't // lift via config because they overlap with auto-discovered parent functions. extern "C" REX_FUNC(__restgprlr_19); REXCVAR_DEFINE_STRING(skate3_dlc_root, "", "Skate 3", "Directory containing Skate 3 DLC package files"); REXCVAR_DEFINE_BOOL(skate3_auto_install_dlc, true, "Skate 3", "Install DLC package files found in configured DLC folders"); REXCVAR_DEFINE_BOOL(skate3_ultrawide, false, "Skate 3", "Automatically derive an ultrawide guest video mode from the host display"); REXCVAR_DEFINE_DOUBLE(skate3_field_of_view, 60.0, "Skate 3", "Gameplay camera field of view in degrees") .range(40.0, 120.0); REXCVAR_DEFINE_INT32(skate3_ultrawide_base_height, 720, "Skate 3", "Guest video mode height used when deriving ultrawide modes") .range(480, 2160) .lifecycle(rex::cvar::Lifecycle::kRequiresRestart); REXCVAR_DEFINE_BOOL(skate3_ultrawide_hor_plus, true, "Skate 3", "Apply Hor+ clip-space correction for ultrawide video modes"); REXCVAR_DEFINE_DOUBLE(skate3_ultrawide_hor_plus_scale, 0.0, "Skate 3", "Manual Hor+ X scale override (0 = derive from host aspect)") .range(0.0, 4.0) .lifecycle(rex::cvar::Lifecycle::kRequiresRestart); REXCVAR_DEFINE_BOOL(skate3_ultrawide_disable_ndc_correction, false, "Skate 3", "Diagnostic: disable the GPU NDC Hor+ correction while preserving guest-side " "ultrawide diagnostics"); REXCVAR_DEFINE_BOOL(skate3_ultrawide_trace_draws, false, "Skate 3", "Diagnostic: collect live draw fingerprints for the F7 ultrawide overlay"); REXCVAR_DEFINE_BOOL(skate3_ultrawide_object_stage_trace_to_disk, false, "Skate 3", "Diagnostic: write object render-stage transition summaries to disk"); REXCVAR_DEFINE_BOOL(skate3_ultrawide_object_stage_trace_continuous, false, "Skate 3", "Diagnostic: continuously trace object render-stage summaries"); REXCVAR_DEFINE_INT32(skate3_ultrawide_object_stage_trace_frames_remaining, 0, "Skate 3", "Diagnostic: trace this many object render-stage frames") .range(0, 1000000); REXCVAR_DEFINE_INT32(skate3_ultrawide_object_stage_marker, 0, "Skate 3", "Diagnostic: mark the next object-stage frame (1 visible, 2 invisible)") .range(0, 2); REXCVAR_DEFINE_INT32(skate3_ultrawide_object_stage_missing_limit, 256, "Skate 3", "Diagnostic: maximum missing-main objects to log per object-stage frame") .range(0, 4096); REXCVAR_DEFINE_BOOL(skate3_ultrawide_force_main_visibility_flags, false, "Skate 3", "Force main render object visibility flags while Hor+ ultrawide is active"); REXCVAR_DEFINE_BOOL(skate3_ultrawide_widen_game_frustum, true, "Skate 3", "Widen the game-side main-world cull frustum to match the ultrawide view"); REXCVAR_DEFINE_DOUBLE(skate3_ultrawide_target_aspect, 0.0, "Skate 3", "Derived host display aspect for ultrawide presentation (0 = disabled)") .range(0.0, 8.0); namespace { void ApplyDemoPathProfileOverride() { if (!rex::cvar::Query("skate3_demo_path") && !rex::cvar::Query("skate3_demo_path_probe")) { return; } rex::cvar::SetFlagByName("user_profile_signed_in", "false"); rex::cvar::SetFlagByName("user_live_signed_in", "false"); } #if defined(__linux__) || defined(__APPLE__) std::vector CurrentProcessArgumentsForRestart( const std::filesystem::path& executable_path) { std::vector args; #if defined(__APPLE__) int argc = *_NSGetArgc(); char** argv = *_NSGetArgv(); args.reserve(static_cast(argc > 0 ? argc : 1)); for (int i = 0; i < argc; ++i) { args.emplace_back(argv[i] ? argv[i] : ""); } #else std::ifstream cmdline("/proc/self/cmdline", std::ios::binary); std::string arg; char ch = 0; while (cmdline.get(ch)) { if (ch == '\0') { args.push_back(std::move(arg)); arg.clear(); } else { arg.push_back(ch); } } if (!arg.empty()) { args.push_back(std::move(arg)); } #endif const std::string executable = rex::path_to_utf8(executable_path); if (args.empty()) { args.push_back(executable); } else { args[0] = executable; } return args; } void SetRestartArgument(std::vector& args, std::string name, std::string value) { const std::string option = "--" + name; const std::string option_with_equals = option + "="; for (size_t i = 1; i < args.size(); ++i) { if (args[i] == option) { if (i + 1 < args.size()) { args[i + 1] = std::move(value); } else { args.push_back(std::move(value)); } return; } if (args[i].starts_with(option_with_equals)) { args[i] = option_with_equals + value; return; } } args.push_back(std::move(option)); args.push_back(std::move(value)); } #endif constexpr std::string_view kUserDirectoryName = "skate3"; constexpr std::string_view kSettingsFilename = "settings.toml"; constexpr std::string_view kDlcDirectoryName = "dlc"; constexpr double kSixteenNineAspect = 16.0 / 9.0; constexpr double kUltrawideAspectEpsilon = 0.01; struct DisplaySize { int32_t width = 0; int32_t height = 0; }; #if defined(_WIN32) BOOL CALLBACK CollectMonitorCallback(HMONITOR monitor_handle, HDC, LPRECT, LPARAM data) { auto* monitors = reinterpret_cast*>(data); monitors->push_back(monitor_handle); return TRUE; } HMONITOR GetConfiguredMonitorHandle() { const int32_t monitor_index = REXCVAR_GET(monitor); if (monitor_index > 0) { std::vector monitors; EnumDisplayMonitors(nullptr, nullptr, CollectMonitorCallback, reinterpret_cast(&monitors)); if (monitor_index <= static_cast(monitors.size())) { return monitors[monitor_index - 1]; } } POINT origin{0, 0}; return MonitorFromPoint(origin, MONITOR_DEFAULTTOPRIMARY); } std::optional QueryFullscreenMonitorSize() { HMONITOR monitor_handle = GetConfiguredMonitorHandle(); if (!monitor_handle) { return std::nullopt; } MONITORINFO monitor_info{}; monitor_info.cbSize = sizeof(monitor_info); if (!GetMonitorInfo(monitor_handle, &monitor_info)) { return std::nullopt; } const int32_t width = monitor_info.rcMonitor.right - monitor_info.rcMonitor.left; const int32_t height = monitor_info.rcMonitor.bottom - monitor_info.rcMonitor.top; if (width <= 0 || height <= 0) { return std::nullopt; } return DisplaySize{width, height}; } #else std::optional QueryFullscreenMonitorSize() { return std::nullopt; } #endif std::optional ResolveUltrawideTargetDisplaySize() { if (rex::cvar::HasNonDefaultValue("resolution")) { return std::nullopt; } if (REXCVAR_GET(fullscreen)) { return QueryFullscreenMonitorSize(); } const int32_t configured_window_width = REXCVAR_GET(window_width); const int32_t configured_window_height = REXCVAR_GET(window_height); if (rex::cvar::HasNonDefaultValue("window_width") && rex::cvar::HasNonDefaultValue("window_height") && configured_window_width > 0 && configured_window_height > 0) { return DisplaySize{configured_window_width, configured_window_height}; } return std::nullopt; } void ApplyUltrawideVideoDefaults() { if (!REXCVAR_GET(skate3_ultrawide) || rex::cvar::HasNonDefaultValue("skate3_ultrawide_target_aspect")) { return; } const std::optional target_size = ResolveUltrawideTargetDisplaySize(); if (!target_size || target_size->width <= 0 || target_size->height <= 0) { return; } const double target_aspect = static_cast(target_size->width) / static_cast(target_size->height); if (target_aspect <= kSixteenNineAspect + kUltrawideAspectEpsilon) { return; } rex::cvar::SetFlagByName("skate3_ultrawide_target_aspect", std::to_string(target_aspect)); if (!rex::cvar::HasNonDefaultValue("present_letterbox")) { rex::cvar::SetFlagByName("present_letterbox", "true"); } } void DisableActiveUltrawideDiagnostics() { constexpr std::string_view kFalseFlags[] = { "skate3_ultrawide_trace_draws", "skate3_ultrawide_object_stage_trace_to_disk", "skate3_ultrawide_object_stage_trace_continuous", "skate3_ultrawide_force_main_visibility_flags", "skate3_ultrawide_texture_trace_bind_keys", "skate3_ultrawide_texture_trace_scaled_resolve", "skate3_ultrawide_ignore_streamer_texture_invalidations", "skate3_ultrawide_screen_callback_tracking", "skate3_ultrawide_fake_occlusion_queries", "perf_draw_fingerprints", "perf_keep_heavyweight_draw_diagnostics", "trace_gpu_stream", }; for (std::string_view flag : kFalseFlags) { rex::cvar::SetFlagByName(std::string(flag), "false"); } constexpr std::string_view kZeroFlags[] = { "skate3_ultrawide_texture_trace_remaining", "skate3_ultrawide_object_stage_trace_frames_remaining", "skate3_ultrawide_object_stage_marker", "vulkan_debug_log_frame_summaries_remaining", "vulkan_debug_log_resolve_decisions_remaining", "vulkan_debug_log_team_profile_background_candidates_remaining", "vulkan_debug_log_team_profile_background_bindings_remaining", "filesystem_debug_log_fe_asset_ops_remaining", "filesystem_debug_log_team_profile_background_remaining", }; for (std::string_view flag : kZeroFlags) { rex::cvar::SetFlagByName(std::string(flag), "0"); } rex::cvar::SetFlagByName("skate3_ultrawide_fake_occlusion_sample_count", "1000"); } std::filesystem::path DefaultDocumentsUserRoot() { return rex::filesystem::GetUserFolder() / std::string(kUserDirectoryName); } std::filesystem::path DefaultRoamingUserRoot() { #if defined(_WIN32) char* appdata = nullptr; size_t appdata_size = 0; if (_dupenv_s(&appdata, &appdata_size, "APPDATA") == 0 && appdata && *appdata) { std::filesystem::path result = std::filesystem::path(appdata) / std::string(kUserDirectoryName); std::free(appdata); return result; } std::free(appdata); #endif return DefaultDocumentsUserRoot(); } std::filesystem::path ResolveSkate3UserRoot(const rex::PathConfig& paths) { const auto executable_root = rex::filesystem::GetExecutableFolder(); if (std::filesystem::exists(executable_root / "portable.txt")) { return executable_root; } const auto old_default = DefaultDocumentsUserRoot(); if (!paths.user_data_root.empty() && paths.user_data_root != old_default) { return paths.user_data_root; } return DefaultRoamingUserRoot(); } void ConfigureSkate3UserPaths(rex::PathConfig& paths, std::filesystem::path& settings_path, std::filesystem::path& profiles_path) { const auto old_user_root = DefaultDocumentsUserRoot(); const auto old_cache_root = old_user_root / "cache"; const auto original_cache_root = paths.cache_root; const auto resolved_user_root = ResolveSkate3UserRoot(paths); paths.user_data_root = resolved_user_root; if (original_cache_root.empty() || original_cache_root == old_cache_root) { paths.cache_root = resolved_user_root / "cache"; } settings_path = resolved_user_root / std::string(kSettingsFilename); profiles_path = skate3::ProfilesFilePath(resolved_user_root); } bool ConfigContainsAnyKey(const toml::table& table, std::initializer_list keys) { for (std::string_view key : keys) { if (table.contains(key)) { return true; } } return false; } bool ConfigFileContainsAnyKey(const std::filesystem::path& config_path, std::initializer_list keys) { if (config_path.empty() || !std::filesystem::exists(config_path)) { return false; } try { auto config = toml::parse_file(config_path.string()); return ConfigContainsAnyKey(config, keys); } catch (const toml::parse_error&) { return false; } } bool DeveloperConfigHasResolutionScaleOverride(const std::filesystem::path& config_path) { return ConfigFileContainsAnyKey(config_path, {"resolution_scale", "draw_resolution_scale_x", "draw_resolution_scale_y"}); } #if REX_PLATFORM_MAC constexpr int kDefaultResolutionScale = 1; #else constexpr int kDefaultResolutionScale = 2; #endif void ApplyFirstRunVideoDefaults(const std::filesystem::path& settings_path, const std::filesystem::path& developer_config_path) { if (std::filesystem::exists(settings_path) || DeveloperConfigHasResolutionScaleOverride(developer_config_path) || rex::cvar::HasNonDefaultValue("resolution_scale") || rex::cvar::HasNonDefaultValue("draw_resolution_scale_x") || rex::cvar::HasNonDefaultValue("draw_resolution_scale_y")) { return; } const auto scale = std::to_string(kDefaultResolutionScale); rex::cvar::SetFlagByName("resolution_scale", scale); rex::cvar::SetFlagByName("draw_resolution_scale_x", scale); rex::cvar::SetFlagByName("draw_resolution_scale_y", scale); } void LoadAndNormalizeSimpleSettings(const std::filesystem::path& settings_path, const std::filesystem::path& developer_config_path) { if (std::filesystem::exists(settings_path)) { rex::cvar::LoadConfig(settings_path); } else { ApplyFirstRunVideoDefaults(settings_path, developer_config_path); } rex::ui::EnsureSimpleSettingsConfig(settings_path); } std::filesystem::path ResolveRuntimeGameDataRoot(const rex::PathConfig& paths) { if (!paths.game_data_root.empty()) { return paths.game_data_root; } const auto working_directory_game = std::filesystem::current_path() / "game"; if (skate3::IsGameInstalled(working_directory_game)) { return working_directory_game; } return paths.config_path.parent_path() / "game"; } const char* FirstExistingFontPath(std::initializer_list paths) { for (const char* path : paths) { if (std::filesystem::exists(path)) { return path; } } return nullptr; } std::string Hex8(uint32_t value) { std::ostringstream stream; stream << std::uppercase << std::hex << std::setw(8) << std::setfill('0') << value; return stream.str(); } std::filesystem::path InstalledMarketplaceContentPath(const std::filesystem::path& content_root, uint32_t title_id, const std::filesystem::path& package_path) { return content_root / "0000000000000000" / Hex8(title_id) / "00000002" / package_path.filename(); } std::filesystem::path InstalledMarketplaceHeaderPath(const std::filesystem::path& content_root, uint32_t title_id, const std::filesystem::path& package_path) { return content_root / "0000000000000000" / Hex8(title_id) / "Headers" / "00000002" / (package_path.filename().string() + ".header"); } bool IsInstalledMarketplaceContent(const std::filesystem::path& content_root, uint32_t title_id, const std::filesystem::path& package_path) { return std::filesystem::is_directory( InstalledMarketplaceContentPath(content_root, title_id, package_path)) && std::filesystem::is_regular_file( InstalledMarketplaceHeaderPath(content_root, title_id, package_path)); } std::vector DiscoverDlcSourceDirectories( const std::filesystem::path& executable_root, const std::filesystem::path& game_data_root, const std::filesystem::path& user_data_root) { std::vector dirs; auto add_dir = [&](std::filesystem::path dir) { if (dir.empty()) { return; } std::error_code ec; dir = std::filesystem::absolute(dir, ec); if (ec) { return; } for (const auto& existing : dirs) { if (std::filesystem::equivalent(existing, dir, ec)) { return; } ec.clear(); } dirs.push_back(std::move(dir)); }; const std::string configured_root = REXCVAR_GET(skate3_dlc_root); if (!configured_root.empty()) { add_dir(configured_root); } add_dir(executable_root / std::string(kDlcDirectoryName)); add_dir(game_data_root / std::string(kDlcDirectoryName)); add_dir(user_data_root / std::string(kDlcDirectoryName)); return dirs; } } // namespace #if !SKATE3_HAS_TITLE_UPDATE // Retail sub_82EBAE4C: alternate entry into sub_82EBAE34 (37 vtable refs in .data). // Body: lwz r3, 0x164(r31); addi r1, r31, 0x140; b __restgprlr_19 static void Sub82EBAE4CImpl(PPCContext& ctx, uint8_t* base) { ctx.r3.u64 = REX_LOAD_U32(ctx.r31.u32 + 0x164); ctx.r1.s64 = ctx.r31.s64 + 0x140; __restgprlr_19(ctx, base); } #endif Skate3BaseApp::~Skate3BaseApp() = default; void Skate3BaseApp::OnConfigurePaths(rex::PathConfig& paths) { ConfigureSkate3UserPaths(paths, user_settings_path_, profiles_path_); config_path_ = paths.config_path; LoadAndNormalizeSimpleSettings(user_settings_path_, config_path_); Skate3InitializeFieldOfViewOverride(); ApplyUltrawideVideoDefaults(); if (!rex::graphics::ultrawide_debug::LoadTargets(paths.cache_root / "skate3_ultrawide_targets.toml")) { rex::graphics::ultrawide_debug::LoadBuiltInSkate3Classifier(); } DisableActiveUltrawideDiagnostics(); } void Skate3BaseApp::OnConfigureFonts(ImFontAtlas* atlas) { if (!atlas) { return; } atlas->Clear(); ImFontConfig font_config; font_config.OversampleH = 2; font_config.OversampleV = 2; font_config.PixelSnapH = false; const char* base_font_path = FirstExistingFontPath({ #if defined(_WIN32) "C:\\Windows\\Fonts\\Helvetica.ttf", "C:\\Windows\\Fonts\\helvetica.ttf", "C:\\Windows\\Fonts\\HelveticaNeue.ttf", "C:\\Windows\\Fonts\\arial.ttf", #elif defined(__APPLE__) "/System/Library/Fonts/SFNS.ttf", "/System/Library/Fonts/SFCompact.ttf", "/System/Library/Fonts/HelveticaNeue.ttc", "/System/Library/Fonts/LucidaGrande.ttc", "/System/Library/Fonts/Supplemental/Arial.ttf", #else "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf", "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", "/usr/local/share/fonts/NotoSans-Regular.ttf", "/usr/local/share/fonts/LiberationSans-Regular.ttf", #endif }); if (base_font_path) { atlas->AddFontFromFileTTF(base_font_path, 16.0f, &font_config, atlas->GetGlyphRangesDefault()); } else { atlas->AddFontDefault(); } #if defined(_WIN32) const char* jp_font_path = "C:\\Windows\\Fonts\\msgothic.ttc"; if (std::filesystem::exists(jp_font_path)) { ImFontConfig jp_font_config; jp_font_config.MergeMode = true; jp_font_config.OversampleH = jp_font_config.OversampleV = 1; jp_font_config.PixelSnapH = true; jp_font_config.FontNo = 0; atlas->AddFontFromFileTTF(jp_font_path, 16.0f, &jp_font_config, atlas->GetGlyphRangesJapanese()); } #endif } std::optional Skate3BaseApp::OnFinalizePaths( const rex::PathConfig& defaults, std::function resume) { config_path_ = defaults.config_path; user_settings_path_ = defaults.user_data_root / std::string(kSettingsFilename); profiles_path_ = skate3::ProfilesFilePath(defaults.user_data_root); auto profiles = skate3::LoadProfiles(profiles_path_); const bool has_profiles_file = std::filesystem::exists(profiles_path_); skate3::EnsureUsableProfileStore(profiles, "Player"); if (auto* profile = skate3::FindSelectedProfile(profiles)) { skate3::ApplyProfileCvars(*profile); ApplyDemoPathProfileOverride(); } const bool has_config_file = std::filesystem::exists(defaults.config_path); const bool has_game_path = std::filesystem::is_directory(defaults.game_data_root); if (!has_profiles_file && has_config_file && has_game_path) { skate3::SaveProfiles(profiles_path_, profiles); } auto runtime_paths = defaults; runtime_paths.game_data_root = ResolveRuntimeGameDataRoot(runtime_paths); runtime_paths.config_path.clear(); if (!skate3::IsGameInstalled(runtime_paths.game_data_root)) { REXLOG_INFO("Game files not found at {}; launching rexglue ISO installer", runtime_paths.game_data_root.string()); #if defined(__APPLE__) if (const char* automated_iso = std::getenv("SKATE3_INSTALL_ISO"); automated_iso == nullptr || *automated_iso == '\0') { #if SKATE3_HAS_TITLE_UPDATE // Chain the title update wizard after the ISO install completes. auto resume_after_title_update = [this, resume = std::move(resume)](rex::PathConfig paths) mutable { if (!skate3::IsTitleUpdateInstalled(paths.game_data_root)) { skate3::ShowTitleUpdateInstallWizard(imgui_drawer(), std::move(paths), std::move(resume)); return; } resume(std::move(paths)); }; skate3::ShowRexglueIsoInstallWizard(imgui_drawer(), std::move(runtime_paths), std::move(resume_after_title_update)); #else skate3::ShowRexglueIsoInstallWizard(imgui_drawer(), std::move(runtime_paths), std::move(resume)); #endif return std::nullopt; } #endif rex::PathConfig installed_paths; const bool installed = skate3::RunRexglueIsoInstallWizardBlocking( app_context(), window(), imgui_drawer(), runtime_paths, installed_paths); if (!installed) { app_context().QuitFromUIThread(); return std::nullopt; } runtime_paths = std::move(installed_paths); } #if SKATE3_HAS_TITLE_UPDATE // This build executes Title Update 3 code; the game cannot boot without the // TU payloads staged next to the installed game files. Existing installs // from releases that predate TU support land here with the game present but // the title update missing. if (!skate3::IsTitleUpdateInstalled(runtime_paths.game_data_root)) { REXLOG_INFO("Skate 3 Title Update 3 not staged at {}; launching title update installer", runtime_paths.game_data_root.string()); #if defined(__APPLE__) if (const char* automated_tu = std::getenv("SKATE3_INSTALL_TU"); automated_tu == nullptr || *automated_tu == '\0') { skate3::ShowTitleUpdateInstallWizard(imgui_drawer(), std::move(runtime_paths), std::move(resume)); return std::nullopt; } #endif rex::PathConfig tu_paths; const bool tu_installed = skate3::RunTitleUpdateInstallWizardBlocking( app_context(), window(), imgui_drawer(), runtime_paths, tu_paths); if (!tu_installed) { app_context().QuitFromUIThread(); return std::nullopt; } runtime_paths = std::move(tu_paths); } #endif #if defined(_WIN32) // Window/taskbar + Explorer icon sourced from the user's OWN game art at // runtime (game/nxeart); the shipped exe and the repo carry no EA // artwork (see skate3_win_icon.h). Game files are guaranteed installed by // this point on Windows (the install wizards above run blocking). skate3::ApplyGameIconFromGameData( runtime_paths.game_data_root, window() ? window()->GetNativeWindowHandle() : nullptr); #endif return runtime_paths; } void Skate3BaseApp::OnCreateDialogs(rex::ui::ImGuiDrawer* drawer) { (void)drawer; rex::ui::RegisterBind("bind_skate3_menu", "Escape", "Skate 3 settings", [this] { ToggleSimpleSettings(); }); rex::ui::RegisterBind("bind_skate3_menu_alt", "F1", "Skate 3 settings alternate", [this] { ToggleSimpleSettings(); }); // Remembered handle: the F11 paired A/B parity capture (native + emulated // screenshots + gsnap, sequenced from the guest frame loop in // skate3_native_render.cpp) needs the window without an app pointer. skate3::screenshot::RememberWindow(window() ? window()->GetNativeWindowHandle() : nullptr); rex::ui::RegisterBind("bind_skate3_screenshot", "F6", "Save screenshot to screenshots/", [this] { skate3::screenshot::CaptureWindow( window() ? window()->GetNativeWindowHandle() : nullptr); }); rex::ui::RegisterBind("bind_skate3_native_render_toggle", "F5", "Toggle native/emulated renderer", [] { skate3::native_scene::ToggleSceneEnabled(); }); rex::ui::RegisterBind("bind_skate3_ultrawide_targets", "F7", "Skate 3 ultrawide targets", [this] { ToggleUltrawideTargets(); }); rex::ui::RegisterBind("bind_skate3_save_draw_fingerprints", "F8", "Save draw fingerprint log", [this] { SaveDrawFingerprintLog(); }); rex::ui::RegisterBind("bind_skate3_log_debug_marker", "F9", "Write debug marker to log", [this] { LogDebugMarker(); }); rex::ui::RegisterBind("bind_skate3_log_user_marker", "F10", "Write marker to log", [this] { LogUserMarker(); }); rex::ui::RegisterBind("bind_skate3_native_debug", "F12", "Native render debug menu", [this] { ToggleNativeDebug(); }); } void Skate3BaseApp::OnPostSetup() { skate3::shader_disasm::RunIfRequested(); ApplySelectedProfileToRuntime(); ApplyGameplayCursorMode(); if (auto* input_system = static_cast(runtime()->input_system())) { input_system->SetActiveCallback([this]() { const bool settings_visible = simple_settings_dialog_ && simple_settings_dialog_->visible(); const bool xam_ui_active = rex::kernel::xam::xeXamIsUIActive(); return !settings_visible && !xam_ui_active; }); input_system->SetMenuChordCallback([this]() { app_context().CallInUIThreadDeferred([this]() { ToggleSimpleSettings(); }); }); } if (std::getenv("SKATE3_DISABLE_BIG_ALIASES") == nullptr) { InstallBigDeviceAliases(); } InstallDlcPackages(); InstallRecipeOverlay(); // Register retail multi-entry-function alternate entries. #if !SKATE3_HAS_TITLE_UPDATE runtime()->function_dispatcher()->SetFunction(0x82EBAE4C, &Sub82EBAE4CImpl); #endif auto* dispatcher = runtime()->function_dispatcher(); skate3::native_render::Install(); skate3::demo_path::InstallHooks(dispatcher); if (dispatcher->InitializeFunctionTable(eawebkit_PPCImageConfig.code_base, eawebkit_PPCImageConfig.code_size, eawebkit_PPCImageConfig.image_base, eawebkit_PPCImageConfig.image_size)) { for (int i = 0; eawebkit_PPCImageConfig.func_mappings[i].guest != 0; ++i) { auto* host = eawebkit_PPCImageConfig.func_mappings[i].host; if (host) { dispatcher->SetFunction( static_cast(eawebkit_PPCImageConfig.func_mappings[i].guest), host); } } } } void Skate3BaseApp::OnShutdown() { rex::ui::UnregisterBind("bind_skate3_menu"); rex::ui::UnregisterBind("bind_skate3_menu_alt"); rex::ui::UnregisterBind("bind_skate3_ultrawide_targets"); rex::ui::UnregisterBind("bind_skate3_save_draw_fingerprints"); rex::ui::UnregisterBind("bind_skate3_log_debug_marker"); rex::ui::UnregisterBind("bind_skate3_log_user_marker"); rex::ui::UnregisterBind("bind_skate3_native_debug"); ApplyGameplayCursorMode(); simple_settings_dialog_.reset(); ultrawide_targets_dialog_.reset(); native_debug_dialog_.reset(); } void Skate3BaseApp::ToggleSimpleSettings() { if (simple_settings_dialog_) { if (simple_settings_dialog_->visible()) { simple_settings_dialog_->Hide(); } else { ApplySettingsCursorMode(); simple_settings_dialog_->Show(); } return; } auto load_profiles = [this]() { rex::ui::SimpleProfileState state; auto store = skate3::LoadProfiles(profiles_path_); skate3::EnsureUsableProfileStore(store, "Player"); state.selected_index = 0; for (int i = 0; i < static_cast(store.profiles.size()); ++i) { const auto& profile = store.profiles[i]; state.profiles.push_back({profile.id, profile.gamertag, profile.signed_in}); if (profile.id == store.selected_profile) { state.selected_index = i; } } return state; }; auto save_profile = [this](int selected_index, std::string gamertag, bool signed_in) { auto store = skate3::LoadProfiles(profiles_path_); skate3::EnsureUsableProfileStore(store, "Player"); if (store.profiles.empty()) { return; } selected_index = std::clamp(selected_index, 0, static_cast(store.profiles.size()) - 1); auto& profile = store.profiles[selected_index]; if (!gamertag.empty()) { profile.gamertag = std::move(gamertag); } profile.signed_in = signed_in; if (!profile.signed_in) { profile.live_signed_in = false; } store.selected_profile = profile.id; skate3::SaveProfiles(profiles_path_, store); skate3::ApplyProfileCvars(profile); ApplyDemoPathProfileOverride(); ApplySelectedProfileToRuntime(); }; auto close_settings = [this]() { ApplyGameplayCursorMode(); }; auto close_game = [this]() { #if REX_PLATFORM_MAC || REX_PLATFORM_LINUX std::thread([]() { std::this_thread::sleep_for(std::chrono::seconds(10)); REXLOG_WARN("Close Game watchdog exiting process after shutdown timeout"); std::_Exit(EXIT_SUCCESS); }).detach(); #endif app_context().CallInUIThreadDeferred([this]() { #if REX_PLATFORM_MAC app_context().QuitFromUIThread(); #else if (window()) { window()->RequestClose(); } else { app_context().QuitFromUIThread(); } #endif }); }; auto restart_game = [this]() { RestartGame(); }; simple_settings_dialog_ = std::make_unique( imgui_drawer(), user_settings_path_, std::move(load_profiles), std::move(save_profile), std::move(close_settings), std::move(close_game), std::move(restart_game)); ApplySettingsCursorMode(); simple_settings_dialog_->Show(); } void Skate3BaseApp::ToggleUltrawideTargets() { if (ultrawide_targets_dialog_) { if (ultrawide_targets_dialog_->visible()) { ultrawide_targets_dialog_->Hide(); } else { ApplySettingsCursorMode(); ultrawide_targets_dialog_->Show(); } return; } const auto export_path = cache_root() / "skate3_ultrawide_targets.toml"; ultrawide_targets_dialog_ = std::make_unique( imgui_drawer(), export_path, [this]() { ApplyGameplayCursorMode(); }); ApplySettingsCursorMode(); ultrawide_targets_dialog_->Show(); } void Skate3BaseApp::ToggleNativeDebug() { if (!native_debug_dialog_) { native_debug_dialog_ = std::make_unique(imgui_drawer()); } if (native_debug_dialog_->visible()) { native_debug_dialog_->Hide(); ApplyGameplayCursorMode(); } else { ApplySettingsCursorMode(); native_debug_dialog_->Show(); } } void Skate3BaseApp::ApplySettingsCursorMode() { if (window()) { window()->SetCursorVisibility(rex::ui::Window::CursorVisibility::kVisible); } } void Skate3BaseApp::ApplyGameplayCursorMode() { if (window()) { window()->SetCursorVisibility(rex::ui::Window::CursorVisibility::kAutoHidden); } } void Skate3BaseApp::RestartGame() { app_context().CallInUIThreadDeferred([this]() { #if defined(_WIN32) wchar_t executable_path[MAX_PATH] = {}; if (!GetModuleFileNameW(nullptr, executable_path, MAX_PATH)) { REXLOG_WARN("Restart requested, but the executable path could not be resolved"); return; } std::wstring command_line = GetCommandLineW(); STARTUPINFOW startup_info{}; startup_info.cb = sizeof(startup_info); PROCESS_INFORMATION process_info{}; if (!CreateProcessW(executable_path, command_line.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &startup_info, &process_info)) { REXLOG_WARN("Restart requested, but launching a new process failed"); return; } CloseHandle(process_info.hThread); CloseHandle(process_info.hProcess); #elif defined(__linux__) || defined(__APPLE__) const auto executable_path = rex::filesystem::GetExecutablePath(); if (executable_path.empty()) { REXLOG_WARN("Restart requested, but the executable path could not be resolved"); return; } auto args = CurrentProcessArgumentsForRestart(executable_path); std::filesystem::path restart_game_data_root; if (runtime()) { restart_game_data_root = runtime()->game_data_root(); } if (restart_game_data_root.empty()) { restart_game_data_root = game_data_root(); } if (!restart_game_data_root.empty()) { SetRestartArgument(args, "game_data_root", rex::path_to_utf8(restart_game_data_root)); } std::vector argv; argv.reserve(args.size() + 1); for (auto& arg : args) { argv.push_back(arg.data()); } argv.push_back(nullptr); const std::string executable = rex::path_to_utf8(executable_path); pid_t child_pid = 0; const int spawn_result = posix_spawn(&child_pid, executable.c_str(), nullptr, nullptr, argv.data(), environ); if (spawn_result != 0) { REXLOG_WARN("Restart requested, but launching a new process failed: {}", std::strerror(spawn_result)); return; } #else REXLOG_WARN("Restart requested, but automatic restart is not implemented on this platform"); return; #endif #if REX_PLATFORM_MAC app_context().QuitFromUIThread(); #else if (window()) { window()->RequestClose(); } else { app_context().QuitFromUIThread(); } #endif }); } void Skate3BaseApp::SaveDrawFingerprintLog() { #ifndef REXGLUE_ENABLE_PERF_COUNTERS REXLOG_WARN("Perf capture is unavailable because perf counters are disabled in this build"); return; #else auto now = std::chrono::system_clock::now(); std::time_t now_time = std::chrono::system_clock::to_time_t(now); std::tm local_time{}; #if defined(_WIN32) localtime_s(&local_time, &now_time); #else localtime_r(&now_time, &local_time); #endif std::ostringstream counters_filename; counters_filename << "perf_capture_counters_" << std::put_time(&local_time, "%Y%m%d_%H%M%S") << ".csv"; std::ostringstream filename; filename << "perf_capture_draw_fingerprints_" << std::put_time(&local_time, "%Y%m%d_%H%M%S") << ".csv"; std::error_code ec; std::filesystem::create_directories(cache_root(), ec); const auto counters_path = cache_root() / counters_filename.str(); const auto path = cache_root() / filename.str(); if (rex::perf::StartCapture(counters_path, path)) { REXLOG_INFO("Started perf capture; counter log will be saved to {}", counters_path.string()); REXLOG_INFO("Started perf capture; draw fingerprint log will be saved to {}", path.string()); } else { REXLOG_WARN("Perf capture is already running"); } #endif } void Skate3BaseApp::LogUserMarker() { const uint32_t marker = debug_marker_count_.fetch_add(1, std::memory_order_relaxed) + 1; REXLOG_WARN("USER LOG MARKER #{}: F10 pressed", marker); } void Skate3BaseApp::LogDebugMarker() { const uint32_t marker = debug_marker_count_.fetch_add(1, std::memory_order_relaxed) + 1; DisableActiveUltrawideDiagnostics(); REXLOG_WARN("USER DEBUG MARKER #{}: F9 pressed; active diagnostics disabled", marker); } void Skate3BaseApp::ApplySelectedProfileToRuntime() { if (!runtime() || !runtime()->kernel_state() || !runtime()->kernel_state()->user_profile()) { return; } auto profiles = skate3::LoadProfiles(profiles_path_); if (auto* profile = skate3::FindSelectedProfile(profiles)) { skate3::ApplyProfileCvars(*profile); ApplyDemoPathProfileOverride(); runtime()->kernel_state()->user_profile()->SetIdentity(profile->xuid, profile->gamertag); } } bool Skate3BaseApp::IsRecipeNameChar(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-'; } std::set Skate3BaseApp::DiscoverRecipeAliases( const std::filesystem::path& content_root) { static constexpr size_t kScanBytes = 8 * 1024 * 1024; static constexpr std::string_view kPrefix = "data/content/recipe/"; std::set aliases; std::error_code ec; for (const auto& entry : std::filesystem::directory_iterator(content_root, ec)) { if (ec || !entry.is_regular_file() || entry.path().extension() != ".big") { continue; } std::ifstream stream(entry.path(), std::ios::binary); if (!stream) { continue; } std::string buffer; buffer.resize(kScanBytes); stream.read(buffer.data(), static_cast(buffer.size())); buffer.resize(static_cast(stream.gcount())); size_t pos = 0; while ((pos = buffer.find(kPrefix, pos)) != std::string::npos) { size_t name_start = pos + kPrefix.size(); size_t name_end = name_start; while (name_end < buffer.size() && IsRecipeNameChar(buffer[name_end])) { ++name_end; } if (name_end > name_start) { aliases.insert(buffer.substr(name_start, name_end - name_start)); } pos = name_end; } } return aliases; } bool Skate3BaseApp::CreateOverlayDirectory( const std::filesystem::path& overlay_root, std::string_view guest_path) { std::filesystem::path path = overlay_root; size_t segment_start = 0; while (segment_start < guest_path.size()) { const size_t slash = guest_path.find('/', segment_start); const size_t segment_end = slash == std::string_view::npos ? guest_path.size() : slash; if (segment_end > segment_start) { path /= std::string(guest_path.substr(segment_start, segment_end - segment_start)); } if (slash == std::string_view::npos) { break; } segment_start = slash + 1; } std::error_code ec; std::filesystem::create_directories(path, ec); return !ec; } void Skate3BaseApp::InstallRecipeOverlay() { if (recipe_overlay_installed_ || !runtime() || !runtime()->file_system()) { return; } const auto content_root = runtime()->game_data_root() / "data" / "content"; if (!std::filesystem::exists(content_root)) { REXLOG_WARN("Skipping Skate 3 recipe VFS overlay; content root not found: {}", content_root.string()); return; } const auto aliases = DiscoverRecipeAliases(content_root); if (aliases.empty()) { REXLOG_WARN("Skipping Skate 3 recipe VFS overlay; no recipe aliases found in {}", content_root.string()); return; } const auto overlay_root = cache_root() / "vfs_big_directory_aliases"; std::error_code ec; std::filesystem::create_directories(overlay_root, ec); if (ec) { REXLOG_WARN("Skipping Skate 3 BIG-directory VFS overlay; failed to create {}: {}", overlay_root.string(), ec.message()); return; } size_t created = 0; for (const auto& alias : aliases) { const std::string guest_path = std::string("data/content/recipe/") + alias; if (CreateOverlayDirectory(overlay_root, guest_path)) { ++created; } } static constexpr std::string_view kBigDirectoryAliases[] = { "data/scene", "data/scene/anim", "data/scene/trickguide", "data/livingworld/PluginDescriptor", "data/state/livingworldentities/pedestrian/plugin", }; for (std::string_view alias : kBigDirectoryAliases) { if (CreateOverlayDirectory(overlay_root, alias)) { ++created; } } if (!created) { REXLOG_WARN("Skipping Skate 3 BIG-directory VFS overlay; failed to create alias directories in {}", overlay_root.string()); return; } auto device = std::make_unique( "skate3bigdirs:", overlay_root, true); if (!device->Initialize()) { REXLOG_WARN("Skipping Skate 3 BIG-directory VFS overlay; failed to initialize host device for {}", overlay_root.string()); return; } runtime()->file_system()->RegisterDevice(std::move(device)); runtime()->file_system()->RegisterSymbolicLink( "\\Device\\Harddisk0\\Partition1\\data\\content\\recipe", "skate3bigdirs:\\data\\content\\recipe"); runtime()->file_system()->RegisterSymbolicLink( "\\Device\\Harddisk0\\Partition1\\data\\scene", "skate3bigdirs:\\data\\scene"); runtime()->file_system()->RegisterSymbolicLink( "\\Device\\Harddisk0\\Partition1\\data\\livingworld\\PluginDescriptor", "skate3bigdirs:\\data\\livingworld\\PluginDescriptor"); runtime()->file_system()->RegisterSymbolicLink( "\\Device\\Harddisk0\\Partition1\\data\\state\\livingworldentities\\pedestrian\\plugin", "skate3bigdirs:\\data\\state\\livingworldentities\\pedestrian\\plugin"); recipe_overlay_installed_ = true; REXLOG_INFO("Installed Skate 3 BIG-directory VFS overlay with {} aliases from {}", created, content_root.string()); } void Skate3BaseApp::InstallBigDeviceAliases() { if (big_device_aliases_installed_ || !runtime() || !runtime()->file_system()) { return; } // Skate 3 uses these file-server style schemes for world content probes. // Route them to the normal title mount so existing disc files resolve. runtime()->file_system()->RegisterSymbolicLink( "big:", "\\Device\\Harddisk0\\Partition1"); runtime()->file_system()->RegisterSymbolicLink( "dlcbig:", "\\Device\\Harddisk0\\Partition1"); big_device_aliases_installed_ = true; } void Skate3BaseApp::InstallDlcPackages() { if (!REXCVAR_GET(skate3_auto_install_dlc) || !runtime() || !runtime()->kernel_state() || !runtime()->kernel_state()->content_manager()) { return; } const uint32_t title_id = runtime()->kernel_state()->title_id(); if (title_id == 0) { REXLOG_WARN("Skipping Skate 3 DLC install; title ID is not available"); return; } const auto user_dlc_root = runtime()->user_data_root() / std::string(kDlcDirectoryName); std::error_code create_ec; std::filesystem::create_directories(user_dlc_root, create_ec); if (create_ec) { REXLOG_WARN("Could not create Skate 3 DLC drop folder {}: {}", user_dlc_root.string(), create_ec.message()); } const auto source_dirs = DiscoverDlcSourceDirectories(rex::filesystem::GetExecutableFolder(), runtime()->game_data_root(), runtime()->user_data_root()); std::unordered_set seen_packages; size_t installed_count = 0; size_t skipped_count = 0; for (const auto& source_dir : source_dirs) { if (!std::filesystem::is_directory(source_dir)) { continue; } std::error_code iter_ec; for (const auto& entry : std::filesystem::recursive_directory_iterator(source_dir, iter_ec)) { if (iter_ec) { REXLOG_WARN("Could not scan Skate 3 DLC folder {}: {}", source_dir.string(), iter_ec.message()); break; } if (!entry.is_regular_file()) { continue; } const auto package_path = entry.path(); std::error_code canonical_ec; auto package_key = std::filesystem::weakly_canonical(package_path, canonical_ec).string(); if (canonical_ec) { package_key = std::filesystem::absolute(package_path).string(); } if (!seen_packages.insert(package_key).second) { continue; } const auto header = rex::filesystem::StfsContainerDevice::ReadPackageHeader(package_path); if (!header) { REXLOG_WARN("Skipping DLC candidate with invalid STFS header: {}", package_path.string()); ++skipped_count; continue; } const auto content_type = static_cast(header->metadata.content_type); if (content_type != rex::system::XContentType::kMarketplaceContent) { REXLOG_WARN("Skipping non-DLC content package {} with type {:08X}", package_path.filename().string(), static_cast(content_type)); ++skipped_count; continue; } const uint32_t package_title_id = header->metadata.execution_info.title_id; if (package_title_id != 0 && package_title_id != title_id) { REXLOG_WARN("Skipping DLC package {} for title {:08X}; running title is {:08X}", package_path.filename().string(), package_title_id, title_id); ++skipped_count; continue; } if (IsInstalledMarketplaceContent(runtime()->user_data_root(), title_id, package_path)) { REXLOG_INFO("DLC package already installed: {}", package_path.filename().string()); ++skipped_count; continue; } REXLOG_INFO("Installing Skate 3 DLC package: {}", package_path.string()); const auto result = runtime()->kernel_state()->content_manager()->InstallContent(package_path); if (result == 0) { ++installed_count; REXLOG_INFO("Installed Skate 3 DLC package: {}", package_path.filename().string()); } else { ++skipped_count; REXLOG_WARN("Failed to install Skate 3 DLC package {}: {:08X}", package_path.filename().string(), static_cast(result)); } } } if (installed_count || skipped_count) { REXLOG_INFO("Skate 3 DLC scan complete: {} installed, {} skipped", installed_count, skipped_count); } else { REXLOG_INFO("No Skate 3 DLC packages found. Drop legally obtained DLC package files in {}", user_dlc_root.string()); } }