using System; using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("Better Low Health Warning")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("Better Low Health Warning")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("4704650f-69e7-4932-a25e-3b23ceda6943")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("1.0.0.0")] namespace BetterLowHealthWarning; [BepInPlugin("kumo.sulfur.better_low_health_warning", "Better Low Health Warning", "1.1.0")] public sealed class Plugin : BaseUnityPlugin { internal static ManualLogSource Log; internal static ConfigEntry EnableMod; internal static ConfigEntry HideWhenHudHidden; internal static ConfigEntry WarningHealthPercent; internal static ConfigEntry CriticalHealthPercent; internal static ConfigEntry BorderThickness; internal static ConfigEntry MinOpacity; internal static ConfigEntry MaxOpacity; internal static ConfigEntry EnablePulse; internal static ConfigEntry PulseSpeed; internal static ConfigEntry FadeSpeed; internal static ConfigEntry FullscreenTintOpacity; internal static ConfigEntry LogErrors; internal static BetterLowHealthWarningOverlay Overlay; private Harmony harmony; private static bool loggedError; private void Awake() { //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Expected O, but got Unknown //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Expected O, but got Unknown //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Expected O, but got Unknown //IL_012d: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Expected O, but got Unknown //IL_016a: Unknown result type (might be due to invalid IL or missing references) //IL_0174: Expected O, but got Unknown //IL_01c7: Unknown result type (might be due to invalid IL or missing references) //IL_01d1: Expected O, but got Unknown //IL_0204: Unknown result type (might be due to invalid IL or missing references) //IL_020e: Expected O, but got Unknown //IL_0241: Unknown result type (might be due to invalid IL or missing references) //IL_024b: Expected O, but got Unknown //IL_0275: Unknown result type (might be due to invalid IL or missing references) //IL_027b: Expected O, but got Unknown //IL_029c: Unknown result type (might be due to invalid IL or missing references) //IL_02a6: Expected O, but got Unknown //IL_0317: Unknown result type (might be due to invalid IL or missing references) //IL_031d: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; EnableMod = ((BaseUnityPlugin)this).Config.Bind("General", "EnableMod", true, "Enable better low health warning."); HideWhenHudHidden = ((BaseUnityPlugin)this).Config.Bind("General", "HideWhenHudHidden", true, "Hide the warning when the game HUD is hidden."); WarningHealthPercent = ((BaseUnityPlugin)this).Config.Bind("Visual", "WarningHealthPercent", 0.35f, new ConfigDescription("Show warning below this health percentage. 0.35 = 35%.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 1f), Array.Empty())); CriticalHealthPercent = ((BaseUnityPlugin)this).Config.Bind("Visual", "CriticalHealthPercent", 0.15f, new ConfigDescription("Use stronger warning below this health percentage. 0.15 = 15%.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 1f), Array.Empty())); BorderThickness = ((BaseUnityPlugin)this).Config.Bind("Visual", "BorderThickness", 160, new ConfigDescription("Soft red vignette thickness in pixels.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 700), Array.Empty())); MinOpacity = ((BaseUnityPlugin)this).Config.Bind("Visual", "MinOpacity", 0.08f, new ConfigDescription("Warning opacity at the warning threshold.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 1f), Array.Empty())); MaxOpacity = ((BaseUnityPlugin)this).Config.Bind("Visual", "MaxOpacity", 0.42f, new ConfigDescription("Warning opacity at or below the critical threshold.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 1f), Array.Empty())); EnablePulse = ((BaseUnityPlugin)this).Config.Bind("Visual", "EnablePulse", true, "Pulse the warning when health is low."); PulseSpeed = ((BaseUnityPlugin)this).Config.Bind("Visual", "PulseSpeed", 2.2f, new ConfigDescription("Pulse speed. Higher values pulse faster.", (AcceptableValueBase)(object)new AcceptableValueRange(0.1f, 20f), Array.Empty())); FadeSpeed = ((BaseUnityPlugin)this).Config.Bind("Visual", "FadeSpeed", 10f, new ConfigDescription("Fade in/out speed for the warning.", (AcceptableValueBase)(object)new AcceptableValueRange(0.1f, 50f), Array.Empty())); FullscreenTintOpacity = ((BaseUnityPlugin)this).Config.Bind("Visual", "FullscreenTintOpacity", 0.035f, new ConfigDescription("Very subtle full-screen red tint multiplier. Set to 0 to disable.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 0.2f), Array.Empty())); LogErrors = ((BaseUnityPlugin)this).Config.Bind("Debug", "LogErrors", false, "Log patch errors. Keep false for normal gameplay."); GameObject val = new GameObject("BetterLowHealthWarningOverlay"); Object.DontDestroyOnLoad((Object)(object)val); ((Object)val).hideFlags = (HideFlags)61; Overlay = val.AddComponent(); harmony = new Harmony("kumo.sulfur.better_low_health_warning"); Type type = AccessTools.TypeByName("PerfectRandom.Sulfur.Gameplay.PlayerHUD"); if (type == null) { ((BaseUnityPlugin)this).Logger.LogError((object)"Could not find PlayerHUD type."); return; } MethodInfo methodInfo = AccessTools.Method(type, "Update", (Type[])null, (Type[])null); if (methodInfo == null) { ((BaseUnityPlugin)this).Logger.LogError((object)"Could not find PlayerHUD.Update."); return; } HarmonyMethod val2 = new HarmonyMethod(typeof(Plugin).GetMethod("PlayerHudUpdatePostfix", BindingFlags.Static | BindingFlags.NonPublic)); harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, val2, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); ((BaseUnityPlugin)this).Logger.LogInfo((object)"Better Low Health Warning 1.1.0 loaded. Patched PlayerHUD.Update."); } private void OnDestroy() { Harmony obj = harmony; if (obj != null) { obj.UnpatchSelf(); } if ((Object)(object)Overlay != (Object)null) { Object.Destroy((Object)(object)((Component)Overlay).gameObject); Overlay = null; } } private static void PlayerHudUpdatePostfix(object __instance) { if ((Object)(object)Overlay == (Object)null) { return; } if (!EnableMod.Value) { Overlay.ClearHealth(); return; } try { if (!TryGetNormalizedHealthFromPlayerHud(__instance, out var normalizedHealth)) { Overlay.ClearHealth(); return; } bool isHudVisible = TryGetHudVisible(__instance); Overlay.SetHealth(normalizedHealth, isHudVisible); } catch (Exception ex) { Overlay.ClearHealth(); if (LogErrors.Value && !loggedError) { loggedError = true; ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("Better Low Health Warning patch error: " + ex)); } } } } private static bool TryGetNormalizedHealthFromPlayerHud(object playerHud, out float normalizedHealth) { normalizedHealth = 1f; if (playerHud == null) { return false; } object fieldValue = GetFieldValue(playerHud, "player"); if (fieldValue == null) { return false; } object fieldValue2 = GetFieldValue(fieldValue, "playerUnit"); if (fieldValue2 == null) { return false; } MethodInfo method = fieldValue2.GetType().GetMethod("GetNormalizedHealth", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (method == null) { return false; } object obj = method.Invoke(fieldValue2, null); if (!(obj is float)) { return false; } normalizedHealth = Mathf.Clamp01((float)obj); return true; } private static bool TryGetHudVisible(object playerHud) { if (playerHud == null) { return true; } object fieldValue = GetFieldValue(playerHud, "canvasGroup"); if (fieldValue == null) { return true; } PropertyInfo property = fieldValue.GetType().GetProperty("alpha", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (property == null) { return true; } object value = property.GetValue(fieldValue, null); if (!(value is float)) { return true; } return (float)value > 0.01f; } private static object GetFieldValue(object instance, string fieldName) { if (instance == null || string.IsNullOrEmpty(fieldName)) { return null; } FieldInfo field = instance.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); if (field == null) { return null; } return field.GetValue(instance); } } internal sealed class BetterLowHealthWarningOverlay : MonoBehaviour { private float normalizedHealth = 1f; private bool hasValidHealth; private bool hudVisible = true; private float displayedOpacity; public void SetHealth(float value, bool isHudVisible) { normalizedHealth = Mathf.Clamp01(value); hudVisible = isHudVisible; hasValidHealth = true; } public void ClearHealth() { hasValidHealth = false; normalizedHealth = 1f; hudVisible = true; } private void OnGUI() { float num = 0f; if (Plugin.EnableMod.Value && hasValidHealth && (!Plugin.HideWhenHudHidden.Value || hudVisible)) { float num2 = Mathf.Clamp01(Plugin.WarningHealthPercent.Value); float num3 = Mathf.Clamp01(Plugin.CriticalHealthPercent.Value); float num4 = Mathf.Max(num2, num3); float num5 = Mathf.Min(num2, num3); if (normalizedHealth <= num4) { float num6 = Mathf.Max(0.001f, num4 - num5); float num7 = Mathf.Clamp01((num4 - normalizedHealth) / num6); float num8 = Mathf.Clamp01(Plugin.MinOpacity.Value); float num9 = Mathf.Clamp01(Plugin.MaxOpacity.Value); if (num9 < num8) { float num10 = num8; num8 = num9; num9 = num10; } num = Mathf.Lerp(num8, num9, num7); if (Plugin.EnablePulse.Value) { float num11 = Mathf.Max(0.1f, Plugin.PulseSpeed.Value); float num12 = 0.5f + 0.5f * Mathf.Sin(Time.unscaledTime * num11 * (float)Math.PI * 2f); float num13 = Mathf.Lerp(0.05f, 0.35f, num7); num *= Mathf.Lerp(1f - num13, 1f, num12); } } } float num14 = Mathf.Max(0.1f, Plugin.FadeSpeed.Value); displayedOpacity = Mathf.Lerp(displayedOpacity, num, 1f - Mathf.Exp((0f - num14) * Time.unscaledDeltaTime)); if (!(displayedOpacity <= 0.005f)) { DrawSoftVignette(displayedOpacity); } } private static void DrawSoftVignette(float opacity) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_00ef: Unknown result type (might be due to invalid IL or missing references) //IL_011c: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Unknown result type (might be due to invalid IL or missing references) //IL_0168: Unknown result type (might be due to invalid IL or missing references) //IL_0197: Unknown result type (might be due to invalid IL or missing references) //IL_01b8: Unknown result type (might be due to invalid IL or missing references) int num = Mathf.Clamp(Plugin.BorderThickness.Value, 1, 700); int num2 = 14; Texture2D whiteTexture = Texture2D.whiteTexture; Color color = GUI.color; float num3 = Screen.width; float num4 = Screen.height; float num5 = Mathf.Clamp(Plugin.FullscreenTintOpacity.Value, 0f, 0.2f); if (num5 > 0f) { GUI.color = new Color(0.45f, 0f, 0f, Mathf.Clamp01(opacity * num5)); GUI.DrawTexture(new Rect(0f, 0f, num3, num4), (Texture)(object)whiteTexture); } float num6 = Mathf.Max(1f, (float)num / (float)num2); for (int i = 0; i < num2; i++) { float num7 = (float)i / (float)(num2 - 1); float num8 = opacity * Mathf.Pow(1f - num7, 1.85f); GUI.color = new Color(0.95f, 0.02f, 0.015f, Mathf.Clamp01(num8)); float num9 = (float)i * num6; float num10 = Mathf.Ceil(num6); GUI.DrawTexture(new Rect(num9, num9, num3 - num9 * 2f, num10), (Texture)(object)whiteTexture); GUI.DrawTexture(new Rect(num9, num4 - num9 - num10, num3 - num9 * 2f, num10), (Texture)(object)whiteTexture); GUI.DrawTexture(new Rect(num9, num9 + num10, num10, num4 - num9 * 2f - num10 * 2f), (Texture)(object)whiteTexture); GUI.DrawTexture(new Rect(num3 - num9 - num10, num9 + num10, num10, num4 - num9 * 2f - num10 * 2f), (Texture)(object)whiteTexture); } GUI.color = color; } }