Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of LethalPhonesCustomNumbers v1.2.1
LethalPhonesCustomNumbers.dll
Decompiled 2 weeks agousing System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using Scoops; using Scoops.customization; using Scoops.misc; using Scoops.service; using TMPro; using Unity.Netcode; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("LethalPhonesCustomNumbers")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.2.1.0")] [assembly: AssemblyInformationalVersion("1.2.1")] [assembly: AssemblyProduct("LethalPhonesCustomNumbers")] [assembly: AssemblyTitle("LethalPhonesCustomNumbers")] [assembly: AssemblyVersion("1.2.1.0")] namespace LethalPhonesCustomNumbers; [HarmonyPatch(typeof(PhoneNetworkHandler))] public class ClientPhonePatch { [HarmonyPatch("CreateNewPhone")] [HarmonyPrefix] public static void CreateNewPhonePrefix(ref string skinId) { string text = ConfigManager.ClientRequestedNumber.Value; if (!string.IsNullOrEmpty(text) && text.Length > 0) { if (text.Length < 4) { text = text.PadLeft(4, '0'); } skinId = skinId + "|" + text; Plugin.Logger.LogInfo((object)("Client appended requested number to skinId: " + skinId)); } } } [HarmonyPatch(typeof(Clipboard), "UpdateText")] internal static class ClipboardSyncPatch { [HarmonyPostfix] private static void UpdateTextPostfix(Clipboard __instance) { if ((Object)(object)__instance == (Object)null || (Object)(object)__instance.textArea == (Object)null) { Plugin.Logger.LogWarning((object)"Phonebook clipboard update completed without a readable text area."); return; } string[] array = (from entry in ((TMP_Text)__instance.textArea).text.Split(new char[1] { '\n' }, StringSplitOptions.RemoveEmptyEntries) select entry.Trim() into entry where entry.Length > 0 select entry).ToArray(); string arg = ((array.Length == 0) ? "<empty>" : string.Join(", ", array)); Plugin.Logger.LogInfo((object)$"Phonebook clipboard synchronized ({array.Length}): {arg}"); } } public static class ConfigManager { public static ConfigEntry<string> SteamIdMappingConfig; public static Dictionary<ulong, short> SteamIdToNumber = new Dictionary<ulong, short>(); public static ConfigFile Config { get; private set; } public static ConfigEntry<string> ClientRequestedNumber { get; private set; } public static void Init(ConfigFile config) { Config = config; ClientRequestedNumber = Config.Bind<string>("Client", "RequestedNumber", "", "Your requested 4-digit phone number. Leave empty for a random number."); SteamIdMappingConfig = config.Bind<string>("General", "SteamIdMapping", "76561198000000000:1234,76561198000000001:5678", "Comma-separated list of SteamID:Number mappings."); ParseMapping(); } public static void ParseMapping() { SteamIdToNumber.Clear(); string value = SteamIdMappingConfig.Value; if (string.IsNullOrWhiteSpace(value)) { return; } string[] array = value.Split(','); for (int i = 0; i < array.Length; i++) { string[] array2 = array[i].Split(':'); if (array2.Length == 2 && ulong.TryParse(array2[0].Trim(), out var result) && short.TryParse(array2[1].Trim(), out var result2)) { SteamIdToNumber[result] = result2; Plugin.Logger.LogInfo((object)$"Loaded CustomNumbers map: {result} -> {result2}"); } } } } [HarmonyPatch(typeof(CustomizationManager))] public class CustomizationUIPatch { private static TMP_InputField numberInputField; private static readonly string[] TAB_NAMES = new string[3] { "SkinButton", "CharmButton", "RingtoneButton" }; [HarmonyPatch("SpawnCustomizationGUI")] [HarmonyPostfix] public static void SpawnCustomizationGUIPostfix() { object? obj = AccessTools.Field(typeof(CustomizationManager), "customizationPanel")?.GetValue(null); Transform val = (Transform)((obj is Transform) ? obj : null); if ((Object)(object)val == (Object)null) { Plugin.Logger.LogError((object)"CustomNumbers: customizationPanel not found!"); return; } Transform val2 = val.Find("Panel"); if ((Object)(object)val2 == (Object)null) { Plugin.Logger.LogError((object)"CustomNumbers: 'Panel' not found!"); return; } Plugin.Logger.LogInfo((object)"=== CustomNumbers: Panel Hierarchy Dump ==="); DumpHierarchy(val2, " "); Plugin.Logger.LogInfo((object)"=== CustomNumbers: End Dump ==="); List<Button> list = FindTabButtons(val2); if (list.Count > 0) { Plugin.Logger.LogInfo((object)$"CustomNumbers: Found {list.Count} tab button(s)"); CreatePhoneNumberTab(val2, list); } else { Plugin.Logger.LogError((object)"CustomNumbers: No tab buttons found — cannot create UI!"); } } private static List<Button> FindTabButtons(Transform panel) { return (from b in ((Component)panel).GetComponentsInChildren<Button>(true) where TAB_NAMES.Contains(((Object)((Component)b).gameObject).name) orderby ((Component)b).GetComponent<RectTransform>().anchoredPosition.y descending select b).ToList(); } private static void CreatePhoneNumberTab(Transform panel, List<Button> tabButtons) { //IL_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d7: Unknown result type (might be due to invalid IL or missing references) //IL_00ea: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Unknown result type (might be due to invalid IL or missing references) //IL_0136: Expected O, but got Unknown //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) //IL_017f: Unknown result type (might be due to invalid IL or missing references) //IL_0184: Unknown result type (might be due to invalid IL or missing references) //IL_0191: Unknown result type (might be due to invalid IL or missing references) //IL_0196: Unknown result type (might be due to invalid IL or missing references) //IL_01b7: Unknown result type (might be due to invalid IL or missing references) //IL_01ce: Unknown result type (might be due to invalid IL or missing references) //IL_01d2: Unknown result type (might be due to invalid IL or missing references) //IL_024d: Unknown result type (might be due to invalid IL or missing references) //IL_0257: Expected O, but got Unknown //IL_0284: Unknown result type (might be due to invalid IL or missing references) //IL_0289: Unknown result type (might be due to invalid IL or missing references) //IL_028c: Expected O, but got Unknown //IL_0291: Expected O, but got Unknown //IL_02bd: Unknown result type (might be due to invalid IL or missing references) Button obj = tabButtons.First(); Button val = tabButtons.Last(); RectTransform component = ((Component)obj).GetComponent<RectTransform>(); float num = 31f; if (tabButtons.Count >= 2) { float y = ((Component)tabButtons[0]).GetComponent<RectTransform>().anchoredPosition.y; float y2 = ((Component)tabButtons[1]).GetComponent<RectTransform>().anchoredPosition.y; num = Mathf.Abs(y - y2); Plugin.Logger.LogInfo((object)$"CustomNumbers: Tab spacing = {num}"); } GameObject obj2 = Object.Instantiate<GameObject>(((Component)val).gameObject, ((Component)val).transform.parent); ((Object)obj2).name = "PhoneNumberTab"; RectTransform component2 = obj2.GetComponent<RectTransform>(); float num2 = component.anchoredPosition.x - component.sizeDelta.x * component.pivot.x; float num3 = num2 + component2.sizeDelta.x * component2.pivot.x; component2.anchoredPosition = new Vector2(num3, component.anchoredPosition.y + num); Plugin.Logger.LogInfo((object)$"CustomNumbers: Skin left edge={num2}, our X={num3}"); Button component3 = obj2.GetComponent<Button>(); component3.onClick = new ButtonClickedEvent(); ((Selectable)component3).interactable = true; TextMeshProUGUI componentInChildren = obj2.GetComponentInChildren<TextMeshProUGUI>(); if ((Object)(object)componentInChildren == (Object)null) { Plugin.Logger.LogError((object)"CustomNumbers: Cloned tab has no TextMeshProUGUI!"); return; } ((TMP_Text)componentInChildren).text = "[ Phone # ]"; TMP_FontAsset font = ((TMP_Text)componentInChildren).font; float fontSize = ((TMP_Text)componentInChildren).fontSize; Color color = ((Graphic)componentInChildren).color; Material fontSharedMaterial = ((TMP_Text)componentInChildren).fontSharedMaterial; FontStyles fontStyle = ((TMP_Text)componentInChildren).fontStyle; Plugin.Logger.LogInfo((object)$"CustomNumbers: Tab style — font={((font != null) ? ((Object)font).name : null)} size={fontSize} color={color}"); GameObject phoneDisplay = CreatePhoneNumberDisplay(panel, font, fontSize, color, fontSharedMaterial, fontStyle); phoneDisplay.SetActive(false); Transform phoneModel = panel.Find("PhoneDisplayModel"); Transform charmDisplay = panel.Find("CharmDisplay"); Transform audioDisplay = panel.Find("AudioDisplay"); Transform nextBtn = panel.Find("NextButton"); Transform prevBtn = panel.Find("PrevButton"); ((UnityEvent)component3.onClick).AddListener((UnityAction)delegate { if ((Object)(object)phoneModel != (Object)null) { ((Component)phoneModel).gameObject.SetActive(false); } if ((Object)(object)charmDisplay != (Object)null) { ((Component)charmDisplay).gameObject.SetActive(false); } if ((Object)(object)audioDisplay != (Object)null) { ((Component)audioDisplay).gameObject.SetActive(false); } if ((Object)(object)nextBtn != (Object)null) { ((Component)nextBtn).gameObject.SetActive(false); } if ((Object)(object)prevBtn != (Object)null) { ((Component)prevBtn).gameObject.SetActive(false); } phoneDisplay.SetActive(true); }); UnityAction val2 = default(UnityAction); foreach (Button tabButton in tabButtons) { ButtonClickedEvent onClick = ((Component)tabButton).GetComponent<Button>().onClick; UnityAction obj3 = val2; if (obj3 == null) { UnityAction val3 = delegate { phoneDisplay.SetActive(false); if ((Object)(object)nextBtn != (Object)null) { ((Component)nextBtn).gameObject.SetActive(true); } if ((Object)(object)prevBtn != (Object)null) { ((Component)prevBtn).gameObject.SetActive(true); } }; UnityAction val4 = val3; val2 = val3; obj3 = val4; } ((UnityEvent)onClick).AddListener(obj3); } Plugin.Logger.LogInfo((object)$"CustomNumbers: [ Phone # ] tab created at y={component2.anchoredPosition.y}"); } private static GameObject CreatePhoneNumberDisplay(Transform panel, TMP_FontAsset font, float fontSize, Color color, Material fontMat, FontStyles fontStyle) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00f4: Unknown result type (might be due to invalid IL or missing references) //IL_012e: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Expected O, but got Unknown //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_016c: Unknown result type (might be due to invalid IL or missing references) //IL_0181: Unknown result type (might be due to invalid IL or missing references) //IL_0195: Unknown result type (might be due to invalid IL or missing references) //IL_01a4: Unknown result type (might be due to invalid IL or missing references) //IL_01aa: Expected O, but got Unknown //IL_01c6: Unknown result type (might be due to invalid IL or missing references) //IL_01d2: Unknown result type (might be due to invalid IL or missing references) //IL_01de: Unknown result type (might be due to invalid IL or missing references) //IL_01ea: Unknown result type (might be due to invalid IL or missing references) //IL_0200: Unknown result type (might be due to invalid IL or missing references) //IL_0205: Unknown result type (might be due to invalid IL or missing references) //IL_0217: Unknown result type (might be due to invalid IL or missing references) //IL_021e: Unknown result type (might be due to invalid IL or missing references) //IL_0229: Unknown result type (might be due to invalid IL or missing references) //IL_0234: Unknown result type (might be due to invalid IL or missing references) //IL_023e: Unknown result type (might be due to invalid IL or missing references) //IL_0248: Unknown result type (might be due to invalid IL or missing references) //IL_026c: Unknown result type (might be due to invalid IL or missing references) //IL_026d: Unknown result type (might be due to invalid IL or missing references) //IL_027d: Unknown result type (might be due to invalid IL or missing references) //IL_02cc: Unknown result type (might be due to invalid IL or missing references) //IL_02d1: Unknown result type (might be due to invalid IL or missing references) //IL_02e3: Unknown result type (might be due to invalid IL or missing references) //IL_02ea: Unknown result type (might be due to invalid IL or missing references) //IL_02f5: Unknown result type (might be due to invalid IL or missing references) //IL_0300: Unknown result type (might be due to invalid IL or missing references) //IL_030a: Unknown result type (might be due to invalid IL or missing references) //IL_0314: Unknown result type (might be due to invalid IL or missing references) //IL_033a: Unknown result type (might be due to invalid IL or missing references) //IL_0361: Unknown result type (might be due to invalid IL or missing references) //IL_03d3: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("PhoneNumberDisplay"); val.transform.SetParent(panel, false); RectTransform obj = val.AddComponent<RectTransform>(); obj.anchorMin = Vector2.zero; obj.anchorMax = Vector2.one; obj.offsetMin = new Vector2(150f, 60f); obj.offsetMax = new Vector2(-150f, -60f); GameObject val2 = new GameObject("Title"); val2.transform.SetParent(val.transform, false); RectTransform obj2 = val2.AddComponent<RectTransform>(); obj2.anchorMin = new Vector2(0.5f, 0.5f); obj2.anchorMax = new Vector2(0.5f, 0.5f); obj2.anchoredPosition = new Vector2(0f, 50f); obj2.sizeDelta = new Vector2(400f, 50f); val2.AddComponent<CanvasRenderer>(); TextMeshProUGUI val3 = val2.AddComponent<TextMeshProUGUI>(); ((TMP_Text)val3).font = font; ((TMP_Text)val3).fontSize = fontSize * 1.5f; ((Graphic)val3).color = color; if ((Object)(object)fontMat != (Object)null) { ((TMP_Text)val3).fontSharedMaterial = fontMat; } ((TMP_Text)val3).alignment = (TextAlignmentOptions)514; ((TMP_Text)val3).enableWordWrapping = false; ((TMP_Text)val3).text = "Phone Number"; GameObject val4 = new GameObject("PhoneNumberInput"); val4.transform.SetParent(val.transform, false); RectTransform obj3 = val4.AddComponent<RectTransform>(); obj3.anchorMin = new Vector2(0.5f, 0.5f); obj3.anchorMax = new Vector2(0.5f, 0.5f); obj3.anchoredPosition = new Vector2(0f, -20f); obj3.sizeDelta = new Vector2(400f, 70f); GameObject val5 = new GameObject("Text Area"); val5.transform.SetParent(val4.transform, false); RectTransform val6 = val5.AddComponent<RectTransform>(); val6.anchorMin = Vector2.zero; val6.anchorMax = Vector2.one; val6.offsetMin = Vector2.zero; val6.offsetMax = Vector2.zero; val5.AddComponent<RectMask2D>(); GameObject val7 = new GameObject("Placeholder"); val7.transform.SetParent(val5.transform, false); RectTransform obj4 = val7.AddComponent<RectTransform>(); obj4.anchorMin = Vector2.zero; obj4.anchorMax = Vector2.one; obj4.offsetMin = Vector2.zero; obj4.offsetMax = Vector2.zero; val7.AddComponent<CanvasRenderer>(); TextMeshProUGUI val8 = val7.AddComponent<TextMeshProUGUI>(); ((TMP_Text)val8).font = font; ((TMP_Text)val8).fontSize = fontSize * 2.5f; Color color2 = color; color2.a = 0.4f; ((Graphic)val8).color = color2; if ((Object)(object)fontMat != (Object)null) { ((TMP_Text)val8).fontSharedMaterial = fontMat; } ((TMP_Text)val8).alignment = (TextAlignmentOptions)514; ((TMP_Text)val8).fontStyle = (FontStyles)2; ((TMP_Text)val8).enableWordWrapping = false; ((TMP_Text)val8).overflowMode = (TextOverflowModes)0; ((TMP_Text)val8).text = "[ 0000 ]"; GameObject val9 = new GameObject("Text"); val9.transform.SetParent(val5.transform, false); RectTransform obj5 = val9.AddComponent<RectTransform>(); obj5.anchorMin = Vector2.zero; obj5.anchorMax = Vector2.one; obj5.offsetMin = Vector2.zero; obj5.offsetMax = Vector2.zero; val9.AddComponent<CanvasRenderer>(); TextMeshProUGUI val10 = val9.AddComponent<TextMeshProUGUI>(); ((TMP_Text)val10).font = font; ((TMP_Text)val10).fontSize = fontSize * 2.5f; ((Graphic)val10).color = color; if ((Object)(object)fontMat != (Object)null) { ((TMP_Text)val10).fontSharedMaterial = fontMat; } ((TMP_Text)val10).alignment = (TextAlignmentOptions)514; ((TMP_Text)val10).fontStyle = fontStyle; ((TMP_Text)val10).enableWordWrapping = false; ((TMP_Text)val10).overflowMode = (TextOverflowModes)0; numberInputField = val4.AddComponent<TMP_InputField>(); numberInputField.textViewport = val6; numberInputField.textComponent = (TMP_Text)(object)val10; numberInputField.placeholder = (Graphic)(object)val8; numberInputField.characterLimit = 4; numberInputField.contentType = (ContentType)2; numberInputField.pointSize = fontSize * 2.5f; numberInputField.caretColor = color; numberInputField.customCaretColor = true; string value = ConfigManager.ClientRequestedNumber.Value; if (!string.IsNullOrEmpty(value)) { numberInputField.text = value; } Plugin.Logger.LogInfo((object)"CustomNumbers: Phone number display created"); return val; } private static void DumpHierarchy(Transform t, string indent) { //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00cf: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) string text = ""; Component[] components = ((Component)t).GetComponents<Component>(); foreach (Component val in components) { if ((Object)(object)val != (Object)null && !(val is Transform) && !(val is RectTransform)) { text = text + " [" + ((object)val).GetType().Name + "]"; } } TextMeshProUGUI component = ((Component)t).GetComponent<TextMeshProUGUI>(); string text2 = (((Object)(object)component != (Object)null) ? (" text='" + ((TMP_Text)component).text + "'") : ""); RectTransform component2 = ((Component)t).GetComponent<RectTransform>(); string text3 = (((Object)(object)component2 != (Object)null) ? $" pos={component2.anchoredPosition} size={component2.sizeDelta} anchors=({component2.anchorMin},{component2.anchorMax})" : ""); Plugin.Logger.LogInfo((object)(indent + ((Object)t).name + text + text2 + text3)); for (int j = 0; j < t.childCount; j++) { DumpHierarchy(t.GetChild(j), indent + " "); } } [HarmonyPatch("SaveCustomization")] [HarmonyPostfix] public static void SaveCustomizationPostfix() { if ((Object)(object)numberInputField != (Object)null) { string text = numberInputField.text; if (text.Length > 0 && text.Length < 4) { text = text.PadLeft(4, '0'); } ConfigManager.ClientRequestedNumber.Value = text; ConfigManager.Config.Save(); Plugin.Logger.LogInfo((object)("CustomNumbers: Saved phone number: " + text)); } } } internal static class DelayedPhoneInitializationPatch { private const float InitializationTimeoutSeconds = 60f; private const float RegistrationRetrySeconds = 2f; private static readonly MethodBase ConnectMethod = AccessTools.Method(typeof(PlayerControllerB), "ConnectClientToPlayerObject", (Type[])null, (Type[])null); private static readonly Type UpstreamPatchType = typeof(PhoneNetworkHandler).Assembly.GetType("Scoops.patch.PlayerPhonePatch", throwOnError: false); private static readonly MethodInfo UpstreamInitPhone = ((UpstreamPatchType == null) ? null : UpstreamPatchType.GetMethod("InitPhone", BindingFlags.Static | BindingFlags.NonPublic, null, new Type[1] { typeof(PlayerControllerB).MakeByRefType() }, null)); private static readonly MethodInfo GuardedPostfix = AccessTools.Method(typeof(DelayedPhoneInitializationPatch), "ConnectClientToPlayerObjectPostfix", (Type[])null, (Type[])null); private static readonly HashSet<int> StartedPlayers = new HashSet<int>(); private static readonly HashSet<ulong> AcknowledgedPhoneIds = new HashSet<ulong>(); private static ConfigEntry<bool> harnessEnabled; private static ConfigEntry<float> harnessDelaySeconds; private static bool installed; internal static void Install(Harmony harmony) { //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Expected O, but got Unknown if (installed) { return; } harnessEnabled = ((BaseUnityPlugin)Plugin.Instance).Config.Bind<bool>("Runtime Test Harness", "Enabled", false, "Enables CustomNumbers timing tests. Keep false during normal play."); harnessDelaySeconds = ((BaseUnityPlugin)Plugin.Instance).Config.Bind<float>("Runtime Test Harness", "Phone Initialization Delay Seconds", 0f, "Optional guarded phone initialization delay used only by the runtime harness."); if (harmony == null || ConnectMethod == null || UpstreamInitPhone == null || GuardedPostfix == null) { Plugin.Logger.LogError((object)"Could not resolve the exact LethalPhones 1.3.23 phone initialization methods; its original postfix was left untouched."); return; } bool flag = HasPostfix(Harmony.GetPatchInfo(ConnectMethod), GuardedPostfix); if (!flag) { harmony.Patch(ConnectMethod, (HarmonyMethod)null, new HarmonyMethod(GuardedPostfix), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); flag = HasPostfix(Harmony.GetPatchInfo(ConnectMethod), GuardedPostfix); } if (!flag) { Plugin.Logger.LogError((object)"The guarded phone initialization postfix could not be installed; LethalPhones' original postfix was left untouched."); return; } harmony.Unpatch(ConnectMethod, UpstreamInitPhone); if (HasPostfix(Harmony.GetPatchInfo(ConnectMethod), UpstreamInitPhone)) { harmony.Unpatch(ConnectMethod, GuardedPostfix); Plugin.Logger.LogError((object)"LethalPhones' immediate phone initialization postfix could not be removed; the guarded postfix was rolled back to avoid duplicate initialization."); } else { installed = true; Plugin.Logger.LogInfo((object)"Installed guarded phone initialization, then removed only LethalPhones' exact immediate postfix."); } } private static bool HasPostfix(Patches patches, MethodInfo method) { if (patches == null || method == null) { return false; } foreach (Patch postfix in patches.Postfixes) { if (postfix.PatchMethod == method) { return true; } } return false; } private static void ConnectClientToPlayerObjectPostfix(PlayerControllerB __instance) { if ((Object)(object)__instance == (Object)null) { Plugin.Logger.LogWarning((object)"Skipped guarded phone initialization because the player object was null."); return; } int instanceID = ((Object)__instance).GetInstanceID(); if (!StartedPlayers.Add(instanceID)) { Plugin.Logger.LogInfo((object)("Guarded phone initialization is already active or complete for " + __instance.playerUsername + ".")); } else { ((MonoBehaviour)__instance).StartCoroutine(InitializeWhenReady(__instance, instanceID)); } } private static IEnumerator InitializeWhenReady(PlayerControllerB player, int instanceId) { float num = ((harnessEnabled != null && harnessEnabled.Value && harnessDelaySeconds != null) ? Mathf.Max(0f, harnessDelaySeconds.Value) : 0f); if (num > 0f) { Plugin.Logger.LogInfo((object)$"Runtime harness delaying guarded phone initialization by {num:0.###} seconds."); float delayEnd = Time.realtimeSinceStartup + num; while ((Object)(object)player != (Object)null && Time.realtimeSinceStartup < delayEnd) { yield return null; } } if ((Object)(object)player == (Object)null) { StartedPlayers.Remove(instanceId); yield break; } float deadline = Time.realtimeSinceStartup + 60f; string missing = "startup prerequisites"; PhoneNetworkHandler handler = null; PlayerPhone phone = null; while ((Object)(object)player != (Object)null && Time.realtimeSinceStartup < deadline && !TryGetNetworkPrerequisites(player, out handler, out phone, out missing)) { yield return null; } if ((Object)(object)player == (Object)null || (Object)(object)handler == (Object)null || (Object)(object)phone == (Object)null) { StartedPlayers.Remove(instanceId); string arg = (((Object)(object)player == (Object)null) ? "<destroyed player>" : player.playerUsername); Plugin.Logger.LogError((object)($"Guarded phone registration timed out after {60f:0} seconds for {arg}; " + "still missing " + missing + ".")); yield break; } AcknowledgedPhoneIds.Remove(((NetworkBehaviour)phone).NetworkObjectId); bool setupInvoked = false; bool setupFailed = false; float nextRegistrationAttempt = 0f; int attempts = 0; while ((Object)(object)player != (Object)null && (Object)(object)phone != (Object)null && Time.realtimeSinceStartup < deadline) { if (!AcknowledgedPhoneIds.Contains(((NetworkBehaviour)phone).NetworkObjectId) && Time.realtimeSinceStartup >= nextRegistrationAttempt) { attempts++; try { handler.CreateNewPhone(((NetworkBehaviour)phone).NetworkObjectId, CustomizationManager.SelectedSkin, CustomizationManager.SelectedCharm, CustomizationManager.SelectedRingtone); Plugin.Logger.LogInfo((object)$"Guarded phone registration request {attempts} sent for {player.playerUsername} ({((NetworkBehaviour)phone).NetworkObjectId})."); } catch (Exception arg2) { Plugin.Logger.LogError((object)$"Guarded phone registration request {attempts} failed for {player.playerUsername}: {arg2}"); } nextRegistrationAttempt = Time.realtimeSinceStartup + 2f; } if (!setupInvoked && !setupFailed && ModelAnchorsReady(player, out missing)) { try { object[] parameters = new object[1] { player }; UpstreamInitPhone.Invoke(null, parameters); setupInvoked = true; Plugin.Logger.LogInfo((object)("Guarded LethalPhones model/input setup completed for " + player.playerUsername + ".")); } catch (TargetInvocationException ex) { setupFailed = true; Exception arg3 = ex.InnerException ?? ex; Plugin.Logger.LogError((object)$"Guarded LethalPhones model/input setup failed for {player.playerUsername}: {arg3}"); } catch (Exception arg4) { setupFailed = true; Plugin.Logger.LogError((object)$"Guarded LethalPhones model/input setup failed for {player.playerUsername}: {arg4}"); } } bool flag = AcknowledgedPhoneIds.Contains(((NetworkBehaviour)phone).NetworkObjectId); if (flag && setupInvoked) { Plugin.Logger.LogInfo((object)$"Guarded phone initialization acknowledged for {player.playerUsername} after {attempts} request(s)."); yield break; } if (flag && setupFailed) { Plugin.Logger.LogError((object)("Phone number registration was acknowledged for " + player.playerUsername + ", but LethalPhones model/input setup did not complete.")); yield break; } yield return null; } bool flag2 = (Object)(object)phone != (Object)null && AcknowledgedPhoneIds.Contains(((NetworkBehaviour)phone).NetworkObjectId); if (!flag2) { StartedPlayers.Remove(instanceId); } string text = (((Object)(object)player == (Object)null) ? "<destroyed player>" : player.playerUsername); Plugin.Logger.LogError((object)("Guarded phone initialization timed out for " + text + ": " + $"number acknowledged={flag2}, model/input setup complete={setupInvoked}, last missing={missing}.")); } private static bool TryGetNetworkPrerequisites(PlayerControllerB player, out PhoneNetworkHandler handler, out PlayerPhone phone, out string missing) { handler = PhoneNetworkHandler.Instance; phone = null; if ((Object)(object)handler == (Object)null || !((NetworkBehaviour)handler).IsSpawned || (Object)(object)((NetworkBehaviour)handler).NetworkManager == (Object)null) { missing = "the spawned PhoneNetworkHandler"; return false; } Transform val = ((Component)player).transform.Find("PhonePrefab(Clone)"); if ((Object)(object)val == (Object)null) { missing = "the player's PhonePrefab(Clone) child"; return false; } phone = ((Component)val).GetComponent<PlayerPhone>(); NetworkObject component = ((Component)val).GetComponent<NetworkObject>(); if ((Object)(object)phone == (Object)null || (Object)(object)component == (Object)null || !component.IsSpawned) { missing = "the spawned PlayerPhone NetworkObject"; return false; } if (!((Component)phone).transform.IsChildOf(((Component)player).transform)) { missing = "the PlayerPhone parent relationship"; return false; } if (((NetworkBehaviour)handler).NetworkManager.SpawnManager == null || !((NetworkBehaviour)handler).NetworkManager.SpawnManager.SpawnedObjects.TryGetValue(component.NetworkObjectId, out var value) || (Object)(object)value != (Object)(object)component) { missing = "the PlayerPhone entry in SpawnManager"; return false; } if ((Object)(object)GameNetworkManager.Instance == (Object)null || (Object)(object)GameNetworkManager.Instance.localPlayerController == (Object)null) { missing = "the local player controller"; return false; } missing = null; return true; } private static bool ModelAnchorsReady(PlayerControllerB player, out string missing) { if ((Object)(object)GameNetworkManager.Instance.localPlayerController == (Object)(object)player) { if ((Object)(object)player.playerGlobalHead == (Object)null || (Object)(object)player.localArmsTransform == (Object)null || (Object)(object)player.localArmsTransform.Find("shoulder.L/arm.L_upper/arm.L_lower/hand.L/LocalPhoneModel(Clone)") == (Object)null) { missing = "the local phone model anchors"; return false; } } else if ((Object)(object)player.lowerSpine == (Object)null || (Object)(object)player.lowerSpine.Find("spine.002/spine.003/shoulder.L/arm.L_upper/arm.L_lower/hand.L/ServerPhoneModel(Clone)") == (Object)null) { missing = "the remote phone model anchors"; return false; } missing = null; return true; } internal static void Acknowledge(PlayerPhone phone) { PlayerControllerB val = (((Object)(object)GameNetworkManager.Instance == (Object)null) ? null : GameNetworkManager.Instance.localPlayerController); if (!((Object)(object)phone == (Object)null) && !((Object)(object)val == (Object)null) && ((Component)phone).transform.IsChildOf(((Component)val).transform)) { AcknowledgedPhoneIds.Add(((NetworkBehaviour)phone).NetworkObjectId); } } internal static void ResetSession() { StartedPlayers.Clear(); AcknowledgedPhoneIds.Clear(); } internal static void Forget(PlayerControllerB player) { if (!((Object)(object)player == (Object)null)) { StartedPlayers.Remove(((Object)player).GetInstanceID()); Transform val = ((Component)player).transform.Find("PhonePrefab(Clone)"); PlayerPhone val2 = (((Object)(object)val == (Object)null) ? null : ((Component)val).GetComponent<PlayerPhone>()); if ((Object)(object)val2 != (Object)null) { AcknowledgedPhoneIds.Remove(((NetworkBehaviour)val2).NetworkObjectId); } } } } [HarmonyPatch(typeof(PlayerControllerB), "OnDestroy")] internal static class DelayedPhoneInitializationCleanupPatch { [HarmonyPostfix] private static void OnDestroyPostfix(PlayerControllerB __instance) { DelayedPhoneInitializationPatch.Forget(__instance); } } [HarmonyPatch(typeof(MenuManager))] public class ForceUIPatch { [HarmonyPatch("Start")] [HarmonyPostfix] public static void StartPostfix(MenuManager __instance) { if (!__instance.isInitScene) { ((MonoBehaviour)__instance).StartCoroutine(DelayedMainMenuInjection()); } } private static IEnumerator DelayedMainMenuInjection() { yield return (object)new WaitForSeconds(0f); GameObject val = GameObject.Find("MenuContainer"); if (!Object.op_Implicit((Object)(object)val)) { yield break; } Transform val2 = val.transform.Find("MainButtons"); if (!Object.op_Implicit((Object)(object)val2) || (Object)(object)((IEnumerable<TextMeshProUGUI>)((Component)val2).GetComponentsInChildren<TextMeshProUGUI>()).FirstOrDefault((Func<TextMeshProUGUI, bool>)((TextMeshProUGUI t) => ((TMP_Text)t).text == "> Personalize Phone")) != (Object)null) { yield break; } Transform val3 = val2.Find("QuitButton"); if (!Object.op_Implicit((Object)(object)val3)) { yield break; } Plugin.Logger.LogInfo((object)"LethalPhonesCustomNumbers: Forcing Customization GUI to spawn because LethalPhones skipped it..."); MethodInfo methodInfo = typeof(CustomizationManager).Assembly.GetType("Scoops.customization.CustomizationMenuUtils")?.GetMethod("InjectMenu", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (methodInfo != null) { methodInfo.Invoke(null, new object[2] { val2, ((Component)val3).gameObject }); object? obj = AccessTools.Field(typeof(CustomizationManager), "customizationPanel")?.GetValue(null); Transform val4 = (Transform)((obj is Transform) ? obj : null); if ((Object)(object)val4 != (Object)null) { Plugin.Logger.LogInfo((object)"Dumping CustomizationPanel hierarchy:"); DumpTransform(val4, ""); } } else { Plugin.Logger.LogError((object)"LethalPhonesCustomNumbers: Failed to find CustomizationMenuUtils.InjectMenu via reflection!"); } } private static void DumpTransform(Transform t, string indent) { Plugin.Logger.LogInfo((object)(indent + ((Object)t).name + " (" + ((object)t).GetType().Name + ")")); Component[] components = ((Component)t).GetComponents<Component>(); foreach (Component val in components) { if ((Object)(object)val != (Object)null && ((object)val).GetType() != typeof(Transform)) { Plugin.Logger.LogInfo((object)(indent + " - " + ((object)val).GetType().Name)); } } for (int j = 0; j < t.childCount; j++) { DumpTransform(t.GetChild(j), indent + " "); } } } [HarmonyPatch(typeof(PhoneNetworkHandler))] internal static class PhoneNetworkHandlerPatch { private sealed class DictionaryRecoverySnapshot { internal readonly List<KeyValuePair<short, ulong>> NumberEntries = new List<KeyValuePair<short, ulong>>(); internal readonly List<KeyValuePair<short, PhoneBehavior>> ObjectEntries = new List<KeyValuePair<short, PhoneBehavior>>(); internal short PreferredNumber = -1; internal bool HasEntries { get { if (NumberEntries.Count <= 0) { return ObjectEntries.Count > 0; } return true; } } } private const int MaximumFourDigitNumber = 10000; private const float PhoneListResolveTimeoutSeconds = 60f; private const int MaximumPhoneListRefreshAttempts = 3; private static readonly FieldInfo PhoneNumberDictField = AccessTools.Field(typeof(PhoneNetworkHandler), "phoneNumberDict"); private static readonly FieldInfo PhoneObjectDictField = AccessTools.Field(typeof(PhoneNetworkHandler), "phoneObjectDict"); private static int phoneListGeneration; private static int phoneListRefreshAttempts; private static PhoneNetworkHandler phoneListHandler; private static NetworkManager phoneListNetworkManager; private static readonly Dictionary<ulong, short> ReadyPhoneNumbers = new Dictionary<ulong, short>(); [HarmonyPatch("OnNetworkSpawn")] [HarmonyPostfix] private static void OnNetworkSpawnPostfix(PhoneNetworkHandler __instance) { phoneListGeneration++; phoneListRefreshAttempts = 0; phoneListHandler = __instance; phoneListNetworkManager = (((Object)(object)__instance == (Object)null) ? null : ((NetworkBehaviour)__instance).NetworkManager); ReadyPhoneNumbers.Clear(); DelayedPhoneInitializationPatch.ResetSession(); } [HarmonyPatch("CreateNewPhoneNumberServerRpc")] [HarmonyPrefix] private static bool CreateNewPhoneNumberServerRpcPrefix(ulong phoneId, ref string skinId, string charmId, string ringtoneId, PhoneNetworkHandler __instance, ref ServerRpcParams serverRpcParams) { if ((Object)(object)__instance == (Object)null) { Plugin.Logger.LogError((object)"Custom number RPC received without a PhoneNetworkHandler instance; using LethalPhones fallback."); return true; } if (!((NetworkBehaviour)__instance).IsServer || !RpcExecutionStage.IsExecute((NetworkBehaviour)(object)__instance)) { return true; } string requestedNumber = ExtractRequestedNumber(ref skinId); ulong senderClientId = serverRpcParams.Receive.SenderClientId; if (!TryResolveRegistration(__instance, phoneId, senderClientId, out var networkObject, out var phone, out var senderPlayer, out var useLethalPhonesFallback)) { return useLethalPhonesFallback; } if (PhoneNumberDictField == null || PhoneObjectDictField == null) { Plugin.Logger.LogError((object)"LethalPhones phone dictionaries were not found; using its original number assignment."); return true; } Dictionary<short, ulong> dictionary = PhoneNumberDictField.GetValue(__instance) as Dictionary<short, ulong>; Dictionary<short, PhoneBehavior> dictionary2 = PhoneObjectDictField.GetValue(__instance) as Dictionary<short, PhoneBehavior>; if (dictionary == null || dictionary2 == null) { Plugin.Logger.LogError((object)"LethalPhones phone dictionaries were unavailable; using its original number assignment."); return true; } if (TryGetConsistentRegistration(phoneId, phone, dictionary, dictionary2, out var phoneNumber)) { ResynchronizeExistingRegistration(__instance, phoneId, senderClientId, skinId, charmId, ringtoneId, networkObject, phone, phoneNumber); return false; } DictionaryRecoverySnapshot dictionaryRecoverySnapshot = CaptureAndRemoveStaleEntries(phoneId, phone, dictionary, dictionary2); int value = Config.maxPhoneNumber.Value; if (value <= 0) { RestoreDictionaryEntries(dictionaryRecoverySnapshot, dictionary, dictionary2); Plugin.Logger.LogError((object)$"Rejecting phone registration because maxPhoneNumber is {value}; it must be at least 1."); return false; } int num = Math.Min(value, 10000); if (value > 10000) { Plugin.Logger.LogWarning((object)$"maxPhoneNumber is {value}; CustomNumbers restricts player phones to the four-digit range 0000-9999."); } ulong playerSteamId = senderPlayer.playerSteamId; if (!TryChooseNumber(requestedNumber, playerSteamId, senderClientId, phoneId, num, dictionaryRecoverySnapshot.PreferredNumber, dictionary, dictionary2, out var phoneNumber2, out var allocationSource)) { RestoreDictionaryEntries(dictionaryRecoverySnapshot, dictionary, dictionary2); Plugin.Logger.LogError((object)$"Cannot register phone {phoneId}: all {num} allowed numbers are occupied or reserved."); return false; } ulong ownerClientId = networkObject.OwnerClientId; short phoneNumber3 = ((PhoneBehavior)phone).phoneNumber; string phoneSkinId = ((PhoneBehavior)phone).phoneSkinId; string phoneCharmId = ((PhoneBehavior)phone).phoneCharmId; string phoneRingtoneId = ((PhoneBehavior)phone).phoneRingtoneId; bool ownershipChanged = false; bool numberAdded = false; bool objectAdded = false; try { if (ownerClientId != senderClientId) { networkObject.ChangeOwnership(senderClientId); ownershipChanged = true; } ((PhoneBehavior)phone).phoneSkinId = skinId; ((PhoneBehavior)phone).phoneCharmId = charmId; ((PhoneBehavior)phone).phoneRingtoneId = ringtoneId; dictionary.Add(phoneNumber2, phoneId); numberAdded = true; dictionary2.Add(phoneNumber2, (PhoneBehavior)(object)phone); objectAdded = true; ((PhoneBehavior)phone).SetNewPhoneNumberClientRpc(phoneNumber2); __instance.RequestClientUpdates(); Plugin.Logger.LogInfo((object)($"Registered phone {phoneId} for {senderPlayer.playerUsername} ({playerSteamId}) as {phoneNumber2:D4} " + $"from {allocationSource}; synchronized {dictionary2.Count} phone(s).")); return false; } catch (Exception arg) { RollBackRegistration(__instance, phoneId, phoneNumber2, ownerClientId, phoneNumber3, phoneSkinId, phoneCharmId, phoneRingtoneId, ownershipChanged, numberAdded, objectAdded, networkObject, phone, dictionary, dictionary2, dictionaryRecoverySnapshot); Plugin.Logger.LogError((object)$"Custom number registration failed for phone {phoneId}; the partial registration was rolled back. {arg}"); return false; } } [HarmonyPatch("UpdateAllPhonesListClientRpc")] [HarmonyPrefix] private static bool UpdateAllPhonesListClientRpcPrefix(ulong[] phoneIds, PhoneNetworkHandler __instance) { if ((Object)(object)__instance == (Object)null || ((NetworkBehaviour)__instance).IsServer) { return true; } NetworkManager networkManager = ((NetworkBehaviour)__instance).NetworkManager; if ((Object)(object)phoneListHandler != (Object)(object)__instance || (Object)(object)phoneListNetworkManager != (Object)(object)networkManager) { phoneListGeneration++; phoneListRefreshAttempts = 0; phoneListHandler = __instance; phoneListNetworkManager = networkManager; ReadyPhoneNumbers.Clear(); DelayedPhoneInitializationPatch.ResetSession(); } int num = ++phoneListGeneration; ulong[] array = ((phoneIds == null) ? new ulong[0] : ((ulong[])phoneIds.Clone())); ((MonoBehaviour)__instance).StartCoroutine(ApplyPhoneListWhenSpawned(__instance, array, num)); Plugin.Logger.LogInfo((object)$"Deferred client phone-list generation {num} until {array.Length} referenced phone(s) are spawned."); return false; } private static IEnumerator ApplyPhoneListWhenSpawned(PhoneNetworkHandler handler, ulong[] phoneIds, int generation) { float deadline = Time.realtimeSinceStartup + 60f; List<PhoneBehavior> resolved = new List<PhoneBehavior>(phoneIds.Length); List<ulong> unresolved = new List<ulong>(); while (generation == phoneListGeneration && (Object)(object)handler != (Object)null && (Object)(object)handler == (Object)(object)phoneListHandler && (Object)(object)((NetworkBehaviour)handler).NetworkManager == (Object)(object)phoneListNetworkManager) { resolved.Clear(); unresolved.Clear(); NetworkManager networkManager = ((NetworkBehaviour)handler).NetworkManager; if ((Object)(object)networkManager == (Object)null || networkManager.SpawnManager == null) { unresolved.AddRange(phoneIds); } else { foreach (ulong num in phoneIds) { if (!networkManager.SpawnManager.SpawnedObjects.TryGetValue(num, out var value) || (Object)(object)value == (Object)null || !value.IsSpawned) { unresolved.Add(num); continue; } PhoneBehavior component = ((Component)value).GetComponent<PhoneBehavior>(); if ((Object)(object)component == (Object)null || !ReadyPhoneNumbers.TryGetValue(num, out var value2) || component.phoneNumber != value2) { unresolved.Add(num); } else { resolved.Add(component); } } } if (unresolved.Count == 0) { if (generation == phoneListGeneration && !((Object)(object)handler != (Object)(object)PhoneNetworkHandler.Instance)) { if (PhoneNetworkHandler.allPhoneBehaviors == null) { PhoneNetworkHandler.allPhoneBehaviors = new List<PhoneBehavior>(); } PhoneNetworkHandler.allPhoneBehaviors.Clear(); PhoneNetworkHandler.allPhoneBehaviors.AddRange(resolved); phoneListRefreshAttempts = 0; PhoneNetworkHandler.phoneListUpdateEvent.Invoke(); Plugin.Logger.LogInfo((object)($"Applied client phone-list generation {generation} with {resolved.Count} spawned phone(s); " + "clipboard and switchboard listeners were notified.")); } break; } if (Time.realtimeSinceStartup >= deadline) { if ((Object)(object)handler == (Object)(object)phoneListHandler && (Object)(object)((NetworkBehaviour)handler).NetworkManager == (Object)(object)phoneListNetworkManager && phoneListRefreshAttempts < 3) { phoneListRefreshAttempts++; Plugin.Logger.LogWarning((object)($"Phone-list generation {generation} still has unresolved IDs after {60f:0} seconds: " + string.Format("{0}. Requesting fresh snapshot {1}/{2}.", string.Join(", ", unresolved), phoneListRefreshAttempts, 3))); try { handler.RequestPhoneListUpdates(); break; } catch (Exception arg) { Plugin.Logger.LogError((object)$"Fresh phone-list request failed: {arg}"); break; } } Plugin.Logger.LogError((object)($"Phone-list generation {generation} timed out without overwriting the current list. " + "Unresolved network object IDs: " + string.Join(", ", unresolved))); break; } yield return null; } } internal static void MarkPhoneReady(PhoneBehavior phone, short number) { if (!((Object)(object)phone == (Object)null) && !((Object)(object)((NetworkBehaviour)phone).NetworkManager == (Object)null) && (!((Object)(object)phoneListNetworkManager != (Object)null) || !((Object)(object)((NetworkBehaviour)phone).NetworkManager != (Object)(object)phoneListNetworkManager))) { ReadyPhoneNumbers[((NetworkBehaviour)phone).NetworkObjectId] = number; PlayerPhone val = (PlayerPhone)(object)((phone is PlayerPhone) ? phone : null); if (val != null) { DelayedPhoneInitializationPatch.Acknowledge(val); } } } private static string ExtractRequestedNumber(ref string skinId) { if (string.IsNullOrEmpty(skinId)) { return null; } int num = skinId.IndexOf('|'); if (num < 0) { return null; } string result = skinId.Substring(num + 1).Trim(); skinId = skinId.Substring(0, num); return result; } private static bool TryResolveRegistration(PhoneNetworkHandler handler, ulong phoneId, ulong clientId, out NetworkObject networkObject, out PlayerPhone phone, out PlayerControllerB senderPlayer, out bool useLethalPhonesFallback) { networkObject = null; phone = null; senderPlayer = null; useLethalPhonesFallback = false; NetworkManager networkManager = ((NetworkBehaviour)handler).NetworkManager; if ((Object)(object)networkManager == (Object)null || networkManager.SpawnManager == null || !networkManager.SpawnManager.SpawnedObjects.TryGetValue(phoneId, out networkObject) || (Object)(object)networkObject == (Object)null || !networkObject.IsSpawned || networkObject.NetworkObjectId != phoneId) { Plugin.Logger.LogWarning((object)$"Rejected phone {phoneId}: its spawned NetworkObject is unavailable."); return false; } PhoneBehavior component = ((Component)networkObject).GetComponent<PhoneBehavior>(); if ((Object)(object)component == (Object)null) { Plugin.Logger.LogWarning((object)$"Rejected phone {phoneId}: the requested NetworkObject is not a LethalPhones PhoneBehavior."); return false; } phone = (PlayerPhone)(object)((component is PlayerPhone) ? component : null); if ((Object)(object)phone == (Object)null) { useLethalPhonesFallback = clientId == 0L && networkObject.OwnerClientId == 0; if (useLethalPhonesFallback) { Plugin.Logger.LogInfo((object)$"Server-owned non-player phone {phoneId} is using LethalPhones registration."); } else { Plugin.Logger.LogWarning((object)$"Rejected non-player phone {phoneId}: sender {clientId} or ownership did not match the server."); } return false; } senderPlayer = FindPlayer(clientId); if ((Object)(object)senderPlayer == (Object)null) { Plugin.Logger.LogWarning((object)$"Rejected phone {phoneId}: sender client {clientId} has no active player object."); return false; } if ((Object)(object)((NetworkBehaviour)senderPlayer).NetworkObject == (Object)null || ((NetworkBehaviour)senderPlayer).NetworkObject.OwnerClientId != clientId) { Plugin.Logger.LogWarning((object)$"Rejected phone {phoneId}: sender client {clientId} does not own player {senderPlayer.playerUsername}."); return false; } if ((Object)(object)((Component)phone).GetComponentInParent<PlayerControllerB>() != (Object)(object)senderPlayer || !((Component)phone).transform.IsChildOf(((Component)senderPlayer).transform)) { Plugin.Logger.LogWarning((object)$"Rejected phone {phoneId}: it is not parented to sender {senderPlayer.playerUsername} ({clientId})."); return false; } return true; } private static PlayerControllerB FindPlayer(ulong clientId) { if ((Object)(object)StartOfRound.Instance == (Object)null || StartOfRound.Instance.allPlayerScripts == null) { return null; } PlayerControllerB[] allPlayerScripts = StartOfRound.Instance.allPlayerScripts; foreach (PlayerControllerB val in allPlayerScripts) { if ((Object)(object)val != (Object)null && val.actualClientId == clientId) { return val; } } return null; } private static bool TryGetConsistentRegistration(ulong phoneId, PlayerPhone phone, Dictionary<short, ulong> phoneNumberDict, Dictionary<short, PhoneBehavior> phoneObjectDict, out short phoneNumber) { int num = 0; int num2 = 0; short num3 = -1; short num4 = -1; foreach (KeyValuePair<short, ulong> item in phoneNumberDict) { if (item.Value == phoneId) { num++; num3 = item.Key; } } foreach (KeyValuePair<short, PhoneBehavior> item2 in phoneObjectDict) { if ((Object)(object)item2.Value == (Object)(object)phone) { num2++; num4 = item2.Key; } } phoneNumber = num3; if (num == 1 && num2 == 1 && num3 == num4 && phoneNumberDict.TryGetValue(num3, out var value) && value == phoneId && phoneObjectDict.TryGetValue(num3, out var value2)) { return (Object)(object)value2 == (Object)(object)phone; } return false; } private static void ResynchronizeExistingRegistration(PhoneNetworkHandler handler, ulong phoneId, ulong clientId, string skinId, string charmId, string ringtoneId, NetworkObject networkObject, PlayerPhone phone, short phoneNumber) { ulong ownerClientId = networkObject.OwnerClientId; string phoneSkinId = ((PhoneBehavior)phone).phoneSkinId; string phoneCharmId = ((PhoneBehavior)phone).phoneCharmId; string phoneRingtoneId = ((PhoneBehavior)phone).phoneRingtoneId; bool flag = false; try { if (ownerClientId != clientId) { networkObject.ChangeOwnership(clientId); flag = true; } ((PhoneBehavior)phone).phoneSkinId = skinId; ((PhoneBehavior)phone).phoneCharmId = charmId; ((PhoneBehavior)phone).phoneRingtoneId = ringtoneId; ((PhoneBehavior)phone).SetNewPhoneNumberClientRpc(phoneNumber); handler.RequestClientUpdates(); Plugin.Logger.LogInfo((object)($"Duplicate registration for phone {phoneId} is already complete as {phoneNumber:D4}; " + "ownership and clients were synchronized idempotently.")); } catch (Exception arg) { ((PhoneBehavior)phone).phoneSkinId = phoneSkinId; ((PhoneBehavior)phone).phoneCharmId = phoneCharmId; ((PhoneBehavior)phone).phoneRingtoneId = phoneRingtoneId; try { if (flag && networkObject.IsSpawned) { networkObject.ChangeOwnership(ownerClientId); } ((PhoneBehavior)phone).SetNewPhoneNumberClientRpc(phoneNumber); handler.RequestClientUpdates(); } catch (Exception arg2) { Plugin.Logger.LogError((object)$"Duplicate phone {phoneId} rollback also failed: {arg2}"); } Plugin.Logger.LogError((object)$"Failed to resynchronize duplicate phone {phoneId}; ownership and cosmetics were rolled back. {arg}"); } } private static DictionaryRecoverySnapshot CaptureAndRemoveStaleEntries(ulong phoneId, PlayerPhone phone, Dictionary<short, ulong> phoneNumberDict, Dictionary<short, PhoneBehavior> phoneObjectDict) { DictionaryRecoverySnapshot dictionaryRecoverySnapshot = new DictionaryRecoverySnapshot(); foreach (KeyValuePair<short, ulong> item in phoneNumberDict) { if (item.Value == phoneId) { dictionaryRecoverySnapshot.NumberEntries.Add(item); } } foreach (KeyValuePair<short, PhoneBehavior> item2 in phoneObjectDict) { if ((Object)(object)item2.Value == (Object)(object)phone) { dictionaryRecoverySnapshot.ObjectEntries.Add(item2); } } foreach (KeyValuePair<short, ulong> numberEntry in dictionaryRecoverySnapshot.NumberEntries) { foreach (KeyValuePair<short, PhoneBehavior> objectEntry in dictionaryRecoverySnapshot.ObjectEntries) { if (numberEntry.Key == objectEntry.Key) { dictionaryRecoverySnapshot.PreferredNumber = numberEntry.Key; break; } } if (dictionaryRecoverySnapshot.PreferredNumber >= 0) { break; } } if (dictionaryRecoverySnapshot.PreferredNumber < 0 && dictionaryRecoverySnapshot.NumberEntries.Count > 0) { dictionaryRecoverySnapshot.PreferredNumber = dictionaryRecoverySnapshot.NumberEntries[0].Key; } else if (dictionaryRecoverySnapshot.PreferredNumber < 0 && dictionaryRecoverySnapshot.ObjectEntries.Count > 0) { dictionaryRecoverySnapshot.PreferredNumber = dictionaryRecoverySnapshot.ObjectEntries[0].Key; } foreach (KeyValuePair<short, ulong> numberEntry2 in dictionaryRecoverySnapshot.NumberEntries) { if (phoneNumberDict.TryGetValue(numberEntry2.Key, out var value) && value == phoneId) { phoneNumberDict.Remove(numberEntry2.Key); } } foreach (KeyValuePair<short, PhoneBehavior> objectEntry2 in dictionaryRecoverySnapshot.ObjectEntries) { if (phoneObjectDict.TryGetValue(objectEntry2.Key, out var value2) && (Object)(object)value2 == (Object)(object)phone) { phoneObjectDict.Remove(objectEntry2.Key); } } if (dictionaryRecoverySnapshot.HasEntries) { Plugin.Logger.LogWarning((object)($"Repairing stale registration state for phone {phoneId}; removed " + $"{dictionaryRecoverySnapshot.NumberEntries.Count} number entry/entries and {dictionaryRecoverySnapshot.ObjectEntries.Count} object entry/entries transactionally.")); } return dictionaryRecoverySnapshot; } private static void RestoreDictionaryEntries(DictionaryRecoverySnapshot snapshot, Dictionary<short, ulong> phoneNumberDict, Dictionary<short, PhoneBehavior> phoneObjectDict) { if (snapshot == null || !snapshot.HasEntries) { return; } foreach (KeyValuePair<short, ulong> numberEntry in snapshot.NumberEntries) { if (!phoneNumberDict.ContainsKey(numberEntry.Key)) { phoneNumberDict.Add(numberEntry.Key, numberEntry.Value); } } foreach (KeyValuePair<short, PhoneBehavior> objectEntry in snapshot.ObjectEntries) { if (!phoneObjectDict.ContainsKey(objectEntry.Key)) { phoneObjectDict.Add(objectEntry.Key, objectEntry.Value); } } } private static bool TryChooseNumber(string requestedNumber, ulong steamId, ulong clientId, ulong phoneId, int numberSpace, short recoveredNumber, Dictionary<short, ulong> phoneNumberDict, Dictionary<short, PhoneBehavior> phoneObjectDict, out short phoneNumber, out string allocationSource) { phoneNumber = -1; allocationSource = null; if (recoveredNumber >= 0 && recoveredNumber < numberSpace && IsAvailable(recoveredNumber, phoneNumberDict, phoneObjectDict)) { phoneNumber = recoveredNumber; allocationSource = "recovered registration"; return true; } if (!string.IsNullOrEmpty(requestedNumber)) { if (TryParseFourDigitNumber(requestedNumber, numberSpace, out var number) && IsAvailable(number, phoneNumberDict, phoneObjectDict)) { phoneNumber = number; allocationSource = "requested number"; return true; } Plugin.Logger.LogWarning((object)("Requested number '" + requestedNumber + "' is invalid, reserved, out of range, or already occupied; using a deterministic fallback.")); } if (steamId != 0L && ConfigManager.SteamIdToNumber.TryGetValue(steamId, out var value)) { if (value >= 0 && value < numberSpace && IsAvailable(value, phoneNumberDict, phoneObjectDict)) { phoneNumber = value; allocationSource = "Steam mapping"; return true; } Plugin.Logger.LogWarning((object)$"Mapped number {value:D4} for Steam ID {steamId} is unavailable or out of range; using its deterministic fallback."); } if (TryFindAvailableNumber((int)(((steamId != 0L) ? steamId : MixFallbackSeed(clientId, phoneId)) % (ulong)numberSpace), numberSpace, phoneNumberDict, phoneObjectDict, out phoneNumber)) { allocationSource = ((steamId != 0L) ? "Steam ID hash" : "client/object hash"); return true; } return false; } private static bool TryParseFourDigitNumber(string value, int numberSpace, out short number) { number = -1; if (value.Length != 4) { return false; } for (int i = 0; i < value.Length; i++) { if (value[i] < '0' || value[i] > '9') { return false; } } if (short.TryParse(value, out number) && number >= 0) { return number < numberSpace; } return false; } private static bool TryFindAvailableNumber(int start, int numberSpace, Dictionary<short, ulong> phoneNumberDict, Dictionary<short, PhoneBehavior> phoneObjectDict, out short phoneNumber) { for (int i = 0; i < numberSpace; i++) { short num = (short)((start + i) % numberSpace); if (IsAvailable(num, phoneNumberDict, phoneObjectDict)) { phoneNumber = num; return true; } } phoneNumber = -1; return false; } private static bool IsAvailable(short availableNumber, Dictionary<short, ulong> phoneNumberDict, Dictionary<short, PhoneBehavior> phoneObjectDict) { short result; bool flag = short.TryParse(Config.switchboardNumber.Value, out result) && availableNumber == result; if (!phoneNumberDict.ContainsKey(availableNumber) && !phoneObjectDict.ContainsKey(availableNumber)) { return !flag; } return false; } private static ulong MixFallbackSeed(ulong clientId, ulong phoneId) { long num = (long)clientId ^ ((long)phoneId + -7046029254386353131L + (long)(clientId << 6) + (long)(clientId >> 2)); long num2 = (num ^ (num >>> 30)) * -4658895280553007687L; long num3 = (num2 ^ (num2 >>> 27)) * -7723592293110705685L; return (ulong)(num3 ^ (num3 >>> 31)); } private static bool TryFindNumberByPhoneId(Dictionary<short, ulong> phoneNumberDict, ulong phoneId, out short phoneNumber) { foreach (KeyValuePair<short, ulong> item in phoneNumberDict) { if (item.Value == phoneId) { phoneNumber = item.Key; return true; } } phoneNumber = -1; return false; } private static bool TryFindNumberByPhone(Dictionary<short, PhoneBehavior> phoneObjectDict, PhoneBehavior phone, out short phoneNumber) { foreach (KeyValuePair<short, PhoneBehavior> item in phoneObjectDict) { if ((Object)(object)item.Value == (Object)(object)phone) { phoneNumber = item.Key; return true; } } phoneNumber = -1; return false; } private static void RollBackRegistration(PhoneNetworkHandler handler, ulong phoneId, short phoneNumber, ulong originalOwner, short originalNumber, string originalSkin, string originalCharm, string originalRingtone, bool ownershipChanged, bool numberAdded, bool objectAdded, NetworkObject networkObject, PlayerPhone phone, Dictionary<short, ulong> phoneNumberDict, Dictionary<short, PhoneBehavior> phoneObjectDict, DictionaryRecoverySnapshot recoverySnapshot) { if (objectAdded && phoneObjectDict.TryGetValue(phoneNumber, out var value) && (Object)(object)value == (Object)(object)phone) { phoneObjectDict.Remove(phoneNumber); } if (numberAdded && phoneNumberDict.TryGetValue(phoneNumber, out var value2) && value2 == phoneId) { phoneNumberDict.Remove(phoneNumber); } ((PhoneBehavior)phone).phoneNumber = originalNumber; ((PhoneBehavior)phone).phoneSkinId = originalSkin; ((PhoneBehavior)phone).phoneCharmId = originalCharm; ((PhoneBehavior)phone).phoneRingtoneId = originalRingtone; RestoreDictionaryEntries(recoverySnapshot, phoneNumberDict, phoneObjectDict); try { if (ownershipChanged && (Object)(object)networkObject != (Object)null && networkObject.IsSpawned) { networkObject.ChangeOwnership(originalOwner); } if ((Object)(object)handler != (Object)null && ((NetworkBehaviour)handler).IsServer && (Object)(object)phone != (Object)null && ((NetworkBehaviour)phone).IsSpawned) { ((PhoneBehavior)phone).SetNewPhoneNumberClientRpc(originalNumber); handler.RequestClientUpdates(); } } catch (Exception arg) { Plugin.Logger.LogError((object)$"Phone {phoneId} rollback synchronization also failed: {arg}"); } } } [BepInPlugin("com.ashby.lethalphonescustomnumbers", "LethalPhonesCustomNumbers", "1.2.1")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { public const string PluginGuid = "com.ashby.lethalphonescustomnumbers"; public const string PluginName = "LethalPhonesCustomNumbers"; public const string PluginVersion = "1.2.1"; private const string LegacyPluginGuid = "com.ashby.lethalphonespermanumber"; private Harmony _harmony; public static Plugin Instance { get; private set; } public static ManualLogSource Logger { get; private set; } public Dictionary<ulong, short> CustomNumbersMap { get; private set; } = new Dictionary<ulong, short>(); private void Awake() { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Expected O, but got Unknown Instance = this; Logger = Logger.CreateLogSource("LethalPhonesCustomNumbers"); Logger.LogInfo((object)"Plugin LethalPhonesCustomNumbers is loaded!"); ConfigManager.Init(((BaseUnityPlugin)this).Config); _harmony = new Harmony("com.ashby.lethalphonescustomnumbers"); _harmony.PatchAll(); DelayedPhoneInitializationPatch.Install(_harmony); Logger.LogInfo((object)"Patches applied successfully."); } private void Start() { if (Chainloader.PluginInfos.ContainsKey("com.ashby.lethalphonespermanumber")) { Harmony.UnpatchID("com.ashby.lethalphonespermanumber"); Logger.LogWarning((object)"Detected legacy LethalPhonesPermaNumber. Its overlapping Harmony patches were disabled; remove that legacy plugin from the modpack."); } } } internal static class RpcExecutionStage { private static readonly FieldInfo ExecutionStageField = AccessTools.Field(typeof(NetworkBehaviour), "__rpc_exec_stage"); private static bool missingFieldLogged; internal static bool IsExecute(NetworkBehaviour behaviour) { if ((Object)(object)behaviour == (Object)null || ExecutionStageField == null) { if (!missingFieldLogged) { missingFieldLogged = true; Plugin.Logger.LogError((object)"Unity Netcode RPC execution stage could not be resolved; CustomNumbers will not bypass generated RPC wrappers."); } return false; } object value = ExecutionStageField.GetValue(behaviour); if (value != null) { return string.Equals(value.ToString(), "Execute", StringComparison.Ordinal); } return false; } } [HarmonyPatch(typeof(PhoneBehavior), "SetNewPhoneNumberClientRpc")] internal static class PhoneNumberReadyPatch { [HarmonyPrefix] private static void Prefix(PhoneBehavior __instance, out bool __state) { __state = RpcExecutionStage.IsExecute((NetworkBehaviour)(object)__instance); } [HarmonyPostfix] private static void Postfix(PhoneBehavior __instance, short number, bool __state) { if (__state) { PhoneNetworkHandlerPatch.MarkPhoneReady(__instance, number); } } } [HarmonyPatch(typeof(PhoneBehavior), "PropogateInformationClientRpc")] internal static class PhoneInformationReadyPatch { [HarmonyPrefix] private static void Prefix(PhoneBehavior __instance, out bool __state) { __state = RpcExecutionStage.IsExecute((NetworkBehaviour)(object)__instance); } [HarmonyPostfix] private static void Postfix(PhoneBehavior __instance, short number, bool __state) { if (__state) { PhoneNetworkHandlerPatch.MarkPhoneReady(__instance, number); } } } public static class PluginInfo { public const string PLUGIN_GUID = "LethalPhonesCustomNumbers"; public const string PLUGIN_NAME = "LethalPhonesCustomNumbers"; public const string PLUGIN_VERSION = "1.2.1"; }