From cb175278b61c0845247be8afbc448374df84cda3 Mon Sep 17 00:00:00 2001 From: FlexBy420 <68403300+FlexBy420@users.noreply.github.com> Date: Tue, 18 Aug 2026 06:54:51 +0200 Subject: [PATCH] RPCN: Sync trophies (#18760) Add synching local trophies with RPCN server trophies, allows users to essentially cloud save their trophies when they are connected to RPCN. This PR also aims to add support for global earned % trophies simillar to steam for RPCN part of website. No local trophies and not connected to RPCN obraz After booting the game with RPCN enabled obraz RPCN Side PR https://github.com/RipleyTom/rpcn/pull/140 --- rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp | 53 +++++- rpcs3/Emu/NP/np_handler.cpp | 26 +++ rpcs3/Emu/NP/np_handler.h | 5 + rpcs3/Emu/NP/rpcn_client.cpp | 65 ++++++- rpcs3/Emu/NP/rpcn_client.h | 2 + rpcs3/Emu/NP/rpcn_types.h | 2 + .../Trophies/overlay_trophy_list_dialog.cpp | 159 +++++++++++++++++- .../Trophies/overlay_trophy_list_dialog.h | 9 + rpcs3/Emu/localized_string_id.h | 4 + rpcs3/rpcs3qt/localized_emu.h | 4 + 10 files changed, 324 insertions(+), 5 deletions(-) diff --git a/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp b/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp index dc6add3eb..5f659a4d5 100644 --- a/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp +++ b/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp @@ -14,6 +14,7 @@ #include "sceNp.h" #include "sceNpTrophy.h" #include "cellSysutil.h" +#include "Emu/NP/np_handler.h" #include "Utilities/StrUtil.h" @@ -39,6 +40,7 @@ struct trophy_context_t SAVESTATE_INIT_POS(42); std::string trp_name; + SceNpCommunicationId comm_id{}; // set at CreateContext, not serialized std::unique_ptr tropusr; bool read_only = false; @@ -506,6 +508,7 @@ error_code sceNpTrophyCreateContext(vm::ptr context, vm::cptrtrp_name = name; + ctxt->comm_id = *commId; // stored for RPCN trophy sync/unlock ctxt->read_only = !!(options & SCE_NP_TROPHY_OPTIONS_CREATE_CONTEXT_READ_ONLY); *context = idm::last_id(); @@ -714,7 +717,46 @@ error_code sceNpTrophyRegisterContext(ppu_thread& ppu, u32 context, u32 handle, ensure(tropusr->Load(trophyUsrPath, trophyConfPath).success); - lock2.unlock(); + if (g_cfg.net.psn_status == np_psn_status::psn_rpcn) + { + const SceNpCommunicationId ctx_comm_id = ctxt->comm_id; + const u32 trophy_count = tropusr->GetTrophiesCount(); + + std::vector> local_unlocked; + local_unlocked.reserve(trophy_count); + for (u32 i = 0; i < trophy_count; i++) + { + if (tropusr->GetTrophyUnlockState(static_cast(i))) + { + local_unlocked.emplace_back( + static_cast(i), + static_cast(tropusr->GetTrophyTimestamp(static_cast(i)))); + } + } + + // Release lock before the blocking network call + lock2.unlock(); + + auto& np = g_fxo->get(); + std::vector> srv_trophies = np.rpcn_trophy_sync(ctx_comm_id, local_unlocked); + + bool changed = false; + for (const auto& [tid, ts] : srv_trophies) + { + if (tid >= 0 && tid < static_cast(trophy_count) && ts >= 0 && !tropusr->GetTrophyUnlockState(tid)) + { + static_cast(tropusr->UnlockTrophy(tid, static_cast(ts), static_cast(ts))); + changed = true; + } + } + + if (changed && !tropusr->Save(trophyUsrPath)) + sceNpTrophy.error("sceNpTrophyRegisterContext(): Failed to save trophy data after RPCN sync"); + } + else + { + lock2.unlock(); + } lv2_obj::sleep(ppu); { @@ -1110,6 +1152,15 @@ error_code sceNpTrophyUnlockTrophy(ppu_thread& ppu, u32 context, u32 handle, s32 } } + if (g_cfg.net.psn_status == np_psn_status::psn_rpcn) + { + auto& np = g_fxo->get(); + np.rpcn_trophy_unlock(ctxt->comm_id, trophyId, static_cast(tick->tick)); + + if (unlocked_platinum_id != SCE_NP_TROPHY_INVALID_TROPHY_ID) + np.rpcn_trophy_unlock(ctxt->comm_id, static_cast(unlocked_platinum_id), static_cast(tick->tick)); + } + return CELL_OK; } diff --git a/rpcs3/Emu/NP/np_handler.cpp b/rpcs3/Emu/NP/np_handler.cpp index 34300ae36..c7346061a 100644 --- a/rpcs3/Emu/NP/np_handler.cpp +++ b/rpcs3/Emu/NP/np_handler.cpp @@ -1638,6 +1638,32 @@ namespace np } } + void np_handler::rpcn_trophy_unlock(const SceNpCommunicationId& communication_id, s32 trophy_id, s64 timestamp) + { + if (!is_psn_active || g_cfg.net.psn_status != np_psn_status::psn_rpcn) + return; + + std::lock_guard lock(mutex_rpcn); + if (!rpcn || !rpcn->is_authentified()) + return; + + rpcn->unlock_trophy(communication_id, trophy_id, timestamp); + } + + std::vector> np_handler::rpcn_trophy_sync( + const SceNpCommunicationId& communication_id, + const std::vector>& local_unlocked) + { + if (!is_psn_active || g_cfg.net.psn_status != np_psn_status::psn_rpcn) + return {}; + + std::lock_guard lock(mutex_rpcn); + if (!rpcn || !rpcn->is_authentified()) + return {}; + + return rpcn->sync_trophies(communication_id, local_unlocked); + } + template error_code np_handler::get_friend_presence_by_index(u32 index, SceNpUserInfo* user, T* pres) { diff --git a/rpcs3/Emu/NP/np_handler.h b/rpcs3/Emu/NP/np_handler.h index af30eb3c5..86eaca3bb 100644 --- a/rpcs3/Emu/NP/np_handler.h +++ b/rpcs3/Emu/NP/np_handler.h @@ -249,6 +249,11 @@ namespace np std::pair> get_friend_by_index(u32 index); void set_presence(std::optional status, std::optional> data); + // RPCN trophy support + void rpcn_trophy_unlock(const SceNpCommunicationId& communication_id, s32 trophy_id, s64 timestamp); + std::vector> rpcn_trophy_sync(const SceNpCommunicationId& communication_id, + const std::vector>& local_unlocked); + template error_code get_friend_presence_by_index(u32 index, SceNpUserInfo* user, T* pres); diff --git a/rpcs3/Emu/NP/rpcn_client.cpp b/rpcs3/Emu/NP/rpcn_client.cpp index 410713209..7de63afc0 100644 --- a/rpcs3/Emu/NP/rpcn_client.cpp +++ b/rpcs3/Emu/NP/rpcn_client.cpp @@ -161,6 +161,8 @@ void fmt_class_string::format(std::string& out, u64 arg) case rpcn::CommandType::GetRoomInfoGUI: return "GetRoomInfoGUI"; case rpcn::CommandType::QuickMatchGUI: return "QuickMatchGUI"; case rpcn::CommandType::SearchJoinRoomGUI: return "SearchJoinRoomGUI"; + case rpcn::CommandType::UnlockTrophy: return "UnlockTrophy"; + case rpcn::CommandType::SyncTrophies: return "SyncTrophies"; } return unknown; @@ -257,7 +259,7 @@ namespace rpcn rpcn_log.notice("online: %s, pr_com_id: %s, pr_title: %s, pr_status: %s, pr_comment: %s, pr_data: %s", online ? "true" : "false", pr_com_id.data, pr_title, pr_status, pr_comment, fmt::buf_to_hexstring(pr_data.data(), pr_data.size())); } - constexpr u32 RPCN_PROTOCOL_VERSION = 30; + constexpr u32 RPCN_PROTOCOL_VERSION = 31; constexpr usz RPCN_HEADER_SIZE = 15; const char* error_to_explanation(rpcn::ErrorType error) @@ -663,7 +665,8 @@ namespace rpcn command == CommandType::AddBlock || command == CommandType::RemoveBlock || command == CommandType::SendMessage || command == CommandType::SendToken || command == CommandType::SendResetToken || command == CommandType::ResetPassword || - command == CommandType::GetNetworkTime || command == CommandType::SetPresence || command == CommandType::Terminate) + command == CommandType::GetNetworkTime || command == CommandType::SetPresence || command == CommandType::Terminate || + command == CommandType::SyncTrophies) { std::lock_guard lock(mutex_replies_sync); replies_sync.insert(std::make_pair(packet_id, std::make_pair(command, std::move(data)))); @@ -2605,6 +2608,64 @@ namespace rpcn return forge_request_with_com_id(serialized, pr_com_id, CommandType::SetPresence, rpcn_request_counter.fetch_add(1)); } + bool rpcn_client::unlock_trophy(const SceNpCommunicationId& communication_id, s32 trophy_id, s64 timestamp) + { + std::vector data(COMMUNICATION_ID_SIZE + sizeof(s32) + sizeof(s64)); + rpcn_client::write_communication_id(communication_id, data); + reinterpret_cast&>(data[COMMUNICATION_ID_SIZE]) = trophy_id; + reinterpret_cast&>(data[COMMUNICATION_ID_SIZE + sizeof(s32)]) = timestamp; + return forge_send(CommandType::UnlockTrophy, rpcn_request_counter.fetch_add(1), data); + } + + std::vector> rpcn_client::sync_trophies( + const SceNpCommunicationId& communication_id, + const std::vector>& local_unlocked) + { + const u32 count = static_cast(local_unlocked.size()); + + std::vector data(COMMUNICATION_ID_SIZE + sizeof(u32) + count * (sizeof(s32) + sizeof(s64))), reply_data; + + rpcn_client::write_communication_id(communication_id, data); + reinterpret_cast&>(data[COMMUNICATION_ID_SIZE]) = count; + + usz offset = COMMUNICATION_ID_SIZE + sizeof(u32); + for (const auto& [tid, ts] : local_unlocked) + { + reinterpret_cast&>(data[offset]) = tid; + offset += sizeof(s32); + reinterpret_cast&>(data[offset]) = ts; + offset += sizeof(s64); + } + + if (!forge_send_reply(CommandType::SyncTrophies, rpcn_request_counter.fetch_add(1), data, reply_data)) + return {}; + + vec_stream reply(reply_data); + const auto error = static_cast(reply.get()); + if (error != rpcn::ErrorType::NoError) + { + rpcn_log.error("sync_trophies: server returned error %s", fmt::format("%s", error)); + return {}; + } + + const u32 server_count = reply.get(); + std::vector> result; + result.reserve(server_count); + for (u32 i = 0; i < server_count; i++) + { + const s32 tid = reply.get(); + const s64 ts = reply.get(); + result.emplace_back(tid, ts); + } + + if (reply.is_error()) + { + error_and_disconnect("Malformed reply to SyncTrophies command"); + return {}; + } + return result; + } + bool rpcn_client::createjoin_room_gui(u32 req_id, const SceNpCommunicationId& communication_id, const SceNpMatchingAttr* attr_list) { np2_structs::CreateRoomGUIRequest pb_req; diff --git a/rpcs3/Emu/NP/rpcn_client.h b/rpcs3/Emu/NP/rpcn_client.h index f95fb282b..cb545ade9 100644 --- a/rpcs3/Emu/NP/rpcn_client.h +++ b/rpcs3/Emu/NP/rpcn_client.h @@ -370,6 +370,8 @@ namespace rpcn bool tus_get_friends_data_status(u32 req_id, SceNpCommunicationId& communication_id, SceNpTusSlotId slotId, bool includeSelf, s32 sortType, s32 arrayNum); bool tus_delete_multislot_data(u32 req_id, SceNpCommunicationId& communication_id, const SceNpOnlineId& targetNpId, vm::cptr slotIdArray, s32 arrayNum, bool vuser); bool send_presence(const SceNpCommunicationId& pr_com_id, const std::string& pr_title, const std::string& pr_status, const std::string& pr_comment, const std::vector& pr_data); + bool unlock_trophy(const SceNpCommunicationId& communication_id, s32 trophy_id, s64 timestamp); + std::vector> sync_trophies(const SceNpCommunicationId& communication_id, const std::vector>& local_unlocked); bool createjoin_room_gui(u32 req_id, const SceNpCommunicationId& communication_id, const SceNpMatchingAttr* attr_list); bool join_room_gui(u32 req_id, const SceNpRoomId& room_id); bool leave_room_gui(u32 req_id, const SceNpRoomId& room_id); diff --git a/rpcs3/Emu/NP/rpcn_types.h b/rpcs3/Emu/NP/rpcn_types.h index 28500462e..0a4a281e3 100644 --- a/rpcs3/Emu/NP/rpcn_types.h +++ b/rpcs3/Emu/NP/rpcn_types.h @@ -69,6 +69,8 @@ namespace rpcn QuickMatchGUI, SearchJoinRoomGUI, GetRoomMemberDataExternalList, + UnlockTrophy, + SyncTrophies, }; enum class NotificationType : u16 diff --git a/rpcs3/Emu/RSX/Overlays/Trophies/overlay_trophy_list_dialog.cpp b/rpcs3/Emu/RSX/Overlays/Trophies/overlay_trophy_list_dialog.cpp index 9fc90fcbe..32d8ae172 100644 --- a/rpcs3/Emu/RSX/Overlays/Trophies/overlay_trophy_list_dialog.cpp +++ b/rpcs3/Emu/RSX/Overlays/Trophies/overlay_trophy_list_dialog.cpp @@ -2,6 +2,7 @@ #include "../overlay_manager.h" #include "overlay_trophy_list_dialog.h" #include "Emu/Cell/Modules/sceNpTrophy.h" +#include "Emu/NP/rpcn_config.h" #include "Emu/System.h" #include "Emu/VFS.h" @@ -107,7 +108,7 @@ namespace rsx m_sort_button->set_text(localized_string_id::HOME_MENU_TROPHY_SORT_GAME_DEFAULT); m_sort_button->set_image_resource(resource_config::standard_image_resource::triangle); m_sort_button->set_size(120, 30); - m_sort_button->set_pos(560, trophy_list_y + trophy_list_h + 20); + m_sort_button->set_pos(460, trophy_list_y + trophy_list_h + 20); m_sort_button->set_font("Arial", 16); m_show_hidden_trophies_button = std::make_unique(); @@ -117,6 +118,13 @@ namespace rsx m_show_hidden_trophies_button->set_pos(180, trophy_list_y + trophy_list_h + 20); m_show_hidden_trophies_button->set_font("Arial", 16); + m_sync_trophies_button = std::make_unique(); + m_sync_trophies_button->set_text(localized_string_id::HOME_MENU_TROPHY_SYNC_TROPHIES); + m_sync_trophies_button->set_image_resource(resource_config::standard_image_resource::select); + m_sync_trophies_button->set_size(120, 30); + m_sync_trophies_button->set_pos(700, trophy_list_y + trophy_list_h + 20); + m_sync_trophies_button->set_font("Arial", 16); + fade_animation.duration_sec = 0.15f; return_code = selection_code::canceled; @@ -159,6 +167,13 @@ namespace rsx m_list_dirty = true; break; } + case pad_button::select: + // Only start a new sync if not already in progress + if (m_sync_status.load() != 1) + { + sync_trophies_async(); + } + break; case pad_button::dpad_up: case pad_button::ls_up: m_list->select_previous(); @@ -209,6 +224,15 @@ namespace rsx m_show_hidden_trophies_last = m_show_hidden_trophies; } + // Update sync button label based on current sync state + switch (m_sync_status.load()) + { + case 1: m_sync_trophies_button->set_text(localized_string_id::HOME_MENU_TROPHY_SYNCING_TROPHIES); break; + case 2: m_sync_trophies_button->set_text(localized_string_id::HOME_MENU_TROPHY_SYNC_SUCCESS); break; + case 3: m_sync_trophies_button->set_text(localized_string_id::HOME_MENU_TROPHY_SYNC_FAILED); break; + default: m_sync_trophies_button->set_text(localized_string_id::HOME_MENU_TROPHY_SYNC_TROPHIES); break; + } + if (m_sort_mode_last != m_sort_mode) { localized_string_id sort_label_id; @@ -236,6 +260,7 @@ namespace rsx result.add(m_description->get_compiled()); result.add(m_sort_button->get_compiled()); result.add(m_show_hidden_trophies_button->get_compiled()); + result.add(m_sync_trophies_button->get_compiled()); fade_animation.apply(result); @@ -245,7 +270,8 @@ namespace rsx void trophy_list_dialog::show(const std::string& trop_name) { visible = false; - + + m_trop_name = trop_name; m_trophy_data = load_trophies(trop_name); ensure(m_trophy_data && m_trophy_data->trop_usr); @@ -269,6 +295,126 @@ namespace rsx } } + bool trophy_list_dialog::rpcn_configured() + { + cfg_rpcn cfg; + cfg.load(); + return !cfg.get_npid().empty() && !cfg.get_password().empty(); + } + + void trophy_list_dialog::sync_trophies_async() + { + if (!rpcn_configured()) + { + rsx_log.warning("Trophy sync requested but RPCN is not configured."); + m_sync_status = 3; // error + return; + } + + if (!m_trophy_data || !m_trophy_data->trop_usr) + { + rsx_log.error("Trophy sync requested but trophy data is not loaded."); + m_sync_status = 3; + return; + } + + m_sync_status = 1; // syncing + + const std::string trop_name = m_trop_name; + atomic_t* sync_status = &m_sync_status; + atomic_t* list_dirty = &m_list_dirty; + + SceNpCommunicationId comm_id{}; + { + if (trop_name.size() >= COMMUNICATION_ID_SIZE) + { + const auto& n = trop_name; + std::memcpy(comm_id.data, n.c_str(), COMMUNICATION_ID_COMID_COMPONENT_SIZE); + comm_id.data[COMMUNICATION_ID_COMID_COMPONENT_SIZE] = '\0'; + comm_id.num = static_cast(std::atoi(n.c_str() + COMMUNICATION_ID_COMID_COMPONENT_SIZE + 1)); + } + else + { + rsx_log.error("Trophy sync: unexpected trop_name format: %s", trop_name); + m_sync_status = 3; + return; + } + } + + const u32 trophy_count = m_trophy_data->trop_usr->GetTrophiesCount(); + std::vector> local_unlocked; + local_unlocked.reserve(trophy_count); + for (u32 i = 0; i < trophy_count; i++) + { + if (m_trophy_data->trop_usr->GetTrophyUnlockState(static_cast(i))) + { + local_unlocked.emplace_back( + static_cast(i), + static_cast(m_trophy_data->trop_usr->GetTrophyTimestamp(static_cast(i)))); + } + } + + const std::string tropusr_vfs_path = "/dev_hdd0/home/" + Emu.GetUsr() + "/trophy/" + trop_name + "/TROPUSR.DAT"; + const std::string tropconf_vfs_path = "/dev_hdd0/home/" + Emu.GetUsr() + "/trophy/" + trop_name + "/TROPCONF.SFM"; + + std::thread([=, local_unlocked = std::move(local_unlocked)]() mutable + { + g_cfg_rpcn.load(); + + auto rpcn = rpcn::rpcn_client::get_instance(0); + + if (auto res = rpcn->wait_for_connection(); res != rpcn::rpcn_state::failure_no_failure) + { + rsx_log.error("Trophy sync: failed to connect to RPCN: %s", rpcn::rpcn_state_to_string(res)); + *sync_status = 3; + return; + } + + if (auto res = rpcn->wait_for_authentified(); res != rpcn::rpcn_state::failure_no_failure) + { + rsx_log.error("Trophy sync: failed to authenticate with RPCN: %s", rpcn::rpcn_state_to_string(res)); + *sync_status = 3; + return; + } + + std::vector> srv_trophies = rpcn->sync_trophies(comm_id, local_unlocked); + + if (!srv_trophies.empty()) + { + auto tropusr = std::make_unique(); + if (tropusr->Load(tropusr_vfs_path, tropconf_vfs_path).success) + { + const u32 count = tropusr->GetTrophiesCount(); + bool changed = false; + for (const auto& [tid, ts] : srv_trophies) + { + if (tid >= 0 && tid < static_cast(count) && ts >= 0 && !tropusr->GetTrophyUnlockState(tid)) + { + (void)tropusr->UnlockTrophy(tid, static_cast(ts), static_cast(ts)); + changed = true; + } + } + + if (changed) + { + if (!tropusr->Save(tropusr_vfs_path)) + rsx_log.error("Trophy sync: failed to save TROPUSR after sync for %s", trop_name); + + *list_dirty = true; + } + } + else + { + rsx_log.error("Trophy sync: failed to reload TROPUSR for %s", trop_name); + *sync_status = 3; + return; + } + } + + *sync_status = 2; // success + }).detach(); + } + std::unique_ptr trophy_list_dialog::load_trophies(const std::string& trop_name) const { // Populate GameTrophiesData @@ -344,6 +490,15 @@ namespace rsx { ensure(m_trophy_data); + if (!m_trop_name.empty()) + { + const std::string tropusr_path = "/dev_hdd0/home/" + Emu.GetUsr() + "/trophy/" + m_trop_name + "/TROPUSR.DAT"; + const std::string tropconf_path = "/dev_hdd0/home/" + Emu.GetUsr() + "/trophy/" + m_trop_name + "/TROPCONF.SFM"; + auto fresh_usr = std::make_unique(); + if (fresh_usr->Load(tropusr_path, tropconf_path).success) + m_trophy_data->trop_usr = std::move(fresh_usr); + } + rsx_log.trace("Reloading Trophy List Overlay with %s %s", m_trophy_data->game_name, m_trophy_data->path); s32 selected_index = m_list ? m_list->get_selected_index() : 0; diff --git a/rpcs3/Emu/RSX/Overlays/Trophies/overlay_trophy_list_dialog.h b/rpcs3/Emu/RSX/Overlays/Trophies/overlay_trophy_list_dialog.h index c6ec34750..a5b3b61d7 100644 --- a/rpcs3/Emu/RSX/Overlays/Trophies/overlay_trophy_list_dialog.h +++ b/rpcs3/Emu/RSX/Overlays/Trophies/overlay_trophy_list_dialog.h @@ -4,6 +4,7 @@ #include "../overlay_list_view.hpp" #include "Loader/TROPUSR.h" +#include "Emu/NP/rpcn_client.h" class TROPUSRLoader; @@ -33,22 +34,28 @@ namespace rsx private: std::unique_ptr load_trophies(const std::string& trop_name) const; void reload(); + void sync_trophies_async(); std::unique_ptr m_dim_background; std::unique_ptr m_list; std::unique_ptr