diff --git a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs index e47d775..18bd476 100644 --- a/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs +++ b/MiraAPI/Modifiers/ModifierDisplay/ModifierDisplayComponent.cs @@ -138,7 +138,7 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr) internal void DestroyComponent(ModifierUiComponent component) { _modifiers.Remove(component.Modifier!); - component.gameObject.FakeDestroy(); + component.gameObject.DeepDestroy(); RefreshModifiers(); } @@ -208,7 +208,7 @@ public class ModifierDisplayComponent(nint cppPtr) : MonoBehaviour(cppPtr) continue; } - mod.Value.gameObject.FakeDestroy(); + mod.Value.gameObject.DeepDestroy(); _modifiers.Remove(mod.Key); } diff --git a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs index ea13199..22cea2d 100644 --- a/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs +++ b/MiraAPI/Patches/Freeplay/TaskAdderPatches.cs @@ -276,7 +276,7 @@ public static class TaskAdderPatches __instance.transform.FindChild("TitleText_TMP")?.gameObject.DestroyImmediate(); } - __instance.ActiveItems.ToArray().Do(x => x.gameObject.FakeDestroy()); + __instance.ActiveItems.ToArray().Do(x => x.gameObject.DeepDestroy()); __instance.ActiveItems.Clear(); float num = 0f; diff --git a/MiraAPI/Patches/HudManagerPatches.cs b/MiraAPI/Patches/HudManagerPatches.cs index 7af625e..1f19b5a 100644 --- a/MiraAPI/Patches/HudManagerPatches.cs +++ b/MiraAPI/Patches/HudManagerPatches.cs @@ -143,7 +143,7 @@ public static class HudManagerPatches foreach (var t in BottomLeft.GetComponentsInChildren(true)) { - t.gameObject.FakeDestroy(); + t.gameObject.DeepDestroy(); } var gridArrange = BottomLeft.GetComponent(); diff --git a/MiraAPI/Patches/Menu/HatsTabPatches.cs b/MiraAPI/Patches/Menu/HatsTabPatches.cs index 98bbc92..c6ad646 100644 --- a/MiraAPI/Patches/Menu/HatsTabPatches.cs +++ b/MiraAPI/Patches/Menu/HatsTabPatches.cs @@ -92,9 +92,9 @@ public static class HatsTabPatches if (loadRoutine != null) Coroutines.Stop(loadRoutine); hatIndex = 0; - foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.FakeDestroy(); + foreach (var instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.DeepDestroy(); __instance.ColorChips.Clear(); - __instance.scroller.Inner.GetComponentsInChildren().Do(x => x.gameObject.FakeDestroy()); + __instance.scroller.Inner.GetComponentsInChildren().Do(x => x.gameObject.DeepDestroy()); var groupNameText = __instance.GetComponentInChildren(false); diff --git a/MiraAPI/Patches/Menu/MainMenuManagerPatches.cs b/MiraAPI/Patches/Menu/MainMenuManagerPatches.cs index b2377fe..edc7e9b 100644 --- a/MiraAPI/Patches/Menu/MainMenuManagerPatches.cs +++ b/MiraAPI/Patches/Menu/MainMenuManagerPatches.cs @@ -1,4 +1,5 @@ -using System.Collections; +using System; +using System.Collections; using HarmonyLib; using MiraAPI.LocalSettings; using MiraAPI.Utilities.Assets; @@ -13,6 +14,8 @@ namespace MiraAPI.Patches.Menu; [HarmonyPatch(typeof(MainMenuManager))] public static class MainMenuManagerPatches { + internal static bool NeedsDeepDestroy { get; private set; } + /// /// A postifix on Awake to load all the addressables registered. /// @@ -20,6 +23,9 @@ public static class MainMenuManagerPatches [HarmonyPostfix] public static void AwakePostfix() { + var requiredVersion = new Version(2026, 6, 5); + var version = Version.Parse(Application.version); + NeedsDeepDestroy = version >= requiredVersion; AddressablesLoader.LoadAll(); Coroutines.Start(SetFps()); } diff --git a/MiraAPI/Patches/Menu/NameplatesTabPatches.cs b/MiraAPI/Patches/Menu/NameplatesTabPatches.cs index 6fd1031..e8d15a8 100644 --- a/MiraAPI/Patches/Menu/NameplatesTabPatches.cs +++ b/MiraAPI/Patches/Menu/NameplatesTabPatches.cs @@ -81,9 +81,9 @@ public static class NameplatesTabPatches private static void GenerateHats(NameplatesTab __instance, int page) { - foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.FakeDestroy(); + foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.DeepDestroy(); __instance.ColorChips.Clear(); - __instance.scroller.Inner.GetComponentsInChildren().Do(x => x.gameObject.FakeDestroy()); + __instance.scroller.Inner.GetComponentsInChildren().Do(x => x.gameObject.DeepDestroy()); var groupNameText = __instance.GetComponentInChildren(false); diff --git a/MiraAPI/Patches/Menu/SkinsTabPatches.cs b/MiraAPI/Patches/Menu/SkinsTabPatches.cs index bc8e64e..64b0b54 100644 --- a/MiraAPI/Patches/Menu/SkinsTabPatches.cs +++ b/MiraAPI/Patches/Menu/SkinsTabPatches.cs @@ -83,9 +83,9 @@ public static class SkinsTabPatches private static void GenerateHats(SkinsTab __instance, int page) { - foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.FakeDestroy(); + foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.DeepDestroy(); __instance.ColorChips.Clear(); - __instance.scroller.Inner.GetComponentsInChildren().Do(x => x.gameObject.FakeDestroy()); + __instance.scroller.Inner.GetComponentsInChildren().Do(x => x.gameObject.DeepDestroy()); var groupNameText = __instance.GetComponentInChildren(false); diff --git a/MiraAPI/Patches/Menu/VisorsTabPatches.cs b/MiraAPI/Patches/Menu/VisorsTabPatches.cs index f3a8d93..385fe97 100644 --- a/MiraAPI/Patches/Menu/VisorsTabPatches.cs +++ b/MiraAPI/Patches/Menu/VisorsTabPatches.cs @@ -81,9 +81,9 @@ public static class VisorsTabPatches private static void GenerateHats(VisorsTab __instance, int page) { - foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.FakeDestroy(); + foreach (ColorChip instanceColorChip in __instance.ColorChips) instanceColorChip.gameObject.DeepDestroy(); __instance.ColorChips.Clear(); - __instance.scroller.Inner.GetComponentsInChildren().Do(x => x.gameObject.FakeDestroy()); + __instance.scroller.Inner.GetComponentsInChildren().Do(x => x.gameObject.DeepDestroy()); var groupNameText = __instance.GetComponentInChildren(false); diff --git a/MiraAPI/Patches/Options/GamePresetsTabPatches.cs b/MiraAPI/Patches/Options/GamePresetsTabPatches.cs index bae9b8b..a4b6cca 100644 --- a/MiraAPI/Patches/Options/GamePresetsTabPatches.cs +++ b/MiraAPI/Patches/Options/GamePresetsTabPatches.cs @@ -262,7 +262,7 @@ internal static class GamePresetsTabPatches iconSpriteRend.sprite = MiraAssets.RefreshIcon.LoadAsset(); Helpers.DivideSize(refreshButton.gameObject, 2f); - refreshButton.buttonText.gameObject.FakeDestroy(); + refreshButton.buttonText.gameObject.DeepDestroy(); refreshButton.OnClick = new Button.ButtonClickedEvent(); refreshButton.OnClick.AddListener((UnityAction)Refresh); diff --git a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs index 6e28cd9..1d17d70 100644 --- a/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs +++ b/MiraAPI/Patches/Options/RoleSettingMenuPatches.cs @@ -59,7 +59,7 @@ public static class RoleSettingMenuPatches { continue; } - obj.FakeDestroy(); + obj.DeepDestroy(); } Headers.Clear(); foreach (var obj in RoleOptionSettings) @@ -68,7 +68,7 @@ public static class RoleSettingMenuPatches { continue; } - obj.gameObject.FakeDestroy(); + obj.gameObject.DeepDestroy(); } RoleOptionSettings.Clear(); CurrentRole = null; @@ -218,9 +218,9 @@ public static class RoleSettingMenuPatches var blankLabel = quotaInst.transform.FindChild("BlankLabel").gameObject; var chanceLabel = quotaInst.transform.FindChild("Chance Label").gameObject; var countLabel = quotaInst.transform.FindChild("# Label").gameObject; - blankLabel.FakeDestroy(); - chanceLabel.FakeDestroy(); - countLabel.FakeDestroy(); + blankLabel.DeepDestroy(); + chanceLabel.DeepDestroy(); + countLabel.DeepDestroy(); categoryHeaderMasked.Background.sprite = MiraAssets.CategoryHeader.LoadAsset(); categoryHeaderMasked.Background.sprite.texture.filterMode = FilterMode.Bilinear; @@ -309,7 +309,7 @@ public static class RoleSettingMenuPatches { continue; } - obj.FakeDestroy(); + obj.DeepDestroy(); } Headers.Clear(); foreach (var obj in RoleOptionSettings) @@ -318,7 +318,7 @@ public static class RoleSettingMenuPatches { continue; } - obj.gameObject.FakeDestroy(); + obj.gameObject.DeepDestroy(); } RoleOptionSettings.Clear(); __instance.SetQuotaTab(); @@ -444,7 +444,7 @@ public static class RoleSettingMenuPatches { foreach (var optBehaviour in __instance.AdvancedRolesSettings.GetComponentsInChildren()) { - optBehaviour.gameObject.FakeDestroy(); + optBehaviour.gameObject.DeepDestroy(); } CurrentRole = role; @@ -640,7 +640,7 @@ public static class RoleSettingMenuPatches var newButton = Object.Instantiate(roleOptionSetting.buttons[0], roleOptionSetting.transform); newButton.name = "ConfigButton"; newButton.transform.localPosition = new Vector3(0.4473f, -0.3f, -2f); - newButton.transform.FindChild("Text_TMP").gameObject.FakeDestroy(); + newButton.transform.FindChild("Text_TMP").gameObject.DeepDestroy(); newButton.activeSprites.Destroy(); var btnRend = newButton.transform.FindChild("ButtonSprite").GetComponent(); diff --git a/MiraAPI/Patches/Roles/HudManagerPatches.cs b/MiraAPI/Patches/Roles/HudManagerPatches.cs index cc46c13..d5d23dc 100644 --- a/MiraAPI/Patches/Roles/HudManagerPatches.cs +++ b/MiraAPI/Patches/Roles/HudManagerPatches.cs @@ -73,7 +73,7 @@ public static class HudManagerPatches } else if (RoleTab != null) { - RoleTab.gameObject.FakeDestroy(); + RoleTab.gameObject.DeepDestroy(); RoleTab = null; } } diff --git a/MiraAPI/Presets/PresetManager.cs b/MiraAPI/Presets/PresetManager.cs index 2ef0c55..07f00cd 100644 --- a/MiraAPI/Presets/PresetManager.cs +++ b/MiraAPI/Presets/PresetManager.cs @@ -62,7 +62,7 @@ public static class PresetManager { if (btn != null) { - btn.FakeDestroy(); + btn.DeepDestroy(); } } diff --git a/MiraAPI/Presets/SavePresetPopup.cs b/MiraAPI/Presets/SavePresetPopup.cs index 721f46c..23ce48b 100644 --- a/MiraAPI/Presets/SavePresetPopup.cs +++ b/MiraAPI/Presets/SavePresetPopup.cs @@ -26,7 +26,7 @@ public class SavePresetPopup(nint cppPtr) : Minigame(cppPtr) // Cleanup holder object public void OnDestroy() { - transform.parent.gameObject.FakeDestroy(); + transform.parent.gameObject.DeepDestroy(); } public override void Close() diff --git a/MiraAPI/Utilities/Extensions.cs b/MiraAPI/Utilities/Extensions.cs index b594598..ffc8e72 100644 --- a/MiraAPI/Utilities/Extensions.cs +++ b/MiraAPI/Utilities/Extensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -7,9 +8,11 @@ using System.Text.RegularExpressions; using HarmonyLib; using MiraAPI.GameOptions; using MiraAPI.Networking; +using MiraAPI.Patches.Menu; using MiraAPI.Roles; using MiraAPI.Voting; using Reactor.Utilities; +using Reactor.Utilities.Extensions; using Rewired; using Rewired.Data; using TMPro; @@ -53,7 +56,7 @@ public static class Extensions bool isComplete; if (self.amClosing == Minigame.CloseState.Closing) { - self.gameObject.FakeDestroy(); + self.gameObject.DeepDestroy(); return; } if (self.CloseSound && Constants.ShouldPlaySfx()) @@ -223,19 +226,129 @@ public static class Extensions /// Hides the object away rather than destroy it to prevent memory leaks. /// /// The object to hide. - public static void FakeDestroy(this GameObject obj) + public static void DeepDestroy(this GameObject obj, bool clearGc = true) { - obj.hideFlags = HideFlags.HideAndDontSave; - if (!toDestroyHolder) + if (MainMenuManagerPatches.NeedsDeepDestroy) { - toDestroyHolder = new GameObject("ToDestroyHolder"); - toDestroyHolder.gameObject.SetActive(false); + Coroutines.Start(Nuke(obj, clearGc)); + } + else + { + obj?.Destroy(); } - obj.transform.SetParent(toDestroyHolder.transform); - obj.SetActive(false); } - private static GameObject toDestroyHolder; + private static IEnumerator Nuke(GameObject? go, bool clearGc) + { + if (go == null) + yield break; + + try + { + go.transform.SetParent(null, false); + } + catch + { + // ignored + } + + try + { + go.SetActive(false); + } + catch + { + // ignored + } + + foreach (var mb in go.GetComponentsInChildren(true)) + { + if (mb == null) + continue; + + try + { + mb.StopAllCoroutines(); + } + catch + { + // ignored + } + + try + { + mb.enabled = false; + } + catch + { + // ignored + } + } + + foreach (var renderer in go.GetComponentsInChildren(true)) + { + if (renderer == null) + continue; + + try + { + foreach (var mat in renderer.materials) + { + if (mat != null) + UnityEngine.Object.Destroy(mat); + } + } + catch + { + // ignored + } + } + + foreach (var filter in go.GetComponentsInChildren(true)) + { + if (filter == null) + continue; + + try + { + var mesh = filter.mesh; + if (mesh != null) + UnityEngine.Object.Destroy(mesh); + } + catch + { + // ignored + } + } + + UnityEngine.Object.Destroy(go); + yield return null; + if (clearGc) + { + yield return CoFreeResources(); + } + } + + /// + /// Clears up the Garbage Collector manually if necessary. + /// + public static void ClearGarbageCollector() + { + if (!MainMenuManagerPatches.NeedsDeepDestroy) + { + return; + } + Coroutines.Start(CoFreeResources()); + } + + private static IEnumerator CoFreeResources() + { + yield return Resources.UnloadUnusedAssets(); + + GC.Collect(0, GCCollectionMode.Forced, blocking: true); + GC.WaitForPendingFinalizers(); + GC.Collect(0, GCCollectionMode.Forced, blocking: true); + } /// /// Gets a cache of player's vote data components to improve performance.