From 80cc4ce0bde0c2adbb3805ae30fe7352296b5aa2 Mon Sep 17 00:00:00 2001 From: Alexa <53471801+Alexa-RR@users.noreply.github.com> Date: Thu, 28 May 2026 13:46:39 +0300 Subject: [PATCH] Eager Photon AppId override via OnUpdate poll MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The harmony hook on PhotonNetwork.ConnectUsingSettings registers in March's IL2CPP build but never actually fires — the watch's connect path goes through a different (mangled) entry point. Log confirms [patch-ok] PhotonNetwork.ConnectUsingSettings but no [photon-appid] override line, ever. Fix moves the override out of the prefix-only path: - New PhotonPatches.TryApplyOverride(reason) does the PhotonServerSettings.AppID / VoiceAppID / region mutation and flips an OverrideApplied flag once it succeeds. Idempotent. - OnUpdate polls TryApplyOverride every 6th frame (~10 Hz) until it succeeds, then stops. The "type not found yet" log is throttled to once on first miss + every 300th miss after, so the file doesn't drown if PhotonServerSettings stays unloaded. - Mod.ResolveType made internal (was private) so PhotonPatches + SavePatches can use the broader assembly-scan fallback — the local FindGameType copies were too narrow and missed IL2CPP-mangled type names. Both FindGameType helpers collapse to one-line delegates to Mod.ResolveType. - Prefix on ConnectUsingSettings kept as a belt-and-braces hook (passes reason="prefix"); harmless if it fires, fine if it doesn't. --- DorkNet.ClientMod/Mod.cs | 128 +++++++++++++++++++++------------------ 1 file changed, 69 insertions(+), 59 deletions(-) diff --git a/DorkNet.ClientMod/Mod.cs b/DorkNet.ClientMod/Mod.cs index 494c4f7..71dd26d 100644 --- a/DorkNet.ClientMod/Mod.cs +++ b/DorkNet.ClientMod/Mod.cs @@ -46,6 +46,7 @@ public class Mod : MelonMod private bool _diagnosticCoreRegistered; private bool _diagnosticGameComplete; private int _diagnosticRetryFrame; + private int _photonOverridePollFrame; // Loader-agnostic config — same .Value-shaped accessor the patches // would see in the BepInEx port, just backed by a plain JSON file @@ -241,6 +242,18 @@ public class Mod : MelonMod public override void OnUpdate() { + // Photon AppId override poller. The harmony hook on + // PhotonNetwork.ConnectUsingSettings registers in IL2CPP but + // doesn't actually fire in this build (verified in the + // MelonLoader log — [patch-ok] appears but [photon-appid] + // never does), so we poll until PhotonServerSettings is loaded + // and write the AppId then. Stops calling once the override + // succeeds. Throttled to every 6th frame (~10 Hz at 60 fps) so + // the failure log doesn't drown the file when PhotonServerSettings + // isn't loaded yet. + if (!PhotonPatches.OverrideApplied && (_photonOverridePollFrame++ % 6) == 0) + PhotonPatches.TryApplyOverride(reason: "update-poll"); + if (_diagnosticGameComplete) return; if (_diagnosticRetryFrame++ > 3600) return; if ((_diagnosticRetryFrame % 60) != 0) return; @@ -347,7 +360,7 @@ public class Mod : MelonMod return TryPatch(label, type, methodName, args, prefix, postfix); } - private static Type? ResolveType(string name) + internal static Type? ResolveType(string name) { if (ResolvedTypeCache.TryGetValue(name, out var cached)) return cached; @@ -620,21 +633,57 @@ internal static class UriPatches internal static class PhotonPatches { + /// True once has successfully + /// written the user's AppId into PhotonServerSettings. The + /// per-frame poller in stops calling once + /// this flips true. + public static bool OverrideApplied; + public static void PhotonAppIdOverride_Prefix() { - if (string.IsNullOrEmpty(Mod.Cfg.PhotonAppId)) return; + // Kept as a belt-and-braces hook in case the watch's actual + // connect path does happen to flow through ConnectUsingSettings + // (it doesn't, in March's IL2CPP build — verified via log). The + // poller in OnUpdate is what actually gets the override in + // before the wire connect. + TryApplyOverride(reason: "prefix"); + } + + /// Tracks how many polling attempts we've logged so the + /// "type not found yet" line doesn't drown the log file. Cleared + /// once the override succeeds. + private static int _missLogCount; + + /// Mutates + + /// VoiceAppID + region with the values from + /// . Returns true once the override has been + /// written successfully; subsequent calls become no-ops. Returns + /// false (and logs nothing on most retries) when PhotonServerSettings + /// hasn't been loaded by the watch yet — the poller retries every + /// frame until the ScriptableObject shows up. + public static bool TryApplyOverride(string reason) + { + if (OverrideApplied) return true; + if (string.IsNullOrEmpty(Mod.Cfg.PhotonAppId)) return false; try { - // PhotonNetwork.PhotonServerSettings — accessed via reflection - // so the type-name resolution stays in one place. Mutating the - // ScriptableObject in-place is what the BepInEx plugin does - // too; the Photon Connect path reads AppID / VoiceAppID off - // this object immediately after. - var photonNetwork = Mod.Instance is null ? null : FindGameType("PhotonNetwork"); - if (photonNetwork is null) return; + // Use Mod.ResolveType (the same resolver TryPatchByName uses) + // — it falls back to a full assembly scan after the prefix + // attempts, which is how it finds the IL2CPP-mangled types + // that the local FindGameType helper misses. + var photonNetwork = Mod.ResolveType("PhotonNetwork"); + if (photonNetwork is null) + { + // Log the miss only on the first call + once every ~10s + // after that, to avoid the log spam previous build hit. + if (_missLogCount == 0 || _missLogCount % 300 == 0) + Mod.Log.Msg($"[photon-appid] PhotonNetwork type not found yet (#{_missLogCount}, {reason})"); + _missLogCount++; + return false; + } var settingsProp = photonNetwork.GetProperty("PhotonServerSettings", BindingFlags.Public | BindingFlags.Static); var settings = settingsProp?.GetValue(null); - if (settings is null) return; + if (settings is null) return false; // ScriptableObject not loaded yet; quietly retry next frame. var settingsType = settings.GetType(); var appIdProp = settingsType.GetProperty("AppID") ?? settingsType.GetProperty("AppId"); @@ -645,7 +694,7 @@ internal static class PhotonPatches appIdProp?.SetValue(settings, newId); voiceProp?.SetValue(settings, newVoice); - Mod.Log.Msg($"[photon-appid] override AppID/VoiceAppID → {newId} / {newVoice}"); + Mod.Log.Msg($"[photon-appid] override AppID/VoiceAppID → {newId} / {newVoice} ({reason})"); // Force a deterministic Photon region so two watches running // this mod end up on the same Photon master and "JoinByName" @@ -667,8 +716,14 @@ internal static class PhotonPatches // any) over the config default — that way a server-side // region change propagates without redeploying the mod. ApplyForcedRegion(settings, settingsType); + OverrideApplied = true; + return true; + } + catch (Exception ex) + { + Mod.Log.Warning($"[photon-appid] override failed ({reason}): {ex.Message}"); + return false; } - catch (Exception ex) { Mod.Log.Warning($"[photon-appid] override failed: {ex.Message}"); } } /// Forces PhotonServerSettings.HostType = PhotonCloud @@ -753,29 +808,7 @@ internal static class PhotonPatches // watch no longer needs to attach userid+LoginLock to Photon // Authenticate ops. - private static Type? FindGameType(string name) - { - // Mirrors Mod.ResolveType but inlined here so the holder doesn't - // depend on Mod's internal helpers. - foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) - { - try - { - var t = asm.GetType(name, throwOnError: false); - if (t is not null) return t; - var prefixed = asm.GetType("Il2Cpp" + name, throwOnError: false); - if (prefixed is not null) return prefixed; - var dot = name.IndexOf('.'); - if (dot > 0) - { - var nsPrefixed = asm.GetType("Il2Cpp" + name.Substring(0, dot) + name.Substring(dot), throwOnError: false); - if (nsPrefixed is not null) return nsPrefixed; - } - } - catch { } - } - return null; - } + private static Type? FindGameType(string name) => Mod.ResolveType(name); } internal static class SavePatches @@ -1023,30 +1056,7 @@ internal static class SavePatches } } - // Local copy of the FindGameType helper used by PhotonPatches — IL2CPP - // type-name resolution stays per-holder so SavePatches doesn't reach - // across into another internal class. - private static Type? FindGameType(string name) - { - foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) - { - try - { - var t = asm.GetType(name, throwOnError: false); - if (t is not null) return t; - var prefixed = asm.GetType("Il2Cpp" + name, throwOnError: false); - if (prefixed is not null) return prefixed; - var dot = name.IndexOf('.'); - if (dot > 0) - { - var nsPrefixed = asm.GetType("Il2Cpp" + name.Substring(0, dot) + name.Substring(dot), throwOnError: false); - if (nsPrefixed is not null) return nsPrefixed; - } - } - catch { } - } - return null; - } + private static Type? FindGameType(string name) => Mod.ResolveType(name); } internal static class TlsPatches