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 DoubleDamageConfigurator v0.5.1
DoubleDamageConfigurator.dll
Decompiled 3 weeks agousing System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Threading; using AssetHelperLib.BundleTools; using AssetsTools.NET; using AssetsTools.NET.Extra; using BepInEx; using BepInEx.Logging; using GlobalEnums; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using Silksong.AssetHelper.Core; using Silksong.ModMenu.Elements; using Silksong.ModMenu.Models; using Silksong.ModMenu.Plugin; using Silksong.ModMenu.Screens; using UnityEngine; using UnityEngine.UI; [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: AssemblyCompany("DoubleDamageConfigurator")] [assembly: AssemblyConfiguration("Debug")] [assembly: AssemblyFileVersion("0.5.1.0")] [assembly: AssemblyInformationalVersion("0.5.1+29664d613fd7138c15b6b511b689b3e42b2ca243")] [assembly: AssemblyProduct("DoubleDamageConfigurator")] [assembly: AssemblyTitle("DoubleDamageConfigurator")] [assembly: AssemblyMetadata("RepositoryUrl", "https://github.com/json-commits/ss-double-damage-configurator")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.5.1.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace System.Runtime.CompilerServices { [CompilerGenerated] [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] [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] [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 BepInEx { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [Conditional("CodeGeneration")] [Embedded] internal sealed class BepInAutoPluginAttribute : Attribute { public BepInAutoPluginAttribute(string? id = null, string? name = null, string? version = null) { } } } namespace BepInEx.Preloader.Core.Patching { [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] [Conditional("CodeGeneration")] [Embedded] internal sealed class PatcherAutoPluginAttribute : Attribute { public PatcherAutoPluginAttribute(string? id = null, string? name = null, string? version = null) { } } } namespace Microsoft.CodeAnalysis { [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace DoubleDamageConfigurator { public static class DamageCap { public static bool ShouldCap(bool resolvedToCatalog, bool isOff) { return resolvedToCatalog && !isOff; } public static int Apply(int damageAmount, bool shouldCap) { if (!shouldCap || damageAmount <= 1) { return damageAmount; } return 1; } } [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInPlugin("io.github.change_me.doubledamageconfigurator", "DoubleDamageConfigurator", "0.5.1")] public class DoubleDamageConfiguratorPlugin : BaseUnityPlugin, IModMenuCustomMenu, IModMenuInterface { private readonly ConcurrentQueue<Action> _mainThread = new ConcurrentQueue<Action>(); private EnemyAssetScanner _scanner = null; private EnemySettings _settings = null; private EnemyCatalog _catalog = EnemyCatalog.Empty; private ScanUiState _scanUi; private AbstractMenuScreen? _openScreen; private Action? _refreshScanBanner; private string _catalogPath = string.Empty; private string _settingsPath = string.Empty; private string _gameVersion = string.Empty; public const string Id = "io.github.change_me.doubledamageconfigurator"; public static string Name => "DoubleDamageConfigurator"; public static string Version => "0.5.1"; public LocalizedText ModMenuName() { return LocalizedText.Raw("Double Damage Configurator"); } public AbstractMenuScreen BuildCustomMenu() { _openScreen = EnemyListMenu.Build(_catalog, _settings, () => _scanUi, StartRegenerate, delegate(Action refresh) { _refreshScanBanner = refresh; }, ((BaseUnityPlugin)this).Logger); return _openScreen; } private void Awake() { //IL_0133: Unknown result type (might be due to invalid IL or missing references) _gameVersion = Application.version; _catalogPath = Path.Combine(Paths.ConfigPath, CatalogCache.FileName); _settingsPath = Path.Combine(Paths.ConfigPath, EnemySettings.FileName); _settings = EnemySettings.Load(_settingsPath); TakeDamageHook.Settings = _settings; _scanner = new EnemyAssetScanner(((BaseUnityPlugin)this).Logger, delegate(Action action) { _mainThread.Enqueue(action); }); CatalogCacheFile catalogCacheFile = CatalogCache.TryLoad(_catalogPath); if (catalogCacheFile != null && catalogCacheFile.Status == ScanStatus.Complete && catalogCacheFile.GameVersion == _gameVersion) { AssignCatalog(catalogCacheFile.Catalog); _scanUi = new ScanUiState(ScanPhase.Ready, _catalog.Enemies.Count); ((BaseUnityPlugin)this).Logger.LogInfo((object)$"Loaded enemy catalog with {_catalog.Enemies.Count} entries for game {_gameVersion}."); } else { ((BaseUnityPlugin)this).Logger.LogInfo((object)"Enemy catalog missing, stale, or incomplete. Starting scan."); _scanUi = new ScanUiState(ScanPhase.Running, 0); StartScan(); } new Harmony(((BaseUnityPlugin)this).Info.Metadata.GUID).PatchAll(typeof(TakeDamageHook).Assembly); } private void AssignCatalog(EnemyCatalog catalog) { _catalog = catalog; TakeDamageHook.SetCatalog(catalog); } private void Update() { Action result; while (_mainThread.TryDequeue(out result)) { result(); } } private void StartRegenerate() { if (!_scanner.IsScanning) { ((BaseUnityPlugin)this).Logger.LogInfo((object)"Regenerating enemy catalog."); _scanUi = new ScanUiState(ScanPhase.Running, _catalog.Enemies.Count); StartScan(); } } private void StartScan() { _scanner.TryBeginScan(_gameVersion, _catalogPath, AssetPaths.BundleFolder, OnScanProgress, OnScanCompleted); } private void OnScanProgress(int filesDone, int filesTotal) { _scanUi = new ScanUiState(ScanPhase.Running, _catalog.Enemies.Count, filesDone, filesTotal); _refreshScanBanner?.Invoke(); } private void OnScanCompleted() { _refreshScanBanner = null; CatalogCacheFile catalogCacheFile = CatalogCache.TryLoad(_catalogPath); if (catalogCacheFile == null) { ((BaseUnityPlugin)this).Logger.LogWarning((object)"Enemy catalog scan finished but no cache file was readable."); _scanUi = new ScanUiState(ScanPhase.Failed, _catalog.Enemies.Count); } else { switch (catalogCacheFile.Status) { case ScanStatus.Complete: if (catalogCacheFile.GameVersion != _gameVersion) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Enemy catalog game version " + catalogCacheFile.GameVersion + " does not match " + _gameVersion + ".")); _scanUi = new ScanUiState(ScanPhase.Failed, _catalog.Enemies.Count); } else { AssignCatalog(catalogCacheFile.Catalog); _scanUi = new ScanUiState(ScanPhase.Ready, _catalog.Enemies.Count); ((BaseUnityPlugin)this).Logger.LogInfo((object)$"Enemy catalog ready with {_catalog.Enemies.Count} entries."); } break; case ScanStatus.Incomplete: ((BaseUnityPlugin)this).Logger.LogWarning((object)"Enemy catalog is still incomplete after the scan."); _scanUi = new ScanUiState(ScanPhase.Failed, _catalog.Enemies.Count); break; default: throw new ArgumentOutOfRangeException("Status", catalogCacheFile.Status, "Unknown ScanStatus."); } } if (_openScreen != null && _openScreen == MenuScreenNavigation.CurrentModMenuScreen) { AbstractMenuScreen val = BuildCustomMenu(); MenuScreenNavigation.Show(val, (HistoryMode)1); } } } public sealed class EnemyAssetScanner { private sealed class ScanCensus { public int BundlesAttempted; public int SceneBundlesAccepted; public int AssetsFilesScanned; public int CreateFromFileCalls; public int CreateFromFileSkipped; public int MonoBehavioursVisited; public int GetBaseFieldOk; public int FilesWithHits; public int FilesWithHealth; public int FilesNeedingLookup; public long MemoryAtStart; public long MemoryAtEnd; } private readonly struct FoundEnemy { public string DisplayName { get; } public int MaxDamageDealt { get; } public bool IsBoss { get; } public FoundEnemy(string displayName, int maxDamageDealt, bool isBoss) { DisplayName = displayName; MaxDamageDealt = maxDamageDealt; IsBoss = isBoss; } } private const int DamageThreshold = 2; private readonly ManualLogSource _log; private readonly Action<Action> _runOnMain; private int _running; public bool IsScanning => Volatile.Read(in _running) != 0; public EnemyAssetScanner(ManualLogSource log, Action<Action> runOnMain) { _log = log ?? throw new ArgumentNullException("log"); _runOnMain = runOnMain ?? throw new ArgumentNullException("runOnMain"); } public bool TryBeginScan(string gameVersion, string catalogPath, string bundleFolder, Action<int, int> onProgressOnMain, Action onCompletedOnMain) { if (string.IsNullOrWhiteSpace(gameVersion)) { throw new ArgumentException("Game version is required.", "gameVersion"); } if (string.IsNullOrWhiteSpace(catalogPath)) { throw new ArgumentException("Catalog path is required.", "catalogPath"); } if (onProgressOnMain == null) { throw new ArgumentNullException("onProgressOnMain"); } if (onCompletedOnMain == null) { throw new ArgumentNullException("onCompletedOnMain"); } if (Interlocked.CompareExchange(ref _running, 1, 0) != 0) { return false; } ThreadPool.QueueUserWorkItem(delegate { try { RunScan(gameVersion, catalogPath, bundleFolder ?? string.Empty, onProgressOnMain); } catch (Exception arg) { _log.LogError((object)$"Enemy catalog scan failed: {arg}"); } finally { Interlocked.Exchange(ref _running, 0); _runOnMain(onCompletedOnMain); } }); return true; } private void RunScan(string gameVersion, string catalogPath, string bundleFolder, Action<int, int> onProgressOnMain) { string text = Path.Combine(Path.GetDirectoryName(catalogPath) ?? string.Empty, CatalogCache.TempFileName); if (File.Exists(text)) { File.Delete(text); } if (!File.Exists(catalogPath)) { CatalogCache.WriteIncompletePlaceholder(catalogPath, gameVersion); } if (!Directory.Exists(bundleFolder)) { _log.LogError((object)("Bundle folder does not exist: " + bundleFolder)); return; } List<string> list = new List<string>(); foreach (string item in Directory.EnumerateFiles(bundleFolder, "*", SearchOption.AllDirectories)) { if (!IsSkippableBundlePath(item)) { list.Add(item); } } _log.LogInfo((object)$"Walking {list.Count} files under {bundleFolder}"); ReportProgress(0, list.Count, onProgressOnMain); List<FoundEnemy> list2 = new List<FoundEnemy>(); int num = 0; int num2 = 0; ScanCensus scanCensus = new ScanCensus(); AssetsManager val = BundleUtils.CreateDefaultManager(); try { scanCensus.MemoryAtStart = GC.GetTotalMemory(forceFullCollection: false); for (int i = 0; i < list.Count; i++) { string text2 = list[i]; try { if (TryScanBundle(val, text2, list2, scanCensus)) { num++; if (num % 50 == 0) { _log.LogInfo((object)$"Scan progress: {num} scene bundles, {list2.Count} candidate hits, {num2} errors"); } } } catch (Exception ex) { num2++; _log.LogWarning((object)("Skipping bundle " + text2 + ": " + ex.Message)); } ReportProgress(i + 1, list.Count, onProgressOnMain); } } finally { val.UnloadAll(false); } scanCensus.MemoryAtEnd = GC.GetTotalMemory(forceFullCollection: false); EnemyCatalog enemyCatalog = BuildCatalog(list2); CatalogCache.WriteCompleteAtomic(catalogPath, text, gameVersion, enemyCatalog); _log.LogInfo((object)$"Enemy catalog complete. Scenes={num} enemies={enemyCatalog.Enemies.Count} errors={num2}"); _log.LogInfo((object)($"Scan census: bundlesAttempted={scanCensus.BundlesAttempted} sceneBundlesAccepted={scanCensus.SceneBundlesAccepted} " + $"assetsFilesScanned={scanCensus.AssetsFilesScanned} createFromFileCalls={scanCensus.CreateFromFileCalls} " + $"createFromFileSkipped={scanCensus.CreateFromFileSkipped} monoBehavioursVisited={scanCensus.MonoBehavioursVisited} " + $"getBaseFieldOk={scanCensus.GetBaseFieldOk} filesWithHits={scanCensus.FilesWithHits} " + $"filesWithHealth={scanCensus.FilesWithHealth} filesNeedingLookup={scanCensus.FilesNeedingLookup} " + $"memoryStart={scanCensus.MemoryAtStart} memoryEnd={scanCensus.MemoryAtEnd}")); } private void ReportProgress(int done, int total, Action<int, int> onProgress) { if (done == 0 || done == total || done % 25 == 0) { int d = done; int t = total; _runOnMain(delegate { onProgress(d, t); }); } } private bool TryScanBundle(AssetsManager mgr, string path, List<FoundEnemy> found, ScanCensus census) { census.BundlesAttempted++; BundleFileInstance val; try { val = mgr.LoadBundleFile(path, true); } catch (Exception ex) { _log.LogDebug((object)("Not a loadable bundle (" + Path.GetFileName(path) + "): " + ex.Message)); return false; } try { SceneBundleInfo val2 = default(SceneBundleInfo); if (!BundleUtils.TryFindAssetsFiles(mgr, val, ref val2)) { return false; } AssetsFileInstance fileInst = mgr.LoadAssetsFileFromBundle(val, val2.mainAfileInstIndex, false); ScanAssetsFile(mgr, fileInst, found, census); if (val2.sharedAssetsAfileIndex >= 0 && val2.sharedAssetsAfileIndex != val2.mainAfileInstIndex) { AssetsFileInstance fileInst2 = mgr.LoadAssetsFileFromBundle(val, val2.sharedAssetsAfileIndex, false); ScanAssetsFile(mgr, fileInst2, found, census); } census.SceneBundlesAccepted++; return true; } finally { mgr.UnloadBundleFile(val); } } private static void ScanAssetsFile(AssetsManager mgr, AssetsFileInstance fileInst, List<FoundEnemy> found, ScanCensus census) { census.AssetsFilesScanned++; HashSet<long> hashSet = new HashSet<long>(); HashSet<long> titleGos = new HashSet<long>(); List<(long, int)> list = new List<(long, int)>(); CollectFromFile(mgr, fileInst, hashSet, titleGos, list, census); if (list.Count > 0) { census.FilesWithHits++; } if (hashSet.Count > 0) { census.FilesWithHealth++; } if (list.Count > 0 && hashSet.Count > 0) { census.FilesNeedingLookup++; } if (list.Count == 0 || hashSet.Count == 0) { census.CreateFromFileSkipped++; return; } census.CreateFromFileCalls++; GameObjectLookup lookup = GameObjectLookup.CreateFromFile(mgr, fileInst); EmitFoundEnemies(lookup, hashSet, titleGos, list, found); } private static void CollectFromFile(AssetsManager mgr, AssetsFileInstance fileInst, HashSet<long> healthGos, HashSet<long> titleGos, List<(long GoPathId, int Damage)> hits, ScanCensus census) { foreach (AssetFileInfo item in fileInst.file.GetAssetsOfType((AssetClassID)114)) { census.MonoBehavioursVisited++; AssetTypeValueField baseField; try { baseField = mgr.GetBaseField(fileInst, item, (AssetReadFlags)0); } catch (Exception) { continue; } if (baseField == null || baseField.IsDummy) { continue; } census.GetBaseFieldOk++; if (!TryGetGameObjectPathId(baseField, out var goPathId)) { continue; } if (ContainsDisplayBossTitle(baseField)) { titleGos.Add(goPathId); } if (IsHealthManager(baseField)) { healthGos.Add(goPathId); } else { if (!IsDamageHero(baseField)) { continue; } int asInt = baseField["damageDealt"].AsInt; if (asInt >= 2) { bool flag; try { flag = IsEnemyHazardType(baseField["hazardType"].AsInt); } catch (ArgumentOutOfRangeException) { continue; } if (flag) { hits.Add((goPathId, asInt)); } } } } } private static void EmitFoundEnemies(GameObjectLookup lookup, HashSet<long> healthGos, HashSet<long> titleGos, List<(long GoPathId, int Damage)> hits, List<FoundEnemy> found) { Dictionary<long, int> dictionary = new Dictionary<long, int>(); foreach (var (goPathId, num) in hits) { if (TryFindHealthManagerGo(lookup, goPathId, healthGos, out var healthGo) && (!dictionary.TryGetValue(healthGo, out var value) || num > value)) { dictionary[healthGo] = num; } } GameObjectInfo val = default(GameObjectInfo); foreach (KeyValuePair<long, int> item in dictionary) { if (lookup.TryLookupGameObject(item.Key, ref val) && val != null) { string hierarchyPath = val.GameObjectName ?? string.Empty; string text = EnemyNames.Canonical(LeafName(hierarchyPath)); if (!string.IsNullOrEmpty(text)) { bool isBoss = titleGos.Contains(item.Key) || TitleOnParentSubtree(lookup, item.Key, titleGos); found.Add(new FoundEnemy(text, item.Value, isBoss)); } } } } private static EnemyCatalog BuildCatalog(List<FoundEnemy> found) { List<EnemyRecord> list = new List<EnemyRecord>(found.Count); foreach (FoundEnemy item in found) { list.Add(new EnemyRecord(new EnemyId(item.DisplayName), item.DisplayName, item.MaxDamageDealt, item.IsBoss)); } return new EnemyCatalog(list); } private static bool TitleOnParentSubtree(GameObjectLookup lookup, long healthGo, HashSet<long> titleGos) { GameObjectInfo val = default(GameObjectInfo); if (!lookup.TryLookupGameObject(healthGo, ref val) || val == null || val.ParentPathId == 0) { return false; } GameObjectInfo val2 = default(GameObjectInfo); if (!lookup.TryLookupTransfrom(val.ParentPathId, ref val2) || val2 == null) { return false; } long gameObjectPathId = val2.GameObjectPathId; if (titleGos.Contains(gameObjectPathId)) { return true; } foreach (long titleGo in titleGos) { if (IsUnderOrIs(lookup, titleGo, gameObjectPathId)) { return true; } } return false; } private static bool IsUnderOrIs(GameObjectLookup lookup, long go, long ancestorOrSelf) { if (go == ancestorOrSelf) { return true; } HashSet<long> hashSet = new HashSet<long>(); long num = go; GameObjectInfo val = default(GameObjectInfo); GameObjectInfo val2 = default(GameObjectInfo); while (hashSet.Add(num)) { if (!lookup.TryLookupGameObject(num, ref val) || val == null) { return false; } if (val.ParentPathId == 0) { return false; } if (!lookup.TryLookupTransfrom(val.ParentPathId, ref val2) || val2 == null) { return false; } num = val2.GameObjectPathId; if (num == ancestorOrSelf) { return true; } } return false; } private static bool ContainsDisplayBossTitle(AssetTypeValueField root) { Stack<AssetTypeValueField> stack = new Stack<AssetTypeValueField>(); stack.Push(root); while (stack.Count > 0) { AssetTypeValueField val = stack.Pop(); if (val == null || val.IsDummy) { continue; } try { string type = val.TemplateField.Type; if ((type == "string" || type == "TypelessData") ? true : false) { string asString = val.AsString; if (asString.Contains("DisplayBossTitle", StringComparison.Ordinal)) { return true; } } } catch (Exception) { } foreach (AssetTypeValueField item in val) { if (item != null && !item.IsDummy) { stack.Push(item); } } } return false; } private static bool TryFindHealthManagerGo(GameObjectLookup lookup, long goPathId, HashSet<long> healthGos, out long healthGo) { HashSet<long> hashSet = new HashSet<long>(); long num = goPathId; GameObjectInfo val = default(GameObjectInfo); GameObjectInfo val2 = default(GameObjectInfo); while (hashSet.Add(num)) { if (healthGos.Contains(num)) { healthGo = num; return true; } if (!lookup.TryLookupGameObject(num, ref val) || val == null) { healthGo = 0L; return false; } if (val.ParentPathId == 0) { healthGo = 0L; return false; } if (!lookup.TryLookupTransfrom(val.ParentPathId, ref val2) || val2 == null) { healthGo = 0L; return false; } num = val2.GameObjectPathId; } healthGo = 0L; return false; } private static bool IsHealthManager(AssetTypeValueField mb) { return !mb["hp"].IsDummy && !mb["enemyType"].IsDummy && !mb["hasSpecialDeath"].IsDummy; } private static bool IsDamageHero(AssetTypeValueField mb) { return !mb["damageDealt"].IsDummy && !mb["hazardType"].IsDummy && !mb["OnDamagedHero"].IsDummy; } private static bool TryGetGameObjectPathId(AssetTypeValueField mb, out long goPathId) { AssetTypeValueField val = mb["m_GameObject"]; if (val.IsDummy) { goPathId = 0L; return false; } AssetTypeValueField val2 = val["m_FileID"]; if (!val2.IsDummy && val2.AsInt != 0) { goPathId = 0L; return false; } AssetTypeValueField val3 = val["m_PathID"]; if (val3.IsDummy) { goPathId = 0L; return false; } goPathId = val3.AsLong; return goPathId != 0; } private static bool IsEnemyHazardType(int hazardType) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0004: 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_0006: 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_0009: Invalid comparison between Unknown and I4 //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_000f: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Invalid comparison between Unknown and I4 HazardType val = (HazardType)hazardType; HazardType val2 = val; HazardType val3 = val2; if ((int)val3 > 1) { if (val3 - 2 <= 10) { return false; } throw new ArgumentOutOfRangeException("hazardType", hazardType, "Unknown HazardType."); } return true; } private static bool IsSkippableBundlePath(string path) { string extension = Path.GetExtension(path); return extension.Equals(".json", StringComparison.OrdinalIgnoreCase) || extension.Equals(".hash", StringComparison.OrdinalIgnoreCase) || extension.Equals(".xml", StringComparison.OrdinalIgnoreCase) || extension.Equals(".manifest", StringComparison.OrdinalIgnoreCase) || extension.Equals(".txt", StringComparison.OrdinalIgnoreCase); } private static string LeafName(string hierarchyPath) { int num = hierarchyPath.LastIndexOf('/'); return (num < 0) ? hierarchyPath : hierarchyPath.Substring(num + 1); } } public readonly struct EnemyId : IEquatable<EnemyId> { public string Value { get; } public EnemyId(string value) { if (string.IsNullOrWhiteSpace(value)) { throw new ArgumentException("EnemyId cannot be empty.", "value"); } Value = value; } public bool Equals(EnemyId other) { return string.Equals(Value, other.Value, StringComparison.Ordinal); } public override bool Equals(object? obj) { return obj is EnemyId other && Equals(other); } public override int GetHashCode() { return StringComparer.Ordinal.GetHashCode(Value); } public override string ToString() { return Value; } public static bool operator ==(EnemyId left, EnemyId right) { return left.Equals(right); } public static bool operator !=(EnemyId left, EnemyId right) { return !left.Equals(right); } } public readonly struct EnemyRecord { public EnemyId Id { get; } public string DisplayName { get; } public int MaxDamageDealt { get; } public bool IsBoss { get; } public EnemyRecord(EnemyId id, string displayName, int maxDamageDealt, bool isBoss = false) { Id = id; DisplayName = displayName ?? throw new ArgumentNullException("displayName"); MaxDamageDealt = maxDamageDealt; IsBoss = isBoss; } } public enum ScanStatus { Complete, Incomplete } public sealed class EnemyCatalog { public static EnemyCatalog Empty { get; } = new EnemyCatalog(Array.Empty<EnemyRecord>()); public IReadOnlyList<EnemyRecord> Enemies { get; } public EnemyCatalog(IEnumerable<EnemyRecord> enemies) { if (enemies == null) { throw new ArgumentNullException("enemies"); } Dictionary<string, EnemyRecord> dictionary = new Dictionary<string, EnemyRecord>(StringComparer.Ordinal); foreach (EnemyRecord enemy in enemies) { if (enemy.MaxDamageDealt < 2) { continue; } string text = EnemyNames.Canonical(enemy.DisplayName); if (!string.IsNullOrWhiteSpace(text)) { EnemyId id = new EnemyId(text); if (dictionary.TryGetValue(text, out var value)) { int maxDamageDealt = ((enemy.MaxDamageDealt > value.MaxDamageDealt) ? enemy.MaxDamageDealt : value.MaxDamageDealt); dictionary[text] = new EnemyRecord(id, text, maxDamageDealt, value.IsBoss || enemy.IsBoss); } else { dictionary[text] = new EnemyRecord(id, text, enemy.MaxDamageDealt, enemy.IsBoss); } } } List<EnemyRecord> list = new List<EnemyRecord>(dictionary.Values); list.Sort(CompareEnemies); Enemies = list; } private static int CompareEnemies(EnemyRecord left, EnemyRecord right) { int num = string.CompareOrdinal(left.DisplayName, right.DisplayName); return (num != 0) ? num : string.CompareOrdinal(left.Id.Value, right.Id.Value); } } internal static class EnemyNames { public static string Canonical(string dumpName) { if (string.IsNullOrWhiteSpace(dumpName)) { return string.Empty; } string text = dumpName.Trim(); int num = text.LastIndexOf(" (", StringComparison.Ordinal); if (num <= 0 || !text.EndsWith(")", StringComparison.Ordinal)) { return text; } string text2 = text.Substring(num + 2, text.Length - num - 3); if (text2.Length == 0) { return text; } foreach (char c in text2) { if (c < '0' || c > '9') { return text; } } return text.Substring(0, num); } } public sealed class CatalogCacheFile { public string GameVersion { get; } public ScanStatus Status { get; } public EnemyCatalog Catalog { get; } public CatalogCacheFile(string gameVersion, ScanStatus status, EnemyCatalog catalog) { GameVersion = gameVersion ?? throw new ArgumentNullException("gameVersion"); Status = status; Catalog = catalog ?? throw new ArgumentNullException("catalog"); } } internal static class CatalogCache { private sealed class CatalogFileDto { public string? GameVersion { get; set; } public string? Status { get; set; } public List<EnemyDto>? Enemies { get; set; } } private sealed class EnemyDto { public string? Id { get; set; } public string? DisplayName { get; set; } public int MaxDamageDealt { get; set; } public bool IsBoss { get; set; } } public static string FileName { get; } = "DoubleDamageConfigurator.enemy-catalog.json"; public static string TempFileName { get; } = "DoubleDamageConfigurator.enemy-catalog.json.tmp"; public static CatalogCacheFile? TryLoad(string path) { if (!File.Exists(path)) { return null; } try { string text = File.ReadAllText(path); if (text.Length == 0) { return null; } CatalogFileDto catalogFileDto = JsonConvert.DeserializeObject<CatalogFileDto>(text); if (catalogFileDto == null || string.IsNullOrWhiteSpace(catalogFileDto.GameVersion)) { return null; } ScanStatus status = ParseStatus(catalogFileDto.Status); List<EnemyRecord> list = new List<EnemyRecord>(); if (catalogFileDto.Enemies != null) { foreach (EnemyDto enemy in catalogFileDto.Enemies) { if (enemy != null && !string.IsNullOrWhiteSpace(enemy.Id) && enemy.DisplayName != null) { string text2 = EnemyNames.Canonical(enemy.DisplayName); if (!string.IsNullOrWhiteSpace(text2)) { list.Add(new EnemyRecord(new EnemyId(text2), text2, enemy.MaxDamageDealt, enemy.IsBoss)); } } } } return new CatalogCacheFile(catalogFileDto.GameVersion, status, new EnemyCatalog(list)); } catch (JsonException) { return null; } catch (ArgumentOutOfRangeException) { return null; } catch (IOException) { return null; } } public static void WriteIncompletePlaceholder(string path, string gameVersion) { CatalogFileDto dto = new CatalogFileDto { GameVersion = gameVersion, Status = "Incomplete", Enemies = new List<EnemyDto>() }; WriteDto(path, dto); } public static void WriteCompleteAtomic(string catalogPath, string tempPath, string gameVersion, EnemyCatalog catalog) { List<EnemyDto> list = new List<EnemyDto>(catalog.Enemies.Count); foreach (EnemyRecord enemy in catalog.Enemies) { list.Add(new EnemyDto { Id = enemy.Id.Value, DisplayName = enemy.DisplayName, MaxDamageDealt = enemy.MaxDamageDealt, IsBoss = enemy.IsBoss }); } CatalogFileDto dto = new CatalogFileDto { GameVersion = gameVersion, Status = "Complete", Enemies = list }; WriteDto(tempPath, dto); ReplaceCatalog(tempPath, catalogPath); } private static void WriteDto(string path, CatalogFileDto dto) { string directoryName = Path.GetDirectoryName(path); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } File.WriteAllText(path, JsonConvert.SerializeObject((object)dto, (Formatting)1)); } private static void ReplaceCatalog(string tempPath, string catalogPath) { if (File.Exists(catalogPath)) { File.Replace(tempPath, catalogPath, null); } else { File.Move(tempPath, catalogPath); } } private static ScanStatus ParseStatus(string? raw) { if (!(raw == "Complete")) { if (raw == "Incomplete") { return ScanStatus.Incomplete; } throw new ArgumentOutOfRangeException("raw", raw, "Unknown catalog scan status."); } return ScanStatus.Complete; } } internal static class EnemyListMenu { private sealed class CatalogSession { private readonly EnemyCatalog _catalog; private readonly EnemySettings _settings; private readonly JournalPortraitIndex _journal; private readonly Func<ScanUiState> _getScan; private readonly TextLabel _status; private readonly TextLabel _empty; private readonly Slot[] _slots; private readonly ScrollingPane _pane; private EnemyListQuery _query = new EnemyListQuery(EnemyTab.All, string.Empty); private bool _rebinding; public FreeGroup Root { get; } public CatalogSession(EnemyCatalog catalog, EnemySettings settings, JournalPortraitIndex journal, Func<ScanUiState> getScan, Action onRegenerate, Action<Action> bindStatusRefresh) { //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005b: Expected O, but got Unknown //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0069: Expected O, but got Unknown //IL_0138: Unknown result type (might be due to invalid IL or missing references) //IL_013f: Expected O, but got Unknown //IL_0142: Unknown result type (might be due to invalid IL or missing references) //IL_015b: Unknown result type (might be due to invalid IL or missing references) //IL_0166: Unknown result type (might be due to invalid IL or missing references) //IL_016d: Expected O, but got Unknown //IL_018b: Unknown result type (might be due to invalid IL or missing references) //IL_0195: Expected O, but got Unknown //IL_01d3: Unknown result type (might be due to invalid IL or missing references) //IL_01dd: Expected O, but got Unknown //IL_02d9: Unknown result type (might be due to invalid IL or missing references) //IL_02e3: Expected O, but got Unknown //IL_0306: Unknown result type (might be due to invalid IL or missing references) //IL_0311: Unknown result type (might be due to invalid IL or missing references) //IL_0318: Expected O, but got Unknown //IL_031c: Unknown result type (might be due to invalid IL or missing references) //IL_033a: Unknown result type (might be due to invalid IL or missing references) CatalogSession catalogSession = this; _catalog = catalog; _settings = settings; _journal = journal; _getScan = getScan; TextLabel val = new TextLabel(LocalizedText.Raw("On limits that enemy to 1 mask. Off keeps vanilla damage.")); ((MenuElement)val).SetFontSizes((FontSizes)0); VerticalGroup val2 = new VerticalGroup(); val2.Add((IMenuEntity)(object)RegenerateButton(_getScan, delegate { onRegenerate(); catalogSession.Rebind(); })); TextInput<string> val3 = new TextInput<string>(LocalizedText.Raw("Search"), (ITextModel<string>)(object)TextModels.ForStrings()); val3.OnTextValueChanged += delegate(string text) { if (!catalogSession._rebinding && catalogSession._getScan().Occupancy().ShowRows) { catalogSession._query = catalogSession._query.WithSearch(text); catalogSession.Rebind(); } }; val2.Add((IMenuEntity)(object)val3); ChoiceElement<EnemyTab> val4 = new ChoiceElement<EnemyTab>(LocalizedText.Raw("Show"), (IChoiceModel<EnemyTab>)(object)ChoiceModels.ForNamedValues<EnemyTab>((IEnumerable<ValueTuple<EnemyTab, string>>)new(EnemyTab, string)[4] { (EnemyTab.All, "All"), (EnemyTab.Enemies, "Enemies"), (EnemyTab.Bosses, "Bosses"), (EnemyTab.Other, "Other") })); ((SelectableValueElement<EnemyTab>)(object)val4).OnValueChanged += delegate(EnemyTab tab) { if (!catalogSession._rebinding && catalogSession._getScan().Occupancy().ShowRows) { catalogSession._query = catalogSession._query.WithTab(tab); catalogSession.Rebind(); } }; val2.Add((IMenuEntity)(object)val4); FreeGroup val5 = new FreeGroup(); val5.Add((IMenuEntity)(object)val, Vector2.zero); val5.Add((IMenuEntity)(object)val2, new Vector2(0f, 0f - SpacingConstants.VSPACE_SMALL)); VerticalGroup val6 = new VerticalGroup(); val6.HideInactiveElements = true; _status = new TextLabel(LocalizedText.Raw(getScan().Format())); IMenuEntityExtensions[(IMenuEntity)(object)_status] = getScan().Occupancy().ShowBanner; val6.Add((IMenuEntity)(object)_status); _empty = new TextLabel(LocalizedText.Raw("No enemies match.")); IMenuEntityExtensions[(IMenuEntity)(object)_empty] = false; val6.Add((IMenuEntity)(object)_empty); EnemyListView enemyListView = EnemyListQueryApply.Apply(catalog.Enemies, journal, new EnemyListQuery(EnemyTab.All, string.Empty)); _slots = new Slot[enemyListView.Rows.Count]; for (int num = 0; num < enemyListView.Rows.Count; num++) { EnemyListRow row = enemyListView.Rows[num]; settings.SyncRowOff(row); ChoiceElement<bool> val7 = new ChoiceElement<bool>(LocalizedText.Raw(row.Label), (IChoiceModel<bool>)(object)ChoiceModels.ForBool()); Image portrait = AttachPortrait(val7); int slotIndex = num; ((SelectableValueElement<bool>)(object)val7).OnValueChanged += delegate(bool value) { catalogSession.OnSlotChanged(slotIndex, value); }; _slots[num] = new Slot(val7, portrait, row); val6.Add((IMenuEntity)(object)val7); } _pane = new ScrollingPane((INavigableMenuEntity)(object)val6); _pane.Axes = (ScrollAxes)1; _pane.ViewportSize = new Vector2(1920f, SpacingConstants.VSPACE_MEDIUM * 5f); FreeGroup val8 = new FreeGroup(); val8.Add((IMenuEntity)(object)val5, Vector2.zero); val8.Add((IMenuEntity)(object)_pane, new Vector2(0f, 0f - ChromeHeight)); Root = val8; bindStatusRefresh(RefreshStatus); } private void RefreshStatus() { if (_status.Text != null) { _status.Text.text = _getScan().Format(); } } public void Rebind() { _rebinding = true; try { EnemyListView enemyListView = EnemyListQueryApply.Apply(_catalog.Enemies, _journal, _query); HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); for (int i = 0; i < enemyListView.Rows.Count; i++) { EnemyListRow enemyListRow = enemyListView.Rows[i]; for (int j = 0; j < enemyListRow.Members.Count; j++) { hashSet.Add(enemyListRow.Members[j].Id.Value); } } EnemyListOccupancy enemyListOccupancy = _getScan().Occupancy(); if (enemyListOccupancy.ShowBanner && (Object)(object)_status.Text != (Object)null) { _status.Text.text = _getScan().Format(); } IMenuEntityExtensions[(IMenuEntity)(object)_status] = enemyListOccupancy.ShowBanner; IMenuEntityExtensions[(IMenuEntity)(object)_empty] = enemyListOccupancy.ShowRows && enemyListView.IsEmpty; for (int k = 0; k < _slots.Length; k++) { BindSlot(_slots[k], enemyListOccupancy.ShowRows && RowTouches(_slots[k].Row, hashSet)); } _pane.QueueContentResize(); } finally { _rebinding = false; } } private void BindSlot(Slot slot, bool visible) { //IL_00b5: Unknown result type (might be due to invalid IL or missing references) IMenuEntityExtensions[(IMenuEntity)(object)slot.Choice] = visible; if (visible) { _settings.SyncRowOff(slot.Row); slot.Choice.LabelText.text = slot.Row.Label; ((SelectableValueElement<bool>)(object)slot.Choice).Value = !_settings.IsRowOff(slot.Row); if (_journal.TryGetIcon(slot.Row.Primary.DisplayName, out Sprite sprite) && (Object)(object)sprite != (Object)null) { slot.Portrait.sprite = sprite; ((Graphic)slot.Portrait).color = Color.white; } else { ClearPortrait(slot.Portrait); } } } private void OnSlotChanged(int slotIndex, bool value) { if (!_rebinding && _getScan().Occupancy().ShowRows) { EnemyListRow row = _slots[slotIndex].Row; if (value) { _settings.SetRowOn(row); } else { _settings.SetRowOff(row); } } } private static bool RowTouches(EnemyListRow row, HashSet<string> visibleIds) { for (int i = 0; i < row.Members.Count; i++) { if (visibleIds.Contains(row.Members[i].Id.Value)) { return true; } } return false; } } private sealed class Slot { public ChoiceElement<bool> Choice { get; } public Image Portrait { get; } public EnemyListRow Row { get; } public Slot(ChoiceElement<bool> choice, Image portrait, EnemyListRow row) { Choice = choice; Portrait = portrait; Row = row; } } private const float PortraitSize = 90f; private const float PortraitInset = 8f; private const float ChoiceGutter = 200f; private static readonly float ChromeHeight = SpacingConstants.VSPACE_SMALL + 3f * SpacingConstants.VSPACE_MEDIUM; private static readonly Color PlaceholderColor = new Color(0.12f, 0.12f, 0.12f, 1f); public static AbstractMenuScreen Build(EnemyCatalog catalog, EnemySettings settings, Func<ScanUiState> getScan, Action onRegenerate, Action<Action> bindStatusRefresh, ManualLogSource log) { //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Expected O, but got Unknown settings.RebindFrom(catalog); CatalogSession catalogSession = new CatalogSession(catalog, settings, new JournalPortraitIndex(log), getScan, onRegenerate, bindStatusRefresh); catalogSession.Rebind(); return (AbstractMenuScreen)new BasicMenuScreen(LocalizedText.Raw("Double Damage Configurator"), (INavigableMenuEntity)(object)catalogSession.Root); } private static TextButton RegenerateButton(Func<ScanUiState> getScan, Action onRegenerate) { //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Expected O, but got Unknown TextButton val = new TextButton(LocalizedText.Raw("Regenerate list")); val.OnSubmit = (Action)Delegate.Combine(val.OnSubmit, (Action)delegate { if (getScan().Phase != ScanPhase.Running) { onRegenerate(); } }); return val; } private static Image AttachPortrait(ChoiceElement<bool> choice) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Expected O, but got Unknown //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_006d: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00db: 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_0100: Unknown result type (might be due to invalid IL or missing references) //IL_010d: Unknown result type (might be due to invalid IL or missing references) //IL_011e: Unknown result type (might be due to invalid IL or missing references) //IL_0128: Unknown result type (might be due to invalid IL or missing references) RectTransform rectTransform = ((Graphic)choice.LabelText).rectTransform; GameObject val = new GameObject("Portrait"); val.transform.SetParent(((Transform)rectTransform).parent, false); val.transform.SetAsFirstSibling(); Image val2 = val.AddComponent<Image>(); val2.preserveAspect = true; ((Graphic)val2).raycastTarget = false; ClearPortrait(val2); RectTransform rectTransform2 = ((Graphic)val2).rectTransform; rectTransform2.anchorMin = new Vector2(rectTransform.anchorMin.x, 0.5f); rectTransform2.anchorMax = new Vector2(rectTransform.anchorMin.x, 0.5f); rectTransform2.pivot = new Vector2(0f, 0.5f); rectTransform2.sizeDelta = new Vector2(90f, 90f); float x = rectTransform.offsetMin.x; rectTransform2.anchoredPosition = new Vector2(x + 8f, 0f); rectTransform.offsetMin = new Vector2(x + 8f + 90f, rectTransform.offsetMin.y); rectTransform.offsetMax = new Vector2(rectTransform.offsetMax.x - 200f, rectTransform.offsetMax.y); return val2; } private static void ClearPortrait(Image image) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) image.sprite = null; ((Graphic)image).color = PlaceholderColor; } } public enum EnemyTab { All, Enemies, Bosses, Other } public readonly struct JournalMatch { public int Order { get; } public bool IsOther { get; } public string LocalizedName { get; } public JournalMatch(int order, bool isOther, string localizedName) { if (order < 0) { throw new ArgumentOutOfRangeException("order", order, "Journal order cannot be negative."); } Order = order; IsOther = isOther; LocalizedName = localizedName ?? string.Empty; } } public interface IJournalIndex { bool TryMatch(string displayName, out JournalMatch match); } public sealed class EmptyJournalIndex : IJournalIndex { public static EmptyJournalIndex Instance { get; } = new EmptyJournalIndex(); public bool TryMatch(string displayName, out JournalMatch match) { match = default(JournalMatch); return false; } } public readonly struct EnemyListQuery { public EnemyTab Tab { get; } public string Search { get; } public EnemyListQuery(EnemyTab tab, string search) { Tab = tab; Search = search ?? string.Empty; } public EnemyListQuery WithTab(EnemyTab tab) { return new EnemyListQuery(tab, Search); } public EnemyListQuery WithSearch(string search) { return new EnemyListQuery(Tab, search); } } public readonly struct EnemyListRow { public IReadOnlyList<EnemyRecord> Members { get; } public string Label { get; } public EnemyRecord Primary => Members[0]; public bool IsBoss { get { for (int i = 0; i < Members.Count; i++) { if (Members[i].IsBoss) { return true; } } return false; } } public EnemyListRow(IReadOnlyList<EnemyRecord> members, string label) { if (members == null || members.Count == 0) { throw new ArgumentException("EnemyListRow requires at least one member.", "members"); } Members = members; Label = label ?? throw new ArgumentNullException("label"); } } public readonly struct EnemyListView { public IReadOnlyList<EnemyListRow> Rows { get; } public bool IsEmpty => Rows.Count == 0; public EnemyListView(IReadOnlyList<EnemyListRow> rows) { Rows = rows ?? throw new ArgumentNullException("rows"); } } public static class EnemyListQueryApply { public static EnemyListView Apply(IReadOnlyList<EnemyRecord> enemies, IJournalIndex journal, EnemyListQuery query) { if (enemies == null) { throw new ArgumentNullException("enemies"); } if (journal == null) { throw new ArgumentNullException("journal"); } Dictionary<string, List<(EnemyRecord, JournalMatch)>> dictionary = new Dictionary<string, List<(EnemyRecord, JournalMatch)>>(StringComparer.Ordinal); List<EnemyRecord> list = new List<EnemyRecord>(); foreach (EnemyRecord enemy in enemies) { if (journal.TryMatch(enemy.DisplayName, out var match) && !string.IsNullOrEmpty(match.LocalizedName)) { if (!dictionary.TryGetValue(match.LocalizedName, out var value)) { value = new List<(EnemyRecord, JournalMatch)>(); dictionary[match.LocalizedName] = value; } value.Add((enemy, match)); } else { list.Add(enemy); } } List<(EnemyListRow, bool, bool)> list2 = new List<(EnemyListRow, bool, bool)>(dictionary.Count + list.Count); foreach (KeyValuePair<string, List<(EnemyRecord, JournalMatch)>> item3 in dictionary) { List<EnemyRecord> list3 = new List<EnemyRecord>(item3.Value.Count); for (int i = 0; i < item3.Value.Count; i++) { list3.Add(item3.Value[i].Item1); } JournalMatch item = item3.Value[0].Item2; list2.Add((new EnemyListRow(list3, item3.Key), true, item.IsOther)); } for (int j = 0; j < list.Count; j++) { EnemyRecord enemyRecord = list[j]; list2.Add((new EnemyListRow(new EnemyRecord[1] { enemyRecord }, enemyRecord.DisplayName), false, false)); } List<EnemyListRow> list4 = new List<EnemyListRow>(list2.Count); foreach (var (item2, matched, isOther) in list2) { if (MatchesTab(query.Tab, item2.IsBoss, matched, isOther)) { list4.Add(item2); } } ApplySearch(list4, query.Search); list4.Sort(CompareRows); return new EnemyListView(list4); } private static bool MatchesTab(EnemyTab tab, bool isBoss, bool matched, bool isOther) { return tab switch { EnemyTab.All => true, EnemyTab.Enemies => matched && !isOther, EnemyTab.Bosses => isBoss, EnemyTab.Other => !matched || isOther, _ => throw new ArgumentOutOfRangeException("tab", tab, "Unknown EnemyTab."), }; } private static void ApplySearch(List<EnemyListRow> filtered, string search) { string text = search.Trim(); if (text.Length == 0) { return; } for (int num = filtered.Count - 1; num >= 0; num--) { EnemyListRow row = filtered[num]; if (!ContainsIgnoreCase(row.Label, text) && !MemberDisplayNameMatches(row, text)) { filtered.RemoveAt(num); } } } private static bool MemberDisplayNameMatches(EnemyListRow row, string needle) { for (int i = 0; i < row.Members.Count; i++) { if (ContainsIgnoreCase(row.Members[i].DisplayName, needle)) { return true; } } return false; } private static bool ContainsIgnoreCase(string haystack, string needle) { return haystack.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0; } private static int CompareRows(EnemyListRow left, EnemyListRow right) { int num = string.Compare(left.Label, right.Label, StringComparison.OrdinalIgnoreCase); return (num != 0) ? num : string.CompareOrdinal(left.Primary.Id.Value, right.Primary.Id.Value); } } public readonly struct EnemySetting { public EnemyId Id { get; } public string DisplayName { get; } public EnemySetting(EnemyId id, string displayName) { Id = id; DisplayName = displayName ?? throw new ArgumentNullException("displayName"); } } public sealed class EnemySettings { private sealed class SettingsFileDto { public List<SettingDto>? Off { get; set; } } private sealed class SettingDto { public string? Id { get; set; } public string? DisplayName { get; set; } } private readonly List<EnemySetting> _off = new List<EnemySetting>(); private readonly Dictionary<string, int> _indexById = new Dictionary<string, int>(StringComparer.Ordinal); private readonly string _path; public static string FileName { get; } = "DoubleDamageConfigurator.enemy-settings.json"; public IReadOnlyList<EnemySetting> OffChoices => _off; private EnemySettings(string path) { _path = path; } public static EnemySettings Load(string path) { EnemySettings enemySettings = new EnemySettings(path); if (!File.Exists(path)) { return enemySettings; } try { string text = File.ReadAllText(path); if (text.Length == 0) { return enemySettings; } SettingsFileDto settingsFileDto = JsonConvert.DeserializeObject<SettingsFileDto>(text); if (settingsFileDto?.Off == null) { return enemySettings; } foreach (SettingDto item in settingsFileDto.Off) { if (item != null && !string.IsNullOrWhiteSpace(item.Id) && item.DisplayName != null) { string text2 = EnemyNames.Canonical(item.DisplayName); string value = EnemyNames.Canonical(item.Id); if (!string.IsNullOrWhiteSpace(text2) && !string.IsNullOrWhiteSpace(value)) { enemySettings.Upsert(new EnemySetting(new EnemyId(value), text2)); } } } } catch (JsonException) { return enemySettings; } catch (IOException) { return enemySettings; } return enemySettings; } public void RebindFrom(EnemyCatalog catalog) { HashSet<string> hashSet = UniqueDisplayNames(catalog); bool flag = false; foreach (EnemyRecord enemy in catalog.Enemies) { if (_indexById.ContainsKey(enemy.Id.Value) || !hashSet.Contains(enemy.DisplayName)) { continue; } int num = 0; int num2 = -1; for (int i = 0; i < _off.Count; i++) { if (string.Equals(_off[i].DisplayName, enemy.DisplayName, StringComparison.Ordinal)) { num++; num2 = i; } } if (num == 1) { EnemySetting enemySetting = _off[num2]; EnemySetting value = new EnemySetting(enemy.Id, enemySetting.DisplayName); _off[num2] = value; _indexById.Remove(enemySetting.Id.Value); _indexById[value.Id.Value] = num2; flag = true; } } if (flag) { Save(); } } public bool IsOff(EnemyId id) { return _indexById.ContainsKey(id.Value); } public bool IsRowOff(EnemyListRow row) { for (int i = 0; i < row.Members.Count; i++) { if (IsOff(row.Members[i].Id)) { return true; } } return false; } public void SetOff(EnemyId id, string displayName) { Upsert(new EnemySetting(id, displayName)); Save(); } public void SetOn(EnemyId id) { if (_indexById.TryGetValue(id.Value, out var value)) { _off.RemoveAt(value); RebuildIndex(); Save(); } } public void SetRowOff(EnemyListRow row) { bool flag = false; for (int i = 0; i < row.Members.Count; i++) { EnemyRecord enemyRecord = row.Members[i]; if (!IsOff(enemyRecord.Id)) { Upsert(new EnemySetting(enemyRecord.Id, enemyRecord.DisplayName)); flag = true; } } if (flag) { Save(); } } public void SetRowOn(EnemyListRow row) { bool flag = false; for (int i = 0; i < row.Members.Count; i++) { EnemyId id = row.Members[i].Id; if (_indexById.TryGetValue(id.Value, out var value)) { _off.RemoveAt(value); RebuildIndex(); flag = true; } } if (flag) { Save(); } } public void SyncRowOff(EnemyListRow row) { if (!IsRowOff(row)) { return; } bool flag = true; for (int i = 0; i < row.Members.Count; i++) { if (!IsOff(row.Members[i].Id)) { flag = false; break; } } if (!flag) { SetRowOff(row); } } public void Save() { List<SettingDto> list = new List<SettingDto>(_off.Count); foreach (EnemySetting item in _off) { list.Add(new SettingDto { Id = item.Id.Value, DisplayName = item.DisplayName }); } SettingsFileDto settingsFileDto = new SettingsFileDto { Off = list }; string directoryName = Path.GetDirectoryName(_path); if (!string.IsNullOrEmpty(directoryName)) { Directory.CreateDirectory(directoryName); } File.WriteAllText(_path, JsonConvert.SerializeObject((object)settingsFileDto, (Formatting)1)); } private void Upsert(EnemySetting setting) { if (_indexById.TryGetValue(setting.Id.Value, out var value)) { _off[value] = setting; return; } _indexById[setting.Id.Value] = _off.Count; _off.Add(setting); } private void RebuildIndex() { _indexById.Clear(); for (int i = 0; i < _off.Count; i++) { _indexById[_off[i].Id.Value] = i; } } private static HashSet<string> UniqueDisplayNames(EnemyCatalog catalog) { Dictionary<string, int> dictionary = new Dictionary<string, int>(StringComparer.Ordinal); foreach (EnemyRecord enemy in catalog.Enemies) { dictionary.TryGetValue(enemy.DisplayName, out var value); dictionary[enemy.DisplayName] = value + 1; } HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); foreach (KeyValuePair<string, int> item in dictionary) { if (item.Value == 1) { hashSet.Add(item.Key); } } return hashSet; } } public sealed class JournalPortraitIndex : IJournalIndex { private readonly ManualLogSource _log; private readonly Dictionary<string, EnemyJournalRecord> _exact = new Dictionary<string, EnemyJournalRecord>(StringComparer.Ordinal); private readonly HashSet<string> _ambiguous = new HashSet<string>(StringComparer.Ordinal); private readonly List<(string Key, EnemyJournalRecord Record)> _keys = new List<(string, EnemyJournalRecord)>(); private readonly Dictionary<EnemyJournalRecord, int> _order = new Dictionary<EnemyJournalRecord, int>(); private readonly bool _empty; private bool _logged; public JournalPortraitIndex(ManualLogSource log) { //IL_00f4: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) _log = log ?? throw new ArgumentNullException("log"); List<EnemyJournalRecord> list = null; try { list = EnemyJournalManager.GetAllEnemies(); } catch (Exception arg) { LogOnce($"Enemy journal is unavailable. All dump names will show under Other. {arg}"); _empty = true; return; } if (list == null || list.Count == 0) { LogOnce("Enemy journal list is empty. All dump names will show under Other."); _empty = true; return; } try { for (int i = 0; i < list.Count; i++) { EnemyJournalRecord val = list[i]; if (!((Object)(object)val == (Object)null)) { _order[val] = i; IndexKey(Normalize(((Object)val).name), val); IndexKey(Normalize(((object)val.DisplayName/*cast due to .constrained prefix*/).ToString()), val); } } } catch (Exception arg2) { _exact.Clear(); _ambiguous.Clear(); _keys.Clear(); _order.Clear(); LogOnce($"Enemy journal index failed. All dump names will show under Other. {arg2}"); _empty = true; return; } _empty = _order.Count == 0; if (_empty) { LogOnce("Enemy journal list is empty. All dump names will show under Other."); } } public bool TryMatch(string displayName, out JournalMatch match) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Invalid comparison between Unknown and I4 //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) match = default(JournalMatch); if (_empty) { return false; } try { if (!TryResolve(displayName, out EnemyJournalRecord record, out int order)) { return false; } match = new JournalMatch(order, (int)record.RecordType == 1, ((object)record.DisplayName/*cast due to .constrained prefix*/).ToString()); return true; } catch (Exception arg) { LogOnce($"Enemy journal match failed. Treating the name as unmatched / Other. {arg}"); match = default(JournalMatch); return false; } } public bool TryGetIcon(string displayName, out Sprite sprite) { sprite = null; if (_empty) { return false; } try { if (!TryResolve(displayName, out EnemyJournalRecord record, out int _)) { return false; } Sprite iconSprite = record.IconSprite; if ((Object)(object)iconSprite != (Object)null) { sprite = iconSprite; return true; } Sprite enemySprite = record.EnemySprite; if ((Object)(object)enemySprite != (Object)null) { sprite = enemySprite; return true; } return false; } catch (Exception arg) { LogOnce($"Enemy journal icon failed. Using the placeholder. {arg}"); sprite = null; return false; } } private bool TryResolve(string displayName, out EnemyJournalRecord record, out int order) { record = null; order = 0; if (string.IsNullOrEmpty(displayName)) { return false; } EnemyJournalRecord val = FindByGetRecord(displayName); if ((Object)(object)val == (Object)null) { val = FindExact(displayName); } if ((Object)(object)val == (Object)null) { val = FindFuzzy(displayName); } if ((Object)(object)val == (Object)null) { return false; } record = val; order = OrderOf(record); return true; } private static EnemyJournalRecord? FindByGetRecord(string displayName) { EnemyJournalRecord record = EnemyJournalManager.GetRecord(displayName); if ((Object)(object)record != (Object)null) { return record; } record = EnemyJournalManager.GetRecord(displayName.Replace(" ", string.Empty)); if ((Object)(object)record != (Object)null) { return record; } record = EnemyJournalManager.GetRecord(displayName.Replace(' ', '_')); if ((Object)(object)record != (Object)null) { return record; } return null; } private EnemyJournalRecord? FindExact(string displayName) { string text = Normalize(displayName); if (text.Length == 0) { return null; } if (_exact.TryGetValue(text, out EnemyJournalRecord value) && (Object)(object)value != (Object)null) { return value; } return null; } private EnemyJournalRecord? FindFuzzy(string displayName) { string text = Normalize(displayName); if (text.Length == 0) { return null; } EnemyJournalRecord val = null; foreach (var (text2, val2) in _keys) { if (text2.Length != 0 && !((Object)(object)val2 == (Object)null) && FuzzyHit(text2, text)) { if ((Object)(object)val == (Object)null) { val = val2; } else if ((Object)(object)val != (Object)(object)val2) { return null; } } } return val; } private static bool FuzzyHit(string key, string needle) { return key.Length >= 4 && needle.Length >= 4 && (key.StartsWith(needle, StringComparison.Ordinal) || needle.StartsWith(key, StringComparison.Ordinal)); } private int OrderOf(EnemyJournalRecord record) { int value; return _order.TryGetValue(record, out value) ? value : _order.Count; } private void IndexKey(string key, EnemyJournalRecord record) { _keys.Add((key, record)); if (key.Length == 0 || _ambiguous.Contains(key)) { return; } if (_exact.TryGetValue(key, out EnemyJournalRecord value)) { if (!((Object)(object)value == (Object)(object)record)) { _exact.Remove(key); _ambiguous.Add(key); } } else { _exact[key] = record; } } private static string Normalize(string value) { if (string.IsNullOrEmpty(value)) { return string.Empty; } return value.ToLowerInvariant().Replace(" ", string.Empty).Replace("_", string.Empty); } private void LogOnce(string message) { if (!_logged) { _logged = true; _log.LogWarning((object)message); } } } public enum ScanPhase { Ready, Running, Failed } public readonly struct ScanUiState { public ScanPhase Phase { get; } public int EnemyCount { get; } public int FilesDone { get; } public int FilesTotal { get; } public ScanUiState(ScanPhase phase, int enemyCount) : this(phase, enemyCount, 0, 0) { } public ScanUiState(ScanPhase phase, int enemyCount, int filesDone, int filesTotal) { if (enemyCount < 0) { throw new ArgumentOutOfRangeException("enemyCount", enemyCount, "Enemy count cannot be negative."); } if (filesDone < 0) { throw new ArgumentOutOfRangeException("filesDone", filesDone, "Files done cannot be negative."); } if (filesTotal < 0) { throw new ArgumentOutOfRangeException("filesTotal", filesTotal, "Files total cannot be negative."); } if (filesTotal > 0 && filesDone > filesTotal) { throw new ArgumentOutOfRangeException("filesDone", filesDone, "Files done cannot exceed files total."); } if ((uint)phase > 2u) { throw new ArgumentOutOfRangeException("phase", phase, "Unknown ScanPhase."); } Phase = phase; EnemyCount = enemyCount; FilesDone = filesDone; FilesTotal = filesTotal; } public string Format() { switch (Phase) { case ScanPhase.Ready: return (EnemyCount == 1) ? "List ready. 1 enemy." : ("List ready. " + EnemyCount + " enemies."); case ScanPhase.Running: { if (FilesTotal == 0) { return "Scanning game files…"; } int num = FilesDone * 100 / FilesTotal; return "Scanning… " + num + "% (" + FilesDone + " / " + FilesTotal + ")"; } case ScanPhase.Failed: return "Scan incomplete. Try regenerate."; default: throw new ArgumentOutOfRangeException("Phase", Phase, "Unknown ScanPhase."); } } public EnemyListOccupancy Occupancy() { return Phase switch { ScanPhase.Ready => new EnemyListOccupancy(showBanner: false, showRows: true), ScanPhase.Running => new EnemyListOccupancy(showBanner: true, showRows: false), ScanPhase.Failed => new EnemyListOccupancy(showBanner: true, showRows: true), _ => throw new ArgumentOutOfRangeException("Phase", Phase, "Unknown ScanPhase."), }; } } public readonly struct EnemyListOccupancy { public bool ShowBanner { get; } public bool ShowRows { get; } public EnemyListOccupancy(bool showBanner, bool showRows) { ShowBanner = showBanner; ShowRows = showRows; } } internal static class TakeDamageHook { [HarmonyPatch(typeof(HeroController), "TakeDamage", new Type[] { typeof(GameObject), typeof(CollisionSide), typeof(int), typeof(HazardType), typeof(DamagePropertyFlags) })] private static class Patch { [HarmonyPrefix] private static void Prefix(GameObject go, ref int damageAmount, HazardType hazardType) { //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Invalid comparison between Unknown and I4 if (damageAmount > 1 && ((int)hazardType == 0 || (int)hazardType == 1)) { EnemySettings settings = Settings; if (settings != null && TryResolve(go, out var id)) { damageAmount = DamageCap.Apply(damageAmount, DamageCap.ShouldCap(resolvedToCatalog: true, settings.IsOff(id))); } } } } internal static EnemySettings? Settings; private static HashSet<string> _catalogIds = new HashSet<string>(StringComparer.Ordinal); internal static void SetCatalog(EnemyCatalog catalog) { HashSet<string> hashSet = new HashSet<string>(StringComparer.Ordinal); foreach (EnemyRecord enemy in catalog.Enemies) { hashSet.Add(enemy.Id.Value); } _catalogIds = hashSet; } private static bool TryResolve(GameObject go, out EnemyId id) { id = default(EnemyId); if ((Object)(object)go == (Object)null) { return false; } Transform val = go.transform; while ((Object)(object)val != (Object)null) { if ((Object)(object)((Component)val).GetComponent<HealthManager>() == (Object)null) { val = val.parent; continue; } string text = EnemyNames.Canonical(((Object)((Component)val).gameObject).name); if (text.Length == 0 || !_catalogIds.Contains(text)) { return false; } id = new EnemyId(text); return true; } return false; } } }