diff --git a/DorkNet.ClientMod/Mod.cs b/DorkNet.ClientMod/Mod.cs index 8e1e218..fa1cd15 100644 --- a/DorkNet.ClientMod/Mod.cs +++ b/DorkNet.ClientMod/Mod.cs @@ -156,6 +156,14 @@ public class Mod : MelonMod // "N"). Empty = no hotkey. A hotkey also arms the feature when // RecRoyaleNightMode is false, so it can be switched on in-game. public static string RecRoyaleNightToggleKey = ""; + + // A client with no remembered account for this machine. false: the + // title screen opens the sign-in page so the player logs into their + // existing account. true: Rec Room's own behaviour, which creates a + // new account automatically -- only useful when the server allows + // sign-ups (otherwise the title screen keeps retrying and failing). + // See TitleScreenLoginPatches. + public static bool AutoCreateAccount = false; } public override void OnInitializeMelon() { @@ -225,6 +233,14 @@ public class Mod : MelonMod postfix: nameof(QuestTeamSize.RelaxEmptySpawnFilter_Postfix)); } + // Title-screen auto-login: with no remembered account, open the + // sign-in page instead of creating a new account. + if (!Cfg.AutoCreateAccount) + { + TryPatchByName("RRUI.Data.TitleScreenFlowModel", "IAKHIJIHAPJ", + prefix: nameof(TitleScreenLoginPatches.AutoLogin_Prefix)); + } + // Rec Royale night mode: armed when enabled OR when a hotkey is set // (so it can be switched on in-game). Scene detection + mood apply // run from OnSceneWasLoaded / OnUpdate below. @@ -457,6 +473,7 @@ public class Mod : MelonMod if (TryGetConfigValue(r, "DesktopScreenShareRaisePhotonRate", out v)) Cfg.DesktopScreenShareRaisePhotonRate = v.GetBoolean(); if (TryGetConfigValue(r, "DesktopScreenShareResolution", out v)) Cfg.DesktopScreenShareResolution = v.GetInt32(); if (TryGetConfigValue(r, "DesktopScreenShareQuality", out v)) Cfg.DesktopScreenShareQuality = v.GetInt32(); + if (TryGetConfigValue(r, "AutoCreateAccount", out v)) Cfg.AutoCreateAccount = v.GetBoolean(); if (TryGetConfigValue(r, "QuestMaxTeamSize", out v)) Cfg.QuestMaxTeamSize = v.GetInt32(); if (TryGetConfigValue(r, "QuestMaxTeamSizeRooms", out v) && v.ValueKind == JsonValueKind.Array) { @@ -903,6 +920,50 @@ internal static class FileHashCheckerPatches return false; } } +internal static class TitleScreenLoginPatches +{ + // TitleScreenFlowModel.IAKHIJIHAPJ(cachedLogin) is the boot-time + // auto-login: the title screen downloads this machine's remembered + // accounts (CFILOMKCAPL), picks one (BFHOLLPGBPB) and logs into it here. + // With none (null) it creates a brand-new account instead (PJMGFADOEEM -> + // grant_type create_account). A server with sign-ups off refuses that, the + // error handler (IBDFENJNMAA) shows the connection-error page and the flow + // tries again, forever. Open the sign-in page instead (GoToLogin, what the + // "Log in" button does). ManuallyLogin carries the player into the game + // from there on its own, so the boot step gets a promise that never + // settles (MAMJDBJDFAF, the game's non-generic promise). + private static Type? _promiseType; + + public static bool AutoLogin_Prefix(object __instance, object? __0, MethodBase __originalMethod, ref object? __result) + { + if (__0 is not null || Mod.Cfg.AutoCreateAccount) return true; + try + { + _promiseType ??= Mod.ResolveType("MAMJDBJDFAF"); + var goToLogin = __instance.GetType().GetMethod("GoToLogin", BindingFlags.Public | BindingFlags.Instance, null, Type.EmptyTypes, null); + if (_promiseType is null || goToLogin is null) + { + Mod.Log.Warning("[title] no remembered account, but the sign-in page could not be opened; creating one as Rec Room does"); + return true; + } + // Il2CppInterop wraps the interface return type (LDGADANDBIO) as + // its own class, so hand back the new promise's native object + // wrapped in that type. + var pending = (Il2CppObjectBase)Activator.CreateInstance(_promiseType)!; + var returnType = ((MethodInfo)__originalMethod).ReturnType; + var result = Activator.CreateInstance(returnType, pending.Pointer); + goToLogin.Invoke(__instance, null); + __result = result; + Mod.Log.Msg("[title] no remembered account; opened the sign-in page instead of creating a new account"); + return false; + } + catch (Exception ex) + { + Mod.Log.Warning($"[title] sign-in redirect failed, creating an account as Rec Room does: {ex.Message}"); + return true; + } + } +} internal static class ToxModPatches { public static bool CanInitializeToxMod_Prefix(ref bool __result) diff --git a/DorkNet.ClientMod/dorknet-clientmod.json.template b/DorkNet.ClientMod/dorknet-clientmod.json.template index f3622c7..81111f1 100644 --- a/DorkNet.ClientMod/dorknet-clientmod.json.template +++ b/DorkNet.ClientMod/dorknet-clientmod.json.template @@ -1,6 +1,7 @@ { "ServerHost": "{HOST}", "EnableTlsTrustBypass": {ENABLE_TLS_TRUST_BYPASS}, + "AutoCreateAccount": false, "EnableDebugConsole": false, "DebugConsoleToggleKey": "BackQuote", "DevCommands": [], diff --git a/DorkNet.Quest/README.md b/DorkNet.Quest/README.md index e025cbf..b331869 100644 --- a/DorkNet.Quest/README.md +++ b/DorkNet.Quest/README.md @@ -30,7 +30,8 @@ Photon shards with PC DorkNet players with no server-side version change. | image RSA/SHA1 verify | accept Content-Signatures the server signs with its own key | PC ImageSigning | | hile-warning / unknown-DLL / file-hash / ToxMod callbacks | neutralised so a re-signed, hook-loaded client is not warned/quit | `ClientMod` AntiTamper/ToxMod/FileHash | | integrity report + quit funnels | **log only** — names any new rejection in logcat | — | -| Oculus `ovr_*` platform calls | (no-Meta build only) steer the entitlement/login path to the client's own credential screen | — | +| Oculus `ovr_*` platform calls | (no-Meta build only) pass the entitlement / user-proof / logged-in-user requests Meta refuses for the renamed app (including the SDK's second check on the inner native message), answering the logged-in user with the placeholder user id `1` | — | +| title-screen auto-login (`TitleScreenFlowModel`) | a headset with no remembered account opens the **sign-in page** instead of auto-creating an account (`AutoCreateAccount = true` restores Rec Room's behaviour) | — | ### Two builds, selected by `OculusAppId` @@ -204,6 +205,8 @@ every `armed `, then `intercepted https://ns.rec.net/ -> https://ns. grant_type create_account). A server with sign-ups off +// refuses that, the error handler shows the connection-error page and the +// flow tries again, forever. Unless AutoCreateAccount is on, open the sign-in +// page instead (TitleScreenFlowModel.GoToLogin, what the "Log in" button +// does). ManuallyLogin carries the player into the game from there on its own, +// so the boot step gets a promise that never settles. +static void *(*orig_TitleAutoLogin)(void *thiz, void *cached, void *method); +static void *new_pending_promise() { + // MAMJDBJDFAF: the game's non-generic promise (RecRoom.Promises.Runtime); + // a fresh one stays pending. + static void *cls = find_class("RecRoom.Promises.Runtime.dll", "", "MAMJDBJDFAF"); + if (!cls) return nullptr; + void *ctor = il2cpp_class_get_method_from_name(cls, ".ctor", 0); + if (!ctor) return nullptr; + void *p = il2cpp_object_new(cls); + void *exc = nullptr; + if (p) il2cpp_runtime_invoke(ctor, p, nullptr, &exc); + return exc ? nullptr : p; +} +static void *hk_TitleAutoLogin(void *thiz, void *cached, void *method) { + if (cached || g_auto_create_account) return orig_TitleAutoLogin(thiz, cached, method); + void *pending = new_pending_promise(); + if (!pending || !invoke_void(thiz, "GoToLogin")) { + LOGE("no remembered account, but the sign-in page could not be opened; creating one as Rec Room does"); + return orig_TitleAutoLogin(thiz, cached, method); + } + LOGI("no remembered account on this headset; opened the sign-in page instead of creating a new account"); + return pending; +} + // ----------------------------------------------------------------- init ------ static uintptr_t module_base(const char *name) { @@ -516,6 +650,12 @@ static bool resolve_il2cpp(void *h) { SYM(il2cpp_string_new) SYM(il2cpp_runtime_invoke) #undef SYM + // Optional: only the title-screen sign-in redirect needs these. + il2cpp_domain_get = (il2cpp_domain_get_t)dlsym(h, "il2cpp_domain_get"); + il2cpp_domain_get_assemblies = (il2cpp_domain_get_assemblies_t)dlsym(h, "il2cpp_domain_get_assemblies"); + il2cpp_assembly_get_image = (il2cpp_assembly_get_image_t)dlsym(h, "il2cpp_assembly_get_image"); + il2cpp_image_get_name = (il2cpp_image_get_name_t)dlsym(h, "il2cpp_image_get_name"); + il2cpp_class_from_name = (il2cpp_class_from_name_t)dlsym(h, "il2cpp_class_from_name"); return true; } @@ -537,6 +677,8 @@ static void arm_oculus_platform_bypass() { return; } void *a; + if ((a = dlsym(ovr, "ovr_Message_GetNativeMessage"))) + A64HookFunction(a, (void *)hk_ovr_GetNativeMessage, (void **)&orig_ovr_GetNativeMessage); if ((a = dlsym(ovr, "ovr_GetLoggedInUserID"))) A64HookFunction(a, (void *)hk_ovr_GetLoggedInUserID, (void **)&orig_ovr_GetLoggedInUserID); if ((a = dlsym(ovr, "ovr_User_GetID"))) @@ -545,6 +687,21 @@ static void arm_oculus_platform_bypass() { A64HookFunction(a, (void *)hk_ovr_GetUserProof, (void **)&orig_ovr_GetUserProof); if ((a = dlsym(ovr, "ovr_UserProof_GetNonce"))) A64HookFunction(a, (void *)hk_ovr_GetNonce, (void **)&orig_ovr_GetNonce); + if ((a = dlsym(ovr, "ovr_Message_GetUser"))) + A64HookFunction(a, (void *)hk_ovr_Message_GetUser, (void **)&orig_ovr_Message_GetUser); +#define DN_HOOK_USER(name) if ((a = dlsym(ovr, #name))) A64HookFunction(a, (void *)hk_##name, (void **)&orig_##name); + DN_HOOK_USER(ovr_User_GetDisplayName) + DN_HOOK_USER(ovr_User_GetOculusID) + DN_HOOK_USER(ovr_User_GetImageUrl) + DN_HOOK_USER(ovr_User_GetSmallImageUrl) + DN_HOOK_USER(ovr_User_GetInviteToken) + DN_HOOK_USER(ovr_User_GetPresence) + DN_HOOK_USER(ovr_User_GetPresenceDeeplinkMessage) + DN_HOOK_USER(ovr_User_GetPresenceDestinationApiName) + DN_HOOK_USER(ovr_User_GetPresenceLobbySessionId) + DN_HOOK_USER(ovr_User_GetPresenceMatchSessionId) + DN_HOOK_USER(ovr_User_GetPresenceStatus) +#undef DN_HOOK_USER } static void *init_thread(void *) { @@ -562,7 +719,7 @@ static void *init_thread(void *) { g_photon_realtime, g_photon_voice, g_photon_chat, g_photon_server, sizeof(g_photon_server), &g_photon_port, g_photon_region, sizeof(g_photon_region), - &g_tls_bypass, &g_accept_images, &g_log_http, &g_debug_http, &g_body_max); + &g_tls_bypass, &g_accept_images, &g_auto_create_account, &g_log_http, &g_debug_http, &g_body_max); LOGI("DorkNet.Quest init: build %s/%s il2cpp @ 0x%lx; host-rewrite -> *.%s", DN_TARGET_VERSION_NAME, DN_TARGET_VERSION_CODE, (unsigned long)g_il2cpp_base, g_server_host); @@ -586,6 +743,8 @@ static void *init_thread(void *) { arm(&dn_targets.pin_bc_verifier, (void *)hk_pin_true4, nullptr); } + arm(&dn_targets.title_auto_login, (void *)hk_TitleAutoLogin, (void **)&orig_TitleAutoLogin); + if (g_accept_images) arm(&dn_targets.image_signature_verify, (void *)hk_image_true, nullptr); diff --git a/DorkNet.Quest/targets/20230317-2128210.json b/DorkNet.Quest/targets/20230317-2128210.json index 4616bfa..09e01ee 100644 --- a/DorkNet.Quest/targets/20230317-2128210.json +++ b/DorkNet.Quest/targets/20230317-2128210.json @@ -111,6 +111,12 @@ "length": 412, "fingerprint": "0xb12b0044ac4f844f", "why": "Log-only: prints the quit code and caller." + }, + "title_auto_login": { + "rva": 58892048, + "length": 668, + "fingerprint": "0x83bebb9a30983b33", + "why": "Title-screen auto-login. With no remembered account (null) it creates a new account; unless AutoCreateAccount is on, open the sign-in page instead." } } } diff --git a/DorkNet.Quest/targets/spec.json b/DorkNet.Quest/targets/spec.json index be4d1be..c6eddb4 100644 --- a/DorkNet.Quest/targets/spec.json +++ b/DorkNet.Quest/targets/spec.json @@ -114,6 +114,12 @@ "class": "public class SessionManager ", "method": "public static void TryApplicationQuit(int DKOGJICFAEP)", "why": "Log-only: prints the quit code and caller." + }, + { + "id": "title_auto_login", + "class": "public class TitleScreenFlowModel ", + "method": "private LDGADANDBIO IAKHIJIHAPJ(LBNJFPOLCDL.MDGJJCFGJNF AJCKAHHJDEN)", + "why": "Title-screen auto-login. With no remembered account (null) it creates a new account; unless AutoCreateAccount is on, open the sign-in page instead." } ] } diff --git a/README.md b/README.md index e2e9306..13726ad 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,16 @@ keep the prefab value) trade per-frame size for bandwidth if a high FPS saturates the link. Only the player *sharing* needs the override. Default `0` (off). +### Sign-in page instead of a new account + +When the title screen finds no remembered account for the machine, Rec Room +creates a brand-new account automatically. On a server with sign-ups off that +is refused and the title screen keeps retrying. By default the mod opens the +**sign-in page** instead, so players log into their existing DorkNet account. +Set `"AutoCreateAccount": true` in `dorknet-clientmod.json` to keep Rec Room's +behaviour on a server that allows sign-ups. The Quest build has the same +option (`AutoCreateAccount` in `dorknet-quest.cfg`). + ### RRO quest team size (opt-in) RRO quests ship with a baked 4-player `GameConfigurationAsset` team cap. diff --git a/docs/quest.md b/docs/quest.md index 5d93992..2f68452 100644 --- a/docs/quest.md +++ b/docs/quest.md @@ -55,6 +55,17 @@ an account, never matched by the cached-login lookup players are keyed by `deviceId` alone, exactly like the PC Steam-emulator path. A real Meta platform id (the Meta build) still tags and lists normally. +Because the placeholder never lists a remembered account, the title screen +would normally create a new account on every fresh headset. With +`AutoCreateAccount = false` (the default) the hook library opens the sign-in +page instead, so players log into their existing DorkNet account; set it to +`true` only on a server that allows sign-ups. + +On a headset that reaches Meta, the Oculus platform also refuses the +"logged-in user" request for the renamed app ("Unable to load Oculus user +account"). The no-Meta build passes that refusal and hands the game the same +placeholder user id `1`. + Thanks to [RecFlare](https://github.com/recflare) for documenting this pitfall in their Quest client notes.