Files
Radicade/docs/2023-ONBOARDING-FORCE-NOTES.md
T
2026-10-16 23:33:13 -07:00

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.HasStarted
  • Recroom.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.