using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Bootstrap; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using Microsoft.CodeAnalysis; using MonstersGordion.Compat; using MonstersGordion.Patches; using Unity.Netcode; using UnityEngine; using UnityEngine.AI; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("MonstersGordion")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("1.9.0.0")] [assembly: AssemblyInformationalVersion("1.9.0+6ebcd4f96dbc0f4617f06cdb5691906ae869c699")] [assembly: AssemblyProduct("MonstersGordion")] [assembly: AssemblyTitle("MonstersGordion")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.9.0.0")] [module: UnverifiableCode] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [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 MonstersGordion { internal sealed class CompanyMonsterSpawner : MonoBehaviour { private readonly List _aiNodes = new List(); private readonly List _fakeTrees = new List(); private readonly List _ownedEnemies = new List(); private readonly List _ownedNests = new List(); private readonly Dictionary _spawnTimes = new Dictionary(); private string[] _originalSurfaceTags; private readonly List<(EnemyAI ai, string name, float time)> _recentSpawns = new List<(EnemyAI, string, float)>(); private readonly Dictionary _earlyDeathCounts = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly HashSet _disabledThisLanding = new HashSet(StringComparer.OrdinalIgnoreCase); private const int EarlyDeathsBeforeDisable = 2; private readonly Dictionary _lastMovement = new Dictionary(); private const float StuckSeconds = 25f; private const float StuckDistance = 1.5f; private NavMeshSampler _sampler; private const float AnnounceDelay = 3f; private Coroutine _loop; private Coroutine _maintenance; private Coroutine _events; private readonly List _ownedHazards = new List(); private bool _shuttingDown; private bool _hasVainShrouds; private readonly HashSet _warnedRequirements = new HashSet(StringComparer.OrdinalIgnoreCase); private static readonly HashSet HarmlessCreatures = new HashSet(StringComparer.OrdinalIgnoreCase) { "Manticoil", "Docile Locust Bees" }; private const float EarlyDeathSeconds = 4f; private int _earlyDeathTracesLogged; private static readonly HashSet StationaryByDesign = new HashSet(StringComparer.OrdinalIgnoreCase) { "Flowerman", "Spring", "Clay Surgeon", "Jester", "Girl", "Cadaver Bloom", "Feiopar" }; private const int TreeCanopyLayer = 25; private static Material _trunkMaterial; private static Mesh _realTreeMesh; private static Material _realTreeMaterial; private static Quaternion _realTreeRotation = Quaternion.identity; private static Vector3 _realTreeScale = Vector3.one; private static bool _realTreeSearched; internal static CompanyMonsterSpawner Instance { get; private set; } internal static bool IsCompanyLevel() { return IsCompanyLevel(((Object)(object)StartOfRound.Instance != (Object)null) ? StartOfRound.Instance.currentLevel : null); } internal static bool IsCompanyLevel(SelectableLevel level) { if ((Object)(object)level == (Object)null) { return false; } if (!(level.sceneName == "CompanyBuilding") && level.levelID != 3) { if (level.PlanetName != null) { return level.PlanetName.Contains("Gordion"); } return false; } return true; } internal static void OnShipLanded() { //IL_00cb: Unknown result type (might be due to invalid IL or missing references) StartOfRound instance = StartOfRound.Instance; if (!((Object)(object)instance == (Object)null)) { SelectableLevel currentLevel = instance.currentLevel; bool isServer = ((NetworkBehaviour)instance).IsServer; bool flag = IsCompanyLevel(); Plugin.Log.LogInfo((object)("Ship landed: planet='" + currentLevel?.PlanetName + "', scene='" + currentLevel?.sceneName + "', " + $"host={isServer}, companyMoon={flag}.")); if (!isServer) { Plugin.Log.LogInfo((object)"This game session is not the host — the spawner only runs on the host's machine. Make sure the lobby host has MonstersGordion installed."); } else if (!flag) { Plugin.DebugLog("Not the Company moon — spawner stays idle."); } else if (!((Object)(object)Instance != (Object)null)) { ToilHeadCompat.Scan(); StarlancerCompat.Scan(); BcmeCompat.Scan(); Instance = new GameObject("MonstersGordion_Spawner").AddComponent(); Instance.Begin(); } } } internal static void OnShipLeaving() { if ((Object)(object)Instance != (Object)null) { Instance.Shutdown(); } } private void Begin() { PluginConfig cfg = Plugin.Cfg; EnemyCatalog.Resolve(); if (cfg.RespawnOnLoad.Value) { DespawnAllEnemies(); } _sampler = new NavMeshSampler(cfg.RequireIndoorPoints.Value); if (_sampler.Build() == 0) { Plugin.Log.LogError((object)"No usable navmesh found inside the Company building. Make sure NavMeshInCompanyRedux is installed and working (or set RequireIndoorPoints=false to loosen the filter). Spawning is disabled for this landing."); return; } _hasVainShrouds = CheckVainShrouds(); if (!_hasVainShrouds && BushWolfIsSpawnable()) { int iterations = ((cfg.VainShroudIterations.Value > 0) ? cfg.VainShroudIterations.Value : 12); _hasVainShrouds = GrowVainShrouds(iterations); } PlaceQueuedHazards(); CreateAINodes(cfg.AINodeCount.Value); CreateDeadTreesIfNeeded(); EnableWormFloorEmergeIfNeeded(); _loop = ((MonoBehaviour)this).StartCoroutine(SpawnLoop()); _maintenance = ((MonoBehaviour)this).StartCoroutine(MaintenanceLoop()); _events = ((MonoBehaviour)this).StartCoroutine(EventLoop()); Plugin.Log.LogInfo((object)($"Company spawner active: cap={cfg.GlobalCap.Value}, " + $"interval=[{cfg.MinSpawnInterval.Value:F0}s..{cfg.MaxSpawnInterval.Value:F0}s], " + $"outsideAIMode={cfg.TreatEnemiesAsOutside.Value}, foreignEnemies={cfg.ForeignEnemies.Value}, " + "ToilHead=" + (ToilHeadCompat.Present ? $"Coil {cfg.CoilHeadTurretChance.Value}% / Manti {cfg.ManticoilTurretChance.Value}% / Masked {cfg.MaskedTurretChance.Value}%" : "absent") + ", StarlancerAIFix=" + (StarlancerCompat.Present ? "present" : "absent") + ", BCME=" + (BcmeCompat.Present ? "present" : "absent") + ".")); } private void Shutdown() { if (_shuttingDown) { return; } _shuttingDown = true; if (_loop != null) { ((MonoBehaviour)this).StopCoroutine(_loop); _loop = null; } if (_maintenance != null) { ((MonoBehaviour)this).StopCoroutine(_maintenance); _maintenance = null; } if (_events != null) { ((MonoBehaviour)this).StopCoroutine(_events); _events = null; } GordionEvents.Reset(); try { if (Plugin.Cfg.DespawnOnShipLeave.Value) { DespawnOwnedEnemies(); } DespawnOwnedNests(); RestoreWormFloorEmerge(); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Cleanup despawn failed (scene tearing down?): " + ex.Message)); } foreach (GameObject aiNode in _aiNodes) { if ((Object)(object)aiNode != (Object)null) { Object.Destroy((Object)(object)aiNode); } } _aiNodes.Clear(); foreach (GameObject fakeTree in _fakeTrees) { if ((Object)(object)fakeTree != (Object)null) { Object.Destroy((Object)(object)fakeTree); } } _fakeTrees.Clear(); DespawnOwnedHazards(); _ownedEnemies.Clear(); Instance = null; Object.Destroy((Object)(object)((Component)this).gameObject); Plugin.DebugLog("Company spawner shut down."); } private void OnDestroy() { if ((Object)(object)Instance == (Object)(object)this && !_shuttingDown) { _shuttingDown = true; ((MonoBehaviour)this).StopAllCoroutines(); Instance = null; } } private IEnumerator SpawnLoop() { PluginConfig cfg = Plugin.Cfg; while (true) { float num = Mathf.Max(1f, cfg.MinSpawnInterval.Value); float num2 = Mathf.Max(num, cfg.MaxSpawnInterval.Value); float num3 = Random.Range(num, num2); Plugin.DebugLog($"Next spawn attempt in {num3:F1}s."); yield return (object)new WaitForSeconds(num3); try { TrySpawnCycle(); } catch (Exception arg) { Plugin.Log.LogError((object)$"Spawn cycle failed: {arg}"); } } } private void TrySpawnCycle() { //IL_030d: Unknown result type (might be due to invalid IL or missing references) PluginConfig cfg = Plugin.Cfg; if ((Object)(object)RoundManager.Instance == (Object)null || (Object)(object)StartOfRound.Instance == (Object)null || StartOfRound.Instance.shipIsLeaving) { return; } Dictionary perType; int num = CountAliveEnemies(out perType); if (num >= cfg.GlobalCap.Value) { Plugin.DebugLog($"Global cap reached ({num}/{cfg.GlobalCap.Value}) — skipping."); return; } ScanForEarlyDeaths(); List<(EnemyType, EnemySpawnSettings, int)> list = new List<(EnemyType, EnemySpawnSettings, int)>(); foreach (EnemyType enemy in EnemyCatalog.Enemies) { EnemySpawnSettings enemySpawnSettings = Plugin.Cfg.For(enemy); if (enemySpawnSettings.Enabled.Value && enemySpawnSettings.SpawnWeight.Value > 0 && enemySpawnSettings.MaxSpawnCount.Value > 0 && (cfg.AllowHarmlessCreatures.Value || !IsHarmlessCreature(enemy)) && !_disabledThisLanding.Contains(enemy.enemyName)) { perType.TryGetValue(enemy.enemyName, out var value); if (value < enemySpawnSettings.MaxSpawnCount.Value && MeetsMapRequirements(enemy)) { list.Add((enemy, enemySpawnSettings, value)); } } } if (list.Count == 0) { Plugin.DebugLog("No eligible enemy types (all disabled or at their max)."); return; } List<(EnemyType, EnemySpawnSettings, int)> list2 = list.Where<(EnemyType, EnemySpawnSettings, int)>(((EnemyType type, EnemySpawnSettings s, int alive) c) => c.alive < c.s.MinSpawnCount.Value).ToList(); List<(EnemyType, EnemySpawnSettings, int)> pool; string arg; if (list2.Count > 0) { pool = list2; arg = "min-deficit pool"; } else { bool wantOutside = Random.Range(0, 100) < cfg.OutsideEnemyShare.Value; List<(EnemyType, EnemySpawnSettings, int)> list3 = list.Where<(EnemyType, EnemySpawnSettings, int)>(((EnemyType type, EnemySpawnSettings s, int alive) c) => c.type.isOutsideEnemy == wantOutside).ToList(); if (list3.Count == 0) { list3 = list; arg = (wantOutside ? "outdoor pool empty → all" : "indoor pool empty → all"); } else { arg = (wantOutside ? "outdoor pool" : "indoor pool"); } pool = list3; } (EnemyType, EnemySpawnSettings, int) tuple = WeightedPick(pool); Plugin.DebugLog($"Picked '{tuple.Item1.enemyName}' (weight {tuple.Item2.SpawnWeight.Value}, " + $"alive {tuple.Item3}/{tuple.Item2.MaxSpawnCount.Value}, {arg}, " + $"total alive {num}/{cfg.GlobalCap.Value})."); Vector3? randomPoint = _sampler.GetRandomPoint(cfg.MinDistanceFromPlayers.Value, 12, UpperShareFor(tuple.Item1)); if (!randomPoint.HasValue) { Plugin.Log.LogWarning((object)("Could not find a valid interior navmesh point for '" + tuple.Item1.enemyName + "' — skipping this cycle.")); } else { SpawnEnemy(tuple.Item1, randomPoint.Value); } } private bool EnsureNestFor(EnemyType type) { //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Unknown result type (might be due to invalid IL or missing references) //IL_01c6: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)type.nestSpawnPrefab == (Object)null || !type.requireNestObjectsToSpawn) { return true; } RoundManager instance = RoundManager.Instance; if ((Object)(object)instance == (Object)null) { return true; } instance.enemyNestSpawnObjects.RemoveAll((EnemyAINestSpawnObject n) => (Object)(object)n == (Object)null); if (instance.enemyNestSpawnObjects.Any((EnemyAINestSpawnObject n) => (Object)(object)n != (Object)null && (Object)(object)n.enemyType == (Object)(object)type)) { return true; } Vector3? randomPoint = _sampler.GetRandomPoint(Plugin.Cfg.MinDistanceFromPlayers.Value, 12, Plugin.Cfg.UpperFloorSpawnShare.Value); if (!randomPoint.HasValue) { WarnRequirementOnce(type, "no interior navmesh point was free for its nest object"); return false; } GameObject val = null; try { val = Object.Instantiate(type.nestSpawnPrefab, randomPoint.Value, Quaternion.Euler(0f, Random.Range(-180f, 180f), 0f)); EnemyAINestSpawnObject component = val.GetComponent(); NetworkObject componentInChildren = val.GetComponentInChildren(); if ((Object)(object)component == (Object)null || (Object)(object)componentInChildren == (Object)null) { WarnRequirementOnce(type, "its nest prefab '" + ((Object)type.nestSpawnPrefab).name + "' has no " + (((Object)(object)component == (Object)null) ? "EnemyAINestSpawnObject" : "NetworkObject") + " component"); Object.Destroy((Object)(object)val); return false; } componentInChildren.Spawn(true); instance.enemyNestSpawnObjects.Add(component); _ownedNests.Add(val); Plugin.DebugLog($"Placed a nest for '{type.enemyName}' at {randomPoint.Value:F1}."); return true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Could not place a nest for '" + type.enemyName + "': " + ex.Message)); if ((Object)(object)val != (Object)null) { Object.Destroy((Object)(object)val); } return false; } } private void WarnRequirementOnce(EnemyType type, string reason) { if (_warnedRequirements.Add(type.enemyName)) { Plugin.Log.LogWarning((object)("Not spawning '" + type.enemyName + "' [" + EnemyCatalog.AIClassName(type) + "]: " + reason + ". Without it the game destroys the enemy inside EnemyAI.Start().")); } } private void DespawnOwnedNests() { foreach (GameObject item in _ownedNests.Where((GameObject n) => (Object)(object)n != (Object)null).ToList()) { try { EnemyAINestSpawnObject component = item.GetComponent(); if ((Object)(object)component != (Object)null && (Object)(object)RoundManager.Instance != (Object)null) { RoundManager.Instance.enemyNestSpawnObjects.Remove(component); } NetworkObject componentInChildren = item.GetComponentInChildren(); if ((Object)(object)componentInChildren != (Object)null && componentInChildren.IsSpawned) { componentInChildren.Despawn(true); } else { Object.Destroy((Object)(object)item); } } catch (Exception ex) { Plugin.DebugLog("Nest cleanup failed: " + ex.Message); } } _ownedNests.Clear(); } private bool GrowVainShrouds(int iterations) { //IL_012d: Unknown result type (might be due to invalid IL or missing references) //IL_013c: Unknown result type (might be due to invalid IL or missing references) //IL_0141: Unknown result type (might be due to invalid IL or missing references) try { MoldSpreadManager val = Object.FindObjectOfType(); if ((Object)(object)val == (Object)null) { Plugin.Log.LogWarning((object)"Cannot grow vain shrouds: no MoldSpreadManager on this moon."); return false; } try { typeof(MoldSpreadManager).GetField("finishedGeneratingMold", BindingFlags.Instance | BindingFlags.NonPublic)?.SetValue(val, false); } catch (Exception ex) { Plugin.DebugLog("Could not reset finishedGeneratingMold: " + ex.Message); } FieldInfo field = typeof(MoldSpreadManager).GetField("finishedGeneratingMold", BindingFlags.Instance | BindingFlags.NonPublic); SelectableLevel val2 = (((Object)(object)StartOfRound.Instance != (Object)null) ? StartOfRound.Instance.currentLevel : null); int num = Mathf.Max(1, Plugin.Cfg.VainShroudPatches.Value); List list = new List(); for (int i = 0; i < num; i++) { try { field?.SetValue(val, false); } catch (Exception ex2) { Plugin.DebugLog("Could not reset finishedGeneratingMold: " + ex2.Message); } if ((Object)(object)val2 != (Object)null) { val2.canSpawnMold = true; val2.moldSpreadIterations = iterations; val2.moldStartPosition = -1; } Vector3? val3 = PickVainShroudOrigin(); if (!val3.HasValue) { Plugin.DebugLog("No free interior point left for another weed patch."); break; } val.GenerateMold(val3.Value, iterations); Vector3 value = val3.Value; list.Add(((Vector3)(ref value)).ToString("F1")); } bool weeds = val.GetWeeds(); string text = ((list.Count > 0) ? string.Join(", ", list) : "nowhere"); if (weeds) { Plugin.Log.LogInfo((object)($"Vain shrouds grown on Gordion: {list.Count} patch(es) x {iterations} iterations " + "at " + text + ". Bush Wolf can now hide and will survive.")); } else { Plugin.Log.LogWarning((object)($"Tried to grow vain shrouds ({list.Count} patch(es) x {iterations} iterations at " + text + ") but the game still reports none — Bush Wolf will be skipped. Try a higher [Integration] VainShroudIterations.")); } return weeds; } catch (Exception arg) { Plugin.Log.LogError((object)$"Growing vain shrouds failed: {arg}"); return false; } } private Vector3? PickVainShroudOrigin() { //IL_0074: Unknown result type (might be due to invalid IL or missing references) if (_sampler != null) { Vector3? result = _sampler.GetRandomPoint(Plugin.Cfg.MinDistanceFromPlayers.Value, 12, 50) ?? _sampler.GetRandomPoint(0f, 12, 50); if (result.HasValue) { return result; } } if (!((Object)(object)StartOfRound.Instance != (Object)null)) { return null; } return StartOfRound.Instance.shipLandingPosition.position; } private void EnableWormFloorEmergeIfNeeded() { //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: 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_00cb: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: 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 (!Plugin.Cfg.EarthLeviathanFloorEmerge.Value) { return; } EnemyType val = ((IEnumerable)EnemyCatalog.Enemies).FirstOrDefault((Func)((EnemyType e) => string.Equals(e.enemyName, "Earth Leviathan", StringComparison.OrdinalIgnoreCase))); if ((Object)(object)val == (Object)null || !Plugin.Cfg.For(val).Enabled.Value) { return; } StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || instance.naturalSurfaceTags == null) { return; } try { HashSet hashSet = new HashSet(instance.naturalSurfaceTags, StringComparer.Ordinal); RaycastHit val2 = default(RaycastHit); if (Physics.Raycast((Vector3)(((??)_sampler?.Anchor) ?? instance.shipLandingPosition.position) + Vector3.up * 3f, Vector3.down, ref val2, 12f, instance.collidersAndRoomMaskAndDefault, (QueryTriggerInteraction)1)) { string tag = ((Component)((RaycastHit)(ref val2)).collider).tag; if (!string.IsNullOrEmpty(tag)) { hashSet.Add(tag); } Plugin.DebugLog("Worm floor probe hit '" + ((Object)((RaycastHit)(ref val2)).collider).name + "' tag='" + tag + "'."); } string[] array = new string[6] { "Untagged", "Catwalk", "Metal", "Concrete", "Wood", "Tiles" }; foreach (string item in array) { hashSet.Add(item); } _originalSurfaceTags = instance.naturalSurfaceTags; instance.naturalSurfaceTags = hashSet.ToArray(); Plugin.Log.LogInfo((object)($"Earth Leviathan: extended naturalSurfaceTags to {instance.naturalSurfaceTags.Length} entries " + "so the worm can breach up through the Company floor.")); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Worm floor-emerge setup failed: " + ex.Message)); _originalSurfaceTags = null; } } private void RestoreWormFloorEmerge() { if (_originalSurfaceTags == null) { return; } try { if ((Object)(object)StartOfRound.Instance != (Object)null) { StartOfRound.Instance.naturalSurfaceTags = _originalSurfaceTags; } } catch (Exception ex) { Plugin.DebugLog("Restoring naturalSurfaceTags failed: " + ex.Message); } _originalSurfaceTags = null; } private static bool BushWolfIsSpawnable() { EnemyType val = ((IEnumerable)EnemyCatalog.Enemies).FirstOrDefault((Func)((EnemyType e) => string.Equals(e.enemyName, "Bush Wolf", StringComparison.OrdinalIgnoreCase))); if ((Object)(object)val != (Object)null) { return Plugin.Cfg.For(val).Enabled.Value; } return false; } private static bool CheckVainShrouds() { try { MoldSpreadManager val = Object.FindObjectOfType(); if ((Object)(object)val == (Object)null) { Plugin.DebugLog("No MoldSpreadManager on this moon — no vain shrouds."); return false; } bool weeds = val.GetWeeds(); Plugin.Log.LogInfo((object)$"Vain shrouds present: {weeds}."); return weeds; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Vain shroud check failed: " + ex.Message)); return false; } } private bool MeetsMapRequirements(EnemyType type) { if (string.Equals(type.enemyName, "Bush Wolf", StringComparison.OrdinalIgnoreCase) && !_hasVainShrouds) { if (_warnedRequirements.Add(type.enemyName)) { Plugin.Log.LogWarning((object)("Not spawning '" + type.enemyName + "' [" + EnemyCatalog.AIClassName(type) + "]: it needs vain shrouds to hide in and none grew on this moon. Weeds are grown at level load, so set [Integration] VainShroudIterations > 0 (or keep it at 0 with Bush Wolf enabled) and fly to Gordion again — enabling it mid-round is too late.")); } return false; } if (string.Equals(type.enemyName, "Cadaver Bloom", StringComparison.OrdinalIgnoreCase) && !Plugin.Cfg.CadaverBloomTraps.Value) { if (_warnedRequirements.Add(type.enemyName)) { Plugin.Log.LogWarning((object)"Not spawning 'Cadaver Bloom': it needs its Growth (a dungeon) to activate. Enable [Integration] CadaverBloomTraps to plant standalone burst traps instead."); } return false; } return true; } private void ScanForEarlyDeaths() { float realtimeSinceStartup = Time.realtimeSinceStartup; for (int num = _recentSpawns.Count - 1; num >= 0; num--) { var (val, text, num2) = _recentSpawns[num]; if (!((Object)(object)val == (Object)null) && !val.isEnemyDead) { if (realtimeSinceStartup - num2 > 4f) { _recentSpawns.RemoveAt(num); } } else { _recentSpawns.RemoveAt(num); if (!(realtimeSinceStartup - num2 > 4f)) { _earlyDeathCounts.TryGetValue(text, out var value); value++; _earlyDeathCounts[text] = value; if (value >= 2 && _disabledThisLanding.Add(text)) { Plugin.Log.LogWarning((object)($"'{text}' died within {4f:F0}s of spawning {value} times — it " + "cannot survive on Gordion (it likely needs a dungeon, weeds or other map feature this moon lacks). Disabling it until the next landing.")); } } } } } private static bool IsHarmlessCreature(EnemyType type) { return HarmlessCreatures.Contains(type.enemyName); } private static int UpperShareFor(EnemyType type) { if (Plugin.Cfg.OldBirdUpperFloorOnly.Value && string.Equals(type.enemyName, "RadMech", StringComparison.OrdinalIgnoreCase)) { return 100; } return Plugin.Cfg.UpperFloorSpawnShare.Value; } private static (EnemyType type, EnemySpawnSettings s, int alive) WeightedPick(List<(EnemyType type, EnemySpawnSettings s, int alive)> pool) { int num = 0; foreach (var item in pool) { num += item.s.SpawnWeight.Value; } int num2 = Random.Range(0, num); foreach (var item2 in pool) { num2 -= item2.s.SpawnWeight.Value; if (num2 < 0) { return item2; } } return pool[pool.Count - 1]; } private int CountAliveEnemies(out Dictionary perType) { perType = new Dictionary(StringComparer.OrdinalIgnoreCase); int num = 0; _ownedEnemies.RemoveAll((EnemyAI e) => (Object)(object)e == (Object)null || e.isEnemyDead); foreach (EnemyAI item in (IEnumerable)(Plugin.Cfg.CountForeignEnemies.Value ? RoundManager.Instance.SpawnedEnemies : _ownedEnemies)) { if (!((Object)(object)item == (Object)null) && !item.isEnemyDead && !((Object)(object)item.enemyType == (Object)null)) { num++; string enemyName = item.enemyType.enemyName; perType.TryGetValue(enemyName, out var value); perType[enemyName] = value + 1; } } return num; } internal static void NotifyEnemyKilled(EnemyAI enemy) { if (!((Object)(object)Instance == (Object)null) && !((Object)(object)enemy == (Object)null) && !((Object)(object)enemy.enemyType == (Object)null)) { Instance.OnEnemyKilled(enemy); } } private void OnEnemyKilled(EnemyAI enemy) { string enemyName = enemy.enemyType.enemyName; int instanceID = ((Object)enemy).GetInstanceID(); if (_spawnTimes.TryGetValue(instanceID, out var value)) { float num = Time.realtimeSinceStartup - value; _spawnTimes.Remove(instanceID); if (num <= 4f) { if (_earlyDeathTracesLogged < 5) { _earlyDeathTracesLogged++; Plugin.Log.LogWarning((object)($"'{enemyName}' [{EnemyCatalog.AIClassName(enemy.enemyType)}] died only {num:F2}s " + "after we spawned it — something is culling it. Caller stack trace:\n" + new StackTrace(fNeedFileInfo: false))); } else { Plugin.Log.LogWarning((object)$"'{enemyName}' died {num:F2}s after spawn (stack trace suppressed after 5)."); } return; } } Plugin.DebugLog("Enemy died: " + enemyName + "."); } internal void SpawnEnemy(EnemyType type, Vector3 point) { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_011f: Unknown result type (might be due to invalid IL or missing references) PluginConfig cfg = Plugin.Cfg; try { if (!EnsureNestFor(type)) { return; } Vector3 val = point + Vector3.up * cfg.SpawnYOffset.Value; float num = Random.Range(0f, 360f); NetworkObjectReference val2 = RoundManager.Instance.SpawnEnemyGameObject(val, num, -1, type); NetworkObject val3 = default(NetworkObject); if (!((NetworkObjectReference)(ref val2)).TryGet(ref val3, (NetworkManager)null) || (Object)(object)val3 == (Object)null) { Plugin.Log.LogWarning((object)("Spawn of '" + type.enemyName + "' returned no NetworkObject.")); return; } EnemyAI val4 = ((Component)val3).GetComponent(); if ((Object)(object)val4 == (Object)null) { val4 = ((Component)val3).GetComponentInChildren(); } if ((Object)(object)val4 == (Object)null) { Plugin.Log.LogWarning((object)("Spawned '" + type.enemyName + "' has no EnemyAI component.")); return; } _ownedEnemies.Add(val4); _spawnTimes[((Object)val4).GetInstanceID()] = Time.realtimeSinceStartup; _recentSpawns.Add((val4, type.enemyName, Time.realtimeSinceStartup)); Plugin.DebugLog($"Spawned '{type.enemyName}' at {val}."); ((MonoBehaviour)this).StartCoroutine(PostSpawnSetup(val4)); } catch (Exception arg) { Plugin.Log.LogError((object)$"Failed to spawn '{type.enemyName}': {arg}"); } } private IEnumerator PostSpawnSetup(EnemyAI ai) { yield return null; yield return null; if ((Object)(object)ai == (Object)null || ai.isEnemyDead) { yield break; } ApplyInteriorAI(ai); string text = (((Object)(object)ai.enemyType != (Object)null) ? ai.enemyType.enemyName : string.Empty); ToilHeadCompat.Kind kind = ToilHeadCompat.KindOf(text); if (ToilHeadCompat.Present && kind != ToilHeadCompat.Kind.None) { int num = Plugin.Cfg.ToilHeadTurretChance(kind); int num2 = Random.Range(0, 100); if (num2 < num) { bool flag = Random.Range(0, 100) < Plugin.Cfg.ToilHeadSlayerChance(kind); bool flag2 = ToilHeadCompat.TryApply(ai, flag); Plugin.DebugLog($"ToilHead: '{text}' rolled {num2} < {num} (slayer={flag}) — " + (flag2 ? "turret attached" : "attach FAILED, see warnings above") + "."); } else { Plugin.DebugLog($"ToilHead: '{text}' rolled {num2} >= {num} — plain spawn."); } } } private void ApplyInteriorAI(EnemyAI ai) { //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) try { bool value = Plugin.Cfg.TreatEnemiesAsOutside.Value; try { ai.SetEnemyOutside(value); } catch (Exception ex) { Plugin.DebugLog("SetEnemyOutside threw: " + ex.Message); } ai.isOutside = value; AssignNodes(ai); NavMeshHit val = default(NavMeshHit); if ((Object)(object)ai.agent != (Object)null && ((Behaviour)ai.agent).isActiveAndEnabled && NavMesh.SamplePosition(((Component)ai).transform.position, ref val, 4f, -1)) { ai.agent.Warp(((NavMeshHit)(ref val)).position); } } catch (Exception ex2) { Plugin.Log.LogWarning((object)("Interior AI fix-up failed for '" + ai.enemyType?.enemyName + "': " + ex2.Message)); } } private void AssignNodes(EnemyAI ai) { GameObject[] array = _aiNodes.Where((GameObject n) => (Object)(object)n != (Object)null).ToArray(); if (array.Length != 0) { ai.allAINodes = array; } } private void PlaceQueuedHazards() { //IL_0138: Unknown result type (might be due to invalid IL or missing references) List<(GameObject, float)> list = BcmeCompat.DrainQueuedOutdoorObjects(); if (list.Count == 0) { return; } PluginConfig cfg = Plugin.Cfg; float num = ((_sampler != null) ? _sampler.WalkableArea : 0f); if (num <= 0f) { Plugin.Log.LogWarning((object)$"{list.Count} queued outdoor object(s) dropped: no walkable area was measured."); return; } foreach (var (val, num2) in list) { if ((Object)(object)val == (Object)null) { continue; } int num3 = Mathf.Clamp(Mathf.RoundToInt(num2 * num * cfg.HazardDensityMultiplier.Value), 0, cfg.HazardMaxPerEvent.Value); Plugin.Log.LogInfo((object)($"Placing '{((Object)val).name}': density {num2:0.####} over {num:F0} m^2 " + $"x{cfg.HazardDensityMultiplier.Value:0.##} = {num3}.")); int num4 = 0; for (int i = 0; i < num3; i++) { Vector3? randomPoint = _sampler.GetRandomPoint(cfg.MinDistanceFromPlayers.Value, 12, cfg.UpperFloorSpawnShare.Value); if (randomPoint.HasValue && SpawnHazard(val, randomPoint.Value)) { num4++; } } if (num4 < num3) { Plugin.DebugLog($"'{((Object)val).name}': placed {num4} of {num3} (no room for the rest)."); } } } private bool SpawnHazard(GameObject prefab, Vector3 point) { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) try { Quaternion val = Quaternion.Euler(0f, Random.Range(0f, 360f), 0f); GameObject val2 = Object.Instantiate(prefab, point, val); NetworkObject component = val2.GetComponent(); if ((Object)(object)component != (Object)null) { component.Spawn(true); } _ownedHazards.Add(val2); return true; } catch (Exception ex) { Plugin.Log.LogWarning((object)("Failed to place '" + ((Object)prefab).name + "': " + ex.Message)); return false; } } private void DespawnOwnedHazards() { foreach (GameObject ownedHazard in _ownedHazards) { if ((Object)(object)ownedHazard == (Object)null) { continue; } try { NetworkObject component = ownedHazard.GetComponent(); if ((Object)(object)component != (Object)null && component.IsSpawned) { component.Despawn(true); } else { Object.Destroy((Object)(object)ownedHazard); } } catch (Exception ex) { Plugin.DebugLog("Hazard cleanup failed: " + ex.Message); } } _ownedHazards.Clear(); } private IEnumerator EventLoop() { PluginConfig cfg = Plugin.Cfg; yield return (object)new WaitForSeconds(3f); if (!cfg.HordeEnabled.Value) { yield break; } float num = Mathf.Max(1f, (float)cfg.HordeDelaySeconds.Value - 3f); Plugin.DebugLog($"Horde due in {num:F0}s."); yield return (object)new WaitForSeconds(num); while (true) { yield return RunHorde(); int value = cfg.HordeRepeatSeconds.Value; if (value <= 0) { break; } yield return (object)new WaitForSeconds((float)value); } } private IEnumerator RunHorde() { string wanted = Plugin.Cfg.HordeEnemy.Value; EnemyType enemy = ((IEnumerable)EnemyCatalog.Enemies).FirstOrDefault((Func)((EnemyType e) => string.Equals(e.enemyName, wanted, StringComparison.OrdinalIgnoreCase))); if ((Object)(object)enemy == (Object)null) { Plugin.Log.LogWarning((object)("Horde: '" + wanted + "' is not in the spawn pool — check the [Horde] Enemy name and that it is not barred by ExcludedEnemies. Skipping.")); yield break; } if (_sampler == null) { Plugin.Log.LogWarning((object)"Horde: no navmesh sample available — skipping."); yield break; } int perSide = Mathf.Max(1, Plugin.Cfg.HordeCountPerSide.Value); float value = Plugin.Cfg.MinDistanceFromPlayers.Value; if (!_sampler.TryGetOppositeEdgePoints(perSide, value, out var sideA, out var sideB) && !_sampler.TryGetOppositeEdgePoints(perSide, 0f, out sideA, out sideB)) { Plugin.Log.LogWarning((object)"Horde: could not find usable points at both edges of the map — skipping. On a heavily reshaped Company moon the walkable area can be too fragmented for this."); yield break; } GordionEvents.AnnounceHorde(enemy.enemyName); Plugin.Log.LogInfo((object)$"Horde: spawning {sideA.Count} + {sideB.Count} '{enemy.enemyName}' at opposite map edges."); int most = Mathf.Max(sideA.Count, sideB.Count); for (int i = 0; i < most; i++) { if (i < sideA.Count) { SpawnEnemy(enemy, sideA[i]); } yield return null; if (i < sideB.Count) { SpawnEnemy(enemy, sideB[i]); } yield return null; } } private IEnumerator MaintenanceLoop() { while (true) { yield return (object)new WaitForSeconds(Mathf.Max(1f, Plugin.Cfg.MaintenanceInterval.Value)); try { MaintenanceTick(); } catch (Exception arg) { Plugin.Log.LogError((object)$"Maintenance tick failed: {arg}"); } } } private void MaintenanceTick() { if ((Object)(object)StartOfRound.Instance == (Object)null || StartOfRound.Instance.shipIsLeaving) { return; } ScanForEarlyDeaths(); _ownedEnemies.RemoveAll((EnemyAI e) => (Object)(object)e == (Object)null || e.isEnemyDead); bool value = Plugin.Cfg.TreatEnemiesAsOutside.Value; GameObject[] array = _aiNodes.Where((GameObject n) => (Object)(object)n != (Object)null).ToArray(); foreach (EnemyAI ownedEnemy in _ownedEnemies) { if (!((Object)(object)ownedEnemy == (Object)null) && !ownedEnemy.isEnemyDead && !ownedEnemy.inSpecialAnimation) { if (ownedEnemy.isOutside != value) { ownedEnemy.isOutside = value; Plugin.DebugLog($"Re-applied isOutside={value} to '{ownedEnemy.enemyType?.enemyName}'."); } if (array.Length != 0 && (ownedEnemy.allAINodes == null || ownedEnemy.allAINodes.Length != array.Length || (ownedEnemy.allAINodes.Length != 0 && (Object)(object)ownedEnemy.allAINodes[0] != (Object)(object)array[0]))) { ownedEnemy.allAINodes = array; Plugin.DebugLog("Restored patrol nodes for '" + ownedEnemy.enemyType?.enemyName + "'."); } if (!RescueIfStranded(ownedEnemy)) { UnstickIfIdle(ownedEnemy); } } } _lastMovement.Keys.Where((int id) => _ownedEnemies.All((EnemyAI e) => (Object)(object)e == (Object)null || ((Object)e).GetInstanceID() != id)).ToList().ForEach(delegate(int id) { _lastMovement.Remove(id); }); DriveCadaverBlooms(); EnforceForeignEnemyPolicy(); } private void DriveCadaverBlooms() { //IL_0068: 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_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00f0: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { return; } float value = Plugin.Cfg.CadaverBloomTriggerRange.Value; float num = value * value; foreach (EnemyAI ownedEnemy in _ownedEnemies) { CadaverBloomAI val = (CadaverBloomAI)(object)((ownedEnemy is CadaverBloomAI) ? ownedEnemy : null); if (val == null || ((EnemyAI)val).isEnemyDead || val.hasBurst) { continue; } PlayerControllerB val2 = null; float num2 = num; Vector3 position = ((Component)val).transform.position; PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val3 in allPlayerScripts) { if (!((Object)(object)val3 == (Object)null) && val3.isPlayerControlled && !val3.isPlayerDead) { Vector3 val4 = ((Component)val3).transform.position - position; float sqrMagnitude = ((Vector3)(ref val4)).sqrMagnitude; if (sqrMagnitude < num2) { num2 = sqrMagnitude; val2 = val3; } } } if (!((Object)(object)val2 == (Object)null)) { try { val.BurstForth(val2, false, position, ((Component)val).transform.eulerAngles); Plugin.Log.LogInfo((object)$"Cadaver Bloom burst on a player within {Mathf.Sqrt(num2):F1} m at {position:F1}."); } catch (Exception ex) { Plugin.Log.LogWarning((object)("Cadaver Bloom BurstForth failed: " + ex.Message)); } } } } private void UnstickIfIdle(EnemyAI ai) { //IL_0133: 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_005d: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_00ac: Unknown result type (might be due to invalid IL or missing references) //IL_0104: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ai.agent == (Object)null || !((Behaviour)ai.agent).isActiveAndEnabled) { return; } string text = (((Object)(object)ai.enemyType != (Object)null) ? ai.enemyType.enemyName : string.Empty); if (StationaryByDesign.Contains(text)) { return; } int instanceID = ((Object)ai).GetInstanceID(); Vector3 position = ((Component)ai).transform.position; float realtimeSinceStartup = Time.realtimeSinceStartup; if (!_lastMovement.TryGetValue(instanceID, out (Vector3, float) value)) { _lastMovement[instanceID] = (position, realtimeSinceStartup); return; } Vector3 val = position - value.Item1; if (((Vector3)(ref val)).sqrMagnitude > 2.25f) { _lastMovement[instanceID] = (position, realtimeSinceStartup); } else { if (realtimeSinceStartup - value.Item2 < 25f) { return; } Vector3? randomPoint = _sampler.GetRandomPoint(Plugin.Cfg.MinDistanceFromPlayers.Value, 8, Plugin.Cfg.UpperFloorSpawnShare.Value); if (randomPoint.HasValue) { try { ai.SetDestinationToPosition(randomPoint.Value, false); } catch (Exception ex) { Plugin.DebugLog("Unstick SetDestination failed: " + ex.Message); } _lastMovement[instanceID] = (position, realtimeSinceStartup); Plugin.DebugLog($"Nudged idle '{text}' toward a new point (was stuck ~{25f:F0}s)."); } } } private bool RescueIfStranded(EnemyAI ai) { //IL_0031: Unknown result type (might be due to invalid IL or missing references) //IL_007d: 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_0096: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) if (_sampler == null || (Object)(object)ai.agent == (Object)null || !((Behaviour)ai.agent).isActiveAndEnabled) { return false; } if (_sampler.IsReachable(((Component)ai).transform.position)) { return false; } Vector3? randomPoint = _sampler.GetRandomPoint(Plugin.Cfg.MinDistanceFromPlayers.Value, 8, Plugin.Cfg.UpperFloorSpawnShare.Value); if (!randomPoint.HasValue) { return false; } ai.agent.Warp(randomPoint.Value + Vector3.up * Plugin.Cfg.SpawnYOffset.Value); _lastMovement[((Object)ai).GetInstanceID()] = (randomPoint.Value, Time.realtimeSinceStartup); Plugin.Log.LogInfo((object)("Rescued stranded '" + ai.enemyType?.enemyName + "' — teleported back onto the interior navmesh.")); return true; } private void EnforceForeignEnemyPolicy() { ForeignEnemyPolicy value = Plugin.Cfg.ForeignEnemies.Value; if (value == ForeignEnemyPolicy.Ignore) { return; } RoundManager instance = RoundManager.Instance; if ((Object)(object)instance == (Object)null) { return; } foreach (EnemyAI item in instance.SpawnedEnemies.Where((EnemyAI e) => (Object)(object)e != (Object)null).ToList()) { if (!item.isEnemyDead && !((Object)(object)item.enemyType == (Object)null) && !_ownedEnemies.Contains(item) && !item.inSpecialAnimation && !((Object)(object)item.inSpecialAnimationWithPlayer != (Object)null)) { string enemyName = item.enemyType.enemyName; bool flag = EnemyCatalog.IsExcluded(enemyName); if (!flag && value == ForeignEnemyPolicy.RemoveNotEnabled) { flag = !Plugin.Cfg.For(item.enemyType).Enabled.Value; } if (flag && TryDespawn(instance, item)) { Plugin.Log.LogInfo((object)$"Removed '{enemyName}' spawned by another mod (ForeignEnemies={value})."); } } } } private void CreateAINodes(int count) { //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) for (int i = 0; i < count; i++) { Vector3? randomPoint = _sampler.GetRandomPoint(0f, 15, 50); if (randomPoint.HasValue) { _aiNodes.Add(CreateNode($"MG_AINode_{i}", randomPoint.Value, "AINode")); _aiNodes.Add(CreateNode($"MG_OutsideAINode_{i}", randomPoint.Value, "OutsideAINode")); } } Plugin.Log.LogInfo((object)$"Created {_aiNodes.Count / 2} interior AI patrol nodes (indoor + outdoor tagged)."); } private GameObject CreateNode(string name, Vector3 position, string tagName) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Expected O, but got Unknown //IL_001f: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject(name); val.transform.SetParent(((Component)this).transform, false); val.transform.position = position; try { val.tag = tagName; } catch (Exception ex) { Plugin.DebugLog("Could not tag AI node '" + tagName + "': " + ex.Message); } return val; } private void CreateDeadTreesIfNeeded() { //IL_014c: Unknown result type (might be due to invalid IL or missing references) //IL_0186: Unknown result type (might be due to invalid IL or missing references) //IL_018b: Unknown result type (might be due to invalid IL or missing references) //IL_0195: Expected O, but got Unknown //IL_01b1: Unknown result type (might be due to invalid IL or missing references) //IL_01b6: Unknown result type (might be due to invalid IL or missing references) //IL_01c0: Unknown result type (might be due to invalid IL or missing references) //IL_01c5: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Expected O, but got Unknown //IL_00e6: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.Cfg.FeioparDeadTrees.Value) { return; } EnemyType val = ((IEnumerable)EnemyCatalog.Enemies).FirstOrDefault((Func)((EnemyType e) => string.Equals(e.enemyName, "Feiopar", StringComparison.OrdinalIgnoreCase))); if ((Object)(object)val == (Object)null) { return; } if (!Plugin.Cfg.For(val).Enabled.Value) { Plugin.DebugLog("Feiopar disabled — skipping dead-tree generation."); return; } int value = Plugin.Cfg.FeioparTreeCount.Value; int num = 0; List list = new List(); for (int num2 = 0; num2 < value; num2++) { Vector3? val2 = PickOpenTreePoint(list); if (val2.HasValue) { list.Add(val2.Value); GameObject val3 = new GameObject($"MG_DeadTree_{num2}") { layer = 25 }; val3.transform.SetParent(((Component)this).transform, false); val3.transform.position = val2.Value; try { val3.tag = "Tree"; } catch (Exception ex) { Plugin.DebugLog("Could not tag tree: " + ex.Message); } CapsuleCollider obj = val3.AddComponent(); obj.direction = 1; obj.radius = 0.5f; obj.height = 16f; obj.center = new Vector3(0f, 8f, 0f); ((Collider)obj).isTrigger = false; AttachTreeVisual(val3, 10f); _fakeTrees.Add(val3); GameObject val4 = new GameObject($"MG_DeadTreeCanopy_{num2}") { layer = 25 }; val4.transform.SetParent(((Component)this).transform, false); val4.transform.position = val2.Value + Vector3.up * 16f; SphereCollider obj2 = val4.AddComponent(); obj2.radius = 2f; ((Collider)obj2).isTrigger = false; _fakeTrees.Add(val4); num++; } } ResetPumaTreeCache(val); Plugin.Log.LogInfo((object)($"Feiopar: grew {num} dead trees (tagged 'Tree', layer 25, with collider + canopy) " + "for it to stalk from. Set [Integration] FeioparDeadTrees=false to disable.")); } private Vector3? PickOpenTreePoint(List alreadyPlaced) { //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0088: 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) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) int num = (((Object)(object)StartOfRound.Instance != (Object)null) ? StartOfRound.Instance.collidersAndRoomMaskAndDefault : (-5)); Vector3? val = null; for (int i = 0; i < 40; i++) { Vector3? p = _sampler.GetRandomPoint(0f, 10, 0); if (!p.HasValue) { continue; } bool flag = alreadyPlaced.Any(delegate(Vector3 q) { //IL_0000: 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_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) Vector3 val3 = q - p.Value; return ((Vector3)(ref val3)).sqrMagnitude < 25f; }); bool flag2 = Physics.CheckSphere(p.Value + Vector3.up * 1.5f, 1f, num, (QueryTriggerInteraction)1); bool num2 = SameLevelFloorCount(p.Value, 4f) == 8; bool flag3 = SameLevelFloorCount(p.Value, 7f) >= 6; if (num2 && !flag && !flag2) { Vector3? val2 = val; if (!val2.HasValue) { val = p; } } if (num2 && flag3 && !flag && !flag2) { return p; } } return val; } private static int SameLevelFloorCount(Vector3 center, float radius) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_004e: 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) int num = 0; NavMeshHit val = default(NavMeshHit); for (int i = 0; i < 8; i++) { float num2 = (float)i * MathF.PI * 2f / 8f; if (NavMesh.SamplePosition(center + new Vector3(Mathf.Cos(num2), 0f, Mathf.Sin(num2)) * radius, ref val, 1.2f, -1) && Mathf.Abs(((NavMeshHit)(ref val)).position.y - center.y) <= 1.5f) { num++; } } return num; } private static void AttachTreeVisual(GameObject tree, float targetHeight) { //IL_0013: 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_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_005c: 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_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Expected O, but got Unknown //IL_0090: 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) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Expected O, but got Unknown //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00e4: Unknown result type (might be due to invalid IL or missing references) //IL_00f8: Unknown result type (might be due to invalid IL or missing references) //IL_010c: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Expected O, but got Unknown //IL_0126: Unknown result type (might be due to invalid IL or missing references) //IL_013a: Unknown result type (might be due to invalid IL or missing references) //IL_014e: Unknown result type (might be due to invalid IL or missing references) //IL_0158: Expected O, but got Unknown try { if (!TryAttachRealTree(tree, targetHeight)) { GameObject val = new GameObject("ProcTree"); val.transform.SetParent(tree.transform, false); val.transform.localPosition = Vector3.zero; val.transform.localScale = Vector3.one * (targetHeight / 4.4f); Material trunkMaterial = GetTrunkMaterial(); AddBranch(val, trunkMaterial, new Vector3(0f, 2.2f, 0f), new Vector3(0.35f, 2.2f, 0.35f), Quaternion.identity); AddBranch(val, trunkMaterial, new Vector3(0.35f, 3.6f, 0.1f), new Vector3(0.12f, 1.1f, 0.12f), Quaternion.Euler(0f, 0f, 55f)); AddBranch(val, trunkMaterial, new Vector3(-0.3f, 3.9f, -0.15f), new Vector3(0.1f, 1f, 0.1f), Quaternion.Euler(0f, 120f, -50f)); AddBranch(val, trunkMaterial, new Vector3(0.05f, 4.3f, 0.3f), new Vector3(0.09f, 0.9f, 0.09f), Quaternion.Euler(35f, 0f, 20f)); } } catch (Exception ex) { Plugin.DebugLog("Could not build tree visual: " + ex.Message); } } private static void AddBranch(GameObject tree, Material mat, Vector3 localPos, Vector3 localScale, Quaternion localRot) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_004b: 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) GameObject obj = GameObject.CreatePrimitive((PrimitiveType)2); ((Object)obj).name = "Branch"; Collider component = obj.GetComponent(); if ((Object)(object)component != (Object)null) { Object.Destroy((Object)(object)component); } obj.transform.SetParent(tree.transform, false); obj.transform.localPosition = localPos; obj.transform.localRotation = localRot; obj.transform.localScale = localScale; Renderer component2 = obj.GetComponent(); if ((Object)(object)component2 != (Object)null && (Object)(object)mat != (Object)null) { component2.sharedMaterial = mat; } } private static bool TryAttachRealTree(GameObject tree, float targetHeight) { //IL_019c: Unknown result type (might be due to invalid IL or missing references) //IL_01a2: Expected O, but got Unknown //IL_01ba: Unknown result type (might be due to invalid IL or missing references) //IL_01ca: Unknown result type (might be due to invalid IL or missing references) //IL_01f6: Unknown result type (might be due to invalid IL or missing references) //IL_01fb: Unknown result type (might be due to invalid IL or missing references) //IL_01ff: Unknown result type (might be due to invalid IL or missing references) //IL_023e: Unknown result type (might be due to invalid IL or missing references) //IL_0243: Unknown result type (might be due to invalid IL or missing references) //IL_0247: Unknown result type (might be due to invalid IL or missing references) //IL_0259: Unknown result type (might be due to invalid IL or missing references) //IL_025e: Unknown result type (might be due to invalid IL or missing references) //IL_0269: Unknown result type (might be due to invalid IL or missing references) //IL_0275: Unknown result type (might be due to invalid IL or missing references) //IL_027a: Unknown result type (might be due to invalid IL or missing references) //IL_022d: Unknown result type (might be due to invalid IL or missing references) //IL_0234: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Unknown result type (might be due to invalid IL or missing references) //IL_0107: Unknown result type (might be due to invalid IL or missing references) //IL_010c: Unknown result type (might be due to invalid IL or missing references) if (!_realTreeSearched) { _realTreeSearched = true; try { MeshFilter[] array = Resources.FindObjectsOfTypeAll(); foreach (MeshFilter val in array) { if ((Object)(object)val == (Object)null || (Object)(object)val.sharedMesh == (Object)null) { continue; } string text = (((Object)val.sharedMesh).name + " " + ((Object)((Component)val).gameObject).name).ToLowerInvariant(); if (text.Contains("tree") || text.Contains("leafless") || text.Contains("pine") || text.Contains("birch") || text.Contains("trunk")) { MeshRenderer component = ((Component)val).GetComponent(); if (!((Object)(object)component == (Object)null) && !((Object)(object)((Renderer)component).sharedMaterial == (Object)null)) { _realTreeMesh = val.sharedMesh; _realTreeMaterial = ((Renderer)component).sharedMaterial; _realTreeRotation = ((Component)val).transform.rotation; _realTreeScale = ((Component)val).transform.lossyScale; Plugin.Log.LogInfo((object)("Feiopar: using real tree mesh '" + ((Object)val.sharedMesh).name + "' for visuals.")); break; } } } if ((Object)(object)_realTreeMesh == (Object)null) { Plugin.DebugLog("No real tree mesh loaded — using procedural dead trees."); } } catch (Exception ex) { Plugin.DebugLog("Real-tree search failed: " + ex.Message); } } if ((Object)(object)_realTreeMesh == (Object)null || (Object)(object)_realTreeMaterial == (Object)null) { return false; } GameObject val2 = new GameObject("TreeMesh"); val2.transform.SetParent(tree.transform, false); val2.transform.rotation = _realTreeRotation; val2.transform.localScale = _realTreeScale; val2.AddComponent().sharedMesh = _realTreeMesh; MeshRenderer obj = val2.AddComponent(); ((Renderer)obj).sharedMaterial = _realTreeMaterial; Bounds bounds = ((Renderer)obj).bounds; float y = ((Bounds)(ref bounds)).size.y; if (y > 0.01f) { float num = Mathf.Clamp(targetHeight / y, 0.05f, 20f); Transform transform = val2.transform; transform.localScale *= num; } bounds = ((Renderer)obj).bounds; float y2 = ((Bounds)(ref bounds)).min.y; Transform transform2 = val2.transform; transform2.position += Vector3.up * (tree.transform.position.y - y2); return true; } private static Material GetTrunkMaterial() { //IL_007d: 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_008e: Expected O, but got Unknown //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00d1: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)_trunkMaterial != (Object)null) { return _trunkMaterial; } try { MeshRenderer[] array = Object.FindObjectsOfType(); Color val2 = default(Color); for (int i = 0; i < array.Length; i++) { Material sharedMaterial = ((Renderer)array[i]).sharedMaterial; if (!((Object)(object)sharedMaterial == (Object)null) && !((Object)(object)sharedMaterial.shader == (Object)null) && (((Object)sharedMaterial.shader).name.IndexOf("HDRP", StringComparison.OrdinalIgnoreCase) >= 0 || ((Object)sharedMaterial.shader).name.IndexOf("Lit", StringComparison.OrdinalIgnoreCase) >= 0)) { Material val = new Material(sharedMaterial) { name = "MG_DeadTreeBark" }; ((Color)(ref val2))..ctor(0.16f, 0.11f, 0.08f); if (val.HasProperty("_BaseColor")) { val.SetColor("_BaseColor", val2); } if (val.HasProperty("_Color")) { val.SetColor("_Color", val2); } if (val.HasProperty("_BaseColorMap")) { val.SetTexture("_BaseColorMap", (Texture)null); } if (val.HasProperty("_MainTex")) { val.SetTexture("_MainTex", (Texture)null); } _trunkMaterial = val; Plugin.DebugLog("Dead-tree material cloned from shader '" + ((Object)sharedMaterial.shader).name + "'."); return _trunkMaterial; } } Plugin.DebugLog("No HDRP material found in scene for dead-tree trunks (will render default)."); } catch (Exception ex) { Plugin.DebugLog("GetTrunkMaterial failed: " + ex.Message); } return null; } private static void ResetPumaTreeCache(EnemyType feiopar) { try { Type type = ((object)(((Object)(object)feiopar.enemyPrefab != (Object)null) ? feiopar.enemyPrefab.GetComponentInChildren(true) : null))?.GetType(); FieldInfo fieldInfo = type?.GetField("AllTreeNodes", BindingFlags.Static | BindingFlags.Public); if (fieldInfo != null) { fieldInfo.SetValue(null, null); Plugin.DebugLog("Reset " + type.Name + ".AllTreeNodes so it rebuilds from our fake trees."); } else { Plugin.DebugLog("Could not find PumaAI.AllTreeNodes to reset (field renamed?)."); } } catch (Exception ex) { Plugin.DebugLog("ResetPumaTreeCache failed: " + ex.Message); } } private void DespawnAllEnemies() { RoundManager instance = RoundManager.Instance; if ((Object)(object)instance == (Object)null) { return; } List list = instance.SpawnedEnemies.Where((EnemyAI e) => (Object)(object)e != (Object)null).ToList(); int num = 0; foreach (EnemyAI item in list) { if (TryDespawn(instance, item)) { num++; } } instance.SpawnedEnemies.RemoveAll((EnemyAI e) => (Object)(object)e == (Object)null); if (num > 0) { Plugin.Log.LogInfo((object)$"RespawnOnLoad: despawned {num} pre-existing enemies."); } } private void DespawnOwnedEnemies() { RoundManager instance = RoundManager.Instance; if ((Object)(object)instance == (Object)null) { return; } int num = 0; foreach (EnemyAI item in _ownedEnemies.Where((EnemyAI e) => (Object)(object)e != (Object)null).ToList()) { if (TryDespawn(instance, item)) { num++; } } if (num > 0) { Plugin.DebugLog($"Ship leaving: despawned {num} of our enemies."); } } private static bool TryDespawn(RoundManager rm, EnemyAI enemy) { try { NetworkObject val = enemy.thisNetworkObject; if ((Object)(object)val == (Object)null) { val = ((Component)enemy).GetComponent(); } if ((Object)(object)val != (Object)null && val.IsSpawned) { rm.DespawnEnemyOnServer(val); return true; } } catch (Exception ex) { Plugin.Log.LogWarning((object)("Failed to despawn '" + enemy.enemyType?.enemyName + "': " + ex.Message)); } return false; } } internal static class EnemyCatalog { private static readonly HashSet HardExcluded = new HashSet(StringComparer.OrdinalIgnoreCase) { "Lasso", "Red pill" }; private static readonly Dictionary Notes = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["Bush Wolf"] = "BushWolfEnemy needs vain shrouds to hide in. The mod now grows them on Gordion at save load (see [Integration] VainShroudIterations), so enable Bush Wolf and reload the save / fly fresh — enabling it mid-round is too late for weed generation", ["Cadaver Growths"] = "Intentionally unsupported: CadaverGrowthAI hard-requires a DunGen dungeon, which the Company building is not (even BrutalCompanyMinus's own Cadaver event can't bypass it). Use Cadaver Bloom traps (CadaverBloomTraps) for a working cadaver on Gordion", ["Cadaver Bloom"] = "Enabled via [Integration] CadaverBloomTraps: planted directly as standalone corpse traps that burst and chase when a player walks close, so no Growth/dungeon is needed", ["Feiopar"] = "PumaAI hunts from 'Tree'-tagged colliders on layer 25. With [Integration] FeioparDeadTrees the mod grows proper dead trees (tag + layer 25 collider + canopy) so it perches ~3 m up on a tree and pounces players who come near", ["Earth Leviathan"] = "SandWormAI only emerges through natural ground, which the Company interior lacks. With [Balance] EarthLeviathanFloorEmerge the mod lets it breach up through the building floor" }; private static readonly HashSet ListedNames = new HashSet(StringComparer.OrdinalIgnoreCase); private static bool _whitelistMode; internal static readonly List Enemies = new List(); internal static bool IsExcluded(string enemyName) { if (enemyName == null) { return false; } if (HardExcluded.Contains(enemyName)) { return true; } if (!_whitelistMode) { return ListedNames.Contains(enemyName); } return !ListedNames.Contains(enemyName); } internal static string AIClassName(EnemyType type) { try { EnemyAI val = (((Object)(object)type.enemyPrefab != (Object)null) ? type.enemyPrefab.GetComponentInChildren(true) : null); return ((Object)(object)val != (Object)null) ? ((object)val).GetType().Name : ""; } catch (Exception ex) { return ""; } } internal static void Resolve() { Enemies.Clear(); ListedNames.Clear(); _whitelistMode = Plugin.Cfg.ExcludedEnemiesIsWhitelist.Value; foreach (string item in from s in (Plugin.Cfg.ExcludedEnemies.Value ?? string.Empty).Split(',') select s.Trim() into s where s.Length > 0 select s) { ListedNames.Add(item); } Plugin.Log.LogInfo((object)(_whitelistMode ? $"Enemy list is a WHITELIST of {ListedNames.Count} name(s): only these may spawn." : $"Enemy list is a blacklist of {ListedNames.Count} name(s).")); if (_whitelistMode && ListedNames.Count == 0) { Plugin.Log.LogWarning((object)"ExcludedEnemiesIsWhitelist=true but ExcludedEnemies is empty — nothing will spawn."); } HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); List list = new List(); EnemyType[] array = Resources.FindObjectsOfTypeAll(); foreach (EnemyType val in array) { if (!((Object)(object)val == (Object)null) && !string.IsNullOrWhiteSpace(val.enemyName) && hashSet.Add(val.enemyName)) { if ((Object)(object)val.enemyPrefab == (Object)null) { list.Add("'" + val.enemyName + "' [no prefab] — the EnemyType asset has no enemyPrefab, so it cannot be instantiated"); } else if (IsExcluded(val.enemyName)) { string text = (HardExcluded.Contains(val.enemyName) ? "built-in exclusion" : (_whitelistMode ? "not on the whitelist" : "on the blacklist")); list.Add("'" + val.enemyName + "' [" + AIClassName(val) + "] — excluded (" + text + ")"); } else { Enemies.Add(val); } } } Enemies.Sort((EnemyType a, EnemyType b) => string.CompareOrdinal(a.enemyName, b.enemyName)); foreach (EnemyType enemy in Enemies) { Plugin.Cfg.For(enemy); } Plugin.Cfg.FinishLegacyCapMigration(); Plugin.Log.LogInfo((object)($"Enemy catalog resolved: {Enemies.Count} spawnable types " + $"({list.Count} rejected).")); LogSpawnabilityReport(list); } private static void LogSpawnabilityReport(List rejected) { foreach (string item in rejected) { Plugin.Log.LogInfo((object)(" rejected: " + item)); } List list = new List(); foreach (EnemyType enemy in Enemies) { EnemySpawnSettings enemySpawnSettings = Plugin.Cfg.For(enemy); string text = null; if (!enemySpawnSettings.Enabled.Value) { text = "Enabled = false"; } else if (enemySpawnSettings.SpawnWeight.Value <= 0) { text = "SpawnWeight = 0"; } else if (enemySpawnSettings.MaxSpawnCount.Value <= 0) { text = "MaxSpawnCount = 0"; } Notes.TryGetValue(enemy.enemyName, out var value); if (text != null) { if (Plugin.Cfg.DebugMode.Value) { Plugin.DebugLog(" off: '" + enemy.enemyName + "' [" + AIClassName(enemy) + "] — " + text + ((value != null) ? ("; note: " + value) : string.Empty)); } continue; } list.Add($"{enemy.enemyName} (w{enemySpawnSettings.SpawnWeight.Value}, max {enemySpawnSettings.MaxSpawnCount.Value}" + (enemy.isOutsideEnemy ? ", outdoor" : ", indoor") + ")"); if (value != null) { Plugin.Log.LogWarning((object)("'" + enemy.enemyName + "' [" + AIClassName(enemy) + "] is enabled, note: " + value + ".")); } } Plugin.Log.LogInfo((object)((list.Count > 0) ? ("Spawn pool: " + string.Join(", ", list)) : "Spawn pool is EMPTY — nothing is enabled with a non-zero weight and max count.")); } } internal static class GordionEvents { private const string HordeColor = "#cc0000"; private static readonly string[] HordeLines = new string[3] { "Something has been wearing their faces for a while now.", "They came in from both ends of the dock. All of them.", "You stayed too long." }; internal static void Reset() { } internal static void RollForLanding() { Reset(); if (!Plugin.Cfg.EnableStockEvents.Value) { return; } BcmeCompat.Scan(); List<(string, string, int, object)> list = new List<(string, string, int, object)>(); CollectStockCandidates(list); if (list.Count == 0) { BcmeCompat.PublishCurrentEvent(null); Plugin.Log.LogInfo((object)"No event is eligible on the Company moon this landing — see the reasons above."); return; } (string, string, int, object) tuple = PickWeighted(list); Plugin.Log.LogInfo((object)("Company moon event chosen: '" + tuple.Item2 + "' " + $"(from {list.Count} eligible candidate(s)).")); if (!BcmeCompat.Execute(tuple.Item4, tuple.Item1)) { BcmeCompat.PublishCurrentEvent(null); } else { BcmeCompat.PublishCurrentEvent(tuple.Item4); } } private static void CollectStockCandidates(List<(string name, string display, int weight, object mEvent)> candidates) { if (!BcmeCompat.Present) { Plugin.Log.LogInfo((object)"EnableStockEvents is on but BrutalCompanyMinus is not installed — skipping."); return; } if (!BcmeCompat.EventsUsable) { Plugin.Log.LogWarning((object)"EnableStockEvents is on but BCMER's event API is unavailable — skipping."); return; } foreach (string item in Plugin.Cfg.ResolveStockEventWhitelist()) { string reason; object obj = BcmeCompat.TryResolve(item, out reason); if (obj == null) { Plugin.Log.LogInfo((object)(" stock event '" + item + "' skipped: " + reason + ".")); } else { int num = Mathf.Max(1, BcmeCompat.WeightOf(item)); candidates.Add((item, item, num, obj)); Plugin.DebugLog($" stock event '{item}' eligible ({BcmeCompat.TypeOf(item)}, w{num})."); } } } private static (string name, string display, int weight, object mEvent) PickWeighted(List<(string name, string display, int weight, object mEvent)> candidates) { long num = 0L; foreach (var candidate in candidates) { num += candidate.weight; } if (num <= 0) { return candidates[Random.Range(0, candidates.Count)]; } long num2 = (long)(Random.value * (float)num); foreach (var candidate2 in candidates) { num2 -= candidate2.weight; if (num2 < 0) { return candidate2; } } return candidates[candidates.Count - 1]; } internal static void AnnounceHorde(string enemyName) { Say(Format(string.IsNullOrWhiteSpace(enemyName) ? "Horde" : (enemyName + " Horde"), "#cc0000", HordeLines[Random.Range(0, HordeLines.Length)]), Plugin.Cfg.HordeAnnounce.Value); } private static string Format(string title, string colorHex, string description) { if (string.IsNullOrWhiteSpace(colorHex) || colorHex[0] != '#') { colorHex = "#ffffff"; } string text = "" + title + ""; if (!string.IsNullOrWhiteSpace(description)) { return text + ": " + description; } return text; } private static void Say(string message, bool allowed) { if (!allowed) { return; } try { if ((Object)(object)HUDManager.Instance != (Object)null) { HUDManager.Instance.AddChatMessage(message, "", -1, false); } } catch (Exception ex) { Plugin.DebugLog("Event announcement failed: " + ex.Message); } } } internal sealed class LandingWatcher : MonoBehaviour { private const float PollInterval = 0.5f; private const float FallbackLandedAfter = 20f; private float _nextPoll; private bool _landedHandled; private float _fallbackTimer; internal static void Create() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown //IL_0010: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("MonstersGordion_LandingWatcher"); Object.DontDestroyOnLoad((Object)val); ((Object)val).hideFlags = (HideFlags)61; val.AddComponent(); } private void Update() { if (Time.unscaledTime < _nextPoll) { return; } _nextPoll = Time.unscaledTime + 0.5f; try { Poll(); } catch (Exception ex) { Plugin.DebugLog("LandingWatcher poll failed: " + ex.Message); } } private void Poll() { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { ResetLandingState(); return; } bool flag = instance.shipHasLanded && !instance.shipIsLeaving; if (!flag && !instance.shipIsLeaving && !instance.inShipPhase && IsCompanySceneLoaded()) { _fallbackTimer += 0.5f; if (_fallbackTimer >= 20f && !_landedHandled) { Plugin.Log.LogWarning((object)"shipHasLanded was never set (another mod likely altered the landing sequence) — starting the spawner via fallback detection."); flag = true; } } else { _fallbackTimer = 0f; } if (flag && !_landedHandled) { _landedHandled = true; CompanyMonsterSpawner.OnShipLanded(); } else if (!flag && _landedHandled && (instance.shipIsLeaving || !instance.shipHasLanded)) { ResetLandingState(); CompanyMonsterSpawner.OnShipLeaving(); } } private void ResetLandingState() { _landedHandled = false; _fallbackTimer = 0f; } private static bool IsCompanySceneLoaded() { //IL_000e: 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) if (!CompanyMonsterSpawner.IsCompanyLevel()) { return false; } Scene sceneByName = SceneManager.GetSceneByName("CompanyBuilding"); if (((Scene)(ref sceneByName)).IsValid()) { return ((Scene)(ref sceneByName)).isLoaded; } return false; } } internal sealed class NavMeshSampler { private sealed class Tier { public readonly List<(int a, int b, int c)> Triangles = new List<(int, int, int)>(); public readonly List CumulativeAreas = new List(); public float TotalArea; public void Add((int a, int b, int c) triangle, float area) { Triangles.Add(triangle); TotalArea += area; CumulativeAreas.Add(TotalArea); } } private const float EdgeSliceFraction = 0.2f; private const float EdgeSpacing = 3f; private readonly bool _requireIndoor; private readonly int _roofMask; private readonly NavMeshPath _pathCache = new NavMeshPath(); private Vector3[] _vertices; private readonly Tier _upper = new Tier(); private readonly Tier _lower = new Tier(); private Vector3? _anchor; public Vector3? Anchor => _anchor; public bool HasUpperTier => _upper.Triangles.Count > 0; public bool HasLowerTier => _lower.Triangles.Count > 0; public float WalkableArea => _upper.TotalArea + _lower.TotalArea; public NavMeshSampler(bool requireIndoor) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown _requireIndoor = requireIndoor; _roofMask = BuildRoofMask(); } private static int BuildRoofMask() { int num = 0; string[] array = new string[5] { "Room", "Colliders", "Default", "MapHazards", "Terrain" }; for (int i = 0; i < array.Length; i++) { int num2 = LayerMask.NameToLayer(array[i]); if (num2 >= 0) { num |= 1 << num2; } } if (num != 0) { return num; } return -5; } public int Build() { //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0071: 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_0170: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Unknown result type (might be due to invalid IL or missing references) //IL_00bf: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_00dd: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Unknown result type (might be due to invalid IL or missing references) //IL_00e4: Unknown result type (might be due to invalid IL or missing references) //IL_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00ed: Unknown result type (might be due to invalid IL or missing references) //IL_00ef: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_0119: 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_0120: Unknown result type (might be due to invalid IL or missing references) //IL_012a: Unknown result type (might be due to invalid IL or missing references) //IL_012f: Unknown result type (might be due to invalid IL or missing references) //IL_0131: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Unknown result type (might be due to invalid IL or missing references) //IL_0143: Unknown result type (might be due to invalid IL or missing references) //IL_0206: Unknown result type (might be due to invalid IL or missing references) //IL_02fd: Unknown result type (might be due to invalid IL or missing references) //IL_0302: Unknown result type (might be due to invalid IL or missing references) //IL_0354: Unknown result type (might be due to invalid IL or missing references) //IL_0368: Unknown result type (might be due to invalid IL or missing references) //IL_033d: Unknown result type (might be due to invalid IL or missing references) //IL_0342: Unknown result type (might be due to invalid IL or missing references) //IL_0397: Unknown result type (might be due to invalid IL or missing references) //IL_03ae: Unknown result type (might be due to invalid IL or missing references) //IL_03c4: Unknown result type (might be due to invalid IL or missing references) //IL_054d: Unknown result type (might be due to invalid IL or missing references) //IL_0552: Unknown result type (might be due to invalid IL or missing references) _upper.Triangles.Clear(); _upper.CumulativeAreas.Clear(); _upper.TotalArea = 0f; _lower.Triangles.Clear(); _lower.CumulativeAreas.Clear(); _lower.TotalArea = 0f; _anchor = null; NavMeshTriangulation val = NavMesh.CalculateTriangulation(); _vertices = val.vertices; List<(int, int, int, float, Vector3)> list = new List<(int, int, int, float, Vector3)>(); float num = 0f; Vector3 val5; for (int i = 0; i + 2 < val.indices.Length; i += 3) { int num2 = val.indices[i]; int num3 = val.indices[i + 1]; int num4 = val.indices[i + 2]; Vector3 val2 = _vertices[num2]; Vector3 val3 = _vertices[num3]; Vector3 val4 = _vertices[num4]; val5 = Vector3.Cross(val3 - val2, val4 - val2); float num5 = ((Vector3)(ref val5)).magnitude * 0.5f; if (!(num5 < 0.05f)) { Vector3 val6 = (val2 + val3 + val4) / 3f; if (!IsInsideShip(val6) && (!_requireIndoor || HasCeilingAbove(val6))) { list.Add((num2, num3, num4, num5, val6)); num += num5; } } } if (list.Count == 0) { return 0; } int[] array = new int[list.Count]; for (int j = 0; j < array.Length; j++) { array[j] = j; } Dictionary dictionary = new Dictionary(); for (int k = 0; k < list.Count; k++) { int[] array2 = new int[3] { list[k].Item1, list[k].Item2, list[k].Item3 }; foreach (int num6 in array2) { long key = WeldKey(_vertices[num6]); if (dictionary.TryGetValue(key, out var value)) { Union(array, k, value); } else { dictionary[key] = k; } } } Dictionary dictionary2 = new Dictionary(); for (int m = 0; m < list.Count; m++) { int key2 = Find(array, m); dictionary2.TryGetValue(key2, out var value2); dictionary2[key2] = value2 + list[m].Item4; } int num7 = -1; float num8 = 0f; foreach (KeyValuePair item in dictionary2) { if (item.Value > num8) { num8 = item.Value; num7 = item.Key; } } float num9 = 0f; Vector3 val7 = Vector3.zero; for (int n = 0; n < list.Count; n++) { if (Find(array, n) == num7 && list[n].Item4 > num9) { num9 = list[n].Item4; val7 = list[n].Item5; } } NavMeshHit val8 = default(NavMeshHit); if (NavMesh.SamplePosition(val7, ref val8, 5f, -1)) { _anchor = ((NavMeshHit)(ref val8)).position; } float num10 = DetermineSplitY(list); int num11 = 0; NavMeshHit val9 = default(NavMeshHit); foreach (var item2 in list) { if (!NavMesh.SamplePosition(item2.Item5, ref val9, 3f, -1) || !IsReachable(((NavMeshHit)(ref val9)).position)) { num11++; } else { ((item2.Item5.y >= num10) ? _upper : _lower).Add((a: item2.Item1, b: item2.Item2, c: item2.Item3), item2.Item4); } } int num12 = _upper.Triangles.Count + _lower.Triangles.Count; ManualLogSource log = Plugin.Log; string[] obj = new string[8] { $"NavMeshSampler: kept {num12}/{list.Count} triangles ", $"({_upper.TotalArea + _lower.TotalArea:F0} of {num:F0} m², ", $"{dictionary2.Count} regions, {num11} unreachable). ", $"Tiers @ splitY={num10:F1}: upper {_upper.Triangles.Count} tris / {_upper.TotalArea:F0} m², ", $"lower {_lower.Triangles.Count} tris / {_lower.TotalArea:F0} m². ", "Anchor=", null, null }; object obj2; if (!_anchor.HasValue) { obj2 = "NONE"; } else { val5 = _anchor.Value; obj2 = ((Vector3)(ref val5)).ToString("F1"); } obj[6] = (string)obj2; obj[7] = "."; log.LogInfo((object)string.Concat(obj)); return num12; } private static float DetermineSplitY(List<(int a, int b, int c, float area, Vector3 centroid)> candidates) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) try { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance != (Object)null && (Object)(object)instance.shipBounds != (Object)null) { Bounds bounds = instance.shipBounds.bounds; return ((Bounds)(ref bounds)).min.y - 3f; } } catch { } List list = new List(candidates.Count); foreach (var candidate in candidates) { list.Add(candidate.centroid.y); } list.Sort(); return list[list.Count / 2]; } public Vector3? GetRandomPoint(float minPlayerDistance, int attempts, int upperSharePercent) { //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_0085: Unknown result type (might be due to invalid IL or missing references) if (_upper.Triangles.Count == 0 && _lower.Triangles.Count == 0) { return null; } for (int i = 0; i < attempts; i++) { bool flag = Random.Range(0, 100) < upperSharePercent; Tier tier = (flag ? _upper : _lower); if (tier.Triangles.Count == 0) { tier = (flag ? _lower : _upper); } Vector3 candidate = RandomPointInTriangle(tier); if (TryValidate(candidate, minPlayerDistance, out var point)) { return point; } } return null; } public bool TryGetOppositeEdgePoints(int perSide, float minPlayerDistance, out List sideA, out List sideB) { //IL_0089: Unknown result type (might be due to invalid IL or missing references) //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00a0: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: 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_00c6: Unknown result type (might be due to invalid IL or missing references) //IL_00c8: Unknown result type (might be due to invalid IL or missing references) //IL_00da: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_00e4: 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_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_010b: Unknown result type (might be due to invalid IL or missing references) sideA = new List(); sideB = new List(); if (perSide <= 0) { return false; } List<((int, int, int), Vector3)> list = new List<((int, int, int), Vector3)>(); float num = float.MaxValue; float num2 = float.MinValue; float num3 = float.MaxValue; float num4 = float.MinValue; Tier[] array = new Tier[2] { _upper, _lower }; for (int i = 0; i < array.Length; i++) { foreach (var triangle in array[i].Triangles) { Vector3 val = (_vertices[triangle.a] + _vertices[triangle.b] + _vertices[triangle.c]) / 3f; if (val.x < num) { num = val.x; } if (val.x > num2) { num2 = val.x; } if (val.z < num3) { num3 = val.z; } if (val.z > num4) { num4 = val.z; } list.Add((triangle, val)); } } if (list.Count < 2) { return false; } bool useX = num2 - num >= num4 - num3; list.Sort((((int a, int b, int c) tri, Vector3 centroid) l, ((int a, int b, int c) tri, Vector3 centroid) r) => (useX ? l.centroid.x : l.centroid.z).CompareTo(useX ? r.centroid.x : r.centroid.z)); int num5 = Mathf.Clamp(Mathf.RoundToInt((float)list.Count * 0.2f), 1, list.Count / 2); FillEdge(list, 0, num5, perSide, minPlayerDistance, sideA); FillEdge(list, list.Count - num5, list.Count, perSide, minPlayerDistance, sideB); Plugin.DebugLog("Edge sampling on " + (useX ? "X" : "Z") + " axis, span " + $"{(useX ? (num2 - num) : (num4 - num3)):F0} m over {list.Count} triangles " + $"(slice {num5}): got {sideA.Count} + {sideB.Count} of {perSide} per side."); if (sideA.Count > 0) { return sideB.Count > 0; } return false; } private void FillEdge(List<((int a, int b, int c) tri, Vector3 centroid)> tris, int from, int to, int wanted, float minPlayerDistance, List into) { //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) int num = wanted * 12; for (int i = 0; i < num; i++) { if (into.Count >= wanted) { break; } (int, int, int) item = tris[Random.Range(from, to)].tri; if (!TryValidate(RandomPointInTriangle(item), minPlayerDistance, out var point)) { continue; } bool flag = false; foreach (Vector3 item2 in into) { Vector3 val = item2 - point; if (((Vector3)(ref val)).sqrMagnitude < 9f) { flag = true; break; } } if (!flag) { into.Add(point); } } } private bool TryValidate(Vector3 candidate, float minPlayerDistance, out Vector3 point) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0002: 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_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) point = candidate; NavMeshHit val = default(NavMeshHit); if (!NavMesh.SamplePosition(candidate, ref val, 3f, -1)) { return false; } point = ((NavMeshHit)(ref val)).position; if (IsInsideShip(point)) { return false; } if (_requireIndoor && !HasCeilingAbove(point)) { return false; } if (minPlayerDistance > 0f && IsTooCloseToAnyPlayer(point, minPlayerDistance)) { return false; } if (!IsReachable(point)) { Plugin.DebugLog($"Rejected unreachable point {point:F1}."); return false; } return true; } public bool IsReachable(Vector3 point) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Invalid comparison between Unknown and I4 if (!_anchor.HasValue) { return true; } _pathCache.ClearCorners(); if (NavMesh.CalculatePath(point, _anchor.Value, -1, _pathCache)) { return (int)_pathCache.status == 0; } return false; } private Vector3 RandomPointInTriangle((int a, int b, int c) triangle) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) //IL_003a: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) float num = Mathf.Sqrt(Random.value); float value = Random.value; return _vertices[triangle.a] * (1f - num) + _vertices[triangle.b] * (num * (1f - value)) + _vertices[triangle.c] * (num * value); } private Vector3 RandomPointInTriangle(Tier tier) { //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_006e: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_009a: 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) float value = Random.value * tier.TotalArea; int index = LowerBound(tier.CumulativeAreas, value); (int a, int b, int c) tuple = tier.Triangles[index]; int item = tuple.a; int item2 = tuple.b; int item3 = tuple.c; float num = Mathf.Sqrt(Random.value); float value2 = Random.value; return _vertices[item] * (1f - num) + _vertices[item2] * (num * (1f - value2)) + _vertices[item3] * (num * value2); } private static long WeldKey(Vector3 v) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) long num = (long)Mathf.Round(v.x * 10f); long num2 = (long)Mathf.Round(v.y * 10f); long num3 = (long)Mathf.Round(v.z * 10f); return (num & 0x1FFFFF) | ((num2 & 0x1FFFFF) << 21) | ((num3 & 0x1FFFFF) << 42); } private static int Find(int[] parent, int i) { while (parent[i] != i) { parent[i] = parent[parent[i]]; i = parent[i]; } return i; } private static void Union(int[] parent, int a, int b) { int num = Find(parent, a); int num2 = Find(parent, b); if (num != num2) { parent[num] = num2; } } private static int LowerBound(List sorted, float value) { int num = 0; int num2 = sorted.Count - 1; while (num < num2) { int num3 = (num + num2) / 2; if (sorted[num3] < value) { num = num3 + 1; } else { num2 = num3; } } return num; } private bool HasCeilingAbove(Vector3 point) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) return Physics.Raycast(point + Vector3.up * 0.3f, Vector3.up, 60f, _roofMask, (QueryTriggerInteraction)1); } private static bool IsInsideShip(Vector3 point) { //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance.shipBounds == (Object)null) { return false; } Bounds bounds = instance.shipBounds.bounds; ((Bounds)(ref bounds)).Expand(4f); return ((Bounds)(ref bounds)).Contains(point); } private static bool IsTooCloseToAnyPlayer(Vector3 point, float minDistance) { //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { return false; } float num = minDistance * minDistance; PlayerControllerB[] allPlayerScripts = instance.allPlayerScripts; foreach (PlayerControllerB val in allPlayerScripts) { if (!((Object)(object)val == (Object)null) && val.isPlayerControlled && !val.isPlayerDead) { Vector3 val2 = ((Component)val).transform.position - point; if (((Vector3)(ref val2)).sqrMagnitude < num) { return true; } } } return false; } } [BepInPlugin("Timofey.MonstersGordion", "MonstersGordion", "1.9.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { private Harmony _harmony; internal static Plugin Instance { get; private set; } internal static ManualLogSource Log { get; private set; } internal static PluginConfig Cfg { get; private set; } private void Awake() { //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_0031: Expected O, but got Unknown Instance = this; Log = ((BaseUnityPlugin)this).Logger; Cfg = new PluginConfig(((BaseUnityPlugin)this).Config); _harmony = new Harmony("Timofey.MonstersGordion"); _harmony.PatchAll(typeof(StartOfRoundPatches)); _harmony.PatchAll(typeof(EnemyAIPatches)); _harmony.PatchAll(typeof(MoldSpreadPatches)); _harmony.PatchAll(typeof(RoundManagerPatches)); LandingWatcher.Create(); Log.LogInfo((object)"MonstersGordion v1.9.0 loaded. Waiting for a landing on 71-Gordion (Company building)."); } internal static void DebugLog(string message) { if (Cfg != null && Cfg.DebugMode.Value) { Log.LogInfo((object)("[Debug] " + message)); } } private void OnDestroy() { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } } internal enum ForeignEnemyPolicy { Ignore, RemoveExcluded, RemoveNotEnabled } internal sealed class EnemySpawnSettings { public ConfigEntry Enabled; public ConfigEntry SpawnWeight; public ConfigEntry MinSpawnCount; public ConfigEntry MaxSpawnCount; } internal sealed class PluginConfig { public readonly ConfigEntry GlobalCap; public readonly ConfigEntry MinSpawnInterval; public readonly ConfigEntry MaxSpawnInterval; public readonly ConfigEntry RespawnOnLoad; public readonly ConfigEntry DebugMode; public readonly ConfigEntry UpperFloorSpawnShare; public readonly ConfigEntry OutsideEnemyShare; public readonly ConfigEntry OldBirdUpperFloorOnly; public readonly ConfigEntry AllowHarmlessCreatures; public readonly ConfigEntry EarthLeviathanFloorEmerge; public readonly ConfigEntry CoilHeadTurretChance; public readonly ConfigEntry CoilHeadSlayerChance; public readonly ConfigEntry ManticoilTurretChance; public readonly ConfigEntry ManticoilSlayerChance; public readonly ConfigEntry MaskedTurretChance; public readonly ConfigEntry MaskedSlayerChance; public readonly ConfigEntry VainShroudIterations; public readonly ConfigEntry VainShroudPatches; public readonly ConfigEntry FeioparDeadTrees; public readonly ConfigEntry FeioparTreeCount; public readonly ConfigEntry CadaverBloomTraps; public readonly ConfigEntry CadaverBloomTriggerRange; public readonly ConfigEntry EnableStockEvents; public readonly ConfigEntry StockEventWhitelist; public readonly ConfigEntry HazardDensityMultiplier; public readonly ConfigEntry HazardMaxPerEvent; public readonly ConfigEntry HordeEnabled; public readonly ConfigEntry HordeEnemy; public readonly ConfigEntry HordeCountPerSide; public readonly ConfigEntry HordeDelaySeconds; public readonly ConfigEntry HordeRepeatSeconds; public readonly ConfigEntry HordeAnnounce; public readonly ConfigEntry DespawnOnShipLeave; public readonly ConfigEntry CountForeignEnemies; public readonly ConfigEntry MinDistanceFromPlayers; public readonly ConfigEntry SpawnYOffset; public readonly ConfigEntry AINodeCount; public readonly ConfigEntry RequireIndoorPoints; public readonly ConfigEntry ExcludedEnemies; public readonly ConfigEntry ExcludedEnemiesIsWhitelist; public readonly ConfigEntry TreatEnemiesAsOutside; public readonly ConfigEntry MaintenanceInterval; public readonly ConfigEntry ForeignEnemies; public readonly ConfigEntry LegacyCapsMigrated; private readonly ConfigFile _file; private readonly Dictionary _enemySettings = new Dictionary(StringComparer.OrdinalIgnoreCase); private const string DefaultStockEvents = "Nothing, Gloomy, Meteors, OutsideLandmines, OutsideTurrets, AllWeather, DoorFailure, ShipLightsFailure, ManualCameraFailure, IsMetal, LeaflessTrees, LeaflessBrownTrees"; private const string LegacyStockEvents = "Nothing, Gloomy, Meteors, OutsideLandmines, OutsideTurrets, Warzone, AllWeather"; private const int MaxSpawnDefault = 40; private static readonly (bool en, int w, int min, int max) UnknownEnemyDefaults = (en: false, w: 10, min: 0, max: 1); private static readonly Dictionary VanillaDefaults = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["Flowerman"] = (true, 10, 0, 1), ["Crawler"] = (true, 15, 0, 2), ["Hoarding bug"] = (true, 15, 0, 3), ["Centipede"] = (true, 15, 0, 3), ["Bunker Spider"] = (true, 12, 0, 2), ["Spring"] = (true, 10, 0, 2), ["Girl"] = (true, 3, 0, 1), ["Puffer"] = (true, 12, 0, 2), ["Nutcracker"] = (true, 8, 0, 2), ["Jester"] = (true, 2, 0, 1), ["Masked"] = (true, 8, 0, 2), ["Blob"] = (true, 8, 0, 1), ["Butler"] = (true, 8, 0, 2), ["Clay Surgeon"] = (true, 6, 0, 2), ["Maneater"] = (true, 4, 0, 1), ["CaveDweller"] = (true, 4, 0, 1), ["Stingray"] = (true, 8, 0, 2), ["Tulip Snake"] = (true, 10, 0, 3), ["Flowersnake"] = (true, 10, 0, 3), ["Baboon hawk"] = (true, 8, 0, 2), ["MouthDog"] = (true, 5, 0, 1), ["ForestGiant"] = (true, 3, 0, 1), ["RadMech"] = (true, 3, 0, 1), ["Old Bird"] = (true, 3, 0, 1), ["GiantKiwi"] = (true, 4, 0, 1), ["Bush Wolf"] = (false, 6, 0, 1), ["Manticoil"] = (false, 10, 0, 3), ["Red Locust Bees"] = (true, 4, 0, 1), ["Docile Locust Bees"] = (false, 5, 0, 2), ["Butler Bees"] = (true, 3, 0, 1), ["Earth Leviathan"] = (false, 2, 0, 1), ["Feiopar"] = (false, 6, 0, 1), ["Cadaver Bloom"] = (false, 4, 0, 2) }; public int ToilHeadTurretChance(ToilHeadCompat.Kind kind) { return kind switch { ToilHeadCompat.Kind.CoilHead => CoilHeadTurretChance.Value, ToilHeadCompat.Kind.Manticoil => ManticoilTurretChance.Value, ToilHeadCompat.Kind.Masked => MaskedTurretChance.Value, _ => 0, }; } public int ToilHeadSlayerChance(ToilHeadCompat.Kind kind) { return kind switch { ToilHeadCompat.Kind.CoilHead => CoilHeadSlayerChance.Value, ToilHeadCompat.Kind.Manticoil => ManticoilSlayerChance.Value, ToilHeadCompat.Kind.Masked => MaskedSlayerChance.Value, _ => 0, }; } public List ResolveStockEventWhitelist() { string text = StockEventWhitelist.Value; if (string.IsNullOrWhiteSpace(text)) { text = "Nothing, Gloomy, Meteors, OutsideLandmines, OutsideTurrets, AllWeather, DoorFailure, ShipLightsFailure, ManualCameraFailure, IsMetal, LeaflessTrees, LeaflessBrownTrees"; } List list = new List(); HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); string[] array = text.Split(','); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.Length > 0 && hashSet.Add(text2)) { list.Add(text2); } } return list; } public int ResolveVainShroudIterations() { if (VainShroudIterations.Value > 0) { return VainShroudIterations.Value; } if (!ForName("Bush Wolf").Enabled.Value) { return 0; } return 12; } public PluginConfig(ConfigFile file) { //IL_003c: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Expected O, but got Unknown //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Expected O, but got Unknown //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Expected O, but got Unknown //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_011f: Expected O, but got Unknown //IL_0144: Unknown result type (might be due to invalid IL or missing references) //IL_014e: Expected O, but got Unknown //IL_01c9: Unknown result type (might be due to invalid IL or missing references) //IL_01d3: Expected O, but got Unknown //IL_01f0: Unknown result type (might be due to invalid IL or missing references) //IL_01fa: Expected O, but got Unknown //IL_0218: Unknown result type (might be due to invalid IL or missing references) //IL_0222: Expected O, but got Unknown //IL_023f: Unknown result type (might be due to invalid IL or missing references) //IL_0249: Expected O, but got Unknown //IL_0266: Unknown result type (might be due to invalid IL or missing references) //IL_0270: Expected O, but got Unknown //IL_028d: Unknown result type (might be due to invalid IL or missing references) //IL_0297: Expected O, but got Unknown //IL_02bb: Unknown result type (might be due to invalid IL or missing references) //IL_02c5: Expected O, but got Unknown //IL_02e8: Unknown result type (might be due to invalid IL or missing references) //IL_02f2: Expected O, but got Unknown //IL_0333: Unknown result type (might be due to invalid IL or missing references) //IL_033d: Expected O, but got Unknown //IL_0388: Unknown result type (might be due to invalid IL or missing references) //IL_0392: Expected O, but got Unknown //IL_0445: Unknown result type (might be due to invalid IL or missing references) //IL_044f: Expected O, but got Unknown //IL_0477: Unknown result type (might be due to invalid IL or missing references) //IL_0481: Expected O, but got Unknown //IL_04e1: Unknown result type (might be due to invalid IL or missing references) //IL_04eb: Expected O, but got Unknown //IL_0517: Unknown result type (might be due to invalid IL or missing references) //IL_0521: Expected O, but got Unknown //IL_0548: Unknown result type (might be due to invalid IL or missing references) //IL_0552: Expected O, but got Unknown //IL_05d5: Unknown result type (might be due to invalid IL or missing references) //IL_05df: Expected O, but got Unknown //IL_060e: Unknown result type (might be due to invalid IL or missing references) //IL_0618: Expected O, but got Unknown //IL_063d: Unknown result type (might be due to invalid IL or missing references) //IL_0647: Expected O, but got Unknown //IL_06ea: Unknown result type (might be due to invalid IL or missing references) //IL_06f4: Expected O, but got Unknown _file = file; GlobalCap = file.Bind("General", "GlobalCap", 5, new ConfigDescription("Maximum number of enemies alive at the same time on the moon. Keep this at or below ~10-15 to preserve FPS.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 40), Array.Empty())); MinSpawnInterval = file.Bind("General", "MinSpawnInterval", 15f, new ConfigDescription("Minimum delay between spawn attempts, in seconds.", (AcceptableValueBase)(object)new AcceptableValueRange(1f, 600f), Array.Empty())); MaxSpawnInterval = file.Bind("General", "MaxSpawnInterval", 45f, new ConfigDescription("Maximum delay between spawn attempts, in seconds. Each cycle picks a random value in [Min, Max].", (AcceptableValueBase)(object)new AcceptableValueRange(1f, 600f), Array.Empty())); RespawnOnLoad = file.Bind("General", "RespawnOnLoad", true, "If true, every landing on the Company moon despawns all existing enemies and spawning restarts from zero. If false, existing enemies are left alone and the spawner just tops up toward the configured limits."); DebugMode = file.Bind("General", "DebugMode", false, "Verbose logging: timer intervals, alive counts, weighted-pick results, spawn errors."); UpperFloorSpawnShare = file.Bind("Balance", "UpperFloorSpawnShare", 70, new ConfigDescription("Percent of spawn points placed on the upper level (at the ship landing height). The rest go to the lower level / basement. The basement floor is much larger, so pure area-weighted sampling would funnel almost everything down there.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 100), Array.Empty())); OutsideEnemyShare = file.Bind("Balance", "OutsideEnemyShare", 50, new ConfigDescription("Percent chance each spawn picks from OUTDOOR enemy types (dogs, giants, baboon hawks, Old Birds...) instead of indoor ones. Each pool only contains enemies you enabled; if the rolled pool is empty the other one is used. 50 = even split.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 100), Array.Empty())); OldBirdUpperFloorOnly = file.Bind("Balance", "OldBirdUpperFloorOnly", true, "Spawn the Old Bird (RadMech) only on the upper floor (ship-landing level), ignoring UpperFloorSpawnShare for it. The Old Bird is huge and the basement is cramped, so it moves and fights much better upstairs."); AllowHarmlessCreatures = file.Bind("Balance", "AllowHarmlessCreatures", true, "Master switch for the genuinely harmless ambient creatures — the Manticoil (bird) and the Roaming/Docile Locust swarm. Set false to keep the building free of just those two, regardless of their per-enemy Enabled setting. (Tulip Snake is not affected — it can grab players and stays in the normal pool.)"); EarthLeviathanFloorEmerge = file.Bind("Balance", "EarthLeviathanFloorEmerge", true, "EXPERIMENTAL. Lets the Earth Leviathan (worm) breach up through the Company building floor. The worm normally only emerges through natural ground, which Gordion's interior lacks, so without this it roams under the floor forever and never attacks. Disable to leave the worm inert (or if it emerges through unwanted surfaces)."); AcceptableValueRange val = new AcceptableValueRange(0, 100); CoilHeadTurretChance = file.Bind("ToilHead", "CoilHeadTurretChance", 25, new ConfigDescription("Percent chance a spawned Coil-Head gets a turret head.", (AcceptableValueBase)(object)val, Array.Empty())); CoilHeadSlayerChance = file.Bind("ToilHead", "CoilHeadSlayerChance", 0, new ConfigDescription("Percent of Coil-Head turrets that are the Slayer (minigun) variant.", (AcceptableValueBase)(object)val, Array.Empty())); ManticoilTurretChance = file.Bind("ToilHead", "ManticoilTurretChance", 25, new ConfigDescription("Percent chance a spawned Manticoil gets a turret head.", (AcceptableValueBase)(object)val, Array.Empty())); ManticoilSlayerChance = file.Bind("ToilHead", "ManticoilSlayerChance", 0, new ConfigDescription("Percent of Manticoil turrets that are the Slayer (minigun) variant.", (AcceptableValueBase)(object)val, Array.Empty())); MaskedTurretChance = file.Bind("ToilHead", "MaskedTurretChance", 0, new ConfigDescription("Percent chance a spawned Masked (mimic) gets a turret head.", (AcceptableValueBase)(object)val, Array.Empty())); MaskedSlayerChance = file.Bind("ToilHead", "MaskedSlayerChance", 0, new ConfigDescription("Percent of Masked turrets that are the Slayer (minigun) variant.", (AcceptableValueBase)(object)val, Array.Empty())); VainShroudIterations = file.Bind("Integration", "VainShroudIterations", 0, new ConfigDescription("Grows vain shrouds (weeds) on the Company moon so the Kidnapper Fox (Bush Wolf) has somewhere to hide instead of despawning itself. The mod flips Gordion's canSpawnMold flag and sets its moldSpreadIterations in StartOfRound.LoadPlanetsMoldSpreadData (the same hook FoxLover uses), and the vanilla pipeline then generates and network-syncs the weeds on landing. Applied at save load, so set this and reload the save / fly fresh. 0 = automatic: weeds grow only when Bush Wolf is enabled (12 iterations). Higher = more overgrowth.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 40), Array.Empty())); VainShroudPatches = file.Bind("Integration", "VainShroudPatches", 1, new ConfigDescription("How many separate weed patches to grow, each at its own random spot on the interior navmesh. The location is re-rolled every landing, so the Fox's nest is never in the same place twice. Raise this for several overgrown areas.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 4), Array.Empty())); FeioparDeadTrees = file.Bind("Integration", "FeioparDeadTrees", true, "EXPERIMENTAL. Feiopar (PumaAI) only stalks players from trees tagged 'Tree'; the Company building has none, so without this it just stands still. When enabled, the mod grows simple dead-tree trunks on the interior navmesh (with the overhead canopy collider the game validates against) so Feiopar can climb, stalk from ~3 m up, and pounce. Ignored when Feiopar is disabled or absent."); FeioparTreeCount = file.Bind("Integration", "FeioparTreeCount", 14, new ConfigDescription("How many dead trees to grow for Feiopar to stalk and jump between (placed mostly on the open upper floor).", (AcceptableValueBase)(object)new AcceptableValueRange(3, 30), Array.Empty())); CadaverBloomTraps = file.Bind("Integration", "CadaverBloomTraps", true, "EXPERIMENTAL. Cadaver Bloom normally needs its map-wide Growth (which requires a dungeon the Company building lacks). When enabled, the mod instead plants Blooms directly at random spots as standalone corpse traps that burst and chase when a player walks close, so no dungeon is needed. Ignored when Cadaver Bloom is disabled or absent."); CadaverBloomTriggerRange = file.Bind("Integration", "CadaverBloomTriggerRange", 4f, new ConfigDescription("How close a player must get (meters) for a planted Cadaver Bloom to burst.", (AcceptableValueBase)(object)new AcceptableValueRange(1.5f, 12f), Array.Empty())); EnableStockEvents = file.Bind("BrutalCompany", "EnableStockEvents", false, "Let a curated set of stock BrutalCompanyMinusExtraReborn events run on the Company moon. BCMER itself skips this moon entirely (its LoadNewLevel prefix returns early on levelID 3), so nothing here happens without this mod. Only events that do not touch the level's enemy lists are eligible, because BCMER never snapshots those on this moon and could not restore them. Your BCMER config is always obeyed: an event that is disabled, whose type has weight 0, or whose moon lists exclude Gordion, will not run."); StockEventWhitelist = file.Bind("BrutalCompany", "StockEventWhitelist", "Nothing, Gloomy, Meteors, OutsideLandmines, OutsideTurrets, AllWeather, DoorFailure, ShipLightsFailure, ManualCameraFailure, IsMetal, LeaflessTrees, LeaflessBrownTrees", "Comma-separated BCMER event names allowed on the Company moon, checked against your BCMER config before each landing. Empty means the built-in list. Extras you can add at your own risk: DoorCircuitFailure (shuts the ship door 3PM-10PM, so it needs a mod running the clock here and can strand you), ShipmentFees, LateShip and VeryLateShip (they move the departure time, which can fight a mod that leaves at midnight), and BigBonus. Scrap and item events (Clock, ToiletPaper, TransmuteScrapBig and the rest) are pointless here: this moon generates no loot for them to act on, and they share BCMER's one-transmutation-per-round flag, which this moon never clears. Names not listed here are usually blocked by a gate of their own - the log always says which."); if (StockEventWhitelist.Value != null && StockEventWhitelist.Value.Trim() == "Nothing, Gloomy, Meteors, OutsideLandmines, OutsideTurrets, Warzone, AllWeather") { StockEventWhitelist.Value = "Nothing, Gloomy, Meteors, OutsideLandmines, OutsideTurrets, AllWeather, DoorFailure, ShipLightsFailure, ManualCameraFailure, IsMetal, LeaflessTrees, LeaflessBrownTrees"; Plugin.Log.LogInfo((object)"Config migration: StockEventWhitelist was still the 1.6.0 list, replaced with the current one (Warzone dropped, it can never fire on this moon; six events added)."); } HazardDensityMultiplier = file.Bind("BrutalCompany", "HazardDensityMultiplier", 1f, new ConfigDescription("Scales how many turrets, landmines or trees an event places here. Events that put objects outdoors hand BrutalCompanyMinus a density in objects per square metre, which it multiplies by the moon's terrain area - but it only does that from a hook the game never reaches on this moon, so nothing ever appeared. The mod now places them itself, against the walkable area it measured, and this scales the result.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 10f), Array.Empty())); HazardMaxPerEvent = file.Bind("BrutalCompany", "HazardMaxPerEvent", 40, new ConfigDescription("Hard ceiling on objects placed by one event, whatever the density works out to.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 200), Array.Empty())); HordeEnabled = file.Bind("Horde", "Enabled", true, "A group of enemies closes in from both far edges of the map on every landing at the Company. This is a standing feature of the moon, not a random event: it does not take part in the [BrutalCompany] roll and does not compete with the events there, so a landing can have both. The edges are worked out from the navmesh as the mod runs, so moon overhaul mods that reshape the outdoor area do not strand the arrivals."); HordeEnemy = file.Bind("Horde", "Enemy", "Masked", "Which enemy arrives, by its in-game name (Masked, Flowerman, Crawler, Spring, Baboon hawk...). It must also be spawnable — an enemy your ExcludedEnemies list bars will not arrive, and the reason is logged."); HordeCountPerSide = file.Bind("Horde", "CountPerSide", 5, new ConfigDescription("How many arrive at each of the two edges, so the total is twice this. The horde deliberately ignores GlobalCap — one a cap could shave down to two would not be a horde.", (AcceptableValueBase)(object)new AcceptableValueRange(1, 20), Array.Empty())); HordeDelaySeconds = file.Bind("Horde", "DelaySeconds", 240, new ConfigDescription("Seconds after landing before they arrive. Real time, so it behaves the same whether or not a mod runs the clock on this moon.", (AcceptableValueBase)(object)new AcceptableValueRange(10, 1800), Array.Empty())); HordeRepeatSeconds = file.Bind("Horde", "RepeatSeconds", 0, new ConfigDescription("0 arrives once per landing. Any other value is the gap between waves, which then keep coming for as long as you stay.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 1800), Array.Empty())); HordeAnnounce = file.Bind("Horde", "Announce", true, "Announce each wave in chat as it arrives."); DespawnOnShipLeave = file.Bind("Advanced", "DespawnOnShipLeave", true, "Despawn enemies created by this mod when the ship leaves the Company moon."); CountForeignEnemies = file.Bind("Advanced", "CountForeignEnemies", true, "If true, enemies spawned by the game or other mods (e.g. BrutalCompanyMinus events) count toward GlobalCap and per-type limits, so the mods share one budget instead of stacking on top of each other. If false, only enemies spawned by this mod are counted."); MinDistanceFromPlayers = file.Bind("Advanced", "MinDistanceFromPlayers", 12f, new ConfigDescription("Never spawn closer than this many meters to a player.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 60f), Array.Empty())); SpawnYOffset = file.Bind("Advanced", "SpawnYOffset", 0.25f, new ConfigDescription("Small vertical offset applied to spawn points so enemies don't clip into the floor.", (AcceptableValueBase)(object)new AcceptableValueRange(0f, 2f), Array.Empty())); AINodeCount = file.Bind("Advanced", "AINodeCount", 20, new ConfigDescription("Number of AI patrol nodes generated on the interior navmesh for spawned enemies to roam between.", (AcceptableValueBase)(object)new AcceptableValueRange(4, 64), Array.Empty())); RequireIndoorPoints = file.Bind("Advanced", "RequireIndoorPoints", true, "Only accept navmesh points that have a ceiling above them (i.e. inside the building). Disable if the spawner reports it cannot find valid points."); ExcludedEnemies = file.Bind("Advanced", "ExcludedEnemies", "", "Comma-separated list of EnemyType names. By default this is a BLACKLIST: the listed types are removed from the pool (on top of the built-in exclusions Lasso and Red pill). See ExcludedEnemiesIsWhitelist to flip its meaning."); ExcludedEnemiesIsWhitelist = file.Bind("Advanced", "ExcludedEnemiesIsWhitelist", false, "When true, the ExcludedEnemies list becomes a WHITELIST: ONLY the listed types are allowed to spawn and everything else is excluded. Lasso and Red pill stay excluded regardless. An empty list in whitelist mode means nothing spawns. Combine with ForeignEnemies=RemoveExcluded to also strip non-whitelisted enemies spawned by other mods."); TreatEnemiesAsOutside = file.Bind("Advanced", "TreatEnemiesAsOutside", true, "REQUIRED for enemies to be able to see, chase and kill you. The game decides whether an enemy may target a player with 'player.isInsideFactory != enemy.isOutside'. Inside the Company building players are NOT flagged as being in a factory, so enemies must be flagged as outside enemies or they will walk past you and never attack. Only turn this off for debugging."); MaintenanceInterval = file.Bind("Advanced", "MaintenanceInterval", 3f, new ConfigDescription("How often (seconds) to re-apply AI settings to spawned enemies, rescue enemies that wandered somewhere unreachable, and apply the ForeignEnemies policy.", (AcceptableValueBase)(object)new AcceptableValueRange(1f, 30f), Array.Empty())); ForeignEnemies = file.Bind("Advanced", "ForeignEnemies", ForeignEnemyPolicy.RemoveExcluded, "What to do with enemies this mod did NOT spawn (vanilla spawns, BrutalCompanyMinus, MoreEnemies...). Ignore = leave them; RemoveExcluded = despawn types listed in ExcludedEnemies, so the blacklist applies to the whole moon no matter who spawned them; RemoveNotEnabled = also despawn any type with Enabled = false."); LegacyCapsMigrated = file.Bind("Advanced", "LegacyCapsMigrated", false, "Set automatically. Older versions shipped a low per-enemy MaxSpawnCount (1-3), and BepInEx never overwrites values that already exist in a config file — so upgrading kept those caps and GlobalCap could not be reached. On first run the mod raises any MaxSpawnCount still sitting on its old default up to the new one and sets this to true, so it only ever happens once. Values you changed yourself are left alone."); } public void FinishLegacyCapMigration() { if (!LegacyCapsMigrated.Value) { LegacyCapsMigrated.Value = true; } } public EnemySpawnSettings For(EnemyType type) { return ForName(type.enemyName); } public EnemySpawnSettings ForName(string name) { //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Expected O, but got Unknown //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Expected O, but got Unknown //IL_00f3: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: Expected O, but got Unknown if (_enemySettings.TryGetValue(name, out var value)) { return value; } if (!VanillaDefaults.TryGetValue(name, out (bool, int, int, int) value2)) { value2 = UnknownEnemyDefaults; } string text = "Enemy." + SanitizeForSection(name); EnemySpawnSettings enemySpawnSettings = new EnemySpawnSettings { Enabled = _file.Bind(text, "Enabled", value2.Item1, "Include '" + name + "' in the spawn pool."), SpawnWeight = _file.Bind(text, "SpawnWeight", value2.Item2, new ConfigDescription("Relative weight for the weighted-random pick (0 disables).", (AcceptableValueBase)(object)new AcceptableValueRange(0, 100), Array.Empty())), MinSpawnCount = _file.Bind(text, "MinSpawnCount", value2.Item3, new ConfigDescription("The spawner tries to keep at least this many alive (global cap permitting).", (AcceptableValueBase)(object)new AcceptableValueRange(0, 40), Array.Empty())), MaxSpawnCount = _file.Bind(text, "MaxSpawnCount", 40, new ConfigDescription("Never allow more than this many of this type alive at once. Defaults high so only GlobalCap limits the total — lower it to cap this type.", (AcceptableValueBase)(object)new AcceptableValueRange(0, 40), Array.Empty())) }; if (!LegacyCapsMigrated.Value && value2.Item4 < 40 && enemySpawnSettings.MaxSpawnCount.Value == value2.Item4) { enemySpawnSettings.MaxSpawnCount.Value = 40; Plugin.Log.LogInfo((object)($"Config migration: '{name}' MaxSpawnCount {value2.Item4} -> {40} " + "(old per-enemy cap raised; GlobalCap is now the limit).")); } _enemySettings[name] = enemySpawnSettings; return enemySpawnSettings; } private static string SanitizeForSection(string name) { StringBuilder stringBuilder = new StringBuilder(name.Length); foreach (char c in name) { bool flag; switch (c) { case '\t': case '\n': case '"': case '\'': case '=': case '[': case '\\': case ']': flag = true; break; default: flag = false; break; } if (!flag) { stringBuilder.Append(c); } } return stringBuilder.ToString().Trim(); } } public static class MyPluginInfo { public const string PLUGIN_GUID = "Timofey.MonstersGordion"; public const string PLUGIN_NAME = "MonstersGordion"; public const string PLUGIN_VERSION = "1.9.0"; } } namespace MonstersGordion.Patches { [HarmonyPatch] internal static class EnemyAIPatches { [HarmonyPostfix] [HarmonyPatch(typeof(EnemyAI), "KillEnemy")] private static void KillEnemy_Postfix(EnemyAI __instance) { CompanyMonsterSpawner.NotifyEnemyKilled(__instance); } } [HarmonyPatch] internal static class MoldSpreadPatches { [HarmonyPostfix] [HarmonyPatch(typeof(StartOfRound), "LoadPlanetsMoldSpreadData")] private static void LoadPlanetsMoldSpreadData_Postfix(StartOfRound __instance) { try { int num = Plugin.Cfg.ResolveVainShroudIterations(); if ((Object)(object)__instance == (Object)null || __instance.levels == null) { return; } SelectableLevel[] levels = __instance.levels; foreach (SelectableLevel val in levels) { if (!((Object)(object)val == (Object)null) && CompanyMonsterSpawner.IsCompanyLevel(val)) { if (num <= 0) { Plugin.DebugLog("Vain shrouds: not forcing weeds on '" + val.PlanetName + "' (VainShroudIterations resolved to 0 — Bush Wolf disabled and no explicit value)."); break; } val.canSpawnMold = true; int moldSpreadIterations = val.moldSpreadIterations; val.moldSpreadIterations = Math.Max(moldSpreadIterations, num); Plugin.Log.LogInfo((object)("Vain shrouds: enabled weed growth on '" + val.PlanetName + "' " + $"(canSpawnMold set true, moldSpreadIterations {moldSpreadIterations} -> {val.moldSpreadIterations}). " + "Weeds generate on landing via the vanilla pipeline; this lets Bush Wolf survive.")); break; } } } catch (Exception arg) { Plugin.Log.LogError((object)$"Vain shroud setup failed: {arg}"); } } } [HarmonyPatch] internal static class RoundManagerPatches { [HarmonyPrefix] [HarmonyPriority(200)] [HarmonyPatch(typeof(RoundManager), "LoadNewLevel", new Type[] { typeof(int), typeof(SelectableLevel) })] private static void LoadNewLevel_Prefix(SelectableLevel newLevel) { try { if (!CompanyMonsterSpawner.IsCompanyLevel(newLevel)) { GordionEvents.Reset(); return; } RoundManager instance = RoundManager.Instance; if (!((Object)(object)instance == (Object)null) && ((NetworkBehaviour)instance).IsServer) { GordionEvents.RollForLanding(); } } catch (Exception arg) { Plugin.Log.LogError((object)$"Company moon event roll failed: {arg}"); } } } [HarmonyPatch] internal static class StartOfRoundPatches { [HarmonyPostfix] [HarmonyPatch(typeof(StartOfRound), "ShipLeave")] private static void ShipLeave_Postfix() { try { CompanyMonsterSpawner.OnShipLeaving(); } catch (Exception arg) { Plugin.Log.LogError((object)$"ShipLeave handler failed: {arg}"); } } [HarmonyPostfix] [HarmonyPatch(typeof(StartOfRound), "OnDestroy")] private static void OnDestroy_Postfix() { try { CompanyMonsterSpawner.OnShipLeaving(); } catch { } } } } namespace MonstersGordion.Compat { internal static class BcmeCompat { private const string RebornGuid = "SoftDiamond.BrutalCompanyMinusExtraReborn"; private const string LegacyGuid = "SoftDiamond.BrutalCompanyMinus"; private static bool _scanned; private static MethodInfo _doesEventExist; private static MethodInfo _getEvent; private static MethodInfo _isEnabled; private static MethodInfo _getWeight; private static MethodInfo _getType; private static MethodInfo _isSpecial; private static MethodInfo _isBeta; private static MethodInfo _onBlacklist; private static MethodInfo _onWhitelist; private static MethodInfo _mEventName; private static MethodInfo _mEventExecute; private static MethodInfo _mEventAddIfOnly; private static FieldInfo _mEventMoonMode; private static FieldInfo _currentEventsField; private static FieldInfo _hazardQueueField; private static FieldInfo _objectInfoObj; private static FieldInfo _objectInfoDensity; private static FieldInfo _netInstanceField; private static FieldInfo _propQueueField; private static FieldInfo _propDensity; private static FieldInfo _propEnumId; private static MethodInfo _assetsGetObject; private static Type _objectNameEnum; public static bool Present { get; private set; } public static bool IsReborn { get; private set; } public static bool EventsUsable { get { if (IsReborn && _doesEventExist != null && _getEvent != null) { return _mEventExecute != null; } return false; } } public static void Scan() { if (_scanned) { return; } _scanned = true; try { PluginInfo val = CompatScanner.FindPluginInfo("SoftDiamond.BrutalCompanyMinusExtraReborn", "BrutalCompanyMinus"); if (val == null) { Plugin.DebugLog("BrutalCompanyMinus(-ExtraReborn) not detected."); return; } Present = true; BepInPlugin metadata = val.Metadata; string text = ((metadata != null) ? metadata.GUID : null) ?? ""; BepInPlugin metadata2 = val.Metadata; string text2 = ((metadata2 == null) ? null : metadata2.Version?.ToString()) ?? "?"; IsReborn = !string.Equals(text, "SoftDiamond.BrutalCompanyMinus", StringComparison.OrdinalIgnoreCase); Plugin.Log.LogInfo((object)("BrutalCompanyMinus detected (" + text + " v" + text2 + "). Its enemies " + (Plugin.Cfg.CountForeignEnemies.Value ? "count toward this mod's GlobalCap (shared budget)." : "are IGNORED by this mod's caps (CountForeignEnemies=false)."))); if (!IsReborn) { Plugin.Log.LogInfo((object)"This is the legacy BrutalCompanyMinus, which has no event API — the [BrutalCompany] event settings will stay inactive."); } else { BindEventApi(val); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("BCME detection failed: " + ex.Message)); } } private static void BindEventApi(PluginInfo info) { string how; Assembly assembly = CompatScanner.FindAssembly(info, "BrutalCompanyMinus", out how); if (assembly == null) { Plugin.Log.LogWarning((object)"BCMER is loaded but its assembly could not be resolved — event integration is off."); return; } Type type = null; Type type2 = null; foreach (Type item in CompatScanner.SafeGetTypes(assembly)) { if (type == null && item.FullName == "BrutalCompanyMinus.Minus.API") { type = item; } else if (type2 == null && item.FullName == "BrutalCompanyMinus.Minus.MEvent") { type2 = item; } if (type != null && type2 != null) { break; } } if (type == null || type2 == null) { Plugin.Log.LogWarning((object)("BCMER assembly found (via " + how + ") but its API/MEvent types were not — event integration is off. Please report your BCMER version.")); return; } _doesEventExist = FindApi(type, "DoesEventExist"); _getEvent = FindApi(type, "GetEventsByName", "GetEventByName"); _isEnabled = FindApi(type, "IsEventEnabled"); _getWeight = FindApi(type, "GetEventWeight"); _getType = FindApi(type, "GetEventType"); _isSpecial = FindApi(type, "IsEventSpecial"); _isBeta = FindApi(type, "IsEventBeta"); _onBlacklist = FindApi(type, "IsEventOnBlacklist"); _onWhitelist = FindApi(type, "IsEventOnWhitelist"); _mEventName = type2.GetMethod("Name", Type.EmptyTypes); _mEventExecute = type2.GetMethod("Execute", Type.EmptyTypes); _mEventAddIfOnly = type2.GetMethod("AddEventIfOnly", Type.EmptyTypes); _mEventMoonMode = type2.GetField("MoonMode", BindingFlags.Instance | BindingFlags.Public); foreach (Type item2 in CompatScanner.SafeGetTypes(assembly)) { if (!(item2.FullName != "BrutalCompanyMinus.Minus.EventManager")) { _currentEventsField = item2.GetField("currentEvents", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); break; } } BindHazardQueues(assembly); Plugin.Log.LogInfo((object)("BCMER event API bound (assembly via " + how + "). " + $"exists={_doesEventExist != null}, get={_getEvent != null}, " + $"enabled={_isEnabled != null}, weight={_getWeight != null}, " + $"moonLists={_onBlacklist != null && _onWhitelist != null}, " + $"execute={_mEventExecute != null}, veto={_mEventAddIfOnly != null}, currentEvents={_currentEventsField != null}.")); if (!EventsUsable) { Plugin.Log.LogWarning((object)"BCMER is installed but its event API did not resolve well enough to run events. Stock events on the Company moon will be skipped."); } } private static MethodInfo FindApi(Type api, params string[] names) { MethodInfo[] methods = api.GetMethods(BindingFlags.Static | BindingFlags.Public); foreach (MethodInfo method in methods) { if (names.Any((string n) => string.Equals(n, method.Name, StringComparison.OrdinalIgnoreCase))) { ParameterInfo[] parameters = method.GetParameters(); if (parameters.Length == 1 && parameters[0].ParameterType == typeof(string)) { return method; } } } return null; } private static bool CallBool(MethodInfo method, string name, bool fallback) { if (method == null) { return fallback; } try { return (method.Invoke(null, new object[1] { name }) is bool flag) ? flag : fallback; } catch (Exception e) { Plugin.DebugLog("BCMER " + method.Name + "('" + name + "') threw: " + Unwrap(e)); return fallback; } } public static bool EventExists(string name) { return CallBool(_doesEventExist, name, fallback: false); } public static int WeightOf(string name) { if (_getWeight == null) { return 1; } try { return (!(_getWeight.Invoke(null, new object[1] { name }) is int num)) ? 1 : num; } catch (Exception e) { Plugin.DebugLog("BCMER GetEventWeight('" + name + "') threw: " + Unwrap(e)); return 1; } } public static string TypeOf(string name) { if (_getType == null) { return "?"; } try { return (_getType.Invoke(null, new object[1] { name }) as string) ?? "?"; } catch { return "?"; } } public static object TryResolve(string name, out string reason) { reason = null; if (!EventsUsable) { reason = "BCMER event API unavailable"; return null; } if (!EventExists(name)) { reason = "no such event in this BCMER build"; return null; } if (!CallBool(_isEnabled, name, fallback: true)) { reason = "'Event Enabled?' is false in your BCMER config"; return null; } if (WeightOf(name) <= 0) { reason = "its type (" + TypeOf(name) + ") has weight 0 — raise that type's scale in BCMER's Difficulty_Settings.cfg [_EventType Weights] to allow it"; return null; } if (CallBool(_isSpecial, name, fallback: false)) { reason = "it is a Special event and 'Enable Special Events?' is off"; return null; } if (CallBool(_isBeta, name, fallback: false)) { reason = "it is a Beta event and 'Enable Beta Events?' is off"; return null; } object obj; try { obj = _getEvent.Invoke(null, new object[1] { name }); } catch (Exception e) { reason = "BCMER failed to hand over the event: " + Unwrap(e); return null; } if (obj == null) { reason = "BCMER returned no event object"; return null; } string text = NameOf(obj); if (text != null && !string.Equals(text, name, StringComparison.OrdinalIgnoreCase)) { reason = "BCMER resolved it to '" + text + "' instead — check the spelling"; return null; } bool flag = false; try { if (_mEventMoonMode != null) { object value = _mEventMoonMode.GetValue(obj); bool flag2 = default(bool); int num; if (value is bool) { flag2 = (bool)value; num = 1; } else { num = 0; } flag = (byte)((uint)num & (flag2 ? 1u : 0u)) != 0; } } catch { } if (flag) { if (!CallBool(_onWhitelist, name, fallback: true)) { reason = "its 'Moons To Spawn Only On' list does not include this moon"; return null; } } else if (CallBool(_onBlacklist, name, fallback: false)) { reason = "its 'Moons To Not Spawn On' list excludes this moon"; return null; } if (_mEventAddIfOnly != null) { try { object value = _mEventAddIfOnly.Invoke(obj, null); if (value is bool && !(bool)value) { reason = "the event's own AddEventIfOnly() check failed — it needs something this moon or your modpack does not currently provide"; return null; } } catch (Exception e2) { reason = "its AddEventIfOnly() threw: " + Unwrap(e2); return null; } } return obj; } public static string NameOf(object mEvent) { if (_mEventName == null || mEvent == null) { return null; } try { return _mEventName.Invoke(mEvent, null) as string; } catch { return null; } } public static bool Execute(object mEvent, string name) { if (_mEventExecute == null || mEvent == null) { return false; } try { _mEventExecute.Invoke(mEvent, null); return true; } catch (Exception e) { Plugin.Log.LogWarning((object)("BCMER event '" + name + "' threw while executing: " + Unwrap(e))); return false; } } public static void PublishCurrentEvent(object mEvent) { if (_currentEventsField == null) { return; } try { if (_currentEventsField.GetValue(null) is IList list) { list.Clear(); if (mEvent != null) { list.Add(mEvent); } } } catch (Exception e) { Plugin.DebugLog("Publishing to BCMER's currentEvents failed: " + Unwrap(e)); } } private static void BindHazardQueues(Assembly assembly) { try { Type type = null; Type type2 = null; Type type3 = null; foreach (Type item in CompatScanner.SafeGetTypes(assembly)) { switch (item.FullName) { case "BrutalCompanyMinus.Minus.Manager": type = item; break; case "BrutalCompanyMinus.Minus.Net": type2 = item; break; case "BrutalCompanyMinus.Minus.Assets": type3 = item; break; } } if (type != null) { _hazardQueueField = type.GetField("insideObjectsToSpawnOutside", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); Type nestedType = type.GetNestedType("ObjectInfo", BindingFlags.Public | BindingFlags.NonPublic); if (nestedType != null) { _objectInfoObj = nestedType.GetField("obj", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); _objectInfoDensity = nestedType.GetField("density", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } } if (type2 != null) { _netInstanceField = type2.GetField("Instance", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); _propQueueField = type2.GetField("outsideObjectsToSpawn", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); Type nestedType2 = type2.GetNestedType("OutsideObjectsToSpawnMethod", BindingFlags.Public | BindingFlags.NonPublic); if (nestedType2 != null) { _propDensity = nestedType2.GetField("density", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); _propEnumId = nestedType2.GetField("objectEnumID", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); } } if (!(type3 != null)) { return; } _objectNameEnum = type3.GetNestedType("ObjectName", BindingFlags.Public | BindingFlags.NonPublic); MethodInfo[] methods = type3.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { ParameterInfo[] parameters = methodInfo.GetParameters(); if (methodInfo.Name == "GetObject" && parameters.Length == 1 && parameters[0].ParameterType.IsEnum) { _assetsGetObject = methodInfo; break; } } } catch (Exception e) { Plugin.DebugLog("Binding BCMER's outdoor queues failed: " + Unwrap(e)); } } public static List<(GameObject prefab, float density)> DrainQueuedOutdoorObjects() { List<(GameObject, float)> list = new List<(GameObject, float)>(); if (!Present) { return list; } try { if (_hazardQueueField?.GetValue(null) is IList list2) { foreach (object item3 in list2) { object? obj = _objectInfoObj?.GetValue(item3); GameObject val = (GameObject)((obj is GameObject) ? obj : null); if ((Object)(object)val != (Object)null && _objectInfoDensity?.GetValue(item3) is float item) { list.Add((val, item)); } } list2.Clear(); } } catch (Exception e) { Plugin.DebugLog("Draining BCMER's hazard queue failed: " + Unwrap(e)); } try { object obj2 = _netInstanceField?.GetValue(null); if (obj2 != null && _propQueueField?.GetValue(obj2) is IList list3) { foreach (object item4 in list3) { if (_propDensity?.GetValue(item4) is float item2 && _propEnumId?.GetValue(item4) is int value && !(_assetsGetObject == null) && !(_objectNameEnum == null)) { object obj3 = Enum.ToObject(_objectNameEnum, value); object? obj4 = _assetsGetObject.Invoke(null, new object[1] { obj3 }); GameObject val2 = (GameObject)((obj4 is GameObject) ? obj4 : null); if (val2 != null) { list.Add((val2, item2)); } } } list3.Clear(); } } catch (Exception e2) { Plugin.DebugLog("Draining BCMER's outdoor prop queue failed: " + Unwrap(e2)); } return list; } private static string Unwrap(Exception e) { return e.InnerException?.Message ?? e.Message; } } internal static class CompatScanner { public static PluginInfo FindPluginInfo(string preferredGuid, string fragment) { try { Dictionary pluginInfos = Chainloader.PluginInfos; if (pluginInfos.TryGetValue(preferredGuid, out var value)) { return value; } return ((IEnumerable)pluginInfos.Values).FirstOrDefault((Func)delegate(PluginInfo p) { object value2; if (p == null) { value2 = null; } else { BepInPlugin metadata = p.Metadata; value2 = ((metadata != null) ? metadata.GUID : null); } if (!Matches((string)value2, fragment)) { object value3; if (p == null) { value3 = null; } else { BepInPlugin metadata2 = p.Metadata; value3 = ((metadata2 != null) ? metadata2.Name : null); } return Matches((string)value3, fragment); } return true; }); } catch (Exception ex) { Plugin.DebugLog("PluginInfos lookup for '" + fragment + "' failed: " + ex.Message); return null; } } public static Assembly FindAssembly(PluginInfo info, string assemblyFragment, out string how) { if ((Object)(object)((info != null) ? info.Instance : null) != (Object)null) { how = "plugin instance"; return ((object)info.Instance).GetType().Assembly; } Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { string name; try { name = assembly.GetName().Name; } catch { continue; } if (Matches(name, assemblyFragment)) { object obj2; if (info == null) { obj2 = "AppDomain scan (no chainloader entry matched)"; } else { BepInPlugin metadata = info.Metadata; obj2 = "AppDomain scan (PluginInfo.Instance was null for '" + ((metadata != null) ? metadata.GUID : null) + "')"; } how = (string)obj2; return assembly; } } how = "not found"; return null; } public static IEnumerable SafeGetTypes(Assembly assembly) { try { return assembly.GetTypes(); } catch (ReflectionTypeLoadException ex) { return ex.Types.Where((Type t) => t != null); } } private static bool Matches(string value, string fragment) { if (value != null) { return value.IndexOf(fragment, StringComparison.OrdinalIgnoreCase) >= 0; } return false; } } internal static class StarlancerCompat { private const string PreferredGuid = "AudioKnight.StarlancerAIFix"; private static bool _scanned; public static bool Present { get; private set; } public static void Scan() { if (_scanned) { return; } _scanned = true; try { PluginInfo val = CompatScanner.FindPluginInfo("AudioKnight.StarlancerAIFix", "Starlancer"); if (CompatScanner.FindAssembly(val, "Starlancer", out var how) == null) { Plugin.Log.LogInfo((object)"StarlancerAIFix not detected. Not required: this mod applies its own interior AI fix-up after spawning."); return; } Present = true; ManualLogSource log = Plugin.Log; string[] obj = new string[5] { "StarlancerAIFix detected (assembly via ", how, ", v", null, null }; object obj2; if (val == null) { obj2 = null; } else { BepInPlugin metadata = val.Metadata; obj2 = ((metadata == null) ? null : metadata.Version?.ToString()); } if (obj2 == null) { obj2 = "?"; } obj[3] = (string)obj2; obj[4] = "). Its EnemyAI.Start postfix applies to spawned enemies automatically; our interior node assignment runs afterwards and takes precedence."; log.LogInfo((object)string.Concat(obj)); } catch (Exception ex) { Plugin.Log.LogWarning((object)("StarlancerAIFix detection failed: " + ex.Message)); } } } internal static class ToilHeadCompat { internal enum Kind { None, CoilHead, Manticoil, Masked } private const string PreferredGuid = "com.github.zehsteam.ToilHead"; private static readonly string[] CoilHeadNames = new string[2] { "SetToilHeadOnServer", "SpawnToilHeadOnServer" }; private static readonly string[] CoilSlayerNames = new string[1] { "SetToilSlayerOnServer" }; private static readonly string[] ManticoilNames = new string[3] { "SetMantiToilOnServer", "SetManToilOnServer", "SetMantoilOnServer" }; private static readonly string[] MantiSlayerNames = new string[1] { "SetMantiSlayerOnServer" }; private static readonly string[] MaskedNames = new string[1] { "SetToilMaskedOnServer" }; private static readonly string[] MaskedSlayerNames = new string[1] { "SetSlayerMaskedOnServer" }; private static bool _scanned; private static MethodInfo _coilHead; private static MethodInfo _coilSlayer; private static MethodInfo _manticoil; private static MethodInfo _mantiSlayer; private static MethodInfo _masked; private static MethodInfo _maskedSlayer; public static bool Present { get; private set; } public static Kind KindOf(string enemyName) { return enemyName switch { "Spring" => Kind.CoilHead, "Manticoil" => Kind.Manticoil, "Masked" => Kind.Masked, _ => Kind.None, }; } public static bool IsEligible(string enemyName) { return KindOf(enemyName) != Kind.None; } public static void Scan() { if (_scanned) { return; } _scanned = true; try { PluginInfo val = CompatScanner.FindPluginInfo("com.github.zehsteam.ToilHead", "ToilHead"); string how; Assembly assembly = CompatScanner.FindAssembly(val, "ToilHead", out how); if (assembly == null) { Plugin.Log.LogInfo((object)"ToilHead not detected — its turret chances will be ignored."); return; } Present = true; foreach (Type item in CompatScanner.SafeGetTypes(assembly)) { MethodInfo[] methods = item.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); foreach (MethodInfo methodInfo in methods) { ParameterInfo[] parameters = methodInfo.GetParameters(); if (parameters.Length == 1 && typeof(EnemyAI).IsAssignableFrom(parameters[0].ParameterType)) { if (_coilHead == null && CoilHeadNames.Contains(methodInfo.Name)) { _coilHead = methodInfo; } else if (_coilSlayer == null && CoilSlayerNames.Contains(methodInfo.Name)) { _coilSlayer = methodInfo; } else if (_manticoil == null && ManticoilNames.Contains(methodInfo.Name)) { _manticoil = methodInfo; } else if (_mantiSlayer == null && MantiSlayerNames.Contains(methodInfo.Name)) { _mantiSlayer = methodInfo; } else if (_masked == null && MaskedNames.Contains(methodInfo.Name)) { _masked = methodInfo; } else if (_maskedSlayer == null && MaskedSlayerNames.Contains(methodInfo.Name)) { _maskedSlayer = methodInfo; } } } } ManualLogSource log = Plugin.Log; string[] obj = new string[17] { "ToilHead detected (assembly via ", how, ", v", null, null, null, null, null, null, null, null, null, null, null, null, null, null }; object obj2; if (val == null) { obj2 = null; } else { BepInPlugin metadata = val.Metadata; obj2 = ((metadata == null) ? null : metadata.Version?.ToString()); } if (obj2 == null) { obj2 = "?"; } obj[3] = (string)obj2; obj[4] = "). Hooks — Coil-Head: "; obj[5] = Describe(_coilHead); obj[6] = "/"; obj[7] = Describe(_coilSlayer); obj[8] = ", Manticoil: "; obj[9] = Describe(_manticoil); obj[10] = "/"; obj[11] = Describe(_mantiSlayer); obj[12] = ", Masked: "; obj[13] = Describe(_masked); obj[14] = "/"; obj[15] = Describe(_maskedSlayer); obj[16] = "."; log.LogInfo((object)string.Concat(obj)); if (_coilHead == null && _manticoil == null && _masked == null) { Plugin.Log.LogWarning((object)"ToilHead is installed but no compatible attach method was found — the turret integration will be inactive. Please report your ToilHead version."); } } catch (Exception ex) { Plugin.Log.LogWarning((object)("ToilHead detection failed: " + ex.Message)); } } public static bool TryApply(EnemyAI enemy, bool slayer) { if ((Object)(object)enemy == (Object)null || (Object)(object)enemy.enemyType == (Object)null) { return false; } MethodInfo methodInfo = KindOf(enemy.enemyType.enemyName) switch { Kind.CoilHead => slayer ? (_coilSlayer ?? _coilHead) : _coilHead, Kind.Manticoil => slayer ? (_mantiSlayer ?? _manticoil) : _manticoil, Kind.Masked => slayer ? (_maskedSlayer ?? _masked) : _masked, _ => null, }; if (methodInfo == null) { return false; } try { object obj = methodInfo.Invoke(null, new object[1] { enemy }); bool flag = default(bool); int num; if (obj is bool) { flag = (bool)obj; num = ((1 == 0) ? 1 : 0); } else { num = 1; } return (byte)((uint)num | (flag ? 1u : 0u)) != 0; } catch (Exception ex) { Plugin.Log.LogWarning((object)("ToilHead attach failed for '" + enemy.enemyType.enemyName + "': " + (ex.InnerException?.Message ?? ex.Message))); return false; } } private static string Describe(MethodInfo method) { if (!(method == null)) { return method.DeclaringType?.Name + "." + method.Name; } return "—"; } } }