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.2.4
BattleScars.dll
Decompiled 2 weeks 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.Versioning; using System.Security; using System.Security.Permissions; using BattleScars.Configuration; using BattleScars.Services; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Photon.Pun; using Photon.Realtime; using UnityEngine; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: IgnoresAccessChecksTo("Assembly-CSharp")] [assembly: AssemblyCompany("Vippy")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.2.4.0")] [assembly: AssemblyInformationalVersion("1.2.4+46c90de84a55dfbb5eeebd73b246dc73f84563a4")] [assembly: AssemblyProduct("BattleScars")] [assembly: AssemblyTitle("BattleScars")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.2.4.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace BattleScars { [BepInPlugin("Vippy.BattleScars", "BattleScars", "1.2.4")] public class BattleScars : BaseUnityPlugin { private Harmony? _harmony; internal static BattleScars Instance { get; private set; } internal static ManualLogSource Log => ((BaseUnityPlugin)Instance).Logger; private void Awake() { //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Expected O, but got Unknown Instance = this; ((Component)this).gameObject.transform.parent = null; ((Object)((Component)this).gameObject).hideFlags = (HideFlags)61; PluginConfig.Init(((BaseUnityPlugin)this).Config); BindModeRevert(); _harmony = new Harmony(((BaseUnityPlugin)this).Info.Metadata.GUID); _harmony.PatchAll(); Log.LogInfo((object)$"{((BaseUnityPlugin)this).Info.Metadata.GUID} v{((BaseUnityPlugin)this).Info.Metadata.Version} loaded."); } private static void BindModeRevert() { PluginConfig.Mode.SettingChanged += delegate { if (PluginConfig.Mode.Value == RunMode.Off) { PlayerAvatar val = PlayerLookup.LocalAvatar(); if (!((Object)(object)val == (Object)null)) { Cosmetics.RestoreToLocal(val); Cosmetics.RefreshExpressionPreview(); Driver.Instance?.InvalidateAppliedCosmetics(); } } }; } } internal static class BuildInfo { public const string Version = "1.2.4"; } } namespace BattleScars.Services { public static class ConfigService { public static bool IsEnabled() { return PluginConfig.Mode.Value != RunMode.Off; } public static bool IsVisualOnly() { return PluginConfig.Mode.Value == RunMode.VisualOnly; } public static bool VignetteEnabled() { if (IsEnabled() && PluginConfig.Vignette.Value) { return PluginConfig.VignetteIntensity.Value > 0f; } return false; } public static bool CameraGlitchesEnabled() { if (IsEnabled()) { return PluginConfig.CameraGlitches.Value; } return false; } public static bool NerfsEnabled() { if (IsEnabled()) { return !IsVisualOnly(); } return false; } public static bool PhotosensitivityOn() { if ((Object)(object)GameplayManager.instance != (Object)null) { return GameplayManager.instance.photosensitivity; } return false; } public static bool InActiveScene() { RunManager instance = RunManager.instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance.levelCurrent == (Object)null) { return false; } if (!SemiFunc.RunIsLevel() && !SemiFunc.RunIsShop()) { return SemiFunc.RunIsLobby(); } return true; } public static void LogDiag(string msg) { if (PluginConfig.DebugLogging.Value) { BattleScars.Log.LogInfo((object)("[Scars] " + msg)); } } public static int DamageDepth(int currentHP) { return Mathf.Max(0, PluginConfig.Curve.FirstScarHP - currentHP); } public static int ScarSlotCount(int currentHP) { ScarCurve curve = PluginConfig.Curve; if (currentHP > curve.FirstScarHP) { return 0; } return DamageDepth(currentHP) / curve.SlotStepHP + 1; } public static ScarSeverity SeverityForSlot(int currentHP, int slotIndex) { if (currentHP <= 0) { return ScarSeverity.Broken; } ScarCurve curve = PluginConfig.Curve; return (ScarSeverity)Mathf.Clamp((DamageDepth(currentHP) - slotIndex * curve.SlotStaggerHP) / curve.SeverityStepHP, 0, 3); } public static bool BrokenHeadActive(int currentHP) { return currentHP <= PluginConfig.Curve.BrokenHeadHP; } public static Tier TierForHealth(int currentHP) { int num = ScarSlotCount(currentHP); if (num <= 0) { return Tier.Healthy; } if (num <= 2) { return Tier.Scratched; } if (num <= 4) { return Tier.Damaged; } if (num == 5) { return Tier.Battered; } return Tier.Wrecked; } public static float SpeedMultiplierFor(Tier tier) { return Mathf.Lerp(1f, 0.7f, (float)tier / 4f); } public static float StaminaMultiplierFor(Tier tier) { return Mathf.Lerp(1f, 0.45f, (float)tier / 4f); } } public enum ScarSeverity { Cracks, Bandages, Damaged, Broken } public static class Cosmetics { private sealed class Region { public string Key = ""; public int Bandage = -1; public int CrackOverlay = -1; public int DamagedOverlay = -1; public int BrokenMesh = -1; public readonly List<int> RareBrokenMeshes = new List<int>(); public bool IsHead; public bool IsHeadTop => Key == "HeadTop"; } private const int MaxBandagedLimbs = 3; private static readonly string[] RareBrokenTokens = new string[1] { "cords" }; private const double RareBrokenMeshChance = 0.25; private static List<Region>? _regions; private static bool _discoveryRan; private static int _roundSeed; public static void RerollRoundSeed() { _roundSeed = Guid.NewGuid().GetHashCode(); } public static void DiscoverIfNeeded() { if (_discoveryRan || (Object)(object)MetaManager.instance == (Object)null || MetaManager.instance.cosmeticAssets == null) { return; } List<CosmeticAsset> cosmeticAssets = MetaManager.instance.cosmeticAssets; Dictionary<string, Region> dictionary = new Dictionary<string, Region>(); foreach (int item in BuildPool(cosmeticAssets, "Bandages")) { AssignLayer(cosmeticAssets, dictionary, item, ScarSeverity.Bandages); } foreach (int item2 in BuildPool(cosmeticAssets, "Cracks")) { AssignLayer(cosmeticAssets, dictionary, item2, ScarSeverity.Cracks); } foreach (int item3 in BuildPool(cosmeticAssets, "Damaged")) { AssignLayer(cosmeticAssets, dictionary, item3, ScarSeverity.Damaged); } foreach (int item4 in BuildPool(cosmeticAssets, "Broken")) { AssignLayer(cosmeticAssets, dictionary, item4, ScarSeverity.Broken); } _regions = dictionary.Values.ToList(); _regions.Sort((Region a, Region b) => string.CompareOrdinal(a.Key, b.Key)); _discoveryRan = true; BattleScars.Log.LogInfo((object)$"[Cosmetics] scar regions={_regions.Count}"); foreach (Region region in _regions) { ConfigService.LogDiag($"[Cosmetics] {region.Key}: bandage={region.Bandage >= 0} " + $"crack={region.CrackOverlay >= 0} damaged={region.DamagedOverlay >= 0} " + $"broken={region.BrokenMesh >= 0} rareBroken={region.RareBrokenMeshes.Count}"); } if (_regions.Count == 0) { BattleScars.Log.LogWarning((object)"[Cosmetics] no scar cosmetics matched, cosmetic effects disabled"); } } private static void AssignLayer(IList<CosmeticAsset> assets, Dictionary<string, Region> byKey, int idx, ScarSeverity layer) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_00d5: Unknown result type (might be due to invalid IL or missing references) if (idx < 0 || idx >= assets.Count) { return; } CosmeticAsset val = assets[idx]; if ((Object)(object)val == (Object)null) { return; } string text = RegionKey(val.type); if (text == null) { return; } if (!byKey.TryGetValue(text, out Region value)) { value = (byKey[text] = new Region { Key = text }); } switch (layer) { case ScarSeverity.Bandages: if (value.Bandage < 0) { value.Bandage = idx; } break; case ScarSeverity.Cracks: if (value.CrackOverlay < 0) { value.CrackOverlay = idx; } break; case ScarSeverity.Damaged: if (value.DamagedOverlay < 0) { value.DamagedOverlay = idx; } break; case ScarSeverity.Broken: if (IsRareBroken(val)) { value.RareBrokenMeshes.Add(idx); if (IsHeadMesh(val.type)) { value.IsHead = true; } } else if (value.BrokenMesh < 0) { value.BrokenMesh = idx; value.IsHead = IsHeadMesh(val.type); } break; } } private static string? RegionKey(CosmeticType type) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Expected I4, but got Unknown switch ((int)type) { case 2: case 10: case 27: return "ArmLeft"; case 1: case 9: case 26: return "ArmRight"; case 4: case 12: case 29: return "LegLeft"; case 3: case 11: case 28: return "LegRight"; case 7: case 16: case 20: return "BodyTop"; case 8: case 21: case 23: return "BodyBottom"; case 0: case 5: case 24: return "HeadTop"; case 6: case 25: case 30: return "HeadBottom"; default: return null; } } private static bool IsHeadMesh(CosmeticType type) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Invalid comparison between Unknown and I4 //IL_0004: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Invalid comparison between Unknown and I4 //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Invalid comparison between Unknown and I4 //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Invalid comparison between Unknown and I4 if ((int)type != 5 && (int)type != 6 && (int)type != 14) { return (int)type == 15; } return true; } private static bool IsRareBroken(CosmeticAsset asset) { string text = (((Object)asset).name ?? string.Empty).ToLowerInvariant(); string text2 = (asset.assetName ?? string.Empty).ToLowerInvariant(); string[] rareBrokenTokens = RareBrokenTokens; foreach (string value in rareBrokenTokens) { if (text.Contains(value) || text2.Contains(value)) { return true; } } return false; } private static int PickBrokenMesh(Region region, string steamID) { if (region.RareBrokenMeshes.Count == 0) { return region.BrokenMesh; } Random random = new Random((string.IsNullOrEmpty(steamID) ? 1 : steamID.GetHashCode()) * 1009 + _roundSeed * 31 + region.Key.GetHashCode()); foreach (int rareBrokenMesh in region.RareBrokenMeshes) { if (random.NextDouble() < 0.25) { return rareBrokenMesh; } } return region.BrokenMesh; } private static List<int> BuildPool(IList<CosmeticAsset> assets, string allowList) { List<string> list = ParseList(allowList); List<int> list2 = new List<int>(); if (list.Count == 0) { return list2; } for (int i = 0; i < assets.Count; i++) { CosmeticAsset val = assets[i]; if (!((Object)(object)val == (Object)null)) { string a = (((Object)val).name ?? string.Empty).ToLowerInvariant(); string b = (val.assetName ?? string.Empty).ToLowerInvariant(); if (list.Any((string t) => a.Contains(t) || b.Contains(t))) { list2.Add(i); } } } return list2; } private static List<string> ParseList(string raw) { List<string> list = new List<string>(); if (string.IsNullOrWhiteSpace(raw)) { return list; } string[] array = raw.Split(','); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim().ToLowerInvariant(); if (text.Length > 0) { list.Add(text); } } return list; } public static List<int> ForcedSetForHealth(string steamID, int currentHP, bool playerHasHat) { DiscoverIfNeeded(); List<int> list = new List<int>(); if (_regions == null || _regions.Count == 0) { return list; } int num = ConfigService.ScarSlotCount(currentHP); if (num <= 0) { return list; } bool flag = ConfigService.BrokenHeadActive(currentHP); List<Region> list2 = OrderedRegions(steamID); int num2 = Math.Min(num, list2.Count); int num3 = 0; for (int i = 0; i < num2; i++) { Region region = list2[i]; ScarSeverity num4 = ConfigService.SeverityForSlot(currentHP, i); int num5 = ((num4 >= ScarSeverity.Damaged && region.DamagedOverlay >= 0) ? region.DamagedOverlay : region.CrackOverlay); if (num5 >= 0) { list.Add(num5); } if (num4 >= ScarSeverity.Bandages && region.Bandage >= 0 && num3 < 3 && !(region.IsHeadTop && playerHasHat)) { list.Add(region.Bandage); num3++; } int num6 = PickBrokenMesh(region, steamID); if (num4 >= ScarSeverity.Broken && num6 >= 0 && (!region.IsHead || flag)) { list.Add(num6); } } return list; } private static List<Region> OrderedRegions(string steamID) { int num = (string.IsNullOrEmpty(steamID) ? 1 : steamID.GetHashCode()); Random rng = new Random(num * 31 + _roundSeed); return _regions.OrderBy((Region _) => rng.Next()).ToList(); } public static bool PlayerWearsHat(PlayerAvatar avatar) { //IL_0073: Unknown result type (might be due to invalid IL or missing references) List<CosmeticAsset> list = MetaManager.instance?.cosmeticAssets; List<int> list2 = (((Object)(object)avatar != (Object)null && (Object)(object)avatar.playerCosmetics != (Object)null) ? avatar.playerCosmetics.cosmeticEquippedRaw : null); if (list == null || list2 == null) { return false; } foreach (int item in list2) { if (item >= 0 && item < list.Count) { CosmeticAsset val = list[item]; if ((Object)(object)val != (Object)null && (int)val.type == 0) { return true; } } } return false; } private static Dictionary<CosmeticType, string> ByType(IList<CosmeticAsset> assets, IEnumerable<int> indices) { //IL_0053: Unknown result type (might be due to invalid IL or missing references) Dictionary<CosmeticType, string> dictionary = new Dictionary<CosmeticType, string>(); foreach (int index in indices) { if (index >= 0 && index < assets.Count) { CosmeticAsset val = assets[index]; if (!((Object)(object)val == (Object)null)) { string arg = ((!string.IsNullOrEmpty(val.assetName)) ? val.assetName : ((Object)val).name); dictionary[val.type] = $"{arg}#{index}"; } } } return dictionary; } public static string Describe(IEnumerable<int> indices) { List<CosmeticAsset> list = MetaManager.instance?.cosmeticAssets; if (list == null) { return "?"; } Dictionary<CosmeticType, string> dictionary = ByType(list, indices); if (dictionary.Count == 0) { return "(none)"; } List<string> list2 = dictionary.Select((KeyValuePair<CosmeticType, string> kv) => $"{kv.Key}={kv.Value}").ToList(); list2.Sort(StringComparer.Ordinal); return string.Join(" ", list2); } public static string DescribeDiff(IEnumerable<int> before, IEnumerable<int> after) { //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0082: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) List<CosmeticAsset> list = MetaManager.instance?.cosmeticAssets; if (list == null) { return "?"; } Dictionary<CosmeticType, string> dictionary = ByType(list, before); Dictionary<CosmeticType, string> dictionary2 = ByType(list, after); List<string> list2 = new List<string>(); foreach (CosmeticType item in dictionary2.Keys.Union(dictionary.Keys)) { dictionary.TryGetValue(item, out var value); dictionary2.TryGetValue(item, out var value2); if (!(value == value2)) { if (value == null) { list2.Add($"{item} +{value2}"); } else if (value2 == null) { list2.Add($"{item} -{value}"); } else { list2.Add($"{item} {value}->{value2}"); } } } if (list2.Count == 0) { return "(no change)"; } list2.Sort(StringComparer.Ordinal); return string.Join(" ", list2); } public static List<int> Merge(IList<CosmeticAsset>? assets, IList<int> ownList, IList<int> forced) { //IL_004f: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Unknown result type (might be due to invalid IL or missing references) List<int> list = new List<int>(ownList.Count + forced.Count); HashSet<CosmeticType> hashSet = new HashSet<CosmeticType>(); if (assets != null) { foreach (int item in forced) { if (item >= 0 && item < assets.Count) { CosmeticAsset val = assets[item]; if ((Object)(object)val != (Object)null) { hashSet.Add(val.type); } } } } foreach (int item2 in forced) { if (!list.Contains(item2)) { list.Add(item2); } } foreach (int own in ownList) { if (list.Contains(own)) { continue; } if (assets != null && own >= 0 && own < assets.Count) { CosmeticAsset val2 = assets[own]; if ((Object)(object)val2 != (Object)null && hashSet.Contains(val2.type)) { continue; } } list.Add(own); } return list; } public static void Apply(PlayerAvatar avatar, IList<int> forced) { if (!((Object)(object)avatar == (Object)null) && !((Object)(object)avatar.playerCosmetics == (Object)null) && (avatar.photonView.IsMine || !SemiFunc.IsMultiplayer())) { List<CosmeticAsset> assets = MetaManager.instance?.cosmeticAssets; List<int> ownList = avatar.playerCosmetics.cosmeticEquippedRaw ?? new List<int>(); List<int> list = Merge(assets, ownList, forced); ConfigService.LogDiag($"apply steam={avatar.steamID} forced={{ {Describe(forced)} }} (combined {list.Count} total)"); avatar.playerCosmetics.SetupCosmetics(SemiFunc.IsMultiplayer(), true, list); avatar.playerCosmetics.SetupColors(SemiFunc.IsMultiplayer(), (int[])null); ApplyToDeathHead(avatar, list); SyncToMaster(avatar.playerCosmetics, list); PlayerDeathHead playerDeathHead = avatar.playerDeathHead; if ((Object)(object)playerDeathHead != (Object)null) { SyncToMaster(playerDeathHead.playerCosmetics, list); } } } public static void RestoreToLocal(PlayerAvatar avatar) { if (!((Object)(object)avatar == (Object)null) && !((Object)(object)avatar.playerCosmetics == (Object)null) && (avatar.photonView.IsMine || !SemiFunc.IsMultiplayer())) { ConfigService.LogDiag("restore steam=" + avatar.steamID + " (back to the saved loadout)"); avatar.playerCosmetics.SetupCosmetics(SemiFunc.IsMultiplayer(), true, (List<int>)null); avatar.playerCosmetics.SetupColors(SemiFunc.IsMultiplayer(), (int[])null); ApplyToDeathHead(avatar, null); List<int> equipped = MetaManager.instance?.cosmeticEquipped; SyncToMaster(avatar.playerCosmetics, equipped); PlayerDeathHead playerDeathHead = avatar.playerDeathHead; if ((Object)(object)playerDeathHead != (Object)null) { SyncToMaster(playerDeathHead.playerCosmetics, equipped); } } } public static void RefreshExpressionPreview() { PlayerExpressionsUI instance = PlayerExpressionsUI.instance; if (!((Object)(object)instance == (Object)null)) { PlayerAvatarVisuals playerAvatarVisuals = instance.playerAvatarVisuals; if (!((Object)(object)playerAvatarVisuals == (Object)null) && !((Object)(object)playerAvatarVisuals.playerCosmetics == (Object)null)) { playerAvatarVisuals.playerCosmetics.SetupCosmetics(false, false, (List<int>)null); playerAvatarVisuals.playerCosmetics.SetupColors(false, (int[])null); } } } private static void ApplyToDeathHead(PlayerAvatar avatar, List<int>? combined) { PlayerDeathHead playerDeathHead = avatar.playerDeathHead; PlayerCosmetics val = (((Object)(object)playerDeathHead != (Object)null) ? playerDeathHead.playerCosmetics : null); if (!((Object)(object)playerDeathHead == (Object)null) && !((Object)(object)val == (Object)null)) { val.SetupCosmetics(SemiFunc.IsMultiplayer(), true, combined); val.SetupColors(SemiFunc.IsMultiplayer(), (int[])null); } } private static void SyncToMaster(PlayerCosmetics? cosmetics, IList<int>? equipped) { if (!SemiFunc.IsMultiplayer() || (Object)(object)cosmetics == (Object)null || (Object)(object)cosmetics.photonView == (Object)null) { return; } Player masterClient = PhotonNetwork.MasterClient; if (masterClient != null && !masterClient.IsLocal) { int[] array = ((equipped != null) ? equipped.ToArray() : Array.Empty<int>()); cosmetics.photonView.RPC("SetupCosmeticsRPC", masterClient, new object[2] { array, true }); int[] colorsEquipped = cosmetics.colorsEquipped; if (colorsEquipped != null) { cosmetics.photonView.RPC("SetupColorsRPC", masterClient, new object[1] { colorsEquipped }); } } } } public class Driver : MonoBehaviour { private const float SlowTickInterval = 0.2f; private float _slowTickTimer; private bool _wasActive; private bool _wasFullHealth = true; private readonly Dictionary<string, List<int>> _applied = new Dictionary<string, List<int>>(); public static Driver? Instance { get; private set; } private void Awake() { Instance = this; Cosmetics.DiscoverIfNeeded(); Cosmetics.RerollRoundSeed(); } private void Update() { HandleDevHotkeys(); if ((Object)(object)StatsManager.instance == (Object)null || !ConfigService.InActiveScene()) { if (_wasActive) { ConfigService.LogDiag("left the active scene, tearing down"); TeardownLocal(); _wasActive = false; } return; } if (!_wasActive) { RunManager instance = RunManager.instance; object obj; if (instance == null) { obj = null; } else { Level levelCurrent = instance.levelCurrent; obj = ((levelCurrent != null) ? ((Object)levelCurrent).name : null); } ConfigService.LogDiag("entered active scene level=" + (string?)obj); } _wasActive = true; PlayerAvatar val = PlayerLookup.LocalAvatar(); bool num = ConfigService.IsEnabled(); bool flag = (Object)(object)val != (Object)null && val.deadSet; bool flag2 = (Object)(object)val != (Object)null && val.isDisabled; RerollSeedWhenFullHealth(val); Tier tier = Tier.Healthy; if (num && !flag && !flag2 && (Object)(object)val != (Object)null) { tier = ConfigService.TierForHealth(EffectiveHealthFor(val)); } if ((Object)(object)val != (Object)null && !flag) { Effects.ApplySpeedTick(val, tier); Effects.ApplyStaminaTick(val, tier); } _slowTickTimer -= Time.deltaTime; if (!(_slowTickTimer > 0f)) { _slowTickTimer = 0.2f; SlowTick(val, flag, flag2); } } private void HandleDevHotkeys() { int? num = null; if (Input.GetKeyDown((KeyCode)256)) { num = -1; } else if (Input.GetKeyDown((KeyCode)257)) { num = 1; } else if (Input.GetKeyDown((KeyCode)258)) { num = 20; } else if (Input.GetKeyDown((KeyCode)259)) { num = 30; } else if (Input.GetKeyDown((KeyCode)260)) { num = 40; } else if (Input.GetKeyDown((KeyCode)261)) { num = 50; } else if (Input.GetKeyDown((KeyCode)262)) { num = 60; } else if (Input.GetKeyDown((KeyCode)263)) { num = 70; } else if (Input.GetKeyDown((KeyCode)264)) { num = 80; } else if (Input.GetKeyDown((KeyCode)265)) { num = 90; } if (num.HasValue && PluginConfig.TestHealth.Value != num.Value) { PluginConfig.TestHealth.Value = num.Value; BattleScars.Log.LogInfo((object)("[Dev] TestHealth -> " + ((num.Value < 0) ? "off" : num.Value.ToString()))); InvalidateAppliedCosmetics(); } } private void RerollSeedWhenFullHealth(PlayerAvatar? local) { bool flag = (Object)(object)local != (Object)null && (Object)(object)local.playerHealth != (Object)null && EffectiveHealthFor(local) >= local.playerHealth.maxHealth; if (flag && !_wasFullHealth) { Cosmetics.RerollRoundSeed(); } _wasFullHealth = flag; } public static int EffectiveHealthFor(PlayerAvatar avatar) { int value = PluginConfig.TestHealth.Value; if (value >= 0) { return value; } if (!((Object)(object)avatar.playerHealth != (Object)null)) { return 0; } return avatar.playerHealth.health; } private void SlowTick(PlayerAvatar? local, bool dead, bool disabled) { SaveBackup.TryBackupOnce(local); if ((Object)(object)local == (Object)null || string.IsNullOrEmpty(local.steamID) || !ConfigService.IsEnabled()) { return; } int num = ((!dead) ? EffectiveHealthFor(local) : 0); List<int> list = Cosmetics.ForcedSetForHealth(local.steamID, num, Cosmetics.PlayerWearsHat(local)); if (_applied.TryGetValue(local.steamID, out List<int> value) && SameSet(value, list)) { ConfigService.LogDiag($"tick hp={num} dead={dead} disabled={disabled} scars={list.Count} -> skip (no change)"); return; } List<int> before = value ?? new List<int>(); _applied[local.steamID] = list; ConfigService.LogDiag(string.Format("tick hp={0} dead={1} disabled={2} scars={3} -> {4}", num, dead, disabled, list.Count, (list.Count == 0) ? "restore" : "apply")); ConfigService.LogDiag(" diff: " + Cosmetics.DescribeDiff(before, list)); ConfigService.LogDiag(" set: " + Cosmetics.Describe(list)); if (list.Count == 0) { Cosmetics.RestoreToLocal(local); } else { Cosmetics.Apply(local, list); } Cosmetics.RefreshExpressionPreview(); } public void InvalidateAppliedCosmetics() { _applied.Clear(); } private void TeardownLocal() { PlayerAvatar val = PlayerLookup.LocalAvatar(); ConfigService.LogDiag("teardown localAvatar=" + (((Object)(object)val != (Object)null) ? "found" : "null")); if ((Object)(object)val != (Object)null) { Cosmetics.RestoreToLocal(val); } _applied.Clear(); } public void ReassertLocalCosmeticsImmediate() { PlayerAvatar val = PlayerLookup.LocalAvatar(); if ((Object)(object)val == (Object)null || string.IsNullOrEmpty(val.steamID)) { return; } if (!ConfigService.IsEnabled()) { ConfigService.LogDiag("reassert skipped: mod disabled"); return; } if (!ConfigService.InActiveScene()) { ConfigService.LogDiag("reassert skipped: not in an active scene"); return; } int currentHP = ((!val.deadSet) ? EffectiveHealthFor(val) : 0); List<int> list = Cosmetics.ForcedSetForHealth(val.steamID, currentHP, Cosmetics.PlayerWearsHat(val)); if (list.Count == 0) { ConfigService.LogDiag("reassert: nothing to re-apply at this HP"); return; } _applied[val.steamID] = list; ConfigService.LogDiag("reassert -> apply " + Cosmetics.Describe(list)); Cosmetics.Apply(val, list); Cosmetics.RefreshExpressionPreview(); } private static bool SameSet(List<int> a, List<int> b) { if (a.Count != b.Count) { return false; } for (int i = 0; i < a.Count; i++) { if (a[i] != b[i]) { return false; } } return true; } } public static class Effects { public static void ApplySpeedTick(PlayerAvatar avatar, Tier tier) { if (ConfigService.NerfsEnabled() && tier != 0 && !((Object)(object)avatar == (Object)null) && avatar.isLocal) { PlayerController instance = PlayerController.instance; if (!((Object)(object)instance == (Object)null)) { instance.OverrideSpeed(ConfigService.SpeedMultiplierFor(tier), 0.2f); } } } public static void ApplyStaminaTick(PlayerAvatar avatar, Tier tier) { if (!ConfigService.NerfsEnabled() || tier == Tier.Healthy || (Object)(object)avatar == (Object)null || !avatar.isLocal) { return; } PlayerController instance = PlayerController.instance; if (!((Object)(object)instance == (Object)null)) { float num = instance.EnergyStart * ConfigService.StaminaMultiplierFor(tier); if (instance.EnergyCurrent > num) { instance.EnergyCurrent = num; } } } } internal static class PlayerLookup { public static PlayerAvatar? LocalAvatar() { foreach (PlayerAvatar item in SemiFunc.PlayerGetAll()) { if ((Object)(object)item != (Object)null && item.isLocal) { return item; } } return null; } } public static class SaveBackup { private const int BackupsToKeep = 5; private static bool _ranThisSession; public static void TryBackupOnce(PlayerAvatar? avatar) { if (_ranThisSession || (Object)(object)avatar == (Object)null || string.IsNullOrWhiteSpace(avatar.playerName)) { return; } try { string text = Path.Combine(Application.persistentDataPath, "MetaSave.es3"); if (!File.Exists(text)) { _ranThisSession = true; return; } string text2 = Path.Combine(Paths.ConfigPath, "BattleScars", "backups", Sanitize(avatar.playerName)); Directory.CreateDirectory(text2); string stamp = DateTime.Now.ToString("yyyy-MM-dd_HHmmss"); string text3 = UniqueDestination(text2, stamp); File.Copy(text, text3, overwrite: false); BattleScars.Log.LogInfo((object)("[Backup] saved " + Path.GetFileName(text3))); Prune(text2); _ranThisSession = true; } catch (Exception ex) { BattleScars.Log.LogWarning((object)("[Backup] failed: " + ex.Message)); _ranThisSession = true; } } private static string Sanitize(string raw) { char[] invalid = Path.GetInvalidFileNameChars(); string text = new string(raw.Select((char c) => (!invalid.Contains(c)) ? c : '_').ToArray()).Trim(); if (text.Length != 0) { return text; } return "unknown_player"; } private static string UniqueDestination(string dir, string stamp) { string text = Path.Combine(dir, stamp + "_MetaSave.es3"); if (!File.Exists(text)) { return text; } for (int i = 1; i < 1000; i++) { string text2 = Path.Combine(dir, $"{stamp}_{i:D2}_MetaSave.es3"); if (!File.Exists(text2)) { return text2; } } return Path.Combine(dir, $"{stamp}_{Guid.NewGuid():N}_MetaSave.es3"); } private static void Prune(string dir) { try { List<string> list = Directory.GetFiles(dir, "*_MetaSave.es3").OrderByDescending(File.GetCreationTimeUtc).ToList(); for (int i = 5; i < list.Count; i++) { File.Delete(list[i]); } } catch { } } } public class ScreenOverlay : MonoBehaviour { private const float VignetteInner = 0.48f; private const float VignetteOuter = 1.5f; private const float OverlaySmoothTime = 0.35f; private const float OverlayEpsilon = 0.002f; private float _glitchTimer; private float _overlayT; private float _overlayVel; private float _pulsePhase; private Texture2D? _vignette; private void Awake() { _vignette = BuildVignette(); } private void OnDestroy() { if ((Object)(object)_vignette != (Object)null) { Object.Destroy((Object)(object)_vignette); } } private void Update() { _overlayT = Mathf.SmoothDamp(_overlayT, OverlayTarget(), ref _overlayVel, 0.35f); if (_overlayT < 0.002f && _overlayVel <= 0f) { _overlayT = 0f; } _pulsePhase += Time.deltaTime * (3.5f + _overlayT * 4f); UpdateGlitch(); } private void UpdateGlitch() { if (!ConfigService.CameraGlitchesEnabled() || !ConfigService.InActiveScene()) { return; } PlayerAvatar val = PlayerLookup.LocalAvatar(); if (!((Object)(object)val == (Object)null) && !val.deadSet && !val.isDisabled && ConfigService.TierForHealth(Driver.EffectiveHealthFor(val)) >= Tier.Wrecked && !ConfigService.PhotosensitivityOn()) { _glitchTimer -= Time.deltaTime; if (!(_glitchTimer > 0f)) { _glitchTimer = GlitchInterval(val) * Random.Range(0.85f, 1.15f); FireGlitch(); } } } private static void FireGlitch() { CameraGlitch instance = CameraGlitch.Instance; if (!((Object)(object)instance == (Object)null)) { float value = Random.value; if (value < 0.15f) { instance.PlayLong(); } else if (value < 0.45f) { instance.PlayShort(); } else { instance.PlayTiny(); } } } private static float GlitchInterval(PlayerAvatar avatar) { if ((Object)(object)avatar.playerHealth == (Object)null) { return 12f; } int num = Mathf.Max(1, avatar.playerHealth.health); float num2 = Mathf.InverseLerp(1f, (float)PluginConfig.Curve.FirstScarHP, (float)num); return Mathf.Lerp(8f, 18f, num2); } private static float OverlayTarget() { if (!ConfigService.VignetteEnabled() || !ConfigService.InActiveScene()) { return 0f; } PlayerAvatar val = PlayerLookup.LocalAvatar(); if ((Object)(object)val == (Object)null) { return 0f; } return Mathf.InverseLerp((float)PluginConfig.Curve.FirstScarHP, 1f, (float)Driver.EffectiveHealthFor(val)); } private void OnGUI() { //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) if (ConfigService.VignetteEnabled() && !((Object)(object)_vignette == (Object)null) && !(_overlayT < 0.002f)) { float num = (ConfigService.PhotosensitivityOn() ? 0.9f : (0.82f + 0.18f * Mathf.Sin(_pulsePhase))); float num2 = PluginConfig.VignetteIntensity.Value * _overlayT * num; Color color = GUI.color; GUI.color = new Color(0.7f, 0.04f, 0.04f, num2); GUI.DrawTexture(new Rect(0f, 0f, (float)Screen.width, (float)Screen.height), (Texture)(object)_vignette, (ScaleMode)0); GUI.color = color; } } private static Texture2D BuildVignette() { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Expected O, but got Unknown //IL_009c: Unknown result type (might be due to invalid IL or missing references) //IL_00a1: Unknown result type (might be due to invalid IL or missing references) Texture2D val = new Texture2D(128, 128, (TextureFormat)4, false) { filterMode = (FilterMode)1, wrapMode = (TextureWrapMode)1 }; Color[] array = (Color[])(object)new Color[16384]; float num = 63.5f; for (int i = 0; i < 128; i++) { for (int j = 0; j < 128; j++) { float num2 = ((float)j - num) / num; float num3 = ((float)i - num) / num; float num4 = Mathf.Sqrt(num2 * num2 + num3 * num3); float num5 = Mathf.InverseLerp(0.48f, 1.5f, num4); array[i * 128 + j] = new Color(1f, 1f, 1f, num5 * num5 * (3f - 2f * num5)); } } val.SetPixels(array); val.Apply(); return val; } } public enum Tier { Healthy, Scratched, Damaged, Battered, Wrecked } } namespace BattleScars.Patches { [HarmonyPatch(typeof(PlayerCosmetics), "SetupCosmeticsLogic")] internal static class MenuAvatarCosmeticsPatch { [HarmonyPrefix] public static void Prefix(PlayerCosmetics __instance, ref int[] _cosmeticEquipped) { if (!ConfigService.IsEnabled() || !ConfigService.InActiveScene() || (Object)(object)__instance == (Object)null) { return; } PlayerAvatarVisuals playerAvatarVisuals = __instance.playerAvatarVisuals; if ((Object)(object)playerAvatarVisuals == (Object)null || !playerAvatarVisuals.isMenuAvatar) { return; } PlayerAvatarMenu playerAvatarMenu = playerAvatarVisuals.playerAvatarMenu; if ((Object)(object)playerAvatarMenu == (Object)null) { return; } bool num = (Object)(object)playerAvatarMenu == (Object)(object)PlayerAvatarMenu.instance; bool expressionAvatar = playerAvatarMenu.expressionAvatar; if (!num && !expressionAvatar) { return; } PlayerAvatar val = PlayerLookup.LocalAvatar(); if (!((Object)(object)val == (Object)null) && !string.IsNullOrEmpty(val.steamID)) { int num2 = Driver.EffectiveHealthFor(val); List<int> list = Cosmetics.ForcedSetForHealth(val.steamID, num2, Cosmetics.PlayerWearsHat(val)); ConfigService.LogDiag(string.Format("{0} preview hp={1} {2}", expressionAvatar ? "expression" : "menu", num2, Cosmetics.Describe(list))); if (list.Count != 0) { List<int> list2 = Cosmetics.Merge(MetaManager.instance?.cosmeticAssets, _cosmeticEquipped, list); _cosmeticEquipped = list2.ToArray(); } } } } [HarmonyPatch(typeof(PlayerAvatar), "PlayerDeathRPC")] internal static class PlayerDeathPatch { [HarmonyPostfix] public static void Postfix(PlayerAvatar __instance) { if (!((Object)(object)__instance == (Object)null) && __instance.isLocal) { ConfigService.LogDiag("local death: reasserting broken set"); Driver.Instance?.ReassertLocalCosmeticsImmediate(); } } } [HarmonyPatch(typeof(PlayerCosmetics), "SetupCosmetics")] internal static class SetupCosmeticsReassertPatch { [HarmonyPostfix] public static void Postfix(PlayerCosmetics __instance, bool _forced) { if (!_forced && !((Object)(object)__instance == (Object)null) && !((Object)(object)__instance.playerAvatarVisuals == (Object)null) && !__instance.playerAvatarVisuals.isMenuAvatar) { PlayerAvatar playerAvatar = __instance.playerAvatarVisuals.playerAvatar; if (!((Object)(object)playerAvatar == (Object)null) && playerAvatar.isLocal) { ConfigService.LogDiag("vanilla cosmetic refresh on the local body, reasserting"); Driver.Instance?.ReassertLocalCosmeticsImmediate(); } } } } [HarmonyPatch(typeof(StatsManager), "Start")] internal static class StatsManagerStartPatch { private static GameObject? _services; [HarmonyPostfix] public static void Postfix() { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_001d: Expected O, but got Unknown if (!((Object)(object)_services != (Object)null)) { _services = new GameObject("BattleScars_Services"); _services.AddComponent<Driver>(); _services.AddComponent<ScreenOverlay>(); Object.DontDestroyOnLoad((Object)(object)_services); } } } } namespace BattleScars.Configuration { public enum RunMode { Off, VisualOnly, Full } public enum ScarIntensity { Light, Normal, Heavy } public sealed class ScarCurve { public readonly int FirstScarHP; public readonly int SlotStepHP; public readonly int SeverityStepHP; public readonly int SlotStaggerHP; public readonly int BrokenHeadHP; public ScarCurve(int firstScarHP, int slotStepHP, int severityStepHP, int slotStaggerHP, int brokenHeadHP) { FirstScarHP = firstScarHP; SlotStepHP = slotStepHP; SeverityStepHP = severityStepHP; SlotStaggerHP = slotStaggerHP; BrokenHeadHP = brokenHeadHP; } } internal static class PluginConfig { private static readonly ScarCurve LightCurve = new ScarCurve(60, 11, 22, 9, 5); private static readonly ScarCurve NormalCurve = new ScarCurve(75, 8, 15, 7, 9); private static readonly ScarCurve HeavyCurve = new ScarCurve(95, 6, 11, 5, 14); public const float SpeedNerfMax = 0.7f; public const float StaminaNerfMax = 0.45f; public const string BandagesAllowList = "Bandages"; public const string CracksAllowList = "Cracks"; public const string DamagedAllowList = "Damaged"; public const string BrokenAllowList = "Broken"; public static ConfigEntry<RunMode> Mode = null; public static ConfigEntry<ScarIntensity> Intensity = null; public static ConfigEntry<bool> Vignette = null; public static ConfigEntry<float> VignetteIntensity = null; public static ConfigEntry<bool> CameraGlitches = null; public static ConfigEntry<int> TestHealth = null; public static ConfigEntry<bool> DebugLogging = null; public static ScarCurve Curve => Intensity.Value switch { ScarIntensity.Light => LightCurve, ScarIntensity.Heavy => HeavyCurve, _ => NormalCurve, }; public static void Init(ConfigFile config) { //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0084: Expected O, but got Unknown //IL_00c2: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Expected O, but got Unknown Mode = config.Bind<RunMode>("General", "Mode", RunMode.VisualOnly, "Off: mod inactive. VisualOnly: scars and the screen vignette, no nerfs. Full: adds the move-speed and stamina nerfs on top."); Intensity = config.Bind<ScarIntensity>("General", "ScarIntensity", ScarIntensity.Heavy, "How early and how hard scars build up. Light holds them off until you're badly hurt, Heavy starts them after the first few hits."); Vignette = config.Bind<bool>("Effects", "Vignette", true, "Red edge vignette that ramps up at low HP. Off hides it entirely; on uses VignetteIntensity for strength."); VignetteIntensity = config.Bind<float>("Effects", "VignetteIntensity", 0.25f, new ConfigDescription("How strong the red vignette gets at its worst, near death. Ramps up as HP drops. 0 fades it to nothing, 1 is intense.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 1f), Array.Empty<object>())); CameraGlitches = config.Bind<bool>("Effects", "CameraGlitches", false, "Screen flashes and camera faults that fire at very low HP. Off (default) keeps the camera steady regardless of damage. REPO's photosensitivity accessibility setting also forces this off."); TestHealth = config.Bind<int>("Testing", "TestHealth", -1, new ConfigDescription("Preview a synthetic HP value. -1 disables. 0-100 forces that HP through the tier pipeline without touching real health or networked state. Numpad 0-9 in-game also drives this (0=off, 1=HP 1, 2=HP 20, etc).", (AcceptableValueBase)(object)new AcceptableValueRange<int>(-1, 100), Array.Empty<object>())); DebugLogging = config.Bind<bool>("Testing", "DebugLogging", false, "Log every scar apply, restore and reassert to the BepInEx console. For bug reports; leave off otherwise."); } } }