7.5 KiB
Forced new-account onboarding: what the 2026-10-12 runs proved
The client-side force (2023Patch/RadicadeSeptemberClient/ForcedNewAccountOnboarding.cs) has been
removed. This file records what it measured, because the measurements are the useful part and
several of them contradict the guesses that built it.
The conclusion they point at: this is a server-side problem. The 2023 client decides everything about the title screen from data the server sends it, and every attempt to override that decision inside the client either did nothing or produced a state the client was never designed to be in.
1. Where the title screen decision is actually made
From the Beta install's BepInEx/LogOutput.log, in this order, before the token request:
[Onboarding] enter KFGCEPNLFBA (BaseRouteFlowModel route-flow update)
[Onboarding] enter NHAJFLJDHBA (route-flow getter)
[Onboarding] exit NHAJFLJDHBA => HHGLIBCDIDA
[Onboarding] enter get_IsInAccountCreationState
[Onboarding] exit get_IsInAccountCreationState => false <- and then "Welcome back!"
[Net] Post /auth/connect/token [AuthGrant] grant_type=cached_login
So the question the client asks itself is "am I mid-account-creation?", answered before it decides how to sign in, from the player-settings markers it holds:
Recroom.AccountCreation.HasStartedRecroom.AccountCreation.HasFinished
and it reads those back out of GET /playersettings/playersettings. Answer false, and the client
takes the cached-account route and resumes with grant_type=cached_login.
get_IsInAccountCreationState and get_IsInCachedAccountState are both public bool getters on
RRUI.Data.TitleScreenFlowModel — cheap to patch, and the first one is definitely called. Overriding
it was tried: it answers the question the client asks, but the client is not the only thing that
decides, so the result was a different sign-in, not a different flow (see 3).
2. The flow update crashes when the cached-account list is not empty
RRUI.Data.TitleScreenFlowModel.OFOIOHEFGAB() is the client's own title-screen update. In the runs
where the server returned a real cached account, it threw, immediately after that response arrived:
[Net] Get /auth/cachedlogin/forplatformid/{redacted} -> 200 bytes=179
[Onboarding] enter OFOIOHEFGAB
Exception: Il2CppInterop.Runtime.Il2CppException: System.NullReferenceException
at RRUI.Data.TitleScreenFlowModel.OFOIOHEFGAB ()
In the run where the same endpoint answered [] (2 bytes, no account), the same method did not
throw, and no [ForceOnboard] ... OFOIOHEFGAB threw line was recorded all run. Same client build,
same plugins; the only difference was that one response.
That member is what moves the title screen to its next page. While it throws, no page — the client's or a patch's — is applied, which is why "the hook installed but the screen did not move" looked like a patching problem for so long. It is worth finding out whether the client is being handed a cached account that is missing something the real service always sends.
What the server sends for that endpoint today (live, forplatformid/0/76561198624498265):
[{ "accountId": 18, "platform": 0, "platformId": "76561198624498265",
"lastLoginTime": "2026-10-12T14:41:14.8984545", "requirePassword": false }]
3. Overriding the state changes the sign-in, not the flow
With get_IsInAccountCreationState forced to true:
[ForceOnboard] forced get_IsInAccountCreationState false -> true
[HttpStatus] #6 Get /auth/cachedlogin/forplatformid/{redacted} -> 200 bytes=2 (empty)
[AuthGrant] connect/token grant_type=create_account (was: cached_login)
[HttpStatus] #8 Post /auth/connect/token -> 200 bytes=703
... token claims carry "userId":"19" ...
[HttpStatus] #26 Post /matchmaking/player/login -> 200
The client left the cached-account path, which is what was wanted, and then provisioned a brand-new
account rather than being put through the flow for the account that already existed. The creation
pages' own hooks (SubmitAvatarCustomizationAndGoToNext, SubmitAccountCreationBirthdayAndGoToNext,
AcceptCodeOfConductAndGoToNext, LaunchGameAccountCreation) never fired: the client did not walk
the pages, it just made an account and launched.
So forcing the state gets a new account, not onboarding. The account that needs onboarding is
chosen by the server, from CachedLogins and the creation markers.
4. Two server-side things found on the way
The standalone guard cannot fire for this client. Server/Auth/Cached/CachedLoginAPI.cs:
if (platform == Platforms.Standalone) return Ok(new List<CachedLogin>());
Platforms.Standalone is 7, but this client identifies as platform 0 (Steam, with a SteamID64
as its platform id — "rn.plat":"0", "rn.platid":"76561198624498265" in its own token). That guard
therefore never applies to the build being tested, and the row is returned.
Two SteamIDs appear for the same machine. ...24498265 (account 18, the one with the cached
row) and ...23533377 (the identity that created account 19). A platform change explains it, but it
means CachedLogins is keyed on the platform identity, not on the machine, and a device that
changes platform looks like a brand-new device to every "has this device been here before?" rule in
AuthAPI and PlayerUtils.
5. Client interop facts worth keeping
RRUI.Data.TitleScreenFlowModel, from BepInEx/interop/Assembly-CSharp.dll (metadata, not inferred):
| Member | Signature |
|---|---|
HBKLANJACMF |
void (JPDJBDGFIGP) — the page navigator; its parameter is named ENNJBOCEGDH, which is why a named-value-type prefix was rejected by HarmonyX |
DBHMKMHNJOO |
JPDJBDGFIGP () — the reported page |
get_IsInAccountCreationState |
bool () |
get_IsInCachedAccountState |
bool () |
OFOIOHEFGAB |
void () — the title-screen update |
KFGCEPNLFBA |
void () — the route-flow update |
get_LocalAccount |
INMPLDLFHAF () |
LaunchGameCachedAccount / LaunchGameAccountCreation |
void () |
SubmitAccountCreationBirthdayAndGoToNext, SubmitAccountCreationUsernameAndGoToNext, SubmitAccountCreationPasswordAndGoToNext, AcceptCodeOfConductAndGoToNext, SubmitAvatarCustomizationAndGoToNext |
void () |
Page enum JPDJBDGFIGP: TITLE_SCREEN_NEW_ACCOUNT = 8, TITLE_SCREEN_CACHED_ACCOUNT = 9,
TITLE_SCREEN_INTERRUPTED_ACCOUNT_CREATION = 10, AVATAR_CUSTOMIZATION = 18, BIRTHDAY = 19,
USERNAME_NON_JUNIOR = 20, USERNAME_JUNIOR = 21, PASSWORD = 22.
TitleScreenFlowModel also exposes initialBootPage, titleScreenNewAccountPage,
titleScreenCachedAccountPage, titleScreenInterruptedAccountCreationPage, avatarCustomizationPage,
birthdayPage, usernamePage, passwordPage, accountCreationCodeOfConductPage,
accountCreationLaunchGamePage as TitleScreenPage properties, and the flow navigates by
RRUI.Navigation.Route through RRUI.Data.BaseRouteFlowModel.
6. The one thing not to repeat
The force was gated on a marker file, and an early build wrote that marker when the flow reached its
own end regardless of whether anything had been forced. A plain walk of the vanilla flow
therefore consumed the arming, and every later boot printed
the marker file is present, so nothing will be forced — which is exactly what the 01:26, 10:41 and
10:43 logs all say. Two test runs in a row were spent on a force that was switched off by its own
bookkeeping, and the gate now says so in the log rather than in a comment.