[NAK_CVR_Mods] Unfucked for 2026r182

This commit is contained in:
NotAKid 2026-06-18 22:20:56 -05:00
parent c13dc8375a
commit 281403d68b
209 changed files with 3936 additions and 1122 deletions

View file

@ -0,0 +1,37 @@
using UnityEngine;
namespace NAK.PropLoadingHexagon.Components;
public class LoadingHexagonController : MonoBehaviour
{
public bool IsLoadingCanceled { get; set; }
[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)
{
if (_loadingText.text == text) return;
_loadingText.text = text;
}
public void SetLoadingShape(float value)
=> _hexRenderer.SetBlendShapeWeight(0, value);
}

View file

@ -0,0 +1,37 @@
// using NAK.PropLoadingHexagon.Components;
// using UnityEngine;
//
// namespace NAK.PropLoadingHexagon.Integrations;
//
// // ReSharper disable once ClassNeverInstantiated.Global
// public class TheClapperIntegration
// {
// public static void Init()
// {
// PropLoadingHexagonMod.OnPropPlaceholderCreated += (placeholder) =>
// {
// if (placeholder.TryGetComponent(out LoadingHexagonController loadingHexagon))
// ClappableLoadingHex.Create(loadingHexagon);
// };
// }
// }
//
// public class ClappableLoadingHex : Kafe.TheClapper.Clappable
// {
// private LoadingHexagonController _loadingHexagon;
//
// public override void OnClapped(Vector3 clappablePosition)
// {
// if (_loadingHexagon == null) return;
// _loadingHexagon.IsLoadingCanceled = true;
// Kafe.TheClapper.TheClapper.EmitParticles(clappablePosition, new Color(1f, 1f, 0f), 2f); // why this internal
// }
//
// public static void Create(LoadingHexagonController loadingHexagon)
// {
// GameObject target = loadingHexagon.transform.GetChild(0).gameObject;
// if (!target.gameObject.TryGetComponent(out ClappableLoadingHex clappableHexagon))
// clappableHexagon = target.AddComponent<ClappableLoadingHex>();
// clappableHexagon._loadingHexagon = loadingHexagon;
// }
// }

View 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.HandleSpawnableClicked),
BindingFlags.NonPublic | BindingFlags.Instance),
prefix: new HarmonyMethod(typeof(PropLoadingHexagonMod).GetMethod(nameof(OnHandleSpawnableClicked),
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<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 OnHandleSpawnableClicked(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
}

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<RootNamespace>PropLoadingHexagon</RootNamespace>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Resources\loading_hexagon.assets">
<LogicalName>loading_hexagon.assets</LogicalName>
</EmbeddedResource> </ItemGroup>
</Project>

View file

@ -0,0 +1,32 @@
using NAK.PropLoadingHexagon.Properties;
using MelonLoader;
using System.Reflection;
[assembly: AssemblyVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)]
[assembly: AssemblyTitle(nameof(NAK.PropLoadingHexagon))]
[assembly: AssemblyCompany(AssemblyInfoParams.Author)]
[assembly: AssemblyProduct(nameof(NAK.PropLoadingHexagon))]
[assembly: MelonInfo(
typeof(NAK.PropLoadingHexagon.PropLoadingHexagonMod),
nameof(NAK.PropLoadingHexagon),
AssemblyInfoParams.Version,
AssemblyInfoParams.Author,
downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/PropLoadingHexagon"
)]
[assembly: MelonGame("ChilloutVR", "ChilloutVR")]
[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)]
[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)]
[assembly: MelonColor(255, 246, 25, 99)] // red-pink
[assembly: MelonAuthorColor(255, 158, 21, 32)] // red
[assembly: HarmonyDontPatchAll]
namespace NAK.PropLoadingHexagon.Properties;
internal static class AssemblyInfoParams
{
public const string Version = "1.0.2";
public const string Author = "Exterrata & NotAKidoS";
}

View file

@ -0,0 +1,18 @@
# PropLoadingHexagon
https://github.com/NotAKidoS/NAK_CVR_Mods/assets/37721153/a892c765-71c1-47f3-a781-bdb9b60ba117
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.
---
Here is the block of text where I tell you this mod is not affiliated with or endorsed by ChilloutVR.
https://docs.chilloutvr.net/official/legal/tos/#6-modding-our-game
> This mod is an independent creation not affiliated with, supported by, or approved by ChilloutVR.
> 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 ChilloutVR.

View file

@ -0,0 +1,23 @@
{
"_id": 220,
"name": "PropLoadingHexagon",
"modversion": "1.0.2",
"gameversion": "2025r181",
"loaderversion": "0.7.2",
"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 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/NotAKidoS/NAK_CVR_Mods/releases/download/r48/PropLoadingHexagon.dll",
"sourcelink": "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/PropLoadingHexagon/",
"changelog": "- Rebuilt for CVR 2025r181",
"embedcolor": "#f61963"
}