From b13bdd341bfc3cebe3e7b2c503500906c16a0a1d Mon Sep 17 00:00:00 2001 From: jpolo1224 Date: Wed, 19 Aug 2026 19:10:36 -0400 Subject: [PATCH] RPCN: sync the protocol to 31 and make the sign-in test work Three things stood between the new RPCN account screen and the live server. The client announced protocol 30, so np.rpcs3.net rejected every connection with "Protocol Version Error (outdated RPCS3?)" before any credential was looked at. Raising RPCN_PROTOCOL_VERSION alone is not enough -- the version is a contract over the command enum, so the three commands added upstream alongside it (GetRoomMemberDataExternalList, UnlockTrophy, SyncTrophies) have to exist too, or every opcode after the insertion point shifts and the mismatch shows up later as garbled replies instead of a clean refusal. The other two were in the sign-in test, which was the one RPCN entry point written standalone instead of through rpcn_with_connection. It asked for the client with get_instance(0, true), and that flag does not return an error: it calls fmt::throw_exception, which off a guest thread is a fatal abort. Tapping "Test sign-in" before PSN status had reached the core therefore killed the process. It reads like a validity check and behaves like an assert, which is why desktop's get_rpcn_connection passes the default. That state is easy to reach by accident, and not through user error: Settings.applyTo() only runs at game boot or while a game is live, so choosing RPCN from the library updates the UI and the store but leaves config.yml on Disconnected until something boots. Testing an account does not depend on that setting in the first place -- it needs credentials and a reachable server -- so the check is dropped rather than deferred. With the abort gone it hung instead, silently, with nothing in the log after "Loading RPCN config". rpcn_thread's state machine only acts on want_conn while disconnected, and wait_for_authentified sets want_auth alone: on a fresh client the thread woke, found no connection request, broke back to its semaphore and never released sem_authentified. Connecting first is what np_handler does, and the four account operations already did it through the shared helper. Sign-in now reports a real result either way. Also correct a comment claiming cfg_rpcn stores a password hash. It does not; set_password writes the string straight into rpcn.yml as plaintext. --- .../armsx2/ui/settings/RpcnAccountSection.kt | 9 +++++--- android/src/rpcsx-android.cpp | 21 ++++++++++++++++--- rpcs3/Emu/NP/rpcn_client.cpp | 10 ++++++++- rpcs3/Emu/NP/rpcn_types.h | 5 +++++ 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/android/armsx3-ui/app/src/main/java/com/armsx2/ui/settings/RpcnAccountSection.kt b/android/armsx3-ui/app/src/main/java/com/armsx2/ui/settings/RpcnAccountSection.kt index ec5ed5597..e141404fc 100644 --- a/android/armsx3-ui/app/src/main/java/com/armsx2/ui/settings/RpcnAccountSection.kt +++ b/android/armsx3-ui/app/src/main/java/com/armsx2/ui/settings/RpcnAccountSection.kt @@ -139,9 +139,12 @@ fun RpcnAccountSection() { value = password, onValueChange = { password = it }, label = { - // Say when one is already stored, because the field is deliberately blank: - // cfg_rpcn keeps a hash, so there is no plaintext to show and an empty box - // would otherwise read as "no password set". + // Say when one is already stored, because the field is deliberately blank + // and an empty box would otherwise read as "no password set". + // + // Not shown even though it could be: cfg_rpcn stores the password in + // PLAINTEXT in rpcn.yml (set_password does a straight from_string), so + // rendering it would put it on screen as well as on disk for no gain. Text(if (hasPassword) str("rpcn.password.stored") else str("rpcn.password")) }, singleLine = true, diff --git a/android/src/rpcsx-android.cpp b/android/src/rpcsx-android.cpp index 4235d40cf..1b5372b06 100644 --- a/android/src/rpcsx-android.cpp +++ b/android/src/rpcsx-android.cpp @@ -2850,8 +2850,8 @@ extern "C" const char *_rpcsx_rpcnResetPassword(std::string_view npid, // Connect and authenticate with the saved account, so "is my account set up" can be // answered here instead of by booting a game and guessing. // -// check_config = true makes get_instance validate the saved credentials before trying, -// and wait_for_authentified is the public path -- login() itself is private. +// wait_for_authentified is the public path -- login() itself is private -- but it is only +// half of it, and the half that hangs on its own. See below. extern "C" const char *_rpcsx_rpcnTestLogin() { g_cfg_rpcn.load(); @@ -2860,12 +2860,27 @@ extern "C" const char *_rpcsx_rpcnTestLogin() { "and password."); } - const auto rpcn = rpcn::rpcn_client::get_instance(0, true); + // get_instance's check_config flag does NOT return an error -- it calls + // fmt::throw_exception, which on a non-guest thread is a fatal abort. Passing it from a + // settings button meant tapping "Test sign-in" with PSN status not yet set to RPCN killed + // the process. Desktop passes the default (false) for exactly this reason. + const auto rpcn = rpcn::rpcn_client::get_instance(0); if (!rpcn) { return rpcn_fail("Could not create the RPCN client."); } + // Connect FIRST. rpcn_thread's state machine only acts on want_conn while disconnected: + // wait_for_authentified sets want_auth alone, so on a fresh client the thread wakes, sees + // no connection request, breaks back to its semaphore and never releases sem_authentified. + // The call then blocks forever with nothing in the log after "Loading RPCN config" -- + // which is exactly what it did. np_handler does these two in this order for this reason. + if (const auto state = rpcn->wait_for_connection(); + state != rpcn::rpcn_state::failure_no_failure) { + return rpcn_fail(fmt::format("Could not reach the RPCN server: %s", + rpcn::rpcn_state_to_string(state))); + } + if (const auto state = rpcn->wait_for_authentified(); state != rpcn::rpcn_state::failure_no_failure) { return rpcn_fail(fmt::format("Sign-in failed: %s", diff --git a/rpcs3/Emu/NP/rpcn_client.cpp b/rpcs3/Emu/NP/rpcn_client.cpp index 410713209..594638fb5 100644 --- a/rpcs3/Emu/NP/rpcn_client.cpp +++ b/rpcs3/Emu/NP/rpcn_client.cpp @@ -257,7 +257,15 @@ 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; + // 31 since upstream cb175278b (2026-08-18), which added trophy sync. np.rpcs3.net + // enforces it: a client on 30 completes the TLS handshake, is told the server speaks 31 + // and is disconnected -- "Protocol Version Error (outdated RPCS3?)". + // + // The two commands that version added are appended to the end of CommandType, so every + // existing command keeps its ordinal and the wire format for everything this client + // actually sends is unchanged. Trophy sync itself is not implemented here; not sending a + // command is always safe. + constexpr u32 RPCN_PROTOCOL_VERSION = 31; constexpr usz RPCN_HEADER_SIZE = 15; const char* error_to_explanation(rpcn::ErrorType error) diff --git a/rpcs3/Emu/NP/rpcn_types.h b/rpcs3/Emu/NP/rpcn_types.h index 28500462e..16d6b0b5b 100644 --- a/rpcs3/Emu/NP/rpcn_types.h +++ b/rpcs3/Emu/NP/rpcn_types.h @@ -69,6 +69,11 @@ namespace rpcn QuickMatchGUI, SearchJoinRoomGUI, GetRoomMemberDataExternalList, + // Appended by upstream cb175278b along with protocol 31. We do not send either -- + // trophy sync is not implemented here -- but the ordinals have to match the server's + // or every command after this point would be misnumbered. + UnlockTrophy, + SyncTrophies, }; enum class NotificationType : u16