From aa9bc6fbc3630affbbd6d2a3bb868c24faea3856 Mon Sep 17 00:00:00 2001 From: NotAKidoS <37721153+NotAKidOnSteam@users.noreply.github.com> Date: Tue, 4 Jun 2024 00:04:57 -0500 Subject: [PATCH] [LazyPrune] Initial Release --- LazyPrune/LazyPrune.csproj | 6 ++ LazyPrune/Main.cs | 128 +++++++++++++++++++++++++++ LazyPrune/Properties/AssemblyInfo.cs | 30 +++++++ LazyPrune/README.md | 16 ++++ LazyPrune/format.json | 24 +++++ NAK_CVR_Mods.sln | 6 ++ 6 files changed, 210 insertions(+) create mode 100644 LazyPrune/LazyPrune.csproj create mode 100644 LazyPrune/Main.cs create mode 100644 LazyPrune/Properties/AssemblyInfo.cs create mode 100644 LazyPrune/README.md create mode 100644 LazyPrune/format.json diff --git a/LazyPrune/LazyPrune.csproj b/LazyPrune/LazyPrune.csproj new file mode 100644 index 0000000..bec5b03 --- /dev/null +++ b/LazyPrune/LazyPrune.csproj @@ -0,0 +1,6 @@ + + + + LoadedObjectHack + + diff --git a/LazyPrune/Main.cs b/LazyPrune/Main.cs new file mode 100644 index 0000000..f7b186c --- /dev/null +++ b/LazyPrune/Main.cs @@ -0,0 +1,128 @@ +using ABI_RC.Core.IO; +using ABI_RC.Systems.GameEventSystem; +using MelonLoader; +using UnityEngine; + +namespace NAK.LazyPrune; + +public class LazyPrune : MelonMod +{ + private static MelonLogger.Instance Logger; + + //private const int MAX_OBJECTS_UNLOADED_AT_ONCE = 5; // just to alleviate hitch on mass destruction + private const float OBJECT_CACHE_TIMEOUT = 3f; // minutes + private static readonly Dictionary _loadedObjects = new(); + + private static string _lastLoadedWorld; + + public override void OnInitializeMelon() + { + Logger = LoggerInstance; + + // every minute, check for objects to prune + SchedulerSystem.AddJob(CheckForObjectsToPrune, 1f, 60f, -1); + + // listen for world load + CVRGameEventSystem.World.OnLoad.AddListener(OnWorldLoaded); + + // listen for local avatar load/clear events + CVRGameEventSystem.Avatar.OnLocalAvatarLoad.AddListener((avatar) => OnObjectCreated(avatar.loadedObject)); + CVRGameEventSystem.Avatar.OnLocalAvatarClear.AddListener((avatar) => OnObjectDestroyed(avatar ? avatar.loadedObject : null)); + + // listen for remote avatar load/clear events + CVRGameEventSystem.Avatar.OnRemoteAvatarLoad.AddListener((_, avatar) => OnObjectCreated(avatar.loadedObject)); + CVRGameEventSystem.Avatar.OnRemoteAvatarClear.AddListener((_, avatar) => OnObjectDestroyed(avatar != null ? avatar.loadedObject : null)); + + // listen for spawnable instantiate/destroy events + CVRGameEventSystem.Spawnable.OnInstantiate.AddListener((_, spawnable) => OnObjectCreated(spawnable.loadedObject)); + CVRGameEventSystem.Spawnable.OnDestroy.AddListener((_, spawnable) => OnObjectDestroyed(spawnable != null ? spawnable.loadedObject : null)); + } + + #region Game Events + + private static void OnWorldLoaded(string guid) + { + if (_lastLoadedWorld != guid) + ForcePrunePendingObjects(); + + _lastLoadedWorld = guid; + } + + private static void OnObjectCreated(CVRObjectLoader.LoadedObject loadedObject) + { + if (loadedObject == null) + return; // uhh + + if (_loadedObjects.ContainsKey(loadedObject)) + { + _loadedObjects[loadedObject] = -1; // mark as ineligible for pruning + return; // already in cache + } + + loadedObject.refCount++; // increment ref count + _loadedObjects.Add(loadedObject, -1); // mark as ineligible for pruning + } + + private static void OnObjectDestroyed(CVRObjectLoader.LoadedObject loadedObject) + { + if (loadedObject == null) + return; // handled by AttemptPruneObject + + if (loadedObject.refCount > 2) + return; // we added our own ref, so begin death count at 2 (decrements one more after this callback) + + if (_loadedObjects.ContainsKey(loadedObject)) + _loadedObjects[loadedObject] = Time.time + OBJECT_CACHE_TIMEOUT * 60f; + } + + #endregion Game Events + + #region Lazy Pruning + + private static void ForcePrunePendingObjects() + { + for (int i = _loadedObjects.Count - 1; i >= 0; i--) + { + (CVRObjectLoader.LoadedObject loadedObject, var killTime) = _loadedObjects.ElementAt(i); + if (killTime > 0) AttemptPruneObject(loadedObject); // prune all pending objects + } + } + + private static void CheckForObjectsToPrune() + { + //int unloaded = 0; + float time = Time.time; + for (int i = _loadedObjects.Count - 1; i >= 0; i--) + { + (CVRObjectLoader.LoadedObject loadedObject, var killTime) = _loadedObjects.ElementAt(i); + if (!(killTime < time)) continue; + AttemptPruneObject(loadedObject); // prune expired objects + //if (unloaded++ >= MAX_OBJECTS_UNLOADED_AT_ONCE) break; // limit unloads per check + } + } + + private static void AttemptPruneObject(CVRObjectLoader.LoadedObject loadedObject) + { + if (loadedObject == null) + { + Logger.Error("Attempted to prune null object. This happens on initial load sometimes."); + return; + } + + if (loadedObject.refCount > 1) + { + Logger.Error($"Object {loadedObject.prefabName} has ref count {loadedObject.refCount}, expected 1"); + _loadedObjects[loadedObject] = -1; // mark as ineligible for pruning ??? + return; // is something somehow holding a reference? + } + + Logger.Msg($"Pruning object {loadedObject.prefabName}"); + _loadedObjects.Remove(loadedObject); // remove from cache + + loadedObject.refCount--; // decrement ref count + if (CVRObjectLoader.Instance != null) // provoke destruction + CVRObjectLoader.Instance.CheckForDestruction(loadedObject); + } + + #endregion Lazy Pruning +} \ No newline at end of file diff --git a/LazyPrune/Properties/AssemblyInfo.cs b/LazyPrune/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..44dffc5 --- /dev/null +++ b/LazyPrune/Properties/AssemblyInfo.cs @@ -0,0 +1,30 @@ +using MelonLoader; +using NAK.LazyPrune.Properties; +using System.Reflection; + +[assembly: AssemblyVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyTitle(nameof(NAK.LazyPrune))] +[assembly: AssemblyCompany(AssemblyInfoParams.Author)] +[assembly: AssemblyProduct(nameof(NAK.LazyPrune))] + +[assembly: MelonInfo( + typeof(NAK.LazyPrune.LazyPrune), + nameof(NAK.LazyPrune), + AssemblyInfoParams.Version, + AssemblyInfoParams.Author, + downloadLink: "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/LazyPrune" +)] + +[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")] +[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] +[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)] +[assembly: HarmonyDontPatchAll] + +namespace NAK.LazyPrune.Properties; +internal static class AssemblyInfoParams +{ + public const string Version = "1.0.0"; + public const string Author = "NotAKidoS"; +} \ No newline at end of file diff --git a/LazyPrune/README.md b/LazyPrune/README.md new file mode 100644 index 0000000..0f03252 --- /dev/null +++ b/LazyPrune/README.md @@ -0,0 +1,16 @@ +# LazyPrune + +Prevents loaded objects from immediately unloading on destruction. Should prevent needlessly unloading & reloading all avatars/props on world rejoin or GS reconnection. + +Unused objects are pruned after 3 minutes, or when loading into a different world. + +--- + +Here is the block of text where I tell you this mod is not affiliated with or endorsed by ABI. +https://documentation.abinteractive.net/official/legal/tos/#7-modding-our-games + +> This mod is an independent creation not affiliated with, supported by, or approved by Alpha Blend Interactive. + +> Use of this mod is done so at the user's own risk and the creator cannot be held responsible for any issues arising from its use. + +> To the best of my knowledge, I have adhered to the Modding Guidelines established by Alpha Blend Interactive. diff --git a/LazyPrune/format.json b/LazyPrune/format.json new file mode 100644 index 0000000..f921e3c --- /dev/null +++ b/LazyPrune/format.json @@ -0,0 +1,24 @@ +{ + "_id": -1, + "name": "LazyPrune", + "modversion": "1.0.0", + "gameversion": "2024r175", + "loaderversion": "0.6.1", + "modtype": "Mod", + "author": "NotAKidoS", + "description": "Prevents loaded objects from immediately unloading on destruction. Should prevent needlessly unloading & reloading all avatars/props on world rejoin or GS reconnection.\n\nUnused objects are pruned after 3 minutes, or when loading into a different world.", + "searchtags": [ + "cache", + "prune", + "bundle", + "download", + "load", + ], + "requirements": [ + "None" + ], + "downloadlink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/releases/download/r30/LazyPrune.dll", + "sourcelink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/LazyPrune/", + "changelog": "- Initial Release", + "embedcolor": "#1c75f1" +} \ No newline at end of file diff --git a/NAK_CVR_Mods.sln b/NAK_CVR_Mods.sln index 24b7d75..f1314b6 100644 --- a/NAK_CVR_Mods.sln +++ b/NAK_CVR_Mods.sln @@ -53,6 +53,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScriptingSpoofer", "Scripti EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LuaTTS", "LuaTTS\LuaTTS.csproj", "{24A069F4-4D69-4ABD-AA16-77765469245B}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LazyPrune", "LazyPrune\LazyPrune.csproj", "{8FA6D481-5801-4E4C-822E-DE561155D22B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -159,6 +161,10 @@ Global {24A069F4-4D69-4ABD-AA16-77765469245B}.Debug|Any CPU.Build.0 = Debug|Any CPU {24A069F4-4D69-4ABD-AA16-77765469245B}.Release|Any CPU.ActiveCfg = Release|Any CPU {24A069F4-4D69-4ABD-AA16-77765469245B}.Release|Any CPU.Build.0 = Release|Any CPU + {8FA6D481-5801-4E4C-822E-DE561155D22B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8FA6D481-5801-4E4C-822E-DE561155D22B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8FA6D481-5801-4E4C-822E-DE561155D22B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8FA6D481-5801-4E4C-822E-DE561155D22B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE