From 2c029b1452a6b47db8fdde8dbee144ddfb3d720f Mon Sep 17 00:00:00 2001 From: NotAKidoS <37721153+NotAKidOnSteam@users.noreply.github.com> Date: Wed, 12 Apr 2023 03:06:46 -0500 Subject: [PATCH] add redo button --- PropUndoButton/Main.cs | 141 ++++++++++++++++++++-- PropUndoButton/PropUndoButton.csproj | 5 + PropUndoButton/Properties/AssemblyInfo.cs | 3 +- PropUndoButton/SFX/sfx_redo.wav | Bin 0 -> 14464 bytes PropUndoButton/format.json | 6 +- 5 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 PropUndoButton/SFX/sfx_redo.wav diff --git a/PropUndoButton/Main.cs b/PropUndoButton/Main.cs index 105616a..7dc2791 100644 --- a/PropUndoButton/Main.cs +++ b/PropUndoButton/Main.cs @@ -1,6 +1,9 @@ using ABI_RC.Core.AudioEffects; +using ABI_RC.Core.Networking; using ABI_RC.Core.Savior; +using ABI_RC.Core.UI; using ABI_RC.Core.Util; +using DarkRift; using MelonLoader; using System.Reflection; using UnityEngine; @@ -11,6 +14,8 @@ namespace NAK.Melons.PropUndoButton; public class PropUndoButton : MelonMod { + public static List deletedProps = new List(); + private static readonly MelonPreferences_Category Category = MelonPreferences.CreateCategory(nameof(PropUndoButton)); @@ -23,19 +28,22 @@ public class PropUndoButton : MelonMod // audio clip names, InterfaceAudio adds "PropUndo_" prefix public const string sfx_spawn = "PropUndo_sfx_spawn"; public const string sfx_undo = "PropUndo_sfx_undo"; + public const string sfx_redo = "PropUndo_sfx_redo"; public const string sfx_warn = "PropUndo_sfx_warn"; public override void OnInitializeMelon() { HarmonyInstance.Patch( // prop spawn sfx typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.SpawnProp)), - null, - new HarmonyLib.HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnSpawnProp), BindingFlags.NonPublic | BindingFlags.Static)) + postfix: new HarmonyLib.HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnSpawnProp), BindingFlags.NonPublic | BindingFlags.Static)) + ); + HarmonyInstance.Patch( // prop delete sfx + typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.DeletePropByInstanceId)), + postfix: new HarmonyLib.HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnDeletePropByInstanceId), BindingFlags.NonPublic | BindingFlags.Static)) ); HarmonyInstance.Patch( // desktop input patch so we don't run in menus/gui typeof(InputModuleMouseKeyboard).GetMethod(nameof(InputModuleMouseKeyboard.UpdateInput)), - null, - new HarmonyLib.HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnUpdateInput), BindingFlags.NonPublic | BindingFlags.Static)) + postfix: new HarmonyLib.HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnUpdateInput), BindingFlags.NonPublic | BindingFlags.Static)) ); SetupDefaultAudioClips(); } @@ -51,7 +59,7 @@ public class PropUndoButton : MelonMod } // copy embedded resources to this folder if they do not exist - string[] clipNames = { "sfx_spawn.wav", "sfx_undo.wav", "sfx_warn.wav" }; + string[] clipNames = { "sfx_spawn.wav", "sfx_undo.wav", "sfx_redo.wav", "sfx_warn.wav" }; foreach (string clipName in clipNames) { string clipPath = Path.Combine(path, clipName); @@ -79,9 +87,16 @@ public class PropUndoButton : MelonMod private static void OnUpdateInput() { - if (Input.GetKeyDown(KeyCode.Z)) + if (Input.GetKey(KeyCode.LeftControl)) { - DeleteLatestSpawnable(); + if (Input.GetKey(KeyCode.LeftShift) && Input.GetKeyDown(KeyCode.Z)) + { + RedoProp(); + } + else if (Input.GetKeyDown(KeyCode.Z)) + { + UndoProp(); + } } } @@ -95,7 +110,8 @@ public class PropUndoButton : MelonMod return; } - if (GetAllProps().Count >= 20) + // might need to touch + if (IsAtPropLimit()) { PlayAudioModule(sfx_warn); return; @@ -104,12 +120,26 @@ public class PropUndoButton : MelonMod PlayAudioModule(sfx_spawn); } - private static void DeleteLatestSpawnable() + private static void OnDeletePropByInstanceId(string instanceId) + { + if (string.IsNullOrEmpty(instanceId)) return; + + CVRSyncHelper.PropData propData = GetPropByInstanceId(instanceId); + if (propData == null) return; + + // Add the spawned prop to the history of deleted props + if (deletedProps.Count >= 5) deletedProps.RemoveAt(0); // Remove the oldest item + DeletedProp deletedProp = new DeletedProp(propData.ObjectId, propData.Spawnable.transform.position, propData.Spawnable.transform.rotation); + deletedProps.Add(deletedProp); + + PlayAudioModule(sfx_undo); + } + + private static void UndoProp() { if (!EntryEnabled.Value) return; var propData = GetLatestProp(); - if (propData == null) { PlayAudioModule(sfx_warn); @@ -124,15 +154,86 @@ public class PropUndoButton : MelonMod } propData.Spawnable.Delete(); - PlayAudioModule(sfx_undo); } - private static void PlayAudioModule(string module) + public static void RedoProp() + { + int index = deletedProps.Count - 1; + if (index < 0 || index >= deletedProps.Count || IsAtPropLimit()) + { + PlayAudioModule(sfx_warn); + return; + } + + DeletedProp deletedProp = deletedProps[index]; + if (Time.time - deletedProp.timeDeleted <= 60) // only allow redo of prop spawned in last minute + { + if (AttemptRedoProp(deletedProp.propGuid, deletedProp.position, deletedProp.rotation)) + { + deletedProps.RemoveAt(index); + PlayAudioModule(sfx_redo); + return; + } + } + + PlayAudioModule(sfx_warn); + } + + // original spawn prop method does not let you specify rotation + public static bool AttemptRedoProp(string propGuid, Vector3 position, Quaternion quaternion) + { + if (MetaPort.Instance.worldAllowProps + && MetaPort.Instance.settings.GetSettingsBool("ContentFilterPropsEnabled", false) + && NetworkManager.Instance.GameNetwork.ConnectionState == ConnectionState.Connected) + { + using (DarkRiftWriter darkRiftWriter = DarkRiftWriter.Create()) + { + darkRiftWriter.Write(propGuid); + darkRiftWriter.Write(position.x); + darkRiftWriter.Write(position.y); + darkRiftWriter.Write(position.z); + darkRiftWriter.Write(0f); + darkRiftWriter.Write(quaternion.eulerAngles.y); + darkRiftWriter.Write(0f); + darkRiftWriter.Write(1f); + darkRiftWriter.Write(1f); + darkRiftWriter.Write(1f); + darkRiftWriter.Write(0f); + using (Message message = Message.Create(10050, darkRiftWriter)) + { + NetworkManager.Instance.GameNetwork.SendMessage(message, SendMode.Reliable); + } + return true; + } + } + else + { + if (!MetaPort.Instance.worldAllowProps) + { + CohtmlHud.Instance.ViewDropText("Props are not allowed in this world", ""); + } + return false; + } + } + + public static void PlayAudioModule(string module) { if (!EntryUseSFX.Value) return; InterfaceAudio.PlayModule(module); } + public static bool IsAtPropLimit() + { + // might need rework + return GetAllProps().Count >= 20; + } + + private static CVRSyncHelper.PropData GetPropByInstanceId(string instanceId) + { + // find prop by instance id and if it is ours + return CVRSyncHelper.Props.Find((CVRSyncHelper.PropData match) => (match.InstanceId == instanceId && match.SpawnedBy == MetaPort.Instance.ownerId)); + } + private static CVRSyncHelper.PropData GetLatestProp() { // should already be sorted by spawn order @@ -144,4 +245,20 @@ public class PropUndoButton : MelonMod // im not storing the count because there is good chance itll desync from server return CVRSyncHelper.Props.FindAll((CVRSyncHelper.PropData match) => match.SpawnedBy == MetaPort.Instance.ownerId); } + + public class DeletedProp + { + public string propGuid; + public Vector3 position; + public Quaternion rotation; + public float timeDeleted; + + public DeletedProp(string propGuid, Vector3 position, Quaternion rotation) + { + this.propGuid = propGuid; + this.position = position; + this.rotation = rotation; + this.timeDeleted = Time.time; + } + } } \ No newline at end of file diff --git a/PropUndoButton/PropUndoButton.csproj b/PropUndoButton/PropUndoButton.csproj index fda9805..e6a31c9 100644 --- a/PropUndoButton/PropUndoButton.csproj +++ b/PropUndoButton/PropUndoButton.csproj @@ -10,12 +10,14 @@ + + @@ -31,6 +33,9 @@ C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\Assembly-CSharp-firstpass.dll + + ..\..\..\_ManagedLibs\DarkRift.dll + C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\MelonLoader\MelonLoader.dll diff --git a/PropUndoButton/Properties/AssemblyInfo.cs b/PropUndoButton/Properties/AssemblyInfo.cs index fc06330..a20c993 100644 --- a/PropUndoButton/Properties/AssemblyInfo.cs +++ b/PropUndoButton/Properties/AssemblyInfo.cs @@ -20,12 +20,11 @@ using System.Reflection; [assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")] [assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] [assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)] -[assembly: MelonOptionalDependencies("BTKUILib")] [assembly: HarmonyDontPatchAll] namespace NAK.Melons.PropUndoButton.Properties; internal static class AssemblyInfoParams { - public const string Version = "1.0.0"; + public const string Version = "1.0.1"; public const string Author = "NotAKidoS"; } \ No newline at end of file diff --git a/PropUndoButton/SFX/sfx_redo.wav b/PropUndoButton/SFX/sfx_redo.wav new file mode 100644 index 0000000000000000000000000000000000000000..cdc632a44ff1576ad9139ea8baaf87bef6b47ab7 GIT binary patch literal 14464 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