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.InteropServices; using System.Runtime.Versioning; using System.Threading.Tasks; using BepInEx; using BepInEx.Configuration; using HarmonyLib; using PerfectRandom.Sulfur.Core; using PerfectRandom.Sulfur.Core.LevelGeneration; using PerfectRandom.Sulfur.Core.Units; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.InputSystem.Controls; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.Default | DebuggableAttribute.DebuggingModes.DisableOptimizations | DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints | DebuggableAttribute.DebuggingModes.EnableEditAndContinue)] [assembly: AssemblyTitle("Dynamic Pressure")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("Dynamic Pressure")] [assembly: AssemblyCopyright("Copyright © 2026")] [assembly: AssemblyTrademark("")] [assembly: ComVisible(false)] [assembly: Guid("bf243145-c378-4bad-8447-4747dbf03372")] [assembly: AssemblyFileVersion("1.0.0.0")] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyVersion("1.0.0.0")] namespace Ryuka.Sulfur.DynamicPressure; [BepInPlugin("ryuka.sulfur.dynamicpressure", "Dynamic Pressure", "0.1.0")] public sealed class DynamicPressurePlugin : BaseUnityPlugin { private struct SpawnPointChoice { public Vector3 position; public Room room; public float distanceToPlayer; public string source; } private sealed class Snapshot { public bool hasGameManager; public bool hasPlayer; public bool playerAlive; public bool inSafeZone; public string gameState = "Unknown"; public string currentEnvironment = "Unknown"; public float playerHp; public float playerTimeSinceDamage; public string playerRoomName = "null"; public bool playerRoomIsEndRoom; public int aliveNpcs; public int originalHostileAlive; public int originalEngagedHostiles; public int nearbyOriginalHostiles; public int originalTargetingPlayer; public int modHostileAlive; public int nearbyModHostiles; public int modTargetingPlayer; public int originalPressure; public int modPressure; public int currentPressure; public int targetPressure; public int deficit; public int candidateCount; public string candidateSource = "None"; public int spawnedThisLevel; public int spawnedSinceLastOriginalKill; public float timeSinceLastOriginalKill; public float timeSinceLastModKill; public float nextSpawnIn; public bool wouldDelayOnAllEnemiesDead; } [CompilerGenerated] private sealed class d__70 : IEnumerator, IDisposable, IEnumerator { private int <>1__state; private object <>2__current; public Npc npc; public DynamicPressureSpawnMarker marker; public DynamicPressurePlugin <>4__this; private GameManager 5__1; private Exception 5__2; object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } object IEnumerator.Current { [DebuggerHidden] get { return <>2__current; } } [DebuggerHidden] public d__70(int <>1__state) { this.<>1__state = <>1__state; } [DebuggerHidden] void IDisposable.Dispose() { 5__1 = null; 5__2 = null; <>1__state = -2; } private bool MoveNext() { //IL_0026: Unknown result type (might be due to invalid IL or missing references) //IL_0030: Expected O, but got Unknown switch (<>1__state) { default: return false; case 0: <>1__state = -1; <>2__current = (object)new WaitForSeconds(0.5f); <>1__state = 1; return true; case 1: <>1__state = -1; 5__1 = StaticInstance.Instance; if ((Object)(object)5__1 == (Object)null || (Object)(object)5__1.PlayerObject == (Object)null || (Object)(object)5__1.PlayerUnit == (Object)null) { <>4__this._lastAggroReport = "Failed: no player"; return false; } if ((Object)(object)npc == (Object)null || !((Unit)npc).IsAlive || (Object)(object)npc.AiAgent == (Object)null) { <>4__this._lastAggroReport = "Failed: invalid npc"; return false; } try { npc.AiAgent.ReportLastSeen(5__1.PlayerObject); marker.aggroReported = true; marker.lastAggroReportTime = Time.time; marker.targetingPlayerAfterReport = <>4__this.SafeTargetIsPlayer(npc); marker.hasKnownPlayerPositionAfterReport = <>4__this.SafeHasKnownPlayerPosition(npc, 5__1.PlayerUnit); <>4__this._lastAggroReport = ((marker.targetingPlayerAfterReport || marker.hasKnownPlayerPositionAfterReport) ? "Success" : "Reported but not targeting yet"); <>4__this.AddEvent("ReportLastSeen: " + <>4__this._lastAggroReport); } catch (Exception ex) { 5__2 = ex; <>4__this._lastAggroReport = "Exception: " + 5__2.GetType().Name; ((BaseUnityPlugin)<>4__this).Logger.LogWarning((object)5__2); } return false; } } bool IEnumerator.MoveNext() { //ILSpy generated this explicit interface implementation from .override directive in MoveNext return this.MoveNext(); } [DebuggerHidden] void IEnumerator.Reset() { throw new NotSupportedException(); } } public const string PluginGuid = "ryuka.sulfur.dynamicpressure"; public const string PluginName = "Dynamic Pressure"; public const string PluginVersion = "0.1.0"; private static DynamicPressurePlugin _instance; private Harmony _harmony; private ConfigEntry _enableMod; private ConfigEntry _enableAutoSpawn; private ConfigEntry _pressureStyle; private ConfigEntry _enableOverlay; private ConfigEntry _overlayToggleKey; private ConfigEntry _manualSpawnKey; private ConfigEntry _scanInterval; private ConfigEntry _enemyScanRadius; private ConfigEntry _closeEnemyDistance; private ConfigEntry _lowHealthStopThreshold; private ConfigEntry _recentDamageStopWindow; private ConfigEntry _minOriginalEngagedHostilesForSpawn; private ConfigEntry _cooldownAfterModKillOnly; private ConfigEntry _maxSecondsWithoutOriginalKill; private ConfigEntry _maxModSpawnsPerOriginalEngaged; private ConfigEntry _allowEnvironmentFallbackCandidates; private ConfigEntry _spawnDistanceMin; private ConfigEntry _spawnDistanceMax; private ConfigEntry _style1Cooldown; private ConfigEntry _style2Cooldown; private ConfigEntry _style3Cooldown; private ConfigEntry _style1TargetPressure; private ConfigEntry _style2TargetPressure; private ConfigEntry _style3TargetPressure; private ConfigEntry _style1MaxSpawnPerWave; private ConfigEntry _style2MaxSpawnPerWave; private ConfigEntry _style3MaxSpawnPerWave; private ConfigEntry _style1MaxModAlive; private ConfigEntry _style2MaxModAlive; private ConfigEntry _style3MaxModAlive; private ConfigEntry _style1MaxModPerRoom; private ConfigEntry _style2MaxModPerRoom; private ConfigEntry _style3MaxModPerRoom; private ConfigEntry _style1MaxModPerLevel; private ConfigEntry _style2MaxModPerLevel; private ConfigEntry _style3MaxModPerLevel; private readonly List _candidatePool = new List(); private readonly List _recentEvents = new List(); private readonly Dictionary _modSpawnedPerRoom = new Dictionary(); private readonly HashSet _deadNpcIds = new HashSet(); private object _lastGraphContext; private float _nextScanTime; private float _nextSpawnTime; private bool _spawnInProgress; private bool _inLevelTransition; private int _waveId; private int _spawnedThisLevel; private int _spawnedSinceLastOriginalKill; private float _lastOriginalKillTime = -9999f; private float _lastModKillTime = -9999f; private Snapshot _snapshot = new Snapshot(); private SpawnBlockReason _lastBlockReason = SpawnBlockReason.None; private string _lastDecision = "None"; private string _lastSpawnSummary = "None"; private string _lastAggroReport = "None"; private void Awake() { //IL_0014: Unknown result type (might be due to invalid IL or missing references) //IL_001e: Expected O, but got Unknown _instance = this; BindConfig(); _harmony = new Harmony("ryuka.sulfur.dynamicpressure"); TryPatchTransitions(); TryPatchNpcDie(); Log("Loaded."); } private void OnDestroy() { try { Harmony harmony = _harmony; if (harmony != null) { harmony.UnpatchSelf(); } } catch { } if ((Object)(object)_instance == (Object)(object)this) { _instance = null; } } private void BindConfig() { _enableMod = ((BaseUnityPlugin)this).Config.Bind("General", "EnableMod", true, "Enable Dynamic Pressure."); _enableAutoSpawn = ((BaseUnityPlugin)this).Config.Bind("General", "EnableAutoSpawn", true, "Enable automatic dynamic pressure spawning. Keep false for first debug testing."); _pressureStyle = ((BaseUnityPlugin)this).Config.Bind("General", "PressureStyle", 1, "1 = Light, 2 = Heavy, 3 = Nightmare."); _enableOverlay = ((BaseUnityPlugin)this).Config.Bind("Debug Overlay", "EnableOverlay", false, "Show debug overlay."); _overlayToggleKey = ((BaseUnityPlugin)this).Config.Bind("Debug Overlay", "OverlayToggleKey", (Key)102, "Toggle overlay key."); _manualSpawnKey = ((BaseUnityPlugin)this).Config.Bind("Debug Overlay", "ManualSpawnKey", (Key)101, "Manual debug spawn key."); _scanInterval = ((BaseUnityPlugin)this).Config.Bind("Pressure", "ScanInterval", 1f, "Seconds between pressure scans."); _enemyScanRadius = ((BaseUnityPlugin)this).Config.Bind("Pressure", "EnemyScanRadius", 25f, "Nearby enemy scan radius."); _closeEnemyDistance = ((BaseUnityPlugin)this).Config.Bind("Pressure", "CloseEnemyDistance", 8f, "Enemies closer than this add extra pressure."); _lowHealthStopThreshold = ((BaseUnityPlugin)this).Config.Bind("Safety", "LowHealthStopThreshold", 0.35f, "Do not spawn if player health is below this normalized value."); _recentDamageStopWindow = ((BaseUnityPlugin)this).Config.Bind("Safety", "RecentDamageStopWindow", 4f, "Do not spawn if player was damaged within this many seconds."); _minOriginalEngagedHostilesForSpawn = ((BaseUnityPlugin)this).Config.Bind("Anti Loop", "MinOriginalEngagedHostilesForSpawn", 1, "Require this many engaged original hostiles before spawning mod enemies."); _cooldownAfterModKillOnly = ((BaseUnityPlugin)this).Config.Bind("Anti Loop", "CooldownAfterModKillOnly", 8f, "If only mod enemies are dying, pause spawning for this many seconds."); _maxSecondsWithoutOriginalKill = ((BaseUnityPlugin)this).Config.Bind("Anti Loop", "MaxSecondsWithoutOriginalKill", 30f, "Stop spawning if no original enemy has died for this long after mod spawns."); _maxModSpawnsPerOriginalEngaged = ((BaseUnityPlugin)this).Config.Bind("Anti Loop", "MaxModSpawnsPerOriginalEngaged", 2, "Soft budget: each engaged original enemy can support this many mod spawns."); _allowEnvironmentFallbackCandidates = ((BaseUnityPlugin)this).Config.Bind("Candidates", "AllowEnvironmentFallbackCandidates", false, "Use current environment enemy metadata if current level alive NPC candidate pool is empty."); _spawnDistanceMin = ((BaseUnityPlugin)this).Config.Bind("Spawn", "SpawnDistanceMin", 8f, "Minimum spawn distance from player."); _spawnDistanceMax = ((BaseUnityPlugin)this).Config.Bind("Spawn", "SpawnDistanceMax", 60f, "Maximum spawn distance from player."); _style1TargetPressure = ((BaseUnityPlugin)this).Config.Bind("Style 1 - Light", "TargetPressure", 6, "Target pressure."); _style1Cooldown = ((BaseUnityPlugin)this).Config.Bind("Style 1 - Light", "SpawnCooldown", 16f, "Spawn cooldown."); _style1MaxSpawnPerWave = ((BaseUnityPlugin)this).Config.Bind("Style 1 - Light", "MaxSpawnPerWave", 1, "Max spawns per wave."); _style1MaxModAlive = ((BaseUnityPlugin)this).Config.Bind("Style 1 - Light", "MaxModSpawnedAlive", 2, "Max alive mod-spawned NPCs."); _style1MaxModPerRoom = ((BaseUnityPlugin)this).Config.Bind("Style 1 - Light", "MaxModSpawnedPerRoom", 3, "Max mod spawns per room."); _style1MaxModPerLevel = ((BaseUnityPlugin)this).Config.Bind("Style 1 - Light", "MaxModSpawnedPerLevel", 8, "Max mod spawns per level."); _style2TargetPressure = ((BaseUnityPlugin)this).Config.Bind("Style 2 - Heavy", "TargetPressure", 9, "Target pressure."); _style2Cooldown = ((BaseUnityPlugin)this).Config.Bind("Style 2 - Heavy", "SpawnCooldown", 10f, "Spawn cooldown."); _style2MaxSpawnPerWave = ((BaseUnityPlugin)this).Config.Bind("Style 2 - Heavy", "MaxSpawnPerWave", 1, "Max spawns per wave."); _style2MaxModAlive = ((BaseUnityPlugin)this).Config.Bind("Style 2 - Heavy", "MaxModSpawnedAlive", 4, "Max alive mod-spawned NPCs."); _style2MaxModPerRoom = ((BaseUnityPlugin)this).Config.Bind("Style 2 - Heavy", "MaxModSpawnedPerRoom", 5, "Max mod spawns per room."); _style2MaxModPerLevel = ((BaseUnityPlugin)this).Config.Bind("Style 2 - Heavy", "MaxModSpawnedPerLevel", 16, "Max mod spawns per level."); _style3TargetPressure = ((BaseUnityPlugin)this).Config.Bind("Style 3 - Nightmare", "TargetPressure", 13, "Target pressure."); _style3Cooldown = ((BaseUnityPlugin)this).Config.Bind("Style 3 - Nightmare", "SpawnCooldown", 7f, "Spawn cooldown."); _style3MaxSpawnPerWave = ((BaseUnityPlugin)this).Config.Bind("Style 3 - Nightmare", "MaxSpawnPerWave", 2, "Max spawns per wave."); _style3MaxModAlive = ((BaseUnityPlugin)this).Config.Bind("Style 3 - Nightmare", "MaxModSpawnedAlive", 6, "Max alive mod-spawned NPCs."); _style3MaxModPerRoom = ((BaseUnityPlugin)this).Config.Bind("Style 3 - Nightmare", "MaxModSpawnedPerRoom", 8, "Max mod spawns per room."); _style3MaxModPerLevel = ((BaseUnityPlugin)this).Config.Bind("Style 3 - Nightmare", "MaxModSpawnedPerLevel", 28, "Max mod spawns per level."); } private void Update() { HandleKeys(); if (!_enableMod.Value) { _lastDecision = "BLOCKED"; _lastBlockReason = SpawnBlockReason.Disabled; return; } GameManager instance = StaticInstance.Instance; if ((Object)(object)instance != (Object)null && (Object)(object)instance.graphContext != (Object)null && _lastGraphContext != instance.graphContext) { _lastGraphContext = instance.graphContext; ResetRuntimeState("New graphContext"); } if (Time.time >= _nextScanTime) { _nextScanTime = Time.time + Mathf.Max(0.2f, _scanInterval.Value); ScanPressure(); } if (_enableAutoSpawn.Value && !_spawnInProgress && Time.time >= _nextSpawnTime) { TryAutoSpawn(); } } private void HandleKeys() { //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_004f: Unknown result type (might be due to invalid IL or missing references) Keyboard current = Keyboard.current; if (current != null) { if (((ButtonControl)current[_overlayToggleKey.Value]).wasPressedThisFrame) { _enableOverlay.Value = !_enableOverlay.Value; } if (((ButtonControl)current[_manualSpawnKey.Value]).wasPressedThisFrame && !_spawnInProgress) { SpawnWaveAsync(1, "Manual debug spawn"); } } } private void ScanPressure() { //IL_0043: 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_01ca: Unknown result type (might be due to invalid IL or missing references) //IL_01d0: Unknown result type (might be due to invalid IL or missing references) Snapshot snapshot = new Snapshot(); _candidatePool.Clear(); GameManager instance = StaticInstance.Instance; if ((Object)(object)instance == (Object)null) { _snapshot = snapshot; _lastBlockReason = SpawnBlockReason.NoGameManager; return; } snapshot.hasGameManager = true; GameState gameState = instance.gameState; snapshot.gameState = ((object)(GameState)(ref gameState)).ToString(); snapshot.inSafeZone = instance.InSafeZone; snapshot.currentEnvironment = (((Object)(object)instance.currentEnvironment != (Object)null) ? ((object)(WorldEnvironmentIds)(ref instance.currentEnvironment.id)).ToString() : "null"); if ((Object)(object)instance.PlayerUnit == (Object)null || (Object)(object)instance.PlayerObject == (Object)null) { _snapshot = snapshot; _lastBlockReason = SpawnBlockReason.NoPlayer; return; } Unit playerUnit = instance.PlayerUnit; Room unitRoom = GetUnitRoom(playerUnit); snapshot.hasPlayer = true; snapshot.playerAlive = playerUnit.IsAlive; snapshot.playerHp = SafeGetPlayerHp(playerUnit); snapshot.playerTimeSinceDamage = playerUnit.TimeSinceLastDamageTaken; snapshot.playerRoomName = (((Object)(object)unitRoom != (Object)null) ? ((Object)unitRoom).name : "null"); snapshot.playerRoomIsEndRoom = (Object)(object)unitRoom != (Object)null && unitRoom.IsEndRoom; List aliveNpcs = instance.aliveNpcs; snapshot.aliveNpcs = aliveNpcs?.Count ?? 0; if (aliveNpcs != null) { for (int i = 0; i < aliveNpcs.Count; i++) { Npc val = aliveNpcs[i]; if (!IsValidAliveNpc(val)) { continue; } DynamicPressureSpawnMarker component = ((Component)val).GetComponent(); bool flag = (Object)(object)component != (Object)null; if (!IsHostileToPlayer(val)) { continue; } float num = Vector3.Distance(((Component)val).transform.position, instance.PlayerPosition); bool flag2 = num <= _enemyScanRadius.Value; bool flag3 = SafeTargetIsPlayer(val); bool flag4 = SafeHasKnownPlayerPosition(val, instance.PlayerUnit); bool flag5 = IsSameOrConnectedRoom(GetUnitRoom((Unit)(object)val), unitRoom); bool flag6 = flag2 || flag3 || flag4 || flag5; int num2 = CalculateNpcPressure(val, num, flag3, flag4); if (flag) { snapshot.modHostileAlive++; if (flag2) { snapshot.nearbyModHostiles++; snapshot.modPressure += num2; } if (flag3) { snapshot.modTargetingPlayer++; } continue; } snapshot.originalHostileAlive++; if (flag6) { snapshot.originalEngagedHostiles++; } if (flag2) { snapshot.nearbyOriginalHostiles++; snapshot.originalPressure += num2; } if (flag3) { snapshot.originalTargetingPlayer++; } TryAddCandidateFromNpc(val); } } if (_candidatePool.Count == 0 && _allowEnvironmentFallbackCandidates.Value) { AddEnvironmentFallbackCandidates(instance); snapshot.candidateSource = ((_candidatePool.Count > 0) ? "EnvironmentMetadata" : "None"); } else { snapshot.candidateSource = ((_candidatePool.Count > 0) ? "CurrentLevelAliveNpcs" : "None"); } snapshot.candidateCount = _candidatePool.Count; snapshot.targetPressure = GetTargetPressure(); snapshot.currentPressure = snapshot.originalPressure + snapshot.modPressure; snapshot.deficit = snapshot.targetPressure - snapshot.currentPressure; snapshot.spawnedThisLevel = _spawnedThisLevel; snapshot.spawnedSinceLastOriginalKill = _spawnedSinceLastOriginalKill; snapshot.timeSinceLastOriginalKill = Time.time - _lastOriginalKillTime; snapshot.timeSinceLastModKill = Time.time - _lastModKillTime; snapshot.nextSpawnIn = Mathf.Max(0f, _nextSpawnTime - Time.time); snapshot.wouldDelayOnAllEnemiesDead = snapshot.originalHostileAlive == 0 && snapshot.modHostileAlive > 0; _snapshot = snapshot; } private void TryAutoSpawn() { if (!CanSpawn(out var blockReason, out var spawnCount)) { _lastDecision = "BLOCKED"; _lastBlockReason = blockReason; } else { SpawnWaveAsync(spawnCount, "Auto pressure deficit"); } } private bool CanSpawn(out SpawnBlockReason blockReason, out int spawnCount) { //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) spawnCount = 0; GameManager instance = StaticInstance.Instance; if ((Object)(object)instance == (Object)null) { blockReason = SpawnBlockReason.NoGameManager; return false; } if (_inLevelTransition) { blockReason = SpawnBlockReason.LevelTransition; return false; } if ((Object)(object)instance.PlayerUnit == (Object)null || (Object)(object)instance.PlayerObject == (Object)null) { blockReason = SpawnBlockReason.NoPlayer; return false; } GameState gameState = instance.gameState; if (((object)(GameState)(ref gameState)).ToString() != "Running") { blockReason = SpawnBlockReason.GameStateNotRunning; return false; } if (instance.InSafeZone) { blockReason = SpawnBlockReason.SafeZone; return false; } Unit playerUnit = instance.PlayerUnit; if (!playerUnit.IsAlive) { blockReason = SpawnBlockReason.PlayerDead; return false; } if (SafeGetPlayerHp(playerUnit) <= _lowHealthStopThreshold.Value) { blockReason = SpawnBlockReason.LowHealth; return false; } if (playerUnit.TimeSinceLastDamageTaken <= _recentDamageStopWindow.Value) { blockReason = SpawnBlockReason.RecentDamage; return false; } Room unitRoom = GetUnitRoom(playerUnit); if ((Object)(object)unitRoom != (Object)null && unitRoom.IsEndRoom) { blockReason = SpawnBlockReason.EndRoomBlocked; return false; } if (_snapshot.originalEngagedHostiles < _minOriginalEngagedHostilesForSpawn.Value) { blockReason = SpawnBlockReason.NoEngagedOriginalHostiles; return false; } if (_snapshot.currentPressure >= _snapshot.targetPressure) { blockReason = SpawnBlockReason.PressureAlreadyHigh; return false; } if (_snapshot.modHostileAlive >= GetMaxModAlive()) { blockReason = SpawnBlockReason.MaxModSpawnedAlive; return false; } if (_spawnedThisLevel >= GetMaxModPerLevel()) { blockReason = SpawnBlockReason.LevelSpawnBudgetExceeded; return false; } int num = Mathf.Max(1, _snapshot.originalEngagedHostiles) * Mathf.Max(1, _maxModSpawnsPerOriginalEngaged.Value); if (_spawnedSinceLastOriginalKill >= num) { blockReason = SpawnBlockReason.SpawnBudgetPerOriginalExceeded; return false; } if (_spawnedSinceLastOriginalKill > 0 && Time.time - _lastOriginalKillTime > _maxSecondsWithoutOriginalKill.Value) { blockReason = SpawnBlockReason.NoOriginalKillProgress; return false; } if (_lastModKillTime > _lastOriginalKillTime && Time.time - _lastModKillTime < _cooldownAfterModKillOnly.Value) { blockReason = SpawnBlockReason.RecentModKillOnly; return false; } if (_candidatePool.Count == 0) { blockReason = SpawnBlockReason.NoCandidateUnits; return false; } spawnCount = Mathf.Min(GetMaxSpawnPerWave(), Mathf.Max(1, _snapshot.deficit)); spawnCount = Mathf.Min(spawnCount, GetMaxModAlive() - _snapshot.modHostileAlive); spawnCount = Mathf.Min(spawnCount, GetMaxModPerLevel() - _spawnedThisLevel); if (spawnCount <= 0) { blockReason = SpawnBlockReason.MaxModSpawnedAlive; return false; } blockReason = SpawnBlockReason.None; return true; } private async Task SpawnWaveAsync(int count, string reason) { if (_spawnInProgress) { return; } _spawnInProgress = true; _waveId++; int spawned = 0; try { ScanPressure(); for (int i = 0; i < count; i++) { UnitSO unitSo = PickCandidate(); if ((Object)(object)unitSo == (Object)null) { _lastDecision = "BLOCKED"; _lastBlockReason = SpawnBlockReason.NoCandidateUnits; AddEvent("Spawn blocked: no candidate."); break; } if (!TryFindSpawnPoint(unitSo, out var spawnPoint)) { _lastDecision = "BLOCKED"; _lastBlockReason = SpawnBlockReason.NoValidNpcSpawnPoint; AddEvent("Spawn blocked: no valid NPCSpawn point."); break; } int roomId = GetRoomId(spawnPoint.room); _modSpawnedPerRoom.TryGetValue(roomId, out var roomCount); if (roomCount >= GetMaxModPerRoom()) { _lastDecision = "BLOCKED"; _lastBlockReason = SpawnBlockReason.RoomSpawnBudgetExceeded; AddEvent("Spawn blocked: room budget exceeded."); break; } if (!(await SpawnOneAsync(unitSo, spawnPoint, reason))) { _lastDecision = "FAILED"; _lastBlockReason = SpawnBlockReason.SpawnAsyncFailed; break; } spawned++; spawnPoint = default(SpawnPointChoice); } if (spawned > 0) { _lastDecision = "SPAWNED"; _lastBlockReason = SpawnBlockReason.None; _nextSpawnTime = Time.time + GetSpawnCooldown(); } } catch (Exception ex) { Exception e = ex; _lastDecision = "FAILED"; _lastBlockReason = SpawnBlockReason.SpawnAsyncFailed; AddEvent("Spawn exception: " + e.GetType().Name); ((BaseUnityPlugin)this).Logger.LogError((object)e); } finally { _spawnInProgress = false; ScanPressure(); } } private async Task SpawnOneAsync(UnitSO unitSo, SpawnPointChoice spawnPoint, string reason) { if ((Object)(object)unitSo == (Object)null) { return false; } Unit spawnedUnit = await unitSo.SpawnUnitAsync((MonoBehaviour)(object)this, spawnPoint.position, Quaternion.identity); if ((Object)(object)spawnedUnit == (Object)null) { return false; } Npc npc = (Npc)(object)((spawnedUnit is Npc) ? spawnedUnit : null); if ((Object)(object)npc == (Object)null) { npc = ((Component)spawnedUnit).GetComponent(); } if ((Object)(object)npc == (Object)null) { return false; } if ((Object)(object)spawnPoint.room != (Object)null) { ((Unit)npc).currentRoom = spawnPoint.room; ((Unit)npc).lastValidCurrentRoom = spawnPoint.room; ((Unit)npc).lastRoomCalcPosition = ((Component)npc).transform.position; } DynamicPressureSpawnMarker marker = ((Component)npc).gameObject.AddComponent(); marker.sourceUnitSo = unitSo; marker.spawnTime = Time.time; marker.spawnPosition = spawnPoint.position; marker.spawnRoom = spawnPoint.room; marker.spawnReason = reason; marker.waveId = _waveId; int roomId = GetRoomId(spawnPoint.room); _modSpawnedPerRoom.TryGetValue(roomId, out var roomCount); _modSpawnedPerRoom[roomId] = roomCount + 1; _spawnedThisLevel++; _spawnedSinceLastOriginalKill++; _lastSpawnSummary = string.Format("{0} at {1}, room={2}, source={3}", ((Object)unitSo).name, spawnPoint.position, ((Object)(object)spawnPoint.room != (Object)null) ? ((Object)spawnPoint.room).name : "null", spawnPoint.source); AddEvent("Spawned: " + ((Object)unitSo).name); ((MonoBehaviour)this).StartCoroutine(ReportPlayerPositionLater(npc, marker)); return true; } [IteratorStateMachine(typeof(d__70))] private IEnumerator ReportPlayerPositionLater(Npc npc, DynamicPressureSpawnMarker marker) { //yield-return decompiler failed: Unexpected instruction in Iterator.Dispose() return new d__70(0) { <>4__this = this, npc = npc, marker = marker }; } private UnitSO PickCandidate() { if (_candidatePool.Count == 0) { return null; } return _candidatePool[Random.Range(0, _candidatePool.Count)]; } private void TryAddCandidateFromNpc(Npc npc) { if (!((Object)(object)npc == (Object)null) && !((Object)(object)((Unit)npc).unitSO == (Object)null)) { UnitSO unitSO = ((Unit)npc).unitSO; if (IsValidCandidateUnitSo(unitSO) && !_candidatePool.Contains(unitSO)) { _candidatePool.Add(unitSO); } } } private void AddEnvironmentFallbackCandidates(GameManager gm) { //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0048: 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_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_0061: 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) try { if ((Object)(object)gm == (Object)null || (Object)(object)gm.environmentsInOrder == (Object)null || (Object)(object)gm.currentEnvironment == (Object)null) { return; } Metadata metadata = gm.environmentsInOrder.GetMetadata(gm.currentEnvironment.id); if (metadata.availableEnemiesReadOnly == null) { return; } for (int i = 0; i < metadata.availableEnemiesReadOnly.Length; i++) { UnitSO asset = AssetAccess.GetAsset(metadata.availableEnemiesReadOnly[i]); if ((Object)(object)asset != (Object)null && IsValidCandidateUnitSo(asset) && !_candidatePool.Contains(asset)) { _candidatePool.Add(asset); } } } catch (Exception ex) { AddEvent("Candidate fallback failed: " + ex.GetType().Name); } } private bool IsValidCandidateUnitSo(UnitSO so) { //IL_0047: 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_004f: Invalid comparison between Unknown and I4 if ((Object)(object)so == (Object)null) { return false; } if (so.isCivilian) { return false; } if (so.isProtectedNpc) { return false; } if (so.ExperienceOnKill <= 0) { return false; } if ((so.unitType & 8) > 0) { return false; } return true; } private bool TryFindSpawnPoint(UnitSO unitSo, out SpawnPointChoice result) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_0231: Unknown result type (might be due to invalid IL or missing references) //IL_0254: 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_00f7: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Unknown result type (might be due to invalid IL or missing references) //IL_0118: 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_0123: Unknown result type (might be due to invalid IL or missing references) //IL_0125: Invalid comparison between Unknown and I4 //IL_0132: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Unknown result type (might be due to invalid IL or missing references) //IL_0139: Unknown result type (might be due to invalid IL or missing references) //IL_014b: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_0152: Unknown result type (might be due to invalid IL or missing references) result = default(SpawnPointChoice); GameManager instance = StaticInstance.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)instance.PlayerUnit == (Object)null || (Object)(object)instance.PlayerObject == (Object)null || (Object)(object)unitSo == (Object)null) { return false; } Vector3 playerPosition = instance.PlayerPosition; List list = new List(); List list2 = new List(); List orderedRooms = instance.orderedRooms; if (orderedRooms == null || orderedRooms.Count == 0) { return false; } for (int i = 0; i < orderedRooms.Count; i++) { Room val = orderedRooms[i]; if (!IsValidSpawnRoom(val)) { continue; } Data[] nPCSpawns = val.GetNPCSpawns(); if (nPCSpawns == null || nPCSpawns.Length == 0) { continue; } foreach (Data val2 in nPCSpawns) { Room room = (((Object)(object)val2.room != (Object)null) ? val2.room : val); if (IsValidSpawnRoom(room) && (val2.usableByTypes & unitSo.unitType) != 0) { float num = Vector3.Distance(val2.position, playerPosition); SpawnPointChoice spawnPointChoice = default(SpawnPointChoice); spawnPointChoice.position = val2.position; spawnPointChoice.room = room; spawnPointChoice.distanceToPlayer = num; spawnPointChoice.source = "GameManager.orderedRooms NPCSpawn"; SpawnPointChoice item = spawnPointChoice; if (num >= _spawnDistanceMin.Value && num <= _spawnDistanceMax.Value) { list.Add(item); } else if (num > _spawnDistanceMax.Value && num <= _spawnDistanceMax.Value * 2f) { list2.Add(item); } } } } if (list.Count > 0) { result = PickNearestRandomized(list, playerPosition); return true; } if (list2.Count > 0) { result = PickNearestRandomized(list2, playerPosition); result.source += " / loose distance"; return true; } return false; } private bool IsValidSpawnRoom(Room room) { if ((Object)(object)room == (Object)null) { return false; } if (room.IsStartRoom) { return false; } if (room.IsEndRoom) { return false; } if (room.disallowEnemySpawn) { return false; } return true; } private SpawnPointChoice PickNearestRandomized(List choices, Vector3 playerPos) { choices.Sort((SpawnPointChoice a, SpawnPointChoice b) => a.distanceToPlayer.CompareTo(b.distanceToPlayer)); int num = Mathf.Min(choices.Count, 5); return choices[Random.Range(0, num)]; } private int CalculateNpcPressure(Npc npc, float distance, bool targeting, bool knowsPlayer) { int num = 1; if ((Object)(object)npc != (Object)null && (Object)(object)((Unit)npc).unitSO != (Object)null) { num = Mathf.Clamp(((Unit)npc).unitSO.SpawnCost, 1, 4); } if (distance <= _closeEnemyDistance.Value) { num++; } if (targeting) { num += 2; } else if (knowsPlayer) { num++; } return num; } private bool IsValidAliveNpc(Npc npc) { return (Object)(object)npc != (Object)null && (Object)(object)((Component)npc).gameObject != (Object)null && ((Unit)npc).IsAlive; } private bool IsHostileToPlayer(Npc npc) { try { return (Object)(object)npc != (Object)null && ((Unit)npc).IsHostileTo((FactionIds)16); } catch { return false; } } private bool SafeTargetIsPlayer(Npc npc) { try { return (Object)(object)npc != (Object)null && npc.targetIsPlayer; } catch { return false; } } private bool SafeHasKnownPlayerPosition(Npc npc, Unit playerUnit) { try { return (Object)(object)npc != (Object)null && (Object)(object)npc.AiAgent != (Object)null && (Object)(object)playerUnit != (Object)null && npc.AiAgent.HasKnownPosition(playerUnit); } catch { return false; } } private float SafeGetPlayerHp(Unit playerUnit) { try { return ((Object)(object)playerUnit != (Object)null) ? playerUnit.GetNormalizedHealth() : 0f; } catch { return 0f; } } private Room GetUnitRoom(Unit unit) { if ((Object)(object)unit == (Object)null) { return null; } if ((Object)(object)unit.currentRoom != (Object)null) { return unit.currentRoom; } return unit.lastValidCurrentRoom; } private bool IsSameOrConnectedRoom(Room npcRoom, Room playerRoom) { if ((Object)(object)npcRoom == (Object)null || (Object)(object)playerRoom == (Object)null) { return false; } if ((Object)(object)npcRoom == (Object)(object)playerRoom) { return true; } if (playerRoom.connectedRooms == null) { return false; } for (int i = 0; i < playerRoom.connectedRooms.Length; i++) { if ((Object)(object)playerRoom.connectedRooms[i] == (Object)(object)npcRoom) { return true; } } return false; } private int GetRoomId(Room room) { return ((Object)(object)room != (Object)null) ? ((Object)room).GetInstanceID() : 0; } private int GetStyle() { return Mathf.Clamp(_pressureStyle.Value, 1, 3); } private int GetTargetPressure() { return GetStyle() switch { 1 => _style1TargetPressure.Value, 2 => _style2TargetPressure.Value, _ => _style3TargetPressure.Value, }; } private float GetSpawnCooldown() { return GetStyle() switch { 1 => _style1Cooldown.Value, 2 => _style2Cooldown.Value, _ => _style3Cooldown.Value, }; } private int GetMaxSpawnPerWave() { return GetStyle() switch { 1 => _style1MaxSpawnPerWave.Value, 2 => _style2MaxSpawnPerWave.Value, _ => _style3MaxSpawnPerWave.Value, }; } private int GetMaxModAlive() { return GetStyle() switch { 1 => _style1MaxModAlive.Value, 2 => _style2MaxModAlive.Value, _ => _style3MaxModAlive.Value, }; } private int GetMaxModPerRoom() { return GetStyle() switch { 1 => _style1MaxModPerRoom.Value, 2 => _style2MaxModPerRoom.Value, _ => _style3MaxModPerRoom.Value, }; } private int GetMaxModPerLevel() { return GetStyle() switch { 1 => _style1MaxModPerLevel.Value, 2 => _style2MaxModPerLevel.Value, _ => _style3MaxModPerLevel.Value, }; } private void ResetRuntimeState(string reason) { _candidatePool.Clear(); _recentEvents.Clear(); _modSpawnedPerRoom.Clear(); _deadNpcIds.Clear(); _waveId = 0; _spawnedThisLevel = 0; _spawnedSinceLastOriginalKill = 0; _lastOriginalKillTime = Time.time; _lastModKillTime = -9999f; _lastDecision = "Reset"; _lastBlockReason = SpawnBlockReason.None; _lastSpawnSummary = "None"; _lastAggroReport = "None"; _inLevelTransition = false; _nextSpawnTime = Time.time + 3f; AddEvent("Reset: " + reason); } private static void OnNpcDiePostfix(Npc __instance) { if (!((Object)(object)_instance == (Object)null) && !((Object)(object)__instance == (Object)null)) { _instance.HandleNpcDeath(__instance); } } private void HandleNpcDeath(Npc npc) { int instanceID = ((Object)npc).GetInstanceID(); if (_deadNpcIds.Contains(instanceID)) { return; } _deadNpcIds.Add(instanceID); bool flag = (Object)(object)((Component)npc).GetComponent() != (Object)null; if (IsHostileToPlayer(npc)) { if (flag) { _lastModKillTime = Time.time; AddEvent("Mod NPC died: " + ((Object)npc).name); } else { _lastOriginalKillTime = Time.time; _spawnedSinceLastOriginalKill = 0; AddEvent("Original NPC died: " + ((Object)npc).name); } } } private static void OnLevelTransitionPrefix() { if (!((Object)(object)_instance == (Object)null)) { _instance._inLevelTransition = true; _instance.ResetRuntimeState("Level transition"); } } private void TryPatchTransitions() { MethodInfo prefix = AccessTools.Method(typeof(DynamicPressurePlugin), "OnLevelTransitionPrefix", (Type[])null, (Type[])null); TryPatchMethod(typeof(NextLevelTrigger), "MakeTransition", prefix); TryPatchMethod(typeof(GameManager), "CompleteLevel", prefix); TryPatchAllNamedMethods(typeof(GameManager), "GoToLevel", prefix); TryPatchAllNamedMethods(typeof(GameManager), "GoToChurchHub", prefix); TryPatchAllNamedMethods(typeof(GameManager), "GoToCarHub", prefix); TryPatchAllNamedMethods(typeof(GameManager), "SwitchLevel", prefix); } private void TryPatchNpcDie() { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Expected O, but got Unknown try { MethodInfo methodInfo = AccessTools.Method(typeof(Npc), "Die", (Type[])null, (Type[])null); MethodInfo methodInfo2 = AccessTools.Method(typeof(DynamicPressurePlugin), "OnNpcDiePostfix", (Type[])null, (Type[])null); if (methodInfo != null && methodInfo2 != null) { _harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Log("Patched Npc.Die."); } else { ((BaseUnityPlugin)this).Logger.LogWarning((object)"Failed to patch Npc.Die: method not found."); } } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Failed to patch Npc.Die: " + ex.Message)); } } private void TryPatchMethod(Type type, string methodName, MethodInfo prefix) { //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Expected O, but got Unknown try { MethodInfo methodInfo = AccessTools.Method(type, methodName, (Type[])null, (Type[])null); if (methodInfo == null) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Patch skipped: " + type.Name + "." + methodName)); return; } _harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(prefix), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); Log("Patched " + type.Name + "." + methodName); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Patch failed: " + type.Name + "." + methodName + " / " + ex.Message)); } } private void TryPatchAllNamedMethods(Type type, string methodName, MethodInfo prefix) { //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Expected O, but got Unknown try { MethodInfo[] array = (from m in AccessTools.GetDeclaredMethods(type) where m.Name == methodName select m).ToArray(); if (array.Length == 0) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Patch skipped: " + type.Name + "." + methodName)); return; } for (int i = 0; i < array.Length; i++) { _harmony.Patch((MethodBase)array[i], new HarmonyMethod(prefix), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null); } Log("Patched " + type.Name + "." + methodName + " x" + array.Length); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("Patch failed: " + type.Name + "." + methodName + " / " + ex.Message)); } } private void AddEvent(string text) { _recentEvents.Add(DateTime.Now.ToString("HH:mm:ss") + " " + text); while (_recentEvents.Count > 8) { _recentEvents.RemoveAt(0); } } private void Log(string text) { ((BaseUnityPlugin)this).Logger.LogInfo((object)text); } private void OnGUI() { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0074: Unknown result type (might be due to invalid IL or missing references) if (_enableOverlay.Value) { Rect val = default(Rect); ((Rect)(ref val))..ctor(20f, 120f, 620f, 620f); GUI.Box(val, "Dynamic Pressure Debug"); GUILayout.BeginArea(new Rect(((Rect)(ref val)).x + 10f, ((Rect)(ref val)).y + 25f, ((Rect)(ref val)).width - 20f, ((Rect)(ref val)).height - 35f)); GUILayout.Label("Enabled: " + _enableMod.Value + " | AutoSpawn: " + _enableAutoSpawn.Value + " | Style: " + GetStyle(), Array.Empty()); GUILayout.Label("GameState: " + _snapshot.gameState + " | SafeZone: " + _snapshot.inSafeZone + " | Env: " + _snapshot.currentEnvironment, Array.Empty()); GUILayout.Label("Player: " + (_snapshot.hasPlayer ? "OK" : "Missing") + " | Alive: " + _snapshot.playerAlive + " | HP: " + Mathf.RoundToInt(_snapshot.playerHp * 100f) + "% | DamageAgo: " + _snapshot.playerTimeSinceDamage.ToString("0.0") + "s", Array.Empty()); GUILayout.Label("Room: " + _snapshot.playerRoomName + " | EndRoom: " + _snapshot.playerRoomIsEndRoom, Array.Empty()); GUILayout.Space(8f); GUILayout.Label("Pressure", Array.Empty()); GUILayout.Label("Original Hostile Alive: " + _snapshot.originalHostileAlive + " | Engaged: " + _snapshot.originalEngagedHostiles + " | Targeting: " + _snapshot.originalTargetingPlayer, Array.Empty()); GUILayout.Label("Mod Hostile Alive: " + _snapshot.modHostileAlive + " | Targeting: " + _snapshot.modTargetingPlayer, Array.Empty()); GUILayout.Label("Original Pressure: " + _snapshot.originalPressure + " | Mod Pressure: +" + _snapshot.modPressure + " | Current: " + _snapshot.currentPressure + " / Target: " + _snapshot.targetPressure + " | Deficit: " + _snapshot.deficit, Array.Empty()); GUILayout.Space(8f); GUILayout.Label("Spawn Decision", Array.Empty()); GUILayout.Label("Last Decision: " + _lastDecision + " | Reason: " + _lastBlockReason, Array.Empty()); GUILayout.Label("Next Spawn In: " + _snapshot.nextSpawnIn.ToString("0.0") + "s | Candidate Source: " + _snapshot.candidateSource + " | Candidates: " + _snapshot.candidateCount, Array.Empty()); GUILayout.Space(8f); GUILayout.Label("Mod Impact", Array.Empty()); GUILayout.Label("Spawned This Level: " + _spawnedThisLevel + " / " + GetMaxModPerLevel(), Array.Empty()); GUILayout.Label("Spawned Since Last Original Kill: " + _spawnedSinceLastOriginalKill, Array.Empty()); GUILayout.Label("Time Since Original Kill: " + _snapshot.timeSinceLastOriginalKill.ToString("0.0") + "s | Time Since Mod Kill: " + _snapshot.timeSinceLastModKill.ToString("0.0") + "s", Array.Empty()); GUILayout.Label("Would Delay OnAllEnemiesDead: " + _snapshot.wouldDelayOnAllEnemiesDead, Array.Empty()); GUILayout.Label("Last Spawn: " + _lastSpawnSummary, Array.Empty()); GUILayout.Label("Last Aggro Report: " + _lastAggroReport, Array.Empty()); GUILayout.Space(8f); GUILayout.Label("Keys: F8 Manual Spawn | F9 Toggle Overlay", Array.Empty()); GUILayout.Space(8f); GUILayout.Label("Recent Events", Array.Empty()); for (int i = 0; i < _recentEvents.Count; i++) { GUILayout.Label(_recentEvents[i], Array.Empty()); } GUILayout.EndArea(); } } } public sealed class DynamicPressureSpawnMarker : MonoBehaviour { public UnitSO sourceUnitSo; public float spawnTime; public Vector3 spawnPosition; public Room spawnRoom; public string spawnReason; public int waveId; public bool aggroReported; public float lastAggroReportTime; public bool targetingPlayerAfterReport; public bool hasKnownPlayerPositionAfterReport; } public enum SpawnBlockReason { None, Disabled, NoGameManager, NoPlayer, PlayerDead, SafeZone, GameStateNotRunning, LowHealth, RecentDamage, EndRoomBlocked, SpawnRoomIsEndRoom, NoOriginalHostiles, NoEngagedOriginalHostiles, NotEnoughOriginalHostiles, NoCandidateUnits, NoValidNpcSpawnPoint, Cooldown, MaxModSpawnedAlive, RoomSpawnBudgetExceeded, LevelSpawnBudgetExceeded, PressureAlreadyHigh, SpawnAsyncFailed, LevelTransition, RecentModKillOnly, NoOriginalKillProgress, SpawnBudgetPerOriginalExceeded }