From bd3e41b28f234c81c0803b7d87384f0c2cff47da Mon Sep 17 00:00:00 2001 From: Julien Legay Date: Mon, 28 Sep 2026 09:53:27 +0200 Subject: [PATCH] Implement player invitation dialog HLE stub --- PR_DESCRIPTION.md | 27 ++++++++ core/libs/CMakeLists.txt | 9 +++ .../CMakeLists.txt | 3 + .../libScePlayerInvitationDialog.cpp | 69 +++++++++++++++++++ .../libScePlayerInvitationDialog.h | 30 ++++++++ core/libs/tests/PlayerInvitationDialog.cpp | 40 +++++++++++ 6 files changed, 178 insertions(+) create mode 100644 PR_DESCRIPTION.md create mode 100644 core/libs/prx/libScePlayerInvitationDialog/CMakeLists.txt create mode 100644 core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp create mode 100644 core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.h create mode 100644 core/libs/tests/PlayerInvitationDialog.cpp diff --git a/PR_DESCRIPTION.md b/PR_DESCRIPTION.md new file mode 100644 index 000000000..28806eb5c --- /dev/null +++ b/PR_DESCRIPTION.md @@ -0,0 +1,27 @@ +# Implement an HLE stub for libScePlayerInvitationDialog + +## Why + +Games that load `libScePlayerInvitationDialog` currently have no HLE implementation for the invitation dialog. This can prevent startup or leave games waiting indefinitely for the dialog to finish. + +## Changes + +- Add the PRX module and its `Initialize`, `Open`, `UpdateStatus`, `GetStatus`, `Close`, and `Terminate` exports. +- Simulate the `None -> Initialized -> Running -> Finished` lifecycle: the first `UpdateStatus` after `Open` finishes the dialog without displaying UI or starting a network operation. +- Handle repeated or out-of-order calls and protect shared state with a mutex. +- Log calls and available arguments to the console without dereferencing the opening parameters, whose ABI layout has not been confirmed. +- Add a focused test covering opening, polling, closing, reopening, and out-of-order calls. + +## Verification + +```text +cmake --build build_ninja --target player_invitation_dialog_tests +ctest --test-dir build_ninja -R "^player_invitation_dialog$" --output-on-failure +ninja -C build_ninja core/libs/libs/libScePlayerInvitationDialog.prx +``` + +The focused test passes, and the NID-patched PRX builds successfully. + +## Limitations + +This stub does not display system UI or send invitations. `ScePlayerInvitationDialogParam` is opaque; its exact layout and any additional exports still need to be confirmed against traces or a compatible SDK. diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 1108074b9..a3e7b1519 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -122,6 +122,15 @@ list(APPEND PATCHED_OUTPUTS ${LIBCOHTML_PROSPERO_PATCHED}) add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) if(BUILD_TESTING) + add_executable(player_invitation_dialog_tests tests/PlayerInvitationDialog.cpp) + target_include_directories(player_invitation_dialog_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + target_link_libraries(player_invitation_dialog_tests PRIVATE libScePlayerInvitationDialog libc) + configure_windows_unwind(player_invitation_dialog_tests) + add_test(NAME player_invitation_dialog COMMAND player_invitation_dialog_tests) + if(WIN32) + set_tests_properties(player_invitation_dialog PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_library(host_thread_local_test_library SHARED tests/HostThreadLocalLibrary.cpp) target_include_directories(host_thread_local_test_library PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) add_executable(host_thread_local_tests tests/HostThreadLocal.cpp) diff --git a/core/libs/prx/libScePlayerInvitationDialog/CMakeLists.txt b/core/libs/prx/libScePlayerInvitationDialog/CMakeLists.txt new file mode 100644 index 000000000..6f51b0562 --- /dev/null +++ b/core/libs/prx/libScePlayerInvitationDialog/CMakeLists.txt @@ -0,0 +1,3 @@ +add_library(libScePlayerInvitationDialog SHARED EXCLUDE_FROM_ALL libScePlayerInvitationDialog.cpp) +target_include_directories(libScePlayerInvitationDialog PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libScePlayerInvitationDialog) diff --git a/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp b/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp new file mode 100644 index 000000000..c7e8aaa42 --- /dev/null +++ b/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.cpp @@ -0,0 +1,69 @@ +#include "prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.h" + +#include +#include "prx/libc/include/general/LogMacros.hpp" + +namespace { + +std::mutex g_dialog_mutex; +PlayerInvitationDialogStatus g_dialog_status = PlayerInvitationDialogStatus::None; + +std::int32_t status() { + return static_cast(g_dialog_status); +} + +} + +extern "C" { + +std::int32_t APS5_VABI scePlayerInvitationDialogInitialize(void) { + std::lock_guard lock(g_dialog_mutex); + APS5_LOG_OUT("status=%d", status()); + if (g_dialog_status == PlayerInvitationDialogStatus::None) { + g_dialog_status = PlayerInvitationDialogStatus::Initialized; + } + return 0; +} + +std::int32_t APS5_VABI scePlayerInvitationDialogOpen(const ScePlayerInvitationDialogParam* param) { + std::lock_guard lock(g_dialog_mutex); + APS5_LOG_OUT("param=%p status=%d", static_cast(param), status()); + if (g_dialog_status == PlayerInvitationDialogStatus::None) { + g_dialog_status = PlayerInvitationDialogStatus::Initialized; + } + g_dialog_status = PlayerInvitationDialogStatus::Running; + return 0; +} + +std::int32_t APS5_VABI scePlayerInvitationDialogUpdateStatus(void) { + std::lock_guard lock(g_dialog_mutex); + APS5_LOG_OUT("status=%d", status()); + if (g_dialog_status == PlayerInvitationDialogStatus::Running) { + g_dialog_status = PlayerInvitationDialogStatus::Finished; + } + return status(); +} + +std::int32_t APS5_VABI scePlayerInvitationDialogGetStatus(void) { + std::lock_guard lock(g_dialog_mutex); + APS5_LOG_OUT("status=%d", status()); + return status(); +} + +std::int32_t APS5_VABI scePlayerInvitationDialogClose(void) { + std::lock_guard lock(g_dialog_mutex); + APS5_LOG_OUT("status=%d", status()); + if (g_dialog_status == PlayerInvitationDialogStatus::Running) { + g_dialog_status = PlayerInvitationDialogStatus::Finished; + } + return 0; +} + +std::int32_t APS5_VABI scePlayerInvitationDialogTerminate(void) { + std::lock_guard lock(g_dialog_mutex); + APS5_LOG_OUT("status=%d", status()); + g_dialog_status = PlayerInvitationDialogStatus::None; + return 0; +} + +} diff --git a/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.h b/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.h new file mode 100644 index 000000000..86c42bc78 --- /dev/null +++ b/core/libs/prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.h @@ -0,0 +1,30 @@ +#ifndef CORE_LIBS_PRX_LIBSCEPLAYERINVITATIONDIALOG_LIBSCEPLAYERINVITATIONDIALOG_H +#define CORE_LIBS_PRX_LIBSCEPLAYERINVITATIONDIALOG_LIBSCEPLAYERINVITATIONDIALOG_H + +#include +#include "SceTypes.hpp" + +enum class PlayerInvitationDialogStatus : std::int32_t { + None = 0, + Initialized = 1, + Running = 2, + Finished = 3, +}; + +// Placeholder only: the guest ABI layout is not known and is never read by this HLE stub. +struct ScePlayerInvitationDialogParam { + std::uint8_t reserved[1]; +}; + +extern "C" { + +std::int32_t APS5_VABI scePlayerInvitationDialogInitialize(void); +std::int32_t APS5_VABI scePlayerInvitationDialogOpen(const ScePlayerInvitationDialogParam* param); +std::int32_t APS5_VABI scePlayerInvitationDialogUpdateStatus(void); +std::int32_t APS5_VABI scePlayerInvitationDialogGetStatus(void); +std::int32_t APS5_VABI scePlayerInvitationDialogClose(void); +std::int32_t APS5_VABI scePlayerInvitationDialogTerminate(void); + +} + +#endif diff --git a/core/libs/tests/PlayerInvitationDialog.cpp b/core/libs/tests/PlayerInvitationDialog.cpp new file mode 100644 index 000000000..34fa83ede --- /dev/null +++ b/core/libs/tests/PlayerInvitationDialog.cpp @@ -0,0 +1,40 @@ +#include "prx/libScePlayerInvitationDialog/libScePlayerInvitationDialog.h" + +#include + +namespace { + +void expect(std::int32_t actual, std::int32_t expected) { + if (actual != expected) { + std::abort(); + } +} + +} + +int main() { + using enum PlayerInvitationDialogStatus; + expect(scePlayerInvitationDialogUpdateStatus(), static_cast(None)); + expect(scePlayerInvitationDialogClose(), 0); + expect(scePlayerInvitationDialogTerminate(), 0); + + expect(scePlayerInvitationDialogInitialize(), 0); + expect(scePlayerInvitationDialogInitialize(), 0); + expect(scePlayerInvitationDialogGetStatus(), static_cast(Initialized)); + expect(scePlayerInvitationDialogUpdateStatus(), static_cast(Initialized)); + + ScePlayerInvitationDialogParam param{}; + expect(scePlayerInvitationDialogOpen(¶m), 0); + expect(scePlayerInvitationDialogGetStatus(), static_cast(Running)); + expect(scePlayerInvitationDialogUpdateStatus(), static_cast(Finished)); + expect(scePlayerInvitationDialogUpdateStatus(), static_cast(Finished)); + expect(scePlayerInvitationDialogOpen(nullptr), 0); + expect(scePlayerInvitationDialogClose(), 0); + expect(scePlayerInvitationDialogGetStatus(), static_cast(Finished)); + expect(scePlayerInvitationDialogTerminate(), 0); + expect(scePlayerInvitationDialogGetStatus(), static_cast(None)); + + expect(scePlayerInvitationDialogOpen(nullptr), 0); + expect(scePlayerInvitationDialogUpdateStatus(), static_cast(Finished)); + expect(scePlayerInvitationDialogTerminate(), 0); +}