further cleanup of repo

This commit is contained in:
NotAKidoS 2025-04-03 03:03:24 -05:00
parent 4f8dcb0cd0
commit 323eb92f2e
140 changed files with 1 additions and 2430 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,44 @@
using UnityEngine;
namespace NAK.CVRLuaToolsExtension;
public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
private static T _instance;
private static readonly object _lock = new();
private static bool _isShuttingDown;
public static T Instance
{
get
{
if (_isShuttingDown)
{
Debug.LogWarning($"[Singleton] Instance of {typeof(T)} already destroyed. Returning null.");
return null;
}
lock (_lock)
{
if (_instance != null) return _instance;
_instance = (T)FindObjectOfType(typeof(T));
if (_instance != null) return _instance;
GameObject singletonObject = new($"{typeof(T).Name} (Singleton)");
_instance = singletonObject.AddComponent<T>();
DontDestroyOnLoad(singletonObject);
return _instance;
}
}
}
private void OnApplicationQuit()
{
_isShuttingDown = true;
}
private void OnDestroy()
{
if (_instance == this)
_isShuttingDown = true;
}
}

View file

@ -0,0 +1,47 @@
using ABI_RC.Core.Player;
using ABI_RC.Core.Savior;
using ABI.CCK.Components;
using ABI.Scripting.CVRSTL.Common;
using UnityEngine;
namespace NAK.CVRLuaToolsExtension;
public static class CVRBaseLuaBehaviourExtensions
{
// check if the script is eligible for hot reload
public static bool IsScriptEligibleForHotReload(this CVRBaseLuaBehaviour script)
{
return script.Context.IsWornByMe // avatar if worn
|| script.Context.IsSpawnedByMe // prop if spawned
|| script.Context.objContext == CVRLuaObjectContext.WORLD; // always world scripts
}
// gets the asset id from the script
public static string GetAssetIdFromScript(this CVRBaseLuaBehaviour script)
{
switch (script.Context.objContext)
{
case CVRLuaObjectContext.WORLD:
//return MetaPort.Instance.CurrentWorldId; // do not trust CVRAssetInfo, can be destroyed at runtime
return "SYSTEM"; // default for world scripts is SYSTEM, but TODO: use actual world id
case CVRLuaObjectContext.AVATAR:
Component rootComponent = script.Context.RootComponent;
return rootComponent switch
{
PlayerSetup => MetaPort.Instance.currentAvatarGuid, // local avatar
PuppetMaster puppetMaster => puppetMaster.CVRPlayerEntity.AvatarId, // remote avatar
_ => string.Empty // fuck
};
case CVRLuaObjectContext.PROP:
{
CVRSpawnable spawnable = (CVRSpawnable)script.Context.RootComponent;
if (!string.IsNullOrEmpty(spawnable.guid)) return spawnable.guid; // after filtering has occured
return spawnable.TryGetComponent(out CVRAssetInfo assetInfo)
? assetInfo.objectId // before filtering has occured
: string.Empty; // well shit
}
default:
throw new ArgumentOutOfRangeException();
}
}
}

View file

@ -0,0 +1,128 @@
using ABI.CCK.Components;
using ABI.Scripting.CVRSTL.Client;
using System.Diagnostics;
using MTJobSystem;
using UnityEngine;
namespace NAK.CVRLuaToolsExtension;
public static class CVRLuaClientBehaviourExtensions
{
internal static readonly Dictionary<CVRLuaClientBehaviour, bool> _isRestarting = new();
private static string PersistentDataPath;
#region Public Methods
public static void Restart(this CVRLuaClientBehaviour behaviour)
{
PersistentDataPath ??= Application.persistentDataPath; // needs to be set on main
if (_isRestarting.TryGetValue(behaviour, out bool isRestarting) && isRestarting)
{
CVRLuaToolsExtensionMod.Logger.Warning($"Restart is already in progress for {behaviour.ScriptName}.");
return;
}
_isRestarting[behaviour] = true;
bool wasEnabled = behaviour.enabled;
if (behaviour.Crashed)
{
CVRLuaToolsExtensionMod.Logger.Warning($"Restarting a script ({behaviour.ScriptName}) in a crashed state. Unable to determine original enabled state, defaulting to true.");
wasEnabled = true;
}
behaviour.enabled = false;
Task.Run(() =>
{
try
{
behaviour.ResetScriptCompletely();
if (CVRLuaToolsExtensionMod.EntryAttemptInitOffMainThread.Value)
{
CVRLuaToolsExtensionMod.Logger.Warning("Attempting to initialize the lua script off main thread. This may cause crashes or corruption if you are accessing UnityEngine Objects outside of Unity's callbacks!");
behaviour.DoInitialLoadOfScript();
}
}
catch (Exception e)
{
CVRLuaToolsExtensionMod.Logger.Error(e); // don't wanna die in task
}
MTJobManager.RunOnMainThread("RestartScript", () =>
{
Stopwatch stopwatch = new();
stopwatch.Start();
try
{
if (!CVRLuaToolsExtensionMod.EntryAttemptInitOffMainThread.Value)
behaviour.DoInitialLoadOfScript();
if (behaviour.InitialCodeRun) behaviour.IsScriptInitialized = true; // allow callbacks to run again
// invoke custom event on reset
if (!behaviour.ShouldSkipEvent("CVRLuaTools_OnReset")) behaviour.ExecuteEvent(5000, "CVRLuaTools_OnReset");
// re-enable the script & invoke the lifecycle events
if (!behaviour.ShouldSkipEvent("Awake")) behaviour.ExecuteEvent(1000, "Awake");
behaviour.enabled = wasEnabled;
if (wasEnabled)
{
if (!behaviour.ShouldSkipEvent("OnEnable")) behaviour.ExecuteEvent(1000, "OnEnable");
if (!behaviour.ShouldSkipEvent("Start")) behaviour.ExecuteEvent(1000, "Start");
}
}
catch (Exception e)
{
CVRLuaToolsExtensionMod.Logger.Error(e); // don't wanna die prior to resetting restart flag
}
_isRestarting.Remove(behaviour);
stopwatch.Stop();
CVRLuaToolsExtensionMod.Logger.Msg($"Restarted {behaviour.ScriptName} in {stopwatch.ElapsedMilliseconds}ms.");
});
});
}
#endregion Public Methods
#region Private Methods
private static void ResetScriptCompletely(this CVRLuaClientBehaviour behaviour)
{
var boundObjectEntries = new List<LuaScriptFactory.BoundObjectEntry>();
foreach (var kvp in behaviour.BoundObjects)
{
LuaScriptFactory.BoundObjectEntry entry = new(behaviour, behaviour.Context, kvp.Key, kvp.Value);
boundObjectEntries.Add(entry);
}
behaviour.IsScriptInitialized = false; // prevent callbacks from causing null refs while restarting
behaviour.InitialCodeRun = false; // needs to be set so LoadAndRunScriptIfNeeded will run the script again
behaviour.Crashed = false; // reset the crash flag
behaviour._scriptGlobalFunctions.Clear(); // will be repopulated
behaviour._startupMessageQueue.Clear(); // will be repopulated
behaviour.LogInfo("[CVRLuaToolsExtension] Resetting script...\n");
behaviour.script = null;
behaviour.script = LuaScriptFactory.ForLuaBehaviour(behaviour, boundObjectEntries, behaviour.gameObject, behaviour.transform, PersistentDataPath);
behaviour.InitTimerIfNeeded(); // only null if crashed prior
behaviour.script.AttachDebugger(behaviour.timer); // reattach the debugger
}
private static void DoInitialLoadOfScript(this CVRLuaClientBehaviour behaviour)
{
behaviour.LogInfo("[CVRLuaToolsExtension] Interpreting " + behaviour.ScriptName + "...\n");
behaviour.LoadAndRunScript(behaviour.ScriptText); // fucking slow
behaviour.PopulateScriptGlobalFunctionsCache(behaviour.script.Globals); // tbh don't think we need to clear in first place
behaviour.HandleMessageQueueEntries(); // shouldn't be any
behaviour.InitialCodeRun = true;
}
#endregion Private Methods
}

View file

@ -0,0 +1,147 @@
using System.Text;
using ABI_RC.Core.InteractionSystem;
using ABI_RC.Core.UI;
using ABI.CCK.Components;
using ABI.CCK.Components.ScriptableObjects;
using UnityEngine;
namespace NAK.CVRLuaToolsExtension;
public static class LuaHotReloadManager
{
// (asset id + lua component id) -> index is component reference
private static readonly List<int> s_CombinedKeys = new();
private static readonly List<CVRLuaClientBehaviour> s_LuaComponentInstances = new();
#region Game Events
public static void OnCVRLuaBaseBehaviourLoadAndRunScript(CVRLuaClientBehaviour clientBehaviour)
{
if (!clientBehaviour.IsScriptEligibleForHotReload())
return;
// check if the component is already in the list (shouldn't happen)
if (s_LuaComponentInstances.Contains(clientBehaviour))
{
CVRLuaToolsExtensionMod.Logger.Warning($"[LuaHotReloadManager] Script already added: {clientBehaviour.name}");
return;
}
// combine the assetId and instanceId into a single key, so multiple instances of the same script can be tracked
string assetId = clientBehaviour.GetAssetIdFromScript();
int instanceId = GetGameObjectPathHashCode(clientBehaviour.transform);
int combinedKey = GenerateCombinedKey(assetId.GetHashCode(), instanceId);
s_CombinedKeys.Add(combinedKey);
s_LuaComponentInstances.Add(clientBehaviour);
CVRLuaToolsExtensionMod.Logger.Msg($"[LuaHotReloadManager] Added script: {clientBehaviour.name}");
}
public static void OnCVRLuaBaseBehaviourDestroy(CVRLuaClientBehaviour clientBehaviour)
{
if (!clientBehaviour.IsScriptEligibleForHotReload())
return;
if (!s_LuaComponentInstances.Contains(clientBehaviour))
{
CVRLuaToolsExtensionMod.Logger.Warning($"[LuaHotReloadManager] Eligible for Hot Reload script destroyed without being tracked first: {clientBehaviour.name}");
return;
}
int index = s_LuaComponentInstances.IndexOf(clientBehaviour);
s_CombinedKeys.RemoveAt(index);
s_LuaComponentInstances.RemoveAt(index);
CVRLuaToolsExtensionMod.Logger.Msg($"[LuaHotReloadManager] Removed script: {clientBehaviour.name}");
}
public static void OnReceiveUpdatedScript(ScriptInfo info)
{
int combinedKey = GenerateCombinedKey(info.AssetId.GetHashCode(), info.LuaComponentId);
bool found = false;
for (int i = 0; i < s_CombinedKeys.Count; i++)
{
if (combinedKey != s_CombinedKeys[i])
continue;
CVRLuaClientBehaviour clientBehaviour = s_LuaComponentInstances[i];
if (clientBehaviour.asset.m_ScriptPath == info.ScriptPath)
{
CVRLuaToolsExtensionMod.Logger.Msg("[LuaHotReloadManager] Script path match, updating script.");
clientBehaviour.asset.name = info.ScriptName;
clientBehaviour.asset.m_ScriptText = info.ScriptText;
clientBehaviour.Restart();
found = true;
}
else
{
CVRLuaToolsExtensionMod.Logger.Msg("[LuaHotReloadManager] Script path mismatch, creating new script.");
clientBehaviour.asset = null;
clientBehaviour.asset = ScriptableObject.CreateInstance<CVRLuaScript>();
clientBehaviour.asset.name = info.ScriptName;
clientBehaviour.asset.m_ScriptPath = info.ScriptPath;
clientBehaviour.asset.m_ScriptText = info.ScriptText;
clientBehaviour.Restart();
found = true;
}
}
if (found)
{
CohtmlHud.Instance.ViewDropTextImmediate("(Local) CVRLuaTools", "Received script update", "Reloaded script: " + info.ScriptName);
}
}
#endregion Game Events
#region Private Methods
private static int GenerateCombinedKey(int assetId, int instanceId)
{
return (assetId << 16) | instanceId; // yes
}
private static int GetGameObjectPathHashCode(Transform transform)
{
// both CVRAvatar & CVRSpawnable *should* have an asset info component
Transform rootComponentTransform = null;
CVRAssetInfo rootComponent = transform.GetComponentInParent<CVRAssetInfo>(true);
if (rootComponent != null && rootComponent.type != CVRAssetInfo.AssetType.World) // ignore if under world instance
rootComponentTransform = rootComponent.transform;
// easy case, no need to crawl up the hierarchy
if (rootComponentTransform == transform)
return 581452743; // hash code for "[Root]"
StringBuilder pathBuilder = new(transform.name);
Transform parentTransform = transform.parent;
while (parentTransform != null)
{
// reached root component
// due to object loader renaming root, we can't rely on transform name, so we use "[Root]" instead
if (parentTransform == rootComponentTransform)
{
pathBuilder.Insert(0, "[Root]/");
break;
}
pathBuilder.Insert(0, parentTransform.name + "/");
parentTransform = parentTransform.parent;
}
string path = pathBuilder.ToString();
//Debug.Log($"[LuaComponentManager] Path: {path}");
return path.GetHashCode();
}
#endregion Private Methods
}

View file

@ -0,0 +1,105 @@
using System.Diagnostics;
using System.IO.Pipes;
using System.Text;
using ABI_RC.Core;
namespace NAK.CVRLuaToolsExtension.NamedPipes;
public class NamedPipeServer : Singleton<NamedPipeServer>
{
private const string PipeName = "UnityPipe";
public async void StartListening()
{
Stopwatch stopwatch = new();
while (!CommonTools.IsQuitting)
{
await using NamedPipeServerStream namedPipeServer = new(PipeName, PipeDirection.In);
try
{
CVRLuaToolsExtensionMod.Logger.Msg("Waiting for client...");
await namedPipeServer.WaitForConnectionAsync();
stopwatch.Restart();
if (!namedPipeServer.IsConnected)
continue;
//Debug.Log("Client connected.");
// receive and process the ScriptInfo
ScriptInfo receivedScript = await ReceiveScriptInfo(namedPipeServer);
ProcessScriptInfo(receivedScript);
}
catch (Exception e)
{
CVRLuaToolsExtensionMod.Logger.Error(e);
}
if (namedPipeServer.IsConnected)
{
namedPipeServer.Disconnect();
CVRLuaToolsExtensionMod.Logger.Msg("Client disconnected.");
}
stopwatch.Stop();
CVRLuaToolsExtensionMod.Logger.Msg($"Elapsed time: {stopwatch.ElapsedMilliseconds}ms");
}
CVRLuaToolsExtensionMod.Logger.Msg("Quitting...");
}
private async Task<ScriptInfo> ReceiveScriptInfo(NamedPipeServerStream stream)
{
// Read LuaComponentId (4 bytes for an int)
byte[] intBuffer = new byte[4];
await stream.ReadAsync(intBuffer, 0, intBuffer.Length);
int luaComponentId = BitConverter.ToInt32(intBuffer, 0);
string assetId = await ReadStringAsync(stream);
//CVRLuaToolsExtensionMod.Logger.Msg($"Received assetId: {assetId}");
string scriptName = await ReadStringAsync(stream);
//CVRLuaToolsExtensionMod.Logger.Msg($"Received scriptName: {scriptName}");
string scriptPath = await ReadStringAsync(stream);
//CVRLuaToolsExtensionMod.Logger.Msg($"Received scriptPath: {scriptPath}");
string scriptText = await ReadStringAsync(stream);
//CVRLuaToolsExtensionMod.Logger.Msg($"Received scriptText: {scriptText}");
//Debug.Log("ScriptInfo received.");
return new ScriptInfo
{
LuaComponentId = luaComponentId,
AssetId = assetId,
ScriptName = scriptName,
ScriptPath = scriptPath,
ScriptText = scriptText
};
}
private static async Task<string> ReadStringAsync(Stream stream)
{
// Define a buffer size and initialize a memory stream
byte[] buffer = new byte[1024];
using MemoryStream memoryStream = new();
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{
memoryStream.Write(buffer, 0, bytesRead);
//CVRLuaToolsExtensionMod.Logger.Msg($"Read {bytesRead} bytes.");
if (bytesRead < buffer.Length) break; // Assuming no partial writes
}
return Encoding.UTF8.GetString(memoryStream.ToArray());
}
private static void ProcessScriptInfo(ScriptInfo scriptInfo)
{
// Handle the received ScriptInfo data
//Debug.Log($"Received ScriptInfo: {scriptInfo.AssetId}, {scriptInfo.LuaComponentId}, {scriptInfo.ScriptName}");
LuaHotReloadManager.OnReceiveUpdatedScript(scriptInfo);
}
}

View file

@ -0,0 +1,12 @@
namespace NAK.CVRLuaToolsExtension;
[Serializable]
public struct ScriptInfo
{
public string AssetId; // we will reload all scripts with this asset id
public int LuaComponentId; // the target lua component id
public string ScriptName; // the new script name
public string ScriptPath;
public string ScriptText; // the new script text
}

View file

@ -0,0 +1,68 @@
using System.Reflection;
using ABI_RC.Systems.GameEventSystem;
using ABI.CCK.Components;
using HarmonyLib;
using MelonLoader;
using NAK.CVRLuaToolsExtension.NamedPipes;
using UnityEngine;
namespace NAK.CVRLuaToolsExtension;
public class CVRLuaToolsExtensionMod : MelonMod
{
internal static MelonLogger.Instance Logger;
#region Melon Preferences
private const string ModName = nameof(CVRLuaToolsExtension);
private static readonly MelonPreferences_Category Category =
MelonPreferences.CreateCategory(ModName);
internal static readonly MelonPreferences_Entry<bool> EntryAttemptInitOffMainThread =
Category.CreateEntry("attempt_init_off_main_thread", false,
"Attempt init off Main Thread", "Attempt to initialize the lua script off main thread.");
#endregion Melon Preferences
#region Melon Events
public override void OnInitializeMelon()
{
Logger = LoggerInstance;
HarmonyInstance.Patch(
typeof(CVRLuaClientBehaviour).GetMethod(nameof(CVRLuaClientBehaviour.InitializeLuaVm)), // protected
postfix: new HarmonyMethod(typeof(CVRLuaToolsExtensionMod).GetMethod(nameof(OnCVRBaseLuaBehaviourLoadAndRunScript),
BindingFlags.NonPublic | BindingFlags.Static))
);
HarmonyInstance.Patch(
typeof(CVRLuaClientBehaviour).GetMethod(nameof(CVRLuaClientBehaviour.OnDestroy)), // public for some reason
postfix: new HarmonyMethod(typeof(CVRLuaToolsExtensionMod).GetMethod(nameof(OnCVRBaseLuaBehaviourDestroy),
BindingFlags.NonPublic | BindingFlags.Static))
);
CVRGameEventSystem.Initialization.OnPlayerSetupStart.AddListener(
() => NamedPipeServer.Instance.StartListening());
}
#endregion Melon Events
#region Harmony Patches
// theres no good place to patch where GetOwnerId & GetObjectId would be valid to call...
// pls move InitializeMain after Context is created... pls
private static void OnCVRBaseLuaBehaviourLoadAndRunScript(CVRLuaClientBehaviour __instance)
{
LuaHotReloadManager.OnCVRLuaBaseBehaviourLoadAndRunScript(__instance);
}
private static void OnCVRBaseLuaBehaviourDestroy(CVRLuaClientBehaviour __instance)
{
LuaHotReloadManager.OnCVRLuaBaseBehaviourDestroy(__instance);
if (CVRLuaClientBehaviourExtensions._isRestarting.ContainsKey(__instance))
CVRLuaClientBehaviourExtensions._isRestarting.Remove(__instance);
}
#endregion Harmony Patches
}

View file

@ -0,0 +1,32 @@
using MelonLoader;
using NAK.CVRLuaToolsExtension.Properties;
using System.Reflection;
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyTitle(nameof(NAK.CVRLuaToolsExtension))]
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
[assembly: AssemblyProduct(nameof(NAK.CVRLuaToolsExtension))]
[assembly: MelonInfo(
typeof(NAK.CVRLuaToolsExtension.CVRLuaToolsExtensionMod),
nameof(NAK.CVRLuaToolsExtension),
AssemblyInfoParams.Version,
AssemblyInfoParams.Author,
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/CVRLuaToolsExtension"
)]
[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.CVRLuaToolsExtension.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.0.1";
public const string Author = "NotAKidoS";
}

View file

@ -0,0 +1,14 @@
# CVRLuaToolsExtension
Extension mod for [CVRLuaTools](https://github.com/NotAKidoS/CVRLuaTools) Hot Reload functionality.
---
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": -1,
"name": "CVRLuaToolsExtension",
"modversion": "1.0.0",
"gameversion": "2024r175",
"loaderversion": "0.6.1",
"modtype": "Mod",
"author": "NotAKidoS",
"description": "Extension mod for [CVRLuaTools](https://github.com/NotAKidoS/CVRLuaTools) Hot Reload functionality.",
"searchtags": [
"lua",
"scripting",
"hotreload",
"reload",
"development"
],
"requirements": [
"None"
],
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r33/CVRLuaToolsExtension.dll",
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/CVRLuaToolsExtension/",
"changelog": "- Initial release",
"embedcolor": "#f61963"
}