using System.Reflection; using ABI_RC.Core.InteractionSystem; using ABI_RC.Core.IO; using ABI_RC.Core.Player; using ABI_RC.Core.Util; using DarkRift; using HarmonyLib; using MelonLoader; using NAK.PropLoadingHexagon.Components; using UnityEngine; using Object = UnityEngine.Object; namespace NAK.PropLoadingHexagon; public class PropLoadingHexagonMod : MelonMod { internal static Action OnPropPlaceholderCreated; private static readonly List Loading_Hex_List = new(); private static GameObject loadingHexContainer; private static GameObject loadingHexPrefab; #region Melon Preferences private static readonly MelonPreferences_Category Category = MelonPreferences.CreateCategory(nameof(PropLoadingHexagon)); private static readonly MelonPreferences_Entry EntryKeepIndicatorWhenFiltered = Category.CreateEntry("keep_indicator_when_filtered", false, "Keep Loading Hex When Filtered", description: "Keeps the loading hexagon when the prop is filtered."); #endregion Melon Preferences #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(PropLoadingHexagonMod).GetMethod(nameof(OnPlayerSetupStart), BindingFlags.NonPublic | BindingFlags.Static)) ); HarmonyInstance.Patch( // spawn prop placeholder typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.SpawnPropFromNetwork), BindingFlags.Public | BindingFlags.Static), postfix: new HarmonyMethod(typeof(PropLoadingHexagonMod).GetMethod(nameof(OnPropSpawnedFromNetwork), BindingFlags.NonPublic | BindingFlags.Static)) ); HarmonyInstance.Patch( // delete mode on prop placeholder typeof(ControllerRay).GetMethod(nameof(ControllerRay.DeleteSpawnable), BindingFlags.NonPublic | BindingFlags.Instance), prefix: new HarmonyMethod(typeof(PropLoadingHexagonMod).GetMethod(nameof(OnDeleteSpawnableCheck), BindingFlags.NonPublic | BindingFlags.Static)) ); // this luckily is called for all props, not just our own, otherwise we'd be fucked HarmonyInstance.Patch( // check for if prop failed to load typeof(CVRSyncHelper).GetMethod(nameof(CVRSyncHelper.DeleteMyPropByInstanceIdOverNetwork), BindingFlags.Public | BindingFlags.Static), postfix: new HarmonyMethod(typeof(PropLoadingHexagonMod).GetMethod(nameof(OnDeleteMyPropByInstanceIdOverNetwork), BindingFlags.NonPublic | BindingFlags.Static)) ); LoadAssetBundle(); //HandleIntegrations(); } public override void OnUpdate() { if (Loading_Hex_List.Count <= 0) return; for (int i = Loading_Hex_List.Count - 1; i >= 0; i--) { LoadingPropMarker marker = Loading_Hex_List[i]; if (marker.propData == null // i dont think can happen || string.IsNullOrEmpty(marker.propData.InstanceId) // prop data likely recycled || (marker.propData.Wrapper != null && marker.propData.Spawnable != null)) // prop has spawned { marker.Reset(); Loading_Hex_List.RemoveAt(i); return; } if (marker.IsLoadingCanceled) { marker.Cancel(); marker.Reset(); Loading_Hex_List.RemoveAt(i); return; } marker.Update(); } } #endregion Melon Events // #region Integrations // // [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)] // private void HandleIntegrations() // { // if (RegisteredMelons.Any(it => it.Info.Name == "TheClapper")) // { // LoggerInstance.Msg("Initializing TheClapper integration."); // Integrations.TheClapperIntegration.Init(); // } // } // // #endregion Integrations #region Asset Bundle Loading private const string LoadingHexagonAssets = "loading_hexagon.assets"; private const string LoadingHexagonPrefab = "Assets/Mods/PropLoadingHexagon/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"); tmp.sharedMaterial.SetFloat(Shader.PropertyToID("_FadeStartDistance"), 0f); tmp.sharedMaterial.SetFloat(Shader.PropertyToID("_FadeEndDistance"), 0f); 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 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 loadingPropLoadingHexagonPropHex LoadingPropMarker loadingHex = new() { downloadTask = downloadTask }; loadingHex.Initialize(propData); OnPropPlaceholderCreated?.Invoke(loadingHex.loadingObject); // add to list Loading_Hex_List.Add(loadingHex); } private static void OnDeleteSpawnableCheck(ref ControllerRay __instance) { if (!__instance._interactDown) return; // not interacted, no need to check if (PlayerSetup.Instance.GetCurrentPropSelectionMode() != PlayerSetup.PropSelectionMode.Delete) return; // not in delete mode, no need to check LoadingHexagonController propLoadingHex = __instance.hitTransform.GetComponentInParent(); if (propLoadingHex != null) propLoadingHex.IsLoadingCanceled = true; // cancel loading } private static void OnDeleteMyPropByInstanceIdOverNetwork(string instanceId) { // find prop data CVRSyncHelper.PropData propData = CVRSyncHelper.Props.Find(match => match.InstanceId == instanceId); if (propData == null) return; // shouldn't happen // check if shits null to nuke loading hex if (propData.Wrapper != null && propData.Spawnable != null) return; LoadingPropMarker marker = Loading_Hex_List.Find(match => match.propData == propData); if (marker == null) return; if (EntryKeepIndicatorWhenFiltered.Value) { marker.IsLikelyBlocked = true; return; } marker.Cancel(); marker.Reset(); Loading_Hex_List.Remove(marker); } #endregion Harmony Patches #region LoadingPropMarker Class private class LoadingPropMarker { public bool IsLikelyBlocked { get; set; } internal DownloadTask downloadTask; internal CVRSyncHelper.PropData propData; internal GameObject loadingObject; private LoadingHexagonController 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(); float avatarHeight = PlayerSetup.Instance._avatarHeight; Transform hexTransform = loadingObject.transform; hexTransform.localScale = Vector3.one * avatarHeight / 4f; // scale modifier hexTransform.GetChild(0).position = Vector3.up * avatarHeight / 2f; // position modifier Update(); // set initial position and rotation } } public bool IsLoadingCanceled => loadingHexComponent.IsLoadingCanceled; public void Update() { string text; if (!IsLikelyBlocked) { 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.SetLoadingShape(progress); } else { text = "Filtered"; } loadingHexComponent.SetLoadingText(text); loadingObject.transform.SetPositionAndRotation( new Vector3(propData.PositionX, propData.PositionY, propData.PositionZ), Quaternion.Euler(propData.RotationX, propData.RotationY, propData.RotationZ)); } public void Cancel() { CVRDownloadManager.Instance.CancelAttachment(propData.InstanceId); if (propData.Spawnable == null) propData.Recycle(); else propData.Spawnable.Delete(); } public void Reset() { if (loadingObject != null) { Object.Destroy(loadingObject); loadingObject = null; } propData = null; downloadTask = null; loadingObject = null; loadingHexComponent = null; IsLikelyBlocked = false; } } #endregion LoadingPropMarker Class }