diff --git a/CMakeLists.txt b/CMakeLists.txt index aa53d6c..fe26aeb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -183,6 +183,7 @@ include(cmake/CodegenTargets.cmake) set(SKATE3_COMMON_SOURCES src/exception_compat.cpp src/skate3_app_common.cpp + src/skate3_demo_path.cpp src/skate3_fov.cpp src/skate3_iso_installer.cpp src/skate3_title_update_installer.cpp diff --git a/cmake/ApplySkate3CodegenPatches.cmake b/cmake/ApplySkate3CodegenPatches.cmake index aef82db..c53d75c 100644 --- a/cmake/ApplySkate3CodegenPatches.cmake +++ b/cmake/ApplySkate3CodegenPatches.cmake @@ -86,3 +86,45 @@ endforeach() if(NOT _fov_patched) message(FATAL_ERROR "Failed to apply Skate 3 generated projection FOV patch; projection FOV anchor not found") endif() + +set(_demo_path_movie_patched FALSE) +foreach(_file IN LISTS _skate3_recomp_files) + file(READ "${_file}" _contents) + if(_contents MATCHES "ShouldForceIntroMovieComplete") + set(_demo_path_movie_patched TRUE) + break() + endif() + + set(_demo_path_movie_site +" // bl 0x825d60c8 + ctx.lr = 0x825E05A0; + sub_825D60C8(ctx, base);") + if(NOT _contents MATCHES "ctx\\.lr = 0x825E05A0;") + continue() + endif() + if(NOT _contents MATCHES "DEFINE_REX_FUNC\\(sub_825E0510\\)") + continue() + endif() + string(FIND "${_contents}" "${_demo_path_movie_site}" _demo_path_movie_anchor) + if(_demo_path_movie_anchor EQUAL -1) + continue() + endif() + + set(_demo_path_movie_patch +" if (skate3::demo_path::ShouldForceIntroMovieComplete()) { + ctx.r3.u64 = 0; + } else { + // bl 0x825d60c8 + ctx.lr = 0x825E05A0; + sub_825D60C8(ctx, base); + }") + string(REPLACE "${_demo_path_movie_site}" "${_demo_path_movie_patch}" _contents "${_contents}") + _skate3_add_include(_contents "skate3_demo_path.h") + file(WRITE "${_file}" "${_contents}") + set(_demo_path_movie_patched TRUE) + message(STATUS "Applied Skate 3 demo path intro movie patch in ${_file}") + break() +endforeach() +if(NOT _demo_path_movie_patched) + message(FATAL_ERROR "Failed to apply Skate 3 demo path intro movie patch; FEMoviePlayer::Update anchor not found") +endif() diff --git a/src/skate3_app_common.cpp b/src/skate3_app_common.cpp index 75e32ad..1712904 100644 --- a/src/skate3_app_common.cpp +++ b/src/skate3_app_common.cpp @@ -1,5 +1,6 @@ #include "skate3_app_common.h" +#include "skate3_demo_path.h" #include "skate3_fov.h" #include "skate3_iso_installer.h" #include "skate3_title_update_installer.h" @@ -121,6 +122,16 @@ REXCVAR_DEFINE_DOUBLE(skate3_ultrawide_target_aspect, 0.0, "Skate 3", 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) { @@ -599,6 +610,7 @@ std::optional Skate3BaseApp::OnFinalizePaths( 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); @@ -727,6 +739,7 @@ void Skate3BaseApp::OnPostSetup() { #endif auto* dispatcher = runtime()->function_dispatcher(); + skate3::demo_path::InstallHooks(dispatcher); if (dispatcher->InitializeFunctionTable(eawebkit_PPCImageConfig.code_base, eawebkit_PPCImageConfig.code_size, eawebkit_PPCImageConfig.image_base, @@ -797,6 +810,7 @@ void Skate3BaseApp::ToggleSimpleSettings() { store.selected_profile = profile.id; skate3::SaveProfiles(profiles_path_, store); skate3::ApplyProfileCvars(profile); + ApplyDemoPathProfileOverride(); ApplySelectedProfileToRuntime(); }; auto close_settings = [this]() { ApplyGameplayCursorMode(); }; @@ -984,6 +998,7 @@ void Skate3BaseApp::ApplySelectedProfileToRuntime() { 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); } } diff --git a/src/skate3_demo_path.cpp b/src/skate3_demo_path.cpp new file mode 100644 index 0000000..5388c66 --- /dev/null +++ b/src/skate3_demo_path.cpp @@ -0,0 +1,212 @@ +#include "skate3_demo_path.h" + +#include "generated/skate3_init.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#include +#endif + +REXCVAR_DEFINE_BOOL(skate3_demo_path, false, "Skate 3", + "Probe and automate the boot path to gameplay"); +REXCVAR_DEFINE_BOOL(skate3_demo_path_probe, false, "Skate 3", + "Log Skate 3 boot/frontend states used by the demo path"); + +namespace skate3::demo_path { +namespace { + +constexpr uint32_t kFrontEndStatePressStart = 24; +constexpr uint32_t kFrontEndStateLanguageSelect = 47; + +std::atomic g_last_requested_state{0}; +std::atomic g_seen_language_update{false}; +std::atomic g_last_language_select_event{std::numeric_limits::max()}; +std::atomic g_last_press_start_event{std::numeric_limits::max()}; +std::atomic g_automation_stage{0}; +std::atomic g_skip_intro_movie{false}; +std::atomic g_logged_intro_movie_skip{false}; +std::atomic g_f10_poll_was_down{false}; + +bool ProbeEnabled() { + return rex::cvar::Query("skate3_demo_path") || + rex::cvar::Query("skate3_demo_path_probe"); +} + +bool AutomationEnabled() { + return rex::cvar::Query("skate3_demo_path"); +} + +const char* KnownFrontEndStateName(uint32_t state_id) { + switch (state_id) { + case kFrontEndStatePressStart: + return "press-start"; + case kFrontEndStateLanguageSelect: + return "language-select"; + default: + return "unknown"; + } +} + +void LogBootFlowEventChange(std::atomic& last_event, const char* name, uint32_t event, + uint32_t state_this) { + if (!ProbeEnabled()) { + return; + } + + uint32_t previous = last_event.load(std::memory_order_relaxed); + while (previous != event) { + if (last_event.compare_exchange_weak(previous, event, std::memory_order_relaxed)) { + REXLOG_INFO("Skate 3 demo path: {} event={} this=0x{:08X}", name, event, state_this); + return; + } + } +} + +void QueueLanguageAcceptIfNeeded() { + if (!AutomationEnabled()) { + return; + } + + uint32_t expected = 0; + if (!g_automation_stage.compare_exchange_strong(expected, 1, std::memory_order_relaxed)) { + return; + } + + rex::kernel::xam::QueueSyntheticInput(rex::input::X_INPUT_GAMEPAD_A, 8); + REXLOG_INFO("Skate 3 demo path: queued language-select A pulse"); +} + +void EnableTitleStartAutoTapIfNeeded(const char* reason) { + if (!AutomationEnabled()) { + return; + } + + uint32_t stage = g_automation_stage.load(std::memory_order_relaxed); + while (stage < 2) { + if (g_automation_stage.compare_exchange_weak(stage, 2, std::memory_order_relaxed)) { + rex::kernel::xam::SetSyntheticAutoTap(rex::input::X_INPUT_GAMEPAD_START, true); + REXLOG_INFO("Skate 3 demo path: enabled press-start auto-tap ({})", reason); + return; + } + } +} + +void EnableIntroMovieSkipIfNeeded() { + if (!AutomationEnabled()) { + return; + } + + bool expected = false; + if (g_skip_intro_movie.compare_exchange_strong(expected, true, std::memory_order_relaxed)) { + REXLOG_INFO("Skate 3 demo path: enabled intro movie completion override"); + } +} + +void PollF10Marker() { +#if defined(_WIN32) + if (!ProbeEnabled()) { + return; + } + + const bool f10_down = (GetAsyncKeyState(VK_F10) & 0x8000) != 0; + const bool was_down = g_f10_poll_was_down.exchange(f10_down, std::memory_order_relaxed); + if (f10_down && !was_down) { + REXLOG_WARN("Skate 3 demo path: polled F10 milestone marker"); + } +#endif +} + +extern "C" REX_FUNC(Skate3DemoPath_SetFrontEndStateHook) { + const uint32_t manager = ctx.r3.u32; + const uint32_t state_id = ctx.r4.u32; + const uint32_t mode = ctx.r5.u32; + const uint32_t caller_lr = ctx.lr; + + if (ProbeEnabled()) { + g_last_requested_state.store(state_id, std::memory_order_relaxed); + REXLOG_INFO( + "Skate 3 demo path: FE SetState state={} ({}) mode={} manager=0x{:08X} lr=0x{:08X}", + state_id, KnownFrontEndStateName(state_id), mode, manager, caller_lr); + } + + sub_82D0AFA0(ctx, base); +} + +extern "C" REX_FUNC(Skate3DemoPath_LanguageSelectStateHook) { + PollF10Marker(); + LogBootFlowEventChange(g_last_language_select_event, "BootFlow LanguageSelectState", + ctx.r4.u32, ctx.r3.u32); + if (ctx.r4.u32 == 4) { + EnableIntroMovieSkipIfNeeded(); + } + + sub_826FDD70(ctx, base); +} + +extern "C" REX_FUNC(Skate3DemoPath_ShowPressStartModeHook) { + PollF10Marker(); + LogBootFlowEventChange(g_last_press_start_event, "BootFlow ShowPressStartMode", ctx.r4.u32, + ctx.r3.u32); + if (ctx.r4.u32 == 1) { + g_skip_intro_movie.store(false, std::memory_order_relaxed); + EnableTitleStartAutoTapIfNeeded("press-start state"); + } + + sub_826FE1D8(ctx, base); +} + +extern "C" REX_FUNC(Skate3DemoPath_LanguageSelectUpdateHook) { + PollF10Marker(); + if (ProbeEnabled()) { + bool expected = false; + if (g_seen_language_update.compare_exchange_strong(expected, true, + std::memory_order_relaxed)) { + REXLOG_INFO( + "Skate 3 demo path: FrontEndState_LanguageSelect::OnUpdate first seen " + "dt={} this=0x{:08X}", + ctx.r4.u32, ctx.r3.u32); + QueueLanguageAcceptIfNeeded(); + } + } + + sub_82639400(ctx, base); +} + +} // namespace + +void InstallHooks(rex::runtime::FunctionDispatcher* dispatcher) { + if (!dispatcher || !ProbeEnabled()) { + return; + } + + dispatcher->SetFunction(0x82D0AFA0, &Skate3DemoPath_SetFrontEndStateHook); + dispatcher->SetFunction(0x826FDD70, &Skate3DemoPath_LanguageSelectStateHook); + dispatcher->SetFunction(0x826FE1D8, &Skate3DemoPath_ShowPressStartModeHook); + dispatcher->SetFunction(0x82639400, &Skate3DemoPath_LanguageSelectUpdateHook); + REXLOG_INFO("Skate 3 demo path: frontend probe hooks installed"); +} + +bool ShouldForceIntroMovieComplete() { + if (!AutomationEnabled() || !g_skip_intro_movie.load(std::memory_order_relaxed)) { + return false; + } + + bool expected = false; + if (g_logged_intro_movie_skip.compare_exchange_strong(expected, true, + std::memory_order_relaxed)) { + REXLOG_INFO("Skate 3 demo path: forcing frontend intro movie complete"); + } + return true; +} + +} // namespace skate3::demo_path diff --git a/src/skate3_demo_path.h b/src/skate3_demo_path.h new file mode 100644 index 0000000..853a012 --- /dev/null +++ b/src/skate3_demo_path.h @@ -0,0 +1,12 @@ +#pragma once + +namespace rex::runtime { +class FunctionDispatcher; +} + +namespace skate3::demo_path { + +void InstallHooks(rex::runtime::FunctionDispatcher* dispatcher); +bool ShouldForceIntroMovieComplete(); + +} // namespace skate3::demo_path diff --git a/third_party/rexglue-sdk b/third_party/rexglue-sdk index 4e46808..8ce31c2 160000 --- a/third_party/rexglue-sdk +++ b/third_party/rexglue-sdk @@ -1 +1 @@ -Subproject commit 4e468082dce6f5cf32cad0ce2622abd6468e1219 +Subproject commit 8ce31c280a92a041474e95db1a6958c5b1158537