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
This commit is contained in:
georgemoralis 2026-06-25 17:35:07 +03:00 committed by GitHub
parent 221efa40c7
commit 583e6851fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 719 additions and 161 deletions

View File

@ -628,20 +628,10 @@ 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_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

View File

@ -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"

View File

@ -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<s32>(Error::NOT_INITIALIZED);
}
if (g_dialog_status != Status::FINISHED) {
return static_cast<s32>(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<s32>(Error::ARG_NULL);
}
if (g_dialog_status != Status::FINISHED) {
return static_cast<s32>(Error::NOT_FINISHED);
}
*result = static_cast<s32>(g_dialog_result);
return ORBIS_OK;
}
s8 PS4_SYSV_ABI sceNpCommerceDialogGetStatus() {
LOG_DEBUG(Lib_NpCommerce, "called, status = {}", static_cast<u32>(g_dialog_status));
return static_cast<s8>(g_dialog_status);
}
s32 PS4_SYSV_ABI sceNpCommerceDialogInitialize() {
LOG_INFO(Lib_NpCommerce, "called");
if (g_dialog_status != Status::NONE) {
return static_cast<s32>(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<s32>(Error::NOT_INITIALIZED);
}
if (g_dialog_status == Status::RUNNING) {
return static_cast<s32>(Error::NOT_FINISHED);
}
g_dialog_status = Status::NONE;
return ORBIS_OK;
}
s32 PS4_SYSV_ABI sceNpCommerceDialogUpdateStatus() {
LOG_DEBUG(Lib_NpCommerce, "called, status = {}", static_cast<u32>(g_dialog_status));
return static_cast<s32>(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

View File

@ -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

View File

@ -0,0 +1,192 @@
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <cstddef>
#include <cstring>
#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<s32>(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<s32>(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<s32>(Error::NOT_INITIALIZED);
case Status::INITIALIZED:
return static_cast<s32>(Error::NOT_RUNNING);
case Status::FINISHED:
return static_cast<s32>(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<u32>(g_dialog_status));
return static_cast<s8>(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<u32>(g_dialog_status));
return static_cast<s32>(g_dialog_status);
}
s32 PS4_SYSV_ABI sceNpCommerceDialogGetResult(OrbisNpCommerceDialogResult* result) {
if (result == nullptr) {
return static_cast<s32>(Error::ARG_NULL);
}
if (g_dialog_status != Status::FINISHED) {
return static_cast<s32>(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<const void*>(param));
if (g_dialog_status != Status::INITIALIZED && g_dialog_status != Status::FINISHED) {
LOG_WARNING(Lib_NpCommerce, "Open in bad state {}", static_cast<u32>(g_dialog_status));
return static_cast<s32>(Error::NOT_INITIALIZED);
}
if (param == nullptr) {
return static_cast<s32>(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<s32>(Error::PARAM_INVALID);
g_dialog_status = Status::FINISHED;
return static_cast<s32>(Error::PARAM_INVALID);
}
g_state = CommerceDialogState{};
g_state.mode = static_cast<CommerceMode>(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<PsStoreIconLayout>(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<PsStoreIconPos>(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

View File

@ -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

View File

@ -0,0 +1,358 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
#include <imgui.h>
#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<s32>(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<s32>(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();
}

View File

@ -0,0 +1,90 @@
// SPDX-FileCopyrightText: Copyright 2026 shadPS4 Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <atomic>
#include <string>
#include <vector>
#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<std::string> 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<bool> shown{false};
std::atomic<PsStoreIconPos> pos{PsStoreIconPos::CENTER};
std::atomic<PsStoreIconLayout> layout{PsStoreIconLayout::DEFAULT};
public:
static PsStoreIconLayer& Instance();
void Show(PsStoreIconPos p);
void Hide();
void SetLayout(PsStoreIconLayout l);
void Draw() override;
};
} // namespace Libraries::Np::NpCommerce