Merge remote-tracking branch 'Blackout/main'

Merged Blackout
This commit is contained in:
NotAKidoS 2023-04-16 06:45:31 -05:00
commit ad2da17f48
14 changed files with 1041 additions and 0 deletions

95
Blackout/AssetHandler.cs Normal file
View file

@ -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<string> ms_assets = new List<string>()
{
"blackout_controller.asset"
};
static Dictionary<string, AssetBundle> ms_loadedAssets = new Dictionary<string, AssetBundle>();
static Dictionary<string, GameObject> ms_loadedObjects = new Dictionary<string, GameObject>();
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();
}
}

84
Blackout/Blackout.csproj Normal file
View file

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>
<ItemGroup>
<None Remove="resources\blackout_controller.asset" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="resources\blackout_controller.asset" />
</ItemGroup>
<ItemGroup>
<Reference Include="0Harmony">
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\MelonLoader\0Harmony.dll</HintPath>
</Reference>
<Reference Include="Assembly-CSharp">
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\Assembly-CSharp.dll</HintPath>
</Reference>
<Reference Include="Assembly-CSharp-firstpass">
<HintPath>..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\Assembly-CSharp-firstpass.dll</HintPath>
</Reference>
<Reference Include="BTKUILib">
<HintPath>..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\ChilloutVR\Mods\BTKUILib.dll</HintPath>
</Reference>
<Reference Include="Cohtml.Runtime">
<HintPath>..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\Cohtml.Runtime.dll</HintPath>
</Reference>
<Reference Include="Giamoz">
<HintPath>..\..\Giamoz\Giamoz\bin\Debug\net472\Giamoz.dll</HintPath>
</Reference>
<Reference Include="MelonLoader">
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\MelonLoader\MelonLoader.dll</HintPath>
</Reference>
<Reference Include="SteamVR">
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\SteamVR.dll</HintPath>
</Reference>
<Reference Include="UIExpansionKit">
<HintPath>..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\ChilloutVR\Mods\UIExpansionKit.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.AnimationModule">
<HintPath>..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\UnityEngine.AnimationModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.AssetBundleModule">
<HintPath>..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\UnityEngine.AssetBundleModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.CoreModule">
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.InputLegacyModule">
<HintPath>C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\UnityEngine.InputLegacyModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.PhysicsModule">
<HintPath>..\..\..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\ChilloutVR\ChilloutVR_Data\Managed\UnityEngine.PhysicsModule.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Update="Resource1.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resource1.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resource1.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resource1.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<Target Name="Deploy" AfterTargets="Build">
<Copy SourceFiles="$(TargetPath)" DestinationFolder="C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\Mods\" />
<Message Text="Copied $(TargetPath) to C:\Program Files (x86)\Steam\steamapps\common\ChilloutVR\Mods\" Importance="high" />
</Target>
</Project>

25
Blackout/Blackout.sln Normal file
View file

@ -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

View file

@ -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<Animator>();
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<Camera>();
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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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<bool> entry)
{
category.AddToggle(entry.DisplayName, entry.Description, entry.Value).OnValueUpdated += b => entry.Value = b;
}
private static void AddMelonSlider(ref Page page, MelonLoader.MelonPreferences_Entry<float> entry, float min, float max)
{
page.AddSlider(entry.DisplayName, entry.Description, entry.Value, min, max).OnValueUpdated += f => entry.Value = f;
}
}

View file

@ -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);
}

111
Blackout/Main.cs Normal file
View file

@ -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<bool>
m_entryEnabled,
m_entryAutoSleepState,
m_entryHudMessages,
m_entryDropFPSOnSleep,
m_entryDrowsyVelocityMultiplier;
internal static MelonPreferences_Entry<float>
m_entryDrowsyThreshold, m_entryAwakeThreshold,
m_entryDrowsyModeTimer, m_entrySleepModeTimer,
m_entryDrowsyDimStrength;
public override void OnInitializeMelon()
{
m_categoryBlackout = MelonPreferences.CreateCategory(SettingsCategory);
m_entryEnabled = m_categoryBlackout.CreateEntry<bool>("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<bool>("Hud Messages", true, description: "Notify on state change.");
m_entryDropFPSOnSleep = m_categoryBlackout.CreateEntry<bool>("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<bool>("Drowsy Velocity Multiplier", true, description: "Should head velocity act as a multiplier to Drowsy Dim Strength?");
m_entryAutoSleepState = m_categoryBlackout.CreateEntry<bool>("Auto Sleep State", true, description: "Should the sleep state be used during Automatic State Change?");
m_entryDrowsyThreshold = m_categoryBlackout.CreateEntry<float>("Drowsy Threshold", 2f, description: "Velocity to return partial vision.");
m_entryAwakeThreshold = m_categoryBlackout.CreateEntry<float>("Awake Threshold", 4f, description: "Velocity to return full vision.");
m_entryDrowsyModeTimer = m_categoryBlackout.CreateEntry<float>("Enter Drowsy Time (Minutes)", 3f, description: "How many minutes without movement until enter drowsy mode.");
m_entrySleepModeTimer = m_categoryBlackout.CreateEntry<float>("Enter Sleep Time (Seconds)", 10f, description: "How many seconds without movement until enter sleep mode.");
m_entryDrowsyDimStrength = m_categoryBlackout.CreateEntry<float>("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<BlackoutController>();
//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();
}

View file

@ -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";
}

73
Blackout/Resource1.Designer.cs generated Normal file
View file

@ -0,0 +1,73 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 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.
// </auto-generated>
//------------------------------------------------------------------------------
namespace NAK.Melons.Blackout {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// 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() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[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;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized resource of type System.Byte[].
/// </summary>
internal static byte[] blackout_controller {
get {
object obj = ResourceManager.GetObject("blackout_controller", resourceCulture);
return ((byte[])(obj));
}
}
}
}

124
Blackout/Resource1.resx Normal file
View file

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="blackout_controller" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>Resources\blackout_controller.asset;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</data>
</root>

24
Blackout/format.json Normal file
View file

@ -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"
}

Binary file not shown.

View file

@ -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.