[AvatarScaleMod] Test 2: Height Request

Now requests initial height on instance join instead of syncing every 10s.
Reworked inbould & outbound message queue.
Fixed potential race condition where scalefactor could become 0.

A lot of this is still incredibly jank. Just trying to get things working before cleanup.
This commit is contained in:
NotAKidoS 2023-09-20 20:26:38 -05:00
parent d599f57973
commit c7eb5715ba
7 changed files with 440 additions and 146 deletions

View file

@ -1,4 +1,7 @@
using ABI_RC.Core.Player; using ABI_RC.Core.IO;
using ABI_RC.Core.Player;
using ABI_RC.Core.Player.AvatarTracking;
using ABI_RC.Systems.GameEventSystem;
using NAK.AvatarScaleMod.Networking; using NAK.AvatarScaleMod.Networking;
using UnityEngine; using UnityEngine;
@ -8,9 +11,11 @@ public class AvatarScaleManager : MonoBehaviour
{ {
public static AvatarScaleManager Instance; public static AvatarScaleManager Instance;
public bool Setting_PersistantHeight = false;
private Dictionary<string, UniversalAvatarScaler> _networkedScalers; private Dictionary<string, UniversalAvatarScaler> _networkedScalers;
private UniversalAvatarScaler _localAvatarScaler; private UniversalAvatarScaler _localAvatarScaler;
#region Unity Methods #region Unity Methods
private void Awake() private void Awake()
@ -25,6 +30,27 @@ public class AvatarScaleManager : MonoBehaviour
_networkedScalers = new Dictionary<string, UniversalAvatarScaler>(); _networkedScalers = new Dictionary<string, UniversalAvatarScaler>();
} }
private void Start()
{
CVRGameEventSystem.Instance.OnConnected.AddListener(OnInstanceConnected);
//SchedulerSystem.AddJob(new SchedulerSystem.Job(ForceHeightUpdate), 0f, 10f, -1);
}
private void OnDestroy()
{
CVRGameEventSystem.Instance.OnConnected.RemoveListener(OnInstanceConnected);
//SchedulerSystem.RemoveJob(new SchedulerSystem.Job(ForceHeightUpdate));
}
#endregion
#region Game Events
public void OnInstanceConnected(string instanceId)
{
SchedulerSystem.AddJob(ModNetwork.RequestHeightSync, 2f, 1f, 1);
}
#endregion #endregion
#region Local Methods #region Local Methods
@ -33,28 +59,37 @@ public class AvatarScaleManager : MonoBehaviour
{ {
if (playerSetup._avatar == null) if (playerSetup._avatar == null)
return; return;
if (_localAvatarScaler != null)
Destroy(_localAvatarScaler);
_localAvatarScaler = playerSetup._avatar.AddComponent<UniversalAvatarScaler>(); if (_localAvatarScaler == null)
_localAvatarScaler.Initialize(playerSetup._initialAvatarHeight, playerSetup.initialScale); {
_localAvatarScaler = playerSetup.gameObject.AddComponent<UniversalAvatarScaler>();
_localAvatarScaler.Initialize();
}
_localAvatarScaler.OnAvatarInstantiated(playerSetup._avatar, playerSetup._initialAvatarHeight,
playerSetup.initialScale);
if (Setting_PersistantHeight && _localAvatarScaler.IsValid())
SchedulerSystem.AddJob(() => { ModNetwork.SendNetworkHeight(_localAvatarScaler.GetHeight()); }, 0.5f, 0f, 1);
} }
public void OnAvatarDestroyed() public void OnAvatarDestroyed(PlayerSetup playerSetup)
{ {
if (_localAvatarScaler != null) if (_localAvatarScaler != null)
Destroy(_localAvatarScaler); _localAvatarScaler.OnAvatarDestroyed(Setting_PersistantHeight);
if (Setting_PersistantHeight && _localAvatarScaler.IsValid())
SchedulerSystem.AddJob(() => { ModNetwork.SendNetworkHeight(_localAvatarScaler.GetHeight()); }, 0.5f, 0f, 1);
} }
public void SetHeight(float targetHeight) public void SetHeight(float targetHeight)
{ {
if (_localAvatarScaler == null) if (_localAvatarScaler == null)
return; return;
_localAvatarScaler.SetHeight(targetHeight); _localAvatarScaler.SetTargetHeight(targetHeight);
ModNetwork.SendNetworkHeight(targetHeight); ModNetwork.SendNetworkHeight(targetHeight);
// immediately update play space scale // immediately update play space scale
PlayerSetup.Instance.CheckUpdateAvatarScaleToPlaySpaceRelation(); PlayerSetup.Instance.CheckUpdateAvatarScaleToPlaySpaceRelation();
} }
@ -64,49 +99,98 @@ public class AvatarScaleManager : MonoBehaviour
if (_localAvatarScaler != null) if (_localAvatarScaler != null)
_localAvatarScaler.ResetHeight(); _localAvatarScaler.ResetHeight();
} }
public float GetHeight() public float GetHeight()
{ {
return (_localAvatarScaler != null) ? _localAvatarScaler.GetHeight() : -1f; return _localAvatarScaler != null ? _localAvatarScaler.GetHeight() : -1f;
} }
#endregion #endregion
#region Network Methods #region Network Methods
public void OnNetworkAvatarInstantiated(PuppetMaster puppetMaster)
{
if (puppetMaster.avatarObject == null)
return;
string playerId = puppetMaster._playerDescriptor.ownerId;
if (_networkedScalers.ContainsKey(playerId))
_networkedScalers.Remove(playerId);
UniversalAvatarScaler scaler = puppetMaster.avatarObject.AddComponent<UniversalAvatarScaler>();
scaler.Initialize(puppetMaster._initialAvatarHeight, puppetMaster.initialAvatarScale);
_networkedScalers[playerId] = scaler;
}
public void OnNetworkAvatarDestroyed(string playerId)
{
if (_networkedScalers.ContainsKey(playerId))
_networkedScalers.Remove(playerId);
}
public void OnNetworkHeightUpdateReceived(string playerId, float targetHeight)
{
if (_networkedScalers.TryGetValue(playerId, out UniversalAvatarScaler scaler))
scaler.SetHeight(targetHeight);
}
public float GetNetworkHeight(string playerId) public float GetNetworkHeight(string playerId)
{ {
if (_networkedScalers.TryGetValue(playerId, out UniversalAvatarScaler scaler)) if (_networkedScalers.TryGetValue(playerId, out UniversalAvatarScaler scaler))
return scaler.GetHeight(); return scaler.GetHeight();
return -1f; return -1f;
} }
// we will create a Universal Scaler for only users that send a height update
// this is sent at a rate of 10s locally!
internal void OnNetworkHeightUpdateReceived(string playerId, float targetHeight)
{
if (_networkedScalers.TryGetValue(playerId, out UniversalAvatarScaler scaler))
scaler.SetTargetHeight(targetHeight);
else
SetupHeightScalerForNetwork(playerId, targetHeight);
}
internal void OnNetworkAvatarInstantiated(PuppetMaster puppetMaster)
{
var playerId = puppetMaster._playerDescriptor.ownerId;
if (_networkedScalers.TryGetValue(playerId, out UniversalAvatarScaler scaler))
scaler.OnAvatarInstantiated(puppetMaster.avatarObject, puppetMaster._initialAvatarHeight,
puppetMaster.initialAvatarScale);
}
internal void OnNetworkAvatarDestroyed(PuppetMaster puppetMaster)
{
// on disconnect
if (puppetMaster == null || puppetMaster._playerDescriptor == null)
return;
var playerId = puppetMaster._playerDescriptor.ownerId;
if (_networkedScalers.TryGetValue(playerId, out UniversalAvatarScaler scaler))
scaler.OnAvatarDestroyed();
}
internal void RemoveNetworkHeightScaler(string playerId)
{
if (_networkedScalers.ContainsKey(playerId))
{
AvatarScaleMod.Logger.Msg(
$"Removed user height scaler! This is hopefully due to a disconnect or block. : {playerId}");
_networkedScalers.Remove(playerId);
return;
}
AvatarScaleMod.Logger.Msg(
$"Failed to remove a user height scaler! This shouldn't happen. : {playerId}");
}
private void SetupHeightScalerForNetwork(string playerId, float targetHeight)
{
CVRPlayerEntity playerEntity =
CVRPlayerManager.Instance.NetworkPlayers.Find(players => players.Uuid == playerId);
PuppetMaster puppetMaster = playerEntity?.PuppetMaster;
if (playerEntity == null || puppetMaster == null)
{
AvatarScaleMod.Logger.Error(
$"Attempted to set up height scaler for user which does not exist! : {playerId}");
return;
}
AvatarScaleMod.Logger.Msg(
$"Setting up new height scaler for user which has sent a height update! : {playerId}");
if (_networkedScalers.ContainsKey(playerId))
_networkedScalers.Remove(playerId); // ??
UniversalAvatarScaler scaler = puppetMaster.gameObject.AddComponent<UniversalAvatarScaler>();
scaler.Initialize(playerId);
scaler.OnAvatarInstantiated(puppetMaster.avatarObject, puppetMaster._initialAvatarHeight,
puppetMaster.initialAvatarScale);
_networkedScalers[playerId] = scaler;
scaler.SetTargetHeight(targetHeight); // set initial height
}
#endregion #endregion
} }

View file

@ -122,7 +122,7 @@ public class ScaledScaleConstraint
public void Scale(float scaleFactor) public void Scale(float scaleFactor)
{ {
Component.scaleAtRest = InitialScaleAtRest * scaleFactor; Component.scaleAtRest = InitialScaleAtRest * scaleFactor;
Component.scaleOffset = InitialScaleOffset * scaleFactor; // Component.scaleOffset = InitialScaleOffset * scaleFactor;
} }
public void Reset() public void Reset()

View file

@ -1,5 +1,6 @@
using ABI_RC.Core; using ABI_RC.Core;
using ABI_RC.Core.Player; using ABI_RC.Core.Player;
using ABI.CCK.Components;
using NAK.AvatarScaleMod.ScaledComponents; using NAK.AvatarScaleMod.ScaledComponents;
using UnityEngine; using UnityEngine;
using UnityEngine.Animations; using UnityEngine.Animations;
@ -22,6 +23,12 @@ public class UniversalAvatarScaler : MonoBehaviour
#region Variables #region Variables
internal bool requestedInitial;
[NonSerialized]
internal string ownerId;
private Transform _avatarTransform;
private CVRAnimatorManager _animatorManager; private CVRAnimatorManager _animatorManager;
private float _initialHeight; private float _initialHeight;
@ -33,52 +40,106 @@ public class UniversalAvatarScaler : MonoBehaviour
private bool _isLocalAvatar; private bool _isLocalAvatar;
private bool _heightWasUpdated; private bool _heightWasUpdated;
private bool _isAvatarInstantiated;
#endregion #endregion
#region Unity Methods #region Unity Methods
private async void Start()
{
await FindComponentsOfTypeAsync(scalableComponentTypes);
}
private void LateUpdate() private void LateUpdate()
{ {
ScaleAvatarRoot(); // override animation-based scaling ScaleAvatarRoot(); // override animation-based scaling
} }
private void OnDestroy()
{
ClearComponentLists();
if (!_isLocalAvatar) AvatarScaleManager.Instance.RemoveNetworkHeightScaler(ownerId);
}
#endregion #endregion
#region Public Methods #region Public Methods
public void Initialize(float initialHeight, Vector3 initialScale) public void Initialize(string playerId = null)
{ {
_initialHeight = _targetHeight = initialHeight; ownerId = playerId;
_initialScale = initialScale;
_scaleFactor = 1f;
_isLocalAvatar = gameObject.layer == 8; _isLocalAvatar = gameObject.layer == 8;
_animatorManager = _isLocalAvatar _animatorManager = _isLocalAvatar
? GetComponentInParent<PlayerSetup>().animatorManager ? GetComponentInParent<PlayerSetup>().animatorManager
: GetComponentInParent<PuppetMaster>()._animatorManager; : GetComponentInParent<PuppetMaster>()._animatorManager;
_heightWasUpdated = false; _heightWasUpdated = false;
_isAvatarInstantiated = false;
} }
public void SetHeight(float height) public async void OnAvatarInstantiated(GameObject avatarObject, float initialHeight, Vector3 initialScale)
{ {
_targetHeight = Mathf.Clamp(height, MinHeight, MaxHeight); if (avatarObject == null)
{
AvatarScaleMod.Logger.Error("Avatar was somehow null?????");
return;
}
AvatarScaleMod.Logger.Msg($"Avatar Object : {_avatarTransform} : {_avatarTransform == null}");
if (_isAvatarInstantiated) return;
_isAvatarInstantiated = true;
// if we don't have a queued height update, apply initial scaling
if (!_heightWasUpdated)
_targetHeight = initialHeight;
_initialHeight = initialHeight;
_initialScale = initialScale;
_scaleFactor = _targetHeight / _initialHeight; _scaleFactor = _targetHeight / _initialHeight;
_avatarTransform = avatarObject.transform;
await FindComponentsOfTypeAsync(scalableComponentTypes);
ApplyScaling(); // apply queued scaling if avatar was loading
}
public void OnAvatarDestroyed(bool shouldPersist = false)
{
if (!_isAvatarInstantiated) return;
_isAvatarInstantiated = false;
AvatarScaleMod.Logger.Msg($"Destroying Avatar Object : {_avatarTransform} : {_avatarTransform == null}");
_avatarTransform = null;
_heightWasUpdated = shouldPersist;
ClearComponentLists();
}
public void SetTargetHeight(float height)
{
if (Math.Abs(height - _targetHeight) < float.Epsilon)
return;
_targetHeight = Mathf.Clamp(height, MinHeight, MaxHeight);
_heightWasUpdated = true; _heightWasUpdated = true;
if (!_isAvatarInstantiated)
return;
_scaleFactor = _targetHeight / _initialHeight;
ApplyScaling(); ApplyScaling();
} }
public void ResetHeight() public void ResetHeight()
{ {
if (Math.Abs(_initialHeight - _targetHeight) < float.Epsilon)
return;
_targetHeight = _initialHeight; _targetHeight = _initialHeight;
_scaleFactor = 1f;
_heightWasUpdated = true; _heightWasUpdated = true;
if (!_isAvatarInstantiated)
return;
_scaleFactor = 1f;
ApplyScaling(); ApplyScaling();
} }
@ -87,13 +148,21 @@ public class UniversalAvatarScaler : MonoBehaviour
return _targetHeight; return _targetHeight;
} }
public bool IsValid()
{
return _isAvatarInstantiated;
}
#endregion #endregion
#region Private Methods #region Private Methods
private void ScaleAvatarRoot() private void ScaleAvatarRoot()
{ {
transform.localScale = _initialScale * _scaleFactor; if (_avatarTransform == null)
return;
_avatarTransform.localScale = _initialScale * _scaleFactor;
} }
private void UpdateAnimatorParameter() private void UpdateAnimatorParameter()
@ -109,8 +178,9 @@ public class UniversalAvatarScaler : MonoBehaviour
private void ApplyScaling() private void ApplyScaling()
{ {
if (!_heightWasUpdated) if (_avatarTransform == null)
return; return;
_heightWasUpdated = false; _heightWasUpdated = false;
ScaleAvatarRoot(); ScaleAvatarRoot();
@ -138,10 +208,19 @@ public class UniversalAvatarScaler : MonoBehaviour
private readonly List<ScaledPositionConstraint> _scaledPositionConstraints = new List<ScaledPositionConstraint>(); private readonly List<ScaledPositionConstraint> _scaledPositionConstraints = new List<ScaledPositionConstraint>();
private readonly List<ScaledScaleConstraint> _scaledScaleConstraints = new List<ScaledScaleConstraint>(); private readonly List<ScaledScaleConstraint> _scaledScaleConstraints = new List<ScaledScaleConstraint>();
private void ClearComponentLists()
{
_scaledLights.Clear();
_scaledAudioSources.Clear();
_scaledParentConstraints.Clear();
_scaledPositionConstraints.Clear();
_scaledScaleConstraints.Clear();
}
private async Task FindComponentsOfTypeAsync(Type[] types) private async Task FindComponentsOfTypeAsync(Type[] types)
{ {
var tasks = new List<Task>(); var tasks = new List<Task>();
var components = GetComponentsInChildren<Component>(true); var components = _avatarTransform.gameObject.GetComponentsInChildren<Component>(true);
foreach (Component component in components) foreach (Component component in components)
{ {

View file

@ -8,38 +8,53 @@ using Object = UnityEngine.Object;
namespace NAK.AvatarScaleMod.HarmonyPatches; namespace NAK.AvatarScaleMod.HarmonyPatches;
internal class PlayerSetupPatches internal class PlayerSetupPatches
{ {
[HarmonyPostfix] [HarmonyPostfix]
[HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.Start))] [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.Start))]
private static void Postfix_PlayerSetup_Start() private static void Postfix_PlayerSetup_Start()
{ {
try try
{ {
GameObject scaleManager = new(nameof(AvatarScaleManager), typeof(AvatarScaleManager)); GameObject scaleManager = new (nameof(AvatarScaleManager), typeof(AvatarScaleManager));
Object.DontDestroyOnLoad(scaleManager); Object.DontDestroyOnLoad(scaleManager);
} }
catch (Exception e) catch (Exception e)
{ {
AvatarScaleMod.Logger.Error($"Error during the patched method {nameof(Postfix_PlayerSetup_Start)}"); AvatarScaleMod.Logger.Error($"Error during the patched method {nameof(Postfix_PlayerSetup_Start)}");
AvatarScaleMod.Logger.Error(e); AvatarScaleMod.Logger.Error(e);
} }
} }
[HarmonyPostfix] [HarmonyPostfix]
[HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.SetupAvatar))] [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.SetupAvatar))]
private static void Postfix_PlayerSetup_SetupAvatar(ref PlayerSetup __instance) private static void Postfix_PlayerSetup_SetupAvatar(ref PlayerSetup __instance)
{ {
try try
{ {
AvatarScaleManager.Instance.OnAvatarInstantiated(__instance); AvatarScaleManager.Instance.OnAvatarInstantiated(__instance);
} }
catch (Exception e) catch (Exception e)
{ {
AvatarScaleMod.Logger.Error($"Error during the patched method {nameof(Postfix_PlayerSetup_SetupAvatar)}"); AvatarScaleMod.Logger.Error($"Error during the patched method {nameof(Postfix_PlayerSetup_SetupAvatar)}");
AvatarScaleMod.Logger.Error(e); AvatarScaleMod.Logger.Error(e);
} }
} }
}
[HarmonyPostfix]
[HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.ClearAvatar))]
private static void Postfix_PlayerSetup_ClearAvatar(ref PlayerSetup __instance)
{
try
{
AvatarScaleManager.Instance.OnAvatarDestroyed(__instance);
}
catch (Exception e)
{
AvatarScaleMod.Logger.Error($"Error during the patched method {nameof(Postfix_PlayerSetup_ClearAvatar)}");
AvatarScaleMod.Logger.Error(e);
}
}
}
internal class PuppetMasterPatches internal class PuppetMasterPatches
{ {
@ -58,6 +73,22 @@ internal class PuppetMasterPatches
AvatarScaleMod.Logger.Error(e); AvatarScaleMod.Logger.Error(e);
} }
} }
[HarmonyPostfix]
[HarmonyPatch(typeof(PuppetMaster), nameof(PuppetMaster.AvatarDestroyed))]
private static void Postfix_PuppetMaster_AvatarDestroyed(ref PuppetMaster __instance)
{
try
{
AvatarScaleManager.Instance.OnNetworkAvatarDestroyed(__instance);
}
catch (Exception e)
{
AvatarScaleMod.Logger.Error(
$"Error during the patched method {nameof(Postfix_PuppetMaster_AvatarDestroyed)}");
AvatarScaleMod.Logger.Error(e);
}
}
} }
internal class GesturePlaneTestPatches internal class GesturePlaneTestPatches

View file

@ -1,4 +1,5 @@
using MelonLoader; using MelonLoader;
using NAK.AvatarScaleMod.AvatarScaling;
namespace NAK.AvatarScaleMod; namespace NAK.AvatarScaleMod;
@ -8,12 +9,12 @@ internal static class ModSettings
{ {
public static readonly MelonPreferences_Category Category = public static readonly MelonPreferences_Category Category =
MelonPreferences.CreateCategory(nameof(AvatarScaleMod)); MelonPreferences.CreateCategory(nameof(AvatarScaleMod));
public static MelonPreferences_Entry<bool> PersistantHeight;
// AvatarScaleTool supported scaling settings // AvatarScaleTool supported scaling settings
public static readonly MelonPreferences_Entry<bool> EntryEnabled = public static readonly MelonPreferences_Entry<bool> EntryEnabled =
Category.CreateEntry("AvatarScaleTool Scaling", true, description: "Should there be persistant avatar scaling? This only works properly across supported avatars."); Category.CreateEntry("AvatarScaleTool Scaling", true, description: "Should there be persistant avatar scaling? This only works properly across supported avatars.");
public static readonly MelonPreferences_Entry<bool> EntryPersistAnyways =
Category.CreateEntry("Persist From Unsupported", true, description: "Should avatar scale persist even from unsupported avatars?");
// Universal scaling settings (Mod Network, requires others to have mod) // Universal scaling settings (Mod Network, requires others to have mod)
public static readonly MelonPreferences_Entry<bool> EntryUniversalScaling = public static readonly MelonPreferences_Entry<bool> EntryUniversalScaling =
@ -37,7 +38,6 @@ internal static class ModSettings
{ {
EntryEnabled.OnEntryValueChanged.Subscribe(OnEntryEnabledChanged); EntryEnabled.OnEntryValueChanged.Subscribe(OnEntryEnabledChanged);
EntryUseScaleGesture.OnEntryValueChanged.Subscribe(OnEntryUseScaleGestureChanged); EntryUseScaleGesture.OnEntryValueChanged.Subscribe(OnEntryUseScaleGestureChanged);
EntryPersistAnyways.OnEntryValueChanged.Subscribe(OnEntryPersistAnywaysChanged);
EntryUniversalScaling.OnEntryValueChanged.Subscribe(OnEntryUniversalScalingChanged); EntryUniversalScaling.OnEntryValueChanged.Subscribe(OnEntryUniversalScalingChanged);
EntryScaleConstraints.OnEntryValueChanged.Subscribe(OnEntryScaleConstraintsChanged); EntryScaleConstraints.OnEntryValueChanged.Subscribe(OnEntryScaleConstraintsChanged);
} }
@ -52,10 +52,7 @@ internal static class ModSettings
//AvatarScaleGesture.GestureEnabled = newValue; //AvatarScaleGesture.GestureEnabled = newValue;
} }
private static void OnEntryPersistAnywaysChanged(bool oldValue, bool newValue)
{
}
private static void OnEntryUniversalScalingChanged(bool oldValue, bool newValue) private static void OnEntryUniversalScalingChanged(bool oldValue, bool newValue)
{ {
@ -69,7 +66,15 @@ internal static class ModSettings
public static void InitializeModSettings() public static void InitializeModSettings()
{ {
PersistantHeight = Category.CreateEntry("Persistant Height", false, description: "Should the avatar height persist between avatar switches?");
PersistantHeight.OnEntryValueChanged.Subscribe(OnPersistantHeightChanged);
//AvatarScaleManager.UseUniversalScaling = EntryEnabled.Value; //AvatarScaleManager.UseUniversalScaling = EntryEnabled.Value;
//AvatarScaleGesture.GestureEnabled = EntryUseScaleGesture.Value; //AvatarScaleGesture.GestureEnabled = EntryUseScaleGesture.Value;
} }
private static void OnPersistantHeightChanged(bool oldValue, bool newValue)
{
AvatarScaleManager.Instance.Setting_PersistantHeight = newValue;
}
} }

View file

@ -1,10 +1,15 @@
using ABI_RC.Systems.ModNetwork; using ABI_RC.Core.Networking;
using ABI_RC.Systems.ModNetwork;
using DarkRift;
using MelonLoader; using MelonLoader;
using NAK.AvatarScaleMod.AvatarScaling; using NAK.AvatarScaleMod.AvatarScaling;
using UnityEngine; using UnityEngine;
namespace NAK.AvatarScaleMod.Networking; namespace NAK.AvatarScaleMod.Networking;
// overcomplicated, but functional
// a
public static class ModNetwork public static class ModNetwork
{ {
#region Constants #region Constants
@ -14,19 +19,37 @@ public static class ModNetwork
private const float ReceiveRateLimit = 0.2f; private const float ReceiveRateLimit = 0.2f;
private const int MaxWarnings = 2; private const int MaxWarnings = 2;
private const float TimeoutDuration = 10f; private const float TimeoutDuration = 10f;
private class QueuedMessage
{
public MessageType Type { get; set; }
public float Height { get; set; }
public string TargetPlayer { get; set; }
public string Sender { get; set; }
}
#endregion #endregion
#region Private State #region Private State
private static float? OutboundQueue; private static readonly Dictionary<string, QueuedMessage> OutboundQueue = new();
private static float LastSentTime; private static float LastSentTime;
private static readonly Dictionary<string, float> InboundQueue = new Dictionary<string, float>(); private static readonly Dictionary<string, QueuedMessage> InboundQueue = new();
private static readonly Dictionary<string, float> LastReceivedTimes = new Dictionary<string, float>(); private static readonly Dictionary<string, float> LastReceivedTimes = new();
private static readonly Dictionary<string, int> UserWarnings = new Dictionary<string, int>(); private static readonly Dictionary<string, int> UserWarnings = new();
private static readonly Dictionary<string, float> UserTimeouts = new Dictionary<string, float>(); private static readonly Dictionary<string, float> UserTimeouts = new();
#endregion
#region Enums
private enum MessageType : byte
{
SyncHeight = 0, // just send height
RequestHeight = 1 // send height, request height back
}
#endregion #endregion
@ -36,26 +59,63 @@ public static class ModNetwork
{ {
ModNetworkManager.Subscribe(ModId, OnMessageReceived); ModNetworkManager.Subscribe(ModId, OnMessageReceived);
} }
internal static void Update() internal static void Update()
{ {
ProcessOutboundQueue(); ProcessOutboundQueue();
ProcessInboundQueue(); ProcessInboundQueue();
} }
private static void SendMessageToAll(float height) private static void SendMessage(MessageType messageType, float height, string playerId = null)
{ {
using ModNetworkMessage modMsg = new ModNetworkMessage(ModId); if (!IsConnectedToGameNetwork())
modMsg.Write(height); return;
modMsg.Send();
//MelonLogger.Msg($"Sending height: {height}"); if (!string.IsNullOrEmpty(playerId))
{
// to specific user
using ModNetworkMessage modMsg = new(ModId, playerId);
modMsg.Write((byte)messageType);
modMsg.Write(height);
modMsg.Send();
}
else
{
// to all users
using ModNetworkMessage modMsg = new(ModId);
modMsg.Write((byte)messageType);
modMsg.Write(height);
modMsg.Send();
}
var typeDesc = messageType == MessageType.SyncHeight ? "height" : "height request";
MelonLogger.Msg($"Sending {typeDesc}: {height}");
} }
private static void OnMessageReceived(ModNetworkMessage msg) private static void OnMessageReceived(ModNetworkMessage msg)
{ {
msg.Read(out byte msgTypeRaw);
msg.Read(out float receivedHeight); msg.Read(out float receivedHeight);
ProcessReceivedHeight(msg.Sender, receivedHeight);
//MelonLogger.Msg($"Received height from {msg.Sender}: {receivedHeight}"); if (!Enum.IsDefined(typeof(MessageType), msgTypeRaw))
return;
// User is in timeout
if (UserTimeouts.TryGetValue(msg.Sender, out var timeoutEnd) && Time.time < timeoutEnd)
return;
if (IsRateLimited(msg.Sender))
return;
QueuedMessage inboundMessage = new()
{
Type = (MessageType)msgTypeRaw,
Height = receivedHeight,
Sender = msg.Sender
};
InboundQueue[msg.Sender] = inboundMessage;
MelonLogger.Msg($"Received message from {msg.Sender}: {receivedHeight}");
} }
#endregion #endregion
@ -64,7 +124,14 @@ public static class ModNetwork
public static void SendNetworkHeight(float newHeight) public static void SendNetworkHeight(float newHeight)
{ {
OutboundQueue = newHeight; OutboundQueue["global"] = new QueuedMessage { Type = MessageType.SyncHeight, Height = newHeight };
}
public static void RequestHeightSync()
{
var myCurrentHeight = AvatarScaleManager.Instance.GetHeight();
if (myCurrentHeight > 0)
OutboundQueue["global"] = new QueuedMessage { Type = MessageType.RequestHeight, Height = myCurrentHeight };
} }
#endregion #endregion
@ -73,32 +140,27 @@ public static class ModNetwork
private static void ProcessOutboundQueue() private static void ProcessOutboundQueue()
{ {
if (!OutboundQueue.HasValue) if (OutboundQueue.Count == 0 || Time.time - LastSentTime < SendRateLimit)
return; return;
if (!(Time.time - LastSentTime >= SendRateLimit)) foreach (QueuedMessage message in OutboundQueue.Values)
return; SendMessage(message.Type, message.Height, message.TargetPlayer);
SendMessageToAll(OutboundQueue.Value); OutboundQueue.Clear();
LastSentTime = Time.time; LastSentTime = Time.time;
OutboundQueue = null;
} }
#endregion #endregion
#region Inbound Height Queue #region Inbound Height Queue
private static void ProcessReceivedHeight(string userId, float receivedHeight) private static bool IsRateLimited(string userId)
{ {
// User is in timeout
if (UserTimeouts.TryGetValue(userId, out float timeoutEnd) && Time.time < timeoutEnd)
return;
// Rate-limit checking // Rate-limit checking
if (LastReceivedTimes.TryGetValue(userId, out float lastReceivedTime) && if (LastReceivedTimes.TryGetValue(userId, out var lastReceivedTime) &&
Time.time - lastReceivedTime < ReceiveRateLimit) Time.time - lastReceivedTime < ReceiveRateLimit)
{ {
if (UserWarnings.TryGetValue(userId, out int warnings)) if (UserWarnings.TryGetValue(userId, out var warnings))
{ {
warnings++; warnings++;
UserWarnings[userId] = warnings; UserWarnings[userId] = warnings;
@ -107,34 +169,63 @@ public static class ModNetwork
{ {
UserTimeouts[userId] = Time.time + TimeoutDuration; UserTimeouts[userId] = Time.time + TimeoutDuration;
MelonLogger.Msg($"User is sending height updates too fast! Applying 10s timeout... : {userId}"); MelonLogger.Msg($"User is sending height updates too fast! Applying 10s timeout... : {userId}");
return; return true;
} }
} }
else else
{ {
UserWarnings[userId] = 1; UserWarnings[userId] = 1;
} }
}
else return true;
{
LastReceivedTimes[userId] = Time.time;
UserWarnings.Remove(userId); // Reset warnings
// MelonLogger.Msg($"Clearing timeout from user : {userId}");
} }
InboundQueue[userId] = receivedHeight; LastReceivedTimes[userId] = Time.time;
UserWarnings.Remove(userId); // Reset warnings
// MelonLogger.Msg($"Clearing timeout from user : {userId}");
return false;
} }
private static void ProcessInboundQueue() private static void ProcessInboundQueue()
{ {
foreach (var (userId, height) in InboundQueue) foreach (QueuedMessage message in InboundQueue.Values)
{ switch (message.Type)
MelonLogger.Msg($"Applying inbound queued height {height} from : {userId}"); {
AvatarScaleManager.Instance.OnNetworkHeightUpdateReceived(userId, height); case MessageType.RequestHeight:
} {
var myCurrentHeight = AvatarScaleManager.Instance.GetHeight();
if (myCurrentHeight > 0)
OutboundQueue[message.Sender] = new QueuedMessage
{
Type = MessageType.SyncHeight,
Height = myCurrentHeight,
TargetPlayer = message.Sender
};
AvatarScaleManager.Instance.OnNetworkHeightUpdateReceived(message.Sender, message.Height);
break;
}
case MessageType.SyncHeight:
AvatarScaleManager.Instance.OnNetworkHeightUpdateReceived(message.Sender, message.Height);
break;
default:
AvatarScaleMod.Logger.Error($"Invalid message type received from: {message.Sender}");
break;
}
InboundQueue.Clear(); InboundQueue.Clear();
} }
#endregion #endregion
#region Private Methods
private static bool IsConnectedToGameNetwork()
{
return NetworkManager.Instance != null
&& NetworkManager.Instance.GameNetwork != null
&& NetworkManager.Instance.GameNetwork.ConnectionState == ConnectionState.Connected;
}
#endregion
} }

View file

@ -16,22 +16,26 @@ public static class ModNetworkDebugger
if (AvatarScaleManager.Instance == null) if (AvatarScaleManager.Instance == null)
return; return;
float currentHeight = AvatarScaleManager.Instance.GetHeight(); float currentHeight;
const float step = 0.1f; const float step = 0.1f;
if (Input.GetKeyDown(KeyCode.Equals) || Input.GetKeyDown(KeyCode.KeypadPlus)) if (Input.GetKeyDown(KeyCode.Equals) || Input.GetKeyDown(KeyCode.KeypadPlus))
{ {
currentHeight += step; currentHeight = AvatarScaleManager.Instance.GetHeight();
AvatarScaleMod.Logger.Msg($"Networking height: {currentHeight}"); AvatarScaleManager.Instance.SetHeight(currentHeight + step);
currentHeight = AvatarScaleManager.Instance.GetHeight();
ModNetwork.SendNetworkHeight(currentHeight); ModNetwork.SendNetworkHeight(currentHeight);
AvatarScaleManager.Instance.SetHeight(currentHeight); AvatarScaleMod.Logger.Msg($"Networking height: {currentHeight}");
} }
else if (Input.GetKeyDown(KeyCode.Minus) || Input.GetKeyDown(KeyCode.KeypadMinus)) else if (Input.GetKeyDown(KeyCode.Minus) || Input.GetKeyDown(KeyCode.KeypadMinus))
{ {
currentHeight -= step; currentHeight = AvatarScaleManager.Instance.GetHeight();
AvatarScaleMod.Logger.Msg($"Networking height: {currentHeight}"); AvatarScaleManager.Instance.SetHeight(currentHeight - step);
currentHeight = AvatarScaleManager.Instance.GetHeight();
ModNetwork.SendNetworkHeight(currentHeight); ModNetwork.SendNetworkHeight(currentHeight);
AvatarScaleManager.Instance.SetHeight(currentHeight); AvatarScaleMod.Logger.Msg($"Networking height: {currentHeight}");
} }
else if (Input.GetKeyDown(KeyCode.Backspace)) else if (Input.GetKeyDown(KeyCode.Backspace))
{ {