diff --git a/LegacyContentMitigation/Components/CameraCallbackLogger.cs b/LegacyContentMitigation/Components/CameraCallbackLogger.cs new file mode 100644 index 0000000..ecd37be --- /dev/null +++ b/LegacyContentMitigation/Components/CameraCallbackLogger.cs @@ -0,0 +1,69 @@ +using System.Collections; +using UnityEngine; +using System.Text; +using MelonLoader; + +namespace NAK.LegacyContentMitigation.Debug; + +public class CameraCallbackLogger +{ + private static CameraCallbackLogger instance; + private readonly List frameCallbacks = new(); + private bool isListening; + private readonly StringBuilder logBuilder = new(); + + public static CameraCallbackLogger Instance => instance ??= new CameraCallbackLogger(); + + private void RegisterCallbacks() + { + Camera.onPreCull += (cam) => LogCallback(cam, "OnPreCull"); + Camera.onPreRender += (cam) => LogCallback(cam, "OnPreRender"); + Camera.onPostRender += (cam) => LogCallback(cam, "OnPostRender"); + } + + private void UnregisterCallbacks() + { + Camera.onPreCull -= (cam) => LogCallback(cam, "OnPreCull"); + Camera.onPreRender -= (cam) => LogCallback(cam, "OnPreRender"); + Camera.onPostRender -= (cam) => LogCallback(cam, "OnPostRender"); + } + + public void LogCameraEvents() + { + MelonCoroutines.Start(LoggingCoroutine()); + } + + private IEnumerator LoggingCoroutine() + { + yield return null; // idk at what point in frame start occurs + + // First frame: Register and listen + RegisterCallbacks(); + isListening = true; + yield return null; + + // Second frame: Log and cleanup + isListening = false; + PrintFrameLog(); + UnregisterCallbacks(); + } + + private void LogCallback(Camera camera, string callbackName) + { + if (!isListening) return; + frameCallbacks.Add($"{camera.name} - {callbackName} (Depth: {camera.depth})"); + } + + private void PrintFrameLog() + { + logBuilder.Clear(); + logBuilder.AppendLine("\nCamera Callbacks for Frame:"); + + foreach (var callback in frameCallbacks) + logBuilder.AppendLine(callback); + + LegacyContentMitigationMod.Logger.Msg(logBuilder.ToString()); + + frameCallbacks.Clear(); + } +} \ No newline at end of file diff --git a/LegacyContentMitigation/Components/FakeMultiPassHack.cs b/LegacyContentMitigation/Components/FakeMultiPassHack.cs new file mode 100644 index 0000000..31924db --- /dev/null +++ b/LegacyContentMitigation/Components/FakeMultiPassHack.cs @@ -0,0 +1,228 @@ +using ABI_RC.Core; +using ABI_RC.Core.Player; +using ABI_RC.Systems.UI; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.XR; + +namespace NAK.LegacyContentMitigation; + +public class FakeMultiPassHack : MonoBehaviour +{ + private static readonly int s_WorldSpaceCameraPos = Shader.PropertyToID("_WorldSpaceCameraPos"); + + public static Action OnMultiPassActiveChanged; + + #region Properties + + public static FakeMultiPassHack Instance { get; set; } + public bool IsActive => IsEnabled && isActiveAndEnabled; + public bool IsEnabled { get; private set; } + public Camera.MonoOrStereoscopicEye RenderingEye { get; private set; } + + #endregion + + #region Private Fields + + private Camera _mainCamera; + private Camera _leftEye; + private Camera _rightEye; + + private GameObject _leftEyeObject; + private GameObject _rightEyeObject; + + private RenderTexture _leftTexture; + private RenderTexture _rightTexture; + + private CommandBuffer _shaderGlobalBuffer; + private CommandBuffer _leftEyeBuffer; + + private int CachedCullingMask; + private bool _isInitialized; + + #endregion + + #region Unity Lifecycle + + private void OnEnable() + { + Camera.onPreRender += OnPreRenderCallback; + if (IsEnabled) _mainCamera.cullingMask = 0; + } + + private void OnDisable() + { + Camera.onPreRender -= OnPreRenderCallback; + if (IsEnabled) _mainCamera.cullingMask = CachedCullingMask; + } + + private void OnDestroy() + { + if (_leftEye != null) RemoveCameraFromWorldTransitionSystem(_leftEye); + if (_rightEye != null) RemoveCameraFromWorldTransitionSystem(_rightEye); + + if (_leftTexture != null) _leftTexture.Release(); + if (_rightTexture != null) _rightTexture.Release(); + _shaderGlobalBuffer?.Release(); + _leftEyeBuffer?.Release(); + + if (_leftEyeObject != null) Destroy(_leftEyeObject); + if (_rightEyeObject != null) Destroy(_rightEyeObject); + + return; + void RemoveCameraFromWorldTransitionSystem(Camera cam) + { + if (cam.TryGetComponent(out WorldTransitionCamera effectCam)) Destroy(effectCam); + WorldTransitionSystem.Cameras.Remove(cam); + } + } + + #endregion + + #region Public Methods + + public void SetMultiPassActive(bool active) + { + if (active == IsEnabled) return; + IsEnabled = active; + + if (active && !_isInitialized) DoInitialSetup(); + + _mainCamera.cullingMask = IsActive ? 0 : CachedCullingMask; + + OnMultiPassActiveChanged?.Invoke(active); + } + + public void OnMainCameraChanged() + { + if (!_isInitialized) return; + + CachedCullingMask = _mainCamera.cullingMask; + if (IsActive) _mainCamera.cullingMask = 0; + + CVRTools.CopyToDestCam(_mainCamera, _leftEye); + CVRTools.CopyToDestCam(_mainCamera, _rightEye); + } + + #endregion + + #region Initialization + + private void DoInitialSetup() + { + _mainCamera = GetComponent(); + CachedCullingMask = _mainCamera.cullingMask; + + _shaderGlobalBuffer = new CommandBuffer(); + _leftEyeBuffer = new CommandBuffer(); + + SetupEye("Left Eye", out _leftEyeObject, out _leftEye, _leftEyeBuffer); + SetupEye("Right Eye", out _rightEyeObject, out _rightEye, null); + + _isInitialized = true; + + return; + void SetupEye(string camName, out GameObject eyeObj, out Camera eye, CommandBuffer eyeBuffer) + { + eyeObj = new GameObject(camName); + eyeObj.transform.parent = transform; + eyeObj.transform.localScale = Vector3.one; + eye = eyeObj.AddComponent(); + eye.enabled = false; + + // Correct camera world space pos (nameplate shader) + eye.AddCommandBuffer(CameraEvent.BeforeDepthTexture, _shaderGlobalBuffer); + eye.AddCommandBuffer(CameraEvent.BeforeForwardOpaque, _shaderGlobalBuffer); + eye.AddCommandBuffer(CameraEvent.BeforeForwardAlpha, _shaderGlobalBuffer); + + // normalizedViewport parameter is ignored, so we cannot draw mesh on right eye :) + if (eyeBuffer != null) + { + eye.AddCommandBuffer(CameraEvent.BeforeDepthTexture, eyeBuffer); + eye.AddCommandBuffer(CameraEvent.BeforeForwardOpaque, eyeBuffer); + eye.AddCommandBuffer(CameraEvent.BeforeForwardAlpha, eyeBuffer); + // notice how we pass fucked param vs UnityEngine.Rendering.XRUtils + eyeBuffer.DrawOcclusionMesh(new RectInt(0, 0, 0, 0)); + } + + WorldTransitionSystem.AddCamera(eye); + CVRTools.CopyToDestCam(_mainCamera, eye); + } + } + + #endregion + + #region Rendering + + private void OnPreRenderCallback(Camera cam) + { + if (!IsEnabled || !_isInitialized) return; + + if (cam.CompareTag("MainCamera")) + { + EnsureRenderTexturesCreated(); + RenderEyePair(); + } + } + + private void EnsureRenderTexturesCreated() + { + int eyeWidth = XRSettings.eyeTextureWidth; + int eyeHeight = XRSettings.eyeTextureHeight; + + bool needsUpdate = _leftTexture == null || _rightTexture == null || + _leftTexture.width != eyeWidth || _leftTexture.height != eyeHeight; + + if (!needsUpdate) return; + + if (_leftTexture != null) _leftTexture.Release(); + if (_rightTexture != null) _rightTexture.Release(); + + _leftTexture = new RenderTexture(eyeWidth, eyeHeight, 24, RenderTextureFormat.ARGBHalf); + _rightTexture = new RenderTexture(eyeWidth, eyeHeight, 24, RenderTextureFormat.ARGBHalf); + } + + private void RenderEyePair() + { + _shaderGlobalBuffer.Clear(); + _shaderGlobalBuffer.SetGlobalVector(s_WorldSpaceCameraPos, _mainCamera.transform.position); + + Camera realVRCamera = PlayerSetup.Instance.vrCam; + + RenderingEye = Camera.MonoOrStereoscopicEye.Left; + PlayerSetup.Instance.vrCam = _leftEye; // so we trigger head hiding + RenderEye(_leftEye, _leftTexture, Camera.StereoscopicEye.Left); + + RenderingEye = Camera.MonoOrStereoscopicEye.Right; + PlayerSetup.Instance.vrCam = _rightEye; // so we trigger head hiding + RenderEye(_rightEye, _rightTexture, Camera.StereoscopicEye.Right); + + RenderingEye = Camera.MonoOrStereoscopicEye.Mono; // bleh + PlayerSetup.Instance.vrCam = realVRCamera; // reset back to real cam + + return; + void RenderEye(Camera eyeCamera, RenderTexture targetTexture, Camera.StereoscopicEye eye) + { + eyeCamera.CopyFrom(_mainCamera); + eyeCamera.targetTexture = targetTexture; + eyeCamera.cullingMask = CachedCullingMask; + eyeCamera.stereoTargetEye = StereoTargetEyeMask.None; + eyeCamera.projectionMatrix = _mainCamera.GetStereoProjectionMatrix(eye); + eyeCamera.worldToCameraMatrix = _mainCamera.GetStereoViewMatrix(eye); + eyeCamera.Render(); + } + } + + private void OnRenderImage(RenderTexture source, RenderTexture destination) + { + if (!IsEnabled || !_isInitialized || _leftTexture == null || _rightTexture == null) + { + Graphics.Blit(source, destination); + return; + } + + Graphics.CopyTexture(_leftTexture, 0, destination, 0); + Graphics.CopyTexture(_rightTexture, 0, destination, 1); + } + #endregion +} \ No newline at end of file diff --git a/LegacyContentMitigation/Integrations/BtkUiAddon.cs b/LegacyContentMitigation/Integrations/BtkUiAddon.cs new file mode 100644 index 0000000..7a11187 --- /dev/null +++ b/LegacyContentMitigation/Integrations/BtkUiAddon.cs @@ -0,0 +1,86 @@ +using ABI_RC.Core.InteractionSystem; +using ABI_RC.Core.Networking.API.Responses; +using ABI.CCK.Components; +using BTKUILib; +using BTKUILib.UIObjects; +using BTKUILib.UIObjects.Components; +using NAK.LegacyContentMitigation.Debug; + +namespace NAK.LegacyContentMitigation.Integrations; + +public static class BtkUiAddon +{ + private static ToggleButton _currentModState; + + public static void Initialize() + { + // Create menu late to ensure we at bottom. + // Doing this cause these settings are "Advanced" & mostly for debugging. + QuickMenuAPI.OnMenuGenerated += SetupCategory; + } + + private static void SetupCategory(CVR_MenuManager _) + { + QuickMenuAPI.OnMenuGenerated -= SetupCategory; + + Category category = QuickMenuAPI.MiscTabPage.AddCategory(ModSettings.LCM_SettingsCategory, ModSettings.ModName, true, true, true); + + ToggleButton autoButton = category.AddMelonToggle(ModSettings.EntryAutoForLegacyWorlds); + autoButton.OnValueUpdated += (_) => + { + if (CVRWorld.CompatibilityVersion == CompatibilityVersions.NotSpi) + QuickMenuAPI.ShowNotice("Legacy World Notice", + "You must reload this World Bundle for Shader Replacement to be undone / applied. " + + "Load into a different world and then rejoin."); + }; + + _currentModState = category.AddToggle(string.Empty, string.Empty, false); + _currentModState.OnValueUpdated += OnCurrentStateToggled; + + Button printCameraCallbacksButton = category.AddButton("DEBUG LOG CAMERAS", + string.Empty, "Records Camera events & logs them next frame. Useful for determining camera render order shenanigans."); + printCameraCallbacksButton.OnPress += () => CameraCallbackLogger.Instance.LogCameraEvents(); + + OnCurrentStateToggled(FakeMultiPassHack.Instance.IsEnabled); + FakeMultiPassHack.OnMultiPassActiveChanged += OnMultiPassActiveChanged; + } + + private static void OnCurrentStateToggled(bool state) + { + if (state) + { + _currentModState.ToggleValue = false; // dont visually update + QuickMenuAPI.ShowConfirm("Legacy Mitigation Warning", + "This will change how the main VR view is rendered and cause a noticeable performance hit. " + + "It is recommended to only enable this within Worlds that require it (Legacy Content/Broken Shaders). " + + "Shader Replacement will not occur for ALL content that is loaded while enabled. " + + "If this World is Legacy and already Shader Replaced, you must enable Auto For Legacy Worlds instead, " + + "load a different World, and then join back.", + () => + { + FakeMultiPassHack.Instance.SetMultiPassActive(true); + OnMultiPassActiveChanged(true); + }); + } + else + { + FakeMultiPassHack.Instance.SetMultiPassActive(false); + OnMultiPassActiveChanged(false); + } + } + + private static void OnMultiPassActiveChanged(bool state) + { + _currentModState.ToggleValue = state; + if (state) + { + _currentModState.ToggleName = "Currently Active"; + _currentModState.ToggleTooltip = "Fake Multi Pass is currently active."; + } + else + { + _currentModState.ToggleName = "Currently Inactive"; + _currentModState.ToggleTooltip = "Fake Multi Pass is inactive."; + } + } +} \ No newline at end of file diff --git a/LegacyContentMitigation/LegacyContentMitigation.csproj b/LegacyContentMitigation/LegacyContentMitigation.csproj new file mode 100644 index 0000000..d8860da --- /dev/null +++ b/LegacyContentMitigation/LegacyContentMitigation.csproj @@ -0,0 +1,14 @@ + + + + net48 + + + + ..\.ManagedLibs\BTKUILib.dll + + + ..\.ManagedLibs\TheClapper.dll + + + diff --git a/LegacyContentMitigation/Main.cs b/LegacyContentMitigation/Main.cs new file mode 100644 index 0000000..8db0910 --- /dev/null +++ b/LegacyContentMitigation/Main.cs @@ -0,0 +1,67 @@ +using ABI_RC.Core.InteractionSystem; +using MelonLoader; +using UnityEngine; + +namespace NAK.LegacyContentMitigation; + +public class LegacyContentMitigationMod : MelonMod +{ + internal static MelonLogger.Instance Logger; + + #region Melon Preferences + + // private static readonly MelonPreferences_Category Category = + // MelonPreferences.CreateCategory(nameof(LegacyContentMitigationMod)); + // + // private static readonly MelonPreferences_Entry EntryEnabled = + // Category.CreateEntry( + // "use_legacy_mitigation", + // true, + // "Enabled", + // description: "Enable legacy content camera hack when in Legacy worlds."); + + #endregion Melon Preferences + + #region Melon Events + + public override void OnInitializeMelon() + { + Logger = LoggerInstance; + + ApplyPatches(typeof(Patches.PlayerSetup_Patches)); // add MultiPassCamera to VR camera + ApplyPatches(typeof(Patches.SceneLoaded_Patches)); // enable / disable in legacy worlds + ApplyPatches(typeof(Patches.CVRWorld_Patches)); // post processing shit + ApplyPatches(typeof(Patches.CVRTools_Patches)); // prevent shader replacement when fix is active + ApplyPatches(typeof(Patches.HeadHiderManager_Patches)); // prevent main cam triggering early head hide + + InitializeIntegration("BTKUILib", Integrations.BtkUiAddon.Initialize); // quick menu options + } + + #endregion Melon Events + + #region Melon Mod Utilities + + private static void InitializeIntegration(string modName, Action integrationAction) + { + if (RegisteredMelons.All(it => it.Info.Name != modName)) + return; + + Logger.Msg($"Initializing {modName} integration."); + integrationAction.Invoke(); + } + + private void ApplyPatches(Type type) + { + try + { + HarmonyInstance.PatchAll(type); + } + catch (Exception e) + { + LoggerInstance.Msg($"Failed while patching {type.Name}!"); + LoggerInstance.Error(e); + } + } + + #endregion Melon Mod Utilities +} \ No newline at end of file diff --git a/LegacyContentMitigation/ModSettings.cs b/LegacyContentMitigation/ModSettings.cs new file mode 100644 index 0000000..0fdef02 --- /dev/null +++ b/LegacyContentMitigation/ModSettings.cs @@ -0,0 +1,24 @@ +using MelonLoader; + +namespace NAK.LegacyContentMitigation; + +internal static class ModSettings +{ + #region Constants + + internal const string ModName = nameof(LegacyContentMitigation); + internal const string LCM_SettingsCategory = "Legacy Content Mitigation"; + + #endregion Constants + + #region Melon Preferences + + private static readonly MelonPreferences_Category Category = + MelonPreferences.CreateCategory(ModName); + + internal static readonly MelonPreferences_Entry EntryAutoForLegacyWorlds = + Category.CreateEntry("auto_for_legacy_worlds", true, + "Auto For Legacy Worlds", description: "Should Legacy View be auto enabled for detected Legacy worlds?"); + + #endregion Melon Preferences +} \ No newline at end of file diff --git a/LegacyContentMitigation/Patches.cs b/LegacyContentMitigation/Patches.cs new file mode 100644 index 0000000..8fb82c5 --- /dev/null +++ b/LegacyContentMitigation/Patches.cs @@ -0,0 +1,125 @@ +using ABI_RC.Core; +using ABI_RC.Core.Base; +using ABI_RC.Core.Base.Jobs; +using ABI_RC.Core.Networking.API.Responses; +using ABI_RC.Core.Player; +using ABI_RC.Core.Player.LocalClone; +using ABI_RC.Core.Player.TransformHider; +using ABI.CCK.Components; +using HarmonyLib; +using UnityEngine; +using UnityEngine.Rendering.PostProcessing; + +namespace NAK.LegacyContentMitigation.Patches; + +internal static class PlayerSetup_Patches +{ + [HarmonyPostfix] + [HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.Start))] + private static void Postfix_PlayerSetup_Start(ref PlayerSetup __instance) + { + FakeMultiPassHack.Instance = __instance.vrCam.AddComponentIfMissing(); + FakeMultiPassHack.Instance.enabled = ModSettings.EntryAutoForLegacyWorlds.Value; + } +} + +internal static class SceneLoaded_Patches +{ + [HarmonyPrefix] + [HarmonyPatch(typeof(SceneLoaded), nameof(SceneLoaded.OnSceneLoadedHandleJob))] + private static void Prefix_SceneLoaded_OnSceneLoadedHandleJob() + { + if (!ModSettings.EntryAutoForLegacyWorlds.Value) + { + LegacyContentMitigationMod.Logger.Msg("LegacyContentMitigationMod is disabled."); + FakeMultiPassHack.Instance.SetMultiPassActive(false); + return; + } + + bool sceneIsNotSpi = CVRWorld.CompatibilityVersion == CompatibilityVersions.NotSpi; + string logText = sceneIsNotSpi + ? "Legacy world detected, enabling legacy content mitigation." + : "Loaded scene is not considered Legacy content. Disabling if active."; + + LegacyContentMitigationMod.Logger.Msg(logText); + FakeMultiPassHack.Instance.SetMultiPassActive(sceneIsNotSpi); + } +} + +internal static class CVRWorld_Patches +{ + // Third Person patches same methods: + // https://github.com/NotAKidoS/NAK_CVR_Mods/blob/3d6b1bbd59d23be19fe3594e104ad26e4ac0adcd/ThirdPerson/Patches.cs#L15-L22 + [HarmonyPriority(Priority.Last)] + [HarmonyPostfix] + [HarmonyPatch(typeof(CVRWorld), nameof(CVRWorld.CopyRefCamValues))] + [HarmonyPatch(typeof(CVRWorld), nameof(CVRWorld.SetDefaultCamValues))] + private static void Postfix_CVRWorld_SetDefaultCamValues(ref CVRWorld __instance) + { + LegacyContentMitigationMod.Logger.Msg("Legacy world camera values updated."); + FakeMultiPassHack.Instance.OnMainCameraChanged(); + } + + [HarmonyPostfix] + [HarmonyPatch(typeof(CVRWorld), nameof(CVRWorld.UpdatePostProcessing))] + private static void Postfix_CVRWorld_UpdatePostProcessing(ref CVRWorld __instance) + { + if (!FakeMultiPassHack.Instance.IsActive) return; + foreach (PostProcessEffectSettings motionBlur in __instance._postProcessingMotionBlurList) + motionBlur.active = false; // force off cause its fucked and no one cares + } +} + +internal static class CVRTools_Patches +{ + [HarmonyPrefix] + [HarmonyPatch(typeof(CVRTools), nameof(CVRTools.ReplaceShaders), typeof(Material), typeof(string))] + private static bool Prefix_CVRTools_ReplaceShaders(Material material, string fallbackShaderName = "") + { + // When in a legacy world with the hack enabled, do not replace shaders + return !FakeMultiPassHack.Instance.IsActive; + } +} + +internal static class HeadHiderManager_Patches +{ + // despite the visual clone not being normally accessible, i fix it cause mod: + // https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/VisualCloneFix + + [HarmonyPrefix] + [HarmonyPatch(typeof(TransformHiderManager), nameof(TransformHiderManager.OnPreRenderCallback))] + [HarmonyPatch(typeof(TransformHiderManager), nameof(TransformHiderManager.OnPreRenderCallback))] + [HarmonyPatch(typeof(LocalCloneManager), nameof(LocalCloneManager.OnPreRenderCallback))] + [HarmonyPatch(typeof(LocalCloneManager), nameof(LocalCloneManager.OnPostRenderCallback))] + private static bool Prefix_HeadHiderManagers_OnRenderCallbacks(Camera cam) + { + if (!FakeMultiPassHack.Instance.IsActive) + return true; // not active, no need + + // dont let real camera trigger head hiding to occur or reset- leave it to the left/right eyes + return !cam.CompareTag("MainCamera"); // we spoof playersetup.activeCam, need check tag + } + + [HarmonyPrefix] + [HarmonyPatch(typeof(TransformHiderManager), nameof(TransformHiderManager.OnPostRenderCallback))] + [HarmonyPatch(typeof(LocalCloneManager), nameof(LocalCloneManager.OnPostRenderCallback))] + private static void Prefix_HeadHiderManagers_OnPostRenderCallback(Camera cam, ref MonoBehaviour __instance) + { + if (!FakeMultiPassHack.Instance.IsActive) return; + + if (FakeMultiPassHack.Instance.RenderingEye == Camera.MonoOrStereoscopicEye.Left) + SetResetAfterRenderFlag(__instance, true); // so right eye mirror sees head + + if (FakeMultiPassHack.Instance.RenderingEye == Camera.MonoOrStereoscopicEye.Right) + SetResetAfterRenderFlag(__instance, !TransformHiderManager.s_UseCloneToCullUi); // dont undo if ui culling + + return; + void SetResetAfterRenderFlag(MonoBehaviour headHiderManager, bool flag) + { + if (headHiderManager is LocalCloneManager localCloneManager) + localCloneManager._resetAfterThisRender = flag; + else if (headHiderManager is TransformHiderManager transformHiderManager) + transformHiderManager._resetAfterThisRender = flag; + } + } +} \ No newline at end of file diff --git a/LegacyContentMitigation/Properties/AssemblyInfo.cs b/LegacyContentMitigation/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..afc8905 --- /dev/null +++ b/LegacyContentMitigation/Properties/AssemblyInfo.cs @@ -0,0 +1,32 @@ +using NAK.LegacyContentMitigation.Properties; +using MelonLoader; +using System.Reflection; + +[assembly: AssemblyVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyTitle(nameof(NAK.LegacyContentMitigation))] +[assembly: AssemblyCompany(AssemblyInfoParams.Author)] +[assembly: AssemblyProduct(nameof(NAK.LegacyContentMitigation))] + +[assembly: MelonInfo( + typeof(NAK.LegacyContentMitigation.LegacyContentMitigationMod), + nameof(NAK.LegacyContentMitigation), + AssemblyInfoParams.Version, + AssemblyInfoParams.Author, + downloadLink: "https://github.com/NotAKidoS/NAK_CVR_Mods/tree/main/LegacyContentMitigation" +)] + +[assembly: MelonGame("Alpha Blend Interactive", "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.LegacyContentMitigation.Properties; +internal static class AssemblyInfoParams +{ + public const string Version = "1.0.0"; + public const string Author = "Exterrata & NotAKidoS"; +} \ No newline at end of file