Some mods target the Mono version of the game, which is available by opting into the Steam beta branch "alternate"
Decompiled source of LooseEnds v1.1.0
LooseEnds.dll
Decompiled 9 hours agousing System; using System.Collections.Generic; using System.Diagnostics; using System.Reflection; using System.Resources; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using HarmonyLib; using Il2CppFishNet; using Il2CppFishNet.Connection; using Il2CppFishNet.Managing; using Il2CppInterop.Runtime.InteropTypes; using Il2CppScheduleOne.Combat; using Il2CppScheduleOne.DevUtilities; using Il2CppScheduleOne.Dragging; using Il2CppScheduleOne.Employees; using Il2CppScheduleOne.Law; using Il2CppScheduleOne.NPCs; using Il2CppScheduleOne.NPCs.Behaviour; using Il2CppScheduleOne.Networking; using Il2CppScheduleOne.PlayerScripts; using Il2CppScheduleOne.Police; using Il2CppScheduleOne.Vehicles; using Il2CppScheduleOne.Vision; using Il2CppScheduleOne.VoiceOver; using Il2CppSystem.Collections.Generic; using LooseEnds; using LooseEnds.Config; using LooseEnds.Detection; using LooseEnds.Killer; using LooseEnds.Networking; using LooseEnds.Reaction; using LooseEnds.Weight; using MelonLoader; using MelonLoader.Preferences; using Microsoft.CodeAnalysis; using ModManagerPhoneApp; using S1API.Lifecycle; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: MelonInfo(typeof(Core), "Loose Ends", "1.1.0", "DooDesch", null)] [assembly: MelonGame("TVGS", "Schedule I")] [assembly: MelonOptionalDependencies(new string[] { "ModManager&PhoneApp" })] [assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] [assembly: AssemblyCompany("LooseEnds")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.1.0.0")] [assembly: AssemblyInformationalVersion("1.1.0+099c7ee3f707ee3b1700548f6e165dbd879f6ede")] [assembly: AssemblyProduct("LooseEnds")] [assembly: AssemblyTitle("LooseEnds")] [assembly: NeutralResourcesLanguage("en-US")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.1.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [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 LooseEnds { public sealed class Core : MelonMod { private bool _inWorld; private float _scanAccum; private float _reconcileAccum; private int _prevMinsSinceArrested = -1; private readonly List<CorpseRecord> _discovered = new List<CorpseRecord>(); private int _activeLogState = -1; internal static string WitnessStatus = "init"; public static Core Instance { get; private set; } public static Instance Log { get; private set; } [Conditional("DEBUG")] public static void LogDebug(string msg) { Instance log = Log; if (log != null) { log.Msg(msg); } } public override void OnInitializeMelon() { Instance = this; Log = ((MelonBase)this).LoggerInstance; Preferences.Initialize(); try { ((MelonBase)this).HarmonyInstance.PatchAll(); } catch (Exception ex) { Log.Warning("[Core] Harmony patch failed: " + ex.Message); } GameLifecycle.OnSaveStart += ResetState; GameLifecycle.OnPreSceneChange += ResetState; HookModManager(); Log.Msg("Loose Ends v1.1.0 - witness system + corpse weight."); } private static void ResetState() { CorpseTracker.Clear(); KillerRegistry.Clear(); CorpseWeight.RestoreAll(); if (Instance != null) { Instance._prevMinsSinceArrested = -1; } } private void HookModManager() { try { ModSettingsEvents.OnPhonePreferencesSaved += OnSettingsSaved; ModSettingsEvents.OnMenuPreferencesSaved += OnSettingsSaved; Log.Msg("[Core] Mod Manager & Phone App hooked (settings apply live)."); } catch (Exception) { Log.Msg("[Core] Mod Manager & Phone App not present - settings via the MelonPreferences config file (apply live on save)."); } } private void OnSettingsSaved() { } public override void OnPreferencesSaved() { OnSettingsSaved(); } public override void OnSceneWasLoaded(int buildIndex, string sceneName) { _inWorld = sceneName == "Main"; } public override void OnSceneWasUnloaded(int buildIndex, string sceneName) { _inWorld = false; WitnessStatus = "[not in world]"; ResetState(); } private bool WitnessActive() { if (!Preferences.Enabled) { WitnessStatus = "[OFF: disabled in settings]"; if (_activeLogState != 0) { Log.Msg("[Core] Disabled via preferences - running vanilla."); _activeLogState = 0; } return false; } if (Net.IsCoop() && !Preferences.EnableInMultiplayer) { WitnessStatus = "[OFF: co-op - set EnableInMultiplayer]"; if (_activeLogState != 1) { Log.Msg("[Core] Co-op session detected - witness system auto-DISABLED (set EnableInMultiplayer to opt in after a 2-player test)."); _activeLogState = 1; } return false; } if (!Net.IsServer) { WitnessStatus = "[client - host runs detection]"; if (_activeLogState != 2) { Log.Msg("[Core] Not the network authority - witness detection runs on the host."); _activeLogState = 2; } return false; } WitnessStatus = "[ACTIVE]"; if (_activeLogState != 3) { Log.Msg("[Core] Witness system ACTIVE."); _activeLogState = 3; } return true; } public override void OnUpdate() { if (!_inWorld) { return; } CheckArrest(); if (!WitnessActive()) { return; } float deltaTime = Time.deltaTime; _reconcileAccum += deltaTime; if (_reconcileAccum >= 2f) { _reconcileAccum = 0f; CorpseTracker.Reconcile(); } if (CorpseTracker.Count == 0) { _scanAccum = 0f; return; } _scanAccum += deltaTime; if (!(_scanAccum < Preferences.ScanIntervalSeconds)) { _scanAccum = 0f; WitnessTick(); } } private void CheckArrest() { try { if (!Net.IsServer) { return; } Player local = Player.Local; if ((Object)(object)local == (Object)null) { return; } PlayerCrimeData crimeData = local.CrimeData; if ((Object)(object)crimeData == (Object)null) { return; } int minsSinceLastArrested = crimeData.MinsSinceLastArrested; if (_prevMinsSinceArrested >= 0 && minsSinceLastArrested < _prevMinsSinceArrested) { KillerRegistry.Clear(); CorpseTracker.ResolveAll(); Instance log = Log; if (log != null) { log.Msg("[Core] Player arrested - cleared killer attribution for all bodies."); } } _prevMinsSinceArrested = minsSinceLastArrested; } catch (Exception ex) { Instance log2 = Log; if (log2 != null) { log2.Warning("[Core] arrest watcher failed: " + ex.Message); } } } private void WitnessTick() { SightingScanner.Scan(_discovered); float time = Time.time; float reactionDelaySeconds = Preferences.ReactionDelaySeconds; bool oncePerCorpse = Preferences.OncePerCorpse; float responseCooldownSeconds = Preferences.ResponseCooldownSeconds; foreach (CorpseRecord record in CorpseTracker.Records) { if (record.Calling) { ReactionDispatcher.UpdateCall(record, time); } else { if (!record.Discovered) { continue; } if (record.Dispatched) { if (!oncePerCorpse && time - record.DispatchedTime >= responseCooldownSeconds) { record.Discovered = false; record.Dispatched = false; record.Discoverer = null; } } else if (!(time - record.FirstSeenTime < reactionDelaySeconds)) { ReactionDispatcher.TryStartResponse(record); } } } } } } namespace LooseEnds.Weight { internal static class CorpseWeight { private struct Saved { public Draggable D; public int NpcId; public float DragMult; public Rigidbody Rb; public float Drag; public float AngularDrag; public float Mass; public bool HasRb; } private static readonly Dictionary<int, Saved> _active = new Dictionary<int, Saved>(); internal static int ActiveCount => _active.Count; internal static void OnStartDragging(Draggable d) { if (!Preferences.Enabled || (Object)(object)d == (Object)null) { return; } float corpseWeightMultiplier = Preferences.CorpseWeightMultiplier; if (corpseWeightMultiplier <= 1.0001f) { return; } int instanceID; try { instanceID = ((Object)d).GetInstanceID(); } catch { return; } if (_active.ContainsKey(instanceID) || !IsBodyDraggable(d)) { return; } Saved value = default(Saved); try { value.D = d; try { NPC componentInParent = ((Component)d).GetComponentInParent<NPC>(); value.NpcId = (((Object)(object)componentInParent != (Object)null) ? ((Object)componentInParent).GetInstanceID() : 0); } catch { value.NpcId = 0; } value.DragMult = d.DragForceMultiplier; d.DragForceMultiplier = Mathf.Max(0.05f, value.DragMult / corpseWeightMultiplier); Rigidbody rigidbody = d.Rigidbody; if ((Object)(object)rigidbody != (Object)null) { value.Rb = rigidbody; value.HasRb = true; value.Drag = rigidbody.drag; value.AngularDrag = rigidbody.angularDrag; value.Mass = rigidbody.mass; rigidbody.drag = value.Drag + (corpseWeightMultiplier - 1f) * 2f; rigidbody.angularDrag = value.AngularDrag + (corpseWeightMultiplier - 1f) * 2f; rigidbody.mass = value.Mass * Mathf.Sqrt(corpseWeightMultiplier); } _active[instanceID] = value; } catch { } } internal static void OnStopDragging(Draggable d) { if (!((Object)(object)d == (Object)null)) { int instanceID; try { instanceID = ((Object)d).GetInstanceID(); } catch { return; } if (_active.TryGetValue(instanceID, out var value)) { RestoreEntry(value); _active.Remove(instanceID); } } } private static void RestoreEntry(Saved s) { try { if ((Object)(object)s.D != (Object)null) { s.D.DragForceMultiplier = s.DragMult; } } catch { } try { if (s.HasRb && (Object)(object)s.Rb != (Object)null) { s.Rb.drag = s.Drag; s.Rb.angularDrag = s.AngularDrag; s.Rb.mass = s.Mass; } } catch { } } internal static void RestoreForNpcId(int npcId) { if (npcId == 0 || _active.Count == 0) { return; } List<int> list = null; foreach (KeyValuePair<int, Saved> item in _active) { if (item.Value.NpcId == npcId) { RestoreEntry(item.Value); (list ?? (list = new List<int>())).Add(item.Key); } } if (list != null) { for (int i = 0; i < list.Count; i++) { _active.Remove(list[i]); } } } internal static void RestoreAll() { foreach (KeyValuePair<int, Saved> item in _active) { RestoreEntry(item.Value); } _active.Clear(); } private static bool IsBodyDraggable(Draggable d) { try { NPC componentInParent = ((Component)d).GetComponentInParent<NPC>(); if ((Object)(object)componentInParent != (Object)null) { NPCHealth health = componentInParent.Health; if ((Object)(object)health != (Object)null && (health.IsDead || health.IsKnockedOut)) { return true; } } } catch { } try { foreach (CorpseRecord record in CorpseTracker.Records) { NPC npc = record.Npc; if (!((Object)(object)npc == (Object)null)) { NPCMovement movement = npc.Movement; if ((Object)(object)movement != (Object)null && (Object)(object)movement.RagdollDraggable == (Object)(object)d) { return true; } } } } catch { } return false; } internal static void Clear() { _active.Clear(); } } } namespace LooseEnds.Reaction { internal static class CitizenReaction { internal static void React(NPC discoverer, Vector3 bodyPos) { //IL_0056: 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) //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_0085: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)discoverer == (Object)null) { return; } Vo(discoverer, (EVOLineType)4); bool flag = false; bool flag2 = false; try { flag = (Object)(object)((Il2CppObjectBase)discoverer).TryCast<PoliceOfficer>() != (Object)null; } catch { } try { flag2 = (Object)(object)((Il2CppObjectBase)discoverer).TryCast<Employee>() != (Object)null; } catch { } if (flag || flag2) { return; } try { NPCMovement movement = discoverer.Movement; if ((Object)(object)movement != (Object)null) { Vector3 val = bodyPos - ((Component)discoverer).transform.position; val.y = 0f; if (((Vector3)(ref val)).sqrMagnitude > 0.0001f) { movement.FaceDirection(((Vector3)(ref val)).normalized, 0.5f); } } } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[Reaction] face-body failed: " + ex.Message); } } } internal static void Alert(NPC npc) { if (!((Object)(object)npc == (Object)null)) { Vo(npc, (EVOLineType)4); } } private static void Vo(NPC npc, EVOLineType type) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) try { npc.PlayVO(type, false); } catch { } } } internal static class ReactionDispatcher { private const float CallWindowSeconds = 4f; private const float CallTimeoutSeconds = 25f; private const float CallActivationGrace = 1.5f; private const float CallRetryBackoff = 2f; internal static void TryStartResponse(CorpseRecord rec) { //IL_0109: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) if (rec == null || rec.Dispatched || rec.Calling) { return; } float time = Time.time; if (Preferences.RequirePlayerAlsoSeen && !DiscovererSeesAnyPlayer(rec.Discoverer)) { return; } Player val = ResolveKiller(rec); Vector3 bodyPos = CorpsePosition(rec); if ((Object)(object)val == (Object)null) { Instance log = Core.Log; if (log != null) { log.Msg($"[Reaction] corpse={rec.Id} discovered but no known culprit - nothing to investigate."); } rec.Dispatched = true; rec.DispatchedTime = time; } else { if (Preferences.WitnessCallsPolice && !IsPolice(rec.Discoverer) && (time < rec.CallRetryAfter || IsBusyCalling(rec.Discoverer) || StartCall(rec, val, bodyPos, time))) { return; } try { CitizenReaction.React(rec.Discoverer, bodyPos); } catch (Exception ex) { Instance log2 = Core.Log; if (log2 != null) { log2.Warning("[Reaction] witness reaction failed: " + ex.Message); } } DispatchToScene(rec, val, bodyPos); rec.Dispatched = true; rec.DispatchedTime = time; } } private static bool StartCall(CorpseRecord rec, Player killer, Vector3 bodyPos, float now) { //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Expected O, but got Unknown //IL_0078: 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) NPC discoverer = rec.Discoverer; if ((Object)(object)discoverer == (Object)null) { return false; } try { NPCBehaviour behaviour = discoverer.Behaviour; CallPoliceBehaviour val = (((Object)(object)behaviour != (Object)null) ? behaviour.CallPoliceBehaviour : null); if ((Object)(object)val == (Object)null) { return false; } CitizenReaction.Alert(discoverer); val.ReportedCrime = (Crime)new DeadlyAssault(); val.Target = killer; ((Behaviour)val).Enable_Networked(); rec.Calling = true; rec.CallStartTime = now; rec.CallWasActive = false; rec.CallKiller = killer; rec.ScenePos = bodyPos; Instance log = Core.Log; if (log != null) { log.Msg($"[Reaction] witness {SafeId(discoverer)} CALLING police on {SafeCode(killer)} for corpse={rec.Id} - {4f:F0}s window (silence them to stop it)."); } return true; } catch (Exception ex) { Instance log2 = Core.Log; if (log2 != null) { log2.Warning("[Reaction] StartCall failed: " + ex.Message); } return false; } } internal static void UpdateCall(CorpseRecord rec, float now) { //IL_00a0: Unknown result type (might be due to invalid IL or missing references) if (rec == null || !rec.Calling) { return; } float num = now - rec.CallStartTime; NPC discoverer = rec.Discoverer; CallPoliceBehaviour val = SafeCpb(discoverer); bool flag = SafeConscious(discoverer); bool flag2 = (Object)(object)val != (Object)null && SafeActive(val); bool flag3 = (Object)(object)val != (Object)null && SafeEnabled(val); if (flag2) { rec.CallWasActive = true; } if (rec.CallWasActive && !flag3 && num >= 4f) { try { Player callKiller = rec.CallKiller; if ((Object)(object)((callKiller != null) ? callKiller.CrimeData : null) != (Object)null) { rec.CallKiller.CrimeData.LastKnownPosition = rec.ScenePos; } } catch (Exception ex) { Instance log = Core.Log; if (log != null) { log.Warning("[Reaction] scene redirect failed: " + ex.Message); } } rec.Calling = false; rec.Dispatched = true; rec.DispatchedTime = now; Instance log2 = Core.Log; if (log2 != null) { log2.Msg($"[Reaction] call CONNECTED for corpse={rec.Id} - officers sent to the scene ({rec.ScenePos.x:F1},{rec.ScenePos.y:F1},{rec.ScenePos.z:F1})."); } } else if (!flag) { CancelCall(discoverer); rec.Calling = false; rec.CallWasActive = false; rec.Discovered = false; rec.Discoverer = null; Instance log3 = Core.Log; if (log3 != null) { log3.Msg($"[Reaction] witness SILENCED before the call connected for corpse={rec.Id} - no police called."); } } else if (((!rec.CallWasActive && num > 1.5f) || (rec.CallWasActive && flag3 && !flag2)) && IsFighting(discoverer)) { CancelCall(discoverer); rec.Calling = false; rec.CallWasActive = false; rec.CallRetryAfter = now + 2f; Instance log4 = Core.Log; if (log4 != null) { log4.Msg($"[Reaction] witness is fighting the player for corpse={rec.Id} - holding at SEEN, will call once combat ends."); } } else if (num > 25f) { CancelCall(discoverer); rec.Calling = false; rec.CallWasActive = false; rec.Discovered = false; rec.Discoverer = null; Instance log5 = Core.Log; if (log5 != null) { log5.Warning($"[Reaction] call for corpse={rec.Id} did not resolve within {25f:F0}s - re-armed."); } } } private static void CancelCall(NPC witness) { try { object obj; if (witness == null) { obj = null; } else { NPCBehaviour behaviour = witness.Behaviour; obj = ((behaviour != null) ? behaviour.CallPoliceBehaviour : null); } CallPoliceBehaviour val = (CallPoliceBehaviour)obj; if ((Object)(object)val != (Object)null && ((Behaviour)val).Enabled) { ((Behaviour)val).Disable_Networked((NetworkConnection)null); } } catch { } } private static bool IsBusyCalling(NPC witness) { try { object obj; if (witness == null) { obj = null; } else { NPCBehaviour behaviour = witness.Behaviour; obj = ((behaviour != null) ? behaviour.CallPoliceBehaviour : null); } CallPoliceBehaviour val = (CallPoliceBehaviour)obj; return (Object)(object)val != (Object)null && ((Behaviour)val).Active; } catch { return false; } } private static bool IsFighting(NPC witness) { try { NPCBehaviour val = (((Object)(object)witness != (Object)null) ? witness.Behaviour : null); if ((Object)(object)val == (Object)null) { return false; } Behaviour activeBehaviour = val.activeBehaviour; if ((Object)(object)activeBehaviour == (Object)null) { return false; } CombatBehaviour combatBehaviour = val.CombatBehaviour; return (Object)(object)combatBehaviour != (Object)null && ((Object)combatBehaviour).GetInstanceID() == ((Object)activeBehaviour).GetInstanceID(); } catch { return false; } } private static CallPoliceBehaviour SafeCpb(NPC witness) { try { return ((Object)(object)witness != (Object)null && (Object)(object)witness.Behaviour != (Object)null) ? witness.Behaviour.CallPoliceBehaviour : null; } catch { return null; } } private static bool SafeActive(CallPoliceBehaviour cpb) { try { return ((Behaviour)cpb).Active; } catch { return false; } } private static bool SafeEnabled(CallPoliceBehaviour cpb) { try { return ((Behaviour)cpb).Enabled; } catch { return false; } } private static Player ResolveKiller(CorpseRecord rec) { if ((Object)(object)rec.Killer != (Object)null) { return rec.Killer; } if (Preferences.AttributeUnknownToLocalPlayer && !Net.IsCoop()) { try { return Player.Local; } catch { return null; } } return null; } private static void DispatchToScene(CorpseRecord rec, Player killer, Vector3 bodyPos) { //IL_0065: 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_006c: 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_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00a8: Expected O, but got Unknown //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_0191: Unknown result type (might be due to invalid IL or missing references) //IL_0197: Expected I4, but got Unknown //IL_01a5: Unknown result type (might be due to invalid IL or missing references) //IL_01c3: Unknown result type (might be due to invalid IL or missing references) //IL_01e1: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00e7: Expected O, but got Unknown if ((Object)(object)killer == (Object)null) { return; } PlayerCrimeData crimeData = killer.CrimeData; if ((Object)(object)crimeData == (Object)null) { Instance log = Core.Log; if (log != null) { log.Warning($"[Reaction] SCENE corpse={rec.Id} - suspect has no CrimeData; skipping dispatch."); } return; } EPursuitLevel val = (EPursuitLevel)Preferences.PursuitLevelInt; try { if (crimeData.CurrentPursuitLevel < val) { crimeData.SetPursuitLevel(val); } } catch (Exception ex) { Instance log2 = Core.Log; if (log2 != null) { log2.Warning("[Reaction] SetPursuitLevel failed: " + ex.Message); } } try { crimeData.AddCrime((Crime)new DeadlyAssault(), 1); } catch (Exception ex2) { Instance log3 = Core.Log; if (log3 != null) { log3.Warning("[Reaction] AddCrime failed: " + ex2.Message); } } try { if (Singleton<LawManager>.InstanceExists) { Singleton<LawManager>.Instance.PoliceCalled(killer, (Crime)new DeadlyAssault()); } } catch (Exception ex3) { Instance log4 = Core.Log; if (log4 != null) { log4.Warning("[Reaction] PoliceCalled failed: " + ex3.Message); } } try { crimeData.LastKnownPosition = bodyPos; } catch (Exception ex4) { Instance log5 = Core.Log; if (log5 != null) { log5.Warning("[Reaction] LastKnownPosition redirect failed: " + ex4.Message); } } Instance log6 = Core.Log; if (log6 != null) { log6.Msg($"[Reaction] SCENE corpse={rec.Id} suspect={SafeCode(killer)} pursuit={(int)val} body=({bodyPos.x:F1},{bodyPos.y:F1},{bodyPos.z:F1})"); } } private static Vector3 CorpsePosition(CorpseRecord rec) { //IL_005f: 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_0057: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_0044: 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) try { NPC npc = rec.Npc; if ((Object)(object)npc != (Object)null) { NPCMovement movement = npc.Movement; if ((Object)(object)movement != (Object)null) { Draggable ragdollDraggable = movement.RagdollDraggable; if ((Object)(object)ragdollDraggable != (Object)null && (Object)(object)((Component)ragdollDraggable).transform != (Object)null) { return ((Component)ragdollDraggable).transform.position; } } return ((Component)npc).transform.position; } } catch { } return Vector3.zero; } private static bool DiscovererSeesAnyPlayer(NPC discoverer) { if ((Object)(object)discoverer == (Object)null) { return false; } try { NPCAwareness awareness = discoverer.Awareness; VisionCone val = (((Object)(object)awareness != (Object)null) ? awareness.VisionCone : null); if ((Object)(object)val == (Object)null) { return false; } Player local = Player.Local; if ((Object)(object)local != (Object)null && val.IsPlayerVisible(local)) { return true; } } catch { } return false; } private static bool IsPolice(NPC npc) { try { return (Object)(object)npc != (Object)null && (Object)(object)((Il2CppObjectBase)npc).TryCast<PoliceOfficer>() != (Object)null; } catch { return false; } } private static bool SafeConscious(NPC npc) { try { return (Object)(object)npc != (Object)null && npc.IsConscious; } catch { return false; } } internal static string SafeCode(Player p) { if ((Object)(object)p == (Object)null) { return "null"; } try { Player local = Player.Local; if ((Object)(object)local != (Object)null && ((Object)local).GetInstanceID() == ((Object)p).GetInstanceID()) { return "you"; } } catch { } try { return p.PlayerCode; } catch { return "?"; } } private static string SafeId(NPC npc) { try { return ((Object)(object)npc != (Object)null) ? ((Object)npc).GetInstanceID().ToString() : "null"; } catch { return "?"; } } } } namespace LooseEnds.Patches { [HarmonyPatch(typeof(Draggable), "StartDragging")] internal static class Draggable_StartDragging_Patch { private static void Postfix(Draggable __instance) { try { CorpseWeight.OnStartDragging(__instance); } catch { } } } [HarmonyPatch(typeof(Draggable), "StopDragging")] internal static class Draggable_StopDragging_Patch { private static void Postfix(Draggable __instance) { try { CorpseWeight.OnStopDragging(__instance); } catch { } } } [HarmonyPatch(typeof(NPCHealth), "NotifyAttackedByPlayer")] internal static class NPCHealth_NotifyAttackedByPlayer_Patch { private static void Postfix(NPCHealth __instance, Player player) { try { KillerRegistry.RecordAttacker(__instance.npc, player); } catch { } } } [HarmonyPatch(typeof(NPCHealth), "Die")] internal static class NPCHealth_Die_Patch { private static void Postfix(NPCHealth __instance) { try { NPC npc = __instance.npc; if (!((Object)(object)npc == (Object)null)) { Player killer = KillerRegistry.RecordKill(npc); CorpseTracker.OnNpcDied(npc, killer); } } catch { } } } [HarmonyPatch(typeof(NPCHealth), "KnockOut")] internal static class NPCHealth_KnockOut_Patch { private static void Postfix(NPCHealth __instance) { try { if (Preferences.ReactToKnockedOut) { NPC npc = __instance.npc; if (!((Object)(object)npc == (Object)null)) { CorpseTracker.OnNpcDied(npc, KillerRegistry.GetKiller(npc)); } } } catch { } } } } namespace LooseEnds.Networking { internal static class Net { private static NetworkManager Nm { get { try { return InstanceFinder.NetworkManager; } catch { return null; } } } internal static bool Online { get { NetworkManager nm = Nm; try { return (Object)(object)nm != (Object)null && (nm.IsServer || nm.IsClient); } catch { return false; } } } internal static bool IsServer { get { NetworkManager nm = Nm; try { return (Object)(object)nm != (Object)null && nm.IsServer; } catch { return false; } } } internal static bool IsCoop() { try { Lobby instance = Singleton<Lobby>.Instance; if ((Object)(object)instance == (Object)null) { return false; } return instance.IsInLobby && instance.PlayerCount > 1; } catch { return false; } } } } namespace LooseEnds.Killer { internal static class KillerRegistry { private struct Attack { public Player Player; public float Time; } private const float AttributionWindowSeconds = 120f; private static readonly Dictionary<int, Attack> _lastAttacker = new Dictionary<int, Attack>(); private static readonly Dictionary<int, Player> _killer = new Dictionary<int, Player>(); internal static void RecordAttacker(NPC npc, Player player) { if (!((Object)(object)npc == (Object)null) && !((Object)(object)player == (Object)null)) { _lastAttacker[((Object)npc).GetInstanceID()] = new Attack { Player = player, Time = Time.time }; } } internal static Player RecordKill(NPC npc) { if ((Object)(object)npc == (Object)null) { return null; } int instanceID = ((Object)npc).GetInstanceID(); if (_lastAttacker.TryGetValue(instanceID, out var value) && (Object)(object)value.Player != (Object)null && Time.time - value.Time <= 120f) { _killer[instanceID] = value.Player; return value.Player; } return null; } internal static Player GetKiller(NPC npc) { if ((Object)(object)npc == (Object)null) { return null; } int instanceID = ((Object)npc).GetInstanceID(); if (_killer.TryGetValue(instanceID, out var value) && (Object)(object)value != (Object)null) { return value; } if (_lastAttacker.TryGetValue(instanceID, out var value2) && (Object)(object)value2.Player != (Object)null && Time.time - value2.Time <= 120f) { return value2.Player; } return null; } internal static void Forget(NPC npc) { if (!((Object)(object)npc == (Object)null)) { ForgetId(((Object)npc).GetInstanceID()); } } internal static void ForgetId(int id) { _lastAttacker.Remove(id); _killer.Remove(id); } internal static void Clear() { _lastAttacker.Clear(); _killer.Clear(); } } } namespace LooseEnds.Detection { internal sealed class CorpseRecord { public NPC Npc; public int Id; public Player Killer; public bool Discovered; public float FirstSeenTime; public NPC Discoverer; public bool Dispatched; public float DispatchedTime; public bool Calling; public float CallStartTime; public bool CallWasActive; public float CallRetryAfter; public Player CallKiller; public Vector3 ScenePos; } internal static class CorpseTracker { private static readonly Dictionary<int, CorpseRecord> _corpses = new Dictionary<int, CorpseRecord>(); private static readonly List<int> _toRemove = new List<int>(); internal static int Count => _corpses.Count; internal static Dictionary<int, CorpseRecord>.ValueCollection Records => _corpses.Values; internal static bool TryGet(NPC npc, out CorpseRecord rec) { rec = null; if ((Object)(object)npc == (Object)null) { return false; } return _corpses.TryGetValue(((Object)npc).GetInstanceID(), out rec); } internal static void OnNpcDied(NPC npc, Player killer) { if (!((Object)(object)npc == (Object)null)) { int instanceID = ((Object)npc).GetInstanceID(); if (!_corpses.TryGetValue(instanceID, out var value)) { value = new CorpseRecord { Npc = npc, Id = instanceID }; _corpses[instanceID] = value; } if ((Object)(object)killer != (Object)null) { value.Killer = killer; } } } internal static void Reconcile() { bool reactToKnockedOut = Preferences.ReactToKnockedOut; try { List<NPC> nPCRegistry = NPCManager.NPCRegistry; if (nPCRegistry != null) { int count = nPCRegistry.Count; for (int i = 0; i < count; i++) { NPC val = nPCRegistry[i]; if ((Object)(object)val == (Object)null) { continue; } NPCHealth health; try { health = val.Health; } catch { continue; } if ((Object)(object)health == (Object)null) { continue; } bool flag; try { flag = health.IsDead || (reactToKnockedOut && health.IsKnockedOut); } catch { continue; } if (flag) { int instanceID = ((Object)val).GetInstanceID(); if (!_corpses.ContainsKey(instanceID)) { _corpses[instanceID] = new CorpseRecord { Npc = val, Id = instanceID, Killer = KillerRegistry.GetKiller(val) }; } } } } } catch { } _toRemove.Clear(); foreach (KeyValuePair<int, CorpseRecord> corpse in _corpses) { NPC npc = corpse.Value.Npc; bool flag2 = false; try { if ((Object)(object)npc == (Object)null) { flag2 = true; } else { NPCHealth health2 = npc.Health; if (!((Object)(object)health2 != (Object)null) || (!health2.IsDead && (!reactToKnockedOut || !health2.IsKnockedOut))) { flag2 = true; } } } catch { flag2 = true; } if (flag2) { _toRemove.Add(corpse.Key); } } for (int j = 0; j < _toRemove.Count; j++) { int num = _toRemove[j]; CorpseWeight.RestoreForNpcId(num); _corpses.Remove(num); KillerRegistry.ForgetId(num); } } internal static void Remove(int id) { _corpses.Remove(id); KillerRegistry.ForgetId(id); } internal static void Clear() { _corpses.Clear(); } internal static void ResolveAll() { foreach (CorpseRecord value in _corpses.Values) { value.Discovered = true; value.Dispatched = true; } } } internal static class SightingScanner { private static int _observerCursor; private const float EyeHeight = 1.6f; private const float NoticeExposureThreshold = 0.4f; internal static void Scan(List<CorpseRecord> newlyDiscovered) { //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_0140: Unknown result type (might be due to invalid IL or missing references) //IL_0145: Unknown result type (might be due to invalid IL or missing references) //IL_014c: Unknown result type (might be due to invalid IL or missing references) //IL_014e: Unknown result type (might be due to invalid IL or missing references) //IL_0150: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Unknown result type (might be due to invalid IL or missing references) //IL_016c: Unknown result type (might be due to invalid IL or missing references) newlyDiscovered.Clear(); List<NPC> nPCRegistry = NPCManager.NPCRegistry; if (nPCRegistry == null) { return; } int count; try { count = nPCRegistry.Count; } catch { return; } if (count == 0) { return; } bool requireLineOfSight = Preferences.RequireLineOfSight; bool useVisionConeRange = Preferences.UseVisionConeRange; float detectionRange = Preferences.DetectionRange; float num = Mathf.Max(useVisionConeRange ? Preferences.ObserverCullRadius : detectionRange, Preferences.NoticeRadius); float num2 = num * num; int maxRaycastsPerScan = Preferences.MaxRaycastsPerScan; int num3 = 0; foreach (CorpseRecord record in CorpseTracker.Records) { if (record.Discovered) { continue; } NPC npc = record.Npc; if ((Object)(object)npc == (Object)null) { continue; } Vector3 val; try { val = CorpsePosition(record); } catch { continue; } for (int i = 0; i < count; i++) { if (num3 >= maxRaycastsPerScan) { AdvanceCursor(num3, count); return; } int num4 = (_observerCursor + i) % count; NPC val2 = nPCRegistry[num4]; if ((Object)(object)val2 == (Object)null || ((Object)val2).GetInstanceID() == record.Id) { continue; } NPCHealth health; try { health = val2.Health; } catch { continue; } if ((Object)(object)health == (Object)null) { continue; } try { if (health.IsDead || health.IsKnockedOut) { continue; } goto IL_012f; } catch { } continue; IL_012f: if (!RoleEnabled(val2)) { continue; } Vector3 position; try { position = ((Component)val2).transform.position; } catch { continue; } Vector3 val3 = position - val; if (!(((Vector3)(ref val3)).sqrMagnitude > num2)) { num3++; if (CanSee(val2, npc, val, requireLineOfSight)) { record.Discovered = true; record.FirstSeenTime = Time.time; record.Discoverer = val2; newlyDiscovered.Add(record); break; } } } } AdvanceCursor(num3, count); } private static void AdvanceCursor(int checksUsed, int regCount) { if (regCount > 0) { _observerCursor = (_observerCursor + Mathf.Max(1, checksUsed)) % regCount; } } private static Vector3 CorpsePosition(CorpseRecord rec) { //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_006f: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) NPC npc = rec.Npc; try { NPCMovement movement = npc.Movement; if ((Object)(object)movement != (Object)null) { Draggable ragdollDraggable = movement.RagdollDraggable; if ((Object)(object)ragdollDraggable != (Object)null) { Transform transform = ((Component)ragdollDraggable).transform; if ((Object)(object)transform != (Object)null) { return transform.position + Vector3.up * 0.5f; } } } } catch { } return ((Component)npc).transform.position + Vector3.up * 0.5f; } private static bool CanSee(NPC observer, NPC corpse, Vector3 bodyPos, bool requireLos) { //IL_0076: 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_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: 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_0031: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) if (!requireLos) { return true; } NPCAwareness awareness; try { awareness = observer.Awareness; } catch { return false; } VisionCone val = (((Object)(object)awareness != (Object)null) ? awareness.VisionCone : null); Vector3 val2 = EyeOf(observer, val); Vector3 val3 = val2 - bodyPos; float magnitude = ((Vector3)(ref val3)).magnitude; if (magnitude <= Preferences.BodySightRange) { try { if ((Object)(object)val != (Object)null && val.IsPointWithinSight(bodyPos, false, (LandVehicle)null)) { return true; } } catch { } } try { float noticeRadius = Preferences.NoticeRadius; if (magnitude <= noticeRadius && BodyExposedTo(corpse, observer, val2, noticeRadius, bodyPos, val)) { return true; } } catch { } return false; } private static bool BodyExposedTo(NPC corpse, NPC observer, Vector3 eye, float range, Vector3 bodyPos, VisionCone cone) { //IL_003d: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) try { EntityVisibility val = (((Object)(object)corpse != (Object)null) ? corpse.Visibility : null); if ((Object)(object)val != (Object)null) { return val.CalculateExposureToPoint(eye, range + 3f, observer) >= 0.4f; } } catch { } return HasClearPath(eye, bodyPos, cone); } private static Vector3 EyeOf(NPC observer, VisionCone cone) { //IL_0030: 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_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0022: 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) try { if ((Object)(object)cone != (Object)null && (Object)(object)cone.VisionOrigin != (Object)null) { return cone.VisionOrigin.position; } } catch { } return ((Component)observer).transform.position + Vector3.up * 1.6f; } private static bool HasClearPath(Vector3 eye, Vector3 bodyPos, VisionCone cone) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_001a: 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_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0044: Unknown result type (might be due to invalid IL or missing references) Vector3 val = bodyPos - eye; float magnitude = ((Vector3)(ref val)).magnitude; if (magnitude <= 0.6f) { return true; } val /= magnitude; float num = magnitude - 0.5f; try { if ((Object)(object)cone != (Object)null) { LayerMask visibilityBlockingLayers = cone.VisibilityBlockingLayers; if (((LayerMask)(ref visibilityBlockingLayers)).value != 0) { return !Physics.Raycast(eye, val, num, ((LayerMask)(ref visibilityBlockingLayers)).value); } } } catch { } return true; } private static bool RoleEnabled(NPC obs) { try { if ((Object)(object)((Il2CppObjectBase)obs).TryCast<PoliceOfficer>() != (Object)null) { return Preferences.ReactPolice; } if ((Object)(object)((Il2CppObjectBase)obs).TryCast<Employee>() != (Object)null) { return Preferences.ReactEmployees; } } catch { } return Preferences.ReactCitizens; } } } namespace LooseEnds.Config { internal static class Preferences { private const string CategoryId = "LooseEnds_01_Main"; private static MelonPreferences_Category _category; private static MelonPreferences_Entry<bool> _enabled; private static MelonPreferences_Entry<bool> _enableInMp; private static MelonPreferences_Entry<bool> _reactCitizens; private static MelonPreferences_Entry<bool> _reactPolice; private static MelonPreferences_Entry<bool> _reactEmployees; private static MelonPreferences_Entry<bool> _requireLos; private static MelonPreferences_Entry<bool> _useVisionRange; private static MelonPreferences_Entry<float> _detectionRange; private static MelonPreferences_Entry<float> _reactionDelay; private static MelonPreferences_Entry<float> _observerCullRadius; private static MelonPreferences_Entry<float> _bodySightRange; private static MelonPreferences_Entry<float> _noticeRadius; private static MelonPreferences_Entry<float> _scanInterval; private static MelonPreferences_Entry<int> _maxRaycastsPerScan; private static MelonPreferences_Entry<bool> _reactToKnockedOut; private static MelonPreferences_Entry<bool> _witnessCallsPolice; private static MelonPreferences_Entry<bool> _requirePlayerSeen; private static MelonPreferences_Entry<int> _pursuitLevel; private static MelonPreferences_Entry<bool> _oncePerCorpse; private static MelonPreferences_Entry<float> _responseCooldown; private static MelonPreferences_Entry<bool> _attributeUnknownToLocal; private static MelonPreferences_Entry<float> _weightMultiplier; internal static bool Enabled => _enabled?.Value ?? true; internal static bool EnableInMultiplayer => _enableInMp?.Value ?? false; internal static bool ReactCitizens => _reactCitizens?.Value ?? true; internal static bool ReactPolice => _reactPolice?.Value ?? true; internal static bool ReactEmployees => _reactEmployees?.Value ?? false; internal static bool RequireLineOfSight => _requireLos?.Value ?? true; internal static bool UseVisionConeRange => _useVisionRange?.Value ?? true; internal static float DetectionRange => Mathf.Clamp(_detectionRange?.Value ?? 12f, 3f, 40f); internal static float ReactionDelaySeconds => Mathf.Clamp(_reactionDelay?.Value ?? 3f, 0f, 30f); internal static float ObserverCullRadius => Mathf.Clamp(_observerCullRadius?.Value ?? 25f, 5f, 60f); internal static float BodySightRange => Mathf.Clamp(_bodySightRange?.Value ?? 12f, 4f, 30f); internal static float NoticeRadius => Mathf.Clamp(_noticeRadius?.Value ?? 6f, 2f, 15f); internal static float ScanIntervalSeconds => Mathf.Clamp(_scanInterval?.Value ?? 0.4f, 0.1f, 2f); internal static int MaxRaycastsPerScan => Mathf.Clamp(_maxRaycastsPerScan?.Value ?? 64, 8, 256); internal static bool ReactToKnockedOut => _reactToKnockedOut?.Value ?? false; internal static bool WitnessCallsPolice => _witnessCallsPolice?.Value ?? true; internal static bool RequirePlayerAlsoSeen => _requirePlayerSeen?.Value ?? false; internal static int PursuitLevelInt => Mathf.Clamp(_pursuitLevel?.Value ?? 1, 1, 4); internal static bool OncePerCorpse => _oncePerCorpse?.Value ?? true; internal static float ResponseCooldownSeconds => Mathf.Clamp(_responseCooldown?.Value ?? 60f, 5f, 600f); internal static bool AttributeUnknownToLocalPlayer => _attributeUnknownToLocal?.Value ?? true; internal static float CorpseWeightMultiplier => Mathf.Clamp(_weightMultiplier?.Value ?? 5f, 1f, 20f); internal static void Initialize() { if (_category == null) { _category = MelonPreferences.CreateCategory("LooseEnds_01_Main", "Loose Ends"); _enabled = Create("Enabled", def: true, "Enabled", "Master switch. When ON, NPCs who SEE a dead body react and carried corpses are heavier. OFF = fully vanilla."); _enableInMp = Create("EnableInMultiplayer", def: false, "Enable in multiplayer (experimental)", "OFF (default): the witness system auto-disables in a real co-op lobby until it has been tested with 2 players. ON: force it on - it is host-authoritative and uses the game's own networked police calls, but verify pursuit syncs to all clients first."); _reactCitizens = Create("ReactCitizens", def: true, "Citizens react", "Civilian NPCs who see a corpse call the police."); _reactPolice = Create("ReactPolice", def: true, "Police react", "Police who see a corpse begin investigating."); _reactEmployees = Create("ReactEmployees", def: false, "Employees react", "Your hired employees (dealers/handlers) react to a corpse. OFF by default - they are your crew."); _requireLos = Create("RequireLineOfSight", def: true, "Require line of sight", "ON (default): use the NPC's vision cone, so a body behind a wall / in a dumpster / indoors / underwater is NOT seen (this is the whole 'hide the body' mechanic). OFF: a pure radius check (notices through walls)."); _useVisionRange = Create("UseVisionConeRange", def: true, "Use the NPC's own vision range", "ON (default): rely on the game's vision-cone distance. OFF: use 'Detection range' below as an explicit radius."); _detectionRange = Create("DetectionRange", 12f, "Detection range (m)", "Explicit notice radius used when 'Use the NPC's own vision range' is OFF. Clamped 3-40.", (ValueValidator)(object)new ValueRange<float>(3f, 40f)); _reactionDelay = Create("ReactionDelaySeconds", 3f, "Reaction delay (s)", "After the first sighting, wait this long before the response fires (the NPC 'processes' the scene; also debounces a quick glance). Clamped 0-30.", (ValueValidator)(object)new ValueRange<float>(0f, 30f)); _observerCullRadius = Create("ObserverCullRadius", 25f, "Observer cull radius (m)", "Performance: only NPCs within this distance of a corpse are even considered as witnesses. Clamped 5-60.", (ValueValidator)(object)new ValueRange<float>(5f, 60f)); _bodySightRange = Create("BodySightRange", 12f, "Body sight range (m)", "How far an NPC can NOTICE a body through their vision cone. A body lying flat on the ground is far less conspicuous than a standing person, so this is capped well below the NPC's normal sight range - otherwise a corpse in the open is spotted by anyone within ~25m and the response feels instant. Clamped 4-30.", (ValueValidator)(object)new ValueRange<float>(4f, 30f)); _noticeRadius = Create("NoticeRadius", 6f, "Close-range notice radius (m)", "NPCs never look down at their feet, so a body lying flat is below their forward vision cone. Any living NPC within this radius with a clear line of sight (occlusion still respected) notices the body even if it is not in their cone - this is what makes someone standing over a corpse actually react. Clamped 2-15.", (ValueValidator)(object)new ValueRange<float>(2f, 15f)); _scanInterval = Create("ScanIntervalSeconds", 0.4f, "Scan interval (s)", "How often the witness scan runs. Lower = faster notice, slightly more cost. Clamped 0.1-2.", (ValueValidator)(object)new ValueRange<float>(0.1f, 2f)); _maxRaycastsPerScan = Create("MaxRaycastsPerScan", 64, "Max sight checks per scan", "Performance cap: at most this many vision checks per scan tick (round-robin if exceeded). Clamped 8-256.", (ValueValidator)(object)new ValueRange<int>(8, 256)); _reactToKnockedOut = Create("ReactToKnockedOut", def: false, "React to unconscious NPCs", "OFF (default): only react to actually-dead NPCs. ON: also react to merely knocked-out NPCs."); _witnessCallsPolice = Create("WitnessCallsPolice", def: true, "Witness phones the police (call window)", "ON (default): a civilian witness pulls out their phone and calls the police over ~4 seconds (the game's own call animation, with a progress icon over their head). Knock them out or kill them before the call connects to stop it - that is your chance to silence the only witness. OFF: the police are alerted instantly. (A police officer who finds a body always reports it instantly - you cannot phone-block a cop.)"); _requirePlayerSeen = Create("RequirePlayerAlsoSeen", def: false, "Killer must also be seen", "OFF (default): seeing the BODY is enough to start the response - hiding yourself is not enough, you must hide the body. ON: the discovering NPC must ALSO have the killer in sight before heat is applied (closer to vanilla crime-witnessing)."); _pursuitLevel = Create("PursuitLevel", 1, "Pursuit level to apply (1-4)", "Maps to the game's pursuit levels: 1 = Investigating, 2 = Arresting, 3 = NonLethal, 4 = Lethal. Default 1 (Investigating - the status named in the request).", (ValueValidator)(object)new ValueRange<int>(1, 4)); _oncePerCorpse = Create("OncePerCorpse", def: true, "Respond only once per corpse", "ON (default): a corpse that has already triggered a response will not trigger again. OFF: re-trigger is allowed, subject to the response cooldown."); _responseCooldown = Create("ResponseCooldownSeconds", 60f, "Re-trigger cooldown (s)", "Per-corpse re-trigger gap used ONLY when 'Respond only once per corpse' is OFF (how long before the same body can raise a fresh response). It does NOT throttle different bodies - every body a witness sees gets its own response. Clamped 5-600.", (ValueValidator)(object)new ValueRange<float>(5f, 600f)); _attributeUnknownToLocal = Create("AttributeUnknownToLocalPlayer", def: true, "Blame local player when killer unknown", "ON (default, single-player): if a discovered corpse has no recorded killer, attribute it to the local player so 'Hunt' still has a target. In a co-op lobby this is treated as OFF (an unattributed body falls back to a scene response instead of blaming a random player)."); _weightMultiplier = Create("CorpseWeightMultiplier", 5f, "Dragged body weight x", "How heavy a dragged body (dead OR knocked-out) feels to pull. Higher = the body resists more - it lags behind the carry point and drags sluggishly (and is heavier to throw). Does NOT slow the player. 1 = vanilla. Clamped 1-20.", (ValueValidator)(object)new ValueRange<float>(1f, 20f)); } } private static MelonPreferences_Entry<T> Create<T>(string id, T def, string name, string desc = null, ValueValidator validator = null) { if (validator != null) { return _category.CreateEntry<T>(id, def, name, desc, false, false, validator); } return _category.CreateEntry<T>(id, def, name, desc, false, false, (ValueValidator)null, (string)null); } } }