mirror of
https://github.com/NotAKidoS/NAK_CVR_Mods.git
synced 2025-09-02 06:19:22 +00:00
further cleanup of repo
This commit is contained in:
parent
4f8dcb0cd0
commit
323eb92f2e
140 changed files with 1 additions and 2430 deletions
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<RootNamespace>LoadedObjectHack</RootNamespace>
|
||||
</PropertyGroup>
|
||||
</Project>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
68
.Experimental/CVRLuaToolsExtension/Main.cs
Normal file
68
.Experimental/CVRLuaToolsExtension/Main.cs
Normal 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
|
||||
}
|
|
@ -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";
|
||||
}
|
14
.Experimental/CVRLuaToolsExtension/README.md
Normal file
14
.Experimental/CVRLuaToolsExtension/README.md
Normal 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.
|
24
.Experimental/CVRLuaToolsExtension/format.json
Normal file
24
.Experimental/CVRLuaToolsExtension/format.json
Normal 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"
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
58
.Experimental/LuaNetworkVariables/Main.cs
Normal file
58
.Experimental/LuaNetworkVariables/Main.cs
Normal file
|
@ -0,0 +1,58 @@
|
|||
using ABI_RC.Core.Player;
|
||||
using MelonLoader;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.LuaNetVars;
|
||||
|
||||
public class LuaNetVarsMod : MelonMod
|
||||
{
|
||||
internal static MelonLogger.Instance Logger;
|
||||
|
||||
#region Melon Preferences
|
||||
|
||||
|
||||
#endregion Melon Preferences
|
||||
|
||||
#region Melon Events
|
||||
|
||||
public override void OnInitializeMelon()
|
||||
{
|
||||
Logger = LoggerInstance;
|
||||
|
||||
ApplyPatches(typeof(Patches.LuaScriptFactory_Patches));
|
||||
ApplyPatches(typeof(Patches.CVRSyncHelper_Patches));
|
||||
}
|
||||
|
||||
public override void OnUpdate()
|
||||
{
|
||||
// if (Input.GetKeyDown(KeyCode.F1))
|
||||
// {
|
||||
// PlayerSetup.Instance.DropProp("be0b5acc-a987-48dc-a28b-62bd912fe3a0");
|
||||
// }
|
||||
//
|
||||
// if (Input.GetKeyDown(KeyCode.F2))
|
||||
// {
|
||||
// GameObject go = new("TestSyncedObject");
|
||||
// go.AddComponent<TestSyncedObject>();
|
||||
// }
|
||||
}
|
||||
|
||||
#endregion Melon Events
|
||||
|
||||
#region Melon Mod Utilities
|
||||
|
||||
private void ApplyPatches(Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
HarmonyInstance.PatchAll(type);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LoggerInstance.Msg($"Failed while patching {type.Name}!");
|
||||
LoggerInstance.Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Melon Mod Utilities
|
||||
}
|
137
.Experimental/LuaNetworkVariables/NetLuaModule.cs
Normal file
137
.Experimental/LuaNetworkVariables/NetLuaModule.cs
Normal file
|
@ -0,0 +1,137 @@
|
|||
using ABI_RC.Core.Base;
|
||||
using ABI.Scripting.CVRSTL.Common;
|
||||
using JetBrains.Annotations;
|
||||
using NAK.LuaNetVars;
|
||||
using MoonSharp.Interpreter;
|
||||
|
||||
namespace NAK.LuaNetVars.Modules;
|
||||
|
||||
[PublicAPI] // Indicates that this class is used and should not be considered unused
|
||||
public class LuaNetModule : BaseScriptedStaticWrapper
|
||||
{
|
||||
public const string MODULE_ID = "NetworkModule";
|
||||
|
||||
private LuaNetVarController _controller;
|
||||
|
||||
public LuaNetModule(CVRLuaContext context) : base(context)
|
||||
{
|
||||
_controller = context.behaviour.AddComponentIfMissing<LuaNetVarController>();
|
||||
}
|
||||
|
||||
internal static object RegisterUserData(Script script, CVRLuaContext context)
|
||||
{
|
||||
// Register the LuaNetModule type with MoonSharp
|
||||
UserData.RegisterType<LuaNetModule>(InteropAccessMode.Default, MODULE_ID);
|
||||
return new LuaNetModule(context);
|
||||
}
|
||||
|
||||
#region Module Instance Methods
|
||||
|
||||
/// <summary>
|
||||
/// Registers a network variable that can be synchronized over the network.
|
||||
/// </summary>
|
||||
/// <param name="varName">The name of the variable to register.</param>
|
||||
public void RegisterNetworkVar(string varName)
|
||||
{
|
||||
CheckIfCanAccessMethod(nameof(RegisterNetworkVar), false,
|
||||
CVRLuaEnvironmentContext.CLIENT, CVRLuaObjectContext.ALL_BUT_EVENTS, CVRLuaOwnerContext.ANY);
|
||||
|
||||
if (_controller == null)
|
||||
{
|
||||
LuaNetVarsMod.Logger.Error("LuaNetVarController is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
_controller.RegisterNetworkVar(varName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a callback function to be called when the specified network variable changes.
|
||||
/// </summary>
|
||||
/// <param name="varName">The name of the variable to watch.</param>
|
||||
/// <param name="callback">The Lua function to call when the variable changes.</param>
|
||||
public void RegisterNotifyCallback(string varName, DynValue callback)
|
||||
{
|
||||
CheckIfCanAccessMethod(nameof(RegisterNotifyCallback), false,
|
||||
CVRLuaEnvironmentContext.CLIENT, CVRLuaObjectContext.ALL_BUT_EVENTS, CVRLuaOwnerContext.ANY);
|
||||
|
||||
if (_controller == null)
|
||||
{
|
||||
LuaNetVarsMod.Logger.Error("LuaNetVarController is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
_controller.RegisterNotifyCallback(varName, callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a callback function to be called when the specified event is received.
|
||||
/// </summary>
|
||||
/// <param name="eventName">The name of the event to watch.</param>
|
||||
/// <param name="callback">The Lua function to call when the event occurs.</param>
|
||||
public void RegisterEventCallback(string eventName, DynValue callback)
|
||||
{
|
||||
CheckIfCanAccessMethod(nameof(RegisterEventCallback), false,
|
||||
CVRLuaEnvironmentContext.CLIENT, CVRLuaObjectContext.ALL_BUT_EVENTS, CVRLuaOwnerContext.ANY);
|
||||
|
||||
if (_controller == null)
|
||||
{
|
||||
LuaNetVarsMod.Logger.Error("LuaNetVarController is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
_controller.RegisterEventCallback(eventName, callback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a Lua event to other clients.
|
||||
/// </summary>
|
||||
/// <param name="eventName">The name of the event to send.</param>
|
||||
/// <param name="args">Optional arguments to send with the event.</param>
|
||||
public void SendLuaEvent(string eventName, params DynValue[] args)
|
||||
{
|
||||
CheckIfCanAccessMethod(nameof(SendLuaEvent), false,
|
||||
CVRLuaEnvironmentContext.CLIENT, CVRLuaObjectContext.ALL_BUT_EVENTS, CVRLuaOwnerContext.ANY);
|
||||
|
||||
if (_controller == null)
|
||||
{
|
||||
LuaNetVarsMod.Logger.Error("LuaNetVarController is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
_controller.SendLuaEvent(eventName, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the current client is the owner of the synchronized object.
|
||||
/// </summary>
|
||||
public bool IsSyncOwner()
|
||||
{
|
||||
CheckIfCanAccessMethod(nameof(IsSyncOwner), false,
|
||||
CVRLuaEnvironmentContext.CLIENT, CVRLuaObjectContext.ALL_BUT_EVENTS, CVRLuaOwnerContext.ANY);
|
||||
|
||||
if (_controller == null)
|
||||
{
|
||||
LuaNetVarsMod.Logger.Error("LuaNetVarController is null.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return _controller.IsSyncOwner();
|
||||
}
|
||||
|
||||
public string GetSyncOwner()
|
||||
{
|
||||
CheckIfCanAccessMethod(nameof(GetSyncOwner), false,
|
||||
CVRLuaEnvironmentContext.CLIENT, CVRLuaObjectContext.ALL_BUT_EVENTS, CVRLuaOwnerContext.ANY);
|
||||
|
||||
if (_controller == null)
|
||||
{
|
||||
LuaNetVarsMod.Logger.Error("LuaNetVarController is null.");
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return _controller.GetSyncOwner();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
using MoonSharp.Interpreter;
|
||||
using ABI_RC.Core.Player;
|
||||
using ABI_RC.Core.Savior;
|
||||
|
||||
namespace NAK.LuaNetVars;
|
||||
|
||||
public struct LuaEventContext
|
||||
{
|
||||
private string SenderId { get; set; }
|
||||
public string SenderName { get; private set; }
|
||||
private DateTime LastInvokeTime { get; set; }
|
||||
private double TimeSinceLastInvoke { get; set; }
|
||||
private bool IsLocal { get; set; }
|
||||
|
||||
public static LuaEventContext Create(string senderId, DateTime lastInvokeTime)
|
||||
{
|
||||
var playerName = CVRPlayerManager.Instance.TryGetPlayerName(senderId);
|
||||
|
||||
return new LuaEventContext
|
||||
{
|
||||
SenderId = senderId,
|
||||
SenderName = playerName ?? "Unknown",
|
||||
LastInvokeTime = lastInvokeTime,
|
||||
TimeSinceLastInvoke = (DateTime.Now - lastInvokeTime).TotalSeconds,
|
||||
IsLocal = senderId == MetaPort.Instance.ownerId
|
||||
};
|
||||
}
|
||||
|
||||
public Table ToLuaTable(Script script)
|
||||
{
|
||||
Table table = new(script)
|
||||
{
|
||||
["senderId"] = SenderId,
|
||||
["senderName"] = SenderName,
|
||||
["lastInvokeTime"] = LastInvokeTime.ToUniversalTime().ToString("O"),
|
||||
["timeSinceLastInvoke"] = TimeSinceLastInvoke,
|
||||
["isLocal"] = IsLocal
|
||||
};
|
||||
return table;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
namespace NAK.LuaNetVars;
|
||||
|
||||
internal class LuaEventTracker
|
||||
{
|
||||
private class EventMetadata
|
||||
{
|
||||
public DateTime LastInvokeTime { get; set; }
|
||||
public Dictionary<string, DateTime> LastInvokeTimePerSender { get; } = new();
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, EventMetadata> _eventMetadata = new();
|
||||
|
||||
public DateTime GetLastInvokeTime(string eventName)
|
||||
{
|
||||
if (!_eventMetadata.TryGetValue(eventName, out var metadata))
|
||||
{
|
||||
metadata = new EventMetadata();
|
||||
_eventMetadata[eventName] = metadata;
|
||||
}
|
||||
return metadata.LastInvokeTime;
|
||||
}
|
||||
|
||||
public DateTime GetLastInvokeTimeForSender(string eventName, string senderId)
|
||||
{
|
||||
if (!_eventMetadata.TryGetValue(eventName, out var metadata))
|
||||
{
|
||||
metadata = new EventMetadata();
|
||||
_eventMetadata[eventName] = metadata;
|
||||
}
|
||||
|
||||
if (!metadata.LastInvokeTimePerSender.TryGetValue(senderId, out DateTime time))
|
||||
{
|
||||
return DateTime.MinValue;
|
||||
}
|
||||
return time;
|
||||
}
|
||||
|
||||
public void UpdateInvokeTime(string eventName, string senderId)
|
||||
{
|
||||
if (!_eventMetadata.TryGetValue(eventName, out EventMetadata metadata))
|
||||
{
|
||||
metadata = new EventMetadata();
|
||||
_eventMetadata[eventName] = metadata;
|
||||
}
|
||||
|
||||
DateTime now = DateTime.Now;
|
||||
metadata.LastInvokeTime = now;
|
||||
metadata.LastInvokeTimePerSender[senderId] = now;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_eventMetadata.Clear();
|
||||
}
|
||||
|
||||
public void ClearEvent(string eventName)
|
||||
{
|
||||
_eventMetadata.Remove(eventName);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
using ABI_RC.Core.Savior;
|
||||
using ABI_RC.Core.Util;
|
||||
using ABI_RC.Systems.ModNetwork;
|
||||
using ABI.CCK.Components;
|
||||
using ABI.Scripting.CVRSTL.Common;
|
||||
using MoonSharp.Interpreter;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.LuaNetVars;
|
||||
|
||||
public partial class LuaNetVarController : MonoBehaviour
|
||||
{
|
||||
private static readonly HashSet<int> _hashes = new();
|
||||
|
||||
private const string MODULE_ID = "NAK.LNV:";
|
||||
|
||||
private int _uniquePathHash;
|
||||
private string ModNetworkID { get; set; }
|
||||
private CVRLuaClientBehaviour _luaClientBehaviour;
|
||||
|
||||
private readonly Dictionary<string, DynValue> _registeredNetworkVars = new();
|
||||
private readonly Dictionary<string, DynValue> _registeredNotifyCallbacks = new();
|
||||
private readonly Dictionary<string, DynValue> _registeredEventCallbacks = new();
|
||||
private readonly HashSet<string> _dirtyVariables = new();
|
||||
|
||||
private bool _requestInitialSync;
|
||||
private CVRSpawnable _spawnable;
|
||||
private CVRObjectSync _objectSync;
|
||||
|
||||
#region Unity Events
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (!Initialize())
|
||||
return;
|
||||
|
||||
// TODO: a manager script should be in charge of this
|
||||
// TODO: disabling object will kill coroutine
|
||||
StartCoroutine(SendVariableUpdatesCoroutine());
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
=> Cleanup();
|
||||
|
||||
#endregion Unity Events
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private bool Initialize()
|
||||
{
|
||||
if (!TryGetComponent(out _luaClientBehaviour)) return false;
|
||||
if (!TryGetUniqueNetworkID(out _uniquePathHash)) return false;
|
||||
|
||||
ModNetworkID = MODULE_ID + _uniquePathHash.ToString("X8");
|
||||
|
||||
if (ModNetworkID.Length > ModNetworkManager.MaxMessageIdLength)
|
||||
{
|
||||
LuaNetVarsMod.Logger.Error($"ModNetworkID ({ModNetworkID}) exceeds max length of {ModNetworkManager.MaxMessageIdLength} characters!");
|
||||
return false;
|
||||
}
|
||||
|
||||
_hashes.Add(_uniquePathHash);
|
||||
ModNetworkManager.Subscribe(ModNetworkID, OnMessageReceived);
|
||||
LuaNetVarsMod.Logger.Msg($"Registered LuaNetVarController with ModNetworkID: {ModNetworkID}");
|
||||
|
||||
switch (_luaClientBehaviour.Context.objContext)
|
||||
{
|
||||
case CVRLuaObjectContext.AVATAR:
|
||||
_requestInitialSync = !_luaClientBehaviour.Context.IsWornByMe;
|
||||
break;
|
||||
case CVRLuaObjectContext.PROP:
|
||||
_spawnable = _luaClientBehaviour.Context.RootComponent as CVRSpawnable;
|
||||
_requestInitialSync = !_luaClientBehaviour.Context.IsSpawnedByMe;
|
||||
break;
|
||||
case CVRLuaObjectContext.WORLD:
|
||||
_objectSync = GetComponentInParent<CVRObjectSync>();
|
||||
_requestInitialSync = true; // idk probably works
|
||||
break;
|
||||
default:
|
||||
_requestInitialSync = true;
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: evaluate if having dedicated globals is better behaviour (i think so)
|
||||
// private void ConfigureLuaEnvironment()
|
||||
// {
|
||||
// _luaClientBehaviour.script.Globals["SendLuaEvent"] = DynValue.NewCallback(SendLuaEventCallback);
|
||||
// }
|
||||
|
||||
private void Cleanup()
|
||||
{
|
||||
_eventTracker.Clear();
|
||||
|
||||
if (_uniquePathHash == 0 || string.IsNullOrEmpty(ModNetworkID))
|
||||
return;
|
||||
|
||||
ModNetworkManager.Unsubscribe(ModNetworkID);
|
||||
LuaNetVarsMod.Logger.Msg($"Unsubscribed LuaNetVarController with ModNetworkID: {ModNetworkID}");
|
||||
_hashes.Remove(_uniquePathHash);
|
||||
}
|
||||
|
||||
private System.Collections.IEnumerator SendVariableUpdatesCoroutine()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
yield return new WaitForSeconds(0.1f);
|
||||
if (IsSyncOwner()) SendVariableUpdates();
|
||||
if (!_requestInitialSync) continue;
|
||||
_requestInitialSync = false;
|
||||
RequestVariableSync();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Private Methods
|
||||
|
||||
#region Ownership Methods
|
||||
|
||||
public bool IsSyncOwner()
|
||||
{
|
||||
if (_objectSync) return _objectSync.SyncedByMe; // idk
|
||||
if (_spawnable)
|
||||
{
|
||||
if (_spawnable.IsSyncedByMe()) return true; // is held / attached locally
|
||||
CVRSyncHelper.PropData propData = CVRSyncHelper.Props.Find(x => x.InstanceId == _spawnable.instanceId);
|
||||
if (propData != null) return propData.syncedBy == MetaPort.Instance.ownerId; // last updated by me
|
||||
return false; // not held / attached locally and not last updated by me
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public string GetSyncOwner()
|
||||
{
|
||||
if (_objectSync) return _objectSync.syncedBy;
|
||||
if (_spawnable)
|
||||
{
|
||||
CVRSyncHelper.PropData propData = CVRSyncHelper.Props.Find(x => x.InstanceId == _spawnable.instanceId);
|
||||
return propData?.syncedBy ?? string.Empty;
|
||||
}
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
#endregion Ownership Methods
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
using ABI_RC.Core.Savior;
|
||||
using ABI_RC.Systems.ModNetwork;
|
||||
using MoonSharp.Interpreter;
|
||||
using Unity.Services.Authentication.Internal;
|
||||
|
||||
namespace NAK.LuaNetVars
|
||||
{
|
||||
public partial class LuaNetVarController
|
||||
{
|
||||
private enum MessageType : byte
|
||||
{
|
||||
LuaVariable = 0,
|
||||
LuaEvent = 1,
|
||||
SyncVariables = 2,
|
||||
RequestSync = 3
|
||||
}
|
||||
|
||||
private readonly LuaEventTracker _eventTracker = new();
|
||||
|
||||
#region Mod Network Events
|
||||
|
||||
private void OnMessageReceived(ModNetworkMessage msg)
|
||||
{
|
||||
msg.Read(out byte msgTypeRaw);
|
||||
if (!Enum.IsDefined(typeof(MessageType), msgTypeRaw)) return;
|
||||
|
||||
MessageType msgType = (MessageType)msgTypeRaw;
|
||||
switch (msgType)
|
||||
{
|
||||
case MessageType.LuaVariable:
|
||||
HandleLuaVariableUpdate(msg);
|
||||
break;
|
||||
case MessageType.LuaEvent:
|
||||
HandleLuaEvent(msg);
|
||||
break;
|
||||
case MessageType.SyncVariables:
|
||||
HandleSyncVariables(msg);
|
||||
break;
|
||||
case MessageType.RequestSync:
|
||||
HandleRequestSyncVariables(msg);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleLuaVariableUpdate(ModNetworkMessage msg)
|
||||
{
|
||||
msg.Read(out string varName);
|
||||
DynValue newValue = DeserializeDynValue(msg);
|
||||
|
||||
LuaNetVarsMod.Logger.Msg($"Received LuaVariable update: {varName} = {newValue}");
|
||||
|
||||
if (_registeredNetworkVars.TryGetValue(varName, out DynValue var))
|
||||
{
|
||||
UpdateNetworkVariable(varName, var, newValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
LuaNetVarsMod.Logger.Warning($"Received update for unregistered variable {varName}");
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleLuaEvent(ModNetworkMessage msg)
|
||||
{
|
||||
string senderId = msg.Sender;
|
||||
msg.Read(out string eventName);
|
||||
msg.Read(out int argsCount);
|
||||
|
||||
DateTime lastInvokeTime = _eventTracker.GetLastInvokeTimeForSender(eventName, senderId);
|
||||
LuaEventContext context = LuaEventContext.Create(senderId, lastInvokeTime);
|
||||
|
||||
// Update tracking
|
||||
_eventTracker.UpdateInvokeTime(eventName, senderId);
|
||||
|
||||
// Read event arguments
|
||||
var args = new DynValue[argsCount + 1]; // +1 for context
|
||||
args[0] = DynValue.FromObject(_luaClientBehaviour.script, context.ToLuaTable(_luaClientBehaviour.script));
|
||||
|
||||
for (int i = 0; i < argsCount; i++)
|
||||
{
|
||||
args[i + 1] = DeserializeDynValue(msg);
|
||||
}
|
||||
|
||||
LuaNetVarsMod.Logger.Msg($"Received LuaEvent: {eventName} from {context.SenderName} with {argsCount} args");
|
||||
|
||||
InvokeLuaEvent(eventName, args);
|
||||
}
|
||||
|
||||
private void HandleSyncVariables(ModNetworkMessage msg)
|
||||
{
|
||||
msg.Read(out int varCount);
|
||||
for (int i = 0; i < varCount; i++)
|
||||
{
|
||||
msg.Read(out string varName);
|
||||
DynValue newValue = DeserializeDynValue(msg);
|
||||
|
||||
if (_registeredNetworkVars.TryGetValue(varName, out DynValue var))
|
||||
{
|
||||
UpdateNetworkVariable(varName, var, newValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
LuaNetVarsMod.Logger.Warning($"Received sync for unregistered variable {varName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleRequestSyncVariables(ModNetworkMessage msg)
|
||||
{
|
||||
if (!IsSyncOwner()) return;
|
||||
SendVariableSyncToUser(msg.Sender);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Invocation
|
||||
|
||||
private void InvokeLuaEvent(string eventName, DynValue[] args)
|
||||
{
|
||||
if (_registeredEventCallbacks.TryGetValue(eventName, out DynValue callback))
|
||||
{
|
||||
_luaClientBehaviour.script.Call(callback, args);
|
||||
}
|
||||
else
|
||||
{
|
||||
LuaNetVarsMod.Logger.Warning($"No registered callback for event {eventName}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Sending Methods
|
||||
|
||||
private void SendVariableUpdates()
|
||||
{
|
||||
if (_dirtyVariables.Count == 0) return;
|
||||
|
||||
using ModNetworkMessage modMsg = new(ModNetworkID); // can pass target userids as params if needed
|
||||
modMsg.Write((byte)MessageType.SyncVariables);
|
||||
modMsg.Write(_dirtyVariables.Count);
|
||||
modMsg.Send();
|
||||
|
||||
foreach (var varName in _dirtyVariables)
|
||||
{
|
||||
modMsg.Write(varName);
|
||||
SerializeDynValue(modMsg, _registeredNetworkVars[varName]);
|
||||
}
|
||||
|
||||
_dirtyVariables.Clear();
|
||||
}
|
||||
|
||||
private void SendVariableSyncToUser(string userId)
|
||||
{
|
||||
using ModNetworkMessage modMsg = new(ModNetworkID, userId);
|
||||
modMsg.Write((byte)MessageType.SyncVariables);
|
||||
modMsg.Write(_registeredNetworkVars.Count);
|
||||
foreach (var kvp in _registeredNetworkVars)
|
||||
{
|
||||
modMsg.Write(kvp.Key);
|
||||
SerializeDynValue(modMsg, kvp.Value);
|
||||
}
|
||||
modMsg.Send();
|
||||
|
||||
LuaNetVarsMod.Logger.Msg($"Sent variable sync to {userId}");
|
||||
}
|
||||
|
||||
private void RequestVariableSync()
|
||||
{
|
||||
using ModNetworkMessage modMsg = new(ModNetworkID);
|
||||
modMsg.Write((byte)MessageType.RequestSync);
|
||||
modMsg.Send();
|
||||
LuaNetVarsMod.Logger.Msg("Requested variable sync");
|
||||
}
|
||||
|
||||
// private DynValue SendLuaEventCallback(ScriptExecutionContext context, CallbackArguments args)
|
||||
// {
|
||||
// if (args.Count < 1) return DynValue.Nil;
|
||||
//
|
||||
// var eventName = args[0].CastToString();
|
||||
// var eventArgs = args.GetArray().Skip(1).ToArray();
|
||||
//
|
||||
// SendLuaEvent(eventName, eventArgs);
|
||||
//
|
||||
// return DynValue.Nil;
|
||||
// }
|
||||
|
||||
internal void SendLuaEvent(string eventName, DynValue[] args)
|
||||
{
|
||||
string senderId = MetaPort.Instance.ownerId;
|
||||
DateTime lastInvokeTime = _eventTracker.GetLastInvokeTimeForSender(eventName, senderId);
|
||||
LuaEventContext context = LuaEventContext.Create(senderId, lastInvokeTime);
|
||||
|
||||
// Update tracking
|
||||
_eventTracker.UpdateInvokeTime(eventName, senderId);
|
||||
|
||||
var argsWithContext = new DynValue[args.Length + 1];
|
||||
argsWithContext[0] = DynValue.FromObject(_luaClientBehaviour.script, context.ToLuaTable(_luaClientBehaviour.script));
|
||||
Array.Copy(args, 0, argsWithContext, 1, args.Length);
|
||||
|
||||
InvokeLuaEvent(eventName, argsWithContext);
|
||||
|
||||
using ModNetworkMessage modMsg = new(ModNetworkID);
|
||||
modMsg.Write((byte)MessageType.LuaEvent);
|
||||
modMsg.Write(eventName);
|
||||
modMsg.Write(args.Length);
|
||||
|
||||
foreach (DynValue arg in args)
|
||||
SerializeDynValue(modMsg, arg);
|
||||
|
||||
modMsg.Send();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
using MoonSharp.Interpreter;
|
||||
|
||||
namespace NAK.LuaNetVars;
|
||||
|
||||
public partial class LuaNetVarController
|
||||
{
|
||||
internal void RegisterNetworkVar(string varName)
|
||||
{
|
||||
if (_registeredNetworkVars.ContainsKey(varName))
|
||||
{
|
||||
LuaNetVarsMod.Logger.Warning($"Network variable {varName} already registered!");
|
||||
return;
|
||||
}
|
||||
|
||||
_registeredNetworkVars[varName] = DynValue.Nil;
|
||||
_luaClientBehaviour.script.Globals[varName] = DynValue.Nil;
|
||||
|
||||
RegisterGetterFunction(varName);
|
||||
RegisterSetterFunction(varName);
|
||||
|
||||
LuaNetVarsMod.Logger.Msg($"Registered network variable {varName}");
|
||||
}
|
||||
|
||||
private void RegisterGetterFunction(string varName)
|
||||
{
|
||||
_luaClientBehaviour.script.Globals["Get" + varName] = DynValue.NewCallback((context, args) =>
|
||||
{
|
||||
return _registeredNetworkVars.TryGetValue(varName, out var value) ? value : DynValue.Nil;
|
||||
});
|
||||
}
|
||||
|
||||
private void RegisterSetterFunction(string varName)
|
||||
{
|
||||
_luaClientBehaviour.script.Globals["Set" + varName] = DynValue.NewCallback((context, args) =>
|
||||
{
|
||||
if (args.Count < 1) return DynValue.Nil;
|
||||
|
||||
var newValue = args[0];
|
||||
if (!IsSupportedDynValue(newValue))
|
||||
{
|
||||
LuaNetVarsMod.Logger.Error($"Unsupported DynValue type: {newValue.Type} for variable {varName}");
|
||||
return DynValue.Nil;
|
||||
}
|
||||
|
||||
if (_registeredNetworkVars.TryGetValue(varName, out var oldValue))
|
||||
{
|
||||
UpdateNetworkVariable(varName, oldValue, newValue);
|
||||
}
|
||||
|
||||
return DynValue.Nil;
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateNetworkVariable(string varName, DynValue oldValue, DynValue newValue)
|
||||
{
|
||||
_registeredNetworkVars[varName] = newValue;
|
||||
_luaClientBehaviour.script.Globals[varName] = newValue;
|
||||
_dirtyVariables.Add(varName);
|
||||
|
||||
if (_registeredNotifyCallbacks.TryGetValue(varName, out var callback))
|
||||
{
|
||||
_luaClientBehaviour.script.Call(callback, DynValue.NewString(varName), oldValue, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
internal void RegisterNotifyCallback(string varName, DynValue callback)
|
||||
{
|
||||
if (!ValidateCallback(callback) || !ValidateNetworkVar(varName)) return;
|
||||
|
||||
if (_registeredNotifyCallbacks.ContainsKey(varName))
|
||||
LuaNetVarsMod.Logger.Warning($"Overwriting notify callback for {varName}");
|
||||
|
||||
_registeredNotifyCallbacks[varName] = callback;
|
||||
LuaNetVarsMod.Logger.Msg($"Registered notify callback for {varName}");
|
||||
}
|
||||
|
||||
internal void RegisterEventCallback(string eventName, DynValue callback)
|
||||
{
|
||||
if (!ValidateCallback(callback)) return;
|
||||
|
||||
if (_registeredEventCallbacks.ContainsKey(eventName))
|
||||
LuaNetVarsMod.Logger.Warning($"Overwriting event callback for {eventName}");
|
||||
|
||||
_registeredEventCallbacks[eventName] = callback;
|
||||
LuaNetVarsMod.Logger.Msg($"Registered event callback for {eventName}");
|
||||
}
|
||||
|
||||
private bool ValidateCallback(DynValue callback)
|
||||
{
|
||||
if (callback?.Function != null) return true;
|
||||
LuaNetVarsMod.Logger.Error("Passed DynValue must be a function");
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool ValidateNetworkVar(string varName)
|
||||
{
|
||||
if (_registeredNetworkVars.ContainsKey(varName)) return true;
|
||||
LuaNetVarsMod.Logger.Error($"Attempted to register notify callback for non-registered variable {varName}.");
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
using ABI_RC.Systems.ModNetwork;
|
||||
using MoonSharp.Interpreter;
|
||||
|
||||
namespace NAK.LuaNetVars;
|
||||
|
||||
public partial class LuaNetVarController
|
||||
{
|
||||
private static DynValue DeserializeDynValue(ModNetworkMessage msg)
|
||||
{
|
||||
msg.Read(out byte dataTypeByte);
|
||||
DataType dataType = (DataType)dataTypeByte;
|
||||
|
||||
switch (dataType)
|
||||
{
|
||||
case DataType.Boolean:
|
||||
msg.Read(out bool boolValue);
|
||||
return DynValue.NewBoolean(boolValue);
|
||||
case DataType.Number:
|
||||
msg.Read(out double numberValue);
|
||||
return DynValue.NewNumber(numberValue);
|
||||
case DataType.String:
|
||||
msg.Read(out string stringValue);
|
||||
return DynValue.NewString(stringValue);
|
||||
case DataType.Nil:
|
||||
return DynValue.Nil;
|
||||
default:
|
||||
LuaNetVarsMod.Logger.Error($"Unsupported data type received: {dataType}");
|
||||
return DynValue.Nil;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SerializeDynValue(ModNetworkMessage msg, DynValue value)
|
||||
{
|
||||
switch (value.Type)
|
||||
{
|
||||
case DataType.Boolean:
|
||||
msg.Write((byte)DataType.Boolean);
|
||||
msg.Write(value.Boolean);
|
||||
break;
|
||||
case DataType.Number:
|
||||
msg.Write((byte)DataType.Number);
|
||||
msg.Write(value.Number);
|
||||
break;
|
||||
case DataType.String:
|
||||
msg.Write((byte)DataType.String);
|
||||
msg.Write(value.String);
|
||||
break;
|
||||
case DataType.Nil:
|
||||
msg.Write((byte)DataType.Nil);
|
||||
break;
|
||||
default:
|
||||
LuaNetVarsMod.Logger.Error($"Unsupported DynValue type: {value.Type}");
|
||||
msg.Write((byte)DataType.Nil);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSupportedDynValue(DynValue value)
|
||||
{
|
||||
return value.Type is DataType.Boolean or DataType.Number or DataType.String or DataType.Nil;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
using ABI_RC.Core.Savior;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.LuaNetVars;
|
||||
|
||||
public partial class LuaNetVarController
|
||||
{
|
||||
private bool TryGetUniqueNetworkID(out int hash)
|
||||
{
|
||||
string path = GetGameObjectPath(transform);
|
||||
hash = path.GetHashCode();
|
||||
|
||||
// Check if it already exists (this **should** only matter in worlds)
|
||||
if (_hashes.Contains(hash))
|
||||
{
|
||||
LuaNetVarsMod.Logger.Warning($"Duplicate RelativeSyncMarker found at path {path}");
|
||||
if (!FindAvailableHash(ref hash)) // Super lazy fix idfc
|
||||
{
|
||||
LuaNetVarsMod.Logger.Error($"Failed to find available hash for RelativeSyncMarker after 16 tries! {path}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_hashes.Add(hash);
|
||||
|
||||
return true;
|
||||
|
||||
static bool FindAvailableHash(ref int hash)
|
||||
{
|
||||
for (int i = 0; i < 16; i++)
|
||||
{
|
||||
hash += 1;
|
||||
if (!_hashes.Contains(hash)) return true;
|
||||
}
|
||||
return false; // Failed to find a hash in 16 tries
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetGameObjectPath(Transform transform)
|
||||
{
|
||||
string path = transform.name;
|
||||
while (transform.parent != null)
|
||||
{
|
||||
transform = transform.parent;
|
||||
|
||||
// Only true at root of local player object
|
||||
if (transform.CompareTag("Player"))
|
||||
{
|
||||
path = MetaPort.Instance.ownerId + "/" + path;
|
||||
break;
|
||||
}
|
||||
|
||||
path = transform.name + "/" + path;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
65
.Experimental/LuaNetworkVariables/Patches.cs
Normal file
65
.Experimental/LuaNetworkVariables/Patches.cs
Normal file
|
@ -0,0 +1,65 @@
|
|||
using ABI_RC.Core.Base;
|
||||
using ABI_RC.Core.Savior;
|
||||
using ABI_RC.Core.Util;
|
||||
using ABI.CCK.Components;
|
||||
using ABI.Scripting.CVRSTL.Client;
|
||||
using ABI.Scripting.CVRSTL.Common;
|
||||
using HarmonyLib;
|
||||
using MoonSharp.Interpreter;
|
||||
using NAK.LuaNetVars.Modules;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.LuaNetVars.Patches;
|
||||
|
||||
internal static class LuaScriptFactory_Patches
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(LuaScriptFactory.CVRRequireModule), nameof(LuaScriptFactory.CVRRequireModule.Require))]
|
||||
private static void Postfix_CVRRequireModule_require(
|
||||
string moduleFriendlyName,
|
||||
ref LuaScriptFactory.CVRRequireModule __instance,
|
||||
ref object __result,
|
||||
ref Script ____script,
|
||||
ref CVRLuaContext ____context)
|
||||
{
|
||||
if (LuaNetModule.MODULE_ID != moduleFriendlyName)
|
||||
return; // not our module
|
||||
|
||||
__result = LuaNetModule.RegisterUserData(____script, ____context);
|
||||
__instance.RegisteredModules[LuaNetModule.MODULE_ID] = __result; // add module to cache
|
||||
}
|
||||
}
|
||||
|
||||
internal static class CVRSyncHelper_Patches
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(CVRSyncHelper), nameof(CVRSyncHelper.UpdatePropValues))]
|
||||
private static void Postfix_CVRSyncHelper_UpdatePropValues(
|
||||
Vector3 position, Vector3 rotation, Vector3 scale,
|
||||
float[] syncValues, string guid, string instanceId,
|
||||
Span<float> subSyncValues, int numSyncValues, int syncType = 0)
|
||||
{
|
||||
CVRSyncHelper.PropData propData = CVRSyncHelper.Props.Find(prop => prop.InstanceId == instanceId);
|
||||
if (propData == null) return;
|
||||
|
||||
// Update locally stored prop data with new values
|
||||
// as GS does not reply with our own data...
|
||||
|
||||
propData.PositionX = position.x;
|
||||
propData.PositionY = position.y;
|
||||
propData.PositionZ = position.z;
|
||||
propData.RotationX = rotation.x;
|
||||
propData.RotationY = rotation.y;
|
||||
propData.RotationZ = rotation.z;
|
||||
propData.ScaleX = scale.x;
|
||||
propData.ScaleY = scale.y;
|
||||
propData.ScaleZ = scale.z;
|
||||
propData.CustomFloatsAmount = numSyncValues;
|
||||
for (int i = 0; i < numSyncValues; i++)
|
||||
propData.CustomFloats[i] = syncValues[i];
|
||||
|
||||
//propData.SpawnedBy
|
||||
propData.syncedBy = MetaPort.Instance.ownerId;
|
||||
propData.syncType = syncType;
|
||||
}
|
||||
}
|
32
.Experimental/LuaNetworkVariables/Properties/AssemblyInfo.cs
Normal file
32
.Experimental/LuaNetworkVariables/Properties/AssemblyInfo.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using NAK.LuaNetVars.Properties;
|
||||
using MelonLoader;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
|
||||
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
|
||||
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
|
||||
[assembly: AssemblyTitle(nameof(NAK.LuaNetVars))]
|
||||
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
|
||||
[assembly: AssemblyProduct(nameof(NAK.LuaNetVars))]
|
||||
|
||||
[assembly: MelonInfo(
|
||||
typeof(NAK.LuaNetVars.LuaNetVarsMod),
|
||||
nameof(NAK.LuaNetVars),
|
||||
AssemblyInfoParams.Version,
|
||||
AssemblyInfoParams.Author,
|
||||
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/WhereAmIPointing"
|
||||
)]
|
||||
|
||||
[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.LuaNetVars.Properties;
|
||||
internal static class AssemblyInfoParams
|
||||
{
|
||||
public const string Version = "1.0.0";
|
||||
public const string Author = "NotAKidoS";
|
||||
}
|
14
.Experimental/LuaNetworkVariables/README.md
Normal file
14
.Experimental/LuaNetworkVariables/README.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
# WhereAmIPointing
|
||||
|
||||
Simple mod that makes your controller rays always visible when the menus are open. Useful for when you're trying to aim at something in the distance. Also visualizes which ray is being used for menu interaction.
|
||||
|
||||
---
|
||||
|
||||
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.
|
|
@ -0,0 +1,326 @@
|
|||
using UnityEngine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using ABI_RC.Core.Savior;
|
||||
using ABI_RC.Systems.ModNetwork;
|
||||
|
||||
namespace NAK.LuaNetVars
|
||||
{
|
||||
public abstract class MNSyncedBehaviour : IDisposable
|
||||
{
|
||||
// Add static property for clarity
|
||||
protected static string LocalUserId => MetaPort.Instance.ownerId;
|
||||
|
||||
protected enum MessageType : byte
|
||||
{
|
||||
OwnershipRequest,
|
||||
OwnershipResponse,
|
||||
OwnershipTransfer,
|
||||
StateRequest,
|
||||
StateUpdate,
|
||||
CustomData
|
||||
}
|
||||
|
||||
protected enum OwnershipResponse : byte
|
||||
{
|
||||
Accepted,
|
||||
Rejected
|
||||
}
|
||||
|
||||
protected readonly string networkId;
|
||||
protected string currentOwnerId;
|
||||
private readonly bool autoAcceptTransfers;
|
||||
private readonly Dictionary<string, Action<bool>> pendingRequests;
|
||||
private bool isInitialized;
|
||||
private bool disposedValue;
|
||||
private bool isSoftOwner = false;
|
||||
private Timer stateRequestTimer;
|
||||
private const int StateRequestTimeout = 3000; // 3 seconds
|
||||
|
||||
public string CurrentOwnerId => currentOwnerId;
|
||||
public bool HasOwnership => currentOwnerId == LocalUserId;
|
||||
|
||||
protected MNSyncedBehaviour(string networkId, string currentOwnerId = "", bool autoAcceptTransfers = false)
|
||||
{
|
||||
this.networkId = networkId;
|
||||
this.currentOwnerId = currentOwnerId;
|
||||
this.autoAcceptTransfers = autoAcceptTransfers;
|
||||
this.pendingRequests = new Dictionary<string, Action<bool>>();
|
||||
|
||||
ModNetworkManager.Subscribe(networkId, OnMessageReceived);
|
||||
|
||||
if (!HasOwnership)
|
||||
RequestInitialState();
|
||||
else
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
private void RequestInitialState()
|
||||
{
|
||||
using ModNetworkMessage msg = new(networkId);
|
||||
msg.Write((byte)MessageType.StateRequest);
|
||||
msg.Send();
|
||||
|
||||
stateRequestTimer = new Timer(StateRequestTimeoutCallback, null, StateRequestTimeout, Timeout.Infinite);
|
||||
}
|
||||
|
||||
private void StateRequestTimeoutCallback(object state)
|
||||
{
|
||||
// If isInitialized is still false, we assume soft ownership
|
||||
if (!isInitialized)
|
||||
{
|
||||
currentOwnerId = LocalUserId;
|
||||
isSoftOwner = true;
|
||||
isInitialized = true;
|
||||
OnOwnershipChanged(currentOwnerId);
|
||||
}
|
||||
|
||||
stateRequestTimer.Dispose();
|
||||
stateRequestTimer = null;
|
||||
}
|
||||
|
||||
public virtual void RequestOwnership(Action<bool> callback = null)
|
||||
{
|
||||
if (HasOwnership)
|
||||
{
|
||||
callback?.Invoke(true);
|
||||
return;
|
||||
}
|
||||
|
||||
using (ModNetworkMessage msg = new(networkId))
|
||||
{
|
||||
msg.Write((byte)MessageType.OwnershipRequest);
|
||||
msg.Send();
|
||||
}
|
||||
|
||||
if (callback != null)
|
||||
{
|
||||
pendingRequests[LocalUserId] = callback;
|
||||
}
|
||||
}
|
||||
|
||||
protected void SendNetworkedData(Action<ModNetworkMessage> writeData)
|
||||
{
|
||||
if (!HasOwnership)
|
||||
{
|
||||
Debug.LogWarning($"[MNSyncedBehaviour] Cannot send data without ownership. NetworkId: {networkId}");
|
||||
return;
|
||||
}
|
||||
|
||||
using (ModNetworkMessage msg = new(networkId))
|
||||
{
|
||||
msg.Write((byte)MessageType.CustomData);
|
||||
writeData(msg);
|
||||
msg.Send();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void OnMessageReceived(ModNetworkMessage message)
|
||||
{
|
||||
message.Read(out byte type);
|
||||
MessageType messageType = (MessageType)type;
|
||||
|
||||
if (!Enum.IsDefined(typeof(MessageType), messageType))
|
||||
return;
|
||||
|
||||
switch (messageType)
|
||||
{
|
||||
case MessageType.OwnershipRequest:
|
||||
if (!HasOwnership) break;
|
||||
HandleOwnershipRequest(message);
|
||||
break;
|
||||
|
||||
case MessageType.OwnershipResponse:
|
||||
if (message.Sender != currentOwnerId) break;
|
||||
HandleOwnershipResponse(message);
|
||||
break;
|
||||
|
||||
case MessageType.OwnershipTransfer:
|
||||
if (message.Sender != currentOwnerId) break;
|
||||
currentOwnerId = message.Sender;
|
||||
OnOwnershipChanged(currentOwnerId);
|
||||
break;
|
||||
|
||||
case MessageType.StateRequest:
|
||||
if (!HasOwnership) break; // this is the only safeguard against ownership hijacking... idk how to prevent it
|
||||
// TODO: only respond to a StateUpdate if expecting one
|
||||
HandleStateRequest(message);
|
||||
break;
|
||||
|
||||
case MessageType.StateUpdate:
|
||||
// Accept state updates from current owner or if we have soft ownership
|
||||
if (message.Sender != currentOwnerId && !isSoftOwner) break;
|
||||
HandleStateUpdate(message);
|
||||
break;
|
||||
|
||||
case MessageType.CustomData:
|
||||
if (message.Sender != currentOwnerId)
|
||||
{
|
||||
// If we have soft ownership and receive data from real owner, accept it
|
||||
if (isSoftOwner && message.Sender != LocalUserId)
|
||||
{
|
||||
currentOwnerId = message.Sender;
|
||||
isSoftOwner = false;
|
||||
OnOwnershipChanged(currentOwnerId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ignore data from non-owner
|
||||
break;
|
||||
}
|
||||
}
|
||||
HandleCustomData(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void HandleOwnershipRequest(ModNetworkMessage message)
|
||||
{
|
||||
if (!HasOwnership)
|
||||
return;
|
||||
|
||||
string requesterId = message.Sender;
|
||||
var response = autoAcceptTransfers ? OwnershipResponse.Accepted :
|
||||
OnOwnershipRequested(requesterId);
|
||||
|
||||
using (ModNetworkMessage responseMsg = new(networkId))
|
||||
{
|
||||
responseMsg.Write((byte)MessageType.OwnershipResponse);
|
||||
responseMsg.Write((byte)response);
|
||||
responseMsg.Send();
|
||||
}
|
||||
|
||||
if (response == OwnershipResponse.Accepted)
|
||||
{
|
||||
TransferOwnership(requesterId);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void HandleOwnershipResponse(ModNetworkMessage message)
|
||||
{
|
||||
message.Read(out byte responseByte);
|
||||
OwnershipResponse response = (OwnershipResponse)responseByte;
|
||||
|
||||
if (pendingRequests.TryGetValue(LocalUserId, out var callback))
|
||||
{
|
||||
bool accepted = response == OwnershipResponse.Accepted;
|
||||
callback(accepted);
|
||||
pendingRequests.Remove(LocalUserId);
|
||||
|
||||
// Update ownership locally only if accepted
|
||||
if (accepted)
|
||||
{
|
||||
currentOwnerId = LocalUserId;
|
||||
OnOwnershipChanged(currentOwnerId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void HandleStateRequest(ModNetworkMessage message)
|
||||
{
|
||||
if (!HasOwnership)
|
||||
return;
|
||||
|
||||
using ModNetworkMessage response = new(networkId, message.Sender);
|
||||
response.Write((byte)MessageType.StateUpdate);
|
||||
WriteState(response);
|
||||
response.Send();
|
||||
}
|
||||
|
||||
protected virtual void HandleStateUpdate(ModNetworkMessage message)
|
||||
{
|
||||
currentOwnerId = message.Sender;
|
||||
isSoftOwner = false;
|
||||
ReadState(message);
|
||||
isInitialized = true;
|
||||
|
||||
// Dispose of the state request timer if it's still running
|
||||
if (stateRequestTimer != null)
|
||||
{
|
||||
stateRequestTimer.Dispose();
|
||||
stateRequestTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void HandleCustomData(ModNetworkMessage message)
|
||||
{
|
||||
if (!isInitialized)
|
||||
{
|
||||
Debug.LogWarning($"[MNSyncedBehaviour] Received custom data before initialization. NetworkId: {networkId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.Sender != currentOwnerId)
|
||||
{
|
||||
// If we have soft ownership and receive data from real owner, accept it
|
||||
if (isSoftOwner && message.Sender != LocalUserId)
|
||||
{
|
||||
currentOwnerId = message.Sender;
|
||||
isSoftOwner = false;
|
||||
OnOwnershipChanged(currentOwnerId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ignore data from non-owner
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ReadCustomData(message);
|
||||
}
|
||||
|
||||
protected virtual void TransferOwnership(string newOwnerId)
|
||||
{
|
||||
using (ModNetworkMessage msg = new(networkId))
|
||||
{
|
||||
msg.Write((byte)MessageType.OwnershipTransfer);
|
||||
msg.Write(newOwnerId); // Include the new owner ID in transfer message
|
||||
msg.Send();
|
||||
}
|
||||
|
||||
currentOwnerId = newOwnerId;
|
||||
OnOwnershipChanged(newOwnerId);
|
||||
}
|
||||
|
||||
protected virtual OwnershipResponse OnOwnershipRequested(string requesterId)
|
||||
{
|
||||
return OwnershipResponse.Rejected;
|
||||
}
|
||||
|
||||
protected virtual void OnOwnershipChanged(string newOwnerId)
|
||||
{
|
||||
// Override to handle ownership changes
|
||||
}
|
||||
|
||||
protected virtual void WriteState(ModNetworkMessage message) { }
|
||||
protected virtual void ReadState(ModNetworkMessage message) { }
|
||||
protected virtual void ReadCustomData(ModNetworkMessage message) { }
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposedValue)
|
||||
return;
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
ModNetworkManager.Unsubscribe(networkId);
|
||||
pendingRequests.Clear();
|
||||
|
||||
if (stateRequestTimer != null)
|
||||
{
|
||||
stateRequestTimer.Dispose();
|
||||
stateRequestTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
disposedValue = true;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
using ABI_RC.Systems.ModNetwork;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.LuaNetVars;
|
||||
|
||||
|
||||
// Test implementation
|
||||
public class TestSyncedBehaviour : MNSyncedBehaviour
|
||||
{
|
||||
private readonly System.Random random = new();
|
||||
private int testValue;
|
||||
private int incrementValue;
|
||||
|
||||
public TestSyncedBehaviour(string networkId) : base(networkId, autoAcceptTransfers: true)
|
||||
{
|
||||
Debug.Log($"[TestSyncedBehaviour] Initialized. NetworkId: {networkId}");
|
||||
}
|
||||
|
||||
public void SendTestMessage()
|
||||
{
|
||||
if (!HasOwnership) return;
|
||||
|
||||
SendNetworkedData(msg => {
|
||||
testValue = random.Next(1000);
|
||||
incrementValue++;
|
||||
msg.Write(testValue);
|
||||
msg.Write(incrementValue);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void WriteState(ModNetworkMessage message)
|
||||
{
|
||||
message.Write(testValue);
|
||||
message.Write(incrementValue);
|
||||
}
|
||||
|
||||
protected override void ReadState(ModNetworkMessage message)
|
||||
{
|
||||
message.Read(out testValue);
|
||||
message.Read(out incrementValue);
|
||||
Debug.Log($"[TestSyncedBehaviour] State synchronized. TestValue: {testValue}, IncrementValue: {incrementValue}");
|
||||
}
|
||||
|
||||
protected override void ReadCustomData(ModNetworkMessage message)
|
||||
{
|
||||
message.Read(out int receivedValue);
|
||||
message.Read(out int receivedIncrement);
|
||||
testValue = receivedValue;
|
||||
incrementValue = receivedIncrement;
|
||||
Debug.Log($"[TestSyncedBehaviour] Received custom data: TestValue: {testValue}, IncrementValue: {incrementValue}");
|
||||
}
|
||||
|
||||
protected override void OnOwnershipChanged(string newOwnerId)
|
||||
{
|
||||
Debug.Log($"[TestSyncedBehaviour] Ownership changed to: {newOwnerId}");
|
||||
}
|
||||
|
||||
protected override OwnershipResponse OnOwnershipRequested(string requesterId)
|
||||
{
|
||||
Debug.Log($"[TestSyncedBehaviour] Ownership requested by: {requesterId}");
|
||||
return OwnershipResponse.Accepted;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
using ABI_RC.Core.Savior;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.LuaNetVars;
|
||||
|
||||
public class TestSyncedObject : MonoBehaviour
|
||||
{
|
||||
private const string TEST_NETWORK_ID = "test.synced.object.1";
|
||||
private TestSyncedBehaviour syncBehaviour;
|
||||
private float messageTimer = 0f;
|
||||
private const float MESSAGE_INTERVAL = 2f;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
syncBehaviour = new TestSyncedBehaviour(TEST_NETWORK_ID);
|
||||
Debug.Log($"TestSyncedObject started. Local Player ID: {MetaPort.Instance.ownerId}");
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Request ownership on Space key
|
||||
if (Input.GetKeyDown(KeyCode.Space))
|
||||
{
|
||||
Debug.Log("Requesting ownership...");
|
||||
syncBehaviour.RequestOwnership((success) =>
|
||||
{
|
||||
Debug.Log($"Ownership request {(success ? "accepted" : "rejected")}");
|
||||
});
|
||||
}
|
||||
|
||||
// If we have ownership, send custom data periodically
|
||||
if (syncBehaviour.HasOwnership)
|
||||
{
|
||||
messageTimer += Time.deltaTime;
|
||||
if (messageTimer >= MESSAGE_INTERVAL)
|
||||
{
|
||||
messageTimer = 0f;
|
||||
syncBehaviour.SendTestMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
23
.Experimental/LuaNetworkVariables/format.json
Normal file
23
.Experimental/LuaNetworkVariables/format.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"_id": 234,
|
||||
"name": "WhereAmIPointing",
|
||||
"modversion": "1.0.1",
|
||||
"gameversion": "2024r175",
|
||||
"loaderversion": "0.6.1",
|
||||
"modtype": "Mod",
|
||||
"author": "NotAKidoS",
|
||||
"description": "Simple mod that makes your controller rays always visible when the menus are open. Useful for when you're trying to aim at something in the distance. Also visualizes which ray is being used for menu interaction.",
|
||||
"searchtags": [
|
||||
"controller",
|
||||
"ray",
|
||||
"line",
|
||||
"tomato"
|
||||
],
|
||||
"requirements": [
|
||||
"None"
|
||||
],
|
||||
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r40/WhereAmIPointing.dll",
|
||||
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/WhereAmIPointing/",
|
||||
"changelog": "- Fixed line renderer alpha not being reset when the menu is closed.",
|
||||
"embedcolor": "#f61963"
|
||||
}
|
6
.Experimental/LuaTTS/LuaTTS.csproj
Normal file
6
.Experimental/LuaTTS/LuaTTS.csproj
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Sprays</RootNamespace>
|
||||
</PropertyGroup>
|
||||
</Project>
|
54
.Experimental/LuaTTS/Main.cs
Normal file
54
.Experimental/LuaTTS/Main.cs
Normal file
|
@ -0,0 +1,54 @@
|
|||
using ABI_RC.Scripting.ScriptNetwork;
|
||||
using ABI_RC.Systems.ModNetwork;
|
||||
using MelonLoader;
|
||||
using NAK.LuaTTS.Patches;
|
||||
|
||||
namespace NAK.LuaTTS;
|
||||
|
||||
public class LuaTTSMod : MelonMod
|
||||
{
|
||||
public override void OnInitializeMelon()
|
||||
{
|
||||
ApplyPatches(typeof(LuaScriptFactoryPatches));
|
||||
|
||||
ModNetworkMessage.AddConverter<ScriptID>(ReadScriptID, WriteScriptID);
|
||||
ModNetworkMessage.AddConverter<ScriptInstanceID>(ReadScriptInstanceID, WriteScriptInstanceID);
|
||||
}
|
||||
|
||||
private static ScriptID ReadScriptID(ModNetworkMessage msg)
|
||||
{
|
||||
msg.Read(out byte[] value);
|
||||
ScriptID scriptID = new(value);
|
||||
return scriptID;
|
||||
}
|
||||
|
||||
private static void WriteScriptID(ModNetworkMessage msg, ScriptID scriptID)
|
||||
{
|
||||
msg.Write(scriptID.value);
|
||||
}
|
||||
|
||||
private static ScriptInstanceID ReadScriptInstanceID(ModNetworkMessage msg)
|
||||
{
|
||||
msg.Read(out byte[] value);
|
||||
ScriptInstanceID scriptInstanceID = new(value);
|
||||
return scriptInstanceID;
|
||||
}
|
||||
|
||||
private static void WriteScriptInstanceID(ModNetworkMessage msg, ScriptInstanceID scriptInstanceID)
|
||||
{
|
||||
msg.Write(scriptInstanceID.value);
|
||||
}
|
||||
|
||||
private void ApplyPatches(Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
HarmonyInstance.PatchAll(type);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LoggerInstance.Msg($"Failed while patching {type.Name}!");
|
||||
LoggerInstance.Error(e);
|
||||
}
|
||||
}
|
||||
}
|
27
.Experimental/LuaTTS/Patches.cs
Normal file
27
.Experimental/LuaTTS/Patches.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using ABI.Scripting.CVRSTL.Client;
|
||||
using ABI.Scripting.CVRSTL.Common;
|
||||
using HarmonyLib;
|
||||
using MoonSharp.Interpreter;
|
||||
using NAK.LuaTTS.Modules;
|
||||
|
||||
namespace NAK.LuaTTS.Patches;
|
||||
|
||||
internal static class LuaScriptFactoryPatches
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(LuaScriptFactory.CVRRequireModule), nameof(LuaScriptFactory.CVRRequireModule.Require))]
|
||||
private static void Postfix_CVRRequireModule_require(
|
||||
string moduleFriendlyName,
|
||||
ref LuaScriptFactory.CVRRequireModule __instance,
|
||||
ref object __result,
|
||||
ref Script ____script,
|
||||
ref CVRLuaContext ____context)
|
||||
{
|
||||
const string TTSModuleID = "TextToSpeech";
|
||||
if (TTSModuleID != moduleFriendlyName)
|
||||
return; // not our module
|
||||
|
||||
__result = TTSLuaModule.RegisterUserData(____script, ____context);
|
||||
__instance.RegisteredModules[TTSModuleID] = __result; // add module to cache
|
||||
}
|
||||
}
|
32
.Experimental/LuaTTS/Properties/AssemblyInfo.cs
Normal file
32
.Experimental/LuaTTS/Properties/AssemblyInfo.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using NAK.LuaTTS.Properties;
|
||||
using MelonLoader;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
|
||||
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
|
||||
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
|
||||
[assembly: AssemblyTitle(nameof(NAK.LuaTTS))]
|
||||
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
|
||||
[assembly: AssemblyProduct(nameof(NAK.LuaTTS))]
|
||||
|
||||
[assembly: MelonInfo(
|
||||
typeof(NAK.LuaTTS.LuaTTSMod),
|
||||
nameof(NAK.LuaTTS),
|
||||
AssemblyInfoParams.Version,
|
||||
AssemblyInfoParams.Author,
|
||||
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/LuaTTS"
|
||||
)]
|
||||
|
||||
[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.LuaTTS.Properties;
|
||||
internal static class AssemblyInfoParams
|
||||
{
|
||||
public const string Version = "1.0.1";
|
||||
public const string Author = "NotAKidoS";
|
||||
}
|
14
.Experimental/LuaTTS/README.md
Normal file
14
.Experimental/LuaTTS/README.md
Normal file
|
@ -0,0 +1,14 @@
|
|||
# LuaTTS
|
||||
|
||||
Provides access to the built-in text-to-speech (TTS) functionality to lua scripts. Allows you to make the local player speak.
|
||||
|
||||
---
|
||||
|
||||
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.
|
120
.Experimental/LuaTTS/TTSLuaModule.cs
Normal file
120
.Experimental/LuaTTS/TTSLuaModule.cs
Normal file
|
@ -0,0 +1,120 @@
|
|||
using ABI_RC.Systems.Communications.Audio.TTS;
|
||||
using ABI.Scripting.CVRSTL.Common;
|
||||
using JetBrains.Annotations;
|
||||
using MoonSharp.Interpreter;
|
||||
|
||||
namespace NAK.LuaTTS.Modules;
|
||||
|
||||
[PublicAPI] // fak off its used
|
||||
public class TTSLuaModule : BaseScriptedStaticWrapper
|
||||
{
|
||||
public TTSLuaModule(CVRLuaContext context) : base(context)
|
||||
{
|
||||
// yes
|
||||
}
|
||||
|
||||
internal static object RegisterUserData(Script script, CVRLuaContext context)
|
||||
{
|
||||
UserData.RegisterType<TTSLuaModule>(InteropAccessMode.Default, "TextToSpeech");
|
||||
return new TTSLuaModule(context);
|
||||
}
|
||||
|
||||
// Check if TTS is playing
|
||||
public bool IsPlaying()
|
||||
{
|
||||
CheckIfCanAccessMethod(nameof(IsPlaying), false,
|
||||
CVRLuaEnvironmentContext.CLIENT, CVRLuaObjectContext.ALL_BUT_EVENTS, CVRLuaOwnerContext.LOCAL);
|
||||
|
||||
return Comms_TTSHandler.Instance.IsPlaying;
|
||||
}
|
||||
|
||||
// Check if TTS is processing a message
|
||||
public bool IsProcessing()
|
||||
{
|
||||
CheckIfCanAccessMethod(nameof(IsProcessing), false,
|
||||
CVRLuaEnvironmentContext.CLIENT, CVRLuaObjectContext.ALL_BUT_EVENTS, CVRLuaOwnerContext.LOCAL);
|
||||
|
||||
return Comms_TTSHandler.Instance.IsProcessing;
|
||||
}
|
||||
|
||||
// Check if TTS has no modules (only true for proton?)
|
||||
public bool HasAnyModules()
|
||||
{
|
||||
CheckIfCanAccessMethod(nameof(HasAnyModules), false,
|
||||
CVRLuaEnvironmentContext.CLIENT, CVRLuaObjectContext.ALL_BUT_EVENTS, CVRLuaOwnerContext.LOCAL);
|
||||
|
||||
return Comms_TTSHandler._modules.Count > 0;
|
||||
}
|
||||
|
||||
// Get all available TTS modules
|
||||
public string[] GetAvailableModules()
|
||||
{
|
||||
CheckIfCanAccessMethod(nameof(GetAvailableModules), false,
|
||||
CVRLuaEnvironmentContext.CLIENT, CVRLuaObjectContext.ALL_BUT_EVENTS, CVRLuaOwnerContext.LOCAL);
|
||||
|
||||
return Comms_TTSHandler._modules.Keys.ToArray();
|
||||
}
|
||||
|
||||
// Get the current TTS module
|
||||
public string GetCurrentModule()
|
||||
{
|
||||
CheckIfCanAccessMethod(nameof(GetCurrentModule), false,
|
||||
CVRLuaEnvironmentContext.CLIENT, CVRLuaObjectContext.ALL_BUT_EVENTS, CVRLuaOwnerContext.LOCAL);
|
||||
|
||||
return Comms_TTSHandler.Instance.CurrentModuleId;
|
||||
}
|
||||
|
||||
// Set the current TTS module
|
||||
public void SetCurrentModule(string moduleId)
|
||||
{
|
||||
CheckIfCanAccessMethod(nameof(SetCurrentModule), false,
|
||||
CVRLuaEnvironmentContext.CLIENT, CVRLuaObjectContext.ALL_BUT_EVENTS, CVRLuaOwnerContext.LOCAL);
|
||||
|
||||
Comms_TTSHandler.Instance.ChangeModule(moduleId);
|
||||
}
|
||||
|
||||
// Process a message for TTS playback
|
||||
public void ProcessMessage(string message)
|
||||
{
|
||||
CheckIfCanAccessMethod(nameof(ProcessMessage), false,
|
||||
CVRLuaEnvironmentContext.CLIENT, CVRLuaObjectContext.ALL_BUT_EVENTS, CVRLuaOwnerContext.LOCAL);
|
||||
|
||||
Comms_TTSHandler.Instance.ProcessMessage(message);
|
||||
}
|
||||
|
||||
// Cancel any currently playing TTS message
|
||||
public void CancelMessage()
|
||||
{
|
||||
CheckIfCanAccessMethod(nameof(CancelMessage), false,
|
||||
CVRLuaEnvironmentContext.CLIENT, CVRLuaObjectContext.ALL_BUT_EVENTS, CVRLuaOwnerContext.LOCAL);
|
||||
|
||||
Comms_TTSHandler.Instance.ProcessMessage(string.Empty); // empty message cancels the current message
|
||||
}
|
||||
|
||||
// Get all available voices for the current module
|
||||
public string[] GetAvailableVoices()
|
||||
{
|
||||
CheckIfCanAccessMethod(nameof(GetAvailableVoices), false,
|
||||
CVRLuaEnvironmentContext.CLIENT, CVRLuaObjectContext.ALL_BUT_EVENTS, CVRLuaOwnerContext.LOCAL);
|
||||
|
||||
return Comms_TTSHandler.Instance.CurrentModule.Voices.Keys.ToArray();
|
||||
}
|
||||
|
||||
// Get the current voice for the module
|
||||
public string GetCurrentVoice()
|
||||
{
|
||||
CheckIfCanAccessMethod(nameof(GetCurrentVoice), false,
|
||||
CVRLuaEnvironmentContext.CLIENT, CVRLuaObjectContext.ALL_BUT_EVENTS, CVRLuaOwnerContext.LOCAL);
|
||||
|
||||
return Comms_TTSHandler.Instance.CurrentModule.CurrentVoice;
|
||||
}
|
||||
|
||||
// Set the current voice for the module
|
||||
public void SetCurrentVoice(string voiceName)
|
||||
{
|
||||
CheckIfCanAccessMethod(nameof(SetCurrentVoice), false,
|
||||
CVRLuaEnvironmentContext.CLIENT, CVRLuaObjectContext.ALL_BUT_EVENTS, CVRLuaOwnerContext.LOCAL);
|
||||
|
||||
Comms_TTSHandler.Instance.ChangeVoice(voiceName);
|
||||
}
|
||||
}
|
23
.Experimental/LuaTTS/format.json
Normal file
23
.Experimental/LuaTTS/format.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"_id": -1,
|
||||
"name": "LuaTTS",
|
||||
"modversion": "1.0.2",
|
||||
"gameversion": "2024r176",
|
||||
"loaderversion": "0.6.1",
|
||||
"modtype": "Mod",
|
||||
"author": "NotAKidoS",
|
||||
"description": "Provides access to the built-in text-to-speech (TTS) functionality to lua scripts. Allows you to make the local player speak.",
|
||||
"searchtags": [
|
||||
"lua",
|
||||
"tts",
|
||||
"texttospeech",
|
||||
"tomato"
|
||||
],
|
||||
"requirements": [
|
||||
"None"
|
||||
],
|
||||
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r28/LuaTTS.dll",
|
||||
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/LuaTTS/",
|
||||
"changelog": "- Initial release.",
|
||||
"embedcolor": "#f61963"
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
using ABI_RC.Core.Player;
|
||||
using BTKUILib;
|
||||
using BTKUILib.UIObjects;
|
||||
using BTKUILib.UIObjects.Components;
|
||||
using NAK.OriginShift;
|
||||
|
||||
namespace NAK.OriginShiftMod.Integrations;
|
||||
|
||||
public static partial class BtkUiAddon
|
||||
{
|
||||
private static Category _ourCategory;
|
||||
|
||||
private static Button _ourMainButton;
|
||||
private static bool _isForcedMode;
|
||||
|
||||
private static ToggleButton _ourToggle;
|
||||
|
||||
private static void Setup_OriginShiftModCategory(Page page)
|
||||
{
|
||||
// dear category
|
||||
_ourCategory = page.AddCategory(ModSettings.OSM_SettingsCategory, ModSettings.ModName, true, true, false);
|
||||
|
||||
// the button
|
||||
_ourMainButton = _ourCategory.AddButton(string.Empty, string.Empty, string.Empty, ButtonStyle.TextWithIcon);
|
||||
_ourMainButton.OnPress += OnMainButtonClick;
|
||||
SetButtonState(OriginShiftManager.OriginShiftState.Inactive); // default state
|
||||
|
||||
// compatibility mode
|
||||
_ourToggle = _ourCategory.AddToggle(ModSettings.EntryCompatibilityMode.DisplayName,
|
||||
ModSettings.EntryCompatibilityMode.Description, ModSettings.EntryCompatibilityMode.Value);
|
||||
_ourToggle.OnValueUpdated += OnCompatibilityModeToggle;
|
||||
|
||||
// listen for state changes
|
||||
OriginShiftManager.OnStateChanged += OnOriginShiftStateChanged;
|
||||
|
||||
// debug toggle
|
||||
ToggleButton debugToggle = _ourCategory.AddToggle("Debug Overlay", "Displays coordinate & chunk debug information on the Desktop view.", false);
|
||||
debugToggle.OnValueUpdated += OnDebugToggle;
|
||||
}
|
||||
|
||||
#region Category Actions
|
||||
|
||||
private static void UpdateCategoryModUserCount()
|
||||
{
|
||||
int modUsers = 1; // we are always here :3
|
||||
int playerCount = CVRPlayerManager.Instance.NetworkPlayers.Count + 1; // +1 for us :3
|
||||
_ourCategory.CategoryName = $"{ModSettings.OSM_SettingsCategory} ({modUsers}/{playerCount})";
|
||||
}
|
||||
|
||||
#endregion Category Actions
|
||||
|
||||
#region Button Actions
|
||||
|
||||
private static void SetButtonState(OriginShiftManager.OriginShiftState state)
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
default:
|
||||
case OriginShiftManager.OriginShiftState.Inactive:
|
||||
_ourMainButton.ButtonText = "Inactive";
|
||||
_ourMainButton.ButtonTooltip = "This world is not using Origin Shift.";
|
||||
_ourMainButton.ButtonIcon = "OriginShift-Icon-Inactive";
|
||||
break;
|
||||
case OriginShiftManager.OriginShiftState.Active:
|
||||
_ourMainButton.ButtonText = "Active";
|
||||
_ourMainButton.ButtonTooltip = "This world is currently using Origin Shift.";
|
||||
_ourMainButton.ButtonIcon = "OriginShift-Icon-Active";
|
||||
break;
|
||||
case OriginShiftManager.OriginShiftState.Forced:
|
||||
_ourMainButton.ButtonText = "Forced";
|
||||
_ourMainButton.ButtonTooltip = "You have forced Origin Shift for this world.";
|
||||
_ourMainButton.ButtonIcon = "OriginShift-Icon-Forced";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnMainButtonClick()
|
||||
{
|
||||
// if active, return as world is using Origin Shift
|
||||
if (OriginShiftManager.Instance.CurrentState
|
||||
is OriginShiftManager.OriginShiftState.Active)
|
||||
return;
|
||||
|
||||
if (_isForcedMode)
|
||||
{
|
||||
OriginShiftManager.Instance.ResetManager();
|
||||
}
|
||||
else
|
||||
{
|
||||
QuickMenuAPI.ShowConfirm("Force Origin Shift",
|
||||
"Are you sure you want to force Origin Shift for this world? " +
|
||||
"This is a highly experimental feature that may break the world in unexpected ways!",
|
||||
() =>
|
||||
{
|
||||
OriginShiftManager.Instance.ForceManager();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnOriginShiftStateChanged(OriginShiftManager.OriginShiftState state)
|
||||
{
|
||||
_isForcedMode = state == OriginShiftManager.OriginShiftState.Forced;
|
||||
SetButtonState(state);
|
||||
SetToggleLocked(_isForcedMode);
|
||||
}
|
||||
|
||||
#endregion Button Actions
|
||||
|
||||
#region Toggle Actions
|
||||
|
||||
private static void SetToggleLocked(bool value)
|
||||
{
|
||||
if (value)
|
||||
{
|
||||
// lock the toggle
|
||||
_ourToggle.ToggleTooltip = "This setting is locked while Origin Shift is forced.";
|
||||
_ourToggle.ToggleValue = true;
|
||||
_ourToggle.Disabled = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// unlock the toggle
|
||||
_ourToggle.ToggleValue = ModSettings.EntryCompatibilityMode.Value;
|
||||
_ourToggle.ToggleTooltip = ModSettings.EntryCompatibilityMode.Description;
|
||||
_ourToggle.Disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnCompatibilityModeToggle(bool value)
|
||||
{
|
||||
ModSettings.EntryCompatibilityMode.Value = value;
|
||||
}
|
||||
|
||||
#endregion Toggle Actions
|
||||
|
||||
#region Debug Toggle Actions
|
||||
|
||||
private static void OnDebugToggle(bool value)
|
||||
{
|
||||
OriginShiftManager.Instance.ToggleDebugOverlay(value);
|
||||
}
|
||||
|
||||
#endregion Debug Toggle Actions
|
||||
}
|
65
.Experimental/OriginShift/Integrations/BTKUI/BtkuiAddon.cs
Normal file
65
.Experimental/OriginShift/Integrations/BTKUI/BtkuiAddon.cs
Normal file
|
@ -0,0 +1,65 @@
|
|||
using ABI_RC.Core.Player;
|
||||
using BTKUILib;
|
||||
using BTKUILib.UIObjects;
|
||||
using NAK.OriginShift;
|
||||
|
||||
namespace NAK.OriginShiftMod.Integrations;
|
||||
|
||||
public static partial class BtkUiAddon
|
||||
{
|
||||
private static Page _miscTabPage;
|
||||
private static string _miscTabElementID;
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
Prepare_Icons();
|
||||
Setup_OriginShiftTab();
|
||||
}
|
||||
|
||||
#region Initialization
|
||||
|
||||
private static void Prepare_Icons()
|
||||
{
|
||||
QuickMenuAPI.PrepareIcon(ModSettings.ModName, "OriginShift-Icon-Active",
|
||||
GetIconStream("OriginShift-Icon-Active.png"));
|
||||
|
||||
QuickMenuAPI.PrepareIcon(ModSettings.ModName, "OriginShift-Icon-Inactive",
|
||||
GetIconStream("OriginShift-Icon-Inactive.png"));
|
||||
|
||||
QuickMenuAPI.PrepareIcon(ModSettings.ModName, "OriginShift-Icon-Forced",
|
||||
GetIconStream("OriginShift-Icon-Forced.png"));
|
||||
}
|
||||
|
||||
private static void Setup_OriginShiftTab()
|
||||
{
|
||||
_miscTabPage = QuickMenuAPI.MiscTabPage;
|
||||
_miscTabElementID = _miscTabPage.ElementID;
|
||||
QuickMenuAPI.UserJoin += OnUserJoinLeave;
|
||||
QuickMenuAPI.UserLeave += OnUserJoinLeave;
|
||||
QuickMenuAPI.OnWorldLeave += OnWorldLeave;
|
||||
|
||||
// // Origin Shift Mod
|
||||
Setup_OriginShiftModCategory(_miscTabPage);
|
||||
//
|
||||
// // Origin Shift Tool
|
||||
// Setup_OriginShiftToolCategory(_miscTabPage);
|
||||
//
|
||||
// // Universal Shifting Settings
|
||||
// Setup_UniversalShiftingSettings(_miscTabPage);
|
||||
//
|
||||
// // Debug Options
|
||||
// Setup_DebugOptionsCategory(_miscTabPage);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Player Count Display
|
||||
|
||||
private static void OnWorldLeave()
|
||||
=> UpdateCategoryModUserCount();
|
||||
|
||||
private static void OnUserJoinLeave(CVRPlayerEntity _)
|
||||
=> UpdateCategoryModUserCount();
|
||||
|
||||
#endregion
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
using System.Reflection;
|
||||
using BTKUILib;
|
||||
using BTKUILib.UIObjects;
|
||||
using BTKUILib.UIObjects.Components;
|
||||
using MelonLoader;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.OriginShiftMod.Integrations;
|
||||
|
||||
public static partial class BtkUiAddon
|
||||
{
|
||||
#region Melon Preference Helpers
|
||||
|
||||
private static ToggleButton AddMelonToggle(ref Category category, MelonPreferences_Entry<bool> entry)
|
||||
{
|
||||
ToggleButton toggle = category.AddToggle(entry.DisplayName, entry.Description, entry.Value);
|
||||
toggle.OnValueUpdated += b => entry.Value = b;
|
||||
return toggle;
|
||||
}
|
||||
|
||||
private static SliderFloat AddMelonSlider(ref Category category, MelonPreferences_Entry<float> entry, float min,
|
||||
float max, int decimalPlaces = 2, bool allowReset = true)
|
||||
{
|
||||
SliderFloat slider = category.AddSlider(entry.DisplayName, entry.Description,
|
||||
Mathf.Clamp(entry.Value, min, max), min, max, decimalPlaces, entry.DefaultValue, allowReset);
|
||||
slider.OnValueUpdated += f => entry.Value = f;
|
||||
return slider;
|
||||
}
|
||||
|
||||
private static Button AddMelonStringInput(ref Category category, MelonPreferences_Entry<string> entry, string buttonIcon = "", ButtonStyle buttonStyle = ButtonStyle.TextOnly)
|
||||
{
|
||||
Button button = category.AddButton(entry.DisplayName, buttonIcon, entry.Description, buttonStyle);
|
||||
button.OnPress += () => QuickMenuAPI.OpenKeyboard(entry.Value, s => entry.Value = s);
|
||||
return button;
|
||||
}
|
||||
|
||||
private static Button AddMelonNumberInput(ref Category category, MelonPreferences_Entry<float> entry, string buttonIcon = "", ButtonStyle buttonStyle = ButtonStyle.TextOnly)
|
||||
{
|
||||
Button button = category.AddButton(entry.DisplayName, buttonIcon, entry.Description, buttonStyle);
|
||||
button.OnPress += () => QuickMenuAPI.OpenNumberInput(entry.DisplayName, entry.Value, f => entry.Value = f);
|
||||
return button;
|
||||
}
|
||||
|
||||
private static Category AddMelonCategory(ref Page page, MelonPreferences_Entry<bool> entry, bool showHeader = true)
|
||||
{
|
||||
Category category = page.AddCategory(entry.DisplayName, showHeader, true, entry.Value);
|
||||
category.OnCollapse += b => entry.Value = b;
|
||||
return category;
|
||||
}
|
||||
|
||||
#endregion Melon Preference Helpers
|
||||
|
||||
#region Icon Utils
|
||||
|
||||
private static Stream GetIconStream(string iconName)
|
||||
{
|
||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||
string assemblyName = assembly.GetName().Name;
|
||||
return assembly.GetManifestResourceStream($"{assemblyName}.Resources.{iconName}");
|
||||
}
|
||||
|
||||
#endregion Icon Utils
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
using System.Collections;
|
||||
using ABI_RC.Core.Base;
|
||||
using ABI_RC.Core.Player;
|
||||
using HarmonyLib;
|
||||
using MelonLoader;
|
||||
using NAK.OriginShift;
|
||||
using NAK.OriginShift.Components;
|
||||
using UnityEngine;
|
||||
|
||||
namespace OriginShift.Integrations;
|
||||
|
||||
public static class RagdollAddon
|
||||
{
|
||||
public static void Initialize()
|
||||
{
|
||||
OriginShiftMod.HarmonyInst.Patch(
|
||||
AccessTools.Method(typeof(PlayerSetup), nameof(PlayerSetup.SetupAvatar)),
|
||||
postfix: new HarmonyMethod(typeof(RagdollAddon), nameof(OnPostPlayerSetupSetupAvatar))
|
||||
);
|
||||
}
|
||||
|
||||
private static void OnPostPlayerSetupSetupAvatar()
|
||||
{
|
||||
OriginShiftMod.Logger.Msg("Found Ragdoll, fixing compatibility...");
|
||||
MelonCoroutines.Start(FixRagdollCompatibility());
|
||||
}
|
||||
|
||||
private static IEnumerator FixRagdollCompatibility()
|
||||
{
|
||||
yield return null; // wait a frame for the avatar to be setup
|
||||
GameObject ragdollObj = GameObject.Find("_PLAYERLOCAL/[PlayerAvatarPuppet]");
|
||||
|
||||
// get all rigidbodies in the ragdoll
|
||||
var ragdollRigidbodies = ragdollObj.GetComponentsInChildren<Rigidbody>();
|
||||
foreach (Rigidbody rb in ragdollRigidbodies) rb.AddComponentIfMissing<OriginShiftRigidbodyReceiver>();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
using System.Collections;
|
||||
using ABI_RC.Core.Base;
|
||||
using ABI_RC.Core.Player;
|
||||
using MelonLoader;
|
||||
using NAK.OriginShift;
|
||||
using NAK.OriginShift.Hacks;
|
||||
using UnityEngine;
|
||||
using AccessTools = HarmonyLib.AccessTools;
|
||||
using HarmonyMethod = HarmonyLib.HarmonyMethod;
|
||||
|
||||
namespace OriginShift.Integrations;
|
||||
|
||||
public static class ThirdPersonAddon
|
||||
{
|
||||
public static void Initialize()
|
||||
{
|
||||
OriginShiftMod.HarmonyInst.Patch(
|
||||
AccessTools.Method(typeof(PlayerSetup), nameof(PlayerSetup.Start)),
|
||||
postfix: new HarmonyMethod(typeof(ThirdPersonAddon), nameof(OnPostPlayerSetupStart))
|
||||
);
|
||||
}
|
||||
|
||||
private static void OnPostPlayerSetupStart()
|
||||
{
|
||||
OriginShiftMod.Logger.Msg("Found ThirdPerson, fixing compatibility...");
|
||||
MelonCoroutines.Start(FixThirdPersonCompatibility());
|
||||
}
|
||||
|
||||
private static IEnumerator FixThirdPersonCompatibility()
|
||||
{
|
||||
yield return null; // wait a frame for the camera to be setup
|
||||
GameObject thirdPersonCameraObj = GameObject.Find("_PLAYERLOCAL/[CameraRigDesktop]/Camera/ThirdPersonCameraObj");
|
||||
thirdPersonCameraObj.AddComponentIfMissing<OriginShiftOcclusionCullingDisabler>();
|
||||
}
|
||||
}
|
||||
|
99
.Experimental/OriginShift/Main.cs
Normal file
99
.Experimental/OriginShift/Main.cs
Normal file
|
@ -0,0 +1,99 @@
|
|||
#if !UNITY_EDITOR
|
||||
using System.Globalization;
|
||||
using ABI_RC.Core.UI;
|
||||
using ABI_RC.Core.Util.AssetFiltering;
|
||||
using ABI_RC.Systems.Movement;
|
||||
using MelonLoader;
|
||||
using NAK.OriginShift.Components;
|
||||
using NAK.OriginShiftMod.Integrations;
|
||||
using OriginShift.Integrations;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.OriginShift;
|
||||
|
||||
// Links I looked at:
|
||||
// Controller/Event Listener Setup: https://manuel-rauber.com/2022/04/06/floating-origin-in-unity/amp/
|
||||
// Move Scene Roots: https://gist.github.com/brihernandez/9ebbaf35070181fa1ee56f9e702cc7a5
|
||||
// Looked cool but didn't really find anything to use: https://docs.coherence.io/coherence-sdk-for-unity/world-origin-shifting
|
||||
// One Day when we move to 2022: https://docs.unity3d.com/6000.0/Documentation/Manual/LightProbes-Moving.html
|
||||
|
||||
public class OriginShiftMod : MelonMod
|
||||
{
|
||||
internal static MelonLogger.Instance Logger;
|
||||
internal static HarmonyLib.Harmony HarmonyInst;
|
||||
|
||||
#region Melon Mod Overrides
|
||||
|
||||
public override void OnInitializeMelon()
|
||||
{
|
||||
Logger = LoggerInstance;
|
||||
HarmonyInst = HarmonyInstance;
|
||||
|
||||
ModSettings.Initialize();
|
||||
|
||||
ApplyPatches(typeof(Patches.BetterBetterCharacterControllerPatches)); // origin shift monitor
|
||||
ApplyPatches(typeof(Patches.CVRSpawnablePatches)); // components & remote spawnable pos
|
||||
|
||||
// Compatibility Mode
|
||||
ApplyPatches(typeof(Patches.PlayerSetupPatches)); // net ik, camera occlusion culling
|
||||
ApplyPatches(typeof(Patches.Comms_ClientPatches)); // voice pos
|
||||
ApplyPatches(typeof(Patches.CVRSyncHelperPatches)); // spawnable pos
|
||||
ApplyPatches(typeof(Patches.PuppetMasterPatches)); // remote player pos
|
||||
ApplyPatches(typeof(Patches.CVRObjectSyncPatches)); // remote object pos
|
||||
|
||||
ApplyPatches(typeof(Patches.DbJobsAvatarManagerPatches)); // dynamic bones
|
||||
ApplyPatches(typeof(Patches.CVRPortalManagerPatches)); // portals
|
||||
ApplyPatches(typeof(Patches.RCC_SkidmarksManagerPatches)); // skidmarks
|
||||
ApplyPatches(typeof(Patches.CVRPickupObjectPatches)); // pickup object respawn height
|
||||
|
||||
ApplyPatches(typeof(Patches.PortableCameraPatches)); // camera occlusion culling
|
||||
ApplyPatches(typeof(Patches.PathingCameraPatches)); // camera occlusion culling
|
||||
|
||||
// add our components to the world whitelist
|
||||
WorldFilter._Base.Add(typeof(OriginShiftController)); // base component
|
||||
WorldFilter._Base.Add(typeof(OriginShiftEventReceiver)); // generic event listener
|
||||
|
||||
WorldFilter._Base.Add(typeof(OriginShiftParticleSystemReceiver)); // particle system
|
||||
WorldFilter._Base.Add(typeof(OriginShiftRigidbodyReceiver)); // rigidbody
|
||||
WorldFilter._Base.Add(typeof(OriginShiftTrailRendererReceiver)); // trail renderer
|
||||
WorldFilter._Base.Add(typeof(OriginShiftTransformReceiver)); // transform
|
||||
|
||||
// chunk controller
|
||||
WorldFilter._Base.Add(typeof(ChunkController));
|
||||
WorldFilter._Base.Add(typeof(ChunkListener));
|
||||
WorldFilter._Base.Add(typeof(ChunkCreator));
|
||||
|
||||
InitializeIntegration("BTKUILib", BtkUiAddon.Initialize); // quick menu ui
|
||||
InitializeIntegration("ThirdPerson", ThirdPersonAddon.Initialize); // camera occlusion culling
|
||||
InitializeIntegration("PlayerRagdollMod", RagdollAddon.Initialize); // ragdoll rigidbodys
|
||||
}
|
||||
|
||||
#endregion Melon Mod Overrides
|
||||
|
||||
#region Melon Mod Utilities
|
||||
|
||||
private static void InitializeIntegration(string modName, Action integrationAction)
|
||||
{
|
||||
if (RegisteredMelons.All(it => it.Info.Name != modName))
|
||||
return;
|
||||
|
||||
Logger.Msg($"Initializing {modName} integration.");
|
||||
integrationAction.Invoke();
|
||||
}
|
||||
|
||||
private void ApplyPatches(Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
HarmonyInstance.PatchAll(type);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LoggerInstance.Msg($"Failed while patching {type.Name}!");
|
||||
LoggerInstance.Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Melon Mod Utilities
|
||||
}
|
||||
#endif
|
41
.Experimental/OriginShift/ModSettings.cs
Normal file
41
.Experimental/OriginShift/ModSettings.cs
Normal file
|
@ -0,0 +1,41 @@
|
|||
using MelonLoader;
|
||||
|
||||
namespace NAK.OriginShift;
|
||||
|
||||
internal static class ModSettings
|
||||
{
|
||||
#region Constants
|
||||
|
||||
internal const string ModName = nameof(OriginShift);
|
||||
internal const string OSM_SettingsCategory = "Origin Shift Mod";
|
||||
|
||||
#endregion Constants
|
||||
|
||||
#region Melon Preferences
|
||||
|
||||
private static readonly MelonPreferences_Category Category =
|
||||
MelonPreferences.CreateCategory(ModName);
|
||||
|
||||
internal static readonly MelonPreferences_Entry<bool> EntryCompatibilityMode =
|
||||
Category.CreateEntry("EntryCompatibilityMode", true,
|
||||
"Compatibility Mode", description: "Origin Shifts locally, but modifies outbound network messages to be compatible with non-Origin Shifted clients.");
|
||||
|
||||
#endregion Melon Preferences
|
||||
|
||||
#region Settings Managment
|
||||
|
||||
internal static void Initialize()
|
||||
{
|
||||
foreach (MelonPreferences_Entry setting in Category.Entries)
|
||||
setting.OnEntryValueChangedUntyped.Subscribe(OnSettingsChanged);
|
||||
|
||||
OnSettingsChanged();
|
||||
}
|
||||
|
||||
private static void OnSettingsChanged(object oldValue = null, object newValue = null)
|
||||
{
|
||||
OriginShiftManager.CompatibilityMode = EntryCompatibilityMode.Value;
|
||||
}
|
||||
|
||||
#endregion Settings Managment
|
||||
}
|
151
.Experimental/OriginShift/Networking/ModNetwork.cs
Normal file
151
.Experimental/OriginShift/Networking/ModNetwork.cs
Normal file
|
@ -0,0 +1,151 @@
|
|||
// using ABI_RC.Core.Networking;
|
||||
// using ABI_RC.Systems.ModNetwork;
|
||||
// using DarkRift;
|
||||
// using UnityEngine;
|
||||
//
|
||||
// namespace NAK.OriginShift.Networking;
|
||||
//
|
||||
// public static class ModNetwork
|
||||
// {
|
||||
// public static bool Debug_NetworkInbound = false;
|
||||
// public static bool Debug_NetworkOutbound = false;
|
||||
//
|
||||
// private static bool _isSubscribedToModNetwork;
|
||||
//
|
||||
// private struct MovementParentSyncData
|
||||
// {
|
||||
// public bool HasSyncedThisData;
|
||||
// public int MarkerHash;
|
||||
// public Vector3 RootPosition;
|
||||
// public Vector3 RootRotation;
|
||||
// // public Vector3 HipPosition;
|
||||
// // public Vector3 HipRotation;
|
||||
// }
|
||||
//
|
||||
// private static MovementParentSyncData _latestMovementParentSyncData;
|
||||
//
|
||||
// #region Constants
|
||||
//
|
||||
// private const string ModId = "MelonMod.NAK.RelativeSync";
|
||||
//
|
||||
// #endregion
|
||||
//
|
||||
// #region Enums
|
||||
//
|
||||
// private enum MessageType : byte
|
||||
// {
|
||||
// MovementParentOrChair = 0
|
||||
// //RelativePickup = 1,
|
||||
// //RelativeAttachment = 2,
|
||||
// }
|
||||
//
|
||||
// #endregion
|
||||
//
|
||||
// #region Mod Network Internals
|
||||
//
|
||||
// internal static void Subscribe()
|
||||
// {
|
||||
// ModNetworkManager.Subscribe(ModId, OnMessageReceived);
|
||||
//
|
||||
// _isSubscribedToModNetwork = ModNetworkManager.IsSubscribed(ModId);
|
||||
// if (!_isSubscribedToModNetwork)
|
||||
// Debug.LogError("Failed to subscribe to Mod Network!");
|
||||
// }
|
||||
//
|
||||
// // Called right after NetworkRootDataUpdate.Submit()
|
||||
// internal static void SendRelativeSyncUpdate()
|
||||
// {
|
||||
// if (!_isSubscribedToModNetwork)
|
||||
// return;
|
||||
//
|
||||
// if (_latestMovementParentSyncData.HasSyncedThisData)
|
||||
// return;
|
||||
//
|
||||
// SendMessage(MessageType.MovementParentOrChair, _latestMovementParentSyncData.MarkerHash,
|
||||
// _latestMovementParentSyncData.RootPosition, _latestMovementParentSyncData.RootRotation);
|
||||
//
|
||||
// _latestMovementParentSyncData.HasSyncedThisData = true;
|
||||
// }
|
||||
//
|
||||
// public static void SetLatestRelativeSync(
|
||||
// int markerHash,
|
||||
// Vector3 position, Vector3 rotation)
|
||||
// {
|
||||
// // check if the data has changed
|
||||
// if (_latestMovementParentSyncData.MarkerHash == markerHash
|
||||
// && _latestMovementParentSyncData.RootPosition == position
|
||||
// && _latestMovementParentSyncData.RootRotation == rotation)
|
||||
// return; // no need to update (shocking)
|
||||
//
|
||||
// _latestMovementParentSyncData.HasSyncedThisData = false; // reset
|
||||
// _latestMovementParentSyncData.MarkerHash = markerHash;
|
||||
// _latestMovementParentSyncData.RootPosition = position;
|
||||
// _latestMovementParentSyncData.RootRotation = rotation;
|
||||
// }
|
||||
//
|
||||
// private static void SendMessage(MessageType messageType, int markerHash, Vector3 position, Vector3 rotation)
|
||||
// {
|
||||
// if (!IsConnectedToGameNetwork())
|
||||
// return;
|
||||
//
|
||||
// using ModNetworkMessage modMsg = new(ModId);
|
||||
// modMsg.Write((byte)messageType);
|
||||
// modMsg.Write(markerHash);
|
||||
// modMsg.Write(position);
|
||||
// modMsg.Write(rotation);
|
||||
// modMsg.Send();
|
||||
//
|
||||
// if (Debug_NetworkOutbound)
|
||||
// Debug.Log(
|
||||
// $"[Outbound] MessageType: {messageType}, MarkerHash: {markerHash}, Position: {position}, " +
|
||||
// $"Rotation: {rotation}");
|
||||
// }
|
||||
//
|
||||
// private static void OnMessageReceived(ModNetworkMessage msg)
|
||||
// {
|
||||
// msg.Read(out byte msgTypeRaw);
|
||||
//
|
||||
// if (!Enum.IsDefined(typeof(MessageType), msgTypeRaw))
|
||||
// return;
|
||||
//
|
||||
// switch ((MessageType)msgTypeRaw)
|
||||
// {
|
||||
// case MessageType.MovementParentOrChair:
|
||||
// msg.Read(out int markerHash);
|
||||
// msg.Read(out Vector3 receivedPosition);
|
||||
// msg.Read(out Vector3 receivedRotation);
|
||||
// // msg.Read(out Vector3 receivedHipPosition);
|
||||
// // msg.Read(out Vector3 receivedHipRotation);
|
||||
//
|
||||
// OnNetworkPositionUpdateReceived(msg.Sender, markerHash, receivedPosition, receivedRotation);
|
||||
//
|
||||
// if (Debug_NetworkInbound)
|
||||
// Debug.Log($"[Inbound] Sender: {msg.Sender}, MarkerHash: {markerHash}, " +
|
||||
// $"Position: {receivedPosition}, Rotation: {receivedRotation}");
|
||||
// break;
|
||||
// default:
|
||||
// Debug.LogError($"Invalid message type received from: {msg.Sender}");
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// #endregion
|
||||
//
|
||||
// #region Private Methods
|
||||
//
|
||||
// private static bool IsConnectedToGameNetwork()
|
||||
// {
|
||||
// return NetworkManager.Instance != null
|
||||
// && NetworkManager.Instance.GameNetwork != null
|
||||
// && NetworkManager.Instance.GameNetwork.ConnectionState == ConnectionState.Connected;
|
||||
// }
|
||||
//
|
||||
// private static void OnNetworkPositionUpdateReceived(
|
||||
// string sender, int markerHash,
|
||||
// Vector3 position, Vector3 rotation)
|
||||
// {
|
||||
// RelativeSyncManager.ApplyRelativeSync(sender, markerHash, position, rotation);
|
||||
// }
|
||||
//
|
||||
// #endregion
|
||||
// }
|
16
.Experimental/OriginShift/OriginShift.csproj
Normal file
16
.Experimental/OriginShift/OriginShift.csproj
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<Reference Include="BTKUILib">
|
||||
<HintPath>..\.ManagedLibs\BTKUILib.dll</HintPath>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\origin-shift-inactive.png" />
|
||||
<EmbeddedResource Include="Resources\OriginShift-Icon-Inactive.png" />
|
||||
<None Remove="Resources\origin-shift-forced.png" />
|
||||
<EmbeddedResource Include="Resources\OriginShift-Icon-Forced.png" />
|
||||
<None Remove="Resources\origin-shift-active.png" />
|
||||
<EmbeddedResource Include="Resources\OriginShift-Icon-Active.png" />
|
||||
</ItemGroup>
|
||||
</Project>
|
|
@ -0,0 +1,144 @@
|
|||
using UnityEngine;
|
||||
using UnityEngine.Animations;
|
||||
|
||||
namespace NAK.OriginShift.Components;
|
||||
|
||||
public class ChunkController : MonoBehaviour
|
||||
{
|
||||
// manage all chunks (all axis, likely 10kx10k10)
|
||||
// keep track of nearby chunks to player (these are "active" chunks)
|
||||
// when origin shift occurs, need to mark flag to go through and disable/enable chunks again
|
||||
// allow 1ms each frame to be spent on updating chunks, until work is completely done
|
||||
// when a chunk is enabled/disabled, a callback is sent to the chunk to update its state
|
||||
|
||||
#region Serialized Fields
|
||||
|
||||
[SerializeField, NotKeyable] private int _maxChunkDistance = 10;
|
||||
|
||||
#endregion Serialized Fields
|
||||
|
||||
#region Chunk Class
|
||||
|
||||
public class Chunk
|
||||
{
|
||||
public Vector3Int Position { get; }
|
||||
|
||||
public bool IsActive { get; private set; }
|
||||
|
||||
public void SetActive(bool active)
|
||||
{
|
||||
IsActive = active;
|
||||
if (active)
|
||||
OnChunkLoad?.Invoke();
|
||||
else
|
||||
OnChunkUnload?.Invoke();
|
||||
}
|
||||
|
||||
public Chunk(Vector3Int position, Action onChunkLoad, Action onChunkUnload)
|
||||
{
|
||||
Position = position;
|
||||
OnChunkLoad = onChunkLoad;
|
||||
OnChunkUnload = onChunkUnload;
|
||||
}
|
||||
|
||||
private readonly Action OnChunkLoad;
|
||||
private readonly Action OnChunkUnload;
|
||||
}
|
||||
|
||||
#endregion Chunk Class
|
||||
|
||||
public static Dictionary<Vector3Int, Chunk> Chunks { get; private set; } = new();
|
||||
|
||||
public static void AddChunk(Chunk chunk)
|
||||
=> Chunks.Add(chunk.Position, chunk);
|
||||
|
||||
public static void RemoveChunk(Chunk chunk)
|
||||
=> Chunks.Remove(chunk.Position);
|
||||
|
||||
private Chunk[,,] _loadedChunks;
|
||||
private int _halfChunkDistance;
|
||||
|
||||
private int _originX = int.MaxValue;
|
||||
private int _originY = int.MaxValue;
|
||||
private int _originZ = int.MaxValue;
|
||||
|
||||
#region Unity Events
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_maxChunkDistance = Mathf.Max(1, _maxChunkDistance);
|
||||
_halfChunkDistance = _maxChunkDistance / 2;
|
||||
_loadedChunks = new Chunk[_maxChunkDistance, _maxChunkDistance, _maxChunkDistance];
|
||||
UpdateMap(0, 0, 0); // initial load
|
||||
|
||||
OriginShiftManager.OnPostOriginShifted += OnOriginShift;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
OriginShiftManager.OnPostOriginShifted -= OnOriginShift;
|
||||
}
|
||||
|
||||
private void UpdateMap(int xPos, int yPos, int zPos) {
|
||||
int deltaX = xPos - _originX;
|
||||
int deltaY = yPos - _originY;
|
||||
int deltaZ = zPos - _originZ;
|
||||
|
||||
_originX = xPos;
|
||||
_originY = yPos;
|
||||
_originZ = zPos;
|
||||
|
||||
for (int x = 0; x < _maxChunkDistance; x++) {
|
||||
for (int y = 0; y < _maxChunkDistance; y++) {
|
||||
for (int z = 0; z < _maxChunkDistance; z++) {
|
||||
int offsetX = x + deltaX;
|
||||
int offsetY = y + deltaY;
|
||||
int offsetZ = z + deltaZ;
|
||||
|
||||
if (offsetX < 0 || offsetX >= _maxChunkDistance || offsetY < 0 || offsetY >= _maxChunkDistance
|
||||
|| offsetZ < 0 || offsetZ >= _maxChunkDistance) {
|
||||
int tileX = x + xPos;
|
||||
int tileY = y + yPos;
|
||||
int tileZ = z + zPos;
|
||||
|
||||
int mapX = tileX % _maxChunkDistance;
|
||||
int mapY = tileY % _maxChunkDistance;
|
||||
int mapZ = tileZ % _maxChunkDistance;
|
||||
|
||||
if (mapX < 0) mapX += _maxChunkDistance;
|
||||
if (mapY < 0) mapY += _maxChunkDistance;
|
||||
if (mapZ < 0) mapZ += _maxChunkDistance;
|
||||
|
||||
// call OnChunkUnload on chunk
|
||||
_loadedChunks[mapX, mapY, mapZ]?.SetActive(false);
|
||||
|
||||
// access new chunk
|
||||
tileX -= _halfChunkDistance;
|
||||
tileY -= _halfChunkDistance;
|
||||
tileZ -= _halfChunkDistance;
|
||||
|
||||
// create Vector3Int for lookup
|
||||
Vector3Int newChunkPosition = new(tileX, tileY, tileZ);
|
||||
if (Chunks.TryGetValue(newChunkPosition, out Chunk chunk))
|
||||
{
|
||||
chunk.SetActive(true);
|
||||
_loadedChunks[mapX, mapY, mapZ] = chunk;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Unity Events
|
||||
|
||||
#region Origin Shift Events
|
||||
|
||||
private void OnOriginShift(Vector3 _)
|
||||
{
|
||||
Vector3Int currentChunk = OriginShiftManager.Instance.ChunkOffset;
|
||||
UpdateMap(currentChunk.x, currentChunk.y, currentChunk.z);
|
||||
}
|
||||
|
||||
#endregion Origin Shift Events
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace NAK.OriginShift.Components;
|
||||
|
||||
public class ChunkCreator : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private GameObject _chunkPrefab;
|
||||
|
||||
private bool _isChunkCreated;
|
||||
|
||||
public void CreateChunk()
|
||||
{
|
||||
_isChunkCreated = true;
|
||||
|
||||
Transform transform1 = transform;
|
||||
Instantiate(_chunkPrefab, transform1.position, Quaternion.identity, transform1);
|
||||
}
|
||||
|
||||
public void DestroyChunk()
|
||||
{
|
||||
if (!_isChunkCreated)
|
||||
return;
|
||||
|
||||
_isChunkCreated = false;
|
||||
Destroy(gameObject.transform.GetChild(0).gameObject);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace NAK.OriginShift.Components;
|
||||
|
||||
public class ChunkListener : MonoBehaviour
|
||||
{
|
||||
#region Serialized Properties
|
||||
|
||||
[SerializeField] private Vector3Int _chunkCoords;
|
||||
[SerializeField] private UnityEvent _onChunkLoad;
|
||||
[SerializeField] private UnityEvent _onChunkUnload;
|
||||
|
||||
#endregion Serialized Properties
|
||||
|
||||
#region Private Variables
|
||||
|
||||
private ChunkController.Chunk _chunk;
|
||||
|
||||
#endregion Private Variables
|
||||
|
||||
#region Unity Events
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_chunk = new ChunkController.Chunk(_chunkCoords, OnChunkLoad, OnChunkUnload);
|
||||
ChunkController.AddChunk(_chunk);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
ChunkController.RemoveChunk(_chunk);
|
||||
}
|
||||
|
||||
#endregion Unity Events
|
||||
|
||||
#region Chunk Events
|
||||
|
||||
private void OnChunkLoad()
|
||||
{
|
||||
Vector3Int chunkAbsPos = _chunk.Position * OriginShiftController.ORIGIN_SHIFT_THRESHOLD;
|
||||
transform.position = OriginShiftManager.GetLocalizedPosition(chunkAbsPos);
|
||||
_onChunkLoad?.Invoke();
|
||||
}
|
||||
|
||||
private void OnChunkUnload()
|
||||
{
|
||||
_onChunkUnload?.Invoke();
|
||||
}
|
||||
|
||||
#endregion Chunk Events
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
using UnityEngine;
|
||||
|
||||
#if !UNITY_EDITOR
|
||||
using UnityEngine.SceneManagement;
|
||||
using NAK.OriginShift.Utility;
|
||||
#endif
|
||||
|
||||
// Creator Exposed component
|
||||
|
||||
namespace NAK.OriginShift.Components;
|
||||
|
||||
public class OriginShiftController : MonoBehaviour
|
||||
{
|
||||
public static OriginShiftController Instance { get; private set; }
|
||||
|
||||
#region Serialized Fields
|
||||
|
||||
[Header("Config / Shift Params")]
|
||||
|
||||
[SerializeField] private bool _shiftVertical = true;
|
||||
[SerializeField] [Range(10, 2500)] private int _shiftThreshold = 15;
|
||||
|
||||
[Header("Config / Scene Objects")]
|
||||
|
||||
[SerializeField] private bool _autoMoveSceneRoots = true;
|
||||
[SerializeField] private Transform[] _toShiftTransforms = Array.Empty<Transform>();
|
||||
|
||||
[Header("Config / Additive Objects")]
|
||||
|
||||
[SerializeField] private bool _shiftRemotePlayers = true;
|
||||
[SerializeField] private bool _shiftSpawnedObjects = true;
|
||||
|
||||
#endregion Serialized Fields
|
||||
|
||||
#region Internal Fields
|
||||
|
||||
internal bool IsForced { get; set; }
|
||||
|
||||
#endregion Internal Fields
|
||||
|
||||
#if !UNITY_EDITOR
|
||||
|
||||
public static int ORIGIN_SHIFT_THRESHOLD = 15;
|
||||
|
||||
#region Unity Events
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null
|
||||
&& Instance != this)
|
||||
{
|
||||
Destroy(this);
|
||||
OriginShiftMod.Logger.Error("Only one OriginShiftController can exist in a scene.");
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// set threshold (we can not support dynamic threshold change)
|
||||
ORIGIN_SHIFT_THRESHOLD = IsForced ? 1000 : _shiftThreshold;
|
||||
|
||||
OriginShiftManager.OnOriginShifted += OnOriginShifted;
|
||||
OriginShiftManager.Instance.SetupManager(IsForced);
|
||||
|
||||
// if auto, we will just move everything :)
|
||||
if (_autoMoveSceneRoots)
|
||||
GetAllSceneRootTransforms();
|
||||
|
||||
// if we have scene roots, we will anchor all static renderers
|
||||
if (_toShiftTransforms.Length != 0)
|
||||
AnchorAllStaticRenderers();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted -= OnOriginShifted;
|
||||
OriginShiftManager.Instance.ResetManager();
|
||||
}
|
||||
|
||||
#endregion Unity Events
|
||||
|
||||
#region Private Methods
|
||||
|
||||
private void GetAllSceneRootTransforms()
|
||||
{
|
||||
Scene scene = gameObject.scene;
|
||||
var sceneRoots = scene.GetRootGameObjects();
|
||||
_toShiftTransforms = new Transform[sceneRoots.Length + 1]; // +1 for the static batch anchor
|
||||
for (var i = 0; i < sceneRoots.Length; i++) _toShiftTransforms[i] = sceneRoots[i].transform;
|
||||
}
|
||||
|
||||
private void AnchorAllStaticRenderers()
|
||||
{
|
||||
// create an anchor object at 0,0,0
|
||||
Transform anchor = new GameObject("NAK.StaticBatchAnchor").transform;
|
||||
anchor.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
|
||||
anchor.localScale = Vector3.one;
|
||||
|
||||
// add to end of root transforms
|
||||
_toShiftTransforms[^1] = anchor;
|
||||
|
||||
// crawl all children and find Renderers part of static batch
|
||||
foreach (Transform toShiftTransform in _toShiftTransforms)
|
||||
{
|
||||
var renderers = toShiftTransform.GetComponentsInChildren<Renderer>(true);
|
||||
foreach (Renderer renderer in renderers)
|
||||
{
|
||||
if (renderer.isPartOfStaticBatch) // access staticBatchRootTransform using reflection and override it
|
||||
RendererReflectionUtility.SetStaticBatchRootTransform(renderer, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Private Methods
|
||||
|
||||
#region Origin Shift Events
|
||||
|
||||
private void OnOriginShifted(Vector3 shift)
|
||||
{
|
||||
foreach (Transform toShiftTransform in _toShiftTransforms)
|
||||
{
|
||||
if (toShiftTransform == null) continue; // skip nulls
|
||||
toShiftTransform.position += shift;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Origin Shift Events
|
||||
|
||||
#endif
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
using ABI.CCK.Components;
|
||||
using JetBrains.Annotations;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
#if !UNITY_EDITOR
|
||||
using ABI_RC.Core.Util;
|
||||
using ABI_RC.Core.Util.AssetFiltering;
|
||||
#endif
|
||||
|
||||
namespace NAK.OriginShift.Components;
|
||||
|
||||
public class OriginShiftEventReceiver : MonoBehaviour
|
||||
{
|
||||
#region Serialized Fields
|
||||
|
||||
[SerializeField] private UnityEvent _onOriginShifted = new();
|
||||
[SerializeField] private bool _filterChunkBoundary;
|
||||
[SerializeField] private Vector3 _chunkBoundaryMin = Vector3.zero;
|
||||
[SerializeField] private Vector3 _chunkBoundaryMax = Vector3.one;
|
||||
|
||||
#endregion Serialized Fields
|
||||
|
||||
#if !UNITY_EDITOR
|
||||
|
||||
#region Private Fields
|
||||
|
||||
private bool _isInitialized;
|
||||
|
||||
#endregion Private Fields
|
||||
|
||||
#region Unity Events
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (!_isInitialized)
|
||||
{
|
||||
//SharedFilter.SanitizeUnityEvents("OriginShiftEventReceiver", _onOriginShifted);
|
||||
|
||||
UnityEventsHelper.SanitizeUnityEvents("OriginShiftEventReceiver",
|
||||
UnityEventsHelper.EventSource.Unknown,
|
||||
this,
|
||||
CVRWorld.Instance,
|
||||
_onOriginShifted);
|
||||
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
OriginShiftManager.OnOriginShifted += HandleOriginShifted;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted -= HandleOriginShifted;
|
||||
}
|
||||
|
||||
#endregion Unity Events
|
||||
|
||||
#region Origin Shift Events
|
||||
|
||||
private void HandleOriginShifted(Vector3 shift)
|
||||
{
|
||||
if (_filterChunkBoundary && !IsWithinChunkBoundary(shift))
|
||||
return;
|
||||
|
||||
// wrap user-defined event because the user can't be trusted
|
||||
try
|
||||
{
|
||||
_onOriginShifted.Invoke();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
OriginShiftMod.Logger.Error("OriginShiftEventReceiver: Exception invoking OnOriginShifted event: " + e, this);
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsWithinChunkBoundary(Vector3 shift)
|
||||
{
|
||||
return shift.x >= _chunkBoundaryMin.x && shift.x <= _chunkBoundaryMax.x &&
|
||||
shift.y >= _chunkBoundaryMin.y && shift.y <= _chunkBoundaryMax.y &&
|
||||
shift.z >= _chunkBoundaryMin.z && shift.z <= _chunkBoundaryMax.z;
|
||||
}
|
||||
|
||||
#endregion Origin Shift Events
|
||||
|
||||
#endif
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace NAK.OriginShift.Components;
|
||||
|
||||
public class OriginShiftParticleSystemReceiver : MonoBehaviour
|
||||
{
|
||||
#if !UNITY_EDITOR
|
||||
|
||||
// max particles count cause i said so 2
|
||||
private static readonly ParticleSystem.Particle[] _tempParticles = new ParticleSystem.Particle[10000];
|
||||
|
||||
private ParticleSystem[] _particleSystems;
|
||||
|
||||
#region Unity Events
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_particleSystems = GetComponentsInChildren<ParticleSystem>(true);
|
||||
if (_particleSystems.Length == 0)
|
||||
{
|
||||
OriginShiftMod.Logger.Error("OriginShiftParticleSystemReceiver: No ParticleSystems found on GameObject: " + gameObject.name, this);
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted += OnOriginShifted;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted -= OnOriginShifted;
|
||||
}
|
||||
|
||||
#endregion Unity Events
|
||||
|
||||
#region Origin Shift Events
|
||||
|
||||
private void OnOriginShifted(Vector3 offset)
|
||||
{
|
||||
foreach (ParticleSystem particleSystem in _particleSystems)
|
||||
ShiftParticleSystem(particleSystem, offset);
|
||||
}
|
||||
|
||||
private static void ShiftParticleSystem(ParticleSystem particleSystem, Vector3 offset)
|
||||
{
|
||||
int particleCount = particleSystem.GetParticles(_tempParticles);
|
||||
for (int i = 0; i < particleCount; i++) _tempParticles[i].position += offset;
|
||||
particleSystem.SetParticles(_tempParticles, particleCount);
|
||||
}
|
||||
|
||||
#endregion Origin Shift Events
|
||||
|
||||
#endif
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace NAK.OriginShift.Components;
|
||||
|
||||
public class OriginShiftRigidbodyReceiver : MonoBehaviour
|
||||
{
|
||||
#if !UNITY_EDITOR
|
||||
|
||||
private Rigidbody _rigidbody;
|
||||
|
||||
#region Unity Events
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_rigidbody = GetComponentInChildren<Rigidbody>();
|
||||
if (_rigidbody == null)
|
||||
{
|
||||
OriginShiftMod.Logger.Error("OriginShiftRigidbodyReceiver: No Rigidbody found on GameObject: " + gameObject.name, this);
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted += OnOriginShifted;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted -= OnOriginShifted;
|
||||
}
|
||||
|
||||
#endregion Unity Events
|
||||
|
||||
#region Origin Shift Events
|
||||
|
||||
private void OnOriginShifted(Vector3 shift)
|
||||
{
|
||||
_rigidbody.position += shift;
|
||||
}
|
||||
|
||||
#endregion Origin Shift Events
|
||||
|
||||
#endif
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace NAK.OriginShift.Components;
|
||||
|
||||
public class OriginShiftTrailRendererReceiver : MonoBehaviour
|
||||
{
|
||||
#if !UNITY_EDITOR
|
||||
|
||||
// max positions count cause i said so
|
||||
private static readonly Vector3[] _tempPositions = new Vector3[10000];
|
||||
|
||||
private TrailRenderer[] _trailRenderers;
|
||||
|
||||
#region Unity Events
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_trailRenderers = GetComponentsInChildren<TrailRenderer>(true);
|
||||
if (_trailRenderers.Length == 0)
|
||||
{
|
||||
OriginShiftMod.Logger.Error("OriginShiftTrailRendererReceiver: No TrailRenderers found on GameObject: " + gameObject.name, this);
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted += OnOriginShifted;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted -= OnOriginShifted;
|
||||
}
|
||||
|
||||
#endregion Unity Events
|
||||
|
||||
#region Origin Shift Events
|
||||
|
||||
private void OnOriginShifted(Vector3 offset)
|
||||
{
|
||||
foreach (TrailRenderer trailRenderer in _trailRenderers)
|
||||
ShiftTrailRenderer(trailRenderer, offset);
|
||||
}
|
||||
|
||||
private static void ShiftTrailRenderer(TrailRenderer trailRenderer, Vector3 offset)
|
||||
{
|
||||
trailRenderer.GetPositions(_tempPositions);
|
||||
for (var i = 0; i < _tempPositions.Length; i++) _tempPositions[i] += offset;
|
||||
trailRenderer.SetPositions(_tempPositions);
|
||||
}
|
||||
|
||||
#endregion Origin Shift Events
|
||||
|
||||
#endif
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace NAK.OriginShift.Components;
|
||||
|
||||
public class OriginShiftTransformReceiver : MonoBehaviour
|
||||
{
|
||||
#if !UNITY_EDITOR
|
||||
|
||||
#region Unity Events
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted += OnOriginShifted;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted -= OnOriginShifted;
|
||||
}
|
||||
|
||||
#endregion Unity Events
|
||||
|
||||
#region Origin Shift Events
|
||||
|
||||
private void OnOriginShifted(Vector3 shift)
|
||||
{
|
||||
transform.position += shift;
|
||||
}
|
||||
|
||||
#endregion Origin Shift Events
|
||||
|
||||
#endif
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
using ABI_RC.Core;
|
||||
using ABI_RC.Core.Player;
|
||||
using ABI_RC.Systems.Movement;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.OriginShift.Extensions;
|
||||
|
||||
public static class BetterBetterCharacterControllerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Offsets the player by the given vector.
|
||||
/// This is a simple move operation that does not affect velocity, grounded state, movement parent, ect.
|
||||
/// </summary>
|
||||
/// <param name="controller"></param>
|
||||
/// <param name="offset"></param>
|
||||
public static void OffsetBy(this BetterBetterCharacterController controller, Vector3 offset)
|
||||
{
|
||||
controller.MoveTo(PlayerSetup.Instance.GetPlayerPosition() + offset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the player to the target position while keeping velocity, grounded state, movement parent, ect.
|
||||
/// Allows moving the player in a way that is meant to be seamless, unlike TeleportTo or SetPosition.
|
||||
/// </summary>
|
||||
/// <param name="controller"></param>
|
||||
/// <param name="targetPos"></param>
|
||||
/// <param name="interpolate"></param>
|
||||
public static void MoveTo(this BetterBetterCharacterController controller, Vector3 targetPos,
|
||||
bool interpolate = false)
|
||||
{
|
||||
// character controller is not built to account for the player's VR offset
|
||||
Vector3 vector = targetPos - PlayerSetup.Instance.GetPlayerPosition();
|
||||
Vector3 vector2 = controller.GetPosition() + vector;
|
||||
|
||||
if (!CVRTools.IsWithinMaxBounds(vector2))
|
||||
{
|
||||
// yeah, ill play your game
|
||||
CommonTools.LogAuto(CommonTools.LogLevelType_t.Warning,
|
||||
"Attempted to move player further than the maximum allowed bounds.", "",
|
||||
"OriginShift/Extensions/BetterBetterCharacterControllerExtensions.cs",
|
||||
"MoveTo", 19);
|
||||
return;
|
||||
}
|
||||
|
||||
controller.TeleportPosition(vector2, interpolate); // move player
|
||||
controller.SetVelocity(controller.characterMovement.velocity); // keep velocity
|
||||
controller.UpdateColliderCenter(vector2, true); // update collider center
|
||||
controller.characterMovement.UpdateCurrentPlatform(); // recalculate stored local offset
|
||||
|
||||
// invoke event so ik can update
|
||||
BetterBetterCharacterController.OnMovementParentMove.Invoke(
|
||||
new BetterBetterCharacterController.PlayerMoveOffset(
|
||||
PlayerSetup.Instance.GetPlayerPosition(),
|
||||
vector,
|
||||
Quaternion.identity));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
using ABI_RC.Core.Player;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.OriginShift.Extensions;
|
||||
|
||||
public static class PlayerSetupExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Utility method to offset the player's currently stored avatar movement data.
|
||||
/// Needs to be called, otherwise outbound net ik will be one-frame behind on teleport events.
|
||||
/// </summary>
|
||||
/// <param name="playerSetup"></param>
|
||||
/// <param name="offset"></param>
|
||||
public static void OffsetAvatarMovementData(this PlayerSetup playerSetup, Vector3 offset)
|
||||
{
|
||||
playerSetup._playerAvatarMovementData.RootPosition += offset;
|
||||
playerSetup._playerAvatarMovementData.BodyPosition += offset; // why in world space -_-
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
using HarmonyLib;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.OriginShift.Hacks;
|
||||
|
||||
#region Harmony Patches
|
||||
|
||||
internal static class OcclusionCullingPatches
|
||||
{
|
||||
// i wish i had this when working on the head hiding & shadow clones... would have had a much better
|
||||
// method of hiding mesh that wouldn't have broken magica cloth :< (one day: make test mod to do that)
|
||||
|
||||
[HarmonyPostfix] // after all onprecull listeners
|
||||
[HarmonyPatch(typeof(Camera), "FireOnPreCull")]
|
||||
private static void OnPreCullPostfix(Camera cam)
|
||||
{
|
||||
OcclusionCullingHack hackInstance = cam.GetComponent<OcclusionCullingHack>();
|
||||
if (hackInstance != null) hackInstance.OnPostFirePreCull(cam);
|
||||
}
|
||||
|
||||
[HarmonyPrefix] // before all onprerender listeners
|
||||
[HarmonyPatch(typeof(Camera), "FireOnPreRender")]
|
||||
private static void OnPreRenderPrefix(Camera cam)
|
||||
{
|
||||
OcclusionCullingHack hackInstance = cam.GetComponent<OcclusionCullingHack>();
|
||||
if (hackInstance != null) hackInstance.OnPreFirePreRender(cam);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Harmony Patches
|
||||
|
||||
/// <summary>
|
||||
/// Attempted hack to fix occlusion culling for *static* objects. This does not fix dynamic objects, they will be culled
|
||||
/// by the camera's frustum & original baked occlusion culling data. Nothing can be done about that. :>
|
||||
/// </summary>
|
||||
public class OcclusionCullingHack : MonoBehaviour
|
||||
{
|
||||
private Vector3 originalPosition;
|
||||
|
||||
internal void OnPostFirePreCull(Camera cam)
|
||||
{
|
||||
originalPosition = cam.transform.position;
|
||||
cam.transform.position = OriginShiftManager.GetAbsolutePosition(originalPosition);
|
||||
}
|
||||
|
||||
internal void OnPreFirePreRender(Camera cam)
|
||||
{
|
||||
cam.transform.position = originalPosition;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
using UnityEngine;
|
||||
|
||||
namespace NAK.OriginShift.Hacks;
|
||||
|
||||
public class OriginShiftOcclusionCullingDisabler : MonoBehaviour
|
||||
{
|
||||
private Camera _camera;
|
||||
private bool _originalCullingState;
|
||||
|
||||
#region Unity Events
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_camera = GetComponent<Camera>();
|
||||
if (_camera == null)
|
||||
{
|
||||
Debug.LogError("OriginShiftOcclusionCullingDisabler requires a Camera component on the same GameObject.");
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
_originalCullingState = _camera.useOcclusionCulling;
|
||||
}
|
||||
|
||||
private void Awake() // we want to execute even if the component is disabled
|
||||
{
|
||||
OriginShiftManager.OnStateChanged += OnOriginShiftStateChanged;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
OriginShiftManager.OnStateChanged -= OnOriginShiftStateChanged;
|
||||
}
|
||||
|
||||
#endregion Unity Events
|
||||
|
||||
#region Origin Shift Events
|
||||
|
||||
private void OnOriginShiftStateChanged(OriginShiftManager.OriginShiftState state)
|
||||
{
|
||||
_camera.useOcclusionCulling = state != OriginShiftManager.OriginShiftState.Forced && _originalCullingState;
|
||||
}
|
||||
|
||||
#endregion Origin Shift Events
|
||||
}
|
245
.Experimental/OriginShift/OriginShift/OriginShiftManager.cs
Normal file
245
.Experimental/OriginShift/OriginShift/OriginShiftManager.cs
Normal file
|
@ -0,0 +1,245 @@
|
|||
using System;
|
||||
using ABI_RC.Core.Base;
|
||||
using ABI_RC.Core.Player;
|
||||
using ABI.CCK.Components;
|
||||
using JetBrains.Annotations;
|
||||
using MagicaCloth;
|
||||
using NAK.OriginShift.Components;
|
||||
using NAK.OriginShift.Utility;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.OriginShift;
|
||||
|
||||
public class OriginShiftManager : MonoBehaviour
|
||||
{
|
||||
#region Singleton
|
||||
|
||||
private static OriginShiftManager _instance;
|
||||
|
||||
public static OriginShiftManager Instance
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_instance) return _instance;
|
||||
_instance = new GameObject("NAKOriginShiftManager").AddComponent<OriginShiftManager>();
|
||||
DontDestroyOnLoad(_instance.gameObject);
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool _useOriginShift;
|
||||
|
||||
public static bool UseOriginShift
|
||||
{
|
||||
get => _useOriginShift;
|
||||
set
|
||||
{
|
||||
if (_useOriginShift == value)
|
||||
return;
|
||||
|
||||
if (value)
|
||||
{
|
||||
PlayerSetup.Instance.gameObject.AddComponentIfMissing<OriginShiftMonitor>();
|
||||
}
|
||||
else
|
||||
{
|
||||
Instance.ResetOrigin();
|
||||
if (PlayerSetup.Instance.TryGetComponent(out OriginShiftMonitor originShiftMonitor))
|
||||
DestroyImmediate(originShiftMonitor);
|
||||
}
|
||||
|
||||
_useOriginShift = value;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool _compatibilityMode = true;
|
||||
public static bool CompatibilityMode
|
||||
{
|
||||
get => _useOriginShift && _compatibilityMode;
|
||||
set => _compatibilityMode = value;
|
||||
}
|
||||
|
||||
#endregion Singleton
|
||||
|
||||
#region Shader Globals
|
||||
|
||||
private static readonly int s_OriginShiftChunkOffset = Shader.PropertyToID("_OriginShiftChunkOffset"); // Vector3
|
||||
private static readonly int s_OriginShiftChunkPosition = Shader.PropertyToID("_OriginShiftChunkPosition"); // Vector3
|
||||
private static readonly int s_OriginShiftChunkThreshold = Shader.PropertyToID("_OriginShiftChunkThreshold"); // float
|
||||
|
||||
private static void SetShaderGlobals(Vector3 chunk, float threshold)
|
||||
{
|
||||
Shader.SetGlobalVector(s_OriginShiftChunkOffset, chunk);
|
||||
Shader.SetGlobalFloat(s_OriginShiftChunkThreshold, threshold);
|
||||
Shader.SetGlobalVector(s_OriginShiftChunkPosition, chunk * threshold);
|
||||
}
|
||||
|
||||
private static void ResetShaderGlobals()
|
||||
{
|
||||
Shader.SetGlobalVector(s_OriginShiftChunkOffset, Vector3.zero);
|
||||
Shader.SetGlobalFloat(s_OriginShiftChunkThreshold, -1f);
|
||||
Shader.SetGlobalVector(s_OriginShiftChunkPosition, Vector3.zero);
|
||||
}
|
||||
|
||||
#endregion Shader Globals
|
||||
|
||||
#region Actions
|
||||
|
||||
public static Action<OriginShiftState> OnStateChanged = delegate { };
|
||||
|
||||
public static Action<Vector3> OnOriginShifted = delegate { }; // move everything
|
||||
public static Action<Vector3> OnPostOriginShifted = delegate { }; // player & chunks
|
||||
|
||||
#endregion Actions
|
||||
|
||||
#region Public Properties
|
||||
|
||||
[PublicAPI] public bool IsOriginShifted => ChunkOffset != Vector3Int.zero;
|
||||
[PublicAPI] public Vector3Int ChunkOffset { get; internal set; } = Vector3Int.zero;
|
||||
[PublicAPI] public Vector3Int ChunkPosition => ChunkOffset * OriginShiftController.ORIGIN_SHIFT_THRESHOLD;
|
||||
|
||||
public enum OriginShiftState
|
||||
{
|
||||
Inactive, // world is not using Origin Shift
|
||||
Active, // world is using Origin Shift
|
||||
Forced // temp for this session, force world to use Origin Shift
|
||||
}
|
||||
|
||||
private OriginShiftState _currentState = OriginShiftState.Inactive;
|
||||
[PublicAPI] public OriginShiftState CurrentState
|
||||
{
|
||||
get => _currentState;
|
||||
private set
|
||||
{
|
||||
if (_currentState == value)
|
||||
return;
|
||||
|
||||
_currentState = value;
|
||||
OnStateChanged.Invoke(value);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion Public Properties
|
||||
|
||||
#region Manager Lifecycle
|
||||
|
||||
private OriginShiftController _forceController;
|
||||
|
||||
public void ForceManager()
|
||||
{
|
||||
if (CVRWorld.Instance == null)
|
||||
{
|
||||
OriginShiftMod.Logger.Error("Cannot force Origin Shift without a world.");
|
||||
return;
|
||||
}
|
||||
OriginShiftMod.Logger.Msg("Forcing Origin Shift...");
|
||||
|
||||
_forceController = CVRWorld.Instance.gameObject.AddComponentIfMissing<OriginShiftController>();
|
||||
_forceController.IsForced = true;
|
||||
}
|
||||
|
||||
public void SetupManager(bool isForced = false)
|
||||
{
|
||||
if (CVRWorld.Instance == null)
|
||||
{
|
||||
OriginShiftMod.Logger.Error("Cannot setup Origin Shift without a world.");
|
||||
return;
|
||||
}
|
||||
OriginShiftMod.Logger.Msg("Setting up Origin Shift...");
|
||||
|
||||
CurrentState = isForced ? OriginShiftState.Forced : OriginShiftState.Active;
|
||||
UseOriginShift = true;
|
||||
}
|
||||
|
||||
public void ResetManager()
|
||||
{
|
||||
OriginShiftMod.Logger.Msg("Resetting Origin Shift...");
|
||||
|
||||
if (_forceController) Destroy(_forceController);
|
||||
|
||||
CurrentState = OriginShiftState.Inactive;
|
||||
|
||||
ResetOrigin();
|
||||
ResetShaderGlobals();
|
||||
UseOriginShift = false;
|
||||
}
|
||||
|
||||
#endregion Manager Lifecycle
|
||||
|
||||
#region Public Methods
|
||||
|
||||
// Called by OriginShiftMonitor when the local player needs to shit
|
||||
public void ShiftOrigin(Vector3 rawPosition)
|
||||
{
|
||||
if (!_useOriginShift) return;
|
||||
|
||||
// create stopwatch
|
||||
StopWatch stopwatch = new();
|
||||
stopwatch.Start();
|
||||
|
||||
// normalize
|
||||
float halfThreshold = (OriginShiftController.ORIGIN_SHIFT_THRESHOLD / 2f);
|
||||
rawPosition += new Vector3(halfThreshold, halfThreshold, halfThreshold);
|
||||
|
||||
// add to chunk
|
||||
Vector3Int chunkDifference;
|
||||
Vector3Int calculatedChunk = chunkDifference = ChunkOffset;
|
||||
|
||||
calculatedChunk.x += Mathf.FloorToInt(rawPosition.x / OriginShiftController.ORIGIN_SHIFT_THRESHOLD);
|
||||
calculatedChunk.y += Mathf.FloorToInt(rawPosition.y / OriginShiftController.ORIGIN_SHIFT_THRESHOLD);
|
||||
calculatedChunk.z += Mathf.FloorToInt(rawPosition.z / OriginShiftController.ORIGIN_SHIFT_THRESHOLD);
|
||||
|
||||
// get offset
|
||||
chunkDifference = (ChunkOffset - calculatedChunk) * OriginShiftController.ORIGIN_SHIFT_THRESHOLD;
|
||||
|
||||
// store & invoke
|
||||
ChunkOffset = calculatedChunk;
|
||||
OnOriginShifted.Invoke(chunkDifference);
|
||||
OnPostOriginShifted.Invoke(chunkDifference);
|
||||
|
||||
// set shader globals
|
||||
SetShaderGlobals(ChunkOffset, OriginShiftController.ORIGIN_SHIFT_THRESHOLD);
|
||||
|
||||
// log
|
||||
stopwatch.Stop();
|
||||
OriginShiftMod.Logger.Msg($"Shifted Origin: {chunkDifference} in {stopwatch.ElapsedMilliseconds:F11}ms");
|
||||
}
|
||||
|
||||
public void ResetOrigin()
|
||||
{
|
||||
if (!_useOriginShift) return;
|
||||
|
||||
ShiftOrigin(-ChunkPosition);
|
||||
ChunkOffset = Vector3Int.zero;
|
||||
}
|
||||
|
||||
#endregion Origin Shift Implementation
|
||||
|
||||
#region Utility Methods
|
||||
|
||||
public static Vector3 GetAbsolutePosition(Vector3 localizedPosition)
|
||||
{
|
||||
// absolute coordinates can be reconstructed using current chunk and threshold
|
||||
localizedPosition += Instance.ChunkOffset * OriginShiftController.ORIGIN_SHIFT_THRESHOLD;
|
||||
return localizedPosition;
|
||||
}
|
||||
|
||||
public static Vector3 GetLocalizedPosition(Vector3 absolutePosition)
|
||||
{
|
||||
return absolutePosition - (Instance.ChunkOffset * OriginShiftController.ORIGIN_SHIFT_THRESHOLD);
|
||||
}
|
||||
|
||||
#endregion Utility Methods
|
||||
|
||||
#region Debug Methods
|
||||
|
||||
public void ToggleDebugOverlay(bool state)
|
||||
{
|
||||
if (TryGetComponent(out DebugTextDisplay debugTextDisplay))
|
||||
Destroy(debugTextDisplay);
|
||||
else
|
||||
gameObject.AddComponent<DebugTextDisplay>();
|
||||
}
|
||||
|
||||
#endregion Debug Methods
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
using Zettai;
|
||||
|
||||
namespace NAK.OriginShift;
|
||||
|
||||
public class OriginShiftDbAvatarReceiver : MonoBehaviour
|
||||
{
|
||||
private DbJobsAvatarManager _avatarManager;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted += OnOriginShifted;
|
||||
_avatarManager = GetComponent<DbJobsAvatarManager>();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted -= OnOriginShifted;
|
||||
}
|
||||
|
||||
private void OnOriginShifted(Vector3 shift)
|
||||
{
|
||||
// get all particles
|
||||
var particles = _avatarManager.particlesArray;
|
||||
for (var index = 0; index < particles.Length; index++)
|
||||
{
|
||||
ParticleStruct particle = particles[index];
|
||||
|
||||
float3 position = particle.m_Position;
|
||||
position.x += shift.x;
|
||||
position.y += shift.y;
|
||||
position.z += shift.z;
|
||||
particle.m_Position = position;
|
||||
|
||||
position = particle.m_PrevPosition;
|
||||
position.x += shift.x;
|
||||
position.y += shift.y;
|
||||
position.z += shift.z;
|
||||
particle.m_PrevPosition = position;
|
||||
|
||||
particles[index] = particle;
|
||||
}
|
||||
_avatarManager.particlesArray = particles;
|
||||
|
||||
// get all transforminfo
|
||||
var transformInfos = _avatarManager.transformInfoArray;
|
||||
for (var index = 0; index < transformInfos.Length; index++)
|
||||
{
|
||||
TransformInfo transformInfo = transformInfos[index];
|
||||
|
||||
float3 position = transformInfo.position;
|
||||
position.x += shift.x;
|
||||
position.y += shift.y;
|
||||
position.z += shift.z;
|
||||
transformInfo.position = position;
|
||||
|
||||
position = transformInfo.prevPosition;
|
||||
position.x += shift.x;
|
||||
position.y += shift.y;
|
||||
position.z += shift.z;
|
||||
transformInfo.prevPosition = position;
|
||||
|
||||
transformInfos[index] = transformInfo;
|
||||
}
|
||||
_avatarManager.transformInfoArray = transformInfos;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
using System.Collections;
|
||||
using ABI_RC.Core;
|
||||
using ABI_RC.Core.Player;
|
||||
using NAK.OriginShift.Components;
|
||||
using NAK.OriginShift.Extensions;
|
||||
using UnityEngine;
|
||||
|
||||
#if !UNITY_EDITOR
|
||||
using ABI_RC.Systems.Movement;
|
||||
#endif
|
||||
|
||||
namespace NAK.OriginShift;
|
||||
|
||||
[DefaultExecutionOrder(int.MaxValue)]
|
||||
public class OriginShiftMonitor : MonoBehaviour
|
||||
{
|
||||
#if !UNITY_EDITOR
|
||||
private PlayerSetup _playerSetup;
|
||||
private BetterBetterCharacterController _characterController;
|
||||
#endif
|
||||
|
||||
#region Unity Events
|
||||
|
||||
private void Start()
|
||||
{
|
||||
#if !UNITY_EDITOR
|
||||
_playerSetup = GetComponent<PlayerSetup>();
|
||||
_characterController = GetComponent<BetterBetterCharacterController>();
|
||||
#endif
|
||||
OriginShiftManager.OnPostOriginShifted += OnPostOriginShifted;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
OriginShiftManager.OnPostOriginShifted -= OnPostOriginShifted;
|
||||
StopAllCoroutines();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// in CVR use GetPlayerPosition to account for VR offset
|
||||
Vector3 position = PlayerSetup.Instance.GetPlayerPosition();
|
||||
|
||||
// respawn height check
|
||||
// Vector3 absPosition = OriginShiftManager.GetAbsolutePosition(position);
|
||||
// if (absPosition.y < BetterBetterCharacterController.Instance.respawnHeight)
|
||||
// {
|
||||
// RootLogic.Instance.Respawn();
|
||||
// return;
|
||||
// }
|
||||
|
||||
float halfThreshold = OriginShiftController.ORIGIN_SHIFT_THRESHOLD / 2f; // i keep forgetting this
|
||||
if (Mathf.Abs(position.x) > halfThreshold
|
||||
|| Mathf.Abs(position.y) > halfThreshold
|
||||
|| Mathf.Abs(position.z) > halfThreshold)
|
||||
OriginShiftManager.Instance.ShiftOrigin(position);
|
||||
}
|
||||
|
||||
#endregion Unity Events
|
||||
|
||||
#region Origin Shift Events
|
||||
|
||||
private void OnPostOriginShifted(Vector3 shift)
|
||||
{
|
||||
#if UNITY_EDITOR
|
||||
// shift our transform back
|
||||
transform.position += shift;
|
||||
#else
|
||||
_characterController.OffsetBy(shift);
|
||||
_playerSetup.OffsetAvatarMovementData(shift);
|
||||
#endif
|
||||
}
|
||||
|
||||
#endregion Origin Shift Events
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
using ABI_RC.Core.Player;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.OriginShift;
|
||||
|
||||
public class OriginShiftNetIkReceiver : MonoBehaviour
|
||||
{
|
||||
private PuppetMaster _puppetMaster;
|
||||
|
||||
#region Unity Events
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_puppetMaster = GetComponent<PuppetMaster>();
|
||||
if (_puppetMaster == null)
|
||||
{
|
||||
OriginShiftMod.Logger.Error("OriginShiftNetIkReceiver: No PuppetMaster found on GameObject: " + gameObject.name, this);
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted += OnOriginShifted;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted -= OnOriginShifted;
|
||||
}
|
||||
|
||||
#endregion Unity Events
|
||||
|
||||
#region Origin Shift Events
|
||||
|
||||
private void OnOriginShifted(Vector3 shift)
|
||||
{
|
||||
// would be nice if these were relative positions like sub-syncs :)
|
||||
_puppetMaster._playerAvatarMovementDataCurrent.RootPosition += shift;
|
||||
_puppetMaster._playerAvatarMovementDataCurrent.BodyPosition += shift;
|
||||
_puppetMaster._playerAvatarMovementDataPast.RootPosition += shift;
|
||||
_puppetMaster._playerAvatarMovementDataPast.BodyPosition += shift;
|
||||
// later in frame puppetmaster will update remote player position
|
||||
}
|
||||
|
||||
#endregion Origin Shift Events
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
using ABI.CCK.Components;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.OriginShift;
|
||||
|
||||
public class OriginShiftObjectSyncReceiver : MonoBehaviour
|
||||
{
|
||||
private CVRObjectSync _objectSync;
|
||||
|
||||
#region Unity Events
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_objectSync = GetComponent<CVRObjectSync>();
|
||||
if (_objectSync == null)
|
||||
{
|
||||
OriginShiftMod.Logger.Error("OriginShiftObjectSyncReceiver: No CVRObjectSync found on GameObject: " + gameObject.name, this);
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted += OnOriginShifted;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted -= OnOriginShifted;
|
||||
}
|
||||
|
||||
#endregion Unity Events
|
||||
|
||||
#region Origin Shift Events
|
||||
|
||||
private void OnOriginShifted(Vector3 shift)
|
||||
{
|
||||
// idc, just shift all of them
|
||||
_objectSync._oldData.position += shift;
|
||||
_objectSync._currData.position += shift;
|
||||
_objectSync._futureData.position += shift;
|
||||
}
|
||||
|
||||
#endregion Origin Shift Events
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
using ABI.CCK.Components;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.OriginShift;
|
||||
|
||||
public class OriginShiftPickupObjectReceiver : MonoBehaviour
|
||||
{
|
||||
private CVRPickupObject _pickupObject;
|
||||
|
||||
#region Unity Events
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_pickupObject = GetComponent<CVRPickupObject>();
|
||||
if (_pickupObject == null)
|
||||
{
|
||||
OriginShiftMod.Logger.Error("OriginShiftPickupObjectReceiver requires a CVRPickupObject component!");
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted += OnOriginShifted;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted -= OnOriginShifted;
|
||||
}
|
||||
|
||||
#endregion Unity Events
|
||||
|
||||
#region Origin Shift Events
|
||||
|
||||
private void OnOriginShifted(Vector3 shift)
|
||||
{
|
||||
_pickupObject._respawnHeight += shift.y;
|
||||
}
|
||||
|
||||
#endregion Origin Shift Events
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
using ABI.CCK.Components;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.OriginShift;
|
||||
|
||||
public class OriginShiftSpawnableReceiver : MonoBehaviour
|
||||
{
|
||||
private CVRSpawnable _spawnable;
|
||||
|
||||
#region Unity Events
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_spawnable = GetComponent<CVRSpawnable>();
|
||||
if (_spawnable == null)
|
||||
{
|
||||
OriginShiftMod.Logger.Error("OriginShiftSpawnableReceiver: No CVRSpawnable found on GameObject: " + gameObject.name, this);
|
||||
enabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted += OnOriginShifted;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
OriginShiftManager.OnOriginShifted -= OnOriginShifted;
|
||||
}
|
||||
|
||||
#endregion Unity Events
|
||||
|
||||
#region Origin Shift Events
|
||||
|
||||
private void OnOriginShifted(Vector3 shift)
|
||||
{
|
||||
_spawnable.futurePosition += shift;
|
||||
_spawnable.currentPosition += shift;
|
||||
_spawnable.pastPosition += shift; // not used by game, just cached ?
|
||||
}
|
||||
|
||||
#endregion Origin Shift Events
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
#if !UNITY_EDITOR
|
||||
using ABI_RC.Core.Player;
|
||||
using NAK.OriginShift.Components;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.OriginShift.Utility;
|
||||
|
||||
public class DebugTextDisplay : MonoBehaviour
|
||||
{
|
||||
#region Private Variables
|
||||
|
||||
private string _debugText = "Initializing...";
|
||||
private Color _textColor = Color.white;
|
||||
|
||||
private bool _originShiftEventOccurred;
|
||||
private const float _blendDuration = 1.0f;
|
||||
private float _blendTime;
|
||||
|
||||
#endregion Private Variables
|
||||
|
||||
#region Unity Events
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
OriginShiftManager.OnPostOriginShifted += OnPostOriginShifted;
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
OriginShiftManager.OnPostOriginShifted -= OnPostOriginShifted;
|
||||
}
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
GUIStyle style = new()
|
||||
{
|
||||
fontSize = 25,
|
||||
normal = { textColor = _textColor },
|
||||
alignment = TextAnchor.UpperRight
|
||||
};
|
||||
|
||||
float screenWidth = Screen.width;
|
||||
var xPosition = screenWidth - 10;
|
||||
float yPosition = 10;
|
||||
|
||||
GUI.Label(new Rect(xPosition - 490, yPosition, 500, 150), _debugText, style);
|
||||
}
|
||||
|
||||
#endregion Unity Events
|
||||
|
||||
#region Public Methods
|
||||
|
||||
public void UpdateDebugText(string newText)
|
||||
{
|
||||
_debugText = newText;
|
||||
}
|
||||
|
||||
#endregion Public Methods
|
||||
|
||||
private void Update()
|
||||
{
|
||||
Vector3 currentChunk = OriginShiftManager.Instance.ChunkOffset;
|
||||
Vector3 localCoordinates = PlayerSetup.Instance.GetPlayerPosition();
|
||||
Vector3 absoluteCoordinates = localCoordinates;
|
||||
|
||||
// absolute coordinates can be reconstructed using current chunk and threshold
|
||||
absoluteCoordinates += currentChunk * OriginShiftController.ORIGIN_SHIFT_THRESHOLD;
|
||||
|
||||
// Update the debug text with the current coordinates
|
||||
UpdateDebugText($"Local Coordinates:\n{localCoordinates}\n\n" +
|
||||
$"Absolute Coordinates:\n{absoluteCoordinates}\n\n" +
|
||||
$"Current Chunk:\n{currentChunk}");
|
||||
|
||||
// Blend back to white if the origin shift event occurred
|
||||
if (_originShiftEventOccurred)
|
||||
{
|
||||
_blendTime += Time.deltaTime;
|
||||
_textColor = Color.Lerp(Color.red, Color.white, _blendTime / _blendDuration);
|
||||
if (_blendTime >= _blendDuration)
|
||||
{
|
||||
_originShiftEventOccurred = false;
|
||||
_blendTime = 0.0f;
|
||||
_textColor = Color.white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Origin Shift Events
|
||||
|
||||
private void OnPostOriginShifted(Vector3 _)
|
||||
{
|
||||
_originShiftEventOccurred = true;
|
||||
_textColor = Color.green;
|
||||
_blendTime = 0.0f;
|
||||
}
|
||||
|
||||
#endregion Origin Shift Events
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,33 @@
|
|||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace NAK.OriginShift.Utility;
|
||||
|
||||
/// <summary>
|
||||
/// Dumb little utility class to access the private staticBatchRootTransform property in the Renderer class.
|
||||
/// Using this we can move static batched objects with the scene! :)
|
||||
/// </summary>
|
||||
public static class RendererReflectionUtility
|
||||
{
|
||||
private static readonly PropertyInfo _staticBatchRootTransformProperty;
|
||||
|
||||
static RendererReflectionUtility()
|
||||
{
|
||||
Type rendererType = typeof(Renderer);
|
||||
_staticBatchRootTransformProperty = rendererType.GetProperty("staticBatchRootTransform", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (_staticBatchRootTransformProperty == null) OriginShiftMod.Logger.Error("Property staticBatchRootTransform not found in Renderer class.");
|
||||
}
|
||||
|
||||
public static void SetStaticBatchRootTransform(Renderer renderer, Transform newTransform)
|
||||
{
|
||||
if (_staticBatchRootTransformProperty != null)
|
||||
_staticBatchRootTransformProperty.SetValue(renderer, newTransform);
|
||||
}
|
||||
|
||||
public static Transform GetStaticBatchRootTransform(Renderer renderer)
|
||||
{
|
||||
if (_staticBatchRootTransformProperty != null)
|
||||
return (Transform)_staticBatchRootTransformProperty.GetValue(renderer);
|
||||
return null;
|
||||
}
|
||||
}
|
296
.Experimental/OriginShift/Patches.cs
Normal file
296
.Experimental/OriginShift/Patches.cs
Normal file
|
@ -0,0 +1,296 @@
|
|||
#if !UNITY_EDITOR
|
||||
using ABI_RC.Core.Base;
|
||||
using ABI_RC.Core.InteractionSystem;
|
||||
using ABI_RC.Core.IO;
|
||||
using ABI_RC.Core.Player;
|
||||
using ABI_RC.Core.Util;
|
||||
using ABI_RC.Systems.Camera;
|
||||
using ABI_RC.Systems.Communications.Networking;
|
||||
using ABI_RC.Systems.GameEventSystem;
|
||||
using ABI_RC.Systems.Movement;
|
||||
using ABI.CCK.Components;
|
||||
using DarkRift;
|
||||
using ECM2;
|
||||
using HarmonyLib;
|
||||
using NAK.OriginShift.Components;
|
||||
using NAK.OriginShift.Hacks;
|
||||
using UnityEngine;
|
||||
using Zettai;
|
||||
|
||||
namespace NAK.OriginShift.Patches;
|
||||
|
||||
internal static class BetterBetterCharacterControllerPatches
|
||||
{
|
||||
internal static bool PreventNextClearMovementParent;
|
||||
internal static bool PreventNextClearAccumulatedForces;
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(BetterBetterCharacterController), nameof(BetterBetterCharacterController.Start))]
|
||||
private static void Postfix_BetterCharacterController_Start(ref BetterBetterCharacterController __instance)
|
||||
{
|
||||
__instance.AddComponentIfMissing<OriginShiftMonitor>();
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(BetterBetterCharacterController), nameof(BetterBetterCharacterController.ClearLastMovementParent))]
|
||||
private static bool Prefix_BetterCharacterController_ClearLastMovementParent()
|
||||
{
|
||||
if (!PreventNextClearMovementParent)
|
||||
return true;
|
||||
|
||||
// skip this call if we are preventing it
|
||||
PreventNextClearMovementParent = false;
|
||||
Debug.Log("Prevented ClearLastMovementParent");
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(BetterBetterCharacterController), nameof(BetterBetterCharacterController.ClearAccumulatedForces))]
|
||||
private static bool Prefix_BetterCharacterController_ClearAccumulatedForces()
|
||||
{
|
||||
if (!PreventNextClearAccumulatedForces)
|
||||
return true;
|
||||
|
||||
// skip this call if we are preventing it
|
||||
PreventNextClearAccumulatedForces = false;
|
||||
Debug.Log("Prevented ClearAccumulatedForces");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class CVRSpawnablePatches
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(CVRSpawnable), nameof(CVRSpawnable.Start))]
|
||||
private static void Postfix_CVRSpawnable_Start(ref CVRSpawnable __instance)
|
||||
{
|
||||
__instance.AddComponentIfMissing<OriginShiftSpawnableReceiver>(); //todo: investigate if this is needed
|
||||
|
||||
// test adding to the wrapper of the spawnable
|
||||
Transform wrapper = __instance.transform.parent;
|
||||
wrapper.AddComponentIfMissing<OriginShiftTransformReceiver>();
|
||||
wrapper.AddComponentIfMissing<OriginShiftParticleSystemReceiver>();
|
||||
wrapper.AddComponentIfMissing<OriginShiftTrailRendererReceiver>();
|
||||
wrapper.AddComponentIfMissing<OriginShiftRigidbodyReceiver>();
|
||||
}
|
||||
|
||||
[HarmonyPrefix] // inbound spawnable
|
||||
[HarmonyPatch(typeof(CVRSpawnable), nameof(CVRSpawnable.UpdateFromNetwork))]
|
||||
private static void Prefix_CVRSpawnable_UpdateFromNetwork(ref CVRSyncHelper.PropData propData)
|
||||
{
|
||||
if (OriginShiftManager.CompatibilityMode) // adjust root position back to absolute world position
|
||||
{
|
||||
Vector3 position = new(propData.PositionX, propData.PositionY, propData.PositionZ); // imagine not using Vector3
|
||||
position = OriginShiftManager.GetLocalizedPosition(position);
|
||||
propData.PositionX = position.x;
|
||||
propData.PositionY = position.y;
|
||||
propData.PositionZ = position.z;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static class RCC_SkidmarksManagerPatches
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(RCC_SkidmarksManager), nameof(RCC_SkidmarksManager.Start))]
|
||||
private static void Postfix_RCC_SkidMarkManager_Start(ref RCC_SkidmarksManager __instance)
|
||||
{
|
||||
// todo: blacklist from OriginShiftController so there isn't 2x shift in some cases
|
||||
__instance.gameObject.AddComponentIfMissing<OriginShiftTransformReceiver>();
|
||||
}
|
||||
}
|
||||
|
||||
internal static class PlayerSetupPatches
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.Start))]
|
||||
private static void Postfix_PlayerSetup_Start(ref PlayerSetup __instance)
|
||||
{
|
||||
__instance.desktopCam.AddComponentIfMissing<OriginShiftOcclusionCullingDisabler>();
|
||||
__instance.vrCam.AddComponentIfMissing<OriginShiftOcclusionCullingDisabler>();
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.GetPlayerMovementData))]
|
||||
private static void Postfix_PlayerSetup_GetPlayerMovementData(ref PlayerAvatarMovementData __result)
|
||||
{
|
||||
if (OriginShiftManager.CompatibilityMode)
|
||||
{
|
||||
// adjust root position back to absolute world position
|
||||
__result.RootPosition = OriginShiftManager.GetAbsolutePosition(__result.RootPosition); // player root
|
||||
__result.BodyPosition = OriginShiftManager.GetAbsolutePosition(__result.BodyPosition); // player hips (pls fix, why in world space?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal static class PortableCameraPatches
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(PortableCamera), nameof(PortableCamera.Start))]
|
||||
private static void Postfix_PortableCamera_Start(ref PortableCamera __instance)
|
||||
{
|
||||
__instance.cameraComponent.AddComponentIfMissing<OriginShiftOcclusionCullingDisabler>();
|
||||
}
|
||||
}
|
||||
|
||||
internal static class PathingCameraPatches
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(CVRPathCamController), nameof(CVRPathCamController.Start))]
|
||||
private static void Postfix_CVRPathCamController_Start(ref CVRPathCamController __instance)
|
||||
{
|
||||
__instance.cam.AddComponentIfMissing<OriginShiftOcclusionCullingDisabler>();
|
||||
}
|
||||
}
|
||||
|
||||
internal static class Comms_ClientPatches
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(Comms_Client), nameof(Comms_Client.SetPosition))]
|
||||
private static void Prefix_Comms_Client_GetPlayerMovementData(ref Vector3 listenerPosition)
|
||||
{
|
||||
if (OriginShiftManager.CompatibilityMode) // adjust root position back to absolute world position
|
||||
listenerPosition = OriginShiftManager.GetAbsolutePosition(listenerPosition);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class CVRSyncHelperPatches
|
||||
{
|
||||
[HarmonyPrefix] // outbound spawnable
|
||||
[HarmonyPatch(typeof(CVRSyncHelper), nameof(CVRSyncHelper.UpdatePropValues))]
|
||||
private static void Prefix_CVRSyncHelper_UpdatePropValues(ref Vector3 position)
|
||||
{
|
||||
if (OriginShiftManager.CompatibilityMode) // adjust root position back to absolute world position
|
||||
position = OriginShiftManager.GetAbsolutePosition(position);
|
||||
}
|
||||
|
||||
[HarmonyPrefix] // outbound object sync
|
||||
[HarmonyPatch(typeof(CVRSyncHelper), nameof(CVRSyncHelper.MoveObject))]
|
||||
private static void Prefix_CVRSyncHelper_MoveObject(ref float PosX, ref float PosY, ref float PosZ)
|
||||
{
|
||||
if (OriginShiftManager.CompatibilityMode) // adjust root position back to absolute world position
|
||||
{
|
||||
Vector3 position = new(PosX, PosY, PosZ); // imagine not using Vector3
|
||||
position = OriginShiftManager.GetAbsolutePosition(position);
|
||||
PosX = position.x;
|
||||
PosY = position.y;
|
||||
PosZ = position.z;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPrefix] // outbound spawn prop
|
||||
[HarmonyPatch(typeof(CVRSyncHelper), nameof(CVRSyncHelper.SpawnProp))]
|
||||
private static void Prefix_CVRSyncHelper_SpawnProp(ref float posX, ref float posY, ref float posZ)
|
||||
{
|
||||
if (OriginShiftManager.CompatibilityMode) // adjust root position back to absolute world position
|
||||
{
|
||||
Vector3 position = new(posX, posY, posZ); // imagine not using Vector3
|
||||
position = OriginShiftManager.GetAbsolutePosition(position);
|
||||
posX = position.x;
|
||||
posY = position.y;
|
||||
posZ = position.z;
|
||||
}
|
||||
}
|
||||
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(CVRSyncHelper), nameof(CVRSyncHelper.SpawnPropFromNetwork))]
|
||||
private static void Postfix_CVRSyncHelper_SpawnPropFromNetwork(Message message)
|
||||
{
|
||||
if (!OriginShiftManager.CompatibilityMode)
|
||||
return;
|
||||
|
||||
using DarkRiftReader reader = message.GetReader();
|
||||
reader.ReadString(); // objectId, don't care
|
||||
|
||||
string instanceId = reader.ReadString();
|
||||
CVRSyncHelper.PropData propData = CVRSyncHelper.Props.Find((match) => match.InstanceId == instanceId);
|
||||
if (propData == null)
|
||||
return; // uh oh
|
||||
|
||||
Vector3 position = new(propData.PositionX, propData.PositionY, propData.PositionZ); // imagine not using Vector3
|
||||
position = OriginShiftManager.GetLocalizedPosition(position);
|
||||
propData.PositionX = position.x;
|
||||
propData.PositionY = position.y;
|
||||
propData.PositionZ = position.z;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class CVRObjectSyncPatches
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(CVRObjectSync), nameof(CVRObjectSync.Start))]
|
||||
private static void Postfix_CVRObjectSync_Start(ref CVRObjectSync __instance)
|
||||
{
|
||||
__instance.gameObject.AddComponentIfMissing<OriginShiftObjectSyncReceiver>(); // todo: investigate if this is needed
|
||||
}
|
||||
|
||||
[HarmonyPrefix] // inbound object sync
|
||||
[HarmonyPatch(typeof(CVRObjectSync), nameof(CVRObjectSync.receiveNetworkData))]
|
||||
[HarmonyPatch(typeof(CVRObjectSync), nameof(CVRObjectSync.receiveNetworkDataJoin))]
|
||||
private static void Prefix_CVRObjectSync_Update(ref Vector3 position)
|
||||
{
|
||||
if (OriginShiftManager.CompatibilityMode) // adjust root position back to localized world position
|
||||
position = OriginShiftManager.GetLocalizedPosition(position);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class PuppetMasterPatches
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(PuppetMaster), nameof(PuppetMaster.Start))]
|
||||
private static void Postfix_PuppetMaster_Start(ref PuppetMaster __instance)
|
||||
{
|
||||
__instance.gameObject.AddComponentIfMissing<OriginShiftNetIkReceiver>();
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(PuppetMaster), nameof(PuppetMaster.CycleData))]
|
||||
private static void Prefix_PuppetMaster_CycleData(ref PlayerAvatarMovementData ___PlayerAvatarMovementDataInput)
|
||||
{
|
||||
if (OriginShiftManager.CompatibilityMode) // && if user is not using OriginShift
|
||||
{
|
||||
// adjust root position back to absolute world position
|
||||
___PlayerAvatarMovementDataInput.RootPosition = OriginShiftManager.GetLocalizedPosition(___PlayerAvatarMovementDataInput.RootPosition); // player root
|
||||
___PlayerAvatarMovementDataInput.BodyPosition = OriginShiftManager.GetLocalizedPosition(___PlayerAvatarMovementDataInput.BodyPosition); // player hips (pls fix, why in world space?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//OriginShiftDbAvatarReceiver
|
||||
internal static class DbJobsAvatarManagerPatches
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(DbJobsAvatarManager), nameof(DbJobsAvatarManager.Awake))]
|
||||
private static void Postfix_DbJobsAvatarManager_Start(ref DbJobsAvatarManager __instance)
|
||||
{
|
||||
__instance.gameObject.AddComponentIfMissing<OriginShiftDbAvatarReceiver>();
|
||||
}
|
||||
}
|
||||
|
||||
// CVRPortalManager
|
||||
internal static class CVRPortalManagerPatches
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(CVRPortalManager), nameof(CVRPortalManager.Start))]
|
||||
private static void Postfix_CVRPortalManager_Start(ref CVRPortalManager __instance)
|
||||
{
|
||||
// parent portal to the object below it using a physics cast
|
||||
Transform portalTransform = __instance.transform;
|
||||
Vector3 origin = portalTransform.position;
|
||||
Vector3 direction = Vector3.down;
|
||||
if (Physics.Raycast(origin, direction, out RaycastHit hit, 0.5f))
|
||||
portalTransform.SetParent(hit.transform);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class CVRPickupObjectPatches
|
||||
{
|
||||
[HarmonyPostfix]
|
||||
[HarmonyPatch(typeof(CVRPickupObject), nameof(CVRPickupObject.Start))]
|
||||
private static void Postfix_CVRPickupObject_Start(ref CVRPickupObject __instance)
|
||||
{
|
||||
__instance.gameObject.AddComponentIfMissing<OriginShiftPickupObjectReceiver>();
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
34
.Experimental/OriginShift/Properties/AssemblyInfo.cs
Normal file
34
.Experimental/OriginShift/Properties/AssemblyInfo.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
#if !UNITY_EDITOR
|
||||
using NAK.OriginShift.Properties;
|
||||
using MelonLoader;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
|
||||
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
|
||||
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
|
||||
[assembly: AssemblyTitle(nameof(NAK.OriginShift))]
|
||||
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
|
||||
[assembly: AssemblyProduct(nameof(NAK.OriginShift))]
|
||||
|
||||
[assembly: MelonInfo(
|
||||
typeof(NAK.OriginShift.OriginShiftMod),
|
||||
nameof(NAK.OriginShift),
|
||||
AssemblyInfoParams.Version,
|
||||
AssemblyInfoParams.Author,
|
||||
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/OriginShift"
|
||||
)]
|
||||
|
||||
[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")]
|
||||
[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
|
||||
[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)]
|
||||
[assembly: MelonColor(255, 125, 126, 129)]
|
||||
[assembly: MelonAuthorColor(255, 158, 21, 32)]
|
||||
[assembly: HarmonyDontPatchAll]
|
||||
|
||||
namespace NAK.OriginShift.Properties;
|
||||
internal static class AssemblyInfoParams
|
||||
{
|
||||
public const string Version = "1.0.0";
|
||||
public const string Author = "NotAKidoS";
|
||||
}
|
||||
#endif
|
52
.Experimental/OriginShift/README.md
Normal file
52
.Experimental/OriginShift/README.md
Normal file
|
@ -0,0 +1,52 @@
|
|||
# OriginShift
|
||||
|
||||
Experimental mod that allows world origin to be shifted to prevent floating point precision issues.
|
||||
|
||||
## Compromises
|
||||
- Steam Audio data cannot be shifted.
|
||||
- NavMesh data cannot be shifted.
|
||||
- Light Probe data cannot be shifted (until [unity 2022](https://docs.unity3d.com/2022.3/Documentation/Manual/LightProbes-Moving.html)).
|
||||
- Occlusion Culling data cannot be shifted.
|
||||
- When using "Forced" mode, occlusion culling is disabled.
|
||||
- Only 10k trail positions can be shifted per Trail Renderer (artificial limit).
|
||||
- Only 10k particle positions can be shifted per Particle System (artificial limit).
|
||||
- Potentially can fix by changing Particle System to Custom Simulation Space ? (untested)
|
||||
- World Constraints are not shifted.
|
||||
|
||||
## Known Issues
|
||||
- Mod Network is not yet implemented, so Compatibility Mode is required to play with others.
|
||||
- Portable Camera drone mode is not yet offset by the world origin shift.
|
||||
- Chunk threshold past 10 units will break Voice Chat with remote players in some cases (without Compatibility Mode).
|
||||
- This is because the voice server dictates who can hear who based on distance from each other and the world origin shift messes with that.
|
||||
- Teleports past 50k units will not work.
|
||||
- BetterBetterCharacterController prevents teleports past 50k units.
|
||||
- Magica Cloth.
|
||||
|
||||
## Mod Incompatibilities
|
||||
- PlayerRagdollMod will freak out when you ragdoll between chunk boundaries.
|
||||
|
||||
## Provided Components
|
||||
- `OriginShiftController` - World script to configure origin shift.
|
||||
- `OriginShiftEventReceiver` - Event receiver for OriginShift events.
|
||||
- `OriginShiftTransformReceiver` - Shifts the transform of the GameObject it is attached to.
|
||||
- `OriginShiftRigidbodyReceiver` - Shifts the rigidbody of the GameObject it is attached to.
|
||||
- `OriginShiftTrailRendererReceiver` - Shifts the positions of the Trail Renderer of the GameObject it is attached to.
|
||||
- `OriginShiftParticleSystemReceiver` - Shifts the positions of the Particle System of the GameObject it is attached to.
|
||||
|
||||
The provided receiver components are automatically added to Props, Players, and Object Syncs.
|
||||
|
||||
## Provided Shader Globals
|
||||
- `_OriginShiftChunkOffset` - The current amount of chunks offset from the origin.
|
||||
- `_OriginShiftChunkThreshold` - The size of a chunk in world units.
|
||||
- `_OriginShiftChunkPosition` - The chunk offset multiplied by the chunk threshold.
|
||||
|
||||
---
|
||||
|
||||
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.
|
BIN
.Experimental/OriginShift/Resources/OriginShift-Icon-Active.png
Normal file
BIN
.Experimental/OriginShift/Resources/OriginShift-Icon-Active.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
.Experimental/OriginShift/Resources/OriginShift-Icon-Forced.png
Normal file
BIN
.Experimental/OriginShift/Resources/OriginShift-Icon-Forced.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
23
.Experimental/OriginShift/format.json
Normal file
23
.Experimental/OriginShift/format.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"_id": 211,
|
||||
"name": "RelativeSync",
|
||||
"modversion": "1.0.3",
|
||||
"gameversion": "2024r175",
|
||||
"loaderversion": "0.6.1",
|
||||
"modtype": "Mod",
|
||||
"author": "NotAKidoS",
|
||||
"description": "Relative sync for Movement Parent & Chairs. Requires both users to have the mod installed. Synced over Mod Network.\n\nProvides some Experimental settings to also fix local jitter on movement parents.",
|
||||
"searchtags": [
|
||||
"relative",
|
||||
"sync",
|
||||
"movement",
|
||||
"chair"
|
||||
],
|
||||
"requirements": [
|
||||
"None"
|
||||
],
|
||||
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r29/RelativeSync.dll",
|
||||
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/RelativeSync/",
|
||||
"changelog": "- Enabled the Experimental settings to fix **local** jitter on Movement Parents by default\n- Adjusted BBCC No Interpolation fix to account for potential native fix (now respects initial value)",
|
||||
"embedcolor": "#507e64"
|
||||
}
|
107
.Experimental/ScriptingSpoofer/Main.cs
Normal file
107
.Experimental/ScriptingSpoofer/Main.cs
Normal file
|
@ -0,0 +1,107 @@
|
|||
using System.Reflection;
|
||||
using ABI_RC.API;
|
||||
using ABI_RC.Core.Networking;
|
||||
using ABI_RC.Core.Networking.API.Responses;
|
||||
using ABI_RC.Systems.GameEventSystem;
|
||||
using HarmonyLib;
|
||||
using MelonLoader;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
namespace NAK.ScriptingSpoofer;
|
||||
|
||||
public class ScriptingSpoofer : MelonMod
|
||||
{
|
||||
private static readonly MelonPreferences_Category Category =
|
||||
MelonPreferences.CreateCategory(nameof(ScriptingSpoofer));
|
||||
|
||||
private static readonly MelonPreferences_Entry<bool> EntryEnabled =
|
||||
Category.CreateEntry("mod_enabled", true, "Enabled", description: "Toggle scripting spoofer.");
|
||||
|
||||
private static readonly MelonPreferences_Entry<bool> EntryCensorUsername =
|
||||
Category.CreateEntry("censor_username", true, "Censor Username", description: "Censor username. Toggle to randomize username instead.");
|
||||
|
||||
private static string spoofedUsername;
|
||||
private static string spoofedUserId;
|
||||
|
||||
private static readonly char[] CensorChars = {'!', '@', '#', '$', '%', '^', '&', '*'};
|
||||
|
||||
public override void OnInitializeMelon()
|
||||
{
|
||||
ApplyPatches(typeof(PlayerApiPatches));
|
||||
|
||||
// Regenerate spoofed data on login
|
||||
CVRGameEventSystem.Authentication.OnLogin.AddListener(GenerateRandomSpoofedData);
|
||||
}
|
||||
|
||||
private void ApplyPatches(Type type)
|
||||
{
|
||||
try
|
||||
{
|
||||
HarmonyInstance.PatchAll(type);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LoggerInstance.Msg($"Failed while patching {type.Name}!");
|
||||
LoggerInstance.Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GenerateRandomSpoofedData(UserAuthResponse _) // we get manually
|
||||
{
|
||||
spoofedUsername = EntryCensorUsername.Value ? GenerateCensoredUsername() : GenerateRandomUsername();
|
||||
spoofedUserId = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
private static string GenerateCensoredUsername()
|
||||
{
|
||||
var originalUsername = AuthManager.Username;
|
||||
var usernameArray = originalUsername.ToCharArray();
|
||||
|
||||
for (var i = 0; i < usernameArray.Length; i++)
|
||||
if (Random.value > 0.7f) usernameArray[i] = CensorChars[Random.Range(0, CensorChars.Length)];
|
||||
|
||||
var modifiedUsername = new string(usernameArray);
|
||||
string[] prefixes = { "xX", "_", Random.Range(10, 99).ToString() };
|
||||
string[] suffixes = { "Xx", "_", Random.Range(10, 99).ToString() };
|
||||
|
||||
var prefix = prefixes[Random.Range(0, prefixes.Length)];
|
||||
var suffix = suffixes[Random.Range(0, suffixes.Length)];
|
||||
|
||||
return prefix + modifiedUsername + suffix;
|
||||
}
|
||||
|
||||
private static string GenerateRandomUsername()
|
||||
{
|
||||
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
char[] username = new char[Random.Range(5, 15)];
|
||||
for (int i = 0; i < username.Length; i++)
|
||||
username[i] = chars[Random.Range(0, chars.Length)];
|
||||
|
||||
return new string(username);
|
||||
}
|
||||
|
||||
private static class PlayerApiPatches
|
||||
{
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(Player), nameof(Player.Username), MethodType.Getter)]
|
||||
private static bool GetSpoofedUsername(ref Player __instance, ref string __result)
|
||||
{
|
||||
if (__instance.IsRemote) return true;
|
||||
if (!EntryEnabled.Value) return true;
|
||||
|
||||
__result = spoofedUsername;
|
||||
return false;
|
||||
}
|
||||
|
||||
[HarmonyPrefix]
|
||||
[HarmonyPatch(typeof(Player), nameof(Player.UserID), MethodType.Getter)]
|
||||
private static bool GetSpoofedUserId(ref Player __instance, ref string __result)
|
||||
{
|
||||
if (__instance.IsRemote) return true;
|
||||
if (!EntryEnabled.Value) return true;
|
||||
|
||||
__result = spoofedUserId;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
32
.Experimental/ScriptingSpoofer/Properties/AssemblyInfo.cs
Normal file
32
.Experimental/ScriptingSpoofer/Properties/AssemblyInfo.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using MelonLoader;
|
||||
using NAK.ScriptingSpoofer.Properties;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
|
||||
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
|
||||
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
|
||||
[assembly: AssemblyTitle(nameof(NAK.ScriptingSpoofer))]
|
||||
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
|
||||
[assembly: AssemblyProduct(nameof(NAK.ScriptingSpoofer))]
|
||||
|
||||
[assembly: MelonInfo(
|
||||
typeof(NAK.ScriptingSpoofer.ScriptingSpoofer),
|
||||
nameof(NAK.ScriptingSpoofer),
|
||||
AssemblyInfoParams.Version,
|
||||
AssemblyInfoParams.Author,
|
||||
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/ScriptingSpoofer"
|
||||
)]
|
||||
|
||||
[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.ScriptingSpoofer.Properties;
|
||||
internal static class AssemblyInfoParams
|
||||
{
|
||||
public const string Version = "1.0.1";
|
||||
public const string Author = "NotAKidoS";
|
||||
}
|
21
.Experimental/ScriptingSpoofer/README.md
Normal file
21
.Experimental/ScriptingSpoofer/README.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
# ScriptingSpoofer
|
||||
|
||||
Prevents **local** scripts from accessing your Username or UserID by spoofing them with random values each session.
|
||||
|
||||
This mod is made to prevent malicious scripts executing on your local client from accessing your Username or UserID to target you with. **This mod can not prevent scripts executing on remote clients from accessing your Username or UserID.**
|
||||
|
||||
Example: Prevents a **blacklist** script from targeting you. **Whitelist** scripts are not affected by this mod, only **blacklists**, as the spoofed info is not exposed for customization.
|
||||
|
||||
## Mod Settings
|
||||
- **Censor Username**: Censors existing username by replacing and appending random characters to it. Disable to generate a completely random username.
|
||||
|
||||
---
|
||||
|
||||
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.
|
2
.Experimental/ScriptingSpoofer/ScriptingSpoofer.csproj
Normal file
2
.Experimental/ScriptingSpoofer/ScriptingSpoofer.csproj
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk" />
|
23
.Experimental/ScriptingSpoofer/format.json
Normal file
23
.Experimental/ScriptingSpoofer/format.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"_id": -1,
|
||||
"name": "ScriptingSpoofer",
|
||||
"modversion": "1.0.1",
|
||||
"gameversion": "2024r176",
|
||||
"loaderversion": "0.6.1",
|
||||
"modtype": "Mod",
|
||||
"author": "NotAKidoS",
|
||||
"description": "Prevents **local** scripts from accessing your Username or UserID by spoofing them with random values each session.",
|
||||
"searchtags": [
|
||||
"spoof",
|
||||
"username",
|
||||
"whitelist",
|
||||
"blacklist"
|
||||
],
|
||||
"requirements": [
|
||||
"None"
|
||||
],
|
||||
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r25/ScriptingSpoofer.dll",
|
||||
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/ScriptingSpoofer/",
|
||||
"changelog": "- Initial release",
|
||||
"embedcolor": "#f61963"
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue