mirror of
https://github.com/shadps4-emu/shadPS4.git
synced 2026-06-01 12:15:43 -06:00
Trophies fixes (#4114)
* added extraction to users based on npcommid * npcommids are stored in elf_info now Made nptrophy to work correcty with npcommids * fixed loading of keymanager * make linux happy * partial trophy rewrite with lanugages support * fixed troph file naming
This commit is contained in:
parent
d06a3d4777
commit
ff96c54451
@ -6,6 +6,7 @@
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "assert.h"
|
||||
#include "bit_field.h"
|
||||
@ -73,6 +74,7 @@ class ElfInfo {
|
||||
|
||||
std::filesystem::path splash_path{};
|
||||
std::filesystem::path game_folder{};
|
||||
std::vector<std::string> npCommIds{};
|
||||
|
||||
public:
|
||||
static constexpr u32 FW_10 = 0x1000000;
|
||||
@ -139,6 +141,10 @@ public:
|
||||
[[nodiscard]] const std::filesystem::path& GetGameFolder() const {
|
||||
return game_folder;
|
||||
}
|
||||
|
||||
[[nodiscard]] const std::vector<std::string> GetNpCommIds() const {
|
||||
return npCommIds;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Common
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
#include "common/key_manager.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/path_util.h"
|
||||
#include "core/file_format/npbind.h"
|
||||
#include "core/file_format/trp.h"
|
||||
|
||||
static void DecryptEFSM(std::span<const u8, 16> trophyKey, std::span<const u8, 16> NPcommID,
|
||||
@ -43,8 +42,10 @@ static void hexToBytes(const char* hex, unsigned char* dst) {
|
||||
}
|
||||
}
|
||||
|
||||
bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string titleId) {
|
||||
std::filesystem::path gameSysDir = trophyPath / "sce_sys/trophy/";
|
||||
bool TRP::Extract(const std::filesystem::path& trophyPath, int index, std::string npCommId,
|
||||
const std::filesystem::path& outputPath) {
|
||||
std::filesystem::path gameSysDir =
|
||||
trophyPath / "sce_sys/trophy/" / std::format("trophy{:02d}.trp", index);
|
||||
if (!std::filesystem::exists(gameSysDir)) {
|
||||
LOG_WARNING(Common_Filesystem, "Game trophy directory doesn't exist");
|
||||
return false;
|
||||
@ -61,117 +62,82 @@ bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string tit
|
||||
std::array<u8, 16> user_key{};
|
||||
std::copy(user_key_vec.begin(), user_key_vec.end(), user_key.begin());
|
||||
|
||||
// Load npbind.dat using the new class
|
||||
std::filesystem::path npbindPath = trophyPath / "sce_sys/npbind.dat";
|
||||
NPBindFile npbind;
|
||||
if (!npbind.Load(npbindPath.string())) {
|
||||
LOG_WARNING(Common_Filesystem, "Failed to load npbind.dat file");
|
||||
}
|
||||
|
||||
auto npCommIds = npbind.GetNpCommIds();
|
||||
if (npCommIds.empty()) {
|
||||
LOG_WARNING(Common_Filesystem, "No NPComm IDs found in npbind.dat");
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
int trpFileIndex = 0;
|
||||
|
||||
try {
|
||||
// Process each TRP file in the trophy directory
|
||||
for (const auto& it : std::filesystem::directory_iterator(gameSysDir)) {
|
||||
if (!it.is_regular_file() || it.path().extension() != ".trp") {
|
||||
continue; // Skip non-TRP files
|
||||
}
|
||||
const auto& it = gameSysDir;
|
||||
if (it.extension() != ".trp") {
|
||||
return false;
|
||||
}
|
||||
Common::FS::IOFile file(it, Common::FS::FileAccessMode::Read);
|
||||
if (!file.IsOpen()) {
|
||||
LOG_ERROR(Common_Filesystem, "Unable to open trophy file: {}", it.string());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get NPCommID for this TRP file (if available)
|
||||
std::string npCommId;
|
||||
if (trpFileIndex < static_cast<int>(npCommIds.size())) {
|
||||
npCommId = npCommIds[trpFileIndex];
|
||||
LOG_DEBUG(Common_Filesystem, "Using NPCommID: {} for {}", npCommId,
|
||||
it.path().filename().string());
|
||||
} else {
|
||||
LOG_WARNING(Common_Filesystem, "No NPCommID found for TRP file index {}",
|
||||
trpFileIndex);
|
||||
}
|
||||
TrpHeader header;
|
||||
if (!file.Read(header)) {
|
||||
LOG_ERROR(Common_Filesystem, "Failed to read TRP header from {}", it.string());
|
||||
return false;
|
||||
}
|
||||
|
||||
Common::FS::IOFile file(it.path(), Common::FS::FileAccessMode::Read);
|
||||
if (!file.IsOpen()) {
|
||||
LOG_ERROR(Common_Filesystem, "Unable to open trophy file: {}", it.path().string());
|
||||
if (header.magic != TRP_MAGIC) {
|
||||
LOG_ERROR(Common_Filesystem, "Wrong trophy magic number in {}", it.string());
|
||||
return false;
|
||||
}
|
||||
|
||||
s64 seekPos = sizeof(TrpHeader);
|
||||
// Create output directories
|
||||
if (!std::filesystem::create_directories(outputPath / "Icons") ||
|
||||
!std::filesystem::create_directories(outputPath / "Xml")) {
|
||||
LOG_ERROR(Common_Filesystem, "Failed to create output directories for {}", npCommId);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Process each entry in the TRP file
|
||||
for (int i = 0; i < header.entry_num; i++) {
|
||||
if (!file.Seek(seekPos)) {
|
||||
LOG_ERROR(Common_Filesystem, "Failed to seek to TRP entry offset");
|
||||
success = false;
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
seekPos += static_cast<s64>(header.entry_size);
|
||||
|
||||
TrpHeader header;
|
||||
if (!file.Read(header)) {
|
||||
LOG_ERROR(Common_Filesystem, "Failed to read TRP header from {}",
|
||||
it.path().string());
|
||||
TrpEntry entry;
|
||||
if (!file.Read(entry)) {
|
||||
LOG_ERROR(Common_Filesystem, "Failed to read TRP entry");
|
||||
success = false;
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
|
||||
if (header.magic != TRP_MAGIC) {
|
||||
LOG_ERROR(Common_Filesystem, "Wrong trophy magic number in {}", it.path().string());
|
||||
success = false;
|
||||
continue;
|
||||
}
|
||||
std::string_view name(entry.entry_name);
|
||||
|
||||
s64 seekPos = sizeof(TrpHeader);
|
||||
std::filesystem::path trpFilesPath(
|
||||
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / titleId /
|
||||
"TrophyFiles" / it.path().stem());
|
||||
|
||||
// Create output directories
|
||||
if (!std::filesystem::create_directories(trpFilesPath / "Icons") ||
|
||||
!std::filesystem::create_directories(trpFilesPath / "Xml")) {
|
||||
LOG_ERROR(Common_Filesystem, "Failed to create output directories for {}", titleId);
|
||||
success = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process each entry in the TRP file
|
||||
for (int i = 0; i < header.entry_num; i++) {
|
||||
if (!file.Seek(seekPos)) {
|
||||
LOG_ERROR(Common_Filesystem, "Failed to seek to TRP entry offset");
|
||||
if (entry.flag == ENTRY_FLAG_PNG) {
|
||||
if (!ProcessPngEntry(file, entry, outputPath, name)) {
|
||||
success = false;
|
||||
break;
|
||||
// Continue with next entry
|
||||
}
|
||||
seekPos += static_cast<s64>(header.entry_size);
|
||||
|
||||
TrpEntry entry;
|
||||
if (!file.Read(entry)) {
|
||||
LOG_ERROR(Common_Filesystem, "Failed to read TRP entry");
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
|
||||
std::string_view name(entry.entry_name);
|
||||
|
||||
if (entry.flag == ENTRY_FLAG_PNG) {
|
||||
if (!ProcessPngEntry(file, entry, trpFilesPath, name)) {
|
||||
} else if (entry.flag == ENTRY_FLAG_ENCRYPTED_XML) {
|
||||
// Check if we have a valid NPCommID for decryption
|
||||
if (npCommId.size() >= 12 && npCommId[0] == 'N' && npCommId[1] == 'P') {
|
||||
if (!ProcessEncryptedXmlEntry(file, entry, outputPath, name, user_key,
|
||||
npCommId)) {
|
||||
success = false;
|
||||
// Continue with next entry
|
||||
}
|
||||
} else if (entry.flag == ENTRY_FLAG_ENCRYPTED_XML) {
|
||||
// Check if we have a valid NPCommID for decryption
|
||||
if (npCommId.size() >= 12 && npCommId[0] == 'N' && npCommId[1] == 'P') {
|
||||
if (!ProcessEncryptedXmlEntry(file, entry, trpFilesPath, name, user_key,
|
||||
npCommId)) {
|
||||
success = false;
|
||||
// Continue with next entry
|
||||
}
|
||||
} else {
|
||||
LOG_WARNING(Common_Filesystem,
|
||||
"Skipping encrypted XML entry - invalid NPCommID");
|
||||
// Skip this entry but continue
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG(Common_Filesystem, "Unknown entry flag: {} for {}",
|
||||
static_cast<unsigned int>(entry.flag), name);
|
||||
LOG_WARNING(Common_Filesystem,
|
||||
"Skipping encrypted XML entry - invalid NPCommID");
|
||||
// Skip this entry but continue
|
||||
}
|
||||
} else {
|
||||
LOG_DEBUG(Common_Filesystem, "Unknown entry flag: {} for {}",
|
||||
static_cast<unsigned int>(entry.flag), name);
|
||||
}
|
||||
|
||||
trpFileIndex++;
|
||||
}
|
||||
|
||||
} catch (const std::filesystem::filesystem_error& e) {
|
||||
LOG_CRITICAL(Common_Filesystem, "Filesystem error during trophy extraction: {}", e.what());
|
||||
return false;
|
||||
@ -182,7 +148,7 @@ bool TRP::Extract(const std::filesystem::path& trophyPath, const std::string tit
|
||||
|
||||
if (success) {
|
||||
LOG_INFO(Common_Filesystem, "Successfully extracted {} trophy files for {}", trpFileIndex,
|
||||
titleId);
|
||||
npCommId);
|
||||
}
|
||||
|
||||
return success;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2024-2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#pragma once
|
||||
@ -36,7 +36,8 @@ class TRP {
|
||||
public:
|
||||
TRP();
|
||||
~TRP();
|
||||
bool Extract(const std::filesystem::path& trophyPath, const std::string titleId);
|
||||
bool Extract(const std::filesystem::path& trophyPath, int index, std::string npCommId,
|
||||
const std::filesystem::path& outputPath);
|
||||
|
||||
private:
|
||||
bool ProcessPngEntry(Common::FS::IOFile& file, const TrpEntry& entry,
|
||||
@ -45,9 +46,6 @@ private:
|
||||
const std::filesystem::path& outputPath, std::string_view name,
|
||||
const std::array<u8, 16>& user_key, const std::string& npCommId);
|
||||
|
||||
std::vector<u8> NPcommID = std::vector<u8>(12);
|
||||
std::array<u8, 16> np_comm_id{};
|
||||
std::array<u8, 16> esfmIv{};
|
||||
std::filesystem::path trpFilesPath;
|
||||
static constexpr int iv_len = 16;
|
||||
};
|
||||
|
||||
@ -1,22 +1,139 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2025 shadPS4 Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2025-2026 shadPS4 Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <unordered_map>
|
||||
#include <pugixml.hpp>
|
||||
|
||||
#include "common/elf_info.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "common/path_util.h"
|
||||
#include "common/slot_vector.h"
|
||||
#include "core/emulator_settings.h"
|
||||
#include "core/libraries/libs.h"
|
||||
#include "core/libraries/np/np_error.h"
|
||||
#include "core/libraries/np/np_trophy.h"
|
||||
#include "core/libraries/np/np_trophy_error.h"
|
||||
#include "core/libraries/np/trophy_ui.h"
|
||||
#include "core/libraries/system/userservice.h"
|
||||
#include "core/memory.h"
|
||||
|
||||
namespace Libraries::Np::NpTrophy {
|
||||
|
||||
std::string game_serial;
|
||||
// PS4 system language IDs map directly to TROP00.XML .. TROP30.XML.
|
||||
// Index = OrbisSystemServiceParamId language value reported by the system.
|
||||
// clang-format off
|
||||
static constexpr std::array<std::string_view, 31> s_language_xml_names = {
|
||||
"TROP_00.XML", // 00 Japanese
|
||||
"TROP_01.XML", // 01 English (US)
|
||||
"TROP_02.XML", // 02 French
|
||||
"TROP_03.XML", // 03 Spanish (ES)
|
||||
"TROP_04.XML", // 04 German
|
||||
"TROP_05.XML", // 05 Italian
|
||||
"TROP_06.XML", // 06 Dutch
|
||||
"TROP_07.XML", // 07 Portuguese (PT)
|
||||
"TROP_08.XML", // 08 Russian
|
||||
"TROP_09.XML", // 09 Korean
|
||||
"TROP_10.XML", // 10 Traditional Chinese
|
||||
"TROP_11.XML", // 11 Simplified Chinese
|
||||
"TROP_12.XML", // 12 Finnish
|
||||
"TROP_13.XML", // 13 Swedish
|
||||
"TROP_14.XML", // 14 Danish
|
||||
"TROP_15.XML", // 15 Norwegian
|
||||
"TROP_16.XML", // 16 Polish
|
||||
"TROP_17.XML", // 17 Portuguese (BR)
|
||||
"TROP_18.XML", // 18 English (GB)
|
||||
"TROP_19.XML", // 19 Turkish
|
||||
"TROP_20.XML", // 20 Spanish (LA)
|
||||
"TROP_21.XML", // 21 Arabic
|
||||
"TROP_22.XML", // 22 French (CA)
|
||||
"TROP_23.XML", // 23 Czech
|
||||
"TROP_24.XML", // 24 Hungarian
|
||||
"TROP_25.XML", // 25 Greek
|
||||
"TROP_26.XML", // 26 Romanian
|
||||
"TROP_27.XML", // 27 Thai
|
||||
"TROP_28.XML", // 28 Vietnamese
|
||||
"TROP_29.XML", // 29 Indonesian
|
||||
"TROP_30.XML", // 30 Unkrainian
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
// Returns the best available trophy XML path for the current system language.
|
||||
// Resolution order:
|
||||
// 1. TROP_XX.XML for the active system language (e.g. TROP01.XML for English)
|
||||
// 2. TROP.XML (master / language-neutral fallback)
|
||||
static std::filesystem::path GetTrophyXmlPath(const std::filesystem::path& xml_dir,
|
||||
int system_language) {
|
||||
// Try the exact language file first.
|
||||
if (system_language >= 0 && system_language < static_cast<int>(s_language_xml_names.size())) {
|
||||
auto lang_path = xml_dir / s_language_xml_names[system_language];
|
||||
if (std::filesystem::exists(lang_path)) {
|
||||
return lang_path;
|
||||
}
|
||||
}
|
||||
// Final fallback: master TROP.XML (always present).
|
||||
return xml_dir / "TROP.XML";
|
||||
}
|
||||
|
||||
static std::vector<std::filesystem::path> GetAllTrophyXmlPaths(
|
||||
const std::filesystem::path& xml_dir) {
|
||||
std::vector<std::filesystem::path> paths;
|
||||
|
||||
// Always include the master file.
|
||||
auto master = xml_dir / "TROP.XML";
|
||||
if (std::filesystem::exists(master)) {
|
||||
paths.push_back(master);
|
||||
}
|
||||
|
||||
// Include every language file that is actually present.
|
||||
for (const auto& name : s_language_xml_names) {
|
||||
auto lang_path = xml_dir / name;
|
||||
if (std::filesystem::exists(lang_path)) {
|
||||
paths.push_back(lang_path);
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
static void ApplyUnlockToXmlFile(const std::filesystem::path& xml_path, OrbisNpTrophyId trophyId,
|
||||
u64 trophyTimestamp, bool unlock_platinum,
|
||||
OrbisNpTrophyId platinumId, u64 platinumTimestamp) {
|
||||
pugi::xml_document doc;
|
||||
if (!doc.load_file(xml_path.native().c_str())) {
|
||||
LOG_WARNING(Lib_NpTrophy, "ApplyUnlock: failed to load {}", xml_path.string());
|
||||
return;
|
||||
}
|
||||
|
||||
auto trophyconf = doc.child("trophyconf");
|
||||
for (pugi::xml_node& node : trophyconf.children()) {
|
||||
if (std::string_view(node.name()) != "trophy") {
|
||||
continue;
|
||||
}
|
||||
int id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID);
|
||||
|
||||
auto set_unlock = [&](u64 ts) {
|
||||
if (node.attribute("unlockstate").empty()) {
|
||||
node.append_attribute("unlockstate") = "true";
|
||||
} else {
|
||||
node.attribute("unlockstate").set_value("true");
|
||||
}
|
||||
const auto ts_str = std::to_string(ts);
|
||||
if (node.attribute("timestamp").empty()) {
|
||||
node.append_attribute("timestamp") = ts_str.c_str();
|
||||
} else {
|
||||
node.attribute("timestamp").set_value(ts_str.c_str());
|
||||
}
|
||||
};
|
||||
|
||||
if (id == trophyId) {
|
||||
set_unlock(trophyTimestamp);
|
||||
} else if (unlock_platinum && id == platinumId) {
|
||||
set_unlock(platinumTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
doc.save_file(xml_path.native().c_str());
|
||||
}
|
||||
|
||||
static constexpr auto MaxTrophyHandles = 4u;
|
||||
static constexpr auto MaxTrophyContexts = 8u;
|
||||
@ -30,6 +147,10 @@ struct ContextKeyHash {
|
||||
|
||||
struct TrophyContext {
|
||||
u32 context_id;
|
||||
bool registered = false;
|
||||
std::filesystem::path trophy_xml_path; // resolved once at CreateContext
|
||||
std::filesystem::path xml_dir; // .../Xml/
|
||||
std::filesystem::path icons_dir; // .../Icons/
|
||||
};
|
||||
static Common::SlotVector<OrbisNpTrophyHandle> trophy_handles{};
|
||||
static Common::SlotVector<ContextKey> trophy_contexts{};
|
||||
@ -94,66 +215,10 @@ OrbisNpTrophyGrade GetTrophyGradeFromChar(char trophyType) {
|
||||
}
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophyAbortHandle(OrbisNpTrophyHandle handle) {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophyCaptureScreenshot() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyDetails() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyFlagArray() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyGroupArray() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyGroupDetails() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetInfo() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetInfoInGroup() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetVersion() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyTitleDetails() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophyConfigHasGroupFeature() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context,
|
||||
Libraries::UserService::OrbisUserServiceUserId user_id,
|
||||
uint32_t service_label, u64 options) {
|
||||
ASSERT(options == 0ull);
|
||||
if (!context) {
|
||||
if (!context || options != 0ull) {
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
@ -169,7 +234,18 @@ s32 PS4_SYSV_ABI sceNpTrophyCreateContext(OrbisNpTrophyContext* context,
|
||||
const auto ctx_id = trophy_contexts.insert(user_id, service_label);
|
||||
|
||||
*context = ctx_id.index + 1;
|
||||
contexts_internal[key].context_id = *context;
|
||||
|
||||
auto& ctx = contexts_internal[key];
|
||||
ctx.context_id = *context;
|
||||
|
||||
// Resolve and cache all paths once so callers never recompute them.
|
||||
const std::string trophy_folder = Common::ElfInfo::Instance().GetNpCommIds()[service_label];
|
||||
const auto trophy_base = Common::FS::GetUserPath(Common::FS::PathType::HomeDir) /
|
||||
std::to_string(user_id) / "trophy" / trophy_folder;
|
||||
ctx.xml_dir = trophy_base / "Xml";
|
||||
ctx.icons_dir = trophy_base / "Icons";
|
||||
ctx.trophy_xml_path = GetTrophyXmlPath(ctx.xml_dir, EmulatorSettings.GetConsoleLanguage());
|
||||
|
||||
LOG_INFO(Lib_NpTrophy, "New context = {}, user_id = {} service label = {}", *context, user_id,
|
||||
service_label);
|
||||
|
||||
@ -206,6 +282,10 @@ int PS4_SYSV_ABI sceNpTrophyDestroyContext(OrbisNpTrophyContext context) {
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
|
||||
}
|
||||
|
||||
if (!trophy_contexts.is_allocated(contextId)) {
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
|
||||
}
|
||||
|
||||
ContextKey contextkey = trophy_contexts[contextId];
|
||||
trophy_contexts.erase(contextId);
|
||||
contexts_internal.erase(contextkey);
|
||||
@ -251,12 +331,10 @@ int PS4_SYSV_ABI sceNpTrophyGetGameIcon(OrbisNpTrophyContext context, OrbisNpTro
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
|
||||
}
|
||||
ContextKey contextkey = trophy_contexts[contextId];
|
||||
char trophy_folder[9];
|
||||
snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second);
|
||||
|
||||
const auto trophy_dir =
|
||||
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles";
|
||||
auto icon_file = trophy_dir / trophy_folder / "Icons" / "ICON0.PNG";
|
||||
const auto& ctx = contexts_internal[contextkey];
|
||||
|
||||
auto icon_file = ctx.icons_dir / "ICON0.PNG";
|
||||
|
||||
Common::FS::IOFile icon(icon_file, Common::FS::FileAccessMode::Read);
|
||||
if (!icon.IsOpen()) {
|
||||
@ -304,12 +382,10 @@ int PS4_SYSV_ABI sceNpTrophyGetGameInfo(OrbisNpTrophyContext context, OrbisNpTro
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
|
||||
}
|
||||
ContextKey contextkey = trophy_contexts[contextId];
|
||||
char trophy_folder[9];
|
||||
snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second);
|
||||
|
||||
const auto trophy_dir =
|
||||
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles";
|
||||
auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML";
|
||||
const auto& ctx = contexts_internal[contextkey];
|
||||
if (!ctx.registered)
|
||||
return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED;
|
||||
const auto& trophy_file = ctx.trophy_xml_path;
|
||||
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str());
|
||||
@ -368,8 +444,9 @@ int PS4_SYSV_ABI sceNpTrophyGetGameInfo(OrbisNpTrophyContext context, OrbisNpTro
|
||||
data->unlocked_silver = game_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_SILVER];
|
||||
data->unlocked_bronze = game_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_BRONZE];
|
||||
|
||||
// maybe this should be 1 instead of 100?
|
||||
data->progress_percentage = 100;
|
||||
data->progress_percentage = (game_info.num_trophies > 0)
|
||||
? (game_info.unlocked_trophies * 100u) / game_info.num_trophies
|
||||
: 0;
|
||||
|
||||
return ORBIS_OK;
|
||||
}
|
||||
@ -411,12 +488,10 @@ int PS4_SYSV_ABI sceNpTrophyGetGroupInfo(OrbisNpTrophyContext context, OrbisNpTr
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
|
||||
}
|
||||
ContextKey contextkey = trophy_contexts[contextId];
|
||||
char trophy_folder[9];
|
||||
snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second);
|
||||
|
||||
const auto trophy_dir =
|
||||
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles";
|
||||
auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML";
|
||||
const auto& ctx = contexts_internal[contextkey];
|
||||
if (!ctx.registered)
|
||||
return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED;
|
||||
const auto& trophy_file = ctx.trophy_xml_path;
|
||||
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str());
|
||||
@ -484,15 +559,84 @@ int PS4_SYSV_ABI sceNpTrophyGetGroupInfo(OrbisNpTrophyContext context, OrbisNpTr
|
||||
data->unlocked_silver = group_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_SILVER];
|
||||
data->unlocked_bronze = group_info.unlocked_trophies_by_rarity[ORBIS_NP_TROPHY_GRADE_BRONZE];
|
||||
|
||||
// maybe this should be 1 instead of 100?
|
||||
data->progress_percentage = 100;
|
||||
data->progress_percentage =
|
||||
(group_info.num_trophies > 0)
|
||||
? (group_info.unlocked_trophies * 100u) / group_info.num_trophies
|
||||
: 0;
|
||||
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophyGetTrophyIcon(OrbisNpTrophyContext context, OrbisNpTrophyHandle handle,
|
||||
OrbisNpTrophyId trophyId, void* buffer, u64* size) {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
if (size == nullptr)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT;
|
||||
|
||||
if (trophyId < 0 || trophyId >= ORBIS_NP_TROPHY_NUM_MAX)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID;
|
||||
|
||||
if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
|
||||
|
||||
if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
|
||||
|
||||
Common::SlotId contextId;
|
||||
contextId.index = context - 1;
|
||||
if (contextId.index >= trophy_contexts.size() || !trophy_contexts.is_allocated(contextId)) {
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
|
||||
}
|
||||
|
||||
s32 handle_index = handle - 1;
|
||||
if (handle_index >= trophy_handles.size() ||
|
||||
!trophy_handles.is_allocated({static_cast<u32>(handle_index)})) {
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
ContextKey contextkey = trophy_contexts[contextId];
|
||||
const auto& ctx = contexts_internal[contextkey];
|
||||
if (!ctx.registered)
|
||||
return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED;
|
||||
|
||||
// Check that the trophy is unlocked — icons are only available for earned trophies.
|
||||
pugi::xml_document doc;
|
||||
if (!doc.load_file(ctx.trophy_xml_path.native().c_str())) {
|
||||
LOG_ERROR(Lib_NpTrophy, "Failed to open trophy XML: {}", ctx.trophy_xml_path.string());
|
||||
return ORBIS_NP_TROPHY_ERROR_ICON_FILE_NOT_FOUND;
|
||||
}
|
||||
|
||||
bool unlocked = false;
|
||||
bool found = false;
|
||||
for (const pugi::xml_node& node : doc.child("trophyconf").children()) {
|
||||
if (std::string_view(node.name()) != "trophy")
|
||||
continue;
|
||||
if (node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID) == trophyId) {
|
||||
found = true;
|
||||
unlocked = node.attribute("unlockstate").as_bool();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID;
|
||||
|
||||
if (!unlocked)
|
||||
return ORBIS_NP_TROPHY_ERROR_TROPHY_NOT_UNLOCKED;
|
||||
|
||||
const std::string icon_name = fmt::format("TROP{:03d}.PNG", trophyId);
|
||||
const auto icon_path = ctx.icons_dir / icon_name;
|
||||
|
||||
Common::FS::IOFile icon(icon_path, Common::FS::FileAccessMode::Read);
|
||||
if (!icon.IsOpen()) {
|
||||
LOG_ERROR(Lib_NpTrophy, "Failed to open trophy icon: {}", icon_path.string());
|
||||
return ORBIS_NP_TROPHY_ERROR_ICON_FILE_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (buffer != nullptr) {
|
||||
ReadFile(icon, buffer, *size);
|
||||
} else {
|
||||
*size = icon.GetSize();
|
||||
}
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
@ -507,7 +651,7 @@ int PS4_SYSV_ABI sceNpTrophyGetTrophyInfo(OrbisNpTrophyContext context, OrbisNpT
|
||||
if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
|
||||
|
||||
if (trophyId >= 127)
|
||||
if (trophyId >= ORBIS_NP_TROPHY_NUM_MAX)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID;
|
||||
|
||||
if (details == nullptr || data == nullptr)
|
||||
@ -522,12 +666,10 @@ int PS4_SYSV_ABI sceNpTrophyGetTrophyInfo(OrbisNpTrophyContext context, OrbisNpT
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
|
||||
}
|
||||
ContextKey contextkey = trophy_contexts[contextId];
|
||||
char trophy_folder[9];
|
||||
snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second);
|
||||
|
||||
const auto trophy_dir =
|
||||
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles";
|
||||
auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML";
|
||||
const auto& ctx = contexts_internal[contextkey];
|
||||
if (!ctx.registered)
|
||||
return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED;
|
||||
const auto& trophy_file = ctx.trophy_xml_path;
|
||||
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str());
|
||||
@ -579,29 +721,34 @@ s32 PS4_SYSV_ABI sceNpTrophyGetTrophyUnlockState(OrbisNpTrophyContext context,
|
||||
OrbisNpTrophyFlagArray* flags, u32* count) {
|
||||
LOG_INFO(Lib_NpTrophy, "called");
|
||||
|
||||
if (flags == nullptr || count == nullptr)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT;
|
||||
|
||||
if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
|
||||
|
||||
if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
|
||||
|
||||
if (flags == nullptr || count == nullptr)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT;
|
||||
|
||||
ORBIS_NP_TROPHY_FLAG_ZERO(flags);
|
||||
|
||||
Common::SlotId contextId;
|
||||
contextId.index = context - 1;
|
||||
if (contextId.index >= trophy_contexts.size()) {
|
||||
if (contextId.index >= trophy_contexts.size() || !trophy_contexts.is_allocated(contextId)) {
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
|
||||
}
|
||||
ContextKey contextkey = trophy_contexts[contextId];
|
||||
char trophy_folder[9];
|
||||
snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second);
|
||||
|
||||
const auto trophy_dir =
|
||||
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles";
|
||||
auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML";
|
||||
s32 handle_index = handle - 1;
|
||||
if (handle_index >= trophy_handles.size() ||
|
||||
!trophy_handles.is_allocated({static_cast<u32>(handle_index)})) {
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
ContextKey contextkey = trophy_contexts[contextId];
|
||||
const auto& ctx = contexts_internal[contextkey];
|
||||
if (!ctx.registered)
|
||||
return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED;
|
||||
const auto& trophy_file = ctx.trophy_xml_path;
|
||||
|
||||
ORBIS_NP_TROPHY_FLAG_ZERO(flags);
|
||||
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str());
|
||||
@ -622,10 +769,9 @@ s32 PS4_SYSV_ABI sceNpTrophyGetTrophyUnlockState(OrbisNpTrophyContext context,
|
||||
|
||||
if (node_name == "trophy") {
|
||||
num_trophies++;
|
||||
}
|
||||
|
||||
if (current_trophy_unlockstate) {
|
||||
ORBIS_NP_TROPHY_FLAG_SET(current_trophy_id, flags);
|
||||
if (current_trophy_unlockstate) {
|
||||
ORBIS_NP_TROPHY_FLAG_SET(current_trophy_id, flags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -633,6 +779,185 @@ s32 PS4_SYSV_ABI sceNpTrophyGetTrophyUnlockState(OrbisNpTrophyContext context,
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophyRegisterContext(OrbisNpTrophyContext context,
|
||||
OrbisNpTrophyHandle handle, uint64_t options) {
|
||||
if (options != 0ull)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT;
|
||||
|
||||
if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
|
||||
|
||||
if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
|
||||
|
||||
Common::SlotId contextId;
|
||||
contextId.index = context - 1;
|
||||
if (contextId.index >= trophy_contexts.size() || !trophy_contexts.is_allocated(contextId)) {
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
|
||||
}
|
||||
|
||||
s32 handle_index = handle - 1;
|
||||
if (handle_index >= trophy_handles.size() ||
|
||||
!trophy_handles.is_allocated({static_cast<u32>(handle_index)})) {
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
ContextKey contextkey = trophy_contexts[contextId];
|
||||
auto& ctx = contexts_internal[contextkey];
|
||||
|
||||
if (ctx.registered)
|
||||
return ORBIS_NP_TROPHY_ERROR_ALREADY_REGISTERED;
|
||||
|
||||
if (!std::filesystem::exists(ctx.trophy_xml_path))
|
||||
return ORBIS_NP_TROPHY_ERROR_TITLE_CONF_NOT_INSTALLED;
|
||||
|
||||
ctx.registered = true;
|
||||
LOG_INFO(Lib_NpTrophy, "Context {} registered", context);
|
||||
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophyUnlockTrophy(OrbisNpTrophyContext context, OrbisNpTrophyHandle handle,
|
||||
OrbisNpTrophyId trophyId, OrbisNpTrophyId* platinumId) {
|
||||
LOG_INFO(Lib_NpTrophy, "Unlocking trophy id {}", trophyId);
|
||||
|
||||
if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
|
||||
|
||||
if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
|
||||
|
||||
if (trophyId >= ORBIS_NP_TROPHY_NUM_MAX)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID;
|
||||
|
||||
if (platinumId == nullptr)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT;
|
||||
|
||||
Common::SlotId contextId;
|
||||
contextId.index = context - 1;
|
||||
if (contextId.index >= trophy_contexts.size() || !trophy_contexts.is_allocated(contextId)) {
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
|
||||
}
|
||||
|
||||
s32 handle_index = handle - 1;
|
||||
if (handle_index >= trophy_handles.size() ||
|
||||
!trophy_handles.is_allocated({static_cast<u32>(handle_index)})) {
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
|
||||
}
|
||||
|
||||
ContextKey contextkey = trophy_contexts[contextId];
|
||||
const auto& ctx = contexts_internal[contextkey];
|
||||
if (!ctx.registered)
|
||||
return ORBIS_NP_TROPHY_ERROR_NOT_REGISTERED;
|
||||
const auto& xml_dir = ctx.xml_dir;
|
||||
const auto& trophy_file = ctx.trophy_xml_path;
|
||||
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str());
|
||||
|
||||
if (!result) {
|
||||
LOG_ERROR(Lib_NpTrophy, "Failed to parse trophy xml : {}", result.description());
|
||||
return ORBIS_NP_TROPHY_ERROR_TITLE_NOT_FOUND;
|
||||
}
|
||||
|
||||
*platinumId = ORBIS_NP_TROPHY_INVALID_TROPHY_ID;
|
||||
|
||||
int num_trophies = 0;
|
||||
int num_trophies_unlocked = 0;
|
||||
pugi::xml_node platinum_node;
|
||||
|
||||
// Outputs filled during the scan.
|
||||
bool trophy_found = false;
|
||||
const char* trophy_name = "";
|
||||
std::string_view trophy_type;
|
||||
std::filesystem::path trophy_icon_path;
|
||||
|
||||
auto trophyconf = doc.child("trophyconf");
|
||||
|
||||
for (pugi::xml_node& node : trophyconf.children()) {
|
||||
if (std::string_view(node.name()) != "trophy")
|
||||
continue;
|
||||
|
||||
int current_trophy_id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID);
|
||||
bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool();
|
||||
std::string_view current_trophy_type = node.attribute("ttype").value();
|
||||
|
||||
if (current_trophy_type == "P") {
|
||||
platinum_node = node;
|
||||
if (trophyId == current_trophy_id) {
|
||||
return ORBIS_NP_TROPHY_ERROR_PLATINUM_CANNOT_UNLOCK;
|
||||
}
|
||||
}
|
||||
|
||||
if (node.attribute("pid").as_int(-1) != ORBIS_NP_TROPHY_INVALID_TROPHY_ID) {
|
||||
num_trophies++;
|
||||
if (current_trophy_unlockstate) {
|
||||
num_trophies_unlocked++;
|
||||
}
|
||||
}
|
||||
|
||||
if (current_trophy_id == trophyId) {
|
||||
if (current_trophy_unlockstate) {
|
||||
LOG_INFO(Lib_NpTrophy, "Trophy already unlocked");
|
||||
return ORBIS_NP_TROPHY_ERROR_TROPHY_ALREADY_UNLOCKED;
|
||||
}
|
||||
trophy_found = true;
|
||||
trophy_name = node.child("name").text().as_string();
|
||||
trophy_type = current_trophy_type;
|
||||
|
||||
const std::string icon_file = fmt::format("TROP{:03d}.PNG", current_trophy_id);
|
||||
trophy_icon_path = ctx.icons_dir / icon_file;
|
||||
}
|
||||
}
|
||||
|
||||
if (!trophy_found)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID;
|
||||
|
||||
// Capture timestamps once so every file gets the exact same value.
|
||||
const auto now_secs = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
const u64 trophy_timestamp = static_cast<u64>(now_secs);
|
||||
|
||||
// Decide platinum.
|
||||
bool unlock_platinum = false;
|
||||
OrbisNpTrophyId platinum_id = ORBIS_NP_TROPHY_INVALID_TROPHY_ID;
|
||||
u64 platinum_timestamp = 0;
|
||||
const char* platinum_name = "";
|
||||
std::filesystem::path platinum_icon_path;
|
||||
|
||||
if (!platinum_node.attribute("unlockstate").as_bool()) {
|
||||
if ((num_trophies - 1) == num_trophies_unlocked) {
|
||||
unlock_platinum = true;
|
||||
platinum_id = platinum_node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID);
|
||||
platinum_timestamp = trophy_timestamp; // same second is fine
|
||||
platinum_name = platinum_node.child("name").text().as_string();
|
||||
|
||||
const std::string plat_icon_file = fmt::format("TROP{:03d}.PNG", platinum_id);
|
||||
platinum_icon_path = ctx.icons_dir / plat_icon_file;
|
||||
|
||||
*platinumId = platinum_id;
|
||||
}
|
||||
}
|
||||
|
||||
// Queue UI notifications (only once, using the primary XML's strings).
|
||||
AddTrophyToQueue(trophy_icon_path, trophy_name, trophy_type);
|
||||
if (unlock_platinum) {
|
||||
AddTrophyToQueue(platinum_icon_path, platinum_name, "P");
|
||||
}
|
||||
|
||||
// TROP.XML + TROP00.XML .. TROP30.XML are all written so that switching
|
||||
// the system language never loses trophy progress.
|
||||
const auto all_xml_paths = GetAllTrophyXmlPaths(xml_dir);
|
||||
for (const auto& xml_path : all_xml_paths) {
|
||||
ApplyUnlockToXmlFile(xml_path, trophyId, trophy_timestamp, unlock_platinum, platinum_id,
|
||||
platinum_timestamp);
|
||||
}
|
||||
LOG_INFO(Lib_NpTrophy, "Trophy {} written to {} XML file(s)", trophyId, all_xml_paths.size());
|
||||
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophyGroupArrayGetNum() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
@ -698,19 +1023,6 @@ int PS4_SYSV_ABI sceNpTrophyNumInfoGetTotal() {
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophyRegisterContext(OrbisNpTrophyContext context,
|
||||
OrbisNpTrophyHandle handle, uint64_t options) {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
|
||||
if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
|
||||
|
||||
if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
|
||||
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophySetInfoGetTrophyFlagArray() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
@ -942,147 +1254,58 @@ int PS4_SYSV_ABI sceNpTrophySystemSetDbgParamInt() {
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophyUnlockTrophy(OrbisNpTrophyContext context, OrbisNpTrophyHandle handle,
|
||||
OrbisNpTrophyId trophyId, OrbisNpTrophyId* platinumId) {
|
||||
LOG_INFO(Lib_NpTrophy, "Unlocking trophy id {}", trophyId);
|
||||
int PS4_SYSV_ABI sceNpTrophyAbortHandle(OrbisNpTrophyHandle handle) {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
if (context == ORBIS_NP_TROPHY_INVALID_CONTEXT)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
|
||||
int PS4_SYSV_ABI sceNpTrophyCaptureScreenshot() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
if (handle == ORBIS_NP_TROPHY_INVALID_HANDLE)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_HANDLE;
|
||||
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyDetails() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
if (trophyId >= 127)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_TROPHY_ID;
|
||||
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyFlagArray() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
if (platinumId == nullptr)
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_ARGUMENT;
|
||||
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyGroupArray() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
Common::SlotId contextId;
|
||||
contextId.index = context - 1;
|
||||
if (contextId.index >= trophy_contexts.size()) {
|
||||
return ORBIS_NP_TROPHY_ERROR_INVALID_CONTEXT;
|
||||
}
|
||||
ContextKey contextkey = trophy_contexts[contextId];
|
||||
char trophy_folder[9];
|
||||
snprintf(trophy_folder, sizeof(trophy_folder), "trophy%02d", contextkey.second);
|
||||
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyGroupDetails() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
const auto trophy_dir =
|
||||
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / game_serial / "TrophyFiles";
|
||||
auto trophy_file = trophy_dir / trophy_folder / "Xml" / "TROP.XML";
|
||||
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetInfo() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
pugi::xml_document doc;
|
||||
pugi::xml_parse_result result = doc.load_file(trophy_file.native().c_str());
|
||||
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetInfoInGroup() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
LOG_ERROR(Lib_NpTrophy, "Failed to parse trophy xml : {}", result.description());
|
||||
return ORBIS_OK;
|
||||
}
|
||||
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophySetVersion() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
*platinumId = ORBIS_NP_TROPHY_INVALID_TROPHY_ID;
|
||||
|
||||
int num_trophies = 0;
|
||||
int num_trophies_unlocked = 0;
|
||||
pugi::xml_node platinum_node;
|
||||
|
||||
auto trophyconf = doc.child("trophyconf");
|
||||
|
||||
for (pugi::xml_node& node : trophyconf.children()) {
|
||||
int current_trophy_id = node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID);
|
||||
bool current_trophy_unlockstate = node.attribute("unlockstate").as_bool();
|
||||
const char* current_trophy_name = node.child("name").text().as_string();
|
||||
std::string_view current_trophy_description = node.child("detail").text().as_string();
|
||||
std::string_view current_trophy_type = node.attribute("ttype").value();
|
||||
|
||||
if (current_trophy_type == "P") {
|
||||
platinum_node = node;
|
||||
if (trophyId == current_trophy_id) {
|
||||
return ORBIS_NP_TROPHY_ERROR_PLATINUM_CANNOT_UNLOCK;
|
||||
}
|
||||
}
|
||||
|
||||
if (std::string_view(node.name()) == "trophy") {
|
||||
if (node.attribute("pid").as_int(-1) != ORBIS_NP_TROPHY_INVALID_TROPHY_ID) {
|
||||
num_trophies++;
|
||||
if (current_trophy_unlockstate) {
|
||||
num_trophies_unlocked++;
|
||||
}
|
||||
}
|
||||
|
||||
if (current_trophy_id == trophyId) {
|
||||
if (current_trophy_unlockstate) {
|
||||
LOG_INFO(Lib_NpTrophy, "Trophy already unlocked");
|
||||
return ORBIS_NP_TROPHY_ERROR_TROPHY_ALREADY_UNLOCKED;
|
||||
} else {
|
||||
if (node.attribute("unlockstate").empty()) {
|
||||
node.append_attribute("unlockstate") = "true";
|
||||
} else {
|
||||
node.attribute("unlockstate").set_value("true");
|
||||
}
|
||||
|
||||
auto trophyTimestamp = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
|
||||
if (node.attribute("timestamp").empty()) {
|
||||
node.append_attribute("timestamp") =
|
||||
std::to_string(trophyTimestamp).c_str();
|
||||
} else {
|
||||
node.attribute("timestamp")
|
||||
.set_value(std::to_string(trophyTimestamp).c_str());
|
||||
}
|
||||
|
||||
std::string trophy_icon_file = "TROP";
|
||||
trophy_icon_file.append(node.attribute("id").value());
|
||||
trophy_icon_file.append(".PNG");
|
||||
|
||||
std::filesystem::path current_icon_path =
|
||||
trophy_dir / trophy_folder / "Icons" / trophy_icon_file;
|
||||
|
||||
AddTrophyToQueue(current_icon_path, current_trophy_name, current_trophy_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!platinum_node.attribute("unlockstate").as_bool()) {
|
||||
if ((num_trophies - 1) == num_trophies_unlocked) {
|
||||
if (platinum_node.attribute("unlockstate").empty()) {
|
||||
platinum_node.append_attribute("unlockstate") = "true";
|
||||
} else {
|
||||
platinum_node.attribute("unlockstate").set_value("true");
|
||||
}
|
||||
|
||||
auto trophyTimestamp = std::chrono::duration_cast<std::chrono::seconds>(
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
|
||||
if (platinum_node.attribute("timestamp").empty()) {
|
||||
platinum_node.append_attribute("timestamp") =
|
||||
std::to_string(trophyTimestamp).c_str();
|
||||
} else {
|
||||
platinum_node.attribute("timestamp")
|
||||
.set_value(std::to_string(trophyTimestamp).c_str());
|
||||
}
|
||||
|
||||
int platinum_trophy_id =
|
||||
platinum_node.attribute("id").as_int(ORBIS_NP_TROPHY_INVALID_TROPHY_ID);
|
||||
const char* platinum_trophy_name = platinum_node.child("name").text().as_string();
|
||||
|
||||
std::string platinum_icon_file = "TROP";
|
||||
platinum_icon_file.append(platinum_node.attribute("id").value());
|
||||
platinum_icon_file.append(".PNG");
|
||||
|
||||
std::filesystem::path platinum_icon_path =
|
||||
trophy_dir / trophy_folder / "Icons" / platinum_icon_file;
|
||||
|
||||
*platinumId = platinum_trophy_id;
|
||||
AddTrophyToQueue(platinum_icon_path, platinum_trophy_name, "P");
|
||||
}
|
||||
}
|
||||
|
||||
doc.save_file((trophy_dir / trophy_folder / "Xml" / "TROP.XML").native().c_str());
|
||||
int PS4_SYSV_ABI sceNpTrophyConfigGetTrophyTitleDetails() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
int PS4_SYSV_ABI sceNpTrophyConfigHasGroupFeature() {
|
||||
LOG_ERROR(Lib_NpTrophy, "(STUBBED) called");
|
||||
return ORBIS_OK;
|
||||
}
|
||||
|
||||
|
||||
@ -13,8 +13,6 @@ class SymbolsResolver;
|
||||
|
||||
namespace Libraries::Np::NpTrophy {
|
||||
|
||||
extern std::string game_serial;
|
||||
|
||||
constexpr int ORBIS_NP_TROPHY_FLAG_SETSIZE = 128;
|
||||
constexpr int ORBIS_NP_TROPHY_FLAG_BITS_SHIFT = 5;
|
||||
|
||||
|
||||
@ -47,3 +47,4 @@ constexpr int ORBIS_NP_TROPHY_ERROR_INCONSISTENT_TITLE_CONF = 0x80551628;
|
||||
constexpr int ORBIS_NP_TROPHY_ERROR_TITLE_BACKGROUND = 0x80551629;
|
||||
constexpr int ORBIS_NP_TROPHY_ERROR_SCREENSHOT_DISABLED = 0x8055162B;
|
||||
constexpr int ORBIS_NP_TROPHY_ERROR_SCREENSHOT_DISPLAY_BUFFER_NOT_IN_USE = 0x8055162D;
|
||||
constexpr int ORBIS_NP_TROPHY_ERROR_TITLE_NOT_FOUND = 0x805516C2;
|
||||
|
||||
@ -38,6 +38,7 @@
|
||||
#include "core/libraries/save_data/save_backup.h"
|
||||
#include "core/linker.h"
|
||||
#include "core/memory.h"
|
||||
#include "core/user_settings.h"
|
||||
#include "emulator.h"
|
||||
#include "video_core/cache_storage.h"
|
||||
#include "video_core/renderdoc.h"
|
||||
@ -50,6 +51,7 @@
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#include <core/file_format/npbind.h>
|
||||
|
||||
Frontend::WindowSDL* g_window = nullptr;
|
||||
|
||||
@ -196,6 +198,18 @@ void Emulator::Run(std::filesystem::path file, std::vector<std::string> args,
|
||||
}
|
||||
|
||||
game_info.game_folder = game_folder;
|
||||
std::filesystem::path npbindPath = game_folder / "sce_sys/npbind.dat";
|
||||
NPBindFile npbind;
|
||||
if (!npbind.Load(npbindPath.string())) {
|
||||
LOG_WARNING(Common_Filesystem, "Failed to load npbind.dat file");
|
||||
} else {
|
||||
auto npCommIds = npbind.GetNpCommIds();
|
||||
if (npCommIds.empty()) {
|
||||
LOG_WARNING(Common_Filesystem, "No NPComm IDs found in npbind.dat");
|
||||
} else {
|
||||
game_info.npCommIds = std::move(npCommIds);
|
||||
}
|
||||
}
|
||||
|
||||
EmulatorSettings.Load(id);
|
||||
|
||||
@ -291,15 +305,20 @@ void Emulator::Run(std::filesystem::path file, std::vector<std::string> args,
|
||||
// Initialize patcher and trophies
|
||||
if (!id.empty()) {
|
||||
MemoryPatcher::g_game_serial = id;
|
||||
Libraries::Np::NpTrophy::game_serial = id;
|
||||
|
||||
const auto trophyDir =
|
||||
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) / id / "TrophyFiles";
|
||||
if (!std::filesystem::exists(trophyDir)) {
|
||||
TRP trp;
|
||||
if (!trp.Extract(game_folder, id)) {
|
||||
LOG_ERROR(Loader, "Couldn't extract trophies");
|
||||
int index = 0;
|
||||
for (std::string npCommId : game_info.npCommIds) {
|
||||
for (User user : UserSettings.GetUserManager().GetValidUsers()) {
|
||||
const auto trophyDir = Common::FS::GetUserPath(Common::FS::PathType::HomeDir) /
|
||||
std::to_string(user.user_id) / "trophy" / npCommId;
|
||||
if (!std::filesystem::exists(trophyDir)) {
|
||||
TRP trp;
|
||||
if (!trp.Extract(game_folder, index, npCommId, trophyDir)) {
|
||||
LOG_ERROR(Loader, "Couldn't extract trophies");
|
||||
}
|
||||
}
|
||||
}
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -40,6 +40,8 @@ int main(int argc, char* argv[]) {
|
||||
UserSettings.Load();
|
||||
|
||||
// TODO add back trophy key migration without the need of entire previous config framework
|
||||
auto key_manager = KeyManager::GetInstance();
|
||||
key_manager->LoadFromFile();
|
||||
|
||||
CLI::App app{"shadPS4 Emulator CLI"};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user