Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of Sanity v0.0.2
plugins/Lowlight.Sanity.dll
Decompiled 19 hours ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using Microsoft.CodeAnalysis; using Sanity.Compat; using Sanity.Symptoms; using TMPro; using Unity.Netcode; using UnityEngine; using UnityEngine.AI; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; using UnityEngine.Rendering; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("Lowlight.Sanity")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.0.2.0")] [assembly: AssemblyInformationalVersion("0.0.2+440439d4ded3a4a22616832f815030fa2ae54529")] [assembly: AssemblyProduct("Lowlight.Sanity")] [assembly: AssemblyTitle("Sanity")] [assembly: AssemblyMetadata("AI_Assisted_Creation", "This assembly was partially or fully created with the assistance of Generative AI (Claude Opus 5.5).")] [assembly: AssemblyMetadata("AI_Model_Vendor", "Anthropic")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.2.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace BepInEx { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [Conditional("CodeGeneration")] [Embedded] internal sealed class BepInAutoPluginAttribute : Attribute { public BepInAutoPluginAttribute(string? id = null, string? name = null, string? version = null) { } } } namespace BepInEx.Preloader.Core.Patching { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [Conditional("CodeGeneration")] [Embedded] internal sealed class PatcherAutoPluginAttribute : Attribute { public PatcherAutoPluginAttribute(string? id = null, string? name = null, string? version = null) { } } } namespace Microsoft.CodeAnalysis { [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace Sanity { internal static class DayLength { private const float MeasureAfterSeconds = 20f; private const float LogAfterSeconds = 60f; private const float RestartJump = 0.01f; private static bool started; private static bool logged; private static float startTime; private static float startFraction; private static float lastFraction; internal static float Factor { get; private set; } = 1f; internal static bool Measured { get; private set; } internal static void Tick(TimeOfDay timeOfDay) { if ((Object)(object)timeOfDay == (Object)null || (Object)(object)timeOfDay.currentLevel == (Object)null || !timeOfDay.currentDayTimeStarted || timeOfDay.totalTime <= 0f) { return; } float normalizedTimeOfDay = timeOfDay.normalizedTimeOfDay; if (!started || Mathf.Abs(normalizedTimeOfDay - lastFraction) > 0.01f) { started = true; startTime = Time.time; startFraction = normalizedTimeOfDay; } lastFraction = normalizedTimeOfDay; float num = Time.time - startTime; float num2 = normalizedTimeOfDay - startFraction; float num3 = 1f / timeOfDay.totalTime; if (!(num < 20f) && !(num2 <= 0.0005f) && !(num3 <= 0f) && !(normalizedTimeOfDay >= 0.995f)) { Factor = Mathf.Clamp(num3 * num / num2, 0.25f, 8f); Measured = true; if (!logged && num >= 60f) { logged = true; Telemetry.Event("DAY_LENGTH", Factor, FormattableString.Invariant($"day lasts x{Factor:F2} a normal day ({1f / Mathf.Max(1E-06f, num2 / num):F0} real s per day; total time {timeOfDay.totalTime:F0}, moon day speed {timeOfDay.currentLevel.DaySpeedMultiplier:0.###}, global time speed {timeOfDay.globalTimeSpeedMultiplier:0.###})")); } } } internal static void Reset() { Factor = 1f; Measured = false; started = false; logged = false; } } internal static class DebugLog { internal struct Entry { internal float At; internal string Kind; internal string Mark; internal string Text; } private const int Max = 80; private static readonly List<Entry> Entries = new List<Entry>(80); internal static IReadOnlyList<Entry> All => Entries; internal static void Clear() { Entries.Clear(); } internal static void Note(string name, string detail) { string mark = "."; if (name == null) { return; } string text; switch (name.Length) { default: return; case 7: { char c = name[0]; if (c != 'F') { if (c != 'R') { if (c != 'S' || !(name == "SYMPTOM")) { return; } text = (detail.Contains("(chain from") ? "chain" : "symptom"); mark = ((text == "chain") ? "->" : ">"); break; } if (!(name == "REVIVED")) { return; } goto IL_0522; } if (!(name == "FLICKER")) { return; } goto IL_0538; } case 13: switch (name[0]) { case 'S': break; case 'T': goto IL_0220; case 'F': goto IL_0231; case 'P': goto IL_0242; case 'L': goto IL_0263; default: return; } if (!(name == "SYMPTOM_ARMED")) { return; } text = "symptom"; mark = "+"; break; case 15: { char c = name[0]; if (c != 'F') { if (c != 'P') { if (c != 'S' || !(name == "SYMPTOM_DROPPED")) { return; } goto IL_04bc; } if (!(name == "PHANTOM_HIT_END")) { return; } } else if (!(name == "FAKE_DOOR_AFTER")) { return; } goto IL_0538; } case 14: { char c = name[2]; if ((uint)c <= 69u) { if (c != 'A') { if (c != 'E' || !(name == "PRESENCE_EVENT")) { return; } goto IL_0500; } if (!(name == "PHANTOM_WALKIE")) { return; } goto IL_0538; } if (c != 'M') { if (c != 'O' || !(name == "PROMPT_MISSING")) { return; } goto IL_052a; } if (!(name == "SYMPTOM_FAILED")) { return; } goto IL_04bc; } case 5: { char c = name[0]; if (c != 'C') { if (c != 'R' || !(name == "RELAX")) { return; } goto IL_0522; } if (!(name == "CHAIN")) { return; } text = "chain"; mark = (detail.EndsWith("played") ? "->" : (detail.Contains("didn't play") ? "x" : ".")); break; } case 12: { char c = name[0]; if (c != 'P') { if (c != 'S' || !(name == "SANITY_ERROR")) { return; } goto IL_052a; } if (!(name == "PRESENCE_END")) { return; } goto IL_0500; } case 6: { char c = name[0]; if ((uint)c <= 68u) { switch (c) { default: return; case 'B': if (!(name == "BOUNCE")) { return; } break; case 'D': if (!(name == "DAMAGE")) { return; } break; } } else { if (c != 'R') { if (c != 'V' || !(name == "VANISH")) { return; } goto IL_0538; } if (!(name == "RELIEF")) { return; } } goto IL_0522; } case 4: switch (name[0]) { default: return; case 'M': if (!(name == "MOOD")) { return; } break; case 'E': if (!(name == "ECHO")) { return; } break; } goto IL_0522; case 9: { char c = name[1]; if (c != 'A') { if (c == 'E') { if (!(name == "FEAR_JUMP")) { return; } goto IL_0522; } if (c != 'O' || !(name == "LOST_TIME")) { return; } } else if (!(name == "FAKE_DOOR")) { return; } goto IL_0538; } case 20: { char c = name[0]; if (c != 'P') { if (c != 'T' || !(name == "TEAMMATE_DIED_NEARBY")) { return; } goto IL_0522; } if (!(name == "PHANTOM_WALKIE_REPLY")) { return; } goto IL_0538; } case 18: { char c = name[8]; if (c != 'H') { if (c != 'W') { if (c != '_' || !(name == "TEAMMATE_DIED_SHIP")) { return; } goto IL_0522; } if (!(name == "PHANTOM_WALKIE_END")) { return; } } else if (!(name == "PHANTOM_HIT_HEALED")) { return; } goto IL_0538; } case 16: if (!(name == "SYMPTOM_REJECTED")) { return; } goto IL_04bc; case 8: if (!(name == "PRESENCE")) { return; } goto IL_0500; case 10: if (!(name == "VANISH_END")) { return; } goto IL_0538; case 11: if (!(name == "PHANTOM_HIT")) { return; } goto IL_0538; case 17: if (!(name == "PHANTOM_STEPS_END")) { return; } goto IL_0538; case 19: return; IL_04bc: text = "symptom"; mark = "x"; break; IL_0220: if (!(name == "TEAMMATE_DIED")) { return; } goto IL_0522; IL_0538: text = "symptom"; break; IL_0522: text = "meter"; break; IL_0500: text = "presence"; mark = ((name == "PRESENCE") ? ">" : "."); break; IL_0263: if (!(name == "LOST_TIME_END")) { return; } goto IL_0538; IL_0242: if (!(name == "PHANTOM_SOUND") && !(name == "PHANTOM_STEPS")) { return; } goto IL_0538; IL_0231: if (!(name == "FAKE_DOOR_END")) { return; } goto IL_0538; IL_052a: text = "problem"; mark = "!"; break; } if (Entries.Count >= 80) { Entries.RemoveAt(0); } Entries.Add(new Entry { At = Time.time, Kind = text, Mark = mark, Text = Label(name) + detail }); } private static string Label(string name) { switch (name) { case "PRESENCE_EVENT": case "SYMPTOM": case "SYMPTOM_ARMED": case "CHAIN": return (name == "SYMPTOM_ARMED") ? "armed " : ""; case "SYMPTOM_DROPPED": return "dropped "; case "SYMPTOM_FAILED": return "failed "; case "SYMPTOM_REJECTED": return ""; default: return name.ToLowerInvariant().Replace('_', ' ') + ": "; } } } internal sealed class DebugMenu : MonoBehaviour { private const float Width = 460f; private const float Height = 540f; private static readonly float[] SanityPresets = new float[6] { 100f, 70f, 60f, 40f, 15f, 0f }; private static readonly string[] StrengthNames = new string[4] { "Current", "Uneasy", "Disturbed", "Breaking" }; private static readonly string[] Tabs = new string[4] { "Now", "Symptoms", "Log", "Tools" }; private static readonly string[] LogFilters = new string[6] { "All", "Symptoms", "Chains", "Presence", "Meter", "Problems" }; private static readonly string[] LogKinds = new string[6] { "", "symptom", "chain", "presence", "meter", "problem" }; private bool open; private bool weDisabledLook; private int strength; private int tab; private int logFilter; private string message = ""; private Rect window; private Vector2 scroll; private string[] statuses = Array.Empty<string>(); private string[] statusesShort = Array.Empty<string>(); private readonly List<string> nowLines = new List<string>(); private float statusTimer; private GUIStyle? small; private GUIStyle? heading; internal static bool IsOpen { get; private set; } internal static void Create() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown GameObject val = new GameObject("Sanity.DebugMenu"); ((Object)val).hideFlags = (HideFlags)61; Object.DontDestroyOnLoad((Object)(object)val); val.AddComponent<DebugMenu>(); } private void Awake() { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) window = new Rect((float)Screen.width - 460f - 20f, 40f, 460f, 540f); } private void Update() { bool value = Plugin.DebugKeys.Value; if (value && Keyboard.current != null && ((ButtonControl)Keyboard.current.f7Key).wasPressedThisFrame) { SetOpen(!open); } else if (open && !value) { SetOpen(value: false); } if (open) { Reactions.NoteConfound("debug menu"); Cursor.lockState = (CursorLockMode)0; Cursor.visible = true; statusTimer -= Time.deltaTime; if (statusTimer <= 0f) { statusTimer = 0.5f; Refresh(); } } } private void Refresh() { PlayerControllerB val = LocalPlayer(); int count = SanityDirector.Symptoms.Count; if (statuses.Length != count) { statuses = new string[count]; statusesShort = new string[count]; } for (int i = 0; i < count; i++) { Symptom symptom = SanityDirector.Symptoms[i]; string text; if (!symptom.Enabled) { text = "off in config"; } else if ((Object)(object)val == (Object)null || !SanityMeter.OnMoon) { text = "not on a moon"; } else if (symptom.RunningState(val).Length > 0) { text = "RUNNING: " + symptom.RunningState(val); } else if (symptom is ArmedSymptom { IsArmed: not false } armedSymptom) { string heldByNow = armedSymptom.HeldByNow; text = FormattableString.Invariant(FormattableStringFactory.Create("ARMED {0:F0}/{1:F0}s{2}; {3}", armedSymptom.ArmedFor, armedSymptom.WaitLimit, (heldByNow.Length > 0) ? (", held: " + heldByNow) : "", armedSymptom.MomentStatus(val))); } else { string text2 = SanityDirector.NaturalCantPlayReason(symptom, val); text = ((text2.Length > 0) ? ("can't: " + text2) : ((SanityMeter.Tier < symptom.MinTier) ? $"ready, needs {symptom.MinTier}" : "ready")); } statuses[i] = FormattableString.Invariant($"{text} (recovery {symptom.Recovery * 100f:F0}%)"); statusesShort[i] = Short(text, 44); } nowLines.Clear(); if ((Object)(object)val == (Object)null) { nowLines.Add("Not in a game."); } else { BuildNow(val); } } private void BuildNow(PlayerControllerB player) { string text = (SanityDirector.Relaxing ? FormattableString.Invariant($", RELAX {SanityDirector.RelaxLeft:F0}s") : ""); nowLines.Add("#Director"); nowLines.Add(FormattableString.Invariant(FormattableStringFactory.Create("chance {0:F3}/s, wave {1}, mood {2}{3}", SanityDirector.ChancePerSecond, Waves.High ? "high x1.6" : "low x0.4", DayMood.Name, text))); nowLines.Add(FormattableString.Invariant($"last: {SanityDirector.LastSymptom}, {Ago(SanityDirector.SecondsSinceVisible)} since something showed, quiet {SanityDirector.QuietSeconds:F0}s")); string pendingSummary = Chains.PendingSummary; if (pendingSummary.Length > 0) { nowLines.Add("chain coming: " + pendingSummary); } nowLines.Add("#Armed and showing"); int num = 0; foreach (Symptom symptom in SanityDirector.Symptoms) { if (symptom is ArmedSymptom armedSymptom && (armedSymptom.IsArmed || armedSymptom.IsShowing)) { string heldByNow = armedSymptom.HeldByNow; nowLines.Add(armedSymptom.IsArmed ? FormattableString.Invariant(FormattableStringFactory.Create("{0}: armed {1:F0}/{2:F0}s{3}. {4}", armedSymptom.Name, armedSymptom.ArmedFor, armedSymptom.WaitLimit, (heldByNow.Length > 0) ? (", HELD by " + heldByNow) : "", armedSymptom.MomentStatus(player))) : (armedSymptom.Name + ": showing. " + armedSymptom.MomentStatus(player))); num++; } } if (SprintHold.Waiting) { nowLines.Add(SprintHold.Status); num++; } if (PresenceSymptom.Active) { nowLines.Add("Presence: " + PresenceSymptom.DebugState(player)); nowLines.Add(" did: " + PresenceSymptom.LastNotes); num++; } if (PhantomHitSymptom.DebugState.Length > 0) { nowLines.Add("Phantom hit: " + PhantomHitSymptom.DebugState); num++; } if (PhantomAudio.IsPlaying && !PresenceSymptom.Active) { nowLines.Add("a phantom sound or steps is playing"); num++; } if (num == 0) { nowLines.Add("nothing armed or showing. Presence: " + PresenceSymptom.DebugState(player)); } nowLines.Add("#Meter"); nowLines.Add(FormattableString.Invariant($"inside {SanityMeter.PartInside:F2} outside {SanityMeter.PartOutside:F2} night {SanityMeter.PartNight:F2} fear {SanityMeter.PartFear:F2} ship +{SanityMeter.PartShip:F2} /s")); nowLines.Add(SanityReliefs.DebugSummary); string text2 = ((SanityMeter.NearestTeammateDistance < 0f) ? "nobody" : FormattableString.Invariant(FormattableStringFactory.Create("{0:F0} m{1}", SanityMeter.NearestTeammateDistance, SanityMeter.NearTeammate ? " (company)" : ""))); nowLines.Add(FormattableString.Invariant($"nearest teammate {text2}, lobby {SanityMeter.PlayersAtStart} (x{SanityMeter.LobbyMultiplier:0.##}), loot today +{LootRecovery.GainedToday:F1}")); nowLines.Add("#World and loaded"); nowLines.Add(FormattableString.Invariant(FormattableStringFactory.Create("{0} (out x{1:0.##} in x{2:0.##}), day x{3:0.##} long{4}, phantom loudness {5:0.##}x closer", Weather.Name, Weather.Outside, Weather.Inside, DayLength.Factor, DayLength.Measured ? "" : " (measuring)", PhantomAudio.Closer))); nowLines.Add("mods seen: " + Telemetry.ModsSummary()); string text3 = ((!Plugin.TelemetryEnabled.Value) ? "off" : (Telemetry.Failed ? "FAILED (see BepInEx log)" : ((Telemetry.FilePath.Length == 0) ? "on (file starts at first landing)" : Path.GetFileName(Telemetry.FilePath)))); nowLines.Add("telemetry: " + text3); if (Diagnostics.LastProblem.Length > 0) { nowLines.Add("problem seen: " + Diagnostics.LastProblem); } } private void SetOpen(bool value) { open = value; IsOpen = value; PlayerControllerB val = LocalPlayer(); if (open) { message = ""; statusTimer = 0f; if ((Object)(object)val != (Object)null && !val.disableLookInput) { val.disableLookInput = true; weDisabledLook = true; } return; } if ((Object)(object)val != (Object)null) { if (weDisabledLook) { val.disableLookInput = false; } if (!val.quickMenuManager.isMenuOpen && !val.inTerminalMenu && !val.inSpecialMenu) { Cursor.lockState = (CursorLockMode)1; Cursor.visible = false; } } weDisabledLook = false; } private void OnGUI() { //IL_00a4: Unknown result type (might be due to invalid IL or missing references) //IL_00b0: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Expected O, but got Unknown //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_00df: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Expected O, but got Unknown //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Expected O, but got Unknown //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Expected O, but got Unknown //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Expected O, but got Unknown if (open) { if (small == null) { small = new GUIStyle(GUI.skin.label) { fontSize = 11, wordWrap = true, richText = false }; small.margin = new RectOffset(2, 2, 0, 0); small.padding = new RectOffset(2, 2, 1, 1); heading = new GUIStyle(small) { fontStyle = (FontStyle)1 }; } ((Rect)(ref window)).width = 460f; ((Rect)(ref window)).height = 540f; window = GUILayout.Window(((Object)this).GetInstanceID(), window, new WindowFunction(DrawWindow), "Sanity debug (F7 closes)", (GUILayoutOption[])(object)new GUILayoutOption[2] { GUILayout.Width(460f), GUILayout.Height(540f) }); } } private void DrawWindow(int id) { //IL_00d9: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) PlayerControllerB player = LocalPlayer(); GUIStyle val = small; string text = (SanityMeter.Frozen ? " FROZEN" : "") + (SanityDirector.Paused ? " PAUSED" : "") + (SanityDirector.IgnoreConditions ? " NO-CHECKS" : "") + (SanityDirector.ChainsAlways ? " CHAINS-ALWAYS" : ""); GUILayout.Label(FormattableString.Invariant($"Sanity {SanityMeter.Value:F1} {SanityMeter.Tier} {SanityMeter.Rate:+0.00;-0.00}/s fear {SanityMeter.Fear:F2}{text}"), heading, Array.Empty<GUILayoutOption>()); tab = GUILayout.Toolbar(tab, Tabs, Array.Empty<GUILayoutOption>()); scroll = GUILayout.BeginScrollView(scroll, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(420f) }); switch (tab) { case 0: DrawNow(val); break; case 1: DrawSymptoms(player, val); break; case 2: DrawLog(val); break; default: DrawTools(val); break; } GUILayout.EndScrollView(); string text2 = ((GUI.tooltip.Length > 0) ? GUI.tooltip : message); GUILayout.Label(text2, val, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Height(34f) }); GUI.DragWindow(); } private void DrawNow(GUIStyle text) { foreach (string nowLine in nowLines) { if (nowLine.StartsWith("#")) { GUILayout.Label(nowLine.Substring(1), heading, Array.Empty<GUILayoutOption>()); } else { GUILayout.Label(nowLine, text, Array.Empty<GUILayoutOption>()); } } } private void DrawSymptoms(PlayerControllerB? player, GUIStyle text) { //IL_015f: Unknown result type (might be due to invalid IL or missing references) //IL_016f: Expected O, but got Unknown GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUILayout.Label("Fire at:", text, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(48f) }); strength = GUILayout.Toolbar(strength, StrengthNames, Array.Empty<GUILayoutOption>()); GUILayout.EndHorizontal(); GUILayout.Label("Tick = allowed naturally (saved). Button = fire / arm now. Hover a line for all of it.", text, Array.Empty<GUILayoutOption>()); for (int i = 0; i < SanityDirector.Symptoms.Count; i++) { Symptom symptom = SanityDirector.Symptoms[i]; GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); if (symptom.EnabledEntry != null) { bool flag = GUILayout.Toggle(symptom.EnabledEntry.Value, "", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(16f) }); if (flag != symptom.EnabledEntry.Value) { symptom.EnabledEntry.Value = flag; statusTimer = 0f; } } if (GUILayout.Button(symptom.Name, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(112f) })) { message = (((Object)(object)player == (Object)null) ? "Not in a game." : SanityDirector.DebugFire(player, symptom, StrengthTier())); statusTimer = 0f; } string text2 = ((i < statuses.Length) ? statuses[i] : ""); GUILayout.Label(new GUIContent((i < statusesShort.Length) ? statusesShort[i] : "", symptom.Name + ": " + text2), text, Array.Empty<GUILayoutOption>()); GUILayout.EndHorizontal(); } } private void DrawLog(GUIStyle text) { GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); logFilter = GUILayout.Toolbar(logFilter, LogFilters, Array.Empty<GUILayoutOption>()); if (GUILayout.Button("Clear", (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(48f) })) { DebugLog.Clear(); } GUILayout.EndHorizontal(); string text2 = LogKinds[logFilter]; IReadOnlyList<DebugLog.Entry> all = DebugLog.All; int num = 0; for (int num2 = all.Count - 1; num2 >= 0; num2--) { DebugLog.Entry entry = all[num2]; if (text2.Length <= 0 || !(entry.Kind != text2) || (text2 == "symptom" && entry.Kind == "chain")) { GUILayout.Label(FormattableString.Invariant($"{Ago(Time.time - entry.At),5} {entry.Mark,-2} {entry.Text}"), text, Array.Empty<GUILayoutOption>()); num++; } } if (num == 0) { GUILayout.Label("Nothing yet.", text, Array.Empty<GUILayoutOption>()); } GUILayout.Label("> played + armed -> chained follow-up x dropped/failed . detail ! problem", text, Array.Empty<GUILayoutOption>()); } private void DrawTools(GUIStyle text) { GUILayout.Label("Set sanity", heading, Array.Empty<GUILayoutOption>()); GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); float[] sanityPresets = SanityPresets; for (int i = 0; i < sanityPresets.Length; i++) { float value = sanityPresets[i]; if (GUILayout.Button(value.ToString("0"), Array.Empty<GUILayoutOption>())) { message = SanityMeter.DebugSetSanity(value); } } GUILayout.EndHorizontal(); GUILayout.Label("Tests", heading, Array.Empty<GUILayoutOption>()); Toggle(ref SanityMeter.Frozen, "Freeze sanity", "sanity frozen", "sanity unfrozen"); Toggle(ref SanityDirector.Paused, "Pause natural symptoms", "director paused", "director resumed"); Toggle(ref SanityDirector.IgnoreGapsNatural, "Ignore gaps: natural rolls (recovery check, fear gap)", "gaps ignored (natural)", "gaps restored (natural)"); Toggle(ref SanityDirector.IgnoreGapsForced, "Ignore gaps: F8 and buttons", "gaps ignored (forced)", "gaps restored (forced)"); Toggle(ref SanityDirector.IgnoreConditions, "Ignore conditions: F8 and buttons skip every check (may look wrong)", "conditions ignored (forced)", "conditions restored (forced)"); Toggle(ref SanityDirector.ChainsAlways, "Chains always follow (every link rolls in)", "chains always", "chains by chance"); GUILayout.Label("Armed from a button: no 5 s gap or relax wait (tier still applies unless Ignore conditions).", text, Array.Empty<GUILayoutOption>()); GUILayout.Label("Display", heading, Array.Empty<GUILayoutOption>()); ConfigToggle(Plugin.ShowReadout, "On-screen readout"); ConfigToggle(Plugin.ShowPresence, "Show the Presence (a red ball)"); if (PhantomAudio.Loudness != null) { GUILayout.BeginHorizontal(Array.Empty<GUILayoutOption>()); GUILayout.Label(FormattableString.Invariant($"Phantom loudness {PhantomAudio.Loudness.Value:0.00}"), text, (GUILayoutOption[])(object)new GUILayoutOption[1] { GUILayout.Width(150f) }); float num = GUILayout.HorizontalSlider(PhantomAudio.Loudness.Value, 0.25f, 4f, Array.Empty<GUILayoutOption>()); if (Mathf.Abs(num - PhantomAudio.Loudness.Value) > 0.049f) { PhantomAudio.Loudness.Value = Mathf.Round(num * 10f) / 10f; } GUILayout.EndHorizontal(); } } private static void Toggle(ref bool value, string label, string on, string off) { bool flag = GUILayout.Toggle(value, " " + label, Array.Empty<GUILayoutOption>()); if (flag != value) { value = flag; Telemetry.Event("DEBUG", flag ? 1f : 0f, flag ? on : off); } } private static void ConfigToggle(ConfigEntry<bool> entry, string label) { bool flag = GUILayout.Toggle(entry.Value, " " + label, Array.Empty<GUILayoutOption>()); if (flag != entry.Value) { entry.Value = flag; } } private SanityTier StrengthTier() { if (strength > 0) { return (SanityTier)strength; } if (SanityMeter.Tier != SanityTier.Calm) { return SanityMeter.Tier; } return SanityTier.Uneasy; } private static string Short(string value, int length) { if (value.Length > length) { return value.Substring(0, length - 1) + "~"; } return value; } private static string Ago(float seconds) { if (!(seconds > 9000f)) { if (!(seconds >= 60f)) { return FormattableString.Invariant($"{seconds:F0}s"); } return FormattableString.Invariant($"{seconds / 60f:F0}m"); } return "-"; } private static PlayerControllerB? LocalPlayer() { GameNetworkManager instance = GameNetworkManager.Instance; if (!((Object)(object)instance != (Object)null)) { return null; } return instance.localPlayerController; } } internal static class Diagnostics { private const float CheckSeconds = 0.5f; private const float ReportGapSeconds = 30f; private static float nextCheck; private static float lastReport = -9999f; private static int missingChecks; internal static string LastProblem { get; private set; } = ""; internal static void Tick(PlayerControllerB player) { if (Time.time < nextCheck || player.isPlayerDead || (Object)(object)player.cursorTip == (Object)null) { return; } nextCheck = Time.time + 0.5f; if (player.isGrabbingObjectAnimation || player.inSpecialMenu || player.inTerminalMenu || player.isTypingChat || ((Object)(object)player.quickMenuManager != (Object)null && player.quickMenuManager.isMenuOpen) || player.inSpecialInteractAnimation || player.twoHanded) { missingChecks = 0; return; } string text = Expected(player); bool flag = ((Behaviour)player.cursorTip).enabled && ((Component)player.cursorTip).gameObject.activeInHierarchy && !string.IsNullOrEmpty(((TMP_Text)player.cursorTip).text); if (text.Length == 0 || flag) { missingChecks = 0; return; } missingChecks++; if (missingChecks >= 3 && !(Time.time - lastReport < 30f)) { lastReport = Time.time; LastProblem = FormattableString.Invariant($"{Time.time:F0}s: no prompt over {text}"); Telemetry.Event("PROMPT_MISSING", 0f, FormattableString.Invariant($"over {text}; tip enabled {((Behaviour)player.cursorTip).enabled} active {((Component)player.cursorTip).gameObject.activeInHierarchy} text \"{((TMP_Text)player.cursorTip).text}\"; ") + FormattableString.Invariant($"sanity: debug menu {(DebugMenu.IsOpen ? 1 : 0)}, presence {(PresenceSymptom.Active ? 1 : 0)}, last symptom {SanityDirector.LastSymptom}, fake hp {PhantomHitSymptom.Lost}")); } } private static string Expected(PlayerControllerB player) { //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_0087: Unknown result type (might be due to invalid IL or missing references) InteractTrigger hoveringOverTrigger = player.hoveringOverTrigger; if ((Object)(object)hoveringOverTrigger != (Object)null && (player.isHoldingInteract || hoveringOverTrigger.isPlayingSpecialAnimation)) { return ""; } if ((Object)(object)hoveringOverTrigger != (Object)null) { string value = (hoveringOverTrigger.interactable ? hoveringOverTrigger.hoverTip : hoveringOverTrigger.disabledHoverTip); if (!string.IsNullOrEmpty(value)) { return "a trigger (" + ((Object)((Component)hoveringOverTrigger).gameObject).name + ")"; } return ""; } Transform transform = ((Component)player.gameplayCamera).transform; RaycastHit val = default(RaycastHit); if (Physics.Raycast(new Ray(transform.position, transform.forward), ref val, player.grabDistance, player.interactableObjectsMask) && ((Component)((RaycastHit)(ref val)).collider).gameObject.layer != 8 && ((Component)((RaycastHit)(ref val)).collider).gameObject.layer != 30 && ((Component)((RaycastHit)(ref val)).collider).CompareTag("PhysicsProp")) { GrabbableObject component = ((Component)((RaycastHit)(ref val)).collider).GetComponent<GrabbableObject>(); if (!((Object)(object)component != (Object)null) || !component.grabbable) { return ""; } return "an item (" + ((Object)component).name + ")"; } return ""; } } internal static class DayMood { private struct Mood { internal string Name; internal float Rate; internal float Sound; internal float Sight; internal float Body; internal float Odds; } private static readonly Mood[] Moods = new Mood[6] { new Mood { Name = "steady", Rate = 1f, Sound = 1f, Sight = 1f, Body = 1f, Odds = 0.3f }, new Mood { Name = "whispers", Rate = 1f, Sound = 1.5f, Sight = 0.8f, Body = 0.8f, Odds = 0.175f }, new Mood { Name = "tricks", Rate = 1f, Sound = 0.8f, Sight = 1.5f, Body = 0.8f, Odds = 0.175f }, new Mood { Name = "nerves", Rate = 1f, Sound = 0.8f, Sight = 0.8f, Body = 1.5f, Odds = 0.175f }, new Mood { Name = "quiet", Rate = 0.85f, Sound = 1f, Sight = 1f, Body = 1f, Odds = 0.0875f }, new Mood { Name = "restless", Rate = 1.15f, Sound = 1f, Sight = 1f, Body = 1f, Odds = 0.0875f } }; private static int index = -1; internal static string Name { get { if (index < 0) { return "none"; } return Moods[index].Name; } } internal static float Rate { get { if (index < 0) { return 1f; } return Moods[index].Rate; } } internal static string TuningSummary { get { List<string> list = new List<string>(); Mood[] moods = Moods; for (int i = 0; i < moods.Length; i++) { Mood mood = moods[i]; list.Add(FormattableString.Invariant($"{mood.Name} {mood.Odds:0.###} rate x{mood.Rate} sound/sight/body x{mood.Sound}/{mood.Sight}/{mood.Body}")); } return "moods " + string.Join(", ", list); } } internal static float Tilt(string family) { if (index < 0) { return 1f; } Mood mood = Moods[index]; return family switch { "body" => mood.Body, "sight" => mood.Sight, "sound" => mood.Sound, _ => 1f, }; } internal static void EnsureRolled() { if (index >= 0) { return; } float num = Random.value; index = Moods.Length - 1; for (int i = 0; i < Moods.Length; i++) { num -= Moods[i].Odds; if (num <= 0f) { index = i; break; } } Mood mood = Moods[index]; Telemetry.Event("MOOD", mood.Rate, FormattableString.Invariant($"{mood.Name}: rate x{mood.Rate} sound x{mood.Sound} sight x{mood.Sight} body x{mood.Body}")); } internal static void Reset() { index = -1; } } internal static class Waves { private const float HighMultiplier = 1.6f; private const float LowMultiplier = 0.4f; private const float HighMeanSeconds = 120f; private const float LowMeanSeconds = 90f; internal static bool High { get; private set; } = true; internal static float Multiplier { get { if (!High) { return 0.4f; } return 1.6f; } } internal static string TuningSummary => FormattableString.Invariant($"waves x{1.6f} ~{120f}s / x{0.4f} ~{90f}s"); internal static void Tick(float dt) { if (Random.value < dt / (High ? 120f : 90f)) { High = !High; } } internal static void Reset() { High = Random.value < 0.5714286f; } } internal static class Chains { private sealed class Link { internal string From = ""; internal string Trigger = ""; internal string To = ""; internal float Chance; internal SanityTier MinTier; internal float MinDelay; internal float MaxDelay; internal Func<PlayerControllerB, SanityTier, Vector3, Vector3, string> Play = (PlayerControllerB _, SanityTier _, Vector3 _, Vector3 _) => "no action"; } private struct Pending { internal Link Link; internal float At; internal SanityTier Tier; internal Vector3 Point; internal Vector3 Direction; } private static readonly List<Pending> Queue = new List<Pending>(); private static readonly Link[] Links = new Link[4] { new Link { From = "PhantomSteps", Trigger = "reached you", To = "PhantomHit", Chance = 0.6f, MinTier = SanityTier.Breaking, MinDelay = 0.1f, MaxDelay = 0.3f, Play = (PlayerControllerB p, SanityTier t, Vector3 _, Vector3 _) => PlayHit(p, t, "the rush reached you") }, new Link { From = "PhantomWalkie", Trigger = "death static", To = "PhantomDread", Chance = 0.5f, MinTier = SanityTier.Breaking, MinDelay = 3f, MaxDelay = 6f, Play = (PlayerControllerB p, SanityTier t, Vector3 _, Vector3 _) => PlayCard("PhantomDread", p, t) }, new Link { From = "PhantomSound", Trigger = "approaching", To = "Flicker", Chance = 0.3f, MinTier = SanityTier.Breaking, MinDelay = 0.5f, MaxDelay = 1.5f, Play = (PlayerControllerB p, SanityTier t, Vector3 _, Vector3 _) => FlickerSymptom.FlickNow(p, t) }, new Link { From = "LostTime", Trigger = "shown", To = "PhantomWalkie", Chance = 0.5f, MinTier = SanityTier.Uneasy, MinDelay = 3f, MaxDelay = 6f, Play = (PlayerControllerB p, SanityTier _, Vector3 _, Vector3 _) => PlayCard("PhantomWalkie", p, SanityTier.Uneasy) } }; internal static string TuningSummary { get { List<string> list = new List<string>(); Link[] links = Links; foreach (Link link in links) { list.Add(FormattableString.Invariant($"{link.From}:{link.Trigger}->{link.To} {link.Chance:0.##} {link.MinTier}+")); } return "chains " + string.Join(", ", list); } } internal static string PendingSummary { get { if (Queue.Count == 0) { return ""; } List<string> list = new List<string>(); foreach (Pending item in Queue) { list.Add(FormattableString.Invariant($"{item.Link.From} -> {item.Link.To} in {Mathf.Max(0f, item.At - Time.time):F1}s")); } return string.Join(", ", list); } } internal static void Trigger(Symptom from, string trigger, Vector3 point = default(Vector3), Vector3 direction = default(Vector3)) { //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Unknown result type (might be due to invalid IL or missing references) SanityTier tier = SanityMeter.Tier; Link[] links = Links; foreach (Link link in links) { if (!(link.From != from.ConfigKey) && !(link.Trigger != trigger) && tier >= link.MinTier && (SanityDirector.ChainsAlways || !(Random.value >= link.Chance))) { Symptom symptom = SanityDirector.Find(link.To); if (symptom == null || symptom.Enabled) { Queue.Add(new Pending { Link = link, At = Time.time + Random.Range(link.MinDelay, link.MaxDelay), Tier = tier, Point = point, Direction = direction }); Telemetry.Event("CHAIN", 0f, link.From + " " + link.Trigger + " -> " + link.To + " scheduled"); } } } } internal static void Tick(PlayerControllerB player) { //IL_0057: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) for (int num = Queue.Count - 1; num >= 0; num--) { Pending pending = Queue[num]; if (!(Time.time < pending.At)) { Queue.RemoveAt(num); if (!player.isPlayerDead) { string text = pending.Link.Play(player, pending.Tier, pending.Point, pending.Direction); Link link = pending.Link; if (text.Length == 0) { Symptom symptom = SanityDirector.Find(link.To); if (symptom != null) { SanityDirector.NoteChained(symptom, pending.Tier, link.From); } Telemetry.Event("CHAIN", 1f, link.From + " " + link.Trigger + " -> " + link.To + " played"); } else { Telemetry.Event("CHAIN", 0f, link.From + " " + link.Trigger + " -> " + link.To + " didn't play: " + text); } } } } } internal static void Reset() { Queue.Clear(); } private static string PlayCard(string key, PlayerControllerB player, SanityTier tier) { Symptom symptom = SanityDirector.Find(key); if (symptom == null) { return "no such symptom"; } string text = symptom.CantPlayReason(player); if (text.Length > 0) { return text; } symptom.LastFailure = ""; if (!symptom.TryPlay(player, tier)) { return symptom.LastFailure; } return ""; } private static string PlayHit(PlayerControllerB player, SanityTier tier, string why) { if (!(SanityDirector.Find("PhantomHit") is PhantomHitSymptom phantomHitSymptom)) { return "no such symptom"; } string text = phantomHitSymptom.CantPlayReason(player); if (text.Length > 0) { return text; } if (!phantomHitSymptom.Start(player, tier, withCause: false, why)) { return phantomHitSymptom.LastFailure; } return ""; } } internal static class EligibilityLog { private const float SampleSeconds = 5f; private static readonly string[] TierLetters = new string[4] { "C", "U", "D", "B" }; private static readonly string[] Places = new string[3] { "inside", "outside", "ship" }; private static float timer; private static readonly float[] sampledByTier = new float[4]; private static readonly float[] sampledByPlace = new float[3]; private static float[,] eligibleByTier = new float[0, 4]; private static float[,] eligibleByPlace = new float[0, 3]; private static Dictionary<string, float>[] blockedBy = Array.Empty<Dictionary<string, float>>(); private static readonly StringBuilder Text = new StringBuilder(640); internal static void Tick(PlayerControllerB player, float dt) { if (!Plugin.TelemetryEnabled.Value) { return; } timer += dt; if (timer < 5f) { return; } float num = timer; timer = 0f; IReadOnlyList<Symptom> symptoms = SanityDirector.Symptoms; EnsureSize(symptoms.Count); int tier = (int)SanityMeter.Tier; int num2 = ((!SanityMeter.Inside) ? ((!SanityMeter.InShip) ? 1 : 2) : 0); sampledByTier[tier] += num; sampledByPlace[num2] += num; for (int i = 0; i < symptoms.Count; i++) { string text = SanityDirector.NaturalWhyNot(symptoms[i], player); if (text.Length == 0) { eligibleByTier[i, tier] += num; eligibleByPlace[i, num2] += num; } else { string key = Kind(text); blockedBy[i].TryGetValue(key, out var value); blockedBy[i][key] = value + num; } } } internal static string Summary() { IReadOnlyList<Symptom> symptoms = SanityDirector.Symptoms; EnsureSize(symptoms.Count); Text.Clear(); Text.Append("sampled"); AppendTiersAndPlaces(sampledByTier, sampledByPlace); for (int i = 0; i < symptoms.Count; i++) { Text.Append(" | ").Append(symptoms[i].ConfigKey); for (int j = 1; j < TierLetters.Length; j++) { Text.Append(FormattableString.Invariant($" {TierLetters[j]}={eligibleByTier[i, j]:F0}")); } for (int k = 0; k < Places.Length; k++) { Text.Append(FormattableString.Invariant($" {Places[k]}={eligibleByPlace[i, k]:F0}")); } Text.Append(" blocked="); bool flag = true; foreach (KeyValuePair<string, float> item in blockedBy[i]) { Text.Append(flag ? "" : "/").Append(item.Key).Append(FormattableString.Invariant($":{item.Value:F0}")); flag = false; } } return Text.ToString(); } internal static void Reset() { timer = 0f; Array.Clear(sampledByTier, 0, sampledByTier.Length); Array.Clear(sampledByPlace, 0, sampledByPlace.Length); Array.Clear(eligibleByTier, 0, eligibleByTier.Length); Array.Clear(eligibleByPlace, 0, eligibleByPlace.Length); Dictionary<string, float>[] array = blockedBy; foreach (Dictionary<string, float> dictionary in array) { dictionary.Clear(); } } private static void AppendTiersAndPlaces(float[] byTier, float[] byPlace) { for (int i = 1; i < TierLetters.Length; i++) { Text.Append(FormattableString.Invariant($" {TierLetters[i]}={byTier[i]:F0}")); } for (int j = 0; j < Places.Length; j++) { Text.Append(FormattableString.Invariant($" {Places[j]}={byPlace[j]:F0}")); } } private static string Kind(string reason) { int num = reason.IndexOf(':'); if (num <= 0) { return reason; } return reason.Substring(0, num); } private static void EnsureSize(int count) { if (blockedBy.Length != count) { eligibleByTier = new float[count, TierLetters.Length]; eligibleByPlace = new float[count, Places.Length]; blockedBy = new Dictionary<string, float>[count]; for (int i = 0; i < count; i++) { blockedBy[i] = new Dictionary<string, float>(); } } } } internal static class InputWatch { private static readonly Dictionary<string, InputAction> Actions = new Dictionary<string, InputAction>(); internal static bool Pressed(string name) { try { if (!Actions.TryGetValue(name, out InputAction value) || value == null) { InputActionAsset actions = InputSystem.actions; value = ((actions != null) ? actions.FindAction(name, false) : null); if (value == null) { return false; } Actions[name] = value; } return value.WasPressedThisFrame(); } catch (Exception) { return false; } } } internal static class Insights { private static float alive; private static float inside; private static float outside; private static float ship; private static float aloneInside; private static float aloneOutside; private static float sprint; private static float distance; private static float turnInside; private static float turnOutside; private static float turnShip; private static float lightInside; private static float walkieCarried; private static float still; private static int scans; private static int uses; private static int walkiePresses; private static int trips; private static readonly List<float> TripLengths = new List<float>(); private static float tripStart = -1f; private static float firstEntry = -1f; private static float lastFraction; private static bool wasInside; private static bool wasTalking; private static float lastYaw; private static bool hasYaw; private static Vector3 lastPos; private static bool hasPos; private static readonly Dictionary<string, int> DoorHovers = new Dictionary<string, int>(); private static InteractTrigger? lastTrigger; private static int itemMoments; private static int lightLooks; private static int longExits; private static int longExitsBelievable; private static readonly int[] ItemByTier = new int[4]; private static readonly int[] LightByTier = new int[4]; private static readonly int[] DoorByTier = new int[4]; private static readonly int[] ExitByTier = new int[4]; private static float nextItemScan; private static float nextLightScan; private static GrabbableObject? lastLooked; private static bool lastInReach; private static bool lookingAtLight; private static readonly StringBuilder Text = new StringBuilder(600); internal static void Reset() { alive = (inside = (outside = (ship = (aloneInside = (aloneOutside = (sprint = (distance = 0f))))))); turnInside = (turnOutside = (turnShip = (lightInside = (walkieCarried = (still = 0f))))); scans = (uses = (walkiePresses = (trips = 0))); TripLengths.Clear(); tripStart = (firstEntry = -1f); lastFraction = 0f; wasInside = (wasTalking = (hasYaw = (hasPos = false))); DoorHovers.Clear(); lastTrigger = null; itemMoments = (lightLooks = (longExits = (longExitsBelievable = 0))); Array.Clear(ItemByTier, 0, 4); Array.Clear(LightByTier, 0, 4); Array.Clear(DoorByTier, 0, 4); Array.Clear(ExitByTier, 0, 4); lastLooked = null; lookingAtLight = false; } internal static void Tick(PlayerControllerB player, float dayTime) { //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0090: 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_0097: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) float deltaTime = Time.deltaTime; int tier = (int)SanityMeter.Tier; bool isInsideFactory = player.isInsideFactory; bool isInHangarShipRoom = player.isInHangarShipRoom; alive += deltaTime; lastFraction = SanityMeter.DayFraction; float y = ((Component)player.gameplayCamera).transform.eulerAngles.y; float num = (hasYaw ? Mathf.Abs(Mathf.DeltaAngle(lastYaw, y)) : 0f); lastYaw = y; hasYaw = true; Vector3 position = ((Component)player).transform.position; Vector3 val = (hasPos ? (position - lastPos) : Vector3.zero); val.y = 0f; float num2 = ((((Vector3)(ref val)).magnitude < 4f) ? ((Vector3)(ref val)).magnitude : 0f); lastPos = position; hasPos = true; distance += num2; if (deltaTime > 0f && num2 / deltaTime < 0.3f) { still += deltaTime; } if (isInsideFactory) { inside += deltaTime; turnInside += num; if (!SanityMeter.NearTeammate) { aloneInside += deltaTime; } if (Reactions.LightOn(player)) { lightInside += deltaTime; } } else if (isInHangarShipRoom) { ship += deltaTime; turnShip += num; } else { outside += deltaTime; turnOutside += num; if (!SanityMeter.NearTeammate) { aloneOutside += deltaTime; } } if (player.isSprinting) { sprint += deltaTime; } if (InputWatch.Pressed("PingScan")) { scans++; } if (InputWatch.Pressed("Interact")) { uses++; } bool speakingToWalkieTalkie = player.speakingToWalkieTalkie; if (speakingToWalkieTalkie && !wasTalking) { walkiePresses++; } wasTalking = speakingToWalkieTalkie; if (CarriesWalkie(player)) { walkieCarried += deltaTime; } if (isInsideFactory && !wasInside) { trips++; tripStart = alive; if (firstEntry < 0f) { firstEntry = dayTime; } } if (!isInsideFactory && wasInside && tripStart >= 0f) { float num3 = alive - tripStart; TripLengths.Add(num3); if (num3 >= 180f && !isInHangarShipRoom) { longExits++; ExitByTier[tier]++; if (LostTimeSymptom.LateEnough) { longExitsBelievable++; } } } wasInside = isInsideFactory; InteractTrigger hoveringOverTrigger = player.hoveringOverTrigger; if ((Object)(object)hoveringOverTrigger != (Object)(object)lastTrigger) { lastTrigger = hoveringOverTrigger; if ((Object)(object)hoveringOverTrigger != (Object)null && isInsideFactory) { string text = FakeLockedDoorSymptom.Assess(player, hoveringOverTrigger); if (text != "not a door") { string key = ((text.Length == 0) ? "would fire" : text); DoorHovers[key] = ((!DoorHovers.TryGetValue(key, out var value)) ? 1 : (value + 1)); if (text.Length == 0) { DoorByTier[tier]++; } } } } if (Time.time >= nextItemScan && !isInHangarShipRoom) { nextItemScan = Time.time + 0.25f; float reachDistance; Bounds bounds; GrabbableObject val2 = VanishingItemSymptom.LookedAtItem(player, out reachDistance, out bounds); bool flag = (Object)(object)val2 != (Object)null && reachDistance <= player.grabDistance + 1f; if (flag && (!((Object)(object)val2 == (Object)(object)lastLooked) || !lastInReach)) { itemMoments++; ItemByTier[tier]++; } lastLooked = val2; lastInReach = flag; } if (Time.time >= nextLightScan && isInsideFactory) { nextLightScan = Time.time + 0.5f; RoundManager instance = RoundManager.Instance; string reason; bool flag2 = (Object)(object)instance != (Object)null && FlickerSymptom.FacilityHasPower(instance) && FlickerSymptom.NearestVisibleLight(player, instance, out reason) >= 0f; if (flag2 && !lookingAtLight) { lightLooks++; LightByTier[tier]++; } lookingAtLight = flag2; } } internal static void WriteDay(int day) { if (alive <= 1f) { return; } float num = alive / 60f; TripLengths.Sort(); float num2 = ((TripLengths.Count > 0) ? TripLengths[TripLengths.Count / 2] : 0f); Text.Clear(); Text.Append(FormattableString.Invariant($"alive={alive:F0}s inside={Share(inside)} outside={Share(outside)} ship={Share(ship)} ")).Append(FormattableString.Invariant($"alone_inside={Share(aloneInside, inside)} alone_outside={Share(aloneOutside, outside)} ")).Append(FormattableString.Invariant($"speed={distance / Mathf.Max(1f, alive):F2} still={Share(still)} sprint={Share(sprint)} ")) .Append(FormattableString.Invariant($"turn_inside={turnInside / Mathf.Max(1f, inside):F0} turn_outside={turnOutside / Mathf.Max(1f, outside):F0} turn_ship={turnShip / Mathf.Max(1f, ship):F0} ")) .Append(FormattableString.Invariant($"scans_min={(float)scans / num:F2} use_min={(float)uses / num:F2} walkie_min={(float)walkiePresses / num:F2} walkie_carried={Share(walkieCarried)} ")) .Append(FormattableString.Invariant($"light_inside={Share(lightInside, inside)} trips={trips} trip_median={num2:F0}s first_entry={firstEntry:F0}s left_at={lastFraction:F2}")); Telemetry.Event("DAY_PROFILE", day, Text.ToString()); Text.Clear(); Text.Append(FormattableString.Invariant($"minutes={num:F1} doors")); foreach (KeyValuePair<string, int> doorHover in DoorHovers) { Text.Append(' ').Append(doorHover.Key.Replace(' ', '_')).Append('=') .Append(doorHover.Value); } Text.Append(FormattableString.Invariant($" | items={itemMoments} lights={lightLooks} long_exits={longExits} long_exits_after_noon={longExitsBelievable}")).Append(" | by tier C/U/D/B: door ").Append(Join(DoorByTier)) .Append(" item ") .Append(Join(ItemByTier)) .Append(" light ") .Append(Join(LightByTier)) .Append(" exit ") .Append(Join(ExitByTier)); Telemetry.Event("DAY_OPPORTUNITIES", day, Text.ToString()); } private static string Share(float part, float whole = -1f) { float num = ((whole < 0f) ? alive : whole); if (!(num > 0f)) { return "0"; } return FormattableString.Invariant($"{part / num:F2}"); } private static string Join(int[] values) { return string.Join("/", values); } private static bool CarriesWalkie(PlayerControllerB player) { if (player.ItemSlots == null) { return false; } GrabbableObject[] itemSlots = player.ItemSlots; foreach (GrabbableObject val in itemSlots) { if (val is WalkieTalkie) { return true; } } return false; } } internal static class LootRecovery { private sealed class Haul { internal PlayerControllerB Finder; internal readonly List<PlayerControllerB> Haulers = new List<PlayerControllerB>(); } private sealed class EarlyDelivery { internal string Name = ""; internal int Value; internal float Share; internal string Role = ""; } private const float Pool = 50f; private const float MinPerItem = 1f; private const float MaxPerItem = 15f; private const float FinderShare = 0.65f; private const float DailySoftLimit = 30f; private const float FallbackDelay = 20f; private const float StaleSeconds = 300f; private const float PollSeconds = 0.2f; private static readonly Dictionary<GrabbableObject, Haul> hauls = new Dictionary<GrabbableObject, Haul>(); private static readonly List<GrabbableObject> arrived = new List<GrabbableObject>(); private static readonly List<EarlyDelivery> early = new List<EarlyDelivery>(); private static bool onMoon; private static float secondsOnMoon; private static float dayStartedAt; private static float pollTimer; private static int[]? syncedValues; private static float syncedAt; private static bool syncApplied; internal static int LevelTotal { get; private set; } internal static int LevelItems { get; private set; } internal static int PlayersAtLanding { get; private set; } = 1; internal static float GainedToday { get; private set; } internal static float FairShare { get { if (LevelTotal <= 0) { return 0f; } return (float)LevelTotal / (float)PlayersAtLanding; } } internal static string CountSource { get; private set; } = ""; internal static string TuningSummary => FormattableString.Invariant($"pool {50f} per item {1f}-{15f} finder {0.65f} soft limit {30f} count from scrap sync (fallback {20f}s)"); internal static void Tick(PlayerControllerB me) { StartOfRound instance = StartOfRound.Instance; if (!SanityMeter.OnMoon || (Object)(object)instance == (Object)null) { if (onMoon) { ResetDay(); } return; } if (!onMoon) { onMoon = true; dayStartedAt = Time.time; } secondsOnMoon += Time.deltaTime; if (!syncApplied && syncedValues != null) { if (Time.time - syncedAt <= 300f) { ApplySync(instance); } else { syncedValues = null; } } if (CountSource.Length == 0 && secondsOnMoon >= 20f) { CountScene(instance, out var items, out var total); SetCount(instance, total, items, "fallback"); } pollTimer -= Time.deltaTime; if (!(pollTimer > 0f)) { pollTimer = 0.2f; TrackHolders(instance); CheckArrivals(instance, me); } } internal static void OnScrapValuesSynced(int[]? values) { if (values != null && !syncApplied) { syncedValues = values; syncedAt = Time.time; StartOfRound instance = StartOfRound.Instance; if (onMoon && (Object)(object)instance != (Object)null) { ApplySync(instance); } } } private static void ApplySync(StartOfRound startOfRound) { int[] array = syncedValues; syncedValues = null; syncApplied = true; int num = 0; int[] array2 = array; foreach (int num2 in array2) { num += num2; } CountScene(startOfRound, out var items, out var total); float num3 = syncedAt - dayStartedAt; Telemetry.Event("SCRAP_SYNC", num, FormattableString.Invariant(FormattableStringFactory.Create("items {0} total {1} at {2:F1}s after landing (scene now {3} items worth {4}) replaces {5}", array.Length, num, num3, items, total, (CountSource.Length > 0) ? CountSource : "nothing"))); SetCount(startOfRound, num, array.Length, "sync"); } private static void SetCount(StartOfRound startOfRound, int total, int items, string source) { LevelTotal = total; LevelItems = items; CountSource = source; PlayersAtLanding = startOfRound.connectedPlayersAmount + 1; float num = ((items > 0) ? ((float)total / (float)items) : 0f); Telemetry.Event("LEVEL_SCRAP", total, FormattableString.Invariant($"total {total} items {items} average {num:F0} players {PlayersAtLanding} fair share {FairShare:F0} source {source}")); if (early.Count == 0 || FairShare <= 0f) { return; } GameNetworkManager instance = GameNetworkManager.Instance; PlayerControllerB val = (((Object)(object)instance != (Object)null) ? instance.localPlayerController : null); if ((Object)(object)val != (Object)null && Alive(val)) { foreach (EarlyDelivery item in early) { PayOut(item.Name, item.Value, item.Share, item.Role, " (let go before the count)"); } } early.Clear(); } private static void TrackHolders(StartOfRound startOfRound) { PlayerControllerB[] allPlayerScripts = startOfRound.allPlayerScripts; if (allPlayerScripts == null) { return; } PlayerControllerB[] array = allPlayerScripts; foreach (PlayerControllerB val in array) { if ((Object)(object)val == (Object)null || !val.isPlayerControlled || val.isPlayerDead || val.ItemSlots == null) { continue; } GrabbableObject[] itemSlots = val.ItemSlots; foreach (GrabbableObject val2 in itemSlots) { if ((Object)(object)val2 == (Object)null) { continue; } if (hauls.TryGetValue(val2, out Haul value)) { if ((Object)(object)val != (Object)(object)value.Finder && !value.Haulers.Contains(val)) { value.Haulers.Add(val); } } else if (IsFresh(val2, startOfRound)) { hauls[val2] = new Haul { Finder = val }; } } } } private static void CheckArrivals(StartOfRound startOfRound, PlayerControllerB me) { arrived.Clear(); foreach (KeyValuePair<GrabbableObject, Haul> haul in hauls) { GrabbableObject key = haul.Key; if ((Object)(object)key == (Object)null || (!key.isHeld && !key.isPocketed && (key.isInShipRoom || InShip(key, startOfRound)))) { arrived.Add(key); } } foreach (GrabbableObject item in arrived) { if ((Object)(object)item != (Object)null && hauls.TryGetValue(item, out Haul value)) { Pay(item, value, me); } hauls.Remove(item); } } private static void Pay(GrabbableObject item, Haul haul, PlayerControllerB me) { float num = ((haul.Haulers.Count > 0) ? 0.65f : 1f); float num2 = ((haul.Haulers.Count > 0) ? (0.35000002f / (float)haul.Haulers.Count) : 0f); float num3 = (Alive(haul.Finder) ? num : 0f); foreach (PlayerControllerB hauler in haul.Haulers) { num3 += (Alive(hauler) ? num2 : 0f); } float num4 = 0f; string text = ""; if ((Object)(object)me == (Object)(object)haul.Finder && Alive(me)) { num4 += num; text = "finder"; } if (haul.Haulers.Contains(me) && Alive(me)) { num4 += num2; text = ((text.Length > 0) ? "finder+hauler" : "hauler"); } if (!(num4 <= 0f) && !(num3 <= 0f) && Plugin.ModEnabled.Value) { float num5 = num4 / num3; string text2 = (((Object)(object)item.itemProperties != (Object)null) ? item.itemProperties.itemName : "item"); if (FairShare <= 0f) { early.Add(new EarlyDelivery { Name = text2, Value = item.scrapValue, Share = num5, Role = text }); Telemetry.Event("LOOT_QUEUED", item.scrapValue, FormattableString.Invariant($"{text2} value {item.scrapValue} share {num5 * 100f:F0}% ({text}): today's loot not counted yet, paid once it is")); } else { PayOut(text2, item.scrapValue, num5, text, ""); } } } private static void PayOut(string name, int value, float share, string role, string note) { float num = Mathf.Clamp(50f * (float)value / FairShare, 1f, 15f); float num2 = num * share; float num3 = Mathf.Max(0f, 30f - GainedToday); num2 = ((num2 <= num3) ? num2 : (num3 + (num2 - num3) * 0.5f)); num2 *= Plugin.RecoveryMultiplier.Value; float num4 = SanityMeter.LootGain(num2); GainedToday += num4; Telemetry.Event("LOOT_RECOVERY", num4, FormattableString.Invariant($"{name} value {value} fair share {FairShare:F0} full {num:F1} share {share * 100f:F0}% ({role}) day total {GainedToday:F1}{note}")); } private static void CountScene(StartOfRound startOfRound, out int items, out int total) { items = 0; total = 0; GrabbableObject[] array = Object.FindObjectsOfType<GrabbableObject>(); foreach (GrabbableObject val in array) { if (IsFresh(val, startOfRound)) { items++; total += val.scrapValue; } } } private static bool IsFresh(GrabbableObject item, StartOfRound startOfRound) { if ((Object)(object)item == (Object)null || (Object)(object)item.itemProperties == (Object)null || !item.itemProperties.isScrap || item.scrapValue <= 0 || item.scrapPersistedThroughRounds || item.isInShipRoom || InShip(item, startOfRound)) { return false; } RoundManager instance = RoundManager.Instance; if (!((Object)(object)instance == (Object)null)) { return !instance.scrapDroppedInShip.Contains(item); } return true; } private static bool InShip(GrabbableObject item, StartOfRound startOfRound) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) Collider shipInnerRoomBounds = startOfRound.shipInnerRoomBounds; if ((Object)(object)shipInnerRoomBounds != (Object)null) { Bounds bounds = shipInnerRoomBounds.bounds; return ((Bounds)(ref bounds)).Contains(((Component)item).transform.position); } return false; } private static bool Alive(PlayerControllerB player) { if ((Object)(object)player != (Object)null && player.isPlayerControlled) { return !player.isPlayerDead; } return false; } private static void ResetDay() { onMoon = false; secondsOnMoon = 0f; dayStartedAt = 0f; pollTimer = 0f; LevelTotal = 0; LevelItems = 0; PlayersAtLanding = 1; GainedToday = 0f; CountSource = ""; syncedValues = null; syncApplied = false; hauls.Clear(); early.Clear(); } } internal sealed class PhantomAudio : MonoBehaviour { private static PhantomAudio? instance; internal const float CloserFactor = 1.8f; internal static ConfigEntry<float>? Loudness; internal static float Closer => 1.8f * ((Loudness != null) ? Mathf.Clamp(Loudness.Value, 0.25f, 4f) : 1f); internal static bool IsPlaying { get; private set; } internal static bool TryClaim() { if ((Object)(object)instance == (Object)null || IsPlaying) { return false; } IsPlaying = true; return true; } internal static void Release() { IsPlaying = false; } internal static bool Run(IEnumerator routine) { if ((Object)(object)instance == (Object)null) { return false; } ((MonoBehaviour)instance).StartCoroutine(routine); return true; } internal static void Create() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown GameObject val = new GameObject("Sanity.PhantomAudio"); ((Object)val).hideFlags = (HideFlags)61; Object.DontDestroyOnLoad((Object)(object)val); instance = val.AddComponent<PhantomAudio>(); } internal static bool Play(PlayerControllerB player, MonsterSound sound, AudioClip[] clips, AudioSource? template, Vector3[] positions) { if ((Object)(object)instance == (Object)null || IsPlaying || clips.Length == 0 || positions.Length == 0) { return false; } IsPlaying = true; ((MonoBehaviour)instance).StartCoroutine(instance.PlayMonster(player, sound, clips, template, positions)); return true; } internal static AudioSource CreateSource(string name, AudioSource? template, Vector3 position, bool louder = true, float extra = 1f) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Expected O, but got Unknown //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject(name); val.transform.position = position; AudioSource val2 = val.AddComponent<AudioSource>(); CopySettings(template, val2); if (louder) { Stretch(val2, Closer * Mathf.Max(1f, extra)); } OccludeAudio val3 = (((Object)(object)template != (Object)null) ? ((Component)template).GetComponent<OccludeAudio>() : null); if ((Object)(object)val3 != (Object)null) { AudioLowPassFilter val4 = val.AddComponent<AudioLowPassFilter>(); val4.cutoffFrequency = StartingCutoff(position, val2.maxDistance); OccludeAudio val5 = val.AddComponent<OccludeAudio>(); val5.useReverb = val3.useReverb; } return val2; } private IEnumerator PlayMonster(PlayerControllerB player, MonsterSound sound, AudioClip[] clips, AudioSource? template, Vector3[] positions) { GameObject host = null; float started = Time.time; try { AudioSource source = CreateSource("Sanity.PhantomSound", template, positions[0]); host = ((Component)source).gameObject; AudioClip lastClip = null; for (int i = 0; i < positions.Length; i++) { if (Gone(player)) { yield break; } host.transform.position = positions[i]; AudioClip val = clips[Random.Range(0, clips.Length)]; if ((Object)(object)val != (Object)null) { source.pitch = Random.Range(sound.MinPitch, sound.MaxPitch); source.PlayOneShot(val, Random.Range(sound.MinVolume, sound.MaxVolume)); lastClip = val; } if (i >= positions.Length - 1) { continue; } float gap = Random.Range(sound.MinGap, sound.MaxGap); if (sound.GapAfterClip && (Object)(object)val != (Object)null) { gap += val.length / Mathf.Max(0.1f, source.pitch); } for (float t = 0f; t < gap; t += Time.deltaTime) { yield return null; if (Gone(player)) { yield break; } } } float tail = (((Object)(object)lastClip != (Object)null) ? (lastClip.length / Mathf.Max(0.1f, source.pitch)) : 0f) + 0.25f; for (float gap = 0f; gap < tail; gap += Time.deltaTime) { yield return null; if (Gone(player)) { break; } } } finally { if ((Object)(object)host != (Object)null) { Object.Destroy((Object)(object)host); } IsPlaying = false; float value = Time.time - started; Telemetry.Event("PHANTOM_SOUND_END", value, sound.Name + " lasted " + value.ToString("F1", CultureInfo.InvariantCulture) + "s"); } } private static bool Gone(PlayerControllerB? player) { StartOfRound val = StartOfRound.Instance; if (!((Object)(object)player == (Object)null) && !player.isPlayerDead && SanityMeter.OnMoon) { if ((Object)(object)val != (Object)null) { return val.shipIsLeaving; } return false; } return true; } private static float StartingCutoff(Vector3 position, float maxDistance) { //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004a: Unknown result type (might be due to invalid IL or missing references) StartOfRound val = StartOfRound.Instance; if ((Object)(object)val == (Object)null || (Object)(object)val.audioListener == (Object)null) { return 10000f; } Vector3 position2 = ((Component)val.audioListener).transform.position; if (!Physics.Linecast(position, position2, 256, (QueryTriggerInteraction)1)) { return 10000f; } float num = Vector3.Distance(position2, position); return Mathf.Clamp(2500f / (num / (Mathf.Max(1f, maxDistance) / 2f)), 900f, 4000f); } private static void Stretch(AudioSource source, float factor) { if (!(factor <= 1.001f)) { source.minDistance *= factor; source.maxDistance *= factor; source.volume = 1f; } } private static void CopySettings(AudioSource? from, AudioSource to) { //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00c7: Invalid comparison between Unknown and I4 to.playOnAwake = false; to.loop = false; to.spatialBlend = 1f; if ((Object)(object)from == (Object)null) { to.rolloffMode = (AudioRolloffMode)0; to.minDistance = 2f; to.maxDistance = 40f; return; } to.outputAudioMixerGroup = from.outputAudioMixerGroup; to.volume = ((from.volume > 0.05f) ? from.volume : 1f); to.rolloffMode = from.rolloffMode; to.minDistance = from.minDistance; to.maxDistance = from.maxDistance; to.dopplerLevel = from.dopplerLevel; to.spread = from.spread; to.priority = from.priority; to.reverbZoneMix = from.reverbZoneMix; if ((int)from.rolloffMode == 2) { to.SetCustomCurve((AudioSourceCurveType)0, from.GetCustomCurve((AudioSourceCurveType)0)); } } } [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInPlugin("Lowlight.Sanity", "Sanity", "0.0.2")] public class Plugin : BaseUnityPlugin { public const string Id = "Lowlight.Sanity"; internal static ManualLogSource Log { get; private set; } internal static ConfigEntry<bool> ModEnabled { get; private set; } internal static ConfigEntry<bool> ShowReadout { get; private set; } internal static ConfigEntry<bool> DebugKeys { get; private set; } internal static ConfigEntry<bool> TelemetryEnabled { get; private set; } internal static ConfigEntry<bool> ShowPresence { get; private set; } internal static ConfigEntry<float> DrainMultiplier { get; private set; } internal static ConfigEntry<float> RecoveryMultiplier { get; private set; } internal static string BuildVersion { get { string text = typeof(Plugin).Assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), inherit: false).OfType<AssemblyInformationalVersionAttribute>().FirstOrDefault()?.InformationalVersion ?? "?"; int num = text.IndexOf('+'); if (num < 0 || text.Length <= num + 8) { return text; } return text.Substring(0, num + 8); } } public static string Name => "Sanity"; public static string Version => "0.0.2"; private void Awake() { //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Expected O, but got Unknown //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Expected O, but got Unknown //IL_017c: Unknown result type (might be due to invalid IL or missing references) //IL_0186: Expected O, but got Unknown //IL_01cf: Unknown result type (might be due to invalid IL or missing references) //IL_01d5: Expected O, but got Unknown Log = ((BaseUnityPlugin)this).Logger; ModEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "EnableMod", true, "Master switch. Off: sanity stays at 100 and no symptoms happen. Takes effect immediately."); ModEnabled.SettingChanged += delegate { Telemetry.Event("MOD_TOGGLED", ModEnabled.Value ? 1f : 0f, ModEnabled.Value ? "mod enabled" : "mod disabled"); }; DrainMultiplier = ((BaseUnityPlugin)this).Config.Bind<float>("Tuning", "DrainMultiplier", 1f, new ConfigDescription("Scales how fast sanity drops. 2 = twice as fast, 0.5 = half as fast.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 5f), Array.Empty<object>())); RecoveryMultiplier = ((BaseUnityPlugin)this).Config.Bind<float>("Tuning", "RecoveryMultiplier", 1f, new ConfigDescription("Scales how fast sanity recovers. 2 = twice as fast, 0.5 = half as fast.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 5f), Array.Empty<object>())); ShowReadout = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "ShowSanityReadout", false, "Show the current sanity value on screen. For testing and tuning: it shows the hidden meter, so it spoils the mystery."); DebugKeys = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "EnableDebugKeys", false, "Testing keys: F7 opens the debug menu, F8 fires a random symptom that makes sense right now."); ShowPresence = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug", "ShowPresence", false, "Testing: draw a red ball where the Presence is while it's around (only on your screen)."); TelemetryEnabled = ((BaseUnityPlugin)this).Config.Bind<bool>("Telemetry", "EnableTelemetry", false, "Record sanity and the events that change it to a CSV file in BepInEx/Sanity_Telemetry. Local only: nothing is sent anywhere. The file includes in-game player names."); PhantomAudio.Loudness = ((BaseUnityPlugin)this).Config.Bind<float>("Tuning", "PhantomLoudness", 1f, new ConfigDescription("How loud phantom steps and sounds are. 1 = default (already louder than the real thing), 2 = louder, 0.5 = quieter. Takes effect on the next sound.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0.25f, 4f), Array.Empty<object>())); PhantomAudio.Loudness.SettingChanged += delegate { Telemetry.Event("PHANTOM_LOUDNESS", PhantomAudio.Loudness.Value, $"phantom loudness set to {PhantomAudio.Loudness.Value:0.##}"); }; SanityDirector.BindConfig(((BaseUnityPlugin)this).Config); Weather.BindConfig(((BaseUnityPlugin)this).Config); Harmony val = new Harmony("Lowlight.Sanity"); Type[] typesFromAssembly = AccessTools.GetTypesFromAssembly(typeof(Plugin).Assembly); foreach (Type type in typesFromAssembly) { try { val.CreateClassProcessor(type).Patch(); } catch (Exception ex) { Log.LogWarning((object)("Couldn't apply " + type.Name + " (that part of Sanity is off): " + ex.Message)); } } LethalHudCompat.Apply(val); GeneralImprovementsCompat.Apply(val); SanityReadout.Create(); DebugMenu.Create(); PhantomAudio.Create(); Telemetry.Create(); Log.LogInfo((object)("Plugin " + Name + " " + BuildVersion + " is loaded!")); } } internal static class Reactions { private sealed class Probe { internal bool Real; internal string Name = ""; internal float T0; internal bool Directional; internal Vector3 Source; internal float StartAngle; internal bool Turned; internal float TurnTime = -1f; internal float YawBefore; internal float SpeedBefore; internal float YawAfter; internal float DistAfter; internal bool Stopped; internal bool Sprinted; internal bool SprintingAtStart; internal bool Scan; internal bool Use; internal bool Walkie; internal bool HasWalkie; internal bool Away; internal bool Toward; internal bool Moved; internal Vector3 StartPos; internal string Confound = ""; internal string Context = ""; } private const float Window = 6f; private const float WalkieWindow = 20f; private const float AfterSeconds = 3f; private const float BinSeconds = 0.1f; private const int Bins = 30; private const float ControlMeanGap = 40f; private const float ControlQuietBefore = 10f; private const float TurnCone = 30f; private const float TurnStartMin = 90f; private const float StopFrom = 2f; private const float StopTo = 0.6f; private const float MoveMetres = 2f; private static readonly List<Probe> Open = new List<Probe>(); private static readonly float[] YawBins = new float[30]; private static readonly float[] DistBins = new float[30]; private static int bin; private static float binTime; private static float lastYaw; private static bool hasYaw; private static Vector3 lastPos; private static bool hasPos; private static float smoothSpeed; private static float nextControl = -1f; private static readonly StringBuilder Text = new StringBuilder(400); internal static void OnSymptom(Symptom symptom, Vector3? source) { foreach (Probe item in Open) { if (item.Confound.Length == 0) { item.Confound = "another symptom"; } } Open.RemoveAll((Probe p) => !p.Real); PlayerControllerB val = Local(); if (!((Object)(object)val == (Object)null) && Plugin.TelemetryEnabled.Value) { Open.Add(NewProbe(val, real: true, symptom.ConfigKey, source)); } } internal static void NoteConfound(string why) { foreach (Probe item in Open) { if (item.Confound.Length == 0) { item.Confound = why; } } } internal static void Reset() { Open.Clear(); hasYaw = false; hasPos = false; nextControl = -1f; Array.Clear(YawBins, 0, 30); Array.Clear(DistBins, 0, 30); } internal static void Tick(PlayerControllerB player) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_0061: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0098: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Unknown result type (might be due to invalid IL or missing references) //IL_01a6: Unknown result type (might be due to invalid IL or missing references) //IL_01ab: Unknown result type (might be due to invalid IL or missing references) //IL_01b0: Unknown result type (might be due to invalid IL or missing references) //IL_01be: Unknown result type (might be due to invalid IL or missing references) //IL_01bf: Unknown result type (might be due to invalid IL or missing references) //IL_01c6: Unknown result type (might be due to invalid IL or missing references) //IL_01cb: Unknown result type (might be due to invalid IL or missing references) float deltaTime = Time.deltaTime; float y = ((Component)player.gameplayCamera).transform.eulerAngles.y; float num = (hasYaw ? Mathf.Abs(Mathf.DeltaAngle(lastYaw, y)) : 0f); lastYaw = y; hasYaw = true; Vector3 position = ((Component)player).transform.position; Vector3 val = (hasPos ? (position - lastPos) : Vector3.zero); val.y = 0f; float num2 = ((((Vector3)(ref val)).magnitude < 4f) ? ((Vector3)(ref val)).magnitude : 0f); lastPos = position; hasPos = true; if (deltaTime > 0f) { smoothSpeed = Mathf.Lerp(smoothSpeed, num2 / deltaTime, 1f - Mathf.Exp((0f - deltaTime) / 0.3f)); } binTime += deltaTime; YawBins[bin] += num; DistBins[bin] += num2; if (binTime >= 0.1f) { binTime = 0f; bin = (bin + 1) % 30; YawBins[bin] = 0f; DistBins[bin] = 0f; } if (nextControl < 0f) { nextControl = Time.time + NextGap(); } if (Time.time >= nextControl) { nextControl = Time.time + NextGap(); if (SanityDirector.SecondsSinceLast >= 10f && !player.isPlayerDead && !player.quickMenuManager.isMenuOpen) { Vector3 val2 = PhantomPlacement.RandomBehindDirection(PhantomPlacement.LookDirection(player)); Open.Add(NewProbe(player, real: false, "control", position + val2 * 8f)); } } for (int num3 = Open.Count - 1; num3 >= 0; num3--) { Probe probe = Open[num3]; float num4 = Time.time - probe.T0; Update(player, probe, num4, num, num2); float num5 = (probe.HasWalkie ? 20f : 6f); if (num4 >= num5) { Emit(probe); Open.RemoveAt(num3); } } } internal static void Flush(string why) { foreach (Probe item in Open) { if (item.Confound.Length == 0) { item.Confound = why; } Emit(item); } Open.Clear(); } private static float NextGap() { return (0f - Mathf.Log(Mathf.Max(0.0001f, Random.value))) * 40f; } private static Probe NewProbe(PlayerControllerB player, bool real, string name, Vector3? source) { //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_006b: 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_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) float num = 0f; float num2 = 0f; for (int i = 0; i < 30; i++) { num += YawBins[i]; num2 += DistBins[i]; } Probe probe = new Probe { Real = real, Name = name, T0 = Time.time, Directional = source.HasValue, Source = (Vector3)(((??)source) ?? Vector3.zero), YawBefore = num / 3f, SpeedBefore = num2 / 3f, SprintingAtStart = player.isSprinting, HasWalkie = CarriesPoweredWalkie(player), StartPos = ((Component)player).transform.position }; if (probe.Directional) { probe.StartAngle = PhantomPlacement.AngleFromLook(player, probe.Source); } string text = (player.isInsideFactory ? "inside" : (player.isInHangarShipRoom ? "ship" : "outside")); probe.Context = FormattableString.Invariant($"place={text} company={(SanityMeter.NearTeammate ? 1 : 0)} moving={((probe.SpeedBefore >= 1f) ? 1 : 0)} tier={SanityMeter.Tier} fear={SanityMeter.Fear:F2} light={(LightOn(player) ? 1 : 0)} has_walkie={(probe.HasWalkie ? 1 : 0)}"); return probe; } private static void Update(PlayerControllerB player, Probe probe, float elapsed, float yawDelta, float dist) { //IL_0112: Unknown result type (might be due to invalid IL or missing references) //IL_0151: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_015c: Unknown result type (might be due to invalid IL or missing references) //IL_0161: Unknown result type (might be due to invalid IL or missing references) //IL_0166: Unknown result type (might be due to invalid IL or missing references) //IL_016d: Unknown result type (might be due to invalid IL or missing references) //IL_0173: Unknown result type (might be due to invalid IL or missing references) //IL_0178: Unknown result type (might be due to invalid IL or missing references) //IL_017d: Unknown result type (might be due to invalid IL or missing references) //IL_018a: Unknown result type (might be due to invalid IL or missing references) //IL_018b: Unknown result type (might be due to invalid IL or missing references) if (player.isPlayerDead) { probe.Confound = ((probe.Confound.Length == 0) ? "died" : probe.Confound); return; } if (probe.Confound.Length == 0) { if (SanityMeter.SecondsSinceRealFear < elapsed) { probe.Confound = "real fear"; } else if (SanityMeter.SecondsSinceDamage < elapsed) { probe.Confound = "damage"; } } if (elapsed <= 3f) { probe.YawAfter += yawDelta; probe.DistAfter += dist; if (probe.SpeedBefore >= 2f && smoothSpeed <= 0.6f) { probe.Stopped = true; } if (!probe.SprintingAtStart && player.isSprinting) { probe.Sprinted = true; } } if (elapsed <= 6f) { probe.Scan |= InputWatch.Pressed("PingScan"); probe.Use |= InputWatch.Pressed("Interact"); if (probe.Directional && !probe.Turned && probe.StartAngle >= 90f && PhantomPlacement.AngleFromLook(player, probe.Source) <= 30f) { probe.Turned = true; probe.TurnTime = elapsed; } if (probe.Directional && !probe.Moved && elapsed >= 4f) { probe.Moved = true; Vector3 val = PhantomPlacement.Flat(probe.StartPos - probe.Source); Vector3 val2 = ((Component)player).transform.position - probe.StartPos; val2.y = 0f; float num = Vector3.Dot(val2, val); probe.Away = num >= 2f; probe.Toward = num <= -2f; } } if (probe.HasWalkie && player.speakingToWalkieTalkie) { probe.Walkie = true; } } private static void Emit(Probe probe) { Text.Clear(); Text.Append("kind=").Append(probe.Real ? "real" : "control").Append(" name=") .Append(probe.Name) .Append(" dir=") .Append(probe.Directional ? 1 : 0); if (probe.Directional) { Text.Append(FormattableString.Invariant($" start_angle={probe.StartAngle:F0} turned={(probe.Turned ? 1 : 0)} turn_s={probe.TurnTime:F1} away={(probe.Away ? 1 : 0)} toward={(probe.Toward ? 1 : 0)}")); } Text.Append(FormattableString.Invariant($" yaw_b={probe.YawBefore:F0} yaw_a={probe.YawAfter / 3f:F0} v_b={probe.SpeedBefore:F1} v_a={probe.DistAfter / 3f:F1}")).Append(FormattableString.Invariant($" stopped={(probe.Stopped ? 1 : 0)} sprinted={(probe.Sprinted ? 1 : 0)} scan={(probe.Scan ? 1 : 0)} use={(probe.Use ? 1 : 0)} walkie={(probe.Walkie ? 1 : 0)}")).Append(' ') .Append(probe.Context) .Append(" confound=") .Append((probe.Confound.Length == 0) ? "none" : probe.Confound.Replace(' ', '_')); Telemetry.Event("REACTION", probe.Real ? 1f : 0f, Text.ToString()); } private static bool CarriesPoweredWalkie(PlayerControllerB player) { if (player.ItemSlots == null) { return false; } GrabbableObject[] itemSlots = player.ItemSlots; foreach (GrabbableObject val in itemSlots) { WalkieTalkie val2 = (WalkieTalkie)(object)((val is WalkieTalkie) ? val : null); if (val2 != null && ((GrabbableObject)val2).isBeingUsed) { return true; } } return false; } internal static bool LightOn(PlayerControllerB player) { if ((Object)(object)player.helmetLight != (Object)null && ((Behaviour)player.helmetLight).enabled) { return true; } GrabbableObject currentlyHeldObjectServer = player.currentlyHeldObjectServer; return ((currentlyHeldObjectServer is FlashlightItem) ? currentlyHeldObjectServer : null)?.isBeingUsed ?? false; } private static PlayerControllerB? Local() { GameNetworkManager instance = GameNetworkManager.Instance; if (!((Object)(object)instance != (Object)null)) { return null; } return instance.localPlayerController; } } internal static class Safety { private static readonly HashSet<string> Reported = new HashSet<string>(); internal static void Report(string where, Exception error) { if (!Reported.Add(where)) { return; } Plugin.Log.LogWarning((object)$"Error in {where} (logged once, Sanity keeps going): {error}"); if (where != "telemetry") { try { Telemetry.Event("SANITY_ERROR", 0f, where + ": " + error.GetType().Name + ": " + error.Message); } catch (Exception) { } } } } internal static class SanityDirector { private const float MaxChancePerSecond = 0.044f; private const float DepthFloor = 0.4f; private const float DipFloor = 0.1f; private const float DipRecoverySeconds = 8f; private const float TensionDoublingSeconds = 120f; private const float PeakRelaxSeconds = 50f; private static readonly List<Symptom> AllSymptoms = new List<Symptom> { new FlickerSymptom(), new PhantomFearSymptom(), new PhantomDreadSymptom(), new PhantomSoundSymptom(), new PhantomHitSymptom(), new PhantomStepsSymptom(), new PhantomWalkieSymptom(), new FakeLockedDoorSymptom(), new VanishingItemSymptom(), new LostTimeSymptom(), new PresenceSymptom() }; private static readonly List<Symptom> Eligible = new List<Symptom>(); private static readonly HashSet<Symptom> HeldForSprint = new HashSet<Symptom>(); private static readonly StringBuilder RollText = new StringBuilder(320); internal static bool Paused; internal static bool IgnoreConditions; internal static bool IgnoreGapsNatural; internal static bool IgnoreGapsForced; internal const string SprintingReason = "busy: sprinting"; internal static bool SprintCheckOff; internal static bool ChainsAlways; internal static float ChancePerSecond; internal static float SecondsSinceLast = 9999f; internal static float SecondsSinceVisible = 9999f; internal static string LastSymptom = "none"; private static float quietSeconds; private static float relaxUntil; private static float secondsOnMoon; internal static IReadOnlyList<Symptom> Symptoms => AllSymptoms; internal static bool GapsIgnored { get; private set; } internal static bool ConditionsIgnored { get; private set; } internal static bool ForcedContext { get; private set; } internal static float RelaxLeft => Mathf.Max(0f, relaxUntil - Time.time); internal static float QuietSeconds => quietSeconds; internal static string TuningSummary => FormattableString.Invariant($"chance {0.044f}/s floor {0.4f} dip {0.1f}/{8f}s tension x2 per {120f}s; ") + Waves.TuningSummary + "; " + DayMood.TuningSummary + "; " + Chains.TuningSummary; internal static bool Relaxing => Time.time < relaxUntil; internal static void Relax(float seconds, string why) { float num = Time.time + seconds; if (num > relaxUntil) { relaxUntil = num; Telemetry.Event("RELAX", seconds, why); } } internal static void BindConfig(ConfigFile config) { foreach (Symptom allSymptom in AllSymptoms) { ConfigEntry<bool> entry = config.Bind<bool>("Symptoms", allSymptom.ConfigKey, true, "Allow the " + allSymptom.Name + " symptom. Off: it never happens on its own (the debug menu can still fire it)."); Symptom captured = allSymptom; entry.SettingChanged += delegate { Telemetry.Event("SYMPTOM_TOGGLED", entry.Value ? 1f : 0f, captured.Name + " " + (entry.Value ? "on" : "off")); }; allSymptom.EnabledEntry = entry; } } internal static void Tick(PlayerControllerB player) { if (Plugin.DebugKeys.Value && Keyboard.current != null && ((ButtonControl)Keyboard.current.f8Key).wasPressedThisFrame) { TryPlay(player, forced: true); } if (!SanityMeter.OnMoon) { quietSeconds = 0f; ChancePerSecond = 0f; SecondsSinceLast = 9999f; SecondsSinceVisible = 9999f; LastSymptom = "none"; relaxUntil = 0f; secondsOnMoon = 0f; DayMood.Reset(); Waves.Reset(); Chains.Reset(); SprintHold.Reset(); { foreach (Symptom allSymptom in AllSymptoms) { allSymptom.SecondsSincePlayed = 9999f; allSymptom.ResetDay(); } return; } } float deltaTime = Time.deltaTime; SecondsSinceLast += deltaTime; SecondsSinceVisible += deltaTime; secondsOnMoon += deltaTime; if (secondsOnMoon > 2f) { DayMood.EnsureRolled(); } Waves.Tick(deltaTime); foreach (Symptom allSymptom2 in AllSymptoms) { allSymptom2.SecondsSincePlayed += deltaTime; allSymptom2.Watch(player); } Chains.Tick(player); SprintHold.Tick(player); if (player.isPlayerDead || SanityMeter.Tier == SanityTier.Calm) { ChancePerSecond = 0f; return; } if (Relaxing) { ChancePerSecond = 0f; return; } quietSeconds += deltaTime; EligibilityLog.Tick(player, deltaTime); float calmLine = SanityMeter.CalmLine; float num = 0.4f + 0.6f * Mathf.Clamp01((calmLine - SanityMeter.Value) / calmLine); float num2 = 1f - 0.9f * Mathf.Exp((0f - SecondsSinceLast) / 8f); float num3 = 1f + quietSeconds / 120f; ChancePerSecond = 0.044f * num * num2 * num3 * Waves.Multiplier * DayMood.Rate; if (!Paused && Random.value < ChancePerSecond * deltaTime) { TryPlay(player, forced: false); } } private static void TryPlay(PlayerControllerB player, bool forced) { SetContext(forced); try { SanityTier sanityTier = SanityMeter.Tier; if (ConditionsIgnored && sanityTier == SanityTier.Calm) { sanityTier = SanityTier.Uneasy; } string text = (SanityMeter.Inside ? "inside" : (SanityMeter.InShip ? "ship" : "outside")); RollText.Clear(); RollText.Append(FormattableString.Invariant(FormattableStringFactory.Create("{0} {1} {2} chance {3:F3} wave {4} mood {5}{6} |", sanityTier, text, SanityMeter.NearTeammate ? "company" : "alone", ChancePerSecond, Waves.High ? "high" : "low", DayMood.Name, forced ? " F8" : ""))); Eligible.Clear(); HeldForSprint.Clear(); float num = 0f; foreach (Symptom allSymptom in AllSymptoms) { string text2 = WhyNot(allSymptom, player, sanityTier); if (text2 == "busy: sprinting" && allSymptom.HoldsForSprint && !SprintHold.Waiting && WouldPlayWithoutSprint(allSymptom, player, sanityTier)) { HeldForSprint.Add(allSymptom); text2 = ""; } if (text2.Length == 0) { Eligible.Add(allSymptom); float num2 = Weight(allSymptom); num += num2; RollText.Append(FormattableString.Invariant($" {allSymptom.ConfigKey}=w{num2:0.##}")).Append(HeldForSprint.Contains(allSymptom) ? "(sprint)" : ""); } else { RollText.Append(' ').Append(allSymptom.ConfigKey).Append("=[") .Append(text2) .Append(']'); } } RollText.Append(" |"); if (Eligible.Count == 0) { Telemetry.Event("ROLL_NOTHING_ELIGIBLE", (float)sanityTier, forced ? "F8" : ""); } string text3 = "none"; while (Eligible.Count > 0) { Symptom symptom = Pick(num); float recovery = symptom.Recovery; float value = Random.value; if (!GapsIgnored && value >= recovery) { Telemetry.Event("SYMPTOM_REJECTED", recovery, FormattableString.Invariant($"{symptom.Name} turned down (recovery {recovery * 100f:F0}%)") + (forced ? " (F8)" : "")); RollText.Append(FormattableString.Invariant($" {symptom.ConfigKey}:rejected[recovery {recovery:F2} roll {value:F2}]")); } else { if (HeldForSprint.Contains(symptom)) { HoldForSprint(symptom, sanityTier, forced ? "F8" : ""); RollText.Append(' ').Append(symptom.ConfigKey).Append(":held"); text3 = symptom.ConfigKey; break; } if (PlayChosen(player, symptom, sanityTier, forced ? "F8" : "")) { RollText.Append(' ').Append(symptom.ConfigKey).Append((symptom is ArmedSymptom) ? ":armed" : ":played"); text3 = symptom.ConfigKey; break; } RollText.Append(' ').Append(symptom.ConfigKey).Append(":failed[") .Append(symptom.LastFailure) .Append(']'); } num -= Weight(symptom); Eligible.Remove(symptom); } RollText.Append(" -> ").Append(text3); Telemetry.Event("ROLL", ChancePerSecond, RollText.ToString()); if (forced && text3 == "none") { Plugin.Log.LogInfo((object)"Debug: no symptom could play here right now."); } } finally { SetContext(forced: false); } } internal static string DebugFire(PlayerControllerB player, Symptom symptom, SanityTier tier) { SetContext(forced: true); try { if (!ConditionsIgnored) { if (!symptom.Enabled) { return symptom.Name + " is off (tick it, or turn on Ignore conditions)."; } if (SanityMeter.Tier < symptom.MinTier) { return $"{symptom.Name} needs {symptom.MinTier} or lower; you're {SanityMeter.Tier}."; } string text = symptom.CantPlayReason(player); if (text.Length > 0) { return symptom.Name + " can't play here: " + text + "."; } } if (!GapsIgnored && Random.value >= symptom.Recovery) { Telemetry.Event("SYMPTOM_REJECTED", symptom.Recovery, FormattableString.Invariant($"{symptom.Name} turned down (recovery {symptom.Recovery * 100f:F0}%) (debug menu)")); return FormattableString.Invariant($"{symptom.Name} was turned down by the recovery check ({symptom.Recovery * 100f:F0}% recovered)."); } if (!PlayChosen(player, symptom, tier, "debug menu")) { return symptom.Name + " couldn't play: " + symptom.LastFailure + "."; } return (symptom is ArmedSymptom armedSymptom) ? ("Armed " + symptom.Name + ": it fires on its own at " + armedSymptom.Waits + ".") : $"Fired {symptom.Name} at {tier} strength."; } finally { SetContext(forced: false); } } internal static string NaturalCantPlayReason(Symptom symptom, PlayerControllerB player) { SetContext(force