using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using BepInEx;
using BepInEx.Configuration;
using HarmonyLib;
using I2.Loc;
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.IgnoreSymbolStoreSequencePoints)]
[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.9")]
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;
}
public const string PluginGuid = "ryuka.sulfur.dynamicpressure";
public const string PluginName = "Dynamic Pressure";
public const string PluginVersion = "0.1.9";
private static DynamicPressurePlugin _instance;
private Harmony _harmony;
private ConfigEntry<bool> _enableMod;
private ConfigEntry<bool> _enableAutoSpawn;
private ConfigEntry<int> _pressureStyle;
private ConfigEntry<bool> _enableOverlay;
private ConfigEntry<Key> _overlayToggleKey;
private ConfigEntry<Key> _manualSpawnKey;
private ConfigEntry<bool> _logSpawnDecisions;
private ConfigEntry<float> _scanInterval;
private ConfigEntry<float> _enemyScanRadius;
private ConfigEntry<float> _closeEnemyDistance;
private ConfigEntry<float> _lowHealthStopThreshold;
private ConfigEntry<float> _recentDamageStopWindow;
private ConfigEntry<int> _minOriginalEngagedHostilesForSpawn;
private ConfigEntry<float> _cooldownAfterModKillOnly;
private ConfigEntry<float> _maxSecondsWithoutOriginalKill;
private ConfigEntry<int> _maxModSpawnsPerOriginalEngaged;
private ConfigEntry<bool> _allowEnvironmentFallbackCandidates;
private ConfigEntry<float> _spawnDistanceMin;
private ConfigEntry<float> _spawnDistanceMax;
private ConfigEntry<float> _style1Cooldown;
private ConfigEntry<float> _style2Cooldown;
private ConfigEntry<float> _style3Cooldown;
private ConfigEntry<int> _style1TargetPressure;
private ConfigEntry<int> _style2TargetPressure;
private ConfigEntry<int> _style3TargetPressure;
private ConfigEntry<int> _style1MaxSpawnPerWave;
private ConfigEntry<int> _style2MaxSpawnPerWave;
private ConfigEntry<int> _style3MaxSpawnPerWave;
private ConfigEntry<int> _style1MaxModAlive;
private ConfigEntry<int> _style2MaxModAlive;
private ConfigEntry<int> _style3MaxModAlive;
private ConfigEntry<int> _style1MaxModPerRoom;
private ConfigEntry<int> _style2MaxModPerRoom;
private ConfigEntry<int> _style3MaxModPerRoom;
private ConfigEntry<int> _style1MaxModPerLevel;
private ConfigEntry<int> _style2MaxModPerLevel;
private ConfigEntry<int> _style3MaxModPerLevel;
private const float FailedWaveRetrySeconds = 1f;
private const float DiagLogIntervalSeconds = 5f;
private readonly List<UnitSO> _candidatePool = new List<UnitSO>();
private readonly List<string> _recentEvents = new List<string>();
private readonly Dictionary<int, int> _modSpawnedPerRoom = new Dictionary<int, int>();
private readonly HashSet<int> _deadNpcIds = new HashSet<int>();
private object _lastGraphContext;
private float _nextScanTime;
private float _nextSpawnTime;
private float _nextDiagLogTime;
private int _diagWindowWaves;
private int _diagWindowSpawned;
private string _pluginDirectory;
private float _nextLanguageRetryTime;
private GUIStyle _overlayStyle;
private Vector2 _overlayScroll;
private OnLanguageChange _reloadOverlayLanguage;
private bool _spawnInProgress;
private bool _inLevelTransition;
private int _waveId;
private int _spawnedThisLevel;
private int _spawnedSinceLastOriginalKill;
private float _lastOriginalKillTime = -9999f;
private float _lastModKillTime = -9999f;
private const float NeverDamagedTime = -99999f;
private const float NeverDamagedSeconds = 9999f;
private static float _playerLastDamageTime = -99999f;
private static bool _playerDamageTimerActive;
private string _lastUpdateFailure;
private int _updateFailureCount;
private Snapshot _snapshot = new Snapshot();
private SpawnBlockReason _lastBlockReason;
private string _lastDecision = "None";
private string _lastSpawnSummary = "None";
private string _lastAggroReport = "None";
private static float PlayerTimeSinceDamage
{
get
{
if (_playerLastDamageTime <= -99999f)
{
return 9999f;
}
return Mathf.Clamp(Time.time - _playerLastDamageTime, 0f, 9999f);
}
}
private void Awake()
{
//IL_0031: Unknown result type (might be due to invalid IL or missing references)
//IL_003b: Expected O, but got Unknown
_instance = this;
BindConfig();
_pluginDirectory = Path.GetDirectoryName(((BaseUnityPlugin)this).Info.Location) ?? string.Empty;
_harmony = new Harmony("ryuka.sulfur.dynamicpressure");
TryPatchTransitions();
TryPatchNpcDie();
TryPatchAllDeadTrigger();
TryPatchPlayerDamageTimer();
Log("Loaded.");
}
private void OnDestroy()
{
StopFollowingLanguage();
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<bool>("General", "EnableMod", true, "Enable Dynamic Pressure.");
_enableAutoSpawn = ((BaseUnityPlugin)this).Config.Bind<bool>("General", "EnableAutoSpawn", true, "Enable automatic dynamic pressure spawning. Keep false for first debug testing.");
_pressureStyle = ((BaseUnityPlugin)this).Config.Bind<int>("General", "PressureStyle", 1, "1 = Light, 2 = Heavy, 3 = Nightmare.");
_enableOverlay = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug Overlay", "EnableOverlay", false, "Show debug overlay.");
_overlayToggleKey = ((BaseUnityPlugin)this).Config.Bind<Key>("Debug Overlay", "OverlayToggleKey", (Key)102, "Toggle overlay key.");
_manualSpawnKey = ((BaseUnityPlugin)this).Config.Bind<Key>("Debug Overlay", "ManualSpawnKey", (Key)101, "Manual debug spawn key.");
_logSpawnDecisions = ((BaseUnityPlugin)this).Config.Bind<bool>("Debug Overlay", "LogSpawnDecisions", false, "Write a periodic summary of spawn wave outcomes to the BepInEx log. Diagnostics only; does not affect spawning.");
_scanInterval = ((BaseUnityPlugin)this).Config.Bind<float>("Pressure", "ScanInterval", 1f, "Seconds between pressure scans.");
_enemyScanRadius = ((BaseUnityPlugin)this).Config.Bind<float>("Pressure", "EnemyScanRadius", 25f, "Nearby enemy scan radius.");
_closeEnemyDistance = ((BaseUnityPlugin)this).Config.Bind<float>("Pressure", "CloseEnemyDistance", 8f, "Enemies closer than this add extra pressure.");
_lowHealthStopThreshold = ((BaseUnityPlugin)this).Config.Bind<float>("Safety", "LowHealthStopThreshold", 0.35f, "Do not spawn if player health is below this normalized value.");
_recentDamageStopWindow = ((BaseUnityPlugin)this).Config.Bind<float>("Safety", "RecentDamageStopWindow", 4f, "Do not spawn if player was damaged within this many seconds.");
_minOriginalEngagedHostilesForSpawn = ((BaseUnityPlugin)this).Config.Bind<int>("Anti Loop", "MinOriginalEngagedHostilesForSpawn", 1, "Require this many engaged original hostiles before spawning mod enemies.");
_cooldownAfterModKillOnly = ((BaseUnityPlugin)this).Config.Bind<float>("Anti Loop", "CooldownAfterModKillOnly", 8f, "If only mod enemies are dying, pause spawning for this many seconds.");
_maxSecondsWithoutOriginalKill = ((BaseUnityPlugin)this).Config.Bind<float>("Anti Loop", "MaxSecondsWithoutOriginalKill", 30f, "Stop spawning if no original enemy has died for this long after mod spawns.");
_maxModSpawnsPerOriginalEngaged = ((BaseUnityPlugin)this).Config.Bind<int>("Anti Loop", "MaxModSpawnsPerOriginalEngaged", 2, "Soft budget: each engaged original enemy can support this many mod spawns.");
_allowEnvironmentFallbackCandidates = ((BaseUnityPlugin)this).Config.Bind<bool>("Candidates", "AllowEnvironmentFallbackCandidates", false, "Use current environment enemy metadata if current level alive NPC candidate pool is empty.");
_spawnDistanceMin = ((BaseUnityPlugin)this).Config.Bind<float>("Spawn", "SpawnDistanceMin", 8f, "Minimum spawn distance from player.");
_spawnDistanceMax = ((BaseUnityPlugin)this).Config.Bind<float>("Spawn", "SpawnDistanceMax", 60f, "Maximum spawn distance from player.");
_style1TargetPressure = ((BaseUnityPlugin)this).Config.Bind<int>("Style 1 - Light", "TargetPressure", 6, "Target pressure.");
_style1Cooldown = ((BaseUnityPlugin)this).Config.Bind<float>("Style 1 - Light", "SpawnCooldown", 16f, "Spawn cooldown.");
_style1MaxSpawnPerWave = ((BaseUnityPlugin)this).Config.Bind<int>("Style 1 - Light", "MaxSpawnPerWave", 1, "Max spawns per wave.");
_style1MaxModAlive = ((BaseUnityPlugin)this).Config.Bind<int>("Style 1 - Light", "MaxModSpawnedAlive", 2, "Max alive mod-spawned NPCs.");
_style1MaxModPerRoom = ((BaseUnityPlugin)this).Config.Bind<int>("Style 1 - Light", "MaxModSpawnedPerRoom", 3, "Max mod spawns per room.");
_style1MaxModPerLevel = ((BaseUnityPlugin)this).Config.Bind<int>("Style 1 - Light", "MaxModSpawnedPerLevel", 8, "Max mod spawns per level.");
_style2TargetPressure = ((BaseUnityPlugin)this).Config.Bind<int>("Style 2 - Heavy", "TargetPressure", 9, "Target pressure.");
_style2Cooldown = ((BaseUnityPlugin)this).Config.Bind<float>("Style 2 - Heavy", "SpawnCooldown", 10f, "Spawn cooldown.");
_style2MaxSpawnPerWave = ((BaseUnityPlugin)this).Config.Bind<int>("Style 2 - Heavy", "MaxSpawnPerWave", 1, "Max spawns per wave.");
_style2MaxModAlive = ((BaseUnityPlugin)this).Config.Bind<int>("Style 2 - Heavy", "MaxModSpawnedAlive", 4, "Max alive mod-spawned NPCs.");
_style2MaxModPerRoom = ((BaseUnityPlugin)this).Config.Bind<int>("Style 2 - Heavy", "MaxModSpawnedPerRoom", 5, "Max mod spawns per room.");
_style2MaxModPerLevel = ((BaseUnityPlugin)this).Config.Bind<int>("Style 2 - Heavy", "MaxModSpawnedPerLevel", 16, "Max mod spawns per level.");
_style3TargetPressure = ((BaseUnityPlugin)this).Config.Bind<int>("Style 3 - Nightmare", "TargetPressure", 13, "Target pressure.");
_style3Cooldown = ((BaseUnityPlugin)this).Config.Bind<float>("Style 3 - Nightmare", "SpawnCooldown", 7f, "Spawn cooldown.");
_style3MaxSpawnPerWave = ((BaseUnityPlugin)this).Config.Bind<int>("Style 3 - Nightmare", "MaxSpawnPerWave", 2, "Max spawns per wave.");
_style3MaxModAlive = ((BaseUnityPlugin)this).Config.Bind<int>("Style 3 - Nightmare", "MaxModSpawnedAlive", 6, "Max alive mod-spawned NPCs.");
_style3MaxModPerRoom = ((BaseUnityPlugin)this).Config.Bind<int>("Style 3 - Nightmare", "MaxModSpawnedPerRoom", 8, "Max mod spawns per room.");
_style3MaxModPerLevel = ((BaseUnityPlugin)this).Config.Bind<int>("Style 3 - Nightmare", "MaxModSpawnedPerLevel", 28, "Max mod spawns per level.");
}
private void Update()
{
HandleKeys();
EnsureLanguageSubscription();
if (!_enableMod.Value)
{
_lastDecision = "BLOCKED";
_lastBlockReason = SpawnBlockReason.Disabled;
return;
}
try
{
UpdatePressure();
}
catch (Exception e)
{
ReportUpdateFailure(e);
}
LogSpawnDiagnostics();
}
private void UpdatePressure()
{
GameManager instance = StaticInstance<GameManager>.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 ReportUpdateFailure(Exception e)
{
string text = e.GetType().Name + ": " + e.Message;
if (text != _lastUpdateFailure)
{
_lastUpdateFailure = text;
_updateFailureCount = 0;
AddEvent("Pressure update failed: " + e.GetType().Name);
((BaseUnityPlugin)this).Logger.LogError((object)"Pressure update failed. Repeats of this same failure are counted, not logged.");
((BaseUnityPlugin)this).Logger.LogError((object)e);
}
_updateFailureCount++;
_lastDecision = "FAILED";
_lastBlockReason = SpawnBlockReason.UpdateFailed;
}
private void LogSpawnDiagnostics()
{
if (!_logSpawnDecisions.Value)
{
_nextDiagLogTime = Time.time + 5f;
_diagWindowWaves = 0;
_diagWindowSpawned = 0;
}
else if (!(Time.time < _nextDiagLogTime))
{
_nextDiagLogTime = Time.time + 5f;
if (_diagWindowWaves > 0)
{
((BaseUnityPlugin)this).Logger.LogInfo((object)("Spawn waves in last " + 5f.ToString("0") + "s: " + _diagWindowWaves + " | placed: " + _diagWindowSpawned + " | last decision: " + _lastDecision + " / " + _lastBlockReason.ToString() + " | room: " + _snapshot.playerRoomName + " | mod alive: " + _snapshot.modHostileAlive + " | this level: " + _spawnedThisLevel + "/" + GetMaxModPerLevel()));
}
_diagWindowWaves = 0;
_diagWindowSpawned = 0;
}
}
private void HandleKeys()
{
//IL_0011: Unknown result type (might be due to invalid IL or missing references)
//IL_0042: 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_0038: Unknown result type (might be due to invalid IL or missing references)
//IL_003d: Unknown result type (might be due to invalid IL or missing references)
//IL_018d: Unknown result type (might be due to invalid IL or missing references)
//IL_0193: Unknown result type (might be due to invalid IL or missing references)
Snapshot snapshot = new Snapshot();
_candidatePool.Clear();
GameManager instance = StaticInstance<GameManager>.Instance;
if ((Object)(object)instance == (Object)null)
{
_snapshot = snapshot;
_lastBlockReason = SpawnBlockReason.NoGameManager;
return;
}
snapshot.hasGameManager = true;
snapshot.gameState = ((object)instance.gameState/*cast due to .constrained prefix*/).ToString();
snapshot.inSafeZone = instance.InSafeZone;
snapshot.currentEnvironment = (((Object)(object)instance.currentEnvironment != (Object)null) ? ((object)Unsafe.As<WorldEnvironmentIds, WorldEnvironmentIds>(ref instance.currentEnvironment.id)/*cast due to .constrained prefix*/).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 = PlayerTimeSinceDamage;
snapshot.playerRoomName = (((Object)(object)unitRoom != (Object)null) ? ((Object)unitRoom).name : "null");
snapshot.playerRoomIsEndRoom = (Object)(object)unitRoom != (Object)null && unitRoom.IsEndRoom;
List<Npc> 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;
}
bool flag = (Object)(object)((Component)val).GetComponent<DynamicPressureSpawnMarker>() != (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_0047: Unknown result type (might be due to invalid IL or missing references)
//IL_004c: Unknown result type (might be due to invalid IL or missing references)
spawnCount = 0;
GameManager instance = StaticInstance<GameManager>.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;
}
if (((object)instance.gameState/*cast due to .constrained prefix*/).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 (PlayerTimeSinceDamage <= _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 val = PickCandidate();
if ((Object)(object)val == (Object)null)
{
_lastDecision = "BLOCKED";
_lastBlockReason = SpawnBlockReason.NoCandidateUnits;
AddEvent("Spawn blocked: no candidate.");
break;
}
if (!TryFindSpawnPoint(val, out var result))
{
_lastDecision = "BLOCKED";
_lastBlockReason = SpawnBlockReason.NoValidNpcSpawnPoint;
AddEvent("Spawn blocked: no valid NPCSpawn point.");
break;
}
int roomId = GetRoomId(result.room);
_modSpawnedPerRoom.TryGetValue(roomId, out var value);
if (value >= GetMaxModPerRoom())
{
_lastDecision = "BLOCKED";
_lastBlockReason = SpawnBlockReason.RoomSpawnBudgetExceeded;
AddEvent("Spawn blocked: room budget exceeded.");
break;
}
if (!(await SpawnOneAsync(val, result, reason)))
{
_lastDecision = "FAILED";
_lastBlockReason = SpawnBlockReason.SpawnAsyncFailed;
break;
}
spawned++;
}
if (spawned > 0)
{
_lastDecision = "SPAWNED";
_lastBlockReason = SpawnBlockReason.None;
_nextSpawnTime = Time.time + GetSpawnCooldown();
}
}
catch (Exception ex)
{
_lastDecision = "FAILED";
_lastBlockReason = SpawnBlockReason.SpawnAsyncFailed;
AddEvent("Spawn exception: " + ex.GetType().Name);
((BaseUnityPlugin)this).Logger.LogError((object)ex);
}
finally
{
if (spawned == 0)
{
_nextSpawnTime = Time.time + 1f;
}
_diagWindowWaves++;
_diagWindowSpawned += spawned;
_spawnInProgress = false;
ScanPressure();
}
}
private async Task<bool> SpawnOneAsync(UnitSO unitSo, SpawnPointChoice spawnPoint, string reason)
{
if ((Object)(object)unitSo == (Object)null)
{
return false;
}
Unit val = await unitSo.SpawnUnitAsync((MonoBehaviour)(object)this, spawnPoint.position, Quaternion.identity);
if ((Object)(object)val == (Object)null)
{
return false;
}
Npc val2 = (Npc)(object)((val is Npc) ? val : null);
if ((Object)(object)val2 == (Object)null)
{
val2 = ((Component)val).GetComponent<Npc>();
}
if ((Object)(object)val2 == (Object)null)
{
return false;
}
if ((Object)(object)spawnPoint.room != (Object)null)
{
((Unit)val2).currentRoom = spawnPoint.room;
((Unit)val2).lastValidCurrentRoom = spawnPoint.room;
((Unit)val2).lastRoomCalcPosition = ((Component)val2).transform.position;
}
DynamicPressureSpawnMarker dynamicPressureSpawnMarker = ((Component)val2).gameObject.AddComponent<DynamicPressureSpawnMarker>();
dynamicPressureSpawnMarker.sourceUnitSo = unitSo;
dynamicPressureSpawnMarker.spawnTime = Time.time;
dynamicPressureSpawnMarker.spawnPosition = spawnPoint.position;
dynamicPressureSpawnMarker.spawnRoom = spawnPoint.room;
dynamicPressureSpawnMarker.spawnReason = reason;
dynamicPressureSpawnMarker.waveId = _waveId;
int roomId = GetRoomId(spawnPoint.room);
_modSpawnedPerRoom.TryGetValue(roomId, out var value);
_modSpawnedPerRoom[roomId] = value + 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(val2, dynamicPressureSpawnMarker));
return true;
}
private IEnumerator ReportPlayerPositionLater(Npc npc, DynamicPressureSpawnMarker marker)
{
yield return (object)new WaitForSeconds(0.5f);
GameManager instance = StaticInstance<GameManager>.Instance;
if ((Object)(object)instance == (Object)null || (Object)(object)instance.PlayerObject == (Object)null || (Object)(object)instance.PlayerUnit == (Object)null)
{
_lastAggroReport = "Failed: no player";
yield break;
}
if ((Object)(object)npc == (Object)null || !((Unit)npc).IsAlive || (Object)(object)npc.AiAgent == (Object)null)
{
_lastAggroReport = "Failed: invalid npc";
yield break;
}
try
{
npc.AiAgent.ReportLastSeen(instance.PlayerUnit, ((Component)instance.PlayerUnit).transform.position, ((Component)npc.AiAgent).transform.position, npc.AiAgent.propagateToGroup);
marker.aggroReported = true;
marker.lastAggroReportTime = Time.time;
marker.targetingPlayerAfterReport = SafeTargetIsPlayer(npc);
marker.hasKnownPlayerPositionAfterReport = SafeHasKnownPlayerPosition(npc, instance.PlayerUnit);
_lastAggroReport = ((marker.targetingPlayerAfterReport || marker.hasKnownPlayerPositionAfterReport) ? "Success" : "Reported but not targeting yet");
AddEvent("ReportLastSeen: " + _lastAggroReport);
}
catch (Exception ex)
{
_lastAggroReport = "Exception: " + ex.GetType().Name;
((BaseUnityPlugin)this).Logger.LogWarning((object)ex);
}
}
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_0036: Unknown result type (might be due to invalid IL or missing references)
//IL_003b: Unknown result type (might be due to invalid IL or missing references)
//IL_0040: Unknown result type (might be due to invalid IL or missing references)
//IL_0041: 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_004f: Unknown result type (might be due to invalid IL or missing references)
//IL_0056: Unknown result type (might be due to invalid IL or missing references)
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_002b: Unknown result type (might be due to invalid IL or missing references)
//IL_0031: Unknown result type (might be due to invalid IL or missing references)
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_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_019e: Unknown result type (might be due to invalid IL or missing references)
//IL_01b7: Unknown result type (might be due to invalid IL or missing references)
//IL_00bd: Unknown result type (might be due to invalid IL or missing references)
//IL_00c3: Unknown result type (might be due to invalid IL or missing references)
//IL_00c8: Unknown result type (might be due to invalid IL or missing references)
//IL_00d5: Unknown result type (might be due to invalid IL or missing references)
//IL_00da: Unknown result type (might be due to invalid IL or missing references)
//IL_00dc: Unknown result type (might be due to invalid IL or missing references)
//IL_00de: Unknown result type (might be due to invalid IL or missing references)
//IL_00f0: Unknown result type (might be due to invalid IL or missing references)
//IL_00f2: Unknown result type (might be due to invalid IL or missing references)
result = default(SpawnPointChoice);
GameManager instance = StaticInstance<GameManager>.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<SpawnPointChoice> list = new List<SpawnPointChoice>();
List<SpawnPointChoice> list2 = new List<SpawnPointChoice>();
List<Room> 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;
}
NPCSpawn[] nPCSpawns = val.GetNPCSpawns();
if (nPCSpawns == null || nPCSpawns.Length == 0)
{
continue;
}
foreach (NPCSpawn val2 in nPCSpawns)
{
if (!((Object)(object)val2 == (Object)null) && (val2.usableByTypes & unitSo.unitType) != 0)
{
Vector3 position = ((Component)val2).transform.position;
float num = Vector3.Distance(position, playerPosition);
SpawnPointChoice item = new SpawnPointChoice
{
position = position,
room = val,
distanceToPlayer = num,
source = "GameManager.orderedRooms NPCSpawn"
};
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;
}
_modSpawnedPerRoom.TryGetValue(GetRoomId(room), out var value);
if (value >= GetMaxModPerRoom())
{
return false;
}
return true;
}
private SpawnPointChoice PickNearestRandomized(List<SpawnPointChoice> 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)
{
if ((Object)(object)npc != (Object)null && (Object)(object)((Component)npc).gameObject != (Object)null)
{
return ((Unit)npc).IsAlive;
}
return false;
}
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)
{
if (!((Object)(object)room != (Object)null))
{
return 0;
}
return ((Object)room).GetInstanceID();
}
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 OnAllDeadTriggerCheckAllDeadPrefix(List<Npc> ___allAliveNpcs)
{
if (___allAliveNpcs != null && ___allAliveNpcs.Count != 0)
{
___allAliveNpcs.RemoveAll(IsDynamicPressureSpawnedNpc);
}
}
private static bool OnAllDeadTriggerRegisterDeathPrefix(Unit __0)
{
return !IsDynamicPressureSpawnedNpc((Npc)(object)((__0 is Npc) ? __0 : null));
}
private static bool IsDynamicPressureSpawnedNpc(Npc npc)
{
if ((Object)(object)npc != (Object)null)
{
return (Object)(object)((Component)npc).GetComponent<DynamicPressureSpawnMarker>() != (Object)null;
}
return false;
}
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<DynamicPressureSpawnMarker>() != (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);
}
private static void OnUnitReceiveDamagePostfix(Unit __instance, bool __result)
{
if (__result && (Object)(object)__instance != (Object)null && __instance.isPlayer)
{
_playerLastDamageTime = Time.time;
}
}
private void TryPatchPlayerDamageTimer()
{
//IL_0094: Unknown result type (might be due to invalid IL or missing references)
//IL_00a1: Expected O, but got Unknown
try
{
MethodInfo methodInfo = AccessTools.Method(typeof(Unit), "ReceiveDamage", new Type[4]
{
typeof(float),
typeof(DamageSourceData),
typeof(Data),
typeof(Vector3?)
}, (Type[])null);
MethodInfo methodInfo2 = AccessTools.Method(typeof(DynamicPressurePlugin), "OnUnitReceiveDamagePostfix", (Type[])null, (Type[])null);
if (methodInfo == null || methodInfo2 == null)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)"Failed to patch Unit.ReceiveDamage: method not found. The RecentDamage safety hold is inactive - waves will not wait out incoming player damage.");
return;
}
_harmony.Patch((MethodBase)methodInfo, (HarmonyMethod)null, new HarmonyMethod(methodInfo2), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
_playerDamageTimerActive = true;
Log("Patched Unit.ReceiveDamage (player damage timer).");
}
catch (Exception ex)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("Failed to patch Unit.ReceiveDamage: " + ex.Message + " | The RecentDamage safety hold is inactive."));
}
}
private void TryPatchNpcDie()
{
//IL_0049: Unknown result type (might be due to invalid IL or missing references)
//IL_0056: 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 TryPatchAllDeadTrigger()
{
//IL_00b0: Unknown result type (might be due to invalid IL or missing references)
//IL_00be: Expected O, but got Unknown
//IL_00c7: Unknown result type (might be due to invalid IL or missing references)
//IL_00d5: Expected O, but got Unknown
try
{
MethodInfo methodInfo = AccessTools.Method(typeof(AllDeadTrigger), "CheckAllDead", Type.EmptyTypes, (Type[])null);
MethodInfo methodInfo2 = AccessTools.Method(typeof(AllDeadTrigger), "RegisterDeath", new Type[1] { typeof(Unit) }, (Type[])null);
MethodInfo methodInfo3 = AccessTools.Method(typeof(DynamicPressurePlugin), "OnAllDeadTriggerCheckAllDeadPrefix", (Type[])null, (Type[])null);
MethodInfo methodInfo4 = AccessTools.Method(typeof(DynamicPressurePlugin), "OnAllDeadTriggerRegisterDeathPrefix", (Type[])null, (Type[])null);
if (methodInfo == null || methodInfo2 == null || methodInfo3 == null || methodInfo4 == null)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)"Failed to patch AllDeadTrigger: expected methods were not found.");
return;
}
_harmony.Patch((MethodBase)methodInfo, new HarmonyMethod(methodInfo3), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
_harmony.Patch((MethodBase)methodInfo2, new HarmonyMethod(methodInfo4), (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null, (HarmonyMethod)null);
Log("Patched AllDeadTrigger progression filtering.");
}
catch (Exception ex)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)("Failed to patch AllDeadTrigger: " + ex.Message));
}
}
private void TryPatchMethod(Type type, string methodName, MethodInfo prefix)
{
//IL_003e: Unknown result type (might be due to invalid IL or missing references)
//IL_004c: 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_0067: Unknown result type (might be due to invalid IL or missing references)
//IL_0075: 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 num = 0; num < array.Length; num++)
{
_harmony.Patch((MethodBase)array[num], 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 static string Label(string key, string english)
{
return OverlayPalette.Tint("#B9B9B9", OverlayText.Get(key, english) + ": ");
}
private static string Header(string key, string english)
{
return OverlayPalette.Tint("#7EC8FF", OverlayText.Get(key, english));
}
private static string Separator()
{
return OverlayPalette.Tint("#8A8A8A", " | ");
}
private static string TintDecision(string decision)
{
return decision switch
{
"SPAWNED" => OverlayPalette.Tint("#7BE07B", decision),
"FAILED" => OverlayPalette.Tint("#FF6B6B", decision),
"BLOCKED" => OverlayPalette.Tint("#FFC451", decision),
_ => OverlayPalette.Tint("#8A8A8A", decision),
};
}
private static string TintReason(SpawnBlockReason reason)
{
switch (reason)
{
case SpawnBlockReason.None:
return OverlayPalette.Tint("#7BE07B", reason);
case SpawnBlockReason.NoCandidateUnits:
case SpawnBlockReason.NoValidNpcSpawnPoint:
case SpawnBlockReason.RoomSpawnBudgetExceeded:
case SpawnBlockReason.SpawnAsyncFailed:
case SpawnBlockReason.UpdateFailed:
return OverlayPalette.Tint("#FF6B6B", reason);
default:
return OverlayPalette.Tint("#FFC451", reason);
}
}
private void OnGUI()
{
//IL_007e: Unknown result type (might be due to invalid IL or missing references)
//IL_00c7: 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_0021: Unknown result type (might be due to invalid IL or missing references)
//IL_002b: Expected O, but got Unknown
if (_enableOverlay.Value)
{
if (_overlayStyle == null)
{
_overlayStyle = new GUIStyle(GUI.skin.label);
_overlayStyle.richText = true;
}
float num = Mathf.Min(680f, (float)Screen.width - 40f);
float num2 = Mathf.Max(240f, (float)Screen.height - 120f - 20f);
Rect val = default(Rect);
((Rect)(ref val))..ctor(20f, 120f, num, num2);
GUI.Box(val, OverlayText.Get("overlay.title", "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));
_overlayScroll = GUILayout.BeginScrollView(_overlayScroll, Array.Empty<GUILayoutOption>());
Row(Label("overlay.enabled", "Enabled") + OverlayPalette.Flag(_enableMod.Value, nominal: true) + Separator() + Label("overlay.autoSpawn", "AutoSpawn") + OverlayPalette.Flag(_enableAutoSpawn.Value, nominal: true) + Separator() + Label("overlay.style", "Style") + OverlayPalette.Tint("#D89CFF", GetStyle()));
Row(Label("overlay.gameState", "GameState") + OverlayPalette.Tint((_snapshot.gameState == "Running") ? "#7BE07B" : "#FFC451", _snapshot.gameState) + Separator() + Label("overlay.safeZone", "SafeZone") + OverlayPalette.Flag(_snapshot.inSafeZone, nominal: false) + Separator() + Label("overlay.env", "Env") + OverlayPalette.Tint("#FFFFFF", _snapshot.currentEnvironment));
string hex = "#7BE07B";
if (_snapshot.playerHp <= _lowHealthStopThreshold.Value)
{
hex = "#FF6B6B";
}
else if (_snapshot.playerHp < 0.5f)
{
hex = "#FFC451";
}
Row(Label("overlay.player", "Player") + (_snapshot.hasPlayer ? OverlayPalette.Tint("#7BE07B", OverlayText.Get("overlay.ok", "OK")) : OverlayPalette.Tint("#FF6B6B", OverlayText.Get("overlay.missing", "Missing"))) + Separator() + Label("overlay.alive", "Alive") + OverlayPalette.Flag(_snapshot.playerAlive, nominal: true) + Separator() + Label("overlay.hp", "HP") + OverlayPalette.Tint(hex, Mathf.RoundToInt(_snapshot.playerHp * 100f) + "%") + Separator() + Label("overlay.damageAgo", "DamageAgo") + (_playerDamageTimerActive ? OverlayPalette.Tint((_snapshot.playerTimeSinceDamage <= _recentDamageStopWindow.Value) ? "#FFC451" : "#FFFFFF", (_snapshot.playerTimeSinceDamage >= 9999f) ? OverlayText.Get("overlay.never", "never") : (_snapshot.playerTimeSinceDamage.ToString("0.0") + "s")) : OverlayPalette.Tint("#FF6B6B", OverlayText.Get("overlay.unavailable", "n/a"))));
if (_lastUpdateFailure != null)
{
Row(Label("overlay.updateFailure", "Update Error") + OverlayPalette.Tint("#FF6B6B", _lastUpdateFailure) + OverlayPalette.Tint("#8A8A8A", " x" + _updateFailureCount));
}
Row(Label("overlay.room", "Room") + OverlayPalette.Tint("#FFFFFF", _snapshot.playerRoomName) + Separator() + Label("overlay.endRoom", "EndRoom") + OverlayPalette.Flag(_snapshot.playerRoomIsEndRoom, nominal: false));
GUILayout.Space(8f);
Row(Header("overlay.section.pressure", "Pressure"));
Row(Label("overlay.originalHostileAlive", "Original Hostile Alive") + OverlayPalette.Count(_snapshot.originalHostileAlive) + Separator() + Label("overlay.engaged", "Engaged") + OverlayPalette.Count(_snapshot.originalEngagedHostiles) + Separator() + Label("overlay.targeting", "Targeting") + OverlayPalette.Count(_snapshot.originalTargetingPlayer));
Row(Label("overlay.modHostileAlive", "Mod Hostile Alive") + OverlayPalette.Budget(_snapshot.modHostileAlive, GetMaxModAlive()) + Separator() + Label("overlay.targeting", "Targeting") + OverlayPalette.Count(_snapshot.modTargetingPlayer));
Row(Label("overlay.originalPressure", "Original Pressure") + OverlayPalette.Count(_snapshot.originalPressure) + Separator() + Label("overlay.modPressure", "Mod Pressure") + OverlayPalette.Count(_snapshot.modPressure) + Separator() + Label("overlay.current", "Current") + OverlayPalette.Tint("#FFFFFF", _snapshot.currentPressure) + OverlayPalette.Tint("#8A8A8A", " / " + _snapshot.targetPressure) + Separator() + Label("overlay.deficit", "Deficit") + OverlayPalette.Tint((_snapshot.deficit > 0) ? "#FFC451" : "#7BE07B", _snapshot.deficit));
GUILayout.Space(8f);
Row(Header("overlay.section.spawnDecision", "Spawn Decision"));
Row(Label("overlay.lastDecision", "Last Decision") + TintDecision(_lastDecision) + Separator() + Label("overlay.reason", "Reason") + TintReason(_lastBlockReason));
Row(Label("overlay.nextSpawnIn", "Next Spawn In") + OverlayPalette.Tint((_snapshot.nextSpawnIn > 0f) ? "#FFFFFF" : "#8A8A8A", _snapshot.nextSpawnIn.ToString("0.0") + "s") + Separator() + Label("overlay.candidateSource", "Candidate Source") + OverlayPalette.Tint("#FFFFFF", _snapshot.candidateSource) + Separator() + Label("overlay.candidates", "Candidates") + OverlayPalette.Tint((_snapshot.candidateCount > 0) ? "#FFFFFF" : "#FF6B6B", _snapshot.candidateCount));
GUILayout.Space(8f);
Row(Header("overlay.section.modImpact", "Mod Impact"));
Row(Label("overlay.spawnedThisLevel", "Spawned This Level") + OverlayPalette.Budget(_spawnedThisLevel, GetMaxModPerLevel()));
Row(Label("overlay.spawnedSinceOriginalKill", "Spawned Since Last Original Kill") + OverlayPalette.Count(_spawnedSinceLastOriginalKill));
Row(Label("overlay.timeSinceOriginalKill", "Time Since Original Kill") + OverlayPalette.Tint("#FFFFFF", _snapshot.timeSinceLastOriginalKill.ToString("0.0") + "s") + Separator() + Label("overlay.timeSinceModKill", "Time Since Mod Kill") + OverlayPalette.Tint("#FFFFFF", _snapshot.timeSinceLastModKill.ToString("0.0") + "s"));
Row(Label("overlay.wouldDelayAllDead", "Would Delay OnAllEnemiesDead") + OverlayPalette.Flag(_snapshot.wouldDelayOnAllEnemiesDead, nominal: false));
Row(Label("overlay.lastSpawn", "Last Spawn") + OverlayPalette.Tint("#FFFFFF", _lastSpawnSummary));
Row(Label("overlay.lastAggroReport", "Last Aggro Report") + OverlayPalette.Tint("#FFFFFF", _lastAggroReport));
GUILayout.Space(8f);
Row(OverlayPalette.Tint("#8A8A8A", OverlayText.Get("overlay.keys", "Keys: F8 Manual Spawn | F9 Toggle Overlay")));
GUILayout.Space(8f);
Row(Header("overlay.section.recentEvents", "Recent Events"));
for (int i = 0; i < _recentEvents.Count; i++)
{
Row(OverlayPalette.Tint("#8A8A8A", _recentEvents[i]));
}
GUILayout.EndScrollView();
GUILayout.EndArea();
}
}
private void Row(string text)
{
GUILayout.Label(text, _overlayStyle, Array.Empty<GUILayoutOption>());
}
private void EnsureLanguageSubscription()
{
//IL_004e: Unknown result type (might be due to invalid IL or missing references)
//IL_0058: Expected O, but got Unknown
//IL_006a: Unknown result type (might be due to invalid IL or missing references)
//IL_0074: Expected O, but got Unknown
if (_reloadOverlayLanguage != null)
{
if (OverlayText.GameCode == null && Time.unscaledTime >= _nextLanguageRetryTime)
{
_nextLanguageRetryTime = Time.unscaledTime + 1f;
LoadOverlayLanguage();
}
return;
}
try
{
AsyncAssetLoading instance = StaticInstance<AsyncAssetLoading>.Instance;
if (!((Object)(object)instance == (Object)null))
{
_reloadOverlayLanguage = new OnLanguageChange(LoadOverlayLanguage);
instance.onLanguageChange = (OnLanguageChange)Delegate.Combine((Delegate?)(object)instance.onLanguageChange, (Delegate?)(object)_reloadOverlayLanguage);
LoadOverlayLanguage();
}
}
catch (Exception ex)
{
_reloadOverlayLanguage = null;
((BaseUnityPlugin)this).Logger.LogWarning((object)("Overlay language will not follow the game language: " + ex.Message));
}
}
private void StopFollowingLanguage()
{
//IL_002f: Unknown result type (might be due to invalid IL or missing references)
//IL_0039: Expected O, but got Unknown
OnLanguageChange reloadOverlayLanguage = _reloadOverlayLanguage;
_reloadOverlayLanguage = null;
if (reloadOverlayLanguage == null)
{
return;
}
try
{
AsyncAssetLoading instance = StaticInstance<AsyncAssetLoading>.Instance;
if ((Object)(object)instance != (Object)null)
{
instance.onLanguageChange = (OnLanguageChange)Delegate.Remove((Delegate?)(object)instance.onLanguageChange, (Delegate?)(object)reloadOverlayLanguage);
}
}
catch (Exception)
{
}
}
private void LoadOverlayLanguage()
{
if (OverlayText.Reload(_pluginDirectory, out var resolvedCode))
{
string text = "Overlay language: " + resolvedCode + " (game language code: " + (OverlayText.GameCode ?? "not loaded yet") + ") - " + OverlayText.LoadedCount + " strings from " + (OverlayText.LoadedFrom ?? _pluginDirectory);
if (OverlayText.LoadedCount == 0)
{
((BaseUnityPlugin)this).Logger.LogWarning((object)(text + " | " + (OverlayText.LastLoadNote ?? "no reason recorded")));
}
else
{
((BaseUnityPlugin)this).Logger.LogInfo((object)text);
}
}
}
}
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,
UpdateFailed
}
internal static class GameLanguage
{
public const string FallbackCode = "en";
private static readonly string[] ShippedCodes = new string[14]
{
"en", "sv", "fr", "it", "de", "es", "pt", "ru", "pl", "ja",
"ko", "zh-CN", "tr", "ar"
};
public static string CurrentCode()
{
try
{
string currentLanguageCode = LocalizationManager.CurrentLanguageCode;
return string.IsNullOrEmpty(currentLanguageCode) ? null : currentLanguageCode;
}
catch (Exception)
{
return null;
}
}
public static bool IsRightToLeft()
{
try
{
return LocalizationManager.IsRight2Left;
}
catch (Exception)
{
return false;
}
}
public static string FixRightToLeft(string text)
{
try
{
return LocalizationManager.FixRTL_IfNeeded(text, 0, false);
}
catch (Exception)
{
return text;
}
}
public static string ResolveCode(string languageCode)
{
if (string.IsNullOrEmpty(languageCode))
{
return "en";
}
for (int i = 0; i < ShippedCodes.Length; i++)
{
if (string.Equals(ShippedCodes[i], languageCode, StringComparison.OrdinalIgnoreCase))
{
return ShippedCodes[i];
}
}
string b = LanguageOf(languageCode);
for (int j = 0; j < ShippedCodes.Length; j++)
{
if (string.Equals(LanguageOf(ShippedCodes[j]), b, StringComparison.OrdinalIgnoreCase))
{
return ShippedCodes[j];
}
}
return "en";
}
private static string LanguageOf(string languageCode)
{
int num = languageCode.IndexOf('-');
if (num <= 0)
{
return languageCode;
}
return languageCode.Substring(0, num);
}
}
internal static class OverlayText
{
[DataContract]
private sealed class LangEntry
{
[DataMember(Name = "key")]
public string Key;
[DataMember(Name = "value")]
public string Value;
}
[DataContract]
private sealed class LangFile
{
[DataMember(Name = "entries")]
public LangEntry[] Entries;
}
private static readonly Dictionary<string, string> Strings = new Dictionary<string, string>(StringComparer.Ordinal);
public static string LoadedCode { get; private set; }
public static string GameCode { get; private set; }
public static int LoadedCount => Strings.Count;
public static string LastLoadNote { get; private set; }
public static string LoadedFrom { get; private set; }
private static IEnumerable<string> CandidateDirectories(string pluginDirectory)
{
if (!string.IsNullOrEmpty(pluginDirectory))
{
yield return Path.Combine(pluginDirectory, "lang");
yield return pluginDirectory;
string text;
try
{
text = Directory.GetParent(pluginDirectory)?.FullName;
}
catch (Exception)
{
text = null;
}
if (!string.IsNullOrEmpty(text))
{
yield return Path.Combine(text, "lang");
}
}
}
public static bool Reload(string pluginDirectory, out string resolvedCode)
{
string text = GameLanguage.CurrentCode();
resolvedCode = GameLanguage.ResolveCode(text);
if (LoadedCode == resolvedCode && GameCode == text)
{
return false;
}
Strings.Clear();
LastLoadNote = null;
LoadedFrom = null;
if (resolvedCode != "en")
{
LoadFirstMatch(pluginDirectory, "en");
}
LoadFirstMatch(pluginDirectory, resolvedCode);
LoadedCode = resolvedCode;
GameCode = text;
return true;
}
private static void LoadFirstMatch(string pluginDirectory, string code)
{
foreach (string item in CandidateDirectories(pluginDirectory))
{
if (LoadInto(item, code))
{
LoadedFrom = item;
return;
}
}
LastLoadNote = "no readable " + code + ".json under " + (pluginDirectory ?? "<unknown>");
}
private static bool LoadInto(string langDirectory, string code)
{
//IL_0034: Unknown result type (might be due to invalid IL or missing references)
string text = Path.Combine(langDirectory, code + ".json");
if (!File.Exists(text))
{
return false;
}
try
{
LangFile langFile;
using (MemoryStream memoryStream = new MemoryStream(File.ReadAllBytes(text)))
{
langFile = ((XmlObjectSerializer)new DataContractJsonSerializer(typeof(LangFile))).ReadObject((Stream)memoryStream) as LangFile;
}
if (langFile == null || langFile.Entries == null)
{
LastLoadNote = "no entries in " + text;
return false;
}
bool flag = GameLanguage.IsRightToLeft();
int num = 0;
for (int i = 0; i < langFile.Entries.Length; i++)
{
LangEntry langEntry = langFile.Entries[i];
if (langEntry != null && !string.IsNullOrEmpty(langEntry.Key) && langEntry.Value != null)
{
Strings[langEntry.Key] = (flag ? GameLanguage.FixRightToLeft(langEntry.Value) : langEntry.Value);
num++;
}
}
return num > 0;
}
catch (Exception ex)
{
LastLoadNote = ex.GetType().Name + " reading " + text;
return false;
}
}
public static string Get(string key, string englishFallback)
{
if (Strings.TryGetValue(key, out var value) && !string.IsNullOrEmpty(value))
{
return value;
}
return englishFallback;
}
}
internal static class OverlayPalette
{
public const string Header = "#7EC8FF";
public const string Label = "#B9B9B9";
public const string Value = "#FFFFFF";
public const string Good = "#7BE07B";
public const string Warn = "#FFC451";
public const string Bad = "#FF6B6B";
public const string Muted = "#8A8A8A";
public const string Accent = "#D89CFF";
public static string Tint(string hex, object value)
{
return "<color=" + hex + ">" + value?.ToString() + "</color>";
}
public static string Flag(bool value, bool nominal)
{
return Tint((value == nominal) ? "#7BE07B" : "#FFC451", value);
}
public static string Budget(int used, int limit)
{
string hex = "#7BE07B";
if (limit > 0 && used >= limit)
{
hex = "#FF6B6B";
}
else if (limit > 0 && used >= limit - 1)
{
hex = "#FFC451";
}
return Tint(hex, used) + Tint("#8A8A8A", " / " + limit);
}
public static string Count(int value)
{
return Tint((value > 0) ? "#FFFFFF" : "#8A8A8A", value);
}
}