mirror of
https://github.com/NotAKidoS/NAK_CVR_Mods.git
synced 2025-09-02 06:19:22 +00:00
[PropLoadingHexagon] Initial release
This commit is contained in:
parent
4307d85329
commit
a5e5b05ab8
11 changed files with 372 additions and 325 deletions
|
@ -1,9 +1,11 @@
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace NAK.PropSpawnTweaks.Components;
|
namespace NAK.PropLoadingHexagon.Components;
|
||||||
|
|
||||||
public class PropLoadingHexagon : MonoBehaviour
|
public class LoadingHexagonController : MonoBehaviour
|
||||||
{
|
{
|
||||||
|
public bool IsLoadingCanceled { get; set; }
|
||||||
|
|
||||||
[SerializeField] private SkinnedMeshRenderer _hexRenderer;
|
[SerializeField] private SkinnedMeshRenderer _hexRenderer;
|
||||||
[SerializeField] private TMPro.TextMeshPro _loadingText;
|
[SerializeField] private TMPro.TextMeshPro _loadingText;
|
||||||
[SerializeField] private Transform[] _hexTransforms;
|
[SerializeField] private Transform[] _hexTransforms;
|
||||||
|
@ -25,7 +27,10 @@ public class PropLoadingHexagon : MonoBehaviour
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetLoadingText(string text)
|
public void SetLoadingText(string text)
|
||||||
=> _loadingText.text = text; // SetAllDirty
|
{
|
||||||
|
if (_loadingText.text == text) return;
|
||||||
|
_loadingText.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
public void SetLoadingShape(float value)
|
public void SetLoadingShape(float value)
|
||||||
=> _hexRenderer.SetBlendShapeWeight(0, value);
|
=> _hexRenderer.SetBlendShapeWeight(0, value);
|
|
@ -1,15 +1,15 @@
|
||||||
using NAK.PropSpawnTweaks.Components;
|
using NAK.PropLoadingHexagon.Components;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
namespace NAK.PropSpawnTweaks.Integrations;
|
namespace NAK.PropLoadingHexagon.Integrations;
|
||||||
|
|
||||||
public static class TheClapperIntegration
|
public static class TheClapperIntegration
|
||||||
{
|
{
|
||||||
public static void Init()
|
public static void Init()
|
||||||
{
|
{
|
||||||
PropSpawnTweaksMod.OnPropPlaceholderCreated += (placeholder) =>
|
PropLoadingHexagonMod.OnPropPlaceholderCreated += (placeholder) =>
|
||||||
{
|
{
|
||||||
if (placeholder.TryGetComponent(out PropLoadingHexagon loadingHexagon))
|
if (placeholder.TryGetComponent(out LoadingHexagonController loadingHexagon))
|
||||||
ClappableLoadingHex.Create(loadingHexagon);
|
ClappableLoadingHex.Create(loadingHexagon);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -17,16 +17,16 @@ public static class TheClapperIntegration
|
||||||
|
|
||||||
public class ClappableLoadingHex : Kafe.TheClapper.Clappable
|
public class ClappableLoadingHex : Kafe.TheClapper.Clappable
|
||||||
{
|
{
|
||||||
[SerializeField] private PropLoadingHexagon _loadingHexagon;
|
private LoadingHexagonController _loadingHexagon;
|
||||||
|
|
||||||
public override void OnClapped(Vector3 clappablePosition)
|
public override void OnClapped(Vector3 clappablePosition)
|
||||||
{
|
{
|
||||||
if (_loadingHexagon == null) return;
|
if (_loadingHexagon == null) return;
|
||||||
_loadingHexagon.IsLoadingCanceled = true;
|
_loadingHexagon.IsLoadingCanceled = true;
|
||||||
Kafe.TheClapper.TheClapper.EmitParticles(clappablePosition, new Color(1f, 1f, 0f), 2f);
|
Kafe.TheClapper.TheClapper.EmitParticles(clappablePosition, new Color(1f, 1f, 0f), 2f); // why this internal
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Create(PropLoadingHexagon loadingHexagon)
|
public static void Create(LoadingHexagonController loadingHexagon)
|
||||||
{
|
{
|
||||||
GameObject target = loadingHexagon.gameObject;
|
GameObject target = loadingHexagon.gameObject;
|
||||||
if (!target.gameObject.TryGetComponent(out ClappableLoadingHex clappableHexagon))
|
if (!target.gameObject.TryGetComponent(out ClappableLoadingHex clappableHexagon))
|
313
PropLoadingHexagon/Main.cs
Normal file
313
PropLoadingHexagon/Main.cs
Normal file
|
@ -0,0 +1,313 @@
|
||||||
|
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<GameObject> OnPropPlaceholderCreated;
|
||||||
|
|
||||||
|
private static readonly List<LoadingPropMarker> 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<bool> 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();
|
||||||
|
|
||||||
|
InitializeIntegration("TheClapper", Integrations.TheClapperIntegration.Init);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
private void InitializeIntegration(string modName, Action integrationAction)
|
||||||
|
{
|
||||||
|
if (RegisteredMelons.All(it => it.Info.Name != modName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
LoggerInstance.Msg($"Initializing {modName} integration.");
|
||||||
|
integrationAction.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
#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<GameObject>(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<MeshRenderer>();
|
||||||
|
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<LoadingHexagonController>();
|
||||||
|
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<LoadingHexagonController>();
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
|
@ -2,7 +2,13 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net48</TargetFramework>
|
<TargetFramework>net48</TargetFramework>
|
||||||
|
<RootNamespace>PropSpawnTweaks</RootNamespace>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="TheClapper">
|
||||||
|
<HintPath>..\.ManagedLibs\TheClapper.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Resources\loading_hexagon.assets">
|
<EmbeddedResource Include="Resources\loading_hexagon.assets">
|
||||||
<LogicalName>loading_hexagon.assets</LogicalName>
|
<LogicalName>loading_hexagon.assets</LogicalName>
|
|
@ -1,30 +1,31 @@
|
||||||
using NAK.PropSpawnTweaks.Properties;
|
using NAK.PropLoadingHexagon.Properties;
|
||||||
using MelonLoader;
|
using MelonLoader;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
|
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
|
||||||
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
|
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
|
||||||
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
|
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
|
||||||
[assembly: AssemblyTitle(nameof(NAK.PropSpawnTweaks))]
|
[assembly: AssemblyTitle(nameof(NAK.PropLoadingHexagon))]
|
||||||
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
|
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
|
||||||
[assembly: AssemblyProduct(nameof(NAK.PropSpawnTweaks))]
|
[assembly: AssemblyProduct(nameof(NAK.PropLoadingHexagon))]
|
||||||
|
|
||||||
[assembly: MelonInfo(
|
[assembly: MelonInfo(
|
||||||
typeof(NAK.PropSpawnTweaks.PropSpawnTweaksMod),
|
typeof(NAK.PropLoadingHexagon.PropLoadingHexagonMod),
|
||||||
nameof(NAK.PropSpawnTweaks),
|
nameof(NAK.PropLoadingHexagon),
|
||||||
AssemblyInfoParams.Version,
|
AssemblyInfoParams.Version,
|
||||||
AssemblyInfoParams.Author,
|
AssemblyInfoParams.Author,
|
||||||
downloadLink: "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/PropSpawnTweaks"
|
downloadLink: "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/PropLoadingHexagon"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")]
|
[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")]
|
||||||
[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
|
[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
|
||||||
[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)]
|
[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)]
|
||||||
[assembly: MelonColor(255, 181, 137, 236)]
|
[assembly: MelonColor(255, 246, 25, 99)] // red-pink
|
||||||
[assembly: MelonAuthorColor(255, 158, 21, 32)]
|
[assembly: MelonAuthorColor(255, 158, 21, 32)] // red
|
||||||
|
[assembly: MelonOptionalDependencies("TheClapper")]
|
||||||
[assembly: HarmonyDontPatchAll]
|
[assembly: HarmonyDontPatchAll]
|
||||||
|
|
||||||
namespace NAK.PropSpawnTweaks.Properties;
|
namespace NAK.PropLoadingHexagon.Properties;
|
||||||
internal static class AssemblyInfoParams
|
internal static class AssemblyInfoParams
|
||||||
{
|
{
|
||||||
public const string Version = "1.0.0";
|
public const string Version = "1.0.0";
|
|
@ -1,6 +1,8 @@
|
||||||
# PropSpawnTweaks
|
# PropLoadingHexagon
|
||||||
|
|
||||||
Gives the Drop Prop button more utility by allowing you to drop props in the air, as well as providing a prop loading indicator.
|
Adds a hexagon indicator to downloading props. Indicator is styled to look similar to Portals.
|
||||||
|
|
||||||
|
Can use Delete Mode & The Clapper on loading hexagons to cancel stuck downloads. Setting is provided to display the hexagon for Blocked/Filtered props.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
BIN
PropLoadingHexagon/Resources/loading_hexagon.assets
Normal file
BIN
PropLoadingHexagon/Resources/loading_hexagon.assets
Normal file
Binary file not shown.
23
PropLoadingHexagon/format.json
Normal file
23
PropLoadingHexagon/format.json
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"_id": -1,
|
||||||
|
"name": "PropLoadingHexagon",
|
||||||
|
"modversion": "1.0.0",
|
||||||
|
"gameversion": "2024r175",
|
||||||
|
"loaderversion": "0.6.1",
|
||||||
|
"modtype": "Mod",
|
||||||
|
"author": "Exterrata & NotAKidoS",
|
||||||
|
"description": "Adds a hexagon indicator to downloading props. Indicator is styled to look similar to Portals.\n\nCan use Delete Mode & The Clapper on loading hexagons to cancel stuck downloads. Setting is provided to display the hexagon for Blocked/Filtered props.",
|
||||||
|
"searchtags": [
|
||||||
|
"prop",
|
||||||
|
"spawn",
|
||||||
|
"indicator",
|
||||||
|
"loading"
|
||||||
|
],
|
||||||
|
"requirements": [
|
||||||
|
"None"
|
||||||
|
],
|
||||||
|
"downloadlink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/releases/download/r34/PropLoadingHexagon.dll",
|
||||||
|
"sourcelink": "https://github.com/NotAKidOnSteam/NAK_CVR_Mods/tree/main/PropLoadingHexagon/",
|
||||||
|
"changelog": "- Initial release",
|
||||||
|
"embedcolor": "#f61963"
|
||||||
|
}
|
|
@ -1,235 +0,0 @@
|
||||||
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<LoadingPropHex> Loading_Hex_Pool = new(0, () => new LoadingPropHex());
|
|
||||||
private static readonly List<LoadingPropHex> 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 // i dont think can happen
|
|
||||||
|| string.IsNullOrEmpty(loadingHex.propData.InstanceId) // prop data likely recycled
|
|
||||||
|| (loadingHex.propData.Wrapper != null && loadingHex.propData.Spawnable != null)) // prop has spawned
|
|
||||||
{
|
|
||||||
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<GameObject>(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<MeshRenderer>();
|
|
||||||
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<PropLoadingHexagon>();
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
namespace NAK.PropSpawnTweaks;
|
|
||||||
|
|
||||||
public class ObjectPool<T>
|
|
||||||
{
|
|
||||||
public int Count { get; private set; }
|
|
||||||
|
|
||||||
private readonly Queue<T> _available;
|
|
||||||
private readonly Func<T> _createObjectFunc;
|
|
||||||
|
|
||||||
public ObjectPool(int numPreallocated = 0, Func<T> createObjectFunc = null)
|
|
||||||
{
|
|
||||||
_available = new Queue<T>();
|
|
||||||
_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");
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
{
|
|
||||||
"_id": -1,
|
|
||||||
"name": "PropSpawnTweaks",
|
|
||||||
"modversion": "1.0.0",
|
|
||||||
"gameversion": "2024r175",
|
|
||||||
"loaderversion": "0.6.1",
|
|
||||||
"modtype": "Mod",
|
|
||||||
"author": "Exterrata & 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.",
|
|
||||||
"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"
|
|
||||||
}
|
|
Loading…
Add table
Add a link
Reference in a new issue