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 RareBrokenMeshes = new List(); 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? _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 cosmeticAssets = MetaManager.instance.cosmeticAssets; Dictionary dictionary = new Dictionary(); 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 assets, Dictionary 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 BuildPool(IList assets, string allowList) { List list = ParseList(allowList); List list2 = new List(); 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 ParseList(string raw) { List list = new List(); 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 ForcedSetForHealth(string steamID, int currentHP, bool playerHasHat) { DiscoverIfNeeded(); List list = new List(); if (_regions == null || _regions.Count == 0) { return list; } int num = ConfigService.ScarSlotCount(currentHP); if (num <= 0) { return list; } bool flag = ConfigService.BrokenHeadActive(currentHP); List 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 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 list = MetaManager.instance?.cosmeticAssets; List 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 ByType(IList assets, IEnumerable indices) { //IL_0053: Unknown result type (might be due to invalid IL or missing references) Dictionary dictionary = new Dictionary(); 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 indices) { List list = MetaManager.instance?.cosmeticAssets; if (list == null) { return "?"; } Dictionary dictionary = ByType(list, indices); if (dictionary.Count == 0) { return "(none)"; } List list2 = dictionary.Select((KeyValuePair kv) => $"{kv.Key}={kv.Value}").ToList(); list2.Sort(StringComparer.Ordinal); return string.Join(" ", list2); } public static string DescribeDiff(IEnumerable before, IEnumerable 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 list = MetaManager.instance?.cosmeticAssets; if (list == null) { return "?"; } Dictionary dictionary = ByType(list, before); Dictionary dictionary2 = ByType(list, after); List list2 = new List(); 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 Merge(IList? assets, IList ownList, IList 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 list = new List(ownList.Count + forced.Count); HashSet hashSet = new HashSet(); 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 forced) { if (!((Object)(object)avatar == (Object)null) && !((Object)(object)avatar.playerCosmetics == (Object)null) && (avatar.photonView.IsMine || !SemiFunc.IsMultiplayer())) { List assets = MetaManager.instance?.cosmeticAssets; List ownList = avatar.playerCosmetics.cosmeticEquippedRaw ?? new List(); List 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)null); avatar.playerCosmetics.SetupColors(SemiFunc.IsMultiplayer(), (int[])null); ApplyToDeathHead(avatar, null); List 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)null); playerAvatarVisuals.playerCosmetics.SetupColors(false, (int[])null); } } } private static void ApplyToDeathHead(PlayerAvatar avatar, List? 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? 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()); 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> _applied = new Dictionary>(); 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 list = Cosmetics.ForcedSetForHealth(local.steamID, num, Cosmetics.PlayerWearsHat(local)); if (_applied.TryGetValue(local.steamID, out List value) && SameSet(value, list)) { ConfigService.LogDiag($"tick hp={num} dead={dead} disabled={disabled} scars={list.Count} -> skip (no change)"); return; } List before = value ?? new List(); _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 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 a, List 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 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 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 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(); _services.AddComponent(); 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 Mode = null; public static ConfigEntry Intensity = null; public static ConfigEntry Vignette = null; public static ConfigEntry VignetteIntensity = null; public static ConfigEntry CameraGlitches = null; public static ConfigEntry TestHealth = null; public static ConfigEntry 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("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("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("Effects", "Vignette", true, "Red edge vignette that ramps up at low HP. Off hides it entirely; on uses VignetteIntensity for strength."); VignetteIntensity = config.Bind("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(0f, 1f), Array.Empty())); CameraGlitches = config.Bind("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("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(-1, 100), Array.Empty())); DebugLogging = config.Bind("Testing", "DebugLogging", false, "Log every scar apply, restore and reassert to the BepInEx console. For bug reports; leave off otherwise."); } } }