using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using CompanionKit; using CompanionKit.Core; using DangerousRoads.Anchors; using DangerousRoads.Core; using DangerousRoads.Placement; using ForgeKit; using HarmonyLib; using SpawnKit; using SpawnKit.Core; using UnityEngine; using UnityEngine.AI; using UnityEngine.SceneManagement; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("DangerousRoads")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.1.0")] [assembly: AssemblyInformationalVersion("0.1.1+fb85873cdf7f439cbcb58ddab6d8ef85c9144594")] [assembly: AssemblyProduct("DangerousRoads")] [assembly: AssemblyTitle("DangerousRoads")] [assembly: AssemblyMetadata("BuildStamp", "fb85873 2026-08-02")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.1.1.0")] [module: UnverifiableCode] namespace DangerousRoads { internal enum DirectorState { Disabled, Idle, Warming, Armed, WaveInFlight, Cooldown } internal sealed class AmbushDirector { private readonly Plugin _host; private readonly RegionWatch _region = new RegionWatch(); private readonly SpeciesRoster _roster = new SpeciesRoster(); private readonly Prewarmer _prewarmer = new Prewarmer(); private readonly AnchorRegistry _anchors = new AnchorRegistry(); private readonly WaveRunner _wave; private readonly List _recentSpecies = new List(); private ClockState _clock = ClockState.Disarmed; private DirectorState _state = DirectorState.Idle; private readonly BlockCounters _blocks = new BlockCounters(); private double _combatDeferSince = double.NegativeInfinity; private double _nextRegionPollAt; private double _nextLedgerDumpAt; internal RegionWatch Region => _region; internal SpeciesRoster Roster => _roster; internal Prewarmer Warmer => _prewarmer; internal AnchorRegistry Anchors => _anchors; internal WaveRunner Wave => _wave; internal DirectorState State => _state; internal ClockState Clock => _clock; internal string LastBlock => _blocks.Last; internal BlockCounters Blocks => _blocks; internal AmbushDirector(Plugin host) { //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_004a: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Expected O, but got Unknown _host = host; _wave = new WaveRunner(_anchors); } internal void OnRegionReady(Character player, string sceneName) { //IL_00e6: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_006a: Unknown result type (might be due to invalid IL or missing references) _region.Poll(DrConfig.Enabled.Value); _anchors.OnSceneChanged(sceneName); Toasts.ResetThrottle(); _recentSpecies.Clear(); _blocks.Reset(); _combatDeferSince = double.NegativeInfinity; CompassBlips.ClearAll(); if (!_region.IsOverworld) { Disarm($"not overworld ({_region.LastVerdict})"); ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)("[AMBUSH] " + _region.Describe() + " — idle.")); } return; } _roster.Rebuild(player, sceneName); _prewarmer.Reset(_roster.PrewarmTargets(), DrConfig.PrewarmCount.Value); _clock = AmbushClock.Arm((double)Time.time, DrConfig.FirstArmDelaySeconds.Value); _state = DirectorState.Armed; ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogMessage((object)("[AMBUSH] armed in " + _region.Describe() + "; first wave in " + $"~{DrConfig.FirstArmDelaySeconds.Value:F0}s.")); } } internal void Tick() { //IL_009b: Unknown result type (might be due to invalid IL or missing references) //IL_00e8: Unknown result type (might be due to invalid IL or missing references) double num = Time.unscaledTime; if (PhotonNetwork.isNonMasterClientInRoom) { if (_state != DirectorState.Disabled) { Disarm("guest (master-only)"); } return; } if (!DrConfig.Enabled.Value) { if (_state != DirectorState.Disabled) { Disarm("[General] Enabled=false"); } return; } if (_state == DirectorState.Disabled) { _state = DirectorState.Idle; } if (num >= _nextRegionPollAt) { _nextRegionPollAt = num + 0.5; if (_region.Poll(DrConfig.Enabled.Value) && !_region.IsOverworld) { Disarm($"left the overworld ({_region.LastVerdict})"); } } _prewarmer.Tick(num); AutoDumpLedger(num); if (_region.IsOverworld && !_wave.Running && _state != DirectorState.Idle && AmbushClock.IsDue(_clock, (double)Time.time)) { TryWave(); } } private void TryWave() { Character localPlayer = Plugin.LocalPlayer; string text = SoftBlock(localPlayer); if (text != null) { SoftRetry(text); return; } List list = _roster.SpawnableNow(); if (list.Count == 0) { SoftRetry("no warm species"); return; } _state = DirectorState.WaveInFlight; ((MonoBehaviour)_host).StartCoroutine(_wave.Run(localPlayer, _region.LedgerKey, list, _recentSpecies, Spawner.Active("dangerousroads").Count, 0, null, delegate(WaveOutcome outcome) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) if (outcome == WaveOutcome.Placed) { WavePlan lastPlan = _wave.LastPlan; RememberSpecies(((WavePlan)(ref lastPlan)).PrimarySpecies); _clock = AmbushClock.AfterWave((double)Time.time, DrConfig.CooldownAfterWaveSeconds.Value, AmbushClock.NextDelay((double)Random.value, DrConfig.MinIntervalSeconds.Value, DrConfig.MaxIntervalSeconds.Value)); _state = DirectorState.Cooldown; } else { SoftRetry(outcome.ToString()); } if (_state == DirectorState.Cooldown) { _state = DirectorState.Armed; } })); } private string SoftBlock(Character player) { if ((Object)(object)player == (Object)null) { return "no local player"; } if (!player.Alive) { return "player is dead"; } NetworkLevelLoader instance = NetworkLevelLoader.Instance; if ((Object)(object)instance != (Object)null && !instance.IsOverallLoadingDone) { return "level still loading"; } if ((Object)(object)AISquadManager.Instance == (Object)null) { return "no AISquadManager"; } if (Spawner.IsExpeditionRunning) { return "expedition in flight"; } bool inCombat = player.InCombat; _combatDeferSince = CombatDefer.Advance(inCombat, _combatDeferSince, (double)Time.unscaledTime); if (CombatDefer.ShouldHold(DrConfig.SkipWhileInCombat.Value, inCombat, _combatDeferSince, (double)Time.unscaledTime, DrConfig.CombatDeferMaxSeconds.Value)) { return "player in combat"; } if (Spawner.Active("dangerousroads").Count >= DrConfig.MaxOwnActive.Value) { return "own cap reached"; } return null; } private void SoftRetry(string why) { //IL_001d: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Unknown result type (might be due to invalid IL or missing references) _blocks.Record(why); _clock = AmbushClock.Retry((double)Time.time, DrConfig.RetrySeconds.Value); _state = DirectorState.Armed; bool flag = _blocks.CountOf(why) == 1; if (flag || DrConfig.LogVerbose.Value) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)("[AMBUSH] soft block: " + why + " — retrying in " + $"{DrConfig.RetrySeconds.Value:F0}s." + ((flag && !DrConfig.LogVerbose.Value) ? " (first of this kind here; further ones need [Diag] LogVerbose)" : ""))); } } } private void RememberSpecies(string key) { _recentSpecies.Insert(0, key); while (_recentSpecies.Count > 8) { _recentSpecies.RemoveAt(_recentSpecies.Count - 1); } } private void AutoDumpLedger(double unscaled) { float value = DrConfig.LedgerAutoDumpSeconds.Value; if (value <= 0f) { return; } if (_nextLedgerDumpAt == 0.0) { _nextLedgerDumpAt = unscaled + (double)value; } else if (!(unscaled < _nextLedgerDumpAt)) { _nextLedgerDumpAt = unscaled + (double)value; ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)("[LEDGER] auto-dump\n" + _anchors.Ledger.FormatAll())); } } } internal void Disarm(string why) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) _clock = ClockState.Disarmed; _state = DirectorState.Disabled; _blocks.Record(why); } internal void ArmNow() { //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) _clock = AmbushClock.Arm((double)Time.time, 0f); _state = DirectorState.Armed; _combatDeferSince = double.NegativeInfinity; } internal void ForceWave(Character player, int count, string species) { if (_wave.Running) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"[AMBUSH] a wave is already running."); } return; } _state = DirectorState.WaveInFlight; ((MonoBehaviour)_host).StartCoroutine(_wave.Run(player, _region.LedgerKey, _roster.SpawnableNow(), _recentSpecies, Spawner.Active("dangerousroads").Count, (count <= 0) ? 1 : count, species, delegate { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) _state = DirectorState.Armed; if (_wave.LastOutcome == WaveOutcome.Placed) { WavePlan lastPlan = _wave.LastPlan; RememberSpecies(((WavePlan)(ref lastPlan)).PrimarySpecies); } })); } internal float SecondsUntilDue() { if (!_clock.Armed) { return -1f; } return Mathf.Max(0f, (float)(_clock.DueAt - (double)Time.time)); } } [HarmonyPatch(typeof(CharacterUI), "Awake")] internal static class CharacterUI_Awake_Blips { private static void Postfix(CharacterUI __instance) { try { CompassBlips.Attach(__instance); } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[ANCHOR] could not attach compass blips: " + ex.Message)); } } } } internal static class UiDump { internal static string Dump(int maxDepth) { //IL_00d6: Unknown result type (might be due to invalid IL or missing references) //IL_00db: Unknown result type (might be due to invalid IL or missing references) //IL_00df: 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_0104: Unknown result type (might be due to invalid IL or missing references) StringBuilder stringBuilder = new StringBuilder(); CharacterUI val = Plugin.LocalPlayer?.CharacterUI; if ((Object)(object)val == (Object)null) { return "no CharacterUI (no local player yet?)"; } UICompass componentInChildren = ((Component)val).GetComponentInChildren(true); stringBuilder.AppendLine("CharacterUI '" + ((Object)val).name + "' blips: " + CompassBlips.Describe()); if ((Object)(object)componentInChildren != (Object)null) { RectTransform component = ((Component)componentInChildren).GetComponent(); stringBuilder.AppendLine(" UICompass FOUND at " + CompassBlips.Path(((Component)componentInChildren).transform)); string[] obj = new string[6] { $" active={((Component)componentInChildren).gameObject.activeInHierarchy} ", $"angleWidth={componentInChildren.CompassAngleWidth} ", "rect=", null, null, null }; object obj2; if (!((Object)(object)component != (Object)null)) { obj2 = "?"; } else { Rect rect = component.rect; obj2 = ((object)((Rect)(ref rect)).size/*cast due to .constrained prefix*/).ToString(); } obj[3] = (string)obj2; obj[4] = " "; obj[5] = $"dir={componentInChildren.CompassDir}"; stringBuilder.AppendLine(string.Concat(obj)); } else { stringBuilder.AppendLine(" UICompass NOT FOUND — blips cannot work; hierarchy follows."); } stringBuilder.AppendLine(" --- hierarchy ---"); Walk(((Component)val).transform, 0, maxDepth, stringBuilder); return stringBuilder.ToString().TrimEnd(Array.Empty()); } private static void Walk(Transform t, int depth, int maxDepth, StringBuilder sb) { if (depth > maxDepth) { return; } List list = new List(); Component[] components = ((Component)t).GetComponents(); foreach (Component val in components) { if ((Object)(object)val != (Object)null && !(val is RectTransform) && !(val is Transform)) { list.Add(((object)val).GetType().Name); } } sb.Append(' ', 2 + depth * 2).Append(((Component)t).gameObject.activeSelf ? "" : "(inactive) ").Append(((Object)t).name); if (list.Count > 0) { sb.Append(" [").Append(string.Join(", ", list.ToArray())).Append(']'); } sb.AppendLine(); for (int j = 0; j < t.childCount; j++) { Walk(t.GetChild(j), depth + 1, maxDepth, sb); } } } internal static class CompassBlips { private sealed class Rig { internal UICompass Compass; internal readonly List Dots = new List(); internal Transform Root; } private static readonly Dictionary _rigs = new Dictionary(); private static int _consecutiveErrors; private const int ErrorsBeforeDisable = 10; private static bool _disabled; internal static bool? CompassFound { get; private set; } internal static int LiveBlips { get; private set; } internal static void Attach(CharacterUI ui) { //IL_00ab: Unknown result type (might be due to invalid IL or missing references) PruneDestroyed(); if ((Object)(object)ui == (Object)null || _rigs.ContainsKey(ui)) { return; } UICompass componentInChildren = ((Component)ui).GetComponentInChildren(true); if (!CompassFound.HasValue) { CompassFound = (Object)(object)componentInChildren != (Object)null; if ((Object)(object)componentInChildren == (Object)null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"[ANCHOR] no UICompass in this HUD — compass blips are off; the wave toast will name the direction in text instead. Run 'roadsui' to dump the live CharacterUI hierarchy and see what is actually there."); } } else { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogMessage((object)("[ANCHOR] compass found: " + Path(((Component)componentInChildren).transform))); } } } if (!((Object)(object)componentInChildren == (Object)null)) { Transform transform = new GameObject("DR_Blips", new Type[1] { typeof(RectTransform) }).transform; transform.SetParent(((Component)componentInChildren).transform, false); _rigs[ui] = new Rig { Compass = componentInChildren, Root = transform }; } } internal static void Forget(CharacterUI ui) { if ((Object)(object)ui != (Object)null) { _rigs.Remove(ui); } } private static void PruneDestroyed() { List list = null; foreach (KeyValuePair rig in _rigs) { if ((Object)(object)rig.Key == (Object)null || rig.Value == null || (Object)(object)rig.Value.Compass == (Object)null || (Object)(object)rig.Value.Root == (Object)null) { (list ?? (list = new List())).Add(rig.Key); } } if (list != null) { for (int i = 0; i < list.Count; i++) { _rigs.Remove(list[i]); } ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)string.Format("{0} dropped {1} destroyed HUD rig(s) (zone load); {2} live.", "[ANCHOR]", list.Count, _rigs.Count)); } } } internal static void ClearAll() { foreach (KeyValuePair rig in _rigs) { HideFrom(rig.Value, 0); } LiveBlips = 0; } internal static void Tick(IReadOnlyList targets) { //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_003e: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: 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_00f2: 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_0107: Unknown result type (might be due to invalid IL or missing references) //IL_0109: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_0117: Unknown result type (might be due to invalid IL or missing references) //IL_0119: Unknown result type (might be due to invalid IL or missing references) //IL_0143: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_0160: Unknown result type (might be due to invalid IL or missing references) //IL_0167: Unknown result type (might be due to invalid IL or missing references) if (_disabled || !DrConfig.ShowBlips.Value) { ClearAll(); return; } try { Character localPlayer = Plugin.LocalPlayer; if ((Object)(object)localPlayer == (Object)null) { ClearAll(); return; } Vector3 position = ((Component)localPlayer).transform.position; float value = DrConfig.BlipRangeMeters.Value; Color color = ParseColor(DrConfig.BlipColor.Value); int num = 0; Vector3 localPosition = default(Vector3); float num5 = default(float); foreach (KeyValuePair rig in _rigs) { Rig value2 = rig.Value; if ((Object)(object)value2.Compass == (Object)null || (Object)(object)value2.Root == (Object)null) { continue; } int num2 = 0; int num3 = 0; while (targets != null && num3 < targets.Count) { Vector3 val = targets[num3] - position; val.y = 0f; if (!(((Vector3)(ref val)).sqrMagnitude > value * value) && !(((Vector3)(ref val)).sqrMagnitude < 0.01f)) { Vector3 compassDir = value2.Compass.CompassDir; if (!(((Vector3)(ref compassDir)).sqrMagnitude < 0.001f)) { float num4 = Vector3.Angle(compassDir, val) * Mathf.Sign(Vector3.Dot(Vector3.up, Vector3.Cross(compassDir, val))); if (value2.Compass.IsVisibleOnCompass(num4, ref localPosition, ref num5)) { RectTransform val2 = DotAt(value2, num2, color); if (!((Object)(object)val2 == (Object)null)) { ((Transform)val2).localPosition = localPosition; ((Transform)val2).localScale = Vector3.one * num5; SetShown(val2, shown: true); num2++; } } } } num3++; } HideFrom(value2, num2); if (num2 > num) { num = num2; } } LiveBlips = num; _consecutiveErrors = 0; } catch (Exception ex) { if (++_consecutiveErrors >= 10) { _disabled = true; ManualLogSource log = Plugin.Log; if (log != null) { log.LogError((object)(string.Format("{0} compass blips threw {1}x in a row — ", "[ANCHOR]", 10) + $"disabling them for this session rather than flooding the log: {ex}")); } } else { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("[ANCHOR] blip tick threw: " + ex.Message)); } } } } private static RectTransform DotAt(Rig rig, int index, Color color) { //IL_003f: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Expected O, but got Unknown //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Unknown result type (might be due to invalid IL or missing references) //IL_00c9: Unknown result type (might be due to invalid IL or missing references) //IL_00d2: Unknown result type (might be due to invalid IL or missing references) while (rig.Dots.Count <= index) { GameObject val = new GameObject($"DR_Blip{rig.Dots.Count}", new Type[2] { typeof(RectTransform), typeof(Image) }); RectTransform component = val.GetComponent(); ((Transform)component).SetParent(rig.Root, false); component.sizeDelta = new Vector2(10f, 10f); Image component2 = val.GetComponent(); ((Graphic)component2).raycastTarget = false; rig.Dots.Add(component); } RectTransform val2 = rig.Dots[index]; if ((Object)(object)val2 == (Object)null) { return null; } Image component3 = ((Component)val2).GetComponent(); if ((Object)(object)component3 != (Object)null && ((Graphic)component3).color != color) { ((Graphic)component3).color = color; } return val2; } private static void HideFrom(Rig rig, int firstUnused) { for (int i = firstUnused; i < rig.Dots.Count; i++) { if ((Object)(object)rig.Dots[i] != (Object)null) { SetShown(rig.Dots[i], shown: false); } } } private static void SetShown(RectTransform dot, bool shown) { if (((Component)dot).gameObject.activeSelf != shown) { ((Component)dot).gameObject.SetActive(shown); } } internal static Color ParseColor(string hex) { //IL_001e: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) Color result = default(Color); if (!ColorUtility.TryParseHtmlString((hex ?? "").Trim(), ref result)) { return Color.red; } return result; } internal static string Path(Transform t) { string text = ((Object)t).name; Transform parent = t.parent; while ((Object)(object)parent != (Object)null) { text = ((Object)parent).name + "/" + text; parent = parent.parent; } return text; } internal static string Describe() { if (CompassFound.HasValue) { if (CompassFound != false) { if (!_disabled) { return $"{_rigs.Count} HUD(s), {LiveBlips} blip(s) live"; } return "disabled after repeated errors"; } return "NO UICompass in this HUD (text bearing fallback)"; } return "not probed yet"; } } internal static class DrConfig { internal static ConfigEntry Enabled; internal static ConfigEntry MinIntervalSeconds; internal static ConfigEntry MaxIntervalSeconds; internal static ConfigEntry FirstArmDelaySeconds; internal static ConfigEntry CooldownAfterWaveSeconds; internal static ConfigEntry RetrySeconds; internal static ConfigEntry MinCount; internal static ConfigEntry MaxCount; internal static ConfigEntry MaxOwnActive; internal static ConfigEntry MemberSpacingMeters; internal static ConfigEntry SkipWhileInCombat; internal static ConfigEntry CombatDeferMaxSeconds; internal static ConfigEntry ClusterRadiusMeters; internal static ConfigEntry MinDistanceMeters; internal static ConfigEntry MaxDistanceMeters; internal static ConfigEntry PathLengthRatioMax; internal static ConfigEntry RequireOutOfSight; internal static ConfigEntry SightCheckAllPlayers; internal static ConfigEntry SourceOrder; internal static ConfigEntry EnableInteractableAnchors; internal static ConfigEntry MaxCandidatesPerWave; internal static ConfigEntry PlateauProbeRadius; internal static ConfigEntry WarmOnly; internal static ConfigEntry PrewarmCount; internal static ConfigEntry PrewarmIntervalSeconds; internal static ConfigEntry ExtraBlocklist; internal static ConfigEntry ShowToast; internal static ConfigEntry ToastMinGapSeconds; internal static ConfigEntry ToastBearing; internal static ConfigEntry ShowBlips; internal static ConfigEntry BlipRangeMeters; internal static ConfigEntry BlipColor; internal static ConfigEntry LogVerbose; internal static ConfigEntry LedgerAutoDumpSeconds; internal static ConfigEntry ForceWaveKey; internal static void Bind(ConfigFile cfg) { //IL_03f4: Unknown result type (might be due to invalid IL or missing references) Enabled = cfg.Bind("General", "Enabled", true, "Master kill-switch. Guards: an ambush loop misbehaving in a live session with no way to stop it short of a relaunch. Set false + run 'reloadcfg' to disarm instantly."); MinIntervalSeconds = cfg.Bind("Schedule", "MinIntervalSeconds", 20f, "Shortest gap between ambushes, in seconds of ACTUAL PLAY (the clock runs on Time.time, so it freezes while paused or loading). Guards: banking up an ambush during ten minutes in the inventory and having it fire the instant you unpause."); MaxIntervalSeconds = cfg.Bind("Schedule", "MaxIntervalSeconds", 60f, "Longest gap between ambushes. An inverted band (Min > Max) clamps to Min rather than throwing — a typo makes the mod boring, never crashes a timer tick."); FirstArmDelaySeconds = cfg.Bind("Schedule", "FirstArmDelaySeconds", 60f, "Grace period after entering a region before the first ambush can fire. Guards: getting jumped in the first second after a save load or a zone transition, before you have your bearings or even full control."); CooldownAfterWaveSeconds = cfg.Bind("Schedule", "CooldownAfterWaveSeconds", 45f, "Hard floor after a wave actually places, regardless of what the interval rolls. Guards: two unlucky 20s rolls stacking six creatures onto a player who is still fighting the first three."); RetrySeconds = cfg.Bind("Schedule", "RetrySeconds", 15f, "Delay before re-checking after a SOFT BLOCK (loading, in combat, no warm species, no verified anchor, cap reached). Guards: a blocked director re-evaluating every frame — a log flood plus wasted synchronous NavMesh calls."); MinCount = cfg.Bind("Wave", "MinCount", 1, "Fewest creatures in a wave."); MaxCount = cfg.Bind("Wave", "MaxCount", 3, "Most creatures in a wave. NB the structural invariant is ONE FACTION, not one species — a shared faction is the only thing stopping an 'ambush' from fighting itself. A wave composed from a real vanilla squad's roster CAN be mixed-species (and is filtered to a single faction first); when no faction can be PROVEN it collapses to N of one species, which is same-faction by construction."); MaxOwnActive = cfg.Bind("Wave", "MaxOwnActive", 6, "Cap on creatures this mod may have alive at once. Guards: monopolising SpawnKit's [Spawner] MaxActiveSpawns, which defaults to 8 and is GLOBAL ACROSS ALL CONSUMERS — exceeding it also makes SpawnKit toast the player on every refusal."); MemberSpacingMeters = cfg.Bind("Wave", "MemberSpacingMeters", 4f, "Minimum spacing between members of one wave. Guards: three creatures minted at the same point (telefrag, physics pop, one visible blob instead of a group)."); SkipWhileInCombat = cfg.Bind("Wave", "SkipWhileInCombat", true, "Hold a wave back while the player is already fighting. Guards: piling an ambush onto a fight that is already going badly. BOUNDED by CombatDeferMaxSeconds — read that description before assuming this is a simple on/off."); CombatDeferMaxSeconds = cfg.Bind("Wave", "CombatDeferMaxSeconds", 120f, "Longest CONTINUOUS stretch SkipWhileInCombat may hold a wave, after which it fires anyway. Guards: the mod switching itself off. Outward's Character.InCombat is 'anything is engaged with me', NOT 'I am swinging' — with a companion pet picking fights it is true almost permanently, which live produced TWO waves in twenty minutes with 'player in combat' blocking every single tick. 0 restores the old unbounded behaviour; that is the bug, not a feature."); ClusterRadiusMeters = cfg.Bind("Wave", "ClusterRadiusMeters", 12f, "How tightly the members of one wave group around the lead spot. Guards: a 'wave' that is really N unrelated encounters — live, three creatures announced as one group landed ~100m apart and only one was ever found. Members are placed on a ring of this radius around the verified lead and each is still fully verified, so a wave SHRINKS rather than scattering when the ground won't take it."); MinDistanceMeters = cfg.Bind("Placement", "MinDistanceMeters", 40f, "Closest a creature may appear. Guards: something materialising in your lap with no approach to notice — which would waste the out-of-sight rule entirely."); MaxDistanceMeters = cfg.Bind("Placement", "MaxDistanceMeters", 200f, "Farthest a creature may appear. THE BAND IS CHOSEN TO OVERLAP VANILLA'S: AISquadManager deploys its own wandering squads at 50-400m, so its hand-placed AISquadSpawnPoints — the best thematic anchors in the game — barely exist below 50m. 40-200m reaches them while staying close enough that an encounter is actually felt; 400m would be more faithful and mostly invisible. NB this mod does its OWN placement and passes an explicit position to SpawnKit, precisely because SpawnKit's own SpawnDistance clamps to 50m and its ring probe is a near-field tool."); PathLengthRatioMax = cfg.Bind("Placement", "PathLengthRatioMax", 1.8f, "Reject a spot whose WALKING distance exceeds this multiple of its straight-line distance. Guards: the gorge case — a spot 40m away needing a 300m detour around a cliff. It passes a plain reachability test and still never produces an ambush."); RequireOutOfSight = cfg.Bind("Placement", "RequireOutOfSight", true, "Only place where geometry blocks the player's view (vanilla AISquadManager's own rule). Guards: creatures visibly popping into existence, the classic mod tell."); SightCheckAllPlayers = cfg.Bind("Placement", "SightCheckAllPlayers", true, "In co-op, require the spot to be hidden from EVERY player, as vanilla does. Guards: a spot occluded from the host that a guest is staring straight at."); SourceOrder = cfg.Bind("Placement", "SourceOrder", "liveai,gatherpoints,squadpoints,interactables,procedural", "Anchor sources to try, in order. Guards: needing a rebuild to reorder or disable the chain mid-session — reordering IS the experiment the ledger exists to inform. Unknown names are warned about, never silently dropped; an empty or all-unknown value falls back to the full default chain."); EnableInteractableAnchors = cfg.Bind("Placement", "EnableInteractableAnchors", true, "Use world interactables (gatherables, chests) as placement anchors. Guards: a half-mapped interactable type poisoning a session — turn it off and re-measure without losing the rest of the chain."); MaxCandidatesPerWave = cfg.Bind("Placement", "MaxCandidatesPerWave", 24, "Ceiling on candidates verified per wave. Guards: a frame hitch — NavMesh.CalculatePath is synchronous, and an unbounded candidate list would run hundreds of them in one frame."); PlateauProbeRadius = cfg.Bind("Placement", "PlateauProbeRadius", 1f, "Half-width of the four-corner ground check around a candidate. Guards: placing on a ledge, boulder top, tent roof or overhang — which NavMesh.SamplePosition actively invites, because it returns the 3D-NEAREST polygon and a roof is often nearest. Bigger = stricter (demands a wider flat patch)."); WarmOnly = cfg.Bind("Species", "WarmOnly", true, "Only spawn species whose body template is already resident. Guards: THE NEVER-STALL RULE — a cold species costs a 1-6s donor-scene harvest mid-play, and an expedition-only one costs a ~20s party teleport through two loading screens with a save on each leg. Turning this off makes ambushes hitch. Don't."); PrewarmCount = cfg.Bind("Species", "PrewarmCount", 3, "How many of the region's cold species to warm in the background. Guards: an empty warm roster meaning the mod is silent for a whole region."); PrewarmIntervalSeconds = cfg.Bind("Species", "PrewarmIntervalSeconds", 20f, "Gap between background prewarms. Guards: a harvest storm on region entry, and thrashing SpawnKit's 12-entry template LRU so that warming one species evicts the last one."); ExtraBlocklist = cfg.Bind("Species", "ExtraBlocklist", "", "Comma-separated species to never spawn, appended to the built-in list of named story NPCs and set-piece bosses. Matched as a case-insensitive substring. Guards: discovering a bad actor mid-session and needing a rebuild to stop it."); ShowToast = cfg.Bind("Notify", "ShowToast", true, "Show an on-screen message when a wave arrives."); ToastMinGapSeconds = cfg.Bind("Notify", "ToastMinGapSeconds", 30f, "Minimum gap between toasts. Guards: ForgeKit's Notify.Player has NO rate limit of its own — it pushes straight to the UI and the log on every single call."); ToastBearing = cfg.Bind("Notify", "ToastBearing", true, "Name the compass direction in the toast ('...from the north-east'). Guards: an announcement the player cannot act on — the live report that prompted this was 'I saw the toast for 3 bandits and only found one'."); ShowBlips = cfg.Bind("Compass", "ShowBlips", true, "Mark this mod's live spawns on the game's HUD compass. Falls back to the toast bearing alone if no UICompass is found in the HUD — run 'roadsui' to see which."); BlipRangeMeters = cfg.Bind("Compass", "BlipRangeMeters", 250f, "Stop marking a creature past this distance. Guards: a creature walking toward you popping off the compass — keep this comfortably above MaxDistanceMeters."); BlipColor = cfg.Bind("Compass", "BlipColor", "#FF3B30", "Blip colour, #RRGGBB or #RRGGBBAA. An unparseable value falls back to red rather than to an invisible blip."); LogVerbose = cfg.Bind("Diag", "LogVerbose", false, "Log one line per placement candidate with its full verdict chain. Guards: flooding an ordinary session's log, while keeping the detail one 'reloadcfg' away."); LedgerAutoDumpSeconds = cfg.Bind("Diag", "LedgerAutoDumpSeconds", 300f, "Auto-print the anchor-source measurement table this often (0 = never). Guards: THE SPIKE'S ENTIRE DELIVERABLE evaporating because nobody remembered to type 'roadsledger' during a session. Leave this on until anchor coverage is understood."); ForceWaveKey = cfg.Bind("Keys", "ForceWaveKey", new KeyboardShortcut((KeyCode)0, Array.Empty()), "Force an ambush wave immediately (unbound by default). Keys are a CROSS-MOD resource — pick one ForgeKit.Keybinds doesn't already report as taken."); } internal static string Describe() { return $"[General] Enabled={Enabled.Value} · " + $"[Schedule] interval={MinIntervalSeconds.Value:F0}-{MaxIntervalSeconds.Value:F0}s " + $"firstArm={FirstArmDelaySeconds.Value:F0}s cooldown={CooldownAfterWaveSeconds.Value:F0}s " + $"retry={RetrySeconds.Value:F0}s · " + $"[Wave] count={MinCount.Value}-{MaxCount.Value} maxActive={MaxOwnActive.Value} " + $"cluster={ClusterRadiusMeters.Value:F0}m spacing={MemberSpacingMeters.Value:F0}m " + $"skipInCombat={SkipWhileInCombat.Value}/{CombatDeferMaxSeconds.Value:F0}s · " + $"[Placement] band={MinDistanceMeters.Value:F0}-{MaxDistanceMeters.Value:F0}m " + $"ratioMax={PathLengthRatioMax.Value:F2} outOfSight={RequireOutOfSight.Value} " + $"plateau={PlateauProbeRadius.Value:F1}m budget={MaxCandidatesPerWave.Value} " + "order=" + SourceOrder.Value + " · " + $"[Species] warmOnly={WarmOnly.Value} prewarm={PrewarmCount.Value}/" + $"{PrewarmIntervalSeconds.Value:F0}s blocklist='{ExtraBlocklist.Value}' · " + $"[Diag] verbose={LogVerbose.Value}"; } } internal static class FactionBook { internal static readonly SortedDictionary Learned = new SortedDictionary(StringComparer.OrdinalIgnoreCase); internal static FactionTable Table { get; private set; } = FactionTable.Parse(""); internal static void Load() { string text = EmbeddedRes.Text(typeof(FactionBook).Assembly, "SpeciesFactions.txt", "[ROSTER]", Plugin.Log); Table = FactionTable.Parse(text); ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)string.Format("{0} faction table: {1} species.", "[ROSTER]", Table.Count)); } } internal unsafe static void Observe(string species, Factions faction) { string text = (species ?? "").Trim(); if (text.Length == 0) { return; } string text2 = ((object)(*(Factions*)(&faction))/*cast due to .constrained prefix*/).ToString(); string text3 = default(string); if (!Table.Observe(text, text2, ref text3)) { return; } Learned[text] = text2; if (text3 == null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)("[ROSTER] learned '" + text + "' = " + text2 + " (not in the shipped table).")); } return; } ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("[ROSTER] '" + text + "' reported " + text2 + " but the table said " + text3 + " — the shipped row is wrong or this species varies by donor. Worth investigating.")); } } internal static int ScanScene() { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Invalid comparison between Unknown and I4 //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_008f: Unknown result type (might be due to invalid IL or missing references) int count = Learned.Count; CharacterManager instance = CharacterManager.Instance; if ((Object)(object)instance != (Object)null && instance.Characters != null) { foreach (Character value in instance.Characters.Values) { if ((Object)(object)value != (Object)null && value.IsAI && (int)value.Faction != 1 && !string.IsNullOrEmpty(value.Name)) { UID uID = value.UID; if (!SpawnUid.IsSpawnUid(((UID)(ref uID)).Value)) { Observe(value.Name, value.Faction); } } } } AISquadManager instance2 = AISquadManager.Instance; if ((Object)(object)instance2 != (Object)null) { ScanSquads(instance2.SquadsInPlay); ScanSquads(instance2.SquadsInReserve); } return Learned.Count - count; } private static void ScanSquads(List squads) { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Invalid comparison between Unknown and I4 //IL_005d: Unknown result type (might be due to invalid IL or missing references) if (squads == null) { return; } for (int i = 0; i < squads.Count; i++) { AISquad obj = squads[i]; List list = ((obj != null) ? obj.Members : null); if (list == null) { continue; } for (int j = 0; j < list.Count; j++) { AISquadMember obj2 = list[j]; Character val = ((obj2 != null) ? obj2.Character : null); if ((Object)(object)val != (Object)null && !string.IsNullOrEmpty(val.Name) && (int)val.Faction != 1) { Observe(val.Name, val.Faction); } } } } internal static void ObserveSpawn(SpawnHandle handle, string speciesKey) { //IL_002e: Unknown result type (might be due to invalid IL or missing references) Character val = ((handle != null) ? handle.Character : null); if (!((Object)(object)val == (Object)null)) { Observe(string.IsNullOrEmpty(val.Name) ? speciesKey : val.Name, val.Faction); } } } internal static class Ident { internal const string CHANNEL = "DangerousRoads_cmd.txt"; internal const string OWNER_TAG = "dangerousroads"; internal const string T_AMBUSH = "[AMBUSH]"; internal const string T_ANCHOR = "[ANCHOR]"; internal const string T_ROSTER = "[ROSTER]"; internal const string T_LEDGER = "[LEDGER]"; } [BepInPlugin("cobalt.dangerousroads", "DangerousRoads", "0.1.1")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { public const string GUID = "cobalt.dangerousroads"; public const string NAME = "DangerousRoads"; public const string VERSION = "0.1.1"; internal static ManualLogSource Log; internal static Plugin Instance; private CommandRegistry _commands; private VerbHost _verbs; private CommandChannel _channel; private AmbushDirector _director; private readonly List _blipTargets = new List(); internal static Character LocalPlayer { get { CharacterManager instance = CharacterManager.Instance; if (instance == null) { return null; } return instance.GetFirstLocalCharacter(); } } internal void Awake() { //IL_00c3: Unknown result type (might be due to invalid IL or missing references) //IL_00cd: Expected O, but got Unknown //IL_00d2: Unknown result type (might be due to invalid IL or missing references) Instance = this; Log = ((BaseUnityPlugin)this).Logger; Log.LogMessage((object)("[DangerousRoads] build " + BuildStamp.Read(((object)this).GetType().Assembly) + " @ " + ((object)this).GetType().Assembly.Location)); if (Notify.Log == null) { Notify.Log = ((BaseUnityPlugin)this).Logger; } DrConfig.Bind(((BaseUnityPlugin)this).Config); Log.LogMessage((object)("[AMBUSH] config: " + DrConfig.Describe())); Keybinds.Claim("DangerousRoads", "force an ambush wave", DrConfig.ForceWaveKey); FactionBook.Load(); _director = new AmbushDirector(this); RegisterVerbs(); _channel = new CommandChannel("DangerousRoads_cmd.txt", Log, _commands, 0.5f, true, true); new Harmony("cobalt.dangerousroads").PatchAll(); SceneManager.sceneLoaded += OnSceneLoaded; Log.LogMessage((object)"[AMBUSH] ready — verbs on BepInEx/config/DangerousRoads_cmd.txt ('help' lists them; 'roadsstatus' is the one to start with)."); } internal void Update() { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0032: Unknown result type (might be due to invalid IL or missing references) //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) _channel.Tick(); _director.Tick(); if (Time.frameCount % 2 == 0) { TickBlips(); } KeyboardShortcut value = DrConfig.ForceWaveKey.Value; if ((int)((KeyboardShortcut)(ref value)).MainKey != 0) { value = DrConfig.ForceWaveKey.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { _channel.Run("roadsnow"); } } } private void TickBlips() { //IL_004b: Unknown result type (might be due to invalid IL or missing references) _blipTargets.Clear(); IReadOnlyList readOnlyList = Spawner.Active("dangerousroads"); for (int i = 0; i < readOnlyList.Count; i++) { SpawnHandle obj = readOnlyList[i]; Character val = ((obj != null) ? obj.Character : null); if ((Object)(object)val != (Object)null && val.Alive) { _blipTargets.Add(((Component)val).transform.position); } } CompassBlips.Tick(_blipTargets); } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) if ((int)mode == 0 && Scenes.IsGameplay(((Scene)(ref scene)).name)) { string sceneName = ((Scene)(ref scene)).name; ((MonoBehaviour)this).StartCoroutine(Lifecycle.WhenPlayerReady((Func)(() => LocalPlayer), (Action)delegate(Character player) { _director.OnRegionReady(player, sceneName); }, (Action)delegate(string why) { Log.LogWarning((object)("[AMBUSH] " + why + " in '" + sceneName + "'.")); }, 30f, (object)this)); } } private void RegisterVerbs() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Expected O, but got Unknown //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_00a7: Expected O, but got Unknown _commands = new CommandRegistry(Log); _verbs = new VerbHost(_commands, Log, (Func)(() => LocalPlayer)); _verbs.Register("selftest", "Run the DangerousRoads self-test ([SELFTEST] PASS/FAIL ... DONE).", (Action)delegate { SelfTest(); }, "[DangerousRoads]", false, true, false, (string)null); Verbs.Register(_verbs, _director); CommonVerbs.RegisterAll(_verbs, Log, new CommonVerbsOptions { ConfigSource = () => ((BaseUnityPlugin)this).Config }); } private void SelfTest() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0093: Unknown result type (might be due to invalid IL or missing references) //IL_0099: Invalid comparison between Unknown and I4 //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: Invalid comparison between Unknown and I4 //IL_011e: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Unknown result type (might be due to invalid IL or missing references) SelfTestHarness val = new SelfTestHarness(Log); val.Begin("DangerousRoads 0.1.1"); val.Check("logger wired", Log != null); val.Check("config bound", DrConfig.Enabled != null && DrConfig.SourceOrder != null); val.Check("no cross-mod keybind conflicts", !Keybinds.HasConflicts()); val.Check("overworld whitelist has all six regions", RegionGate.OverworldAreaIds.Length == 6); val.Check("Caldera passes, New Sirocco does not", (int)RegionGate.Evaluate(true, (int?)602, false) == 0 && (int)RegionGate.Evaluate(true, (int?)601, false) == 3); val.Check("unknown scene fails closed", (int)RegionGate.Evaluate(true, (int?)null, false) == 2); val.Check("interval band rolls inside [min,max]", AmbushClock.NextDelay(0.5, 20f, 300f) == 160f); val.Check("gorge ratio rejects a 300m walk to a 40m spot", !BandMath.PathRatioOk(300f, 40f, 1.8f)); val.Check("region gate agrees with live AreaManager", _director.Region.LastVerdict == RegionGate.Evaluate(DrConfig.Enabled.Value, _director.Region.LastAreaId, (Object)(object)AreaManager.Instance != (Object)null && AreaManager.Instance.GetIsCurrentAreaTownOrCity())); val.Check("anchor chain resolves to at least one source", _director.Anchors.ActiveOrder().Count > 0); val.Check("every anchor source is registered in the ledger", _director.Anchors.Ledger.Sources.Count == _director.Anchors.KnownSourceIds.Count); val.Done(); } } internal sealed class Prewarmer { private readonly Queue _queue = new Queue(); private readonly HashSet _attempted = new HashSet(StringComparer.OrdinalIgnoreCase); private bool _inFlight; private double _nextAt = double.NegativeInfinity; internal string InFlightKey { get; private set; } = ""; internal int Pending => _queue.Count; internal void Reset(IReadOnlyList targets, int max) { _queue.Clear(); _attempted.Clear(); if (targets == null) { return; } int num = Mathf.Min(max, targets.Count); for (int i = 0; i < num; i++) { _queue.Enqueue(targets[i].Key); } if (num > 0) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)string.Format("{0} prewarm queue: {1} species.", "[ROSTER]", num)); } } } internal void Tick(double unscaledNow) { if (_inFlight || _queue.Count == 0) { return; } if (double.IsNegativeInfinity(_nextAt)) { _nextAt = unscaledNow; } if (unscaledNow < _nextAt) { return; } if (Spawner.IsExpeditionRunning) { _nextAt = unscaledNow + 5.0; return; } string key = _queue.Dequeue(); if (!_attempted.Add(key)) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)("[ROSTER] prewarm skipped '" + key + "' — already attempted this region " + $"({_queue.Count} left).")); } return; } if (Spawner.CanMintNow(key)) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogMessage((object)string.Format("{0} prewarm skipped '{1}' — already warm ({2} left).", "[ROSTER]", key, _queue.Count)); } return; } _inFlight = true; InFlightKey = key; ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogMessage((object)string.Format("{0} prewarming '{1}' ({2} left)...", "[ROSTER]", key, _queue.Count)); } Spawner.Prewarm(key, (Action)delegate(bool ok) { _inFlight = false; InFlightKey = ""; _nextAt = Time.unscaledTime + DrConfig.PrewarmIntervalSeconds.Value; ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogMessage((object)("[ROSTER] prewarm '" + key + "' " + (ok ? "OK" : "FAILED") + " " + $"({_queue.Count} left in queue).")); } }); } internal void RequestNow(string speciesKey) { string text = (speciesKey ?? "").Trim(); if (text.Length != 0) { _attempted.Remove(text); _queue.Enqueue(text); _nextAt = double.NegativeInfinity; } } internal string Describe() { if (!_inFlight) { if (_queue.Count <= 0) { return "idle, queue empty"; } return $"idle, {_queue.Count} queued"; } return $"warming '{InFlightKey}', {_queue.Count} queued"; } } internal sealed class RegionWatch { internal int? LastAreaId { get; private set; } internal GateVerdict LastVerdict { get; private set; } = (GateVerdict)2; internal string LastAreaName { get; private set; } = "(none)"; internal string LedgerKey => LastAreaName; internal bool IsOverworld => (int)LastVerdict == 0; internal bool Poll(bool enabled) { //IL_00b1: Unknown result type (might be due to invalid IL or missing references) int? num = null; string lastAreaName = "(none)"; bool flag = false; AreaManager instance = AreaManager.Instance; if ((Object)(object)instance != (Object)null) { Area currentArea = instance.CurrentArea; if (currentArea != null) { num = currentArea.ID; lastAreaName = (string.IsNullOrEmpty(currentArea.DefaultName) ? $"area#{currentArea.ID}" : currentArea.DefaultName); } flag = instance.GetIsCurrentAreaTownOrCity(); } bool result = num != LastAreaId; LastAreaId = num; LastAreaName = lastAreaName; LastVerdict = RegionGate.Evaluate(enabled, num, flag); return result; } internal string Describe() { //IL_0039: Unknown result type (might be due to invalid IL or missing references) return string.Format("{0} (id={1}) → {2}", LastAreaName, LastAreaId.HasValue ? LastAreaId.Value.ToString() : "none", LastVerdict); } } internal sealed class SpeciesRoster { private List _scratch = new List(); internal List All { get; private set; } = new List(); internal string BuiltForScene { get; private set; } = ""; internal void Rebuild(Character player, string sceneName) { BuiltForScene = sceneName ?? ""; List<(string, SpeciesSource)> list = new List<(string, SpeciesSource)>(); CollectNearby(player, list); CollectFromSquads(list); CollectFromDonorTable(sceneName, list); All = RosterFilter.Build((IEnumerable>)list, (IEnumerable)ExtraBlocklist(), (Func)Spawner.IsExpeditionOnly, (Func)Spawner.CanMintNow); int count = RosterFilter.Spawnable((IReadOnlyList)All, DrConfig.WarmOnly.Value).Count; ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)(string.Format("{0} '{1}': {2} species considered, ", "[ROSTER]", BuiltForScene, All.Count) + $"{count} spawnable now (warmOnly={DrConfig.WarmOnly.Value}).")); } } internal List SpawnableNow() { //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Expected O, but got Unknown List list = new List(All.Count); for (int i = 0; i < All.Count; i++) { SpeciesCandidate val = All[i]; if (!val.Blocked && !val.ExpeditionOnly) { bool flag = Spawner.CanMintNow(val.Key); if (!DrConfig.WarmOnly.Value || flag) { list.Add(new SpeciesCandidate(val.Key, val.Source, val.Blocked, val.ExpeditionOnly, flag)); } } } return list; } internal List PrewarmTargets() { List list = new List(); for (int i = 0; i < All.Count; i++) { SpeciesCandidate val = All[i]; if (!val.Blocked && !val.ExpeditionOnly && !Spawner.CanMintNow(val.Key)) { list.Add(val); } } return list; } internal static string[] ExtraBlocklist() { string value = DrConfig.ExtraBlocklist.Value; if (!string.IsNullOrEmpty(value)) { return value.Split(new char[1] { ',' }); } return new string[0]; } private void CollectNearby(Character player, List<(string, SpeciesSource)> into) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) CharacterManager instance = CharacterManager.Instance; if ((Object)(object)instance == (Object)null || (Object)(object)player == (Object)null) { return; } _scratch.Clear(); instance.FindCharactersInRange(((Component)player).transform.position, 200f, ref _scratch); for (int i = 0; i < _scratch.Count; i++) { Character val = _scratch[i]; if (LiveAiSource.IsUsableAnchor(val) && !string.IsNullOrEmpty(val.Name)) { into.Add((val.Name, (SpeciesSource)2)); } } } private static void CollectFromSquads(List<(string, SpeciesSource)> into) { AISquadManager instance = AISquadManager.Instance; if (!((Object)(object)instance == (Object)null)) { AddSquads(instance.SquadsInPlay, into); AddSquads(instance.SquadsInReserve, into); } } private static void AddSquads(List squads, List<(string, SpeciesSource)> into) { //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Invalid comparison between Unknown and I4 if (squads == null) { return; } for (int i = 0; i < squads.Count; i++) { AISquad obj = squads[i]; List list = ((obj != null) ? obj.Members : null); if (list == null) { continue; } for (int j = 0; j < list.Count; j++) { AISquadMember obj2 = list[j]; Character val = ((obj2 != null) ? obj2.Character : null); if ((Object)(object)val != (Object)null && !string.IsNullOrEmpty(val.Name) && (int)val.Faction != 1) { into.Add((val.Name, (SpeciesSource)1)); } } } } private static void CollectFromDonorTable(string sceneName, List<(string, SpeciesSource)> into) { if (string.IsNullOrEmpty(sceneName)) { return; } Dictionary> donorScenes = DonorHarvest.DonorScenes; if (donorScenes == null) { return; } List list = DonorTable.KeysForScene(donorScenes, sceneName, true); if (list != null) { for (int i = 0; i < list.Count; i++) { into.Add((list[i], (SpeciesSource)0)); } } } } internal static class Toasts { private static double _lastAt = double.NegativeInfinity; internal static void Wave(Character player, string species, int count) { Wave(player, species, count, null); } internal static void Wave(Character player, string species, int count, Vector3? at) { //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_008c: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_0096: Unknown result type (might be due to invalid IL or missing references) //IL_0097: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) if (!DrConfig.ShowToast.Value || count <= 0) { return; } double num = Time.unscaledTime; if (!ThrottlePolicy.Allow(num, _lastAt, DrConfig.ToastMinGapSeconds.Value)) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)string.Format("{0} toast suppressed by throttle ({1} x{2}).", "[AMBUSH]", species, count)); } return; } _lastAt = num; string text = ""; if (DrConfig.ToastBearing.Value && at.HasValue && (Object)(object)player != (Object)null) { Vector3 val = at.Value - ((Component)player).transform.position; text = Bearing.Label(val.x, val.z, 0.5f); } string text2 = ToastText.Wave(species, count, text); if (text2.Length > 0) { Notify.Player(player, text2); } } internal static void ResetThrottle() { _lastAt = double.NegativeInfinity; } } internal static class Verbs { private const string Tag = "[DangerousRoads]"; internal static void Register(VerbHost verbs, AmbushDirector dir) { verbs.Register("roadsstatus", "Director state: region, gate verdict, time to next wave, roster/warm counts, last wave.", (Action)delegate { Status(dir); }, "[DangerousRoads]", false, true, false, (string)null); verbs.Register("roadsroster", "Every species considered for this region: source, faction, blocked, expedition-only, warm.", (Action)delegate { Roster(dir); }, "[DangerousRoads]", false, true, false, (string)null); verbs.Register("roadsfactions", "Harvest species->faction from the loaded scene AND its reserve squads (the whole region roster, no spawning), then print anything new for pasting into SpeciesFactions.txt. 'roadsfactions all' prints the entire merged table.", (Action)delegate(VerbContext ctx) { Factions(ctx.Arg(1)); }, "[DangerousRoads]", false, true, false, (string)null); verbs.Register("roadsledger", "THE SPIKE TABLE — per-region, per-source candidate counts, accept rates and reject histogram. 'roadsledger reset' clears it.", (Action)delegate(VerbContext ctx) { Ledger(dir, ctx.Arg(1)); }, "[DangerousRoads]", false, true, false, (string)null); verbs.Register("roadsanchors", "Run the WHOLE anchor chain now and print every candidate with its full verdict chain. Spawns nothing. Optional arg: how many spots to look for (default 3).", (Action)delegate(VerbContext ctx) { Anchors(dir, ctx.Player, ctx.Arg(1)); }, "[DangerousRoads]", true, true, false, (string)null); verbs.Register("roadssquadpoints", "Raw census of vanilla AISquadSpawnPoints in this scene — position, distance, typeID, CheckValidSpawn, member species. Independent of our own filters.", (Action)delegate(VerbContext ctx) { SquadPoints(dir, ctx.Player); }, "[DangerousRoads]", true, true, false, (string)null); verbs.Register("roadsui", "Dump the live CharacterUI hierarchy and report whether a UICompass was found — the recon that decides whether compass blips can work at all. 'roadsui ' to go deeper (default 6).", (Action)delegate(VerbContext ctx) { Log(UiDump.Dump(ParseInt(ctx.Arg(1), 6))); }, "[DangerousRoads]", true, true, false, (string)null); verbs.Register("roadsblips", "Compass blip status: whether a UICompass was found, how many HUDs are hooked, and how many blips are live right now.", (Action)delegate { Log("[ANCHOR] blips: " + CompassBlips.Describe()); }, "[DangerousRoads]", false, true, false, (string)null); verbs.Register("roadsprobe", "Run the verification pipeline on one point and print every stage. 'roadsprobe' = where you stand; 'roadsprobe ' = an explicit spot.", (Action)delegate(VerbContext ctx) { Probe(dir, ctx.Player, ctx); }, "[DangerousRoads]", true, true, false, (string)null); verbs.Register("roadsmark", "Pick the best anchor right now, log it and draw a marker ray — walk over and judge the terrain yourself. Spawns nothing.", (Action)delegate(VerbContext ctx) { Mark(dir, ctx.Player); }, "[DangerousRoads]", true, true, false, (string)null); verbs.Register("roadsnow", "Force a wave immediately, through the real pipeline. 'roadsnow [count] [species...]'.", (Action)delegate(VerbContext ctx) { Now(dir, ctx); }, "[DangerousRoads]", true, true, true, (string)null); verbs.Register("roadsarm", "Arm the director so the next wave is due immediately.", (Action)delegate { dir.ArmNow(); Log("armed; next wave due now."); }, "[DangerousRoads]", false, true, false, (string)null); verbs.Register("roadsdisarm", "Stop the ambush timer until the next region change.", (Action)delegate { dir.Disarm("roadsdisarm"); Log("disarmed."); }, "[DangerousRoads]", false, true, false, (string)null); verbs.Register("roadswarm", "Queue a species for background prewarm now ('roadswarm ').", (Action)delegate(VerbContext ctx) { Warm(dir, ctx.Tail(1)); }, "[DangerousRoads]", false, true, true, (string)null); verbs.Register("roadssweep", "Despawn every creature THIS mod spawned. 'roadssweep kill' kills them instead (death animation + loot). The panic button.", (Action)delegate(VerbContext ctx) { Sweep(ctx.Arg(1)); }, "[DangerousRoads]", false, true, true, (string)null); } private static void Status(AmbushDirector dir) { //IL_0051: Unknown result type (might be due to invalid IL or missing references) //IL_01a0: Unknown result type (might be due to invalid IL or missing references) StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine("[AMBUSH] status"); stringBuilder.AppendLine($" state {dir.State}"); stringBuilder.AppendLine(" region " + dir.Region.Describe()); stringBuilder.AppendLine($" clock {dir.Clock} (due in {Fmt(dir.SecondsUntilDue())})"); stringBuilder.AppendLine(" last block " + dir.LastBlock); stringBuilder.AppendLine($" blocks {dir.Blocks.Total} total — {dir.Blocks.Format()}"); int count = dir.Roster.SpawnableNow().Count; stringBuilder.AppendLine($" roster {dir.Roster.All.Count} considered, {count} spawnable now " + "(scene '" + dir.Roster.BuiltForScene + "')"); stringBuilder.AppendLine(" prewarm " + dir.Warmer.Describe()); stringBuilder.AppendLine(string.Format(" active {0} / ", Spawner.Active("dangerousroads").Count) + $"{DrConfig.MaxOwnActive.Value} (SpawnKit's global cap is shared)"); stringBuilder.AppendLine($" last wave {dir.Wave.LastOutcome} — placed {dir.Wave.LastPlaced} of " + string.Format("{0} via {1} ", dir.Wave.LastPlan, Or(dir.Wave.LastAnchorSource, "-")) + "@ " + Fmt(dir.Wave.LastAnchorDistance)); stringBuilder.AppendLine(" blips " + CompassBlips.Describe()); stringBuilder.AppendLine($" factions {FactionBook.Table.Count} known" + ((FactionBook.Learned.Count > 0) ? $", {FactionBook.Learned.Count} learned this session (roadsfactions to see them)" : "")); stringBuilder.AppendLine(" chain " + string.Join(" > ", dir.Anchors.ActiveOrder().ToArray())); stringBuilder.Append(" config " + DrConfig.Describe()); Log(stringBuilder.ToString()); } private static void Roster(AmbushDirector dir) { //IL_00c5: Unknown result type (might be due to invalid IL or missing references) //IL_00ca: Unknown result type (might be due to invalid IL or missing references) IReadOnlyList all = dir.Roster.All; if (all.Count == 0) { Log("[ROSTER] roster is empty (not in an overworld region?)."); return; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine(string.Format("{0} {1} species for '{2}'", "[ROSTER]", all.Count, dir.Roster.BuiltForScene)); for (int i = 0; i < all.Count; i++) { SpeciesCandidate val = all[i]; bool flag = !val.Blocked && !val.ExpeditionOnly && Spawner.CanMintNow(val.Key); string text = FactionBook.Table.FactionOf(val.Key) ?? "?"; stringBuilder.AppendLine(" " + val.Key.PadRight(26) + " " + ((object)val.Source/*cast due to .constrained prefix*/).ToString().PadRight(13) + text.PadRight(16) + (val.Blocked ? " BLOCKED" : "") + (val.ExpeditionOnly ? " EXPEDITION-ONLY" : "") + ((val.Blocked || val.ExpeditionOnly) ? "" : (flag ? " warm" : " cold"))); } Log(stringBuilder.ToString().TrimEnd(Array.Empty())); } private static void Factions(string arg) { int num = FactionBook.ScanScene(); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine(string.Format("{0} faction scan: {1} new species this call; ", "[ROSTER]", num) + $"{FactionBook.Table.Count} known, {FactionBook.Learned.Count} learned at runtime."); if (string.Equals(arg, "all", StringComparison.OrdinalIgnoreCase)) { stringBuilder.AppendLine(" --- full merged table (paste into src/DangerousRoads/SpeciesFactions.txt) ---"); foreach (string item in FactionBook.Table.ToLines()) { stringBuilder.AppendLine(" " + item); } } else if (FactionBook.Learned.Count > 0) { stringBuilder.AppendLine(" --- new/changed rows (paste into src/DangerousRoads/SpeciesFactions.txt) ---"); foreach (KeyValuePair item2 in FactionBook.Learned) { stringBuilder.AppendLine(" " + item2.Key + "=" + item2.Value); } } else { stringBuilder.Append(" nothing new — the shipped table already covers everything in this scene."); } Log(stringBuilder.ToString().TrimEnd(Array.Empty())); } private static void Ledger(AmbushDirector dir, string arg) { if (string.Equals(arg, "reset", StringComparison.OrdinalIgnoreCase)) { dir.Anchors.Ledger.Reset(); Log("[LEDGER] reset."); } else { Log("[LEDGER]\n" + dir.Anchors.Ledger.FormatAll()); } } private static void Anchors(AmbushDirector dir, Character player, string countArg) { //IL_0021: Unknown result type (might be due to invalid IL or missing references) int num = ParseInt(countArg, 3); List list = new List(); List viewerCenters = WaveRunner.ViewerCenters(player); List list2 = dir.Anchors.FindSpots(((Component)player).transform.position, viewerCenters, num, dir.Region.LedgerKey, list); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine(string.Format("{0} probe: {1} candidate(s) examined, ", "[ANCHOR]", list.Count) + $"{list2.Count} accepted (wanted {num})"); stringBuilder.AppendLine(" chain " + string.Join(" > ", dir.Anchors.ActiveOrder().ToArray())); for (int i = 0; i < list.Count; i++) { stringBuilder.AppendLine($" {list[i]}"); } if (list.Count == 0) { stringBuilder.AppendLine(" (no source offered anything in band — check roadssquadpoints and the ledger)"); } Log(stringBuilder.ToString().TrimEnd(Array.Empty())); } private static void SquadPoints(AmbushDirector dir, Character player) { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Unknown result type (might be due to invalid IL or missing references) //IL_00d4: Unknown result type (might be due to invalid IL or missing references) if (!(dir.Anchors.Get("squadpoints") is SquadPointSource squadPointSource)) { Log("squadpoints source is not registered."); return; } Vector3 position = ((Component)player).transform.position; AISquadSpawnPoint[] points = squadPointSource.Points; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine(string.Format("{0} squad spawn point census — {1} in scene ", "[ANCHOR]", points.Length) + "(found via " + squadPointSource.FoundVia + ")"); stringBuilder.AppendLine(" NB vanilla deploys these in a 50-400m band; ours is " + $"{DrConfig.MinDistanceMeters.Value:F0}-{DrConfig.MaxDistanceMeters.Value:F0}m. " + "The overlap is the whole question."); int num = 0; for (int i = 0; i < points.Length; i++) { AISquadSpawnPoint val = points[i]; if (!((Object)(object)val == (Object)null)) { float num2 = AnchorUtil.FlatDistance(position, ((Component)val).transform.position); bool flag = num2 >= DrConfig.MinDistanceMeters.Value && num2 <= DrConfig.MaxDistanceMeters.Value; if (flag) { num++; } stringBuilder.AppendLine(string.Format(" #{0,-3} d={1,7:F1}m {2} ", i, num2, flag ? "IN-BAND" : " ") + $"typeID={val.SquadSpawnTypeID,-3} valid={SquadPointSource.SafeCheckValid(val),-5} " + "species=[" + string.Join(", ", SquadPointSource.SpeciesAt(val).ToArray()) + "]"); } } stringBuilder.Append($" → {num} of {points.Length} in band from here."); Log(stringBuilder.ToString()); } private static void Probe(AmbushDirector dir, Character player, VerbContext ctx) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0080: Unknown result type (might be due to invalid IL or missing references) //IL_0091: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_00ba: 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_00fc: 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) //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_0182: Unknown result type (might be due to invalid IL or missing references) //IL_0189: Expected O, but got Unknown //IL_018f: Unknown result type (might be due to invalid IL or missing references) //IL_0194: Unknown result type (might be due to invalid IL or missing references) //IL_01ba: Unknown result type (might be due to invalid IL or missing references) //IL_023b: Unknown result type (might be due to invalid IL or missing references) Vector3 position = ((Component)player).transform.position; if (ctx.Arg(3) != null && float.TryParse(ctx.Arg(1), NumberStyles.Float, CultureInfo.InvariantCulture, out var result) && float.TryParse(ctx.Arg(2), NumberStyles.Float, CultureInfo.InvariantCulture, out var result2) && float.TryParse(ctx.Arg(3), NumberStyles.Float, CultureInfo.InvariantCulture, out var result3)) { ((Vector3)(ref position))..ctor(result, result2, result3); } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine(string.Format("{0} probe at {1}", "[ANCHOR]", position)); Vector3 onMesh; bool flag = NavProbe.SnapToWalkable(position, out onMesh); stringBuilder.AppendLine(" 1 snap-to-walkable " + (flag ? $"OK -> {onMesh} (moved {Vector3.Distance(position, onMesh):F2}m)" : "FAIL (no walkable navmesh in range)")); if (!flag) { Log(stringBuilder.ToString().TrimEnd(Array.Empty())); return; } float num = AnchorUtil.FlatDistance(((Component)player).transform.position, onMesh); stringBuilder.AppendLine($" 2 band {num:F1}m — " + (BandMath.InBand(num, DrConfig.MinDistanceMeters.Value, DrConfig.MaxDistanceMeters.Value) ? "in" : "OUT OF") + " band"); bool flag2 = NavProbe.IsOccludedFrom(player.CenterPosition, onMesh); stringBuilder.AppendLine(" 3 out of sight " + (flag2 ? "OK (occluded)" : "FAIL (player can see it)")); NavMeshPath val = new NavMeshPath(); bool flag3 = NavProbe.CanWalk(((Component)player).transform.position, onMesh, val); float num2 = NavProbe.PathLength(val); stringBuilder.AppendLine(string.Format(" 4 reachable {0} pathLen={1:F1}m", flag3 ? "OK" : $"FAIL ({val.status})", num2)); float num3 = BandMath.Ratio(num2, num); stringBuilder.AppendLine($" 5 path ratio {num3:F2} (max {DrConfig.PathLengthRatioMax.Value:F2}) — " + (BandMath.PathRatioOk(num2, num, DrConfig.PathLengthRatioMax.Value) ? "OK" : "FAIL (detour / wrong side of a cliff)")); NavProbe.GroundCorners(onMesh, DrConfig.PlateauProbeRadius.Value, out var d, out var d2, out var d3, out var d4); stringBuilder.Append($" 6 plateau corners=[{d:F2} {d2:F2} {d3:F2} {d4:F2}] (-1 = void) — " + (PlateauRule.Accept(d, d2, d3, d4, 0f) ? "OK" : "FAIL (ledge/rock/roof)")); Log(stringBuilder.ToString()); } private static void Mark(AmbushDirector dir, Character player) { //IL_0013: 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_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_009a: Unknown result type (might be due to invalid IL or missing references) //IL_009f: Unknown result type (might be due to invalid IL or missing references) //IL_00c4: Unknown result type (might be due to invalid IL or missing references) List viewerCenters = WaveRunner.ViewerCenters(player); List list = dir.Anchors.FindSpots(((Component)player).transform.position, viewerCenters, 1, dir.Region.LedgerKey); if (list.Count == 0) { Log("[ANCHOR] mark: no verified anchor from here."); return; } VerifiedSpot verifiedSpot = list[0]; Vector3 val = verifiedSpot.Position - ((Component)player).transform.position; float num = Mathf.Atan2(val.x, val.z) * 57.29578f; if (num < 0f) { num += 360f; } Debug.DrawRay(verifiedSpot.Position, Vector3.up * 8f, Color.red, 60f); Log(string.Format("{0} mark: {1}\n", "[ANCHOR]", verifiedSpot) + $" at {verifiedSpot.Position} — bearing {num:F0}deg, {verifiedSpot.Distance:F0}m. " + "A red ray marks it for 60s; walk over and judge the ground."); } private static void Now(AmbushDirector dir, VerbContext ctx) { int num = ParseInt(ctx.Arg(1), 0); string text = ((num > 0) ? ctx.Tail(2) : ctx.Tail(1)); if (num <= 0) { num = 1; } dir.ForceWave(ctx.Player, num, string.IsNullOrEmpty(text) ? null : text.Trim()); } private static void Warm(AmbushDirector dir, string species) { if (string.IsNullOrEmpty(species)) { Log("usage: roadswarm "); return; } dir.Warmer.RequestNow(species.Trim()); Log("[ROSTER] queued '" + species.Trim() + "' for prewarm."); } private static void Sweep(string arg) { bool flag = string.Equals(arg, "kill", StringComparison.OrdinalIgnoreCase); int count = Spawner.Active("dangerousroads").Count; Spawner.DespawnAll("dangerousroads", flag); Log(string.Format("{0} swept {1} spawn(s){2}.", "[AMBUSH]", count, flag ? " (killed)" : "")); } private static void Log(string message) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)message); } } private static int ParseInt(string s, int fallback) { if (!int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { return fallback; } return result; } private static string Fmt(float seconds) { if (!(seconds < 0f)) { return $"{seconds:F0}s"; } return "-"; } private static string Or(string s, string fallback) { if (!string.IsNullOrEmpty(s)) { return s; } return fallback; } } internal enum WaveOutcome { Pending, Placed, NoAnchor, NoSpecies, SpawnRefused, Aborted } internal sealed class WaveRunner { private readonly AnchorRegistry _anchors; internal WaveOutcome LastOutcome { get; private set; } internal WavePlan LastPlan { get; private set; } internal int LastPlaced { get; private set; } internal string LastAnchorSource { get; private set; } = ""; internal float LastAnchorDistance { get; private set; } = -1f; internal bool Running { get; private set; } internal string LastSpecies { get { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) WavePlan lastPlan = LastPlan; if (!((WavePlan)(ref lastPlan)).Ok) { return ""; } lastPlan = LastPlan; return ((WavePlan)(ref lastPlan)).PrimarySpecies; } } internal WaveRunner(AnchorRegistry anchors) { _anchors = anchors; } internal IEnumerator Run(Character player, string areaKey, IReadOnlyList spawnable, IReadOnlyList recentSpecies, int ownActive, int forcedCount, string forcedSpecies, Action onDone) { Running = true; LastOutcome = WaveOutcome.Pending; LastPlaced = 0; LastAnchorSource = ""; LastAnchorDistance = -1f; try { if ((Object)(object)player == (Object)null) { Finish(WaveOutcome.Aborted, onDone); yield break; } int want = Mathf.Max(1, (forcedCount > 0) ? forcedCount : DrConfig.MaxCount.Value); List viewerCenters = ViewerCenters(player); List spots = _anchors.FindCluster(((Component)player).transform.position, viewerCenters, want, areaKey); yield return null; if (spots.Count == 0) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)("[AMBUSH] no verified anchor in " + areaKey + " — skipping.")); } Finish(WaveOutcome.NoAnchor, onDone); yield break; } LastAnchorSource = spots[0].SourceId; LastAnchorDistance = spots[0].Distance; _anchors.Ledger.WaveUsed(areaKey, spots[0].SourceId); WavePlan plan = (LastPlan = BuildPlan(spawnable, spots, recentSpecies, ownActive, forcedCount, forcedSpecies)); if (!((WavePlan)(ref plan)).Ok) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogMessage((object)string.Format("{0} composition refused: {1}.", "[AMBUSH]", ((WavePlan)(ref plan)).Refusal)); } Finish(((int)((WavePlan)(ref plan)).Refusal == 1) ? WaveOutcome.NoSpecies : WaveOutcome.SpawnRefused, onDone); yield break; } int n = Mathf.Min(((WavePlan)(ref plan)).Count, spots.Count); ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogMessage((object)(string.Format("{0} wave {1} in {2} via {3} ", "[AMBUSH]", plan, areaKey, spots[0].SourceId) + $"@ {spots[0].Distance:F0}m (ratio {spots[0].Ratio:F2}).")); } int placed = 0; int resolved = 0; for (int i = 0; i < n; i++) { VerifiedSpot verifiedSpot = spots[i]; string species = ((WavePlan)(ref plan)).Members[i]; Vector3 val2 = ((Component)player).transform.position - verifiedSpot.Position; val2.y = 0f; SpawnHandle val3 = Spawner.Spawn(species, new SpawnOptions { OwnerTag = "dangerousroads", Position = verifiedSpot.Position, Rotation = ((((Vector3)(ref val2)).sqrMagnitude > 0.001f) ? Quaternion.LookRotation(((Vector3)(ref val2)).normalized) : Quaternion.identity) }, (Action)delegate(SpawnHandle handle) { //IL_005e: Unknown result type (might be due to invalid IL or missing references) int num2 = resolved; resolved = num2 + 1; if (handle.IsAlive) { num2 = placed; placed = num2 + 1; FactionBook.ObserveSpawn(handle, species); } else { ManualLogSource log5 = Plugin.Log; if (log5 != null) { log5.LogMessage((object)string.Format("{0} member refused: {1}.", "[AMBUSH]", handle.FailReason)); } } }); if (val3 == null) { int num = resolved; resolved = num + 1; } } float deadline = Time.unscaledTime + 20f; while (resolved < n && Time.unscaledTime < deadline) { yield return null; } LastPlaced = placed; if (placed > 0) { Toasts.Wave(player, ((WavePlan)(ref plan)).PrimarySpecies, placed, spots[0].Position); Finish(WaveOutcome.Placed, onDone); } else { ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogWarning((object)string.Format("{0} wave placed nothing ({1}).", "[AMBUSH]", plan)); } Finish(WaveOutcome.SpawnRefused, onDone); } plan = default(WavePlan); } finally { Running = false; } } private static WavePlan BuildPlan(IReadOnlyList spawnable, List spots, IReadOnlyList recentSpecies, int ownActive, int forcedCount, string forcedSpecies) { //IL_004c: Unknown result type (might be due to invalid IL or missing references) //IL_00ed: Unknown result type (might be due to invalid IL or missing references) if (!string.IsNullOrEmpty(forcedSpecies)) { int num = Mathf.Max(1, forcedCount); List list = new List(num); for (int i = 0; i < num; i++) { list.Add(forcedSpecies); } return new WavePlan(FactionBook.Table.FactionOf(forcedSpecies) ?? "", (IReadOnlyList)list, (WaveOrigin)1, (WaveRefusal)0); } IReadOnlyList readOnlyList = null; for (int j = 0; j < spots.Count; j++) { if (spots[j].ThematicSpecies != null && spots[j].ThematicSpecies.Count > 0) { readOnlyList = spots[j].ThematicSpecies; break; } } int num2 = ((forcedCount > 0) ? forcedCount : DrConfig.MinCount.Value); int num3 = ((forcedCount > 0) ? forcedCount : DrConfig.MaxCount.Value); return WavePlanner.Compose(spawnable, readOnlyList, FactionBook.Table, (double)Random.value, (double)Random.value, num2, num3, ownActive, DrConfig.MaxOwnActive.Value, recentSpecies, 3, spots.Count); } private void Finish(WaveOutcome outcome, Action onDone) { LastOutcome = outcome; onDone?.Invoke(outcome); } internal static List ViewerCenters(Character player) { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) List list = new List(); if ((Object)(object)player != (Object)null) { list.Add(player.CenterPosition); } if (!DrConfig.SightCheckAllPlayers.Value) { return list; } CharacterManager instance = CharacterManager.Instance; if ((Object)(object)instance == (Object)null || instance.Characters == null) { return list; } foreach (Character value in instance.Characters.Values) { if (!((Object)(object)value == (Object)null) && !((Object)(object)value == (Object)(object)player) && !value.IsAI && value.Alive) { list.Add(value.CenterPosition); } } return list; } } } namespace DangerousRoads.Placement { internal sealed class VerifiedSpot { internal Vector3 Position; internal string SourceId; internal string Label; internal RejectReason Reason; internal float Distance = -1f; internal float PathLength = -1f; internal float Ratio = -1f; internal float Score; internal IReadOnlyList ThematicSpecies; internal bool Ok => (int)Reason == 0; public override string ToString() { //IL_005a: Unknown result type (might be due to invalid IL or missing references) return $"{SourceId} {Label} d={Distance:F1} path={PathLength:F1} ratio={Ratio:F2} " + (Ok ? $"ACCEPT score={Score:F2}" : $"reject={Reason}"); } } internal sealed class CandidateVerifier { private readonly NavMeshPath _path = new NavMeshPath(); internal VerifiedSpot Verify(AnchorCandidate cand, Vector3 playerPos, IReadOnlyList viewerCenters, IReadOnlyList accepted) { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0054: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_011b: Unknown result type (might be due to invalid IL or missing references) //IL_011c: 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_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) //IL_0195: Unknown result type (might be due to invalid IL or missing references) //IL_00f6: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Unknown result type (might be due to invalid IL or missing references) VerifiedSpot verifiedSpot = new VerifiedSpot { Position = cand.Position, SourceId = cand.SourceId, Label = cand.Label, ThematicSpecies = cand.ThematicSpecies, Reason = (RejectReason)0 }; float value = DrConfig.MinDistanceMeters.Value; float value2 = DrConfig.MaxDistanceMeters.Value; if (!NavProbe.SnapToWalkable(cand.Position, out var onMesh)) { return Fail(verifiedSpot, (RejectReason)3); } verifiedSpot.Position = onMesh; verifiedSpot.Distance = AnchorUtil.FlatDistance(playerPos, onMesh); if (!BandMath.InBand(verifiedSpot.Distance, value, value2)) { return Fail(verifiedSpot, (RejectReason)1); } float value3 = DrConfig.MemberSpacingMeters.Value; if (accepted != null && value3 > 0f) { for (int i = 0; i < accepted.Count; i++) { if (Vector3.Distance(accepted[i], onMesh) < value3) { return Fail(verifiedSpot, (RejectReason)8); } } } if (DrConfig.RequireOutOfSight.Value && viewerCenters != null) { for (int j = 0; j < viewerCenters.Count; j++) { if (!NavProbe.IsOccludedFrom(viewerCenters[j], onMesh)) { return Fail(verifiedSpot, (RejectReason)2); } } } if (!NavMesh.CalculatePath(playerPos, onMesh, -1, _path)) { return Fail(verifiedSpot, (RejectReason)4); } if ((int)_path.status != 0) { return Fail(verifiedSpot, (RejectReason)5); } verifiedSpot.PathLength = NavProbe.PathLength(_path); verifiedSpot.Ratio = BandMath.Ratio(verifiedSpot.PathLength, verifiedSpot.Distance); if (!BandMath.PathRatioOk(verifiedSpot.PathLength, verifiedSpot.Distance, DrConfig.PathLengthRatioMax.Value)) { return Fail(verifiedSpot, (RejectReason)6); } NavProbe.GroundCorners(onMesh, DrConfig.PlateauProbeRadius.Value, out var d, out var d2, out var d3, out var d4); if (!PlateauRule.Accept(d, d2, d3, d4, 0f)) { return Fail(verifiedSpot, (RejectReason)7); } verifiedSpot.Score = BandMath.Score(verifiedSpot.Distance, verifiedSpot.Ratio, value, value2); return verifiedSpot; } private static VerifiedSpot Fail(VerifiedSpot v, RejectReason why) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0002: Unknown result type (might be due to invalid IL or missing references) v.Reason = why; return v; } } internal static class NavProbe { internal const int WalkableMask = 1; internal const int AllAreasMask = -1; internal const float SampleRadius = 10f; internal const float GroundProbeLength = 1.6f; internal static bool SnapToWalkable(Vector3 raw, out Vector3 onMesh) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) //IL_0021: Unknown result type (might be due to invalid IL or missing references) //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) NavMeshHit val = default(NavMeshHit); if (NavMesh.SamplePosition(raw, ref val, 10f, 1)) { onMesh = ((NavMeshHit)(ref val)).position; return true; } onMesh = raw; return false; } internal static bool CanWalk(Vector3 from, Vector3 to, NavMeshPath path) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0012: Invalid comparison between Unknown and I4 if (NavMesh.CalculatePath(from, to, -1, path)) { return (int)path.status == 0; } return false; } internal static float PathLength(NavMeshPath path) { //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) if (path == null || path.corners == null || path.corners.Length < 2) { return -1f; } float num = 0f; Vector3[] corners = path.corners; for (int i = 1; i < corners.Length; i++) { num += Vector3.Distance(corners[i - 1], corners[i]); } return num; } internal static bool IsOccludedFrom(Vector3 viewerCenter, Vector3 candidate) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0015: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_0017: 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_0026: Unknown result type (might be due to invalid IL or missing references) //IL_002b: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0047: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) Vector3 val = viewerCenter + Vector3.up * 2.5f; Vector3 val2 = candidate + Vector3.up * 3f; Vector3 val3 = val2 - val; float magnitude = ((Vector3)(ref val3)).magnitude; if (magnitude < 0.01f) { return false; } return Physics.SphereCast(new Ray(val, val3 / magnitude), 1f, magnitude, Global.LargeEnvironmentMask); } internal static float GroundDistanceAt(Vector3 corner) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_000d: Unknown result type (might be due to invalid IL or missing references) Vector3 val = corner + Vector3.up; RaycastHit val2 = default(RaycastHit); if (!Physics.Raycast(val, Vector3.down, ref val2, 1.6f, Global.LargeEnvironmentMask)) { return -1f; } return ((RaycastHit)(ref val2)).distance; } internal static void GroundCorners(Vector3 at, float radius, out float d0, out float d1, out float d2, out float d3) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Unknown result type (might be due to invalid IL or missing references) //IL_0052: Unknown result type (might be due to invalid IL or missing references) //IL_005a: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Unknown result type (might be due to invalid IL or missing references) d0 = GroundDistanceAt(at + new Vector3(0f - radius, 0f, 0f - radius)); d1 = GroundDistanceAt(at + new Vector3(0f - radius, 0f, radius)); d2 = GroundDistanceAt(at + new Vector3(radius, 0f, 0f - radius)); d3 = GroundDistanceAt(at + new Vector3(radius, 0f, radius)); } } } namespace DangerousRoads.Anchors { internal sealed class AnchorRegistry { private readonly Dictionary _byId = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly List _known = new List(); private readonly CandidateVerifier _verifier = new CandidateVerifier(); private readonly List _raw = new List(); internal MeasureLedger Ledger { get; } = new MeasureLedger(); internal IReadOnlyList KnownSourceIds => _known; internal AnchorRegistry() { //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_003c: Expected O, but got Unknown Add(new LiveAiSource()); Add(new GatherPointSource()); Add(new SquadPointSource()); Add(new InteractableSource()); Add(new ProceduralSource()); } private void Add(IAnchorSource source) { _byId[source.Id] = source; _known.Add(source.Id); Ledger.RegisterSource(source.Id); } internal IAnchorSource Get(string id) { if (!_byId.TryGetValue(id ?? "", out var value)) { return null; } return value; } internal void OnSceneChanged(string sceneName) { for (int i = 0; i < _known.Count; i++) { _byId[_known[i]].OnSceneChanged(sceneName); } } internal List ActiveOrder() { List list = default(List); List result = SourceOrder.Parse(DrConfig.SourceOrder.Value, (IReadOnlyList)_known, ref list); if (list.Count > 0) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[ANCHOR] [Placement] SourceOrder names unknown source(s): " + string.Join(", ", list.ToArray()) + ". Known: " + string.Join(", ", _known.ToArray()) + ".")); } } return result; } internal List FindSpots(Vector3 playerPos, IReadOnlyList viewerCenters, int want, string areaKey, List auditInto = null) { //IL_007b: Unknown result type (might be due to invalid IL or missing references) //IL_00f9: Unknown result type (might be due to invalid IL or missing references) //IL_0114: Unknown result type (might be due to invalid IL or missing references) //IL_0172: Unknown result type (might be due to invalid IL or missing references) List list = new List(); List list2 = new List(); if (want <= 0) { return list; } int num = Mathf.Max(1, DrConfig.MaxCandidatesPerWave.Value); float value = DrConfig.MinDistanceMeters.Value; float value2 = DrConfig.MaxDistanceMeters.Value; bool value3 = DrConfig.LogVerbose.Value; List list3 = ActiveOrder(); for (int i = 0; i < list3.Count; i++) { if (list.Count >= want) { break; } if (num <= 0) { break; } IAnchorSource anchorSource = Get(list3[i]); if (anchorSource == null) { continue; } _raw.Clear(); if (anchorSource.Collect(playerPos, value, value2, num, _raw) == 0) { Ledger.SourceEmpty(areaKey, anchorSource.Id); if (value3) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)("[ANCHOR] " + anchorSource.Id + ": no candidates in band.")); } } continue; } for (int j = 0; j < _raw.Count; j++) { if (list.Count >= want) { break; } if (num <= 0) { break; } num--; VerifiedSpot verifiedSpot = _verifier.Verify(_raw[j], playerPos, viewerCenters, list2); Ledger.Candidate(areaKey, anchorSource.Id, verifiedSpot.Reason, verifiedSpot.Distance, verifiedSpot.Ratio); auditInto?.Add(verifiedSpot); if (value3) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogMessage((object)string.Format("{0} {1}", "[ANCHOR]", verifiedSpot)); } } if (verifiedSpot.Ok) { list.Add(verifiedSpot); list2.Add(verifiedSpot.Position); } } } list.Sort((VerifiedSpot a, VerifiedSpot b) => b.Score.CompareTo(a.Score)); return list; } internal List FindCluster(Vector3 playerPos, IReadOnlyList viewerCenters, int want, string areaKey, List auditInto = null) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00bb: Unknown result type (might be due to invalid IL or missing references) //IL_00c0: Unknown result type (might be due to invalid IL or missing references) //IL_0110: Unknown result type (might be due to invalid IL or missing references) //IL_0130: Unknown result type (might be due to invalid IL or missing references) //IL_0196: Unknown result type (might be due to invalid IL or missing references) List list = new List(); if (want <= 0) { return list; } List list2 = FindSpots(playerPos, viewerCenters, 1, areaKey, auditInto); if (list2.Count == 0) { return list; } list.Add(list2[0]); if (want == 1) { return list; } float num = Mathf.Max(1f, DrConfig.ClusterRadiusMeters.Value); List list3 = new List { list2[0].Position }; int num2 = want - 1; IReadOnlyList<(float, float)> readOnlyList = ClusterPlan.Offsets(num2 * 2, num, (double)Random.value); for (int i = 0; i < readOnlyList.Count; i++) { if (list.Count >= want) { break; } AnchorCandidate cand = new AnchorCandidate { Position = list2[0].Position + new Vector3(readOnlyList[i].Item1, 0f, readOnlyList[i].Item2), SourceId = list2[0].SourceId, Label = $"cluster+{i} r={num:F0}", ThematicSpecies = list2[0].ThematicSpecies }; VerifiedSpot verifiedSpot = _verifier.Verify(cand, playerPos, viewerCenters, list3); Ledger.Candidate(areaKey, list2[0].SourceId, verifiedSpot.Reason, verifiedSpot.Distance, verifiedSpot.Ratio); auditInto?.Add(verifiedSpot); if (DrConfig.LogVerbose.Value) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)string.Format("{0} {1}", "[ANCHOR]", verifiedSpot)); } } if (verifiedSpot.Ok) { list.Add(verifiedSpot); list3.Add(verifiedSpot.Position); } } if (list.Count < want) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogMessage((object)(string.Format("{0} cluster shrank to {1}/{2} — the ground around the ", "[ANCHOR]", list.Count, want) + "lead would not take the rest. Better a small group than a scattered one.")); } } return list; } } internal static class AnchorUtil { internal static float FlatDistance(Vector3 a, Vector3 b) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000e: Unknown result type (might be due to invalid IL or missing references) //IL_0014: Unknown result type (might be due to invalid IL or missing references) float num = a.x - b.x; float num2 = a.z - b.z; return Mathf.Sqrt(num * num + num2 * num2); } internal static bool InBand(Vector3 origin, Vector3 candidate, float minDist, float maxDist) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0001: Unknown result type (might be due to invalid IL or missing references) float num = FlatDistance(origin, candidate); if (num >= minDist) { return num <= maxDist; } return false; } internal static void ShuffleFrom(List list, int startIndex) { for (int num = list.Count - 1; num > startIndex; num--) { int num2 = Random.Range(startIndex, num + 1); if (num2 != num) { AnchorCandidate value = list[num]; list[num] = list[num2]; list[num2] = value; } } } internal static int CollectFrom(IReadOnlyList items, Vector3 origin, float minDist, float maxDist, int budget, List into, Func positionOf, Func make) { //IL_0034: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Unknown result type (might be due to invalid IL or missing references) if (items == null || into == null || budget <= 0) { return 0; } int count = into.Count; int num = 0; for (int i = 0; i < items.Count; i++) { if (num >= budget) { break; } Vector3? val = positionOf(items[i]); if (val.HasValue && InBand(origin, val.Value, minDist, maxDist)) { into.Add(make(items[i], i, val.Value)); num++; } } ShuffleFrom(into, count); return num; } } internal sealed class GatherPointSource : IAnchorSource { internal const string SourceId = "gatherpoints"; private readonly List _points = new List(); private bool _scanned; public string Id => "gatherpoints"; public bool Available => _points.Count > 0; internal int PointCount => _points.Count; public void OnSceneChanged(string sceneName) { _points.Clear(); _scanned = false; } public int Collect(Vector3 origin, float minDist, float maxDist, int budget, List into) { //IL_000c: Unknown result type (might be due to invalid IL or missing references) EnsureScanned(); return AnchorUtil.CollectFrom(_points, origin, minDist, maxDist, budget, into, (Transform t) => (!((Object)(object)t == (Object)null)) ? new Vector3?(t.position) : ((Vector3?)null), (Transform t, int i, Vector3 pos) => new AnchorCandidate { Position = pos, SourceId = "gatherpoints", Label = $"treeaccess#{i}", ThematicSpecies = null }); } private void EnsureScanned() { if (_scanned) { return; } _scanned = true; Dictionary instantiatedCentralGatherables = CentralGatherable.InstantiatedCentralGatherables; if (instantiatedCentralGatherables == null) { return; } foreach (KeyValuePair item in instantiatedCentralGatherables) { CentralGatherable value = item.Value; if ((Object)(object)value == (Object)null || value.AccessPoints == null) { continue; } for (int i = 0; i < value.AccessPoints.Count; i++) { CentralGatherableAccessPoint val = value.AccessPoints[i]; if ((Object)(object)val != (Object)null) { _points.Add(((Component)val).transform); } } } ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)string.Format("{0} {1}: {2} tree access point(s) in this scene.", "[ANCHOR]", "gatherpoints", _points.Count)); } } } internal sealed class AnchorCandidate { internal Vector3 Position; internal string SourceId; internal string Label; internal IReadOnlyList ThematicSpecies; } internal interface IAnchorSource { string Id { get; } bool Available { get; } void OnSceneChanged(string sceneName); int Collect(Vector3 origin, float minDist, float maxDist, int budget, List into); } internal sealed class InteractableSource : IAnchorSource { internal const string SourceId = "interactables"; private readonly List _points = new List(); private bool _scanned; public string Id => "interactables"; public bool Available => _points.Count > 0; internal int PointCount => _points.Count; public void OnSceneChanged(string sceneName) { _points.Clear(); _scanned = false; } public int Collect(Vector3 origin, float minDist, float maxDist, int budget, List into) { //IL_001a: Unknown result type (might be due to invalid IL or missing references) if (!DrConfig.EnableInteractableAnchors.Value) { return 0; } EnsureScanned(); return AnchorUtil.CollectFrom(_points, origin, minDist, maxDist, budget, into, (Transform t) => (!((Object)(object)t == (Object)null)) ? new Vector3?(t.position) : ((Vector3?)null), (Transform t, int i, Vector3 pos) => new AnchorCandidate { Position = pos, SourceId = "interactables", Label = $"interactable#{i} '{((Object)t).name}'", ThematicSpecies = null }); } private void EnsureScanned() { if (_scanned) { return; } _scanned = true; ItemManager instance = ItemManager.Instance; if ((Object)(object)instance == (Object)null || instance.WorldItems == null) { return; } int num = 0; int num2 = 0; foreach (Item value in instance.WorldItems.Values) { if ((Object)(object)value == (Object)null || value is CentralGatherable) { continue; } if (value is Gatherable) { _points.Add(((Component)value).transform); num++; continue; } ItemContainer val = (ItemContainer)(object)((value is ItemContainer) ? value : null); if (val != null && IsWorldChest(val)) { _points.Add(((Component)value).transform); num2++; } } ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)(string.Format("{0} {1}: {2} anchor(s) ", "[ANCHOR]", "interactables", _points.Count) + $"({num} gatherable, {num2} container) in this scene.")); } } private static bool IsWorldChest(ItemContainer c) { if (c is ItemContainerStatic) { return false; } if (c is MerchantPouch) { return false; } if (c is SingleItemContainer) { return false; } if (c is GroupContainer) { return false; } if (c is CookingUstensil) { return false; } if (c is FueledContainer) { return false; } if (((Item)c).IsChildToCharacter) { return false; } return true; } } internal sealed class LiveAiSource : IAnchorSource { internal const string SourceId = "liveai"; private List _scratch = new List(); public string Id => "liveai"; public bool Available => (Object)(object)CharacterManager.Instance != (Object)null; public void OnSceneChanged(string sceneName) { } public int Collect(Vector3 origin, float minDist, float maxDist, int budget, List into) { //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) if (budget <= 0) { return 0; } CharacterManager instance = CharacterManager.Instance; if ((Object)(object)instance == (Object)null) { return 0; } _scratch.Clear(); instance.FindCharactersInRange(origin, maxDist, ref _scratch); return AnchorUtil.CollectFrom(_scratch, origin, minDist, maxDist, budget, into, (Character ch) => (!IsUsableAnchor(ch)) ? ((Vector3?)null) : new Vector3?(((Component)ch).transform.position), (Character ch, int i, Vector3 pos) => new AnchorCandidate { Position = pos, SourceId = "liveai", Label = "liveai '" + ch.Name + "'", ThematicSpecies = new List { ch.Name } }); } internal static bool IsUsableAnchor(Character ch) { //IL_001c: Unknown result type (might be due to invalid IL or missing references) //IL_0022: Invalid comparison between Unknown and I4 //IL_0027: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) if ((Object)(object)ch == (Object)null || !ch.IsAI || !ch.Alive) { return false; } if ((int)ch.Faction == 1) { return false; } UID uID = ch.UID; if (SpawnUid.IsSpawnUid(((UID)(ref uID)).Value)) { return false; } return true; } } internal sealed class ProceduralSource : IAnchorSource { internal const string SourceId = "procedural"; private const int Attempts = 16; private readonly NavMeshPath _path = new NavMeshPath(); public string Id => "procedural"; public bool Available => true; public void OnSceneChanged(string sceneName) { } public int Collect(Vector3 origin, float minDist, float maxDist, int budget, List into) { //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) if (budget <= 0) { return 0; } int count = into.Count; int num = 0; for (int i = 0; i < 16; i++) { if (num >= budget) { break; } if (TrySample(origin, minDist, maxDist, out var result, out var pathLength)) { into.Add(new AnchorCandidate { Position = result, SourceId = "procedural", Label = $"procedural#{i} pathLen={pathLength:F1}", ThematicSpecies = null }); num++; } } AnchorUtil.ShuffleFrom(into, count); return num; } private bool TrySample(Vector3 origin, float minDist, float maxDist, out Vector3 result, out float pathLength) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Unknown result type (might be due to invalid IL or missing references) //IL_0063: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_0071: Unknown result type (might be due to invalid IL or missing references) //IL_0076: Unknown result type (might be due to invalid IL or missing references) //IL_00ad: Unknown result type (might be due to invalid IL or missing references) //IL_00b2: Unknown result type (might be due to invalid IL or missing references) //IL_00c5: 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) result = origin; pathLength = -1f; float num = Random.value * (float)Math.PI * 2f; float num2 = Mathf.Sqrt(Random.value); float num3 = minDist + (maxDist - minDist) * num2; Vector3 raw = origin + new Vector3(Mathf.Cos(num) * num3, 0f, Mathf.Sin(num) * num3); if (!NavProbe.SnapToWalkable(raw, out var onMesh)) { return false; } NavMeshHit val = default(NavMeshHit); if (NavMesh.Raycast(origin, onMesh, ref val, 1)) { onMesh = ((NavMeshHit)(ref val)).position; } if (!NavMesh.CalculatePath(origin, onMesh, 1, _path)) { return false; } Vector3[] corners = _path.corners; if (corners == null || corners.Length == 0) { return false; } result = corners[^1]; pathLength = NavProbe.PathLength(_path); return AnchorUtil.FlatDistance(origin, result) >= minDist; } } internal sealed class SquadPointSource : IAnchorSource { internal const string SourceId = "squadpoints"; private AISquadSpawnPoint[] _points; private string _foundVia = "(not scanned)"; public string Id => "squadpoints"; public bool Available { get { if (_points != null) { return _points.Length != 0; } return false; } } internal int PointCount { get { AISquadSpawnPoint[] points = _points; if (points == null) { return 0; } return points.Length; } } internal string FoundVia => _foundVia; internal AISquadSpawnPoint[] Points => (AISquadSpawnPoint[])(((object)_points) ?? ((object)new AISquadSpawnPoint[0])); public void OnSceneChanged(string sceneName) { _points = null; _foundVia = "(none)"; AISquadManager instance = AISquadManager.Instance; if ((Object)(object)instance == (Object)null) { _foundVia = "(no AISquadManager at scene load)"; ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[ANCHOR] squadpoints: no AISquadManager in '" + sceneName + "' — this source is inert for the whole region (its ledger row will read SourceEmpty).")); } return; } Transform val = ((Component)instance).transform.Find("SquadSpawnPoints"); if ((Object)(object)val != (Object)null) { _points = ((Component)val).GetComponentsInChildren(true); _foundVia = "SquadSpawnPoints child"; } else { _points = ((Component)instance).GetComponentsInChildren(true); _foundVia = "AISquadManager subtree (no 'SquadSpawnPoints' child)"; } ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogMessage((object)(string.Format("{0} {1}: {2} squad spawn point(s) in this scene ", "[ANCHOR]", "squadpoints", PointCount) + "(found via " + _foundVia + ").")); } } public int Collect(Vector3 origin, float minDist, float maxDist, int budget, List into) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) return AnchorUtil.CollectFrom(_points, origin, minDist, maxDist, budget, into, (AISquadSpawnPoint p) => (!((Object)(object)p == (Object)null)) ? new Vector3?(((Component)p).transform.position) : ((Vector3?)null), (AISquadSpawnPoint p, int i, Vector3 pos) => new AnchorCandidate { Position = pos, SourceId = "squadpoints", Label = $"squadpoint#{i} typeID={p.SquadSpawnTypeID} valid={SafeCheckValid(p)}", ThematicSpecies = SpeciesAt(p) }); } internal static string SafeCheckValid(AISquadSpawnPoint p) { try { return p.CheckValidSpawn().ToString(); } catch { return "?"; } } internal static List SpeciesAt(AISquadSpawnPoint p) { List list = new List(); AISquad[] squads = p.Squads; if (squads == null) { return list; } foreach (AISquad val in squads) { if ((Object)(object)val == (Object)null) { continue; } List members = val.Members; if (members == null) { continue; } for (int j = 0; j < members.Count; j++) { AISquadMember obj = members[j]; Character val2 = ((obj != null) ? obj.Character : null); string text = ((val2 != null) ? val2.Name : null); if (!string.IsNullOrEmpty(text) && !list.Contains(text)) { list.Add(text); } } } return list; } } }