From f04d16d702a1a0f902fe79cf4ba4bf101b6c0fae Mon Sep 17 00:00:00 2001 From: Get-fix <100056769+Get-fix@users.noreply.github.com> Date: Sat, 17 Oct 2026 05:05:38 +0300 Subject: [PATCH] feat: PS4 NpTrophy, save data v1, NpWebApi offline and Limbo's other imports NpTrophy reads the trophy count and icons from the title's trophy package (names are encrypted and stay not implemented), PS4 v1 save data calls wrap the native v2/v3 ones, and NpWebApi library contexts, handles and push event filters work offline (service filters fail without a signed-in user). libSceAjm is built from the libSceAjm.native sources for PS4 titles. Limbo's (CUSA01664) remaining PS4 imports, including the synchronous PS4 TUS calls and the Facebook dialog calls next to the existing libraries, are exported as not implemented. --- .../prx/libSceAjm.native/AjmLibrary.cmake | 15 + core/libs/prx/libSceAjm.native/CMakeLists.txt | 14 +- core/libs/prx/libSceAjm/CMakeLists.txt | 2 + core/libs/prx/libSceAjm/Export.cpp | 15 +- .../prx/libSceGameCustomDataDialog/Export.cpp | 31 ++ .../prx/libSceInvitationDialog/Export.cpp | 31 ++ .../prx/libSceNpCommon/PendingExports.cpp | 5 + core/libs/prx/libSceNpManager/Export.cpp | 49 ++++ core/libs/prx/libSceNpMatching2/Export.cpp | 151 ++++++++++ core/libs/prx/libSceNpScoreRanking/Export.cpp | 81 ++++++ core/libs/prx/libSceNpSignaling/Export.cpp | 46 +++ .../prx/libSceNpSnsFacebookDialog/Export.cpp | 25 ++ core/libs/prx/libSceNpTrophy/CMakeLists.txt | 16 ++ core/libs/prx/libSceNpTrophy/Export.cpp | 268 ++++++++++++++++++ core/libs/prx/libSceNpTus/Export.cpp | 50 ++++ core/libs/prx/libSceNpUtility/Export.cpp | 79 ++++++ core/libs/prx/libSceNpWebApi/CMakeLists.txt | 16 ++ core/libs/prx/libSceNpWebApi/Export.cpp | 181 ++++++++++++ .../libs/prx/libSceSaveData.native/Export.cpp | 74 +++++ core/libs/prx/libkernel/Unimplemented.cpp | 14 + core/libs/tests/GuestNpTrophy.cpp | 119 ++++++++ core/libs/tests/GuestNpWebApi.cpp | 67 +++++ core/libs/tests/GuestSaveDataPs4.cpp | 24 ++ docs/dev/debt/libSceNpTrophy.md | 5 + docs/dev/debt/libSceNpWebApi.md | 6 + 25 files changed, 1371 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libSceAjm.native/AjmLibrary.cmake create mode 100644 core/libs/prx/libSceAjm/CMakeLists.txt create mode 100644 core/libs/prx/libSceGameCustomDataDialog/Export.cpp create mode 100644 core/libs/prx/libSceInvitationDialog/Export.cpp create mode 100644 core/libs/prx/libSceNpMatching2/Export.cpp create mode 100644 core/libs/prx/libSceNpScoreRanking/Export.cpp create mode 100644 core/libs/prx/libSceNpSignaling/Export.cpp create mode 100644 core/libs/prx/libSceNpTrophy/CMakeLists.txt create mode 100644 core/libs/prx/libSceNpTrophy/Export.cpp create mode 100644 core/libs/prx/libSceNpWebApi/CMakeLists.txt create mode 100644 core/libs/prx/libSceNpWebApi/Export.cpp create mode 100644 core/libs/tests/GuestNpTrophy.cpp create mode 100644 core/libs/tests/GuestNpWebApi.cpp create mode 100644 docs/dev/debt/libSceNpTrophy.md create mode 100644 docs/dev/debt/libSceNpWebApi.md diff --git a/core/libs/prx/libSceAjm.native/AjmLibrary.cmake b/core/libs/prx/libSceAjm.native/AjmLibrary.cmake new file mode 100644 index 0000000000..ec9f5b9a81 --- /dev/null +++ b/core/libs/prx/libSceAjm.native/AjmLibrary.cmake @@ -0,0 +1,15 @@ +function(add_sce_ajm_library target) + set(ajmDir ${CMAKE_CURRENT_FUNCTION_LIST_DIR}) + add_library(${target} SHARED EXCLUDE_FROM_ALL + ${ajmDir}/Export.cpp + ${ajmDir}/src/Ajm.cpp + ${ARGN} + ) + target_include_directories(${target} PRIVATE + ${LIBS_INCLUDE_DIR} + ${CMAKE_SOURCE_DIR}/3rdparty/LibAtrac9/C/src + ${CMAKE_SOURCE_DIR}/3rdparty/vgmstream/src/coding/libs + ) + target_link_libraries(${target} PRIVATE atrac9 atrac9_vibration ffmpeg libc libkernel) + configure_windows_unwind(${target}) +endfunction() diff --git a/core/libs/prx/libSceAjm.native/CMakeLists.txt b/core/libs/prx/libSceAjm.native/CMakeLists.txt index 51b7c2507f..04c4242ad8 100644 --- a/core/libs/prx/libSceAjm.native/CMakeLists.txt +++ b/core/libs/prx/libSceAjm.native/CMakeLists.txt @@ -29,18 +29,8 @@ add_library(atrac9_vibration STATIC EXCLUDE_FROM_ALL ) set_target_properties(atrac9_vibration PROPERTIES POSITION_INDEPENDENT_CODE ON C_VISIBILITY_PRESET hidden) -add_library(libSceAjm.native SHARED EXCLUDE_FROM_ALL - ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/Ajm.cpp -) - -target_include_directories(libSceAjm.native PRIVATE - ${LIBS_INCLUDE_DIR} - ${LIBATRAC9_DIR} - ${VGMSTREAM_ATRAC9_DIR} -) -target_link_libraries(libSceAjm.native PRIVATE atrac9 atrac9_vibration ffmpeg libc libkernel) -configure_windows_unwind(libSceAjm.native) +include(${CMAKE_CURRENT_SOURCE_DIR}/AjmLibrary.cmake) +add_sce_ajm_library(libSceAjm.native) if(BUILD_TESTING) add_test_executable(atrac9_gradient_tests tests/Atrac9Gradient.c ${LIBATRAC9_DIR}/bit_reader.c) diff --git a/core/libs/prx/libSceAjm/CMakeLists.txt b/core/libs/prx/libSceAjm/CMakeLists.txt new file mode 100644 index 0000000000..7a9c708eae --- /dev/null +++ b/core/libs/prx/libSceAjm/CMakeLists.txt @@ -0,0 +1,2 @@ +include(${CMAKE_CURRENT_SOURCE_DIR}/../libSceAjm.native/AjmLibrary.cmake) +add_sce_ajm_library(libSceAjm ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp) diff --git a/core/libs/prx/libSceAjm/Export.cpp b/core/libs/prx/libSceAjm/Export.cpp index fb34206c21..508f721ede 100644 --- a/core/libs/prx/libSceAjm/Export.cpp +++ b/core/libs/prx/libSceAjm/Export.cpp @@ -5,6 +5,19 @@ extern "C" { -APS5_DUMMY_FUN +int APS5_VABI sceAjmBatchJobControlBufferRa() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAjmBatchJobRunSplitBufferRa() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceAjmBatchStartBuffer() { + NotImplemented_nid_no_patch(__func__); + return 0; +} } diff --git a/core/libs/prx/libSceGameCustomDataDialog/Export.cpp b/core/libs/prx/libSceGameCustomDataDialog/Export.cpp new file mode 100644 index 0000000000..5b647ad000 --- /dev/null +++ b/core/libs/prx/libSceGameCustomDataDialog/Export.cpp @@ -0,0 +1,31 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceGameCustomDataDialogGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameCustomDataDialogInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameCustomDataDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameCustomDataDialogTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceGameCustomDataDialogUpdateStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceInvitationDialog/Export.cpp b/core/libs/prx/libSceInvitationDialog/Export.cpp new file mode 100644 index 0000000000..8ae801bc26 --- /dev/null +++ b/core/libs/prx/libSceInvitationDialog/Export.cpp @@ -0,0 +1,31 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceInvitationDialogGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceInvitationDialogInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceInvitationDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceInvitationDialogTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceInvitationDialogUpdateStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpCommon/PendingExports.cpp b/core/libs/prx/libSceNpCommon/PendingExports.cpp index c9d6e53ffd..d2daed1a92 100644 --- a/core/libs/prx/libSceNpCommon/PendingExports.cpp +++ b/core/libs/prx/libSceNpCommon/PendingExports.cpp @@ -115,6 +115,11 @@ int APS5_VABI sceNpCloseSema(void) { return 0; } +int APS5_VABI sceNpCmpNpId(void) { + NotImplemented_nid_no_patch(__func__); + return 0; +} + int APS5_VABI sceNpCmpNpIdInOrder(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceNpManager/Export.cpp b/core/libs/prx/libSceNpManager/Export.cpp index 1b45d38294..1efce7122e 100644 --- a/core/libs/prx/libSceNpManager/Export.cpp +++ b/core/libs/prx/libSceNpManager/Export.cpp @@ -981,3 +981,52 @@ int APS5_VABI sceNpCheckCallbackForLib() { } } + +extern "C" { + +int APS5_VABI sceNpInGameMessageCreateHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessageDeleteHandle() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessageInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessagePrepare() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessageSendData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpInGameMessageTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookGetAccessToken() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpMatching2/Export.cpp b/core/libs/prx/libSceNpMatching2/Export.cpp new file mode 100644 index 0000000000..2746971c77 --- /dev/null +++ b/core/libs/prx/libSceNpMatching2/Export.cpp @@ -0,0 +1,151 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpMatching2ContextStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2ContextStop() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2CreateContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2CreateJoinRoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2DestroyContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetRoomDataExternalList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetRoomDataInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetRoomMemberDataInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetServerId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetUserInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2GetWorldInfoList() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2Initialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2JoinRoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2KickoutRoomMember() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2LeaveRoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2RegisterContextCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2RegisterRoomEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2RegisterRoomMessageCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2RegisterSignalingCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SearchRoom() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SendRoomChatMessage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SendRoomMessage() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetDefaultRequestOptParam() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetRoomDataExternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetRoomDataInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetRoomMemberDataInternal() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SetUserInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2SignalingGetConnectionInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpMatching2Terminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpScoreRanking/Export.cpp b/core/libs/prx/libSceNpScoreRanking/Export.cpp new file mode 100644 index 0000000000..1bb8bc6df6 --- /dev/null +++ b/core/libs/prx/libSceNpScoreRanking/Export.cpp @@ -0,0 +1,81 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpScoreAbortRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreCreateNpTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreDeleteNpTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetBoardInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetFriendsRanking() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetFriendsRankingAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetRankingByNpId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetRankingByNpIdAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetRankingByRange() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreGetRankingByRangeAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScorePollAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreRecordScore() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpScoreRecordScoreAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpSignaling/Export.cpp b/core/libs/prx/libSceNpSignaling/Export.cpp new file mode 100644 index 0000000000..8ab2a25170 --- /dev/null +++ b/core/libs/prx/libSceNpSignaling/Export.cpp @@ -0,0 +1,46 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +extern "C" { + +int APS5_VABI sceNpSignalingActivateConnection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingCreateContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingDeleteContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingGetConnectionInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingSetContextOption() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSignalingTerminateConnection() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp b/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp index 58bb27df19..9a4fe0e8cd 100644 --- a/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp +++ b/core/libs/prx/libSceNpSnsFacebookDialog/Export.cpp @@ -15,4 +15,29 @@ int APS5_VABI sceNpSnsFacebookDialogGetStatus(void) { return 0; } +int APS5_VABI sceNpSnsFacebookDialogGetResult() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogInitialize() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogOpen() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogTerminate() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpSnsFacebookDialogUpdateStatus() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpTrophy/CMakeLists.txt b/core/libs/prx/libSceNpTrophy/CMakeLists.txt new file mode 100644 index 0000000000..7301fa20ab --- /dev/null +++ b/core/libs/prx/libSceNpTrophy/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceNpTrophy SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpTrophy PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpTrophy) + +if(BUILD_TESTING) + add_test_executable(guest_np_trophy_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpTrophy.cpp) + target_include_directories(guest_np_trophy_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_trophy_tests PRIVATE libSceNpTrophy) + configure_windows_unwind(guest_np_trophy_tests) + add_test(NAME guest_np_trophy COMMAND guest_np_trophy_tests) + set_tests_properties(guest_np_trophy PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_trophy PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpTrophy/Export.cpp b/core/libs/prx/libSceNpTrophy/Export.cpp new file mode 100644 index 0000000000..aad30b5af0 --- /dev/null +++ b/core/libs/prx/libSceNpTrophy/Export.cpp @@ -0,0 +1,268 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" + +namespace { + +constexpr int NP_TROPHY_ERROR_INVALID_ARGUMENT = static_cast(0x80551604u); +constexpr int NP_TROPHY_ERROR_INSUFFICIENT_BUFFER = static_cast(0x80551605u); +constexpr int NP_TROPHY_ERROR_INVALID_HANDLE = static_cast(0x80551608u); +constexpr int NP_TROPHY_ERROR_INVALID_CONTEXT = static_cast(0x80551609u); +constexpr int NP_TROPHY_ERROR_INVALID_TROPHY_ID = static_cast(0x8055160au); +constexpr int NP_TROPHY_ERROR_INVALID_GROUP_ID = static_cast(0x8055160bu); +constexpr int NP_TROPHY_ERROR_TROPHY_ALREADY_UNLOCKED = static_cast(0x8055160cu); +constexpr int NP_TROPHY_ERROR_NOT_REGISTERED = static_cast(0x8055160fu); +constexpr int NP_TROPHY_ERROR_ALREADY_REGISTERED = static_cast(0x80551610u); +constexpr int NP_TROPHY_ERROR_CONTEXT_ALREADY_EXISTS = static_cast(0x80551613u); +constexpr int NP_TROPHY_ERROR_ICON_FILE_NOT_FOUND = static_cast(0x80551614u); +constexpr int NP_TROPHY_ERROR_INVALID_TRP_FILE_FORMAT = static_cast(0x80551616u); +constexpr int NP_TROPHY_ERROR_TROPHY_NOT_UNLOCKED = static_cast(0x8055161au); +constexpr int NP_TROPHY_ERROR_TITLE_CONF_NOT_INSTALLED = static_cast(0x80551626u); +constexpr int NP_TROPHY_INVALID_ID = -1; +constexpr int NP_TROPHY_NUM_MAX = 128; +constexpr std::uint32_t TrpMagic = 0xdca24d00u; +constexpr std::size_t TrpHeaderBytes = 0x60; +constexpr std::size_t TrpEntryBytes = 0x40; + +struct TrophyFlagArray { + std::uint32_t bits[NP_TROPHY_NUM_MAX / 32]; +}; + +struct TrpEntry { + std::uint64_t offset; + std::uint64_t size; +}; + +struct Context { + int userId; + std::uint32_t serviceLabel; + bool registered = false; + std::filesystem::path package; + std::map entries; + int trophies = 0; + std::filesystem::path state; + std::set unlocked; +}; + +std::mutex g_mutex; +std::map g_contexts; +std::set g_handles; +int g_nextContext = 1; +int g_nextHandle = 1; + +std::uint32_t BigEndian32(const std::uint8_t* bytes) { + return (std::uint32_t{bytes[0]} << 24) | (std::uint32_t{bytes[1]} << 16) | (std::uint32_t{bytes[2]} << 8) | bytes[3]; +} + +std::uint64_t BigEndian64(const std::uint8_t* bytes) { + return (std::uint64_t{BigEndian32(bytes)} << 32) | BigEndian32(bytes + 4); +} + +std::optional> ReadPackage(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) return std::nullopt; + std::array header{}; + if (!file.read(reinterpret_cast(header.data()), header.size()) || BigEndian32(header.data()) != TrpMagic) return std::map{}; + const auto count = BigEndian32(header.data() + 0x10); + if (BigEndian32(header.data() + 0x14) != TrpEntryBytes) return std::map{}; + std::map entries; + for (std::uint32_t i = 0; i < count; ++i) { + std::array entry{}; + if (!file.read(reinterpret_cast(entry.data()), entry.size())) return std::map{}; + const std::string name(reinterpret_cast(entry.data()), strnlen(reinterpret_cast(entry.data()), 32)); + entries[name] = {BigEndian64(entry.data() + 32), BigEndian64(entry.data() + 40)}; + } + return entries; +} + +void LoadState(Context& context) { + std::ifstream file(context.state); + int id = 0; + while (file >> id) { + if (id >= 0 && id < context.trophies) context.unlocked.insert(id); + } +} + +void SaveState(const Context& context) { + std::filesystem::create_directories(context.state.parent_path()); + const auto temporary = context.state.string() + ".tmp"; + { + std::ofstream file(temporary, std::ios::trunc); + for (const auto id : context.unlocked) file << id << '\n'; + } + std::filesystem::rename(temporary, context.state); +} + +int Lookup(int contextId, int handle, Context*& context) { + if (contextId == NP_TROPHY_INVALID_ID) return NP_TROPHY_ERROR_INVALID_CONTEXT; + if (handle == NP_TROPHY_INVALID_ID) return NP_TROPHY_ERROR_INVALID_HANDLE; + const auto found = g_contexts.find(contextId); + if (found == g_contexts.end()) return NP_TROPHY_ERROR_INVALID_CONTEXT; + if (!g_handles.contains(handle)) return NP_TROPHY_ERROR_INVALID_HANDLE; + context = &found->second; + return 0; +} + +int CopyIcon(const Context& context, const std::string& name, void* buffer, std::uint64_t* size) { + const auto entry = context.entries.find(name); + if (entry == context.entries.end()) return NP_TROPHY_ERROR_ICON_FILE_NOT_FOUND; + if (buffer == nullptr) { + *size = entry->second.size; + return 0; + } + if (*size < entry->second.size) return NP_TROPHY_ERROR_INSUFFICIENT_BUFFER; + std::ifstream file(context.package, std::ios::binary); + file.seekg(static_cast(entry->second.offset)); + if (!file.read(static_cast(buffer), static_cast(entry->second.size))) return NP_TROPHY_ERROR_INVALID_TRP_FILE_FORMAT; + *size = entry->second.size; + return 0; +} + +} + +extern "C" { + +int APS5_VABI sceNpTrophyCreateContext(int* context, int userId, std::uint32_t serviceLabel, std::uint64_t options) { + if (!context || options != 0) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + for (const auto& [id, existing] : g_contexts) { + if (existing.userId == userId && existing.serviceLabel == serviceLabel) return NP_TROPHY_ERROR_CONTEXT_ALREADY_EXISTS; + } + const int id = g_nextContext++; + g_contexts[id] = Context{userId, serviceLabel}; + *context = id; + return 0; +} + +int APS5_VABI sceNpTrophyDestroyContext(int context) { + std::lock_guard lock(g_mutex); + return g_contexts.erase(context) == 1 ? 0 : NP_TROPHY_ERROR_INVALID_CONTEXT; +} + +int APS5_VABI sceNpTrophyCreateHandle(int* handle) { + if (!handle) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + const int id = g_nextHandle++; + g_handles.insert(id); + *handle = id; + return 0; +} + +int APS5_VABI sceNpTrophyDestroyHandle(int handle) { + std::lock_guard lock(g_mutex); + return g_handles.erase(handle) == 1 ? 0 : NP_TROPHY_ERROR_INVALID_HANDLE; +} + +int APS5_VABI sceNpTrophyRegisterContext(int contextId, int handle, std::uint64_t options) { + if (options != 0) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (context->registered) return NP_TROPHY_ERROR_ALREADY_REGISTERED; + char name[32]; + std::snprintf(name, sizeof(name), "trophy%02u.trp", context->serviceLabel); + context->package = ResolvePath_nid_no_patch((std::string("/app0/sce_sys/trophy/") + name).c_str()); + const auto entries = ReadPackage(context->package); + if (!entries) return NP_TROPHY_ERROR_TITLE_CONF_NOT_INSTALLED; + if (entries->empty()) return NP_TROPHY_ERROR_INVALID_TRP_FILE_FORMAT; + context->entries = *entries; + for (int id = 0; id < NP_TROPHY_NUM_MAX; ++id) { + char icon[16]; + std::snprintf(icon, sizeof(icon), "TROP%03d.PNG", id); + if (!context->entries.contains(icon)) break; + context->trophies = id + 1; + } + std::snprintf(name, sizeof(name), "trophy%02u.%d", context->serviceLabel, context->userId); + context->state = std::filesystem::path("_trophy") / name; + LoadState(*context); + context->registered = true; + return 0; +} + +int APS5_VABI sceNpTrophyGetTrophyUnlockState(int contextId, int handle, TrophyFlagArray* flags, std::uint32_t* count) { + if (!flags || !count) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + std::memset(flags, 0, sizeof(*flags)); + for (const auto id : context->unlocked) flags->bits[id >> 5] |= 1u << (id & 31); + *count = static_cast(context->trophies); + return 0; +} + +int APS5_VABI sceNpTrophyUnlockTrophy(int contextId, int handle, int trophyId, int* platinumId) { + if (!platinumId) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + if (trophyId < 0 || trophyId >= context->trophies) return NP_TROPHY_ERROR_INVALID_TROPHY_ID; + if (context->unlocked.contains(trophyId)) return NP_TROPHY_ERROR_TROPHY_ALREADY_UNLOCKED; + context->unlocked.insert(trophyId); + SaveState(*context); + *platinumId = NP_TROPHY_INVALID_ID; + return 0; +} + +int APS5_VABI sceNpTrophyGetGameIcon(int contextId, int handle, void* buffer, std::uint64_t* size) { + if (!size) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + return CopyIcon(*context, "ICON0.PNG", buffer, size); +} + +int APS5_VABI sceNpTrophyGetGroupIcon(int contextId, int handle, int groupId, void* buffer, std::uint64_t* size) { + if (!size) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + if (groupId < 0) return NP_TROPHY_ERROR_INVALID_GROUP_ID; + char icon[16]; + std::snprintf(icon, sizeof(icon), "GR%03d.PNG", groupId); + return CopyIcon(*context, icon, buffer, size); +} + +int APS5_VABI sceNpTrophyGetTrophyIcon(int contextId, int handle, int trophyId, void* buffer, std::uint64_t* size) { + if (!size) return NP_TROPHY_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(g_mutex); + Context* context = nullptr; + if (const int error = Lookup(contextId, handle, context)) return error; + if (!context->registered) return NP_TROPHY_ERROR_NOT_REGISTERED; + if (trophyId < 0 || trophyId >= context->trophies) return NP_TROPHY_ERROR_INVALID_TROPHY_ID; + if (!context->unlocked.contains(trophyId)) return NP_TROPHY_ERROR_TROPHY_NOT_UNLOCKED; + char icon[16]; + std::snprintf(icon, sizeof(icon), "TROP%03d.PNG", trophyId); + return CopyIcon(*context, icon, buffer, size); +} + +int APS5_VABI sceNpTrophyGetGameInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophyGetGroupInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTrophyGetTrophyInfo() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpTus/Export.cpp b/core/libs/prx/libSceNpTus/Export.cpp index a69e6cb5a6..54f32438da 100644 --- a/core/libs/prx/libSceNpTus/Export.cpp +++ b/core/libs/prx/libSceNpTus/Export.cpp @@ -782,4 +782,54 @@ int APS5_VABI sceNpTusTryAndSetVariableForCrossSaveVUserAsync(void) { return 0; } +int APS5_VABI sceNpTssGetData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusCreateNpTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetDataVUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusGetMultiSlotVariableVUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetDataVUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetMultiSlotVariable() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpTusSetMultiSlotVariableVUser() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + } diff --git a/core/libs/prx/libSceNpUtility/Export.cpp b/core/libs/prx/libSceNpUtility/Export.cpp index 79f5389752..2247cdf62b 100644 --- a/core/libs/prx/libSceNpUtility/Export.cpp +++ b/core/libs/prx/libSceNpUtility/Export.cpp @@ -38,3 +38,82 @@ int APS5_VABI sceNpBandwidthTestShutdown(int context_id, void* result) { } } + +extern "C" { + +int APS5_VABI sceNpBandwidthTestInitStart() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupCreateTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupDeleteTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpLookupNpId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterAbortRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterCensorComment() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterCreateAsyncRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterCreateTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterDeleteTitleCtx() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterPollAsync() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWordFilterSanitizeComment() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceNpWebApi/CMakeLists.txt b/core/libs/prx/libSceNpWebApi/CMakeLists.txt new file mode 100644 index 0000000000..0e3e193000 --- /dev/null +++ b/core/libs/prx/libSceNpWebApi/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceNpWebApi SHARED EXCLUDE_FROM_ALL Export.cpp) +target_include_directories(libSceNpWebApi PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceNpWebApi) + +if(BUILD_TESTING) + add_test_executable(guest_np_web_api_tests ${LIBS_INCLUDE_DIR}/tests/GuestNpWebApi.cpp) + target_include_directories(guest_np_web_api_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_np_web_api_tests PRIVATE libSceNpWebApi) + configure_windows_unwind(guest_np_web_api_tests) + add_test(NAME guest_np_web_api COMMAND guest_np_web_api_tests) + set_tests_properties(guest_np_web_api PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_np_web_api PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceNpWebApi/Export.cpp b/core/libs/prx/libSceNpWebApi/Export.cpp new file mode 100644 index 0000000000..1aece4e185 --- /dev/null +++ b/core/libs/prx/libSceNpWebApi/Export.cpp @@ -0,0 +1,181 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include + +static constexpr int SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT = static_cast(0x80552902); +static constexpr int SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND = static_cast(0x80552904); +static constexpr int SCE_NP_WEBAPI_ERROR_PUSH_EVENT_FILTER_NOT_FOUND = static_cast(0x8055290B); +static constexpr int SCE_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND = static_cast(0x8055290D); +static constexpr int SCE_NP_WEBAPI_ERROR_SERVICE_PUSH_EVENT_FILTER_NOT_FOUND = static_cast(0x8055290E); +static constexpr int SCE_NP_WEBAPI_ERROR_SIGNED_IN_USER_NOT_FOUND = static_cast(0x80552910); +static constexpr int SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_MAX = static_cast(0x8055291A); + +namespace { + +struct LibraryContext { + int libHttpCtxId; + std::size_t poolSize; + std::unordered_set handles; + std::unordered_set pushEventFilters; +}; + +std::mutex contextsMutex; +std::unordered_map contexts; +int lastContext = 0; +int lastObject = 0; + +int nextObject() { + lastObject = lastObject % 0xefffffff + 1; + return lastObject; +} + +} + +extern "C" { + +int APS5_VABI sceNpWebApiAbortRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiCreateContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiCreateHandle(int libCtxId) { + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + const auto handle = nextObject(); + context->second.handles.insert(handle); + return handle; +} + +int APS5_VABI sceNpWebApiCreatePushEventFilter(int libCtxId, const void* filterParam, std::size_t filterParamNum) { + if (!filterParam || filterParamNum == 0) return SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + const auto filter = nextObject(); + context->second.pushEventFilters.insert(filter); + return filter; +} + +int APS5_VABI sceNpWebApiCreateRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiCreateServicePushEventFilter(int libCtxId, int handleId, const char* npServiceName, std::uint32_t npServiceLabel, const void* filterParam, std::size_t filterParamNum) { + (void)npServiceLabel; + if (!npServiceName || !filterParam || filterParamNum == 0) return SCE_NP_WEBAPI_ERROR_INVALID_ARGUMENT; + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + if (!context->second.handles.contains(handleId)) return SCE_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND; + return SCE_NP_WEBAPI_ERROR_SIGNED_IN_USER_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiDeleteContext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiDeleteHandle(int libCtxId, int handleId) { + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + return context->second.handles.erase(handleId) != 0 ? 0 : SCE_NP_WEBAPI_ERROR_HANDLE_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiDeletePushEventFilter(int libCtxId, int filterId) { + std::lock_guard lock(contextsMutex); + const auto context = contexts.find(libCtxId); + if (context == contexts.end()) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + return context->second.pushEventFilters.erase(filterId) != 0 ? 0 : SCE_NP_WEBAPI_ERROR_PUSH_EVENT_FILTER_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiDeleteRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiDeleteServicePushEventFilter(int libCtxId, int filterId) { + (void)filterId; + std::lock_guard lock(contextsMutex); + if (!contexts.contains(libCtxId)) return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; + return SCE_NP_WEBAPI_ERROR_SERVICE_PUSH_EVENT_FILTER_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiGetHttpResponseHeaderValue() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiGetHttpResponseHeaderValueLength() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiGetHttpStatusCode() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiInitialize(int libHttpCtxId, std::size_t poolSize) { + std::lock_guard lock(contextsMutex); + for (int attempt = 0; attempt < 0x7fff; ++attempt) { + lastContext = lastContext % 0x7fff + 1; + if (contexts.contains(lastContext)) continue; + contexts[lastContext] = {libHttpCtxId, poolSize, {}, {}}; + return lastContext; + } + return SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_MAX; +} + +int APS5_VABI sceNpWebApiReadData() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiRegisterPushEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiRegisterServicePushEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiSendRequest() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiTerminate(int libCtxId) { + std::lock_guard lock(contextsMutex); + return contexts.erase(libCtxId) != 0 ? 0 : SCE_NP_WEBAPI_ERROR_LIB_CONTEXT_NOT_FOUND; +} + +int APS5_VABI sceNpWebApiUnregisterPushEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiUnregisterServicePushEventCallback() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sceNpWebApiUtilityParseNpId() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index a95498f7d9..9549157996 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -1031,3 +1031,77 @@ int APS5_VABI sceSaveDataDebugTarget() { } } + +namespace { + +struct SaveDataMountPs4 { + std::int32_t user_id; + std::int32_t pad; + const void* title_id; + const SceSaveDataDirName* dir_name; + const void* fingerprint; + std::uint64_t blocks; + std::uint32_t mount_mode; + std::uint8_t reserved[32]; +}; + +} + +extern "C" { + +int APS5_VABI sceSaveDataInitialize(const void* init) { + return sceSaveDataInitialize3(init); +} + +int APS5_VABI sceSaveDataMount(const SaveDataMountPs4* mount, SaveDataMountResult* mount_result) { + if (mount == nullptr || mount->dir_name == nullptr) return SAVE_DATA_ERROR_PARAMETER; + if (mount->title_id != nullptr) NotImplemented_nid_no_patch("sceSaveDataMount of another title's save data"); + SaveDataMount3 converted{}; + converted.user_id = mount->user_id; + converted.dir_name = mount->dir_name; + converted.blocks = mount->blocks; + converted.mount_mode = mount->mount_mode; + return sceSaveDataMount3(&converted, mount_result); +} + +int APS5_VABI sceSaveDataUmount(const SaveDataMountPoint* mount_point) { + return sceSaveDataUmount2(0, mount_point); +} + +int APS5_VABI sceSaveDataSetupSaveDataMemory(std::int32_t user_id, std::size_t memory_size, const SaveDataParam* param) { + SaveDataMemorySetup2 setup{}; + setup.user_id = user_id; + setup.memory_size = memory_size; + SaveDataMemorySetupResult result{}; + if (const int rc = sceSaveDataSetupSaveDataMemory2(&setup, &result); rc != SAVE_DATA_OK) return rc; + if (param == nullptr) return SAVE_DATA_OK; + SaveDataMemorySet2 set{}; + set.user_id = user_id; + set.param = param; + return sceSaveDataSetSaveDataMemory2(&set); +} + +int APS5_VABI sceSaveDataGetSaveDataMemory(std::int32_t user_id, void* buf, std::size_t buf_size, std::int64_t offset) { + SaveDataMemoryData data{}; + data.buf = buf; + data.buf_size = buf_size; + data.offset = static_cast(offset); + SaveDataMemoryGet2 get{}; + get.user_id = user_id; + get.data = &data; + return sceSaveDataGetSaveDataMemory2(&get); +} + +int APS5_VABI sceSaveDataSetSaveDataMemory(std::int32_t user_id, void* buf, std::size_t buf_size, std::int64_t offset) { + SaveDataMemoryData data{}; + data.buf = buf; + data.buf_size = buf_size; + data.offset = static_cast(offset); + SaveDataMemorySet2 set{}; + set.user_id = user_id; + set.data = &data; + set.data_num = 1; + return sceSaveDataSetSaveDataMemory2(&set); +} + +} diff --git a/core/libs/prx/libkernel/Unimplemented.cpp b/core/libs/prx/libkernel/Unimplemented.cpp index 3dd75f72d8..617dc506f1 100644 --- a/core/libs/prx/libkernel/Unimplemented.cpp +++ b/core/libs/prx/libkernel/Unimplemented.cpp @@ -133,3 +133,17 @@ int APS5_VABI sceKernelGetProcessAbiType() { } } + +extern "C" { + +int APS5_VABI __Ux86_64_setcontext() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +int APS5_VABI sigreturn() { + NotImplemented_nid_no_patch(__func__); + return 0; +} + +} diff --git a/core/libs/tests/GuestNpTrophy.cpp b/core/libs/tests/GuestNpTrophy.cpp new file mode 100644 index 0000000000..e8a9b8749f --- /dev/null +++ b/core/libs/tests/GuestNpTrophy.cpp @@ -0,0 +1,119 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpTrophyCreateContext(int* context, int userId, std::uint32_t serviceLabel, std::uint64_t options); +int APS5_VABI sceNpTrophyDestroyContext(int context); +int APS5_VABI sceNpTrophyCreateHandle(int* handle); +int APS5_VABI sceNpTrophyDestroyHandle(int handle); +int APS5_VABI sceNpTrophyRegisterContext(int context, int handle, std::uint64_t options); +int APS5_VABI sceNpTrophyGetTrophyUnlockState(int context, int handle, std::uint32_t* flags, std::uint32_t* count); +int APS5_VABI sceNpTrophyUnlockTrophy(int context, int handle, int trophyId, int* platinumId); +int APS5_VABI sceNpTrophyGetGameIcon(int context, int handle, void* buffer, std::uint64_t* size); +int APS5_VABI sceNpTrophyGetTrophyIcon(int context, int handle, int trophyId, void* buffer, std::uint64_t* size); +} + +namespace { + +constexpr int InvalidArgument = static_cast(0x80551604u); +constexpr int InsufficientBuffer = static_cast(0x80551605u); +constexpr int InvalidContext = static_cast(0x80551609u); +constexpr int InvalidTrophyId = static_cast(0x8055160au); +constexpr int AlreadyUnlocked = static_cast(0x8055160cu); +constexpr int NotRegistered = static_cast(0x8055160fu); +constexpr int AlreadyRegistered = static_cast(0x80551610u); +constexpr int ContextAlreadyExists = static_cast(0x80551613u); +constexpr int TitleConfNotInstalled = static_cast(0x80551626u); +constexpr int TrophyNotUnlocked = static_cast(0x8055161au); + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "NpTrophy: %s\n", message); + std::abort(); + } +} + +void PutBigEndian(std::vector& bytes, std::size_t at, std::uint64_t value, int width) { + for (int i = 0; i < width; ++i) bytes[at + i] = static_cast(value >> (8 * (width - 1 - i))); +} + +void WritePackage(const std::filesystem::path& path, const std::vector>& files) { + const std::size_t header = 0x60, entry = 0x40; + std::vector bytes(header + entry * files.size()); + PutBigEndian(bytes, 0, 0xdca24d00u, 4); + PutBigEndian(bytes, 0x10, files.size(), 4); + PutBigEndian(bytes, 0x14, entry, 4); + for (std::size_t i = 0; i < files.size(); ++i) { + const auto at = header + entry * i; + std::memcpy(bytes.data() + at, files[i].first.data(), files[i].first.size()); + PutBigEndian(bytes, at + 32, bytes.size(), 8); + PutBigEndian(bytes, at + 40, files[i].second.size(), 8); + bytes.insert(bytes.end(), files[i].second.begin(), files[i].second.end()); + } + std::filesystem::create_directories(path.parent_path()); + std::ofstream(path, std::ios::binary).write(reinterpret_cast(bytes.data()), static_cast(bytes.size())); +} + +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / ("anyps5-np-trophy-" + std::to_string(std::random_device{}())); + std::filesystem::remove_all(root); + WritePackage(root / "app0/sce_sys/trophy/trophy00.trp", {{"TROPCONF.ESFM", "conf"}, {"ICON0.PNG", "game-icon"}, {"TROP000.PNG", "first"}, {"TROP001.PNG", "second"}}); + std::filesystem::current_path(root); + + int context = 0, handle = 0, missing = 0, platinum = 0; + Require(sceNpTrophyCreateContext(nullptr, 1, 0, 0) == InvalidArgument, "null context"); + Require(sceNpTrophyCreateContext(&context, 1, 0, 1) == InvalidArgument, "context options"); + Require(sceNpTrophyCreateContext(&context, 1, 0, 0) == 0, "create the context"); + Require(sceNpTrophyCreateContext(&missing, 1, 0, 0) == ContextAlreadyExists, "second context for the same user and label"); + Require(sceNpTrophyCreateHandle(&handle) == 0, "create the handle"); + + std::uint32_t flags[4] = {}, count = 0; + Require(sceNpTrophyGetTrophyUnlockState(context, handle, flags, &count) == NotRegistered, "state before registration"); + Require(sceNpTrophyRegisterContext(context, handle, 0) == 0, "register the context"); + Require(sceNpTrophyRegisterContext(context, handle, 0) == AlreadyRegistered, "register twice"); + Require(sceNpTrophyGetTrophyUnlockState(context, handle, flags, &count) == 0 && count == 2 && flags[0] == 0, "two trophies, none unlocked"); + + std::uint64_t size = 0; + Require(sceNpTrophyGetGameIcon(context, handle, nullptr, &size) == 0 && size == 9, "game icon size"); + char icon[16] = {}; + size = 4; + Require(sceNpTrophyGetGameIcon(context, handle, icon, &size) == InsufficientBuffer, "game icon into a short buffer"); + size = sizeof(icon); + Require(sceNpTrophyGetGameIcon(context, handle, icon, &size) == 0 && size == 9 && std::memcmp(icon, "game-icon", 9) == 0, "game icon bytes"); + + size = sizeof(icon); + Require(sceNpTrophyGetTrophyIcon(context, handle, 1, icon, &size) == TrophyNotUnlocked, "icon of a locked trophy"); + Require(sceNpTrophyUnlockTrophy(context, handle, 2, &platinum) == InvalidTrophyId, "unlock a trophy past the count"); + Require(sceNpTrophyUnlockTrophy(context, handle, 1, &platinum) == 0 && platinum == -1, "unlock trophy 1"); + Require(sceNpTrophyUnlockTrophy(context, handle, 1, &platinum) == AlreadyUnlocked, "unlock trophy 1 twice"); + Require(sceNpTrophyGetTrophyUnlockState(context, handle, flags, &count) == 0 && flags[0] == 2u, "trophy 1 unlocked"); + size = sizeof(icon); + Require(sceNpTrophyGetTrophyIcon(context, handle, 1, icon, &size) == 0 && size == 6 && std::memcmp(icon, "second", 6) == 0, "icon of the unlocked trophy"); + int saved = -1; + { + std::ifstream state(root / "_trophy/trophy00.1"); + state >> saved; + } + Require(saved == 1, "unlock state saved for the user"); + + Require(sceNpTrophyCreateContext(&missing, 1, 1, 0) == 0, "context for a label without a package"); + Require(sceNpTrophyRegisterContext(missing, handle, 0) == TitleConfNotInstalled, "register without a trophy package"); + Require(sceNpTrophyDestroyContext(missing) == 0, "destroy the second context"); + Require(sceNpTrophyDestroyHandle(handle) == 0 && sceNpTrophyDestroyContext(context) == 0, "destroy the handle and context"); + Require(sceNpTrophyDestroyContext(context) == InvalidContext, "destroy the context twice"); + + std::filesystem::current_path(root.parent_path()); + std::error_code error; + std::filesystem::remove_all(root, error); + return 0; +} diff --git a/core/libs/tests/GuestNpWebApi.cpp b/core/libs/tests/GuestNpWebApi.cpp new file mode 100644 index 0000000000..9281e58632 --- /dev/null +++ b/core/libs/tests/GuestNpWebApi.cpp @@ -0,0 +1,67 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include + +extern "C" { +int APS5_VABI sceNpWebApiInitialize(int libHttpCtxId, std::size_t poolSize); +int APS5_VABI sceNpWebApiTerminate(int libCtxId); +int APS5_VABI sceNpWebApiCreateHandle(int libCtxId); +int APS5_VABI sceNpWebApiDeleteHandle(int libCtxId, int handleId); +int APS5_VABI sceNpWebApiCreatePushEventFilter(int libCtxId, const void* filterParam, std::size_t filterParamNum); +int APS5_VABI sceNpWebApiDeletePushEventFilter(int libCtxId, int filterId); +int APS5_VABI sceNpWebApiCreateServicePushEventFilter(int libCtxId, int handleId, const char* npServiceName, unsigned npServiceLabel, const void* filterParam, std::size_t filterParamNum); +int APS5_VABI sceNpWebApiDeleteServicePushEventFilter(int libCtxId, int filterId); +} + +namespace { + +constexpr int InvalidArgument = static_cast(0x80552902u); +constexpr int LibContextNotFound = static_cast(0x80552904u); +constexpr int PushEventFilterNotFound = static_cast(0x8055290Bu); +constexpr int HandleNotFound = static_cast(0x8055290Du); +constexpr int ServicePushEventFilterNotFound = static_cast(0x8055290Eu); +constexpr int SignedInUserNotFound = static_cast(0x80552910u); + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "NpWebApi: %s\n", message); + std::abort(); + } +} + +} + +int main() { + const int context = sceNpWebApiInitialize(1, 0x10000); + Require(context > 0 && context <= 0x7fff, "library context id out of range"); + const int other = sceNpWebApiInitialize(1, 0x10000); + Require(other > 0 && other != context, "a second library context must get its own id"); + Require(sceNpWebApiCreateHandle(0x7fff + 1) == LibContextNotFound, "handle of an unknown context"); + + const int handle = sceNpWebApiCreateHandle(context); + Require(handle > 0 && static_cast(handle) < 0xf0000000u, "handle id out of range"); + const char filterParam[16] = {}; + Require(sceNpWebApiCreatePushEventFilter(context, nullptr, 1) == InvalidArgument, "push event filter without its parameter"); + Require(sceNpWebApiCreatePushEventFilter(context, filterParam, 0) == InvalidArgument, "push event filter with no parameters"); + const int filter = sceNpWebApiCreatePushEventFilter(context, filterParam, 1); + Require(filter > 0 && filter != handle, "push event filter id"); + + Require(sceNpWebApiCreateServicePushEventFilter(context, handle, nullptr, 0, filterParam, 1) == InvalidArgument, "service filter without a service name"); + Require(sceNpWebApiCreateServicePushEventFilter(0x7fff + 1, handle, "service", 0, filterParam, 1) == LibContextNotFound, "service filter of an unknown context"); + Require(sceNpWebApiCreateServicePushEventFilter(other, handle, "service", 0, filterParam, 1) == HandleNotFound, "service filter with another context's handle"); + Require(sceNpWebApiCreateServicePushEventFilter(context, handle, "service", 0, filterParam, 1) == SignedInUserNotFound, "service filter without a signed-in user"); + Require(sceNpWebApiDeleteServicePushEventFilter(context, 1) == ServicePushEventFilterNotFound, "no service filter exists"); + + Require(sceNpWebApiDeletePushEventFilter(other, filter) == PushEventFilterNotFound, "filter deleted through another context"); + Require(sceNpWebApiDeletePushEventFilter(context, filter) == 0, "delete the push event filter"); + Require(sceNpWebApiDeletePushEventFilter(context, filter) == PushEventFilterNotFound, "push event filter deleted twice"); + Require(sceNpWebApiDeleteHandle(context, handle) == 0, "delete the handle"); + Require(sceNpWebApiDeleteHandle(context, handle) == HandleNotFound, "handle deleted twice"); + + Require(sceNpWebApiTerminate(context) == 0, "terminate the library context"); + Require(sceNpWebApiTerminate(context) == LibContextNotFound, "library context terminated twice"); + Require(sceNpWebApiCreateHandle(context) == LibContextNotFound, "handle of a terminated context"); + Require(sceNpWebApiTerminate(other) == 0, "terminate the second library context"); + return 0; +} diff --git a/core/libs/tests/GuestSaveDataPs4.cpp b/core/libs/tests/GuestSaveDataPs4.cpp index 39e7b14d1e..6ee6868bd4 100644 --- a/core/libs/tests/GuestSaveDataPs4.cpp +++ b/core/libs/tests/GuestSaveDataPs4.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -10,10 +11,16 @@ extern "C" int APS5_VABI sceSaveDataTransferringMountPs4(const SaveDataTransferringMount*, SaveDataMountResult*); extern "C" int APS5_VABI sceSaveDataDirNameSearchPs4(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); extern "C" int APS5_VABI sceSaveDataDirNameSearch(const SaveDataDirNameSearchCond*, SaveDataDirNameSearchResult*); +extern "C" int APS5_VABI sceSaveDataInitialize(const void*); +extern "C" int APS5_VABI sceSaveDataTerminate(); +extern "C" int APS5_VABI sceSaveDataSetupSaveDataMemory(std::int32_t, std::size_t, const SaveDataParam*); +extern "C" int APS5_VABI sceSaveDataSetSaveDataMemory(std::int32_t, void*, std::size_t, std::int64_t); +extern "C" int APS5_VABI sceSaveDataGetSaveDataMemory(std::int32_t, void*, std::size_t, std::int64_t); namespace { constexpr int SaveDataErrorNotFound = -2137063416; +constexpr int SaveDataErrorNotInitialized = -2137063423; int failures = 0; @@ -73,6 +80,22 @@ void TestDirNameSearchPs4() { Check(ThrowsRuntimeError([&] { sceSaveDataDirNameSearchPs4(&cond, nullptr); }), "null result throws"); } +void TestSaveDataMemoryV1() { + Check(sceSaveDataSetupSaveDataMemory(7, 0x1000, nullptr) == SaveDataErrorNotInitialized, "v1 SetupSaveDataMemory before v1 Initialize"); + Check(sceSaveDataInitialize(nullptr) == 0, "v1 Initialize"); + Check(sceSaveDataSetupSaveDataMemory(7, 0x1000, nullptr) == 0, "v1 SetupSaveDataMemory"); + char written[16] = "settings-v1"; + Check(sceSaveDataSetSaveDataMemory(7, written, sizeof(written), 0x100) == 0, "v1 SetSaveDataMemory at an offset"); + char read[16] = {}; + Check(sceSaveDataGetSaveDataMemory(7, read, sizeof(read), 0x100) == 0, "v1 GetSaveDataMemory"); + Check(std::memcmp(read, written, sizeof(read)) == 0, "v1 GetSaveDataMemory returns the bytes v1 set"); + std::ifstream slot("_sd_mem/u7/slot0.bin", std::ios::binary); + slot.seekg(0x100); + char stored[16] = {}; + Check(static_cast(slot.read(stored, sizeof(stored))) && std::memcmp(stored, written, sizeof(stored)) == 0, "v1 SetSaveDataMemory stores into slot 0 of the user"); + Check(sceSaveDataTerminate() == 0, "Terminate after v1 Initialize"); +} + } int main() { @@ -83,6 +106,7 @@ int main() { TestTransferringMountPs4(); TestDirNameSearchPs4(); + TestSaveDataMemoryV1(); std::filesystem::current_path(previous); std::error_code error; diff --git a/docs/dev/debt/libSceNpTrophy.md b/docs/dev/debt/libSceNpTrophy.md new file mode 100644 index 0000000000..c445f4e8d8 --- /dev/null +++ b/docs/dev/debt/libSceNpTrophy.md @@ -0,0 +1,5 @@ +# libSceNpTrophy technical debt + +### Unknown function info + +- [sceNpTrophyRegisterContext, sceNpTrophyGetTrophyUnlockState, sceNpTrophyUnlockTrophy](../../../core/libs/prx/libSceNpTrophy/Export.cpp) (libSceNpTrophy, PS4) - the trophy definitions in `TROPCONF.ESFM` are encrypted, so the trophy count is the number of consecutive `TROPnnn.PNG` icons in the title's `/app0/sce_sys/trophy/trophyNN.trp`, and the calls that return names and details (`sceNpTrophyGetGameInfo`, `sceNpTrophyGetGroupInfo`, `sceNpTrophyGetTrophyInfo`) throw. Unlocks are kept per user and service label in `_trophy/trophyNN.` under the working directory, no platinum trophy is reported (`platinumId` is -1), and a locked trophy's icon fails with `TROPHY_NOT_UNLOCKED`. None of it is checked on a console. diff --git a/docs/dev/debt/libSceNpWebApi.md b/docs/dev/debt/libSceNpWebApi.md new file mode 100644 index 0000000000..95fb314c74 --- /dev/null +++ b/docs/dev/debt/libSceNpWebApi.md @@ -0,0 +1,6 @@ +# libSceNpWebApi technical debt + +### Unknown function info + +- [sceNpWebApiInitialize, sceNpWebApiTerminate, sceNpWebApiCreateHandle, sceNpWebApiDeleteHandle, sceNpWebApiCreatePushEventFilter, sceNpWebApiDeletePushEventFilter](../../../core/libs/prx/libSceNpWebApi/Export.cpp) (libSceNpWebApi, PS4) - signatures, id ranges (library contexts 1 to 0x7fff, handles and filters below 0xf0000000) and errors (`LIB_CONTEXT_NOT_FOUND`, `HANDLE_NOT_FOUND`, `PUSH_EVENT_FILTER_NOT_FOUND`, `INVALID_ARGUMENT` for a missing filter parameter) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/np/np_web_api/np_web_api_internal.cpp); they are not checked on a console. The libHttp context and pool size are kept but not used, since no request is ever sent. Limbo (CUSA01664) creates a context, a handle and filters when its game starts. +- [sceNpWebApiCreateServicePushEventFilter](../../../core/libs/prx/libSceNpWebApi/Export.cpp) (libSceNpWebApi, PS4) - a filter for a PSN service needs the signed-in user, and no user is signed in to PSN, so a valid call returns `SIGNED_IN_USER_NOT_FOUND` after the context and handle checks, as shadPS4 does with its network disabled. shadPS4 also rejects an invalid service label for titles built with SDK 2.00 or later; AnyPS5 has no compiled-SDK version for libraries yet, so such a call returns `SIGNED_IN_USER_NOT_FOUND` instead of `INVALID_ARGUMENT`. `sceNpWebApiDeleteServicePushEventFilter` therefore never finds a filter.