diff --git a/AvatarScale/AvatarScaleMod.csproj b/AvatarScale/AvatarScaleMod.csproj index ce04547..712ea9c 100644 --- a/AvatarScale/AvatarScaleMod.csproj +++ b/AvatarScale/AvatarScaleMod.csproj @@ -20,7 +20,10 @@ - + + + + @@ -28,9 +31,10 @@ $(MsBuildThisFileDirectory)\..\.ManagedLibs\BTKUILib.dll False + + $(MsBuildThisFileDirectory)\..\.ManagedLibs\ActionMenu.dll + False + - - - \ No newline at end of file diff --git a/AvatarScale/AvatarScaling/AvatarScaleManager.cs b/AvatarScale/AvatarScaling/AvatarScaleManager.cs index e9835dd..aa39e3b 100644 --- a/AvatarScale/AvatarScaling/AvatarScaleManager.cs +++ b/AvatarScale/AvatarScaling/AvatarScaleManager.cs @@ -1,5 +1,7 @@ using ABI_RC.Core.IO; using ABI_RC.Core.Player; +using ABI_RC.Core.Player.AvatarTracking; +using ABI_RC.Core.UI; using ABI_RC.Systems.GameEventSystem; using NAK.AvatarScaleMod.Components; using NAK.AvatarScaleMod.Networking; @@ -25,7 +27,7 @@ public class AvatarScaleManager : MonoBehaviour set { if (value != _settingUniversalScaling && value == false) - SetHeight(-1f); + ResetHeight(); _settingUniversalScaling = value; } @@ -54,13 +56,11 @@ public class AvatarScaleManager : MonoBehaviour _settingUniversalScaling = ModSettings.EntryUseUniversalScaling.Value; CVRGameEventSystem.Instance.OnConnected.AddListener(OnInstanceConnected); - //SchedulerSystem.AddJob(new SchedulerSystem.Job(ForceHeightUpdate), 0f, 10f, -1); } private void OnDestroy() { CVRGameEventSystem.Instance.OnConnected.RemoveListener(OnInstanceConnected); - //SchedulerSystem.RemoveJob(new SchedulerSystem.Job(ForceHeightUpdate)); } #endregion @@ -121,18 +121,25 @@ public class AvatarScaleManager : MonoBehaviour public void ResetHeight() { - if (!_settingUniversalScaling) + if (_localAvatarScaler == null) return; + + if (!_localAvatarScaler.IsHeightAdjustedFromInitial()) + return; + + CohtmlHud.Instance.ViewDropTextImmediate("(Local) AvatarScaleMod", "Avatar Scale Reset!", + "Universal Scaling is now disabled."); - if (_localAvatarScaler != null) - _localAvatarScaler.ResetHeight(); - ModNetwork.SendNetworkHeight(-1f); + SetHeight(-1f); } public float GetHeight() { if (_localAvatarScaler == null) - return -1f; + return PlayerAvatarPoint.defaultAvatarHeight; + + if (!_localAvatarScaler.IsHeightAdjustedFromInitial()) + return PlayerSetup.Instance.GetAvatarHeight(); return _localAvatarScaler.GetHeight(); } @@ -161,10 +168,7 @@ public class AvatarScaleManager : MonoBehaviour public bool IsHeightAdjustedFromInitial() { - if (_localAvatarScaler == null) - return false; - - return _localAvatarScaler.IsHeightAdjustedFromInitial(); + return _localAvatarScaler != null && _localAvatarScaler.IsHeightAdjustedFromInitial(); } #endregion @@ -174,9 +178,14 @@ public class AvatarScaleManager : MonoBehaviour public float GetNetworkHeight(string playerId) { if (_networkedScalers.TryGetValue(playerId, out NetworkScaler scaler)) - return scaler.GetHeight(); + if (scaler.IsHeightAdjustedFromInitial()) return scaler.GetHeight(); - //doesn't have mod, get from player avatar directly + //doesn't have mod or has no custom height, get from player avatar directly + CVRPlayerEntity playerEntity = CVRPlayerManager.Instance.NetworkPlayers.Find((players) => players.Uuid == playerId); + if (playerEntity != null && playerEntity.PuppetMaster != null) + return playerEntity.PuppetMaster.GetAvatarHeight(); + + // player is invalid??? return -1f; } diff --git a/AvatarScale/AvatarScaling/Components/BaseScaler.cs b/AvatarScale/AvatarScaling/Components/BaseScaler.cs index 642a9a9..4f108ee 100644 --- a/AvatarScale/AvatarScaling/Components/BaseScaler.cs +++ b/AvatarScale/AvatarScaling/Components/BaseScaler.cs @@ -62,7 +62,8 @@ public class BaseScaler : MonoBehaviour public void SetTargetHeight(float height) { - if (Math.Abs(height - _targetHeight) < float.Epsilon) + if (_isHeightAdjustedFromInitial + && Math.Abs(height - _targetHeight) < float.Epsilon) return; if (height < float.Epsilon) @@ -70,7 +71,7 @@ public class BaseScaler : MonoBehaviour ResetHeight(); return; } - + if (!_isHeightAdjustedFromInitial) _legacyAnimationScale = Vector3.zero; diff --git a/AvatarScale/AvatarScaling/Components/LocalScaler.cs b/AvatarScale/AvatarScaling/Components/LocalScaler.cs index 68146e4..27ba198 100644 --- a/AvatarScale/AvatarScaling/Components/LocalScaler.cs +++ b/AvatarScale/AvatarScaling/Components/LocalScaler.cs @@ -1,4 +1,6 @@ using ABI_RC.Core.Player; +using ABI_RC.Core.UI; +using ABI.CCK.Components; using NAK.AvatarScaleMod.AvatarScaling; using UnityEngine; @@ -16,7 +18,7 @@ public class LocalScaler : BaseScaler _isAvatarInstantiated = false; _isHeightAdjustedFromInitial = false; } - + #endregion #region Overrides @@ -25,7 +27,7 @@ public class LocalScaler : BaseScaler { if (avatarObject == null) return; - + base.OnAvatarInstantiated(avatarObject, initialHeight, initialScale); await FindComponentsOfTypeAsync(scalableComponentTypes); @@ -83,6 +85,7 @@ public class LocalScaler : BaseScaler _legacyAnimationScale = _avatarTransform.localScale; AvatarScaleMod.Logger.Msg("AnimationClip-based avatar scaling detected. Disabling Universal Scaling."); + CohtmlHud.Instance.ViewDropTextImmediate("(Local) AvatarScaleMod", "Avatar Scale Changed!", "Universal Scaling is now disabled in favor of built-in avatar scaling."); AvatarScaleManager.Instance.ResetHeight(); // disable mod, user used a scale slider return true; } diff --git a/AvatarScale/Input/DebugKeybinds.cs b/AvatarScale/Input/DebugKeybinds.cs index 9756930..aa6835e 100644 --- a/AvatarScale/Input/DebugKeybinds.cs +++ b/AvatarScale/Input/DebugKeybinds.cs @@ -3,35 +3,41 @@ using UnityEngine; namespace NAK.AvatarScaleMod.InputHandling; -public static class DebugKeybinds +internal static class DebugKeybinds { - public static void DoDebugInput() + private const float Step = 0.1f; + + internal static void DoDebugInput() { if (AvatarScaleManager.Instance == null) return; - float currentHeight; - const float step = 0.1f; - if (Input.GetKeyDown(KeyCode.Equals) || Input.GetKeyDown(KeyCode.KeypadPlus)) { - currentHeight = AvatarScaleManager.Instance.GetHeight() + step; - AvatarScaleManager.Instance.SetHeight(currentHeight); - - AvatarScaleMod.Logger.Msg($"Setting height: {currentHeight}"); + AdjustHeight(Step); } else if (Input.GetKeyDown(KeyCode.Minus) || Input.GetKeyDown(KeyCode.KeypadMinus)) { - currentHeight = AvatarScaleManager.Instance.GetHeight() - step; - AvatarScaleManager.Instance.SetHeight(currentHeight); - - AvatarScaleMod.Logger.Msg($"Setting height: {currentHeight}"); + AdjustHeight(-Step); } else if (Input.GetKeyDown(KeyCode.Backspace)) { - AvatarScaleManager.Instance.ResetHeight(); - - AvatarScaleMod.Logger.Msg($"Resetting height."); + ResetHeight(); } } + + private static void AdjustHeight(float adjustment) + { + float currentHeight = AvatarScaleManager.Instance.GetHeight() + adjustment; + currentHeight = Mathf.Max(0f, currentHeight); + AvatarScaleManager.Instance.SetHeight(currentHeight); + + AvatarScaleMod.Logger.Msg($"[Debug] Setting height: {currentHeight}"); + } + + private static void ResetHeight() + { + AvatarScaleManager.Instance.ResetHeight(); + AvatarScaleMod.Logger.Msg("[Debug] Resetting height."); + } } \ No newline at end of file diff --git a/AvatarScale/Input/ScaleReconizer.cs b/AvatarScale/Input/ScaleReconizer.cs index 444659f..7bf74de 100644 --- a/AvatarScale/Input/ScaleReconizer.cs +++ b/AvatarScale/Input/ScaleReconizer.cs @@ -32,7 +32,8 @@ public static class ScaleReconizer secondGesture = CVRGestureStep.Gesture.Fist, startDistance = 1f, endDistance = 0.25f, - direction = CVRGestureStep.GestureDirection.MovingIn + direction = CVRGestureStep.GestureDirection.MovingIn, + needsToBeInView = true, }); gesture.onStart.AddListener(OnScaleStart); gesture.onStay.AddListener(OnScaleStay); @@ -50,7 +51,8 @@ public static class ScaleReconizer secondGesture = CVRGestureStep.Gesture.Fist, startDistance = 0.25f, endDistance = 1f, - direction = CVRGestureStep.GestureDirection.MovingOut + direction = CVRGestureStep.GestureDirection.MovingOut, + needsToBeInView = true, }); gesture.onStart.AddListener(OnScaleStart); gesture.onStay.AddListener(OnScaleStay); diff --git a/AvatarScale/Integrations/BTKUI/BTKUIAddon.cs b/AvatarScale/Integrations/BTKUI/BTKUIAddon.cs index c2a7ae5..6e276c4 100644 --- a/AvatarScale/Integrations/BTKUI/BTKUIAddon.cs +++ b/AvatarScale/Integrations/BTKUI/BTKUIAddon.cs @@ -1,71 +1,137 @@ -using System.Runtime.CompilerServices; +using System.IO; +using System.Reflection; using ABI_RC.Core.InteractionSystem; using ABI_RC.Core.IO; using BTKUILib; using BTKUILib.UIObjects; +using BTKUILib.UIObjects.Components; using MelonLoader; -using NAK.AvatarScaleMod.Integrations.BTKUI; +using NAK.AvatarScaleMod.AvatarScaling; using UnityEngine; -namespace NAK.AvatarScaleMod.Integrations; - -public static class BTKUIAddon +namespace NAK.AvatarScaleMod.Integrations { - #region Initialization - - [MethodImpl(MethodImplOptions.NoInlining)] - public static void Initialize() + public static class BtkUiAddon { - Page as_RootPage = new(ModSettings.SettingsCategory, ModSettings.SettingsCategory, true, "") - { - MenuTitle = ModSettings.SettingsCategory, - MenuSubtitle = "Avatar Scale Mod Settings" - }; + private static string _selectedPlayer; - QuickMenuAPI.OnMenuRegenerate += (_) => - { - SchedulerSystem.AddJob((InjectMenu), 1f, 1f, 1); - }; - } + #region Initialization - // private static void InjectMenu() - // { - // MelonLogger.Msg("Injecting into QM!"); - // CVR_MenuManager.Instance.quickMenu.View.ExecuteScript(Scripts.GetEmbeddedScript("menu.js")); - // } - - private static void InjectMenu() - { - MelonLogger.Msg("Injecting into QM!"); - string menuJsPath = Path.Combine(Application.streamingAssetsPath, "Cohtml", "UIResources", "AvatarScaleMod", "menu.js"); - string menuJsContent = ReadJSFile(menuJsPath); - CVR_MenuManager.Instance.quickMenu.View._view.ExecuteScript(menuJsContent); - } - - private static string ReadJSFile(string path) - { - if(File.Exists(path)) + public static void Initialize() { - return File.ReadAllText(path); + PrepareIcons(); + SetupRootPage(); + SetupPlayerSelectPage(); + RegisterEventHandlers(); } - MelonLogger.Warning($"File not found: {path}"); - return string.Empty; + private static void PrepareIcons() + { + QuickMenuAPI.PrepareIcon(ModSettings.ModName, "ASM_Icon_AvatarHeightConfig", GetIconStream("ASM_Icon_AvatarHeightConfig.png")); + QuickMenuAPI.PrepareIcon(ModSettings.ModName, "ASM_Icon_AvatarHeightCopy", GetIconStream("ASM_Icon_AvatarHeightCopy.png")); + } + + private static void SetupRootPage() + { + // we only need the page, as we inject our own elements into it aa + Page rootPage = new Page(ModSettings.ModName, ModSettings.SettingsCategory, true, "ASM_Icon_AvatarHeightConfig") + { + MenuTitle = ModSettings.SettingsCategory, + MenuSubtitle = "Universal Scaling Settings" + }; + } + + private static void SetupPlayerSelectPage() + { + // what other things would be worth adding here? + Category category = QuickMenuAPI.PlayerSelectPage.AddCategory(ModSettings.SettingsCategory, ModSettings.ModName); + Button button = category.AddButton("Copy Height", "ASM_Icon_AvatarHeightCopy", "Copy selected players Eye Height."); + button.OnPress += OnCopyPlayerHeight; + } + + private static void RegisterEventHandlers() + { + QuickMenuAPI.OnPlayerSelected += (_, id) => _selectedPlayer = id; + QuickMenuAPI.OnMenuRegenerate += _ => ScheduleMenuInjection(); + QuickMenuAPI.OnTabChange += OnTabChange; + } + + private static void ScheduleMenuInjection() + { + CVR_MenuManager.Instance.quickMenu.View.BindCall("asm-AvatarHeightUpdated", new Action(OnAvatarHeightUpdated)); + SchedulerSystem.AddJob(InjectMenu, 1f, 1f, 1); + } + + private static void InjectMenu() + { + AvatarScaleMod.Logger.Msg("Injecting into our BTKUI AvatarScaleMod page!"); + string menuJsPath = Path.Combine(Application.streamingAssetsPath, "Cohtml", "UIResources", "AvatarScaleMod", "menu.js"); + string menuJsContent = File.Exists(menuJsPath) ? File.ReadAllText(menuJsPath) : string.Empty; + + if (string.IsNullOrEmpty(menuJsContent)) + { + AvatarScaleMod.Logger.Msg("Injecting embedded menu.js included with mod!"); + CVR_MenuManager.Instance.quickMenu.View._view.ExecuteScript(Scripts.GetEmbeddedScript("menu.js")); + } + else + { + AvatarScaleMod.Logger.Msg($"Injecting development menu.js found in: {menuJsPath}"); + CVR_MenuManager.Instance.quickMenu.View._view.ExecuteScript(menuJsContent); + } + } + + #endregion + + private static void OnCopyPlayerHeight() + { + float networkHeight = AvatarScaleManager.Instance.GetNetworkHeight(_selectedPlayer); + if (networkHeight < 0) return; + AvatarScaleManager.Instance.SetHeight(networkHeight); + } + + private static void OnAvatarHeightUpdated(float height) + { + AvatarScaleManager.Instance.SetHeight(height); + } + + #region Private Methods + + private static DateTime lastTime = DateTime.Now; + private static void OnTabChange(string newTab, string previousTab) + { + if (newTab == "btkUI-AvatarScaleMod-MainPage") + { + TimeSpan timeDifference = DateTime.Now - lastTime; + if (timeDifference.TotalSeconds <= 0.5) + { + AvatarScaleManager.Instance.ResetHeight(); + return; + } + } + lastTime = DateTime.Now; + } + + private static Stream GetIconStream(string iconName) + { + Assembly assembly = Assembly.GetExecutingAssembly(); + string assemblyName = assembly.GetName().Name; + return assembly.GetManifestResourceStream($"{assemblyName}.resources.{iconName}"); + } + + #endregion + + #region Melon Pref Helpers + + internal static void AddMelonToggle(ref Category category, MelonPreferences_Entry entry) + { + category.AddToggle(entry.DisplayName, entry.Description, entry.Value).OnValueUpdated += b => entry.Value = b; + } + + internal static void AddMelonSlider(ref Page page, MelonPreferences_Entry entry, float min, float max, int decimalPlaces = 2) + { + page.AddSlider(entry.DisplayName, entry.Description, entry.Value, min, max, decimalPlaces).OnValueUpdated += f => entry.Value = f; + } + + #endregion } - - #endregion - - #region Melon Pref Helpers - - internal static void AddMelonToggle(ref Category category, MelonPreferences_Entry entry) - { - category.AddToggle(entry.DisplayName, entry.Description, entry.Value).OnValueUpdated += b => entry.Value = b; - } - - internal static void AddMelonSlider(ref Page page, MelonPreferences_Entry entry, float min, float max, int decimalPlaces = 2) - { - page.AddSlider(entry.DisplayName, entry.Description, entry.Value, min, max, decimalPlaces).OnValueUpdated += f => entry.Value = f; - } - - #endregion } \ No newline at end of file diff --git a/AvatarScale/Integrations/BTKUI/Page_AvatarScale.cs b/AvatarScale/Integrations/BTKUI/Page_AvatarScale.cs deleted file mode 100644 index 7e2750f..0000000 --- a/AvatarScale/Integrations/BTKUI/Page_AvatarScale.cs +++ /dev/null @@ -1,87 +0,0 @@ -using ABI_RC.Systems.Camera; -using BTKUILib; -using BTKUILib.UIObjects; -using MelonLoader; -using UnityEngine; - -namespace NAK.AvatarScaleMod.Integrations.BTKUI; - -public class PortableCameraCategory -{ - private static string categoryName = "Portable Camera"; - - internal static void AddCategory(Page parent) - { - QuickMenuAPI.OnTabChange += OnTabChange; - - // Create category and add elements to it - var category = parent.AddCategory(categoryName); - category.AddButton("Take Photo", "TakePhoto-Icon", "Quickly take a photo. This respects set timers & other related settings.").OnPress += TakePhoto; - category.AddButton("Cycle Delay", "CycleDelay-Icon", "Quickly cycle photo timers. Off, 3s, 5s, 10s.").OnPress += CycleCaptureDelay; - category.AddButton("Open Folder", "OpenFolder-Icon", "Quickly open the root of the ChilloutVR screenshots folder in Windows Explorer.").OnPress += OpenScreenshotsFolder; - - // Clone of the default camera settings page - var settingsPage = category.AddPage("Settings", "Settings-Icon", "Sub page of settings to configure the portable camera.", parent.MenuTitle); - settingsPage.AddCategory("Main Settings"); - settingsPage.AddSlider("Field of View", "Field of View of portable camera.", 40f, 10f, 120f); - settingsPage.AddSlider("Focal Length", "Focal Length of portable camera.", 50f, 24f, 200f); - settingsPage.AddSlider("Aperture", "Aperture of portable camera.", 1.8f, 1.2f, 8f); - } - - private static bool PortableCameraReady() - { - bool active = (bool)(PortableCamera.Instance?.IsActive()); - if (!active) CVRCamController.Instance?.Toggle(); - return active; - } - - private static void TakePhoto() - { - MelonLogger.Msg("Took photo!"); - if (PortableCameraReady()) - PortableCamera.Instance?.MakePhoto(); - } - - private static void CycleCaptureDelay() - { - if (PortableCameraReady()) - { - PortableCamera.Instance?.ChangeCameraCaptureDelay(); - QuickMenuAPI.ShowAlertToast("Delay set to " + PortableCamera.Instance.timerText.text, 1); - } - } - - //this was mistake, but now feature cause fuck it - private static void PauseCamera() - { - MelonLogger.Msg("Paused camera!"); - GameObject camera = PortableCamera.Instance.gameObject; - PortableCamera.Instance.gameObject.SetActive(!camera.activeSelf); - } - - private static void OpenScreenshotsFolder() - { - MelonLogger.Msg("Opened screenshots folder!"); - string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures), "ChilloutVR"); - if (!Directory.Exists(path)) - { - Directory.CreateDirectory(path); - } - Application.OpenURL("file:///" + path); - } - - private static DateTime lastTime = DateTime.Now; - private static void OnTabChange(string newTab, string previousTab) - { - if (newTab == "btkUI-AvatarScaleMod-MainPage") - { - TimeSpan timeDifference = DateTime.Now - lastTime; - if (timeDifference.TotalSeconds <= 0.5) - { - // The new page and previous page are equal and were opened within 0.5 seconds of each other - CVRCamController.Instance?.Toggle(); - } - } - lastTime = DateTime.Now; - } -} \ No newline at end of file diff --git a/AvatarScale/Main.cs b/AvatarScale/Main.cs index f2d2661..b688c6e 100644 --- a/AvatarScale/Main.cs +++ b/AvatarScale/Main.cs @@ -11,15 +11,15 @@ public class AvatarScaleMod : MelonMod public override void OnInitializeMelon() { Logger = LoggerInstance; + + ModNetwork.Subscribe(); + ModSettings.Initialize(); ApplyPatches(typeof(HarmonyPatches.PlayerSetupPatches)); ApplyPatches(typeof(HarmonyPatches.PuppetMasterPatches)); ApplyPatches(typeof(HarmonyPatches.GesturePlaneTestPatches)); - InitializeIntegration("BTKUILib", Integrations.BTKUIAddon.Initialize); - - ModNetwork.Subscribe(); - ModSettings.Initialize(); + InitializeIntegration("BTKUILib", Integrations.BtkUiAddon.Initialize); } public override void OnUpdate() diff --git a/AvatarScale/ModSettings.cs b/AvatarScale/ModSettings.cs index 95a8623..f18cefd 100644 --- a/AvatarScale/ModSettings.cs +++ b/AvatarScale/ModSettings.cs @@ -9,10 +9,11 @@ namespace NAK.AvatarScaleMod; internal static class ModSettings { // Constants - internal const string SettingsCategory = nameof(AvatarScaleMod); + internal const string ModName = nameof(AvatarScaleMod); + internal const string SettingsCategory = "Avatar Scale Mod"; public static readonly MelonPreferences_Category Category = - MelonPreferences.CreateCategory(SettingsCategory); + MelonPreferences.CreateCategory(ModName); public static readonly MelonPreferences_Entry EntryUseUniversalScaling = Category.CreateEntry("use_universal_scaling", true, display_name: "Use Universal Scaling", description: "Enable or disable universal scaling."); diff --git a/AvatarScale/Networking/ModNetwork.cs b/AvatarScale/Networking/ModNetwork.cs index 1d129e4..a94147d 100644 --- a/AvatarScale/Networking/ModNetwork.cs +++ b/AvatarScale/Networking/ModNetwork.cs @@ -14,6 +14,8 @@ public static class ModNetwork { public static bool Debug_NetworkInbound = false; public static bool Debug_NetworkOutbound = false; + + private static bool _isSubscribedToModNetwork; #region Constants @@ -60,11 +62,15 @@ public static class ModNetwork internal static void Subscribe() { + _isSubscribedToModNetwork = true; ModNetworkManager.Subscribe(ModId, OnMessageReceived); } internal static void Update() { + if (!_isSubscribedToModNetwork) + return; + ProcessOutboundQueue(); ProcessInboundQueue(); } @@ -74,6 +80,9 @@ public static class ModNetwork if (!IsConnectedToGameNetwork()) return; + if (!Enum.IsDefined(typeof(MessageType), messageType)) + return; + if (!string.IsNullOrEmpty(playerId)) { // to specific user @@ -129,8 +138,7 @@ public static class ModNetwork public static void RequestHeightSync() { var myCurrentHeight = AvatarScaleManager.Instance.GetHeightForNetwork(); - if (myCurrentHeight > 0) - OutboundQueue["global"] = new QueuedMessage { Type = MessageType.RequestHeight, Height = myCurrentHeight }; + OutboundQueue["global"] = new QueuedMessage { Type = MessageType.RequestHeight, Height = myCurrentHeight }; } #endregion @@ -199,13 +207,12 @@ public static class ModNetwork case MessageType.RequestHeight: { var myNetworkHeight = AvatarScaleManager.Instance.GetHeightForNetwork(); - if (myNetworkHeight > 0) - OutboundQueue[message.Sender] = new QueuedMessage - { - Type = MessageType.SyncHeight, - Height = myNetworkHeight, - TargetPlayer = message.Sender - }; + OutboundQueue[message.Sender] = new QueuedMessage + { + Type = MessageType.SyncHeight, + Height = myNetworkHeight, + TargetPlayer = message.Sender + }; AvatarScaleManager.Instance.OnNetworkHeightUpdateReceived(message.Sender, message.Height); break; diff --git a/AvatarScale/Properties/AssemblyInfo.cs b/AvatarScale/Properties/AssemblyInfo.cs index ba2b41c..0227b18 100644 --- a/AvatarScale/Properties/AssemblyInfo.cs +++ b/AvatarScale/Properties/AssemblyInfo.cs @@ -20,6 +20,7 @@ using System.Reflection; [assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")] [assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] [assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)] +[assembly: MelonOptionalDependencies("Action Menu")] [assembly: MelonColor(255, 241, 200, 82)] [assembly: MelonAuthorColor(255, 114, 17, 25)] [assembly: HarmonyDontPatchAll] diff --git a/AvatarScale/resources/ASM_Icon_AvatarHeightConfig.png b/AvatarScale/resources/ASM_Icon_AvatarHeightConfig.png new file mode 100644 index 0000000..08b68ea Binary files /dev/null and b/AvatarScale/resources/ASM_Icon_AvatarHeightConfig.png differ diff --git a/AvatarScale/resources/ASM_Icon_AvatarHeightCopy.png b/AvatarScale/resources/ASM_Icon_AvatarHeightCopy.png new file mode 100644 index 0000000..e665a6d Binary files /dev/null and b/AvatarScale/resources/ASM_Icon_AvatarHeightCopy.png differ diff --git a/AvatarScale/resources/menu.js b/AvatarScale/resources/menu.js index 2bd9ff7..5c6c697 100644 --- a/AvatarScale/resources/menu.js +++ b/AvatarScale/resources/menu.js @@ -1,42 +1,47 @@ (function() { + /* ------------------------------- * CSS Embedding * ------------------------------- */ - { + function injectCSS() { const embeddedCSS = ` - .slider-container { - width: 80%; - padding-left: 2em; - position: relative; - } - .avatar-scale-slider-container { + // ass stands for Avatar Scale Slider + + /* Container styles */ + .ass-flex-container { + display: flex; + justify-content: space-between; + align-items: center; + } + + /* ass Slider styles */ + .ass-slider-container { width: 100%; - height: 3em; + } + .ass-slider-base { + height: 3.25em; position: relative; background: #555; border-radius: 1.5em; cursor: pointer; overflow: hidden; } - - .avatar-scale-track-inner { + .ass-slider-inner { height: 100%; background: #a9a9a9; position: absolute; top: 0; left: 0; } - - .avatar-scale-snap-point { + .ass-snap-point { height: 100%; width: 2px; background: white; position: absolute; top: 0; } - - .slider-display-value { + .ass-slider-value { font-size: 2em; position: relative; left: 0.5em; @@ -44,16 +49,79 @@ white-space: nowrap; } - .lock-icon { + /* Category (label) styles */ + .ass-category-label { + font-size: 2.2em; + position: relative; + left: 0.5em; + color: #fff; + white-space: nowrap; + margin-right: 1em; + } + + /* Circle Button styles */ + .ass-circle-button { + height: 2em; + width: 2em; + border-radius: 50%; + background-color: #555; + border: none; + cursor: pointer; + color: #fff; + font-size: 1.75em; + display: flex; + align-items: center; + justify-content: center; + transition: background-color 0.3s; + } + .ass-circle-button:hover { + background-color: #777; + } + + /* Custom Toggle styles */ + .ass-custom-toggle { + width: 5em; + height: 2.5em; + background-color: #555; + border-radius: 1.25em; position: absolute; top: 50%; - right: 1em; - transform: translateY(-50%); - width: 1.5em; - height: 1.5em; - background: url('path_to_lock_icon.png') no-repeat center; - background-size: contain; + transform: translateY(-65%); + right: 0; + cursor: pointer; + transition: background-color 0.3s; } + + .ass-toggle-circle { + width: 2.5em; + height: 2.5em; + background-color: #a9a9a9; + border-radius: 50%; + position: absolute; + top: 0; + left: 0; + transition: left 0.3s; + } + + .ass-custom-toggle.active .ass-toggle-circle { + left: 50%; + } + + /* Label styles */ + .ass-label { + font-size: 2em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + position: relative; + z-index: 0; + } + + .ass-toggle-setting { + position: relative; + height: 3.5em; + } + `; const styleElement = document.createElement('style'); @@ -63,113 +131,284 @@ } /* ------------------------------- - * Content Injection + * Main Content Element * ------------------------------- */ - { - const contentBlock = document.createElement('div'); - contentBlock.innerHTML = ` -
-
Avatar Motion Tweaker
-
-
- -
-
Crouch limit:
-
-
-
-
- -
-
- 0m -
-
-
-
-
-
- `; - - const targetElement = document.getElementById('btkUI-AvatarScaleMod-MainPage'); - if(targetElement) { - targetElement.appendChild(contentBlock); - } else { - console.warn('Target element "btkUI-AvatarScaleMod-MainPage" not found!'); + class MainContent { + constructor(targetId) { + this.element = document.createElement('div'); + this.element.id = "AvatarScaleModContainer"; + const targetElement = document.getElementById(targetId); + if (targetElement) { + targetElement.appendChild(this.element); + } else { + console.warn(`Target element "${targetId}" not found!`); + } } } /* ------------------------------- - * Event Handlers & Utility Functions + * Generic Element Class + * ------------------------------- */ + class Element { + constructor(tagName, parentElement) { + this.element = document.createElement(tagName); + parentElement.appendChild(this.element); + } + } + + /* ------------------------------- + * Container Object * ------------------------------- */ - { - const sliderContainer = document.querySelector('.avatar-scale-slider-container'); - const trackInner = document.querySelector('.avatar-scale-track-inner'); - const valueDisplay = document.querySelector('.slider-value'); + class Container extends Element { + constructor(parentElement, { + width = '100%', + padding = '0em', + paddingTop = null, + paddingRight = null, + paddingBottom = null, + paddingLeft = null, + margin = '0em', + marginTop = null, + marginRight = null, + marginBottom = null, + marginLeft = null + } = {}) { + super('div', parentElement); + this.element.className = "ass-container"; + this.element.style.width = width; + this.element.style.padding = padding; + this.element.style.margin = margin; - const SNAP_TOLERANCE = 0.02; - let snapPoints = []; + // padding values + if (paddingTop) this.element.style.paddingTop = paddingTop; + if (paddingRight) this.element.style.paddingRight = paddingRight; + if (paddingBottom) this.element.style.paddingBottom = paddingBottom; + if (paddingLeft) this.element.style.paddingLeft = paddingLeft; - let isDragging = false; + // margin values + if (marginTop) this.element.style.marginTop = marginTop; + if (marginRight) this.element.style.marginRight = marginRight; + if (marginBottom) this.element.style.marginBottom = marginBottom; + if (marginLeft) this.element.style.marginLeft = marginLeft; - sliderContainer.addEventListener('mousedown', (e) => { - isDragging = true; - updateTrackWidth(e.clientX); - }); + this.flexContainer = new Element('div', this.element); + this.flexContainer.element.className = "ass-flex-container"; + } - window.addEventListener('mousemove', (e) => { - if (!isDragging) return; - updateTrackWidth(e.clientX); - }); + appendElementToFlex(element) { + this.flexContainer.element.appendChild(element); + } - window.addEventListener('mouseup', () => { - isDragging = false; - }); + appendElement(element) { + this.element.appendChild(element); + } + } - function updateTrackWidth(clientX) { - const rect = sliderContainer.getBoundingClientRect(); + /* ------------------------------- + * Category (label) Class + * ------------------------------- */ + class Category extends Element { + constructor(parentElement, text) { + super('span', parentElement); + this.element.className = "ass-category-label"; + this.element.textContent = text; + } + } - // Get padding values from the slider container - const paddingLeft = parseFloat(getComputedStyle(sliderContainer).paddingLeft); - const paddingRight = parseFloat(getComputedStyle(sliderContainer).paddingRight); + /* ------------------------------- + * Circle Button Class + * ------------------------------- */ + class CircleButton extends Element { + constructor(parentElement, text) { + super('button', parentElement); + this.element.className = "ass-circle-button"; + this.element.textContent = text; + } + } - // Calculate the effective width and position based on padding + /* ------------------------------- + * Custom Toggle Class + * ------------------------------- */ + class CustomToggle extends Element { + constructor(parentElement) { + super('div', parentElement); + this.element.className = "ass-custom-toggle"; + + this.toggleCircle = new Element('div', this.element); + this.toggleCircle.element.className = "ass-toggle-circle"; + this.state = false; + + this.element.addEventListener('click', () => { + this.toggle(); + }); + } + + toggle() { + this.state = !this.state; + if (this.state) { + this.toggleCircle.element.style.left = '50%'; + } else { + this.toggleCircle.element.style.left = '0'; + } + } + } + + /* ------------------------------- + * Label Class + * ------------------------------- */ + class Label extends Element { + constructor(parentElement, text) { + super('span', parentElement); + this.element.className = "ass-label"; + this.element.textContent = text; + } + } + + /* ------------------------------- + * ToggleSetting Class + * ------------------------------- */ + class ToggleSetting extends Element { + constructor(parentElement, labelText) { + super('div', parentElement); + this.element.className = "ass-toggle-setting"; + + const label = new Label(this.element, labelText); + this.toggle = new CustomToggle(this.element); + } + } + + /* ------------------------------- + * Slider Object + * ------------------------------- */ + class Slider extends Element { + constructor(parentElement, min = 0.1, max = 5, initialValue = 1.8) { + super('div', parentElement); + this.element.className = "ass-slider-container"; + this.min = min; + this.max = max; + + // Value display + this.valueDisplay = new Element('span', this.element); + this.valueDisplay.element.className = "ass-slider-value"; + + // Slider content + this.sliderBase = new Element('div', this.element); + this.sliderBase.element.className = "ass-slider-base"; + + this.trackInner = new Element('div', this.sliderBase.element); + this.trackInner.element.className = "ass-slider-inner"; + + this.addEventListeners(); + + this.setInitialValue(initialValue); + } + + setInitialValue(value) { + const percentage = (value - this.min) / (this.max - this.min); + this.trackInner.element.style.width = `${percentage * 100}%`; + this.valueDisplay.element.textContent = value.toFixed(2) + "m"; + this.addSnapPoint(percentage); + } + + addEventListeners() { + this.snapPoints = []; + let isDragging = false; + + this.element.addEventListener('mousedown', (e) => { + isDragging = true; + this.updateTrackWidth(e.clientX); + }); + + window.addEventListener('mousemove', (e) => { + if (!isDragging) return; + this.updateTrackWidth(e.clientX); + }); + + window.addEventListener('mouseup', () => { + isDragging = false; + }); + } + + updateTrackWidth(clientX) { + const rect = this.element.getBoundingClientRect(); + const paddingLeft = parseFloat(getComputedStyle(this.element).paddingLeft); + const paddingRight = parseFloat(getComputedStyle(this.element).paddingRight); const effectiveWidth = rect.width - paddingLeft - paddingRight; let x = clientX - rect.left - paddingLeft; - // Ensure the position is within the bounds of the effective width x = Math.min(Math.max(0, x), effectiveWidth); - const percentage = x / effectiveWidth; - const closestSnap = snapPoints.reduce((closest, snap) => { + let percentage = x / effectiveWidth; + const closestSnap = this.snapPoints.reduce((closest, snap) => { return Math.abs(closest - percentage) < Math.abs(snap - percentage) ? closest : snap; }, 1); + const SNAP_TOLERANCE = 0.01; if (Math.abs(closestSnap - percentage) <= SNAP_TOLERANCE) { x = closestSnap * effectiveWidth; + percentage = closestSnap; } - trackInner.style.width = `${x}px`; - valueDisplay.textContent = (x / effectiveWidth * 100).toFixed(2) + "m"; + this.trackInner.element.style.width = `${x}px`; + + const value = this.min + (this.max - this.min) * percentage; + this.valueDisplay.element.textContent = value.toFixed(2) + "m"; + + engine.call("asm-AvatarHeightUpdated", value); } - function addSnapPoint(percentage) { + addSnapPoint(percentage) { if (percentage < 0 || percentage > 1) return; - const snap = document.createElement('div'); - snap.className = 'avatar-scale-snap-point'; - snap.style.left = `${percentage * 100}%`; - sliderContainer.appendChild(snap); - snapPoints.push(percentage); + const snap = new Element('div', this.sliderBase.element); + snap.element.className = 'ass-snap-point'; + snap.element.style.left = `${percentage * 100}%`; + this.snapPoints.push(percentage); } - // To evenly space out snap points: - function addEvenSnapPoints(count) { + addEvenSnapPoints(count) { for (let i = 1; i <= count; i++) { - addSnapPoint(i / (count + 1)); + this.addSnapPoint(i / (count + 1)); } } - - // Example usage: - addEvenSnapPoints(5); // Adds 5 evenly spaced snap points } + + // Initialization + injectCSS(); + const mainContent = new MainContent('btkUI-AvatarScaleMod-MainPage'); + if (mainContent.element) { + + const mainContainer = new Container(mainContent.element, { + width: '75%', + marginTop: '2em', + marginLeft: '2em', + marginBottom: '1em' + }); + + const slider = new Slider(mainContainer.flexContainer.element, 0.1, 3); + const buttonContainer = new Container(mainContainer.flexContainer.element, { + width: '20%', + marginTop: '2.5em', + marginLeft: '1em', + }); + const circleButton1 = new CircleButton(buttonContainer.flexContainer.element, "+"); + const circleButton2 = new CircleButton(buttonContainer.flexContainer.element, "-"); + + const settingsContainer = new Container(mainContent.element, { + width: '100%', + marginTop: '1em', + marginLeft: '1em', + }); + const categoryLabel = new Category(settingsContainer.element, "Universal Scaling Settings:"); + + const settingsContainerInner = new Container(mainContent.element, { + width: '90%', + marginTop: '1em', + marginLeft: '3em', + }); + + const toggleSetting = new ToggleSetting(settingsContainerInner.element, "Universal Scaling (Mod Network)"); + const toggleSetting2 = new ToggleSetting(settingsContainerInner.element, "Recognize Scale Gesture"); + const toggleSetting3 = new ToggleSetting(settingsContainerInner.element, "Scale Components"); + } + })(); \ No newline at end of file diff --git a/AvatarScale/resources/nak_menu.css b/AvatarScale/resources/nak_menu.css deleted file mode 100644 index d732c3d..0000000 --- a/AvatarScale/resources/nak_menu.css +++ /dev/null @@ -1,13 +0,0 @@ -.slider-container { - margin: 10px 0; - padding: 5px; -} - -.slider-label { - display: block; - margin-bottom: 5px; -} - -.test-slider { - width: 100%; -}