Decompiled source of TheSyndicate v0.9.0

TheSyndicate.dll

Decompiled 10 hours ago
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Core.Logging.Interpolation;
using BepInEx.Logging;
using BepInEx.Unity.IL2CPP;
using HarmonyLib;
using Il2CppInterop.Runtime;
using Il2CppInterop.Runtime.Injection;
using Il2CppInterop.Runtime.InteropTypes;
using Il2CppInterop.Runtime.InteropTypes.Arrays;
using Il2CppSystem;
using Il2CppSystem.Collections.Generic;
using Microsoft.CodeAnalysis;
using SOD.Common;
using SOD.Common.Helpers;
using SOD.Common.Helpers.DialogObjects;
using SOD.Common.Helpers.GameplayObjects;
using SOD.Common.Helpers.SyncDiskObjects;
using TMPro;
using TheSyndicate.Contracts;
using TheSyndicate.Cripple;
using TheSyndicate.CruncherApp;
using TheSyndicate.DebugTools;
using TheSyndicate.Evidence;
using TheSyndicate.Firearms;
using TheSyndicate.Heat;
using TheSyndicate.Mask;
using TheSyndicate.Money;
using TheSyndicate.News;
using TheSyndicate.Patches;
using TheSyndicate.Poison;
using TheSyndicate.Progression;
using TheSyndicate.State;
using TheSyndicate.Supplies;
using TheSyndicate.Tools;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")]
[assembly: AssemblyCompany("TheSyndicate")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+c5b974631e2c7a6af0fc74c3998122ed3d16d298")]
[assembly: AssemblyProduct("TheSyndicate")]
[assembly: AssemblyTitle("TheSyndicate")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.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.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 TheSyndicate
{
	internal static class ConsoleTweaks
	{
		private const int STD_INPUT_HANDLE = -10;

		private const uint ENABLE_QUICK_EDIT_MODE = 64u;

		private const uint ENABLE_EXTENDED_FLAGS = 128u;

		[DllImport("kernel32.dll", SetLastError = true)]
		private static extern IntPtr GetStdHandle(int nStdHandle);

		[DllImport("kernel32.dll", SetLastError = true)]
		private static extern bool GetConsoleMode(IntPtr hConsoleHandle, out uint lpMode);

		[DllImport("kernel32.dll", SetLastError = true)]
		private static extern bool SetConsoleMode(IntPtr hConsoleHandle, uint dwMode);

		internal static void DisableQuickEdit()
		{
			//IL_0067: Unknown result type (might be due to invalid IL or missing references)
			//IL_006e: Expected O, but got Unknown
			try
			{
				IntPtr stdHandle = GetStdHandle(-10);
				if (!(stdHandle == IntPtr.Zero) && !(stdHandle == new IntPtr(-1)) && GetConsoleMode(stdHandle, out var lpMode))
				{
					uint num = (lpMode & 0xFFFFFFBFu) | 0x80;
					if (num != lpMode && SetConsoleMode(stdHandle, num))
					{
						Plugin.Logger.LogInfo((object)"[Perf] Disabled console QuickEdit mode — the click-to-pause game freeze is gone. (Copy logs from BepInEx/LogOutput.log; set General/DisableConsoleQuickEdit=false to restore mouse-select.)");
					}
				}
			}
			catch (Exception ex)
			{
				ManualLogSource logger = Plugin.Logger;
				bool flag = default(bool);
				BepInExWarningLogInterpolatedStringHandler val = new BepInExWarningLogInterpolatedStringHandler(32, 1, ref flag);
				if (flag)
				{
					((BepInExLogInterpolatedStringHandler)val).AppendLiteral("[Perf] DisableQuickEdit failed: ");
					((BepInExLogInterpolatedStringHandler)val).AppendFormatted<string>(ex.Message);
				}
				logger.LogWarning(val);
			}
		}
	}
	[BepInPlugin("Kropath.TheSyndicate", "The Syndicate", "0.9.0")]
	[BepInDependency(/*Could not decode attribute arguments.*/)]
	public class Plugin : BasePlugin
	{
		internal static ManualLogSource Logger;

		internal static Harmony HarmonyInstance;

		internal static void LogVerbose(string msg)
		{
			ConfigEntry<bool> verboseLogging = SyndicateConfig.VerboseLogging;
			if (verboseLogging != null && verboseLogging.Value)
			{
				Logger.LogInfo((object)msg);
			}
		}

		public override void Load()
		{
			//IL_0015: Unknown result type (might be due to invalid IL or missing references)
			//IL_001b: Expected O, but got Unknown
			//IL_0150: Unknown result type (might be due to invalid IL or missing references)
			//IL_015a: Expected O, but got Unknown
			//IL_017d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0183: Expected O, but got Unknown
			Logger = ((BasePlugin)this).Log;
			ManualLogSource logger = Logger;
			bool flag = default(bool);
			BepInExInfoLogInterpolatedStringHandler val = new BepInExInfoLogInterpolatedStringHandler(13, 2, ref flag);
			if (flag)
			{
				((BepInExLogInterpolatedStringHandler)val).AppendFormatted<string>("The Syndicate");
				((BepInExLogInterpolatedStringHandler)val).AppendLiteral(" v");
				((BepInExLogInterpolatedStringHandler)val).AppendFormatted<string>("0.9.0");
				((BepInExLogInterpolatedStringHandler)val).AppendLiteral(" loading...");
			}
			logger.LogInfo(val);
			SyndicateConfig.Init(((BasePlugin)this).Config);
			ConfigEntry<bool> disableConsoleQuickEdit = SyndicateConfig.DisableConsoleQuickEdit;
			if (disableConsoleQuickEdit == null || disableConsoleQuickEdit.Value)
			{
				ConsoleTweaks.DisableQuickEdit();
			}
			Rewards.Register();
			SyndicateState.Hook();
			InvestigationTimer.Hook();
			Lib.Gameplay.OnVictimReported += delegate(object? sender, VictimReportedArgs args)
			{
				//IL_001e: Unknown result type (might be due to invalid IL or missing references)
				//IL_0024: Expected O, but got Unknown
				try
				{
					CrimeSceneManager.OnBodyReported(((VictimKilledArgs)args).Victim, args.Reporter);
				}
				catch (Exception ex)
				{
					ManualLogSource logger3 = Logger;
					bool flag2 = default(bool);
					BepInExWarningLogInterpolatedStringHandler val2 = new BepInExWarningLogInterpolatedStringHandler(45, 1, ref flag2);
					if (flag2)
					{
						((BepInExLogInterpolatedStringHandler)val2).AppendLiteral("[Syndicate] OnVictimReported handler failed: ");
						((BepInExLogInterpolatedStringHandler)val2).AppendFormatted<string>(ex.Message);
					}
					logger3.LogWarning(val2);
				}
			};
			ConfigEntry<bool> debugEnabled = SyndicateConfig.DebugEnabled;
			if (debugEnabled != null && debugEnabled.Value)
			{
				((BasePlugin)this).AddComponent<DebugHud>();
			}
			((BasePlugin)this).AddComponent<PrintCueOverlay>();
			((BasePlugin)this).AddComponent<SceneWatch>();
			((BasePlugin)this).AddComponent<WarrantHud>();
			((BasePlugin)this).AddComponent<SyndicateHud>();
			HeatMeter.Source = () => SyndicateState.Data?.ResidualHeat ?? 0f;
			((BasePlugin)this).AddComponent<FirearmsHotkey>();
			((BasePlugin)this).AddComponent<MaskHotkey>();
			((BasePlugin)this).AddComponent<FallHeaveHotkey>();
			((BasePlugin)this).AddComponent<FirearmsHud>();
			((BasePlugin)this).AddComponent<PoseLab>();
			TipLine.Init();
			PauseLine.Init();
			FirearmsPose.Init();
			FirearmsAssetTuner.Init();
			EchelonHomeGuard.Init();
			ClassInjector.RegisterTypeInIl2Cpp<SyndicateAppContent>();
			ClassInjector.RegisterTypeInIl2Cpp<SuppliesAppContent>();
			HarmonyInstance = new Harmony("Kropath.TheSyndicate");
			HarmonyInstance.PatchAll(typeof(Plugin).Assembly);
			ManualLogSource logger2 = Logger;
			val = new BepInExInfoLogInterpolatedStringHandler(26, 2, ref flag);
			if (flag)
			{
				((BepInExLogInterpolatedStringHandler)val).AppendFormatted<string>("The Syndicate");
				((BepInExLogInterpolatedStringHandler)val).AppendLiteral(" loaded. ");
				((BepInExLogInterpolatedStringHandler)val).AppendFormatted<int>(HarmonyInstance.GetPatchedMethods().Count());
				((BepInExLogInterpolatedStringHandler)val).AppendLiteral(" methods patched.");
			}
			logger2.LogInfo(val);
		}
	}
	internal static class PluginInfo
	{
		public const string GUID = "Kropath.TheSyndicate";

		public const string NAME = "The Syndicate";

		public const string VERSION = "0.9.0";
	}
	internal static class ProtectedCitizens
	{
		internal const string LandlordPresetName = "SelfEmployedLandlord";

		private static readonly Dictionary<int, float> _lastToastByVictim = new Dictionary<int, float>();

		internal static bool IsProtected(Human h)
		{
			if ((Object)(object)h == (Object)null)
			{
				return false;
			}
			try
			{
				ConfigEntry<bool> protectFiniteNPCs = SyndicateConfig.ProtectFiniteNPCs;
				return (protectFiniteNPCs == null || protectFiniteNPCs.Value) && (((Actor)h).isEnforcer || IsLandlord(h));
			}
			catch
			{
				return true;
			}
		}

		internal static bool IsContractExcluded(Human h)
		{
			if ((Object)(object)h == (Object)null)
			{
				return false;
			}
			if (!IsProtected(h))
			{
				return IsLastCompanyWorker(h);
			}
			return true;
		}

		internal static bool IsLandlord(Human h)
		{
			try
			{
				object obj;
				if (h == null)
				{
					obj = null;
				}
				else
				{
					Occupation job = h.job;
					obj = ((job != null) ? job.preset : null);
				}
				OccupationPreset val = (OccupationPreset)obj;
				return (SoCustomComparison)(object)val != (SoCustomComparison)null && ((Object)val).name == "SelfEmployedLandlord";
			}
			catch
			{
				return false;
			}
		}

		internal static bool IsLastCompanyWorker(Human h)
		{
			ConfigEntry<bool> protectLastCompanyWorker = SyndicateConfig.ProtectLastCompanyWorker;
			if (protectLastCompanyWorker != null && !protectLastCompanyWorker.Value)
			{
				return false;
			}
			try
			{
				if ((Object)(object)h == (Object)null)
				{
					return false;
				}
				if (((Actor)h).isEnforcer || IsLandlord(h))
				{
					return false;
				}
				Occupation job = h.job;
				object obj;
				if (job == null)
				{
					obj = null;
				}
				else
				{
					Company employer = job.employer;
					obj = ((employer != null) ? employer.companyRoster : null);
				}
				List<Occupation> val = (List<Occupation>)obj;
				if (val == null)
				{
					return false;
				}
				bool result = false;
				Enumerator<Occupation> enumerator = val.GetEnumerator();
				while (enumerator.MoveNext())
				{
					Occupation current = enumerator.Current;
					Human val2 = ((current != null) ? current.employee : null);
					if (!((Object)(object)val2 == (Object)null))
					{
						if (val2.humanID == h.humanID)
						{
							result = true;
						}
						else if (!((Actor)val2).isDead)
						{
							return false;
						}
					}
				}
				return result;
			}
			catch
			{
				return true;
			}
		}

		internal static void DegradeToKO(Citizen target)
		{
			//IL_0059: Unknown result type (might be due to invalid IL or missing references)
			//IL_005f: Expected O, but got Unknown
			//IL_0031: Unknown result type (might be due to invalid IL or missing references)
			//IL_0036: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if ((Object)(object)target != (Object)null && (Object)(object)((Actor)target).ai != (Object)null && !((Actor)target).ai.ko)
				{
					((Actor)target).ai.SetKO(true, ((Component)target).transform.position, Vector3.up, false, 0f, true, 1f);
				}
			}
			catch (Exception ex)
			{
				ManualLogSource logger = Plugin.Logger;
				bool flag = default(bool);
				BepInExWarningLogInterpolatedStringHandler val = new BepInExWarningLogInterpolatedStringHandler(36, 1, ref flag);
				if (flag)
				{
					((BepInExLogInterpolatedStringHandler)val).AppendLiteral("[Protect] DegradeToKO SetKO failed: ");
					((BepInExLogInterpolatedStringHandler)val).AppendFormatted<string>(ex.Message);
				}
				logger.LogWarning(val);
			}
			try
			{
				int key = (((Object)(object)target != (Object)null) ? ((Human)target).humanID : (-1));
				float time = Time.time;
				if (_lastToastByVictim.TryGetValue(key, out var value) && time - value < 3f)
				{
					return;
				}
				_lastToastByVictim[key] = time;
			}
			catch
			{
			}
			try
			{
				Lib.GameMessage.Broadcast(RefusalLine((Human)(object)target), (GameMessageType)0, (Icon)21, (Color?)null, 0f);
			}
			catch
			{
			}
		}

		internal static string RefusalLine(Human h)
		{
			try
			{
				if ((Object)(object)h != (Object)null && ((Actor)h).isEnforcer)
				{
					return "Enforcers stay breathing — the Syndicate needs the law predictable. Down, not dead.";
				}
				if (IsLandlord(h))
				{
					return "Kill the landlord and the whole block goes to hell. Down, not dead.";
				}
			}
			catch
			{
			}
			return "This one's off-limits — the Syndicate wants them alive. Down, not dead.";
		}
	}
	internal static class SyndicateConfig
	{
		public static ConfigEntry<int> WitnessReportMinHours;

		public static ConfigEntry<int> WitnessReportMaxHours;

		public static ConfigEntry<int> WelfareMinHours;

		public static ConfigEntry<int> WelfareMaxHours;

		public static ConfigEntry<int> SceneColdHours;

		public static ConfigEntry<int> WitnessInterviewHours;

		public static ConfigEntry<int> WarrantResolveHours;

		public static ConfigEntry<int> WarrantDurationHours;

		public static ConfigEntry<int> FingerprintIdHeatThreshold;

		public static ConfigEntry<int> FingerprintIdCountThreshold;

		public static ConfigEntry<int> FingerprintMinHeat;

		public static ConfigEntry<int> FingerprintMaxHeat;

		public static ConfigEntry<bool> SuppressNativePlayerPrints;

		public static ConfigEntry<bool> ResidualHeatEnabled;

		public static ConfigEntry<float> ResidualHeatPerMurderMax;

		public static ConfigEntry<float> HeistOfferThreshold;

		public static ConfigEntry<int> ResidualFinePerMurder;

		public static ConfigEntry<bool> ResidualWarrantAt100;

		public static ConfigEntry<bool> HeistEnabled;

		public static ConfigEntry<int> HeistBackroomPercent;

		public static ConfigEntry<string> HeistFolderPreset;

		public static ConfigEntry<bool> NewsEnabled;

		public static ConfigEntry<int> NewsFreshHours;

		public static ConfigEntry<int> NewsRoundupWindowDays;

		public static ConfigEntry<int> NewsRoundupMinKills;

		public static ConfigEntry<bool> NewsSecondaryEnabled;

		public static ConfigEntry<bool> MaskNotorietyEnabled;

		public static ConfigEntry<float> MaskNotorietyDecayPerDay;

		public static ConfigEntry<float> MaskIncidentBase;

		public static ConfigEntry<float> MaskIncidentMax;

		public static ConfigEntry<float> MaskWitnessWeight;

		public static ConfigEntry<float> MaskPublicPlaceMult;

		public static ConfigEntry<float> MaskDaytimeMult;

		public static ConfigEntry<float> MaskNerveDrainMult;

		public static ConfigEntry<int> MaskRedisguiseCost;

		public static ConfigEntry<float> MaskRedisguiseDrop;

		public static ConfigEntry<bool> MaskRedisguiseClearsHistory;

		public static ConfigEntry<float> MaskMergeMinColdShare;

		public static ConfigEntry<int> MaskMergeFineBasePercent;

		public static ConfigEntry<int> MaskMergeFineNotorietyPercent;

		public static ConfigEntry<bool> MaskBustedDestroysMask;

		public static ConfigEntry<bool> MaskEnforcerSoftStopEnabled;

		public static ConfigEntry<float> MaskEnforcerSoftStopThreshold;

		public static ConfigEntry<float> MaskEnforcerSoftStopRange;

		public static ConfigEntry<float> MaskEnforcerSoftStopCooldownSeconds;

		public static ConfigEntry<bool> RepairEchelonFloors;

		public static ConfigEntry<bool> CrippleEnabled;

		public static ConfigEntry<float> CrippleSpeedMultiplier;

		public static ConfigEntry<float> CrippleDurationSeconds;

		public static ConfigEntry<bool> CrippleBloodTrailEnabled;

		public static ConfigEntry<bool> CamerasIdentify;

		public static ConfigEntry<int> CameraNearSceneHeat;

		public static ConfigEntry<int> CameraNearSceneFreshWindowHours;

		public static ConfigEntry<bool> CameraSweepEnabled;

		public static ConfigEntry<bool> CamerasRespectDarkness;

		public static ConfigEntry<bool> HudClusterEnabled;

		public static ConfigEntry<bool> HudSceneTableEnabled;

		public static ConfigEntry<HuntMode> HuntMode;

		public static ConfigEntry<float> PursuitResponseRange;

		public static ConfigEntry<float> PursuitMaxDistanceMeters;

		public static ConfigEntry<bool> PursuitRequireLineOfSight;

		public static ConfigEntry<bool> RequirePlayerInsideScene;

		public static ConfigEntry<bool> BodyDiscoveryRequireLineOfSight;

		public static ConfigEntry<bool> WelfareRaidEnabled;

		public static ConfigEntry<float> WelfareRaidDelayHours;

		public static ConfigEntry<bool> WelfareRaidForceCrimeScene;

		public static ConfigEntry<int> MurderFineAmount;

		public static ConfigEntry<float> IllegalRefreshSeconds;

		public static ConfigEntry<bool> StakeoutEnabled;

		public static ConfigEntry<float> HomeRaidDelayHours;

		public static ConfigEntry<bool> StakeoutForceCrimeScene;

		public static ConfigEntry<float> StakeoutGuardHours;

		public static ConfigEntry<int> RepPenaltyOnIdentifiedContract;

		public static ConfigEntry<int> RepPenaltyOnIdentifiedRandom;

		public static ConfigEntry<int> HitmanLevelPenaltyOnIdentified;

		public static ConfigEntry<float> RepMessageDelaySeconds;

		public static ConfigEntry<int> RepPenaltyOnContractCancel;

		public static ConfigEntry<bool> CaseBoardEnabled;

		public static ConfigEntry<int> AutoCollectHours;

		public static ConfigEntry<bool> TipLineEnabled;

		public static ConfigEntry<int> TipLineNumber;

		public static ConfigEntry<bool> PauseLineEnabled;

		public static ConfigEntry<int> PauseLineNumber;

		public static ConfigEntry<int> AutoAssignMaxLevel;

		public static ConfigEntry<int> MaxConcurrentContractsL3;

		public static ConfigEntry<int> OfferBoardSize;

		public static ConfigEntry<int> RefreshBoardCooldownHours;

		public static ConfigEntry<int> OfferLifetimeHours;

		public static ConfigEntry<int> ContractCooldownHours;

		public static ConfigEntry<int> FirstContractDelayHours;

		public static ConfigEntry<int> DeadlineHours;

		public static ConfigEntry<bool> CloseToastEnabled;

		public static ConfigEntry<bool> OnboardingEnabled;

		public static ConfigEntry<int> FirstJobAfterActivationHours;

		public static ConfigEntry<int> CheckInReminder1Minutes;

		public static ConfigEntry<int> CheckInReminder2Minutes;

		public static ConfigEntry<bool> MoleEmailEnabled;

		public static ConfigEntry<int> ContractBasePayout;

		public static ConfigEntry<float> DiffMultT2;

		public static ConfigEntry<float> DiffMultT3;

		public static ConfigEntry<float> InfoMultPartial;

		public static ConfigEntry<float> InfoMultCold;

		public static ConfigEntry<int> InfoFullPctT1;

		public static ConfigEntry<int> InfoPartialPctT1;

		public static ConfigEntry<int> InfoFullPctT2;

		public static ConfigEntry<int> InfoPartialPctT2;

		public static ConfigEntry<int> InfoFullPctT3;

		public static ConfigEntry<int> InfoPartialPctT3;

		public static ConfigEntry<int> InvestigationBias;

		public static ConfigEntry<int> ObjChanceZero;

		public static ConfigEntry<int> ObjChanceOne;

		public static ConfigEntry<float> BonusClean;

		public static ConfigEntry<float> BonusDeadline;

		public static ConfigEntry<float> BonusPhoto;

		public static ConfigEntry<float> BonusSuicide;

		public static ConfigEntry<bool> EnablePhotoObjective;

		public static ConfigEntry<bool> EnableSuicideObjective;

		public static ConfigEntry<bool> EnableDisposalObjective;

		public static ConfigEntry<int> DisposalContractPercent;

		public static ConfigEntry<float> DisposalPenalty;

		public static ConfigEntry<int> DisposalMinLevel;

		public static ConfigEntry<string> DisposalWeaponList;

		public static ConfigEntry<bool> EnableFallForge;

		public static ConfigEntry<int> FallVariantPercent;

		public static ConfigEntry<bool> FallKillsEnabled;

		public static ConfigEntry<float> LethalFallHeight;

		public static ConfigEntry<KeyCode> HeaveKey;

		public static ConfigEntry<float> HeaveSpeed;

		public static ConfigEntry<float> DragLiftAssist;

		public static ConfigEntry<bool> FallSceneAtLanding;

		public static ConfigEntry<bool> FallAntiSnagEnabled;

		public static ConfigEntry<float> FallAntiSnagForce;

		public static ConfigEntry<bool> HomeVacateEnabled;

		public static ConfigEntry<int> HomeVacateDelayHours;

		public static ConfigEntry<float> KODurationMultiplier;

		public static ConfigEntry<float> BluntKOMultiplier;

		public static ConfigEntry<float> BladeKOMultiplier;

		public static ConfigEntry<int> Level1Rep;

		public static ConfigEntry<int> Level2Rep;

		public static ConfigEntry<int> Level3Rep;

		public static ConfigEntry<int> Level1Reward;

		public static ConfigEntry<int> Level2Reward;

		public static ConfigEntry<int> Level3Reward;

		public static ConfigEntry<int> SocialCreditPerToken;

		public static ConfigEntry<int> SocialCreditPurchaseMoneyCost;

		public static ConfigEntry<int> FineMinimumBalance;

		public static ConfigEntry<int> FineEarlyPayoffPercent;

		public static ConfigEntry<bool> RaidWeaponSeizureEnabled;

		public static ConfigEntry<int> RaidWeaponSurchargePercent;

		public static ConfigEntry<bool> CarryWeaponSeizureEnabled;

		public static ConfigEntry<int> CarryWeaponSurchargePercent;

		public static ConfigEntry<bool> CruncherRaidPenaltyEnabled;

		public static ConfigEntry<int> CruncherRaidSurchargePercent;

		public static ConfigEntry<int> CruncherRaidRepLoss;

		public static ConfigEntry<bool> SafehouseBreachEnabled;

		public static ConfigEntry<bool> SuppliesEnabled;

		public static ConfigEntry<int> SuppliesDiscountPercent;

		public static ConfigEntry<int> SuppliesDeliveryHours;

		public static ConfigEntry<bool> GroceriesEnabled;

		public static ConfigEntry<int> GroceriesMarkupPercent;

		public static ConfigEntry<float> GearPriceMultiplier;

		public static ConfigEntry<float> AmmoPriceMultiplier;

		public static ConfigEntry<float> GunPriceMultiplier;

		public static ConfigEntry<bool> FirearmsEnabled;

		public static ConfigEntry<bool> FirearmsHeadshotLethal;

		public static ConfigEntry<float> FirearmsHeadshotRadius;

		public static ConfigEntry<bool> FirearmsHeadshotDebug;

		public static ConfigEntry<string> FirearmsOneShotWeapons;

		public static ConfigEntry<float> FirearmsAccuracy;

		public static ConfigEntry<float> FirearmsMinCooldownSeconds;

		public static ConfigEntry<bool> FirearmsConsumeAmmo;

		public static ConfigEntry<bool> FirearmsSpawnCasings;

		public static ConfigEntry<KeyCode> FirearmsFireKey;

		public static ConfigEntry<KeyCode> FirearmsMeleeKey;

		public static ConfigEntry<float> FirearmsRecoil;

		public static ConfigEntry<bool> FirearmsPoseLab;

		public static ConfigEntry<KeyCode> FirearmsReloadKey;

		public static ConfigEntry<float> FirearmsReloadSeconds;

		public static ConfigEntry<string> FirearmsMagazineSizes;

		public static ConfigEntry<string> FirearmsAmmoBoxSizes;

		public static ConfigEntry<bool> FirearmsAmmoHud;

		public static ConfigEntry<string> FirearmsAutoFireWeapons;

		public static ConfigEntry<float> FirearmsAutoFireInterval;

		public static ConfigEntry<float> FirearmsAutoRecoilScale;

		public static ConfigEntry<int> FirearmsShotgunPellets;

		public static ConfigEntry<float> FirearmsShotgunSpread;

		public static ConfigEntry<int> FirearmsShotgunLethalPellets;

		public static ConfigEntry<KeyCode> FirearmsShotgunAmmoKey;

		public static ConfigEntry<bool> FirearmsDrawnDread;

		public static ConfigEntry<float> FirearmsDrawnNerve;

		public static ConfigEntry<float> FirearmsDrawnRange;

		public static ConfigEntry<bool> FirearmsGunshotsAlarm;

		public static ConfigEntry<string> FirearmsAlarmExemptGuns;

		public static ConfigEntry<float> FirearmsGunshotSpook;

		public static ConfigEntry<bool> FirearmsRoomTurnOnHit;

		public static ConfigEntry<float> FirearmsRoomTurnRange;

		public static ConfigEntry<float> FirearmsInteractYieldDistance;

		public static ConfigEntry<KeyCode> MaskToggleKey;

		public static ConfigEntry<bool> MaskScareEnabled;

		public static ConfigEntry<float> MaskScareNerve;

		public static ConfigEntry<float> MaskScareRange;

		public static ConfigEntry<bool> MaskScareEnforcers;

		public static ConfigEntry<int> MaskedDescriptionHeat;

		public static ConfigEntry<bool> MaskUnmaskInViewIdentifies;

		public static ConfigEntry<bool> PoisonEnabled;

		public static ConfigEntry<bool> PoisonPromptOverride;

		public static ConfigEntry<int> SyringeDoses;

		public static ConfigEntry<int> VialDoses;

		public static ConfigEntry<int> FingerprintRemoverCharges;

		public static ConfigEntry<string> FingerprintRemoverPreset;

		public static ConfigEntry<float> FingerprintRemoverRange;

		public static ConfigEntry<string> FingerprintNoPrintPresets;

		public static ConfigEntry<bool> ChloroformEnabled;

		public static ConfigEntry<string> ChloroformPreset;

		public static ConfigEntry<float> ChloroformRange;

		public static ConfigEntry<float> ChloroformBehindDot;

		public static ConfigEntry<int> ChloroformDoses;

		public static ConfigEntry<bool> SedatePromptOverride;

		public static ConfigEntry<bool> EnableNoViolenceObjective;

		public static ConfigEntry<int> NoViolenceChancePercent;

		public static ConfigEntry<float> BonusNoViolence;

		public static ConfigEntry<int> ForgeViolenceHeat;

		public static ConfigEntry<float> PoisonRange;

		public static ConfigEntry<float> PoisonCooldownSeconds;

		public static ConfigEntry<float> SyringeKillDelayGameMinutes;

		public static ConfigEntry<float> SyringeStaggerDrunk;

		public static ConfigEntry<int> ForgeDelayMinutes;

		public static ConfigEntry<bool> EnablePoisonObjective;

		public static ConfigEntry<float> BonusPoison;

		public static ConfigEntry<bool> ProtectFiniteNPCs;

		public static ConfigEntry<bool> ProtectLastCompanyWorker;

		public static ConfigEntry<bool> DisableConsoleQuickEdit;

		public static ConfigEntry<float> SightCheckIntervalSeconds;

		public static ConfigEntry<bool> VerboseLogging;

		public static ConfigEntry<float> MessageGapSeconds;

		public static ConfigEntry<bool> DebugEnabled;

		public static void Init(ConfigFile cfg)
		{
			WitnessReportMinHours = cfg.Bind<int>("Manhunt", "WitnessReportMinHours", 2, "Minimum in-game hours a witness takes to reach the authorities and report. Kill/KO them before this to cancel it.");
			WitnessReportMaxHours = cfg.Bind<int>("Manhunt", "WitnessReportMaxHours", 2, "Maximum in-game hours a witness takes to report.");
			WelfareMinHours = cfg.Bind<int>("Manhunt", "WelfareMinHours", 24, "Minimum in-game hours before an unseen body is found by a welfare check.");
			WelfareMaxHours = cfg.Bind<int>("Manhunt", "WelfareMaxHours", 48, "Maximum in-game hours before a welfare check finds an unseen body.");
			SceneColdHours = cfg.Bind<int>("Manhunt", "SceneColdHours", 8, "In-game hours a discovered-but-unsolved scene stays open. If no witness reports in time, forensics decide, then it goes cold.");
			WitnessInterviewHours = cfg.Bind<int>("Manhunt", "WitnessInterviewHours", 2, "Once a body is officially reported, pending witnesses who can name the player are interviewed within this many in-game hours (kill/KO them first to prevent it).");
			WarrantResolveHours = cfg.Bind<int>("Manhunt", "WarrantResolveHours", 2, "Grace hours after a positive ID before the warrant/hunt actually begins — a heads-up window, NOT an escape: the warrant issues regardless of where you are.");
			WarrantDurationHours = cfg.Bind<int>("Manhunt", "WarrantDurationHours", 48, "How long a warrant stays active once issued.");
			FingerprintIdHeatThreshold = cfg.Bind<int>("Evidence", "FingerprintIdHeatThreshold", 25, "Total fingerprint heat at a scene that alone makes a positive forensic ID.");
			FingerprintIdCountThreshold = cfg.Bind<int>("Evidence", "FingerprintIdCountThreshold", 4, "Number of distinct fingerprinted objects at a scene that alone makes a positive forensic ID.");
			FingerprintMinHeat = cfg.Bind<int>("Evidence", "FingerprintMinHeat", 3, "Minimum random heat weight assigned to each fingerprint left.");
			FingerprintMaxHeat = cfg.Bind<int>("Evidence", "FingerprintMaxHeat", 12, "Maximum random heat weight assigned to each fingerprint left.");
			SuppressNativePlayerPrints = cfg.Bind<bool>("Evidence", "SuppressNativePlayerPrints", true, "Hide the game's always-on player fingerprint from the vanilla scanner on items you own/carry (weapons, casings, delivered supplies). This static print ignores gloves; suppressing it means only the mod's own gloves-gated print system marks you. Leaves NPC prints (incl. the fixer who mailed a weapon) intact. Disable to restore vanilla behaviour.");
			ResidualHeatEnabled = cfg.Bind<bool>("Heat", "ResidualHeatEnabled", true, "F2 cumulative residual-evidence meter: when a murder's case goes COLD but you still left sub-threshold evidence, the top-centre HEAT bar climbs. At ~80% the Syndicate offers a clearing heist; at 100% the law catches up. Turn off to freeze the meter at 0.");
			ResidualHeatPerMurderMax = cfg.Bind<float>("Heat", "ResidualHeatPerMurderMax", 0.2f, "The MOST a single cold-but-messy murder can add to the residual-evidence meter (0.20 = 20%). The actual add is this times how close the scene came to identifying you (its fingerprint heat/count vs the ID thresholds).");
			HeistOfferThreshold = cfg.Bind<float>("Heat", "HeistOfferThreshold", 0.8f, "At this meter level (0.80 = 80%) the Syndicate v-mails you a tip and opens the 'Loose Ends' heist to clear it. Clamped 0.10-0.95.");
			ResidualFinePerMurder = cfg.Bind<int>("Heat", "ResidualFinePerMurder", 1000, "At 100% the law catches up: this fine (credits) is levied for EACH murder that fed the meter. Half a full-ID murder fine by default (residual evidence is weaker than a positive ID).");
			ResidualWarrantAt100 = cfg.Bind<bool>("Heat", "ResidualWarrantAt100", true, "At 100%, also issue a single warrant (enforcers hunt you) alongside the fines. Off = fines only.");
			HeistEnabled = cfg.Bind<bool>("Heist", "Enabled", true, "Enable the 'Loose Ends' evidence heist offered at ~80% meter. Off = no offer/heist (the meter still climbs to the 100% punishment, which becomes the only outcome).");
			HeistBackroomPercent = cfg.Bind<int>("Heist", "BackroomPercent", 80, "Chance (%) the evidence folder spawns in the enforcer division office's password-protected secure BACKROOM (the highest-security room in the office). Otherwise it lands on a desk deeper in the office (never reception). Clamped 0-100. The folder always spawns in a division office now — never a home.");
			HeistFolderPreset = cfg.Bind<string>("Heist", "FolderPreset", "SealedEnvelope", "Item preset spawned as the stealable evidence folder. Default 'SealedEnvelope' = the in-game 'Secret Envelope'. Blank = auto (prefer a carryable documents item, else a briefcase). Set another PascalCase preset name to override — use the 'Find secret-envelope-like presets' debug button to discover names.");
			NewsEnabled = cfg.Bind<bool>("News", "Enabled", true, "Enable The Syndicate's custom front-page stories in the in-game newspaper (murder/manhunt/heist beats named with the real victim and location) and the matching city news-ticker headline. Off = the vanilla paper only.");
			NewsFreshHours = cfg.Bind<int>("News", "FreshHours", 48, "How long (in-game hours) a Syndicate news beat stays eligible to run on the front page. After this it drops off and the paper reverts to vanilla filler (or the next fresher beat).");
			NewsRoundupWindowDays = cfg.Bind<int>("News", "RoundupWindowDays", 3, "Window (in-game days) the 'crime wave' round-up story counts recent killings over.");
			NewsRoundupMinKills = cfg.Bind<int>("News", "RoundupMinKills", 3, "Minimum distinct victims within the round-up window before the 'crime wave' story runs instead of a single-victim story.");
			NewsSecondaryEnabled = cfg.Bind<bool>("News", "SecondaryStory", true, "Allow a second Syndicate beat to occupy the newspaper's article-2 slot when two stories are live. Off = only the top story is injected and any displaced vanilla lead keeps slot 2.");
			MaskNotorietyEnabled = cfg.Bind<bool>("MaskNotoriety", "Enabled", true, "Master switch for the masked-persona notoriety system (meter, HUD bar, gradient effects, merge-on-slip, busted reveal). Off = masked crimes behave as before F7.");
			MaskNotorietyDecayPerDay = cfg.Bind<float>("MaskNotoriety", "DecayPerDay", 0.15f, "Notoriety lost per in-game day of lying low (not being seen committing a masked crime). 0.15 ≈ a maxed persona fades in ~7 days.");
			MaskIncidentBase = cfg.Bind<float>("MaskNotoriety", "IncidentBase", 0.1f, "Base notoriety added per masked crime that gets seen (before brazenness scaling).");
			MaskIncidentMax = cfg.Bind<float>("MaskNotoriety", "IncidentMax", 0.3f, "Cap on the notoriety added by a single masked incident after brazenness scaling.");
			MaskWitnessWeight = cfg.Bind<float>("MaskNotoriety", "WitnessWeight", 0.15f, "+fraction per witness beyond the first (capped at 4 extra), multiplying the incident bump.");
			MaskPublicPlaceMult = cfg.Bind<float>("MaskNotoriety", "PublicPlaceMult", 1.5f, "Multiplier when the masked crime is in a public place (street / lobby / common area).");
			MaskDaytimeMult = cfg.Bind<float>("MaskNotoriety", "DaytimeMult", 1.25f, "Multiplier when the masked crime happens in daytime (~07:00–21:00).");
			MaskNerveDrainMult = cfg.Bind<float>("MaskNotoriety", "NerveDrainMult", 4f, "Extra nerve-drain multiplier at full notoriety for civilians who SEE the masked player (they panic/flee sooner). Scales from 0 (base ambient dread only) up to this at N=1.");
			MaskRedisguiseCost = cfg.Bind<int>("MaskNotoriety", "RedisguiseCost", 2500, "Cost of a 'Persona Wipe' (new disguise) from the Supplies app, which drops notoriety.");
			MaskRedisguiseDrop = cfg.Bind<float>("MaskNotoriety", "RedisguiseDrop", 1f, "Notoriety removed per Persona Wipe (1.0 = full reset).");
			MaskRedisguiseClearsHistory = cfg.Bind<bool>("MaskNotoriety", "RedisguiseClearsHistory", false, "If true, a Persona Wipe also erases the masked-incident case files (a later slip can't merge old crimes). Default false = the street cools but the case files remain.");
			MaskMergeMinColdShare = cfg.Bind<float>("MaskNotoriety", "MergeMinColdShare", 0.25f, "Share of the COLD masked spree merged onto you on a slip at zero notoriety (scales to 100% at full notoriety), newest-first.");
			MaskMergeFineBasePercent = cfg.Bind<int>("MaskNotoriety", "MergeFineBasePercent", 50, "Per-merged-cold-incident fine as a % of the standard murder fine, at zero notoriety.");
			MaskMergeFineNotorietyPercent = cfg.Bind<int>("MaskNotoriety", "MergeFineNotorietyPercent", 50, "Additional per-incident fine %, scaled by notoriety (so at N=1 the fine = Base+this % of the murder fine).");
			MaskBustedDestroysMask = cfg.Bind<bool>("MaskNotoriety", "BustedDestroysMask", true, "If true, getting busted while masked confiscates (uninstalls) the mask disk — you must acquire a new one.");
			MaskEnforcerSoftStopEnabled = cfg.Bind<bool>("MaskNotoriety", "EnforcerSoftStopEnabled", false, "OFF by default. When on, at high notoriety enforcers who SEE the masked player give a soft 'stop and look' (heightened suspicion, NOT a chase). Fires off raw line-of-sight — tune before enabling.");
			MaskEnforcerSoftStopThreshold = cfg.Bind<float>("MaskNotoriety", "EnforcerSoftStopThreshold", 0.5f, "Minimum notoriety before enforcers start eyeing the mask (soft-stop tier).");
			MaskEnforcerSoftStopRange = cfg.Bind<float>("MaskNotoriety", "EnforcerSoftStopRange", 12f, "Metres within which a seeing enforcer reacts to the notorious mask (kept tight vs the 50m warrant pursuit).");
			MaskEnforcerSoftStopCooldownSeconds = cfg.Bind<float>("MaskNotoriety", "EnforcerSoftStopCooldownSeconds", 90f, "Per-enforcer cooldown between soft-stop looks.");
			RepairEchelonFloors = cfg.Bind<bool>("Echelons", "RepairEchelonFloors", true, "Repair high-security 'echelon' floors on city load. A new game LOADS a pre-generated city whose floors deserialize with isEchelons stuck false (a vanilla save/load bug — NewFloor.Load trusts a stale serialized flag instead of recomputing it), so echelon floors never trespass-flag you and cameras/enforcers ignore you there even without the echelon-access perk. This recomputes the flag per floor from the building preset exactly as city-generation does, restoring trespassing + camera/enforcer detection. Idempotent; a no-op on correctly-serialized cities; self-heals existing saves. Disable to restore vanilla (buggy) behaviour.");
			CrippleEnabled = cfg.Bind<bool>("Cripple", "Enabled", true, "Non-lethal player melee/gunshot hits 'cripple' an NPC — they move slower (limp) for a while. Refresh-only (repeat hits reset the timer, never stack). Session-only: a mid-effect save/load forgets it (it's a short combat debuff).");
			CrippleSpeedMultiplier = cfg.Bind<float>("Cripple", "SpeedMultiplier", 0.3f, "Crippled NPCs move at this fraction of normal speed (0.3 = 70% slower). Clamped 0.05-1.");
			CrippleDurationSeconds = cfg.Bind<float>("Cripple", "DurationSeconds", 90f, "How long (REAL seconds) a cripple lasts before the NPC recovers full speed.");
			CrippleBloodTrailEnabled = cfg.Bind<bool>("Cripple", "BloodTrailEnabled", true, "#D: a crippled NPC leaves a light blood drip as they limp (pure flair — no health loss, no aggro; the drops use the game's own blood spatter and fade on the normal decal timer). Off = no trail.");
			CamerasIdentify = cfg.Bind<bool>("Cameras", "Identify", true, "Security cameras that see you commit a murder, OR see you INSIDE the crime-scene apartment, IDENTIFY you — a guaranteed warrant, beaten only by wearing the mask. A camera that only catches you in the common AREA of the scene's floor (hall/landing) does NOT identify you — it just raises heat (see NearSceneHeat), since it never saw the murder or placed you at the body. Cameras act as eyewitnesses that can't be knocked out. Cameras inside your OWN apartment are exempt.");
			CameraNearSceneHeat = cfg.Bind<int>("Cameras", "NearSceneHeat", 15, "Heat added when a common-area camera (hall/landing on the crime-scene floor) catches you UNMASKED near — but not inside — a crime scene. It does NOT identify you (no warrant); it only raises suspicion. The camera can't report a murder it hasn't found, so — like a witness — it RECORDS the sighting at the time (you get a cue) and the heat only lands once the body is discovered and the footage is reviewed (about WitnessReportMinHours-MaxHours later). Once per scene. A camera inside the scene apartment, or one that saw the kill, still identifies you outright.");
			CameraNearSceneFreshWindowHours = cfg.Bind<int>("Cameras", "NearSceneFreshWindowHours", 2, "How many in-game HOURS after a kill a common-area camera sighting still raises suspicion (see NearSceneHeat). Past this window, a camera that catches you in the hall/landing near the scene no longer adds heat — a casual sighting has gone cold. Only affects the SUSPICION path; a camera that saw the murder or caught you INSIDE the scene still identifies you outright, regardless of time. 0 = no window (a sighting counts for as long as the case is open — the previous behaviour).");
			CameraSweepEnabled = cfg.Bind<bool>("Cameras", "SweepEnabled", true, "When a camera spots a WANTED or MASKED player, dispatch a brief enforcer sweep to WHERE THEY WERE SPOTTED — separate from the crime-scene response and the home raid. ONE sweep at a time: a new one is sent only after the previous finishes, so a hunted player is chased with natural delay instead of spamming the city's ~12 enforcers.");
			CamerasRespectDarkness = cfg.Bind<bool>("Cameras", "RespectDarkness", true, "Cameras use the game's OWN light/stealth model: your personal light level (plus crouch-stealth; running or a lit FLASHLIGHT gives you away) sets the maximum distance a camera can detect you, exactly like vanilla camera sighting — standing in the dark defeats a camera unless you're nearly under it (point-blank ~0.75m always spots you). Applies to EVERY Syndicate camera check: kill-witness ID, at-scene ID, near-scene suspicion heat, the wanted-player camera hunt, and the unmask face-connect. Human witnesses are unaffected. It can only ever SHRINK detection in darkness (a provable no-op in bright light). Disable to restore the previous behaviour (cameras ignore darkness).");
			HudClusterEnabled = cfg.Bind<bool>("Hud", "ClusterEnabled", true, "Show the Syndicate HUD cluster along the top of the screen: your Syndicate level and open crime scenes (top-left), a MASK ON/OFF indicator (top-right, when the mask disk is installed), and the heat/evidence meter (top-centre). Turn off to hide the whole cluster.");
			HudSceneTableEnabled = cfg.Bind<bool>("Hud", "SceneTableEnabled", true, "Show a small table under the Syndicate-level badge listing your open crime scenes (victim + status: undiscovered / under investigation / going cold). Auto-hides when you have no open scenes. Requires the HUD cluster to be on.");
			HuntMode = cfg.Bind<HuntMode>("Enforcers", "HuntMode", TheSyndicate.Heat.HuntMode.EnforcerPursuit, "How enforcers become hostile while you're wanted. EnforcerPursuit (recommended): only enforcers who actually SEE you chase you — no civilian panic. GlobalIllegal: vanilla 'wanted' — the whole city reacts (may cause a hitch when the hunt starts).");
			PursuitResponseRange = cfg.Bind<float>("Enforcers", "PursuitResponseRange", 5f, "SetPersue 'response range' when an enforcer acquires you — how far additional enforcers get recruited. Keep low to avoid pathfinding spikes.");
			PursuitMaxDistanceMeters = cfg.Bind<float>("Enforcers", "PursuitMaxDistanceMeters", 50f, "EnforcerPursuit mode: only consider enforcers this close (or in your building) for the sight check each tick.");
			PursuitRequireLineOfSight = cfg.Bind<bool>("Enforcers", "PursuitRequireLineOfSight", true, "Require a physical line-of-sight ray (no wall in between) before an enforcer or witness counts as seeing you. Disable to restore the vanilla awareness-only check.");
			RequirePlayerInsideScene = cfg.Bind<bool>("Manhunt", "RequirePlayerInsideScene", true, "Only count 'caught red-handed' / scene witnesses when the PLAYER is physically inside the crime scene's apartment (any room) — not the lobby/stairs/landing/another floor.");
			BodyDiscoveryRequireLineOfSight = cfg.Bind<bool>("Manhunt", "BodyDiscoveryRequireLineOfSight", true, "Require a physical line-of-sight ray before an NPC in a DIFFERENT room can spot a dead/KO'd body (vanilla sees through walls within ~6.5m). Same-room spotting and body smell are unaffected — a hidden body is still found by anyone who walks in or eventually smells it. Disable to restore vanilla through-wall spotting.");
			WelfareRaidEnabled = cfg.Bind<bool>("Manhunt", "WelfareRaidEnabled", true, "When the welfare window on an undiscovered body lapses, dispatch an enforcer welfare check to the victim's home (a concerned relative / missed-work report). The cops finding the body IS the discovery. Disabled: the scene silently flips to discovered as before.");
			WelfareRaidDelayHours = cfg.Bind<float>("Manhunt", "WelfareRaidDelayHours", 0.25f, "In-GAME hours between the welfare call coming in and enforcers setting out (CallEnforcers' delay is game-hours). 0.25h = 15 game-minutes.");
			WelfareRaidForceCrimeScene = cfg.Bind<bool>("Manhunt", "WelfareRaidForceCrimeScene", false, "If true the welfare response is dispatched pre-flagged as a crime scene (tape + guard). Leave false: the game flags it automatically the moment responders find a dead occupant, and a false alarm (body moved) then stays low-key.");
			MurderFineAmount = cfg.Bind<int>("Enforcers", "MurderFineAmount", 2000, "Credits added to the Syndicate fine ledger per Identified murder. Collected on capture (leaving a minimum), remainder persists forever; settleable early at a discount.");
			IllegalRefreshSeconds = cfg.Bind<float>("Enforcers", "IllegalRefreshSeconds", 30f, "Seconds the player's 'illegal' hunt flag lasts per refresh while warranted (re-applied at half this cadence). Lower = it lapses faster if the mod ever stops ticking.");
			StakeoutEnabled = cfg.Bind<bool>("Enforcers", "StakeoutEnabled", true, "On a warrant, send enforcers to the suspect's home. If the suspect is there, enforcer-pursuit engages; if not, the native response guards/patrols it.");
			HomeRaidDelayHours = cfg.Bind<float>("Enforcers", "HomeRaidDelayHours", 0.05f, "In-GAME hours before enforcers set out for the suspect's home after a warrant (CallEnforcers' delay is game-hours, not seconds). 0.05h ~ 3 game-minutes; set 0 for instant.");
			StakeoutGuardHours = cfg.Bind<float>("Enforcers", "StakeoutGuardHours", 24f, "Game-hours per guard-duty shift at the suspect's home (mirrors the game's own 24h guard goal). The guard is automatically re-posted while the warrant lasts.");
			StakeoutForceCrimeScene = cfg.Bind<bool>("Enforcers", "StakeoutForceCrimeScene", false, "If true the home raid is dispatched as a crime scene (tapes + a lingering guard, like a murder scene); if false it's a plain targeted response. Probe both to see which posts a guard.");
			RepPenaltyOnIdentifiedContract = cfg.Bind<int>("Enforcers", "RepPenaltyOnIdentifiedContract", 25, "Underworld rep the Syndicate docks when you're Identified for killing one of THEIR contract targets (once per murder). Botching a sanctioned job embarrasses them. 0 disables.");
			RepPenaltyOnIdentifiedRandom = cfg.Bind<int>("Enforcers", "RepPenaltyOnIdentifiedRandom", 10, "Underworld rep docked when you're Identified for a NON-contract (freelance/random) kill — less than a botched contract, but still sloppy. 0 disables.");
			HitmanLevelPenaltyOnIdentified = cfg.Bind<int>("Enforcers", "HitmanLevelPenaltyOnIdentified", 0, "Reserved: hitman-level knock on being Identified (tier already derives from rep). 0 = off.");
			RepMessageDelaySeconds = cfg.Bind<float>("Enforcers", "RepMessageDelaySeconds", 20f, "Seconds to delay the 'Syndicate standing has taken a hit' toast so it doesn't flood the screen during the caught/wanted flurry.");
			FineMinimumBalance = cfg.Bind<int>("Fines", "FineMinimumBalance", 5, "Credits always left in your pocket when murder fines are collected at hospital (vanilla leaves 5).");
			FineEarlyPayoffPercent = cfg.Bind<int>("Fines", "FineEarlyPayoffPercent", 60, "Settle your outstanding murder fines EARLY (before capture) for this percent of the total. Capture always collects 100%.");
			RaidWeaponSeizureEnabled = cfg.Bind<bool>("Fines", "RaidWeaponSeizureEnabled", true, "During the warrant home-raid, if a murder weapon you used is at your apartment, enforcers seize it (it's destroyed) and surcharge that murder's fine — even if you're not home.");
			RaidWeaponSurchargePercent = cfg.Bind<int>("Fines", "RaidWeaponSurchargePercent", 50, "Home-raid weapon-seizure surcharge, per seized weapon per victim, as a percent of MurderFineAmount (50 = 1000cr at the 2000 default). 0 = seize with no fine bump.");
			CarryWeaponSeizureEnabled = cfg.Bind<bool>("Fines", "CarryWeaponSeizureEnabled", true, "If you're busted (downed while wanted) while carrying a murder weapon you used, it's confiscated (destroyed) and that murder's fine is surcharged.");
			CarryWeaponSurchargePercent = cfg.Bind<int>("Fines", "CarryWeaponSurchargePercent", 50, "Caught-carrying weapon surcharge, per confiscated weapon per victim, as a percent of MurderFineAmount (50 = 1000cr at the 2000 default). 0 = confiscate with no fine bump.");
			CruncherRaidPenaltyEnabled = cfg.Bind<bool>("Fines", "CruncherRaidPenaltyEnabled", true, "During the warrant home-raid, if a computer at your home is powered on AND logged in as you, enforcers pull your files: the murder fine is surcharged and the Syndicate docks your standing. Power off or log out before you run.");
			CruncherRaidSurchargePercent = cfg.Bind<int>("Fines", "CruncherRaidSurchargePercent", 25, "Cruncher-exposure surcharge as a percent of MurderFineAmount (25 = 500cr at the 2000 default). 0 = no fine bump.");
			CruncherRaidRepLoss = cfg.Bind<int>("Fines", "CruncherRaidRepLoss", 5, "Underworld rep docked when the raid finds your cruncher powered on and logged in. 0 disables.");
			SafehouseBreachEnabled = cfg.Bind<bool>("Enforcers", "SafehouseBreachEnabled", true, "During a warrant, when hunters see you flee into your own (locked) home, dispatch ONE enforcer breach+sweep so they don't just give up at your door. If they don't find you they leave; the landing guard stays posted. Fleeing home again triggers another breach.");
			RepPenaltyOnContractCancel = cfg.Bind<int>("Contracts", "RepPenaltyOnContractCancel", 5, "Underworld rep the Syndicate docks when you cancel/abandon an active contract. Backing out has a price, but a small one. 0 disables.");
			CaseBoardEnabled = cfg.Bind<bool>("Contracts", "CaseBoardEnabled", true, "Give each accepted hit contract a custom case on the detective pinboard (target + objective checklist). Purely presentation — turning it off changes nothing about payouts or objectives; the contract still works entirely through the Cruncher and v-mail.");
			AutoCollectHours = cfg.Bind<int>("Contracts", "AutoCollectHours", 24, "In-game hours after a contract's payment becomes collectable (its crime scene resolves) before the fixer auto-wires it if you never collect at your terminal — a safety net so payment can never get stranded. Clamped to at least 1.");
			TipLineEnabled = cfg.Bind<bool>("TipLine", "Enabled", true, "Enable the Syndicate anonymous tip line — an in-world phone number you can dial to 'call in' a hidden contract body, so the investigation (and your forensics-gated payment) starts sooner instead of waiting for the welfare check. It's accelerate-only: leaving prints/witnesses still gets you caught, just sooner. When off, no tip line is registered and the case-board hint / v-mail line are omitted.");
			TipLineNumber = cfg.Bind<int>("TipLine", "Number", 9008477, "The tip line's phone number (7 digits, shown as XXX-XXXX). Default 9008477 = '900-TIPS'. Keep it outside BOTH the game's real-number band (1000000-6999000) AND its dynamic fake-callback band (8000000-8999000, which murder-ransom and side-job 'call this number' objectives draw from), and away from the reserved service numbers (1540000/5410000/9110000) — only change it if a save shows a collision.");
			PauseLineEnabled = cfg.Bind<bool>("PauseLine", "Enabled", true, "Enable the Dispatcher line — a phone number (in your phonebook from the start) you dial to go off the books: no new auto-assigned contracts, no board refills, no accepts, until you dial again. Active contracts are unaffected. If disabled while paused, contracts auto-resume on next load.");
			PauseLineNumber = cfg.Bind<int>("PauseLine", "Number", 9007378, "The Dispatcher's number (7 digits, shown as XXX-XXXX). Default 9007378 = '900-REST'. Keep it outside the game's real-number band (1000000-6999000), its dynamic fake-callback band (8000000-8999000), away from the reserved service numbers (1540000/5410000/9110000), and different from the tip line — only change it if a save shows a collision.");
			AutoAssignMaxLevel = cfg.Bind<int>("Contracts", "AutoAssignMaxLevel", 2, "Highest Syndicate level at which the fixer AUTO-ASSIGNS contracts (one at a time, via v-mail). From the next level up you pick your own jobs on the home Cruncher's job board.");
			MaxConcurrentContractsL3 = cfg.Bind<int>("Contracts", "MaxConcurrentContractsL3", 3, "How many contracts you may hold active at once once the job board unlocks (Level 3). Below that it is always one at a time. Accept jobs from the board up to this cap; a value of 1 restores the old single-contract behaviour at every level.");
			OfferBoardSize = cfg.Bind<int>("Contracts", "OfferBoardSize", 8, "How many contract offers the L3 Cruncher job board holds (1-8). The app pages the list (store/status/refresh rows stay pinned, roughly 3 offers per page), so larger boards just add pages.");
			RefreshBoardCooldownHours = cfg.Bind<int>("Contracts", "RefreshBoardCooldownHours", 6, "In-game hours between manual board REFRESHes (the L3 Cruncher's REFRESH row rerolls the whole board for free). The cooldown stops reroll-fishing for a specific objective. 0 = no cooldown.");
			OfferLifetimeHours = cfg.Bind<int>("Contracts", "OfferLifetimeHours", 48, "In-game hours an un-accepted listing stays on the L3 Cruncher job board before the market moves on and it is replaced (evaluated when the board renders). Each listing varies by a few hours so the board turns over gradually. Very low values churn the board every open.");
			ContractCooldownHours = cfg.Bind<int>("Contracts", "ContractCooldownHours", 12, "In-game hours after a contract closes (completed/failed/cancelled) before the next auto-assigned job arrives.");
			FirstContractDelayHours = cfg.Bind<int>("Contracts", "FirstContractDelayHours", 1, "In-game hours after becoming eligible (activating with the Dispatcher, or reaching a contractable level with onboarding off) before the first auto-assigned contract v-mail lands. NOTE: onboarding uses [Onboarding] FirstJobAfterActivationHours for the post-check-in first job instead.");
			DeadlineHours = cfg.Bind<int>("Contracts", "DeadlineHours", 48, "In-game hours the optional deadline objective allows from the moment the contract is issued (values below 24 are clamped — the client always gives you at least a day to investigate and plan).");
			CloseToastEnabled = cfg.Bind<bool>("Contracts", "CloseToastEnabled", true, "Feature 4: after a contract closes, a short toast tells you when the next job lands (and, pre-L3, that you can call the Dispatcher to pull one now).");
			OnboardingEnabled = cfg.Bind<bool>("Onboarding", "Enabled", true, "The v1.0 onboarding flow: an intro v-mail on a new game, then contracts stay GATED until you call the Dispatcher to check in (with 30/60-min reminders), a gentle first 'proving' hit, and one-shot tutorial toasts. Turn OFF to start already activated with no intro/tutorial (contracts flow immediately once you reach Level 1).");
			FirstJobAfterActivationHours = cfg.Bind<int>("Onboarding", "FirstJobAfterActivationHours", 2, "In-game hours after you call the Dispatcher to check in before the FIRST contract lands — time to learn the streets. 0 = immediate.");
			CheckInReminder1Minutes = cfg.Bind<int>("Onboarding", "CheckInReminder1Minutes", 30, "In-game minutes after the intro v-mail before the first 'call the Dispatcher' reminder toast. 0 disables it.");
			CheckInReminder2Minutes = cfg.Bind<int>("Onboarding", "CheckInReminder2Minutes", 60, "In-game minutes after the intro v-mail before the SECOND and final check-in reminder. After this, reminders stop — calling in is your choice. 0 disables it.");
			MoleEmailEnabled = cfg.Bind<bool>("Heat", "MoleEmailEnabled", true, "Feature 5: when a case goes cold but you left forensic traces, a v-mail from the Syndicate's source inside the Enforcers itemizes exactly what fed the residual heat (fingerprints by room, a camera that clocked you, a beaten body).");
			ContractBasePayout = cfg.Bind<int>("ContractEconomy", "ContractBasePayout", 900, "BASE of the payout formula: payout = BASE x difficulty x info x (1 + sum of met objective bonuses).");
			DiffMultT2 = cfg.Bind<float>("ContractEconomy", "DiffMultT2", 1.7f, "Difficulty multiplier for Tier 2 contracts (Tier 1 is fixed at 1.0).");
			DiffMultT3 = cfg.Bind<float>("ContractEconomy", "DiffMultT3", 2.8f, "Difficulty multiplier for Tier 3 contracts.");
			InfoMultPartial = cfg.Bind<float>("ContractEconomy", "InfoMultPartial", 1.5f, "Payout multiplier when the offer only carries a PARTIAL lead (you investigate the rest; Full is fixed at 1.0).");
			InfoMultCold = cfg.Bind<float>("ContractEconomy", "InfoMultCold", 2.2f, "Payout multiplier for a COLD lead (a thin scrap — initial, description, district).");
			InfoFullPctT1 = cfg.Bind<int>("ContractEconomy", "InfoFullPctT1", 60, "Tier-1 info distribution: percent of offers with a FULL dossier. Partial below; Cold = 100 - Full - Partial.");
			InfoPartialPctT1 = cfg.Bind<int>("ContractEconomy", "InfoPartialPctT1", 40, "Tier-1 info distribution: percent with a PARTIAL lead.");
			InfoFullPctT2 = cfg.Bind<int>("ContractEconomy", "InfoFullPctT2", 40, "Tier-2 info distribution: percent FULL.");
			InfoPartialPctT2 = cfg.Bind<int>("ContractEconomy", "InfoPartialPctT2", 45, "Tier-2 info distribution: percent PARTIAL (Cold = remainder, 15 by default).");
			InfoFullPctT3 = cfg.Bind<int>("ContractEconomy", "InfoFullPctT3", 15, "Tier-3 info distribution: percent FULL.");
			InfoPartialPctT3 = cfg.Bind<int>("ContractEconomy", "InfoPartialPctT3", 50, "Tier-3 info distribution: percent PARTIAL (Cold = remainder, 35 by default).");
			InvestigationBias = cfg.Bind<int>("ContractEconomy", "InvestigationBias", 0, "Single global shifter (-50..50): positive moves offer chance from Full toward Partial/Cold (more detective work), negative toward Full (more point-and-click).");
			ObjChanceZero = cfg.Bind<int>("ContractEconomy", "ObjChanceZero", 45, "Percent of contracts rolled with ZERO optional objectives.");
			ObjChanceOne = cfg.Bind<int>("ContractEconomy", "ObjChanceOne", 40, "Percent with exactly ONE optional objective (two = the remainder, 15 by default).");
			BonusClean = cfg.Bind<float>("ContractEconomy", "BonusClean", 0.25f, "Payout fraction added for the clean/no-ID objective — paid in ESCROW when the victim's case closes without identifying you.");
			BonusDeadline = cfg.Bind<float>("ContractEconomy", "BonusDeadline", 0.2f, "Payout fraction added for completing within the deadline.");
			BonusPhoto = cfg.Bind<float>("ContractEconomy", "BonusPhoto", 0.3f, "Payout fraction added for the photo-of-body objective — earned by photographing the corpse any time after the kill (before you collect).");
			BonusSuicide = cfg.Bind<float>("ContractEconomy", "BonusSuicide", 0.5f, "Payout fraction for the forge-suicide objective — paid in ESCROW when the victim's forged-overdose case closes WITHOUT identifying you (a blown forgery pays nothing and brings heat). Needs the vial (#48), which is mailed on accept.");
			BonusPoison = cfg.Bind<float>("ContractEconomy", "BonusPoison", 0.35f, "Payout fraction for the poison-kill objective — earned by killing the target with the Syndicate syringe (#48), which is mailed to you on accept. Any other kill method misses it.");
			EnablePhotoObjective = cfg.Bind<bool>("ContractEconomy", "EnablePhotoObjective", true, "Allow the photo-of-body bonus objective to roll (landed in slice 3). Photograph the corpse with the camera any time after the kill and before you collect to earn the bonus; missing it just drops that bonus.");
			EnablePoisonObjective = cfg.Bind<bool>("ContractEconomy", "EnablePoisonObjective", true, "Allow the poison-kill objective to roll (#48). The target must be killed with the mailed syringe. Mutually exclusive with the forge-suicide objective on a given contract.");
			EnableSuicideObjective = cfg.Bind<bool>("ContractEconomy", "EnableSuicideObjective", true, "Allow the forge-suicide objective to roll (#48). Stage a clean overdose with the mailed vial; the bonus escrows and pays only if the case closes without your name on it. Mutually exclusive with the poison-kill objective on a given contract.");
			EnableDisposalObjective = cfg.Bind<bool>("ContractEconomy", "EnableDisposalObjective", true, "Allow the weapon-disposal-at-sea TERM to attach to a contract (#73). The Syndicate mails you a specific poison/melee weapon; you must kill the target with THAT weapon and then throw it in the sea. Keep the weapon and the base fee takes a cut (DisposalPenalty). Not a bonus — a penalty-bearing term.");
			DisposalContractPercent = cfg.Bind<int>("ContractEconomy", "DisposalContractPercent", 25, "Percent of eligible contracts (poison/melee-killable, not already a poison-kill/forge-suicide job) that carry the sea-disposal term. 0 disables it without touching EnableDisposalObjective.");
			DisposalPenalty = cfg.Bind<float>("ContractEconomy", "DisposalPenalty", 0.45f, "Fraction of the BASE fee DEDUCTED when you don't dump the issued weapon at sea (you keep the weapon instead). 0.45 = a ~45% base cut. Clamped so the payout never goes negative.");
			DisposalMinLevel = cfg.Bind<int>("ContractEconomy", "DisposalMinLevel", 0, "Lowest Syndicate level at which the sea-disposal term attaches to a poison/forge job (0 = from the very start, so a Level-0 proving hit that mails you a syringe also asks you to dump it).");
			DisposalWeaponList = cfg.Bind<string>("ContractEconomy", "DisposalWeaponList", "Syringe,KitchenKnifeLarge,CombatKnife,Truncheon,BasBouleBat,VintageSword,Katana", "PascalCase item presets the Syndicate may issue for a sea-disposal job — POISON or MELEE only, never firearms. Comma-separated. One is picked at random per contract, mailed on accept.");
			EnableFallForge = cfg.Bind<bool>("ContractEconomy", "EnableFallForge", true, "Allow the forge-suicide objective to roll its 'forge a fall' variant (drag a KO'd target off a high point — stairwell opening, rooftop) instead of the overdose variant (#75). While off, every forge-suicide job is the overdose variant.");
			FallVariantPercent = cfg.Bind<int>("ContractEconomy", "FallVariantPercent", 40, "When EnableFallForge is on, percent of forge-suicide jobs that use the 'forge a fall' variant (the rest are 'forge an overdose').");
			FallKillsEnabled = cfg.Bind<bool>("Fall", "FallKillsEnabled", true, "Enable fall-damage kills (#75): drag a KO'd (still-living) citizen off a high point — a stairwell opening, a rooftop — and the fall kills them (the base game has no NPC fall damage). A clean case reads as an accidental fall. Off = dropped bodies just land, as in vanilla.");
			LethalFallHeight = cfg.Bind<float>("Fall", "LethalFallHeight", 6f, "How far (in metres) a dragged KO'd body must fall below its release point to die from the impact. ~6m is roughly two floors, so a KO'd body dropped from a second-floor window dies. Lower = easier fall kills.");
			HeaveKey = cfg.Bind<KeyCode>("Fall", "HeaveKey", (KeyCode)103, "While DRAGGING a KO'd body, press this to HEAVE it in the direction you're looking (the vanilla drag can't lift a body up and out a window well). Drag the body to the opening on the ground, aim out, and heave. None = disabled.");
			HeaveSpeed = cfg.Bind<float>("Fall", "HeaveSpeed", 6f, "Launch speed (metres/second) of a heaved body. Higher = it flies further/faster out the window. Tune to taste.");
			DragLiftAssist = cfg.Bind<float>("Fall", "DragLiftAssist", 0.8f, "0..1 lift assist while DRAGGING a KO'd body — cancels this fraction of gravity on the ragdoll so it feels lighter to lift up and out a window (the native drag can't be made mass-lighter; this counters gravity instead). 0 = vanilla weight, 1 = weightless while held (never > gravity, so it won't float away). Only applies while you're dragging; released/heaved bodies fall normally.");
			FallSceneAtLanding = cfg.Bind<bool>("Fall", "FallSceneAtLanding", true, "After a fall kill (#75/#83), re-anchor the crime scene to where the corpse comes to REST, so a fall down a deep shaft or off a rooftop reads at the landing instead of the mid-fall kill point. (The kill itself ALWAYS fires the instant a lethal fall is confirmed — a dead body is immune to the game's ragdoll-recovery teleport that otherwise makes a still-living thrown body vanish to a random spot.) Off = leave the scene at the kill point.");
			FallAntiSnagEnabled = cfg.Bind<bool>("Fall", "FallAntiSnagEnabled", true, "#J: a body heaved out a window sometimes snags on the building's outer wall mid-fall and hangs there instead of dropping to the street. With this on, a body that stalls in mid-air while falling (confirmed by a downward probe finding no ground beneath it) gets a short downward shove to knock it loose so it keeps falling. Collision is NEVER disabled (the body always lands normally — no risk of it dropping through the world), so the worst case is a harmless no-op. Off = vanilla fall physics.");
			FallAntiSnagForce = cfg.Bind<float>("Fall", "FallAntiSnagForce", 6f, "#J: downward shove speed (m/s) applied to a mid-air-snagged falling body to dislodge it. Higher = a firmer knock. Clamped to a small floor.");
			HomeVacateEnabled = cfg.Bind<bool>("Cleanup", "HomeVacateEnabled", true, "#83 crime-scene coherence: when a target is killed AWAY from home and their case resolves, the game only cleans the death location — their apartment is left frozen as if they still live there. With this on, the Syndicate quietly vacates the victim's home a while later (HomeVacateDelayHours) exactly as the base game does for an at-home death: their furniture is removed and the address is cleared, but only once you're not there to see it and no living resident remains. Off = leave the home untouched (vanilla behaviour for away kills).");
			HomeVacateDelayHours = cfg.Bind<int>("Cleanup", "HomeVacateDelayHours", 24, "In-game hours after an away-from-home kill's case resolves before the victim's empty home is vacated (mirrors the base-game crime-scene cleanup delay). Only fires while you're elsewhere.");
			KODurationMultiplier = cfg.Bind<float>("Combat", "KODurationMultiplier", 3f, "Multiplies how long a knocked-out NPC stays down, so you have time to drag + throw them (the base game's KO is short). Vanilla base is ~1 in-game hour, so 3 = ~3 in-game hours. Only affects ROLLED knockouts (a normal non-lethal takedown); the Syndicate's own timed KOs — poison holds, witness pins — are left exactly as set, and the PLAYER's own KO is never lengthened. 1 = vanilla. Higher = longer.");
			BluntKOMultiplier = cfg.Bind<float>("Combat", "BluntKOMultiplier", 1.6f, "#H: scales the per-hit damage of BLUNT weapons (truncheon, bat, etc.) when YOU swing them, so a knockout takes FEWER hits — blunt force stuns. Only affects the player's own attacks; NPC-carried blunt weapons are unchanged. 1 = vanilla. Higher = fewer hits to KO.");
			BladeKOMultiplier = cfg.Bind<float>("Combat", "BladeKOMultiplier", 0.6f, "#H: scales the per-hit damage of BLADED weapons (knife, katana, sword, etc.) when YOU swing them, so a NON-LETHAL knockout takes MORE hits — blades cut, they don't stun (a lethal cut still kills as before, this only changes the KO ramp). Only affects the player's own attacks. 1 = vanilla. Lower = more hits to KO.");
			Level1Rep = cfg.Bind<int>("Progression", "Level1Rep", 20, "Underworld rep needed to reach Syndicate Level 1 (first earned milestone; you start at Level 0).");
			Level2Rep = cfg.Bind<int>("Progression", "Level2Rep", 50, "Underworld rep needed to reach Syndicate Level 2. Also the contract-difficulty Tier-2 boundary.");
			Level3Rep = cfg.Bind<int>("Progression", "Level3Rep", 100, "Underworld rep needed to reach Syndicate Level 3 (top). Also the contract-difficulty Tier-3 boundary.");
			Level1Reward = cfg.Bind<int>("Progression", "Level1Reward", 500, "Credits paid out the first time you reach Level 1.");
			Level2Reward = cfg.Bind<int>("Progression", "Level2Reward", 1500, "Credits paid out the first time you reach Level 2. (Later also delivers the gloves sync disk — pending.)");
			Level3Reward = cfg.Bind<int>("Progression", "Level3Reward", 4000, "Credits paid out the first time you reach Level 3. (Later also unlocks the Cruncher app — pending.)");
			SocialCreditPerToken = cfg.Bind<int>("SocialCredit", "SocialCreditPerToken", 350, "Native social credit gained per standing purchase (1 hit token + money). Native level thresholds: L2=350, L3=750, L4=1250, L5=2000, L6/echelon=3200, L8/Normal-cap=6000. Default 350 = a hit meaningfully advances your standing.");
			SocialCreditPurchaseMoneyCost = cfg.Bind<int>("SocialCredit", "SocialCreditPurchaseMoneyCost", 500, "Credits charged alongside the hit token for each standing purchase (bribe/laundering flavor). Modest by design — the hit token is the real cost.");
			SuppliesEnabled = cfg.Bind<bool>("Supplies", "Enabled", true, "Enable the Syndicate Supply app on your home Cruncher — buy gear (weapons, tools, meds, the Second Skin disk) at a discount and have it dropped at your door in a courier case. When off, no icon, no deliveries.");
			SuppliesDiscountPercent = cfg.Bind<int>("Supplies", "DiscountPercent", 20, "Syndicate discount off an item's street value (its preset max value), as a percent. 20 = you pay 80% of the vanilla price. Clamped 0-90.");
			SuppliesDeliveryHours = cfg.Bind<int>("Supplies", "DeliveryHours", 1, "In-game hours from checkout to the courier's knock at your door. Clamped to at least 0 (0 = essentially immediate on the next tick). Shared by the gear and groceries apps.");
			GroceriesEnabled = cfg.Bind<bool>("Supplies", "GroceriesEnabled", true, "Enable the bonus Syndicate Groceries app on your home Cruncher — buy food/drink delivered in a courier case, same as gear. When off, no groceries icon.");
			GroceriesMarkupPercent = cfg.Bind<int>("Supplies", "GroceriesMarkupPercent", 10, "Delivery markup added to a grocery item's shop price, as a percent (simulates store delivery fees). Default 10 = you pay 110% of the item's value. Clamped 0-500.");
			GearPriceMultiplier = cfg.Bind<float>("Supplies", "GearPriceMultiplier", 3f, "GLOBAL price multiplier on the GEAR bucket — everything that isn't a firearm or ammo (blades, blunt, poison, tools, consumables, the custom sync disks). Applied to the base game item value at load, so EVERY vendor charges more, not just the Syndicate shop (which then still takes DiscountPercent off). See GunPriceMultiplier / AmmoPriceMultiplier for those buckets. 1.0 = vanilla. Groceries (incl. medical) are unaffected. Clamped 0.1-100.");
			AmmoPriceMultiplier = cfg.Bind<float>("Supplies", "AmmoPriceMultiplier", 3f, "GLOBAL price multiplier on AMMUNITION (pistol/rifle/shotgun rounds). Same global mechanism as GearPriceMultiplier, scoped to ammo. 1.0 = vanilla. Clamped 0.1-100.");
			GunPriceMultiplier = cfg.Bind<float>("Supplies", "GunPriceMultiplier", 7f, "GLOBAL price multiplier on FIREARMS (sidearms + long guns). Same global mechanism as GearPriceMultiplier, scoped to guns — guns are the premium tier. 1.0 = vanilla. Clamped 0.1-100.");
			FirearmsEnabled = cfg.Bind<bool>("Firearms", "Enabled", true, "Enable player weapon-firing: aim a held gun and shoot (left-click, or the fallback FireKey). Off = guns behave like vanilla (a melee club at most).");
			FirearmsHeadshotLethal = cfg.Bind<bool>("Firearms", "HeadshotLethal", true, "A headshot with ANY gun kills outright (the precise-shot / silenced-pistol payoff). Off = headshots do normal damage like body shots.");
			FirearmsHeadshotRadius = cfg.Bind<float>("Firearms", "HeadshotRadius", 0.35f, "How close (metres) a bullet's impact must land to the head anchor to count as a headshot. Raise if headshots aren't registering, lower if body shots are wrongly counting as headshots.");
			FirearmsHeadshotDebug = cfg.Bind<bool>("Firearms", "HeadshotDebug", false, "Log a diagnostic line for each PELLET that hits a citizen (collider name, distance to the head anchor, headshot result) so headshot detection can be calibrated. Fires per-pellet per-shot, so leave OFF in normal play (heavy console logging can aggravate the Windows-console freeze) — turn on only when tuning headshots.");
			FirearmsOneShotWeapons = cfg.Bind<string>("Firearms", "OneShotWeapons", "Shotgun,SniperRifle", "Comma-separated PascalCase gun preset names whose BODY shot kills outright. Others rely on the two-hit rule (a shot KOs; a second shot on the KO'd target executes) — same as melee. Blank = no gun one-shots on a body hit.");
			FirearmsAccuracy = cfg.Bind<float>("Firearms", "Accuracy", 1f, "0-1 aim precision fed to the game's Shoot(). 1.0 = the bullet goes exactly where the crosshair points (and makes lethality detection exact). Below 1.0 adds spread to the DAMAGE ray only; lethality still follows the crosshair.");
			FirearmsMinCooldownSeconds = cfg.Bind<float>("Firearms", "MinFireCooldownSeconds", 0.4f, "Floor (real seconds) under the gun's native per-weapon fire cadence, so nothing fires faster than this. Raise to slow all guns down.");
			FirearmsConsumeAmmo = cfg.Bind<bool>("Firearms", "ConsumeAmmo", true, "Require and consume one matching ammo item per shot (the gun's own ammunition list decides the match). Off = unlimited free firing (debug/sandbox).");
			FirearmsSpawnCasings = cfg.Bind<bool>("Firearms", "SpawnCasings", true, "Leave a spent shell casing at the scene per shot (revolvers/no-eject guns leave none). If you're NOT wearing gloves, the casing carries YOUR fingerprint — feeding the forensics/ID system. Off = no casing, no print trail.");
			FirearmsFireKey = cfg.Bind<KeyCode>("Firearms", "FireKey", (KeyCode)0, "Optional ALTERNATE fire key. Left-click already fires a held gun (no melee swing); set a key here only if you also want a second bind. None = left-click only.");
			FirearmsMeleeKey = cfg.Bind<KeyCode>("Firearms", "MeleeKey", (KeyCode)0, "Optional key to pistol-whip (melee swing) with a held firearm — left-click now fires instead of swinging, so the melee is moved here. None = no gun-melee.");
			FirearmsRecoil = cfg.Bind<float>("Firearms", "Recoil", 15f, "Camera recoil kick applied per shot (a JoltCamera amplitude). 0 = no recoil. The game uses ~6 for a mild jolt and 45-60 for a violent one, so 15 is a firm-but-controllable kick. Raise for heavier recoil.");
			FirearmsPoseLab = cfg.Bind<bool>("Firearms", "PoseLab", false, "DEBUG ONLY: enable the first-person Pose Lab hotkeys for surveying/adjusting how a held item is posed — works on any firearm AND the syringe (see the log for the keymap when one is equipped). Leave OFF in normal play.");
			FirearmsReloadKey = cfg.Bind<KeyCode>("Firearms", "ReloadKey", (KeyCode)114, "Key to reload the held gun (tops the magazine up from ammo boxes in your inventory; takes ReloadSeconds). None = no reload key: a gun becomes UNUSABLE once its starting magazine and any reserve are spent, so keep a key bound (or set ConsumeAmmo=false).");
			FirearmsReloadSeconds = cfg.Bind<float>("Firearms", "ReloadSeconds", 2.5f, "Seconds a reload takes. Switching or holstering the gun mid-reload cancels it with no ammo consumed. Clamped to a 0.2s floor.");
			FirearmsMagazineSizes = cfg.Bind<string>("Firearms", "MagazineSizes", "SemiAutomaticPistol:12,SemiAutomaticPistolSilenced:12,Revolver:6,Shotgun:6,BattleRifle:30,SniperRifle:5", "Per-gun magazine capacity (PascalCase preset name:rounds, comma-separated). Rounds loaded per gun are tracked in the save; an unlisted gun defaults to 8.");
			FirearmsAmmoBoxSizes = cfg.Bind<string>("Firearms", "AmmoBoxSizes", "21AmmoSmall:12,21AmmoLarge:50,309Ammo:20,BuckshotAmmo:6,HuntingAmmo:6", "Rounds contained in each ammo BOX item (preset name:rounds). A reload consumes whole boxes and banks any excess as loose rounds in that gun-group's reserve. Unlisted box defaults to 12.");
			FirearmsAmmoHud = cfg.Bind<bool>("Firearms", "AmmoHud", true, "Show a small corner readout of magazine/reserve while a gun is drawn (e.g. '7 / 12 · 38 rounds'). Off = no on-screen ammo counter (reload/empty toasts still fire).");
			FirearmsAutoFireWeapons = cfg.Bind<string>("Firearms", "AutoFireWeapons", "BattleRifle", "Comma-separated PascalCase gun preset names that fire full-auto while you HOLD left-click. All others are semi (one shot per click). Default = the Faucon (BattleRifle) only.");
			FirearmsAutoFireInterval = cfg.Bind<float>("Firearms", "AutoFireIntervalSeconds", 0.12f, "Seconds between shots for an auto weapon while the trigger is held (~0.12 = 500 rpm). Overrides the per-weapon cadence for auto guns only. Clamped to a 0.05s floor.");
			FirearmsAutoRecoilScale = cfg.Bind<float>("Firearms", "AutoRecoilScale", 0.4f, "Multiplier on the per-shot recoil kick for auto weapons (full Recoil per shot at 8 shots/sec would be unplayable). 1.0 = same kick as a semi shot.");
			FirearmsShotgunPellets = cfg.Bind<int>("Firearms", "ShotgunPellets", 6, "Pellets fired per shotgun shell (uses the weapon's own 'shots' value if higher). Each pellet is an independent damage ray; the first flies straight to the crosshair, the rest spread.");
			FirearmsShotgunSpread = cfg.Bind<float>("Firearms", "ShotgunSpread", 0.06f, "Shotgun pellet spread in the game's aim-jitter units (~0.06 ≈ a 4-5 degree cone). 0 = no spread (all pellets on the crosshair). Higher = wider pattern, thinner at range.");
			FirearmsShotgunLethalPellets = cfg.Bind<int>("Firearms", "ShotgunLethalPellets", 3, "How many pellets must strike one citizen for a lethal (one-shot) shotgun hit. Point-blank the whole pattern lands (>= this) and kills; at range the pattern thins and only wounds.");
			FirearmsShotgunAmmoKey = cfg.Bind<KeyCode>("Firearms", "ShotgunAmmoSwitchKey", (KeyCode)98, "Key to switch the shotgun's loaded shell between BUCKSHOT (spreads) and DEER SLUG (a single ball, no spread — a heavy one-shot). The switch is immediate and shows on the ammo HUD; any shells of the other type still chambered are ejected, so reload to fill the new type. None = the shotgun just uses whichever shell you have (buckshot preferred).");
			FirearmsDrawnDread = cfg.Bind<bool>("Firearms", "DrawnGunDread", true, "Citizens NEAR you while you hold a drawn gun lose nerve (fear) — an ambient proximity effect (not tied to precise aim). Off = a drawn gun is not itself intimidating. Enforcers on duty and armed NPCs resist natively.");
			FirearmsDrawnNerve = cfg.Bind<float>("Firearms", "DrawnGunNerve", -0.05f, "Nerve drained per ~0.5s from each nearby citizen while your gun is drawn (negative = fear; citizen nerve maxes ~0.5, so -0.05 rattles them within a couple of seconds). Nerve regenerates when you leave, so it's a soft pressure. More negative = faster panic; 0 = off.");
			FirearmsDrawnRange = cfg.Bind<float>("Firearms", "DrawnGunRange", 8f, "Range (metres) within your current room where citizens feel the drawn-gun dread.");
			FirearmsGunshotsAlarm = cfg.Bind<bool>("Firearms", "GunshotsAlarm", true, "Gunshots alarm citizens who HEAR them — nerve drop + they investigate/flee and remember a gunshot. Off = firing makes noise but no one reacts to hearing it. (The silenced pistol is exempt by default.)");
			FirearmsAlarmExemptGuns = cfg.Bind<string>("Firearms", "AlarmExemptGuns", "SemiAutomaticPistolSilenced", "Comma-separated PascalCase gun preset names whose gunshots do NOT alarm hearers (silenced weapons). Their fire sound is left vanilla. Blank = every gun alarms.");
			FirearmsGunshotSpook = cfg.Bind<float>("Firearms", "GunshotSpook", 0.2f, "Minimum nerve drop applied to a citizen who hears a gunshot (raises the gun sound's spook value if lower). Higher = louder panic from hearing shots.");
			FirearmsRoomTurnOnHit = cfg.Bind<bool>("Firearms", "RoomTurnOnHit", true, "A non-lethal gunshot HIT rouses the victim AND everyone in their room to pursue you (the native Shoot doesn't alert the surroundings, so this is on us). Off = only the wounded victim reacts.");
			FirearmsRoomTurnRange = cfg.Bind<float>("Firearms", "RoomTurnRange", 10f, "Range (metres) within the victim's room whose occupants turn on you after a non-lethal hit. Only used when RoomTurnOnHit is on.");
			FirearmsInteractYieldDistance = cfg.Bind<float>("Firearms", "InteractYieldDistance", 2f, "How close (metres) an interactable under the crosshair must be for a trigger-pull to open/use it INSTEAD of firing. The game registers doors/items you're looking at from across a room; this gate means only a door/item within reach eats the shot, so you can fire at a distant door. Raise if the gun still refuses to fire near doors; lower toward the game's ~1.8m interact reach for stricter behaviour.");
			MaskToggleKey = cfg.Bind<KeyCode>("Mask", "ToggleKey", (KeyCode)110, "Key to pull the 'False Face' mask on/off (only works once the mask sync-disk is installed). While worn, witnesses to your kills can only describe a masked figure — a Suspicious scene, not a warrant in your name — but a covered face unnerves civilians near you. None = no toggle key (use the debug button).");
			MaskScareEnabled = cfg.Bind<bool>("Mask", "ScareCivilians", true, "While the mask is worn, nearby civilians lose nerve (fear) — a proximity effect so masking has a cost, mirroring the drawn-gun dread. Off = the mask is socially invisible.");
			MaskScareNerve = cfg.Bind<float>("Mask", "ScareNerve", -0.02f, "Nerve drained per ~0.5s from each nearby civilian while the mask is worn (negative = fear; citizen nerve maxes ~0.5, so -0.02 rattles them over ~12s of proximity rather than a few seconds). Nerve regenerates when you leave. More negative = faster panic; 0 = off.");
			MaskScareRange = cfg.Bind<float>("Mask", "ScareRange", 8f, "Range (metres) within your current room where civilians feel the masked-figure dread.");
			MaskScareEnforcers = cfg.Bind<bool>("Mask", "ScareEnforcers", false, "Whether the worn-mask scare also unnerves ENFORCERS (like a masked suspect should). Default false — mirrors the drawn-gun dread, which exempts enforcers, and avoids provoking unpredictable enforcer AI. On = a masked figure spooks everyone.");
			MaskedDescriptionHeat = cfg.Bind<int>("Mask", "MaskedDescriptionHeat", 12, "Heat added (once per scene) when a masked witness reports a 'masked figure' — noticeably more than a bare body-discovery (+8) but far below a positive ID (+40). Tunes how much a described-but-unnamed sighting stings.");
			MaskUnmaskInViewIdentifies = cfg.Bind<bool>("Mask", "UnmaskInViewIdentifies", true, "F5: taking the mask OFF while a witness or camera of an ACTIVE case is watching converts their masked-figure account into a positive ID (a witness who saw the masked act can now name you; a camera IDs you outright). With no open case, unmasking is free. Disable to make the mask purely about the moment of the crime.");
			PoisonEnabled = cfg.Bind<bool>("Poison", "Enabled", true, "Enable the poison items: the LOADED SYRINGE (a silent instant kill on any citizen you aim at within reach — conscious or KO'd) and the POISON VIAL (force-feed a KO'd citizen to stage a delayed overdose). Off = both behave like vanilla held items.");
			SyringeDoses = cfg.Bind<int>("Poison", "SyringeDoses", 3, "How many kills one syringe holds before it's spent (shown like ammo). The item is reusable — a use spends one dose; at 0 it's inert. Clamped to at least 1.");
			VialDoses = cfg.Bind<int>("Poison", "VialDoses", 1, "Forced overdoses per vial. The vial is SINGLE-USE by design: using it consumes the vial and leaves a spent overdose bottle at the scene as the murder weapon, so this is effectively 1 (no on-screen counter is shown for the vial). Clamped to at least 1.");
			PoisonRange = cfg.Bind<float>("Poison", "UseRangeMeters", 2f, "Reach (metres) for aiming the syringe/vial at a citizen — roughly arm's length. Clamped to a 0.5m floor.");
			PoisonPromptOverride = cfg.Bind<bool>("Poison", "PromptOverride", true, "#E: when a syringe/vial is aimed at a valid target within reach, swap the on-screen interaction prompt to 'Inject' (syringe) / 'Force-feed' (vial) instead of the vanilla 'Take One'/'Use'. Off = leave the vanilla prompt text.");
			PoisonCooldownSeconds = cfg.Bind<float>("Poison", "UseCooldownSeconds", 1.2f, "Minimum real seconds between poison uses (stops a mashed click from spending several doses). Floored at 0.05s internally — a lower value would let the two same-frame trigger paths both spend a dose on one click.");
			SyringeKillDelayGameMinutes = cfg.Bind<float>("Poison", "SyringeKillDelayGameMinutes", 5f, "IN-GAME minutes a CONSCIOUS syringe target stays alive after the jab — a drunk wobble at the jab, then a vomit tell shortly before they collapse dead. Long enough to walk clear, so the needle is a viable PUBLIC execution. They are alive and mobile the whole time (can wander and be seen). Measured on the in-game clock, so it pauses with the game and scales with time-speed. 0 = instant kill. A KO'd target dies instantly regardless.");
			SyringeStaggerDrunk = cfg.Bind<float>("Poison", "SyringeStaggerDrunk", 0.6f, "How hard a conscious syringe target staggers — the 'drunk' amount applied at the jab (0-1). Higher = a stronger wobble but a higher chance they trip and fall on their own before the collapse. 0 = no stagger.");
			ForgeDelayMinutes = cfg.Bind<int>("Poison", "ForgeDelayMinutes", 10, "In-GAME minutes between force-feeding a KO'd target with the vial and their overdose death. The target stays down until then. Clamped to at least 1.");
			FingerprintRemoverCharges = cfg.Bind<int>("Fingerprints", "RemoverCharges", 50, "#F: charges in one FINGERPRINT REMOVER (cleaning spray). A charge is spent ONLY when a wipe actually lifts one of your prints — looking at a clean object costs nothing. Clamped to at least 1.");
			FingerprintRemoverPreset = cfg.Bind<string>("Fingerprints", "RemoverPreset", "CleaningSpray", "#F: the vanilla item preset the FINGERPRINT REMOVER reuses. Change this only if the Supplies probe shows the cleaning-spray preset resolves under a different PascalCase name.");
			FingerprintRemoverRange = cfg.Bind<float>("Fingerprints", "WipeRangeMeters", 2f, "#F: how close (metres) you must be to actually WIPE prints off an object with the Fingerprint Remover — roughly arm's length, matching the game's interact reach. The 'YOUR PRINTS ARE ON THIS' cue still shows from across the room; only the wipe itself needs you up close. Floored at 0.5m.");
			FingerprintNoPrintPresets = cfg.Bind<string>("Fingerprints", "NoPrintPresets", "DoorUnderneathPeek", "Comma-separated interactable PRESET names that never leave your fingerprint — for 'peek / observe' interactions that aren't a real touch (looking UNDER a door = 'DoorUnderneathPeek'). Case-insensitive. Add more names (comma-separated) if you find another observe-style interaction that shouldn't incriminate you.");
			ChloroformEnabled = cfg.Bind<bool>("Sedative", "Enabled", true, "#G: enable the SEDATIVE (Dorma-Dix) stealth takedown — hold it, get BEHIND an unaware target while UNSEEN, and USE to put them out cold WITHOUT a mark on them. A sedated target keeps the 'no signs of violence' forgery bonus and adds no investigation heat; a target you BEAT unconscious instead loses both. Off = the item is inert.");
			ChloroformPreset = cfg.Bind<string>("Sedative", "Preset", "Sleeping Pills", "#G: the vanilla item preset the SEDATIVE reuses (probe-confirmed 'Sleeping Pills' = Dorma-Dix). Editable without a rebuild if you prefer another item (e.g. ChemicalBottle2).");
			ChloroformRange = cfg.Bind<float>("Sedative", "UseRangeMeters", 2f, "#G: reach (metres) to apply the sedative to a citizen — arm's length. Floored at 0.5m.");
			ChloroformBehindDot = cfg.Bind<float>("Sedative", "BehindDot", -0.25f, "#G: how far BEHIND the target you must be. The dot of the target's facing with the direction to you must be at or below this: -1 = directly behind only, 0 = anywhere in the rear half, positive = even from the side. Lower = stricter.");
			ChloroformDoses = cfg.Bind<int>("Sedative", "Doses", 3, "#G: how many stealth takedowns one SEDATIVE pack holds before it's spent. A dose is used only on a successful KO. Clamped to at least 1.");
			SedatePromptOverride = cfg.Bind<bool>("Sedative", "PromptOverride", true, "#11: while holding the sedative, when a clean from-behind takedown is actually available (target in reach, behind, unseen, doses left) the interaction prompt reads 'Sedate' instead of the vanilla 'Use'/'Take One'. Off = keep the vanilla prompt.");
			EnableNoViolenceObjective = cfg.Bind<bool>("Contracts", "EnableNoViolenceObjective", true, "#G: allow forge jobs (overdose / fall) to carry an OPTIONAL 'no signs of violence' bonus — paid only if you put the target down cleanly (sedative), not by beating them.");
			NoViolenceChancePercent = cfg.Bind<int>("Contracts", "NoViolenceChancePercent", 35, "#G: percent chance a forge job (overdose / fall) carries the 'no signs of violence' bonus. 0 = never.");
			BonusNoViolence = cfg.Bind<float>("ContractEconomy", "BonusNoViolence", 0.3f, "#G: payout fraction added when a forge target is killed with NO signs of violence (a clean sedative KO).");
			ForgeViolenceHeat = cfg.Bind<int>("Cameras", "ForgeViolenceHeat", 10, "#G: investigation HEAT added when a FORGED death (overdose / fall) is staged over a BEATEN body — the trauma reads as suspicious to a coroner, so the case is likelier to be investigated rather than ruled an accident. Applies to any forge with violence, whether or not the job carried the no-violence bonus. 0 = no extra heat.");
			ProtectFiniteNPCs = cfg.Bind<bool>("Protection", "ProtectFiniteNPCs", true, "SAFETY: enforcers and landlords are a FINITE, non-replenishing pool — every one killed is gone for the rest of the save (fewer cops; a landlord-less district). On (recommended): they never appear as contract targets, and any lethal action on them only knocks them out. Off: they can be killed (and landlords contracted) again — permanent save damage is on you.");
			ProtectLastCompanyWorker = cfg.Bind<bool>("Protection", "ProtectLastCompanyWorker", true, "The Syndicate won't ORDER a hit that empties a business: a citizen who is the LAST living employee of their company is never offered as a contract target (nobody replaces the dead, so it'd leave the shop permanently unstaffed). They are NOT murder-immune — you can still kill them freelance if you choose; this only keeps them off the contract board. Off: last workers can be contracted too.");
			DisableConsoleQuickEdit = cfg.Bind<bool>("General", "DisableConsoleQuickEdit", true, "Fixes the intermittent 'game freezes until I click the BepInEx console and hit Enter' stutter by turning off the Windows console's QuickEdit mode at startup (a stray click/selection on the console otherwise SUSPENDS the whole game). ON by default. Side effect: you can't mouse-select text in the console — to grab logs for a bug report, use BepInEx/LogOutput.log (it has everything). Set FALSE only if you specifically need console mouse-select and can live with the freeze.");
			SightCheckIntervalSeconds = cfg.Bind<float>("General", "SightCheckIntervalSeconds", 1.5f, "Seconds between crime-scene perception/report checks. Lower = more responsive, marginally more CPU.");
			VerboseLogging = cfg.Bind<bool>("General", "VerboseLogging", false, "Emit the mod's detailed diagnostic logging (stakeout-guard/raid mechanics, probes). Off keeps the console to gameplay-narration lines only. Turn on when reporting a bug.");
			MessageGapSeconds = cfg.Bind<float>("General", "MessageGapSeconds", 4f, "Minimum seconds between consecutive on-screen Syndicate toast messages, so a burst (e.g. a home raid seizing your weapon AND catching your cruncher logged in) reads as a paced sequence instead of overlapping. Lower = snappier but riskier overlap.");
			DebugEnabled = cfg.Bind<bool>("Debug", "Enabled", false, "Developer/debug mode. Leave OFF for normal play. OFF (the default): the F6 debug HUD and all its buttons are disabled, the F8 print-inspector and the frame input-freeze diagnostic are off, and citizen NAMES and home ADDRESSES are kept out of the BepInEx log — each is written as its numeric id instead (e.g. 'victim #4471'), so the console can't be used as an investigation cheat-sheet while staying traceable for a bug report. ON: restores the F6 debug HUD and prints full un-redacted names/addresses to the log. Either way, on-screen text (toasts, v-mail, the newspaper, the case board) always shows real names — this only affects the debug tooling and what goes to the log file.");
			Plugin.Logger.LogInfo((object)"Syndicate config bound (BepInEx/config/Kropath.TheSyndicate.cfg).");
		}
	}
	internal static class Redact
	{
		internal static bool Reveal => SyndicateConfig.DebugEnabled?.Value ?? false;

		internal static string Name(string name, int id)
		{
			if (!Reveal)
			{
				return "#" + id;
			}
			return Show(name);
		}

		internal static string Name(string name, long id)
		{
			if (!Reveal)
			{
				return "#" + id;
			}
			return Show(name);
		}

		internal static string Loc(string locName, long key)
		{
			if (!Reveal)
			{
				return "loc#" + key;
			}
			return Show(locName);
		}

		internal static string Place(string locName)
		{
			if (!Reveal)
			{
				return "a location";
			}
			return Show(locName);
		}

		internal static string Person(string name)
		{
			if (!Reveal)
			{
				return "the target";
			}
			return Show(name);
		}

		internal static string Actor(Human h)
		{
			if (Reveal)
			{
				try
				{
					return ((Object)(object)h != (Object)null) ? Show(h.GetCitizenName()) : "?";
				}
				catch
				{
					return "?";
				}
			}
			try
			{
				return ((Object)(object)h != (Object)null) ? ("#" + h.humanID) : "#?";
			}
			catch
			{
				return "#?";
			}
		}

		private static string Show(string s)
		{
			if (!string.IsNullOrEmpty(s))
			{
				return s;
			}
			return "?";
		}
	}
}
namespace TheSyndicate.Tools
{
	internal static class FingerprintRemoverController
	{
		internal const string DisplayName = "Fingerprint Remover";

		private static float _nextUse;

		private static long _lastWipeKey;

		internal static string Preset => SyndicateConfig.FingerprintRemoverPreset?.Value ?? "CleaningSpray";

		internal static void RegisterName()
		{
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			//IL_003a: Expected O, but got Unknown
			try
			{
				Lib.DdsStrings.AddOrUpdate("evidence.names", (Preset ?? "CleaningSpray").ToLowerInvariant(), "Fingerprint Remover");
			}
			catch (Exception ex)
			{
				ManualLogSource logger = Plugin.Logger;
				bool flag = default(bool);
				BepInExWarningLogInterpolatedStringHandler val = new BepInExWarningLogInterpolatedStringHandler(36, 1, ref flag);
				if (flag)
				{
					((BepInExLogInterpolatedStringHandler)val).AppendLiteral("[Cleaner] name registration failed: ");
					((BepInExLogInterpolatedStringHandler)val).AppendFormatted<string>(ex.Message);
				}
				logger.LogWarning(val);
			}
		}

		internal static bool IsHolding(out Interactable held)
		{
			held = null;
			try
			{
				Interactable equippedInteractableCached = KillSwitchPatch.GetEquippedInteractableCached();
				if (equippedInteractableCached != null && (SoCustomComparison)(object)equippedInteractableCached.preset != (SoCustomComparison)null && ((Object)equippedInteractableCached.preset).name == Preset)
				{
					held = equippedInteractableCached;
					return true;
				}
			}
			catch
			{
			}
			return false;
		}

		internal static bool IsHolding()
		{
			Interactable held;
			return IsHolding(out held);
		}

		internal static bool WasJustWiped(long key)
		{
			if (key != 0L && key == _lastWipeKey)
			{
				return Time.time < _nextUse;
			}
			return false;
		}

		internal static void Use()
		{
			//IL_008a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0091: Expected O, but got Unknown
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_0046: Unknown result type (might be due to invalid IL or missing references)
			try
			{
				if (!IsHolding())
				{
					return;
				}
				InteractionController instance = InteractionController.Instance;
				if ((Object)(object)instance == (Object)null)
				{
					return;
				}
				float num = Mathf.Max(0.5f, SyndicateConfig.FingerprintRemoverRange?.Value ?? 2f);
				RaycastHit playerCurrentRaycastHit = instance.playerCurrentRaycastHit;
				if (!(((RaycastHit)(ref playerCurrentRaycastHit)).distance > num))
				{
					Interactable val = (((Object)(object)instance.currentLookingAtInteractable != (Object)null) ? instance.currentLookingAtInteractable.interactable : null);
					if (val != null)
					{
						TryWipe(val);
					}
				}
			}
			catch (Exception ex)
			{
				ManualLogSource logger = Plugin.Logger;
				bool flag = default(bool);
				BepInExWarningLogInterpolatedStringHandler val2 = new BepInExWarningLogInterpolatedStringHandler(28, 1, ref flag);
				if (flag)
				{
					((BepInExLogInterpolatedStringHandler)val2).AppendLiteral("[Cleaner] spray use failed: ");
					((BepInExLogInterpolatedStringHandler)val2).AppendFormatted<string>(ex.Message);
				}
				logger.LogWarning(val2);
			}
		}

		internal static bool TryWipe(Interactable target)
		{
			//IL_0103: Unknown result type (might be due to invalid IL or missing references)
			//IL_010a: Expected O, but got Unknown
			try
			{
				if (!IsHolding(out var held) || target == null)
				{
					return false;
				}
				long num = PrintKeys.For(target);
				if (num != 0L && num == _lastWipeKey && Time.time < _nextUse)
				{
					return true;
				}
				int playerId = (((Object)(object)Player.Instance != (Object)null) ? ((Human)Player.Instance).humanID : (-1));
				bool flag = false;
				try
				{
					flag = PlayerInteractionPrintPatch.DfHasPlayerPrint(target, playerId) || (num != 0L && PlayerEvidence.HasPrint(num));
				}
				catch
				{
				}
				if (!flag)
				{
					return false;
				}
				_nextUse = Time.time + 0.4f;
				_lastWipeKey = num;
				if (PrintRemoverCharges.Get(held) <= 0)
				{
					Lib.GameMessage.Broadcast("The can's empty.", (GameMessageType)0, (Icon)21, (Color?)null, 0f);
					return true;
				}
				PrintCleaner.WipePrints(target);
				PrintRemoverCharges.Spend(held);
				Lib.GameMessage.Broadcast("Prints wiped.", (GameMessageType)0, (Icon)21, (Color?)null, 0f);
				return true;
			}
			catch (Exception ex)
			{
				ManualLogSource logger = Plugin.Logger;
				bool flag2 = default(bool);
				BepInExWarningLogInterpolatedStringHandler val = new BepInExWarningLogInterpolatedStringHandler(29, 1, ref flag2);
				if (flag2)
				{
					((BepInExLogInterpolatedStringHandler)val).AppendLiteral("[Cleaner] spray wipe failed: ");
					((BepInExLogInterpolatedStringHandler)val).AppendFormatted<string>(ex.Message);
				}
				logger.LogWarning(val);
				return true;
			}
		}
	}
	internal static class InputFreezeDiag
	{
		private static bool _stuck;

		private static float _stuckSince;

		private static float _lastLog;

		internal static void Tick()
		{
			//IL_01b0: Unknown result type (might be due to invalid IL or missing references)
			//IL_01b7: Expected O, but got Unknown
			try
			{
				SessionData instance = SessionData.Instance;
				if ((Object)(object)instance == (Object)null || !instance.play)
				{
					Recover("left play");
					return;
				}
				Player instance2 = Player.Instance;
				if ((Object)(object)instance2 == (Object)null || ((Actor)instance2).isDead)
				{
					Recover("no player");
					return;
				}
				if (instance2.computerInteractable != null)
				{
					Recover("at computer");
					return;
				}
				PopupMessageController instance3 = PopupMessageController.Instance;
				if ((Object)(object)instance3 != (Object)null && (instance3.active || instance3.appearProgress > 0f))
				{
					Recover("popup open");
					return;
				}
				InputController instance4 = InputController.Instance;
				InteractionController instance5 = InteractionController.Instance;
				bool flag = (Object)(object)instance4 == (Object)null || instance4.enableInput;
				float num = (((Object)(object)instance5 != (Object)null) ? instance5.inputCooldown : 0f);
				bool flag2 = (Object)(object)instance4 != (Object)null && instance4.mouseInputMode;
				if (flag && !(num > 0f))
				{
					Recover("gate open");
					return;
				}
				if (!_stuck)
				{
					_stuck = true;
					_stuckSince = Time.unscaledTime;
					_lastLog = -999f;
				}
				float num2 = Time.unscaledTime - _stuckSince;
				if (num2 >= 0.5f && Time.unscaledTime - _lastLog >= 1f)
				{
					_lastLog = Time.unscaledTime;
					string text = ((!flag) ? "InputController.enableInput=FALSE" : $"InteractionController.inputCooldown={num:F2} (native reset is ~0.1s — stuck)");
					ManualLogSource logger = Plugin.Logger;
					bool flag3 = default(bool);
					BepInExWarningLogInterpolatedStringHandler val = new BepInExWarningLogInterpolatedStringHandler(204, 5, ref flag3);
					if (flag3)
					{
						((BepInExLogInterpolatedStringHandler)val).AppendLiteral("[InputDiag] BUG2: non-movement input GATED ");
						((BepInExLogInterpolatedStringHandler)val).AppendFormatted<float>(num2, "F1");
						((BepInExLogInterpolatedStringHandler)val).AppendLiteral("s in live gameplay — CAUSE: ");
						((BepInExLogInterpolatedStringHandler)val).AppendFormatted<string>(text);
						((BepInExLogInterpolatedStringHandler)val).AppendLiteral(". ");
						((BepInExLogInterpolatedStringHandler)val).AppendLiteral("(enableInput=");
						((BepInExLogInterpolatedStringHandler)val).AppendFormatted<bool>(flag);