Decompiled source of BattleScars v1.0.0

BattleScars.dll

Decompiled 2 days 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 BattleScars.Configuration;
using BattleScars.Services;
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using ExitGames.Client.Photon;
using HarmonyLib;
using Microsoft.CodeAnalysis;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine;

[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)]
[assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")]
[assembly: IgnoresAccessChecksTo("Assembly-CSharp")]
[assembly: AssemblyCompany("Vippy")]
[assembly: AssemblyConfiguration("Debug")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0")]
[assembly: AssemblyProduct("BattleScars")]
[assembly: AssemblyTitle("BattleScars")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.0.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.0.0")]
	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_004b: Unknown result type (might be due to invalid IL or missing references)
			//IL_0055: 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
			{
				PlayerAvatar val = PlayerLookup.LocalAvatar();
				if (!((Object)(object)val == (Object)null))
				{
					if (PluginConfig.Mode.Value == RunMode.Off)
					{
						Cosmetics.RestoreToLocal(val);
						Effects.CancelVoice(val);
						Driver.Instance?.InvalidateAppliedCosmetics();
					}
					else if (PluginConfig.Mode.Value == RunMode.VisualOnly)
					{
						Effects.CancelVoice(val);
					}
				}
			};
		}
	}
	internal static class BuildInfo
	{
		public const string Version = "1.0.0";
	}
}
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 CosmeticsEnabled()
		{
			return IsEnabled() && PluginConfig.EnableCosmetics.Value;
		}

		public static bool SparkParticlesEnabled()
		{
			return IsEnabled() && PluginConfig.EnableSparkParticles.Value;
		}

		public static bool ScreenOverlayEnabled()
		{
			return IsEnabled() && PluginConfig.EnableScreenOverlay.Value;
		}

		public static bool SpeedNerfEnabled()
		{
			return IsEnabled() && !IsVisualOnly() && PluginConfig.EnableSpeedNerf.Value;
		}

		public static bool StaminaNerfEnabled()
		{
			return IsEnabled() && !IsVisualOnly() && PluginConfig.EnableStaminaNerf.Value;
		}

		public static bool VoiceEffectsEnabled()
		{
			return IsEnabled() && !IsVisualOnly() && PluginConfig.EnableVoiceEffects.Value;
		}

		public static int TestHealthOverride()
		{
			return PluginConfig.TestHealth.Value;
		}

		public static int CosmeticCountForHealth(int currentHP)
		{
			if (currentHP > 75)
			{
				return 0;
			}
			return (75 - currentHP) / 8 + 1;
		}

		public static CosmeticPool PoolForHealth(int currentHP)
		{
			if (currentHP <= 50)
			{
				return CosmeticPool.Damaged;
			}
			if (currentHP <= 60)
			{
				return CosmeticPool.Bandages;
			}
			return CosmeticPool.Rusty;
		}

		public static bool WreckedFaceActive(int currentHP)
		{
			return currentHP <= 25 && !string.IsNullOrWhiteSpace("Broken");
		}

		public static Tier TierForHealth(int currentHP)
		{
			int num = CosmeticCountForHealth(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 CosmeticPool
	{
		Rusty,
		Bandages,
		Damaged
	}
	public static class Cosmetics
	{
		private static List<int>? _rustyPool;

		private static List<int>? _bandagesPool;

		private static List<int>? _damagedPool;

		private static List<int>? _wreckedFacePool;

		private static bool _discoveryRan;

		public static void DiscoverIfNeeded()
		{
			if (!_discoveryRan && !((Object)(object)MetaManager.instance == (Object)null) && MetaManager.instance.cosmeticAssets != null)
			{
				List<CosmeticAsset> cosmeticAssets = MetaManager.instance.cosmeticAssets;
				_rustyPool = BuildPool(cosmeticAssets, "Rusty");
				_bandagesPool = BuildPool(cosmeticAssets, "Bandages");
				_damagedPool = BuildPool(cosmeticAssets, "Damaged,Cracks");
				_wreckedFacePool = BuildPool(cosmeticAssets, "Broken");
				_discoveryRan = true;
				int num = _rustyPool.Count + _bandagesPool.Count + _damagedPool.Count + _wreckedFacePool.Count;
				BattleScars.Log.LogInfo((object)$"[Cosmetics] pools rusty={_rustyPool.Count} bandages={_bandagesPool.Count} damaged={_damagedPool.Count} wreckedFace={_wreckedFacePool.Count}");
				if (num == 0)
				{
					BattleScars.Log.LogWarning((object)"[Cosmetics] no pool matches, cosmetic effects disabled");
				}
			}
		}

		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(',');
			foreach (string text in array)
			{
				string text2 = text.Trim().ToLowerInvariant();
				if (text2.Length > 0)
				{
					list.Add(text2);
				}
			}
			return list;
		}

		public static List<int> PickForCount(string steamID, int count, CosmeticPool pool)
		{
			//IL_015b: Unknown result type (might be due to invalid IL or missing references)
			//IL_017b: Unknown result type (might be due to invalid IL or missing references)
			if (count <= 0)
			{
				return new List<int>();
			}
			if (1 == 0)
			{
			}
			List<int> list = pool switch
			{
				CosmeticPool.Rusty => _rustyPool, 
				CosmeticPool.Bandages => _bandagesPool, 
				CosmeticPool.Damaged => _damagedPool, 
				_ => _bandagesPool, 
			};
			if (1 == 0)
			{
			}
			List<int> list2 = list;
			if (list2 == null || list2.Count == 0)
			{
				return new List<int>();
			}
			List<CosmeticAsset> list3 = MetaManager.instance?.cosmeticAssets;
			if (list3 == null)
			{
				return new List<int>();
			}
			int seed = (string.IsNullOrEmpty(steamID) ? 1 : steamID.GetHashCode());
			Random rng = new Random(seed);
			List<int> list4 = list2.OrderBy((int _) => rng.Next()).ToList();
			List<int> list5 = new List<int>(count);
			HashSet<CosmeticType> hashSet = new HashSet<CosmeticType>();
			foreach (int item in list4)
			{
				if (list5.Count >= count)
				{
					break;
				}
				if (item >= 0 && item < list3.Count)
				{
					CosmeticAsset val = list3[item];
					if (!((Object)(object)val == (Object)null) && !hashSet.Contains(val.type))
					{
						list5.Add(item);
						hashSet.Add(val.type);
					}
				}
			}
			if (list5.Count < count)
			{
				foreach (int item2 in list4)
				{
					if (list5.Count >= count)
					{
						break;
					}
					if (!list5.Contains(item2))
					{
						list5.Add(item2);
					}
				}
			}
			return list5;
		}

		public static IReadOnlyList<int> WreckedFaceIndices()
		{
			IReadOnlyList<int> wreckedFacePool = _wreckedFacePool;
			return wreckedFacePool ?? Array.Empty<int>();
		}

		public static List<int> Merge(IList<CosmeticAsset>? assets, IList<int> ownList, IList<int> forced)
		{
			//IL_006e: Unknown result type (might be due to invalid IL or missing references)
			//IL_012e: 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 ApplyForState(PlayerAvatar avatar, int count, CosmeticPool pool, bool wreckedFace)
		{
			if ((Object)(object)avatar == (Object)null || (Object)(object)avatar.playerCosmetics == (Object)null || (!avatar.photonView.IsMine && SemiFunc.IsMultiplayer()))
			{
				return;
			}
			List<CosmeticAsset> assets = MetaManager.instance?.cosmeticAssets;
			List<int> ownList = avatar.playerCosmetics.cosmeticEquippedRaw ?? new List<int>();
			List<int> list = PickForCount(avatar.steamID, count, pool);
			IReadOnlyList<int> readOnlyList2;
			if (!wreckedFace)
			{
				IReadOnlyList<int> readOnlyList = Array.Empty<int>();
				readOnlyList2 = readOnlyList;
			}
			else
			{
				readOnlyList2 = WreckedFaceIndices();
			}
			IReadOnlyList<int> readOnlyList3 = readOnlyList2;
			List<int> list2 = new List<int>(list.Count + readOnlyList3.Count);
			foreach (int item in readOnlyList3)
			{
				list2.Add(item);
			}
			foreach (int item2 in list)
			{
				if (!list2.Contains(item2))
				{
					list2.Add(item2);
				}
			}
			List<int> list3 = Merge(assets, ownList, list2);
			using (CosmeticReassertGuard.Enter())
			{
				avatar.playerCosmetics.SetupCosmetics(SemiFunc.IsMultiplayer(), true, list3);
				avatar.playerCosmetics.SetupColors(SemiFunc.IsMultiplayer(), (int[])null);
			}
		}

		public static void RestoreToLocal(PlayerAvatar avatar)
		{
			if ((Object)(object)avatar == (Object)null || (Object)(object)avatar.playerCosmetics == (Object)null || (!avatar.photonView.IsMine && SemiFunc.IsMultiplayer()))
			{
				return;
			}
			using (CosmeticReassertGuard.Enter())
			{
				avatar.playerCosmetics.SetupCosmetics(SemiFunc.IsMultiplayer(), true, (List<int>)null);
				avatar.playerCosmetics.SetupColors(SemiFunc.IsMultiplayer(), (int[])null);
			}
		}
	}
	internal static class CosmeticReassertGuard
	{
		[StructLayout(LayoutKind.Sequential, Size = 1)]
		public struct Releaser : IDisposable
		{
			public void Dispose()
			{
				if (_depth > 0)
				{
					_depth--;
				}
			}
		}

		[ThreadStatic]
		private static int _depth;

		public static bool IsInside => _depth > 0;

		public static Releaser Enter()
		{
			_depth++;
			return default(Releaser);
		}
	}
	public class Driver : MonoBehaviour
	{
		private struct AppliedCosmeticState
		{
			public int Count;

			public CosmeticPool Pool;

			public bool Face;
		}

		private const float SlowTickInterval = 1f;

		private const float VoiceTickInterval = 0.5f;

		private float _slowTickTimer;

		private float _voiceTickTimer;

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

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

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

		private void Update()
		{
			HandleDevHotkeys();
			if ((Object)(object)StatsManager.instance == (Object)null)
			{
				return;
			}
			PlayerAvatar val = PlayerLookup.LocalAvatar();
			bool flag = ConfigService.IsEnabled();
			bool flag2 = (Object)(object)val != (Object)null && (val.deadSet || val.isDisabled);
			Tier tier = Tier.Healthy;
			if (flag && !flag2 && (Object)(object)val != (Object)null)
			{
				tier = ConfigService.TierForHealth(EffectiveHealthFor(val));
			}
			if ((Object)(object)val != (Object)null)
			{
				Effects.ApplySpeedTick(val, tier);
				Effects.ApplyStaminaTick(val, tier);
			}
			_voiceTickTimer -= Time.deltaTime;
			if (_voiceTickTimer <= 0f && (Object)(object)val != (Object)null)
			{
				_voiceTickTimer = 0.5f;
				if (tier == Tier.Healthy)
				{
					Effects.CancelVoice(val);
				}
				else
				{
					Effects.ApplyVoiceTick(val, tier);
				}
			}
			_slowTickTimer -= Time.deltaTime;
			if (!(_slowTickTimer > 0f))
			{
				_slowTickTimer = 1f;
				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();
			}
		}

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

		private void SlowTick(PlayerAvatar? local, bool enabled, bool deadOrDisabled)
		{
			SaveBackup.TryBackupOnce(local);
			if ((Object)(object)local == (Object)null || string.IsNullOrEmpty(local.steamID))
			{
				return;
			}
			int currentHP = EffectiveHealthFor(local);
			bool flag = enabled && !deadOrDisabled;
			int num = (flag ? ConfigService.CosmeticCountForHealth(currentHP) : 0);
			CosmeticPool cosmeticPool = (flag ? ConfigService.PoolForHealth(currentHP) : CosmeticPool.Rusty);
			bool flag2 = flag && ConfigService.WreckedFaceActive(currentHP);
			_applied.TryGetValue(local.steamID, out var value);
			if (value.Count == num && value.Pool == cosmeticPool && value.Face == flag2)
			{
				return;
			}
			_applied[local.steamID] = new AppliedCosmeticState
			{
				Count = num,
				Pool = cosmeticPool,
				Face = flag2
			};
			if (ConfigService.CosmeticsEnabled())
			{
				if (num <= 0)
				{
					Cosmetics.RestoreToLocal(local);
				}
				else
				{
					Cosmetics.ApplyForState(local, num, cosmeticPool, flag2);
				}
			}
		}

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

		public void ReassertLocalCosmeticsImmediate()
		{
			PlayerAvatar val = PlayerLookup.LocalAvatar();
			if (!((Object)(object)val == (Object)null) && !string.IsNullOrEmpty(val.steamID) && ConfigService.IsEnabled() && ConfigService.CosmeticsEnabled() && !val.deadSet && !val.isDisabled)
			{
				int currentHP = EffectiveHealthFor(val);
				int num = ConfigService.CosmeticCountForHealth(currentHP);
				if (num > 0)
				{
					CosmeticPool pool = ConfigService.PoolForHealth(currentHP);
					bool flag = ConfigService.WreckedFaceActive(currentHP);
					_applied[val.steamID] = new AppliedCosmeticState
					{
						Count = num,
						Pool = pool,
						Face = flag
					};
					Cosmetics.ApplyForState(val, num, pool, flag);
				}
			}
		}
	}
	public static class Effects
	{
		public static void ApplySpeedTick(PlayerAvatar avatar, Tier tier)
		{
			if (ConfigService.SpeedNerfEnabled() && 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.StaminaNerfEnabled() || 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;
				}
			}
		}

		public static void ApplyVoiceTick(PlayerAvatar avatar, Tier tier)
		{
			if (ConfigService.VoiceEffectsEnabled() && tier != 0 && !((Object)(object)avatar == (Object)null) && avatar.isLocal && !((Object)(object)avatar.voiceChat == (Object)null) && avatar.voiceChatFetched)
			{
				if (1 == 0)
				{
				}
				float num = tier switch
				{
					Tier.Scratched => 0.95f, 
					Tier.Damaged => 0.88f, 
					Tier.Battered => 0.78f, 
					Tier.Wrecked => 0.68f, 
					_ => 1f, 
				};
				if (1 == 0)
				{
				}
				float num2 = num;
				if (1 == 0)
				{
				}
				num = tier switch
				{
					Tier.Scratched => 0.02f, 
					Tier.Damaged => 0.05f, 
					Tier.Battered => 0.1f, 
					Tier.Wrecked => 0.16f, 
					_ => 0f, 
				};
				if (1 == 0)
				{
				}
				float num3 = num;
				if (1 == 0)
				{
				}
				num = tier switch
				{
					Tier.Scratched => 2f, 
					Tier.Damaged => 4f, 
					Tier.Battered => 7f, 
					Tier.Wrecked => 11f, 
					_ => 0f, 
				};
				if (1 == 0)
				{
				}
				float num4 = num;
				avatar.voiceChat.OverridePitch(num2, 0.4f, 0.4f, 0.6f, num3, num4);
				if (tier >= Tier.Battered)
				{
					avatar.voiceChat.OverrideVoiceDistortion(0.6f);
				}
				if (tier >= Tier.Wrecked)
				{
					avatar.voiceChat.OverrideVolumeStutter(0.6f);
				}
				if (SemiFunc.IsMultiplayer() && (Object)(object)avatar.photonView != (Object)null)
				{
					NetEvents.SendVoicePitch(avatar.photonView.ViewID, num2, num3, num4, 0.6f);
				}
			}
		}

		public static void CancelVoice(PlayerAvatar avatar)
		{
			if (!((Object)(object)avatar?.voiceChat == (Object)null))
			{
				avatar.voiceChat.OverridePitchCancel();
				if (SemiFunc.IsMultiplayer() && (Object)(object)avatar.photonView != (Object)null)
				{
					NetEvents.SendVoiceCancel(avatar.photonView.ViewID);
				}
			}
		}

		public static void SpawnSparks(PlayerAvatar avatar, int hitDamage)
		{
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_0028: Unknown result type (might be due to invalid IL or missing references)
			//IL_002d: Unknown result type (might be due to invalid IL or missing references)
			//IL_002e: Unknown result type (might be due to invalid IL or missing references)
			//IL_003f: Unknown result type (might be due to invalid IL or missing references)
			if (ConfigService.SparkParticlesEnabled() && !((Object)(object)avatar == (Object)null))
			{
				Vector3 pos = ((Component)avatar).transform.position + Vector3.up;
				SpawnSparksAt(pos, hitDamage);
				if (SemiFunc.IsMultiplayer())
				{
					NetEvents.SendSparkSpawn(pos, hitDamage);
				}
			}
		}

		public static void SpawnSparksAt(Vector3 pos, int hitDamage)
		{
			//IL_0019: Unknown result type (might be due to invalid IL or missing references)
			//IL_001e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0024: Unknown result type (might be due to invalid IL or missing references)
			//IL_002c: Expected O, but got Unknown
			//IL_0034: Unknown result type (might be due to invalid IL or missing references)
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			//IL_0041: Unknown result type (might be due to invalid IL or missing references)
			//IL_0053: Unknown result type (might be due to invalid IL or missing references)
			//IL_0065: Unknown result type (might be due to invalid IL or missing references)
			//IL_0081: Unknown result type (might be due to invalid IL or missing references)
			//IL_0086: Unknown result type (might be due to invalid IL or missing references)
			//IL_0098: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c4: Unknown result type (might be due to invalid IL or missing references)
			//IL_00c9: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d1: Unknown result type (might be due to invalid IL or missing references)
			//IL_00f6: Unknown result type (might be due to invalid IL or missing references)
			//IL_00fb: Unknown result type (might be due to invalid IL or missing references)
			//IL_0107: Unknown result type (might be due to invalid IL or missing references)
			//IL_010c: Unknown result type (might be due to invalid IL or missing references)
			//IL_012e: Unknown result type (might be due to invalid IL or missing references)
			//IL_0133: Unknown result type (might be due to invalid IL or missing references)
			//IL_0143: Unknown result type (might be due to invalid IL or missing references)
			//IL_0150: Expected O, but got Unknown
			if (ConfigService.SparkParticlesEnabled())
			{
				GameObject val = new GameObject("BattleScars_Sparks");
				val.transform.position = pos;
				GameObject val2 = val;
				ParticleSystem val3 = val2.AddComponent<ParticleSystem>();
				MainModule main = val3.main;
				((MainModule)(ref main)).startLifetime = MinMaxCurve.op_Implicit(0.5f);
				((MainModule)(ref main)).startSpeed = MinMaxCurve.op_Implicit(4f);
				((MainModule)(ref main)).startSize = MinMaxCurve.op_Implicit(0.06f);
				((MainModule)(ref main)).startColor = MinMaxGradient.op_Implicit(new Color(1f, 0.7f, 0.1f));
				((MainModule)(ref main)).gravityModifier = MinMaxCurve.op_Implicit(0.8f);
				((MainModule)(ref main)).maxParticles = 64;
				((MainModule)(ref main)).duration = 0.2f;
				((MainModule)(ref main)).loop = false;
				EmissionModule emission = val3.emission;
				((EmissionModule)(ref emission)).rateOverTime = MinMaxCurve.op_Implicit(0f);
				int num = Mathf.Clamp(8 + hitDamage / 3, 8, 60);
				((EmissionModule)(ref emission)).SetBurst(0, new Burst(0f, MinMaxCurve.op_Implicit((float)num)));
				ShapeModule shape = val3.shape;
				((ShapeModule)(ref shape)).shapeType = (ParticleSystemShapeType)0;
				((ShapeModule)(ref shape)).radius = 0.25f;
				Material material = new Material(Shader.Find("Sprites/Default"))
				{
					color = new Color(1f, 0.8f, 0.2f)
				};
				ParticleSystemRenderer component = val2.GetComponent<ParticleSystemRenderer>();
				((Renderer)component).material = material;
				component.renderMode = (ParticleSystemRenderMode)0;
				val3.Play();
				Object.Destroy((Object)(object)val2, 1.5f);
			}
		}
	}
	public class NetEvents : MonoBehaviour, IOnEventCallback
	{
		public const byte EventVoicePitch = 188;

		public const byte EventSparkSpawn = 189;

		private static readonly RaiseEventOptions ToOthers = new RaiseEventOptions
		{
			Receivers = (ReceiverGroup)0
		};

		private static readonly SendOptions Reliable;

		private static readonly SendOptions Unreliable;

		private void OnEnable()
		{
			PhotonNetwork.AddCallbackTarget((object)this);
		}

		private void OnDisable()
		{
			PhotonNetwork.RemoveCallbackTarget((object)this);
		}

		public static void SendVoicePitch(int avatarViewID, float pitch, float oscillation, float oscSpeed, float duration)
		{
			//IL_004d: Unknown result type (might be due to invalid IL or missing references)
			if (PhotonNetwork.InRoom)
			{
				PhotonNetwork.RaiseEvent((byte)188, (object)new object[5] { avatarViewID, pitch, oscillation, oscSpeed, duration }, ToOthers, Reliable);
			}
		}

		public static void SendVoiceCancel(int avatarViewID)
		{
			//IL_005c: Unknown result type (might be due to invalid IL or missing references)
			if (PhotonNetwork.InRoom)
			{
				PhotonNetwork.RaiseEvent((byte)188, (object)new object[5] { avatarViewID, 1f, 0f, 0f, 0f }, ToOthers, Reliable);
			}
		}

		public static void SendSparkSpawn(Vector3 pos, int damage)
		{
			//IL_001c: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0038: Unknown result type (might be due to invalid IL or missing references)
			//IL_0052: Unknown result type (might be due to invalid IL or missing references)
			if (PhotonNetwork.InRoom)
			{
				PhotonNetwork.RaiseEvent((byte)189, (object)new object[4] { pos.x, pos.y, pos.z, damage }, ToOthers, Unreliable);
			}
		}

		public void OnEvent(EventData photonEvent)
		{
			switch (photonEvent.Code)
			{
			case 188:
				HandleVoicePitch(photonEvent.CustomData);
				break;
			case 189:
				HandleSparkSpawn(photonEvent.CustomData);
				break;
			}
		}

		private static void HandleVoicePitch(object? raw)
		{
			if (!(raw is object[] array) || array.Length < 5)
			{
				return;
			}
			object obj = array[0];
			if (!(obj is int))
			{
				return;
			}
			int num = (int)obj;
			if (1 == 0)
			{
				return;
			}
			obj = array[1];
			if (!(obj is float))
			{
				return;
			}
			float num2 = (float)obj;
			if (1 == 0)
			{
				return;
			}
			obj = array[2];
			if (!(obj is float))
			{
				return;
			}
			float num3 = (float)obj;
			if (1 == 0)
			{
				return;
			}
			obj = array[3];
			if (!(obj is float))
			{
				return;
			}
			float num4 = (float)obj;
			if (1 == 0)
			{
				return;
			}
			obj = array[4];
			if (!(obj is float))
			{
				return;
			}
			float num5 = (float)obj;
			if (1 == 0)
			{
				return;
			}
			PhotonView val = PhotonView.Find(num);
			PlayerAvatar val2 = (((Object)(object)val != (Object)null) ? ((Component)val).GetComponent<PlayerAvatar>() : null);
			if (!((Object)(object)val2 == (Object)null) && !((Object)(object)val2.voiceChat == (Object)null))
			{
				if (num5 <= 0.01f && Mathf.Approximately(num2, 1f))
				{
					val2.voiceChat.OverridePitchCancel();
				}
				else
				{
					val2.voiceChat.OverridePitch(num2, 0.4f, 0.4f, num5, num3, num4);
				}
			}
		}

		private static void HandleSparkSpawn(object? raw)
		{
			//IL_009c: Unknown result type (might be due to invalid IL or missing references)
			if (!(raw is object[] array) || array.Length < 4)
			{
				return;
			}
			object obj = array[0];
			if (!(obj is float))
			{
				return;
			}
			float num = (float)obj;
			obj = array[1];
			if (!(obj is float))
			{
				return;
			}
			float num2 = (float)obj;
			obj = array[2];
			float num3 = default(float);
			int num4;
			if (obj is float)
			{
				num3 = (float)obj;
				num4 = 1;
			}
			else
			{
				num4 = 0;
			}
			if (num4 == 0)
			{
				return;
			}
			obj = array[3];
			if (obj is int)
			{
				int hitDamage = (int)obj;
				if (true)
				{
					Effects.SpawnSparksAt(new Vector3(num, num2, num3), hitDamage);
				}
			}
		}

		static NetEvents()
		{
			//IL_0000: Unknown result type (might be due to invalid IL or missing references)
			//IL_0005: Unknown result type (might be due to invalid IL or missing references)
			//IL_0007: Unknown result type (might be due to invalid IL or missing references)
			//IL_0011: Expected O, but got Unknown
			//IL_0013: Unknown result type (might be due to invalid IL or missing references)
			//IL_0022: Unknown result type (might be due to invalid IL or missing references)
			//IL_0023: Unknown result type (might be due to invalid IL or missing references)
			//IL_002a: Unknown result type (might be due to invalid IL or missing references)
			//IL_0039: Unknown result type (might be due to invalid IL or missing references)
			//IL_003a: Unknown result type (might be due to invalid IL or missing references)
			SendOptions val = default(SendOptions);
			((SendOptions)(ref val)).Reliability = true;
			Reliable = val;
			val = default(SendOptions);
			((SendOptions)(ref val)).Reliability = false;
			Unreliable = val;
		}
	}
	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();
			char[] value = raw.Select((char c) => invalid.Contains(c) ? '_' : c).ToArray();
			string text = new string(value).Trim();
			return (text.Length == 0) ? "unknown_player" : text;
		}

		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 float _glitchTimer;

		private void Update()
		{
			if (!ConfigService.ScreenOverlayEnabled())
			{
				return;
			}
			PlayerAvatar val = PlayerLookup.LocalAvatar();
			if ((Object)(object)val == (Object)null || val.deadSet || val.isDisabled)
			{
				return;
			}
			Tier tier = ConfigService.TierForHealth(Driver.EffectiveHealthFor(val));
			if (tier != 0)
			{
				_glitchTimer -= Time.deltaTime;
				if (!(_glitchTimer > 0f))
				{
					_glitchTimer = GlitchInterval(val) * Random.Range(0.7f, 1.3f);
					FireGlitch(tier);
				}
			}
		}

		private static void FireGlitch(Tier tier)
		{
			CameraGlitch instance = CameraGlitch.Instance;
			if ((Object)(object)instance == (Object)null)
			{
				return;
			}
			float value = Random.value;
			if (tier >= Tier.Wrecked)
			{
				if (value < 0.65f)
				{
					instance.PlayLong();
				}
				else
				{
					instance.PlayShort();
				}
			}
			else if (tier >= Tier.Battered)
			{
				if (value < 0.45f)
				{
					instance.PlayLong();
				}
				else if (value < 0.85f)
				{
					instance.PlayShort();
				}
				else
				{
					instance.PlayTiny();
				}
			}
			else if (tier >= Tier.Damaged)
			{
				if (value < 0.5f)
				{
					instance.PlayShort();
				}
				else
				{
					instance.PlayTiny();
				}
			}
			else
			{
				instance.PlayTiny();
			}
		}

		private static float GlitchInterval(PlayerAvatar avatar)
		{
			if ((Object)(object)avatar.playerHealth == (Object)null)
			{
				return 6f;
			}
			int num = Mathf.Max(1, avatar.playerHealth.health);
			return Mathf.Clamp(0.5f + (float)num * 0.15f, 1f, 12f);
		}

		private void OnGUI()
		{
			//IL_00d2: Unknown result type (might be due to invalid IL or missing references)
			//IL_00d7: Unknown result type (might be due to invalid IL or missing references)
			//IL_00ea: Unknown result type (might be due to invalid IL or missing references)
			//IL_010b: Unknown result type (might be due to invalid IL or missing references)
			//IL_011c: Unknown result type (might be due to invalid IL or missing references)
			if (!ConfigService.ScreenOverlayEnabled())
			{
				return;
			}
			PlayerAvatar val = PlayerLookup.LocalAvatar();
			if ((Object)(object)val == (Object)null)
			{
				return;
			}
			Tier tier = ConfigService.TierForHealth(Driver.EffectiveHealthFor(val));
			if (tier != 0)
			{
				if (1 == 0)
				{
				}
				float num = tier switch
				{
					Tier.Scratched => 0.25f, 
					Tier.Damaged => 0.5f, 
					Tier.Battered => 0.75f, 
					Tier.Wrecked => 1f, 
					_ => 0f, 
				};
				if (1 == 0)
				{
				}
				float num2 = num;
				float num3 = 0.85f + 0.15f * Mathf.Sin(Time.time * (2f + num2 * 3f));
				float num4 = Mathf.Lerp(0f, 0.2f, num2) * num3;
				Color color = GUI.color;
				GUI.color = new Color(0.6f, 0f, 0f, num4);
				GUI.DrawTexture(new Rect(0f, 0f, (float)Screen.width, (float)Screen.height), (Texture)(object)Texture2D.whiteTexture, (ScaleMode)0);
				GUI.color = color;
			}
		}
	}
	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.CosmeticsEnabled() || (Object)(object)__instance == (Object)null || (Object)(object)__instance.playerAvatarVisuals == (Object)null || !__instance.playerAvatarVisuals.isMenuAvatar)
			{
				return;
			}
			PlayerAvatar val = PlayerLookup.LocalAvatar();
			if ((Object)(object)val == (Object)null || string.IsNullOrEmpty(val.steamID))
			{
				return;
			}
			int currentHP = ((ConfigService.TestHealthOverride() >= 0) ? ConfigService.TestHealthOverride() : (((Object)(object)val.playerHealth != (Object)null) ? val.playerHealth.health : 100));
			int num = ConfigService.CosmeticCountForHealth(currentHP);
			if (num <= 0)
			{
				return;
			}
			CosmeticPool pool = ConfigService.PoolForHealth(currentHP);
			bool flag = ConfigService.WreckedFaceActive(currentHP);
			List<int> list = Cosmetics.PickForCount(val.steamID, num, pool);
			IReadOnlyList<int> readOnlyList2;
			if (!flag)
			{
				IReadOnlyList<int> readOnlyList = Array.Empty<int>();
				readOnlyList2 = readOnlyList;
			}
			else
			{
				readOnlyList2 = Cosmetics.WreckedFaceIndices();
			}
			IReadOnlyList<int> readOnlyList3 = readOnlyList2;
			List<int> list2 = new List<int>(list.Count + readOnlyList3.Count);
			foreach (int item in readOnlyList3)
			{
				list2.Add(item);
			}
			foreach (int item2 in list)
			{
				if (!list2.Contains(item2))
				{
					list2.Add(item2);
				}
			}
			List<int> list3 = Cosmetics.Merge(MetaManager.instance?.cosmeticAssets, _cosmeticEquipped, list2);
			_cosmeticEquipped = list3.ToArray();
		}
	}
	[HarmonyPatch(typeof(PlayerHealth), "Hurt")]
	internal static class PlayerHealthHurtPatch
	{
		[HarmonyPrefix]
		public static void Prefix(PlayerHealth __instance, out int __state)
		{
			__state = (((Object)(object)__instance != (Object)null) ? __instance.health : 0);
		}

		[HarmonyPostfix]
		public static void Postfix(PlayerHealth __instance, int __state)
		{
			if (!ConfigService.IsEnabled() || (Object)(object)__instance == (Object)null)
			{
				return;
			}
			int num = __state - __instance.health;
			if (num > 0)
			{
				PlayerAvatar component = ((Component)__instance).GetComponent<PlayerAvatar>();
				if (!((Object)(object)component == (Object)null) && !string.IsNullOrEmpty(component.steamID))
				{
					Effects.SpawnSparks(component, num);
				}
			}
		}
	}
	[HarmonyPatch(typeof(PlayerCosmetics), "SetupCosmetics")]
	internal static class SetupCosmeticsReassertPatch
	{
		[HarmonyPostfix]
		public static void Postfix(PlayerCosmetics __instance, bool _forced)
		{
			if (!CosmeticReassertGuard.IsInside && !_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)
				{
					Driver.Instance?.ReassertLocalCosmeticsImmediate();
				}
			}
		}
	}
	[HarmonyPatch(typeof(StatsManager), "Start")]
	internal static class StatsManagerStartPatch
	{
		private static GameObject? _services;

		[HarmonyPostfix]
		public static void Postfix()
		{
			//IL_0017: Unknown result type (might be due to invalid IL or missing references)
			//IL_0021: Expected O, but got Unknown
			if (!((Object)(object)_services != (Object)null))
			{
				_services = new GameObject("BattleScars_Services");
				_services.AddComponent<Driver>();
				_services.AddComponent<ScreenOverlay>();
				_services.AddComponent<NetEvents>();
				Object.DontDestroyOnLoad((Object)(object)_services);
			}
		}
	}
}
namespace BattleScars.Configuration
{
	public enum RunMode
	{
		Off,
		VisualOnly,
		Full
	}
	internal static class PluginConfig
	{
		public const int RustyAtOrBelowHP = 75;

		public const int BandagesAtOrBelowHP = 60;

		public const int DamagedAtOrBelowHP = 50;

		public const int WreckedFaceAtOrBelowHP = 25;

		public const int CosmeticStepHP = 8;

		public const float SpeedNerfMax = 0.7f;

		public const float StaminaNerfMax = 0.45f;

		public const float ScreenOverlayMaxAlpha = 0.2f;

		public const string RustyAllowList = "Rusty";

		public const string BandagesAllowList = "Bandages";

		public const string DamagedAllowList = "Damaged,Cracks";

		public const string WreckedFaceCosmetic = "Broken";

		public static ConfigEntry<RunMode> Mode;

		public static ConfigEntry<bool> EnableCosmetics;

		public static ConfigEntry<bool> EnableSpeedNerf;

		public static ConfigEntry<bool> EnableStaminaNerf;

		public static ConfigEntry<bool> EnableVoiceEffects;

		public static ConfigEntry<bool> EnableSparkParticles;

		public static ConfigEntry<bool> EnableScreenOverlay;

		public static ConfigEntry<int> TestHealth;

		public static void Init(ConfigFile config)
		{
			//IL_00dc: Unknown result type (might be due to invalid IL or missing references)
			//IL_00e6: Expected O, but got Unknown
			Mode = config.Bind<RunMode>("General", "Mode", RunMode.VisualOnly, "Off: mod inactive. VisualOnly: scars + screen overlay + sparks, no nerfs. Full: everything per the Effects toggles below.");
			EnableCosmetics = config.Bind<bool>("Effects", "ForceBrokenCosmetics", true, "Force-apply broken cosmetics as damage stacks. Nothing is unlocked or saved.");
			EnableSpeedNerf = config.Bind<bool>("Effects", "SlowWhenHurt", true, "Reduce move and sprint speed as you take damage. Ignored in VisualOnly.");
			EnableStaminaNerf = config.Bind<bool>("Effects", "DrainStamina", true, "Cap max stamina as you take damage. Ignored in VisualOnly.");
			EnableVoiceEffects = config.Bind<bool>("Effects", "BreakVoice", false, "Pitch wobble and distortion on your voice. Currently not working reliably; default off until fixed.");
			EnableSparkParticles = config.Bind<bool>("Effects", "SpawnSparks", true, "Spark particles on hit. Other players need the mod to see them.");
			EnableScreenOverlay = config.Bind<bool>("Effects", "ScreenOverlay", true, "Red damage vignette on your own screen as you take damage.");
			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>()));
		}
	}
}