diff --git a/PropsButBetter/Main.cs b/PropsButBetter/Main.cs deleted file mode 100644 index 2778b33..0000000 --- a/PropsButBetter/Main.cs +++ /dev/null @@ -1,287 +0,0 @@ -using System.Reflection; -using ABI_RC.Core.InteractionSystem; -using ABI_RC.Core.Player; -using ABI_RC.Core.Util; -using ABI_RC.Systems.InputManagement.InputModules; -using ABI_RC.Systems.UI.UILib; -using ABI.CCK.Components; -using HarmonyLib; -using MelonLoader; -using UnityEngine; - -namespace NAK.PropsButBetter; - -public class PropsButBetterMod : MelonMod -{ - internal static MelonLogger.Instance Logger; - - public override void OnInitializeMelon() - { - Logger = LoggerInstance; - - // Replace prop select method - HarmonyInstance.Patch( - typeof(ControllerRay).GetMethod(nameof(ControllerRay.HandleSpawnableClicked), - BindingFlags.NonPublic | BindingFlags.Instance), - prefix: new HarmonyMethod(typeof(PropsButBetterMod).GetMethod(nameof(OnPreHandleSpawnableClicked), - BindingFlags.NonPublic | BindingFlags.Static)) - ); - // Replace player select method - HarmonyInstance.Patch( - typeof(ControllerRay).GetMethod(nameof(ControllerRay.HandlePlayerClicked), - BindingFlags.NonPublic | BindingFlags.Instance), - prefix: new HarmonyMethod(typeof(PropsButBetterMod).GetMethod(nameof(OnPreHandlePlayerClicked), - BindingFlags.NonPublic | BindingFlags.Static)) - ); - - // Desktop keybindings for undo/redo - HarmonyInstance.Patch( - typeof(CVRInputModule_Keyboard).GetMethod(nameof(CVRInputModule_Keyboard.Update_Binds), - BindingFlags.Public | BindingFlags.Instance), - postfix: new HarmonyMethod(typeof(PropsButBetterMod).GetMethod(nameof(OnUpdateKeyboardBinds), - BindingFlags.NonPublic | BindingFlags.Static)) - ); - - HarmonyInstance.Patch( // delete my props in reverse order for redo - typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.DeleteMyProps), - BindingFlags.Public | BindingFlags.Static), - prefix: new HarmonyMethod(typeof(PropHelper).GetMethod(nameof(PropHelper.OnPreDeleteMyProps), - BindingFlags.Public | BindingFlags.Static)) - ); - HarmonyInstance.Patch( // delete all props in reverse order for redo - typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.DeleteAllProps), - BindingFlags.Public | BindingFlags.Static), - prefix: new HarmonyMethod(typeof(PropHelper).GetMethod(nameof(PropHelper.OnPreDeleteAllProps), - BindingFlags.Public | BindingFlags.Static)) - ); - HarmonyInstance.Patch( // prop spawn sfx - typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.SpawnProp), - BindingFlags.Public | BindingFlags.Static), - postfix: new HarmonyMethod(typeof(PropHelper).GetMethod(nameof(PropHelper.OnTrySpawnProp), - BindingFlags.Public | BindingFlags.Static)) - ); - - HarmonyInstance.Patch( - typeof(PlayerSetup).GetMethod(nameof(PlayerSetup.SelectPropToSpawn), - BindingFlags.Public | BindingFlags.Instance), - postfix: new HarmonyMethod(typeof(PropHelper).GetMethod(nameof(PropHelper.OnSelectPropToSpawn), - BindingFlags.Public | BindingFlags.Static)) - ); - - HarmonyInstance.Patch( - typeof(PlayerSetup).GetMethod(nameof(PlayerSetup.EnterPropDeleteMode), - BindingFlags.Public | BindingFlags.Instance), - postfix: new HarmonyMethod(typeof(PropHelper).GetMethod(nameof(PropHelper.OnClearPropToSpawn), - BindingFlags.Public | BindingFlags.Static)) - ); - - HarmonyInstance.Patch( - typeof(PlayerSetup).GetMethod(nameof(PlayerSetup.ClearPropToSpawn), - BindingFlags.Public | BindingFlags.Instance), - postfix: new HarmonyMethod(typeof(PropHelper).GetMethod(nameof(PropHelper.OnClearPropToSpawn), - BindingFlags.Public | BindingFlags.Static)) - ); - - HarmonyInstance.Patch( - typeof(ControllerRay).GetMethod(nameof(ControllerRay.HandlePropSpawn), - BindingFlags.NonPublic | BindingFlags.Instance), - postfix: new HarmonyMethod(typeof(PropHelper).GetMethod(nameof(PropHelper.OnHandlePropSpawn), - BindingFlags.Public | BindingFlags.Static)) - ); - - UILibHelper.LoadIcons(); - QuickMenuPropList.BuildUI(); - QuickMenuPropSelect.BuildUI(); - PropHelper.Initialize(); - - // Bono approved hack - QuickMenuAPI.InjectCSSStyle(""" - .shit { - width: 100%; - height: 100%; - object-fit: cover; - border-radius: 10px; - display: block; - position: absolute; - top: 3px; - left: 3px; - } - - .shit2 { - opacity: 0; - transition: opacity 0.3s ease-in-out; - } - - """); - - // Build ui once quick menu is loaded - QuickMenuAPI.OnMenuGenerated += _ => - { - QuickMenuPropSelect.ListenForQM(); - PropListEntry.ListenForQM(); - GlobalButton.ListenForQM(); - UndoRedoButtons.ListenForButtons(); - }; - - SetupDefaultAudioClips(); - } - - /*public override void OnUpdate() - { - PropDistanceHider.Tick(); - }*/ - - // ReSharper disable once RedundantAssignment - private static bool OnPreHandleSpawnableClicked(ref ControllerRay __instance, ref CVRSpawnable __result) - { - __result = null; - - CVRSpawnable spawnable = __instance.hitTransform.GetComponentInParent(); - if (spawnable == null) return false; - - PlayerSetup.PropSelectionMode selectionMode = PlayerSetup.Instance.GetCurrentPropSelectionMode(); - switch (selectionMode) - { - case PlayerSetup.PropSelectionMode.None: - { - // Click a prop while a menu is open to open the details menu - if (__instance._interactDown - && __instance.CanSelectPlayersAndProps() - && spawnable.TryGetComponent(out CVRAssetInfo assetInfo)) - { - // Direct to Main Menu if it is open - if (ViewManager.Instance.IsViewShown) - { - ViewManager.Instance.GetPropDetails(assetInfo.objectId); - ViewManager.Instance.UiStateToggle(true); - } - // Direct to Quick Menu by default - else - { - QuickMenuPropSelect.ShowInfo(spawnable.PropData); - } - - __instance._interactDown = false; // Consume the click - } - break; - } - case PlayerSetup.PropSelectionMode.Delete: - { - // Click a prop while in delete mode to delete it - if (__instance._interactDown - && !spawnable.IsSpawnedByAdmin()) - { - spawnable.Delete(); - __instance._interactDown = false; // Consume the click - return false; // Don't return the spawnable, it's been deleted - } - break; - } - } - - // Return normal prop hover - __result = spawnable; - return false; - } - - private static bool OnPreHandlePlayerClicked(ref ControllerRay __instance, ref PlayerBase __result) - { - if (!ModSettings.EntryFixPlayerSelectRedirect.Value) - return true; - - __result = null; - - if (PlayerSetup.Instance.GetCurrentPropSelectionMode() - != PlayerSetup.PropSelectionMode.None) - return false; - - PlayerBase playerBase = null; - if (__instance.CanSelectPlayersAndProps()) - { - bool foundPlayerDescriptor = __instance.hitTransform.TryGetComponent(out playerBase); - if (!foundPlayerDescriptor && __instance.hitTransform.TryGetComponent(out CameraIndicator cameraIndicator)) - { - PlayerBase cameraOwner = cameraIndicator.ownerPlayer; - if (!playerBase.IsLocalPlayer) - { - playerBase = cameraOwner; - foundPlayerDescriptor = true; - } - } - - if (foundPlayerDescriptor && __instance._interactDown) - { - // Direct to Main Menu if it is open - if (ViewManager.Instance.IsViewShown) - { - ViewManager.Instance.RequestUserDetailsPage(playerBase.PlayerId); - ViewManager.Instance.UiStateToggle(true); - } - // Direct to Quick Menu by default - else - { - QuickMenuAPI.OpenPlayerListByUserID(playerBase.PlayerId); - CVR_MenuManager.Instance.ToggleQuickMenu(true); - } - } - } - - __result = playerBase; - return false; - } - - private static void OnUpdateKeyboardBinds() - { - if (!ModSettings.EntryUseUndoRedoKeybinds.Value) - return; - - if (Input.GetKey(KeyCode.LeftControl)) - { - if (Input.GetKey(KeyCode.LeftShift) - && Input.GetKeyDown(KeyCode.Z)) - PropHelper.RedoProp(); - else if (Input.GetKeyDown(KeyCode.Z)) - PropHelper.UndoProp(); - } - } - - private void SetupDefaultAudioClips() - { - // PropUndo and audio folders do not exist, create them if dont exist yet - var path = Application.streamingAssetsPath + $"/Cohtml/UIResources/GameUI/mods/{nameof(PropsButBetter)}/audio/"; - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - LoggerInstance.Msg("Created audio directory!"); - } - - // copy embedded resources to this folder if they do not exist - string[] clipNames = { "sfx_spawn.wav", "sfx_undo.wav", "sfx_redo.wav", "sfx_warn.wav", "sfx_deny.wav" }; - Assembly executingAssembly = Assembly.GetExecutingAssembly(); - - foreach (var clipName in clipNames) - { - var clipPath = Path.Combine(path, clipName); - if (!File.Exists(clipPath)) - { - // read the clip data from embedded resources - byte[] clipData; - var resourceName = $"{nameof(PropsButBetter)}.Resources.SFX." + clipName; - using (Stream stream = executingAssembly.GetManifestResourceStream(resourceName)) - { - clipData = new byte[stream!.Length]; - // ReSharper disable once MustUseReturnValue - stream.Read(clipData, 0, clipData.Length); - } - - // write the clip data to the file - using (FileStream fileStream = new(clipPath, FileMode.CreateNew)) - { - fileStream.Write(clipData, 0, clipData.Length); - } - - LoggerInstance.Msg("Placed missing sfx in audio folder: " + clipName); - } - } - } -} \ No newline at end of file diff --git a/PropsButBetter/ModSettings.cs b/PropsButBetter/ModSettings.cs deleted file mode 100644 index 224677e..0000000 --- a/PropsButBetter/ModSettings.cs +++ /dev/null @@ -1,35 +0,0 @@ -using MelonLoader; - -namespace NAK.PropsButBetter; - -public static class ModSettings -{ - public const string ModName = nameof(PropsButBetter); - - private static readonly MelonPreferences_Category Category = - MelonPreferences.CreateCategory(ModName); - - internal static readonly MelonPreferences_Entry EntryUseUndoRedoKeybinds = - Category.CreateEntry("use_undo_redo_keybinds", true, - "Use Undo/Redo Keybinds", description: "Whether to use the Desktop keybinds to undo/redo Props (CTRL+Z, CTRL+SHIFT+Z)."); - - internal static readonly MelonPreferences_Entry EntryUseSFX = - Category.CreateEntry("use_sfx", true, - "Use SFX", description: "Toggle audio queues for prop spawn, undo, redo, and warning."); - - internal static readonly MelonPreferences_Entry HiddenPropListMode = - Category.CreateEntry("prop_list_mode", QuickMenuPropList.PropListMode.AllProps, - "Prop List Mode", description: "The current prop list mode.", is_hidden: true); - - internal static readonly MelonPreferences_Entry EntryFixPlayerSelectRedirect = - Category.CreateEntry("fix_player_select_redirect", true, - "Fix Player Select Redirect", description: "Whether to fix the player select redirect. Enabling this makes it consistent with Prop Select."); - - internal static readonly MelonPreferences_Entry EntryPropSpawnVisualizer = - Category.CreateEntry("prop_spawn_visualizer", true, - "Prop Spawn Visualizer", description: "Use the experimental probably fucked up prop spawn visualizer."); - - internal static readonly MelonPreferences_Entry EntryPropSpawnVisualizerMode = - Category.CreateEntry("prop_spawn_visualizer_mode", PropHelper.PropVisualizerMode.HologramSource, - "Prop Spawn Visualizer Mode", description: "Choose how the visualizer will attempt to render."); -} \ No newline at end of file diff --git a/PropsButBetter/Properties/AssemblyInfo.cs b/PropsButBetter/Properties/AssemblyInfo.cs deleted file mode 100644 index 151db86..0000000 --- a/PropsButBetter/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,32 +0,0 @@ -using NAK.PropsButBetter.Properties; -using MelonLoader; -using System.Reflection; - -[assembly: AssemblyVersion(AssemblyInfoParams.Version)] -[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)] -[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)] -[assembly: AssemblyTitle(nameof(NAK.PropsButBetter))] -[assembly: AssemblyCompany(AssemblyInfoParams.Author)] -[assembly: AssemblyProduct(nameof(NAK.PropsButBetter))] - -[assembly: MelonInfo( - typeof(NAK.PropsButBetter.PropsButBetterMod), - nameof(NAK.PropsButBetter), - AssemblyInfoParams.Version, - AssemblyInfoParams.Author, - downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/PropsButBetter" -)] - -[assembly: MelonGame("ChilloutVR", "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.PropsButBetter.Properties; -internal static class AssemblyInfoParams -{ - public const string Version = "1.0.0"; - public const string Author = "NotAKidoS"; -} \ No newline at end of file diff --git a/PropsButBetter/PropsButBetter.csproj b/PropsButBetter/PropsButBetter.csproj deleted file mode 100644 index f3feb00..0000000 --- a/PropsButBetter/PropsButBetter.csproj +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/PropsButBetter/PropsButBetter/API/PedestalInfoBatchProcessor.cs b/PropsButBetter/PropsButBetter/API/PedestalInfoBatchProcessor.cs deleted file mode 100644 index b43660d..0000000 --- a/PropsButBetter/PropsButBetter/API/PedestalInfoBatchProcessor.cs +++ /dev/null @@ -1,135 +0,0 @@ -using ABI_RC.Core.Networking.API; -using ABI_RC.Core.Networking.API.Responses; - -namespace NAK.PropsButBetter; - -public enum PedestalType -{ - Avatar, - Prop -} - -public static class PedestalInfoBatchProcessor -{ - private class CachedResponse - { - public PedestalInfoResponse Response; - public DateTime CachedAt; - } - - private static readonly Dictionary> _cache = new() - { - { PedestalType.Avatar, new Dictionary() }, - { PedestalType.Prop, new Dictionary() } - }; - - private static readonly Dictionary>> _pendingRequests = new() - { - { PedestalType.Avatar, new Dictionary>() }, - { PedestalType.Prop, new Dictionary>() } - }; - - private static readonly Dictionary _isBatchProcessing = new() - { - { PedestalType.Avatar, false }, - { PedestalType.Prop, false } - }; - - private static readonly object _lock = new(); - private const float BATCH_DELAY = 2f; - private const float CACHE_DURATION = 300f; // 5 minutes - - public static Task QueuePedestalInfoRequest(PedestalType type, string contentId, bool skipDelayIfNotCached = false) - { - lock (_lock) - { - // Check cache first - if (_cache[type].TryGetValue(contentId, out var cached)) - { - if ((DateTime.UtcNow - cached.CachedAt).TotalSeconds < CACHE_DURATION) - return Task.FromResult(cached.Response); - - _cache[type].Remove(contentId); - } - - // Check if already pending - var requests = _pendingRequests[type]; - if (requests.TryGetValue(contentId, out var existingTcs)) - return existingTcs.Task; - - // Queue new request - var tcs = new TaskCompletionSource(); - requests[contentId] = tcs; - - if (!_isBatchProcessing[type]) - { - _isBatchProcessing[type] = true; - ProcessBatchAfterDelay(type, skipDelayIfNotCached); - } - - return tcs.Task; - } - } - - private static async void ProcessBatchAfterDelay(PedestalType type, bool skipDelayIfNotCached) - { - if (!skipDelayIfNotCached) - await Task.Delay(TimeSpan.FromSeconds(BATCH_DELAY)); - - List contentIds; - Dictionary> requestBatch; - - lock (_lock) - { - contentIds = _pendingRequests[type].Keys.ToList(); - requestBatch = new Dictionary>(_pendingRequests[type]); - _pendingRequests[type].Clear(); - _isBatchProcessing[type] = false; - } - - try - { - ApiConnection.ApiOperation operation = type switch - { - PedestalType.Avatar => ApiConnection.ApiOperation.AvatarPedestal, - PedestalType.Prop => ApiConnection.ApiOperation.PropPedestal, - _ => throw new ArgumentException($"Unsupported pedestal type: {type}") - }; - - var response = await ApiConnection.MakeRequest>(operation, contentIds); - - if (response?.Data != null) - { - var responseDict = response.Data.ToDictionary(info => info.Id); - var now = DateTime.UtcNow; - - lock (_lock) - { - foreach (var kvp in requestBatch) - { - if (responseDict.TryGetValue(kvp.Key, out var info)) - { - _cache[type][kvp.Key] = new CachedResponse { Response = info, CachedAt = now }; - kvp.Value.SetResult(info); - } - else - { - kvp.Value.SetException(new Exception($"Content info not found for ID: {kvp.Key}")); - } - } - } - } - else - { - Exception exception = new($"Failed to fetch {type} info batch"); - foreach (var tcs in requestBatch.Values) - tcs.SetException(exception); - } - } - catch (Exception ex) - { - foreach (var tcs in requestBatch.Values) - tcs.SetException(ex); - } - } -} \ No newline at end of file diff --git a/PropsButBetter/PropsButBetter/API/SpawnablesMeta.cs b/PropsButBetter/PropsButBetter/API/SpawnablesMeta.cs deleted file mode 100644 index ed6e3e0..0000000 --- a/PropsButBetter/PropsButBetter/API/SpawnablesMeta.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.Net; -using ABI_RC.Core; -using ABI_RC.Core.Networking; -using ABI_RC.Core.Networking.API; -using ABI_RC.Core.Networking.API.Responses; -using ABI_RC.Core.Savior; -using NAK.PropsButBetter; -using Newtonsoft.Json; - -public static class PropApiHelper -{ - /// - /// Fetches the metadata for a specific prop/spawnable - /// - /// The ID of the prop to fetch metadata for - /// BaseResponse containing UgcWithFile data - public static async Task> GetPropMeta(string propId) - { - // Check authentication - if (!AuthManager.IsAuthenticated) - { - PropsButBetterMod.Logger.Error("Attempted to fetch prop meta while user was not authenticated."); - return new BaseResponse("The user is not Authenticated.") - { - IsSuccessStatusCode = false, - HttpStatusCode = HttpStatusCode.Unauthorized, - }; - } - - // Construct the URL - string requestUrl = $"{ApiConnection.APIAddress}/{ApiConnection.APIVersion}/spawnables/{propId}/meta"; - - // Validate URL for security - if (!CVRTools.IsSafeAbsoluteHttpUrl(requestUrl, out var uri)) - { - PropsButBetterMod.Logger.Error($"Invalid URI was constructed! URI: {requestUrl}"); - return null; - } - - // Create the HTTP request - var request = new HttpRequestMessage(HttpMethod.Get, uri); - - // Add mature content header - string allowMatureContent = MetaPort.Instance.matureContentAllowed ? "true" : "false"; - request.Headers.Add(ApiConnection.HeaderMatureContent, allowMatureContent); - - try - { - // Send the request - HttpResponseMessage response = await ApiConnection.Client.SendAsync(request); - - // Handle successful response - if (response.IsSuccessStatusCode) - { - var contentStr = await response.Content.ReadAsStringAsync(); - - var baseResponse = string.IsNullOrWhiteSpace(contentStr) - ? new BaseResponse { Message = "CVR_SUCCESS_PropMeta" } - : JsonConvert.DeserializeObject>(contentStr); - - baseResponse.HttpStatusCode = response.StatusCode; - baseResponse.IsSuccessStatusCode = true; - - return baseResponse; - } - - // Handle failed response - PropsButBetterMod.Logger.Warning($"Request failed with status {response.StatusCode} [{(int)response.StatusCode}]"); - - string rawResponseBody = await response.Content.ReadAsStringAsync(); - var res = JsonConvert.DeserializeObject>(rawResponseBody); - - if (res != null) - { - res.HttpStatusCode = response.StatusCode; - res.IsSuccessStatusCode = false; - return res; - } - } - catch (Exception e) - { - PropsButBetterMod.Logger.Error($"Failed to fetch prop meta: {e.Message}"); - } - - return null; - } -} \ No newline at end of file diff --git a/PropsButBetter/PropsButBetter/Extensions/CVRSpawnableExtensions.cs b/PropsButBetter/PropsButBetter/Extensions/CVRSpawnableExtensions.cs deleted file mode 100644 index b76a1aa..0000000 --- a/PropsButBetter/PropsButBetter/Extensions/CVRSpawnableExtensions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using ABI_RC.Core.Util; -using ABI.CCK.Components; - -namespace NAK.PropsButBetter; - -public static class CVRSpawnableExtensions -{ - public static bool IsSpawnedByAdmin(this CVRSpawnable spawnable) - => spawnable.ownerId is CVRSyncHelper.OWNERID_SYSTEM - or CVRSyncHelper.OWNERID_LOCALSERVER; -} \ No newline at end of file diff --git a/PropsButBetter/PropsButBetter/Extensions/PlayerSetupExtensions.cs b/PropsButBetter/PropsButBetter/Extensions/PlayerSetupExtensions.cs deleted file mode 100644 index 9750219..0000000 --- a/PropsButBetter/PropsButBetter/Extensions/PlayerSetupExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -using ABI_RC.Core.Player; - -namespace NAK.PropsButBetter; - -public static class PlayerSetupExtensions -{ - public static void ToggleDeleteMode(this PlayerSetup playerSetup) - { - if (playerSetup.propGuidForSpawn == PlayerSetup.PropModeDeleteString) - playerSetup.ClearPropToSpawn(); - else - playerSetup.EnterPropDeleteMode(); - } -} \ No newline at end of file diff --git a/PropsButBetter/PropsButBetter/Extensions/PropDataExtensions.cs b/PropsButBetter/PropsButBetter/Extensions/PropDataExtensions.cs deleted file mode 100644 index db302f4..0000000 --- a/PropsButBetter/PropsButBetter/Extensions/PropDataExtensions.cs +++ /dev/null @@ -1,42 +0,0 @@ -using ABI_RC.Core.Networking; -using ABI_RC.Core.Util; - -namespace NAK.PropsButBetter; - -public static class PropDataExtensions -{ - extension(CVRSyncHelper.PropData prop) - { - public bool IsSpawnedByMe() - => prop.SpawnedBy == AuthManager.UserId; - - public void CopyFrom(CVRSyncHelper.PropData sourceData) - { - prop.ObjectId = sourceData.ObjectId; - prop.InstanceId = sourceData.InstanceId; - prop.PositionX = sourceData.PositionX; - prop.PositionY = sourceData.PositionY; - prop.PositionZ = sourceData.PositionZ; - prop.RotationX = sourceData.RotationX; - prop.RotationY = sourceData.RotationY; - prop.RotationZ = sourceData.RotationZ; - prop.ScaleX = sourceData.ScaleX; - prop.ScaleY = sourceData.ScaleY; - prop.ScaleZ = sourceData.ScaleZ; - prop.CustomFloatsAmount = sourceData.CustomFloatsAmount; - prop.CustomFloats = sourceData.CustomFloats; - prop.SpawnedBy = sourceData.SpawnedBy; - prop.syncedBy = sourceData.syncedBy; - prop.syncType = sourceData.syncType; - prop.ContentMetadata = sourceData.ContentMetadata; - prop.CanFireDecommissionEvents = sourceData.CanFireDecommissionEvents; - } - - public void RecycleSafe() - { - if (prop.IsSpawnedByMe()) - CVRSyncHelper.DeleteMyPropByInstanceIdOverNetwork(prop.InstanceId); - prop.Recycle(); - } - } -} \ No newline at end of file diff --git a/PropsButBetter/PropsButBetter/Extensions/TextBlockExtensions.cs b/PropsButBetter/PropsButBetter/Extensions/TextBlockExtensions.cs deleted file mode 100644 index abeadbb..0000000 --- a/PropsButBetter/PropsButBetter/Extensions/TextBlockExtensions.cs +++ /dev/null @@ -1,12 +0,0 @@ -using ABI_RC.Systems.UI.UILib.UIObjects.Components; - -namespace NAK.PropsButBetter; - -public static class TextBlockExtensions -{ - public static void SetHiddenIfNeeded(this TextBlock textBlock, bool hidden) - { - // Don't invoke a view trigger event needlessly - if (textBlock.Hidden != hidden) textBlock.Hidden = hidden; - } -} \ No newline at end of file diff --git a/PropsButBetter/PropsButBetter/Helpers/BoundsUtility.cs b/PropsButBetter/PropsButBetter/Helpers/BoundsUtility.cs deleted file mode 100644 index 443b644..0000000 --- a/PropsButBetter/PropsButBetter/Helpers/BoundsUtility.cs +++ /dev/null @@ -1,294 +0,0 @@ -using Unity.Burst; -using Unity.Collections; -using Unity.Jobs; -using Unity.Mathematics; -using UnityEngine; -using UnityEngine.Rendering; -using System.Buffers; -using Unity.Collections.LowLevel.Unsafe; - -public static class MeshBoundsUtility -{ - [BurstCompile] - unsafe struct BoundsJob : IJob - { - [ReadOnly] public NativeArray bytes; - public int vertexCount; - public int stride; - public int positionOffset; - public NativeArray minMax; - - public void Execute() - { - byte* basePtr = (byte*)bytes.GetUnsafeReadOnlyPtr(); - - float3 min = *(float3*)(basePtr + positionOffset); - float3 max = min; - - for (int i = 1; i < vertexCount; i++) - { - float3 p = *(float3*)(basePtr + i * stride + positionOffset); - min = math.min(min, p); - max = math.max(max, p); - } - - minMax[0] = min; - minMax[1] = max; - } - } - - public static Bounds CalculateTightBounds(Mesh mesh) - { - if (!mesh || mesh.vertexCount == 0) - return default; - - var attrs = mesh.GetVertexAttributes(); - - int stream = -1; - int positionOffset = 0; - - for (int i = 0; i < attrs.Length; i++) - { - var a = attrs[i]; - - if (a.attribute == VertexAttribute.Position) - { - stream = a.stream; - break; - } - - if (stream == -1 || a.stream == stream) - { - int size = a.format switch - { - VertexAttributeFormat.Float32 => 4, - VertexAttributeFormat.Float16 => 2, - VertexAttributeFormat.UNorm8 => 1, - VertexAttributeFormat.SNorm8 => 1, - VertexAttributeFormat.UNorm16 => 2, - VertexAttributeFormat.SNorm16 => 2, - VertexAttributeFormat.UInt8 => 1, - VertexAttributeFormat.SInt8 => 1, - VertexAttributeFormat.UInt16 => 2, - VertexAttributeFormat.SInt16 => 2, - VertexAttributeFormat.UInt32 => 4, - VertexAttributeFormat.SInt32 => 4, - _ => 0 - }; - - positionOffset += size * a.dimension; - } - } - - if (stream < 0) - return default; - - using GraphicsBuffer vb = mesh.GetVertexBuffer(stream); - int stride = vb.stride; - int byteCount = vb.count * stride; - - // REQUIRED: managed array - byte[] managedBytes = ArrayPool.Shared.Rent(byteCount); - vb.GetData(managedBytes, 0, 0, byteCount); - - var bytes = new NativeArray( - byteCount, - Allocator.TempJob, - NativeArrayOptions.UninitializedMemory); - - NativeArray.Copy(managedBytes, bytes, byteCount); - ArrayPool.Shared.Return(managedBytes); - - var minMax = new NativeArray(2, Allocator.TempJob); - - new BoundsJob - { - bytes = bytes, - vertexCount = mesh.vertexCount, - stride = stride, - positionOffset = positionOffset, - minMax = minMax - }.Run(); - - bytes.Dispose(); - - float3 min = minMax[0]; - float3 max = minMax[1]; - minMax.Dispose(); - - return new Bounds((min + max) * 0.5f, max - min); - } - - public static bool TryCalculateRendererBounds(Renderer renderer, out Bounds worldBounds) - { - worldBounds = default; - switch (renderer) - { - case MeshRenderer mr: - { - if (!renderer.TryGetComponent(out MeshFilter mf)) - return false; - - Mesh sharedMesh = mf.sharedMesh; - if (!sharedMesh) - return false; - - Bounds local = CalculateTightBounds(sharedMesh); - if (local.size == Vector3.zero) - return false; - - worldBounds = TransformBounds(mr.transform, local); - return true; - } - - case SkinnedMeshRenderer smr: - { - Mesh sharedMesh = smr.sharedMesh; - if (!sharedMesh) - return false; - - Bounds local = CalculateTightBounds(sharedMesh); - if (local.size == Vector3.zero) - return false; - - worldBounds = TransformBounds(smr.transform, local); - return true; - } - - default: - { - worldBounds = renderer.bounds; - return true; - } - } - } - - /// - /// Calculates the combined world-space bounds of all Renderers under a GameObject. - /// - /// The root GameObject to search under. - /// Whether to include inactive GameObjects. - /// Combined bounds, or default if no renderers found. - public static Bounds CalculateCombinedBounds(GameObject root, bool includeInactive = false) - { - if (!root) - return default; - - Renderer[] renderers = root.GetComponentsInChildren(includeInactive); - - if (renderers.Length == 0) - return default; - - // Find first valid bounds - int startIndex = 0; - Bounds combined = default; - - for (int i = 0; i < renderers.Length; i++) - { - if (renderers[i] && renderers[i].enabled) - { - combined = renderers[i].bounds; - startIndex = i + 1; - break; - } - } - - // Encapsulate remaining renderers - for (int i = startIndex; i < renderers.Length; i++) - { - if (renderers[i] && renderers[i].enabled) - { - combined.Encapsulate(renderers[i].bounds); - } - } - - return combined; - } - - /// - /// Calculates tight combined bounds using mesh vertex data instead of renderer bounds. - /// More accurate but slower than CalculateCombinedBounds. - /// - /// The root GameObject to search under. - /// Whether to include inactive GameObjects. - /// Combined tight bounds in world space, or default if no valid meshes found. - public static Bounds CalculateCombinedTightBounds(GameObject root, bool includeInactive = false) - { - if (!root) - return default; - - MeshFilter[] meshFilters = root.GetComponentsInChildren(includeInactive); - SkinnedMeshRenderer[] skinnedRenderers = root.GetComponentsInChildren(includeInactive); - - bool hasAny = false; - Bounds combined = default; - - // Process MeshFilters - foreach (var mf in meshFilters) - { - if (!mf || !mf.sharedMesh) - continue; - - Bounds localBounds = CalculateTightBounds(mf.sharedMesh); - if (localBounds.size == Vector3.zero) - continue; - - Bounds worldBounds = TransformBounds(mf.transform, localBounds); - - if (!hasAny) - { - combined = worldBounds; - hasAny = true; - } - else - { - combined.Encapsulate(worldBounds); - } - } - - // Process SkinnedMeshRenderers - foreach (var smr in skinnedRenderers) - { - if (!smr || !smr.sharedMesh) - continue; - - // For skinned meshes, use the current baked bounds or renderer bounds - // since vertex positions change with animation - Bounds worldBounds = smr.bounds; - - if (!hasAny) - { - combined = worldBounds; - hasAny = true; - } - else - { - combined.Encapsulate(worldBounds); - } - } - - return combined; - } - - /// - /// Transforms an AABB from local space to world space, returning a new AABB that fully contains the transformed box. - /// - private static Bounds TransformBounds(Transform transform, Bounds localBounds) - { - Vector3 center = transform.TransformPoint(localBounds.center); - Vector3 extents = localBounds.extents; - - // Transform each axis extent by the absolute value of the rotation/scale matrix - Vector3 axisX = transform.TransformVector(extents.x, 0, 0); - Vector3 axisY = transform.TransformVector(0, extents.y, 0); - Vector3 axisZ = transform.TransformVector(0, 0, extents.z); - - Vector3 worldExtents = new Vector3( - Mathf.Abs(axisX.x) + Mathf.Abs(axisY.x) + Mathf.Abs(axisZ.x), - Mathf.Abs(axisX.y) + Mathf.Abs(axisY.y) + Mathf.Abs(axisZ.y), - Mathf.Abs(axisX.z) + Mathf.Abs(axisY.z) + Mathf.Abs(axisZ.z) - ); - - return new Bounds(center, worldExtents * 2f); - } -} diff --git a/PropsButBetter/PropsButBetter/Helpers/PropHelper.cs b/PropsButBetter/PropsButBetter/Helpers/PropHelper.cs deleted file mode 100644 index 1905506..0000000 --- a/PropsButBetter/PropsButBetter/Helpers/PropHelper.cs +++ /dev/null @@ -1,777 +0,0 @@ -using System.Collections; -using ABI_RC.Core; -using ABI_RC.Core.AudioEffects; -using ABI_RC.Core.EventSystem; -using ABI_RC.Core.InteractionSystem; -using ABI_RC.Core.IO; -using ABI_RC.Core.Networking; -using ABI_RC.Core.Networking.API.Responses; -using ABI_RC.Core.Networking.GameServer; -using ABI_RC.Core.Player; -using ABI_RC.Core.Savior; -using ABI_RC.Core.Util; -using ABI_RC.Core.Util.AssetFiltering; -using ABI_RC.Systems.GameEventSystem; -using ABI_RC.Systems.Gravity; -using ABI.CCK.Components; -using DarkRift; -using MelonLoader; -using UnityEngine; -using Object = UnityEngine.Object; - -namespace NAK.PropsButBetter; - -public static class PropHelper -{ - public const int TotalKeptSessionHistory = 30; - - public static List SpawnedThisSession { get; } = new(TotalKeptSessionHistory); - - // CVRSyncHelper does not keep track of your own props nicely - public static readonly List MyProps = []; - - // We get prop spawn/destroy events with emptied prop data sometimes, so we need to track shit ourselves :D - private static readonly Dictionary _propToInstanceId = []; - - private static int MaxPropsForUser = 20; - - private const int RedoTimeoutLimit = 120; // seconds - private static readonly int RedoHistoryLimit = Mathf.Max(MaxPropsForUser, CVRSyncHelper.MyPropCount); - - private static readonly List _myDeletedProps = []; - - // audio clip names, InterfaceAudio adds "PropsButBetter" prefix - public const string SFX_Spawn = $"{nameof(PropsButBetter)}_sfx_spawn"; - public const string SFX_Undo = $"{nameof(PropsButBetter)}_sfx_undo"; - public const string SFX_Redo = $"{nameof(PropsButBetter)}_sfx_redo"; - public const string SFX_Warn = $"{nameof(PropsButBetter)}_sfx_warn"; - public const string SFX_Deny = $"{nameof(PropsButBetter)}_sfx_deny"; - - public static void PlaySound(string sound) - { - if (ModSettings.EntryUseSFX.Value) - InterfaceAudio.PlayModule(sound); - } - - public static bool IsPropSpawnAllowed() - => MetaPort.Instance.worldAllowProps - && MetaPort.Instance.settings.GetSettingsBool("ContentFilterPropsEnabled") - && NetworkManager.Instance.GameNetwork.ConnectionState == ConnectionState.Connected; - - public static bool IsAtPropLimit() - => MyProps.Count >= MaxPropsForUser; - - public static bool CanUndo() - => MyProps.Count > 0; - - public static bool CanRedo() - { - int deletedPropsCount = _myDeletedProps.Count; - return deletedPropsCount > 0 - && _myDeletedProps[deletedPropsCount-1].IsWithinTimeLimit - && !IsAtPropLimit() && IsPropSpawnAllowed(); - } - - public static void UndoProp() - { - CVRSyncHelper.PropData latestPropData = MyProps.LastOrDefault(); - if (latestPropData == null) - { - PlaySound(SFX_Warn); - return; - } - latestPropData.RecycleSafe(); - } - - public static void RedoProp() - { - int index = _myDeletedProps.Count - 1; - if (index < 0) - { - PlaySound(SFX_Warn); - return; - } - - if (!IsPropSpawnAllowed() || IsAtPropLimit()) - { - PlaySound(SFX_Deny); - return; - } - - // Only allow redoing props if they were deleted within the timeout - DeletedPropData deletedProp = _myDeletedProps[index]; - if (deletedProp.IsWithinTimeLimit) - { - _skipBecauseIsRedo = true; - CVRSyncHelper.SpawnProp(deletedProp.PropId, deletedProp.Position, deletedProp.Rotation); - _skipBecauseIsRedo = false; - _myDeletedProps.RemoveAt(index); - PlaySound(SFX_Redo); - } - else - { - // If latest prop is too old, same with rest - ClearUndo(); - PlaySound(SFX_Warn); - } - } - - public static void ClearUndo() - { - _myDeletedProps.Clear(); - } - - public static bool RemoveMyProps() - { - int propsCount = MyProps.Count; - if (propsCount == 0) - { - PlaySound(SFX_Warn); - return false; - } - - for (int i = propsCount - 1; i >= 0; i--) - { - CVRSyncHelper.PropData prop = MyProps[i]; - prop.RecycleSafe(); - } - - return true; - } - - public static bool RemoveOthersProps() - { - int propsCount = CVRSyncHelper.Props.Count; - if (propsCount == 0) - { - PlaySound(SFX_Warn); - return false; - } - - for (int i = propsCount - 1; i >= 0; i--) - { - CVRSyncHelper.PropData prop = CVRSyncHelper.Props[i]; - if (!prop.IsSpawnedByMe()) prop.RecycleSafe(); - } - - return true; - } - - public static bool RemoveAllProps() - { - int propsCount = CVRSyncHelper.Props.Count; - if (propsCount == 0) - { - PlaySound(SFX_Warn); - return false; - } - - for (int i = propsCount - 1; i >= 0; i--) - { - CVRSyncHelper.PropData prop = CVRSyncHelper.Props[i]; - prop.RecycleSafe(); - } - - return true; - } - - private static void OnPropSpawned(string assetId, CVRSyncHelper.PropData prop) - { - // Skip broken props - string instanceId = prop.InstanceId; - if (string.IsNullOrEmpty(instanceId)) - return; - - // Ignore the visualizer prop - if (instanceId == _lastVisualizerInstanceId) - { - OnVisualizerPropSpawned(); - return; - } - - // PropDistanceHider.OnPropSpawned(prop); - - // Track ourselves for later - _propToInstanceId.TryAdd(prop, prop.InstanceId); - - // Track our props - if (prop.IsSpawnedByMe()) MyProps.Add(prop); - - // Check if this prop was reloaded - int spawnedThisSessionCount = SpawnedThisSession.Count; - for (int i = 0; i < spawnedThisSessionCount; i++) - { - if (SpawnedThisSession[i].InstanceId == instanceId) - { - SpawnedThisSession[i].IsDestroyed = false; - return; // No need to add - } - } - - // Track for session history - PropHistoryData historyData = new() - { - PropId = prop.ObjectId, - PropName = prop.ContentMetadata.AssetName, - SpawnerName = CVRPlayerManager.Instance.TryGetPlayerName(prop.SpawnedBy), - InstanceId = instanceId, - IsDestroyed = false - }; - - // Insert at beginning for newest first - SpawnedThisSession.Insert(0, historyData); - - // Keep only the most recent entries - if (spawnedThisSessionCount >= TotalKeptSessionHistory) - SpawnedThisSession.RemoveAt(spawnedThisSessionCount - 1); - } - - private static void OnPropDestroyed(string assetId, CVRSyncHelper.PropData prop) - { - // Only handle props which we tracked the spawn for - if (!_propToInstanceId.Remove(prop, out string instanceId)) - return; - - // PropDistanceHider.OnPropDestroyed(prop); - - // Track our props - if (MyProps.Remove(prop)) - { - // Track the deleted prop for undo - if (_myDeletedProps.Count >= RedoHistoryLimit) - _myDeletedProps.RemoveAt(0); - - DeletedPropData deletedProp = new(prop); - _myDeletedProps.Add(deletedProp); - - PlaySound(SFX_Undo); - } - - // Track for session history - int spawnedThisSessionCount = SpawnedThisSession.Count; - for (int i = 0; i < spawnedThisSessionCount; i++) - { - if (SpawnedThisSession[i].InstanceId == instanceId) - { - SpawnedThisSession[i].IsDestroyed = true; - break; - } - } - } - - private static void OnGSInfoUpdate(GSInfoUpdate update, GSInfoChanged changed) - { - if (changed == GSInfoChanged.MaxPropsPerUser) - MaxPropsForUser = update.MaxPropsPerUser; - } - - private static void OnWorldUnload(string _) => ClearUndo(); - - public static void Initialize() - { - CVRGameEventSystem.Spawnable.OnPropSpawned.AddListener(OnPropSpawned); - CVRGameEventSystem.Spawnable.OnPropDestroyed.AddListener(OnPropDestroyed); - GSInfoHandler.OnGSInfoUpdate += OnGSInfoUpdate; - CVRGameEventSystem.World.OnUnload.AddListener(OnWorldUnload); - } - - // Replacement methods for CVRSyncHelper - - public static bool OnPreDeleteMyProps() - { - bool removedProps = RemoveMyProps(); - - if (removedProps) - ViewManager.Instance.NotifyUser("(Synced) Client", "Removed all my props", 1f); - else - ViewManager.Instance.NotifyUser("(Local) Client", "No props to remove", 1f); - - return false; - } - - public static bool OnPreDeleteAllProps() - { - bool removedProps = RemoveAllProps(); - - if (removedProps) - ViewManager.Instance.NotifyUser("(Synced) Client", "Removed all spawned props", 1f); - else - ViewManager.Instance.NotifyUser("(Local) Client", "No props to remove", 1f); - - return false; - } - - private static bool _skipBecauseIsRedo; // Hack - public static void OnTrySpawnProp() - { - if (_skipBecauseIsRedo) return; - if (!IsPropSpawnAllowed() || IsAtPropLimit()) - { - PlaySound(SFX_Deny); - return; - } - PlaySound(SFX_Spawn); - } - - // Prop history for session - public class PropHistoryData - { - public string PropId; - public string PropName; - public string SpawnerName; - public string InstanceId; - public bool IsDestroyed; - } - - // Deleted prop info for undo history - private class DeletedPropData - { - public readonly string PropId; - public readonly Vector3 Position; - public readonly Quaternion Rotation; - public readonly float TimeDeleted; - - public bool IsWithinTimeLimit => Time.time - TimeDeleted <= RedoTimeoutLimit; - - public DeletedPropData(CVRSyncHelper.PropData propData) - { - CVRSpawnable spawnable = propData.Spawnable; - - if (spawnable == null) - { - // use original spawn position and rotation / last known position and rotation - Position = new Vector3(propData.PositionX, propData.PositionY, propData.PositionZ); - Rotation = Quaternion.Euler(new Vector3(propData.RotationX, propData.RotationY, propData.RotationZ)); - } - else - { - Transform spawnableTransform = spawnable.transform; - Position = spawnableTransform.position; - Rotation = spawnableTransform.rotation; - - // Offset spawn height so game can account for it later - Position.y -= spawnable.spawnHeight; - } - - PropId = propData.ObjectId; - TimeDeleted = Time.time; - } - } - - // Visualizer -// Add this enum at the top of your file or in a separate file -public enum PropVisualizerMode -{ - SourceRenderers, - HologramSource, - HologramBounds -} - -// Modified code: - -private static string _lastVisualizerInstanceId; -private static object _currentFetchCoroutine; -private static bool _cancelPendingFetch; -private static CVRSyncHelper.PropData _visualizerPropData; -private static GameObject _visualizerGameObject; -private static List _visualizerAnimators = new List(); -private static object _animatorPulseCoroutine; - -public static bool IsVisualizerActive => _visualizerGameObject != null && !string.IsNullOrEmpty(_lastVisualizerInstanceId); - -public static void OnSelectPropToSpawn(string guid, string propImage, string propName) -{ - // Cancel any existing fetch - OnClearPropToSpawn(); - - if (!ModSettings.EntryPropSpawnVisualizer.Value) - return; - - _cancelPendingFetch = false; - _currentFetchCoroutine = MelonCoroutines.Start(FetchAndSpawnProp(guid)); -} - -private static IEnumerator FetchAndSpawnProp(string guid) -{ - PropsButBetterMod.Logger.Msg($"Fetching prop meta for {guid}..."); - - // Start the async task - var task = PropApiHelper.GetPropMeta(guid); - - // Wait for completion - while (!task.IsCompleted) - { - if (_cancelPendingFetch) - { - PropsButBetterMod.Logger.Msg("Fetch cancelled by user"); - _currentFetchCoroutine = null; - yield break; - } - yield return null; - } - - // Check for cancellation after task completes - if (_cancelPendingFetch) - { - PropsButBetterMod.Logger.Msg("Fetch cancelled by user"); - _currentFetchCoroutine = null; - yield break; - } - - // Check for errors - if (task.IsFaulted) - { - PropsButBetterMod.Logger.Error($"Failed to fetch prop meta: {task.Exception?.Message}"); - _currentFetchCoroutine = null; - yield break; - } - - var response = task.Result; - - // Validate response - if (response == null || !response.IsSuccessStatusCode) - { - PropsButBetterMod.Logger.Error($"Failed to fetch prop meta: {response?.Message ?? "Unknown error"}"); - _currentFetchCoroutine = null; - yield break; - } - - UgcWithFile propMeta = response.Data; - - if (propMeta == null) - { - PropsButBetterMod.Logger.Error("Prop meta data was null"); - _currentFetchCoroutine = null; - yield break; - } - - // Create metadata - AssetManagement.UgcMetadata metadata = new() - { - AssetName = propMeta.Name, - AssetId = propMeta.Id, - FileSize = propMeta.FileSize, - FileKey = propMeta.FileKey, - FileHash = propMeta.FileHash, - TagsData = new UgcContentTags(propMeta.Tags), - CompatibilityVersion = propMeta.CompatibilityVersion, - EncryptionAlgorithm = propMeta.EncryptionAlgorithm - }; - - // Generate instance ID for visualizer - _lastVisualizerInstanceId = Guid.NewGuid().ToString(); - - // Register prop data - CVRSyncHelper.PropData newPropData = CVRSyncHelper.PropData.PropDataPool.GetObject(); - newPropData.InstanceId = _lastVisualizerInstanceId; - newPropData.ContentMetadata = metadata; - newPropData.ObjectId = guid; - newPropData.SpawnedBy = "SYSTEM"; - - CVRSyncHelper.Props.Add(newPropData); - _visualizerPropData = newPropData; // Cache it - - // Queue download - CVRDownloadManager.Instance.QueueTask(metadata, DownloadTask.ObjectType.Prop, - propMeta.FileLocation, propMeta.FileId, _lastVisualizerInstanceId, spawnerId: "SYSTEM"); - - PropsButBetterMod.Logger.Msg($"Queued prop '{propMeta.Name}' for download"); - - _currentFetchCoroutine = null; -} - -public static void OnVisualizerPropSpawned() -{ - if (_visualizerPropData == null || _visualizerPropData.Wrapper == null) - return; - - Transform rootTransform = _visualizerPropData.Wrapper.transform; - _visualizerGameObject = rootTransform.gameObject; - - PropVisualizerMode mode = ModSettings.EntryPropSpawnVisualizerMode.Value; - - if (mode == PropVisualizerMode.HologramBounds) - { - ProcessHologramBoundsMode(); - } - else - { - ProcessSourceRenderersMode(); - } - - // Start the animator pulse coroutine - if (_visualizerAnimators.Count > 0) - _animatorPulseCoroutine = MelonCoroutines.Start(PulseAnimators()); - - PropsButBetterMod.Logger.Msg($"Visualizer prop spawned in {mode} mode"); -} - -private static void ProcessSourceRenderersMode() -{ - var allComponents = _visualizerGameObject.GetComponentsInChildren(true); - _visualizerAnimators.Clear(); - - List _sharedMaterials = new(); - - foreach (var component in allComponents) - { - if (component == null) continue; - - // Keep these components - if (component is Transform) continue; - if (component is Renderer renderer) - { - if (ModSettings.EntryPropSpawnVisualizerMode.Value == PropVisualizerMode.HologramSource) - { - int materialCount = renderer.GetMaterialCount(); - // Resize sharedMaterials list efficiently - if (_sharedMaterials.Count < materialCount) - { - for (int i = _sharedMaterials.Count; i < materialCount; i++) - _sharedMaterials.Add(MetaPort.Instance.hologrammMaterial); - } - else if (_sharedMaterials.Count > materialCount) - { - _sharedMaterials.RemoveRange(materialCount, _sharedMaterials.Count - materialCount); - } - renderer.SetSharedMaterials(_sharedMaterials); - } - continue; - } - if (component is MeshFilter) continue; - if (component is CVRSpawnable) continue; - if (component is CVRAssetInfo) continue; - if (component is ParticleSystem) continue; - if (component is Animator animator) - { - animator.enabled = false; - _visualizerAnimators.Add(animator); - continue; - } - - // Remove everything else - component.DestroyComponentWithRequirements(); - } -} - -private static void ProcessHologramBoundsMode() -{ - var allComponents = _visualizerGameObject.GetComponentsInChildren(true); - _visualizerAnimators.Clear(); - - Mesh cubeMesh = PrimitiveHelper.GetPrimitiveMesh(PrimitiveType.Cube); - - foreach (var component in allComponents) - { - if (component == null) continue; - - // Handle animators - if (component is Animator animator) - { - animator.enabled = false; - _visualizerAnimators.Add(animator); - continue; - } - - // Handle renderers - create hologram cubes for visible ones - if (component is Renderer renderer) - { - // Check if renderer is visible - bool isVisible = renderer.enabled && - renderer.gameObject.activeInHierarchy && - renderer.shadowCastingMode != UnityEngine.Rendering.ShadowCastingMode.ShadowsOnly; - - if (isVisible) - { - Mesh mesh = null; - if (renderer is SkinnedMeshRenderer smr) - mesh = smr.sharedMesh; - else if (renderer.TryGetComponent(out MeshFilter filter)) - mesh = filter.sharedMesh; - - if (mesh != null) - { - // Get bounds of mesh - Bounds localBounds = MeshBoundsUtility.CalculateTightBounds(mesh); - - PropsButBetterMod.Logger.Msg(localBounds); - - // Create child GameObject for the cube - GameObject cubeObj = new GameObject("HologramCube"); - cubeObj.transform.SetParent(renderer.transform, false); - - // Add mesh filter and renderer to child - MeshFilter cubeMeshFilter = cubeObj.AddComponent(); - cubeMeshFilter.sharedMesh = cubeMesh; - - MeshRenderer cubeRenderer = cubeObj.AddComponent(); - cubeRenderer.sharedMaterial = MetaPort.Instance.hologrammMaterial; - - // Account for lossy scale when setting the cube's scale - /*Vector3 sourceScale = renderer.transform.lossyScale; - Bounds scaledBounds = new( - Vector3.Scale(localBounds.center, sourceScale), - Vector3.Scale(localBounds.size, sourceScale) - );*/ - - cubeObj.transform.localPosition = localBounds.center; - cubeObj.transform.localRotation = Quaternion.identity; - cubeObj.transform.localScale = localBounds.size; - } - else - { - PropsButBetterMod.Logger.Msg("No mesh on " + renderer.gameObject.name); - } - } - } - - // Keep these components - if (component is Transform) continue; - if (component is CVRSpawnable) continue; - if (component is CVRAssetInfo) continue; - if (component is MeshFilter) continue; - - // Remove everything else - try { component.DestroyComponentWithRequirements(); } - catch (Exception) - { - // ignored - } - } -} - -private static IEnumerator PulseAnimators() -{ - const float skipDuration = 10f; - const float skipRealTime = 5f; - - const float pulseAmplitude = 2f; // seconds forward/back - const float pulseFrequency = 0.2f; // Hz (1 cycle every 2s) - - float t = 0f; - float lastPos = 0f; - - while (true) - { - if (_visualizerAnimators == null || _visualizerAnimators.Count == 0) - yield break; - - t += Time.deltaTime; - - // 0 → 1 blend from "skip" to "pulse" - float blend = Mathf.SmoothStep(0f, 1f, t / skipRealTime); - - // Linear fast-forward (position-based, not speed-based) - float skipPos = Mathf.Lerp( - 0f, - skipDuration, - Mathf.Clamp01(t / skipRealTime) - ); - - // Continuous oscillation around skipDuration - float pulsePos = - skipDuration + - Mathf.Sin(t * Mathf.PI * 2f * pulseFrequency) * pulseAmplitude; - - // Final blended position - float currentPos = Mathf.Lerp(skipPos, pulsePos, blend); - - float delta = currentPos - lastPos; - lastPos = currentPos; - - foreach (var animator in _visualizerAnimators) - { - if (animator != null) - animator.Update(delta); - } - - yield return null; - } -} - -public static void OnHandlePropSpawn(ref ControllerRay __instance) -{ - if (!__instance.uiActive) return; // Skip offhand - if (!IsVisualizerActive) return; // No visualizer active - - Vector3 spawnPos = __instance.Hit.point; - Quaternion spawnRot = GetPropSpawnRotation(spawnPos); - - // Position active visualizer at this position - _visualizerGameObject.transform.position = spawnPos; - _visualizerGameObject.transform.rotation = spawnRot; -} - -private static Quaternion GetPropSpawnRotation(Vector3 spawnPos) -{ - Vector3 playerPos = PlayerSetup.Instance.GetPlayerPosition(); - Vector3 directionToPlayer = (playerPos - spawnPos).normalized; - - // Get gravity direction for objects - GravitySystem.GravityResult result = GravitySystem.TryGetResultingGravity(spawnPos, false); - Vector3 gravityDirection = result.AppliedGravity.normalized; - - // If there is no gravity, use transform.up - if (gravityDirection == Vector3.zero) - gravityDirection = -PlayerSetup.Instance.transform.up; - - Vector3 projectedDirectionToPlayer = Vector3.ProjectOnPlane(directionToPlayer, gravityDirection).normalized; - - if (projectedDirectionToPlayer == Vector3.zero) - { - projectedDirectionToPlayer = Vector3.Cross(gravityDirection, Vector3.right).normalized; - if (projectedDirectionToPlayer == Vector3.zero) - projectedDirectionToPlayer = Vector3.Cross(gravityDirection, Vector3.forward).normalized; - } - - Vector3 right = Vector3.Cross(gravityDirection, projectedDirectionToPlayer).normalized; - Vector3 finalForward = Vector3.Cross(right, gravityDirection).normalized; - - if (Vector3.Dot(finalForward, directionToPlayer) < 0) - finalForward *= -1; - - Quaternion rotation = Quaternion.LookRotation(finalForward, -gravityDirection); - return rotation; -} - -public static void OnClearPropToSpawn() -{ - // Stop animator pulse coroutine - if (_animatorPulseCoroutine != null) - { - MelonCoroutines.Stop(_animatorPulseCoroutine); - _animatorPulseCoroutine = null; - } - - // Clear animator list - _visualizerAnimators?.Clear(); - - // Stop any pending fetch coroutine - if (_currentFetchCoroutine != null) - { - _cancelPendingFetch = true; - MelonCoroutines.Stop(_currentFetchCoroutine); - _currentFetchCoroutine = null; - PropsButBetterMod.Logger.Msg("Stopped pending fetch operation"); - } - - if (string.IsNullOrEmpty(_lastVisualizerInstanceId)) - return; - - // Cancel pending attachment in download queue - CVRDownloadManager.Instance.CancelAttachment(_lastVisualizerInstanceId); - - // Find and remove prop data - if (_visualizerPropData != null) - { - CVRSyncHelper.Props.Remove(_visualizerPropData); - _visualizerPropData.Recycle(); // this removes the gameobject if spawned - _visualizerPropData = null; - } - - _visualizerGameObject = null; - _lastVisualizerInstanceId = null; - - PropsButBetterMod.Logger.Msg("Cleared prop visualizer"); -} -} \ No newline at end of file diff --git a/PropsButBetter/PropsButBetter/Helpers/RendererHelper.cs b/PropsButBetter/PropsButBetter/Helpers/RendererHelper.cs deleted file mode 100644 index 6c5bee4..0000000 --- a/PropsButBetter/PropsButBetter/Helpers/RendererHelper.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Reflection; -using System.Runtime.CompilerServices; -using UnityEngine; - -public static class RendererHelper -{ - private static readonly Func _getMaterialCount; - - static RendererHelper() - { - // Find the private method - MethodInfo mi = typeof(Renderer).GetMethod( - "GetMaterialCount", - BindingFlags.Instance | BindingFlags.NonPublic - ); - - if (mi != null) - { - // Create a fast delegate - _getMaterialCount = (Func)Delegate.CreateDelegate( - typeof(Func), - null, - mi - ); - } - } - - public static int GetMaterialCount(this Renderer renderer) - { - return _getMaterialCount?.Invoke(renderer) ?? 0; - } -} \ No newline at end of file diff --git a/PropsButBetter/PropsButBetter/Helpers/UILibHelper.cs b/PropsButBetter/PropsButBetter/Helpers/UILibHelper.cs deleted file mode 100644 index f5fb890..0000000 --- a/PropsButBetter/PropsButBetter/Helpers/UILibHelper.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Reflection; -using ABI_RC.Systems.UI.UILib; - -namespace NAK.PropsButBetter; - -public static class UILibHelper -{ - public static string PlaceholderImageCoui => GetIconCoui(ModSettings.ModName, $"{ModSettings.ModName}-placeholder"); - - public static string GetIconCoui(string modName, string iconName) - { - modName = UIUtils.GetCleanString(modName); - return $"coui://uiresources/GameUI/UILib/Images/{modName}/{iconName}.png"; - } - - internal static void LoadIcons() - { - // Load all icons - Assembly assembly = Assembly.GetExecutingAssembly(); - string assemblyName = assembly.GetName().Name; - - QuickMenuAPI.PrepareIcon(ModSettings.ModName, $"{ModSettings.ModName}-remove", GetIconStream("remove.png")); - QuickMenuAPI.PrepareIcon(ModSettings.ModName, $"{ModSettings.ModName}-reload", GetIconStream("reload.png")); - QuickMenuAPI.PrepareIcon(ModSettings.ModName, $"{ModSettings.ModName}-select", GetIconStream("select.png")); - QuickMenuAPI.PrepareIcon(ModSettings.ModName, $"{ModSettings.ModName}-undo", GetIconStream("undo.png")); - QuickMenuAPI.PrepareIcon(ModSettings.ModName, $"{ModSettings.ModName}-redo", GetIconStream("redo.png")); - QuickMenuAPI.PrepareIcon(ModSettings.ModName, $"{ModSettings.ModName}-wand", GetIconStream("wand.png")); - QuickMenuAPI.PrepareIcon(ModSettings.ModName, $"{ModSettings.ModName}-rubiks-cube", GetIconStream("rubiks-cube.png")); - QuickMenuAPI.PrepareIcon(ModSettings.ModName, $"{ModSettings.ModName}-rubiks-cube-eye", GetIconStream("rubiks-cube-eye.png")); - QuickMenuAPI.PrepareIcon(ModSettings.ModName, $"{ModSettings.ModName}-rubiks-cube-star", GetIconStream("rubiks-cube-star.png")); - QuickMenuAPI.PrepareIcon(ModSettings.ModName, $"{ModSettings.ModName}-rubiks-cube-clock", GetIconStream("rubiks-cube-clock.png")); - QuickMenuAPI.PrepareIcon(ModSettings.ModName, $"{ModSettings.ModName}-placeholder", GetIconStream("placeholder.png")); - Stream GetIconStream(string iconName) => assembly.GetManifestResourceStream($"{assemblyName}.Resources.{iconName}"); - } -} \ No newline at end of file diff --git a/PropsButBetter/PropsButBetter/PropDistanceHider.cs b/PropsButBetter/PropsButBetter/PropDistanceHider.cs deleted file mode 100644 index 907a5b9..0000000 --- a/PropsButBetter/PropsButBetter/PropDistanceHider.cs +++ /dev/null @@ -1,858 +0,0 @@ -/*using ABI_RC.Core.Player; -using ABI_RC.Core.Util; -using ABI_RC.Systems.RuntimeDebug; -using ABI.CCK.Components; -using UnityEngine; - -namespace NAK.PropsButBetter; - -public static class PropDistanceHider -{ - public enum HiderMode - { - Disabled, - DisableRendering, - } - - private class PropPart - { - public Transform Transform; - public Transform BoundsCenter; // Child transform at local bounds center - public float BoundsRadius; // Local extents magnitude - } - - private class PropCache - { - public CVRSyncHelper.PropData PropData; - - public PropPart[] Parts; - public int PartCount; - - // Renderers (not Behaviours, have enabled) - public Renderer[] Renderers; - public int RendererCount; - public bool[] RendererEnabled; - - // Colliders (not Behaviours, have enabled) - public Collider[] Colliders; - public int ColliderCount; - public bool[] ColliderEnabled; - - // Rigidbodies (no enabled, use isKinematic) - public Rigidbody[] Rigidbodies; - public int RigidbodyCount; - public bool[] RigidbodyWasKinematic; - - // ParticleSystems (not Behaviours, no enabled, use Play/Stop) - public ParticleSystem[] ParticleSystems; - public int ParticleSystemCount; - public bool[] PsWasPlaying; - public bool[] PsPlayOnAwake; - public float[] PsTime; - public double[] PsStartTime; - public uint[] PsRandomSeed; - public bool[] PsUseAutoSeed; - - // CVRPickupObject - must drop before disabling - public CVRPickupObject[] Pickups; - public int PickupCount; - - // CVRAttachment - must deattach before disabling - public CVRAttachment[] Attachments; - public int AttachmentCount; - - // CVRInteractable - must disable before other behaviours - public CVRInteractable[] Interactables; - public int InteractableCount; - public bool[] InteractableEnabled; - - // Behaviour state tracking - public Behaviour[] Behaviours; - public int BehaviourCount; - public bool[] BehaviourEnabled; - - // Animator state - public bool[] AnimWasEnabled; - public double[] AnimStartTime; - - // AudioSource state - public bool[] AudioWasPlaying; - public bool[] AudioLoop; - public bool[] AudioPlayOnAwake; - public double[] AudioStartDsp; - public float[] AudioStartTime; - - public bool IsHidden; - } - - public static float Hysteresis = 2f; - - private static readonly Dictionary _propCaches = new(); - private static readonly List _propCacheList = new(); - private static readonly List _toRemove = new(); - - // Reusable collections for spawn processing (avoid allocations) - private static readonly Dictionary _tempPartLookup = new(); - private static readonly List _tempBoundsList = new(); - private static readonly List _tempRenderers = new(); - private static readonly List _tempColliders = new(); - private static readonly List _tempRigidbodies = new(); - private static readonly List _tempParticleSystems = new(); - private static readonly List _tempPickups = new(); - private static readonly List _tempAttachments = new(); - private static readonly List _tempInteractables = new(); - private static readonly List _tempBehaviours = new(); - private static readonly List _tempRendererPartIndices = new(); - - public static void OnPropSpawned(CVRSyncHelper.PropData propData) - { - GameObject propObject = propData.Wrapper; - if (!propObject) - return; - - CVRSpawnable spawnable = propData.Spawnable; - if (!spawnable) - return; - - Transform spawnableTransform = spawnable.transform; - var subSyncs = spawnable.subSyncs; - int subSyncCount = subSyncs?.Count ?? 0; - - // Build parts array (root + subsyncs) - int partCount = 1 + subSyncCount; - PropPart[] parts = new PropPart[partCount]; - - // Clear temp collections - _tempPartLookup.Clear(); - _tempRenderers.Clear(); - _tempColliders.Clear(); - _tempRigidbodies.Clear(); - _tempParticleSystems.Clear(); - _tempPickups.Clear(); - _tempAttachments.Clear(); - _tempInteractables.Clear(); - _tempBehaviours.Clear(); - _tempRendererPartIndices.Clear(); - - // Root spawnable part - parts[0] = new PropPart { Transform = spawnableTransform, BoundsCenter = null, BoundsRadius = 0f }; - _tempPartLookup[spawnableTransform] = 0; - - // Subsync parts - for (int i = 0; i < subSyncCount; i++) - { - CVRSpawnableSubSync subSync = subSyncs?[i]; - if (subSync == null) continue; - - Transform subSyncTransform = subSync.transform; - parts[i + 1] = new PropPart { Transform = subSyncTransform, BoundsCenter = null, BoundsRadius = 0f }; - _tempPartLookup[subSyncTransform] = i + 1; - } - - // Get all components once and categorize - Component[] allComponents = propObject.GetComponentsInChildren(true); - int componentCount = allComponents.Length; - - for (int i = 0; i < componentCount; i++) - { - Component comp = allComponents[i]; - if (!comp) continue; - - if (comp is Renderer renderer) - { - _tempRenderers.Add(renderer); - _tempRendererPartIndices.Add(FindOwningPartIndex(renderer.transform)); - } - else if (comp is Collider collider) - { - _tempColliders.Add(collider); - } - else if (comp is Rigidbody rigidbody) - { - _tempRigidbodies.Add(rigidbody); - } - else if (comp is ParticleSystem particleSystem) - { - _tempParticleSystems.Add(particleSystem); - } - else if (comp is CVRPickupObject pickup) - { - _tempPickups.Add(pickup); - } - else if (comp is CVRAttachment attachment) - { - _tempAttachments.Add(attachment); - } - else if (comp is CVRInteractable interactable) - { - _tempInteractables.Add(interactable); - } - else if (comp is Behaviour behaviour) - { - // Skip the spawnable as it needs to be enabled to sync moving - if (behaviour is CVRSpawnable) - continue; - - _tempBehaviours.Add(behaviour); - } - } - - // Copy to final arrays - int rendererCount = _tempRenderers.Count; - int colliderCount = _tempColliders.Count; - int rigidbodyCount = _tempRigidbodies.Count; - int particleSystemCount = _tempParticleSystems.Count; - int pickupCount = _tempPickups.Count; - int attachmentCount = _tempAttachments.Count; - int interactableCount = _tempInteractables.Count; - int behaviourCount = _tempBehaviours.Count; - - Renderer[] renderers = new Renderer[rendererCount]; - int[] rendererPartIndices = new int[rendererCount]; - Collider[] colliders = new Collider[colliderCount]; - Rigidbody[] rigidbodies = new Rigidbody[rigidbodyCount]; - ParticleSystem[] particleSystems = new ParticleSystem[particleSystemCount]; - CVRPickupObject[] pickups = new CVRPickupObject[pickupCount]; - CVRAttachment[] attachments = new CVRAttachment[attachmentCount]; - CVRInteractable[] interactables = new CVRInteractable[interactableCount]; - Behaviour[] behaviours = new Behaviour[behaviourCount]; - - for (int i = 0; i < rendererCount; i++) - { - renderers[i] = _tempRenderers[i]; - rendererPartIndices[i] = _tempRendererPartIndices[i]; - } - - for (int i = 0; i < colliderCount; i++) - colliders[i] = _tempColliders[i]; - - for (int i = 0; i < rigidbodyCount; i++) - rigidbodies[i] = _tempRigidbodies[i]; - - for (int i = 0; i < particleSystemCount; i++) - particleSystems[i] = _tempParticleSystems[i]; - - for (int i = 0; i < pickupCount; i++) - pickups[i] = _tempPickups[i]; - - for (int i = 0; i < attachmentCount; i++) - attachments[i] = _tempAttachments[i]; - - for (int i = 0; i < interactableCount; i++) - interactables[i] = _tempInteractables[i]; - - for (int i = 0; i < behaviourCount; i++) - behaviours[i] = _tempBehaviours[i]; - - // Calculate bounds per part in local space - for (int p = 0; p < partCount; p++) - { - PropPart part = parts[p]; - Transform partTransform = part.Transform; - if (!partTransform) - continue; - - _tempBoundsList.Clear(); - - for (int i = 0; i < rendererCount; i++) - { - if (rendererPartIndices[i] != p) - continue; - - // We are ignoring particle systems because their bounds are cooked. - // Their initial bounds seem leftover from whatever was last in-editor. - Renderer renderer = renderers[i]; - if (!renderer || renderer is ParticleSystemRenderer) - continue; - - Bounds worldBounds = renderer.bounds; - - // Convert bounds to part's local space - Vector3 localCenter = partTransform.InverseTransformPoint(worldBounds.center); - Vector3 localExtents = partTransform.InverseTransformVector(worldBounds.extents); - localExtents = new Vector3( - Mathf.Abs(localExtents.x), - Mathf.Abs(localExtents.y), - Mathf.Abs(localExtents.z) - ); - - Bounds localBounds = new Bounds(localCenter, localExtents * 2f); - _tempBoundsList.Add(localBounds); - } - - int boundsCount = _tempBoundsList.Count; - if (boundsCount > 0) - { - // Combine all renderer local bounds into one - Bounds combined = _tempBoundsList[0]; - for (int i = 1; i < boundsCount; i++) - combined.Encapsulate(_tempBoundsList[i]); - - // Include the prop root as an enforced bounds point. - // This makes some props which have their contents really far away at least visible on spawn - // even with an aggressive distance hider configuration. Ex: the big fish ship - if (p == 0 && ModSettings.EntryIncludePropRootInBounds.Value) - combined.Encapsulate(-partTransform.localPosition); - - // Create the BoundsCenter object - GameObject boundsCenterObj = new GameObject("_BoundsCenter"); - Transform boundsCenterObjTransform = boundsCenterObj.transform; - boundsCenterObjTransform.SetParent(partTransform, false); - boundsCenterObjTransform.localPosition = combined.center; - boundsCenterObjTransform.localScale = Vector3.one; - - float radius = 0f; - Vector3 c = combined.center; - Vector3 e = combined.extents; - radius = Mathf.Max(radius, (c + new Vector3( e.x, e.y, e.z) - c).magnitude); - radius = Mathf.Max(radius, (c + new Vector3( e.x, e.y, -e.z) - c).magnitude); - radius = Mathf.Max(radius, (c + new Vector3( e.x, -e.y, e.z) - c).magnitude); - radius = Mathf.Max(radius, (c + new Vector3( e.x, -e.y, -e.z) - c).magnitude); - radius = Mathf.Max(radius, (c + new Vector3(-e.x, e.y, e.z) - c).magnitude); - radius = Mathf.Max(radius, (c + new Vector3(-e.x, e.y, -e.z) - c).magnitude); - radius = Mathf.Max(radius, (c + new Vector3(-e.x, -e.y, e.z) - c).magnitude); - radius = Mathf.Max(radius, (c + new Vector3(-e.x, -e.y, -e.z) - c).magnitude); - - part.BoundsRadius = radius; - part.BoundsCenter = boundsCenterObjTransform; - } - } - - // Clear temp collections - _tempPartLookup.Clear(); - _tempBoundsList.Clear(); - _tempRenderers.Clear(); - _tempColliders.Clear(); - _tempRigidbodies.Clear(); - _tempParticleSystems.Clear(); - _tempPickups.Clear(); - _tempAttachments.Clear(); - _tempInteractables.Clear(); - _tempBehaviours.Clear(); - _tempRendererPartIndices.Clear(); - - var cache = new PropCache - { - PropData = propData, - Parts = parts, - PartCount = partCount, - Renderers = renderers, - RendererCount = rendererCount, - RendererEnabled = new bool[rendererCount], - Colliders = colliders, - ColliderCount = colliderCount, - ColliderEnabled = new bool[colliderCount], - Rigidbodies = rigidbodies, - RigidbodyCount = rigidbodyCount, - RigidbodyWasKinematic = new bool[rigidbodyCount], - ParticleSystems = particleSystems, - ParticleSystemCount = particleSystemCount, - PsWasPlaying = new bool[particleSystemCount], - PsPlayOnAwake = new bool[particleSystemCount], - PsTime = new float[particleSystemCount], - PsStartTime = new double[particleSystemCount], - PsRandomSeed = new uint[particleSystemCount], - PsUseAutoSeed = new bool[particleSystemCount], - Pickups = pickups, - PickupCount = pickupCount, - Attachments = attachments, - AttachmentCount = attachmentCount, - Interactables = interactables, - InteractableCount = interactableCount, - InteractableEnabled = new bool[interactableCount], - Behaviours = behaviours, - BehaviourCount = behaviourCount, - BehaviourEnabled = new bool[behaviourCount], - AnimWasEnabled = new bool[behaviourCount], - AnimStartTime = new double[behaviourCount], - AudioWasPlaying = new bool[behaviourCount], - AudioLoop = new bool[behaviourCount], - AudioPlayOnAwake = new bool[behaviourCount], - AudioStartDsp = new double[behaviourCount], - AudioStartTime = new float[behaviourCount], - IsHidden = false - }; - - _propCaches[propData] = cache; - _propCacheList.Add(cache); - } - - private static int FindOwningPartIndex(Transform transform) - { - Transform current = transform; - while (current) - { - if (_tempPartLookup.TryGetValue(current, out int partIndex)) - return partIndex; - current = current.parent; - } - return 0; // Default to root - } - - public static void OnPropDestroyed(CVRSyncHelper.PropData propData) - { - if (!_propCaches.TryGetValue(propData, out PropCache cache)) - return; - - if (cache.IsHidden) - ShowProp(cache); - - _propCaches.Remove(propData); - _propCacheList.Remove(cache); - } - - public static void Tick() - { - HiderMode mode = ModSettings.EntryPropDistanceHiderMode.Value; - if (mode == HiderMode.Disabled) - return; - - PlayerSetup playerSetup = PlayerSetup.Instance; - if (!playerSetup) - return; - - Camera activeCam = playerSetup.activeCam; - if (!activeCam) - return; - - Vector3 playerPos = activeCam.transform.position; - float hideDistance = ModSettings.EntryPropDistanceHiderDistance.Value; - float hysteresis = Hysteresis; - float hideDistSqr = hideDistance * hideDistance; - float showDist = hideDistance - hysteresis; - float showDistSqr = showDist * showDist; - - int cacheCount = _propCacheList.Count; - for (int i = 0; i < cacheCount; i++) - { - PropCache cache = _propCacheList[i]; - PropPart[] parts = cache.Parts; - int partCount = cache.PartCount; - - bool isHidden = cache.IsHidden; - float threshold = isHidden ? showDistSqr : hideDistSqr; - - bool anyPartValid = false; - bool anyPartWithinThreshold = false; - - // Debug gizmos for all parts - if (ModSettings.DebugPropMeasuredBounds.Value) - { - Quaternion playerRot = activeCam.transform.rotation; - Vector3 viewOffset = new Vector3(0f, -0.15f, 0f); - Vector3 debugLineStartPos = playerPos + playerRot * viewOffset; - for (int p = 0; p < partCount; p++) - { - PropPart part = parts[p]; - Transform boundsCenter = part.BoundsCenter; - - if (!boundsCenter) - continue; - - Vector3 boundsCenterPos = boundsCenter.position; - - // Compute world-space radius based on runtime scale (largest axis) - Vector3 lossyScale = boundsCenter.lossyScale; - float radius = part.BoundsRadius * Mathf.Max(lossyScale.x, Mathf.Max(lossyScale.y, lossyScale.z)); - - float distToCenter = Vector3.Distance(playerPos, boundsCenterPos); - float distToEdge = distToCenter - radius; - - // RuntimeGizmos DrawSphere is off by 2x - Color boundsColor = isHidden ? Color.red : Color.green; - RuntimeGizmos.DrawSphere(boundsCenterPos, radius * 2f, boundsColor, opacity: 0.2f); - RuntimeGizmos.DrawLineFromTo(debugLineStartPos, boundsCenterPos, 0.01f, boundsColor, opacity: 0.1f); - RuntimeGizmos.DrawText(boundsCenterPos, $"{distToEdge:F1}m {(isHidden ? "[Hidden]" : "[Visible]")}", 0.1f, boundsColor, opacity: 1f); - } - } - - for (int p = 0; p < partCount; p++) - { - PropPart part = parts[p]; - Transform boundsCenter = part.BoundsCenter; - - if (!boundsCenter) - continue; - - anyPartValid = true; - - // World-space radius with runtime scale - Vector3 lossyScale = boundsCenter.lossyScale; - float radius = part.BoundsRadius * Mathf.Max(lossyScale.x, Mathf.Max(lossyScale.y, lossyScale.z)); - - float distToCenter = Vector3.Distance(playerPos, boundsCenter.position); - float distToEdge = distToCenter - radius; - float distToEdgeSqr = distToEdge * distToEdge; - - if (distToEdge < 0f) - distToEdgeSqr = -distToEdgeSqr; - - if (distToEdgeSqr < threshold) - { - anyPartWithinThreshold = true; - break; - } - } - - if (!anyPartValid) - { - _toRemove.Add(cache.PropData); - continue; - } - - if (!isHidden && !anyPartWithinThreshold) - { - HideProp(cache); - } - else if (isHidden && anyPartWithinThreshold) - { - ShowProp(cache); - } - } - - int removeCount = _toRemove.Count; - if (removeCount > 0) - { - for (int i = 0; i < removeCount; i++) - { - CVRSyncHelper.PropData propData = _toRemove[i]; - if (_propCaches.TryGetValue(propData, out PropCache cache)) - { - _propCaches.Remove(propData); - _propCacheList.Remove(cache); - } - } - _toRemove.Clear(); - } - } - - private static void HideProp(PropCache cache) - { - cache.IsHidden = true; - - double dsp = AudioSettings.dspTime; - double time = Time.timeAsDouble; - - // Drop all pickups first to avoid race conditions - CVRPickupObject[] pickups = cache.Pickups; - int pickupCount = cache.PickupCount; - for (int i = 0; i < pickupCount; i++) - { - CVRPickupObject pickup = pickups[i]; - if (!pickup) continue; - pickup.ControllerRay = null; - } - - // Deattach all attachments before disabling - CVRAttachment[] attachments = cache.Attachments; - int attachmentCount = cache.AttachmentCount; - for (int i = 0; i < attachmentCount; i++) - { - CVRAttachment attachment = attachments[i]; - if (!attachment) continue; - attachment.DeAttach(); - } - - // Disable interactables before other behaviours - CVRInteractable[] interactables = cache.Interactables; - bool[] interactableEnabled = cache.InteractableEnabled; - int interactableCount = cache.InteractableCount; - for (int i = 0; i < interactableCount; i++) - { - CVRInteractable interactable = interactables[i]; - if (!interactable) continue; - interactableEnabled[i] = interactable.enabled; - interactable.enabled = false; - } - - // Snapshot and disable renderers - Renderer[] renderers = cache.Renderers; - bool[] rendererEnabled = cache.RendererEnabled; - int rendererCount = cache.RendererCount; - for (int i = 0; i < rendererCount; i++) - { - Renderer renderer = renderers[i]; - if (!renderer) continue; - rendererEnabled[i] = renderer.enabled; - renderer.enabled = false; - } - - // Snapshot and disable colliders - Collider[] colliders = cache.Colliders; - bool[] colliderEnabled = cache.ColliderEnabled; - int colliderCount = cache.ColliderCount; - for (int i = 0; i < colliderCount; i++) - { - Collider collider = colliders[i]; - if (!collider) continue; - colliderEnabled[i] = collider.enabled; - collider.enabled = false; - } - - // Snapshot and set rigidbodies to kinematic - Rigidbody[] rigidbodies = cache.Rigidbodies; - bool[] rigidbodyWasKinematic = cache.RigidbodyWasKinematic; - int rigidbodyCount = cache.RigidbodyCount; - for (int i = 0; i < rigidbodyCount; i++) - { - Rigidbody rb = rigidbodies[i]; - if (!rb) continue; - rigidbodyWasKinematic[i] = rb.isKinematic; - rb.isKinematic = true; - } - - // Snapshot and stop particle systems - ParticleSystem[] particleSystems = cache.ParticleSystems; - bool[] psWasPlaying = cache.PsWasPlaying; - bool[] psPlayOnAwake = cache.PsPlayOnAwake; - float[] psTime = cache.PsTime; - double[] psStartTime = cache.PsStartTime; - uint[] psRandomSeed = cache.PsRandomSeed; - bool[] psUseAutoSeed = cache.PsUseAutoSeed; - int particleSystemCount = cache.ParticleSystemCount; - for (int i = 0; i < particleSystemCount; i++) - { - ParticleSystem ps = particleSystems[i]; - if (!ps) continue; - - var main = ps.main; - psPlayOnAwake[i] = main.playOnAwake; - - if (ps.isPlaying) - { - psWasPlaying[i] = true; - psTime[i] = ps.time; - psStartTime[i] = time; - } - else - { - psWasPlaying[i] = false; - } - - psUseAutoSeed[i] = ps.useAutoRandomSeed; - psRandomSeed[i] = ps.randomSeed; - - main.playOnAwake = false; - ps.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); - } - - // Snapshot and disable behaviours - Behaviour[] behaviours = cache.Behaviours; - bool[] behaviourEnabled = cache.BehaviourEnabled; - bool[] animWasEnabled = cache.AnimWasEnabled; - double[] animStartTime = cache.AnimStartTime; - bool[] audioWasPlaying = cache.AudioWasPlaying; - bool[] audioLoop = cache.AudioLoop; - bool[] audioPlayOnAwake = cache.AudioPlayOnAwake; - double[] audioStartDsp = cache.AudioStartDsp; - float[] audioStartTime = cache.AudioStartTime; - int behaviourCount = cache.BehaviourCount; - - for (int i = 0; i < behaviourCount; i++) - { - Behaviour b = behaviours[i]; - if (!b) continue; - - behaviourEnabled[i] = b.enabled; - - if (!b.enabled) continue; - - if (b is Animator animator) - { - animWasEnabled[i] = true; - animStartTime[i] = time; - animator.enabled = false; - continue; - } - - if (b is AudioSource audio) - { - audioLoop[i] = audio.loop; - audioPlayOnAwake[i] = audio.playOnAwake; - - if (audio.isPlaying) - { - audioWasPlaying[i] = true; - audioStartTime[i] = audio.time; - audioStartDsp[i] = dsp - audio.time; - } - else - { - audioWasPlaying[i] = false; - } - - audio.Stop(); - audio.playOnAwake = false; - audio.enabled = false; - continue; - } - - b.enabled = false; - } - } - - private static void ShowProp(PropCache cache) - { - double dsp = AudioSettings.dspTime; - double time = Time.timeAsDouble; - - cache.IsHidden = false; - - // Restore renderers - Renderer[] renderers = cache.Renderers; - bool[] rendererEnabled = cache.RendererEnabled; - int rendererCount = cache.RendererCount; - for (int i = 0; i < rendererCount; i++) - { - Renderer renderer = renderers[i]; - if (!renderer) continue; - renderer.enabled = rendererEnabled[i]; - } - - // Restore colliders - Collider[] colliders = cache.Colliders; - bool[] colliderEnabled = cache.ColliderEnabled; - int colliderCount = cache.ColliderCount; - for (int i = 0; i < colliderCount; i++) - { - Collider collider = colliders[i]; - if (!collider) continue; - collider.enabled = colliderEnabled[i]; - } - - // Restore rigidbodies - Rigidbody[] rigidbodies = cache.Rigidbodies; - bool[] rigidbodyWasKinematic = cache.RigidbodyWasKinematic; - int rigidbodyCount = cache.RigidbodyCount; - for (int i = 0; i < rigidbodyCount; i++) - { - Rigidbody rb = rigidbodies[i]; - if (!rb) continue; - rb.isKinematic = rigidbodyWasKinematic[i]; - } - - // Restore particle systems - ParticleSystem[] particleSystems = cache.ParticleSystems; - bool[] psWasPlaying = cache.PsWasPlaying; - bool[] psPlayOnAwake = cache.PsPlayOnAwake; - float[] psTime = cache.PsTime; - double[] psStartTime = cache.PsStartTime; - uint[] psRandomSeed = cache.PsRandomSeed; - bool[] psUseAutoSeed = cache.PsUseAutoSeed; - int particleSystemCount = cache.ParticleSystemCount; - for (int i = 0; i < particleSystemCount; i++) - { - ParticleSystem ps = particleSystems[i]; - if (!ps) continue; - - if (psWasPlaying[i]) - { - ps.useAutoRandomSeed = false; - ps.randomSeed = psRandomSeed[i]; - - float originalTime = psTime[i]; - float elapsed = (float)(time - psStartTime[i]); - float simulateTime = originalTime + elapsed; - - ps.Simulate( - simulateTime, - withChildren: true, - restart: true, - fixedTimeStep: false - ); - - ps.Play(true); - - ps.useAutoRandomSeed = psUseAutoSeed[i]; - } - - var main = ps.main; - main.playOnAwake = psPlayOnAwake[i]; - } - - // Restore behaviours - Behaviour[] behaviours = cache.Behaviours; - bool[] behaviourEnabled = cache.BehaviourEnabled; - bool[] animWasEnabled = cache.AnimWasEnabled; - double[] animStartTime = cache.AnimStartTime; - bool[] audioWasPlaying = cache.AudioWasPlaying; - bool[] audioLoop = cache.AudioLoop; - bool[] audioPlayOnAwake = cache.AudioPlayOnAwake; - double[] audioStartDsp = cache.AudioStartDsp; - int behaviourCount = cache.BehaviourCount; - - for (int i = 0; i < behaviourCount; i++) - { - Behaviour b = behaviours[i]; - if (!b) continue; - - bool wasEnabled = behaviourEnabled[i]; - - if (b is Animator animator) - { - animator.enabled = wasEnabled; - - if (wasEnabled && animWasEnabled[i]) - { - float delta = (float)(time - animStartTime[i]); - if (delta > 0f) - animator.Update(delta); - } - - continue; - } - - if (b is AudioSource audio) - { - audio.enabled = wasEnabled; - - if (wasEnabled && audioWasPlaying[i] && audio.clip) - { - double elapsed = dsp - audioStartDsp[i]; - float clipLen = audio.clip.length; - - float t = audioLoop[i] ? (float)(elapsed % clipLen) : (float)elapsed; - if (t < clipLen) - { - audio.time = t; - audio.Play(); - } - } - - audio.playOnAwake = audioPlayOnAwake[i]; - continue; - } - - b.enabled = wasEnabled; - } - - // Restore interactables after other behaviours - CVRInteractable[] interactables = cache.Interactables; - bool[] interactableEnabled = cache.InteractableEnabled; - int interactableCount = cache.InteractableCount; - for (int i = 0; i < interactableCount; i++) - { - CVRInteractable interactable = interactables[i]; - if (!interactable) continue; - interactable.enabled = interactableEnabled[i]; - } - } - - public static void RefreshAll() - { - int count = _propCacheList.Count; - for (int i = 0; i < count; i++) - { - PropCache cache = _propCacheList[i]; - if (cache.IsHidden) - ShowProp(cache); - } - } - - public static void Clear() - { - RefreshAll(); - _propCaches.Clear(); - _propCacheList.Clear(); - } -}*/ \ No newline at end of file diff --git a/PropsButBetter/PropsButBetter/QuickMenuPropList.cs b/PropsButBetter/PropsButBetter/QuickMenuPropList.cs deleted file mode 100644 index e23ae5a..0000000 --- a/PropsButBetter/PropsButBetter/QuickMenuPropList.cs +++ /dev/null @@ -1,440 +0,0 @@ -using ABI_RC.Core.InteractionSystem; -using ABI_RC.Core.IO; -using ABI_RC.Core.Player; -using ABI_RC.Core.Util; -using ABI_RC.Systems.UI.UILib; -using ABI_RC.Systems.UI.UILib.UIObjects; -using ABI_RC.Systems.UI.UILib.UIObjects.Components; -using UnityEngine; - -namespace NAK.PropsButBetter; - -public static class QuickMenuPropList -{ - public const int TotalPropListModes = 3; - public enum PropListMode - { - AllProps, - MyProps, - History, - } - - private static PropListMode _currentPropListMode = PropListMode.AllProps; - - private static string _propListCategoryName; - private static string _propsListPopulatedText; - private static string _propListEmptyText; - - private static Page _page; - private static Category _actionsCategory; - private static Category _propListCategory; - - private static Button _enterDeleteModeButton; - private static Button _deleteMyPropsButton; - private static Button _deleteOthersPropsButton; - private static Button _cyclePropListModeButton; - private static TextBlock _propListTextBlock; - - private static UndoRedoButtons _undoRedoButtons; - private static ScheduledJob _updateJob; - - private static DateTime _lastTabChangedTime = DateTime.Now; - private static string _rootPageElementID; - private static bool _isOurTabOpened; - - public static void BuildUI() - { - QuickMenuAPI.OnTabChange += OnTabChange; - - _page = Page.GetOrCreatePage(nameof(PropsButBetter), nameof(QuickMenuPropList), isRootPage: true, "PropsButBetter-rubiks-cube"); - _page.MenuTitle = "Prop List"; - _page.MenuSubtitle = "You can see all Props currently spawned in here!"; - _page.OnPageOpen += OnPageOpened; - _page.OnPageClosed += OnPageClosed; - - _rootPageElementID = _page.ElementID; - - _actionsCategory = _page.AddCategory("Quick Actions", false, false); - - _undoRedoButtons = new UndoRedoButtons(); - _undoRedoButtons.OnUndo += OnUndo; - _undoRedoButtons.OnRedo += OnRedo; - - _enterDeleteModeButton = _actionsCategory.AddButton("Delete Mode", "PropsButBetter-wand", "Enters Prop delete mode."); - _enterDeleteModeButton.OnPress += OnDeleteMode; - - _deleteMyPropsButton = _actionsCategory.AddButton("Delete My Props", "PropsButBetter-remove", "Deletes all Props spawned by you. This will remove them for everyone."); - _deleteMyPropsButton.OnPress += OnDeleteMyProps; - - _deleteOthersPropsButton = _actionsCategory.AddButton("Remove Others Props", "PropsButBetter-remove", "Removes all Props spawned by other players only for you. This does not delete them for everyone."); - _deleteOthersPropsButton.OnPress += OnRemoveOthersProps; - - _cyclePropListModeButton = _actionsCategory.AddButton("Cycle Prop List Mode", "PropsButBetter-rubiks-cube", "Cycles the Prop List display mode."); - _cyclePropListModeButton.OnPress += OnCyclePropListMode; - - _propListCategory = _page.AddCategory("All Props", true, true); - _propListTextBlock = _propListCategory.AddTextBlock(string.Empty); - - SetPropListMode(ModSettings.HiddenPropListMode.Value); - } - - private static void OnPageOpened() - { - _undoRedoButtons.SetUndoHidden(false); - _undoRedoButtons.SetRedoHidden(false); - } - - private static void OnPageClosed() - { - _undoRedoButtons.SetUndoHidden(true); - _undoRedoButtons.SetRedoHidden(true); - } - - private static void OnPageUpdate() - { - // Don't run while the menu is closed, it's needless - if (!CVR_MenuManager.Instance.IsViewShown) - return; - - ForceUndoRedoButtonUpdate(); - ForcePropListUpdate(); - } - - private static void ForceUndoRedoButtonUpdate() - { - _undoRedoButtons.SetUndoDisabled(!PropHelper.CanUndo()); - _undoRedoButtons.SetRedoDisabled(!PropHelper.CanRedo()); - } - - private static void ForcePropListUpdate() - { - if (_currentPropListMode == PropListMode.History) - UpdateHistoryPropList(); - else - UpdateLivePropList(); - } - - private static readonly Dictionary _entries = new(); - private static readonly Stack _removalBuffer = new(); - - private static int[] _entrySiblingIndices; - private static int _currentLiveUpdateCycle; - private static int _lastPropCount; - - private static void UpdateLivePropList() - { - List props; - switch (_currentPropListMode) - { - case PropListMode.AllProps: props = CVRSyncHelper.Props; break; - case PropListMode.MyProps: props = PropHelper.MyProps; break; - case PropListMode.History: - default: return; - } - - int propCount = props.Count; - - bool hasPropCountChanged = propCount != _lastPropCount; - bool needsRunRemovalPass = _lastPropCount > propCount; - bool noProps = propCount == 0; - - if (hasPropCountChanged) - { - _lastPropCount = propCount; - _propListCategory.CategoryName = $"{_propListCategoryName} ({propCount})"; - - if (noProps) - { - ClearPropList(); - _propListTextBlock.Text = _propListEmptyText; - } - else - { - // Resize our arrays - Array.Resize(ref _entrySiblingIndices, propCount); - for (int i = 0; i < propCount; i++) _entrySiblingIndices[i] = i; // Reset order for sort - _propListTextBlock.Text = _propsListPopulatedText; - } - } - - if (noProps) - { - // No need to continue update - return; - } - - // Sort props by distance - if (propCount > 1) - { - Vector3 playerPos = PlayerSetup.Instance.activeCam.transform.position; - DistanceComparerStruct comparer = new(playerPos, props); - Array.Sort(_entrySiblingIndices, 0, propCount, comparer); - } - - // Increment the live update cycle count - _currentLiveUpdateCycle += 1; - - // Sort or create the menu entries we need - int index = 1; // Leave the no props text area as the first child - for (int i = 0; i < propCount; i++) - { - var prop = props[_entrySiblingIndices[i]]; - string id = prop.InstanceId; - - if (!_entries.TryGetValue(id, out var entry)) - { - string username = CVRPlayerManager.Instance.TryGetPlayerName(prop.SpawnedBy); - entry = new PropListEntry( - id, - prop.ObjectId, - prop.ContentMetadata.AssetName, - username, - _propListCategory - ); - _entries[id] = entry; - } - - // Set last updated cycle and sort the menu entry - entry.LastUpdatedCycle = _currentLiveUpdateCycle; - entry.SetChildIndexIfNeeded(index++); - } - - if (needsRunRemovalPass) - { - // Iterate all entries now and remove all which did not get fishy flipped - foreach ((string instanceId, PropListEntry entry) in _entries) - { - if (entry.LastUpdatedCycle != _currentLiveUpdateCycle) - { - _removalBuffer.Push(instanceId); - entry.Destroy(); - } - } - - // Remove all which have been scheduled for death - int toRemoveCount = _removalBuffer.Count; - for (int i = 0; i < toRemoveCount; i++) - { - string toRemove = _removalBuffer.Pop(); - _entries.Remove(toRemove); - } - } - } - - private static void UpdateHistoryPropList() - { - int historyCount = PropHelper.SpawnedThisSession.Count; - - bool hasPropCountChanged = historyCount != _lastPropCount; - bool noProps = historyCount == 0; - - if (hasPropCountChanged) - { - _lastPropCount = historyCount; - _propListCategory.CategoryName = $"{_propListCategoryName} ({historyCount})"; - - if (noProps) - { - ClearPropList(); - _propListTextBlock.Text = _propListEmptyText; - } - else - { - _propListTextBlock.Text = _propsListPopulatedText; - - } - } - - if (noProps) - { - // No need to continue update - return; - } - - // Increment the live update cycle count - _currentLiveUpdateCycle += 1; - - // Process history entries in order (newest first since list is already ordered that way) - int index = 1; // Leave the no props text area as the first child - for (int i = 0; i < historyCount; i++) - { - var historyData = PropHelper.SpawnedThisSession[i]; - string id = historyData.InstanceId; - - if (!_entries.TryGetValue(id, out var entry)) - { - entry = new PropListEntry( - historyData.InstanceId, - historyData.PropId, - historyData.PropName, - historyData.SpawnerName, - _propListCategory - ); - _entries[id] = entry; - } - - // Update destroyed state - entry.SetIsDestroyed(historyData.IsDestroyed); - - // Set last updated cycle and sort the menu entry - entry.LastUpdatedCycle = _currentLiveUpdateCycle; - entry.SetChildIndexIfNeeded(index++); - } - - // Remove entries that are no longer in history - foreach ((string instanceId, PropListEntry entry) in _entries) - { - if (entry.LastUpdatedCycle != _currentLiveUpdateCycle) - { - _removalBuffer.Push(instanceId); - entry.Destroy(); - } - } - - // Remove all which have been scheduled for death - int toRemoveCount = _removalBuffer.Count; - for (int i = 0; i < toRemoveCount; i++) - { - string toRemove = _removalBuffer.Pop(); - _entries.Remove(toRemove); - } - } - - private static void ClearPropList() - { - _lastPropCount = -1; // Forces rebuild of prop list - foreach (PropListEntry entry in _entries.Values) entry.Destroy(); - _entries.Clear(); - } - - private static void RebuildPropList() - { - ClearPropList(); - ForcePropListUpdate(); - } - - private readonly struct DistanceComparerStruct(Vector3 playerPos, IReadOnlyList props) : IComparer - { - public int Compare(int a, int b) - { - var pa = props[a]; - var pb = props[b]; - - float dx = pa.PositionX - playerPos.x; - float dy = pa.PositionY - playerPos.y; - float dz = pa.PositionZ - playerPos.z; - float da = dx * dx + dy * dy + dz * dz; - - dx = pb.PositionX - playerPos.x; - dy = pb.PositionY - playerPos.y; - dz = pb.PositionZ - playerPos.z; - float db = dx * dx + dy * dy + dz * dz; - - return da.CompareTo(db); - } - } - - private static void OnTabChange(string newTab, string previousTab) - { - bool isOurTabOpened = newTab == _rootPageElementID; - - // Check for change - if (isOurTabOpened != _isOurTabOpened) - { - _isOurTabOpened = isOurTabOpened; - if (_isOurTabOpened) - { - if (_updateJob == null) _updateJob = BetterScheduleSystem.AddJob(OnPageUpdate, 0f, 1f); - OnPageUpdate(); - } - else - { - if (_updateJob != null) BetterScheduleSystem.RemoveJob(_updateJob); - _updateJob = null; - } - } - - if (!_isOurTabOpened) return; - - TimeSpan timeDifference = DateTime.Now - _lastTabChangedTime; - if (timeDifference.TotalSeconds <= 0.5) - { - OnDeleteMode(); - return; - } - _lastTabChangedTime = DateTime.Now; - } - - private static void OnUndo() - { - PropHelper.UndoProp(); - ForceUndoRedoButtonUpdate(); - } - - private static void OnRedo() - { - PropHelper.RedoProp(); - ForceUndoRedoButtonUpdate(); - } - - private static void OnDeleteMode() - { - PlayerSetup.Instance.ToggleDeleteMode(); - } - - private static void OnDeleteMyProps() - { - PropHelper.RemoveMyProps(); - - // Force a page update - ForcePropListUpdate(); - } - - private static void OnRemoveOthersProps() - { - PropHelper.RemoveOthersProps(); - - // Force a page update - ForcePropListUpdate(); - } - - private static void OnCyclePropListMode() - => CyclePropListMode(); - - public static void CyclePropListMode() - { - int nextMode = (int)_currentPropListMode + 1; - SetPropListMode((PropListMode)(nextMode < 0 ? TotalPropListModes - 1 : nextMode % TotalPropListModes)); - } - - public static void SetPropListMode(PropListMode mode) - { - _currentPropListMode = mode; - ModSettings.HiddenPropListMode.Value = mode; - - switch (_currentPropListMode) - { - case PropListMode.AllProps: - _propListCategoryName = "All Props"; - _propsListPopulatedText = "These Props are sorted by distance to you."; - _propListEmptyText = "No Props are spawned in the World."; - _cyclePropListModeButton.ButtonIcon = "PropsButBetter-rubiks-cube-eye"; - break; - case PropListMode.MyProps: - _propListCategoryName = "My Props"; - _propsListPopulatedText = "These Props are sorted by distance to you."; - _propListEmptyText = "You have not spawned any Props."; - _cyclePropListModeButton.ButtonIcon = "PropsButBetter-rubiks-cube-star"; - break; - case PropListMode.History: - _propListCategoryName = "Spawned This Session"; - _propsListPopulatedText = "Showing last 30 Props which have been spawned this session."; - _propListEmptyText = "No Props have been spawned this session."; - _cyclePropListModeButton.ButtonIcon = "PropsButBetter-rubiks-cube-clock"; - break; - } - - // Force rebuild of the props list - RebuildPropList(); - } -} \ No newline at end of file diff --git a/PropsButBetter/PropsButBetter/QuickMenuPropSelect.cs b/PropsButBetter/PropsButBetter/QuickMenuPropSelect.cs deleted file mode 100644 index 4916536..0000000 --- a/PropsButBetter/PropsButBetter/QuickMenuPropSelect.cs +++ /dev/null @@ -1,259 +0,0 @@ -using ABI_RC.Core; -using ABI_RC.Core.EventSystem; -using ABI_RC.Core.InteractionSystem; -using ABI_RC.Core.IO; -using ABI_RC.Core.IO.AssetManagement; -using ABI_RC.Core.Networking.API.Responses; -using ABI_RC.Core.Player; -using ABI_RC.Core.Savior; -using ABI_RC.Core.Util; -using ABI_RC.Core.Util.Encryption; -using ABI_RC.Systems.UI.UILib; -using ABI_RC.Systems.UI.UILib.Components; -using ABI_RC.Systems.UI.UILib.UIObjects; -using ABI_RC.Systems.UI.UILib.UIObjects.Components; -using ABI.CCK.Components; -using UnityEngine; - -namespace NAK.PropsButBetter; - -public static class QuickMenuPropSelect -{ - private static Page _page; - private static Category _contentInfoCategory; - private static ContentDisplay _contentDisplay; - - private static Category _contentActionsCategory; - private static Button _deletePropButton; - private static Button _selectPropButton; - private static Button _reloadPropButton; - private static Button _selectPlayerButton; - - // Page State - private static string _currentSelectedPropId; - private static CVRSyncHelper.PropData _currentSelectedPropData; - private static PedestalInfoResponse _currentSpawnableResponse; - - private static CancellationTokenSource _fetchPropDetailsCts; - - public static void BuildUI() - { - _page = Page.GetOrCreatePage(nameof(PropsButBetter), "Prop Info", isRootPage: false, noTab: true); - _page.OnPageClosed += () => _fetchPropDetailsCts?.Cancel(); - // _page.InPlayerlist = true; // TODO: Investigate removal of page forehead - - _contentInfoCategory = _page.AddCategory("Content Info", false, false); - _contentDisplay = new ContentDisplay(_contentInfoCategory); - - _contentActionsCategory = _page.AddCategory("Quick Actions", true, true); - - _selectPropButton = _contentActionsCategory.AddButton("Spawn Prop", "PropsButBetter-select", "Select the Prop for spawning."); - _selectPropButton.OnPress += OnSelectProp; - - _reloadPropButton = _contentActionsCategory.AddButton("Reload Prop", "PropsButBetter-reload", "Respawns the selected Prop."); - _reloadPropButton.OnPress += OnReloadProp; - - _deletePropButton = _contentActionsCategory.AddButton("Delete Prop", "PropsButBetter-remove", "Delete the selected Prop.", ButtonStyle.TextWithIcon); - _deletePropButton.OnPress += OnDeleteProp; - - _selectPlayerButton = _contentActionsCategory.AddButton("Select Spawner", "PropsButBetter-remove", "Select the spawner of the Prop.", ButtonStyle.FullSizeImage); - _selectPlayerButton.OnPress += OnSelectPlayer; - } - - public static void ListenForQM() - { - CVR_MenuManager.Instance.cohtmlView.View.RegisterForEvent("QuickMenuPropSelect-OpenDetails", (Action)OnOpenDetails); - } - - public static void ShowInfo(CVRSyncHelper.PropData propData) - { - if (propData == null) return; - - _currentSelectedPropData = propData; - - CVR_MenuManager.Instance.ToggleQuickMenu(true); - _page.OpenPage(false, false); - - _currentSelectedPropId = propData.ObjectId; - - AssetManagement.UgcMetadata metadata = propData.ContentMetadata; - - _page.MenuTitle = $"{metadata.AssetName}"; - _page.MenuSubtitle = $"Spawned by {CVRPlayerManager.Instance.TryGetPlayerName(propData.SpawnedBy)}"; - - // Reset stuff - _fetchPropDetailsCts?.Cancel(); - _fetchPropDetailsCts = new CancellationTokenSource(); - _currentSpawnableResponse = null; - _reloadPropButton.Disabled = false; - _deletePropButton.Disabled = false; - - // Display metadata immediately with placeholder image - _contentDisplay.SetContent(metadata); - - // Set player pfp on button - string spawnedBy = _currentSelectedPropData.SpawnedBy; - if (CVRPlayerManager.Instance.UserIdToPlayerEntity.TryGetValue(spawnedBy, out var playerEntity)) - { - _selectPlayerButton.ButtonIcon = playerEntity.ApiProfileImageUrl; - _selectPlayerButton.ButtonText = playerEntity.Username; - } - // Check if this is our own pro - else if (spawnedBy == MetaPort.Instance.ownerId) - { - _selectPlayerButton.ButtonIcon = PlayerSetup.Instance.AuthInfo.Image.ToString(); - _selectPlayerButton.ButtonText = PlayerSetup.Instance.AuthInfo.Username; - } - // Prop is spawned by a user we cannot see... likely blocked - else - { - _selectPlayerButton.ButtonIcon = string.Empty; - _selectPlayerButton.ButtonText = "Unknown Player"; - } - - // Keep disabled until fetch - _selectPropButton.ButtonTooltip = "Select the Prop for spawning."; - _selectPropButton.Disabled = true; - - // Fetch image and update display - CVRTools.Run(async () => - { - try - { - var response = await PedestalInfoBatchProcessor.QueuePedestalInfoRequest( - PedestalType.Prop, - _currentSelectedPropId, - skipDelayIfNotCached: true // Not expecting need for batched response here - ); - - _currentSpawnableResponse = response; - string spawnableImageUrl = ImageCache.QueueProcessImage(response.ImageUrl, fallback: response.ImageUrl); - bool isPermittedToSpawn = response.IsPublished || response.Permitted; - - RootLogic.Instance.MainThreadQueue.Enqueue(() => - { - if (_fetchPropDetailsCts.IsCancellationRequested) return; - // Update with image URL for crossfade and enable button - _contentDisplay.SetContent(metadata, spawnableImageUrl); - if (isPermittedToSpawn) - _selectPropButton.Disabled = false; - else - _selectPropButton.ButtonTooltip = "Lacking permission to spawn Prop."; - }); - } - catch (Exception ex) when (ex is OperationCanceledException) { } - }, _fetchPropDetailsCts.Token); - } - - private static void OnOpenDetails() - { - ViewManager.Instance.GetPropDetails(_currentSelectedPropId); - } - - private static void OnSelectProp() - { - if (_currentSpawnableResponse != null) - { - string imageUrl = ImageCache.QueueProcessImage(_currentSpawnableResponse.ImageUrl, fallback: _currentSpawnableResponse.ImageUrl); - PlayerSetup.Instance.SelectPropToSpawn(_currentSpawnableResponse.Id, imageUrl, _currentSpawnableResponse.Name); - } - } - - private static void OnSelectPlayer() - { - string spawnedBy = _currentSelectedPropData.SpawnedBy; - // Check if this is a remote player and they exist - if (CVRPlayerManager.Instance.TryGetConnected(spawnedBy)) - { - QuickMenuAPI.OpenPlayerListByUserID(spawnedBy); - } - // Check if this is ourselves - else if (spawnedBy == MetaPort.Instance.ownerId) - { - PlayerList.Instance.OpenPlayerActionPage(PlayerList.Instance._localUserObject); - } - // User is not real - else - { - ViewManager.Instance.RequestUserDetailsPage(spawnedBy); - ViewManager.Instance.TriggerPushNotification("Opened profile page as user was not found in Instance.", 2f); - } - } - - private static void OnDeleteProp() - { - CVRSpawnable spawnable = _currentSelectedPropData.Spawnable; - if (spawnable && !spawnable.IsSpawnedByAdmin()) - { - spawnable.Delete(); - - // Disable now unusable buttons - _reloadPropButton.Disabled = true; - _deletePropButton.Disabled = true; - } - } - - private static void OnReloadProp() - { - CVRSpawnable spawnable = _currentSelectedPropData.Spawnable; - if (spawnable.IsSpawnedByAdmin()) - return; // Can't call delete - - CVRSyncHelper.PropData oldData = _currentSelectedPropData; - - // If this prop has not yet fully reloaded do not attempt another reload - if (!oldData.Spawnable) return; - - // Find our cached task in the download manager - // TODO: Noticed issue, download manager doesn't keep track of cached items properly. - // We are comparing the asset id instead of instance id because the download manager doesn't cache - // multiple for the same asset id. - DownloadTask ourTask = null; - foreach ((string assetId, DownloadTask downloadTask) in CVRDownloadManager.Instance._cachedTasks) - { - if (downloadTask.Type != DownloadTask.ObjectType.Prop - || assetId != oldData.ObjectId) continue; - ourTask = downloadTask; - break; - } - if (ourTask == null) return; - - // Create new prop data from the old one - CVRSyncHelper.PropData newData = CVRSyncHelper.PropData.PropDataPool.GetObject(); - newData.CopyFrom(oldData); - - // Prep spawnable for partial delete - spawnable.SpawnedByMe = false; // Prevent telling GS about delete - spawnable.instanceId = null; // Prevent OnDestroy from recycling our new prop data later in frame - - // Destroy the prop - oldData.Recycle(); - - // Add our new prop data to sync helper - CVRSyncHelper.Props.Add(newData); - newData.CanFireDecommissionEvents = true; - - // Do mega jank - CVREncryptionRouter router = null; - string filePath = CacheManager.Instance.GetCachePath(newData.ContentMetadata.AssetId, ourTask.FileId); - if (!string.IsNullOrEmpty(filePath)) router = new CVREncryptionRouter(filePath, newData.ContentMetadata); - - PropQueueSystem.Instance.AddCoroutine( - CoroutineUtil.RunThrowingIterator( - CVRObjectLoader.Instance.InstantiateSpawnableFromBundle( - newData.ContentMetadata.AssetId, - newData.ContentMetadata.FileHash, - newData.InstanceId, - router, - newData.ContentMetadata.TagsData, - newData.ContentMetadata.CompatibilityVersion, - string.Empty, - newData.SpawnedBy), - Debug.LogError - ) - ); - - // Update backing selected prop data - _currentSelectedPropData = newData; - } -} \ No newline at end of file diff --git a/PropsButBetter/PropsButBetter/UIElements/ContentDisplay.cs b/PropsButBetter/PropsButBetter/UIElements/ContentDisplay.cs deleted file mode 100644 index 9bc8205..0000000 --- a/PropsButBetter/PropsButBetter/UIElements/ContentDisplay.cs +++ /dev/null @@ -1,119 +0,0 @@ -using System.Text; -using ABI_RC.Core; -using ABI_RC.Core.EventSystem; -using ABI_RC.Systems.UI.UILib.UIObjects; -using ABI_RC.Systems.UI.UILib.UIObjects.Components; -using ABI_RC.Systems.UI.UILib.UIObjects.Objects; - -namespace NAK.PropsButBetter; - -public class ContentDisplay -{ - private readonly CustomElement _element; - private readonly CustomEngineOnFunction _updateFunction; - - public ContentDisplay(Category parentCategory) - { - _element = new CustomElement( - """{"t": "div", "c": "col-12", "a":{"id":"CVRUI-QMUI-Custom-[UUID]"}}""", - ElementType.InCategoryElement, - parentCategory: parentCategory - ); - parentCategory.AddCustomElement(_element); - - _updateFunction = new CustomEngineOnFunction( - "updateContentDisplay", - """ - var elem = document.getElementById(elementId); - if(elem) { - elem.innerHTML = htmlContent; - } - """, - new Parameter("elementId", typeof(string), true, false), - new Parameter("htmlContent", typeof(string), true, false) - ); - - _element.AddEngineOnFunction(_updateFunction); - } - - public void SetContent(AssetManagement.UgcMetadata metadata, string imageUrl = "") - { - StringBuilder tagsBuilder = new StringBuilder(); - - void AddTag(bool condition, string tagName) - { - if (condition) - { - tagsBuilder.Append( - $"" + - $"{tagName}" - ); - } - } - - AddTag(metadata.TagsData.Gore, "Gore"); - AddTag(metadata.TagsData.Horror, "Horror"); - AddTag(metadata.TagsData.Jumpscare, "Jumpscare"); - AddTag(metadata.TagsData.Explicit, "Explicit"); - AddTag(metadata.TagsData.Suggestive, "Suggestive"); - AddTag(metadata.TagsData.Violence, "Violence"); - AddTag(metadata.TagsData.FlashingEffects, "Flashing Effects"); - AddTag(metadata.TagsData.LoudAudio, "Loud Audio"); - AddTag(metadata.TagsData.ScreenEffects, "Screen Effects"); - AddTag(metadata.TagsData.LongRangeAudio, "Long Range Audio"); - - if (tagsBuilder.Length == 0) - { - tagsBuilder.Append( - "No Tags" - ); - } - - string htmlContent = $@" -
-
- - -
- -
-
- Tags: -
- {tagsBuilder} -
-
- -
- File Size: - - {CVRTools.HumanReadableFilesize(metadata.FileSize)} - -
-
-
"; - - if (!RootLogic.Instance.IsOnMainThread()) - RootLogic.Instance.MainThreadQueue.Enqueue(() => _updateFunction.TriggerEvent(_element.ElementID, htmlContent)); - else - _updateFunction.TriggerEvent(_element.ElementID, htmlContent); - } -} \ No newline at end of file diff --git a/PropsButBetter/PropsButBetter/UIElements/GlobalButton.cs b/PropsButBetter/PropsButBetter/UIElements/GlobalButton.cs deleted file mode 100644 index 3880239..0000000 --- a/PropsButBetter/PropsButBetter/UIElements/GlobalButton.cs +++ /dev/null @@ -1,51 +0,0 @@ -using ABI_RC.Core.InteractionSystem; -using ABI_RC.Systems.UI.UILib.UIObjects.Components; - -namespace NAK.PropsButBetter; - -public class GlobalButton -{ - private static readonly Dictionary _buttonsByElementId = new(); - - private readonly CustomElement _element; - - public event Action OnPress; - - public bool Disabled - { - get => _element.Disabled; - set => _element.Disabled = value; - } - - public static void ListenForQM() - { - CVR_MenuManager.Instance.cohtmlView.View.BindCall("GlobalButton-Click", (Action)HandleButtonClick); - } - - public GlobalButton(string iconName, string tooltip, int x, int y, int width = 80, int height = 80) - { - _element = new CustomElement( - $$$"""{"c": "whatever-i-want", "s": [{"c": "icon", "a": {"style": "background-image: url('UILib/Images/{{{iconName}}}.png'); background-size: contain; background-repeat: no-repeat; width: 100%; height: 100%;"}}], "x": "GlobalButton-Click", "a": {"id": "CVRUI-QMUI-Custom-[UUID]", "style": "position: absolute; left: {{{x}}}px; top: {{{y}}}px; width: {{{width}}}px; height: {{{height}}}px; margin: 0;", "data-tooltip": "{{{tooltip}}}"}}""", - ElementType.GlobalElement - ); - _element.AddAction("GlobalButton-Click", "engine.call(\"GlobalButton-Click\", e.currentTarget.id);"); - _element.OnElementGenerated += OnElementGenerated; - } - - private void OnElementGenerated() - { - _buttonsByElementId[_element.ElementID] = this; - } - - private static void HandleButtonClick(string elementId) - { - if (_buttonsByElementId.TryGetValue(elementId, out GlobalButton button)) - button.OnPress?.Invoke(); - } - - public void Delete() - { - _buttonsByElementId.Remove(_element.ElementID); - _element.Delete(); - } -} \ No newline at end of file diff --git a/PropsButBetter/PropsButBetter/UIElements/PropListEntry.cs b/PropsButBetter/PropsButBetter/UIElements/PropListEntry.cs deleted file mode 100644 index 1fcb509..0000000 --- a/PropsButBetter/PropsButBetter/UIElements/PropListEntry.cs +++ /dev/null @@ -1,256 +0,0 @@ -using ABI_RC.Core; -using ABI_RC.Core.InteractionSystem; -using ABI_RC.Core.IO; -using ABI_RC.Core.Util; -using ABI_RC.Systems.UI.UILib.UIObjects; -using ABI_RC.Systems.UI.UILib.UIObjects.Components; -using ABI_RC.Systems.UI.UILib.UIObjects.Objects; - -namespace NAK.PropsButBetter; - -public class PropListEntry -{ - private readonly CustomElement _element; - private readonly CustomEngineOnFunction _updateFunction; - private readonly CustomEngineOnFunction _setChildIndexFunction; - private CancellationTokenSource _cts; - - private readonly string _instanceId; - private readonly string _propId; - private readonly string _propName; - private readonly string _spawnerUsername; - private string _currentImageUrl; - private int _childIndex; - private bool _isDestroyed; - - // Used to track if this entry is still needed or not by QuickMenuPropList - internal int LastUpdatedCycle; - - public PropListEntry(string instanceId, string contentId, string propName, string spawnerUsername, Category parentCategory) - { - _instanceId = instanceId; - _propId = contentId; - _propName = propName; - _spawnerUsername = spawnerUsername; - _currentImageUrl = string.Empty; - _isDestroyed = false; - _cts = new CancellationTokenSource(); - - _element = new CustomElement( - """{"t": "div", "c": "col-6", "a":{"id":"CVRUI-QMUI-Custom-[UUID]"}}""", - ElementType.InCategoryElement, - parentCategory: parentCategory - ); - - _updateFunction = new CustomEngineOnFunction( - "updatePropListEntry", - """ - var elem = document.getElementById(elementId); - if(elem) { - elem.innerHTML = htmlContent; - } - """, - new Parameter("elementId", typeof(string), true, false), - new Parameter("htmlContent", typeof(string), true, false) - ); - - _setChildIndexFunction = new CustomEngineOnFunction( - "setPropListEntryIndex", - """ - var elem = document.getElementById(elementId2); - if(elem && elem.parentNode) { - var parent = elem.parentNode; - var children = Array.from(parent.children); - if(index < children.length && children[index] !== elem) { - parent.insertBefore(elem, children[index]); - } else if(index >= children.length) { - parent.appendChild(elem); - } - } - """, - new Parameter("elementId2", typeof(string), true, false), - new Parameter("index", typeof(int), true, false) - ); - - _element.AddEngineOnFunction(_updateFunction); - _element.AddEngineOnFunction(_setChildIndexFunction); - _element.OnElementGenerated += UpdateDisplay; - parentCategory.AddCustomElement(_element); - - FetchImageAsync(contentId); - } - - private async void FetchImageAsync(string contentId) - { - try - { - var response = await PedestalInfoBatchProcessor.QueuePedestalInfoRequest(PedestalType.Prop, contentId); - - if (_cts.IsCancellationRequested) return; - - string imageUrl = ImageCache.QueueProcessImage(response.ImageUrl, fallback: response.ImageUrl); - SetImage(imageUrl); - } - catch (OperationCanceledException) { } - catch (Exception) { } - } - - public void SetImage(string imageUrl) - { - _currentImageUrl = imageUrl; - UpdateDisplay(); - } - - public void SetIsDestroyed(bool isDestroyed) - { - if (_isDestroyed != isDestroyed) - { - _isDestroyed = isDestroyed; - UpdateDisplay(); - } - } - - public void SetChildIndexIfNeeded(int index) - { - if (_childIndex == index) return; - _childIndex = index; - - if (!RootLogic.Instance.IsOnMainThread()) - RootLogic.Instance.MainThreadQueue.Enqueue(() => _setChildIndexFunction.TriggerEvent(_element.ElementID, index)); - else - _setChildIndexFunction.TriggerEvent(_element.ElementID, index); - } - - public void Destroy() - { - _cts?.Cancel(); - _cts?.Dispose(); - _cts = null; - _element.Delete(); - } - - private void UpdateDisplay() - { - const int rowHeight = 160; - const int imageSize = rowHeight - 32; - - string dimStyle = _isDestroyed ? "opacity: 0.4; filter: grayscale(0.6);" : ""; - string tooltipSuffix = _isDestroyed ? " (Despawned)" : ""; - - string imageHtml = $@" - - "; - - string htmlContent = $@" -
- -
-
- {imageHtml} -
-
- -
- -
- {_propName} -
- -
- Spawned By {_spawnerUsername} -
- -
-
"; - - if (!RootLogic.Instance.IsOnMainThread()) - RootLogic.Instance.MainThreadQueue.Enqueue(() => _updateFunction.TriggerEvent(_element.ElementID, htmlContent)); - else - _updateFunction.TriggerEvent(_element.ElementID, htmlContent); - } - - public static void ListenForQM() - { - CVR_MenuManager.Instance.cohtmlView.View.BindCall("PropListEntry-Selected", OnSelect); - } - - private static void OnSelect(string instanceId, string propId, bool isDestroyed) - { - // If the prop is destroyed, open the details page - if (isDestroyed) - { - ViewManager.Instance.GetPropDetails(propId); - return; - } - - // Otherwise show the live prop info - var propData = CVRSyncHelper.Props.Find(prop => prop.InstanceId == instanceId); - QuickMenuPropSelect.ShowInfo(propData); - } -} \ No newline at end of file diff --git a/PropsButBetter/PropsButBetter/UIElements/UndoRedoButtons.cs b/PropsButBetter/PropsButBetter/UIElements/UndoRedoButtons.cs deleted file mode 100644 index d194cef..0000000 --- a/PropsButBetter/PropsButBetter/UIElements/UndoRedoButtons.cs +++ /dev/null @@ -1,132 +0,0 @@ -using ABI_RC.Core.InteractionSystem; -using ABI_RC.Systems.UI.UILib; - -namespace NAK.PropsButBetter; - -public class UndoRedoButtons -{ - private const string UndoButtonId = "PropsButBetter-UndoButton"; - private const string RedoButtonId = "PropsButBetter-RedoButton"; - - public event Action OnUndo; - public event Action OnRedo; - - private static UndoRedoButtons _instance; - - public static void ListenForButtons() - { - CVR_MenuManager.Instance.cohtmlView.View.BindCall("UndoRedoButtons-Undo", (Action)HandleUndoStatic); - CVR_MenuManager.Instance.cohtmlView.View.BindCall("UndoRedoButtons-Redo", (Action)HandleRedoStatic); - } - - public UndoRedoButtons() - { - _instance = this; - QuickMenuAPI.OnMenuGenerated += OnMenuGenerate; - if (CVR_MenuManager.IsReadyStatic) GenerateCohtml(); - } - - private void OnMenuGenerate(CVR_MenuManager _) - => GenerateCohtml(); - - private void GenerateCohtml() - { - string script = $@" -(function() {{ - var root = document.getElementById('CVRUI-QMUI-Root'); - if (!root) return; - - // Remove existing buttons if they exist - var existingUndo = document.getElementById('{UndoButtonId}'); - if (existingUndo) existingUndo.remove(); - var existingRedo = document.getElementById('{RedoButtonId}'); - if (existingRedo) existingRedo.remove(); - - // Create undo button - var undoButton = document.createElement('div'); - undoButton.id = '{UndoButtonId}'; - undoButton.className = 'button'; - undoButton.setAttribute('data-tooltip', 'Undo last Prop spawn.'); - undoButton.setAttribute('onclick', ""engine.call('UndoRedoButtons-Undo');""); - undoButton.style.cssText = 'position: absolute; left: 900px; top: 25px; width: 140px; height: 140px; margin: 0; z-index: 9999; cursor: pointer;'; - - var undoIcon = document.createElement('div'); - undoIcon.style.cssText = 'background-image: url(""UILib/Images/PropsButBetter/PropsButBetter-undo.png""); background-size: contain; background-repeat: no-repeat; width: 100%; height: 100%; pointer-events: none;'; - undoButton.appendChild(undoIcon); - - // Create redo button - var redoButton = document.createElement('div'); - redoButton.id = '{RedoButtonId}'; - redoButton.className = 'button'; - redoButton.setAttribute('data-tooltip', 'Redo last Prop spawn.'); - redoButton.setAttribute('onclick', ""engine.call('UndoRedoButtons-Redo');""); - redoButton.style.cssText = 'position: absolute; left: 1060px; top: 25px; width: 140px; height: 140px; margin: 0; z-index: 9999; cursor: pointer;'; - - var redoIcon = document.createElement('div'); - redoIcon.style.cssText = 'background-image: url(""UILib/Images/PropsButBetter/PropsButBetter-redo.png""); background-size: contain; background-repeat: no-repeat; width: 100%; height: 100%; pointer-events: none;'; - redoButton.appendChild(redoIcon); - - // Append to root - root.appendChild(undoButton); - root.appendChild(redoButton); -}})(); -"; - CVR_MenuManager.Instance.cohtmlView.View._view.ExecuteScript(script); - SetUndoHidden(true); - SetRedoHidden(true); - } - - private static void HandleUndoStatic() - { - _instance?.OnUndo?.Invoke(); - } - - private static void HandleRedoStatic() - { - _instance?.OnRedo?.Invoke(); - } - - private bool _isUndoDisabled; - public void SetUndoDisabled(bool disabled) - { - if (_isUndoDisabled == disabled) return; - _isUndoDisabled = disabled; - if (!CVR_MenuManager.IsReadyStatic) return; - CVR_MenuManager.Instance.cohtmlView.View.InternalView.TriggerEvent("CVRUI-QMUI-SetDisabled", UndoButtonId, disabled); - } - - private bool _isRedoDisabled; - public void SetRedoDisabled(bool disabled) - { - if (_isRedoDisabled == disabled) return; - _isRedoDisabled = disabled; - if (!CVR_MenuManager.IsReadyStatic) return; - CVR_MenuManager.Instance.cohtmlView.View.InternalView.TriggerEvent("CVRUI-QMUI-SetDisabled", RedoButtonId, disabled); - } - - private bool _isUndoHidden; - public void SetUndoHidden(bool hidden) - { - if (_isUndoHidden == hidden) return; - _isUndoHidden = hidden; - if (!CVR_MenuManager.IsReadyStatic) return; - CVR_MenuManager.Instance.cohtmlView.View.InternalView.TriggerEvent("CVRUI-QMUI-SetHidden", UndoButtonId, hidden); - } - - private bool _isRedoHidden; - public void SetRedoHidden(bool hidden) - { - if (_isRedoHidden == hidden) return; - _isRedoHidden = hidden; - if (!CVR_MenuManager.IsReadyStatic) return; - CVR_MenuManager.Instance.cohtmlView.View.InternalView.TriggerEvent("CVRUI-QMUI-SetHidden", RedoButtonId, hidden); - } - - public void Cleanup() - { - QuickMenuAPI.OnMenuGenerated -= OnMenuGenerate; - if (!CVR_MenuManager.IsReadyStatic) return; - CVR_MenuManager.Instance.cohtmlView.View.InternalView.TriggerEvent("CVRUI-QMUI-DeleteElement", UndoButtonId); - CVR_MenuManager.Instance.cohtmlView.View.InternalView.TriggerEvent("CVRUI-QMUI-DeleteElement", RedoButtonId); - } -} \ No newline at end of file diff --git a/PropsButBetter/README.md b/PropsButBetter/README.md deleted file mode 100644 index 83db097..0000000 --- a/PropsButBetter/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# PropsButBetter - -Prop quality-of-life suite. Adds a few new ways to interact with and manage Props. - -### Whats included: - -- **A unified Prop List** - - Browse All Props, Your Props, or Session history in a clean, cyclable list. -- **Smarter Prop Selection** - - Clicking a Prop opens the Quick Menu with quick actions and useful info. -- **Undo / Redo for Props** - - Fix mistakes instantly without having to clear all. - - Desktop: CTRL+Z / CTRL+SHIFT+Z - - VR: Dedicated Undo / Redo buttons in the Props Quick Menu tab. -- **Subtle Prop SFX** - - Clean audio feedback when Props are spawned, removed, or respawned by you. -- **Placement Visualizer** - - Preview props before you've placed them while in Prop Select mode. - --# Desktop undo/redo keybinds, Prop SFX, and placement visualizer are configurable in Melon Preferences. - ---- - -Here is the block of text where I tell you this mod is not affiliated or endorsed by ABI. -https://documentation.abinteractive.net/official/legal/tos/#7-modding-our-games - -> This mod is an independent creation and is 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. \ No newline at end of file diff --git a/PropsButBetter/Resources/SFX/sfx_deny.wav b/PropsButBetter/Resources/SFX/sfx_deny.wav deleted file mode 100644 index 0a8b4ba..0000000 Binary files a/PropsButBetter/Resources/SFX/sfx_deny.wav and /dev/null differ diff --git a/PropsButBetter/Resources/SFX/sfx_redo.wav b/PropsButBetter/Resources/SFX/sfx_redo.wav deleted file mode 100644 index 41bb0cf..0000000 Binary files a/PropsButBetter/Resources/SFX/sfx_redo.wav and /dev/null differ diff --git a/PropsButBetter/Resources/SFX/sfx_spawn.wav b/PropsButBetter/Resources/SFX/sfx_spawn.wav deleted file mode 100644 index ab2aef3..0000000 Binary files a/PropsButBetter/Resources/SFX/sfx_spawn.wav and /dev/null differ diff --git a/PropsButBetter/Resources/SFX/sfx_undo.wav b/PropsButBetter/Resources/SFX/sfx_undo.wav deleted file mode 100644 index 84f0cf7..0000000 Binary files a/PropsButBetter/Resources/SFX/sfx_undo.wav and /dev/null differ diff --git a/PropsButBetter/Resources/SFX/sfx_warn.wav b/PropsButBetter/Resources/SFX/sfx_warn.wav deleted file mode 100644 index 8e96b57..0000000 Binary files a/PropsButBetter/Resources/SFX/sfx_warn.wav and /dev/null differ diff --git a/PropsButBetter/Resources/placeholder.png b/PropsButBetter/Resources/placeholder.png deleted file mode 100644 index 0d75334..0000000 Binary files a/PropsButBetter/Resources/placeholder.png and /dev/null differ diff --git a/PropsButBetter/Resources/redo.png b/PropsButBetter/Resources/redo.png deleted file mode 100644 index 61ab3bd..0000000 Binary files a/PropsButBetter/Resources/redo.png and /dev/null differ diff --git a/PropsButBetter/Resources/reload.png b/PropsButBetter/Resources/reload.png deleted file mode 100644 index 61f20fe..0000000 Binary files a/PropsButBetter/Resources/reload.png and /dev/null differ diff --git a/PropsButBetter/Resources/remove.png b/PropsButBetter/Resources/remove.png deleted file mode 100644 index 0f24d17..0000000 Binary files a/PropsButBetter/Resources/remove.png and /dev/null differ diff --git a/PropsButBetter/Resources/rubiks-cube-calender.png b/PropsButBetter/Resources/rubiks-cube-calender.png deleted file mode 100644 index 9b36a1e..0000000 Binary files a/PropsButBetter/Resources/rubiks-cube-calender.png and /dev/null differ diff --git a/PropsButBetter/Resources/rubiks-cube-clock.png b/PropsButBetter/Resources/rubiks-cube-clock.png deleted file mode 100644 index 6494f6e..0000000 Binary files a/PropsButBetter/Resources/rubiks-cube-clock.png and /dev/null differ diff --git a/PropsButBetter/Resources/rubiks-cube-eye.png b/PropsButBetter/Resources/rubiks-cube-eye.png deleted file mode 100644 index 4a73d36..0000000 Binary files a/PropsButBetter/Resources/rubiks-cube-eye.png and /dev/null differ diff --git a/PropsButBetter/Resources/rubiks-cube-star.png b/PropsButBetter/Resources/rubiks-cube-star.png deleted file mode 100644 index 2c9802b..0000000 Binary files a/PropsButBetter/Resources/rubiks-cube-star.png and /dev/null differ diff --git a/PropsButBetter/Resources/rubiks-cube.png b/PropsButBetter/Resources/rubiks-cube.png deleted file mode 100644 index 623e92f..0000000 Binary files a/PropsButBetter/Resources/rubiks-cube.png and /dev/null differ diff --git a/PropsButBetter/Resources/select.png b/PropsButBetter/Resources/select.png deleted file mode 100644 index 89716a6..0000000 Binary files a/PropsButBetter/Resources/select.png and /dev/null differ diff --git a/PropsButBetter/Resources/undo.png b/PropsButBetter/Resources/undo.png deleted file mode 100644 index 37905d3..0000000 Binary files a/PropsButBetter/Resources/undo.png and /dev/null differ diff --git a/PropsButBetter/Resources/wand.png b/PropsButBetter/Resources/wand.png deleted file mode 100644 index 93edcc9..0000000 Binary files a/PropsButBetter/Resources/wand.png and /dev/null differ diff --git a/PropsButBetter/format.json b/PropsButBetter/format.json deleted file mode 100644 index 6c4012e..0000000 --- a/PropsButBetter/format.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "_id": -1, - "name": "PropsButBetter", - "modversion": "1.0.0", - "gameversion": "2026r181", - "loaderversion": "0.7.2", - "modtype": "Mod", - "author": "NotAKidoS", - "description": "Prop quality-of-life suite. Adds a few new ways to interact with and manage Props.\n### Whats included:\n\n- **A unified Prop List**\n - Browse All Props, Your Props, or Session history in a clean, cyclable list.\n- **Smarter Prop Selection**\n - Clicking a Prop opens the Quick Menu with quick actions and useful info.\n- **Undo / Redo for Props**\n - Fix mistakes instantly without having to clear all.\n - Desktop: CTRL+Z / CTRL+SHIFT+Z\n - VR: Dedicated Undo / Redo buttons in the Props Quick Menu tab.\n- **Subtle Prop SFX**\n - Clean audio feedback when Props are spawned, removed, or respawned by you.\n- **Placement Visualizer**\n - Preview props before you've placed them while in Prop Select mode.\n\n-# Desktop undo/redo keybinds, Prop SFX, and placement visualizer are configurable in Melon Preferences.", - "searchtags": [ - "prop", - "qol", - "undo", - "redo", - "sounds", - "spawn", - "list" - ], - "requirements": [ - "None" - ], - "downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r49/PropsButBetter.dll", - "sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/PropsButBetter/", - "changelog": "- Initial release", - "embedcolor": "#f61963" -} \ No newline at end of file diff --git a/README.md b/README.md index 79bc57d..73175b8 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,6 @@ | [FuckToes](FuckToes/README.md) | Prevents VRIK from autodetecting toes in Halfbody or Fullbody. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/FuckToes.dll) | | [PlapPlapForAll](PlapPlapForAll/README.md) | Penetrator SFX mod which adds Noach's PlapPlap prefab to any detected DPS setup on avatars that do not already have it. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/PlapPlapForAll.dll) | | [PropLoadingHexagon](PropLoadingHexagon/README.md) | https://github.com/NotAKidoS/NAK_CVR_Mods/assets/37721153/a892c765-71c1-47f3-a781-bdb9b60ba117 | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/PropLoadingHexagon.dll) | -| [PropsButBetter](PropsButBetter/README.md) | Prop quality-of-life suite. Adds a few new ways to interact with and manage Props. | No Download | | [RCCVirtualSteeringWheel](RCCVirtualSteeringWheel/README.md) | Allows you to physically grab rigged RCC steering wheels in VR to provide steering input. No explicit setup required other than defining the Steering Wheel transform within the RCC component. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/RCCVirtualSteeringWheel.dll) | | [RelativeSyncJitterFix](RelativeSyncJitterFix/README.md) | Relative sync jitter fix is the single harmony patch that could not make it into the native release of RelativeSync. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/RelativeSyncJitterFix.dll) | | [ShareBubbles](ShareBubbles/README.md) | Share Bubbles! Allows you to drop down bubbles containing Avatars & Props. Requires both users to have the mod installed. Synced over Mod Network. | [Download](https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r48/ShareBubbles.dll) |