diff --git a/Blackout/AssetHandler.cs b/Blackout/AssetHandler.cs new file mode 100644 index 0000000..db52492 --- /dev/null +++ b/Blackout/AssetHandler.cs @@ -0,0 +1,95 @@ +using System.Reflection; +using UnityEngine; + +namespace NAK.Melons.Blackout; + +/* + + Kindly stolen from SDraw's leap motion mod. (MIT) + https://github.com/SDraw/ml_mods_cvr/blob/master/ml_lme/AssetsHandler.cs + + *thank u sdraw, i wont be murderer now* + +*/ + + +static class AssetsHandler +{ + static readonly List ms_assets = new List() + { + "blackout_controller.asset" + }; + + static Dictionary ms_loadedAssets = new Dictionary(); + static Dictionary ms_loadedObjects = new Dictionary(); + + public static void Load() + { + Assembly l_assembly = Assembly.GetExecutingAssembly(); + string l_assemblyName = l_assembly.GetName().Name; + + foreach (string l_assetName in ms_assets) + { + try + { + Stream l_assetStream = l_assembly.GetManifestResourceStream(l_assemblyName + ".resources." + l_assetName); + if (l_assetStream != null) + { + MemoryStream l_memorySteam = new MemoryStream((int)l_assetStream.Length); + l_assetStream.CopyTo(l_memorySteam); + AssetBundle l_assetBundle = AssetBundle.LoadFromMemory(l_memorySteam.ToArray(), 0); + if (l_assetBundle != null) + { + l_assetBundle.hideFlags |= HideFlags.DontUnloadUnusedAsset; + ms_loadedAssets.Add(l_assetName, l_assetBundle); + } + else + MelonLoader.MelonLogger.Warning("Unable to load bundled '" + l_assetName + "' asset"); + } + else + MelonLoader.MelonLogger.Warning("Unable to get bundled '" + l_assetName + "' asset stream"); + } + catch (System.Exception e) + { + MelonLoader.MelonLogger.Warning("Unable to load bundled '" + l_assetName + "' asset, reason: " + e.Message); + } + } + } + + public static GameObject GetAsset(string p_name) + { + GameObject l_result = null; + if (ms_loadedObjects.ContainsKey(p_name)) + { + l_result = UnityEngine.Object.Instantiate(ms_loadedObjects[p_name]); + l_result.SetActive(true); + l_result.hideFlags |= HideFlags.DontUnloadUnusedAsset; + } + else + { + foreach (var l_pair in ms_loadedAssets) + { + if (l_pair.Value.Contains(p_name)) + { + GameObject l_bundledObject = (GameObject)l_pair.Value.LoadAsset(p_name, typeof(GameObject)); + if (l_bundledObject != null) + { + ms_loadedObjects.Add(p_name, l_bundledObject); + l_result = UnityEngine.Object.Instantiate(l_bundledObject); + l_result.SetActive(true); + l_result.hideFlags |= HideFlags.DontUnloadUnusedAsset; + } + break; + } + } + } + return l_result; + } + + public static void Unload() + { + foreach (var l_pair in ms_loadedAssets) + UnityEngine.Object.Destroy(l_pair.Value); + ms_loadedAssets.Clear(); + } +} \ No newline at end of file diff --git a/Blackout/Blackout.csproj b/Blackout/Blackout.csproj new file mode 100644 index 0000000..52398a9 --- /dev/null +++ b/Blackout/Blackout.csproj @@ -0,0 +1,84 @@ + + + + + net472 + enable + latest + false + + + + + + + + + + + + + C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\MelonLoader\0Harmony.dll + + + C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\Assembly-CSharp.dll + + + ..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\Assembly-CSharp-firstpass.dll + + + ..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\ChilloutVR\Mods\BTKUILib.dll + + + ..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\Cohtml.Runtime.dll + + + ..\..\Giamoz\Giamoz\bin\Debug\net472\Giamoz.dll + + + C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\MelonLoader\MelonLoader.dll + + + C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\SteamVR.dll + + + ..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\ChilloutVR\Mods\UIExpansionKit.dll + + + ..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\UnityEngine.AnimationModule.dll + + + ..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\UnityEngine.AssetBundleModule.dll + + + C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\UnityEngine.CoreModule.dll + + + C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\UnityEngine.InputLegacyModule.dll + + + ..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\UnityEngine.PhysicsModule.dll + + + + + + True + True + Resource1.resx + + + + + + ResXFileCodeGenerator + Resource1.Designer.cs + + + + + + + + + diff --git a/Blackout/Blackout.sln b/Blackout/Blackout.sln new file mode 100644 index 0000000..4433874 --- /dev/null +++ b/Blackout/Blackout.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.2.32630.192 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blackout", "Blackout.csproj", "{1D5ABEE5-ACFA-4B0C-9195-968FA3DFEECD}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {1D5ABEE5-ACFA-4B0C-9195-968FA3DFEECD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D5ABEE5-ACFA-4B0C-9195-968FA3DFEECD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D5ABEE5-ACFA-4B0C-9195-968FA3DFEECD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D5ABEE5-ACFA-4B0C-9195-968FA3DFEECD}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {698498B4-20BF-45AF-A7A3-2F8091D6AE7E} + EndGlobalSection +EndGlobal diff --git a/Blackout/BlackoutController.cs b/Blackout/BlackoutController.cs new file mode 100644 index 0000000..790024c --- /dev/null +++ b/Blackout/BlackoutController.cs @@ -0,0 +1,323 @@ +using ABI_RC.Core.Player; +using ABI_RC.Core.Savior; +using ABI_RC.Core.UI; +using MelonLoader; +using System.Text; +using UnityEngine; + +namespace NAK.Melons.Blackout; + +/* + + Functionality heavily inspired by VRSleeper on Booth: https://booth.pm/ja/items/2151940 + + There are three states of "blackout": + + 0 - Awake (no effect) + 1 - Drowsy (partial effect) + 2 - Sleep (full effect) + + After staying still for DrowsyModeTimer (minutes), you enter DrowsyMode. + This mode dims the screen to your selected dimming strength. + After continuing to stay still for SleepModeTimer (seconds), you enter SleepMode. + This mode overrenders mostly everything with black. + + Slight movement while in SleepMode will place you in DrowsyMode until SleepModeTimer is reached again. + Hard movement once entering DrowsyMode will fully wake you and return complete vision. + +*/ + +public class BlackoutController : MonoBehaviour +{ + public static BlackoutController Instance; + + // The current state of the player's consciousness. + public BlackoutState CurrentState = BlackoutState.Awake; + + // Should the states automatically change based on time? + public bool AutomaticStateChange = true; + // Should the sleep state be automatically transitioned to? Some may prefer drowsy state only due to dimming. + public bool AutoSleepState = true; + + // The minimum amount of movement required to partially restore vision. + public float drowsyThreshold = 2f; + // The minimum amount of movement required to fully restore vision. + public float wakeThreshold = 4f; + + // The amount of time the player must remain still to enter drowsy state (in minutes). + public float DrowsyModeTimer = 3f; + // The amount of time the player must remain in drowsy state before entering sleep state (in seconds). + public float SleepModeTimer = 10f; + + // The amount by which DrowsyMode affects the screen. + public float DrowsyDimStrength = 0.6f; + // Should DrowsyDimStrength be affected by velocity? + public bool DrowsyVelocityMultiplier = true; + + // Whether to display HUD messages. + public bool HudMessages = true; + + // Whether to lower the frame rate while in sleep mode. + public bool DropFPSOnSleep = false; + + // The available states of consciousness. + public enum BlackoutState + { + Awake = 0, + Drowsy, + Sleeping, + } + + private Camera activeModeCam; + private Vector3 headVelocity = Vector3.zero; + private Vector3 lastHeadPos = Vector3.zero; + private float curTime = 0f; + private float lastAwakeTime = 0f; + private Animator blackoutAnimator; + private int targetFPS; + + public void ChangeBlackoutStateFromInt(int state) => ChangeBlackoutState((BlackoutState)state); + + // Changes the player's state of consciousness. + public void ChangeBlackoutState(BlackoutState newState) + { + if (!blackoutAnimator) return; + if (newState == CurrentState) return; + + lastAwakeTime = curTime; + + // Update the blackout animator based on the new state. + switch (newState) + { + case BlackoutState.Awake: + blackoutAnimator.SetBool("BlackoutState.Drowsy", false); + blackoutAnimator.SetBool("BlackoutState.Sleeping", false); + drowsyMagnitude = 0f; + break; + case BlackoutState.Drowsy: + blackoutAnimator.SetBool("BlackoutState.Drowsy", true); + blackoutAnimator.SetBool("BlackoutState.Sleeping", false); + drowsyMagnitude = 0f; + break; + case BlackoutState.Sleeping: + blackoutAnimator.SetBool("BlackoutState.Drowsy", false); + blackoutAnimator.SetBool("BlackoutState.Sleeping", true); + drowsyMagnitude = 1f; + break; + default: + break; + } + + // Update the current state and send a HUD message if enabled. + BlackoutState prevState = CurrentState; + CurrentState = newState; + SendHUDMessage($"Exiting {prevState} and entering {newState} state."); + ChangeTargetFPS(); + } + + public void AdjustDrowsyDimStrength(float multiplier = 1f) + { + blackoutAnimator.SetFloat("BlackoutSetting.DrowsyStrength", DrowsyDimStrength * multiplier); + } + + // Initialize the BlackoutInstance object. + void Start() + { + Instance = this; + + // Get the blackout asset and instantiate it. + GameObject blackoutAsset = AssetsHandler.GetAsset("Assets/BundledAssets/Blackout/Blackout.prefab"); + GameObject blackoutGO = Instantiate(blackoutAsset, new Vector3(0, 0, 0), Quaternion.identity); + blackoutGO.name = "BlackoutInstance"; + + // Get the blackout animator component. + blackoutAnimator = blackoutGO.GetComponent(); + if (!blackoutAnimator) + { + MelonLogger.Error("Blackout: Could not find blackout animator component!"); + return; + } + + SetupBlackoutInstance(); + + //we dont want this to ever disable + Camera.onPreRender += OnPreRender; + Camera.onPostRender += OnPostRender; + } + + //Automatic State Change + void Update() + { + //get the current position of the player's head + Vector3 curHeadPos = activeModeCam.transform.position; + //calculate the player's head velocity by taking the difference in position + headVelocity = (curHeadPos - lastHeadPos) / Time.deltaTime; + //store the current head position for use in the next frame + lastHeadPos = curHeadPos; + + if (AutomaticStateChange) + { + curTime = Time.time; + //handle current state + switch (CurrentState) + { + case BlackoutState.Awake: + HandleAwakeState(); + break; + case BlackoutState.Drowsy: + HandleDrowsyState(); + break; + case BlackoutState.Sleeping: + HandleSleepingState(); + break; + default: + break; + } + } + else + { + CalculateDimmingMultiplier(); + } + } + + public void OnEnable() + { + curTime = Time.time; + lastAwakeTime = curTime; + } + + public void OnDisable() + { + ChangeBlackoutState(BlackoutState.Awake); + } + + void OnPreRender(Camera cam) + { + if (cam == activeModeCam) return; + blackoutAnimator.transform.localScale = Vector3.zero; + } + + void OnPostRender(Camera cam) + { + blackoutAnimator.transform.localScale = Vector3.one; + } + + public void SetupBlackoutInstance() + { + activeModeCam = PlayerSetup.Instance.GetActiveCamera().GetComponent(); + blackoutAnimator.transform.parent = activeModeCam.transform; + blackoutAnimator.transform.localPosition = Vector3.zero; + blackoutAnimator.transform.localRotation = Quaternion.identity; + blackoutAnimator.transform.localScale = Vector3.one; + } + + private float GetNextStateTimer() + { + switch (CurrentState) + { + case BlackoutState.Awake: + return (lastAwakeTime + DrowsyModeTimer * 60 - curTime); + case BlackoutState.Drowsy: + return (lastAwakeTime + SleepModeTimer - curTime); + case BlackoutState.Sleeping: + return 0f; + default: + return 0f; + } + } + + //broken, needs to run next frame + private void SendHUDMessage(string message) + { + MelonLogger.Msg(message); + if (!CohtmlHud.Instance || !HudMessages) return; + + StringBuilder secondmessage = new StringBuilder(); + if (AutomaticStateChange) + { + if (CurrentState == BlackoutState.Drowsy && !AutoSleepState) + { + secondmessage = new StringBuilder("AutoSleepState is disabled. Staying in Drowsy State."); + } + else + { + secondmessage = new StringBuilder(GetNextStateTimer().ToString() + " seconds till next state change."); + } + } + + CohtmlHud.Instance.ViewDropTextImmediate("Blackout", message, secondmessage.ToString()); + } + + private void ChangeTargetFPS() + { + if (!DropFPSOnSleep) return; + + //store target FPS to restore, i check each time just in case it changed + targetFPS = MetaPort.Instance.settings.GetSettingInt("GraphicsFramerateTarget", 0); + + Application.targetFrameRate = (CurrentState == BlackoutState.Sleeping) ? 5 : targetFPS; + } + + private void HandleAwakeState() + { + //small movement should reset sleep timer + if (headVelocity.magnitude > drowsyThreshold) + { + lastAwakeTime = curTime; + } + //enter drowsy mode after few minutes + if (curTime > lastAwakeTime + DrowsyModeTimer * 60) + { + ChangeBlackoutState(BlackoutState.Drowsy); + } + } + + public float fadeSpeed = 0.8f; // The speed at which the value fades back to 0 or increases + public float minimumThreshold = 0.5f; // The minimum value that the drowsy magnitude can have + public float drowsyMagnitude = 0f; + + private void CalculateDimmingMultiplier() + { + if (!DrowsyVelocityMultiplier) + { + AdjustDrowsyDimStrength(); + return; + } + + float normalizedMagnitude = headVelocity.magnitude / wakeThreshold; + float targetMagnitude = 1f - normalizedMagnitude; + targetMagnitude = Mathf.Max(targetMagnitude, minimumThreshold); + drowsyMagnitude = Mathf.Lerp(drowsyMagnitude, targetMagnitude, fadeSpeed * Time.deltaTime); + AdjustDrowsyDimStrength(drowsyMagnitude); + } + + private void HandleDrowsyState() + { + //hard movement should exit drowsy state + if (headVelocity.magnitude > wakeThreshold) + { + ChangeBlackoutState(BlackoutState.Awake); + return; + } + //small movement should reset sleep timer + if (headVelocity.magnitude > drowsyThreshold) + { + lastAwakeTime = curTime; + } + //enter full sleep mode + if (AutoSleepState && curTime > lastAwakeTime + SleepModeTimer) + { + ChangeBlackoutState(BlackoutState.Sleeping); + } + CalculateDimmingMultiplier(); + } + + private void HandleSleepingState() + { + //small movement should enter drowsy state + if (headVelocity.magnitude > drowsyThreshold) + { + ChangeBlackoutState(BlackoutState.Drowsy); + } + } +} \ No newline at end of file diff --git a/Blackout/HarmonyPatches.cs b/Blackout/HarmonyPatches.cs new file mode 100644 index 0000000..eb53c70 --- /dev/null +++ b/Blackout/HarmonyPatches.cs @@ -0,0 +1,24 @@ +using ABI_RC.Core.Player; +using ABI_RC.Core.Savior; +using HarmonyLib; +using MelonLoader; + +namespace NAK.Melons.Blackout.HarmonyPatches; + +[HarmonyPatch] +internal class HarmonyPatches +{ + //Support for changing VRMode during runtime. + [HarmonyPostfix] + [HarmonyPatch(typeof(PlayerSetup), "CalibrateAvatar")] + private static void CheckVRModeOnSwitch() + { + if (Blackout.inVR != MetaPort.Instance.isUsingVr) + { + MelonLogger.Msg("VRMode change detected! Reinitializing Blackout Instance..."); + Blackout.inVR = MetaPort.Instance.isUsingVr; + BlackoutController.Instance.SetupBlackoutInstance(); + BlackoutController.Instance.ChangeBlackoutState(BlackoutController.BlackoutState.Awake); + } + } +} \ No newline at end of file diff --git a/Blackout/Integrations/BTKUIAddon.cs b/Blackout/Integrations/BTKUIAddon.cs new file mode 100644 index 0000000..e0fb660 --- /dev/null +++ b/Blackout/Integrations/BTKUIAddon.cs @@ -0,0 +1,63 @@ +using BTKUILib; +using BTKUILib.UIObjects; +using System.Runtime.CompilerServices; + +namespace NAK.Melons.Blackout; + +public static class BTKUIAddon +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static void Init() + { + //Add myself to the Misc Menu + Page miscPage = QuickMenuAPI.MiscTabPage; + Category miscCategory = miscPage.AddCategory(Blackout.SettingsCategory); + + AddMelonToggle(ref miscCategory, Blackout.m_entryEnabled); + + //Add my own page to not clog up Misc Menu + + Page blackoutPage = miscCategory.AddPage("Blackout Settings", "", "Configure the settings for Blackout.", "Blackout"); + blackoutPage.MenuTitle = "Blackout Settings"; + blackoutPage.MenuSubtitle = "Dim screen after set time of sitting still, or configure with manual control. Should be nice for VR sleeping."; + + Category blackoutCategory = blackoutPage.AddCategory("Blackout"); + + AddMelonToggle(ref blackoutCategory, Blackout.m_entryEnabled); + + //manual state changing + var state_Awake = blackoutCategory.AddButton("Awake State", null, "Enter the Awake State."); + state_Awake.OnPress += () => BlackoutController.Instance?.ChangeBlackoutState(BlackoutController.BlackoutState.Awake); + var state_Drowsy = blackoutCategory.AddButton("Drowsy State", null, "Enter the Drowsy State."); + state_Drowsy.OnPress += () => BlackoutController.Instance?.ChangeBlackoutState(BlackoutController.BlackoutState.Drowsy); + var state_Sleeping = blackoutCategory.AddButton("Sleeping State", null, "Enter the Sleeping State."); + state_Sleeping.OnPress += () => BlackoutController.Instance?.ChangeBlackoutState(BlackoutController.BlackoutState.Sleeping); + + //dimming strength + AddMelonSlider(ref blackoutPage, Blackout.m_entryDrowsyDimStrength, 0f, 1f); + + //velocity dim multiplier + AddMelonToggle(ref blackoutCategory, Blackout.m_entryDrowsyVelocityMultiplier); + + //hud messages + AddMelonToggle(ref blackoutCategory, Blackout.m_entryHudMessages); + + //lower fps while sleep (desktop) + AddMelonToggle(ref blackoutCategory, Blackout.m_entryDropFPSOnSleep); + + //auto sleep state + AddMelonToggle(ref blackoutCategory, Blackout.m_entryAutoSleepState); + + //i will add the rest of the settings once BTKUILib supports int input + } + + private static void AddMelonToggle(ref Category category, MelonLoader.MelonPreferences_Entry entry) + { + category.AddToggle(entry.DisplayName, entry.Description, entry.Value).OnValueUpdated += b => entry.Value = b; + } + + private static void AddMelonSlider(ref Page page, MelonLoader.MelonPreferences_Entry entry, float min, float max) + { + page.AddSlider(entry.DisplayName, entry.Description, entry.Value, min, max).OnValueUpdated += f => entry.Value = f; + } +} \ No newline at end of file diff --git a/Blackout/Integrations/UIExpansionKitAddon.cs b/Blackout/Integrations/UIExpansionKitAddon.cs new file mode 100644 index 0000000..3e6a4a6 --- /dev/null +++ b/Blackout/Integrations/UIExpansionKitAddon.cs @@ -0,0 +1,29 @@ +using System.Runtime.CompilerServices; +using UIExpansionKit.API; + +namespace NAK.Melons.Blackout; +public static class UIExpansionKitAddon +{ + [MethodImpl(MethodImplOptions.NoInlining)] + public static void Init() + { + /** + ive spent hours debugging this, and no matter what the buttons wont actually call the actions + from logging shit to straight up closing the game, nothing + + implementing btkuilib support, but gonna leave this shit as a reminder why to not use uiexpansionkit + also because it **used to work**... a game update broke it and uiexpansionkit hasnt updated since + + what pisses me off more, is that DesktopVRSwitch works, and that was originally copied from Blackout -_- + https://github.com/NotAKidOnSteam/DesktopVRSwitch/blob/main/DesktopVRSwitch/UIExpansionKitAddon.cs + **/ + var settings = ExpansionKitApi.GetSettingsCategory(Blackout.SettingsCategory); + settings.AddSimpleButton("Awake State", AwakeState); + settings.AddSimpleButton("Drowsy State", DrowsyState); + settings.AddSimpleButton("Sleep State", SleepingState); + } + //UIExpansionKit actions + internal static void AwakeState() => BlackoutController.Instance?.ChangeBlackoutState(BlackoutController.BlackoutState.Awake); + internal static void DrowsyState() => BlackoutController.Instance?.ChangeBlackoutState(BlackoutController.BlackoutState.Drowsy); + internal static void SleepingState() => BlackoutController.Instance?.ChangeBlackoutState(BlackoutController.BlackoutState.Sleeping); +} \ No newline at end of file diff --git a/Blackout/Main.cs b/Blackout/Main.cs new file mode 100644 index 0000000..fbc473b --- /dev/null +++ b/Blackout/Main.cs @@ -0,0 +1,111 @@ +using ABI_RC.Core.Player; +using ABI_RC.Core.Savior; +using MelonLoader; + +namespace NAK.Melons.Blackout; + +public class Blackout : MelonMod +{ + internal static bool inVR; + internal const string SettingsCategory = "Blackout"; + + internal static MelonPreferences_Category m_categoryBlackout; + internal static MelonPreferences_Entry + m_entryEnabled, + m_entryAutoSleepState, + m_entryHudMessages, + m_entryDropFPSOnSleep, + m_entryDrowsyVelocityMultiplier; + internal static MelonPreferences_Entry + m_entryDrowsyThreshold, m_entryAwakeThreshold, + m_entryDrowsyModeTimer, m_entrySleepModeTimer, + m_entryDrowsyDimStrength; + + public override void OnInitializeMelon() + { + m_categoryBlackout = MelonPreferences.CreateCategory(SettingsCategory); + m_entryEnabled = m_categoryBlackout.CreateEntry("Automatic State Change", true, description: "Should the screen automatically dim if head is still for enough time?"); + m_entryEnabled.OnEntryValueChangedUntyped.Subscribe(OnUpdateEnabled); + m_entryHudMessages = m_categoryBlackout.CreateEntry("Hud Messages", true, description: "Notify on state change."); + m_entryDropFPSOnSleep = m_categoryBlackout.CreateEntry("Limit FPS While Sleep", false, description: "Limits FPS to 5 while in Sleep State. This only works in Desktop, as SteamVR/HMD handles VR FPS."); + m_entryDrowsyVelocityMultiplier = m_categoryBlackout.CreateEntry("Drowsy Velocity Multiplier", true, description: "Should head velocity act as a multiplier to Drowsy Dim Strength?"); + m_entryAutoSleepState = m_categoryBlackout.CreateEntry("Auto Sleep State", true, description: "Should the sleep state be used during Automatic State Change?"); + m_entryDrowsyThreshold = m_categoryBlackout.CreateEntry("Drowsy Threshold", 2f, description: "Velocity to return partial vision."); + m_entryAwakeThreshold = m_categoryBlackout.CreateEntry("Awake Threshold", 4f, description: "Velocity to return full vision."); + m_entryDrowsyModeTimer = m_categoryBlackout.CreateEntry("Enter Drowsy Time (Minutes)", 3f, description: "How many minutes without movement until enter drowsy mode."); + m_entrySleepModeTimer = m_categoryBlackout.CreateEntry("Enter Sleep Time (Seconds)", 10f, description: "How many seconds without movement until enter sleep mode."); + m_entryDrowsyDimStrength = m_categoryBlackout.CreateEntry("Drowsy Dim Strength", 0.6f, description: "How strong of a dimming effect should drowsy mode have."); + + foreach (var setting in m_categoryBlackout.Entries) + { + if (!setting.OnEntryValueChangedUntyped.GetSubscribers().Any()) + setting.OnEntryValueChangedUntyped.Subscribe(OnUpdateSettings); + } + + //UIExpansionKit addon + if (MelonMod.RegisteredMelons.Any(it => it.Info.Name == "UI Expansion Kit")) + { + MelonLogger.Msg("Initializing UIExpansionKit support."); + UIExpansionKitAddon.Init(); + } + + //BTKUILib addon + if (MelonMod.RegisteredMelons.Any(it => it.Info.Name == "BTKUILib")) + { + MelonLogger.Msg("Initializing BTKUILib support."); + BTKUIAddon.Init(); + } + + MelonLoader.MelonCoroutines.Start(WaitForLocalPlayer()); + } + + System.Collections.IEnumerator WaitForLocalPlayer() + { + //load blackout_controller.asset + AssetsHandler.Load(); + + while (PlayerSetup.Instance == null) + yield return null; + + inVR = MetaPort.Instance.isUsingVr; + PlayerSetup.Instance.gameObject.AddComponent(); + + //update BlackoutController settings after it initializes + while (BlackoutController.Instance == null) + yield return null; + + UpdateAllSettings(); + OnEnabled(); + } + + private void OnEnabled() + { + if (!BlackoutController.Instance) return; + if (m_entryEnabled.Value) + { + BlackoutController.Instance.OnEnable(); + } + else + { + BlackoutController.Instance.OnDisable(); + } + BlackoutController.Instance.AutomaticStateChange = m_entryEnabled.Value; + } + + private void UpdateAllSettings() + { + if (!BlackoutController.Instance) return; + BlackoutController.Instance.HudMessages = m_entryHudMessages.Value; + BlackoutController.Instance.AutoSleepState = m_entryAutoSleepState.Value; + BlackoutController.Instance.DropFPSOnSleep = m_entryDropFPSOnSleep.Value; + BlackoutController.Instance.drowsyThreshold = m_entryDrowsyThreshold.Value; + BlackoutController.Instance.wakeThreshold = m_entryAwakeThreshold.Value; + BlackoutController.Instance.DrowsyModeTimer = m_entryDrowsyModeTimer.Value; + BlackoutController.Instance.SleepModeTimer = m_entrySleepModeTimer.Value; + BlackoutController.Instance.DrowsyDimStrength = m_entryDrowsyDimStrength.Value; + BlackoutController.Instance.DrowsyVelocityMultiplier = m_entryDrowsyVelocityMultiplier.Value; + } + + private void OnUpdateEnabled(object arg1, object arg2) => OnEnabled(); + private void OnUpdateSettings(object arg1, object arg2) => UpdateAllSettings(); +} \ No newline at end of file diff --git a/Blackout/Properties/AssemblyInfo.cs b/Blackout/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..e69e1a5 --- /dev/null +++ b/Blackout/Properties/AssemblyInfo.cs @@ -0,0 +1,31 @@ +using NAK.Melons.Blackout.Properties; +using MelonLoader; +using System.Reflection; + + +[assembly: AssemblyVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyFileVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyInformationalVersion(AssemblyInfoParams.Version)] +[assembly: AssemblyTitle(nameof(NAK.Melons.Blackout))] +[assembly: AssemblyCompany(AssemblyInfoParams.Author)] +[assembly: AssemblyProduct(nameof(NAK.Melons.Blackout))] + +[assembly: MelonInfo( + typeof(NAK.Melons.Blackout.Blackout), + nameof(NAK.Melons.Blackout), + AssemblyInfoParams.Version, + AssemblyInfoParams.Author, + downloadLink: "https://github.com/NotAKidOnSteam/Blackout" +)] + +[assembly: MelonGame("Alpha Blend Interactive", "ChilloutVR")] +[assembly: MelonPlatform(MelonPlatformAttribute.CompatiblePlatforms.WINDOWS_X64)] +[assembly: MelonPlatformDomain(MelonPlatformDomainAttribute.CompatibleDomains.MONO)] +[assembly: MelonOptionalDependencies("UIExpansionKit", "BTKUILib")] + +namespace NAK.Melons.Blackout.Properties; +internal static class AssemblyInfoParams +{ + public const string Version = "2.0.0"; + public const string Author = "NotAKidoS"; +} \ No newline at end of file diff --git a/Blackout/Resource1.Designer.cs b/Blackout/Resource1.Designer.cs new file mode 100644 index 0000000..0378e77 --- /dev/null +++ b/Blackout/Resource1.Designer.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace NAK.Melons.Blackout { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resource1 { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resource1() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Blackout.Resource1", typeof(Resource1).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] blackout_controller { + get { + object obj = ResourceManager.GetObject("blackout_controller", resourceCulture); + return ((byte[])(obj)); + } + } + } +} diff --git a/Blackout/Resource1.resx b/Blackout/Resource1.resx new file mode 100644 index 0000000..4003b7c --- /dev/null +++ b/Blackout/Resource1.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + Resources\blackout_controller.asset;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Blackout/format.json b/Blackout/format.json new file mode 100644 index 0000000..5f2e9f7 --- /dev/null +++ b/Blackout/format.json @@ -0,0 +1,24 @@ +{ + "_id": 106, + "name": "Blackout", + "modversion": "2.0.0", + "gameversion": "2022r170", + "loaderversion": "0.5.7", + "modtype": "Mod", + "author": "NotAKidoS", + "description": "Dim screen after set time of sitting still. Should be nice for VR sleeping.\n\nNotable Options:\n Cap FPS while sleeping.\nManual control via BTKUILib\nConfigurable dimming strength.", + "searchtags": [ + "black", + "dimmer", + "sleeping", + "sleeper" + ], + "requirements": [ + "BTKUILib", + "UIExpansionKit" + ], + "downloadlink": "https://github.com/NotAKidOnSteam/Blackout/releases/download/v2.0.0/Blackout.dll", + "sourcelink": "https://github.com/NotAKidOnSteam/Blackout/", + "changelog": "- Added BTKUILib support.\n- Added dimming strength velocity multiplier option.\n- Added option to not use Automatic Sleep State.\n- Dimming strengh now updates in realtime when configuring.", + "embedcolor": "#161b22" +} \ No newline at end of file diff --git a/Blackout/resources/blackout_controller.asset b/Blackout/resources/blackout_controller.asset new file mode 100644 index 0000000..415a7d8 Binary files /dev/null and b/Blackout/resources/blackout_controller.asset differ diff --git a/README.md b/README.md index 02dae3e..3c2268e 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,41 @@ Simple mod to clear hud notifications when joining an online instance. Can also There is no native method to clear notifications, so I force an immediate notification to clear the buffer. +# Blackout + + Functionality heavily inspired by VRSleeper on Booth: https://booth.pm/ja/items/2151940 + + There are three states of "blackout": + + 0 - Awake (no effect) + 1 - Drowsy (partial effect) + 2 - Sleep (full effect) + + After staying still for DrowsyModeTimer (minutes), you enter DrowsyMode. + This mode dims the screen to your selected dimming strength. + After continuing to stay still for SleepModeTimer (seconds), you enter SleepMode. + This mode over renders mostly everything with black. + + Slight movement while in SleepMode will place you in DrowsyMode until SleepModeTimer is reached again. + Hard movement once entering DrowsyMode will fully wake you and return complete vision. + + Auto state changing can be disabled. This allows you to use UIExpansionKit to manually change Blackout states. + + Supports DesktopVRSwitch~ if that releases. + +**Settings** + +* Hud Messages - Sends hud notification on state change. +* Lower FPS While Sleep - Caps FPS to 5 while in Sleep State. +* Drowsy Dim Strength - How strong of a dimming effect should drowsy mode have. + +//Automatic State Change related stuff +* Automatic State Change - Dim screen when there is no movement for a while. +* Drowsy Threshold - Degrees of movement to return partial vision. +* Awake Threshold - Degrees of movement to return full vision. +* Enter Drowsy Time - How many minutes without movement until enter drowsy mode. +* Enter Sleep Time - How many seconds without movement until enter sleep mode. + --- Here is the block of text where I tell you this mod is not affiliated or endorsed by ABI.