From 583e6851fdbb2fdd97f6d783448081869589ff39 Mon Sep 17 00:00:00 2001 From: georgemoralis Date: Thu, 25 Jun 2026 17:35:07 +0300 Subject: [PATCH] Faking Np commerce (#4618) * moved np_commerce to dedicated folder * Implemented fake np commerce * we have a store now * shadnet icon * stephen reviews * more stephen reviews --- CMakeLists.txt | 32 +- src/core/libraries/libs.cpp | 2 +- src/core/libraries/np/np_commerce.cpp | 129 ------- src/core/libraries/np/np_commerce.h | 16 - .../libraries/np/np_commerce/np_commerce.cpp | 192 ++++++++++ .../libraries/np/np_commerce/np_commerce.h | 61 +++ .../np/np_commerce/np_commerce_ui.cpp | 358 ++++++++++++++++++ .../libraries/np/np_commerce/np_commerce_ui.h | 90 +++++ 8 files changed, 719 insertions(+), 161 deletions(-) delete mode 100644 src/core/libraries/np/np_commerce.cpp delete mode 100644 src/core/libraries/np/np_commerce.h create mode 100644 src/core/libraries/np/np_commerce/np_commerce.cpp create mode 100644 src/core/libraries/np/np_commerce/np_commerce.h create mode 100644 src/core/libraries/np/np_commerce/np_commerce_ui.cpp create mode 100644 src/core/libraries/np/np_commerce/np_commerce_ui.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c1890de0..b97627bdc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -627,21 +627,11 @@ set(VDEC_LIB src/core/libraries/videodec/videodec2_impl.cpp set(NP_LIBS src/core/libraries/np/np_error.h src/core/libraries/np/np_common.cpp - src/core/libraries/np/np_common.h - src/core/libraries/np/np_commerce.cpp - src/core/libraries/np/np_commerce.h + src/core/libraries/np/np_common.h src/core/libraries/np/np_manager.cpp src/core/libraries/np/np_manager.h src/core/libraries/np/np_matching2.cpp src/core/libraries/np/np_matching2.h - src/core/libraries/np/np_signaling/np_signaling.cpp - src/core/libraries/np/np_signaling/np_signaling.h - src/core/libraries/np/np_signaling/np_signaling_state.cpp - src/core/libraries/np/np_signaling/np_signaling_state.h - src/core/libraries/np/np_signaling/np_signaling_helpers.cpp - src/core/libraries/np/np_signaling/np_signaling_helpers.h - src/core/libraries/np/np_signaling/np_signaling_stubs.cpp - src/core/libraries/np/np_signaling/np_signaling_stubs.h src/core/libraries/np/np_trophy.cpp src/core/libraries/np/np_trophy.h src/core/libraries/np/np_tus.cpp @@ -654,10 +644,6 @@ set(NP_LIBS src/core/libraries/np/np_error.h src/core/libraries/np/np_party.h src/core/libraries/np/np_auth.cpp src/core/libraries/np/np_auth.h - src/core/libraries/np/np_profile_dialog/np_profile_dialog.cpp - src/core/libraries/np/np_profile_dialog/np_profile_dialog.h - src/core/libraries/np/np_profile_dialog/np_profile_dialog_ui.cpp - src/core/libraries/np/np_profile_dialog/np_profile_dialog_ui.h src/core/libraries/np/np_sns_facebook_dialog.cpp src/core/libraries/np/np_sns_facebook_dialog.h src/core/libraries/np/np_partner.cpp @@ -665,9 +651,25 @@ set(NP_LIBS src/core/libraries/np/np_error.h src/core/libraries/np/object_manager.h src/core/libraries/np/np_handler.cpp src/core/libraries/np/np_handler.h + src/core/libraries/np/np_commerce/np_commerce.cpp + src/core/libraries/np/np_commerce/np_commerce.h + src/core/libraries/np/np_commerce/np_commerce_ui.cpp + src/core/libraries/np/np_commerce/np_commerce_ui.h + src/core/libraries/np/np_profile_dialog/np_profile_dialog.cpp + src/core/libraries/np/np_profile_dialog/np_profile_dialog.h + src/core/libraries/np/np_profile_dialog/np_profile_dialog_ui.cpp + src/core/libraries/np/np_profile_dialog/np_profile_dialog_ui.h src/core/libraries/np/np_score/np_score.cpp src/core/libraries/np/np_score/np_score.h src/core/libraries/np/np_score/np_score_ctx.h + src/core/libraries/np/np_signaling/np_signaling.cpp + src/core/libraries/np/np_signaling/np_signaling.h + src/core/libraries/np/np_signaling/np_signaling_state.cpp + src/core/libraries/np/np_signaling/np_signaling_state.h + src/core/libraries/np/np_signaling/np_signaling_helpers.cpp + src/core/libraries/np/np_signaling/np_signaling_helpers.h + src/core/libraries/np/np_signaling/np_signaling_stubs.cpp + src/core/libraries/np/np_signaling/np_signaling_stubs.h src/core/libraries/np/np_web_api/np_web_api.cpp src/core/libraries/np/np_web_api/np_web_api.h src/core/libraries/np/np_web_api/np_web_api_internal.cpp diff --git a/src/core/libraries/libs.cpp b/src/core/libraries/libs.cpp index 590442d7c..b2719b554 100644 --- a/src/core/libraries/libs.cpp +++ b/src/core/libraries/libs.cpp @@ -35,7 +35,7 @@ #include "core/libraries/network/ssl.h" #include "core/libraries/network/ssl2.h" #include "core/libraries/np/np_auth.h" -#include "core/libraries/np/np_commerce.h" +#include "core/libraries/np/np_commerce/np_commerce.h" #include "core/libraries/np/np_common.h" #include "core/libraries/np/np_manager.h" #include "core/libraries/np/np_matching2.h" diff --git a/src/core/libraries/np/np_commerce.cpp b/src/core/libraries/np/np_commerce.cpp deleted file mode 100644 index 99b03384a..000000000 --- a/src/core/libraries/np/np_commerce.cpp +++ /dev/null @@ -1,129 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#include "common/logging/log.h" -#include "core/libraries/error_codes.h" -#include "core/libraries/libs.h" -#include "core/libraries/system/commondialog.h" - -namespace Libraries::Np::NpCommerce { - -using CommonDialog::Error; -using CommonDialog::Result; -using CommonDialog::Status; - -static Status g_dialog_status = Status::NONE; -static Result g_dialog_result = Result::OK; - -s32 PS4_SYSV_ABI sceNpCommerceDialogClose() { - LOG_INFO(Lib_NpCommerce, "called"); - if (g_dialog_status == Status::NONE) { - return static_cast(Error::NOT_INITIALIZED); - } - if (g_dialog_status != Status::FINISHED) { - return static_cast(Error::NOT_FINISHED); - } - g_dialog_status = Status::INITIALIZED; - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpCommerceDialogGetResult(s32* result) { - LOG_INFO(Lib_NpCommerce, "called"); - if (result == nullptr) { - return static_cast(Error::ARG_NULL); - } - if (g_dialog_status != Status::FINISHED) { - return static_cast(Error::NOT_FINISHED); - } - *result = static_cast(g_dialog_result); - return ORBIS_OK; -} - -s8 PS4_SYSV_ABI sceNpCommerceDialogGetStatus() { - LOG_DEBUG(Lib_NpCommerce, "called, status = {}", static_cast(g_dialog_status)); - return static_cast(g_dialog_status); -} - -s32 PS4_SYSV_ABI sceNpCommerceDialogInitialize() { - LOG_INFO(Lib_NpCommerce, "called"); - if (g_dialog_status != Status::NONE) { - return static_cast(Error::ALREADY_INITIALIZED); - } - g_dialog_status = Status::INITIALIZED; - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpCommerceDialogInitializeInternal() { - LOG_INFO(Lib_NpCommerce, "called"); - return sceNpCommerceDialogInitialize(); -} - -s16 PS4_SYSV_ABI sceNpCommerceDialogOpen(s64 check) { - LOG_INFO(Lib_NpCommerce, "called, check = {}", check); - if (g_dialog_status != Status::INITIALIZED) { - LOG_WARNING(Lib_NpCommerce, "Dialog not initialized"); - return ORBIS_OK; - } - - g_dialog_status = Status::FINISHED; - g_dialog_result = Result::USER_CANCELED; - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpCommerceDialogTerminate() { - LOG_INFO(Lib_NpCommerce, "called"); - if (g_dialog_status == Status::NONE) { - return static_cast(Error::NOT_INITIALIZED); - } - if (g_dialog_status == Status::RUNNING) { - return static_cast(Error::NOT_FINISHED); - } - g_dialog_status = Status::NONE; - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpCommerceDialogUpdateStatus() { - LOG_DEBUG(Lib_NpCommerce, "called, status = {}", static_cast(g_dialog_status)); - return static_cast(g_dialog_status); -} - -s32 PS4_SYSV_ABI sceNpCommerceHidePsStoreIcon() { - LOG_ERROR(Lib_NpCommerce, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpCommerceSetPsStoreIconLayout(s32 layout) { - LOG_ERROR(Lib_NpCommerce, "(STUBBED) called"); - return ORBIS_OK; -} - -s32 PS4_SYSV_ABI sceNpCommerceShowPsStoreIcon(s16 icon) { - LOG_ERROR(Lib_NpCommerce, "(STUBBED) called"); - return ORBIS_OK; -} - -void RegisterLib(Core::Loader::SymbolsResolver* sym) { - LIB_FUNCTION("NU3ckGHMFXo", "libSceNpCommerce", 1, "libSceNpCommerce", - sceNpCommerceDialogClose); - LIB_FUNCTION("r42bWcQbtZY", "libSceNpCommerce", 1, "libSceNpCommerce", - sceNpCommerceDialogGetResult); - LIB_FUNCTION("CCbC+lqqvF0", "libSceNpCommerce", 1, "libSceNpCommerce", - sceNpCommerceDialogGetStatus); - LIB_FUNCTION("0aR2aWmQal4", "libSceNpCommerce", 1, "libSceNpCommerce", - sceNpCommerceDialogInitialize); - LIB_FUNCTION("9ZiLXAGG5rg", "libSceNpCommerce", 1, "libSceNpCommerce", - sceNpCommerceDialogInitializeInternal); - LIB_FUNCTION("DfSCDRA3EjY", "libSceNpCommerce", 1, "libSceNpCommerce", sceNpCommerceDialogOpen); - LIB_FUNCTION("m-I92Ab50W8", "libSceNpCommerce", 1, "libSceNpCommerce", - sceNpCommerceDialogTerminate); - LIB_FUNCTION("LR5cwFMMCVE", "libSceNpCommerce", 1, "libSceNpCommerce", - sceNpCommerceDialogUpdateStatus); - LIB_FUNCTION("dsqCVsNM0Zg", "libSceNpCommerce", 1, "libSceNpCommerce", - sceNpCommerceHidePsStoreIcon); - LIB_FUNCTION("uKTDW8hk-ts", "libSceNpCommerce", 1, "libSceNpCommerce", - sceNpCommerceSetPsStoreIconLayout); - LIB_FUNCTION("DHmwsa6S8Tc", "libSceNpCommerce", 1, "libSceNpCommerce", - sceNpCommerceShowPsStoreIcon); -}; - -} // namespace Libraries::Np::NpCommerce diff --git a/src/core/libraries/np/np_commerce.h b/src/core/libraries/np/np_commerce.h deleted file mode 100644 index 003e85a58..000000000 --- a/src/core/libraries/np/np_commerce.h +++ /dev/null @@ -1,16 +0,0 @@ -// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project -// SPDX-License-Identifier: GPL-2.0-or-later - -#pragma once - -#include "common/types.h" - -namespace Core::Loader { -class SymbolsResolver; -} - -namespace Libraries::Np::NpCommerce { - -void RegisterLib(Core::Loader::SymbolsResolver* sym); - -} // namespace Libraries::Np::NpCommerce \ No newline at end of file diff --git a/src/core/libraries/np/np_commerce/np_commerce.cpp b/src/core/libraries/np/np_commerce/np_commerce.cpp new file mode 100644 index 000000000..095cb2b69 --- /dev/null +++ b/src/core/libraries/np/np_commerce/np_commerce.cpp @@ -0,0 +1,192 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include "common/logging/log.h" +#include "core/libraries/error_codes.h" +#include "core/libraries/libs.h" +#include "core/libraries/np/np_commerce/np_commerce_ui.h" +#include "core/libraries/system/commondialog.h" +#include "core/libraries/system/userservice.h" +#include "np_commerce.h" + +namespace Libraries::Np::NpCommerce { + +static Status g_dialog_status = Status::NONE; +static s32 g_result_code = ORBIS_COMMON_DIALOG_RESULT_USER_CANCELED; +static CommerceDialogState g_state; +static CommerceDialogUi g_dialog; + +s32 PS4_SYSV_ABI sceNpCommerceDialogInitialize() { + LOG_INFO(Lib_NpCommerce, "called"); + if (g_dialog_status != Status::NONE) { + return static_cast(Error::ALREADY_INITIALIZED); + } + g_dialog_status = Status::INITIALIZED; + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpCommerceDialogTerminate() { + LOG_INFO(Lib_NpCommerce, "called"); + if (g_dialog_status == Status::NONE) { + return static_cast(Error::NOT_INITIALIZED); + } + g_dialog.Finish(CommerceResult::OK); + g_dialog_status = Status::NONE; + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpCommerceDialogClose() { + LOG_INFO(Lib_NpCommerce, "called"); + switch (g_dialog_status) { + case Status::NONE: + return static_cast(Error::NOT_INITIALIZED); + case Status::INITIALIZED: + return static_cast(Error::NOT_RUNNING); + case Status::FINISHED: + return static_cast(Error::ALREADY_CLOSE); + default: + break; + } + g_dialog.Finish(CommerceResult::OK); + return ORBIS_OK; +} + +s8 PS4_SYSV_ABI sceNpCommerceDialogGetStatus() { + LOG_DEBUG(Lib_NpCommerce, "called, status = {}", static_cast(g_dialog_status)); + return static_cast(g_dialog_status); +} + +s32 PS4_SYSV_ABI sceNpCommerceDialogInitializeInternal() { + LOG_INFO(Lib_NpCommerce, "called"); + return sceNpCommerceDialogInitialize(); +} + +s32 PS4_SYSV_ABI sceNpCommerceDialogUpdateStatus() { + LOG_DEBUG(Lib_NpCommerce, "called, status = {}", static_cast(g_dialog_status)); + return static_cast(g_dialog_status); +} + +s32 PS4_SYSV_ABI sceNpCommerceDialogGetResult(OrbisNpCommerceDialogResult* result) { + if (result == nullptr) { + return static_cast(Error::ARG_NULL); + } + if (g_dialog_status != Status::FINISHED) { + return static_cast(Error::NOT_FINISHED); + } + result->result = g_result_code; + // authorized is only meaningful in PLUS mode + result->authorized = g_state.mode == CommerceMode::PLUS && + g_result_code == ORBIS_NP_COMMERCE_DIALOG_RESULT_PURCHASED; + result->userData = g_state.user_data; + LOG_INFO(Lib_NpCommerce, "result={} authorized={}", result->result, result->authorized); + return g_result_code; +} + +s32 PS4_SYSV_ABI sceNpCommerceDialogOpen(const OrbisNpCommerceDialogParam* param) { + LOG_INFO(Lib_NpCommerce, "called, param = {}", static_cast(param)); + if (g_dialog_status != Status::INITIALIZED && g_dialog_status != Status::FINISHED) { + LOG_WARNING(Lib_NpCommerce, "Open in bad state {}", static_cast(g_dialog_status)); + return static_cast(Error::NOT_INITIALIZED); + } + if (param == nullptr) { + return static_cast(Error::ARG_NULL); + } + if (param->mode < ORBIS_NP_COMMERCE_DIALOG_MODE_CATEGORY || + param->mode > ORBIS_NP_COMMERCE_DIALOG_MODE_PLUS) { + LOG_ERROR(Lib_NpCommerce, "SceNpCommerceDialog Parameter Error (mode={})", param->mode); + g_result_code = static_cast(Error::PARAM_INVALID); + g_dialog_status = Status::FINISHED; + return static_cast(Error::PARAM_INVALID); + } + + g_state = CommerceDialogState{}; + g_state.mode = static_cast(param->mode); + g_state.user_id = param->userId; + g_state.service_label = param->serviceLabel; + g_state.user_data = param->userData; + // features is only meaningful in PLUS mode + g_state.features = g_state.mode == CommerceMode::PLUS ? param->features : 0; + + // Resolve the calling user's name for display + char name_buf[Libraries::UserService::ORBIS_USER_SERVICE_MAX_USER_NAME_LENGTH + 1] = {}; + if (Libraries::UserService::sceUserServiceGetUserName(param->userId, name_buf, + sizeof(name_buf)) >= 0) { + g_state.username = name_buf; + } + + if (param->numTargets > ORBIS_NP_COMMERCE_DIALOG_NUM_TARGETS_MAX) { + LOG_WARNING(Lib_NpCommerce, "numTargets {} exceeds max {}, clamping", param->numTargets, + ORBIS_NP_COMMERCE_DIALOG_NUM_TARGETS_MAX); + } + const u32 count = std::min(param->numTargets, ORBIS_NP_COMMERCE_DIALOG_NUM_TARGETS_MAX); + if (count > 0 && param->targets != nullptr) { + g_state.targets.reserve(count); + for (u32 i = 0; i < count; ++i) { + const char* label = param->targets[i]; + if (label != nullptr) { + g_state.targets.emplace_back(label, strnlen(label, MAX_LABEL_LEN)); + } + } + } + LOG_INFO(Lib_NpCommerce, + "open: mode={} user_id={} serviceLabel={:#x} numTargets={} features={:#x} first='{}'", + param->mode, g_state.user_id, g_state.service_label, param->numTargets, + g_state.features, g_state.targets.empty() ? "" : g_state.targets.front()); + + g_dialog_status = Status::RUNNING; + g_result_code = ORBIS_COMMON_DIALOG_RESULT_USER_CANCELED; + g_dialog = CommerceDialogUi(&g_state, &g_dialog_status, &g_result_code); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpCommerceHidePsStoreIcon() { + LOG_INFO(Lib_NpCommerce, "called"); + PsStoreIconLayer::Instance().Hide(); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpCommerceSetPsStoreIconLayout(s32 layout) { + LOG_INFO(Lib_NpCommerce, "called, layout = {}", layout); + if (layout < 0 || layout > 2) { + LOG_WARNING(Lib_NpCommerce, "invalid layout {}, ignoring", layout); + return ORBIS_OK; + } + PsStoreIconLayer::Instance().SetLayout(static_cast(layout)); + return ORBIS_OK; +} + +s32 PS4_SYSV_ABI sceNpCommerceShowPsStoreIcon(s32 pos) { + LOG_INFO(Lib_NpCommerce, "called, pos = {}", pos); + const auto p = + (pos >= 0 && pos <= 2) ? static_cast(pos) : PsStoreIconPos::CENTER; + PsStoreIconLayer::Instance().Show(p); + return ORBIS_OK; +} + +void RegisterLib(Core::Loader::SymbolsResolver* sym) { + LIB_FUNCTION("NU3ckGHMFXo", "libSceNpCommerce", 1, "libSceNpCommerce", + sceNpCommerceDialogClose); + LIB_FUNCTION("r42bWcQbtZY", "libSceNpCommerce", 1, "libSceNpCommerce", + sceNpCommerceDialogGetResult); + LIB_FUNCTION("CCbC+lqqvF0", "libSceNpCommerce", 1, "libSceNpCommerce", + sceNpCommerceDialogGetStatus); + LIB_FUNCTION("0aR2aWmQal4", "libSceNpCommerce", 1, "libSceNpCommerce", + sceNpCommerceDialogInitialize); + LIB_FUNCTION("9ZiLXAGG5rg", "libSceNpCommerce", 1, "libSceNpCommerce", + sceNpCommerceDialogInitializeInternal); + LIB_FUNCTION("DfSCDRA3EjY", "libSceNpCommerce", 1, "libSceNpCommerce", sceNpCommerceDialogOpen); + LIB_FUNCTION("m-I92Ab50W8", "libSceNpCommerce", 1, "libSceNpCommerce", + sceNpCommerceDialogTerminate); + LIB_FUNCTION("LR5cwFMMCVE", "libSceNpCommerce", 1, "libSceNpCommerce", + sceNpCommerceDialogUpdateStatus); + LIB_FUNCTION("dsqCVsNM0Zg", "libSceNpCommerce", 1, "libSceNpCommerce", + sceNpCommerceHidePsStoreIcon); + LIB_FUNCTION("uKTDW8hk-ts", "libSceNpCommerce", 1, "libSceNpCommerce", + sceNpCommerceSetPsStoreIconLayout); + LIB_FUNCTION("DHmwsa6S8Tc", "libSceNpCommerce", 1, "libSceNpCommerce", + sceNpCommerceShowPsStoreIcon); +}; + +} // namespace Libraries::Np::NpCommerce diff --git a/src/core/libraries/np/np_commerce/np_commerce.h b/src/core/libraries/np/np_commerce/np_commerce.h new file mode 100644 index 000000000..91a1521b7 --- /dev/null +++ b/src/core/libraries/np/np_commerce/np_commerce.h @@ -0,0 +1,61 @@ +// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include "common/types.h" + +namespace Core::Loader { +class SymbolsResolver; +} + +namespace Libraries::Np::NpCommerce { + +using CommonDialog::BaseParam; +using CommonDialog::Error; +using CommonDialog::Status; + +static constexpr u64 MAX_LABEL_LEN = 128; +static constexpr u32 ORBIS_NP_COMMERCE_DIALOG_NUM_TARGETS_MAX = 10; + +struct OrbisNpCommerceDialogParam { + BaseParam baseParam; + s32 size; + Libraries::UserService::OrbisUserServiceUserId userId; + s32 mode; + u32 serviceLabel; + const char* const* targets; + u32 numTargets; + s32 : 32; + u64 features; + void* userData; + u8 reserved[32]; +}; +static_assert(sizeof(OrbisNpCommerceDialogParam) == 0x80); + +struct OrbisNpCommerceDialogResult { + s32 result; + bool authorized; + char : 8; + short : 16; + void* userData; + u8 reserved[32]; +}; +static_assert(sizeof(OrbisNpCommerceDialogResult) == 0x30); + +// OrbisNpCommerceDialogResult return codes +static constexpr s32 ORBIS_COMMON_DIALOG_RESULT_OK = 0; +static constexpr s32 ORBIS_COMMON_DIALOG_RESULT_USER_CANCELED = 1; +static constexpr s32 ORBIS_NP_COMMERCE_DIALOG_RESULT_PURCHASED = 2; + +// OrbisNpCommerceDialogMode +static constexpr s32 ORBIS_NP_COMMERCE_DIALOG_MODE_CATEGORY = 0; // Browse category mode +static constexpr s32 ORBIS_NP_COMMERCE_DIALOG_MODE_PRODUCT = 1; // Browse product mode +static constexpr s32 ORBIS_NP_COMMERCE_DIALOG_MODE_PRODUCT_CODE = 2; // promotion code mode +static constexpr s32 ORBIS_NP_COMMERCE_DIALOG_MODE_CHECKOUT = 3; // Checkout mode +static constexpr s32 ORBIS_NP_COMMERCE_DIALOG_MODE_DOWNLOADLIST = 4; // Download mode +static constexpr s32 ORBIS_NP_COMMERCE_DIALOG_MODE_PLUS = 5; // PlayStation Plus mode + +void RegisterLib(Core::Loader::SymbolsResolver* sym); + +} // namespace Libraries::Np::NpCommerce \ No newline at end of file diff --git a/src/core/libraries/np/np_commerce/np_commerce_ui.cpp b/src/core/libraries/np/np_commerce/np_commerce_ui.cpp new file mode 100644 index 000000000..dd31dccec --- /dev/null +++ b/src/core/libraries/np/np_commerce/np_commerce_ui.cpp @@ -0,0 +1,358 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include +#include +#include "common/logging/log.h" +#include "core/libraries/np/np_commerce/np_commerce_ui.h" +#include "imgui/imgui_std.h" + +using namespace ImGui; +using namespace Libraries::CommonDialog; +using namespace Libraries::Np::NpCommerce; + +static constexpr ImVec2 BUTTON_SIZE{160.0f, 30.0f}; + +static const char* ModeTitle(CommerceMode m) { + switch (m) { + case CommerceMode::CATEGORY: + return "shadNet Store"; + case CommerceMode::PRODUCT: + return "Product"; + case CommerceMode::PRODUCT_CODE: + return "Redeem Code"; + case CommerceMode::CHECKOUT: + return "Purchase Confirmation"; + case CommerceMode::DOWNLOADLIST: + return "My Downloads"; + case CommerceMode::PLUS: + return "shadNet Plus"; + default: + return "shadNet Store"; + } +} + +static const char* ModeActionLabel(CommerceMode m) { + switch (m) { + case CommerceMode::CATEGORY: + case CommerceMode::PRODUCT: + case CommerceMode::CHECKOUT: + return "Simulate purchase"; + case CommerceMode::PRODUCT_CODE: + return "Redeem code"; + case CommerceMode::PLUS: + return "Join SN Plus"; + case CommerceMode::DOWNLOADLIST: + default: + return nullptr; + } +} + +static const char* ModeEmptyText(CommerceMode m) { + switch (m) { + case CommerceMode::CATEGORY: + return "Top store category."; + case CommerceMode::PRODUCT_CODE: + return "Enter a promotion code."; + case CommerceMode::DOWNLOADLIST: + return "All downloadable content."; + case CommerceMode::PLUS: + return "Join shadNet Plus."; + default: + return "No product specified."; + } +} + +static const char* ModeDescription(CommerceMode m) { + switch (m) { + case CommerceMode::CATEGORY: + return "Browsing a store category."; + case CommerceMode::PRODUCT: + return "Viewing a product."; + case CommerceMode::PRODUCT_CODE: + return "Redeeming a promotion code."; + case CommerceMode::CHECKOUT: + return "Confirming a purchase."; + case CommerceMode::DOWNLOADLIST: + return "Viewing owned downloads."; + case CommerceMode::PLUS: + return "Joining shadNet Plus."; + default: + return ""; + } +} + +static std::string DescribeFeatures(u64 features) { + if (features == 0) { + return "none"; + } + std::string out; + if (features & 0x1) { // ORBIS_NP_PLUS_FEATURE_REALTIME_MULTIPLAY + out += "Realtime Multiplay"; + } + if (out.empty()) { + return "unknown"; + } + return out; +} + +// Decomposed CHECKOUT/DOWNLOADLIST element "[serviceLabel:]product[-sku]". +struct ParsedTarget { + std::string service; + std::string product; + std::string sku; +}; + +static ParsedTarget ParseTarget(const std::string& t) { + ParsedTarget out; + std::string s = t; + if (const auto colon = s.find(':'); colon != std::string::npos) { + out.service = s.substr(0, colon); + s = s.substr(colon + 1); + } + if (const auto dash = s.find('-'); dash != std::string::npos) { + out.product = s.substr(0, dash); + out.sku = s.substr(dash + 1); + } else { + out.product = s; + } + return out; +} + +CommerceDialogUi::CommerceDialogUi(CommerceDialogState* state, Status* status, s32* result) + : state(state), status(status), result(result) { + if (status && *status == Status::RUNNING) { + first_render = true; + AddLayer(this); + } +} + +CommerceDialogUi::~CommerceDialogUi() { + Finish(CommerceResult::USER_CANCELED); +} + +CommerceDialogUi::CommerceDialogUi(CommerceDialogUi&& other) noexcept + : Layer(other), first_render(other.first_render), state(other.state), status(other.status), + result(other.result) { + other.state = nullptr; + other.status = nullptr; + other.result = nullptr; +} + +CommerceDialogUi& CommerceDialogUi::operator=(CommerceDialogUi&& other) noexcept { + if (this != &other) { + first_render = other.first_render; + state = other.state; + status = other.status; + result = other.result; + other.state = nullptr; + other.status = nullptr; + other.result = nullptr; + if (status && *status == Status::RUNNING) { + first_render = true; + AddLayer(this); + } + } + return *this; +} + +void CommerceDialogUi::Finish(CommerceResult r) { + if (result) { + *result = static_cast(r); + } + if (status) { + *status = Status::FINISHED; + } + state = nullptr; + status = nullptr; + result = nullptr; + RemoveLayer(this); +} + +void CommerceDialogUi::Draw() { + if (status == nullptr || *status != Status::RUNNING || state == nullptr) { + return; + } + const auto& io = GetIO(); + const ImVec2 window_size{std::min(io.DisplaySize.x, 520.0f), + std::min(io.DisplaySize.y, 380.0f)}; + + CentralizeNextWindow(); + SetNextWindowSize(window_size); + SetNextWindowCollapsed(false); + if (first_render || !io.NavActive) { + SetNextWindowFocus(); + } + KeepNavHighlight(); + if (Begin("CommerceDialog##NpCommerceDialog", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings)) { + std::string title = ModeTitle(state->mode); + if (state->targets.size() == 1 && + (state->mode == CommerceMode::PRODUCT || state->mode == CommerceMode::CATEGORY)) { + title += ": " + state->targets.front(); + } + { + const float tw = CalcTextSize(title.c_str()).x; + SetCursorPosX(std::max(10.0f, (GetWindowSize().x - tw) * 0.5f)); + TextUnformatted(title.c_str()); + } + Separator(); + + TextWrapped("%s", ModeDescription(state->mode)); + Spacing(); + + // Calling user. + if (!state->username.empty()) { + Text("User: %s (id %d)", state->username.c_str(), state->user_id); + } else { + Text("User id: %d", state->user_id); + } + // Service label is meaningful for category/product/checkout/downloadlist. + if (state->mode != CommerceMode::PRODUCT_CODE && state->mode != CommerceMode::PLUS) { + Text("Service label: %u", state->service_label); + } + // PS Plus feature only applies in PLUS mode. + if (state->mode == CommerceMode::PLUS) { + Text("PS Plus feature: %s", DescribeFeatures(state->features).c_str()); + } + + Spacing(); + if (state->targets.empty()) { + TextWrapped("%s", ModeEmptyText(state->mode)); + } else { + const bool checkout_form = + state->mode == CommerceMode::CHECKOUT || state->mode == CommerceMode::DOWNLOADLIST; + TextDisabled(state->targets.size() == 1 ? "Target:" : "Targets:"); + for (const auto& t : state->targets) { + if (checkout_form) { + const ParsedTarget pt = ParseTarget(t); + if (!pt.sku.empty() || !pt.service.empty()) { + BulletText("Product %s", pt.product.c_str()); + if (!pt.sku.empty()) { + Indent(); + Text("SKU: %s", pt.sku.c_str()); + Unindent(); + } + if (!pt.service.empty()) { + Indent(); + Text("Service: %s", pt.service.c_str()); + Unindent(); + } + continue; + } + } + BulletText("%s", t.c_str()); + } + } + + Spacing(); + + const char* action = ModeActionLabel(state->mode); + const auto ws = GetWindowSize(); + const float n = action ? 2.0f : 1.0f; + SetCursorPos({ws.x / 2.0f - (BUTTON_SIZE.x * n + 10.0f * (n - 1.0f)) / 2.0f, + ws.y - 10.0f - BUTTON_SIZE.y}); + BeginGroup(); + if (action) { + if (Button(action, BUTTON_SIZE)) { + LOG_INFO(Lib_NpCommerce, "CommerceDialog: action '{}' mode={} target='{}'", action, + static_cast(state->mode), + state->targets.empty() ? "" : state->targets.front()); + Finish(CommerceResult::PURCHASED); + } + SameLine(); + } + if (Button("Close", BUTTON_SIZE)) { + Finish(CommerceResult::USER_CANCELED); + } + // Default focus on Close so a stray confirm press dismisses, never buys. + if (first_render) { + SetItemCurrentNavFocus(); + } + EndGroup(); + } + End(); + + first_render = false; +} + +static float LayoutScale(PsStoreIconLayout l) { + switch (l) { + case PsStoreIconLayout::FIXED_90: + return 0.90f; + case PsStoreIconLayout::FOLLOW_SAFE_AREA: + return 0.95f; + case PsStoreIconLayout::DEFAULT: + default: + return 1.0f; + } +} + +PsStoreIconLayer& PsStoreIconLayer::Instance() { + static PsStoreIconLayer instance; + return instance; +} + +void PsStoreIconLayer::Show(PsStoreIconPos p) { + pos.store(p); + if (!shown.exchange(true)) { + AddLayer(this); + } +} + +void PsStoreIconLayer::Hide() { + if (shown.exchange(false)) { + RemoveLayer(this); + } +} + +void PsStoreIconLayer::SetLayout(PsStoreIconLayout l) { + layout.store(l); +} + +void PsStoreIconLayer::Draw() { + if (!shown.load()) { + return; + } + const auto& io = GetIO(); + const ImVec2 ds = io.DisplaySize; + const float scale = LayoutScale(layout.load()); + const float inset_x = ds.x * (1.0f - scale) * 0.5f; + const float inset_y = ds.y * (1.0f - scale) * 0.5f; + + constexpr ImVec2 icon{52.0f, 52.0f}; + constexpr float pad = 16.0f; + + float x; + switch (pos.load()) { + case PsStoreIconPos::LEFT: + x = inset_x + pad; + break; + case PsStoreIconPos::RIGHT: + x = ds.x - inset_x - icon.x - pad; + break; + case PsStoreIconPos::CENTER: + default: + x = (ds.x - icon.x) * 0.5f; + break; + } + const float y = ds.y - inset_y - icon.y - pad; + + SetNextWindowPos({x, y}); + SetNextWindowSize(icon); + if (Begin("##PsStoreIcon", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoBackground | + ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)) { + auto* dl = GetWindowDrawList(); + const ImVec2 p0 = GetWindowPos(); + const ImVec2 p1{p0.x + icon.x, p0.y + icon.y}; + dl->AddRectFilled(p0, p1, IM_COL32(0, 55, 145, 230), 10.0f); + dl->AddRect(p0, p1, IM_COL32(255, 255, 255, 210), 10.0f, 0, 2.0f); + const char* label = "SN"; + const ImVec2 ts = CalcTextSize(label); + dl->AddText({p0.x + (icon.x - ts.x) * 0.5f, p0.y + (icon.y - ts.y) * 0.5f}, + IM_COL32(255, 255, 255, 255), label); + } + End(); +} diff --git a/src/core/libraries/np/np_commerce/np_commerce_ui.h b/src/core/libraries/np/np_commerce/np_commerce_ui.h new file mode 100644 index 000000000..e95fdecc0 --- /dev/null +++ b/src/core/libraries/np/np_commerce/np_commerce_ui.h @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include "common/types.h" +#include "core/libraries/system/commondialog.h" +#include "imgui/imgui_layer.h" + +namespace Libraries::Np::NpCommerce { + +// OrbisNpCommerceDialogMode +enum class CommerceMode : s32 { + CATEGORY = 0, + PRODUCT = 1, + PRODUCT_CODE = 2, + CHECKOUT = 3, + DOWNLOADLIST = 4, + PLUS = 5, +}; + +// OrbisNpCommerceDialogResult result values +enum class CommerceResult : s32 { + OK = 0, + USER_CANCELED = 1, + PURCHASED = 2, +}; + +struct CommerceDialogState { + std::vector targets; // product / category labels + std::string username; // resolved from userId, may be empty + CommerceMode mode{CommerceMode::CATEGORY}; + s32 user_id{-1}; + u32 service_label{0}; + u64 features{0}; // ORBIS_NP_PLUS_FEATURE_* (PLUS mode only) + void* user_data{nullptr}; // echoed back into the result struct +}; + +class CommerceDialogUi final : public ImGui::Layer { + bool first_render{false}; + CommerceDialogState* state{}; + CommonDialog::Status* status{}; + s32* result{}; + +public: + explicit CommerceDialogUi(CommerceDialogState* state = nullptr, + CommonDialog::Status* status = nullptr, s32* result = nullptr); + ~CommerceDialogUi() override; + CommerceDialogUi(const CommerceDialogUi&) = delete; + CommerceDialogUi& operator=(const CommerceDialogUi&) = delete; + CommerceDialogUi(CommerceDialogUi&& other) noexcept; + CommerceDialogUi& operator=(CommerceDialogUi&& other) noexcept; + + void Finish(CommerceResult r); + void Draw() override; +}; + +// OrbisNpCommercePsStoreIconPos +enum class PsStoreIconPos : s32 { + CENTER = 0, // screen lower center + LEFT = 1, // screen lower left + RIGHT = 2, // screen lower right +}; + +// OrbisNpCommercePsStoreIconLayout +enum class PsStoreIconLayout : s32 { + DEFAULT = 0, // full screen (100%) + FOLLOW_SAFE_AREA = 1, // follow display safe area (90%-100%) + FIXED_90 = 2, // fixed 90% of screen +}; + +class PsStoreIconLayer final : public ImGui::Layer { + std::atomic shown{false}; + std::atomic pos{PsStoreIconPos::CENTER}; + std::atomic layout{PsStoreIconLayout::DEFAULT}; + +public: + static PsStoreIconLayer& Instance(); + + void Show(PsStoreIconPos p); + void Hide(); + void SetLayout(PsStoreIconLayout l); + + void Draw() override; +}; + +} // namespace Libraries::Np::NpCommerce