Decompiled source of BattleScars v1.2.4

BattleScars.dll

Decompiled 2 weeks 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.Versioning;
using System.Security;
using System.Security.Permissions;
using BattleScars.Configuration;
using BattleScars.Services;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: IgnoresAccessChecksTo("Assembly-CSharp")]
[assembly: AssemblyCompany("Vippy")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.2.4.0")]
[assembly: AssemblyInformationalVersion("1.2.4+46c90de84a55dfbb5eeebd73b246dc73f84563a4")]
[assembly: AssemblyProduct("BattleScars")]
[assembly: AssemblyTitle("BattleScars")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.2.4.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.Embedded]
	internal sealed class EmbeddedAttribute : Attribute
	{
	}
}
namespace System.Runtime.CompilerServices
{
	[CompilerGenerated]
	[Microsoft.CodeAnalysis.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]
	[Microsoft.CodeAnalysis.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]
	[Microsoft.CodeAnalysis.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 BattleScars
{
	[BepInPlugin("Vippy.BattleScars", "BattleScars", "1.2.4")]
	public class BattleScars : BaseUnityPlugin
	{
		private Harmony? _harmony;

		internal static BattleScars Instance { get; private set; }

		internal static ManualLogSource Log => ((BaseUnityPlugin)Instance).Logger;

		private void Awake()
		{
			//IL_0045: Unknown result type (might be due to invalid IL or missing references)
			//IL_004f: Expected O, but got Unknown
			Instance = this;
			((Component)this).gameObject.transform.parent = null;
			((Object)((Component)this).gameObject).hideFlags = (HideFlags)61;
			PluginConfig.Init(((BaseUnityPlugin)this).Config);
			BindModeRevert();
			_harmony = new Harmony(((BaseUnityPlugin)this).Info.Metadata.GUID);
			_harmony.PatchAll();
			Log.LogInfo((object)$"{((BaseUnityPlugin)this).Info.Metadata.GUID} v{((BaseUnityPlugin)this).Info.Metadata.Version} loaded.");
		}

		private static void BindModeRevert()
		{
			PluginConfig.Mode.SettingChanged += delegate
			{
				if (PluginConfig.Mode.Value == RunMode.Off)
				{
					PlayerAvatar val = PlayerLookup.LocalAvatar();
					if (!((Object)(object)val == (Object)null))
					{
						Cosmetics.RestoreToLocal(val);
						Cosmetics.RefreshExpressionPreview();
						Driver.Instance?.InvalidateAppliedCosmetics();
					}
				}
			};
		}
	}
	internal static class BuildInfo
	{
		public const string Version = "1.2.4";
	}
}
namespace BattleScars.Services
{
	public static class ConfigService
	{
		public static bool IsEnabled()
		{
			return PluginConfig.Mode.Value != RunMode.Off;
		}

		public static bool IsVisualOnly()
		{
			return PluginConfig.Mode.Value == RunMode.VisualOnly;
		}

		public static bool VignetteEnabled()
		{
			if (IsEnabled() && PluginConfig.Vignette.Value)
			{
				return PluginConfig.VignetteIntensity.Value > 0f;
			}
			return false;
		}

		public static bool CameraGlitchesEnabled()
		{
			if (IsEnabled())
			{
				return PluginConfig.CameraGlitches.Value;
			}
			return false;
		}

		public static bool NerfsEnabled()
		{
			if (IsEnabled())
			{
				return !IsVisualOnly();
			}
			return false;
		}

		public static bool PhotosensitivityOn()
		{
			if ((Object)(object)GameplayManager.instance != (Object)null)
			{
				return GameplayManager.instance.photosensitivity;
			}
			return false;
		}

		public static bool InActiveScene()
		{
			RunManager instance = RunManager.instance;
			if ((Object)(object)instance == (Object)null || (Object)(object)instance.levelCurrent == (Object)null)
			{
				return false;
			}
			if (!SemiFunc.RunIsLevel() && !SemiFunc.RunIsShop())
			{
				return SemiFunc.RunIsLobby();
			}
			return true;
		}

		public static void LogDiag(string msg)
		{
			if (PluginConfig.DebugLogging.Value)
			{
				BattleScars.Log.LogInfo((object)("[Scars] " + msg));
			}
		}

		public static int DamageDepth(int currentHP)
		{
			return Mathf.Max(0, PluginConfig.Curve.FirstScarHP - currentHP);
		}

		public static int ScarSlotCount(int currentHP)
		{
			ScarCurve curve = PluginConfig.Curve;
			if (currentHP > curve.FirstScarHP)
			{
				return 0;
			}
			return DamageDepth(currentHP) / curve.SlotStepHP + 1;
		}

		public static ScarSeverity SeverityForSlot(int currentHP, int slotIndex)
		{
			if (currentHP <= 0)
			{
				return ScarSeverity.Broken;
			}
			ScarCurve curve = PluginConfig.Curve;
			return (ScarSeverity)Mathf.Clamp((DamageDepth(currentHP) - slotIndex * curve.SlotStaggerHP) / curve.SeverityStepHP, 0, 3);
		}

		public static bool BrokenHeadActive(int currentHP)
		{
			return currentHP <= PluginConfig.Curve.BrokenHeadHP;
		}

		public static Tier TierForHealth(int currentHP)
		{
			int num = ScarSlotCount(currentHP);
			if (num <= 0)
			{
				return Tier.Healthy;
			}
			if (num <= 2)
			{
				return Tier.Scratched;
			}
			if (num <= 4)
			{
				return Tier.Damaged;
			}
			if (num == 5)
			{
				return Tier.Battered;
			}
			return Tier.Wrecked;
		}

		public static float SpeedMultiplierFor(Tier tier)
		{
			return Mathf.Lerp(1f, 0.7f, (float)tier / 4f);
		}

		public static float StaminaMultiplierFor(Tier tier)
		{
			return Mathf.Lerp(1f, 0.45f, (float)tier / 4f);
		}
	}
	public enum ScarSeverity
	{
		Cracks,
		Bandages,
		Damaged,
		Broken
	}
	public static class Cosmetics
	{
		private sealed class Region
		{
			public string Key = "";

			public int Bandage = -1;

			public int CrackOverlay = -1;

			public int DamagedOverlay = -1;

			public int BrokenMesh = -1;

			public readonly List<int> RareBrokenMeshes = new List<int>();

			public bool IsHead;

			public bool IsHeadTop => Key == "HeadTop";
		}

		private const int MaxBandagedLimbs = 3;

		private static readonly string[] RareBrokenTokens = new string[1] { "cords" };

		private const double RareBrokenMeshChance = 0.25;

		private static List<Region>? _regions;

		private static bool _discoveryRan;

		private static int _roundSeed;

		public static void RerollRoundSeed()
		{
			_roundSeed = Guid.NewGuid().GetHashCode();
		}

		public static void DiscoverIfNeeded()
		{
			if (_discoveryRan || (Object)(object)MetaManager.instance == (Object)null || MetaManager.instance.cosmeticAssets == null)
			{
				return;
			}
			List<CosmeticAsset> cosmeticAssets = MetaManager.instance.cosmeticAssets;
			Dictionary<string, Region> dictionary = new Dictionary<string, Region>();
			foreach (int item in BuildPool(cosmeticAssets, "Bandages"))
			{
				AssignLayer(cosmeticAssets, dictionary, item, ScarSeverity.Bandages);
			}
			foreach (int item2 in BuildPool(cosmeticAssets, "Cracks"))
			{
				AssignLayer(cosmeticAssets, dictionary, item2, ScarSeverity.Cracks);
			}
			foreach (int item3 in BuildPool(cosmeticAssets, "Damaged"))
			{
				AssignLayer(cosmeticAssets, dictionary, item3, ScarSeverity.Damaged);
			}
			foreach (int item4 in BuildPool(cosmeticAssets, "Broken"))
			{
				AssignLayer(cosmeticAssets, dictionary, item4, ScarSeverity.Broken);
			}
			_regions = dictionary.Values.ToList();
			_regions.Sort((Region a, Region b) => string.CompareOrdinal(a.Key, b.Key));
			_discoveryRan = true;
			BattleScars.Log.LogInfo((object)$"[Cosmetics] scar regions={_regions.Count}");
			foreach (Region region in _regions)
			{
				ConfigService.LogDiag($"[Cosmetics]   {region.Key}: bandage={region.Bandage >= 0} " + $"crack={region.CrackOverlay >= 0} damaged={region.DamagedOverlay >= 0} " + $"broken={region.BrokenMesh >= 0} rareBroken={region.RareBrokenMeshes.Count}");
			}
			if (_regions.Count == 0)
			{
				BattleScars.Log.LogWarning((object)"[Cosmetics] no scar cosmetics matched, cosmetic effects disabled");
			}
		}

		private static void AssignLayer(IList<CosmeticAsset> assets, Dictionary<string, Region> byKey, int idx, ScarSeverity layer)
		{
			//IL_0021: Unknown result type (might be due to invalid IL or missing references)
			//IL_00af: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d5: Unknown result type (might be due to invalid IL or missing references)
			if (idx < 0 || idx >= assets.Count)
			{
				return;
			}
			CosmeticAsset val = assets[idx];
			if ((Object)(object)val == (Object)null)
			{
				return;
			}
			string text = RegionKey(val.type);
			if (text == null)
			{
				return;
			}
			if (!byKey.TryGetValue(text, out Region value))
			{
				value = (byKey[text] = new Region
				{
					Key = text
				});
			}
			switch (layer)
			{
			case ScarSeverity.Bandages:
				if (value.Bandage < 0)
				{
					value.Bandage = idx;
				}
				break;
			case ScarSeverity.Cracks:
				if (value.CrackOverlay < 0)
				{
					value.CrackOverlay = idx;
				}
				break;
			case ScarSeverity.Damaged:
				if (value.DamagedOverlay < 0)
				{
					value.DamagedOverlay = idx;
				}
				break;
			case ScarSeverity.Broken:
				if (IsRareBroken(val))
				{
					value.RareBrokenMeshes.Add(idx);
					if (IsHeadMesh(val.type))
					{
						value.IsHead = true;
					}
				}
				else if (value.BrokenMesh < 0)
				{
					value.BrokenMesh = idx;
					value.IsHead = IsHeadMesh(val.type);
				}
				break;
			}
		}

		private static string? RegionKey(CosmeticType type)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0082: Expected I4, but got Unknown
			switch ((int)type)
			{
			case 2:
			case 10:
			case 27:
				return "ArmLeft";
			case 1:
			case 9:
			case 26:
				return "ArmRight";
			case 4:
			case 12:
			case 29:
				return "LegLeft";
			case 3:
			case 11:
			case 28:
				return "LegRight";
			case 7:
			case 16:
			case 20:
				return "BodyTop";
			case 8:
			case 21:
			case 23:
				return "BodyBottom";
			case 0:
			case 5:
			case 24:
				return "HeadTop";
			case 6:
			case 25:
			case 30:
				return "HeadBottom";
			default:
				return null;
			}
		}

		private static bool IsHeadMesh(CosmeticType type)
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0002: Invalid comparison between Unknown and I4
			//IL_0004: Unknown result type (might be due to invalid IL or missing references)
			//IL_0006: Invalid comparison between Unknown and I4
			//IL_0008: Unknown result type (might be due to invalid IL or missing references)
			//IL_000b: Invalid comparison between Unknown and I4
			//IL_000d: Unknown result type (might be due to invalid IL or missing references)
			//IL_0010: Invalid comparison between Unknown and I4
			if ((int)type != 5 && (int)type != 6 && (int)type != 14)
			{
				return (int)type == 15;
			}
			return true;
		}

		private static bool IsRareBroken(CosmeticAsset asset)
		{
			string text = (((Object)asset).name ?? string.Empty).ToLowerInvariant();
			string text2 = (asset.assetName ?? string.Empty).ToLowerInvariant();
			string[] rareBrokenTokens = RareBrokenTokens;
			foreach (string value in rareBrokenTokens)
			{
				if (text.Contains(value) || text2.Contains(value))
				{
					return true;
				}
			}
			return false;
		}

		private static int PickBrokenMesh(Region region, string steamID)
		{
			if (region.RareBrokenMeshes.Count == 0)
			{
				return region.BrokenMesh;
			}
			Random random = new Random((string.IsNullOrEmpty(steamID) ? 1 : steamID.GetHashCode()) * 1009 + _roundSeed * 31 + region.Key.GetHashCode());
			foreach (int rareBrokenMesh in region.RareBrokenMeshes)
			{
				if (random.NextDouble() < 0.25)
				{
					return rareBrokenMesh;
				}
			}
			return region.BrokenMesh;
		}

		private static List<int> BuildPool(IList<CosmeticAsset> assets, string allowList)
		{
			List<string> list = ParseList(allowList);
			List<int> list2 = new List<int>();
			if (list.Count == 0)
			{
				return list2;
			}
			for (int i = 0; i < assets.Count; i++)
			{
				CosmeticAsset val = assets[i];
				if (!((Object)(object)val == (Object)null))
				{
					string a = (((Object)val).name ?? string.Empty).ToLowerInvariant();
					string b = (val.assetName ?? string.Empty).ToLowerInvariant();
					if (list.Any((string t) => a.Contains(t) || b.Contains(t)))
					{
						list2.Add(i);
					}
				}
			}
			return list2;
		}

		private static List<string> ParseList(string raw)
		{
			List<string> list = new List<string>();
			if (string.IsNullOrWhiteSpace(raw))
			{
				return list;
			}
			string[] array = raw.Split(',');
			for (int i = 0; i < array.Length; i++)
			{
				string text = array[i].Trim().ToLowerInvariant();
				if (text.Length > 0)
				{
					list.Add(text);
				}
			}
			return list;
		}

		public static List<int> ForcedSetForHealth(string steamID, int currentHP, bool playerHasHat)
		{
			DiscoverIfNeeded();
			List<int> list = new List<int>();
			if (_regions == null || _regions.Count == 0)
			{
				return list;
			}
			int num = ConfigService.ScarSlotCount(currentHP);
			if (num <= 0)
			{
				return list;
			}
			bool flag = ConfigService.BrokenHeadActive(currentHP);
			List<Region> list2 = OrderedRegions(steamID);
			int num2 = Math.Min(num, list2.Count);
			int num3 = 0;
			for (int i = 0; i < num2; i++)
			{
				Region region = list2[i];
				ScarSeverity num4 = ConfigService.SeverityForSlot(currentHP, i);
				int num5 = ((num4 >= ScarSeverity.Damaged && region.DamagedOverlay >= 0) ? region.DamagedOverlay : region.CrackOverlay);
				if (num5 >= 0)
				{
					list.Add(num5);
				}
				if (num4 >= ScarSeverity.Bandages && region.Bandage >= 0 && num3 < 3 && !(region.IsHeadTop && playerHasHat))
				{
					list.Add(region.Bandage);
					num3++;
				}
				int num6 = PickBrokenMesh(region, steamID);
				if (num4 >= ScarSeverity.Broken && num6 >= 0 && (!region.IsHead || flag))
				{
					list.Add(num6);
				}
			}
			return list;
		}

		private static List<Region> OrderedRegions(string steamID)
		{
			int num = (string.IsNullOrEmpty(steamID) ? 1 : steamID.GetHashCode());
			Random rng = new Random(num * 31 + _roundSeed);
			return _regions.OrderBy((Region _) => rng.Next()).ToList();
		}

		public static bool PlayerWearsHat(PlayerAvatar avatar)
		{
			//IL_0073: Unknown result type (might be due to invalid IL or missing references)
			List<CosmeticAsset> list = MetaManager.instance?.cosmeticAssets;
			List<int> list2 = (((Object)(object)avatar != (Object)null && (Object)(object)avatar.playerCosmetics != (Object)null) ? avatar.playerCosmetics.cosmeticEquippedRaw : null);
			if (list == null || list2 == null)
			{
				return false;
			}
			foreach (int item in list2)
			{
				if (item >= 0 && item < list.Count)
				{
					CosmeticAsset val = list[item];
					if ((Object)(object)val != (Object)null && (int)val.type == 0)
					{
						return true;
					}
				}
			}
			return false;
		}

		private static Dictionary<CosmeticType, string> ByType(IList<CosmeticAsset> assets, IEnumerable<int> indices)
		{
			//IL_0053: Unknown result type (might be due to invalid IL or missing references)
			Dictionary<CosmeticType, string> dictionary = new Dictionary<CosmeticType, string>();
			foreach (int index in indices)
			{
				if (index >= 0 && index < assets.Count)
				{
					CosmeticAsset val = assets[index];
					if (!((Object)(object)val == (Object)null))
					{
						string arg = ((!string.IsNullOrEmpty(val.assetName)) ? val.assetName : ((Object)val).name);
						dictionary[val.type] = $"{arg}#{index}";
					}
				}
			}
			return dictionary;
		}

		public static string Describe(IEnumerable<int> indices)
		{
			List<CosmeticAsset> list = MetaManager.instance?.cosmeticAssets;
			if (list == null)
			{
				return "?";
			}
			Dictionary<CosmeticType, string> dictionary = ByType(list, indices);
			if (dictionary.Count == 0)
			{
				return "(none)";
			}
			List<string> list2 = dictionary.Select((KeyValuePair<CosmeticType, string> kv) => $"{kv.Key}={kv.Value}").ToList();
			list2.Sort(StringComparer.Ordinal);
			return string.Join(" ", list2);
		}

		public static string DescribeDiff(IEnumerable<int> before, IEnumerable<int> after)
		{
			//IL_0050: Unknown result type (might be due to invalid IL or missing references)
			//IL_0055: Unknown result type (might be due to invalid IL or missing references)
			//IL_0058: Unknown result type (might be due to invalid IL or missing references)
			//IL_0063: Unknown result type (might be due to invalid IL or missing references)
			//IL_0082: Unknown result type (might be due to invalid IL or missing references)
			//IL_00bc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a1: Unknown result type (might be due to invalid IL or missing references)
			List<CosmeticAsset> list = MetaManager.instance?.cosmeticAssets;
			if (list == null)
			{
				return "?";
			}
			Dictionary<CosmeticType, string> dictionary = ByType(list, before);
			Dictionary<CosmeticType, string> dictionary2 = ByType(list, after);
			List<string> list2 = new List<string>();
			foreach (CosmeticType item in dictionary2.Keys.Union(dictionary.Keys))
			{
				dictionary.TryGetValue(item, out var value);
				dictionary2.TryGetValue(item, out var value2);
				if (!(value == value2))
				{
					if (value == null)
					{
						list2.Add($"{item} +{value2}");
					}
					else if (value2 == null)
					{
						list2.Add($"{item} -{value}");
					}
					else
					{
						list2.Add($"{item} {value}->{value2}");
					}
				}
			}
			if (list2.Count == 0)
			{
				return "(no change)";
			}
			list2.Sort(StringComparer.Ordinal);
			return string.Join("  ", list2);
		}

		public static List<int> Merge(IList<CosmeticAsset>? assets, IList<int> ownList, IList<int> forced)
		{
			//IL_004f: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e9: Unknown result type (might be due to invalid IL or missing references)
			List<int> list = new List<int>(ownList.Count + forced.Count);
			HashSet<CosmeticType> hashSet = new HashSet<CosmeticType>();
			if (assets != null)
			{
				foreach (int item in forced)
				{
					if (item >= 0 && item < assets.Count)
					{
						CosmeticAsset val = assets[item];
						if ((Object)(object)val != (Object)null)
						{
							hashSet.Add(val.type);
						}
					}
				}
			}
			foreach (int item2 in forced)
			{
				if (!list.Contains(item2))
				{
					list.Add(item2);
				}
			}
			foreach (int own in ownList)
			{
				if (list.Contains(own))
				{
					continue;
				}
				if (assets != null && own >= 0 && own < assets.Count)
				{
					CosmeticAsset val2 = assets[own];
					if ((Object)(object)val2 != (Object)null && hashSet.Contains(val2.type))
					{
						continue;
					}
				}
				list.Add(own);
			}
			return list;
		}

		public static void Apply(PlayerAvatar avatar, IList<int> forced)
		{
			if (!((Object)(object)avatar == (Object)null) && !((Object)(object)avatar.playerCosmetics == (Object)null) && (avatar.photonView.IsMine || !SemiFunc.IsMultiplayer()))
			{
				List<CosmeticAsset> assets = MetaManager.instance?.cosmeticAssets;
				List<int> ownList = avatar.playerCosmetics.cosmeticEquippedRaw ?? new List<int>();
				List<int> list = Merge(assets, ownList, forced);
				ConfigService.LogDiag($"apply steam={avatar.steamID} forced={{ {Describe(forced)} }} (combined {list.Count} total)");
				avatar.playerCosmetics.SetupCosmetics(SemiFunc.IsMultiplayer(), true, list);
				avatar.playerCosmetics.SetupColors(SemiFunc.IsMultiplayer(), (int[])null);
				ApplyToDeathHead(avatar, list);
				SyncToMaster(avatar.playerCosmetics, list);
				PlayerDeathHead playerDeathHead = avatar.playerDeathHead;
				if ((Object)(object)playerDeathHead != (Object)null)
				{
					SyncToMaster(playerDeathHead.playerCosmetics, list);
				}
			}
		}

		public static void RestoreToLocal(PlayerAvatar avatar)
		{
			if (!((Object)(object)avatar == (Object)null) && !((Object)(object)avatar.playerCosmetics == (Object)null) && (avatar.photonView.IsMine || !SemiFunc.IsMultiplayer()))
			{
				ConfigService.LogDiag("restore steam=" + avatar.steamID + " (back to the saved loadout)");
				avatar.playerCosmetics.SetupCosmetics(SemiFunc.IsMultiplayer(), true, (List<int>)null);
				avatar.playerCosmetics.SetupColors(SemiFunc.IsMultiplayer(), (int[])null);
				ApplyToDeathHead(avatar, null);
				List<int> equipped = MetaManager.instance?.cosmeticEquipped;
				SyncToMaster(avatar.playerCosmetics, equipped);
				PlayerDeathHead playerDeathHead = avatar.playerDeathHead;
				if ((Object)(object)playerDeathHead != (Object)null)
				{
					SyncToMaster(playerDeathHead.playerCosmetics, equipped);
				}
			}
		}

		public static void RefreshExpressionPreview()
		{
			PlayerExpressionsUI instance = PlayerExpressionsUI.instance;
			if (!((Object)(object)instance == (Object)null))
			{
				PlayerAvatarVisuals playerAvatarVisuals = instance.playerAvatarVisuals;
				if (!((Object)(object)playerAvatarVisuals == (Object)null) && !((Object)(object)playerAvatarVisuals.playerCosmetics == (Object)null))
				{
					playerAvatarVisuals.playerCosmetics.SetupCosmetics(false, false, (List<int>)null);
					playerAvatarVisuals.playerCosmetics.SetupColors(false, (int[])null);
				}
			}
		}

		private static void ApplyToDeathHead(PlayerAvatar avatar, List<int>? combined)
		{
			PlayerDeathHead playerDeathHead = avatar.playerDeathHead;
			PlayerCosmetics val = (((Object)(object)playerDeathHead != (Object)null) ? playerDeathHead.playerCosmetics : null);
			if (!((Object)(object)playerDeathHead == (Object)null) && !((Object)(object)val == (Object)null))
			{
				val.SetupCosmetics(SemiFunc.IsMultiplayer(), true, combined);
				val.SetupColors(SemiFunc.IsMultiplayer(), (int[])null);
			}
		}

		private static void SyncToMaster(PlayerCosmetics? cosmetics, IList<int>? equipped)
		{
			if (!SemiFunc.IsMultiplayer() || (Object)(object)cosmetics == (Object)null || (Object)(object)cosmetics.photonView == (Object)null)
			{
				return;
			}
			Player masterClient = PhotonNetwork.MasterClient;
			if (masterClient != null && !masterClient.IsLocal)
			{
				int[] array = ((equipped != null) ? equipped.ToArray() : Array.Empty<int>());
				cosmetics.photonView.RPC("SetupCosmeticsRPC", masterClient, new object[2] { array, true });
				int[] colorsEquipped = cosmetics.colorsEquipped;
				if (colorsEquipped != null)
				{
					cosmetics.photonView.RPC("SetupColorsRPC", masterClient, new object[1] { colorsEquipped });
				}
			}
		}
	}
	public class Driver : MonoBehaviour
	{
		private const float SlowTickInterval = 0.2f;

		private float _slowTickTimer;

		private bool _wasActive;

		private bool _wasFullHealth = true;

		private readonly Dictionary<string, List<int>> _applied = new Dictionary<string, List<int>>();

		public static Driver? Instance { get; private set; }

		private void Awake()
		{
			Instance = this;
			Cosmetics.DiscoverIfNeeded();
			Cosmetics.RerollRoundSeed();
		}

		private void Update()
		{
			HandleDevHotkeys();
			if ((Object)(object)StatsManager.instance == (Object)null || !ConfigService.InActiveScene())
			{
				if (_wasActive)
				{
					ConfigService.LogDiag("left the active scene, tearing down");
					TeardownLocal();
					_wasActive = false;
				}
				return;
			}
			if (!_wasActive)
			{
				RunManager instance = RunManager.instance;
				object obj;
				if (instance == null)
				{
					obj = null;
				}
				else
				{
					Level levelCurrent = instance.levelCurrent;
					obj = ((levelCurrent != null) ? ((Object)levelCurrent).name : null);
				}
				ConfigService.LogDiag("entered active scene level=" + (string?)obj);
			}
			_wasActive = true;
			PlayerAvatar val = PlayerLookup.LocalAvatar();
			bool num = ConfigService.IsEnabled();
			bool flag = (Object)(object)val != (Object)null && val.deadSet;
			bool flag2 = (Object)(object)val != (Object)null && val.isDisabled;
			RerollSeedWhenFullHealth(val);
			Tier tier = Tier.Healthy;
			if (num && !flag && !flag2 && (Object)(object)val != (Object)null)
			{
				tier = ConfigService.TierForHealth(EffectiveHealthFor(val));
			}
			if ((Object)(object)val != (Object)null && !flag)
			{
				Effects.ApplySpeedTick(val, tier);
				Effects.ApplyStaminaTick(val, tier);
			}
			_slowTickTimer -= Time.deltaTime;
			if (!(_slowTickTimer > 0f))
			{
				_slowTickTimer = 0.2f;
				SlowTick(val, flag, flag2);
			}
		}

		private void HandleDevHotkeys()
		{
			int? num = null;
			if (Input.GetKeyDown((KeyCode)256))
			{
				num = -1;
			}
			else if (Input.GetKeyDown((KeyCode)257))
			{
				num = 1;
			}
			else if (Input.GetKeyDown((KeyCode)258))
			{
				num = 20;
			}
			else if (Input.GetKeyDown((KeyCode)259))
			{
				num = 30;
			}
			else if (Input.GetKeyDown((KeyCode)260))
			{
				num = 40;
			}
			else if (Input.GetKeyDown((KeyCode)261))
			{
				num = 50;
			}
			else if (Input.GetKeyDown((KeyCode)262))
			{
				num = 60;
			}
			else if (Input.GetKeyDown((KeyCode)263))
			{
				num = 70;
			}
			else if (Input.GetKeyDown((KeyCode)264))
			{
				num = 80;
			}
			else if (Input.GetKeyDown((KeyCode)265))
			{
				num = 90;
			}
			if (num.HasValue && PluginConfig.TestHealth.Value != num.Value)
			{
				PluginConfig.TestHealth.Value = num.Value;
				BattleScars.Log.LogInfo((object)("[Dev] TestHealth -> " + ((num.Value < 0) ? "off" : num.Value.ToString())));
				InvalidateAppliedCosmetics();
			}
		}

		private void RerollSeedWhenFullHealth(PlayerAvatar? local)
		{
			bool flag = (Object)(object)local != (Object)null && (Object)(object)local.playerHealth != (Object)null && EffectiveHealthFor(local) >= local.playerHealth.maxHealth;
			if (flag && !_wasFullHealth)
			{
				Cosmetics.RerollRoundSeed();
			}
			_wasFullHealth = flag;
		}

		public static int EffectiveHealthFor(PlayerAvatar avatar)
		{
			int value = PluginConfig.TestHealth.Value;
			if (value >= 0)
			{
				return value;
			}
			if (!((Object)(object)avatar.playerHealth != (Object)null))
			{
				return 0;
			}
			return avatar.playerHealth.health;
		}

		private void SlowTick(PlayerAvatar? local, bool dead, bool disabled)
		{
			SaveBackup.TryBackupOnce(local);
			if ((Object)(object)local == (Object)null || string.IsNullOrEmpty(local.steamID) || !ConfigService.IsEnabled())
			{
				return;
			}
			int num = ((!dead) ? EffectiveHealthFor(local) : 0);
			List<int> list = Cosmetics.ForcedSetForHealth(local.steamID, num, Cosmetics.PlayerWearsHat(local));
			if (_applied.TryGetValue(local.steamID, out List<int> value) && SameSet(value, list))
			{
				ConfigService.LogDiag($"tick hp={num} dead={dead} disabled={disabled} scars={list.Count} -> skip (no change)");
				return;
			}
			List<int> before = value ?? new List<int>();
			_applied[local.steamID] = list;
			ConfigService.LogDiag(string.Format("tick hp={0} dead={1} disabled={2} scars={3} -> {4}", num, dead, disabled, list.Count, (list.Count == 0) ? "restore" : "apply"));
			ConfigService.LogDiag("  diff: " + Cosmetics.DescribeDiff(before, list));
			ConfigService.LogDiag("  set:  " + Cosmetics.Describe(list));
			if (list.Count == 0)
			{
				Cosmetics.RestoreToLocal(local);
			}
			else
			{
				Cosmetics.Apply(local, list);
			}
			Cosmetics.RefreshExpressionPreview();
		}

		public void InvalidateAppliedCosmetics()
		{
			_applied.Clear();
		}

		private void TeardownLocal()
		{
			PlayerAvatar val = PlayerLookup.LocalAvatar();
			ConfigService.LogDiag("teardown localAvatar=" + (((Object)(object)val != (Object)null) ? "found" : "null"));
			if ((Object)(object)val != (Object)null)
			{
				Cosmetics.RestoreToLocal(val);
			}
			_applied.Clear();
		}

		public void ReassertLocalCosmeticsImmediate()
		{
			PlayerAvatar val = PlayerLookup.LocalAvatar();
			if ((Object)(object)val == (Object)null || string.IsNullOrEmpty(val.steamID))
			{
				return;
			}
			if (!ConfigService.IsEnabled())
			{
				ConfigService.LogDiag("reassert skipped: mod disabled");
				return;
			}
			if (!ConfigService.InActiveScene())
			{
				ConfigService.LogDiag("reassert skipped: not in an active scene");
				return;
			}
			int currentHP = ((!val.deadSet) ? EffectiveHealthFor(val) : 0);
			List<int> list = Cosmetics.ForcedSetForHealth(val.steamID, currentHP, Cosmetics.PlayerWearsHat(val));
			if (list.Count == 0)
			{
				ConfigService.LogDiag("reassert: nothing to re-apply at this HP");
				return;
			}
			_applied[val.steamID] = list;
			ConfigService.LogDiag("reassert -> apply " + Cosmetics.Describe(list));
			Cosmetics.Apply(val, list);
			Cosmetics.RefreshExpressionPreview();
		}

		private static bool SameSet(List<int> a, List<int> b)
		{
			if (a.Count != b.Count)
			{
				return false;
			}
			for (int i = 0; i < a.Count; i++)
			{
				if (a[i] != b[i])
				{
					return false;
				}
			}
			return true;
		}
	}
	public static class Effects
	{
		public static void ApplySpeedTick(PlayerAvatar avatar, Tier tier)
		{
			if (ConfigService.NerfsEnabled() && tier != 0 && !((Object)(object)avatar == (Object)null) && avatar.isLocal)
			{
				PlayerController instance = PlayerController.instance;
				if (!((Object)(object)instance == (Object)null))
				{
					instance.OverrideSpeed(ConfigService.SpeedMultiplierFor(tier), 0.2f);
				}
			}
		}

		public static void ApplyStaminaTick(PlayerAvatar avatar, Tier tier)
		{
			if (!ConfigService.NerfsEnabled() || tier == Tier.Healthy || (Object)(object)avatar == (Object)null || !avatar.isLocal)
			{
				return;
			}
			PlayerController instance = PlayerController.instance;
			if (!((Object)(object)instance == (Object)null))
			{
				float num = instance.EnergyStart * ConfigService.StaminaMultiplierFor(tier);
				if (instance.EnergyCurrent > num)
				{
					instance.EnergyCurrent = num;
				}
			}
		}
	}
	internal static class PlayerLookup
	{
		public static PlayerAvatar? LocalAvatar()
		{
			foreach (PlayerAvatar item in SemiFunc.PlayerGetAll())
			{
				if ((Object)(object)item != (Object)null && item.isLocal)
				{
					return item;
				}
			}
			return null;
		}
	}
	public static class SaveBackup
	{
		private const int BackupsToKeep = 5;

		private static bool _ranThisSession;

		public static void TryBackupOnce(PlayerAvatar? avatar)
		{
			if (_ranThisSession || (Object)(object)avatar == (Object)null || string.IsNullOrWhiteSpace(avatar.playerName))
			{
				return;
			}
			try
			{
				string text = Path.Combine(Application.persistentDataPath, "MetaSave.es3");
				if (!File.Exists(text))
				{
					_ranThisSession = true;
					return;
				}
				string text2 = Path.Combine(Paths.ConfigPath, "BattleScars", "backups", Sanitize(avatar.playerName));
				Directory.CreateDirectory(text2);
				string stamp = DateTime.Now.ToString("yyyy-MM-dd_HHmmss");
				string text3 = UniqueDestination(text2, stamp);
				File.Copy(text, text3, overwrite: false);
				BattleScars.Log.LogInfo((object)("[Backup] saved " + Path.GetFileName(text3)));
				Prune(text2);
				_ranThisSession = true;
			}
			catch (Exception ex)
			{
				BattleScars.Log.LogWarning((object)("[Backup] failed: " + ex.Message));
				_ranThisSession = true;
			}
		}

		private static string Sanitize(string raw)
		{
			char[] invalid = Path.GetInvalidFileNameChars();
			string text = new string(raw.Select((char c) => (!invalid.Contains(c)) ? c : '_').ToArray()).Trim();
			if (text.Length != 0)
			{
				return text;
			}
			return "unknown_player";
		}

		private static string UniqueDestination(string dir, string stamp)
		{
			string text = Path.Combine(dir, stamp + "_MetaSave.es3");
			if (!File.Exists(text))
			{
				return text;
			}
			for (int i = 1; i < 1000; i++)
			{
				string text2 = Path.Combine(dir, $"{stamp}_{i:D2}_MetaSave.es3");
				if (!File.Exists(text2))
				{
					return text2;
				}
			}
			return Path.Combine(dir, $"{stamp}_{Guid.NewGuid():N}_MetaSave.es3");
		}

		private static void Prune(string dir)
		{
			try
			{
				List<string> list = Directory.GetFiles(dir, "*_MetaSave.es3").OrderByDescending(File.GetCreationTimeUtc).ToList();
				for (int i = 5; i < list.Count; i++)
				{
					File.Delete(list[i]);
				}
			}
			catch
			{
			}
		}
	}
	public class ScreenOverlay : MonoBehaviour
	{
		private const float VignetteInner = 0.48f;

		private const float VignetteOuter = 1.5f;

		private const float OverlaySmoothTime = 0.35f;

		private const float OverlayEpsilon = 0.002f;

		private float _glitchTimer;

		private float _overlayT;

		private float _overlayVel;

		private float _pulsePhase;

		private Texture2D? _vignette;

		private void Awake()
		{
			_vignette = BuildVignette();
		}

		private void OnDestroy()
		{
			if ((Object)(object)_vignette != (Object)null)
			{
				Object.Destroy((Object)(object)_vignette);
			}
		}

		private void Update()
		{
			_overlayT = Mathf.SmoothDamp(_overlayT, OverlayTarget(), ref _overlayVel, 0.35f);
			if (_overlayT < 0.002f && _overlayVel <= 0f)
			{
				_overlayT = 0f;
			}
			_pulsePhase += Time.deltaTime * (3.5f + _overlayT * 4f);
			UpdateGlitch();
		}

		private void UpdateGlitch()
		{
			if (!ConfigService.CameraGlitchesEnabled() || !ConfigService.InActiveScene())
			{
				return;
			}
			PlayerAvatar val = PlayerLookup.LocalAvatar();
			if (!((Object)(object)val == (Object)null) && !val.deadSet && !val.isDisabled && ConfigService.TierForHealth(Driver.EffectiveHealthFor(val)) >= Tier.Wrecked && !ConfigService.PhotosensitivityOn())
			{
				_glitchTimer -= Time.deltaTime;
				if (!(_glitchTimer > 0f))
				{
					_glitchTimer = GlitchInterval(val) * Random.Range(0.85f, 1.15f);
					FireGlitch();
				}
			}
		}

		private static void FireGlitch()
		{
			CameraGlitch instance = CameraGlitch.Instance;
			if (!((Object)(object)instance == (Object)null))
			{
				float value = Random.value;
				if (value < 0.15f)
				{
					instance.PlayLong();
				}
				else if (value < 0.45f)
				{
					instance.PlayShort();
				}
				else
				{
					instance.PlayTiny();
				}
			}
		}

		private static float GlitchInterval(PlayerAvatar avatar)
		{
			if ((Object)(object)avatar.playerHealth == (Object)null)
			{
				return 12f;
			}
			int num = Mathf.Max(1, avatar.playerHealth.health);
			float num2 = Mathf.InverseLerp(1f, (float)PluginConfig.Curve.FirstScarHP, (float)num);
			return Mathf.Lerp(8f, 18f, num2);
		}

		private static float OverlayTarget()
		{
			if (!ConfigService.VignetteEnabled() || !ConfigService.InActiveScene())
			{
				return 0f;
			}
			PlayerAvatar val = PlayerLookup.LocalAvatar();
			if ((Object)(object)val == (Object)null)
			{
				return 0f;
			}
			return Mathf.InverseLerp((float)PluginConfig.Curve.FirstScarHP, 1f, (float)Driver.EffectiveHealthFor(val));
		}

		private void OnGUI()
		{
			//IL_005e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0073: Unknown result type (might be due to invalid IL or missing references)
			//IL_0093: Unknown result type (might be due to invalid IL or missing references)
			if (ConfigService.VignetteEnabled() && !((Object)(object)_vignette == (Object)null) && !(_overlayT < 0.002f))
			{
				float num = (ConfigService.PhotosensitivityOn() ? 0.9f : (0.82f + 0.18f * Mathf.Sin(_pulsePhase)));
				float num2 = PluginConfig.VignetteIntensity.Value * _overlayT * num;
				Color color = GUI.color;
				GUI.color = new Color(0.7f, 0.04f, 0.04f, num2);
				GUI.DrawTexture(new Rect(0f, 0f, (float)Screen.width, (float)Screen.height), (Texture)(object)_vignette, (ScaleMode)0);
				GUI.color = color;
			}
		}

		private static Texture2D BuildVignette()
		{
			//IL_000c: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Unknown result type (might be due to invalid IL or missing references)
			//IL_0018: Unknown result type (might be due to invalid IL or missing references)
			//IL_0020: Expected O, but got Unknown
			//IL_009c: Unknown result type (might be due to invalid IL or missing references)
			//IL_00a1: Unknown result type (might be due to invalid IL or missing references)
			Texture2D val = new Texture2D(128, 128, (TextureFormat)4, false)
			{
				filterMode = (FilterMode)1,
				wrapMode = (TextureWrapMode)1
			};
			Color[] array = (Color[])(object)new Color[16384];
			float num = 63.5f;
			for (int i = 0; i < 128; i++)
			{
				for (int j = 0; j < 128; j++)
				{
					float num2 = ((float)j - num) / num;
					float num3 = ((float)i - num) / num;
					float num4 = Mathf.Sqrt(num2 * num2 + num3 * num3);
					float num5 = Mathf.InverseLerp(0.48f, 1.5f, num4);
					array[i * 128 + j] = new Color(1f, 1f, 1f, num5 * num5 * (3f - 2f * num5));
				}
			}
			val.SetPixels(array);
			val.Apply();
			return val;
		}
	}
	public enum Tier
	{
		Healthy,
		Scratched,
		Damaged,
		Battered,
		Wrecked
	}
}
namespace BattleScars.Patches
{
	[HarmonyPatch(typeof(PlayerCosmetics), "SetupCosmeticsLogic")]
	internal static class MenuAvatarCosmeticsPatch
	{
		[HarmonyPrefix]
		public static void Prefix(PlayerCosmetics __instance, ref int[] _cosmeticEquipped)
		{
			if (!ConfigService.IsEnabled() || !ConfigService.InActiveScene() || (Object)(object)__instance == (Object)null)
			{
				return;
			}
			PlayerAvatarVisuals playerAvatarVisuals = __instance.playerAvatarVisuals;
			if ((Object)(object)playerAvatarVisuals == (Object)null || !playerAvatarVisuals.isMenuAvatar)
			{
				return;
			}
			PlayerAvatarMenu playerAvatarMenu = playerAvatarVisuals.playerAvatarMenu;
			if ((Object)(object)playerAvatarMenu == (Object)null)
			{
				return;
			}
			bool num = (Object)(object)playerAvatarMenu == (Object)(object)PlayerAvatarMenu.instance;
			bool expressionAvatar = playerAvatarMenu.expressionAvatar;
			if (!num && !expressionAvatar)
			{
				return;
			}
			PlayerAvatar val = PlayerLookup.LocalAvatar();
			if (!((Object)(object)val == (Object)null) && !string.IsNullOrEmpty(val.steamID))
			{
				int num2 = Driver.EffectiveHealthFor(val);
				List<int> list = Cosmetics.ForcedSetForHealth(val.steamID, num2, Cosmetics.PlayerWearsHat(val));
				ConfigService.LogDiag(string.Format("{0} preview hp={1} {2}", expressionAvatar ? "expression" : "menu", num2, Cosmetics.Describe(list)));
				if (list.Count != 0)
				{
					List<int> list2 = Cosmetics.Merge(MetaManager.instance?.cosmeticAssets, _cosmeticEquipped, list);
					_cosmeticEquipped = list2.ToArray();
				}
			}
		}
	}
	[HarmonyPatch(typeof(PlayerAvatar), "PlayerDeathRPC")]
	internal static class PlayerDeathPatch
	{
		[HarmonyPostfix]
		public static void Postfix(PlayerAvatar __instance)
		{
			if (!((Object)(object)__instance == (Object)null) && __instance.isLocal)
			{
				ConfigService.LogDiag("local death: reasserting broken set");
				Driver.Instance?.ReassertLocalCosmeticsImmediate();
			}
		}
	}
	[HarmonyPatch(typeof(PlayerCosmetics), "SetupCosmetics")]
	internal static class SetupCosmeticsReassertPatch
	{
		[HarmonyPostfix]
		public static void Postfix(PlayerCosmetics __instance, bool _forced)
		{
			if (!_forced && !((Object)(object)__instance == (Object)null) && !((Object)(object)__instance.playerAvatarVisuals == (Object)null) && !__instance.playerAvatarVisuals.isMenuAvatar)
			{
				PlayerAvatar playerAvatar = __instance.playerAvatarVisuals.playerAvatar;
				if (!((Object)(object)playerAvatar == (Object)null) && playerAvatar.isLocal)
				{
					ConfigService.LogDiag("vanilla cosmetic refresh on the local body, reasserting");
					Driver.Instance?.ReassertLocalCosmeticsImmediate();
				}
			}
		}
	}
	[HarmonyPatch(typeof(StatsManager), "Start")]
	internal static class StatsManagerStartPatch
	{
		private static GameObject? _services;

		[HarmonyPostfix]
		public static void Postfix()
		{
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_001d: Expected O, but got Unknown
			if (!((Object)(object)_services != (Object)null))
			{
				_services = new GameObject("BattleScars_Services");
				_services.AddComponent<Driver>();
				_services.AddComponent<ScreenOverlay>();
				Object.DontDestroyOnLoad((Object)(object)_services);
			}
		}
	}
}
namespace BattleScars.Configuration
{
	public enum RunMode
	{
		Off,
		VisualOnly,
		Full
	}
	public enum ScarIntensity
	{
		Light,
		Normal,
		Heavy
	}
	public sealed class ScarCurve
	{
		public readonly int FirstScarHP;

		public readonly int SlotStepHP;

		public readonly int SeverityStepHP;

		public readonly int SlotStaggerHP;

		public readonly int BrokenHeadHP;

		public ScarCurve(int firstScarHP, int slotStepHP, int severityStepHP, int slotStaggerHP, int brokenHeadHP)
		{
			FirstScarHP = firstScarHP;
			SlotStepHP = slotStepHP;
			SeverityStepHP = severityStepHP;
			SlotStaggerHP = slotStaggerHP;
			BrokenHeadHP = brokenHeadHP;
		}
	}
	internal static class PluginConfig
	{
		private static readonly ScarCurve LightCurve = new ScarCurve(60, 11, 22, 9, 5);

		private static readonly ScarCurve NormalCurve = new ScarCurve(75, 8, 15, 7, 9);

		private static readonly ScarCurve HeavyCurve = new ScarCurve(95, 6, 11, 5, 14);

		public const float SpeedNerfMax = 0.7f;

		public const float StaminaNerfMax = 0.45f;

		public const string BandagesAllowList = "Bandages";

		public const string CracksAllowList = "Cracks";

		public const string DamagedAllowList = "Damaged";

		public const string BrokenAllowList = "Broken";

		public static ConfigEntry<RunMode> Mode = null;

		public static ConfigEntry<ScarIntensity> Intensity = null;

		public static ConfigEntry<bool> Vignette = null;

		public static ConfigEntry<float> VignetteIntensity = null;

		public static ConfigEntry<bool> CameraGlitches = null;

		public static ConfigEntry<int> TestHealth = null;

		public static ConfigEntry<bool> DebugLogging = null;

		public static ScarCurve Curve => Intensity.Value switch
		{
			ScarIntensity.Light => LightCurve, 
			ScarIntensity.Heavy => HeavyCurve, 
			_ => NormalCurve, 
		};

		public static void Init(ConfigFile config)
		{
			//IL_007a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0084: Expected O, but got Unknown
			//IL_00c2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00cc: Expected O, but got Unknown
			Mode = config.Bind<RunMode>("General", "Mode", RunMode.VisualOnly, "Off: mod inactive. VisualOnly: scars and the screen vignette, no nerfs. Full: adds the move-speed and stamina nerfs on top.");
			Intensity = config.Bind<ScarIntensity>("General", "ScarIntensity", ScarIntensity.Heavy, "How early and how hard scars build up. Light holds them off until you're badly hurt, Heavy starts them after the first few hits.");
			Vignette = config.Bind<bool>("Effects", "Vignette", true, "Red edge vignette that ramps up at low HP. Off hides it entirely; on uses VignetteIntensity for strength.");
			VignetteIntensity = config.Bind<float>("Effects", "VignetteIntensity", 0.25f, new ConfigDescription("How strong the red vignette gets at its worst, near death. Ramps up as HP drops. 0 fades it to nothing, 1 is intense.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>()));
			CameraGlitches = config.Bind<bool>("Effects", "CameraGlitches", false, "Screen flashes and camera faults that fire at very low HP. Off (default) keeps the camera steady regardless of damage. REPO's photosensitivity accessibility setting also forces this off.");
			TestHealth = config.Bind<int>("Testing", "TestHealth", -1, new ConfigDescription("Preview a synthetic HP value. -1 disables. 0-100 forces that HP through the tier pipeline without touching real health or networked state. Numpad 0-9 in-game also drives this (0=off, 1=HP 1, 2=HP 20, etc).", (AcceptableValueBase)(object)new AcceptableValueRange<int>(-1, 100), Array.Empty<object>()));
			DebugLogging = config.Bind<bool>("Testing", "DebugLogging", false, "Log every scar apply, restore and reassert to the BepInEx console. For bug reports; leave off otherwise.");
		}
	}
}