Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of BattleScars v1.0.0
BattleScars.dll
Decompiled 2 days agousing 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>())); } } }