diff --git a/NAK_CVR_Mods.sln b/NAK_CVR_Mods.sln index 927e66a..9a1e52e 100644 --- a/NAK_CVR_Mods.sln +++ b/NAK_CVR_Mods.sln @@ -63,6 +63,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OriginShift", "OriginShift\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScrollFlight", "ScrollFlight\ScrollFlight.csproj", "{1B5D7DCB-01A4-4988-8B25-211948AEED76}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Portals", "Portals\Portals.csproj", "{BE9629C2-8461-481C-B267-1B8A1805DCD7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PropSpawnTweaks", "PropSpawnTweaks\PropSpawnTweaks.csproj", "{642A2BC7-C027-4F8F-969C-EF0F867936FD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -189,6 +193,14 @@ Global {1B5D7DCB-01A4-4988-8B25-211948AEED76}.Debug|Any CPU.Build.0 = Debug|Any CPU {1B5D7DCB-01A4-4988-8B25-211948AEED76}.Release|Any CPU.ActiveCfg = Release|Any CPU {1B5D7DCB-01A4-4988-8B25-211948AEED76}.Release|Any CPU.Build.0 = Release|Any CPU + {BE9629C2-8461-481C-B267-1B8A1805DCD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BE9629C2-8461-481C-B267-1B8A1805DCD7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BE9629C2-8461-481C-B267-1B8A1805DCD7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BE9629C2-8461-481C-B267-1B8A1805DCD7}.Release|Any CPU.Build.0 = Release|Any CPU + {642A2BC7-C027-4F8F-969C-EF0F867936FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {642A2BC7-C027-4F8F-969C-EF0F867936FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {642A2BC7-C027-4F8F-969C-EF0F867936FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {642A2BC7-C027-4F8F-969C-EF0F867936FD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/PropSpawnTweaks/Components/PropLoadingHexagon.cs b/PropSpawnTweaks/Components/PropLoadingHexagon.cs new file mode 100644 index 0000000..a05a479 --- /dev/null +++ b/PropSpawnTweaks/Components/PropLoadingHexagon.cs @@ -0,0 +1,32 @@ +using UnityEngine; + +namespace NAK.PropSpawnTweaks.Components; + +public class PropLoadingHexagon : MonoBehaviour +{ + [SerializeField] private SkinnedMeshRenderer _hexRenderer; + [SerializeField] private TMPro.TextMeshPro _loadingText; + [SerializeField] private Transform[] _hexTransforms; + private float _scale; + + private void Update() + { + if (_scale < 1f) + { + _scale += Time.deltaTime * 4f; + transform.GetChild(0).localScale = Vector3.one * _scale; + } + + for (int i = 0; i < 3; i++) + { + // give slightly different rotation to each hexagon + _hexTransforms[i].Rotate(Vector3.up, 30f * Time.deltaTime * (i + 1)); + } + } + + public void SetLoadingText(string text) + => _loadingText.text = text; // SetAllDirty + + public void SetLoadingShape(float value) + => _hexRenderer.SetBlendShapeWeight(0, value); +} \ No newline at end of file diff --git a/PropSpawnTweaks/Main.cs b/PropSpawnTweaks/Main.cs new file mode 100644 index 0000000..e4d48d3 --- /dev/null +++ b/PropSpawnTweaks/Main.cs @@ -0,0 +1,234 @@ +using System.Reflection; +using ABI_RC.Core.IO; +using ABI_RC.Core.Player; +using ABI_RC.Core.Util; +using DarkRift; +using HarmonyLib; +using MelonLoader; +using NAK.PropSpawnTweaks.Components; +using UnityEngine; +using Object = UnityEngine.Object; + +namespace NAK.PropSpawnTweaks; + +public class PropSpawnTweaksMod : MelonMod +{ + private static readonly ObjectPool Loading_Hex_Pool = new(0, () => new LoadingPropHex()); + private static readonly List Loading_Hex_List = new(); + private static GameObject loadingHexContainer; + private static GameObject loadingHexPrefab; + + #region Melon Events + + public override void OnInitializeMelon() + { + HarmonyInstance.Patch( // create prop placeholder container + typeof(PlayerSetup).GetMethod(nameof(PlayerSetup.Start), + BindingFlags.NonPublic | BindingFlags.Instance), + postfix: new HarmonyMethod(typeof(PropSpawnTweaksMod).GetMethod(nameof(OnPlayerSetupStart), + BindingFlags.NonPublic | BindingFlags.Static)) + ); + + HarmonyInstance.Patch( // make drop prop actually usable + typeof(PlayerSetup).GetMethod(nameof(PlayerSetup.DropProp), + BindingFlags.Public | BindingFlags.Instance), + new HarmonyMethod(typeof(PropSpawnTweaksMod).GetMethod(nameof(OnDropProp), + BindingFlags.NonPublic | BindingFlags.Static)) + ); + + HarmonyInstance.Patch( // spawn prop placeholder + typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.SpawnPropFromNetwork), + BindingFlags.Public | BindingFlags.Static), + postfix: new HarmonyMethod(typeof(PropSpawnTweaksMod).GetMethod(nameof(OnPropSpawnedFromNetwork), + BindingFlags.NonPublic | BindingFlags.Static)) + ); + + LoadAssetBundle(); + } + + public override void OnUpdate() + { + if (Loading_Hex_List.Count <= 0) + return; + + for (int i = Loading_Hex_List.Count - 1; i >= 0; i--) + { + LoadingPropHex loadingHex = Loading_Hex_List[i]; + if (loadingHex.propData == null || (loadingHex.propData.Wrapper != null + && loadingHex.propData.Spawnable != null)) + { + loadingHex.Reset(); + Loading_Hex_Pool.Give(loadingHex); + Loading_Hex_List.RemoveAt(i); + return; + } + + loadingHex.Update(); + } + } + + #endregion Melon Events + + #region Asset Bundle Loading + + private const string LoadingHexagonAssets = "loading_hexagon.assets"; + private const string LoadingHexagonPrefab = "Assets/Mods/PropSpawnTweaks/Loading_Hexagon_Root.prefab"; + + private void LoadAssetBundle() + { + LoggerInstance.Msg($"Loading required asset bundle..."); + using Stream resourceStream = MelonAssembly.Assembly.GetManifestResourceStream(LoadingHexagonAssets); + using MemoryStream memoryStream = new(); + if (resourceStream == null) { + LoggerInstance.Error($"Failed to load {LoadingHexagonAssets}!"); + return; + } + + resourceStream.CopyTo(memoryStream); + AssetBundle assetBundle = AssetBundle.LoadFromMemory(memoryStream.ToArray()); + if (assetBundle == null) { + LoggerInstance.Error($"Failed to load {LoadingHexagonAssets}! Asset bundle is null!"); + return; + } + + loadingHexPrefab = assetBundle.LoadAsset(LoadingHexagonPrefab); + if (loadingHexPrefab == null) { + LoggerInstance.Error($"Failed to load {LoadingHexagonPrefab}! Prefab is null!"); + return; + } + + // modify prefab so nameplate billboard tmp shader is used + MeshRenderer tmp = loadingHexPrefab.GetComponentInChildren(); + tmp.sharedMaterial.shader = Shader.Find("Alpha Blend Interactive/TextMeshPro/Mobile/Distance Field-BillboardFacing"); + + LoggerInstance.Msg("Asset bundle successfully loaded!"); + } + + #endregion Asset Bundle Loading + + #region Harmony Patches + + private static void OnPlayerSetupStart() + { + if (loadingHexContainer != null) return; + loadingHexContainer = new GameObject("NAK.LoadingHexContainer"); + Object.DontDestroyOnLoad(loadingHexContainer); + } + + private static bool OnDropProp(string propGuid, ref PlayerSetup __instance) + { + Vector3 position = __instance.activeCam.transform.position + __instance.GetPlayerForward() * 1.5f; // 1f -> 1.5f + + if (Physics.Raycast(position, + __instance.CharacterController.GetGravityDirection(), // align with gravity, not player up + out RaycastHit raycastHit, 4f, __instance.dropPlacementMask)) + { + // native method passes false, so DropProp doesn't align with gravity :) + CVRSyncHelper.SpawnProp(propGuid, raycastHit.point.x, raycastHit.point.y, raycastHit.point.z, true); + return false; + } + + // unlike original, we will still spawn prop even if raycast fails, giving the method actual utility :3 + + // hack- we want to align with *our* rotation, not affecting gravity + Vector3 ogGravity = __instance.CharacterController.GetGravityDirection(); + __instance.CharacterController.gravity = -__instance.transform.up; // align with our rotation + + // spawn prop with useTargetLocationGravity false, so it pulls our gravity dir we've modified + CVRSyncHelper.SpawnProp(propGuid, position.x, position.y, position.z, false); + + __instance.CharacterController.gravity = ogGravity; // restore gravity + return false; + } + + private static void OnPropSpawnedFromNetwork(Message message) + { + // thank + // https://feedback.abinteractive.net/p/gameeventsystem-spawnable-onload-is-kinda-useless + + using DarkRiftReader reader = message.GetReader(); + var assetId = reader.ReadString(); + var instanceId = reader.ReadString(); + CVRSyncHelper.PropData propData = CVRSyncHelper.Props.Find(match => match.InstanceId == instanceId); + if (propData == null) + return; // props blocked by filter or player blocks, or just broken + + if (!CVRDownloadManager.Instance._downloadTasks.TryGetValue(assetId, out DownloadTask downloadTask)) + return; // no download task, no prop placeholder + + // create loading hex + LoadingPropHex loadingHex = Loading_Hex_Pool.Take(); + loadingHex.downloadTask = downloadTask; + loadingHex.Initialize(propData); + + // add to list + Loading_Hex_List.Add(loadingHex); + } + + #endregion Harmony Patches + + #region LoadingPropHex Class + + private class LoadingPropHex + { + public DownloadTask downloadTask; + public CVRSyncHelper.PropData propData; + private GameObject loadingObject; + private PropLoadingHexagon loadingHexComponent; + + // ReSharper disable once ParameterHidesMember + public void Initialize(CVRSyncHelper.PropData propData) + { + this.propData = propData; + if (loadingObject == null) + { + loadingObject = Object.Instantiate(loadingHexPrefab, Vector3.zero, Quaternion.identity, loadingHexContainer.transform); + loadingHexComponent = loadingObject.GetComponent(); + + Update(); // set initial position + + float avatarHeight = PlayerSetup.Instance._avatarHeight; + Transform hexTransform = loadingObject.transform; + hexTransform.localScale = Vector3.one * avatarHeight / 4f; // scale modifier + hexTransform.GetChild(0).localPosition = Vector3.up * avatarHeight * 2f; // position modifier + } + } + + public void Update() + { + string text; + float progress = downloadTask.Progress; + + if (downloadTask == null + || downloadTask.Status == DownloadTask.ExecutionStatus.Complete + || downloadTask.Progress >= 100f) + text = "LOADING"; + else if (downloadTask.Status == DownloadTask.ExecutionStatus.Failed) + text = "ERROR"; + else + text = $"{progress} %"; + + loadingHexComponent.SetLoadingText(text); + loadingHexComponent.SetLoadingShape(progress); + loadingObject.transform.SetPositionAndRotation( + new Vector3(propData.PositionX, propData.PositionY, propData.PositionZ), + Quaternion.Euler(propData.RotationX, propData.RotationY, propData.RotationZ)); + } + + public void Reset() + { + if (loadingObject != null) + { + Object.Destroy(loadingObject); + loadingObject = null; + } + + propData = null; + downloadTask = null; + loadingObject = null; + loadingHexComponent = null; + } + } + + #endregion LoadingPropHex Class +} \ No newline at end of file diff --git a/PropSpawnTweaks/ObjectPool.cs b/PropSpawnTweaks/ObjectPool.cs new file mode 100644 index 0000000..d8631eb --- /dev/null +++ b/PropSpawnTweaks/ObjectPool.cs @@ -0,0 +1,45 @@ +namespace NAK.PropSpawnTweaks; + +public class ObjectPool +{ + public int Count { get; private set; } + + private readonly Queue _available; + private readonly Func _createObjectFunc; + + public ObjectPool(int numPreallocated = 0, Func createObjectFunc = null) + { + _available = new Queue(); + _createObjectFunc = createObjectFunc; + + if (numPreallocated > 0) Give(MakeObject()); + } + + public T Take() + { + T t; + if (_available.Count > 0) + { + t = _available.Dequeue(); + Count--; + } + else + { + t = MakeObject(); + } + + return t; + } + + public void Give(T t) + { + _available.Enqueue(t); + Count++; + } + + private T MakeObject() + { + if (_createObjectFunc != null) return _createObjectFunc(); + throw new InvalidOperationException("Cannot automatically create new objects without a factory method"); + } +} \ No newline at end of file diff --git a/PropSpawnTweaks/PropSpawnTweaks.csproj b/PropSpawnTweaks/PropSpawnTweaks.csproj new file mode 100644 index 0000000..8c97d06 --- /dev/null +++ b/PropSpawnTweaks/PropSpawnTweaks.csproj @@ -0,0 +1,10 @@ + + + + net48 + + + + loading_hexagon.assets + + diff --git a/PropSpawnTweaks/Properties/AssemblyInfo.cs b/PropSpawnTweaks/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..c79e2da --- /dev/null +++ b/PropSpawnTweaks/Properties/AssemblyInfo.cs @@ -0,0 +1,32 @@ +using NAK.PropSpawnTweaks.Properties; +using MelonLoader; +using System.Reflection; + +[assembly: AssemblyVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyTitle(nameof(NAK.PropSpawnTweaks))] +[assembly: AssemblyCompany(AssemblyInfoParams.Author)] +[assembly: AssemblyProduct(nameof(NAK.PropSpawnTweaks))] + +[assembly: MelonInfo( + typeof(NAK.PropSpawnTweaks.PropSpawnTweaksMod), + nameof(NAK.PropSpawnTweaks), + AssemblyInfoParams.Version, + AssemblyInfoParams.Author, + downloadLink: "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/PropSpawnTweaks" +)] + +[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")] +[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] +[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)] +[assembly: MelonColor(255, 181, 137, 236)] +[assembly: MelonAuthorColor(255, 158, 21, 32)] +[assembly: HarmonyDontPatchAll] + +namespace NAK.PropSpawnTweaks.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/PropSpawnTweaks/README.md b/PropSpawnTweaks/README.md new file mode 100644 index 0000000..bf0dac2 --- /dev/null +++ b/PropSpawnTweaks/README.md @@ -0,0 +1,14 @@ +# PropSpawnTweaks + +Gives the Drop Prop button more utility by allowing you to drop props in the air, as well as providing a prop loading indicator. + +--- + +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/PropSpawnTweaks/Resources/loading_hexagon.assets b/PropSpawnTweaks/Resources/loading_hexagon.assets new file mode 100644 index 0000000..a801b8d Binary files /dev/null and b/PropSpawnTweaks/Resources/loading_hexagon.assets differ diff --git a/PropSpawnTweaks/format.json b/PropSpawnTweaks/format.json new file mode 100644 index 0000000..513c44f --- /dev/null +++ b/PropSpawnTweaks/format.json @@ -0,0 +1,23 @@ +{ + "_id": -1, + "name": "PropSpawnTweaks", + "modversion": "1.0.0", + "gameversion": "2024r175", + "loaderversion": "0.6.1", + "modtype": "Mod", + "author": "NotAKidoS", + "description": "Gives the Drop Prop button more utility by allowing you to drop props in the air, as well as providing a prop loading indicator.\n", + "searchtags": [ + "prop", + "spawn", + "indicator", + "loading", + ], + "requirements": [ + "None" + ], + "downloadlink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/releases/download/r26/PropSpawnTweaks.dll", + "sourcelink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/PropSpawnTweaks/", + "changelog": "- Initial Release", + "embedcolor": "#b589ec" +} \ No newline at end of file