From 392c86ad1ad6d397193e8ff7d5cebfe3bfff8947 Mon Sep 17 00:00:00 2001 From: FlexBy420 Date: Mon, 18 May 2026 15:20:03 +0200 Subject: [PATCH 1/7] sync trophies with rpcn --- rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp | 51 +++++++++++++++++++- rpcs3/Emu/NP/np_handler.cpp | 28 ++++++++++- rpcs3/Emu/NP/np_handler.h | 7 ++- rpcs3/Emu/NP/rpcn_client.cpp | 64 +++++++++++++++++++++++++- rpcs3/Emu/NP/rpcn_client.h | 4 +- rpcs3/Emu/NP/rpcn_types.h | 4 +- 6 files changed, 150 insertions(+), 8 deletions(-) diff --git a/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp b/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp index 866db860ec..46699d7f09 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,42 @@ 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), u64{0}); + } + + // Release lock before the blocking network call + lock2.unlock(); + + auto& np = g_fxo->get(); + const auto 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) && !tropusr->GetTrophyUnlockState(tid)) + { + tropusr->UnlockTrophy(tid, ts, 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 +1148,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, tick->tick); + + if (unlocked_platinum_id != SCE_NP_TROPHY_INVALID_TROPHY_ID) + np.rpcn_trophy_unlock(ctxt->comm_id, static_cast(unlocked_platinum_id), tick->tick); + } + return CELL_OK; } @@ -1559,4 +1606,4 @@ DECLARE(ppu_module_manager::sceNpTrophy)("sceNpTrophy", []() REG_FUNC(sceNpTrophy, sceNpTrophyGetTrophyInfo); REG_FUNC(sceNpTrophy, sceNpTrophyGetGameIcon); REG_FUNC(sceNpTrophy, sceNpTrophyNetworkSync); -}); +}); \ No newline at end of file diff --git a/rpcs3/Emu/NP/np_handler.cpp b/rpcs3/Emu/NP/np_handler.cpp index 0d628e299e..d0a0eb2bbf 100644 --- a/rpcs3/Emu/NP/np_handler.cpp +++ b/rpcs3/Emu/NP/np_handler.cpp @@ -1609,6 +1609,32 @@ namespace np } } + void np_handler::rpcn_trophy_unlock(const SceNpCommunicationId& communication_id, s32 trophy_id, u64 timestamp) + { + if (!is_psn_active || g_cfg.net.psn_status != np_psn_status::psn_rpcn) + return; + + std::lock_guard lock(mutex_rpcn); + if (!rpcn) + 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) + 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) { @@ -1794,4 +1820,4 @@ namespace np return mem_info; } -} // namespace np +} // namespace np \ No newline at end of file diff --git a/rpcs3/Emu/NP/np_handler.h b/rpcs3/Emu/NP/np_handler.h index 575fe68941..0b073102e3 100644 --- a/rpcs3/Emu/NP/np_handler.h +++ b/rpcs3/Emu/NP/np_handler.h @@ -248,6 +248,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, u64 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); @@ -535,4 +540,4 @@ namespace np shared_mutex mutex_quickmatching; std::map pending_quickmatching; }; -} // namespace np +} // namespace np \ No newline at end of file diff --git a/rpcs3/Emu/NP/rpcn_client.cpp b/rpcs3/Emu/NP/rpcn_client.cpp index 96ab505abf..455c3294e1 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; @@ -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,63 @@ 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, u64 timestamp) + { + std::vector data(COMMUNICATION_ID_SIZE + sizeof(s32) + sizeof(u64)); + 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(u64))), 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(u64); + } + + 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 u64 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; @@ -3345,4 +3405,4 @@ namespace rpcn local_addr_sig = std::bit_cast>(addr); } -} // namespace rpcn +} // namespace rpcn \ No newline at end of file diff --git a/rpcs3/Emu/NP/rpcn_client.h b/rpcs3/Emu/NP/rpcn_client.h index daa60d90e7..8d7ca91580 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, u64 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); @@ -465,4 +467,4 @@ namespace rpcn static constexpr int RPCN_TIMEOUT = 10'000; // 10s }; -} // namespace rpcn +} // namespace rpcn \ No newline at end of file diff --git a/rpcs3/Emu/NP/rpcn_types.h b/rpcs3/Emu/NP/rpcn_types.h index 28500462e6..008b76bb97 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 @@ -156,4 +158,4 @@ namespace rpcn CondFail, // Condition related to query failed Unsupported, }; -} // namespace rpcn +} // namespace rpcn \ No newline at end of file From cb66e1d793b4fb74609aaa5c4acece943c9b017d Mon Sep 17 00:00:00 2001 From: FlexBy420 Date: Mon, 18 May 2026 16:35:37 +0200 Subject: [PATCH 2/7] dont sync 0 unlocked trophies --- rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp | 6 +++++- rpcs3/Emu/NP/rpcn_client.cpp | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp b/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp index 46699d7f09..f70cf25a69 100644 --- a/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp +++ b/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp @@ -734,7 +734,11 @@ error_code sceNpTrophyRegisterContext(ppu_thread& ppu, u32 context, u32 handle, lock2.unlock(); auto& np = g_fxo->get(); - const auto srv_trophies = np.rpcn_trophy_sync(ctx_comm_id, local_unlocked); + + // Skip the network sync entirely when the user has no unlocked trophies + std::vector> srv_trophies; + if (!local_unlocked.empty()) + srv_trophies = np.rpcn_trophy_sync(ctx_comm_id, local_unlocked); bool changed = false; for (const auto& [tid, ts] : srv_trophies) diff --git a/rpcs3/Emu/NP/rpcn_client.cpp b/rpcs3/Emu/NP/rpcn_client.cpp index 455c3294e1..0164789d3f 100644 --- a/rpcs3/Emu/NP/rpcn_client.cpp +++ b/rpcs3/Emu/NP/rpcn_client.cpp @@ -2622,6 +2622,9 @@ namespace rpcn const std::vector>& local_unlocked) { const u32 count = static_cast(local_unlocked.size()); + + if (count == 0) + return {}; std::vector data(COMMUNICATION_ID_SIZE + sizeof(u32) + count * (sizeof(s32) + sizeof(u64))), reply_data; rpcn_client::write_communication_id(communication_id, data); From c1e2e09d0e48880e90f9573a4a69002132aa4e8e Mon Sep 17 00:00:00 2001 From: FlexBy420 Date: Mon, 18 May 2026 17:10:48 +0200 Subject: [PATCH 3/7] Update np_handler.cpp --- rpcs3/Emu/NP/np_handler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rpcs3/Emu/NP/np_handler.cpp b/rpcs3/Emu/NP/np_handler.cpp index d0a0eb2bbf..a86c0934ea 100644 --- a/rpcs3/Emu/NP/np_handler.cpp +++ b/rpcs3/Emu/NP/np_handler.cpp @@ -1615,7 +1615,7 @@ namespace np return; std::lock_guard lock(mutex_rpcn); - if (!rpcn) + if (!rpcn || !rpcn->is_authentified()) return; rpcn->unlock_trophy(communication_id, trophy_id, timestamp); @@ -1629,7 +1629,7 @@ namespace np return {}; std::lock_guard lock(mutex_rpcn); - if (!rpcn) + if (!rpcn || !rpcn->is_authentified()) return {}; return rpcn->sync_trophies(communication_id, local_unlocked); From 55041125e56c9ac0b5f8d4ab80e372ebef4d6046 Mon Sep 17 00:00:00 2001 From: FlexBy420 Date: Mon, 18 May 2026 19:15:19 +0200 Subject: [PATCH 4/7] resolve --- rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp | 2 +- rpcs3/Emu/NP/np_handler.cpp | 2 +- rpcs3/Emu/NP/np_handler.h | 2 +- rpcs3/Emu/NP/rpcn_client.cpp | 2 +- rpcs3/Emu/NP/rpcn_client.h | 2 +- rpcs3/Emu/NP/rpcn_types.h | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp b/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp index f70cf25a69..525bf0ec3c 100644 --- a/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp +++ b/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp @@ -1610,4 +1610,4 @@ DECLARE(ppu_module_manager::sceNpTrophy)("sceNpTrophy", []() REG_FUNC(sceNpTrophy, sceNpTrophyGetTrophyInfo); REG_FUNC(sceNpTrophy, sceNpTrophyGetGameIcon); REG_FUNC(sceNpTrophy, sceNpTrophyNetworkSync); -}); \ No newline at end of file +}); diff --git a/rpcs3/Emu/NP/np_handler.cpp b/rpcs3/Emu/NP/np_handler.cpp index a86c0934ea..f8cf2a2f03 100644 --- a/rpcs3/Emu/NP/np_handler.cpp +++ b/rpcs3/Emu/NP/np_handler.cpp @@ -1820,4 +1820,4 @@ namespace np return mem_info; } -} // namespace np \ No newline at end of file +} // namespace np diff --git a/rpcs3/Emu/NP/np_handler.h b/rpcs3/Emu/NP/np_handler.h index 0b073102e3..64badec071 100644 --- a/rpcs3/Emu/NP/np_handler.h +++ b/rpcs3/Emu/NP/np_handler.h @@ -540,4 +540,4 @@ namespace np shared_mutex mutex_quickmatching; std::map pending_quickmatching; }; -} // namespace np \ No newline at end of file +} // namespace np diff --git a/rpcs3/Emu/NP/rpcn_client.cpp b/rpcs3/Emu/NP/rpcn_client.cpp index 0164789d3f..f6243cd2e8 100644 --- a/rpcs3/Emu/NP/rpcn_client.cpp +++ b/rpcs3/Emu/NP/rpcn_client.cpp @@ -3408,4 +3408,4 @@ namespace rpcn local_addr_sig = std::bit_cast>(addr); } -} // namespace rpcn \ No newline at end of file +} // namespace rpcn diff --git a/rpcs3/Emu/NP/rpcn_client.h b/rpcs3/Emu/NP/rpcn_client.h index 8d7ca91580..a41a03e72a 100644 --- a/rpcs3/Emu/NP/rpcn_client.h +++ b/rpcs3/Emu/NP/rpcn_client.h @@ -467,4 +467,4 @@ namespace rpcn static constexpr int RPCN_TIMEOUT = 10'000; // 10s }; -} // namespace rpcn \ No newline at end of file +} // namespace rpcn diff --git a/rpcs3/Emu/NP/rpcn_types.h b/rpcs3/Emu/NP/rpcn_types.h index 008b76bb97..0a4a281e3c 100644 --- a/rpcs3/Emu/NP/rpcn_types.h +++ b/rpcs3/Emu/NP/rpcn_types.h @@ -158,4 +158,4 @@ namespace rpcn CondFail, // Condition related to query failed Unsupported, }; -} // namespace rpcn \ No newline at end of file +} // namespace rpcn From df9b8baab3ab5bd0f957206c9f4520ff28deb5af Mon Sep 17 00:00:00 2001 From: FlexBy420 Date: Fri, 22 May 2026 13:30:51 +0200 Subject: [PATCH 5/7] sync trophies even if 0 local --- rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp | 6 +----- rpcs3/Emu/NP/rpcn_client.cpp | 2 -- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp b/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp index 525bf0ec3c..9ea23ec6dd 100644 --- a/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp +++ b/rpcs3/Emu/Cell/Modules/sceNpTrophy.cpp @@ -734,11 +734,7 @@ error_code sceNpTrophyRegisterContext(ppu_thread& ppu, u32 context, u32 handle, lock2.unlock(); auto& np = g_fxo->get(); - - // Skip the network sync entirely when the user has no unlocked trophies - std::vector> srv_trophies; - if (!local_unlocked.empty()) - srv_trophies = np.rpcn_trophy_sync(ctx_comm_id, local_unlocked); + std::vector> srv_trophies = np.rpcn_trophy_sync(ctx_comm_id, local_unlocked); bool changed = false; for (const auto& [tid, ts] : srv_trophies) diff --git a/rpcs3/Emu/NP/rpcn_client.cpp b/rpcs3/Emu/NP/rpcn_client.cpp index f6243cd2e8..b81aa40765 100644 --- a/rpcs3/Emu/NP/rpcn_client.cpp +++ b/rpcs3/Emu/NP/rpcn_client.cpp @@ -2623,8 +2623,6 @@ namespace rpcn { const u32 count = static_cast(local_unlocked.size()); - if (count == 0) - return {}; std::vector data(COMMUNICATION_ID_SIZE + sizeof(u32) + count * (sizeof(s32) + sizeof(u64))), reply_data; rpcn_client::write_communication_id(communication_id, data); From 753756001628a2737f6688585c0ab26af4262188 Mon Sep 17 00:00:00 2001 From: FlexBy420 Date: Fri, 5 Jun 2026 15:38:46 +0200 Subject: [PATCH 6/7] add sync button in trophy overlay --- .../Trophies/overlay_trophy_list_dialog.cpp | 154 +++++++++++++++++- .../Trophies/overlay_trophy_list_dialog.h | 9 + rpcs3/Emu/localized_string_id.h | 4 + rpcs3/rpcs3qt/localized_emu.h | 4 + 4 files changed, 170 insertions(+), 1 deletion(-) 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 ea47ad80b6..dba5555eb1 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" @@ -110,6 +111,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::triangle); + m_sync_trophies_button->set_size(120, 30); + m_sync_trophies_button->set_pos(460, 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; @@ -139,6 +147,14 @@ namespace rsx m_show_hidden_trophies = !m_show_hidden_trophies; m_list_dirty = true; break; + case pad_button::triangle: + // Only start a new sync if not already in progress + if (m_sync_status.load() != 1) + { + play_sound(sound_effect::cursor); + sync_trophies_async(); + } + break; case pad_button::dpad_up: case pad_button::ls_up: m_list->select_previous(); @@ -189,6 +205,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; + } + compiled_resource result; result.add(m_dim_background->get_compiled()); if (m_list_dirty.exchange(false)) @@ -201,6 +226,7 @@ namespace rsx } result.add(m_description->get_compiled()); result.add(m_show_hidden_trophies_button->get_compiled()); + result.add(m_sync_trophies_button->get_compiled()); fade_animation.apply(result); @@ -210,7 +236,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); @@ -234,6 +261,122 @@ 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), u64{0}); + } + + 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) && !tropusr->GetTrophyUnlockState(tid)) + { + (void)tropusr->UnlockTrophy(tid, ts, 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 @@ -309,6 +452,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); std::string selected_trophy; 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 62be384817..c28de60ebb 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; @@ -25,19 +26,25 @@ 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