diff --git a/rpcs3/Emu/Cell/Modules/sceNp.cpp b/rpcs3/Emu/Cell/Modules/sceNp.cpp index 0f0949ffc1..d3e333c61c 100644 --- a/rpcs3/Emu/Cell/Modules/sceNp.cpp +++ b/rpcs3/Emu/Cell/Modules/sceNp.cpp @@ -1814,7 +1814,7 @@ error_code sceNpBasicGetFriendPresenceByNpId2(vm::cptr npid, vm::ptr npid, vm::ptr description) +error_code sceNpBasicAddPlayersHistory(vm::cptr npid, vm::cptr description) { sceNp.todo("sceNpBasicAddPlayersHistory(npid=*0x%x, description=*0x%x)", npid, description); @@ -1835,10 +1835,12 @@ error_code sceNpBasicAddPlayersHistory(vm::cptr npid, vm::ptr des return SCE_NP_BASIC_ERROR_EXCEEDS_MAX; } + nph.add_player_to_history(npid.get_ptr(), description ? description.get_ptr() : nullptr); + return CELL_OK; } -error_code sceNpBasicAddPlayersHistoryAsync(vm::cptr npids, u32 count, vm::ptr description, vm::ptr reqId) +error_code sceNpBasicAddPlayersHistoryAsync(vm::cptr npids, u32 count, vm::cptr description, vm::ptr reqId) { sceNp.todo("sceNpBasicAddPlayersHistoryAsync(npids=*0x%x, count=%d, description=*0x%x, reqId=*0x%x)", npids, count, description, reqId); @@ -1877,7 +1879,7 @@ error_code sceNpBasicAddPlayersHistoryAsync(vm::cptr npids, u32 count, return SCE_NP_BASIC_ERROR_EXCEEDS_MAX; } - auto req_id = nph.add_players_to_history(npids, count); + auto req_id = nph.add_players_to_history(npids.get_ptr(), description ? description.get_ptr() : nullptr, count); if (reqId) { @@ -1919,8 +1921,7 @@ error_code sceNpBasicGetPlayersHistoryEntryCount(u32 options, vm::ptr count return SCE_NP_ERROR_ID_NOT_FOUND; } - // TODO: Check if there are players histories - *count = 0; + *count = nph.get_players_history_count(options); return CELL_OK; } @@ -1958,6 +1959,11 @@ error_code sceNpBasicGetPlayersHistoryEntry(u32 options, u32 index, vm::ptr @@ -55,6 +56,63 @@ LOG_CHANNEL(ticket_log, "Ticket"); namespace np { + std::string get_players_history_path() + { +#ifdef _WIN32 + return fs::get_config_dir() + "config/players_history.yml"; +#else + return fs::get_config_dir() + "players_history.yml"; +#endif + } + + std::map load_players_history() + { + const auto parsing_error = [](std::string_view error) -> std::map + { + nph_log.error("Error parsing %s: %s", get_players_history_path(), error); + return {}; + }; + + std::map history; + + if (fs::file history_file{get_players_history_path(), fs::read + fs::create}) + { + auto [yml_players_history, error] = yaml_load(history_file.to_string()); + + if (!error.empty()) + return parsing_error(error); + + for (const auto& player : yml_players_history) + { + std::string username = player.first.Scalar(); + const auto& seq = player.second; + + if (!seq.IsSequence() || seq.size() != 3) + return parsing_error("Player history is not a proper sequence!"); + + const u64 timestamp = get_yaml_node_value(seq[0], error); + if (!error.empty()) + return parsing_error(error); + + std::string description = seq[1].Scalar(); + + if (!seq[2].IsSequence()) + return parsing_error("Expected communication ids sequence"); + + std::set com_ids; + + for (usz i = 0; i < seq[2].size(); i++) + { + com_ids.insert(seq[2][i].Scalar()); + } + + history.insert(std::make_pair(std::move(username), player_history{.timestamp = timestamp, .communication_ids = std::move(com_ids), .description = std::move(description)})); + } + } + + return history; + } + ticket::ticket(std::vector&& raw_data) : raw_data(raw_data) { @@ -363,6 +421,13 @@ namespace np np_handler::np_handler() { + { + auto history = load_players_history(); + + std::lock_guard lock(mutex_history); + players_history = std::move(history); + } + g_fxo->need>(); is_connected = (g_cfg.net.net_active == np_internet_status::enabled); @@ -1217,13 +1282,169 @@ namespace np return ::at32(match2_req_results, event_key); } - u32 np_handler::add_players_to_history(vm::cptr /*npids*/, u32 /*count*/) + player_history& np_handler::get_player_and_set_timestamp(const SceNpId& npid, u64 timestamp) { + std::string npid_str = std::string(npid.handle.data); + + if (!players_history.contains(npid_str)) + { + auto [it, success] = players_history.insert(std::make_pair(std::move(npid_str), player_history{.timestamp = timestamp})); + ensure(success); + return it->second; + } + + auto& history = ::at32(players_history, npid_str); + history.timestamp = timestamp; + return history; + } + + constexpr usz MAX_HISTORY_ENTRIES = 200; + + void np_handler::add_player_to_history(const SceNpId* npid, const char* description) + { + std::lock_guard lock(mutex_history); + auto& history = get_player_and_set_timestamp(*npid, get_system_time()); + + if (description) + history.description = description; + + while (players_history.size() > MAX_HISTORY_ENTRIES) + { + auto it = std::min_element(players_history.begin(), players_history.end(), [](const auto& a, const auto& b) { return a.second.timestamp < b.second.timestamp; } ); + players_history.erase(it); + } + + save_players_history(); + } + + u32 np_handler::add_players_to_history(const SceNpId* npids, const char* description, u32 count) + { + std::lock_guard lock(mutex_history); + + const std::string communication_id_str = std::string(basic_handler.context.data); + + for (u32 i = 0; i < count; i++) + { + auto& history = get_player_and_set_timestamp(npids[i], get_system_time()); + + if (description) + history.description = description; + + history.communication_ids.insert(communication_id_str); + } + + while (players_history.size() > MAX_HISTORY_ENTRIES) + { + auto it = std::min_element(players_history.begin(), players_history.end(), [](const auto& a, const auto& b) { return a.second.timestamp < b.second.timestamp; } ); + players_history.erase(it); + } + + save_players_history(); + const u32 req_id = get_req_id(REQUEST_ID_HIGH::MISC); send_basic_event(SCE_NP_BASIC_EVENT_ADD_PLAYERS_HISTORY_RESULT, 0, req_id); return req_id; } + u32 np_handler::get_players_history_count(u32 options) + { + const bool all_history = (options == SCE_NP_BASIC_PLAYERS_HISTORY_OPTIONS_ALL); + + std::lock_guard lock(mutex_history); + + if (all_history) + { + return ::size32(players_history); + } + + const std::string communication_id_str = std::string(basic_handler.context.data); + u32 count = 0; + + for (auto it = players_history.begin(); it != players_history.end(); it++) + { + if (it->second.communication_ids.contains(communication_id_str)) + { + count++; + } + } + + return count; + } + + bool np_handler::get_player_history_entry(u32 options, u32 index, SceNpId* npid) + { + const bool all_history = (options == SCE_NP_BASIC_PLAYERS_HISTORY_OPTIONS_ALL); + + std::lock_guard lock(mutex_history); + + if (all_history) + { + auto it = players_history.begin(); + std::advance(it, index); + + if (it != players_history.end()) + { + string_to_npid(it->first, *npid); + return true; + } + } + else + { + const std::string communication_id_str = std::string(basic_handler.context.data); + + for (auto it = players_history.begin(); it != players_history.end(); it++) + { + if (it->second.communication_ids.contains(communication_id_str)) + { + if (index == 0) + { + string_to_npid(it->first, *npid); + return true; + } + + index--; + } + } + } + + return false; + } + + void np_handler::save_players_history() + { +#ifdef _WIN32 + const std::string path_to_cfg = fs::get_config_dir() + "config/"; + if (!fs::create_path(path_to_cfg)) + { + nph_log.error("Could not create path: %s", path_to_cfg); + } +#endif + fs::file history_file(get_players_history_path(), fs::rewrite); + if (!history_file) + return; + + YAML::Emitter out; + + out << YAML::BeginMap; + for (const auto& [player_npid, player_info] : players_history) + { + out << player_npid; + out << YAML::BeginSeq; + out << player_info.timestamp; + out << player_info.description; + out << YAML::BeginSeq; + for (const auto& com_id : player_info.communication_ids) + { + out << com_id; + } + out << YAML::EndSeq; + out << YAML::EndSeq; + } + out << YAML::EndMap; + + history_file.write(out.c_str(), out.size()); + } + u32 np_handler::get_num_friends() { return get_rpcn()->get_num_friends(); diff --git a/rpcs3/Emu/NP/np_handler.h b/rpcs3/Emu/NP/np_handler.h index 26cabaf9f9..2c2075331f 100644 --- a/rpcs3/Emu/NP/np_handler.h +++ b/rpcs3/Emu/NP/np_handler.h @@ -40,6 +40,13 @@ namespace np } data; }; + struct player_history + { + u64 timestamp; + std::set communication_ids; + std::string description; + }; + class ticket { public: @@ -215,7 +222,10 @@ namespace np // Misc stuff void req_ticket(u32 version, const SceNpId* npid, const char* service_id, const u8* cookie, u32 cookie_size, const char* entitlement_id, u32 consumed_count); const ticket& get_ticket() const; - u32 add_players_to_history(vm::cptr npids, u32 count); + void add_player_to_history(const SceNpId* npid, const char* description); + u32 add_players_to_history(const SceNpId* npids, const char* description, u32 count); + u32 get_players_history_count(u32 options); + bool get_player_history_entry(u32 options, u32 index, SceNpId* npid); bool abort_request(u32 req_id); // For signaling @@ -346,6 +356,7 @@ namespace np u32 generate_callback_info(SceNpMatching2ContextId ctx_id, vm::cptr optParam, SceNpMatching2Event event_type); std::optional take_pending_request(u32 req_id); + private: shared_mutex mutex_pending_requests; std::unordered_map pending_requests; shared_mutex mutex_pending_sign_infos_requests; @@ -356,7 +367,6 @@ namespace np bool m_inited_np_handler_dependencies = false; - private: // Basic event handler; struct { @@ -441,5 +451,11 @@ namespace np std::string pr_comment; std::vector pr_data; } presence_self; + + player_history& get_player_and_set_timestamp(const SceNpId& npid, u64 timestamp); + void save_players_history(); + + shared_mutex mutex_history; + std::map players_history; // npid / history }; } // namespace np diff --git a/rpcs3/rpcs3qt/rpcn_settings_dialog.cpp b/rpcs3/rpcs3qt/rpcn_settings_dialog.cpp index c45588b867..f4c9fbf99b 100644 --- a/rpcs3/rpcs3qt/rpcn_settings_dialog.cpp +++ b/rpcs3/rpcs3qt/rpcn_settings_dialog.cpp @@ -884,6 +884,19 @@ void friend_callback(void* param, rpcn::NotificationType ntype, const std::strin dlg->callback_handler(ntype, username, status); } +// Avoid including np_handler.h +namespace np +{ + struct player_history + { + u64 timestamp; + std::set communication_ids; + std::string description; + }; + + std::map load_players_history(); +} + rpcn_friends_dialog::rpcn_friends_dialog(QWidget* parent) : QDialog(parent), m_green_icon(gui::utils::circle_pixmap(QColorConstants::Svg::green, devicePixelRatioF() * 2)), @@ -944,6 +957,14 @@ rpcn_friends_dialog::rpcn_friends_dialog(QWidget* parent) grp_list_blocks->setLayout(vbox_lst_blocks); hbox_groupboxes->addWidget(grp_list_blocks); + QGroupBox* grp_list_history = new QGroupBox(tr("Recent Players")); + QVBoxLayout* vbox_lst_history = new QVBoxLayout(); + m_lst_history = new QListWidget(this); + m_lst_history->setContextMenuPolicy(Qt::CustomContextMenu); + vbox_lst_history->addWidget(m_lst_history); + grp_list_history->setLayout(vbox_lst_history); + hbox_groupboxes->addWidget(grp_list_history); + vbox_global->addLayout(hbox_groupboxes); setLayout(vbox_global); @@ -990,6 +1011,20 @@ rpcn_friends_dialog::rpcn_friends_dialog(QWidget* parent) add_update_list(m_lst_blocks, QString::fromStdString(blck), m_red_icon, QVariant(false)); } + auto history = np::load_players_history(); + std::map> sorted_history; + + for (const auto& [username, user_info] : history) + { + if (!data.friends.contains(username) && !data.requests_sent.contains(username) && !data.requests_received.contains(username)) + sorted_history.insert(std::make_pair(user_info.timestamp, std::move(username))); + } + + for (const auto& [_, username] : sorted_history) + { + m_lst_history->addItem(new QListWidgetItem(QString::fromStdString(username))); + } + connect(this, &rpcn_friends_dialog::signal_add_update_friend, this, &rpcn_friends_dialog::add_update_friend); connect(this, &rpcn_friends_dialog::signal_remove_friend, this, &rpcn_friends_dialog::remove_friend); connect(this, &rpcn_friends_dialog::signal_add_query, this, &rpcn_friends_dialog::add_query); @@ -1041,9 +1076,9 @@ rpcn_friends_dialog::rpcn_friends_dialog(QWidget* parent) std::string str_sel_friend = selected_item->text().toStdString(); QMenu* context_menu = new QMenu(); - QAction* remove_friend_action = context_menu->addAction(tr("&Accept Request")); + QAction* accept_request_action = context_menu->addAction(tr("&Accept Request")); - connect(remove_friend_action, &QAction::triggered, this, [this, str_sel_friend]() + connect(accept_request_action, &QAction::triggered, this, [this, str_sel_friend]() { if (!m_rpcn->add_friend(str_sel_friend)) { @@ -1059,6 +1094,38 @@ rpcn_friends_dialog::rpcn_friends_dialog(QWidget* parent) context_menu->deleteLater(); }); + connect(m_lst_history, &QListWidget::customContextMenuRequested, this, [this](const QPoint& pos) + { + if (!m_lst_history->itemAt(pos) || m_lst_history->selectedItems().count() != 1) + { + return; + } + + QListWidgetItem* selected_item = m_lst_history->selectedItems().first(); + + std::string str_sel_friend = selected_item->text().toStdString(); + + QMenu* context_menu = new QMenu(); + QAction* send_friend_request_action = context_menu->addAction(tr("&Send Friend Request")); + + connect(send_friend_request_action, &QAction::triggered, this, [this, str_sel_friend]() + { + if (!m_rpcn->add_friend(str_sel_friend)) + { + QMessageBox::critical(this, tr("Error sending a friend request!"), tr("An error occurred while trying to send a friend request!"), QMessageBox::Ok); + } + else + { + QString qstr_friend = QString::fromStdString(str_sel_friend); + add_update_list(m_lst_requests, qstr_friend, m_orange_icon, QVariant(false)); + remove_list(m_lst_history, qstr_friend); + } + }); + + context_menu->exec(m_lst_history->viewport()->mapToGlobal(pos)); + context_menu->deleteLater(); + }); + connect(btn_addfriend, &QAbstractButton::clicked, this, [this]() { std::string str_friend_username; @@ -1146,6 +1213,7 @@ void rpcn_friends_dialog::remove_friend(QString name) void rpcn_friends_dialog::add_query(QString name) { add_update_list(m_lst_requests, name, m_yellow_icon, QVariant(true)); + remove_list(m_lst_history, name); } void rpcn_friends_dialog::callback_handler(rpcn::NotificationType ntype, std::string username, bool status) diff --git a/rpcs3/rpcs3qt/rpcn_settings_dialog.h b/rpcs3/rpcs3qt/rpcn_settings_dialog.h index 831f5eafb9..4e590127c0 100644 --- a/rpcs3/rpcs3qt/rpcn_settings_dialog.h +++ b/rpcs3/rpcs3qt/rpcn_settings_dialog.h @@ -131,6 +131,8 @@ private: QListWidget* m_lst_requests = nullptr; // list of people blocked by the user QListWidget* m_lst_blocks = nullptr; + // list of players in history + QListWidget* m_lst_history = nullptr; std::shared_ptr m_rpcn; bool m_rpcn_ok = false;