mass commit of laziness

This commit is contained in:
NotAKidoS 2025-12-28 20:30:00 -06:00
parent ce992c70ee
commit 6d4fc549d9
167 changed files with 5471 additions and 675 deletions

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>LoadedObjectHack</RootNamespace>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,214 @@
using System.Reflection;
using ABI_RC.Core.EventSystem;
using ABI_RC.Core.IO;
using ABI_RC.Core.Networking.API.Responses;
using ABI_RC.Core.Player;
using ABI_RC.Core.Savior;
using ABI_RC.Systems.GameEventSystem;
using HarmonyLib;
using MelonLoader;
using UnityEngine;
namespace NAK.LazyPrune;
public class LazyPrune : MelonMod
{
private static MelonLogger.Instance Logger;
private const int MAX_OBJECTS_UNLOADED_AT_ONCE = 3; // just to alleviate hitch on mass destruction
private const float OBJECT_CACHE_TIMEOUT = 2f; // minutes
private static readonly Dictionary<CVRObjectLoader.LoadedObject, float> _loadedObjects = new();
private static string _lastLoadedWorld;
private static bool _isInitialized;
public override void OnInitializeMelon()
{
Logger = LoggerInstance;
// listen for world load
CVRGameEventSystem.World.OnLoad.AddListener(OnWorldLoaded);
// listen for local avatar bundle load
HarmonyInstance.Patch(
typeof(CVRObjectLoader).GetMethod(nameof(CVRObjectLoader.InstantiateAvatarFromExistingPrefab),
BindingFlags.Public | BindingFlags.Instance), // earliest callback (why the fuck are you public)
prefix: new HarmonyMethod(typeof(LazyPrune).GetMethod(nameof(OnInstantiateAvatarFromExistingPrefab),
BindingFlags.NonPublic | BindingFlags.Static))
);
// listen for prop bundle load
HarmonyInstance.Patch(
typeof(CVRObjectLoader).GetMethod(nameof(CVRObjectLoader.InstantiateSpawnableFromExistingPrefab),
BindingFlags.NonPublic | BindingFlags.Instance), // earliest callback
prefix: new HarmonyMethod(typeof(LazyPrune).GetMethod(nameof(OnInstantiateSpawnableFromExistingPrefab),
BindingFlags.NonPublic | BindingFlags.Static))
);
// listen for object destruction
HarmonyInstance.Patch(
typeof(CVRObjectLoader).GetMethod(nameof(CVRObjectLoader.CheckForDestruction),
BindingFlags.Public | BindingFlags.Instance), // earliest callback
prefix: new HarmonyMethod(typeof(LazyPrune).GetMethod(nameof(OnObjectDestroyed),
BindingFlags.NonPublic | BindingFlags.Static))
);
// nuke qm debug method as there is a race condition that locks up download queue
HarmonyInstance.Patch(
typeof(CVRDownloadManager).GetMethod(nameof(CVRDownloadManager.UpdateUiDownloadStatus),
BindingFlags.Public | BindingFlags.Instance),
prefix: new HarmonyMethod(typeof(LazyPrune).GetMethod(nameof(OnUpdateUiDownloadStatus),
BindingFlags.NonPublic | BindingFlags.Static))
);
// another race condition that causes instantiation queue to lock up
HarmonyInstance.Patch(
typeof(PuppetMaster).GetMethod(nameof(PuppetMaster.AvatarInstantiated),
BindingFlags.Public | BindingFlags.Instance),
prefix: new HarmonyMethod(typeof(LazyPrune).GetMethod(nameof(OnAvatarInstantiated),
BindingFlags.NonPublic | BindingFlags.Static))
);
}
#region Game Fixes
private static bool OnUpdateUiDownloadStatus() => false;
private static bool OnAvatarInstantiated(ref GameObject ___avatarObject)
{
if (___avatarObject != null) return true; // instantiate coroutine is try-catch'd, so purposefully throw to have it catch
Logger.Warning("Caught null avatar object in PuppetMaster.AvatarInstantiated. Throwing exception so ObjectLoader can catch.");
throw new Exception("LazyPrune: PuppetMaster Avatar instantiated with null object!");
}
#endregion Game Fixes
#region Game Events
private static void OnInstantiateAvatarFromExistingPrefab(string objectId, string instantiationTarget,
GameObject prefabObject,
ref CVRObjectLoader.LoadedObject loadedObject, string blockReason, AssetManagement.AvatarTags? avatarTags,
bool shouldFilter, bool isBlocked,
CompatibilityVersions compatibilityVersion)
{
if (prefabObject == MetaPort.Instance.blockedAvatarPrefab)
{
Logger.Msg($"Blocked avatar prefab is loading. Ignoring...");
return;
}
OnObjectCreated(ref loadedObject);
}
private static void OnInstantiateSpawnableFromExistingPrefab(string objectId, string instantiationTarget,
GameObject prefabObject, CVRObjectLoader.LoadedObject loadedObject, AssetManagement.PropTags propTags,
CompatibilityVersions compatibilityVersion)
{
if (prefabObject == MetaPort.Instance.blockedSpawnablePrefab)
{
Logger.Msg($"Blocked spawnable prefab is loading. Ignoring...");
return;
}
OnObjectCreated(ref loadedObject);
}
private static void OnWorldLoaded(string guid)
{
if (!_isInitialized)
{
// every minute, check for objects to prune
SchedulerSystem.AddJob(CheckForObjectsToPrune, 1f, 10f, -1);
_isInitialized = true;
}
if (_lastLoadedWorld != guid)
ForcePrunePendingObjects();
_lastLoadedWorld = guid;
}
private static void OnObjectCreated(ref CVRObjectLoader.LoadedObject ___loadedObject)
{
if (___loadedObject == null)
{
Logger.Error("Avatar/Prop created with no backed LoadedObject.");
return;
}
if (_loadedObjects.ContainsKey(___loadedObject))
{
_loadedObjects[___loadedObject] = -1; // mark as ineligible for pruning
return;
}
___loadedObject.refCount++; // increment ref count
_loadedObjects.Add(___loadedObject, -1); // mark as ineligible for pruning
}
private static void OnObjectDestroyed(CVRObjectLoader.LoadedObject loadedObject)
{
if (loadedObject == null)
{
Logger.Error("Avatar/Prop destroyed with no backed LoadedObject.");
return;
}
if (loadedObject.refCount > 1)
return;
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);
}
}
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) || killTime < 0) continue;
AttemptPruneObject(loadedObject);
if (unloaded++ >= MAX_OBJECTS_UNLOADED_AT_ONCE) break;
}
}
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;
return;
}
Logger.Msg($"Pruning object {loadedObject.prefabName}");
_loadedObjects.Remove(loadedObject);
loadedObject.refCount--;
if (CVRObjectLoader.Instance != null)
CVRObjectLoader.Instance.CheckForDestruction(loadedObject);
}
#endregion Lazy Pruning
}

View file

@ -0,0 +1,32 @@
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/NotAKidoS/NAK_CVR_Mods/tree/main/LazyPrune"
)]
[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")]
[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)]
[assembly: MelonColor(255, 246, 25, 99)] // red-pink
[assembly: MelonAuthorColor(255, 158, 21, 32)] // red
[assembly: HarmonyDontPatchAll]
namespace NAK.LazyPrune.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.0.3";
public const string Author = "NotAKidoS";
}

View file

@ -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.

View file

@ -0,0 +1,24 @@
{
"_id": 214,
"name": "LazyPrune",
"modversion": "1.0.3",
"gameversion": "2025r179",
"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/NotAKidoS/NAK_CVR_Mods/releases/download/r46/LazyPrune.dll",
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/LazyPrune/",
"changelog": "- Recompiled for 2025r179",
"embedcolor": "#1c75f1"
}