i dont rememebr

This commit is contained in:
NotAKidoS 2024-01-01 11:58:25 -06:00
parent 374ab6c11e
commit 86828a94e2
48 changed files with 1637 additions and 841 deletions

View file

@ -16,15 +16,6 @@ internal class IKHandlerDesktop : IKHandler
public override void OnInitializeIk()
{
// Default tracking for Desktop
DeviceControlManipulator.shouldTrackHead = true;
DeviceControlManipulator.shouldTrackLeftArm = false;
DeviceControlManipulator.shouldTrackRightArm = false;
DeviceControlManipulator.shouldTrackLeftLeg = false;
DeviceControlManipulator.shouldTrackRightLeg = false;
DeviceControlManipulator.shouldTrackPelvis = false;
DeviceControlManipulator.shouldTrackLocomotion = true;
_vrik.onPreSolverUpdate.AddListener(OnPreSolverUpdateDesktop);
}

View file

@ -16,15 +16,6 @@ internal class IKHandlerHalfBody : IKHandler
public override void OnInitializeIk()
{
// Default tracking for HalfBody
DeviceControlManipulator.shouldTrackHead = true;
DeviceControlManipulator.shouldTrackLeftArm = true;
DeviceControlManipulator.shouldTrackRightArm = true;
DeviceControlManipulator.shouldTrackLeftLeg = false;
DeviceControlManipulator.shouldTrackRightLeg = false;
DeviceControlManipulator.shouldTrackPelvis = false;
DeviceControlManipulator.shouldTrackLocomotion = true;
_vrik.onPreSolverUpdate.AddListener(OnPreSolverUpdateHalfBody);
}

View file

@ -16,7 +16,6 @@ public class IKManager : MonoBehaviour
#region Variables
public BodyControl BodyControl = new BodyControl();
public WeightManipulatorManager WeightManipulator = new WeightManipulatorManager();
public static VRIK vrik => _vrik;
private static VRIK _vrik;
@ -86,9 +85,6 @@ public class IKManager : MonoBehaviour
_rightHandTarget = _rightController.Find("RightHandTarget");
_rightHandRotations = _rightHandTarget.Find("RightHandRotations");
WeightManipulator.AddOverride(new TrackingControlManipulator());
WeightManipulator.AddOverride(new DeviceControlManipulator());
BodyControl.Start();
}
@ -99,9 +95,6 @@ public class IKManager : MonoBehaviour
BodyControl.Update();
if (vrik.solver != null)
WeightManipulator.UpdateWeights(vrik.solver);
_ikHandler?.UpdateWeights();
}

View file

@ -20,8 +20,6 @@ public class AlternateIKSystem : MelonMod
ApplyPatches(typeof(HarmonyPatches.PlayerSetupPatches));
ApplyPatches(typeof(HarmonyPatches.IKSystemPatches));
InitializeIntegration("BTKUILib", Integrations.BTKUIAddon.Initialize);
}
private static void InitializeIntegration(string modName, Action integrationAction)

View file

@ -21,7 +21,7 @@ public static class ModSettings
"Determines if VRIK uses humanoid toes for IK solving, which can cause feet to idle behind the avatar.");
public static readonly MelonPreferences_Entry<bool> EntryPlantFeet =
Category.CreateEntry("Enforce Plant Feet", true,
Category.CreateEntry("Enforce Plant Feet", false ,
description: "Forces VRIK Plant Feet enabled to prevent hovering when stopping movement.");
public static readonly MelonPreferences_Entry<bool> EntryResetFootstepsOnIdle =

View file

@ -1,4 +1,5 @@
using ABI_RC.Core.IO;
using System.Collections;
using ABI_RC.Core.IO;
using ABI_RC.Core.Player;
using ABI_RC.Core.Player.AvatarTracking;
using ABI_RC.Core.UI;
@ -11,14 +12,29 @@ namespace NAK.AvatarScaleMod.AvatarScaling;
public class AvatarScaleManager : MonoBehaviour
{
// Universal Scaling Limits
public const float MinHeight = 0.1f;
public const float MaxHeight = 10f;
public static AvatarScaleManager Instance;
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/NotAKidOnSteam/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
@ -27,21 +43,25 @@ public class AvatarScaleManager : MonoBehaviour
set
{
if (value != _settingUniversalScaling && value == false)
ResetHeight();
ResetTargetHeight();
_settingUniversalScaling = value;
SetTargetHeight(_lastTargetHeight); // immediate height update
}
}
public bool Setting_PersistantHeight;
public bool Setting_AnimationClipScalingOverride;
public bool Setting_PersistentHeight;
private float _lastTargetHeight = -1;
#region Unity Methods
#endregion
#region Unity Events
private void Awake()
{
if (Instance != null)
if (Instance != null
&& Instance != this)
{
DestroyImmediate(this);
return;
@ -53,22 +73,82 @@ public class AvatarScaleManager : MonoBehaviour
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;
// update local scaler
if (_localAvatarScaler != null && _localAvatarScaler.heightNeedsUpdate)
{
if (_localAvatarScaler.ApplyTargetHeight())
AvatarScaleEvents.OnLocalAvatarHeightChanged.Invoke(_localAvatarScaler);
}
// update networked scalers (probably a better way to do this)
foreach (var netScaler in _networkedScalers)
{
if (!netScaler.Value.heightNeedsUpdate) continue;
if (netScaler.Value.ApplyTargetHeight())
AvatarScaleEvents.OnRemoteAvatarHeightChanged.Invoke(netScaler.Key, netScaler.Value);
}
}
// ReSharper disable once IteratorNeverReturns
}
#endregion
#region Events
#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);
}
@ -81,19 +161,13 @@ public class AvatarScaleManager : MonoBehaviour
if (playerSetup._avatar == null)
return;
if (_localAvatarScaler == null)
{
_localAvatarScaler = playerSetup.gameObject.AddComponent<LocalScaler>();
_localAvatarScaler.Initialize();
}
_localAvatarScaler.OnAvatarInstantiated(playerSetup._avatar, playerSetup._initialAvatarHeight,
playerSetup.initialScale);
if (!_settingUniversalScaling)
return;
SetHeight(Setting_PersistantHeight ? _lastTargetHeight : -1f);
SetTargetHeight(_lastTargetHeight);
}
public void OnAvatarDestroyed(PlayerSetup playerSetup)
@ -102,35 +176,34 @@ public class AvatarScaleManager : MonoBehaviour
_localAvatarScaler.OnAvatarDestroyed();
}
public void SetHeight(float targetHeight)
public void SetTargetHeight(float targetHeight)
{
_lastTargetHeight = targetHeight; // save for persistent height
ModSettings.EntryHiddenAvatarHeight.Value = targetHeight; // save for restart
if (!_settingUniversalScaling)
return;
if (_localAvatarScaler == null)
return;
_lastTargetHeight = targetHeight;
_localAvatarScaler.SetTargetHeight(targetHeight);
ModNetwork.SendNetworkHeight(targetHeight);
// immediately update play space scale
PlayerSetup.Instance.CheckUpdateAvatarScaleToPlaySpaceRelation();
_localAvatarScaler.SetTargetHeight(_lastTargetHeight);
_localAvatarScaler.heightNeedsUpdate = true; // only local scaler forces update
}
public void ResetHeight()
public void ResetTargetHeight()
{
if (_localAvatarScaler == null)
return;
if (!_localAvatarScaler.IsHeightAdjustedFromInitial())
if (!_localAvatarScaler.IsForcingHeight())
return;
// TODO: doesnt work when hitting Reset on slider in BTK UI (is it on main thread?)
CohtmlHud.Instance.ViewDropTextImmediate("(Local) AvatarScaleMod", "Avatar Scale Reset!",
"Universal Scaling is now disabled.");
SetHeight(-1f);
SetTargetHeight(-1f);
}
public float GetHeight()
@ -138,10 +211,21 @@ public class AvatarScaleManager : MonoBehaviour
if (_localAvatarScaler == null)
return PlayerAvatarPoint.defaultAvatarHeight;
if (!_localAvatarScaler.IsHeightAdjustedFromInitial())
if (!_localAvatarScaler.IsForcingHeight())
return PlayerSetup.Instance.GetAvatarHeight();
return _localAvatarScaler.GetHeight();
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()
@ -152,10 +236,10 @@ public class AvatarScaleManager : MonoBehaviour
if (_localAvatarScaler == null)
return -1f;
if (!_localAvatarScaler.IsHeightAdjustedFromInitial())
if (!_localAvatarScaler.IsForcingHeight())
return -1f;
return _localAvatarScaler.GetHeight();
return _localAvatarScaler.GetTargetHeight();
}
public float GetInitialHeight()
@ -168,17 +252,23 @@ public class AvatarScaleManager : MonoBehaviour
public bool IsHeightAdjustedFromInitial()
{
return _localAvatarScaler != null && _localAvatarScaler.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.IsHeightAdjustedFromInitial()) return scaler.GetHeight();
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);
@ -266,4 +356,40 @@ public class AvatarScaleManager : MonoBehaviour
}
#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

@ -1,5 +1,5 @@
using ABI_RC.Core;
using ABI_RC.Core.Player;
using System.Diagnostics;
using ABI_RC.Core;
using NAK.AvatarScaleMod.AvatarScaling;
using NAK.AvatarScaleMod.ScaledComponents;
using UnityEngine;
@ -12,143 +12,201 @@ public class BaseScaler : MonoBehaviour
{
#region Constants
public const string ScaleFactorParameterName = "ScaleFactor";
public const string ScaleFactorParameterNameLocal = "#ScaleFactor";
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
internal bool _isAvatarInstantiated;
internal bool _isHeightAdjustedFromInitial;
internal bool _heightNeedsUpdate;
// Height update requested
public bool heightNeedsUpdate { get; internal set; }
// Config variables
public bool avatarIsHidden { get; set; }
public bool useTargetHeight { get; set; }
public bool overrideAnimationHeight { get; set; }
// State variables
internal bool _isAvatarInstantiated;
internal bool _shouldForceHeight => useTargetHeight || avatarIsHidden; // universal or hidden avatar
// Avatar info
internal Transform _avatarTransform;
internal CVRAnimatorManager _animatorManager;
// Initial scaling
internal float _initialHeight;
internal Vector3 _initialScale;
// Forced scaling (Universal & Hidden Avatar)
internal float _targetHeight = -1;
internal Vector3 _targetScale = Vector3.one;
internal float _scaleFactor = 1f;
// detection for animation clip-based scaling
internal Vector3 _legacyAnimationScale;
#endregion
#region Public Methods
// 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 = Mathf.Clamp(initialHeight, 0.01f, 100f);
_initialScale = initialScale;
_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;
_heightNeedsUpdate = false;
heightNeedsUpdate = false;
ClearComponentLists();
}
#endregion
#region Public Methods
public float GetInitialHeight() => _initialHeight;
public float GetTargetHeight() => _targetHeight;
public float GetAnimatedHeight() => _animatedHeight;
public bool IsForcingHeight() => _shouldForceHeight;
public void SetTargetHeight(float height)
{
if (_isHeightAdjustedFromInitial
&& Math.Abs(height - _targetHeight) < float.Epsilon)
return;
if (height < float.Epsilon)
{
ResetHeight();
ResetTargetHeight();
return;
}
if (!_isHeightAdjustedFromInitial)
_legacyAnimationScale = Vector3.zero;
_isHeightAdjustedFromInitial = true;
_targetHeight = Mathf.Clamp(height, AvatarScaleManager.MinHeight, AvatarScaleManager.MaxHeight);
_heightNeedsUpdate = true;
UpdateScaleIfInstantiated();
}
public void ResetHeight()
{
if (!_isHeightAdjustedFromInitial) return;
_isHeightAdjustedFromInitial = false;
if (Math.Abs(_initialHeight - _targetHeight) < float.Epsilon)
return;
_legacyAnimationScale = Vector3.zero;
_scaleFactor = Mathf.Max(_targetHeight / _initialHeight, 0.01f); //safety
_targetScale = _initialScale * _scaleFactor;
_targetHeight = _initialHeight;
_heightNeedsUpdate = true;
UpdateScaleIfInstantiated();
InvokeTargetHeightChanged();
}
public float GetHeight() => _targetHeight;
public float GetInitialHeight() => _initialHeight;
public bool IsHeightAdjustedFromInitial() => _isHeightAdjustedFromInitial;
public void ResetTargetHeight()
{
// if (Math.Abs(_initialHeight - _targetHeight) < float.Epsilon)
// return; // no need to change, is close enough
useTargetHeight = false;
_targetHeight = _animatedHeight;
_targetScale = _animatedScale;
_scaleFactor = _animatedScaleFactor;
InvokeTargetHeightReset();
}
public bool ApplyTargetHeight()
{
if (!_isAvatarInstantiated || _initialHeight == 0)
return false;
if (_avatarTransform == null)
return false;
heightNeedsUpdate = false;
ScaleAvatarRoot();
UpdateAnimatorParameter();
ApplyComponentScaling();
return true;
}
#endregion
#region Private Methods
internal void ScaleAvatarRoot()
private void ScaleAvatarRoot()
{
if (_avatarTransform == null) return;
_avatarTransform.localScale = _targetScale;
}
internal virtual void UpdateAnimatorParameter()
protected virtual void UpdateAnimatorParameter()
{
// empty
}
internal void UpdateScaleIfInstantiated()
{
if (!_isAvatarInstantiated || _initialHeight == 0)
return;
if (_avatarTransform == null)
return;
_scaleFactor = Mathf.Max(_targetHeight / _initialHeight, 0.01f); //safety
_heightNeedsUpdate = false;
_targetScale = _initialScale * _scaleFactor;
ScaleAvatarRoot();
UpdateAnimatorParameter();
ApplyComponentScaling();
}
#endregion
#region Unity Methods
#region Unity Events
public virtual void LateUpdate()
{
if (!_isHeightAdjustedFromInitial)
return;
if (!_isAvatarInstantiated)
return; // no avatar
if (!_isAvatarInstantiated)
return;
ScaleAvatarRoot(); // override animationclip-based scaling
if (!_shouldForceHeight)
return; // not universal scaling or hidden avatar
ScaleAvatarRoot();
}
internal virtual void OnDestroy()
@ -169,13 +227,13 @@ public class BaseScaler : MonoBehaviour
typeof(PositionConstraint),
typeof(ScaleConstraint)
};
private readonly List<ScaledLight> _scaledLights = new List<ScaledLight>();
private readonly List<ScaledAudioSource> _scaledAudioSources = new List<ScaledAudioSource>();
private readonly List<ScaledParentConstraint> _scaledParentConstraints = new List<ScaledParentConstraint>();
private readonly List<ScaledPositionConstraint> _scaledPositionConstraints = new List<ScaledPositionConstraint>();
private readonly List<ScaledScaleConstraint> _scaledScaleConstraints = new List<ScaledScaleConstraint>();
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();
@ -184,10 +242,9 @@ public class BaseScaler : MonoBehaviour
_scaledPositionConstraints.Clear();
_scaledScaleConstraints.Clear();
}
internal async Task FindComponentsOfTypeAsync(Type[] types)
internal void FindComponentsOfType(Type[] types)
{
var tasks = new List<Task>();
var components = _avatarTransform.gameObject.GetComponentsInChildren<Component>(true);
foreach (Component component in components)
@ -195,19 +252,12 @@ public class BaseScaler : MonoBehaviour
if (this == null) break;
if (component == null) continue;
tasks.Add(Task.Run(() =>
{
Type componentType = component.GetType();
if (types.Contains(componentType))
{
AddScaledComponent(componentType, component);
}
}));
Type componentType = component.GetType();
if (types.Contains(componentType))
AddScaledComponent(componentType, component);
}
await Task.WhenAll(tasks);
}
private void AddScaledComponent(Type type, Component component)
{
switch (type)
@ -229,7 +279,7 @@ public class BaseScaler : MonoBehaviour
break;
}
}
private void ApplyComponentScaling()
{
// UpdateLightScales(); // might break dps
@ -273,6 +323,6 @@ public class BaseScaler : MonoBehaviour
foreach (ScaledScaleConstraint scaleConstraint in _scaledScaleConstraints)
scaleConstraint.Scale(_scaleFactor);
}
#endregion
}

View file

@ -1,6 +1,5 @@
using ABI_RC.Core.Player;
using ABI_RC.Core.UI;
using ABI.CCK.Components;
using NAK.AvatarScaleMod.AvatarScaling;
using UnityEngine;
@ -14,31 +13,23 @@ public class LocalScaler : BaseScaler
{
_animatorManager = GetComponentInParent<PlayerSetup>().animatorManager;
_heightNeedsUpdate = false;
heightNeedsUpdate = false;
_isAvatarInstantiated = false;
_isHeightAdjustedFromInitial = false;
}
#endregion
#region Overrides
public override async void OnAvatarInstantiated(GameObject avatarObject, float initialHeight, Vector3 initialScale)
public override void OnAvatarInstantiated(GameObject avatarObject, float initialHeight, Vector3 initialScale)
{
if (avatarObject == null)
return;
base.OnAvatarInstantiated(avatarObject, initialHeight, initialScale);
await FindComponentsOfTypeAsync(scalableComponentTypes);
_targetHeight = initialHeight;
_scaleFactor = 1f;
_isHeightAdjustedFromInitial = false;
_legacyAnimationScale = Vector3.zero;
}
internal override void UpdateAnimatorParameter()
protected override void UpdateAnimatorParameter()
{
if (_animatorManager == null)
return;
@ -49,14 +40,8 @@ public class LocalScaler : BaseScaler
public override void LateUpdate()
{
if (!_isHeightAdjustedFromInitial)
return;
if (!_isAvatarInstantiated)
return;
if (!CheckForAnimationScaleChange())
ScaleAvatarRoot();
base.LateUpdate();
}
#endregion
@ -65,30 +50,43 @@ public class LocalScaler : BaseScaler
private bool CheckForAnimationScaleChange()
{
if (_avatarTransform == null) return false;
if (_avatarTransform == null)
return false;
//scale matches last recorded animation scale
if (_avatarTransform.localScale == _legacyAnimationScale)
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 (_avatarTransform.localScale == _targetScale)
if (localScale == _targetScale)
return false;
// scale was likely reset or not initiated
if (_legacyAnimationScale == Vector3.zero)
// this is the first time we've seen the avatar animated scale, record it!
if (_animatedScale == Vector3.zero)
{
_legacyAnimationScale = _avatarTransform.localScale;
_animatedScale = localScale;
return false;
}
_legacyAnimationScale = _avatarTransform.localScale;
// animation scale changed, record it!
Vector3 scaleDifference = PlayerSetup.DivideVectors(localScale - _initialScale, _initialScale);
_animatedScaleFactor = scaleDifference.y;
_animatedHeight = (_initialHeight * _animatedScaleFactor) + _initialHeight;
_animatedScale = localScale;
InvokeAnimatedHeightChanged();
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();
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;
}
#endregion
}

View file

@ -1,4 +1,5 @@
using ABI_RC.Core.Player;
using System.Diagnostics;
using ABI_RC.Core.Player;
using NAK.AvatarScaleMod.AvatarScaling;
using UnityEngine;
@ -16,28 +17,34 @@ public class NetworkScaler : BaseScaler
_animatorManager = GetComponentInParent<PuppetMaster>().animatorManager;
_heightNeedsUpdate = false;
heightNeedsUpdate = false;
_isAvatarInstantiated = false;
_isHeightAdjustedFromInitial = false;
}
#endregion
#region Overrides
public override async void OnAvatarInstantiated(GameObject avatarObject, float initialHeight, Vector3 initialScale)
public override void OnAvatarInstantiated(GameObject avatarObject, float initialHeight, Vector3 initialScale)
{
if (avatarObject == null)
return;
base.OnAvatarInstantiated(avatarObject, initialHeight, initialScale);
await FindComponentsOfTypeAsync(scalableComponentTypes);
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");
if (_isHeightAdjustedFromInitial && _heightNeedsUpdate)
UpdateScaleIfInstantiated();
// TODO: why did i do this? height is never set prior to this method being called
// if (_isHeightAdjustedFromInitial && heightNeedsUpdate)
// UpdateScaleIfInstantiated();
}
internal override void UpdateAnimatorParameter()
protected override void UpdateAnimatorParameter()
{
_animatorManager?.SetAnimatorParameter(ScaleFactorParameterNameLocal, _scaleFactor);
}

View file

@ -0,0 +1,105 @@
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

@ -48,7 +48,6 @@ public class ScaledLight
public void Reset()
{
Component.range = InitialRange;
}
}

View file

@ -46,6 +46,7 @@ internal class PlayerSetupPatches
{
try
{
if (__instance == null) return; // this is called when the game is closed
AvatarScaleManager.Instance.OnAvatarDestroyed(__instance);
}
catch (Exception e)
@ -80,6 +81,7 @@ internal class PuppetMasterPatches
{
try
{
if (__instance == null) return; // this is called when the game is closed
AvatarScaleManager.Instance.OnNetworkAvatarDestroyed(__instance);
}
catch (Exception e)

View file

@ -30,14 +30,14 @@ internal static class DebugKeybinds
{
float currentHeight = AvatarScaleManager.Instance.GetHeight() + adjustment;
currentHeight = Mathf.Max(0f, currentHeight);
AvatarScaleManager.Instance.SetHeight(currentHeight);
AvatarScaleManager.Instance.SetTargetHeight(currentHeight);
AvatarScaleMod.Logger.Msg($"[Debug] Setting height: {currentHeight}");
}
private static void ResetHeight()
{
AvatarScaleManager.Instance.ResetHeight();
AvatarScaleManager.Instance.ResetTargetHeight();
AvatarScaleMod.Logger.Msg("[Debug] Resetting height.");
}
}

View file

@ -92,7 +92,7 @@ public static class ScaleReconizer
float heightAdjustmentFactor = (modifierRatio > 1) ? 1 + (modifierRatio - 1) : 1 - (1 - modifierRatio);
// Apply the adjustment to the target height
AvatarScaleManager.Instance.SetHeight(_initialTargetHeight * heightAdjustmentFactor);
AvatarScaleManager.Instance.SetTargetHeight(_initialTargetHeight * heightAdjustmentFactor);
}
private static void OnScaleEnd(float modifier, Transform transform1, Transform transform2)

View file

@ -1,137 +0,0 @@
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.AvatarScaling;
using UnityEngine;
namespace NAK.AvatarScaleMod.Integrations
{
public static class BtkUiAddon
{
private static string _selectedPlayer;
#region Initialization
public static void Initialize()
{
PrepareIcons();
SetupRootPage();
SetupPlayerSelectPage();
RegisterEventHandlers();
}
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<float>(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<bool> entry)
{
category.AddToggle(entry.DisplayName, entry.Description, entry.Value).OnValueUpdated += b => entry.Value = b;
}
internal static void AddMelonSlider(ref Page page, MelonPreferences_Entry<float> 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
}
}

View file

@ -0,0 +1,100 @@
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.ResetTargetHeight();
return;
}
}
lastTime = DateTime.Now;
}
#endregion
}
}

View file

@ -0,0 +1,17 @@
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,23 @@
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,16 @@
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,75 @@
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.ResetTargetHeight();
}
#endregion
}
}

View file

@ -0,0 +1,65 @@
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,78 @@
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

@ -10,43 +10,113 @@ internal static class ModSettings
{
// Constants
internal const string ModName = nameof(AvatarScaleMod);
internal const string SettingsCategory = "Avatar Scale Mod";
public static readonly MelonPreferences_Category Category =
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.");
public static readonly MelonPreferences_Entry<bool> EntryUseUniversalScaling =
Category.CreateEntry("use_universal_scaling", true, display_name: "Use Universal Scaling", description: "Enable or disable universal scaling.");
// 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.");
public static readonly MelonPreferences_Entry<bool> EntryPersistantHeight =
Category.CreateEntry("persistant_height", false, display_name: "Persistant Height", description: "Should the avatar height persist between avatar switches?");
// 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 Enabled", description: "Enable or disable scale gesture.");
Category.CreateEntry("scale_gesture_enabled", true, display_name: "Scale Gesture", description: "Enable or disable scale gesture.");
public static readonly MelonPreferences_Entry<bool> EntryDebugNetworkInbound =
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> EntryDebugNetworkOutbound =
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<float> EntryHiddenLastAvatarScale =
Category.CreateEntry("last_avatar_scale", -1f, is_hidden: true);
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()
{
foreach (MelonPreferences_Entry entry in Category.Entries)
entry.OnEntryValueChangedUntyped.Subscribe(OnSettingsChanged);
// 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 OnSettingsChanged(object _, object __)
private static void OnSettingsBoolChanged(object _, object __)
{
AvatarScaleManager.Instance.Setting_UniversalScaling = EntryUseUniversalScaling.Value;
AvatarScaleManager.Instance.Setting_PersistantHeight = EntryPersistantHeight.Value;
GestureReconizer.ScaleReconizer.Enabled = EntryScaleGestureEnabled.Value;
ModNetwork.Debug_NetworkInbound = Debug_NetworkInbound.Value;
ModNetwork.Debug_NetworkOutbound = Debug_NetworkOutbound.Value;
ModNetwork.Debug_NetworkInbound = EntryDebugNetworkInbound.Value;
ModNetwork.Debug_NetworkOutbound = EntryDebugNetworkOutbound.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

@ -1,7 +1,6 @@
using ABI_RC.Core.Networking;
using ABI_RC.Systems.ModNetwork;
using DarkRift;
using MelonLoader;
using NAK.AvatarScaleMod.AvatarScaling;
using UnityEngine;
@ -32,7 +31,7 @@ public static class ModNetwork
public string TargetPlayer { get; set; }
public string Sender { get; set; }
}
#endregion
#region Private State
@ -62,10 +61,15 @@ public static class ModNetwork
internal static void Subscribe()
{
_isSubscribedToModNetwork = true;
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)
@ -244,4 +248,36 @@ public static class ModNetwork
}
#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

@ -1,137 +1,26 @@
using ABI.CCK.Components;
using ABI_RC.Core.Savior;
using ABI_RC.Core.Util.Object_Behaviour;
using ABI_RC.Systems.IK;
using ABI_RC.Systems.IK.TrackingModules;
using cohtml.Net;
using ABI_RC.Core;
using ABI_RC.Systems.InputManagement;
using HarmonyLib;
using NAK.DesktopVRSwitch.Patches;
using NAK.DesktopVRSwitch.VRModeTrackers;
using UnityEngine;
using Valve.VR;
using NativeVRModeSwitchManager = ABI_RC.Systems.VRModeSwitch.VRModeSwitchManager;
namespace NAK.DesktopVRSwitch.HarmonyPatches;
internal class CheckVRPatches
internal class CVRInputManagerPatches
{
[HarmonyPostfix]
[HarmonyPatch(typeof(CheckVR), nameof(CheckVR.Awake))]
private static void Postfix_CheckVR_Start(ref CheckVR __instance)
[HarmonyPatch(typeof(CVRInputManager), "OnPostVRModeSwitch")]
private static void Postfix_CVRInputManager_OnPostVRModeSwitch(bool inVr, UnityEngine.Camera playerCamera)
{
try
{
__instance.gameObject.AddComponent<VRModeSwitchManager>();
}
catch (Exception e)
{
DesktopVRSwitch.Logger.Error($"Error during the patched method {nameof(Postfix_CheckVR_Start)}");
DesktopVRSwitch.Logger.Error(e);
}
RootLogic.Instance.ToggleMouse(inVr);
}
}
internal class IKSystemPatches
{
[HarmonyPostfix] //lazy fix so device indices can change properly
[HarmonyPatch(typeof(SteamVRTrackingModule), nameof(SteamVRTrackingModule.ModuleDestroy))]
private static void Postfix_SteamVRTrackingModule_ModuleDestroy(ref SteamVRTrackingModule __instance)
{
try
{
foreach (TrackingPoint t in __instance.TrackingPoints)
{
UnityEngine.Object.Destroy(t.referenceGameObject);
}
__instance.TrackingPoints.Clear();
}
catch (Exception e)
{
DesktopVRSwitch.Logger.Error($"Error during the patched method {nameof(Postfix_SteamVRTrackingModule_ModuleDestroy)}");
DesktopVRSwitch.Logger.Error(e);
}
}
}
internal class CVRWorldPatches
{
[HarmonyPostfix]
[HarmonyPatch(typeof(CVRWorld), nameof(CVRWorld.SetDefaultCamValues))]
[HarmonyPatch(typeof(CVRWorld), nameof(CVRWorld.CopyRefCamValues))]
private static void Postfix_CVRWorld_HandleCamValues()
{
try
{
ReferenceCameraPatch.OnWorldLoad();
}
catch (Exception e)
{
DesktopVRSwitch.Logger.Error($"Error during the patched method {nameof(Postfix_CVRWorld_HandleCamValues)}");
DesktopVRSwitch.Logger.Error(e);
}
}
}
internal class CameraFacingObjectPatches
{
[HarmonyPostfix]
[HarmonyPatch(typeof(CameraFacingObject), nameof(CameraFacingObject.Start))]
private static void Postfix_CameraFacingObject_Start(ref CameraFacingObject __instance)
{
try
{
__instance.gameObject.AddComponent<CameraFacingObjectTracker>();
}
catch (Exception e)
{
DesktopVRSwitch.Logger.Error($"Error during the patched method {nameof(Postfix_CameraFacingObject_Start)}");
DesktopVRSwitch.Logger.Error(e);
}
}
}
internal class CVRPickupObjectPatches
internal class VRModeSwitchManagerPatches
{
[HarmonyPrefix]
[HarmonyPatch(typeof(CVRPickupObject), nameof(CVRPickupObject.Start))]
private static void Prefix_CVRPickupObject_Start(ref CVRPickupObject __instance)
[HarmonyPatch(typeof(NativeVRModeSwitchManager), "StartSwitchInternal")]
private static void Postfix_CVRInputManager_OnPostVRModeSwitch()
{
try
{
if (__instance.gripType == CVRPickupObject.GripType.Free)
return;
Transform vrOrigin = __instance.gripOrigin;
Transform desktopOrigin = vrOrigin != null ? vrOrigin.Find("[Desktop]") : null;
if (vrOrigin != null && desktopOrigin != null)
{
CVRPickupObjectTracker tracker = __instance.gameObject.AddComponent<CVRPickupObjectTracker>();
tracker._pickupObject = __instance;
tracker._storedGripOrigin = (!MetaPort.Instance.isUsingVr ? vrOrigin : desktopOrigin);
}
}
catch (Exception e)
{
DesktopVRSwitch.Logger.Error($"Error during the patched method {nameof(Prefix_CVRPickupObject_Start)}");
DesktopVRSwitch.Logger.Error(e);
}
}
}
internal class SteamVRBehaviourPatches
{
[HarmonyPrefix]
[HarmonyPatch(typeof(SteamVR_Behaviour), nameof(SteamVR_Behaviour.OnQuit))]
private static bool Prefix_SteamVR_Behaviour_OnQuit()
{
if (!ModSettings.EntrySwitchToDesktopOnExit.Value)
return true;
// If we don't switch fast enough, SteamVR will force close.
// World Transition might cause issues. Might need to override.
if (VRModeSwitchManager.Instance != null)
VRModeSwitchManager.Instance.AttemptSwitch();
return false;
CVRInputManager.Instance.inputEnabled = false;
}
}

View file

@ -1,84 +1,14 @@

using System;
using MelonLoader;
using NAK.DesktopVRSwitch.VRModeTrackers;
using UnityEngine;
namespace NAK.DesktopVRSwitch;
public class DesktopVRSwitch : MelonMod
{
internal static MelonLogger.Instance Logger;
public override void OnInitializeMelon()
{
Logger = LoggerInstance;
RegisterVRModeTrackers();
// main manager
ApplyPatches(typeof(HarmonyPatches.CheckVRPatches));
// nameplate fixes
ApplyPatches(typeof(HarmonyPatches.CameraFacingObjectPatches));
// pickup fixes
ApplyPatches(typeof(HarmonyPatches.CVRPickupObjectPatches));
// lazy fix to reset iksystem
ApplyPatches(typeof(HarmonyPatches.IKSystemPatches));
// post processing fixes
ApplyPatches(typeof(HarmonyPatches.CVRWorldPatches));
// prevent steamvr behaviour from closing game
ApplyPatches(typeof(HarmonyPatches.SteamVRBehaviourPatches));
InitializeIntegration("BTKUILib", Integrations.BTKUIAddon.Initialize);
}
public override void OnUpdate()
{
if (!Input.GetKeyDown(KeyCode.F6) || !Input.GetKey(KeyCode.LeftControl))
return;
if (VRModeSwitchManager.Instance != null)
VRModeSwitchManager.Instance.AttemptSwitch();
}
private static void RegisterVRModeTrackers()
{
// Core trackers
VRModeSwitchManager.RegisterVRModeTracker(new CheckVRTracker());
VRModeSwitchManager.RegisterVRModeTracker(new MetaPortTracker());
// HUD trackers
VRModeSwitchManager.RegisterVRModeTracker(new CohtmlHudTracker());
VRModeSwitchManager.RegisterVRModeTracker(new HudOperationsTracker());
// Player trackers
VRModeSwitchManager.RegisterVRModeTracker(new PlayerSetupTracker());
VRModeSwitchManager.RegisterVRModeTracker(new MovementSystemTracker());
VRModeSwitchManager.RegisterVRModeTracker(new IKSystemTracker());
// Menu trackers
VRModeSwitchManager.RegisterVRModeTracker(new CVR_MenuManagerTracker());
VRModeSwitchManager.RegisterVRModeTracker(new ViewManagerTracker());
// Interaction trackers
VRModeSwitchManager.RegisterVRModeTracker(new CVRInputManagerTracker());
VRModeSwitchManager.RegisterVRModeTracker(new CVR_InteractableManagerTracker());
VRModeSwitchManager.RegisterVRModeTracker(new CVRGestureRecognizerTracker());
// Portable camera tracker
VRModeSwitchManager.RegisterVRModeTracker(new PortableCameraTracker());
// CVRWorld tracker - Must come after PlayerSetupTracker
VRModeSwitchManager.RegisterVRModeTracker(new CVRWorldTracker());
}
private static void InitializeIntegration(string modName, Action integrationAction)
{
if (RegisteredMelons.All(it => it.Info.Name != modName))
return;
Logger.Msg($"Initializing {modName} integration.");
integrationAction.Invoke();
ApplyPatches(typeof(HarmonyPatches.CVRInputManagerPatches));
ApplyPatches(typeof(HarmonyPatches.VRModeSwitchManagerPatches));
}
private void ApplyPatches(Type type)

View file

@ -1,32 +1,34 @@
using ABI_RC.Systems.UI;
using NAK.DesktopVRSwitch.VRModeTrackers;
using System.Collections;
using ABI_RC.Core.EventSystem;
using ABI_RC.Core.IO;
using ABI_RC.Core.Player;
using ABI_RC.Core.Savior;
using ABI_RC.Core.UI;
using ABI_RC.Systems.GameEventSystem;
using ABI_RC.Systems.InputManagement;
using ABI_RC.Systems.VRModeSwitch;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.XR.Management;
namespace NAK.DesktopVRSwitch;
public class VRModeSwitchManager : MonoBehaviour
public class VRModeSwitchManager : MonoBehaviour
{
#region Static
public static VRModeSwitchManager Instance { get; private set; }
public static void RegisterVRModeTracker(VRModeTracker observer) => observer.TrackerInit();
public static void UnregisterVRModeTracker(VRModeTracker observer) => observer.TrackerDestroy();
#endregion
#region Variables
// Settings
public bool DesktopVRSwitchEnabled;
public bool UseWorldTransition = true;
public bool ReloadLocalAvatar = true;
public bool SwitchInProgress { get; private set; }
#endregion
#region Unity Methods
private void Awake()
@ -36,73 +38,125 @@ public class VRModeSwitchManager : MonoBehaviour
DestroyImmediate(this);
return;
}
Instance = this;
DesktopVRSwitchEnabled = MetaPort.Instance.settings.GetSettingsBool("ExperimentalDesktopVRSwitch");
MetaPort.Instance.settings.settingBoolChanged.AddListener(OnSettingsBoolChanged);
}
private void Update()
{
if (!DesktopVRSwitchEnabled)
return;
if (SwitchInProgress)
return;
if (CVRInputManager.Instance.switchMode)
AttemptSwitch();
}
#endregion
#region Public Methods
private static bool IsInXR() => XRGeneralSettings.Instance.Manager.activeLoader != null;
public void AttemptSwitch() => StartCoroutine(StartSwitchInternal());
public void AttemptSwitch()
{
if (SwitchInProgress)
return;
// dont allow switching during world transfer, itll explode violently
if (CVRObjectLoader.Instance.IsLoadingWorldToJoin())
return;
StartCoroutine(StartSwitchInternal());
}
#endregion
#region Private Methods
private void OnSettingsBoolChanged(string settingName, bool val)
{
if (settingName == "ExperimentalDesktopVRSwitch")
DesktopVRSwitchEnabled = val;
}
private IEnumerator StartSwitchInternal()
{
if (SwitchInProgress)
if (SwitchInProgress)
yield break;
NotifyOnPreSwitch();
bool useWorldTransition = UseWorldTransition;
SwitchInProgress = true;
yield return null;
if (useWorldTransition)
yield return StartCoroutine(StartTransition());
bool isUsingVr = IsInXR();
var wasInXr = IsInXR();
InvokeOnPreSwitch(isUsingVr);
InvokeOnPreSwitch(!wasInXr);
yield return StartCoroutine(XRAndReloadAvatar(!isUsingVr));
// Note: this assumes that wasInXr has been correctly set earlier in your method.
Task xrTask = wasInXr ? XRHandler.StopXR() : XRHandler.StartXR();
// Wait for the task to complete. This makes the coroutine wait here until the above thread is done.
yield return new WaitUntil(() => xrTask.IsCompleted || xrTask.IsFaulted);
// Check task status, handle any fault that occurred during the execution of the task.
if (xrTask.IsFaulted)
{
// Log and/or handle exceptions that occurred within the task.
Exception innerException = xrTask.Exception.InnerException; // The Exception that caused the Task to enter the faulted state
MelonLoader.MelonLogger.Error("Encountered an error while executing the XR task: " + innerException.Message);
// Handle the exception appropriately.
}
if (wasInXr != IsInXR())
{
ReloadAvatar();
InvokeOnPostSwitch(!wasInXr);
}
else
{
NotifyOnFailedSwitch();
InvokeOnFailedSwitch(!wasInXr);
}
if (useWorldTransition)
yield return StartCoroutine(ContinueTransition());
SwitchInProgress = false;
}
private IEnumerator XRAndReloadAvatar(bool start)
{
yield return StartCoroutine(start ? XRHandler.StartXR() : XRHandler.StopXR());
bool isUsingVr = IsInXR();
if (isUsingVr == start)
{
ReloadAvatar();
InvokeOnPostSwitch(start);
}
else
{
InvokeOnFailedSwitch(start);
}
}
private void ReloadAvatar()
{
if (!ReloadLocalAvatar)
if (!ReloadLocalAvatar)
return;
Utils.ClearLocalAvatar();
Utils.ReloadLocalAvatar();
// TODO: Is there a better way to reload only locally?
PlayerSetup.Instance.ClearAvatar();
AssetManagement.Instance.LoadLocalAvatar(MetaPort.Instance.currentAvatarGuid);
}
#endregion
private bool IsInXR()
{
return XRGeneralSettings.Instance.Manager.activeLoader != null;
}
private UnityEngine.Camera GetPlayerCamera(bool isVr)
{
return (isVr ? PlayerSetup.Instance.vrCamera : PlayerSetup.Instance.desktopCamera)
.GetComponent<UnityEngine.Camera>();
}
#endregion
#region Transition Coroutines
private IEnumerator StartTransition()
@ -123,38 +177,46 @@ public class VRModeSwitchManager : MonoBehaviour
#region Event Handling
public class VRModeEventArgs : EventArgs
private void InvokeOnPreSwitch(bool isUsingVr)
{
public bool IsUsingVr { get; }
public Camera PlayerCamera { get; }
UnityEngine.Camera playerCamera = GetPlayerCamera(isUsingVr);
public VRModeEventArgs(bool isUsingVr, Camera playerCamera)
{
IsUsingVr = isUsingVr;
PlayerCamera = playerCamera;
}
ABI_RC.Systems.VRModeSwitch.VRModeSwitchManager.OnPreSwitchInternal?.Invoke(isUsingVr, playerCamera);
CVRGameEventSystem.VRModeSwitch.OnPreSwitch?.Invoke(isUsingVr, playerCamera);
}
public static event EventHandler<VRModeEventArgs> OnPreVRModeSwitch;
public static event EventHandler<VRModeEventArgs> OnPostVRModeSwitch;
public static event EventHandler<VRModeEventArgs> OnFailVRModeSwitch;
private void InvokeOnPreSwitch(bool isUsingVr) => SafeInvokeUnityEvent(OnPreVRModeSwitch, isUsingVr);
private void InvokeOnPostSwitch(bool isUsingVr) => SafeInvokeUnityEvent(OnPostVRModeSwitch, isUsingVr);
private void InvokeOnFailedSwitch(bool isUsingVr) => SafeInvokeUnityEvent(OnFailVRModeSwitch, isUsingVr);
private void SafeInvokeUnityEvent(EventHandler<VRModeEventArgs> switchEvent, bool isUsingVr)
private void InvokeOnPostSwitch(bool isUsingVr)
{
try
{
Camera playerCamera = Utils.GetPlayerCameraObject(isUsingVr).GetComponent<Camera>();
switchEvent?.Invoke(this, new VRModeEventArgs(isUsingVr, playerCamera));
}
catch (Exception e)
{
DesktopVRSwitch.Logger.Error($"Error in event handler: {e}");
}
UnityEngine.Camera playerCamera = GetPlayerCamera(isUsingVr);
ABI_RC.Systems.VRModeSwitch.VRModeSwitchManager.OnPostSwitchInternal?.Invoke(isUsingVr, playerCamera);
CVRGameEventSystem.VRModeSwitch.OnPostSwitch?.Invoke(isUsingVr, playerCamera);
}
private void InvokeOnFailedSwitch(bool isUsingVr)
{
UnityEngine.Camera playerCamera = GetPlayerCamera(isUsingVr);
ABI_RC.Systems.VRModeSwitch.VRModeSwitchManager.OnFailedSwitchInternal?.Invoke(isUsingVr, playerCamera);
CVRGameEventSystem.VRModeSwitch.OnFailedSwitch?.Invoke(isUsingVr, playerCamera);
}
#endregion
}
#region Notifications
private void NotifyOnPreSwitch()
{
CohtmlHud.Instance.ViewDropTextImmediate("(Local) Client",
"VR Mode Switch", "Switching to " + (IsInXR() ? "Desktop" : "VR") + " Mode");
}
private void NotifyOnFailedSwitch()
{
// TODO: Can we get reason it failed?
CohtmlHud.Instance.ViewDropTextImmediate("(Local) Client",
"VR Mode Switch", "Switch failed");
}
#endregion
}

View file

@ -1,4 +1,8 @@
using System.Collections;
#if !PLATFORM_ANDROID
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using ABI_RC.Core.Savior;
using Unity.XR.OpenVR;
@ -6,62 +10,76 @@ using UnityEngine;
using UnityEngine.XR.Management;
using UnityEngine.XR.OpenXR;
using Valve.VR;
using Object = UnityEngine.Object;
namespace NAK.DesktopVRSwitch;
internal static class XRHandler
namespace ABI_RC.Systems.VRModeSwitch
{
internal static IEnumerator StartXR()
internal static class XRHandler
{
yield return XRGeneralSettings.Instance.Manager.InitializeLoader();
if (XRGeneralSettings.Instance.Manager.activeLoader != null)
XRGeneralSettings.Instance.Manager.StartSubsystems();
else
yield return StopXR();
yield return null;
}
internal static IEnumerator StopXR()
{
if (!XRGeneralSettings.Instance.Manager.isInitializationComplete)
yield break;
// Forces SteamVR to reinitialize SteamVR_Input next switch
SteamVR_ActionSet_Manager.DisableAllActionSets();
SteamVR_Input.initialized = false;
// Remove SteamVR behaviour & render
UnityEngine.Object.DestroyImmediate(SteamVR_Behaviour.instance.gameObject);
SteamVR.enabled = false; // disposes SteamVR
Patches.SteamVRNullReferencePatch.DestroySteamVRInstancesImmediate();
// Disable UnityXR
XRGeneralSettings.Instance.Manager.StopSubsystems();
XRGeneralSettings.Instance.Manager.DeinitializeLoader();
// We don't really need to wait a frame on Stop()
yield return null;
}
internal static void SwitchLoader()
{
XRLoader item;
if (!CheckVR.Instance.forceOpenXr)
private static async Task InitializeXRLoader()
{
item = ScriptableObject.CreateInstance<OpenVRLoader>();
DesktopVRSwitch.Logger.Msg("Using XR Loader: SteamVR");
EnsureXRLoader();
XRGeneralSettings.Instance.Manager.InitializeLoaderSync();
await Task.Yield();
}
else
internal static async Task StartXR()
{
item = ScriptableObject.CreateInstance<OpenXRLoader>();
DesktopVRSwitch.Logger.Msg("Using XR Loader: OpenXR");
await InitializeXRLoader();
if (XRGeneralSettings.Instance.Manager.activeLoader != null)
XRGeneralSettings.Instance.Manager.StartSubsystems();
else
await StopXR(); // assuming StopXR is now an async method.
// Await a delay or equivalent method to wait for a frame.
await Task.Yield(); // This line is to simulate "waiting for the next frame" in an async way.
}
typeof(XRManagerSettings)
.GetField("m_Loaders", BindingFlags.Instance | BindingFlags.NonPublic)
?.SetValue(XRGeneralSettings.Instance.Manager, new List<XRLoader> { item });
internal static Task StopXR()
{
if (!XRGeneralSettings.Instance.Manager.isInitializationComplete)
return Task.CompletedTask;
// Forces SteamVR to reinitialize SteamVR_Input next switch
SteamVR_ActionSet_Manager.DisableAllActionSets();
SteamVR_Input.initialized = false;
// Remove SteamVR behaviour & render
Object.DestroyImmediate(SteamVR_Behaviour.instance.gameObject);
SteamVR.enabled = false; // disposes SteamVR
// Disable UnityXR
XRGeneralSettings.Instance.Manager.StopSubsystems();
XRGeneralSettings.Instance.Manager.DeinitializeLoader();
return Task.CompletedTask;
// If we need to wait for something specific (like a frame), we use Task.Delay or equivalent.
// In this case, it seems like you don't need to wait after stopping XR,
// so we don't necessarily need an equivalent to 'yield return null' here.
}
private static void EnsureXRLoader()
{
Type selectedLoaderType = !CheckVR.Instance.forceOpenXr ? typeof(OpenVRLoader) : typeof(OpenXRLoader);
// dont do anything if we already have the loader selected
if (XRGeneralSettings.Instance.Manager.activeLoaders.Count > 0
&& XRGeneralSettings.Instance.Manager.activeLoaders[0].GetType() == selectedLoaderType)
return;
XRLoader newLoaderInstance = (XRLoader)ScriptableObject.CreateInstance(selectedLoaderType);
FieldInfo field = typeof(XRManagerSettings).GetField("m_Loaders",
BindingFlags.Instance | BindingFlags.NonPublic);
if (field == null) return;
// destroy old loaders, set the new laoder
// this should not happen normally, but changing loader during runtime sounds funni
if (field.GetValue(XRGeneralSettings.Instance.Manager) is List<XRLoader> currentLoaders)
foreach (XRLoader loader in currentLoaders.Where(loader => loader != null)) Object.Destroy(loader);
field.SetValue(XRGeneralSettings.Instance.Manager, new List<XRLoader> { newLoaderInstance });
}
}
}
#endif

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>FuckMLA</RootNamespace>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,99 @@
using System.ComponentModel;
using ABI_RC.Core.Networking;
using ABI_RC.Systems.Communications;
using DarkRift.Client;
using FuckMLA;
using HarmonyLib;
using UnityEngine;
using Unity.Services.Vivox;
namespace NAK.FuckVivox.HarmonyPatches;
internal class VivoxServiceInternalPatches
{
// This is to catch some dumb issue where channel might not exist. There is no return even though the error is logged... -_-
[HarmonyPrefix]
[HarmonyPatch(typeof(VivoxServiceInternal), nameof(VivoxServiceInternal.Set3DPosition),
typeof(Vector3), typeof(Vector3), typeof(Vector3),
typeof(Vector3), typeof(string), typeof(bool))]
private static void Prefix_VivoxServiceInternal_Set3DPosition(
Vector3 speakerPos, Vector3 listenerPos, Vector3 listenerAtOrient, Vector3 listenerUpOrient,
string channelName, bool allowPanning,
ref ILoginSession ___m_LoginSession,
ref bool __runOriginal)
{
__runOriginal = true;
try
{
IChannelSession channelSession = ___m_LoginSession.ChannelSessions.FirstOrDefault(channel =>
channel.Channel.Type == ChannelType.Positional && channel.Channel.Name == channelName);
if (channelSession != null)
return; // no~ fuck you
__runOriginal = false;
FuckVivox.Logger.Msg("Caught an unhandled VivoxServiceInternal error.");
}
catch (Exception e)
{
FuckVivox.Logger.Error(e.ToString());
__runOriginal = false;
}
}
// This is to prevent a race condition between OnLoggedOut and OnConnectionFailedToRecover
[HarmonyPrefix]
[HarmonyPatch(typeof(VivoxServiceManager), nameof(VivoxServiceManager.OnConnectionFailedToRecover))]
private static void Prefix_VivoxServiceInternal_OnConnectionFailedToRecover(ref bool __runOriginal)
{
__runOriginal = false;
FuckVivox.Logger.Msg("(OnConnectionFailedToRecover) Possibly prevented a double re-login attempt!");
}
// This is to log us out until our connection stabilizes
[HarmonyPrefix]
[HarmonyPatch(typeof(NetworkManager), nameof(NetworkManager.ReconnectToGameServer))]
private static void Prefix_NetworkManager_ReconnectToGameServer()
{
//FuckVivox.Logger.Msg("CONNECTION UNSTABLE, PANIC LOGOUT!!!");
//VivoxHelpers.AttemptLogout();
}
[HarmonyPrefix]
[HarmonyPatch(typeof(NetworkManager), nameof(NetworkManager.OnGameNetworkConnected))]
private static void Prefix_NetworkManager_OnGameNetworkConnected()
{
if (VivoxServiceManager.Instance.IsLoggedIn())
return;
//FuckVivox.Logger.Msg("(OnGameNetworkConnected) Not logged into Vivox. Connection is potentially stable now, so attempting to login.");
//VivoxHelpers.AttemptLogin();
}
// This is to potentially fix an issue where on quick restart, we are in a channel before the bind attempts to add it???
[HarmonyPrefix]
[HarmonyPatch(typeof(VivoxServiceInternal), nameof(VivoxServiceInternal.OnChannelPropertyChanged))]
private static void Prefix_VivoxServiceInternal_OnChannelPropertyChanged(
object sender, PropertyChangedEventArgs args,
ref VivoxServiceInternal __instance,
ref bool __runOriginal)
{
__runOriginal = true;
IChannelSession channelSession = (IChannelSession)sender;
if (args.PropertyName != "ChannelState" || channelSession.ChannelState != ConnectionState.Connected)
return;
if (!__instance.m_ActiveChannels.ContainsKey(channelSession.Channel.Name))
return;
FuckVivox.Logger.Warning($"Active Channel already contains key! :: + {channelSession.Channel.Name}");
__runOriginal = false;
}
}

34
FuckVivox/Main.cs Normal file
View file

@ -0,0 +1,34 @@
using FuckMLA;
using MelonLoader;
using UnityEngine.Windows;
namespace NAK.FuckVivox;
public class FuckVivox : MelonMod
{
internal static MelonLogger.Instance Logger;
public override void OnInitializeMelon()
{
Logger = LoggerInstance;
ApplyPatches(typeof(HarmonyPatches.VivoxServiceInternalPatches));
}
public override void OnUpdate()
{
if (UnityEngine.Input.GetKeyDown(UnityEngine.KeyCode.F11))
VivoxHelpers.PleaseReLoginThankYou();
}
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,32 @@
using MelonLoader;
using NAK.FuckVivox.Properties;
using System.Reflection;
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyTitle(nameof(NAK.FuckVivox))]
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
[assembly: AssemblyProduct(nameof(NAK.FuckVivox))]
[assembly: MelonInfo(
typeof(NAK.FuckVivox.FuckVivox),
nameof(NAK.FuckVivox),
AssemblyInfoParams.Version,
AssemblyInfoParams.Author,
downloadLink: "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/FuckVivox"
)]
[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")]
[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)]
[assembly: MelonColor(255, 155, 89, 182)]
[assembly: MelonAuthorColor(255, 158, 21, 32)]
[assembly: HarmonyDontPatchAll]
namespace NAK.FuckVivox.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.0.0";
public const string Author = "NotAKidoS";
}

38
FuckVivox/VivoxHelpers.cs Normal file
View file

@ -0,0 +1,38 @@
using ABI_RC.Core.IO;
using ABI_RC.Core.Networking;
using ABI_RC.Core.Savior;
using ABI_RC.Systems.Communications;
using NAK.FuckVivox;
namespace FuckMLA;
public static class VivoxHelpers
{
public static void AttemptLogin()
{
if (!AuthManager.IsAuthenticated)
{
FuckVivox.Logger.Msg("Attempted to log in without being authenticated!");
return;
}
VivoxServiceManager.Instance.Login(MetaPort.Instance.ownerId, MetaPort.Instance.blockedUserIds);
}
public static void AttemptLogout()
{
if (!VivoxServiceManager.Instance.IsLoggedIn())
{
FuckVivox.Logger.Msg("Attempted to log out when not logged in.");
return;
}
VivoxServiceManager.Instance.Logout();
}
public static void PleaseReLoginThankYou()
{
FuckVivox.Logger.Msg("PleaseReLoginThankYou!!!");
AttemptLogout();
SchedulerSystem.AddJob(AttemptLogin, 3f, 1, 1);
}
}

22
FuckVivox/format.json Normal file
View file

@ -0,0 +1,22 @@
{
"_id": -1,
"name": "FuckMLA",
"modversion": "1.0.0",
"gameversion": "2023r172",
"loaderversion": "0.6.1",
"modtype": "Mod",
"author": "NotAKidoS",
"description": "A simple mod that does three basic things:\n\n- Destroys MOUSELOCKALPHA, the script that locks your mouse on startup.\n- Forces mouse to be initially unlocked when launching the game in VR.\n- Fixes mouse rotating player when cursor is unlocked.",
"searchtags": [
"mouse",
"lock",
"no",
"bad"
],
"requirements": [
],
"downloadlink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/releases/download/r21/FuckMLA.dll",
"sourcelink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/FuckMLA/",
"changelog": "- Initial CVRMG release",
"embedcolor": "#ffc800"
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<RootNamespace>FixedDeltaTimeHack</RootNamespace>
</PropertyGroup>
</Project>

View file

@ -0,0 +1,45 @@
using ABI_RC.Core.Player;
using ABI_RC.Core.Util.AssetFiltering;
using HarmonyLib;
using MelonLoader;
using UnityEngine;
namespace NAK.LateInitComponentHelperHack;
public class LateInitComponentHelperHack : MelonMod
{
private static bool _hasLoggedIn;
public override void OnInitializeMelon()
{
ApplyPatches(typeof(HarmonyPatches));
}
private void ApplyPatches(Type type)
{
try
{
HarmonyInstance.PatchAll(type);
}
catch (Exception e)
{
LoggerInstance.Msg($"Failed while patching {type.Name}!");
LoggerInstance.Error(e);
}
}
private static class HarmonyPatches
{
[HarmonyPrefix]
[HarmonyPatch(typeof(ComponentHelper), nameof(ComponentHelper.Initialize))]
private static bool Prefix_ComponentHelper_Initialize() => _hasLoggedIn;
[HarmonyPostfix]
[HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.Start))]
private static void Postfix_PlayerSetup_Start()
{
_hasLoggedIn = true;
ComponentHelper.Initialize();
}
}
}

View file

@ -0,0 +1,32 @@
using MelonLoader;
using NAK.LateInitComponentHelperHack.Properties;
using System.Reflection;
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyTitle(nameof(NAK.LateInitComponentHelperHack))]
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
[assembly: AssemblyProduct(nameof(NAK.LateInitComponentHelperHack))]
[assembly: MelonInfo(
typeof(NAK.LateInitComponentHelperHack.LateInitComponentHelperHack),
nameof(NAK.LateInitComponentHelperHack),
AssemblyInfoParams.Version,
AssemblyInfoParams.Author,
downloadLink: "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/LateInitComponentHelperHack"
)]
[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")]
[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)]
[assembly: MelonColor(255, 155, 89, 182)]
[assembly: MelonAuthorColor(255, 158, 21, 32)]
[assembly: HarmonyDontPatchAll]
namespace NAK.LateInitComponentHelperHack.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.0.0";
public const string Author = "NotAKidoS";
}

View file

@ -0,0 +1,22 @@
{
"_id": -1,
"name": "FuckMLA",
"modversion": "1.0.0",
"gameversion": "2023r172",
"loaderversion": "0.6.1",
"modtype": "Mod",
"author": "NotAKidoS",
"description": "A simple mod that does three basic things:\n\n- Destroys MOUSELOCKALPHA, the script that locks your mouse on startup.\n- Forces mouse to be initially unlocked when launching the game in VR.\n- Fixes mouse rotating player when cursor is unlocked.",
"searchtags": [
"mouse",
"lock",
"no",
"bad"
],
"requirements": [
],
"downloadlink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/releases/download/r21/FuckMLA.dll",
"sourcelink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/FuckMLA/",
"changelog": "- Initial CVRMG release",
"embedcolor": "#ffc800"
}

View file

@ -51,9 +51,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EzCurls", "EzCurls\EzCurls.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EzGrab", "EzGrab\EzGrab.csproj", "{8CBF5378-A4AE-4594-9A13-0066E5D3FE81}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FuckMLA", "FuckMLA\FuckMLA.csproj", "{41A84C81-E9FD-471D-9A68-4823C9F67CD7}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FuckVivox", "FuckVivox\FuckVivox.csproj", "{41A84C81-E9FD-471D-9A68-4823C9F67CD7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FixedDeltaTimeHack", "FixedDeltaTimeHack\FixedDeltaTimeHack.csproj", "{4845010F-B76C-4D6C-97E1-7D9C468BAD93}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LateInitComponentHelperHack", "LateInitComponentHelperHack\LateInitComponentHelperHack.csproj", "{4845010F-B76C-4D6C-97E1-7D9C468BAD93}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution

View file

@ -11,15 +11,15 @@ internal class PortableCameraPatches
private static void Postfix_PortableCamera_Start(ref PortableCamera __instance)
{
//run mod.Setup() instead of registering full mod with icon
VisualMods.CameraAdditions mainMod = new VisualMods.CameraAdditions();
VisualMods.CameraAdditions mainMod = new ();
mainMod.Setup(__instance);
}
[HarmonyPostfix]
[HarmonyPatch(typeof(PortableCamera), nameof(PortableCamera.OnWorldLoaded))]
private static void Postfix_PortableCamera_OnWorldLoaded(Camera worldCamera)
private static void Postfix_PortableCamera_OnWorldLoaded(Camera refCamera)
{
VisualMods.CameraAdditions.Instance?.OnWorldLoaded(worldCamera);
VisualMods.CameraAdditions.Instance?.OnWorldLoaded(refCamera);
}
[HarmonyPostfix]

View file

@ -27,6 +27,6 @@ using System.Reflection;
namespace NAK.PortableCameraAdditions.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.0.4";
public const string Version = "1.0.5";
public const string Author = "NotAKidoS";
}

View file

@ -7,12 +7,12 @@ public class CameraAdditions
{
public static CameraAdditions Instance;
public Camera referenceCamera;
public bool orthographicMode;
private Camera referenceCamera;
private bool orthographicMode;
//Should I move these to MelonPrefs?
public bool CopyWorldNearClip = true;
public bool CopyWorldFarClip = true;
private bool CopyWorldNearClip = true;
private bool CopyWorldFarClip = true;
private PortableCameraSetting setting_NearClip;
private PortableCameraSetting setting_FarClip;
@ -118,29 +118,25 @@ public class CameraAdditions
OnUpdateOptionsDisplay();
}
public void OnWorldLoaded(Camera playerCamera)
public void OnWorldLoaded(Camera refCamera)
{
orthographicMode = false;
referenceCamera = playerCamera;
if (referenceCamera != null)
{
if (CopyWorldNearClip)
{
setting_NearClip.Set(referenceCamera.nearClipPlane);
}
if (CopyWorldFarClip)
{
setting_FarClip.Set(referenceCamera.farClipPlane);
}
}
referenceCamera = refCamera;
if (referenceCamera == null)
return;
if (CopyWorldNearClip)
setting_NearClip.Set(referenceCamera.nearClipPlane);
if (CopyWorldFarClip)
setting_FarClip.Set(referenceCamera.farClipPlane);
}
public void OnUpdateOptionsDisplay(bool expertMode = true)
{
if (!expertMode)
{
return;
}
setting_NearClip.settingsObject.SetActive(!orthographicMode);
setting_FarClip.settingsObject.SetActive(!orthographicMode);
setting_OrthographicSize.settingsObject.SetActive(orthographicMode);
@ -148,12 +144,11 @@ public class CameraAdditions
setting_OrthographicFarClip.settingsObject.SetActive(orthographicMode);
}
public void UpdateOrthographicMode()
private void UpdateOrthographicMode()
{
if (PortableCamera.Instance != null)
{
PortableCamera.Instance.cameraComponent.orthographic = orthographicMode;
}
if (orthographicMode)
{
UpdateCameraSettingFloat("OrthographicNearClip", setting_OrthographicNearClip.Slider.value);
@ -164,10 +159,11 @@ public class CameraAdditions
UpdateCameraSettingFloat("NearClip", setting_NearClip.Slider.value);
UpdateCameraSettingFloat("FarClip", setting_FarClip.Slider.value);
}
OnUpdateOptionsDisplay();
}
public void UpdateCameraSettingBool(string setting, bool value)
private void UpdateCameraSettingBool(string setting, bool value)
{
if (referenceCamera != null)
{
@ -193,7 +189,7 @@ public class CameraAdditions
}
}
public void UpdateCameraSettingFloat(string setting, float value)
private void UpdateCameraSettingFloat(string setting, float value)
{
if (PortableCamera.Instance != null)
{

View file

@ -1,8 +1,8 @@
{
"_id": 123,
"name": "PortableCameraAdditions",
"modversion": "1.0.4",
"gameversion": "2023r172",
"modversion": "1.0.5",
"gameversion": "2023r173",
"loaderversion": "0.6.1",
"modtype": "Mod",
"author": "NotAKidoS",
@ -18,8 +18,8 @@
"requirements": [
"None"
],
"downloadlink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/releases/download/r21/PortableCameraAdditions.dll",
"downloadlink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/releases/download/r22/PortableCameraAdditions.dll",
"sourcelink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/PortableCameraAdditions/",
"changelog": "- Removed F11 bind to fullscreen Portable Camera.\nIt is now a native bind with 2023r172.",
"changelog": "- Fixes for 2023r173.",
"embedcolor": "#ffd96a"
}

View file

@ -1,11 +1,13 @@
using ABI.CCK.Components;
using System.Reflection;
using ABI_RC.Core.AudioEffects;
using ABI_RC.Core.Networking;
using ABI_RC.Core.Savior;
using ABI_RC.Core.Util;
using ABI_RC.Systems.InputManagement.InputModules;
using ABI.CCK.Components;
using DarkRift;
using HarmonyLib;
using MelonLoader;
using System.Reflection;
using UnityEngine;
namespace NAK.PropUndoButton;
@ -14,16 +16,17 @@ namespace NAK.PropUndoButton;
public class PropUndoButton : MelonMod
{
public static readonly MelonPreferences_Category Category =
public static readonly MelonPreferences_Category Category =
MelonPreferences.CreateCategory(nameof(PropUndoButton));
public static readonly MelonPreferences_Entry<bool> EntryEnabled =
Category.CreateEntry("Enabled", true, description: "Toggle Undo Prop Button.");
public static readonly MelonPreferences_Entry<bool> EntryUseSFX =
Category.CreateEntry("Use SFX", true, description: "Toggle audio queues for prop spawn, undo, redo, and warning.");
internal static List<DeletedProp> deletedProps = new List<DeletedProp>();
Category.CreateEntry("Use SFX", true,
description: "Toggle audio queues for prop spawn, undo, redo, and warning.");
internal static List<DeletedProp> deletedProps = new();
// audio clip names, InterfaceAudio adds "PropUndo_" prefix
private const string sfx_spawn = "PropUndo_sfx_spawn";
@ -39,27 +42,33 @@ public class PropUndoButton : MelonMod
{
HarmonyInstance.Patch( // delete my props in reverse order for redo
typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.DeleteMyProps)),
prefix: new HarmonyLib.HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnDeleteMyProps), BindingFlags.NonPublic | BindingFlags.Static))
new HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnDeleteMyProps),
BindingFlags.NonPublic | BindingFlags.Static))
);
HarmonyInstance.Patch( // delete all props in reverse order for redo
typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.DeleteAllProps)),
prefix: new HarmonyLib.HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnDeleteAllProps), BindingFlags.NonPublic | BindingFlags.Static))
new HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnDeleteAllProps),
BindingFlags.NonPublic | BindingFlags.Static))
);
HarmonyInstance.Patch( // prop spawn sfx
typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.SpawnProp)),
postfix: new HarmonyLib.HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnSpawnProp), BindingFlags.NonPublic | BindingFlags.Static))
postfix: new HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnSpawnProp),
BindingFlags.NonPublic | BindingFlags.Static))
);
HarmonyInstance.Patch( // prop delete sfx, log for possible redo
typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.DeletePropByInstanceId)),
postfix: new HarmonyLib.HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnDeletePropByInstanceId), BindingFlags.NonPublic | BindingFlags.Static))
typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.DeleteMyPropByInstanceIdOverNetwork)),
postfix: new HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnDeletePropByInstanceId),
BindingFlags.NonPublic | BindingFlags.Static))
);
HarmonyInstance.Patch( // desktop input patch so we don't run in menus/gui
typeof(InputModuleMouseKeyboard).GetMethod(nameof(InputModuleMouseKeyboard.UpdateInput)),
postfix: new HarmonyLib.HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnUpdateInput), BindingFlags.NonPublic | BindingFlags.Static))
typeof(CVRInputModule_Keyboard).GetMethod(nameof(CVRInputModule_Keyboard.UpdateInput)),
postfix: new HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnUpdateInput),
BindingFlags.NonPublic | BindingFlags.Static))
);
HarmonyInstance.Patch( // clear redo list on world change
typeof(CVRWorld).GetMethod(nameof(CVRWorld.ConfigureWorld)),
postfix: new HarmonyLib.HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnWorldLoad), BindingFlags.NonPublic | BindingFlags.Static))
postfix: new HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnWorldLoad),
BindingFlags.NonPublic | BindingFlags.Static))
);
SetupDefaultAudioClips();
@ -68,7 +77,7 @@ public class PropUndoButton : MelonMod
private void SetupDefaultAudioClips()
{
// PropUndo and audio folders do not exist, create them if dont exist yet
string path = Application.streamingAssetsPath + "/Cohtml/UIResources/GameUI/mods/PropUndo/audio/";
var path = Application.streamingAssetsPath + "/Cohtml/UIResources/GameUI/mods/PropUndo/audio/";
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
@ -77,14 +86,14 @@ public class PropUndoButton : MelonMod
// copy embedded resources to this folder if they do not exist
string[] clipNames = { "sfx_spawn.wav", "sfx_undo.wav", "sfx_redo.wav", "sfx_warn.wav", "sfx_deny.wav" };
foreach (string clipName in clipNames)
foreach (var clipName in clipNames)
{
string clipPath = Path.Combine(path, clipName);
var clipPath = Path.Combine(path, clipName);
if (!File.Exists(clipPath))
{
// read the clip data from embedded resources
byte[] clipData = null;
string resourceName = "PropUndoButton.SFX." + clipName;
var resourceName = "PropUndoButton.SFX." + clipName;
using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
{
clipData = new byte[stream.Length];
@ -92,7 +101,7 @@ public class PropUndoButton : MelonMod
}
// write the clip data to the file
using (FileStream fileStream = new FileStream(clipPath, FileMode.CreateNew))
using (FileStream fileStream = new(clipPath, FileMode.CreateNew))
{
fileStream.Write(clipData, 0, clipData.Length);
}
@ -102,7 +111,10 @@ public class PropUndoButton : MelonMod
}
}
private static void OnWorldLoad() => deletedProps.Clear();
private static void OnWorldLoad()
{
deletedProps.Clear();
}
private static void OnUpdateInput()
{
@ -111,13 +123,8 @@ public class PropUndoButton : MelonMod
if (Input.GetKey(KeyCode.LeftControl))
{
if (Input.GetKey(KeyCode.LeftShift) && Input.GetKeyDown(KeyCode.Z))
{
RedoProp();
}
else if (Input.GetKeyDown(KeyCode.Z))
{
UndoProp();
}
else if (Input.GetKeyDown(KeyCode.Z)) UndoProp();
}
}
@ -138,7 +145,7 @@ public class PropUndoButton : MelonMod
{
if (!EntryEnabled.Value || string.IsNullOrEmpty(instanceId)) return;
var propData = GetPropByInstanceIdAndOwnerId(instanceId);
CVRSyncHelper.PropData propData = GetPropByInstanceIdAndOwnerId(instanceId);
if (propData == null) return;
AddDeletedProp(propData);
@ -150,7 +157,7 @@ public class PropUndoButton : MelonMod
if (deletedProps.Count >= redoHistoryLimit)
deletedProps.RemoveAt(0);
DeletedProp deletedProp = new DeletedProp(propData);
DeletedProp deletedProp = new(propData);
deletedProps.Add(deletedProp);
}
@ -159,7 +166,7 @@ public class PropUndoButton : MelonMod
{
if (!EntryEnabled.Value) return true;
List<CVRSyncHelper.PropData> propsList = GetAllPropsByOwnerId();
var propsList = GetAllPropsByOwnerId();
if (propsList.Count == 0)
{
@ -167,7 +174,7 @@ public class PropUndoButton : MelonMod
return false;
}
for (int i = propsList.Count - 1; i >= 0; i--)
for (var i = propsList.Count - 1; i >= 0; i--)
{
CVRSyncHelper.PropData propData = propsList[i];
SafeDeleteProp(propData);
@ -181,17 +188,17 @@ public class PropUndoButton : MelonMod
{
if (!EntryEnabled.Value) return true;
CVRSyncHelper.PropData[] propsList = CVRSyncHelper.Props.ToArray();
var propsList = CVRSyncHelper.Props.ToArray();
if (propsList.Length == 0)
{
PlayAudioModule(sfx_warn);
return false;
}
for (int i = propsList.Length - 1; i >= 0; i--)
for (var i = propsList.Length - 1; i >= 0; i--)
{
CVRSyncHelper.PropData propData = propsList[i];
DeleteProp(propData);
SafeDeleteProp(propData);
}
return false;
@ -199,7 +206,7 @@ public class PropUndoButton : MelonMod
private static void UndoProp()
{
var propData = GetLatestPropByOwnerId();
CVRSyncHelper.PropData propData = GetLatestPropByOwnerId();
if (propData == null)
{
PlayAudioModule(sfx_warn);
@ -211,7 +218,7 @@ public class PropUndoButton : MelonMod
public static void RedoProp()
{
int index = deletedProps.Count - 1;
var index = deletedProps.Count - 1;
if (index < 0)
{
PlayAudioModule(sfx_warn);
@ -265,74 +272,44 @@ public class PropUndoButton : MelonMod
public static void PlayAudioModule(string module)
{
if (EntryUseSFX.Value)
{
InterfaceAudio.PlayModule(module);
}
}
private static void DeleteProp(CVRSyncHelper.PropData propData)
{
if (propData.Spawnable != null)
{
propData.Spawnable.Delete();
}
else
{
if (propData.Wrapper != null)
UnityEngine.Object.DestroyImmediate(propData.Wrapper);
propData.Recycle();
}
if (EntryUseSFX.Value) InterfaceAudio.PlayModule(module);
}
private static void SafeDeleteProp(CVRSyncHelper.PropData propData)
{
//fixes getting props stuck in limbo state if spawn & delete fast
if (propData.Spawnable == null && propData.Wrapper != null)
if (propData.Spawnable == null)
{
UnityEngine.Object.DestroyImmediate(propData.Wrapper);
// network delete prop manually, then delete prop data
CVRSyncHelper.DeleteMyPropByInstanceIdOverNetwork(propData.InstanceId);
propData.Recycle();
}
else if (propData.Spawnable != null)
{
propData.Spawnable.Delete();
}
else
{
PlayAudioModule(sfx_deny);
return;
}
//if an undo attempt is made right after spawning a prop, the
//spawnable & wrapper will both be null, so the delete request
//will be ignored- same with Delete All button in Menu now
//so the bug causing props to get stuck is due to the propData
//being wiped before the prop spawnable & wrapper were created
//i am unsure if i should attempt an undo of second in line prop,
//just in case some issue happens and causes the mod to lock up here.
//It should be fine though, as reloading world should clear propData...?
// delete prop, prop data, and network delete prop
propData.Spawnable.Delete();
}
private static bool IsPropSpawnAllowed()
{
return MetaPort.Instance.worldAllowProps
&& MetaPort.Instance.settings.GetSettingsBool("ContentFilterPropsEnabled", false)
&& NetworkManager.Instance.GameNetwork.ConnectionState == ConnectionState.Connected;
&& MetaPort.Instance.settings.GetSettingsBool("ContentFilterPropsEnabled")
&& NetworkManager.Instance.GameNetwork.ConnectionState == ConnectionState.Connected;
}
public static bool IsAtPropLimit(int limit = 20)
public static bool IsAtPropLimit(int limit = 21)
{
return GetAllPropsByOwnerId().Count >= limit;
}
private static CVRSyncHelper.PropData GetPropByInstanceIdAndOwnerId(string instanceId)
{
return CVRSyncHelper.Props.Find(propData => propData.InstanceId == instanceId && propData.SpawnedBy == MetaPort.Instance.ownerId);
return CVRSyncHelper.Props.Find(propData =>
propData.InstanceId == instanceId && propData.SpawnedBy == MetaPort.Instance.ownerId);
}
private static CVRSyncHelper.PropData GetLatestPropByOwnerId()
{
// return last prop spawned by owner in CVRSyncHelper.MySpawnedPropInstanceIds
return CVRSyncHelper.Props.LastOrDefault(propData => propData.SpawnedBy == MetaPort.Instance.ownerId);
}
@ -350,15 +327,24 @@ public class PropUndoButton : MelonMod
public DeletedProp(CVRSyncHelper.PropData propData)
{
// Offset spawn height so game can account for it later
Transform spawnable = propData.Spawnable.transform;
Vector3 position = spawnable.position;
position.y -= propData.Spawnable.spawnHeight;
if (propData.Spawnable == null)
{
// use original spawn position and rotation / last known position and rotation
position = new Vector3(propData.PositionX, propData.PositionY, propData.PositionZ);
rotation = new Vector3(propData.RotationX, propData.RotationY, propData.RotationZ);
}
else
{
Transform spawnableTransform = propData.Spawnable.transform;
position = spawnableTransform.position;
rotation = spawnableTransform.rotation.eulerAngles;
this.propGuid = propData.ObjectId;
this.position = position;
this.rotation = spawnable.rotation.eulerAngles;
this.timeDeleted = Time.time;
// Offset spawn height so game can account for it later
position.y -= propData.Spawnable.spawnHeight;
}
propGuid = propData.ObjectId;
timeDeleted = Time.time;
}
}
}

View file

@ -25,6 +25,6 @@ using System.Reflection;
namespace NAK.PropUndoButton.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.0.1";
public const string Version = "1.0.2";
public const string Author = "NotAKidoS";
}

View file

@ -7,9 +7,8 @@ internal class PlayerSetupPatches
{
[HarmonyPostfix]
[HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.Start))]
private static void Post_PlayerSetup_Start(ref PlayerSetup __instance)
private static void Postfix_PlayerSetup_Start(ref PlayerSetup __instance)
{
__instance.vrLeftHandTracker.gameObject.AddComponent<SmoothRayer>().ray = __instance.leftRay;
__instance.vrRightHandTracker.gameObject.AddComponent<SmoothRayer>().ray = __instance.rightRay;
}
}

View file

@ -8,29 +8,6 @@ namespace NAK.SmoothRay;
public class SmoothRay : MelonMod
{
public static readonly MelonPreferences_Category Category =
MelonPreferences.CreateCategory(nameof(SmoothRay));
public static readonly MelonPreferences_Entry<bool> EntryEnabled =
Category.CreateEntry("Enable Smoothing", true,
description: "Enable or disable smoothing.");
public static readonly MelonPreferences_Entry<bool> EntryMenuOnly =
Category.CreateEntry("Menu Only", true,
description: "Only use smoothing on Main Menu and Quick Menu. This will be fine for most users, but it may be desired on pickups & Unity UI elements too.");
public static readonly MelonPreferences_Entry<float> EntryPositionSmoothing =
Category.CreateEntry("Position Smoothing", 3f,
description: "How much to smooth position changes by. Use the slider to adjust the position smoothing factor. Range: 0 to 20.");
public static readonly MelonPreferences_Entry<float> EntryRotationSmoothing =
Category.CreateEntry("Rotation Smoothing", 12f,
description: "How much to smooth rotation changes by. Use the slider to adjust the rotation smoothing factor. Range: 0 to 20.");
public static readonly MelonPreferences_Entry<float> EntrySmallMovementThresholdAngle =
Category.CreateEntry("Small Angle Threshold", 6f,
description: "Angle difference to consider a 'small' movement. The less shaky your hands are, the lower you probably want to set this. This is probably the primary value you want to tweak. Use the slider to adjust the threshold angle. Range: 4 to 15.");
public override void OnInitializeMelon()
{
ApplyPatches(typeof(HarmonyPatches.PlayerSetupPatches));

View file

@ -9,7 +9,7 @@ $cvrExecutable = "ChilloutVR.exe"
$cvrDefaultPath = "E:\temp\CVR_Experimental"
# Array with the dlls to strip
$dllsToStrip = @('Assembly-CSharp.dll','Assembly-CSharp-firstpass.dll','AVProVideo.Runtime.dll', 'Unity.TextMeshPro.dll', 'MagicaCloth.dll')
$dllsToStrip = @('Assembly-CSharp.dll','Assembly-CSharp-firstpass.dll','AVProVideo.Runtime.dll', 'Unity.TextMeshPro.dll', 'MagicaCloth.dll', 'Unity.Services.Vivox.dll')
# Array with the mods to grab
$modNames = @("BTKUILib", "BTKSAImmersiveHud", "ActionMenu", "MenuScalePatch", "ChatBox", "ml_prm")