Move many mods to Deprecated folder, fix spelling

This commit is contained in:
NotAKidoS 2025-04-03 02:57:35 -05:00
parent 5e822cec8d
commit 0042590aa6
539 changed files with 7475 additions and 3120 deletions

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn />
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<NoWarn />
</PropertyGroup>
<ItemGroup>
<None Remove="resources\menu.js" />
<None Remove="resources\nak_menu.css" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="resources\menu.js" />
<None Remove="resources\Icon_AvatarHeightConfig.png" />
<EmbeddedResource Include="resources\ASM_Icon_AvatarHeightConfig.png" />
<None Remove="resources\ASM_Icon_AvatarHeightCopy.png" />
<EmbeddedResource Include="resources\ASM_Icon_AvatarHeightCopy.png" />
</ItemGroup>
<ItemGroup>
<Reference Include="BTKUILib">
<HintPath>$(MsBuildThisFileDirectory)\..\.ManagedLibs\BTKUILib.dll</HintPath>
<Private>False</Private>
</Reference>
</ItemGroup>
</Project>

View file

@ -0,0 +1,397 @@
using System.Collections;
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;
using UnityEngine;
namespace NAK.AvatarScaleMod.AvatarScaling;
public class AvatarScaleManager : MonoBehaviour
{
public static AvatarScaleManager Instance { get; private set; }
private LocalScaler _localAvatarScaler;
private Dictionary<string, NetworkScaler> _networkedScalers;
private Coroutine _heightUpdateCoroutine;
private readonly YieldInstruction _heightUpdateYield = new WaitForEndOfFrame();
#region Universal Scaling Limits
// ReSharper disable MemberCanBePrivate.Global
// To match AvatarScaleTool: https://github.com/NotAKidoS/AvatarScaleTool/tree/main
public const float DefaultMinHeight = 0.25f;
public const float DefaultMaxHeight = 2.50f;
// ReSharper restore MemberCanBePrivate.Global
// Universal Scaling Limits
public static float MinHeight { get; private set; } = DefaultMinHeight;
public static float MaxHeight { get; private set; } = DefaultMaxHeight;
#endregion
#region Settings
private bool _settingUniversalScaling;
public bool Setting_UniversalScaling
{
get => _settingUniversalScaling;
set
{
if (_settingUniversalScaling == value)
return;
_settingUniversalScaling = value;
if (_localAvatarScaler != null)
_localAvatarScaler.UseTargetHeight = value;
}
}
private bool _settingAnimationClipScalingOverride;
public bool Setting_AnimationClipScalingOverride
{
get => _settingAnimationClipScalingOverride;
set
{
if (_settingAnimationClipScalingOverride == value)
return;
_settingAnimationClipScalingOverride = value;
if (_localAvatarScaler != null)
_localAvatarScaler.overrideAnimationHeight = value;
}
}
private bool _settingPersistentHeight;
public bool Setting_PersistentHeight
{
get => _settingPersistentHeight;
set
{
if (_settingPersistentHeight == value)
return;
_settingPersistentHeight = value;
// if (_localAvatarScaler != null)
// _localAvatarScaler.persistHeight = value;
}
}
private float _lastTargetHeight = -1f;
public float LastTargetHeight
{
get => _lastTargetHeight;
set
{
_lastTargetHeight = value;
ModSettings.EntryHiddenAvatarHeight.Value = _lastTargetHeight;
}
}
#endregion
#region Unity Events
private void Awake()
{
if (Instance != null
&& Instance != this)
{
DestroyImmediate(this);
return;
}
Instance = this;
_networkedScalers = new Dictionary<string, NetworkScaler>();
}
private void Start()
{
_localAvatarScaler = PlayerSetup.Instance.gameObject.AddComponent<LocalScaler>();
_localAvatarScaler.Initialize();
_settingUniversalScaling = ModSettings.EntryUseUniversalScaling.Value;
Setting_AnimationClipScalingOverride = ModSettings.EntryAnimationScalingOverride.Value;
Setting_PersistentHeight = ModSettings.EntryPersistentHeight.Value;
_lastTargetHeight = ModSettings.EntryPersistThroughRestart.Value
? ModSettings.EntryHiddenAvatarHeight.Value : -1f; // -1f is default
// listen for events
_localAvatarScaler.OnAnimatedHeightOverride += OnAnimationHeightOverride;
CVRGameEventSystem.Instance.OnConnected.AddListener(OnInstanceConnected);
}
private void OnEnable()
{
if (_heightUpdateCoroutine != null) StopCoroutine(_heightUpdateCoroutine);
_heightUpdateCoroutine = StartCoroutine(HeightUpdateCoroutine());
}
private void OnDisable()
{
if (_heightUpdateCoroutine != null) StopCoroutine(_heightUpdateCoroutine);
_heightUpdateCoroutine = null;
}
private void OnDestroy()
{
_heightUpdateCoroutine = null;
CVRGameEventSystem.Instance.OnConnected.RemoveListener(OnInstanceConnected);
if (_localAvatarScaler != null) Destroy(_localAvatarScaler);
_localAvatarScaler = null;
foreach (NetworkScaler scaler in _networkedScalers.Values) Destroy(scaler);
_networkedScalers.Clear();
if (Instance == this)
Instance = null;
}
// only update the height per scaler once per frame, to prevent spam & jitter
// this is to ensure that the height is also set at correct time during frame, no matter when it is called
private IEnumerator HeightUpdateCoroutine()
{
while (enabled) yield return _heightUpdateYield;
// ReSharper disable once IteratorNeverReturns
}
#endregion
#region Game Events
public void OnInstanceConnected(string instanceId)
{
// TODO: need to know if this causes issues when in a reconnection loop
SchedulerSystem.AddJob(ModNetwork.RequestHeightSync, 2f, 1f, 1);
}
#endregion
#region Local Methods
public void OnAvatarInstantiated(PlayerSetup playerSetup)
{
if (playerSetup._avatar == null)
return;
_localAvatarScaler.OnAvatarInstantiated(playerSetup._avatar, playerSetup._initialAvatarHeight,
playerSetup.initialScale);
if (!_settingUniversalScaling)
return;
SetTargetHeight(_lastTargetHeight);
}
public void OnAvatarDestroyed(PlayerSetup playerSetup)
{
if (_localAvatarScaler != null)
_localAvatarScaler.OnAvatarDestroyed();
}
public void SetTargetHeight(float targetHeight)
{
_lastTargetHeight = targetHeight; // save for persistent height
if (_localAvatarScaler == null)
return;
_localAvatarScaler.TargetHeight = _lastTargetHeight;
}
public float GetHeight()
{
if (_localAvatarScaler == null)
return PlayerAvatarPoint.DefaultAvatarHeight;
if (!_localAvatarScaler.IsForcingHeight())
return PlayerSetup.Instance.GetAvatarHeight();
return _localAvatarScaler.GetTargetHeight();
}
public float GetAnimationClipHeight()
{
if (_localAvatarScaler == null)
return PlayerAvatarPoint.DefaultAvatarHeight;
if (!_localAvatarScaler.IsForcingHeight())
return PlayerSetup.Instance.GetAvatarHeight();
return _localAvatarScaler.GetAnimatedHeight();
}
public float GetHeightForNetwork()
{
if (!_settingUniversalScaling)
return -1f;
if (_localAvatarScaler == null)
return -1f;
if (!_localAvatarScaler.IsForcingHeight())
return -1f;
return _localAvatarScaler.GetTargetHeight();
}
public float GetInitialHeight()
{
if (_localAvatarScaler == null)
return -1f;
return _localAvatarScaler.GetInitialHeight();
}
public bool IsHeightAdjustedFromInitial()
{
return _localAvatarScaler != null && _localAvatarScaler.IsForcingHeight();
}
#endregion
#region Network Methods
public bool DoesNetworkHeightScalerExist(string playerId)
=> _networkedScalers.ContainsKey(playerId);
public int GetNetworkHeightScalerCount()
=> _networkedScalers.Count;
public float GetNetworkHeight(string playerId)
{
if (_networkedScalers.TryGetValue(playerId, out NetworkScaler scaler))
if (scaler.IsForcingHeight()) return scaler.GetTargetHeight();
//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.netIkController.GetRemoteHeight();
// player is invalid???
return -1f;
}
// we will create a Universal Scaler for only users that send a height update
// this is sent at a rate of 10s locally!
internal void OnNetworkHeightUpdateReceived(string playerId, float targetHeight)
{
if (_networkedScalers.TryGetValue(playerId, out NetworkScaler scaler))
scaler.TargetHeight = targetHeight;
else
SetupHeightScalerForNetwork(playerId, targetHeight);
}
internal void OnNetworkAvatarInstantiated(PuppetMaster puppetMaster)
{
var playerId = puppetMaster._playerDescriptor.ownerId;
if (_networkedScalers.TryGetValue(playerId, out NetworkScaler scaler))
scaler.OnAvatarInstantiated(puppetMaster.avatarObject, puppetMaster.netIkController._initialHeight,
puppetMaster.netIkController._initialScale);
}
internal void OnNetworkAvatarDestroyed(PuppetMaster puppetMaster)
{
// on disconnect
if (puppetMaster == null || puppetMaster._playerDescriptor == null)
return;
var playerId = puppetMaster._playerDescriptor.ownerId;
if (_networkedScalers.TryGetValue(playerId, out NetworkScaler scaler))
scaler.OnAvatarDestroyed();
}
internal void RemoveNetworkHeightScaler(string playerId)
{
if (_networkedScalers.ContainsKey(playerId))
{
AvatarScaleMod.Logger.Msg(
$"Removed user height scaler! This is hopefully due to a disconnect or block. : {playerId}");
_networkedScalers.Remove(playerId);
return;
}
AvatarScaleMod.Logger.Msg(
$"Failed to remove a user height scaler! This shouldn't happen. : {playerId}");
}
private void SetupHeightScalerForNetwork(string playerId, float targetHeight)
{
CVRPlayerEntity playerEntity =
CVRPlayerManager.Instance.NetworkPlayers.Find(players => players.Uuid == playerId);
PuppetMaster puppetMaster = playerEntity?.PuppetMaster;
if (playerEntity == null || puppetMaster == null)
{
AvatarScaleMod.Logger.Error(
$"Attempted to set up height scaler for user which does not exist! : {playerId}");
return;
}
AvatarScaleMod.Logger.Msg(
$"Setting up new height scaler for user which has sent a height update! : {playerId}");
if (_networkedScalers.ContainsKey(playerId))
_networkedScalers.Remove(playerId); // ??
NetworkScaler scaler = puppetMaster.gameObject.AddComponent<NetworkScaler>();
scaler.Initialize(playerId);
scaler.OnAvatarInstantiated(puppetMaster.avatarObject, puppetMaster.netIkController._initialHeight,
puppetMaster.netIkController._initialScale);
_networkedScalers[playerId] = scaler;
scaler.TargetHeight = targetHeight;
}
#endregion
#region Manager Methods
// sometimes fun to play with via UE
public void SetUniversalScalingLimit(float min, float max)
{
const float HardCodedMinLimit = 0.01f;
const float HardCodedMaxLimit = 100f;
MinHeight = Mathf.Clamp(min, HardCodedMinLimit, HardCodedMaxLimit);
MaxHeight = Mathf.Clamp(max, HardCodedMinLimit, HardCodedMaxLimit);
AvatarScaleMod.Logger.Msg($"Universal Scaling Limits changed: {min} - {max}");
AvatarScaleMod.Logger.Warning("This will not network to other users unless they also have the same limits set!");
}
public void ResetUniversalScalingLimit()
{
MinHeight = DefaultMinHeight;
MaxHeight = DefaultMaxHeight;
AvatarScaleMod.Logger.Msg("Universal Scaling Limits reset to default!");
}
#endregion
#region Event Listeners
private static void OnAnimationHeightOverride(BaseScaler scaler)
{
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.");
}
#endregion
}

View file

@ -0,0 +1,367 @@
using System.Diagnostics;
using ABI_RC.Core;
using ABI_RC.Core.Util.AnimatorManager;
using NAK.AvatarScaleMod.AvatarScaling;
using NAK.AvatarScaleMod.ScaledComponents;
using UnityEngine;
using UnityEngine.Animations;
namespace NAK.AvatarScaleMod.Components;
[DefaultExecutionOrder(-99999)] // before playersetup/puppetmaster but after animator
public class BaseScaler : MonoBehaviour
{
#region Constants
protected const string ScaleFactorParameterName = "ScaleFactor";
protected const string ScaleFactorParameterNameLocal = "#" + ScaleFactorParameterName;
#endregion
#region Events
// OnAnimatedHeightChanged
public delegate void AnimatedHeightChangedDelegate(BaseScaler scaler);
public event AnimatedHeightChangedDelegate OnAnimatedHeightChanged;
// OnAnimatedHeightOverride
public delegate void AnimatedHeightOverrideDelegate(BaseScaler scaler);
public event AnimatedHeightOverrideDelegate OnAnimatedHeightOverride;
// OnTargetHeightChanged
public delegate void TargetHeightChangedDelegate(BaseScaler scaler);
public event TargetHeightChangedDelegate OnTargetHeightChanged;
// OnHeightReset
public delegate void HeightResetDelegate(BaseScaler scaler);
public event HeightResetDelegate OnTargetHeightReset;
// ------------------------------------------------
protected void InvokeAnimatedHeightChanged()
=> OnAnimatedHeightChanged?.Invoke(this);
protected void InvokeAnimatedHeightOverride()
=> OnAnimatedHeightOverride?.Invoke(this);
protected void InvokeTargetHeightChanged()
=> OnTargetHeightChanged?.Invoke(this);
protected void InvokeTargetHeightReset()
=> OnTargetHeightReset?.Invoke(this);
#endregion
#region Variables
private float _targetHeight;
public float TargetHeight
{
get => _targetHeight;
set
{
if (value < float.Epsilon)
{
// reset to animated height
_targetHeight = _animatedHeight;
_targetScale = _animatedScale;
_scaleFactor = _animatedScaleFactor;
_targetHeightChanged = true;
return;
}
_targetHeight = Mathf.Clamp(value, AvatarScaleManager.MinHeight, AvatarScaleManager.MaxHeight);
_scaleFactor = Mathf.Max(_targetHeight / _initialHeight, 0.01f); //safety
_targetScale = _initialScale * _scaleFactor;
_targetHeightChanged = true;
}
}
protected bool _useTargetHeight;
public bool UseTargetHeight
{
get => _useTargetHeight;
set
{
if (_useTargetHeight == value)
return;
_useTargetHeight = value;
_targetHeightChanged = true;
}
}
// Config variables
public bool avatarIsHidden { get; set; }
public bool overrideAnimationHeight { get; set; }
// State variables
internal bool _isAvatarInstantiated;
private bool _shouldForceHeight => _useTargetHeight || avatarIsHidden;
private bool _targetHeightChanged;
// Avatar info
internal Transform _avatarTransform;
internal CVRAnimatorManager _animatorManager;
// Initial scaling
internal float _initialHeight;
internal Vector3 _initialScale;
// Forced scaling (Universal & Hidden Avatar)
internal Vector3 _targetScale = Vector3.one;
internal float _scaleFactor = 1f;
// AnimationClip-based scaling (Local Avatar)
internal float _animatedHeight;
internal Vector3 _animatedScale;
internal float _animatedScaleFactor = 1f;
#endregion
#region Avatar Events
public virtual void OnAvatarInstantiated(GameObject avatarObject, float initialHeight, Vector3 initialScale)
{
if (_isAvatarInstantiated) return;
_isAvatarInstantiated = true;
_initialHeight = _animatedHeight = Mathf.Clamp(initialHeight, 0.01f, 100f);
_initialScale = _animatedScale = initialScale;
_animatedScaleFactor = 1f;
if (!_shouldForceHeight) // not universal or hidden avatar
{
_targetHeight = _initialHeight;
_targetScale = _initialScale;
_scaleFactor = 1f;
}
_avatarTransform = avatarObject.transform;
Stopwatch stopwatch = new();
stopwatch.Start();
FindComponentsOfType(scalableComponentTypes);
stopwatch.Stop();
if (ModSettings.Debug_ComponentSearchTime.Value)
AvatarScaleMod.Logger.Msg($"({typeof(LocalScaler)}) Component search time for {avatarObject}: {stopwatch.ElapsedMilliseconds}ms");
}
public void OnAvatarDestroyed()
{
if (!_isAvatarInstantiated) return;
_isAvatarInstantiated = false;
_avatarTransform = null;
ClearComponentLists();
}
#endregion
#region Public Methods
public float GetInitialHeight() => _initialHeight;
public float GetTargetHeight() => _targetHeight;
public float GetAnimatedHeight() => _animatedHeight;
public bool IsForcingHeight() => _shouldForceHeight;
#endregion
#region Private Methods
public bool ApplyTargetHeight()
{
if (!_isAvatarInstantiated || _initialHeight == 0)
return false;
if (_avatarTransform == null)
return false;
_targetHeightChanged = false;
ScaleAvatarRoot();
UpdateAnimatorParameter();
ApplyComponentScaling();
InvokeTargetHeightChanged();
return true;
}
protected void ResetTargetHeight()
{
if (!_isAvatarInstantiated)
return;
if (_avatarTransform == null)
return;
_targetHeight = _initialHeight;
_targetScale = _initialScale;
_scaleFactor = 1f;
_targetHeightChanged = false;
ScaleAvatarRoot();
UpdateAnimatorParameter();
ApplyComponentScaling();
InvokeTargetHeightReset();
}
private void ScaleAvatarRoot()
{
if (_avatarTransform == null) return;
_avatarTransform.localScale = _targetScale;
}
protected virtual void UpdateAnimatorParameter()
{
// empty
}
#endregion
#region Unity Events
public virtual void LateUpdate()
{
if (!_isAvatarInstantiated)
return; // no avatar
if (!_shouldForceHeight)
return; // using built-in scaling
// called on state change
if (_targetHeightChanged)
{
if (_useTargetHeight)
ApplyTargetHeight();
else
ResetTargetHeight();
_targetHeightChanged = false;
InvokeTargetHeightChanged();
}
// called constantly when forcing change
if (_shouldForceHeight)
ScaleAvatarRoot();
}
internal virtual void OnDestroy()
{
ClearComponentLists();
}
#endregion
#region Component Scaling
internal static readonly Type[] scalableComponentTypes =
{
typeof(Light),
typeof(AudioSource),
typeof(ParticleSystem),
typeof(ParentConstraint),
typeof(PositionConstraint),
typeof(ScaleConstraint)
};
private readonly List<ScaledLight> _scaledLights = new();
private readonly List<ScaledAudioSource> _scaledAudioSources = new();
private readonly List<ScaledParentConstraint> _scaledParentConstraints = new();
private readonly List<ScaledPositionConstraint> _scaledPositionConstraints = new();
private readonly List<ScaledScaleConstraint> _scaledScaleConstraints = new();
private void ClearComponentLists()
{
_scaledLights.Clear();
_scaledAudioSources.Clear();
_scaledParentConstraints.Clear();
_scaledPositionConstraints.Clear();
_scaledScaleConstraints.Clear();
}
internal void FindComponentsOfType(Type[] types)
{
var components = _avatarTransform.gameObject.GetComponentsInChildren<Component>(true);
foreach (Component component in components)
{
if (this == null) break;
if (component == null) continue;
Type componentType = component.GetType();
if (types.Contains(componentType))
AddScaledComponent(componentType, component);
}
}
private void AddScaledComponent(Type type, Component component)
{
switch (type)
{
case not null when type == typeof(AudioSource):
_scaledAudioSources.Add(new ScaledAudioSource((AudioSource)component));
break;
case not null when type == typeof(Light):
_scaledLights.Add(new ScaledLight((Light)component));
break;
case not null when type == typeof(ParentConstraint):
_scaledParentConstraints.Add(new ScaledParentConstraint((ParentConstraint)component));
break;
case not null when type == typeof(PositionConstraint):
_scaledPositionConstraints.Add(new ScaledPositionConstraint((PositionConstraint)component));
break;
case not null when type == typeof(ScaleConstraint):
_scaledScaleConstraints.Add(new ScaledScaleConstraint((ScaleConstraint)component));
break;
}
}
private void ApplyComponentScaling()
{
// UpdateLightScales(); // might break dps
UpdateAudioSourceScales();
UpdateParentConstraintScales();
UpdatePositionConstraintScales();
UpdateScaleConstraintScales();
}
private void UpdateLightScales()
{
// Update range of each light component
foreach (ScaledLight light in _scaledLights)
light.Scale(_scaleFactor);
}
private void UpdateAudioSourceScales()
{
// Update min and max distance of each audio source component
foreach (ScaledAudioSource audioSource in _scaledAudioSources)
audioSource.Scale(_scaleFactor);
}
private void UpdateParentConstraintScales()
{
// Update translationAtRest and translationOffsets of each parent constraint component
foreach (ScaledParentConstraint parentConstraint in _scaledParentConstraints)
parentConstraint.Scale(_scaleFactor);
}
private void UpdatePositionConstraintScales()
{
// Update translationAtRest and translationOffset of each position constraint component
foreach (ScaledPositionConstraint positionConstraint in _scaledPositionConstraints)
positionConstraint.Scale(_scaleFactor);
}
private void UpdateScaleConstraintScales()
{
// Update scaleAtRest and scaleOffset of each scale constraint component
foreach (ScaledScaleConstraint scaleConstraint in _scaledScaleConstraints)
scaleConstraint.Scale(_scaleFactor);
}
#endregion
}

View file

@ -0,0 +1,92 @@
using ABI_RC.Core;
using ABI_RC.Core.Player;
using ABI_RC.Core.UI;
using NAK.AvatarScaleMod.AvatarScaling;
using UnityEngine;
namespace NAK.AvatarScaleMod.Components;
public class LocalScaler : BaseScaler
{
#region Public Methods
public void Initialize()
{
_animatorManager = GetComponentInParent<PlayerSetup>().animatorManager;
_isAvatarInstantiated = false;
// listen for events
}
#endregion
#region Overrides
public override void OnAvatarInstantiated(GameObject avatarObject, float initialHeight, Vector3 initialScale)
{
if (avatarObject == null)
return;
base.OnAvatarInstantiated(avatarObject, initialHeight, initialScale);
}
protected override void UpdateAnimatorParameter()
{
if (_animatorManager == null)
return;
_animatorManager.SetParameter(ScaleFactorParameterName, _scaleFactor);
_animatorManager.SetParameter(ScaleFactorParameterNameLocal, _scaleFactor);
}
public override void LateUpdate()
{
if (!CheckForAnimationScaleChange())
base.LateUpdate();
}
#endregion
#region Private Methods
private bool CheckForAnimationScaleChange()
{
if (_avatarTransform == null)
return false;
Vector3 localScale = _avatarTransform.localScale;
// scale matches last recorded animation scale
if (localScale == _animatedScale)
return false;
// avatar may not have scale animation, check if it isn't equal to targetScale
if (localScale == _targetScale)
return false;
// this is the first time we've seen the avatar animated scale, record it!
if (_animatedScale == Vector3.zero)
{
_animatedScale = localScale;
return false;
}
// animation scale changed, record it!
Vector3 scaleDifference = CVRTools.DivideVectors(localScale - _initialScale, _initialScale);
_animatedScaleFactor = scaleDifference.y;
_animatedHeight = (_initialHeight * _animatedScaleFactor) + _initialHeight;
_animatedScale = localScale;
if (overrideAnimationHeight
|| !_useTargetHeight)
return false; // user has disabled animation height override or is not using universal scaling
// animation scale changed and now will override universal scaling
ResetTargetHeight();
InvokeAnimatedHeightOverride();
return true;
}
#endregion
}

View file

@ -0,0 +1,58 @@
using System.Diagnostics;
using ABI_RC.Core.Player;
using NAK.AvatarScaleMod.AvatarScaling;
using UnityEngine;
namespace NAK.AvatarScaleMod.Components;
public class NetworkScaler : BaseScaler
{
private string playerGuid;
#region Public Methods
public void Initialize(string playerId)
{
playerGuid = playerId;
_animatorManager = GetComponentInParent<PuppetMaster>().animatorManager;
_isAvatarInstantiated = false;
}
#endregion
#region Overrides
public override void OnAvatarInstantiated(GameObject avatarObject, float initialHeight, Vector3 initialScale)
{
if (avatarObject == null)
return;
base.OnAvatarInstantiated(avatarObject, initialHeight, initialScale);
Stopwatch stopwatch = new();
stopwatch.Start();
FindComponentsOfType(scalableComponentTypes);
stopwatch.Stop();
if (ModSettings.Debug_ComponentSearchTime.Value)
AvatarScaleMod.Logger.Msg($"({typeof(NetworkScaler)}) Component search time for {avatarObject}: {stopwatch.ElapsedMilliseconds}ms");
// TODO: why did i do this? height is never set prior to this method being called
// if (_isHeightAdjustedFromInitial && heightNeedsUpdate)
// UpdateScaleIfInstantiated();
}
protected override void UpdateAnimatorParameter()
{
_animatorManager?.SetParameter(ScaleFactorParameterNameLocal, _scaleFactor);
}
internal override void OnDestroy()
{
AvatarScaleManager.Instance.RemoveNetworkHeightScaler(playerGuid);
base.OnDestroy();
}
#endregion
}

View file

@ -0,0 +1,102 @@
using NAK.AvatarScaleMod.Components;
namespace NAK.AvatarScaleMod.AvatarScaling;
public static class AvatarScaleEvents
{
#region Local Avatar Scaling Events
/// <summary>
/// Invoked when the local avatar's height changes for any reason.
/// </summary>
public static readonly AvatarScaleEvent<LocalScaler> OnLocalAvatarHeightChanged = new();
/// <summary>
/// Invoked when the local avatar's animated height changes.
/// </summary>
public static readonly AvatarScaleEvent<LocalScaler> OnLocalAvatarAnimatedHeightChanged = new();
/// <summary>
/// Invoked when the local avatar's target height changes.
/// </summary>
public static readonly AvatarScaleEvent<LocalScaler> OnLocalAvatarTargetHeightChanged = new();
/// <summary>
/// Invoked when the local avatar's height is reset.
/// </summary>
public static readonly AvatarScaleEvent<LocalScaler> OnLocalAvatarHeightReset = new();
#endregion
#region Avatar Scaling Events
/// <summary>
/// Invoked when a remote avatar's height changes.
/// </summary>
public static readonly AvatarScaleEvent<string, NetworkScaler> OnRemoteAvatarHeightChanged = new();
/// <summary>
/// Invoked when a remote avatar's height is reset.
/// </summary>
public static readonly AvatarScaleEvent<string, NetworkScaler> OnRemoteAvatarHeightReset = new();
#endregion
#region Event Classes
public class AvatarScaleEvent<T>
{
private Action<T> _listener = arg => { };
public void AddListener(Action<T> listener) => _listener += listener;
public void RemoveListener(Action<T> listener) => _listener -= listener;
public void Invoke(T arg)
{
var invokeList = _listener.GetInvocationList();
foreach (Delegate method in invokeList)
{
if (method is not Action<T> action)
continue;
try
{
action(arg);
}
catch (Exception e)
{
AvatarScaleMod.Logger.Error($"Unable to invoke listener, an exception was thrown and not handled: {e}.");
}
}
}
}
public class AvatarScaleEvent<T1, T2>
{
private Action<T1, T2> _listener = (arg1, arg2) => { };
public void AddListener(Action<T1, T2> listener) => _listener += listener;
public void RemoveListener(Action<T1, T2> listener) => _listener -= listener;
public void Invoke(T1 arg1, T2 arg2)
{
var invokeList = _listener.GetInvocationList();
foreach (Delegate method in invokeList)
{
if (method is not Action<T1, T2> action)
continue;
try
{
action(arg1, arg2);
}
catch (Exception e)
{
AvatarScaleMod.Logger.Error($"Unable to invoke listener, an exception was thrown and not handled: {e}.");
}
}
}
}
#endregion
}

View file

@ -0,0 +1,132 @@
using UnityEngine;
using UnityEngine.Animations;
namespace NAK.AvatarScaleMod.ScaledComponents;
public class ScaledAudioSource
{
private readonly AudioSource Component;
private readonly float InitialMinDistance;
private readonly float InitialMaxDistance;
public ScaledAudioSource(AudioSource component)
{
Component = component;
InitialMinDistance = component.minDistance;
InitialMaxDistance = component.maxDistance;
}
public void Scale(float scaleFactor)
{
Component.minDistance = InitialMinDistance * scaleFactor;
Component.maxDistance = InitialMaxDistance * scaleFactor;
}
public void Reset()
{
Component.minDistance = InitialMinDistance;
Component.maxDistance = InitialMaxDistance;
}
}
public class ScaledLight
{
private readonly Light Component;
private readonly float InitialRange;
public ScaledLight(Light component)
{
Component = component;
InitialRange = component.range;
}
public void Scale(float scaleFactor)
{
Component.range = InitialRange * scaleFactor;
}
public void Reset()
{
Component.range = InitialRange;
}
}
public class ScaledPositionConstraint
{
private readonly PositionConstraint Component;
private readonly Vector3 InitialTranslationAtRest;
private readonly Vector3 InitialTranslationOffset;
public ScaledPositionConstraint(PositionConstraint component)
{
Component = component;
InitialTranslationAtRest = component.translationAtRest;
InitialTranslationOffset = component.translationOffset;
}
public void Scale(float scaleFactor)
{
Component.translationAtRest = InitialTranslationAtRest * scaleFactor;
Component.translationOffset = InitialTranslationOffset * scaleFactor;
}
public void Reset()
{
Component.translationAtRest = InitialTranslationAtRest;
Component.translationOffset = InitialTranslationOffset;
}
}
public class ScaledParentConstraint
{
private readonly ParentConstraint Component;
private readonly Vector3 InitialTranslationAtRest;
private readonly List<Vector3> InitialTranslationOffsets;
public ScaledParentConstraint(ParentConstraint component)
{
Component = component;
InitialTranslationAtRest = component.translationAtRest;
InitialTranslationOffsets = component.translationOffsets.ToList();
}
public void Scale(float scaleFactor)
{
Component.translationAtRest = InitialTranslationAtRest * scaleFactor;
for (int i = 0; i < InitialTranslationOffsets.Count; i++)
Component.translationOffsets[i] = InitialTranslationOffsets[i] * scaleFactor;
}
public void Reset()
{
Component.translationAtRest = InitialTranslationAtRest;
for (int i = 0; i < InitialTranslationOffsets.Count; i++)
Component.translationOffsets[i] = InitialTranslationOffsets[i];
}
}
public class ScaledScaleConstraint
{
private readonly ScaleConstraint Component;
private readonly Vector3 InitialScaleAtRest;
private readonly Vector3 InitialScaleOffset;
public ScaledScaleConstraint(ScaleConstraint component)
{
Component = component;
InitialScaleAtRest = component.scaleAtRest;
InitialScaleOffset = component.scaleOffset;
}
public void Scale(float scaleFactor)
{
Component.scaleAtRest = InitialScaleAtRest * scaleFactor;
// Component.scaleOffset = InitialScaleOffset * scaleFactor;
}
public void Reset()
{
Component.scaleAtRest = InitialScaleAtRest;
Component.scaleOffset = InitialScaleOffset;
}
}

View file

@ -0,0 +1,113 @@
using ABI_RC.Core.Player;
using HarmonyLib;
using NAK.AvatarScaleMod.AvatarScaling;
using NAK.AvatarScaleMod.GestureReconizer;
using UnityEngine;
using Object = UnityEngine.Object;
namespace NAK.AvatarScaleMod.HarmonyPatches;
internal class PlayerSetupPatches
{
[HarmonyPostfix]
[HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.Start))]
private static void Postfix_PlayerSetup_Start()
{
try
{
GameObject scaleManager = new (nameof(AvatarScaleManager), typeof(AvatarScaleManager));
Object.DontDestroyOnLoad(scaleManager);
}
catch (Exception e)
{
AvatarScaleMod.Logger.Error($"Error during the patched method {nameof(Postfix_PlayerSetup_Start)}");
AvatarScaleMod.Logger.Error(e);
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.SetupAvatar))]
private static void Postfix_PlayerSetup_SetupAvatar(ref PlayerSetup __instance)
{
try
{
AvatarScaleManager.Instance.OnAvatarInstantiated(__instance);
}
catch (Exception e)
{
AvatarScaleMod.Logger.Error($"Error during the patched method {nameof(Postfix_PlayerSetup_SetupAvatar)}");
AvatarScaleMod.Logger.Error(e);
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.ClearAvatar))]
private static void Postfix_PlayerSetup_ClearAvatar(ref PlayerSetup __instance)
{
try
{
if (__instance == null) return; // this is called when the game is closed
AvatarScaleManager.Instance.OnAvatarDestroyed(__instance);
}
catch (Exception e)
{
AvatarScaleMod.Logger.Error($"Error during the patched method {nameof(Postfix_PlayerSetup_ClearAvatar)}");
AvatarScaleMod.Logger.Error(e);
}
}
}
internal class PuppetMasterPatches
{
[HarmonyPostfix]
[HarmonyPatch(typeof(PuppetMaster), nameof(PuppetMaster.AvatarInstantiated))]
private static void Postfix_PuppetMaster_AvatarInstantiated(ref PuppetMaster __instance)
{
try
{
AvatarScaleManager.Instance.OnNetworkAvatarInstantiated(__instance);
}
catch (Exception e)
{
AvatarScaleMod.Logger.Error(
$"Error during the patched method {nameof(Postfix_PuppetMaster_AvatarInstantiated)}");
AvatarScaleMod.Logger.Error(e);
}
}
[HarmonyPostfix]
[HarmonyPatch(typeof(PuppetMaster), nameof(PuppetMaster.AvatarDestroyed))]
private static void Postfix_PuppetMaster_AvatarDestroyed(ref PuppetMaster __instance)
{
try
{
if (__instance == null) return; // this is called when the game is closed
AvatarScaleManager.Instance.OnNetworkAvatarDestroyed(__instance);
}
catch (Exception e)
{
AvatarScaleMod.Logger.Error(
$"Error during the patched method {nameof(Postfix_PuppetMaster_AvatarDestroyed)}");
AvatarScaleMod.Logger.Error(e);
}
}
}
internal class GesturePlaneTestPatches
{
[HarmonyPostfix]
[HarmonyPatch(typeof(GesturePlaneTest), nameof(GesturePlaneTest.Start))]
private static void Postfix_GesturePlaneTest_Start()
{
try
{
// nicked from Kafe >:))))
ScaleReconizer.Initialize();
}
catch (Exception e)
{
AvatarScaleMod.Logger.Error($"Error during the patched method {nameof(Postfix_GesturePlaneTest_Start)}");
AvatarScaleMod.Logger.Error(e);
}
}
}

View file

@ -0,0 +1,43 @@
using NAK.AvatarScaleMod.AvatarScaling;
using UnityEngine;
namespace NAK.AvatarScaleMod.InputHandling;
internal static class DebugKeybinds
{
private const float Step = 0.1f;
internal static void DoDebugInput()
{
if (AvatarScaleManager.Instance == null)
return;
if (Input.GetKeyDown(KeyCode.Equals) || Input.GetKeyDown(KeyCode.KeypadPlus))
{
AdjustHeight(Step);
}
else if (Input.GetKeyDown(KeyCode.Minus) || Input.GetKeyDown(KeyCode.KeypadMinus))
{
AdjustHeight(-Step);
}
else if (Input.GetKeyDown(KeyCode.Backspace))
{
ResetHeight();
}
}
private static void AdjustHeight(float adjustment)
{
float currentHeight = AvatarScaleManager.Instance.GetHeight() + adjustment;
currentHeight = Mathf.Max(0f, currentHeight);
AvatarScaleManager.Instance.SetTargetHeight(currentHeight);
AvatarScaleMod.Logger.Msg($"[Debug] Setting height: {currentHeight}");
}
private static void ResetHeight()
{
AvatarScaleManager.Instance.Setting_UniversalScaling = false;
AvatarScaleMod.Logger.Msg("[Debug] Resetting height.");
}
}

View file

@ -0,0 +1,108 @@
using ABI_RC.Core.Savior;
using ABI_RC.Systems.InputManagement;
using NAK.AvatarScaleMod.AvatarScaling;
using UnityEngine;
namespace NAK.AvatarScaleMod.GestureReconizer;
public static class ScaleReconizer
{
public static bool Enabled = true;
// Require triggers to be down while doing fist - Exteratta
public static bool RequireTriggers = true;
// Initial values when scale gesture is started
private static float _initialModifier;
private static float _initialTargetHeight;
public static void Initialize()
{
// This requires arms far outward- pull inward with fist and triggers.
// Release triggers while still holding fist to readjust.
CVRGesture gesture = new CVRGesture
{
name = "avatarScaleIn",
type = CVRGesture.GestureType.Hold
};
gesture.steps.Add(new CVRGestureStep
{
firstGesture = CVRGestureStep.Gesture.Fist,
secondGesture = CVRGestureStep.Gesture.Fist,
startDistance = 1f,
endDistance = 0.25f,
direction = CVRGestureStep.GestureDirection.MovingIn,
needsToBeInView = true,
});
gesture.onStart.AddListener(OnScaleStart);
gesture.onStay.AddListener(OnScaleStay);
gesture.onEnd.AddListener(OnScaleEnd);
CVRGestureRecognizer.Instance.gestures.Add(gesture);
gesture = new CVRGesture
{
name = "avatarScaleOut",
type = CVRGesture.GestureType.Hold
};
gesture.steps.Add(new CVRGestureStep
{
firstGesture = CVRGestureStep.Gesture.Fist,
secondGesture = CVRGestureStep.Gesture.Fist,
startDistance = 0.25f,
endDistance = 1f,
direction = CVRGestureStep.GestureDirection.MovingOut,
needsToBeInView = true,
});
gesture.onStart.AddListener(OnScaleStart);
gesture.onStay.AddListener(OnScaleStay);
gesture.onEnd.AddListener(OnScaleEnd);
CVRGestureRecognizer.Instance.gestures.Add(gesture);
}
private static void OnScaleStart(float modifier, Transform transform1, Transform transform2)
{
if (!Enabled)
return;
// Store initial modifier so we can get difference later
_initialModifier = Mathf.Max(modifier, 0.01f); // no zero
_initialTargetHeight = AvatarScaleManager.Instance.GetHeight();
}
private static void OnScaleStay(float modifier, Transform transform1, Transform transform2)
{
if (!Enabled)
return;
modifier = Mathf.Max(modifier, 0.01f); // no zero
// Allow user to release triggers to reset "world grip"
if (RequireTriggers && !AreBothTriggersDown())
{
_initialModifier = modifier;
_initialTargetHeight = AvatarScaleManager.Instance.GetHeight();
return;
}
// Invert so the gesture is more of a world squish instead of happy hug
float modifierRatio = 1f / (modifier / _initialModifier);
// Determine the adjustment factor for the height, this will be >1 if scaling up, <1 if scaling down.
float heightAdjustmentFactor = (modifierRatio > 1) ? 1 + (modifierRatio - 1) : 1 - (1 - modifierRatio);
// Apply the adjustment to the target height
AvatarScaleManager.Instance.SetTargetHeight(_initialTargetHeight * heightAdjustmentFactor);
}
private static void OnScaleEnd(float modifier, Transform transform1, Transform transform2)
{
// Unused, needed for mod network?
}
private static bool AreBothTriggersDown()
{
// Maybe it should be one trigger? Imagine XSOverlay scaling but for player.
return CVRInputManager.Instance.interactLeftValue > 0.75f && CVRInputManager.Instance.interactRightValue > 0.75f;
}
}

View file

@ -0,0 +1,99 @@
using ABI_RC.Core.Player;
using BTKUILib;
using BTKUILib.UIObjects;
using NAK.AvatarScaleMod.AvatarScaling;
namespace NAK.AvatarScaleMod.Integrations;
public static partial class BtkUiAddon
{
private static Page _asmRootPage;
private static string _rootPageElementID;
public static void Initialize()
{
Prepare_Icons();
Setup_AvatarScaleModTab();
Setup_PlayerSelectPage();
}
#region Initialization
private static void Prepare_Icons()
{
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 Setup_AvatarScaleModTab()
{
_asmRootPage = new Page(ModSettings.ModName, ModSettings.ASM_SettingsCategory, true, "ASM_Icon_AvatarHeightConfig")
{
MenuTitle = ModSettings.ASM_SettingsCategory,
MenuSubtitle = "Everything Avatar Scaling!"
};
_rootPageElementID = _asmRootPage.ElementID;
QuickMenuAPI.OnTabChange += OnTabChange;
QuickMenuAPI.UserJoin += OnUserJoinLeave;
QuickMenuAPI.UserLeave += OnUserJoinLeave;
QuickMenuAPI.OnWorldLeave += OnWorldLeave;
// Avatar Scale Mod
Setup_AvatarScaleModCategory(_asmRootPage);
// Avatar Scale Tool
Setup_AvatarScaleToolCategory(_asmRootPage);
// Universal Scaling Settings
Setup_UniversalScalingSettings(_asmRootPage);
// Debug Options
Setup_DebugOptionsCategory(_asmRootPage);
}
#endregion
#region Player Count Display
private static void OnWorldLeave()
=> UpdatePlayerCountDisplay();
private static void OnUserJoinLeave(CVRPlayerEntity _)
=> UpdatePlayerCountDisplay();
private static void UpdatePlayerCountDisplay()
{
if (_asmRootPage == null)
return;
int modUserCount = AvatarScaleManager.Instance.GetNetworkHeightScalerCount();
int playerCount = CVRPlayerManager.Instance.NetworkPlayers.Count;
_asmRootPage.MenuSubtitle = $"Everything Avatar Scaling! :: ({modUserCount}/{playerCount} players using ASM)";
}
#endregion
#region Double-Click Reset Height
private static DateTime lastTime = DateTime.Now;
private static void OnTabChange(string newTab, string previousTab)
{
if (newTab == _rootPageElementID)
{
UpdatePlayerCountDisplay();
TimeSpan timeDifference = DateTime.Now - lastTime;
if (timeDifference.TotalSeconds <= 0.5)
{
AvatarScaleManager.Instance.Setting_UniversalScaling = false;
return;
}
}
lastTime = DateTime.Now;
}
#endregion
}

View file

@ -0,0 +1,16 @@
using BTKUILib.UIObjects;
namespace NAK.AvatarScaleMod.Integrations;
public static partial class BtkUiAddon
{
private static void Setup_AvatarScaleModCategory(Page page)
{
Category avScaleModCategory = AddMelonCategory(ref page, ModSettings.Hidden_Foldout_ASM_SettingsCategory);
AddMelonToggle(ref avScaleModCategory, ModSettings.EntryScaleGestureEnabled);
AddMelonToggle(ref avScaleModCategory, ModSettings.EntryScaleKeybindingsEnabled);
AddMelonToggle(ref avScaleModCategory, ModSettings.EntryPersistentHeight);
AddMelonToggle(ref avScaleModCategory, ModSettings.EntryPersistThroughRestart);
}
}

View file

@ -0,0 +1,22 @@
using BTKUILib.UIObjects;
using BTKUILib.UIObjects.Components;
namespace NAK.AvatarScaleMod.Integrations;
public static partial class BtkUiAddon
{
private static void Setup_AvatarScaleToolCategory(Page page)
{
Category avScaleToolCategory = AddMelonCategory(ref page, ModSettings.Hidden_Foldout_AST_SettingsCategory);
AddMelonStringInput(ref avScaleToolCategory, ModSettings.EntryASTScaleParameter, "icon");
AddMelonNumberInput(ref avScaleToolCategory, ModSettings.EntryASTMinHeight, "icon");
AddMelonNumberInput(ref avScaleToolCategory, ModSettings.EntryASTMaxHeight, "icon");
avScaleToolCategory.AddButton("Reset Overrides", "", "Reset to Avatar Scale Tool default values.", ButtonStyle.TextOnly)
.OnPress += () =>{
ModSettings.EntryASTScaleParameter.ResetToDefault();
ModSettings.EntryASTMinHeight.ResetToDefault();
ModSettings.EntryASTMaxHeight.ResetToDefault();
};
}
}

View file

@ -0,0 +1,15 @@
using BTKUILib.UIObjects;
namespace NAK.AvatarScaleMod.Integrations;
public static partial class BtkUiAddon
{
private static void Setup_DebugOptionsCategory(Page page)
{
Category debugCategory = AddMelonCategory(ref page, ModSettings.Hidden_Foldout_DEBUG_SettingsCategory);
AddMelonToggle(ref debugCategory, ModSettings.Debug_NetworkInbound);
AddMelonToggle(ref debugCategory, ModSettings.Debug_NetworkOutbound);
AddMelonToggle(ref debugCategory, ModSettings.Debug_ComponentSearchTime);
}
}

View file

@ -0,0 +1,74 @@
using BTKUILib;
using BTKUILib.UIObjects;
using BTKUILib.UIObjects.Components;
using NAK.AvatarScaleMod.AvatarScaling;
using System.Collections.Generic; // Added for list support
namespace NAK.AvatarScaleMod.Integrations;
public static partial class BtkUiAddon
{
private static readonly List<QMUIElement> USM_QmUiElements = new();
private static void Setup_UniversalScalingSettings(Page page)
{
Category uniScalingCategory = AddMelonCategory(ref page, ModSettings.Hidden_Foldout_USM_SettingsCategory);
SliderFloat scaleSlider = AddMelonSlider(ref uniScalingCategory, ModSettings.EntryHiddenAvatarHeight, AvatarScaleManager.DefaultMinHeight, AvatarScaleManager.DefaultMaxHeight);
Button resetHeightButton = uniScalingCategory.AddButton(ModSettings.EntryUseUniversalScaling.DisplayName, "icon", ModSettings.EntryUseUniversalScaling.Description, ButtonStyle.TextOnly);
resetHeightButton.OnPress += () =>
{
if (ModSettings.EntryUseUniversalScaling.Value)
{
ModSettings.EntryUseUniversalScaling.Value = false;
return;
}
QuickMenuAPI.ShowConfirm("Use Universal Scaling?",
"Universal scaling only works when other users also have the mod! Are you sure you want to use Universal Scaling?",
() => ModSettings.EntryUseUniversalScaling.Value = true);
};
// Elements that should be disabled when universal scaling is disabled
USM_QmUiElements.AddRange(new QMUIElement[]
{
scaleSlider,
AddMelonToggle(ref uniScalingCategory, ModSettings.EntryScaleComponents),
AddMelonToggle(ref uniScalingCategory, ModSettings.EntryAnimationScalingOverride)
});
// Events for the slider
scaleSlider.OnValueUpdated += OnAvatarHeightSliderChanged;
scaleSlider.OnSliderReset += OnAvatarHeightSliderReset;
AvatarScaleEvents.OnLocalAvatarAnimatedHeightChanged.AddListener((scaler) =>
{
scaleSlider.SetSliderValue(scaler.GetTargetHeight());
scaleSlider.DefaultValue = scaler.GetAnimatedHeight();
});
// Initial values
OnUniversalScalingChanged(ModSettings.EntryUseUniversalScaling.Value);
ModSettings.EntryUseUniversalScaling.OnEntryValueChanged.Subscribe((_, newValue) => OnUniversalScalingChanged(newValue));
}
private static void OnUniversalScalingChanged(bool value)
{
foreach (QMUIElement uiElement in USM_QmUiElements)
uiElement.Disabled = !value;
}
#region Slider Events
private static void OnAvatarHeightSliderChanged(float height)
{
AvatarScaleManager.Instance.SetTargetHeight(height);
}
private static void OnAvatarHeightSliderReset()
{
AvatarScaleManager.Instance.Setting_UniversalScaling = false;
}
#endregion
}

View file

@ -0,0 +1,64 @@
using BTKUILib;
using BTKUILib.UIObjects;
using BTKUILib.UIObjects.Components;
using NAK.AvatarScaleMod.AvatarScaling;
namespace NAK.AvatarScaleMod.Integrations;
public static partial class BtkUiAddon
{
private static Button _playerHasModElement;
private static string _selectedPlayer;
private static void Setup_PlayerSelectPage()
{
QuickMenuAPI.OnPlayerSelected += OnPlayerSelected;
Category category = QuickMenuAPI.PlayerSelectPage.AddCategory(ModSettings.ASM_SettingsCategory, ModSettings.ModName);
_playerHasModElement = category.AddButton("PLAYER_HAS_MOD", "ASM_Icon_AvatarHeightCopy", "PLAYER_HAS_MOD_TOOLTIP");
Button button = category.AddButton("Copy Height", "ASM_Icon_AvatarHeightCopy", "Copy selected players Eye Height.");
button.OnPress += OnCopyPlayerHeight;
}
#region QM Events
private static void OnPlayerSelected(string _, string id)
{
_selectedPlayer = id;
UpdatePlayerHasModIcon();
}
private static void OnCopyPlayerHeight()
{
float networkHeight = AvatarScaleManager.Instance.GetNetworkHeight(_selectedPlayer);
if (networkHeight < 0) return;
AvatarScaleManager.Instance.SetTargetHeight(networkHeight);
}
#endregion
#region Private Methods
private static void UpdatePlayerHasModIcon()
{
if (_playerHasModElement == null)
return;
if (AvatarScaleManager.Instance.DoesNetworkHeightScalerExist(_selectedPlayer))
{
_playerHasModElement.ButtonIcon = "ASM_Icon_AvatarHeightCopy";
_playerHasModElement.ButtonText = "Player Has Mod";
_playerHasModElement.ButtonTooltip = "This player has the Avatar Scale Mod installed!";
}
else
{
_playerHasModElement.ButtonIcon = "ASM_Icon_AvatarHeightConfig";
_playerHasModElement.ButtonText = "Player Does Not Have Mod";
_playerHasModElement.ButtonTooltip = "This player does not have the Avatar Scale Mod installed!";
}
}
#endregion
}

View file

@ -0,0 +1,77 @@
using System.Reflection;
using BTKUILib;
using BTKUILib.UIObjects;
using BTKUILib.UIObjects.Components;
using MelonLoader;
using UnityEngine;
namespace NAK.AvatarScaleMod.Integrations;
public static partial class BtkUiAddon
{
#region Melon Preference Helpers
private static ToggleButton AddMelonToggle(ref Category category, MelonPreferences_Entry<bool> entry)
{
ToggleButton toggle = category.AddToggle(entry.DisplayName, entry.Description, entry.Value);
toggle.OnValueUpdated += b => entry.Value = b;
return toggle;
}
private static SliderFloat AddMelonSlider(ref Category category, MelonPreferences_Entry<float> entry, float min,
float max, int decimalPlaces = 2, bool allowReset = true)
{
SliderFloat slider = category.AddSlider(entry.DisplayName, entry.Description,
Mathf.Clamp(entry.Value, min, max), min, max, decimalPlaces, entry.DefaultValue, allowReset);
slider.OnValueUpdated += f => entry.Value = f;
return slider;
}
private static Button AddMelonStringInput(ref Category category, MelonPreferences_Entry<string> entry, string buttonIcon = "", ButtonStyle buttonStyle = ButtonStyle.TextOnly)
{
Button button = category.AddButton(entry.DisplayName, buttonIcon, entry.Description, buttonStyle);
button.OnPress += () => QuickMenuAPI.OpenKeyboard(entry.Value, s => entry.Value = s);
return button;
}
private static Button AddMelonNumberInput(ref Category category, MelonPreferences_Entry<float> entry, string buttonIcon = "", ButtonStyle buttonStyle = ButtonStyle.TextOnly)
{
Button button = category.AddButton(entry.DisplayName, buttonIcon, entry.Description, buttonStyle);
button.OnPress += () => QuickMenuAPI.OpenNumberInput(entry.DisplayName, entry.Value, f => entry.Value = f);
return button;
}
// private static SliderFloat AddMelonSlider(ref Page page, MelonPreferences_Entry<float> entry, float min, float max, int decimalPlaces = 2, bool allowReset = true)
// {
// SliderFloat slider = page.AddSlider(entry.DisplayName, entry.Description, Mathf.Clamp(entry.Value, min, max), min, max, decimalPlaces, entry.DefaultValue, allowReset);
// slider.OnValueUpdated += f => entry.Value = f;
// return slider;
// }
/// <summary>
/// Helper method to create a category that saves its collapsed state to a MelonPreferences entry.
/// </summary>
/// <param name="page"></param>
/// <param name="entry"></param>
/// <param name="showHeader"></param>
/// <returns></returns>
private static Category AddMelonCategory(ref Page page, MelonPreferences_Entry<bool> entry, bool showHeader = true)
{
Category category = page.AddCategory(entry.DisplayName, showHeader, true, entry.Value);
category.OnCollapse += b => entry.Value = b;
return category;
}
#endregion
#region Icon Utils
private static Stream GetIconStream(string iconName)
{
Assembly assembly = Assembly.GetExecutingAssembly();
string assemblyName = assembly.GetName().Name;
return assembly.GetManifestResourceStream($"{assemblyName}.resources.{iconName}");
}
#endregion
}

View file

@ -0,0 +1,52 @@
using MelonLoader;
using NAK.AvatarScaleMod.InputHandling;
using NAK.AvatarScaleMod.Networking;
namespace NAK.AvatarScaleMod;
public class AvatarScaleMod : MelonMod
{
internal static MelonLogger.Instance Logger;
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);
}
public override void OnUpdate()
{
ModNetwork.Update();
DebugKeybinds.DoDebugInput();
}
private static void InitializeIntegration(string modName, Action integrationAction)
{
if (RegisteredMelons.All(it => it.Info.Name != modName))
return;
Logger.Msg($"Initializing {modName} integration.");
integrationAction.Invoke();
}
private void ApplyPatches(Type type)
{
try
{
HarmonyInstance.PatchAll(type);
}
catch (Exception e)
{
LoggerInstance.Msg($"Failed while patching {type.Name}!");
LoggerInstance.Error(e);
}
}
}

View file

@ -0,0 +1,122 @@
using MelonLoader;
using NAK.AvatarScaleMod.AvatarScaling;
using NAK.AvatarScaleMod.Networking;
namespace NAK.AvatarScaleMod;
// i like this
internal static class ModSettings
{
// Constants
internal const string ModName = nameof(AvatarScaleMod);
internal const string ASM_SettingsCategory = "Avatar Scale Mod";
internal const string AST_SettingsCategory = "Avatar Scale Tool Support";
internal const string USM_SettingsCategory = "Universal Scaling (Mod Network)";
internal const string DEBUG_SettingsCategory = "Debug Options";
private static readonly MelonPreferences_Category Category =
MelonPreferences.CreateCategory(ModName);
#region Hidden Foldout Entries
// Avatar Scale Mod Foldout
internal static readonly MelonPreferences_Entry<bool> Hidden_Foldout_ASM_SettingsCategory =
Category.CreateEntry("hidden_foldout_asm", true, is_hidden: true, display_name: ASM_SettingsCategory, description: "Foldout state for Avatar Scale Mod settings.");
// Avatar Scale Tool Foldout
internal static readonly MelonPreferences_Entry<bool> Hidden_Foldout_AST_SettingsCategory =
Category.CreateEntry("hidden_foldout_ast", false, is_hidden: true, display_name: AST_SettingsCategory, description: "Foldout state for Avatar Scale Tool settings.");
// Universal Scaling (Mod Network) Foldout
internal static readonly MelonPreferences_Entry<bool> Hidden_Foldout_USM_SettingsCategory =
Category.CreateEntry("hidden_foldout_usm", false, is_hidden: true, display_name: USM_SettingsCategory, description: "Foldout state for Universal Scaling (Mod Network) settings.");
// Debug Options Foldout
internal static readonly MelonPreferences_Entry<bool> Hidden_Foldout_DEBUG_SettingsCategory =
Category.CreateEntry("hidden_foldout_debug", false, is_hidden: true, display_name: DEBUG_SettingsCategory, description: "Foldout state for Debug Options settings.");
// Player Select Page Foldout
internal static readonly MelonPreferences_Entry<bool> Hidden_Foldout_PlayerSelectPage =
Category.CreateEntry("hidden_foldout_player_select_page", true, is_hidden: true, display_name: ASM_SettingsCategory, description: "Foldout state for Player Select Page.");
#endregion
#region Avatar Scale Mod Settings
public static readonly MelonPreferences_Entry<bool> EntryScaleGestureEnabled =
Category.CreateEntry("scale_gesture_enabled", true, display_name: "Scale Gesture", description: "Enable or disable scale gesture.");
public static readonly MelonPreferences_Entry<bool> EntryScaleKeybindingsEnabled =
Category.CreateEntry("scale_keybindings_enabled", true, display_name: "Scale Keybindings", description: "Enable or disable scale keybindings.");
public static readonly MelonPreferences_Entry<bool> EntryPersistentHeight =
Category.CreateEntry("persistent_height", false, display_name: "Persistent Height", description: "Should the avatar height persist between avatar switches?");
public static readonly MelonPreferences_Entry<bool> EntryPersistThroughRestart =
Category.CreateEntry("persistent_height_through_restart", false, display_name: "Persist Through Restart", description: "Should the avatar height persist between game restarts?");
// stores the last avatar height as a melon pref
public static readonly MelonPreferences_Entry<float> EntryHiddenAvatarHeight =
Category.CreateEntry("hidden_avatar_height", -2f, is_hidden: true, display_name: "Avatar Height", description: "Set your avatar height.");
#endregion
#region Avatar Scale Tool Settings
public static readonly MelonPreferences_Entry<string> EntryASTScaleParameter =
Category.CreateEntry("override_scale_parameter", "AvatarScale", display_name: "Override Scale Parameter", description: "Override the scale parameter on the avatar.");
public static readonly MelonPreferences_Entry<float> EntryASTMinHeight =
Category.CreateEntry("override_min_height", 0.25f, display_name: "Override Min Height", description: "Override the minimum height.");
public static readonly MelonPreferences_Entry<float> EntryASTMaxHeight =
Category.CreateEntry("override_max_height", 2.5f, display_name: "Override Max Height", description: "Override the maximum height.");
#endregion
#region Universal Scaling Settings
public static readonly MelonPreferences_Entry<bool> EntryUseUniversalScaling =
Category.CreateEntry("use_universal_scaling", false, display_name: "Use Universal Scaling", description: "Enable or disable universal scaling. This allows scaling to work on any avatar as well as networking via Mod Network.");
public static readonly MelonPreferences_Entry<bool> EntryScaleComponents =
Category.CreateEntry("scale_components", true, display_name: "Scale Components", description: "Scale components on the avatar. (Constraints, Audio Sources, etc.)");
public static readonly MelonPreferences_Entry<bool> EntryAnimationScalingOverride =
Category.CreateEntry("allow_anim_clip_scale_override", true, display_name: "Animation-Clip Scaling Override", description: "Allow animation-clip scaling to override universal scaling.");
#endregion
#region Debug Settings
public static readonly MelonPreferences_Entry<bool> Debug_NetworkInbound =
Category.CreateEntry("debug_inbound", false, display_name: "Debug Inbound", description: "Log inbound Mod Network height updates.");
public static readonly MelonPreferences_Entry<bool> Debug_NetworkOutbound =
Category.CreateEntry("debug_outbound", false, display_name: "Debug Outbound", description: "Log outbound Mod Network height updates.");
public static readonly MelonPreferences_Entry<bool> Debug_ComponentSearchTime =
Category.CreateEntry("debug_component_search_time", false, display_name: "Debug Search Time", description: "Log component search time.");
#endregion
public static void Initialize()
{
// subscribe to all bool settings that aren't hidden
foreach (MelonPreferences_Entry entry in Category.Entries.Where(entry => entry.GetReflectedType() == typeof(bool) && !entry.IsHidden))
entry.OnEntryValueChangedUntyped.Subscribe(OnSettingsBoolChanged);
}
private static void OnSettingsBoolChanged(object _, object __)
{
GestureReconizer.ScaleReconizer.Enabled = EntryScaleGestureEnabled.Value;
ModNetwork.Debug_NetworkInbound = Debug_NetworkInbound.Value;
ModNetwork.Debug_NetworkOutbound = Debug_NetworkOutbound.Value;
if (AvatarScaleManager.Instance == null) return;
AvatarScaleManager.Instance.Setting_UniversalScaling = EntryUseUniversalScaling.Value;
AvatarScaleManager.Instance.Setting_AnimationClipScalingOverride = EntryAnimationScalingOverride.Value;
AvatarScaleManager.Instance.Setting_PersistentHeight = EntryPersistentHeight.Value;
}
}

View file

@ -0,0 +1,283 @@
using ABI_RC.Core.Networking;
using ABI_RC.Systems.ModNetwork;
using DarkRift;
using NAK.AvatarScaleMod.AvatarScaling;
using UnityEngine;
namespace NAK.AvatarScaleMod.Networking;
// overcomplicated, but functional
// a
public static class ModNetwork
{
public static bool Debug_NetworkInbound = false;
public static bool Debug_NetworkOutbound = false;
private static bool _isSubscribedToModNetwork;
#region Constants
private const string ModId = "MelonMod.NAK.AvatarScaleMod";
private const float SendRateLimit = 0.25f;
private const float ReceiveRateLimit = 0.2f;
private const int MaxWarnings = 2;
private const float TimeoutDuration = 10f;
private class QueuedMessage
{
public MessageType Type { get; set; }
public float Height { get; set; }
public string TargetPlayer { get; set; }
public string Sender { get; set; }
}
#endregion
#region Private State
private static readonly Dictionary<string, QueuedMessage> OutboundQueue = new();
private static float LastSentTime;
private static readonly Dictionary<string, QueuedMessage> InboundQueue = new();
private static readonly Dictionary<string, float> LastReceivedTimes = new();
private static readonly Dictionary<string, int> UserWarnings = new();
private static readonly Dictionary<string, float> UserTimeouts = new();
#endregion
#region Enums
private enum MessageType : byte
{
SyncHeight = 0, // just send height
RequestHeight = 1 // send height, request height back
}
#endregion
#region Mod Network Internals
internal static void Subscribe()
{
ModNetworkManager.Subscribe(ModId, OnMessageReceived);
_isSubscribedToModNetwork = ModNetworkManager.IsSubscribed(ModId);
if (!_isSubscribedToModNetwork)
AvatarScaleMod.Logger.Error("Failed to subscribe to Mod Network! This is a critical error! Please report this to the mod author! (NAK) (ModNetwork.cs) (Subscribe) (Line 150) (AvatarScaleMod) (AvatarScale) (MelonLoader) (MelonLoader.Mods) (MelonLoader.Mods.MelonMod) (MelonLoader.MelonMod) (MelonLoader.MelonMod.MelonBaseMod) (MelonLoader.MelonMod.MelonMod) (MelonLoader.MelonMod.MelonModBase) (MelonLoader.MelonMod.MelonModBase`1) (MelonLoader.MelonMod.MelonModBase`1[[NAK.AvatarScaleMod.AvatarScaling.AvatarScaleMod, NAK.AvatarScaleMod, Version=123");
AvatarScaleEvents.OnLocalAvatarHeightChanged.AddListener(scaler => SendNetworkHeight(scaler.GetTargetHeight()));
}
internal static void Update()
{
if (!_isSubscribedToModNetwork)
return;
ProcessOutboundQueue();
ProcessInboundQueue();
}
private static void SendMessage(MessageType messageType, float height, string playerId = null)
{
if (!IsConnectedToGameNetwork())
return;
if (!Enum.IsDefined(typeof(MessageType), messageType))
return;
if (!string.IsNullOrEmpty(playerId))
{
// to specific user
using ModNetworkMessage modMsg = new(ModId, playerId);
modMsg.Write((byte)messageType);
modMsg.Write(height);
modMsg.Send();
}
else
{
// to all users
using ModNetworkMessage modMsg = new(ModId);
modMsg.Write((byte)messageType);
modMsg.Write(height);
modMsg.Send();
}
}
private static void OnMessageReceived(ModNetworkMessage msg)
{
msg.Read(out byte msgTypeRaw);
msg.Read(out float receivedHeight);
if (!Enum.IsDefined(typeof(MessageType), msgTypeRaw))
return;
// User is in timeout
if (UserTimeouts.TryGetValue(msg.Sender, out var timeoutEnd) && Time.time < timeoutEnd)
return;
if (IsRateLimited(msg.Sender))
return;
QueuedMessage inboundMessage = new()
{
Type = (MessageType)msgTypeRaw,
Height = receivedHeight,
Sender = msg.Sender
};
InboundQueue[msg.Sender] = inboundMessage;
}
#endregion
#region Public Methods
public static void SendNetworkHeight(float newHeight)
{
OutboundQueue["global"] = new QueuedMessage { Type = MessageType.SyncHeight, Height = newHeight };
}
public static void RequestHeightSync()
{
var myCurrentHeight = AvatarScaleManager.Instance.GetHeightForNetwork();
OutboundQueue["global"] = new QueuedMessage { Type = MessageType.RequestHeight, Height = myCurrentHeight };
}
#endregion
#region Outbound Height Queue
private static void ProcessOutboundQueue()
{
if (OutboundQueue.Count == 0 || Time.time - LastSentTime < SendRateLimit)
return;
foreach (QueuedMessage message in OutboundQueue.Values)
{
SendMessage(message.Type, message.Height, message.TargetPlayer);
if (Debug_NetworkOutbound)
AvatarScaleMod.Logger.Msg(
$"Sending message {message.Type.ToString()} to {(string.IsNullOrEmpty(message.TargetPlayer) ? "ALL" : message.TargetPlayer)}: {message.Height}");
}
OutboundQueue.Clear();
LastSentTime = Time.time;
}
#endregion
#region Inbound Height Queue
private static bool IsRateLimited(string userId)
{
// Rate-limit checking
if (LastReceivedTimes.TryGetValue(userId, out var lastReceivedTime) &&
Time.time - lastReceivedTime < ReceiveRateLimit)
{
if (UserWarnings.TryGetValue(userId, out var warnings))
{
warnings++;
UserWarnings[userId] = warnings;
if (warnings >= MaxWarnings)
{
UserTimeouts[userId] = Time.time + TimeoutDuration;
AvatarScaleMod.Logger.Warning($"User is sending height updates too fast! Applying 10s timeout... : {userId}");
return true;
}
}
else
{
UserWarnings[userId] = 1;
}
return true;
}
LastReceivedTimes[userId] = Time.time;
UserWarnings.Remove(userId); // Reset warnings
return false;
}
private static void ProcessInboundQueue()
{
foreach (QueuedMessage message in InboundQueue.Values)
{
switch (message.Type)
{
case MessageType.RequestHeight:
{
var myNetworkHeight = AvatarScaleManager.Instance.GetHeightForNetwork();
OutboundQueue[message.Sender] = new QueuedMessage
{
Type = MessageType.SyncHeight,
Height = myNetworkHeight,
TargetPlayer = message.Sender
};
AvatarScaleManager.Instance.OnNetworkHeightUpdateReceived(message.Sender, message.Height);
break;
}
case MessageType.SyncHeight:
AvatarScaleManager.Instance.OnNetworkHeightUpdateReceived(message.Sender, message.Height);
break;
default:
AvatarScaleMod.Logger.Error($"Invalid message type received from: {message.Sender}");
break;
}
if (Debug_NetworkInbound)
AvatarScaleMod.Logger.Msg($"Received message {message.Type.ToString()} from {message.Sender}: {message.Height}");
}
InboundQueue.Clear();
}
#endregion
#region Private Methods
private static bool IsConnectedToGameNetwork()
{
return NetworkManager.Instance != null
&& NetworkManager.Instance.GameNetwork != null
&& NetworkManager.Instance.GameNetwork.ConnectionState == ConnectionState.Connected;
}
#endregion
#region Messages
private class AvatarHeightMessage
{
public float Height { get; set; }
public bool IsUniversal { get; set; }
}
private static void AddAvatarHeightMessageConverter()
{
ModNetworkMessage.AddConverter(Reader, Writer);
return;
AvatarHeightMessage Reader(ModNetworkMessage msg)
{
AvatarHeightMessage avatarHeightMessage = new();
msg.Read(out float height);
avatarHeightMessage.Height = height;
msg.Read(out bool isUniversal);
avatarHeightMessage.IsUniversal = isUniversal;
return avatarHeightMessage;
}
void Writer(ModNetworkMessage msg, AvatarHeightMessage value)
{
msg.Write(value.Height);
msg.Write(value.IsUniversal);
}
}
#endregion
}

View file

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

View file

@ -0,0 +1,28 @@
# AvatarScaleMod
Proof of concept mod to add Avatar Scaling to any avatar. This is local-only, but I may toy with using Mod Network.
Legit threw this together in three hours. ChilloutVR handles all the hard stuff already with its existing animation-clip-based Avatar Scaling.
https://github.com/NotAKidoS/NAK_CVR_Mods/assets/37721153/7405cef5-fd68-4103-8c18-b3164029eab1
## Notes:
* Constraint scaling partially conflicts with avatars run through my [Avatar Scale Tool](https://github.com/NotAKidoS/AvatarScaleTool).
* This is local-only, at least unless I bother with Mod Network.
* The entire thing is pretty messy and I am unsure of the performance impact, especially with scaling all lights, audio, & constraints.
## Relevant Feedback Posts:
https://feedback.abinteractive.net/p/built-in-avatar-scaling-system
This mod is me creating the system I wanted when I wrote the above feedback post.
---
Here is the block of text where I tell you this mod is not affiliated with or endorsed by ABI.
https://documentation.abinteractive.net/official/legal/tos/#7-modding-our-games
> This mod is an independent creation not affiliated with, supported by, or approved by Alpha Blend Interactive.
> Use of this mod is done so at the user's own risk and the creator cannot be held responsible for any issues arising from its use.
> To the best of my knowledge, I have adhered to the Modding Guidelines established by Alpha Blend Interactive.

View file

@ -0,0 +1,26 @@
using System;
using System.IO;
using System.Reflection;
// https://github.com/SDraw/ml_mods_cvr/blob/master/ml_amt/Scripts.cs
namespace NAK.AvatarScaleMod;
static class Scripts
{
public static string GetEmbeddedScript(string p_name)
{
string l_result = "";
Assembly l_assembly = Assembly.GetExecutingAssembly();
string l_assemblyName = l_assembly.GetName().Name;
try
{
Stream l_libraryStream = l_assembly.GetManifestResourceStream(l_assemblyName + ".resources." + p_name);
StreamReader l_streadReader = new StreamReader(l_libraryStream);
l_result = l_streadReader.ReadToEnd();
}
catch(Exception) { }
return l_result;
}
}

View file

@ -0,0 +1,23 @@
{
"_id": 126,
"name": "AvatarScale",
"modversion": "1.0.5",
"gameversion": "2022r170p1",
"loaderversion": "0.6.1",
"modtype": "Mod",
"author": "NotAKidoS",
"description": "Fixes two issues with the Avatar Advanced Settings buffers when loading remote avatars. In simple terms, it means 'fewer wardrobe malfunctions'.\n\nEmpty buffer (all 0/false) will no longer be applied on load.\nReceived AAS data is ignored until the wearer has loaded into the expected avatar.\n(The avatar will sit in its default state until the wearer has loaded and started syncing correct AAS)\nAAS will no longer be sent while switching avatar.\n\nPlease view the GitHub README for links to relevant feedback posts.",
"searchtags": [
"aas",
"sync",
"naked",
"buffer"
],
"requirements": [
"None"
],
"downloadlink": "https://github.com/NotAKidoS/NAK_CVR_Mods/releases/download/r3/AvatarScale.dll",
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/AvatarScale/",
"changelog": "",
"embedcolor": "9b59b6"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1,414 @@
(function() {
/* -------------------------------
* CSS Embedding
* ------------------------------- */
function injectCSS() {
const embeddedCSS = `
// 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%;
}
.ass-slider-base {
height: 3.25em;
position: relative;
background: #555;
border-radius: 1.5em;
cursor: pointer;
overflow: hidden;
}
.ass-slider-inner {
height: 100%;
background: #a9a9a9;
position: absolute;
top: 0;
left: 0;
}
.ass-snap-point {
height: 100%;
width: 2px;
background: white;
position: absolute;
top: 0;
}
.ass-slider-value {
font-size: 2em;
position: relative;
left: 0.5em;
color: #fff;
white-space: nowrap;
}
/* 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%;
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');
styleElement.type = 'text/css';
styleElement.innerHTML = embeddedCSS;
document.head.appendChild(styleElement);
}
/* -------------------------------
* Main Content Element
* ------------------------------- */
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!`);
}
}
}
/* -------------------------------
* Generic Element Class
* ------------------------------- */
class Element {
constructor(tagName, parentElement) {
this.element = document.createElement(tagName);
parentElement.appendChild(this.element);
}
}
/* -------------------------------
* Container Object
* ------------------------------- */
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;
// 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;
// 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;
this.flexContainer = new Element('div', this.element);
this.flexContainer.element.className = "ass-flex-container";
}
appendElementToFlex(element) {
this.flexContainer.element.appendChild(element);
}
appendElement(element) {
this.element.appendChild(element);
}
}
/* -------------------------------
* Category (label) Class
* ------------------------------- */
class Category extends Element {
constructor(parentElement, text) {
super('span', parentElement);
this.element.className = "ass-category-label";
this.element.textContent = text;
}
}
/* -------------------------------
* Circle Button Class
* ------------------------------- */
class CircleButton extends Element {
constructor(parentElement, text) {
super('button', parentElement);
this.element.className = "ass-circle-button";
this.element.textContent = text;
}
}
/* -------------------------------
* 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;
x = Math.min(Math.max(0, x), effectiveWidth);
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;
}
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);
}
addSnapPoint(percentage) {
if (percentage < 0 || percentage > 1) return;
const snap = new Element('div', this.sliderBase.element);
snap.element.className = 'ass-snap-point';
snap.element.style.left = `${percentage * 100}%`;
this.snapPoints.push(percentage);
}
addEvenSnapPoints(count) {
for (let i = 1; i <= count; i++) {
this.addSnapPoint(i / (count + 1));
}
}
}
// 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");
}
})();