diff --git a/RelativeSync/Main.cs b/RelativeSync/Main.cs new file mode 100644 index 0000000..7f39255 --- /dev/null +++ b/RelativeSync/Main.cs @@ -0,0 +1,38 @@ +using MelonLoader; +using NAK.RelativeSync.Networking; +using NAK.RelativeSync.Patches; + +namespace NAK.RelativeSync; + +public class RelativeSyncMod : MelonMod +{ + internal static MelonLogger.Instance Logger; + + public override void OnInitializeMelon() + { + Logger = LoggerInstance; + + ModNetwork.Subscribe(); + + ApplyPatches(typeof(NetworkRootDataUpdatePatches)); + + ApplyPatches(typeof(PlayerSetupPatches)); + ApplyPatches(typeof(PuppetMasterPatches)); + + ApplyPatches(typeof(CVRSeatPatches)); + ApplyPatches(typeof(CVRMovementParentPatches)); + } + + private void ApplyPatches(Type type) + { + try + { + HarmonyInstance.PatchAll(type); + } + catch (Exception e) + { + LoggerInstance.Msg($"Failed while patching {type.Name}!"); + LoggerInstance.Error(e); + } + } +} \ No newline at end of file diff --git a/RelativeSync/Networking/ModNetwork.cs b/RelativeSync/Networking/ModNetwork.cs new file mode 100644 index 0000000..c196572 --- /dev/null +++ b/RelativeSync/Networking/ModNetwork.cs @@ -0,0 +1,151 @@ +using ABI_RC.Core.Networking; +using ABI_RC.Systems.ModNetwork; +using DarkRift; +using UnityEngine; + +namespace NAK.RelativeSync.Networking +{ + public static class ModNetwork + { + public static bool Debug_NetworkInbound = false; + public static bool Debug_NetworkOutbound = false; + + private static bool _isSubscribedToModNetwork; + + private struct RelativeSyncData + { + public bool HasSyncedThisData; + public int MarkerHash; + public Vector3 Position; + public Vector3 Rotation; + } + + private static RelativeSyncData _latestRelativeSyncData; + + #region Constants + + private const string ModId = "MelonMod.NAK.RelativeSync"; + + #endregion + + #region Enums + + private enum MessageType : byte + { + SyncPosition = 0, + RelativeSyncStatus = 1 + } + + #endregion + + #region Mod Network Internals + + internal static void Subscribe() + { + ModNetworkManager.Subscribe(ModId, OnMessageReceived); + + _isSubscribedToModNetwork = ModNetworkManager.IsSubscribed(ModId); + if (!_isSubscribedToModNetwork) + Debug.LogError("Failed to subscribe to Mod Network!"); + } + + internal static void SendRelativeSyncUpdate() + { + if (!_isSubscribedToModNetwork) + return; + + if (!_latestRelativeSyncData.HasSyncedThisData) + { + SendMessage(MessageType.SyncPosition, _latestRelativeSyncData.MarkerHash, + _latestRelativeSyncData.Position, _latestRelativeSyncData.Rotation); + _latestRelativeSyncData.HasSyncedThisData = true; + } + } + + private static void SetLatestRelativeSync(int markerHash, Vector3 position, Vector3 rotation) + { + // check if the data has changed + if (_latestRelativeSyncData.MarkerHash == markerHash + && _latestRelativeSyncData.Position == position + && _latestRelativeSyncData.Rotation == rotation) + return; // no need to update + + _latestRelativeSyncData.HasSyncedThisData = false; // reset + _latestRelativeSyncData.MarkerHash = markerHash; + _latestRelativeSyncData.Position = position; + _latestRelativeSyncData.Rotation = rotation; + } + + private static void SendMessage(MessageType messageType, int markerHash, Vector3 position, Vector3 rotation) + { + if (!IsConnectedToGameNetwork()) + return; + + using ModNetworkMessage modMsg = new(ModId); + modMsg.Write((byte)messageType); + modMsg.Write(markerHash); + modMsg.Write(position); + modMsg.Write(rotation); + modMsg.Send(); + } + + private static void OnMessageReceived(ModNetworkMessage msg) + { + msg.Read(out byte msgTypeRaw); + + if (!Enum.IsDefined(typeof(MessageType), msgTypeRaw)) + return; + + switch ((MessageType)msgTypeRaw) + { + case MessageType.SyncPosition: + msg.Read(out int markerHash); + msg.Read(out Vector3 receivedPosition); + msg.Read(out Vector3 receivedRotation); + OnNetworkPositionUpdateReceived(msg.Sender, markerHash, receivedPosition, receivedRotation); + break; + // case MessageType.RelativeSyncStatus: + // msg.Read(out string guidStr); + // msg.Read(out bool isRelativeSync); + // System.Guid guid = new System.Guid(guidStr); + // OnRelativeSyncStatusReceived(msg.Sender, guid, isRelativeSync); + // break; + default: + Debug.LogError($"Invalid message type received from: {msg.Sender}"); + break; + } + } + + #endregion + + #region Public Methods + + public static void SendNetworkPosition(int markerHash, Vector3 newPosition, Vector3 newRotation) + { + SetLatestRelativeSync(markerHash, newPosition, newRotation); + } + + #endregion + + #region Private Methods + + private static bool IsConnectedToGameNetwork() + { + return NetworkManager.Instance != null + && NetworkManager.Instance.GameNetwork != null + && NetworkManager.Instance.GameNetwork.ConnectionState == ConnectionState.Connected; + } + + private static void OnNetworkPositionUpdateReceived(string sender, int markerHash, Vector3 position, Vector3 rotation) + { + RelativeSyncManager.ApplyRelativeSync(sender, markerHash, position, rotation); + } + + private static void OnRelativeSyncStatusReceived(string sender, System.Guid guid, bool isRelativeSync) + { + // todo: implement + } + + #endregion + } +} \ No newline at end of file diff --git a/RelativeSync/Patches.cs b/RelativeSync/Patches.cs new file mode 100644 index 0000000..f145aa0 --- /dev/null +++ b/RelativeSync/Patches.cs @@ -0,0 +1,61 @@ +using ABI_RC.Core.Base; +using ABI_RC.Core.InteractionSystem; +using ABI_RC.Core.Networking.Jobs; +using ABI_RC.Core.Player; +using ABI.CCK.Components; +using HarmonyLib; +using NAK.RelativeSync.Components; +using NAK.RelativeSync.Networking; + +namespace NAK.RelativeSync.Patches; + +internal static class PlayerSetupPatches +{ + [HarmonyPostfix] + [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.Start))] + private static void Postfix_PlayerSetup_Start(ref PlayerSetup __instance) + { + __instance.AddComponentIfMissing(); + } + +} + +internal static class PuppetMasterPatches +{ + [HarmonyPostfix] + [HarmonyPatch(typeof(PuppetMaster), nameof(PuppetMaster.Start))] + private static void Postfix_PuppetMaster_Start(ref PuppetMaster __instance) + { + __instance.AddComponentIfMissing(); + } +} + +internal static class CVRSeatPatches +{ + [HarmonyPostfix] + [HarmonyPatch(typeof(CVRSeat), nameof(CVRSeat.Awake))] + private static void Postfix_CVRSeat_Awake(ref CVRSeat __instance) + { + __instance.AddComponentIfMissing(); + } +} + +internal static class CVRMovementParentPatches +{ + [HarmonyPostfix] + [HarmonyPatch(typeof(CVRMovementParent), nameof(CVRMovementParent.Start))] + private static void Postfix_CVRMovementParent_Start(ref CVRMovementParent __instance) + { + __instance.AddComponentIfMissing(); + } +} + +internal static class NetworkRootDataUpdatePatches +{ + [HarmonyPostfix] + [HarmonyPatch(typeof(NetworkRootDataUpdate), nameof(NetworkRootDataUpdate.Submit))] + private static void Postfix_NetworkRootDataUpdater_Submit() + { + ModNetwork.SendRelativeSyncUpdate(); // Send the relative sync update after the network root data update + } +} \ No newline at end of file diff --git a/RelativeSync/Properties/AssemblyInfo.cs b/RelativeSync/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..a42a018 --- /dev/null +++ b/RelativeSync/Properties/AssemblyInfo.cs @@ -0,0 +1,32 @@ +using NAK.RelativeSync.Properties; +using MelonLoader; +using System.Reflection; + +[assembly: AssemblyVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyTitle(nameof(NAK.RelativeSync))] +[assembly: AssemblyCompany(AssemblyInfoParams.Author)] +[assembly: AssemblyProduct(nameof(NAK.RelativeSync))] + +[assembly: MelonInfo( + typeof(NAK.RelativeSync.RelativeSyncMod), + nameof(NAK.RelativeSync), + AssemblyInfoParams.Version, + AssemblyInfoParams.Author, + downloadLink: "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/RelativeSync" +)] + +[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")] +[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] +[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)] +[assembly: MelonColor(255, 125, 126, 129)] +[assembly: MelonAuthorColor(255, 158, 21, 32)] +[assembly: HarmonyDontPatchAll] + +namespace NAK.RelativeSync.Properties; +internal static class AssemblyInfoParams +{ + public const string Version = "1.0.0"; + public const string Author = "NotAKidoS"; +} \ No newline at end of file diff --git a/RelativeSync/README.md b/RelativeSync/README.md new file mode 100644 index 0000000..3d63c97 --- /dev/null +++ b/RelativeSync/README.md @@ -0,0 +1,14 @@ +# RelativeSync + +Relative sync for Movement Parent & Chairs. Requires both users to have the mod installed. Synced over Mod Network. + +--- + +Here is the block of text where I tell you this mod is not affiliated with or endorsed by ABI. +https://documentation.abinteractive.net/official/legal/tos/#7-modding-our-games + +> This mod is an independent creation not affiliated with, supported by, or approved by Alpha Blend Interactive. + +> Use of this mod is done so at the user's own risk and the creator cannot be held responsible for any issues arising from its use. + +> To the best of my knowledge, I have adhered to the Modding Guidelines established by Alpha Blend Interactive. diff --git a/RelativeSync/RelativeSync.csproj b/RelativeSync/RelativeSync.csproj new file mode 100644 index 0000000..2bdd4b3 --- /dev/null +++ b/RelativeSync/RelativeSync.csproj @@ -0,0 +1,6 @@ + + + + Sprays + + diff --git a/RelativeSync/RelativeSync/Components/RelativeSyncController.cs b/RelativeSync/RelativeSync/Components/RelativeSyncController.cs new file mode 100644 index 0000000..7744eb2 --- /dev/null +++ b/RelativeSync/RelativeSync/Components/RelativeSyncController.cs @@ -0,0 +1,156 @@ +using ABI_RC.Core.Player; +using UnityEngine; + +namespace NAK.RelativeSync.Components; + +[DefaultExecutionOrder(int.MaxValue)] // make sure this runs after NetIKController +public class RelativeSyncController : MonoBehaviour +{ + private static float MaxMagnitude = 750000000000f; + + private string _userId; + private PuppetMaster puppetMaster { get; set; } + private NetIKController netIkController { get; set; } + + // private bool _syncMarkerChangedSinceLastSync; + private RelativeSyncMarker _relativeSyncMarker; + + private RelativeSyncData _relativeSyncData; + private RelativeSyncData _lastSyncData; + + #region Unity Events + + private void Start() + { + puppetMaster = GetComponent(); + netIkController = GetComponent(); + + _userId = puppetMaster._playerDescriptor.ownerId; + RelativeSyncManager.RelativeSyncControllers.Add(_userId, this); + } + + private void OnDestroy() + { + RelativeSyncManager.RelativeSyncControllers.Remove(_userId); + } + + private void LateUpdate() + { + if (puppetMaster._isHidden) + return; + + if (_relativeSyncMarker == null) + return; + + Animator animator = puppetMaster._animator; + if (animator == null) + return; + + Transform avatarTransform = animator.transform; + + Vector3 worldRootPos = avatarTransform.position; + Quaternion worldRootRot = avatarTransform.rotation; + + Vector3 relativeHipPos = default; + Quaternion relativeHipRot = default; + Transform hipTrans = animator.GetBoneTransform(HumanBodyBones.Hips); + if (hipTrans != null) + { + Vector3 hipPos = hipTrans.position; + Quaternion hipRot = hipTrans.rotation; + + relativeHipPos = Quaternion.Inverse(worldRootRot) * (hipPos - worldRootPos); + relativeHipRot = Quaternion.Inverse(worldRootRot) * hipRot; + } + + // todo: this is fucked and idk why, is technically slightly differing sync rates, + // but even reimplementing dynamic tps here didnt fix the jitter + float lerp = netIkController.GetLerpSpeed(); + + Vector3 targetLocalPosition = _relativeSyncData.LocalRootPosition; + Quaternion targetLocalRotation = Quaternion.Euler(_relativeSyncData.LocalRootRotation); + Transform targetTransform = _relativeSyncMarker.transform; + + if (_relativeSyncMarker.ApplyRelativeRotation && _relativeSyncData.LocalRootRotation.sqrMagnitude < MaxMagnitude) + { + Quaternion rotation = targetTransform.rotation; + Quaternion worldRotation = rotation * targetLocalRotation; + Quaternion lastRotation = rotation * Quaternion.Euler(_lastSyncData.LocalRootRotation); + + if (_relativeSyncMarker.OnlyApplyRelativeHeading) + { + Vector3 currentForward = lastRotation * Vector3.forward; + Vector3 targetForward = worldRotation * Vector3.forward; + Vector3 currentWorldUp = avatarTransform.up; // up direction of player before we touch it + + // project forward vectors to the ground plane + currentForward = Vector3.ProjectOnPlane(currentForward, currentWorldUp).normalized; + targetForward = Vector3.ProjectOnPlane(targetForward, currentWorldUp).normalized; + + lastRotation = Quaternion.LookRotation(currentForward, currentWorldUp); + worldRotation = Quaternion.LookRotation(targetForward, currentWorldUp); + } + + transform.rotation = Quaternion.Slerp(lastRotation, worldRotation, lerp); + } + + if (_relativeSyncMarker.ApplyRelativePosition && _relativeSyncData.LocalRootPosition.sqrMagnitude < MaxMagnitude) + { + Vector3 worldPosition = targetTransform.TransformPoint(targetLocalPosition); + transform.position = Vector3.Lerp(targetTransform.TransformPoint(_lastSyncData.LocalRootPosition), worldPosition, lerp); + } + + // negate avatar transform movement + avatarTransform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity); + + // fix hip syncing because it is not relative to root, it is synced in world space -_- + if (hipTrans != null) + { + hipTrans.position = transform.position + transform.rotation * relativeHipPos; + hipTrans.rotation = transform.rotation * relativeHipRot; + } + + _lastSyncData.LocalRootPosition = targetLocalPosition; + _lastSyncData.LocalRootRotation = targetLocalRotation.eulerAngles; + } + + #endregion Unity Events + + #region Public Methods + + public void SetRelativeSyncMarker(RelativeSyncMarker target) + { + if (_relativeSyncMarker == target) + return; + + _relativeSyncMarker = target; + + // calculate relative position and rotation so lerp can smooth it out (hack) + if (_relativeSyncMarker != null) + { + Transform avatarTransform = puppetMaster._animator.transform; + Transform markerTransform = _relativeSyncMarker.transform; + Vector3 localPosition = markerTransform.InverseTransformPoint(avatarTransform.position); + Quaternion localRotation = Quaternion.Inverse(markerTransform.rotation) * avatarTransform.rotation; + + // set last sync data to current position and rotation so we don't lerp from the last marker + _lastSyncData.LocalRootPosition = localPosition; + _lastSyncData.LocalRootRotation = localRotation.eulerAngles; + //Debug.Log($"SetRelativeSyncMarker: {_relativeSyncMarker.name}"); + } + } + + public void SetRelativePositions(Vector3 position, Vector3 rotation) + { + _relativeSyncData.LocalRootPosition = position; + _relativeSyncData.LocalRootRotation = rotation; + } + + #endregion Public Methods + + public struct RelativeSyncData + { + public Vector3 LocalRootPosition; + public Vector3 LocalRootRotation; + } +} \ No newline at end of file diff --git a/RelativeSync/RelativeSync/Components/RelativeSyncMarker.cs b/RelativeSync/RelativeSync/Components/RelativeSyncMarker.cs new file mode 100644 index 0000000..8a3b45c --- /dev/null +++ b/RelativeSync/RelativeSync/Components/RelativeSyncMarker.cs @@ -0,0 +1,50 @@ +using ABI.CCK.Components; +using UnityEngine; + +namespace NAK.RelativeSync.Components; + +public class RelativeSyncMarker : MonoBehaviour +{ + public int pathHash { get; private set; } + + public bool ApplyRelativePosition = true; + public bool ApplyRelativeRotation = true; + public bool OnlyApplyRelativeHeading; + + private void Start() + { + string path = GetGameObjectPath(transform); + pathHash = path.GetHashCode(); + RelativeSyncManager.RelativeSyncTransforms.Add(pathHash, this); + + ConfigureForPotentialMovementParent(); + } + + private void OnDestroy() + { + RelativeSyncManager.RelativeSyncTransforms.Remove(pathHash); + } + + private void ConfigureForPotentialMovementParent() + { + if (!gameObject.TryGetComponent(out CVRMovementParent movementParent)) + return; + + // TODO: a refactor may be needed to handle the orientation mode being animated + + // respect orientation mode & gravity zone + ApplyRelativeRotation = movementParent.orientationMode == CVRMovementParent.OrientationMode.RotateWithParent; + OnlyApplyRelativeHeading = movementParent.GetComponent() == null; + } + + private static string GetGameObjectPath(Transform transform) + { + string path = transform.name; + while (transform.parent != null) + { + transform = transform.parent; + path = transform.name + "/" + path; + } + return path; + } +} \ No newline at end of file diff --git a/RelativeSync/RelativeSync/Components/RelativeSyncMonitor.cs b/RelativeSync/RelativeSync/Components/RelativeSyncMonitor.cs new file mode 100644 index 0000000..0301dd0 --- /dev/null +++ b/RelativeSync/RelativeSync/Components/RelativeSyncMonitor.cs @@ -0,0 +1,82 @@ +using ABI_RC.Core.Player; +using ABI_RC.Systems.Movement; +using NAK.RelativeSync.Networking; +using UnityEngine; + +namespace NAK.RelativeSync.Components; + +[DefaultExecutionOrder(int.MaxValue)] +public class RelativeSyncMonitor : MonoBehaviour +{ + private BetterBetterCharacterController _characterController { get; set; } + + private RelativeSyncMarker _relativeSyncMarker; + private RelativeSyncMarker _lastRelativeSyncMarker; + + private void Start() + { + _characterController = GetComponent(); + } + + private void LateUpdate() + { + if (_characterController == null) + return; + + CheckForRelativeSyncMarker(); + + if (_relativeSyncMarker == null) + { + if (_lastRelativeSyncMarker == null) + return; + + // send empty position and rotation to stop syncing + SendEmptyPositionAndRotation(); + _lastRelativeSyncMarker = null; + return; + } + + _lastRelativeSyncMarker = _relativeSyncMarker; + + SendCurrentPositionAndRotation(); + } + + private void CheckForRelativeSyncMarker() + { + if (_characterController._isSitting && _characterController._lastCvrSeat) + { + RelativeSyncMarker newMarker = _characterController._lastCvrSeat.GetComponent(); + _relativeSyncMarker = newMarker; + return; + } + + if (_characterController._previousMovementParent != null) + { + RelativeSyncMarker newMarker = _characterController._previousMovementParent.GetComponent(); + _relativeSyncMarker = newMarker; + return; + } + + // none found + _relativeSyncMarker = null; + } + + private void SendCurrentPositionAndRotation() + { + // because our syncing is retarded, we need to sync relative from the avatar root... + Transform avatarRoot = PlayerSetup.Instance._avatar.transform; + Vector3 avatarRootPosition = avatarRoot.position; // PlayerSetup.Instance.GetPlayerPosition() + Quaternion avatarRootRotation = avatarRoot.rotation; // PlayerSetup.Instance.GetPlayerRotation() + + Transform markerTransform = _relativeSyncMarker.transform; + Vector3 localPosition = markerTransform.InverseTransformPoint(avatarRootPosition); + Quaternion localRotation = Quaternion.Inverse(markerTransform.rotation) * avatarRootRotation; + + ModNetwork.SendNetworkPosition(_relativeSyncMarker.pathHash, localPosition, localRotation.eulerAngles); + } + + private void SendEmptyPositionAndRotation() + { + ModNetwork.SendNetworkPosition(RelativeSyncManager.NoTarget, Vector3.zero, Vector3.zero); + } +} \ No newline at end of file diff --git a/RelativeSync/RelativeSync/RelativeSyncManager.cs b/RelativeSync/RelativeSync/RelativeSyncManager.cs new file mode 100644 index 0000000..ea5cdc9 --- /dev/null +++ b/RelativeSync/RelativeSync/RelativeSyncManager.cs @@ -0,0 +1,34 @@ +using ABI_RC.Core.Base; +using ABI_RC.Core.Player; +using NAK.RelativeSync.Components; +using UnityEngine; + +namespace NAK.RelativeSync; + +public static class RelativeSyncManager +{ + public const int NoTarget = -1; + + public static readonly Dictionary RelativeSyncTransforms = new(); + public static readonly Dictionary RelativeSyncControllers = new(); + + public static void ApplyRelativeSync(string userId, int target, Vector3 position, Vector3 rotation) + { + if (!RelativeSyncControllers.TryGetValue(userId, out RelativeSyncController controller)) + if (CVRPlayerManager.Instance.GetPlayerPuppetMaster(userId, out PuppetMaster pm)) + controller = pm.AddComponentIfMissing(); + + if (controller == null) + { + RelativeSyncMod.Logger.Error($"Failed to apply relative sync for user {userId}"); + return; + } + + // find target transform + RelativeSyncMarker syncMarker = null; + if (target != NoTarget) RelativeSyncTransforms.TryGetValue(target, out syncMarker); + + controller.SetRelativePositions(position, rotation); + controller.SetRelativeSyncMarker(syncMarker); + } +} \ No newline at end of file diff --git a/RelativeSync/format.json b/RelativeSync/format.json new file mode 100644 index 0000000..8aedbff --- /dev/null +++ b/RelativeSync/format.json @@ -0,0 +1,23 @@ +{ + "_id": -1, + "name": "RelativeSync", + "modversion": "1.0.0", + "gameversion": "2024r175", + "loaderversion": "0.6.1", + "modtype": "Mod", + "author": "NotAKidoS", + "description": "Relative sync for Movement Parent & Chairs. Requires both users to have the mod installed. Synced over Mod Network.", + "searchtags": [ + "relative", + "sync", + "movement", + "chair" + ], + "requirements": [ + "None" + ], + "downloadlink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/releases/download/r26/RelativeSync.dll", + "sourcelink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/RelativeSync/", + "changelog": "- Initial Release", + "embedcolor": "#507e64" +} \ No newline at end of file