From 3880745efd1b729cba77813b752144abc27334b6 Mon Sep 17 00:00:00 2001 From: Alexa <53471801+Alexa-RR@users.noreply.github.com> Date: Sat, 24 Oct 2026 22:45:57 +0300 Subject: [PATCH 1/2] fix: open the sign-in page instead of creating a new account on a fresh client With no remembered account, the 2023 title screen creates a brand-new account (grant_type create_account). On a server with sign-ups off that fails and the title screen retries forever. PC (ClientMod) and Quest now open the sign-in page instead, so players log into their existing DorkNet account. AutoCreateAccount = true keeps Rec Room's behaviour. Also replaces a literal NUL byte in the Quest config loader with '\0'. --- DorkNet.ClientMod/Mod.cs | 61 +++++++ .../dorknet-clientmod.json.template | 1 + DorkNet.Quest/README.md | 5 +- DorkNet.Quest/dorknet-quest.cfg.example | 6 + DorkNet.Quest/scripts/gen_config_header.py | 1 + DorkNet.Quest/src/config.cpp | Bin 4059 -> 4121 bytes DorkNet.Quest/src/config.h | 7 +- DorkNet.Quest/src/dorknet_quest.cpp | 167 +++++++++++++++++- DorkNet.Quest/targets/20230317-2128210.json | 6 + DorkNet.Quest/targets/spec.json | 6 + README.md | 10 ++ docs/quest.md | 11 ++ 12 files changed, 275 insertions(+), 6 deletions(-) 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.;p8mq=G_8AGJ zNDQzs5WWd115zb62G|%F_ybs3kScXzLySxn=Sx~zR7=+P-Fxr5dw#wj!*B8Hh3?`) zx3}E=RaizNmSfsMli;H1IxTCx+%BSj;B}uiEw|jR9fCZ|VYL8dF zRM-$Oyb{`{_(-^+M`++Vn0f)x7;_d%u_fhywq@8p(KtHLH(NHQy`Yh%R-sH>YEF(}au({uxejt_K|ll(ZI)uut)^~&VcRSl+6>+mjx z;cM#Q0fB8fWFSz*OVAJ{cqw9dFOI@Dai}j0gHp)jJ?F41%A@> z&=bREKVS(Na3x!U@8}r(L?y_iYiNJ8!zcxwr>nefrzd%>W%}!0rpDL341#?2BI_SS z8!EiWPO-KGJJ}gt=W=sly+6wWxy|#K?Z;7+MJDRZO) z6?Accl&#{3L{^rQnU@MvCeEo1RN%y^0VK0HL5jOLLAGz;l!j^n1+7+UAv|0s-{;iS zgjfoaDk;j$)yPh*)P#l+T&yfH2PigKk1G*q+(cd#LI32^oYG>)f&$MJB~49*YTn6* zxm191&$;Y)5a!i_6;Iy4%{Dob8{}`GRv^zB&SIM^%7YMRn|zChYjO>b36lIR9+00P z`q(Dx^J*|MOpfHW0E?wSWhD9BpsacP?34fUf&35B;mL0Z7dHT=Sde#s2?<1{CFkT9 Qr)s2ueD#>$m=&DD01sry@c;k- diff --git a/DorkNet.Quest/src/config.h b/DorkNet.Quest/src/config.h index 1cfff01..f7003ec 100644 --- a/DorkNet.Quest/src/config.h +++ b/DorkNet.Quest/src/config.h @@ -57,6 +57,11 @@ #ifndef DN_ACCEPT_UNSIGNED_IMAGES # define DN_ACCEPT_UNSIGNED_IMAGES true #endif +// A headset with no remembered account: false opens the sign-in page, true +// keeps Rec Room's behaviour of creating a new account automatically. +#ifndef DN_AUTO_CREATE_ACCOUNT +# define DN_AUTO_CREATE_ACCOUNT false +#endif #ifndef DN_LOG_HTTP # define DN_LOG_HTTP true #endif @@ -82,7 +87,7 @@ void dn_load_runtime_config(char *server_host, size_t server_host_cap, char *photon_realtime, char *photon_voice, char *photon_chat, char *photon_server, size_t photon_server_cap, int *photon_port, char *photon_region, size_t photon_region_cap, - bool *tls_bypass, bool *accept_images, + bool *tls_bypass, bool *accept_images, bool *auto_create_account, bool *log_http, bool *debug_http, int *body_max); #endif // DORKNET_QUEST_CONFIG_H diff --git a/DorkNet.Quest/src/dorknet_quest.cpp b/DorkNet.Quest/src/dorknet_quest.cpp index d56101b..6430da7 100644 --- a/DorkNet.Quest/src/dorknet_quest.cpp +++ b/DorkNet.Quest/src/dorknet_quest.cpp @@ -59,6 +59,7 @@ static int g_photon_port = DN_PHOTON_PORT; static char g_photon_region[16] = DN_PHOTON_REGION; static bool g_tls_bypass = DN_TLS_TRUST_BYPASS; static bool g_accept_images = DN_ACCEPT_UNSIGNED_IMAGES; +static bool g_auto_create_account = DN_AUTO_CREATE_ACCOUNT; static bool g_log_http = DN_LOG_HTTP; static bool g_debug_http = DN_DEBUG_HTTP; static int g_body_max = DN_LOG_BODY_MAX; @@ -89,8 +90,12 @@ static const size_t kArrayDataOff = 0x20; // Oculus platform message types (dump.cs ONGCNIAMKCG.DJCEHCFKHME). static const unsigned kMsgEntitlementGetIsViewerEntitled = 409688241u; static const unsigned kMsgUserGetUserProof = 578880643u; +static const unsigned kMsgUserGetLoggedInUser = 1131361373u; static const char *kPlaceholderNonce = "dorknet-no-platform-account"; static const unsigned long long kNoPlatformUserId = 1ULL; +// Stand-in ovrUserHandle for a GetLoggedInUser that Meta refused. Only our +// hooked ovr_User_* accessors ever see it; they never pass it to the SDK. +static void *const kPlaceholderUser = (void *)0x2; static const size_t kLogChunk = 1500; static const uint32_t kUnityTlsSuccess = 0; // UNITYTLS_X509VERIFY_SUCCESS / UNITYTLS_SUCCESS @@ -103,12 +108,22 @@ typedef void *(*il2cpp_class_get_method_from_name_t)(void *klass, const char *na typedef void *(*il2cpp_object_new_t)(void *klass); typedef void *(*il2cpp_string_new_t)(const char *str); typedef void *(*il2cpp_runtime_invoke_t)(void *method, void *obj, void **params, void **exc); +typedef void *(*il2cpp_domain_get_t)(); +typedef void **(*il2cpp_domain_get_assemblies_t)(void *domain, size_t *size); +typedef void *(*il2cpp_assembly_get_image_t)(void *assembly); +typedef const char *(*il2cpp_image_get_name_t)(void *image); +typedef void *(*il2cpp_class_from_name_t)(void *image, const char *ns, const char *name); static il2cpp_object_get_class_t il2cpp_object_get_class; static il2cpp_class_get_method_from_name_t il2cpp_class_get_method_from_name; static il2cpp_object_new_t il2cpp_object_new; static il2cpp_string_new_t il2cpp_string_new; static il2cpp_runtime_invoke_t il2cpp_runtime_invoke; +static il2cpp_domain_get_t il2cpp_domain_get; +static il2cpp_domain_get_assemblies_t il2cpp_domain_get_assemblies; +static il2cpp_assembly_get_image_t il2cpp_assembly_get_image; +static il2cpp_image_get_name_t il2cpp_image_get_name; +static il2cpp_class_from_name_t il2cpp_class_from_name; static uintptr_t g_il2cpp_base; @@ -139,6 +154,30 @@ static const unsigned char *invoke_bytes(void *obj, const char *method, size_t * return (const unsigned char *)((char *)arr + kArrayDataOff); } +static bool invoke_void(void *obj, const char *method) { + if (!obj) return false; + void *cls = il2cpp_object_get_class(obj); + if (!cls) return false; + void *m = il2cpp_class_get_method_from_name(cls, method, 0); + if (!m) return false; + void *exc = nullptr; + il2cpp_runtime_invoke(m, obj, nullptr, &exc); + return !exc; +} + +static void *find_class(const char *image_name, const char *ns, const char *name) { + if (!il2cpp_domain_get || !il2cpp_domain_get_assemblies || !il2cpp_assembly_get_image || + !il2cpp_image_get_name || !il2cpp_class_from_name) return nullptr; + size_t n = 0; + void **assemblies = il2cpp_domain_get_assemblies(il2cpp_domain_get(), &n); + for (size_t i = 0; assemblies && i < n; i++) { + void *image = il2cpp_assembly_get_image(assemblies[i]); + const char *iname = image ? il2cpp_image_get_name(image) : nullptr; + if (iname && !strcmp(iname, image_name)) return il2cpp_class_from_name(image, ns, name); + } + return nullptr; +} + static void invoke_str(void *obj, const char *method, char *out, size_t cap) { out[0] = '\0'; if (!obj) return; @@ -438,14 +477,73 @@ typedef unsigned (*ovr_GetType_t)(void *m); static ovr_IsError_t orig_ovr_IsError; static ovr_GetType_t ovr_GetType; +static bool is_bypassed_type(unsigned t) { + return t == kMsgEntitlementGetIsViewerEntitled || t == kMsgUserGetUserProof || + t == kMsgUserGetLoggedInUser; +} + +// The SDK's Message constructor checks IsError twice: on the message, and -- +// when that passes -- on ovr_Message_GetNativeMessage(message). The inner +// message carries the platform's refusal too but doesn't report the outer +// type, so remember the inner handle of a bypassed message (same thread, the +// constructor asks about it immediately) and pass it as well. +static __thread void *t_bypassed_native; +static __thread unsigned t_bypassed_type; + static bool hk_ovr_IsError(void *m) { bool real = orig_ovr_IsError(m); - if (!real || !ovr_GetType) return real; - unsigned t = ovr_GetType(m); - if (t == kMsgEntitlementGetIsViewerEntitled || t == kMsgUserGetUserProof) return false; + if (!real) return real; + if (m && m == t_bypassed_native) return false; + if (ovr_GetType && is_bypassed_type(ovr_GetType(m))) return false; return real; } +typedef void *(*ovr_GetNativeMessage_t)(void *m); +static ovr_GetNativeMessage_t orig_ovr_GetNativeMessage; +static void *hk_ovr_GetNativeMessage(void *m) { + void *native = orig_ovr_GetNativeMessage(m); + unsigned t = ovr_GetType ? ovr_GetType(m) : 0; + t_bypassed_native = is_bypassed_type(t) ? native : nullptr; + t_bypassed_type = t; + return native; +} + +// OculusPlatformManager.LoadUser asks for the logged-in user; on a headset +// that reaches Meta, Meta refuses it for this re-packaged app, LoadUser +// rejects ("GetLoggedInUser failed") and Initialize shows "Unable to load +// Oculus user account". A headset that can't reach Meta gets a cached user +// and never hits this. Answer a refused request with a placeholder user whose +// ID is kNoPlatformUserId -- the same placeholder the server already treats +// as "no platform account" (PlayerService.IsPlaceholderPlatformId). +static void *(*orig_ovr_Message_GetUser)(void *m); +static void *hk_ovr_Message_GetUser(void *m) { + void *u = orig_ovr_Message_GetUser ? orig_ovr_Message_GetUser(m) : nullptr; + if (u) return u; + // The SDK reads the user from the inner native message + // (GetUser(GetNativeMessage(msg))), so match that handle as well. + bool logged_in_user = (m && m == t_bypassed_native && t_bypassed_type == kMsgUserGetLoggedInUser) || + (ovr_GetType && ovr_GetType(m) == kMsgUserGetLoggedInUser); + if (!logged_in_user) return u; + LOGI("GetLoggedInUser refused by the platform; using the no-platform placeholder user"); + return kPlaceholderUser; +} + +// Accessors the SDK's User model reads. Real users pass straight through; the +// placeholder gets empty strings and zero values. +#define DN_USER_STR_ACCESSOR(name) static const char *(*orig_##name)(void *u); static const char *hk_##name(void *u) { if (u == kPlaceholderUser) return ""; return orig_##name ? orig_##name(u) : ""; } +#define DN_USER_VAL_ACCESSOR(name) static uintptr_t (*orig_##name)(void *u); static uintptr_t hk_##name(void *u) { if (u == kPlaceholderUser) return 0; return orig_##name ? orig_##name(u) : 0; } +DN_USER_STR_ACCESSOR(ovr_User_GetDisplayName) +DN_USER_STR_ACCESSOR(ovr_User_GetOculusID) +DN_USER_STR_ACCESSOR(ovr_User_GetImageUrl) +DN_USER_STR_ACCESSOR(ovr_User_GetSmallImageUrl) +DN_USER_STR_ACCESSOR(ovr_User_GetInviteToken) +DN_USER_STR_ACCESSOR(ovr_User_GetPresence) +DN_USER_STR_ACCESSOR(ovr_User_GetPresenceDeeplinkMessage) +DN_USER_STR_ACCESSOR(ovr_User_GetPresenceDestinationApiName) +DN_USER_STR_ACCESSOR(ovr_User_GetPresenceLobbySessionId) +DN_USER_STR_ACCESSOR(ovr_User_GetPresenceMatchSessionId) +DN_USER_VAL_ACCESSOR(ovr_User_GetPresenceStatus) // ovrUserPresenceStatus: 0 = Unknown + static void *(*orig_ovr_GetUserProof)(void *m); static void *hk_ovr_GetUserProof(void *m) { void *p = orig_ovr_GetUserProof ? orig_ovr_GetUserProof(m) : nullptr; @@ -469,10 +567,46 @@ static unsigned long long hk_ovr_GetLoggedInUserID() { static unsigned long long (*orig_ovr_User_GetID)(void *u); static unsigned long long hk_ovr_User_GetID(void *u) { + if (u == kPlaceholderUser) return kNoPlatformUserId; unsigned long long real = orig_ovr_User_GetID ? orig_ovr_User_GetID(u) : 0; return real ? real : kNoPlatformUserId; } +// ------------------------------------------------------------ title screen -- + +// TitleScreenFlowModel.IAKHIJIHAPJ(cachedLogin) is the boot-time auto-login: +// the title screen downloads the headset's remembered accounts and logs into +// one; 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 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. From 9523525cb86dc3ddb449ddde6b71762c7cfa57aa Mon Sep 17 00:00:00 2001 From: Alexa <53471801+Alexa-RR@users.noreply.github.com> Date: Sat, 24 Oct 2026 22:50:54 +0300 Subject: [PATCH 2/2] fix: register the title-screen login patch so it actually applies on PC GetPatchMethod only resolves patch methods from a fixed list of holder classes; TitleScreenLoginPatches was missing, so the prefix failed with [patch-fail] and a fresh PC client still looped on account creation. Also log the real exception behind a failed sign-in redirect instead of the TargetInvocationException wrapper. --- DorkNet.ClientMod/Mod.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/DorkNet.ClientMod/Mod.cs b/DorkNet.ClientMod/Mod.cs index fa1cd15..84a428f 100644 --- a/DorkNet.ClientMod/Mod.cs +++ b/DorkNet.ClientMod/Mod.cs @@ -679,7 +679,7 @@ public class Mod : MelonMod private MethodInfo GetPatchMethod(string name) { // Look in the core + feature patch holder classes. - foreach (var holder in new[] { typeof(UriPatches), typeof(AntiCheatPatches), typeof(AntiTamperPatches), typeof(FileHashCheckerPatches), typeof(ToxModPatches), typeof(TlsPatches), typeof(ScreenSharePatches), typeof(QuestTeamSize), typeof(RecRoyaleNight) }) + foreach (var holder in new[] { typeof(UriPatches), typeof(AntiCheatPatches), typeof(AntiTamperPatches), typeof(FileHashCheckerPatches), typeof(ToxModPatches), typeof(TlsPatches), typeof(ScreenSharePatches), typeof(QuestTeamSize), typeof(RecRoyaleNight), typeof(TitleScreenLoginPatches) }) { var m = holder.GetMethod(name, BindingFlags.Public | BindingFlags.Static); if (m is not null) return m; @@ -959,7 +959,9 @@ internal static class TitleScreenLoginPatches } catch (Exception ex) { - Mod.Log.Warning($"[title] sign-in redirect failed, creating an account as Rec Room does: {ex.Message}"); + // goToLogin.Invoke wraps the real failure in TargetInvocationException. + var cause = ex is TargetInvocationException { InnerException: not null } tie ? tie.InnerException! : ex; + Mod.Log.Warning($"[title] sign-in redirect failed, creating an account as Rec Room does: {cause.GetType().Name}: {cause.Message}"); return true; } }