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() 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); _vrik.onPreSolverUpdate.AddListener(OnPreSolverUpdateDesktop);
} }

View file

@ -16,15 +16,6 @@ internal class IKHandlerHalfBody : IKHandler
public override void OnInitializeIk() 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); _vrik.onPreSolverUpdate.AddListener(OnPreSolverUpdateHalfBody);
} }

View file

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

View file

@ -20,8 +20,6 @@ public class AlternateIKSystem : MelonMod
ApplyPatches(typeof(HarmonyPatches.PlayerSetupPatches)); ApplyPatches(typeof(HarmonyPatches.PlayerSetupPatches));
ApplyPatches(typeof(HarmonyPatches.IKSystemPatches)); ApplyPatches(typeof(HarmonyPatches.IKSystemPatches));
InitializeIntegration("BTKUILib", Integrations.BTKUIAddon.Initialize);
} }
private static void InitializeIntegration(string modName, Action integrationAction) 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."); "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 = 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."); description: "Forces VRIK Plant Feet enabled to prevent hovering when stopping movement.");
public static readonly MelonPreferences_Entry<bool> EntryResetFootstepsOnIdle = 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;
using ABI_RC.Core.Player.AvatarTracking; using ABI_RC.Core.Player.AvatarTracking;
using ABI_RC.Core.UI; using ABI_RC.Core.UI;
@ -11,15 +12,30 @@ namespace NAK.AvatarScaleMod.AvatarScaling;
public class AvatarScaleManager : MonoBehaviour public class AvatarScaleManager : MonoBehaviour
{ {
// Universal Scaling Limits public static AvatarScaleManager Instance { get; private set; }
public const float MinHeight = 0.1f;
public const float MaxHeight = 10f;
public static AvatarScaleManager Instance;
private LocalScaler _localAvatarScaler; private LocalScaler _localAvatarScaler;
private Dictionary<string, NetworkScaler> _networkedScalers; 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; private bool _settingUniversalScaling;
public bool Setting_UniversalScaling public bool Setting_UniversalScaling
{ {
@ -27,21 +43,25 @@ public class AvatarScaleManager : MonoBehaviour
set set
{ {
if (value != _settingUniversalScaling && value == false) if (value != _settingUniversalScaling && value == false)
ResetHeight(); ResetTargetHeight();
_settingUniversalScaling = value; _settingUniversalScaling = value;
SetTargetHeight(_lastTargetHeight); // immediate height update
} }
} }
public bool Setting_PersistantHeight; public bool Setting_AnimationClipScalingOverride;
public bool Setting_PersistentHeight;
private float _lastTargetHeight = -1; private float _lastTargetHeight = -1;
#endregion
#region Unity Methods #region Unity Events
private void Awake() private void Awake()
{ {
if (Instance != null) if (Instance != null
&& Instance != this)
{ {
DestroyImmediate(this); DestroyImmediate(this);
return; return;
@ -53,22 +73,82 @@ public class AvatarScaleManager : MonoBehaviour
private void Start() private void Start()
{ {
_localAvatarScaler = PlayerSetup.Instance.gameObject.AddComponent<LocalScaler>();
_localAvatarScaler.Initialize();
_settingUniversalScaling = ModSettings.EntryUseUniversalScaling.Value; _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); 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() private void OnDestroy()
{ {
_heightUpdateCoroutine = null;
CVRGameEventSystem.Instance.OnConnected.RemoveListener(OnInstanceConnected); 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 #endregion
#region Events #region Game Events
public void OnInstanceConnected(string instanceId) 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); SchedulerSystem.AddJob(ModNetwork.RequestHeightSync, 2f, 1f, 1);
} }
@ -81,19 +161,13 @@ public class AvatarScaleManager : MonoBehaviour
if (playerSetup._avatar == null) if (playerSetup._avatar == null)
return; return;
if (_localAvatarScaler == null)
{
_localAvatarScaler = playerSetup.gameObject.AddComponent<LocalScaler>();
_localAvatarScaler.Initialize();
}
_localAvatarScaler.OnAvatarInstantiated(playerSetup._avatar, playerSetup._initialAvatarHeight, _localAvatarScaler.OnAvatarInstantiated(playerSetup._avatar, playerSetup._initialAvatarHeight,
playerSetup.initialScale); playerSetup.initialScale);
if (!_settingUniversalScaling) if (!_settingUniversalScaling)
return; return;
SetHeight(Setting_PersistantHeight ? _lastTargetHeight : -1f); SetTargetHeight(_lastTargetHeight);
} }
public void OnAvatarDestroyed(PlayerSetup playerSetup) public void OnAvatarDestroyed(PlayerSetup playerSetup)
@ -102,35 +176,34 @@ public class AvatarScaleManager : MonoBehaviour
_localAvatarScaler.OnAvatarDestroyed(); _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) if (!_settingUniversalScaling)
return; return;
if (_localAvatarScaler == null) if (_localAvatarScaler == null)
return; return;
_lastTargetHeight = targetHeight; _localAvatarScaler.SetTargetHeight(_lastTargetHeight);
_localAvatarScaler.heightNeedsUpdate = true; // only local scaler forces update
_localAvatarScaler.SetTargetHeight(targetHeight);
ModNetwork.SendNetworkHeight(targetHeight);
// immediately update play space scale
PlayerSetup.Instance.CheckUpdateAvatarScaleToPlaySpaceRelation();
} }
public void ResetHeight() public void ResetTargetHeight()
{ {
if (_localAvatarScaler == null) if (_localAvatarScaler == null)
return; return;
if (!_localAvatarScaler.IsHeightAdjustedFromInitial()) if (!_localAvatarScaler.IsForcingHeight())
return; 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!", CohtmlHud.Instance.ViewDropTextImmediate("(Local) AvatarScaleMod", "Avatar Scale Reset!",
"Universal Scaling is now disabled."); "Universal Scaling is now disabled.");
SetHeight(-1f); SetTargetHeight(-1f);
} }
public float GetHeight() public float GetHeight()
@ -138,10 +211,21 @@ public class AvatarScaleManager : MonoBehaviour
if (_localAvatarScaler == null) if (_localAvatarScaler == null)
return PlayerAvatarPoint.defaultAvatarHeight; return PlayerAvatarPoint.defaultAvatarHeight;
if (!_localAvatarScaler.IsHeightAdjustedFromInitial()) if (!_localAvatarScaler.IsForcingHeight())
return PlayerSetup.Instance.GetAvatarHeight(); 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() public float GetHeightForNetwork()
@ -152,10 +236,10 @@ public class AvatarScaleManager : MonoBehaviour
if (_localAvatarScaler == null) if (_localAvatarScaler == null)
return -1f; return -1f;
if (!_localAvatarScaler.IsHeightAdjustedFromInitial()) if (!_localAvatarScaler.IsForcingHeight())
return -1f; return -1f;
return _localAvatarScaler.GetHeight(); return _localAvatarScaler.GetTargetHeight();
} }
public float GetInitialHeight() public float GetInitialHeight()
@ -168,17 +252,23 @@ public class AvatarScaleManager : MonoBehaviour
public bool IsHeightAdjustedFromInitial() public bool IsHeightAdjustedFromInitial()
{ {
return _localAvatarScaler != null && _localAvatarScaler.IsHeightAdjustedFromInitial(); return _localAvatarScaler != null && _localAvatarScaler.IsForcingHeight();
} }
#endregion #endregion
#region Network Methods #region Network Methods
public bool DoesNetworkHeightScalerExist(string playerId)
=> _networkedScalers.ContainsKey(playerId);
public int GetNetworkHeightScalerCount()
=> _networkedScalers.Count;
public float GetNetworkHeight(string playerId) public float GetNetworkHeight(string playerId)
{ {
if (_networkedScalers.TryGetValue(playerId, out NetworkScaler scaler)) 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 //doesn't have mod or has no custom height, get from player avatar directly
CVRPlayerEntity playerEntity = CVRPlayerManager.Instance.NetworkPlayers.Find((players) => players.Uuid == playerId); CVRPlayerEntity playerEntity = CVRPlayerManager.Instance.NetworkPlayers.Find((players) => players.Uuid == playerId);
@ -266,4 +356,40 @@ public class AvatarScaleManager : MonoBehaviour
} }
#endregion #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 System.Diagnostics;
using ABI_RC.Core.Player; using ABI_RC.Core;
using NAK.AvatarScaleMod.AvatarScaling; using NAK.AvatarScaleMod.AvatarScaling;
using NAK.AvatarScaleMod.ScaledComponents; using NAK.AvatarScaleMod.ScaledComponents;
using UnityEngine; using UnityEngine;
@ -12,42 +12,107 @@ public class BaseScaler : MonoBehaviour
{ {
#region Constants #region Constants
public const string ScaleFactorParameterName = "ScaleFactor"; protected const string ScaleFactorParameterName = "ScaleFactor";
public const string ScaleFactorParameterNameLocal = "#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 #endregion
#region Variables #region Variables
internal bool _isAvatarInstantiated; // Height update requested
internal bool _isHeightAdjustedFromInitial; public bool heightNeedsUpdate { get; internal set; }
internal bool _heightNeedsUpdate;
// 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 Transform _avatarTransform;
internal CVRAnimatorManager _animatorManager; internal CVRAnimatorManager _animatorManager;
// Initial scaling
internal float _initialHeight; internal float _initialHeight;
internal Vector3 _initialScale; internal Vector3 _initialScale;
// Forced scaling (Universal & Hidden Avatar)
internal float _targetHeight = -1; internal float _targetHeight = -1;
internal Vector3 _targetScale = Vector3.one; internal Vector3 _targetScale = Vector3.one;
internal float _scaleFactor = 1f; internal float _scaleFactor = 1f;
// detection for animation clip-based scaling // AnimationClip-based scaling (Local Avatar)
internal Vector3 _legacyAnimationScale; internal float _animatedHeight;
internal Vector3 _animatedScale;
internal float _animatedScaleFactor = 1f;
#endregion #endregion
#region Public Methods #region Avatar Events
public virtual void OnAvatarInstantiated(GameObject avatarObject, float initialHeight, Vector3 initialScale) public virtual void OnAvatarInstantiated(GameObject avatarObject, float initialHeight, Vector3 initialScale)
{ {
if (_isAvatarInstantiated) return; if (_isAvatarInstantiated) return;
_isAvatarInstantiated = true; _isAvatarInstantiated = true;
_initialHeight = Mathf.Clamp(initialHeight, 0.01f, 100f); _initialHeight = _animatedHeight = Mathf.Clamp(initialHeight, 0.01f, 100f);
_initialScale = initialScale; _initialScale = _animatedScale = initialScale;
_animatedScaleFactor = 1f;
if (!_shouldForceHeight) // not universal or hidden avatar
{
_targetHeight = _initialHeight;
_targetScale = _initialScale;
_scaleFactor = 1f;
}
_avatarTransform = avatarObject.transform; _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() public void OnAvatarDestroyed()
@ -56,99 +121,92 @@ public class BaseScaler : MonoBehaviour
_isAvatarInstantiated = false; _isAvatarInstantiated = false;
_avatarTransform = null; _avatarTransform = null;
_heightNeedsUpdate = false; heightNeedsUpdate = false;
ClearComponentLists(); 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) public void SetTargetHeight(float height)
{ {
if (_isHeightAdjustedFromInitial
&& Math.Abs(height - _targetHeight) < float.Epsilon)
return;
if (height < float.Epsilon) if (height < float.Epsilon)
{ {
ResetHeight(); ResetTargetHeight();
return; return;
} }
if (!_isHeightAdjustedFromInitial)
_legacyAnimationScale = Vector3.zero;
_isHeightAdjustedFromInitial = true;
_targetHeight = Mathf.Clamp(height, AvatarScaleManager.MinHeight, AvatarScaleManager.MaxHeight); _targetHeight = Mathf.Clamp(height, AvatarScaleManager.MinHeight, AvatarScaleManager.MaxHeight);
_heightNeedsUpdate = true; _scaleFactor = Mathf.Max(_targetHeight / _initialHeight, 0.01f); //safety
_targetScale = _initialScale * _scaleFactor;
UpdateScaleIfInstantiated(); InvokeTargetHeightChanged();
} }
public void ResetHeight() public void ResetTargetHeight()
{ {
if (!_isHeightAdjustedFromInitial) return; // if (Math.Abs(_initialHeight - _targetHeight) < float.Epsilon)
_isHeightAdjustedFromInitial = false; // return; // no need to change, is close enough
if (Math.Abs(_initialHeight - _targetHeight) < float.Epsilon) useTargetHeight = false;
return;
_legacyAnimationScale = Vector3.zero; _targetHeight = _animatedHeight;
_targetScale = _animatedScale;
_scaleFactor = _animatedScaleFactor;
_targetHeight = _initialHeight; InvokeTargetHeightReset();
_heightNeedsUpdate = true;
UpdateScaleIfInstantiated();
} }
public float GetHeight() => _targetHeight; public bool ApplyTargetHeight()
public float GetInitialHeight() => _initialHeight; {
public bool IsHeightAdjustedFromInitial() => _isHeightAdjustedFromInitial; if (!_isAvatarInstantiated || _initialHeight == 0)
return false;
if (_avatarTransform == null)
return false;
heightNeedsUpdate = false;
ScaleAvatarRoot();
UpdateAnimatorParameter();
ApplyComponentScaling();
return true;
}
#endregion #endregion
#region Private Methods #region Private Methods
internal void ScaleAvatarRoot() private void ScaleAvatarRoot()
{ {
if (_avatarTransform == null) return; if (_avatarTransform == null) return;
_avatarTransform.localScale = _targetScale; _avatarTransform.localScale = _targetScale;
} }
internal virtual void UpdateAnimatorParameter() protected virtual void UpdateAnimatorParameter()
{ {
// empty // 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 #endregion
#region Unity Methods #region Unity Events
public virtual void LateUpdate() public virtual void LateUpdate()
{ {
if (!_isHeightAdjustedFromInitial)
return;
if (!_isAvatarInstantiated) if (!_isAvatarInstantiated)
return; return; // no avatar
ScaleAvatarRoot(); // override animationclip-based scaling if (!_shouldForceHeight)
return; // not universal scaling or hidden avatar
ScaleAvatarRoot();
} }
internal virtual void OnDestroy() internal virtual void OnDestroy()
@ -170,11 +228,11 @@ public class BaseScaler : MonoBehaviour
typeof(ScaleConstraint) typeof(ScaleConstraint)
}; };
private readonly List<ScaledLight> _scaledLights = new List<ScaledLight>(); private readonly List<ScaledLight> _scaledLights = new();
private readonly List<ScaledAudioSource> _scaledAudioSources = new List<ScaledAudioSource>(); private readonly List<ScaledAudioSource> _scaledAudioSources = new();
private readonly List<ScaledParentConstraint> _scaledParentConstraints = new List<ScaledParentConstraint>(); private readonly List<ScaledParentConstraint> _scaledParentConstraints = new();
private readonly List<ScaledPositionConstraint> _scaledPositionConstraints = new List<ScaledPositionConstraint>(); private readonly List<ScaledPositionConstraint> _scaledPositionConstraints = new();
private readonly List<ScaledScaleConstraint> _scaledScaleConstraints = new List<ScaledScaleConstraint>(); private readonly List<ScaledScaleConstraint> _scaledScaleConstraints = new();
private void ClearComponentLists() private void ClearComponentLists()
{ {
@ -185,9 +243,8 @@ public class BaseScaler : MonoBehaviour
_scaledScaleConstraints.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); var components = _avatarTransform.gameObject.GetComponentsInChildren<Component>(true);
foreach (Component component in components) foreach (Component component in components)
@ -195,17 +252,10 @@ public class BaseScaler : MonoBehaviour
if (this == null) break; if (this == null) break;
if (component == null) continue; if (component == null) continue;
tasks.Add(Task.Run(() =>
{
Type componentType = component.GetType(); Type componentType = component.GetType();
if (types.Contains(componentType)) if (types.Contains(componentType))
{
AddScaledComponent(componentType, component); AddScaledComponent(componentType, component);
} }
}));
}
await Task.WhenAll(tasks);
} }
private void AddScaledComponent(Type type, Component component) private void AddScaledComponent(Type type, Component component)

View file

@ -1,6 +1,5 @@
using ABI_RC.Core.Player; using ABI_RC.Core.Player;
using ABI_RC.Core.UI; using ABI_RC.Core.UI;
using ABI.CCK.Components;
using NAK.AvatarScaleMod.AvatarScaling; using NAK.AvatarScaleMod.AvatarScaling;
using UnityEngine; using UnityEngine;
@ -14,31 +13,23 @@ public class LocalScaler : BaseScaler
{ {
_animatorManager = GetComponentInParent<PlayerSetup>().animatorManager; _animatorManager = GetComponentInParent<PlayerSetup>().animatorManager;
_heightNeedsUpdate = false; heightNeedsUpdate = false;
_isAvatarInstantiated = false; _isAvatarInstantiated = false;
_isHeightAdjustedFromInitial = false;
} }
#endregion #endregion
#region Overrides #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) if (avatarObject == null)
return; return;
base.OnAvatarInstantiated(avatarObject, initialHeight, initialScale); 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) if (_animatorManager == null)
return; return;
@ -49,14 +40,8 @@ public class LocalScaler : BaseScaler
public override void LateUpdate() public override void LateUpdate()
{ {
if (!_isHeightAdjustedFromInitial)
return;
if (!_isAvatarInstantiated)
return;
if (!CheckForAnimationScaleChange()) if (!CheckForAnimationScaleChange())
ScaleAvatarRoot(); base.LateUpdate();
} }
#endregion #endregion
@ -65,28 +50,41 @@ public class LocalScaler : BaseScaler
private bool CheckForAnimationScaleChange() private bool CheckForAnimationScaleChange()
{ {
if (_avatarTransform == null) return false; if (_avatarTransform == null)
return false;
Vector3 localScale = _avatarTransform.localScale;
// scale matches last recorded animation scale // scale matches last recorded animation scale
if (_avatarTransform.localScale == _legacyAnimationScale) if (localScale == _animatedScale)
return false; return false;
// avatar may not have scale animation, check if it isn't equal to targetScale // avatar may not have scale animation, check if it isn't equal to targetScale
if (_avatarTransform.localScale == _targetScale) if (localScale == _targetScale)
return false; return false;
// scale was likely reset or not initiated // this is the first time we've seen the avatar animated scale, record it!
if (_legacyAnimationScale == Vector3.zero) if (_animatedScale == Vector3.zero)
{ {
_legacyAnimationScale = _avatarTransform.localScale; _animatedScale = localScale;
return false; 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; return true;
} }

View file

@ -1,4 +1,5 @@
using ABI_RC.Core.Player; using System.Diagnostics;
using ABI_RC.Core.Player;
using NAK.AvatarScaleMod.AvatarScaling; using NAK.AvatarScaleMod.AvatarScaling;
using UnityEngine; using UnityEngine;
@ -16,28 +17,34 @@ public class NetworkScaler : BaseScaler
_animatorManager = GetComponentInParent<PuppetMaster>().animatorManager; _animatorManager = GetComponentInParent<PuppetMaster>().animatorManager;
_heightNeedsUpdate = false; heightNeedsUpdate = false;
_isAvatarInstantiated = false; _isAvatarInstantiated = false;
_isHeightAdjustedFromInitial = false;
} }
#endregion #endregion
#region Overrides #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) if (avatarObject == null)
return; return;
base.OnAvatarInstantiated(avatarObject, initialHeight, initialScale); base.OnAvatarInstantiated(avatarObject, initialHeight, initialScale);
await FindComponentsOfTypeAsync(scalableComponentTypes);
if (_isHeightAdjustedFromInitial && _heightNeedsUpdate) Stopwatch stopwatch = new();
UpdateScaleIfInstantiated(); stopwatch.Start();
FindComponentsOfType(scalableComponentTypes);
stopwatch.Stop();
if (ModSettings.Debug_ComponentSearchTime.Value)
AvatarScaleMod.Logger.Msg($"({typeof(NetworkScaler)}) Component search time for {avatarObject}: {stopwatch.ElapsedMilliseconds}ms");
// TODO: why did i do this? height is never set prior to this method being called
// if (_isHeightAdjustedFromInitial && heightNeedsUpdate)
// UpdateScaleIfInstantiated();
} }
internal override void UpdateAnimatorParameter() protected override void UpdateAnimatorParameter()
{ {
_animatorManager?.SetAnimatorParameter(ScaleFactorParameterNameLocal, _scaleFactor); _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() public void Reset()
{ {
Component.range = InitialRange; Component.range = InitialRange;
} }
} }

View file

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

View file

@ -30,14 +30,14 @@ internal static class DebugKeybinds
{ {
float currentHeight = AvatarScaleManager.Instance.GetHeight() + adjustment; float currentHeight = AvatarScaleManager.Instance.GetHeight() + adjustment;
currentHeight = Mathf.Max(0f, currentHeight); currentHeight = Mathf.Max(0f, currentHeight);
AvatarScaleManager.Instance.SetHeight(currentHeight); AvatarScaleManager.Instance.SetTargetHeight(currentHeight);
AvatarScaleMod.Logger.Msg($"[Debug] Setting height: {currentHeight}"); AvatarScaleMod.Logger.Msg($"[Debug] Setting height: {currentHeight}");
} }
private static void ResetHeight() private static void ResetHeight()
{ {
AvatarScaleManager.Instance.ResetHeight(); AvatarScaleManager.Instance.ResetTargetHeight();
AvatarScaleMod.Logger.Msg("[Debug] Resetting height."); 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); float heightAdjustmentFactor = (modifierRatio > 1) ? 1 + (modifierRatio - 1) : 1 - (1 - modifierRatio);
// Apply the adjustment to the target height // 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) 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 // Constants
internal const string ModName = nameof(AvatarScaleMod); internal const string ModName = nameof(AvatarScaleMod);
internal const string SettingsCategory = "Avatar Scale Mod"; 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";
public static readonly MelonPreferences_Category Category = private static readonly MelonPreferences_Category Category =
MelonPreferences.CreateCategory(ModName); MelonPreferences.CreateCategory(ModName);
public static readonly MelonPreferences_Entry<bool> EntryUseUniversalScaling = #region Hidden Foldout Entries
Category.CreateEntry("use_universal_scaling", true, display_name: "Use Universal Scaling", description: "Enable or disable universal scaling.");
public static readonly MelonPreferences_Entry<bool> EntryPersistantHeight = // Avatar Scale Mod Foldout
Category.CreateEntry("persistant_height", false, display_name: "Persistant Height", description: "Should the avatar height persist between avatar switches?"); internal static readonly MelonPreferences_Entry<bool> Hidden_Foldout_ASM_SettingsCategory =
Category.CreateEntry("hidden_foldout_asm", true, is_hidden: true, display_name: ASM_SettingsCategory, description: "Foldout state for Avatar Scale Mod settings.");
// Avatar Scale Tool Foldout
internal static readonly MelonPreferences_Entry<bool> Hidden_Foldout_AST_SettingsCategory =
Category.CreateEntry("hidden_foldout_ast", false, is_hidden: true, display_name: AST_SettingsCategory, description: "Foldout state for Avatar Scale Tool settings.");
// Universal Scaling (Mod Network) Foldout
internal static readonly MelonPreferences_Entry<bool> Hidden_Foldout_USM_SettingsCategory =
Category.CreateEntry("hidden_foldout_usm", false, is_hidden: true, display_name: USM_SettingsCategory, description: "Foldout state for Universal Scaling (Mod Network) settings.");
// Debug Options Foldout
internal static readonly MelonPreferences_Entry<bool> Hidden_Foldout_DEBUG_SettingsCategory =
Category.CreateEntry("hidden_foldout_debug", false, is_hidden: true, display_name: DEBUG_SettingsCategory, description: "Foldout state for Debug Options settings.");
// Player Select Page Foldout
internal static readonly MelonPreferences_Entry<bool> Hidden_Foldout_PlayerSelectPage =
Category.CreateEntry("hidden_foldout_player_select_page", true, is_hidden: true, display_name: ASM_SettingsCategory, description: "Foldout state for Player Select Page.");
#endregion
#region Avatar Scale Mod Settings
public static readonly MelonPreferences_Entry<bool> EntryScaleGestureEnabled = 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."); 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."); Category.CreateEntry("debug_outbound", false, display_name: "Debug Outbound", description: "Log outbound Mod Network height updates.");
public static readonly MelonPreferences_Entry<float> EntryHiddenLastAvatarScale = public static readonly MelonPreferences_Entry<bool> Debug_ComponentSearchTime =
Category.CreateEntry("last_avatar_scale", -1f, is_hidden: true); Category.CreateEntry("debug_component_search_time", false, display_name: "Debug Search Time", description: "Log component search time.");
#endregion
public static void Initialize() public static void Initialize()
{ {
foreach (MelonPreferences_Entry entry in Category.Entries) // subscribe to all bool settings that aren't hidden
entry.OnEntryValueChangedUntyped.Subscribe(OnSettingsChanged); 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; GestureReconizer.ScaleReconizer.Enabled = EntryScaleGestureEnabled.Value;
ModNetwork.Debug_NetworkInbound = Debug_NetworkInbound.Value;
ModNetwork.Debug_NetworkOutbound = Debug_NetworkOutbound.Value;
ModNetwork.Debug_NetworkInbound = EntryDebugNetworkInbound.Value; if (AvatarScaleManager.Instance == null) return;
ModNetwork.Debug_NetworkOutbound = EntryDebugNetworkOutbound.Value; 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.Core.Networking;
using ABI_RC.Systems.ModNetwork; using ABI_RC.Systems.ModNetwork;
using DarkRift; using DarkRift;
using MelonLoader;
using NAK.AvatarScaleMod.AvatarScaling; using NAK.AvatarScaleMod.AvatarScaling;
using UnityEngine; using UnityEngine;
@ -62,8 +61,13 @@ public static class ModNetwork
internal static void Subscribe() internal static void Subscribe()
{ {
_isSubscribedToModNetwork = true;
ModNetworkManager.Subscribe(ModId, OnMessageReceived); 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() internal static void Update()
@ -244,4 +248,36 @@ public static class ModNetwork
} }
#endregion #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;
using ABI_RC.Core.Savior; using ABI_RC.Systems.InputManagement;
using ABI_RC.Core.Util.Object_Behaviour;
using ABI_RC.Systems.IK;
using ABI_RC.Systems.IK.TrackingModules;
using cohtml.Net;
using HarmonyLib; using HarmonyLib;
using NAK.DesktopVRSwitch.Patches; using NativeVRModeSwitchManager = ABI_RC.Systems.VRModeSwitch.VRModeSwitchManager;
using NAK.DesktopVRSwitch.VRModeTrackers;
using UnityEngine;
using Valve.VR;
namespace NAK.DesktopVRSwitch.HarmonyPatches; namespace NAK.DesktopVRSwitch.HarmonyPatches;
internal class CheckVRPatches internal class CVRInputManagerPatches
{ {
[HarmonyPostfix] [HarmonyPostfix]
[HarmonyPatch(typeof(CheckVR), nameof(CheckVR.Awake))] [HarmonyPatch(typeof(CVRInputManager), "OnPostVRModeSwitch")]
private static void Postfix_CheckVR_Start(ref CheckVR __instance) private static void Postfix_CVRInputManager_OnPostVRModeSwitch(bool inVr, UnityEngine.Camera playerCamera)
{ {
try RootLogic.Instance.ToggleMouse(inVr);
{
__instance.gameObject.AddComponent<VRModeSwitchManager>();
}
catch (Exception e)
{
DesktopVRSwitch.Logger.Error($"Error during the patched method {nameof(Postfix_CheckVR_Start)}");
DesktopVRSwitch.Logger.Error(e);
}
} }
} }
internal class IKSystemPatches internal class VRModeSwitchManagerPatches
{
[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
{ {
[HarmonyPrefix] [HarmonyPrefix]
[HarmonyPatch(typeof(CVRPickupObject), nameof(CVRPickupObject.Start))] [HarmonyPatch(typeof(NativeVRModeSwitchManager), "StartSwitchInternal")]
private static void Prefix_CVRPickupObject_Start(ref CVRPickupObject __instance) private static void Postfix_CVRInputManager_OnPostVRModeSwitch()
{ {
try CVRInputManager.Instance.inputEnabled = false;
{
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;
} }
} }

View file

@ -1,84 +1,14 @@
 using System;
using MelonLoader; using MelonLoader;
using NAK.DesktopVRSwitch.VRModeTrackers;
using UnityEngine;
namespace NAK.DesktopVRSwitch; namespace NAK.DesktopVRSwitch;
public class DesktopVRSwitch : MelonMod public class DesktopVRSwitch : MelonMod
{ {
internal static MelonLogger.Instance Logger;
public override void OnInitializeMelon() public override void OnInitializeMelon()
{ {
Logger = LoggerInstance; ApplyPatches(typeof(HarmonyPatches.CVRInputManagerPatches));
ApplyPatches(typeof(HarmonyPatches.VRModeSwitchManagerPatches));
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();
} }
private void ApplyPatches(Type type) private void ApplyPatches(Type type)

View file

@ -1,25 +1,27 @@
using ABI_RC.Systems.UI; using ABI_RC.Systems.UI;
using NAK.DesktopVRSwitch.VRModeTrackers;
using System.Collections; 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;
using UnityEngine.Events;
using UnityEngine.XR.Management; using UnityEngine.XR.Management;
namespace NAK.DesktopVRSwitch; namespace NAK.DesktopVRSwitch;
public class VRModeSwitchManager : MonoBehaviour public class VRModeSwitchManager : MonoBehaviour
{ {
#region Static
public static VRModeSwitchManager Instance { get; private set; } 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 #region Variables
// Settings // Settings
public bool DesktopVRSwitchEnabled;
public bool UseWorldTransition = true; public bool UseWorldTransition = true;
public bool ReloadLocalAvatar = true; public bool ReloadLocalAvatar = true;
@ -36,26 +38,58 @@ public class VRModeSwitchManager : MonoBehaviour
DestroyImmediate(this); DestroyImmediate(this);
return; return;
} }
Instance = this; 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 #endregion
#region Public Methods #region Public Methods
private static bool IsInXR() => XRGeneralSettings.Instance.Manager.activeLoader != null; public void AttemptSwitch()
{
if (SwitchInProgress)
return;
public void AttemptSwitch() => StartCoroutine(StartSwitchInternal()); // dont allow switching during world transfer, itll explode violently
if (CVRObjectLoader.Instance.IsLoadingWorldToJoin())
return;
StartCoroutine(StartSwitchInternal());
}
#endregion #endregion
#region Private Methods #region Private Methods
private void OnSettingsBoolChanged(string settingName, bool val)
{
if (settingName == "ExperimentalDesktopVRSwitch")
DesktopVRSwitchEnabled = val;
}
private IEnumerator StartSwitchInternal() private IEnumerator StartSwitchInternal()
{ {
if (SwitchInProgress) if (SwitchInProgress)
yield break; yield break;
NotifyOnPreSwitch();
bool useWorldTransition = UseWorldTransition; bool useWorldTransition = UseWorldTransition;
SwitchInProgress = true; SwitchInProgress = true;
@ -64,11 +98,35 @@ public class VRModeSwitchManager : MonoBehaviour
if (useWorldTransition) if (useWorldTransition)
yield return StartCoroutine(StartTransition()); 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) if (useWorldTransition)
yield return StartCoroutine(ContinueTransition()); yield return StartCoroutine(ContinueTransition());
@ -76,29 +134,25 @@ public class VRModeSwitchManager : MonoBehaviour
SwitchInProgress = false; 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() private void ReloadAvatar()
{ {
if (!ReloadLocalAvatar) if (!ReloadLocalAvatar)
return; return;
Utils.ClearLocalAvatar(); // TODO: Is there a better way to reload only locally?
Utils.ReloadLocalAvatar(); PlayerSetup.Instance.ClearAvatar();
AssetManagement.Instance.LoadLocalAvatar(MetaPort.Instance.currentAvatarGuid);
}
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 #endregion
@ -123,37 +177,45 @@ public class VRModeSwitchManager : MonoBehaviour
#region Event Handling #region Event Handling
public class VRModeEventArgs : EventArgs private void InvokeOnPreSwitch(bool isUsingVr)
{ {
public bool IsUsingVr { get; } UnityEngine.Camera playerCamera = GetPlayerCamera(isUsingVr);
public Camera PlayerCamera { get; }
public VRModeEventArgs(bool isUsingVr, Camera playerCamera) ABI_RC.Systems.VRModeSwitch.VRModeSwitchManager.OnPreSwitchInternal?.Invoke(isUsingVr, playerCamera);
{ CVRGameEventSystem.VRModeSwitch.OnPreSwitch?.Invoke(isUsingVr, playerCamera);
IsUsingVr = isUsingVr;
PlayerCamera = playerCamera;
}
} }
public static event EventHandler<VRModeEventArgs> OnPreVRModeSwitch; private void InvokeOnPostSwitch(bool isUsingVr)
public static event EventHandler<VRModeEventArgs> OnPostVRModeSwitch; {
public static event EventHandler<VRModeEventArgs> OnFailVRModeSwitch; UnityEngine.Camera playerCamera = GetPlayerCamera(isUsingVr);
private void InvokeOnPreSwitch(bool isUsingVr) => SafeInvokeUnityEvent(OnPreVRModeSwitch, isUsingVr); ABI_RC.Systems.VRModeSwitch.VRModeSwitchManager.OnPostSwitchInternal?.Invoke(isUsingVr, playerCamera);
private void InvokeOnPostSwitch(bool isUsingVr) => SafeInvokeUnityEvent(OnPostVRModeSwitch, isUsingVr); CVRGameEventSystem.VRModeSwitch.OnPostSwitch?.Invoke(isUsingVr, playerCamera);
private void InvokeOnFailedSwitch(bool isUsingVr) => SafeInvokeUnityEvent(OnFailVRModeSwitch, isUsingVr); }
private void SafeInvokeUnityEvent(EventHandler<VRModeEventArgs> switchEvent, bool isUsingVr) private void InvokeOnFailedSwitch(bool isUsingVr)
{ {
try UnityEngine.Camera playerCamera = GetPlayerCamera(isUsingVr);
{
Camera playerCamera = Utils.GetPlayerCameraObject(isUsingVr).GetComponent<Camera>(); ABI_RC.Systems.VRModeSwitch.VRModeSwitchManager.OnFailedSwitchInternal?.Invoke(isUsingVr, playerCamera);
switchEvent?.Invoke(this, new VRModeEventArgs(isUsingVr, playerCamera)); CVRGameEventSystem.VRModeSwitch.OnFailedSwitch?.Invoke(isUsingVr, playerCamera);
} }
catch (Exception e)
#endregion
#region Notifications
private void NotifyOnPreSwitch()
{ {
DesktopVRSwitch.Logger.Error($"Error in event handler: {e}"); 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 #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 System.Reflection;
using ABI_RC.Core.Savior; using ABI_RC.Core.Savior;
using Unity.XR.OpenVR; using Unity.XR.OpenVR;
@ -6,62 +10,76 @@ using UnityEngine;
using UnityEngine.XR.Management; using UnityEngine.XR.Management;
using UnityEngine.XR.OpenXR; using UnityEngine.XR.OpenXR;
using Valve.VR; using Valve.VR;
using Object = UnityEngine.Object;
namespace NAK.DesktopVRSwitch; namespace ABI_RC.Systems.VRModeSwitch
{
internal static class XRHandler internal static class XRHandler
{ {
internal static IEnumerator StartXR() private static async Task InitializeXRLoader()
{ {
yield return XRGeneralSettings.Instance.Manager.InitializeLoader(); EnsureXRLoader();
XRGeneralSettings.Instance.Manager.InitializeLoaderSync();
await Task.Yield();
}
internal static async Task StartXR()
{
await InitializeXRLoader();
if (XRGeneralSettings.Instance.Manager.activeLoader != null) if (XRGeneralSettings.Instance.Manager.activeLoader != null)
XRGeneralSettings.Instance.Manager.StartSubsystems(); XRGeneralSettings.Instance.Manager.StartSubsystems();
else else
yield return StopXR(); await StopXR(); // assuming StopXR is now an async method.
yield return null; // 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.
} }
internal static IEnumerator StopXR() internal static Task StopXR()
{ {
if (!XRGeneralSettings.Instance.Manager.isInitializationComplete) if (!XRGeneralSettings.Instance.Manager.isInitializationComplete)
yield break; return Task.CompletedTask;
// Forces SteamVR to reinitialize SteamVR_Input next switch // Forces SteamVR to reinitialize SteamVR_Input next switch
SteamVR_ActionSet_Manager.DisableAllActionSets(); SteamVR_ActionSet_Manager.DisableAllActionSets();
SteamVR_Input.initialized = false; SteamVR_Input.initialized = false;
// Remove SteamVR behaviour & render // Remove SteamVR behaviour & render
UnityEngine.Object.DestroyImmediate(SteamVR_Behaviour.instance.gameObject); Object.DestroyImmediate(SteamVR_Behaviour.instance.gameObject);
SteamVR.enabled = false; // disposes SteamVR SteamVR.enabled = false; // disposes SteamVR
Patches.SteamVRNullReferencePatch.DestroySteamVRInstancesImmediate();
// Disable UnityXR // Disable UnityXR
XRGeneralSettings.Instance.Manager.StopSubsystems(); XRGeneralSettings.Instance.Manager.StopSubsystems();
XRGeneralSettings.Instance.Manager.DeinitializeLoader(); XRGeneralSettings.Instance.Manager.DeinitializeLoader();
return Task.CompletedTask;
// We don't really need to wait a frame on Stop() // If we need to wait for something specific (like a frame), we use Task.Delay or equivalent.
yield return null; // 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.
} }
internal static void SwitchLoader() private static void EnsureXRLoader()
{ {
XRLoader item; Type selectedLoaderType = !CheckVR.Instance.forceOpenXr ? typeof(OpenVRLoader) : typeof(OpenXRLoader);
if (!CheckVR.Instance.forceOpenXr) // dont do anything if we already have the loader selected
{ if (XRGeneralSettings.Instance.Manager.activeLoaders.Count > 0
item = ScriptableObject.CreateInstance<OpenVRLoader>(); && XRGeneralSettings.Instance.Manager.activeLoaders[0].GetType() == selectedLoaderType)
DesktopVRSwitch.Logger.Msg("Using XR Loader: SteamVR"); return;
}
else
{
item = ScriptableObject.CreateInstance<OpenXRLoader>();
DesktopVRSwitch.Logger.Msg("Using XR Loader: OpenXR");
}
typeof(XRManagerSettings) XRLoader newLoaderInstance = (XRLoader)ScriptableObject.CreateInstance(selectedLoaderType);
.GetField("m_Loaders", BindingFlags.Instance | BindingFlags.NonPublic) FieldInfo field = typeof(XRManagerSettings).GetField("m_Loaders",
?.SetValue(XRGeneralSettings.Instance.Manager, new List<XRLoader> { item }); 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 EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EzGrab", "EzGrab\EzGrab.csproj", "{8CBF5378-A4AE-4594-9A13-0066E5D3FE81}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EzGrab", "EzGrab\EzGrab.csproj", "{8CBF5378-A4AE-4594-9A13-0066E5D3FE81}"
EndProject 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 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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution

View file

@ -11,15 +11,15 @@ internal class PortableCameraPatches
private static void Postfix_PortableCamera_Start(ref PortableCamera __instance) private static void Postfix_PortableCamera_Start(ref PortableCamera __instance)
{ {
//run mod.Setup() instead of registering full mod with icon //run mod.Setup() instead of registering full mod with icon
VisualMods.CameraAdditions mainMod = new VisualMods.CameraAdditions(); VisualMods.CameraAdditions mainMod = new ();
mainMod.Setup(__instance); mainMod.Setup(__instance);
} }
[HarmonyPostfix] [HarmonyPostfix]
[HarmonyPatch(typeof(PortableCamera), nameof(PortableCamera.OnWorldLoaded))] [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] [HarmonyPostfix]

View file

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

View file

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

View file

@ -1,8 +1,8 @@
{ {
"_id": 123, "_id": 123,
"name": "PortableCameraAdditions", "name": "PortableCameraAdditions",
"modversion": "1.0.4", "modversion": "1.0.5",
"gameversion": "2023r172", "gameversion": "2023r173",
"loaderversion": "0.6.1", "loaderversion": "0.6.1",
"modtype": "Mod", "modtype": "Mod",
"author": "NotAKidoS", "author": "NotAKidoS",
@ -18,8 +18,8 @@
"requirements": [ "requirements": [
"None" "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/", "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" "embedcolor": "#ffd96a"
} }

View file

@ -1,11 +1,13 @@
using ABI.CCK.Components; using System.Reflection;
using ABI_RC.Core.AudioEffects; using ABI_RC.Core.AudioEffects;
using ABI_RC.Core.Networking; using ABI_RC.Core.Networking;
using ABI_RC.Core.Savior; using ABI_RC.Core.Savior;
using ABI_RC.Core.Util; using ABI_RC.Core.Util;
using ABI_RC.Systems.InputManagement.InputModules;
using ABI.CCK.Components;
using DarkRift; using DarkRift;
using HarmonyLib;
using MelonLoader; using MelonLoader;
using System.Reflection;
using UnityEngine; using UnityEngine;
namespace NAK.PropUndoButton; namespace NAK.PropUndoButton;
@ -21,9 +23,10 @@ public class PropUndoButton : MelonMod
Category.CreateEntry("Enabled", true, description: "Toggle Undo Prop Button."); Category.CreateEntry("Enabled", true, description: "Toggle Undo Prop Button.");
public static readonly MelonPreferences_Entry<bool> EntryUseSFX = public static readonly MelonPreferences_Entry<bool> EntryUseSFX =
Category.CreateEntry("Use SFX", true, description: "Toggle audio queues for prop spawn, undo, redo, and warning."); Category.CreateEntry("Use SFX", true,
description: "Toggle audio queues for prop spawn, undo, redo, and warning.");
internal static List<DeletedProp> deletedProps = new List<DeletedProp>(); internal static List<DeletedProp> deletedProps = new();
// audio clip names, InterfaceAudio adds "PropUndo_" prefix // audio clip names, InterfaceAudio adds "PropUndo_" prefix
private const string sfx_spawn = "PropUndo_sfx_spawn"; 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 HarmonyInstance.Patch( // delete my props in reverse order for redo
typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.DeleteMyProps)), 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 HarmonyInstance.Patch( // delete all props in reverse order for redo
typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.DeleteAllProps)), 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 HarmonyInstance.Patch( // prop spawn sfx
typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.SpawnProp)), 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 HarmonyInstance.Patch( // prop delete sfx, log for possible redo
typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.DeletePropByInstanceId)), typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.DeleteMyPropByInstanceIdOverNetwork)),
postfix: new HarmonyLib.HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnDeletePropByInstanceId), BindingFlags.NonPublic | BindingFlags.Static)) 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 HarmonyInstance.Patch( // desktop input patch so we don't run in menus/gui
typeof(InputModuleMouseKeyboard).GetMethod(nameof(InputModuleMouseKeyboard.UpdateInput)), typeof(CVRInputModule_Keyboard).GetMethod(nameof(CVRInputModule_Keyboard.UpdateInput)),
postfix: new HarmonyLib.HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnUpdateInput), BindingFlags.NonPublic | BindingFlags.Static)) postfix: new HarmonyMethod(typeof(PropUndoButton).GetMethod(nameof(OnUpdateInput),
BindingFlags.NonPublic | BindingFlags.Static))
); );
HarmonyInstance.Patch( // clear redo list on world change HarmonyInstance.Patch( // clear redo list on world change
typeof(CVRWorld).GetMethod(nameof(CVRWorld.ConfigureWorld)), 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(); SetupDefaultAudioClips();
@ -68,7 +77,7 @@ public class PropUndoButton : MelonMod
private void SetupDefaultAudioClips() private void SetupDefaultAudioClips()
{ {
// PropUndo and audio folders do not exist, create them if dont exist yet // 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)) if (!Directory.Exists(path))
{ {
Directory.CreateDirectory(path); Directory.CreateDirectory(path);
@ -77,14 +86,14 @@ public class PropUndoButton : MelonMod
// copy embedded resources to this folder if they do not exist // 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" }; 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)) if (!File.Exists(clipPath))
{ {
// read the clip data from embedded resources // read the clip data from embedded resources
byte[] clipData = null; byte[] clipData = null;
string resourceName = "PropUndoButton.SFX." + clipName; var resourceName = "PropUndoButton.SFX." + clipName;
using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)) using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
{ {
clipData = new byte[stream.Length]; clipData = new byte[stream.Length];
@ -92,7 +101,7 @@ public class PropUndoButton : MelonMod
} }
// write the clip data to the file // 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); 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() private static void OnUpdateInput()
{ {
@ -111,13 +123,8 @@ public class PropUndoButton : MelonMod
if (Input.GetKey(KeyCode.LeftControl)) if (Input.GetKey(KeyCode.LeftControl))
{ {
if (Input.GetKey(KeyCode.LeftShift) && Input.GetKeyDown(KeyCode.Z)) if (Input.GetKey(KeyCode.LeftShift) && Input.GetKeyDown(KeyCode.Z))
{
RedoProp(); 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; if (!EntryEnabled.Value || string.IsNullOrEmpty(instanceId)) return;
var propData = GetPropByInstanceIdAndOwnerId(instanceId); CVRSyncHelper.PropData propData = GetPropByInstanceIdAndOwnerId(instanceId);
if (propData == null) return; if (propData == null) return;
AddDeletedProp(propData); AddDeletedProp(propData);
@ -150,7 +157,7 @@ public class PropUndoButton : MelonMod
if (deletedProps.Count >= redoHistoryLimit) if (deletedProps.Count >= redoHistoryLimit)
deletedProps.RemoveAt(0); deletedProps.RemoveAt(0);
DeletedProp deletedProp = new DeletedProp(propData); DeletedProp deletedProp = new(propData);
deletedProps.Add(deletedProp); deletedProps.Add(deletedProp);
} }
@ -159,7 +166,7 @@ public class PropUndoButton : MelonMod
{ {
if (!EntryEnabled.Value) return true; if (!EntryEnabled.Value) return true;
List<CVRSyncHelper.PropData> propsList = GetAllPropsByOwnerId(); var propsList = GetAllPropsByOwnerId();
if (propsList.Count == 0) if (propsList.Count == 0)
{ {
@ -167,7 +174,7 @@ public class PropUndoButton : MelonMod
return false; 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]; CVRSyncHelper.PropData propData = propsList[i];
SafeDeleteProp(propData); SafeDeleteProp(propData);
@ -181,17 +188,17 @@ public class PropUndoButton : MelonMod
{ {
if (!EntryEnabled.Value) return true; if (!EntryEnabled.Value) return true;
CVRSyncHelper.PropData[] propsList = CVRSyncHelper.Props.ToArray(); var propsList = CVRSyncHelper.Props.ToArray();
if (propsList.Length == 0) if (propsList.Length == 0)
{ {
PlayAudioModule(sfx_warn); PlayAudioModule(sfx_warn);
return false; 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]; CVRSyncHelper.PropData propData = propsList[i];
DeleteProp(propData); SafeDeleteProp(propData);
} }
return false; return false;
@ -199,7 +206,7 @@ public class PropUndoButton : MelonMod
private static void UndoProp() private static void UndoProp()
{ {
var propData = GetLatestPropByOwnerId(); CVRSyncHelper.PropData propData = GetLatestPropByOwnerId();
if (propData == null) if (propData == null)
{ {
PlayAudioModule(sfx_warn); PlayAudioModule(sfx_warn);
@ -211,7 +218,7 @@ public class PropUndoButton : MelonMod
public static void RedoProp() public static void RedoProp()
{ {
int index = deletedProps.Count - 1; var index = deletedProps.Count - 1;
if (index < 0) if (index < 0)
{ {
PlayAudioModule(sfx_warn); PlayAudioModule(sfx_warn);
@ -265,74 +272,44 @@ public class PropUndoButton : MelonMod
public static void PlayAudioModule(string module) public static void PlayAudioModule(string module)
{ {
if (EntryUseSFX.Value) if (EntryUseSFX.Value) InterfaceAudio.PlayModule(module);
{
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();
}
} }
private static void SafeDeleteProp(CVRSyncHelper.PropData propData) private static void SafeDeleteProp(CVRSyncHelper.PropData propData)
{ {
//fixes getting props stuck in limbo state if spawn & delete fast if (propData.Spawnable == null)
if (propData.Spawnable == null && propData.Wrapper != null)
{ {
UnityEngine.Object.DestroyImmediate(propData.Wrapper); // network delete prop manually, then delete prop data
CVRSyncHelper.DeleteMyPropByInstanceIdOverNetwork(propData.InstanceId);
propData.Recycle(); propData.Recycle();
return;
} }
else if (propData.Spawnable != null)
{ // delete prop, prop data, and network delete prop
propData.Spawnable.Delete(); propData.Spawnable.Delete();
} }
else
{
PlayAudioModule(sfx_deny);
}
//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...?
}
private static bool IsPropSpawnAllowed() private static bool IsPropSpawnAllowed()
{ {
return MetaPort.Instance.worldAllowProps return MetaPort.Instance.worldAllowProps
&& MetaPort.Instance.settings.GetSettingsBool("ContentFilterPropsEnabled", false) && MetaPort.Instance.settings.GetSettingsBool("ContentFilterPropsEnabled")
&& NetworkManager.Instance.GameNetwork.ConnectionState == ConnectionState.Connected; && NetworkManager.Instance.GameNetwork.ConnectionState == ConnectionState.Connected;
} }
public static bool IsAtPropLimit(int limit = 20) public static bool IsAtPropLimit(int limit = 21)
{ {
return GetAllPropsByOwnerId().Count >= limit; return GetAllPropsByOwnerId().Count >= limit;
} }
private static CVRSyncHelper.PropData GetPropByInstanceIdAndOwnerId(string instanceId) 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() private static CVRSyncHelper.PropData GetLatestPropByOwnerId()
{ {
// return last prop spawned by owner in CVRSyncHelper.MySpawnedPropInstanceIds
return CVRSyncHelper.Props.LastOrDefault(propData => propData.SpawnedBy == MetaPort.Instance.ownerId); return CVRSyncHelper.Props.LastOrDefault(propData => propData.SpawnedBy == MetaPort.Instance.ownerId);
} }
@ -350,15 +327,24 @@ public class PropUndoButton : MelonMod
public DeletedProp(CVRSyncHelper.PropData propData) public DeletedProp(CVRSyncHelper.PropData propData)
{ {
// Offset spawn height so game can account for it later if (propData.Spawnable == null)
Transform spawnable = propData.Spawnable.transform; {
Vector3 position = spawnable.position; // use original spawn position and rotation / last known position and rotation
position.y -= propData.Spawnable.spawnHeight; 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; // Offset spawn height so game can account for it later
this.position = position; position.y -= propData.Spawnable.spawnHeight;
this.rotation = spawnable.rotation.eulerAngles; }
this.timeDeleted = Time.time;
propGuid = propData.ObjectId;
timeDeleted = Time.time;
} }
} }
} }

View file

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

View file

@ -7,9 +7,8 @@ internal class PlayerSetupPatches
{ {
[HarmonyPostfix] [HarmonyPostfix]
[HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.Start))] [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 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() public override void OnInitializeMelon()
{ {
ApplyPatches(typeof(HarmonyPatches.PlayerSetupPatches)); ApplyPatches(typeof(HarmonyPatches.PlayerSetupPatches));

View file

@ -9,7 +9,7 @@ $cvrExecutable = "ChilloutVR.exe"
$cvrDefaultPath = "E:\temp\CVR_Experimental" $cvrDefaultPath = "E:\temp\CVR_Experimental"
# Array with the dlls to strip # 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 # Array with the mods to grab
$modNames = @("BTKUILib", "BTKSAImmersiveHud", "ActionMenu", "MenuScalePatch", "ChatBox", "ml_prm") $modNames = @("BTKUILib", "BTKSAImmersiveHud", "ActionMenu", "MenuScalePatch", "ChatBox", "ml_prm")