Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of MonstersGordion v1.2.1
MonstersGordion.dll
Decompiled 3 hours agousing 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.2.1.0")] [assembly: AssemblyInformationalVersion("1.2.1+43694c33d47c9a7d281879973495c0bd4d78c513")] [assembly: AssemblyProduct("MonstersGordion")] [assembly: AssemblyTitle("MonstersGordion")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("1.2.1.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<GameObject> _aiNodes = new List<GameObject>(); private readonly List<GameObject> _fakeTrees = new List<GameObject>(); private readonly List<EnemyAI> _ownedEnemies = new List<EnemyAI>(); private readonly List<GameObject> _ownedNests = new List<GameObject>(); private readonly Dictionary<int, float> _spawnTimes = new Dictionary<int, float>(); private readonly List<(EnemyAI ai, string name, float time)> _recentSpawns = new List<(EnemyAI, string, float)>(); private readonly Dictionary<string, int> _earlyDeathCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); private readonly HashSet<string> _disabledThisLanding = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private const int EarlyDeathsBeforeDisable = 2; private readonly Dictionary<int, (Vector3 pos, float time)> _lastMovement = new Dictionary<int, (Vector3, float)>(); private const float StuckSeconds = 25f; private const float StuckDistance = 1.5f; private NavMeshSampler _sampler; private Coroutine _loop; private Coroutine _maintenance; private bool _shuttingDown; private bool _hasVainShrouds; private readonly HashSet<string> _warnedRequirements = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private const float EarlyDeathSeconds = 4f; private int _earlyDeathTracesLogged; private static readonly HashSet<string> StationaryByDesign = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Flowerman", "Spring", "Clay Surgeon", "Jester", "Girl", "Cadaver Bloom" }; private const int TreeCanopyLayer = 25; private const int FakeTreeCount = 12; internal static CompanyMonsterSpawner Instance { get; private set; } internal static bool IsCompanyLevel() { SelectableLevel val = (((Object)(object)StartOfRound.Instance != (Object)null) ? StartOfRound.Instance.currentLevel : null); if ((Object)(object)val == (Object)null) { return false; } if (!(val.sceneName == "CompanyBuilding") && val.levelID != 3) { if (val.PlanetName != null) { return val.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<CompanyMonsterSpawner>(); 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(); CreateAINodes(cfg.AINodeCount.Value); CreateFakeTreesIfNeeded(); _loop = ((MonoBehaviour)this).StartCoroutine(SpawnLoop()); _maintenance = ((MonoBehaviour)this).StartCoroutine(MaintenanceLoop()); 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; } try { if (Plugin.Cfg.DespawnOnShipLeave.Value) { DespawnOwnedEnemies(); } DespawnOwnedNests(); } 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(); _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_02f4: 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<string, int> 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 && !_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<GameObject>(type.nestSpawnPrefab, randomPoint.Value, Quaternion.Euler(0f, Random.Range(-180f, 180f), 0f)); EnemyAINestSpawnObject component = val.GetComponent<EnemyAINestSpawnObject>(); NetworkObject componentInChildren = val.GetComponentInChildren<NetworkObject>(); 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<EnemyAINestSpawnObject>(); if ((Object)(object)component != (Object)null && (Object)(object)RoundManager.Instance != (Object)null) { RoundManager.Instance.enemyNestSpawnObjects.Remove(component); } NetworkObject componentInChildren = item.GetComponentInChildren<NetworkObject>(); 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 static bool CheckVainShrouds() { try { MoldSpreadManager val = Object.FindObjectOfType<MoldSpreadManager>(); 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; } 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 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<string, int> perType) { perType = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); int num = 0; _ownedEnemies.RemoveAll((EnemyAI e) => (Object)(object)e == (Object)null || e.isEnemyDead); foreach (EnemyAI item in (IEnumerable<EnemyAI>)(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 + "."); } private 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<EnemyAI>(); if ((Object)(object)val4 == (Object)null) { val4 = ((Component)val3).GetComponentInChildren<EnemyAI>(); } 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 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); }); EnforceForeignEnemyPolicy(); } 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 CreateFakeTreesIfNeeded() { //IL_00a5: 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_00d8: Unknown result type (might be due to invalid IL or missing references) //IL_00e2: Expected O, but got Unknown //IL_00fe: Unknown result type (might be due to invalid IL or missing references) //IL_0103: Unknown result type (might be due to invalid IL or missing references) //IL_010d: Unknown result type (might be due to invalid IL or missing references) //IL_0112: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.Cfg.FeioparFakeTrees.Value) { return; } EnemyType val = ((IEnumerable<EnemyType>)EnemyCatalog.Enemies).FirstOrDefault((Func<EnemyType, bool>)((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 fake tree generation."); return; } int num = 0; for (int num2 = 0; num2 < 12; num2++) { Vector3? randomPoint = _sampler.GetRandomPoint(0f, 15, 50); if (randomPoint.HasValue) { GameObject item = CreateNode($"MG_FakeTree_{num2}", randomPoint.Value, "Tree"); _fakeTrees.Add(item); GameObject val2 = new GameObject($"MG_FakeTreeCanopy_{num2}") { layer = 25 }; val2.transform.SetParent(((Component)this).transform, false); val2.transform.position = randomPoint.Value + Vector3.up * 12f; SphereCollider obj = val2.AddComponent<SphereCollider>(); obj.radius = 1f; ((Collider)obj).isTrigger = false; _fakeTrees.Add(val2); num++; } } ResetPumaTreeCache(val); Plugin.Log.LogInfo((object)($"Feiopar fake trees: created {num} (EXPERIMENTAL — it may perch oddly near the " + "ceiling; set [Integration] FeioparFakeTrees=false to disable).")); } private static void ResetPumaTreeCache(EnemyType feiopar) { try { Type type = ((object)(((Object)(object)feiopar.enemyPrefab != (Object)null) ? feiopar.enemyPrefab.GetComponentInChildren<EnemyAI>(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<EnemyAI> 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<NetworkObject>(); } 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<string> HardExcluded = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Lasso", "Red pill" }; private static readonly Dictionary<string, string> Notes = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["Bush Wolf"] = "BushWolfEnemy needs vain shrouds to hide in and despawns itself on spawn without them. Set [Integration] VainShroudIterations (or just leave it at 0 — enabling Bush Wolf grows weeds automatically)", ["Cadaver Growths"] = "CadaverGrowthAI is a map-wide master that requires a dungeon; on Gordion it logs 'Found no dungeon' and self-destructs. It cannot work here and the mod auto-disables it for the landing after it keeps dying", ["Cadaver Bloom"] = "CadaverBloomAI spawns as a dormant, invisible, agent-disabled seed by design — it is planted and woken by Cadaver Growths. Enable 'Cadaver Growths' instead of spawning Blooms directly", ["Feiopar"] = "PumaAI normally stalks from trees; inside the Company building it falls back to ground stalking", ["Earth Leviathan"] = "SandWormAI burrows through terrain and surfaces under players — it works, but looks wrong indoors" }; private static readonly HashSet<string> ListedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase); private static bool _whitelistMode; internal static readonly List<EnemyType> Enemies = new List<EnemyType>(); 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<EnemyAI>(true) : null); return ((Object)(object)val != (Object)null) ? ((object)val).GetType().Name : "<no EnemyAI component>"; } catch (Exception ex) { return "<unreadable: " + ex.GetType().Name + ">"; } } 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<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); List<string> list = new List<string>(); EnemyType[] array = Resources.FindObjectsOfTypeAll<EnemyType>(); 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.Log.LogInfo((object)($"Enemy catalog resolved: {Enemies.Count} spawnable types " + $"({list.Count} rejected).")); LogSpawnabilityReport(list); } private static void LogSpawnabilityReport(List<string> rejected) { foreach (string item in rejected) { Plugin.Log.LogInfo((object)(" rejected: " + item)); } List<string> list = new List<string>(); 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 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<LandingWatcher>(); } 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<float> CumulativeAreas = new List<float>(); public float TotalArea; public void Add((int a, int b, int c) triangle, float area) { Triangles.Add(triangle); TotalArea += area; CumulativeAreas.Add(TotalArea); } } 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 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<long, int> dictionary = new Dictionary<long, int>(); 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<int, float> dictionary2 = new Dictionary<int, float>(); 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<int, float> 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<float> list = new List<float>(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_0074: Unknown result type (might be due to invalid IL or missing references) //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_00a3: 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_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Unknown result type (might be due to invalid IL or missing references) if (_upper.Triangles.Count == 0 && _lower.Triangles.Count == 0) { return null; } NavMeshHit val = default(NavMeshHit); 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); } if (!NavMesh.SamplePosition(RandomPointInTriangle(tier), ref val, 3f, -1)) { continue; } Vector3 position = ((NavMeshHit)(ref val)).position; if (!IsInsideShip(position) && (!_requireIndoor || HasCeilingAbove(position)) && (!(minPlayerDistance > 0f) || !IsTooCloseToAnyPlayer(position, minPlayerDistance))) { if (IsReachable(position)) { return position; } Plugin.DebugLog($"Rejected unreachable point {position:F1}."); } } return null; } 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(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<float> 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.2.1")] [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(RoundManagerPatches)); LandingWatcher.Create(); Log.LogInfo((object)"MonstersGordion v1.2.1 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<bool> Enabled; public ConfigEntry<int> SpawnWeight; public ConfigEntry<int> MinSpawnCount; public ConfigEntry<int> MaxSpawnCount; } internal sealed class PluginConfig { public readonly ConfigEntry<int> GlobalCap; public readonly ConfigEntry<float> MinSpawnInterval; public readonly ConfigEntry<float> MaxSpawnInterval; public readonly ConfigEntry<bool> RespawnOnLoad; public readonly ConfigEntry<bool> DebugMode; public readonly ConfigEntry<int> UpperFloorSpawnShare; public readonly ConfigEntry<int> OutsideEnemyShare; public readonly ConfigEntry<bool> OldBirdUpperFloorOnly; public readonly ConfigEntry<int> CoilHeadTurretChance; public readonly ConfigEntry<int> CoilHeadSlayerChance; public readonly ConfigEntry<int> ManticoilTurretChance; public readonly ConfigEntry<int> ManticoilSlayerChance; public readonly ConfigEntry<int> MaskedTurretChance; public readonly ConfigEntry<int> MaskedSlayerChance; public readonly ConfigEntry<int> VainShroudIterations; public readonly ConfigEntry<bool> FeioparFakeTrees; public readonly ConfigEntry<bool> DespawnOnShipLeave; public readonly ConfigEntry<bool> CountForeignEnemies; public readonly ConfigEntry<float> MinDistanceFromPlayers; public readonly ConfigEntry<float> SpawnYOffset; public readonly ConfigEntry<int> AINodeCount; public readonly ConfigEntry<bool> RequireIndoorPoints; public readonly ConfigEntry<string> ExcludedEnemies; public readonly ConfigEntry<bool> ExcludedEnemiesIsWhitelist; public readonly ConfigEntry<bool> TreatEnemiesAsOutside; public readonly ConfigEntry<float> MaintenanceInterval; public readonly ConfigEntry<ForeignEnemyPolicy> ForeignEnemies; private readonly ConfigFile _file; private readonly Dictionary<string, EnemySpawnSettings> _enemySettings = new Dictionary<string, EnemySpawnSettings>(StringComparer.OrdinalIgnoreCase); private static readonly (bool en, int w, int min, int max) UnknownEnemyDefaults = (en: false, w: 10, min: 0, max: 1); private static readonly Dictionary<string, (bool en, int w, int min, int max)> VanillaDefaults = new Dictionary<string, (bool, int, int, int)>(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), ["Feiopar"] = (false, 6, 0, 1), ["Cadaver Growths"] = (false, 4, 0, 1), ["Cadaver Bloom"] = (false, 4, 0, 2), ["Bush Wolf"] = (false, 6, 0, 1), ["Earth Leviathan"] = (false, 2, 0, 1), ["Manticoil"] = (false, 10, 0, 3), ["Red Locust Bees"] = (false, 5, 0, 1), ["Docile Locust Bees"] = (false, 5, 0, 2), ["Butler Bees"] = (false, 3, 0, 1) }; 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 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_0191: Unknown result type (might be due to invalid IL or missing references) //IL_019b: Expected O, but got Unknown //IL_01b8: Unknown result type (might be due to invalid IL or missing references) //IL_01c2: Expected O, but got Unknown //IL_01e0: Unknown result type (might be due to invalid IL or missing references) //IL_01ea: Expected O, but got Unknown //IL_0207: Unknown result type (might be due to invalid IL or missing references) //IL_0211: Expected O, but got Unknown //IL_022e: Unknown result type (might be due to invalid IL or missing references) //IL_0238: Expected O, but got Unknown //IL_0255: Unknown result type (might be due to invalid IL or missing references) //IL_025f: Expected O, but got Unknown //IL_0283: Unknown result type (might be due to invalid IL or missing references) //IL_028d: Expected O, but got Unknown //IL_0310: Unknown result type (might be due to invalid IL or missing references) //IL_031a: Expected O, but got Unknown //IL_0349: Unknown result type (might be due to invalid IL or missing references) //IL_0353: Expected O, but got Unknown //IL_0378: Unknown result type (might be due to invalid IL or missing references) //IL_0382: Expected O, but got Unknown //IL_0425: Unknown result type (might be due to invalid IL or missing references) //IL_042f: Expected O, but got Unknown _file = file; GlobalCap = file.Bind<int>("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<int>(0, 40), Array.Empty<object>())); MinSpawnInterval = file.Bind<float>("General", "MinSpawnInterval", 15f, new ConfigDescription("Minimum delay between spawn attempts, in seconds.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(1f, 600f), Array.Empty<object>())); MaxSpawnInterval = file.Bind<float>("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<float>(1f, 600f), Array.Empty<object>())); RespawnOnLoad = file.Bind<bool>("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<bool>("General", "DebugMode", false, "Verbose logging: timer intervals, alive counts, weighted-pick results, spawn errors."); UpperFloorSpawnShare = file.Bind<int>("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<int>(0, 100), Array.Empty<object>())); OutsideEnemyShare = file.Bind<int>("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<int>(0, 100), Array.Empty<object>())); OldBirdUpperFloorOnly = file.Bind<bool>("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."); AcceptableValueRange<int> val = new AcceptableValueRange<int>(0, 100); CoilHeadTurretChance = file.Bind<int>("ToilHead", "CoilHeadTurretChance", 25, new ConfigDescription("Percent chance a spawned Coil-Head gets a turret head.", (AcceptableValueBase)(object)val, Array.Empty<object>())); CoilHeadSlayerChance = file.Bind<int>("ToilHead", "CoilHeadSlayerChance", 0, new ConfigDescription("Percent of Coil-Head turrets that are the Slayer (minigun) variant.", (AcceptableValueBase)(object)val, Array.Empty<object>())); ManticoilTurretChance = file.Bind<int>("ToilHead", "ManticoilTurretChance", 25, new ConfigDescription("Percent chance a spawned Manticoil gets a turret head.", (AcceptableValueBase)(object)val, Array.Empty<object>())); ManticoilSlayerChance = file.Bind<int>("ToilHead", "ManticoilSlayerChance", 0, new ConfigDescription("Percent of Manticoil turrets that are the Slayer (minigun) variant.", (AcceptableValueBase)(object)val, Array.Empty<object>())); MaskedTurretChance = file.Bind<int>("ToilHead", "MaskedTurretChance", 0, new ConfigDescription("Percent chance a spawned Masked (mimic) gets a turret head.", (AcceptableValueBase)(object)val, Array.Empty<object>())); MaskedSlayerChance = file.Bind<int>("ToilHead", "MaskedSlayerChance", 0, new ConfigDescription("Percent of Masked turrets that are the Slayer (minigun) variant.", (AcceptableValueBase)(object)val, Array.Empty<object>())); VainShroudIterations = file.Bind<int>("Integration", "VainShroudIterations", 0, new ConfigDescription("Grows vain shrouds (weeds) on the Company moon by setting the level's own moldSpreadIterations, so the game generates and network-syncs them exactly like on any other moon. Required by the Kidnapper Fox (Bush Wolf), which despawns itself on spawn when there is nothing to hide in. 0 = automatic: weeds are grown only when Bush Wolf is enabled (12 iterations). Higher = more overgrowth.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 40), Array.Empty<object>())); FeioparFakeTrees = file.Bind<bool>("Integration", "FeioparFakeTrees", false, "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 fabricates fake tree nodes on the interior navmesh (with the overhead collider the game checks for) so Feiopar can stalk and pounce. It may perch oddly near the ceiling — turn this off if it looks broken. Ignored when Feiopar is disabled or absent."); DespawnOnShipLeave = file.Bind<bool>("Advanced", "DespawnOnShipLeave", true, "Despawn enemies created by this mod when the ship leaves the Company moon."); CountForeignEnemies = file.Bind<bool>("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<float>("Advanced", "MinDistanceFromPlayers", 12f, new ConfigDescription("Never spawn closer than this many meters to a player.", (AcceptableValueBase)(object)new AcceptableValueRange<float>(0f, 60f), Array.Empty<object>())); SpawnYOffset = file.Bind<float>("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<float>(0f, 2f), Array.Empty<object>())); AINodeCount = file.Bind<int>("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<int>(4, 64), Array.Empty<object>())); RequireIndoorPoints = file.Bind<bool>("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<string>("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<bool>("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<bool>("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<float>("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<float>(1f, 30f), Array.Empty<object>())); ForeignEnemies = file.Bind<ForeignEnemyPolicy>("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."); } 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_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_0101: 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<bool>(text, "Enabled", value2.Item1, "Include '" + name + "' in the spawn pool."), SpawnWeight = _file.Bind<int>(text, "SpawnWeight", value2.Item2, new ConfigDescription("Relative weight for the weighted-random pick (0 disables).", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 100), Array.Empty<object>())), MinSpawnCount = _file.Bind<int>(text, "MinSpawnCount", value2.Item3, new ConfigDescription("The spawner tries to keep at least this many alive (global cap permitting).", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 20), Array.Empty<object>())), MaxSpawnCount = _file.Bind<int>(text, "MaxSpawnCount", value2.Item4, new ConfigDescription("Never allow more than this many alive at once.", (AcceptableValueBase)(object)new AcceptableValueRange<int>(0, 20), Array.Empty<object>())) }; _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.2.1"; } } 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 RoundManagerPatches { [HarmonyPrefix] [HarmonyPatch(typeof(RoundManager), "LoadNewLevel")] private static void LoadNewLevel_Prefix(SelectableLevel newLevel) { try { bool flag = (Object)(object)newLevel != (Object)null && IsCompanyLevel(newLevel); Plugin.Log.LogInfo((object)("LoadNewLevel hook fired: planet='" + newLevel?.PlanetName + "', scene='" + newLevel?.sceneName + "', " + $"levelID={newLevel?.levelID}, isCompany={flag}.")); if (!flag) { return; } int num = Plugin.Cfg.ResolveVainShroudIterations(); bool value = Plugin.Cfg.ForName("Bush Wolf").Enabled.Value; Plugin.Log.LogInfo((object)($"Vain shroud setup on '{newLevel.PlanetName}': Bush Wolf enabled={value}, " + $"VainShroudIterations config={Plugin.Cfg.VainShroudIterations.Value}, " + $"resolved iterations={num}, level's current moldSpreadIterations=" + $"{newLevel.moldSpreadIterations}.")); if (newLevel.moldSpreadIterations != num) { newLevel.moldSpreadIterations = num; newLevel.moldStartPosition = -1; if (num > 0) { Plugin.Log.LogInfo((object)("Vain shrouds: growing weeds on '" + newLevel.PlanetName + "' " + $"(moldSpreadIterations={num}). This uses the game's own generation and " + "network sync, and is what lets Bush Wolf (BushWolfEnemy) survive here.")); } else { Plugin.DebugLog("Vain shrouds disabled on '" + newLevel.PlanetName + "'."); } } } catch (Exception arg) { Plugin.Log.LogError((object)$"Vain shroud setup failed: {arg}"); } } private static bool IsCompanyLevel(SelectableLevel level) { if (!(level.sceneName == "CompanyBuilding") && level.levelID != 3) { if (level.PlanetName != null) { return level.PlanetName.Contains("Gordion"); } return false; } return true; } } [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 static bool _scanned; public static bool Present { get; private set; } 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; Plugin.Log.LogInfo((object)($"BrutalCompanyMinus detected ({val.Metadata.GUID} v{val.Metadata.Version}). " + "Its enemies " + (Plugin.Cfg.CountForeignEnemies.Value ? "count toward this mod's GlobalCap (shared budget)." : "are IGNORED by this mod's caps (CountForeignEnemies=false)."))); } catch (Exception ex) { Plugin.Log.LogWarning((object)("BCME detection failed: " + ex.Message)); } } } internal static class CompatScanner { public static PluginInfo FindPluginInfo(string preferredGuid, string fragment) { try { Dictionary<string, PluginInfo> pluginInfos = Chainloader.PluginInfos; if (pluginInfos.TryGetValue(preferredGuid, out var value)) { return value; } return ((IEnumerable<PluginInfo>)pluginInfos.Values).FirstOrDefault((Func<PluginInfo, bool>)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<Type> 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 "—"; } } }