Decompiled source of DangerousRoads v0.1.13
plugins/DangerousRoads.Core.dll
Decompiled 2 weeks ago
The result has been truncated due to the large size, download it to view full contents!
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.Text; using Microsoft.CodeAnalysis; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")] [assembly: AssemblyCompany("DangerousRoads.Core")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.13.0")] [assembly: AssemblyInformationalVersion("0.1.13+06e79d2edf3a19848f2e4105dddc16417cd2c1fe")] [assembly: AssemblyProduct("DangerousRoads.Core")] [assembly: AssemblyTitle("DangerousRoads.Core")] [assembly: AssemblyMetadata("BuildStamp", "06e79d2e 2026-09-05")] [assembly: AssemblyVersion("0.0.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] internal sealed class IsReadOnlyAttribute : Attribute { } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace DangerousRoads.Core { public readonly struct ClockState { public readonly bool Armed; public readonly double DueAt; public static readonly ClockState Disarmed = new ClockState(armed: false, 0.0); public ClockState(bool armed, double dueAt) { Armed = armed; DueAt = dueAt; } public override string ToString() { if (!Armed) { return "disarmed"; } return $"armed@{DueAt:F1}"; } } public static class AmbushClock { public static float NextDelay(double roll01, float min, float max) { if (min < 0f) { min = 0f; } if (max < 0f) { max = 0f; } if (max < min) { max = min; } if (roll01 < 0.0) { roll01 = 0.0; } if (roll01 > 1.0) { roll01 = 1.0; } return min + (float)((double)(max - min) * roll01); } public static ClockState Arm(double now, float delay) { return new ClockState(armed: true, now + (double)((delay < 0f) ? 0f : delay)); } public static bool IsDue(ClockState state, double now) { if (state.Armed) { return now >= state.DueAt; } return false; } public static ClockState AfterWave(double now, float cooldownFloor, float rolledDelay) { return Arm(now, Math.Max(cooldownFloor, rolledDelay)); } public static ClockState Retry(double now, float retrySeconds) { return Arm(now, retrySeconds); } } public static class BandMath { public const float MinCrowFlies = 0.5f; public static bool InBand(float distance, float min, float max) { if (distance >= min) { return distance <= max; } return false; } public static float Ratio(float pathLength, float crowFlies) { if (crowFlies < 0.5f) { return -1f; } if (pathLength < 0f) { return -1f; } return pathLength / crowFlies; } public static bool PathRatioOk(float pathLength, float crowFlies, float k) { if (k <= 0f) { return false; } float num = Ratio(pathLength, crowFlies); if (num < 0f) { return false; } return num <= k; } public static float Score(float distance, float ratio, float bandMin, float bandMax) { float num = bandMax - bandMin; float num2 = ((num <= 0f) ? 0f : ((distance - bandMin) / num)); if (num2 < 0f) { num2 = 0f; } if (num2 > 1f) { num2 = 1f; } float num3 = 1f / Math.Max(1f, (ratio < 0f) ? float.MaxValue : ratio); return 0.6f * num2 + 0.4f * num3; } } public static class Bearing { private static readonly string[] Labels = new string[8] { "north", "north-east", "east", "south-east", "south", "south-west", "west", "north-west" }; public static int Sector(float dx, float dz, float minDistance = 0.5f) { if (dx * dx + dz * dz < minDistance * minDistance) { return -1; } double num = Math.Atan2(dx, dz) * 180.0 / Math.PI; if (num < 0.0) { num += 360.0; } return (int)Math.Round(num / 45.0) % 8; } public static string Label(float dx, float dz, float minDistance = 0.5f) { int num = Sector(dx, dz, minDistance); if (num >= 0) { return Labels[num]; } return ""; } } public sealed class BlockCounters { private readonly Dictionary<string, int> _counts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase); private readonly List<string> _order = new List<string>(); public int Total { get; private set; } public string Last { get; private set; } = "(none)"; public void Record(string reason) { string text = (Last = (string.IsNullOrWhiteSpace(reason) ? "(unknown)" : reason.Trim())); Total++; if (_counts.TryGetValue(text, out var value)) { _counts[text] = value + 1; return; } _counts[text] = 1; _order.Add(text); } public void Reset() { _counts.Clear(); _order.Clear(); Total = 0; Last = "(none)"; } public int CountOf(string reason) { if (!_counts.TryGetValue(reason ?? "", out var value)) { return 0; } return value; } public string Format() { if (_counts.Count == 0) { return "none"; } List<string> list = new List<string>(_order); list.Sort(delegate(string a, string b) { int num2 = _counts[b].CompareTo(_counts[a]); return (num2 == 0) ? _order.IndexOf(a).CompareTo(_order.IndexOf(b)) : num2; }); StringBuilder stringBuilder = new StringBuilder(); for (int num = 0; num < list.Count; num++) { if (num > 0) { stringBuilder.Append(' '); } stringBuilder.Append(list[num]).Append('=').Append(_counts[list[num]].ToString(CultureInfo.InvariantCulture)); } return stringBuilder.ToString(); } } public readonly struct CardArgs { private readonly Dictionary<string, string> _map; private readonly List<string> _positional; public static readonly CardArgs Empty = new CardArgs(null, null); public int Count { get { if (_map == null) { return 0; } return _map.Count; } } public IReadOnlyList<string> Positional { get { IReadOnlyList<string> positional = _positional; return positional ?? Array.Empty<string>(); } } private CardArgs(Dictionary<string, string> map, List<string> positional) { _map = map; _positional = positional; } public static CardArgs Parse(IEnumerable<string> tokens) { if (tokens == null) { return Empty; } Dictionary<string, string> dictionary = null; List<string> list = null; foreach (string token in tokens) { if (string.IsNullOrEmpty(token)) { continue; } string text = token.Trim(); if (text.Length == 0) { continue; } int num = text.IndexOf('='); if (num <= 0) { (list ?? (list = new List<string>())).Add(text); continue; } string text2 = text.Substring(0, num).Trim(); string value = text.Substring(num + 1).Trim(); if (text2.Length == 0) { (list ?? (list = new List<string>())).Add(text); } else { (dictionary ?? (dictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)))[text2] = value; } } if (dictionary != null || list != null) { return new CardArgs(dictionary, list); } return Empty; } public bool Has(string key) { if (key != null && _map != null) { return _map.ContainsKey(key); } return false; } public string String(string key, string fallback) { if (key == null || _map == null || !_map.TryGetValue(key, out string value) || value.Length <= 0) { return fallback; } return value; } public float Float(string key, float fallback) { if (_map == null || key == null || !_map.TryGetValue(key, out string value) || !float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var result) || float.IsNaN(result) || float.IsInfinity(result)) { return fallback; } return result; } public int Int(string key, int fallback) { if (_map == null || key == null || !_map.TryGetValue(key, out string value) || !int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result)) { return fallback; } return result; } public bool Bool(string key, bool fallback) { if (_map == null || key == null || !_map.TryGetValue(key, out string value)) { return fallback; } switch (value.ToLowerInvariant()) { case "yes": case "on": case "1": case "true": return true; case "off": case "no": case "0": case "false": return false; default: return fallback; } } public List<string> Unknown(IEnumerable<string> known) { List<string> list = new List<string>(); if (_map == null) { return list; } HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); if (known != null) { foreach (string item in known) { if (item != null) { hashSet.Add(item); } } } foreach (string key in _map.Keys) { if (!hashSet.Contains(key)) { list.Add(key); } } list.Sort(StringComparer.OrdinalIgnoreCase); return list; } public string Describe() { if (_map == null && _positional == null) { return "(no args)"; } StringBuilder stringBuilder = new StringBuilder(); if (_map != null) { List<string> list = new List<string>(_map.Keys); list.Sort(StringComparer.OrdinalIgnoreCase); foreach (string item in list) { if (stringBuilder.Length > 0) { stringBuilder.Append(' '); } stringBuilder.Append(item).Append('=').Append(_map[item]); } } if (_positional != null) { foreach (string item2 in _positional) { if (stringBuilder.Length > 0) { stringBuilder.Append(' '); } stringBuilder.Append(item2); } } return stringBuilder.ToString(); } public override string ToString() { return Describe(); } } public interface ICardCondition { bool IsMet(WorldSnapshot world); string Describe(); } public sealed class HourWindow : ICardCondition { public float From { get; } public float To { get; } public HourWindow(float fromHour, float toHour) { From = Wrap(fromHour); To = Wrap(toHour); } private static float Wrap(float h) { return (h % 24f + 24f) % 24f; } public bool IsMet(WorldSnapshot world) { if (world == null) { return false; } float hourOfDay = world.HourOfDay; if (From == To) { return true; } if (!(From < To)) { if (!(hourOfDay >= From)) { return hourOfDay < To; } return true; } if (hourOfDay >= From) { return hourOfDay < To; } return false; } public string Describe() { return $"hour={From:0.##}-{To:0.##}"; } } public sealed class NightCondition : ICardCondition { public static readonly NightCondition Night = new NightCondition(wantNight: true); public static readonly NightCondition Day = new NightCondition(wantNight: false); public bool WantNight { get; } private NightCondition(bool wantNight) { WantNight = wantNight; } public bool IsMet(WorldSnapshot world) { if (world != null) { return world.IsNight == WantNight; } return false; } public string Describe() { if (!WantNight) { return "day"; } return "night"; } } public sealed class RainCondition : ICardCondition { public float Threshold { get; } public bool AtLeast { get; } public RainCondition(float threshold01, bool atLeast) { Threshold = ((threshold01 < 0f) ? 0f : ((threshold01 > 1f) ? 1f : threshold01)); AtLeast = atLeast; } public bool IsMet(WorldSnapshot world) { if (world != null) { if (!AtLeast) { return world.Rain01 < Threshold; } return world.Rain01 >= Threshold; } return false; } public string Describe() { return "rain" + (AtLeast ? ">" : "<") + Threshold.ToString("0.##", CultureInfo.InvariantCulture); } } public sealed class QuestFlagCondition : ICardCondition { public string Flag { get; } public bool Present { get; } public QuestFlagCondition(string flag, bool present) { Flag = (flag ?? "").Trim(); Present = present; } public bool IsMet(WorldSnapshot world) { if (world != null) { return world.HasFlag(Flag) == Present; } return false; } public string Describe() { return (Present ? "" : "!") + "flag:" + Flag; } } public sealed class FactionCondition : ICardCondition { public string Faction { get; } public FactionCondition(string faction) { Faction = (faction ?? "").Trim(); } public bool IsMet(WorldSnapshot world) { if (world != null && Faction.Length > 0) { return world.PlayerFaction.IndexOf(Faction, StringComparison.OrdinalIgnoreCase) >= 0; } return false; } public string Describe() { return "faction:" + Faction; } } public sealed class RegionCondition : ICardCondition { public string Region { get; } public RegionCondition(string region) { Region = (region ?? "").Trim(); } public bool IsMet(WorldSnapshot world) { if (world != null && Region.Length > 0) { return world.Region.IndexOf(Region, StringComparison.OrdinalIgnoreCase) >= 0; } return false; } public string Describe() { return "region:" + Region; } } public sealed class RoomWarmCondition : ICardCondition { public IReadOnlyList<string> Species { get; } public RoomWarmCondition(params string[] speciesKeys) : this((IEnumerable<string>)speciesKeys) { } public RoomWarmCondition(IEnumerable<string> speciesKeys) { List<string> list = new List<string>(); if (speciesKeys != null) { foreach (string speciesKey in speciesKeys) { string text = (speciesKey ?? "").Trim(); if (text.Length > 0) { list.Add(text); } } } Species = list; } public bool IsMet(WorldSnapshot world) { if (world == null) { return false; } if (Species.Count == 0) { return true; } for (int i = 0; i < Species.Count; i++) { if (world.IsRoomWarm(Species[i])) { return true; } } return false; } public string Describe() { return "roomwarm:" + string.Join(",", ToArray()); } private string[] ToArray() { string[] array = new string[Species.Count]; for (int i = 0; i < Species.Count; i++) { array[i] = Species[i]; } return array; } } public sealed class ZoneCondition : ICardCondition { public static readonly ZoneCondition Overworld = new ZoneCondition(ZoneKind.Overworld); public static readonly ZoneCondition City = new ZoneCondition(ZoneKind.City); public static readonly ZoneCondition Dungeon = new ZoneCondition(ZoneKind.Dungeon); public ZoneKind Zone { get; } private ZoneCondition(ZoneKind zone) { Zone = zone; } public bool IsMet(WorldSnapshot world) { if (world != null) { return world.Zone == Zone; } return false; } public string Describe() { return "zone:" + Zone.ToString().ToLowerInvariant(); } public static bool TryFor(string name, out ZoneCondition zone) { switch ((name ?? "").Trim().ToLowerInvariant()) { case "overworld": zone = Overworld; return true; case "city": case "town": zone = City; return true; case "dungeon": zone = Dungeon; return true; default: zone = null; return false; } } } public sealed class AllOf : ICardCondition { public IReadOnlyList<ICardCondition> Parts { get; } public AllOf(IEnumerable<ICardCondition> parts) { Parts = Clean(parts); } public AllOf(params ICardCondition[] parts) : this((IEnumerable<ICardCondition>)parts) { } public bool IsMet(WorldSnapshot world) { for (int i = 0; i < Parts.Count; i++) { if (!Parts[i].IsMet(world)) { return false; } } return true; } public string Describe() { return Join(Parts, " & "); } internal static List<ICardCondition> Clean(IEnumerable<ICardCondition> parts) { List<ICardCondition> list = new List<ICardCondition>(); if (parts != null) { foreach (ICardCondition part in parts) { if (part != null) { list.Add(part); } } } return list; } internal static string Join(IReadOnlyList<ICardCondition> parts, string sep) { StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < parts.Count; i++) { if (i > 0) { stringBuilder.Append(sep); } string text = parts[i].Describe(); stringBuilder.Append((parts[i] is AnyOf && parts.Count > 1) ? ("(" + text + ")") : text); } return stringBuilder.ToString(); } } public sealed class AnyOf : ICardCondition { public IReadOnlyList<ICardCondition> Parts { get; } public AnyOf(IEnumerable<ICardCondition> parts) { Parts = AllOf.Clean(parts); } public AnyOf(params ICardCondition[] parts) : this((IEnumerable<ICardCondition>)parts) { } public bool IsMet(WorldSnapshot world) { if (Parts.Count == 0) { return true; } for (int i = 0; i < Parts.Count; i++) { if (Parts[i].IsMet(world)) { return true; } } return false; } public string Describe() { return AllOf.Join(Parts, " | "); } } public sealed class Not : ICardCondition { public ICardCondition Inner { get; } public Not(ICardCondition inner) { Inner = inner ?? Always.Instance; } public bool IsMet(WorldSnapshot world) { return !Inner.IsMet(world); } public string Describe() { string text = Inner.Describe(); if (!(Inner is AllOf) && !(Inner is AnyOf)) { return "!" + text; } return "!(" + text + ")"; } } public sealed class Always : ICardCondition { public static readonly Always Instance = new Always(); private Always() { } public bool IsMet(WorldSnapshot world) { return true; } public string Describe() { return "always"; } } public static class CardCondition { public static ICardCondition Parse(string text) { if (!TryParse(text, out ICardCondition condition, out string error)) { throw new FormatException(error); } return condition; } public static bool TryParse(string text, out ICardCondition condition, out string error) { condition = Always.Instance; error = null; if (text == null || text.Trim().Length == 0) { return true; } List<ICardCondition> list = new List<ICardCondition>(); string[] array = text.Split(new char[1] { '&' }); foreach (string obj in array) { List<ICardCondition> list2 = new List<ICardCondition>(); string[] array2 = obj.Split(new char[1] { '|' }); for (int j = 0; j < array2.Length; j++) { string text2 = array2[j].Trim(); if (text2.Length == 0) { error = "empty term in '" + text + "'"; return false; } bool flag = text2[0] == '!'; if (flag) { text2 = text2.Substring(1).Trim(); } if (!TryLeaf(text2, out ICardCondition leaf, out error)) { return false; } ICardCondition item; if (!flag) { item = leaf; } else { ICardCondition cardCondition = new Not(leaf); item = cardCondition; } list2.Add(item); } ICardCondition item2; if (list2.Count != 1) { ICardCondition cardCondition = new AnyOf(list2); item2 = cardCondition; } else { item2 = list2[0]; } list.Add(item2); } ICardCondition cardCondition2; if (list.Count != 1) { ICardCondition cardCondition = new AllOf(list); cardCondition2 = cardCondition; } else { cardCondition2 = list[0]; } condition = cardCondition2; return true; } private static bool TryLeaf(string atom, out ICardCondition leaf, out string error) { leaf = null; error = null; atom = atom.Trim(); string text = atom.Replace(" ", "").Replace("\t", ""); string text2 = text.ToLowerInvariant(); switch (text2) { case "always": leaf = Always.Instance; return true; case "night": leaf = NightCondition.Night; return true; case "day": leaf = NightCondition.Day; return true; default: if (text2.StartsWith("hour=", StringComparison.Ordinal)) { string[] array = text.Substring(5).Split(new char[1] { '-' }); if (array.Length == 2 && TryNum(array[0], out var v) && TryNum(array[1], out var v2) && v >= 0f && v <= 24f && v2 >= 0f && v2 <= 24f) { leaf = new HourWindow(v, v2); return true; } error = "'" + atom + "': expected hour=<0-24>-<0-24>"; return false; } if (text2.StartsWith("rain>", StringComparison.Ordinal) || text2.StartsWith("rain<", StringComparison.Ordinal)) { if (TryNum(text.Substring(5), out var v3) && v3 >= 0f && v3 <= 1f) { leaf = new RainCondition(v3, text[4] == '>'); return true; } error = "'" + atom + "': expected rain>X or rain<X with X in 0..1"; return false; } if (text2.StartsWith("zone:", StringComparison.Ordinal)) { if (ZoneCondition.TryFor(atom.Substring(5), out ZoneCondition zone)) { leaf = zone; return true; } error = "'" + atom + "': expected zone:overworld, zone:city or zone:dungeon"; return false; } if (text2.StartsWith("flag:", StringComparison.Ordinal)) { return Named(atom.Substring(5), (string s) => new QuestFlagCondition(s, present: true), atom, out leaf, out error); } if (text2.StartsWith("faction:", StringComparison.Ordinal)) { return Named(atom.Substring(8), (string s) => new FactionCondition(s), atom, out leaf, out error); } if (text2.StartsWith("region:", StringComparison.Ordinal)) { return Named(atom.Substring(7), (string s) => new RegionCondition(s), atom, out leaf, out error); } if (text2.StartsWith("roomwarm:", StringComparison.Ordinal)) { return Named(atom.Substring(9), (string s) => new RoomWarmCondition(s.Split(new char[1] { ',' })), atom, out leaf, out error); } error = "'" + atom + "': unknown condition (known: always, night, day, hour=A-B, rain>X, rain<X, zone:overworld|city|dungeon, flag:UID, faction:NAME, region:NAME, roomwarm:NAME[,NAME…])"; return false; } } private static bool Named(string value, Func<string, ICardCondition> make, string atom, out ICardCondition leaf, out string error) { leaf = null; error = null; if (value.Trim().Length == 0) { error = "'" + atom + "': missing name"; return false; } leaf = make(value.Trim()); return true; } private static bool TryNum(string s, out float v) { if (float.TryParse(s.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out v) && !float.IsNaN(v)) { return !float.IsInfinity(v); } return false; } public static ICardCondition And(ICardCondition a, ICardCondition b) { bool flag = a == null || a is Always; bool flag2 = b == null || b is Always; if (flag && flag2) { return Always.Instance; } if (flag) { return b; } if (flag2) { return a; } return new AllOf(a, b); } } public readonly struct CarrotProbe { public const int DefaultStrikesToAct = 4; public const double DefaultDestEpsilonMeters = 5.0; public const double DefaultRemainingEpsilonMeters = 1.5; public const double DefaultTargetFarMeters = 15.0; public const double DefaultPartialEndEpsilonMeters = 5.0; public static readonly CarrotProbe Fresh = new CarrotProbe(0); public int Strikes { get; } public CarrotProbe(int strikes) { Strikes = strikes; } public static bool Suspicious(bool pathPending, bool pathComplete, double partialEndToBodyMeters, double destToCarrotMeters, double remainingMeters, double distToTargetMeters, double destEpsilonMeters = 5.0, double remainingEpsilonMeters = 1.5, double targetFarMeters = 15.0, double partialEndEpsilonMeters = 5.0) { if (pathPending) { return false; } if (distToTargetMeters <= targetFarMeters) { return false; } if (!pathComplete) { return partialEndToBodyMeters <= partialEndEpsilonMeters; } if (destToCarrotMeters > destEpsilonMeters) { return remainingMeters <= remainingEpsilonMeters; } return false; } public static CarrotProbe Advance(CarrotProbe p, bool suspicious, out bool act, int strikesToAct = 4) { if (strikesToAct < 1) { strikesToAct = 1; } if (!suspicious) { act = false; return Fresh; } int num = p.Strikes + 1; act = num >= strikesToAct; if (!act) { return new CarrotProbe(num); } return Fresh; } public override string ToString() { return $"strikes={Strikes}"; } } public static class ClusterPlan { public static IReadOnlyList<(float x, float z)> Offsets(int count, float radius, double startAngle01) { if (count <= 0 || radius <= 0f) { return Array.Empty<(float, float)>(); } if (startAngle01 < 0.0) { startAngle01 = 0.0; } if (startAngle01 > 1.0) { startAngle01 = 1.0; } List<(float, float)> list = new List<(float, float)>(count); double num = startAngle01 * 2.0 * Math.PI; double num2 = Math.PI * 2.0 / (double)count; for (int i = 0; i < count; i++) { double num3 = num + num2 * (double)i; list.Add(((float)(Math.Cos(num3) * (double)radius), (float)(Math.Sin(num3) * (double)radius))); } return list; } public static float MaxSpread(IReadOnlyList<(float x, float z)> positions) { if (positions == null || positions.Count < 2) { return 0f; } float num = 0f; for (int i = 0; i < positions.Count; i++) { for (int j = i + 1; j < positions.Count; j++) { float num2 = positions[i].x - positions[j].x; float num3 = positions[i].z - positions[j].z; float num4 = (float)Math.Sqrt(num2 * num2 + num3 * num3); if (num4 > num) { num = num4; } } } return num; } public static bool MaxSpreadOk(IReadOnlyList<(float x, float z)> positions, float clusterRadius, float tolerance = 6f) { if (clusterRadius <= 0f) { return false; } return MaxSpread(positions) <= clusterRadius * 2f + tolerance; } } public static class CombatDefer { public const double NotDeferring = double.NegativeInfinity; public static bool ShouldHold(bool enabled, bool inCombat, double deferSince, double now, float maxDeferSeconds) { if (!enabled || !inCombat) { return false; } if (maxDeferSeconds <= 0f) { return true; } if (double.IsNegativeInfinity(deferSince)) { return true; } return now - deferSince < (double)maxDeferSeconds; } public static double Advance(bool inCombat, double deferSince, double now) { if (!inCombat) { return double.NegativeInfinity; } if (!double.IsNegativeInfinity(deferSince)) { return deferSince; } return now; } } public enum DefendAction { None, Arm, Disarm, ClearLock } public readonly struct DefendGate { public static readonly DefendGate Fresh = new DefendGate(defending: false, -1.0); public bool Defending { get; } public double CalmSince { get; } public DefendGate(bool defending, double calmSince) { Defending = defending; CalmSince = calmSince; } public static DefendGate Advance(DefendGate g, bool hitThisTick, bool fighting, bool locked, double now, double calmSeconds, out DefendAction action) { action = DefendAction.None; fighting = hitThisTick || fighting; if (!g.Defending) { if (fighting) { action = DefendAction.Arm; return new DefendGate(defending: true, -1.0); } if (locked) { action = DefendAction.ClearLock; } return g; } if (fighting) { return new DefendGate(defending: true, -1.0); } double num = ((g.CalmSince < 0.0) ? now : g.CalmSince); if (calmSeconds <= 0.0 || now - num >= calmSeconds) { action = DefendAction.Disarm; return Fresh; } return new DefendGate(defending: true, num); } public override string ToString() { if (!Defending) { return "peaceful"; } if (!(CalmSince < 0.0)) { return $"defending, calm since {CalmSince:F1}"; } return "defending"; } } public sealed class EligibleDeck { public readonly struct Exclusion { public EventCard Card { get; } public string Reason { get; } public Exclusion(EventCard card, string reason) { Card = card; Reason = reason ?? ""; } public override string ToString() { return Card?.Id + ": " + Reason; } } public IReadOnlyList<EventCard> Cards { get; } public IReadOnlyList<Exclusion> Excluded { get; } public WorldSnapshot World { get; } private EligibleDeck(List<EventCard> cards, List<Exclusion> excluded, WorldSnapshot world) { Cards = cards; Excluded = excluded; World = world; } public static EligibleDeck Build(IReadOnlyList<EventCard> source, WorldSnapshot world) { world = world ?? WorldSnapshot.Empty; List<EventCard> list = new List<EventCard>(source?.Count ?? 0); List<Exclusion> list2 = new List<Exclusion>(); if (source != null) { for (int i = 0; i < source.Count; i++) { EventCard eventCard = source[i]; if (eventCard == null) { continue; } ICardCondition condition = eventCard.Condition; if (condition == null || condition is Always) { list.Add(eventCard); continue; } bool flag; string reason; try { flag = condition.IsMet(world); reason = "needs " + condition.Describe(); } catch (Exception ex) { flag = false; reason = "condition threw " + ex.GetType().Name + ": " + ex.Message; } if (flag) { list.Add(eventCard); } else { list2.Add(new Exclusion(eventCard, reason)); } } } return new EligibleDeck(list, list2, world); } public bool IsExcluded(string id) { for (int i = 0; i < Excluded.Count; i++) { if (string.Equals(Excluded[i].Card.Id, id, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } public string ReasonFor(string id) { for (int i = 0; i < Excluded.Count; i++) { if (string.Equals(Excluded[i].Card.Id, id, StringComparison.OrdinalIgnoreCase)) { return Excluded[i].Reason; } } return ""; } public string DescribeExclusions() { if (Excluded.Count == 0) { return "(every card eligible)"; } StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < Excluded.Count; i++) { if (i > 0) { stringBuilder.Append("; "); } stringBuilder.Append(Excluded[i].ToString()); } return stringBuilder.ToString(); } } public sealed class EventCard { public string Id { get; } public double Weight { get; } public double CooldownSeconds { get; } public bool Enabled { get; } public ICardCondition Condition { get; } public bool Drawable { get { if (Enabled && Weight > 0.0) { return Id.Length > 0; } return false; } } public EventCard(string id, double weight, double cooldownSeconds, bool enabled) : this(id, weight, cooldownSeconds, enabled, null) { } public EventCard(string id, double weight, double cooldownSeconds, bool enabled, ICardCondition condition) { Id = (id ?? "").Trim(); Weight = weight; CooldownSeconds = cooldownSeconds; Enabled = enabled; Condition = condition ?? Always.Instance; } public override string ToString() { return string.Format("{0} w={1:F2} cd={2:F0}s{3}", Id, Weight, CooldownSeconds, Enabled ? "" : " (disabled)") + ((Condition is Always) ? "" : (" when=" + Condition.Describe())); } } public readonly struct DeckState { private readonly Dictionary<string, double> _lastFiredAt; public static DeckState Empty => default(DeckState); private DeckState(Dictionary<string, double> lastFiredAt) { _lastFiredAt = lastFiredAt; } public double LastFiredAt(string id) { if (_lastFiredAt == null || string.IsNullOrEmpty(id)) { return double.NegativeInfinity; } if (!_lastFiredAt.TryGetValue(id, out var value)) { return double.NegativeInfinity; } return value; } public bool HasFired(string id) { return !double.IsNegativeInfinity(LastFiredAt(id)); } internal DeckState With(string id, double now) { Dictionary<string, double> obj = ((_lastFiredAt == null) ? new Dictionary<string, double>(StringComparer.OrdinalIgnoreCase) : new Dictionary<string, double>(_lastFiredAt, StringComparer.OrdinalIgnoreCase)); obj[id] = now; return new DeckState(obj); } } public static class EventDeck { public static string Draw(IReadOnlyList<EventCard> cards, DeckState state, double roll01, double now) { if (cards == null || cards.Count == 0) { return null; } EventCard eventCard = null; int num = 0; for (int i = 0; i < cards.Count; i++) { if (cards[i] != null && cards[i].Drawable) { num++; eventCard = cards[i]; } } switch (num) { case 0: return null; case 1: if (!OnCooldown(eventCard, state, now)) { return eventCard.Id; } return null; default: { double num2 = 0.0; for (int j = 0; j < cards.Count; j++) { if (Eligible(cards[j], state, now)) { num2 += cards[j].Weight; } } if (num2 <= 0.0) { return null; } if (roll01 < 0.0) { roll01 = 0.0; } if (roll01 > 1.0) { roll01 = 1.0; } double num3 = roll01 * num2; string result = null; double num4 = 0.0; for (int k = 0; k < cards.Count; k++) { if (Eligible(cards[k], state, now)) { num4 += cards[k].Weight; result = cards[k].Id; if (num3 < num4) { return cards[k].Id; } } } return result; } } } public static string Draw(IReadOnlyList<EventCard> cards, DeckState state, Func<double> roll01, double now) { if (cards == null || cards.Count == 0) { return null; } if (DrawableCount(cards) <= 1) { return Draw(cards, state, 0.0, now); } return Draw(cards, state, roll01?.Invoke() ?? 0.0, now); } public static DeckState MarkFired(DeckState state, string id, double now) { if (!string.IsNullOrEmpty(id)) { return state.With(id.Trim(), now); } return state; } public static double CooldownRemaining(EventCard card, DeckState state, double now) { if (card == null || card.CooldownSeconds <= 0.0) { return 0.0; } double num = state.LastFiredAt(card.Id); if (double.IsNegativeInfinity(num)) { return 0.0; } double num2 = num + card.CooldownSeconds - now; if (!(num2 > 0.0)) { return 0.0; } return num2; } public static bool OnCooldown(EventCard card, DeckState state, double now) { return CooldownRemaining(card, state, now) > 0.0; } public static bool Eligible(EventCard card, DeckState state, double now) { if (card != null && card.Drawable) { return !OnCooldown(card, state, now); } return false; } public static int DrawableCount(IReadOnlyList<EventCard> cards) { int num = 0; if (cards == null) { return 0; } for (int i = 0; i < cards.Count; i++) { if (cards[i] != null && cards[i].Drawable) { num++; } } return num; } } public static class EventGuard { public static IEnumerator Wrap(Func<IEnumerator> factory, Func<bool> completed, Action<Exception> onThrow, Action onFellOff) { if (factory == null) { onFellOff?.Invoke(); yield break; } IEnumerator inner; try { inner = factory(); } catch (Exception obj) { onThrow?.Invoke(obj); yield break; } IEnumerator wrapped = Wrap(inner, completed, onThrow, onFellOff); while (wrapped.MoveNext()) { yield return wrapped.Current; } } public static IEnumerator Wrap(IEnumerator inner, Func<bool> completed, Action<Exception> onThrow, Action onFellOff) { if (inner == null) { onFellOff?.Invoke(); yield break; } while (true) { object current; try { if (!inner.MoveNext()) { break; } current = inner.Current; goto IL_0067; } catch (Exception obj) { onThrow?.Invoke(obj); yield break; } IL_0067: yield return current; } if (completed == null || !completed()) { onFellOff?.Invoke(); } } } public sealed class FactionTable { private readonly Dictionary<string, string> _byName = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); public int Count => _byName.Count; public static FactionTable Parse(string text) { FactionTable factionTable = new FactionTable(); if (string.IsNullOrEmpty(text)) { return factionTable; } string[] array = text.Split(new char[1] { '\n' }); for (int i = 0; i < array.Length; i++) { string text2 = array[i].Trim(); if (text2.Length == 0 || text2[0] == '#') { continue; } int num = text2.IndexOf('='); if (num > 0 && num != text2.Length - 1) { string text3 = text2.Substring(0, num).Trim(); string text4 = text2.Substring(num + 1).Trim(); if (text3.Length != 0 && text4.Length != 0) { factionTable._byName[text3] = text4; } } } return factionTable; } public string FactionOf(string species) { string text = (species ?? "").Trim(); if (text.Length == 0) { return null; } if (_byName.TryGetValue(text, out string value)) { return value; } string result = null; int num = 0; foreach (KeyValuePair<string, string> item in _byName) { if (item.Key.Length > num && text.IndexOf(item.Key, StringComparison.OrdinalIgnoreCase) >= 0) { result = item.Value; num = item.Key.Length; } } return result; } public bool IsKnown(string species) { return FactionOf(species) != null; } public bool Observe(string species, string faction, out string previous) { previous = null; string text = (species ?? "").Trim(); string text2 = (faction ?? "").Trim(); if (text.Length == 0 || text2.Length == 0) { return false; } string text3 = (previous = FactionOf(text)); _byName[text] = text2; if (text3 != null) { return !string.Equals(text3, text2, StringComparison.OrdinalIgnoreCase); } return true; } public List<string> SameFaction(string species, IEnumerable<string> pool) { List<string> list = new List<string>(); string text = FactionOf(species); if (text == null || pool == null) { return list; } foreach (string item in pool) { if (string.Equals(FactionOf(item), text, StringComparison.OrdinalIgnoreCase)) { list.Add(item); } } return list; } public List<string> ToLines() { List<string> list = new List<string>(_byName.Keys); list.Sort(StringComparer.OrdinalIgnoreCase); List<string> list2 = new List<string>(list.Count); foreach (string item in list) { list2.Add(item + "=" + _byName[item]); } return list2; } } public sealed class FarEntry { public string SpeciesKey = ""; public float X; public float Y; public float Z; public float RotY; public float HpFrac; public double StoredAt; public bool InFlight; public bool WarmHeldLogged; } public sealed class FarCache { private readonly List<FarEntry> _entries = new List<FarEntry>(); private readonly int _maxEntries; public int Count => _entries.Count; public IReadOnlyList<FarEntry> Entries => _entries; public FarCache(int maxEntries) { _maxEntries = Math.Max(1, maxEntries); } public static float ClampRestoreRadius(float despawnRadius, float restoreRadius) { return Math.Min(restoreRadius, despawnRadius * 0.8f); } public static bool FarFromAll(float x, float y, float z, IReadOnlyList<(float x, float y, float z)> players, float radius) { if (players == null || players.Count == 0) { return false; } float num = radius * radius; for (int i = 0; i < players.Count; i++) { float num2 = players[i].x - x; float num3 = players[i].y - y; float num4 = players[i].z - z; if (num2 * num2 + num3 * num3 + num4 * num4 <= num) { return false; } } return true; } private static bool NearAny(FarEntry e, IReadOnlyList<(float x, float y, float z)> players, float radius) { if (players == null) { return false; } float num = radius * radius; for (int i = 0; i < players.Count; i++) { float num2 = players[i].x - e.X; float num3 = players[i].y - e.Y; float num4 = players[i].z - e.Z; if (num2 * num2 + num3 * num3 + num4 * num4 <= num) { return true; } } return false; } public void Add(FarEntry entry, double now, double maxAgeSeconds) { DropExpired(now, maxAgeSeconds); entry.HpFrac = Math.Min(1f, Math.Max(0.01f, entry.HpFrac)); entry.StoredAt = now; while (_entries.Count >= _maxEntries) { int num = OldestEvictableIndex(); if (num < 0) { break; } _entries.RemoveAt(num); } _entries.Add(entry); } public void CollectRestorable(IReadOnlyList<(float x, float y, float z)> players, float restoreRadius, double now, double maxAgeSeconds, List<FarEntry> into) { into.Clear(); DropExpired(now, maxAgeSeconds); foreach (FarEntry entry in _entries) { if (!entry.InFlight && NearAny(entry, players, restoreRadius)) { into.Add(entry); } } into.Sort((FarEntry a, FarEntry b) => a.StoredAt.CompareTo(b.StoredAt)); } public bool Remove(FarEntry entry) { return _entries.Remove(entry); } public void Clear() { _entries.Clear(); } private int OldestEvictableIndex() { int num = -1; for (int i = 0; i < _entries.Count; i++) { if (!_entries[i].InFlight && (num < 0 || _entries[i].StoredAt < _entries[num].StoredAt)) { num = i; } } return num; } private void DropExpired(double now, double maxAgeSeconds) { if (maxAgeSeconds <= 0.0) { return; } for (int num = _entries.Count - 1; num >= 0; num--) { if (!_entries[num].InFlight && now - _entries[num].StoredAt > maxAgeSeconds) { _entries.RemoveAt(num); } } } } public enum FriendlyBlipVerdict { Blip, Disabled, NotAlive, MerchantOwned } public static class FriendlyBlip { public static FriendlyBlipVerdict Classify(bool showFriendly, bool isAlive, string ownerTag, string merchantOwnerTag) { if (!showFriendly) { return FriendlyBlipVerdict.Disabled; } if (!isAlive) { return FriendlyBlipVerdict.NotAlive; } if (!string.IsNullOrEmpty(merchantOwnerTag) && string.Equals(ownerTag, merchantOwnerTag, StringComparison.Ordinal)) { return FriendlyBlipVerdict.MerchantOwned; } return FriendlyBlipVerdict.Blip; } } public enum GreetEdge { None, Paused, Resumed } public readonly struct GreetPause { public static readonly GreetPause Fresh = new GreetPause(paused: false, -1.0); public bool Paused { get; } public double AwayAt { get; } public GreetPause(bool paused, double awayAt) { Paused = paused; AwayAt = awayAt; } public static GreetPause Advance(GreetPause p, bool hold, double now, double resumeSeconds, out GreetEdge edge) { edge = GreetEdge.None; if (!p.Paused) { if (!hold) { return p; } edge = GreetEdge.Paused; return new GreetPause(paused: true, -1.0); } if (hold) { return new GreetPause(paused: true, -1.0); } double num = ((p.AwayAt < 0.0) ? now : p.AwayAt); if (resumeSeconds <= 0.0 || now - num >= resumeSeconds) { edge = GreetEdge.Resumed; return Fresh; } return new GreetPause(paused: true, num); } public override string ToString() { if (!Paused) { return "walking"; } if (!(AwayAt < 0.0)) { return $"paused, away since {AwayAt:F1}"; } return "paused"; } } public enum DefendVerdict { Defend, Disabled, WrongPhase, NotAlive, NotAI, PlayerSide, PatrolMember, PeacefulFaction, PlotOrNamed, NotEngaged, TooFarFromPlayer, BeyondLeash } public readonly struct DefendCandidate { public string Name { get; } public string Faction { get; } public bool IsAI { get; } public bool Alive { get; } public bool PatrolMember { get; } public bool EngagedWithPlayerSide { get; } public double DistanceToPlayer { get; } public double DistanceToParty { get; } public DefendCandidate(string? name, string? faction, bool isAI, bool alive, bool patrolMember, bool engagedWithPlayerSide, double distanceToPlayer, double distanceToParty) { Name = name ?? ""; Faction = faction ?? ""; IsAI = isAI; Alive = alive; PatrolMember = patrolMember; EngagedWithPlayerSide = engagedWithPlayerSide; DistanceToPlayer = distanceToPlayer; DistanceToParty = distanceToParty; } } public readonly struct DefendRules { public bool Enabled { get; } public double RadiusMeters { get; } public double LeashMeters { get; } public DefendRules(bool enabled, double radiusMeters, double leashMeters) { Enabled = enabled; RadiusMeters = radiusMeters; LeashMeters = leashMeters; } } public static class GuardDefend { public static readonly string[] HostileFactions = new string[5] { "Bandits", "Mercs", "Tuanosaurs", "Hounds", "CorruptionSpirit" }; public static bool IsHostileFaction(string? faction) { if (string.IsNullOrWhiteSpace(faction)) { return false; } string a = faction.Trim(); for (int i = 0; i < HostileFactions.Length; i++) { if (string.Equals(a, HostileFactions[i], StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } public static bool PhaseAllows(PatrolPhase phase) { if (phase != PatrolPhase.Spawning && phase != PatrolPhase.Fighting) { return phase == PatrolPhase.Resolved; } return true; } public static DefendVerdict Judge(PatrolPhase phase, in DefendCandidate c, in DefendRules rules, IEnumerable<string>? extraBlocklist = null) { if (!rules.Enabled) { return DefendVerdict.Disabled; } if (!PhaseAllows(phase)) { return DefendVerdict.WrongPhase; } if (!c.Alive) { return DefendVerdict.NotAlive; } if (!c.IsAI) { return DefendVerdict.NotAI; } if (string.Equals(c.Faction, "Player", StringComparison.OrdinalIgnoreCase)) { return DefendVerdict.PlayerSide; } if (c.PatrolMember) { return DefendVerdict.PatrolMember; } if (!IsHostileFaction(c.Faction)) { return DefendVerdict.PeacefulFaction; } if (RosterFilter.IsBlocked(c.Name, extraBlocklist)) { return DefendVerdict.PlotOrNamed; } if (!c.EngagedWithPlayerSide) { return DefendVerdict.NotEngaged; } if (c.DistanceToPlayer > rules.RadiusMeters) { return DefendVerdict.TooFarFromPlayer; } if (!LeashHolds(c.DistanceToParty, rules.LeashMeters)) { return DefendVerdict.BeyondLeash; } return DefendVerdict.Defend; } public static bool LeashHolds(double distanceToParty, double leashMeters) { if (!(leashMeters <= 0.0)) { return distanceToParty <= leashMeters; } return true; } public static string Explain(DefendVerdict v) { return v switch { DefendVerdict.Defend => "defend", DefendVerdict.Disabled => "DefendPlayer=false", DefendVerdict.WrongPhase => "the patrol is leaving", DefendVerdict.NotAlive => "not alive", DefendVerdict.NotAI => "not an AI", DefendVerdict.PlayerSide => "Player faction (a player, a summon or a pet)", DefendVerdict.PatrolMember => "one of the patrol's own", DefendVerdict.PeacefulFaction => "a faction the guards have no quarrel with", DefendVerdict.PlotOrNamed => "blocklisted (unique/boss/plot faction)", DefendVerdict.NotEngaged => "not fighting the player side", DefendVerdict.TooFarFromPlayer => "too far from every player", DefendVerdict.BeyondLeash => "beyond the patrol's leash", _ => v.ToString(), }; } } public enum GuardTown { Cierzo, Berg, Levant, Monsoon, Harmattan, NewSirocco } public static class GuardLook { public static GuardTown? ForAreaId(int areaId) { return areaId switch { 101 => GuardTown.Cierzo, 201 => GuardTown.Monsoon, 301 => GuardTown.Levant, 401 => GuardTown.Harmattan, 501 => GuardTown.Berg, 602 => GuardTown.NewSirocco, _ => null, }; } public static GuardTown? ForName(string name) { if (string.IsNullOrEmpty(name)) { return null; } string haystack = name.Trim(); if (Match(haystack, "cierzo", "chersonese")) { return GuardTown.Cierzo; } if (Match(haystack, "berg", "enmerkar", "forest")) { return GuardTown.Berg; } if (Match(haystack, "levant", "abrassar")) { return GuardTown.Levant; } if (Match(haystack, "monsoon", "hallowed", "marsh")) { return GuardTown.Monsoon; } if (Match(haystack, "harmattan", "antique", "plateau")) { return GuardTown.Harmattan; } if (Match(haystack, "sirocco", "caldera")) { return GuardTown.NewSirocco; } return null; } private static bool Match(string haystack, params string[] needles) { for (int i = 0; i < needles.Length; i++) { if (haystack.IndexOf(needles[i], StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } return false; } public static string Name(GuardTown town) { if (town != GuardTown.NewSirocco) { return town.ToString(); } return "New Sirocco"; } } public enum HazardShapeKind { Bounds, HeightBelow, HeightAbove, Sphere } public struct HazardShape { public HazardShapeKind Kind; public float CenterX; public float CenterY; public float CenterZ; public float ExtentX; public float ExtentY; public float ExtentZ; public bool Contains(float x, float y, float z) { switch (Kind) { case HazardShapeKind.Bounds: if (x >= CenterX - ExtentX && x <= CenterX + ExtentX && y >= CenterY - ExtentY && y <= CenterY + ExtentY && z >= CenterZ - ExtentZ) { return z <= CenterZ + ExtentZ; } return false; case HazardShapeKind.HeightBelow: return y <= CenterY; case HazardShapeKind.HeightAbove: return y >= CenterY; case HazardShapeKind.Sphere: { float num = x - CenterX; float num2 = y - CenterY; float num3 = z - CenterZ; return num * num + num2 * num2 + num3 * num3 <= ExtentX * ExtentX; } default: return false; } } } public sealed class HazardTester { public bool AllRequired; public readonly List<HazardShape> Shapes = new List<HazardShape>(); public bool Covers(float x, float y, float z) { if (Shapes.Count == 0) { return false; } if (AllRequired) { foreach (HazardShape shape in Shapes) { if (!shape.Contains(x, y, z)) { return false; } } return true; } foreach (HazardShape shape2 in Shapes) { if (shape2.Contains(x, y, z)) { return true; } } return false; } } public static class HazardField { public static bool IsHazardous(IReadOnlyList<HazardTester> testers, float x, float y, float z) { if (testers == null) { return false; } foreach (HazardTester tester in testers) { if (tester.Covers(x, y, z)) { return true; } } return false; } } public enum LeaveAction { Skip, RemoveGone, RemoveCorpse, RemoveFar, HoldCorpse, Hold } public static class LeaveRule { public static LeaveAction Decide(bool removed, bool bodyGone, bool alive, bool farFromEveryPlayer) { if (removed) { return LeaveAction.Skip; } if (bodyGone) { return LeaveAction.RemoveGone; } if (!alive) { if (!farFromEveryPlayer) { return LeaveAction.HoldCorpse; } return LeaveAction.RemoveCorpse; } if (!farFromEveryPlayer) { return LeaveAction.Hold; } return LeaveAction.RemoveFar; } public static bool StillHere(LeaveAction action) { return action == LeaveAction.Hold; } } public enum LoadPriorityChoice { Unchanged, Low, BelowNormal, Normal, High } public static class LoadPriority { public static LoadPriorityChoice Parse(string raw) { string text = (raw ?? "").Trim(); if (text.Length == 0) { return LoadPriorityChoice.Unchanged; } if (Eq(text, "unchanged") || Eq(text, "default") || Eq(text, "off") || Eq(text, "none")) { return LoadPriorityChoice.Unchanged; } if (Eq(text, "low")) { return LoadPriorityChoice.Low; } if (Eq(text, "belownormal") || Eq(text, "below normal")) { return LoadPriorityChoice.BelowNormal; } if (Eq(text, "normal")) { return LoadPriorityChoice.Normal; } if (Eq(text, "high")) { return LoadPriorityChoice.High; } return LoadPriorityChoice.Unchanged; } public static bool Applies(LoadPriorityChoice choice) { return choice != LoadPriorityChoice.Unchanged; } private static bool Eq(string a, string b) { return string.Equals(a, b, StringComparison.OrdinalIgnoreCase); } } public sealed class MeasureLedger { private sealed class SourceStats { internal int Offered; internal int Accepted; internal int WavesUsed; internal double DistanceSum; internal int DistanceCount; internal double RatioSum; internal int RatioCount; internal readonly Dictionary<RejectReason, int> Rejects = new Dictionary<RejectReason, int>(); } private readonly List<string> _sources = new List<string>(); private readonly Dictionary<string, Dictionary<string, SourceStats>> _areas = new Dictionary<string, Dictionary<string, SourceStats>>(StringComparer.OrdinalIgnoreCase); public IReadOnlyList<string> Sources => _sources; public IReadOnlyList<string> Areas { get { List<string> list = new List<string>(_areas.Keys); list.Sort(StringComparer.OrdinalIgnoreCase); return list; } } public void RegisterSource(string sourceId) { string text = (sourceId ?? "").Trim(); if (text.Length == 0) { return; } for (int i = 0; i < _sources.Count; i++) { if (string.Equals(_sources[i], text, StringComparison.OrdinalIgnoreCase)) { return; } } _sources.Add(text); } public void Candidate(string areaKey, string sourceId, RejectReason reason, float distance, float ratio) { SourceStats sourceStats = Stats(areaKey, sourceId); sourceStats.Offered++; if (reason == RejectReason.None) { sourceStats.Accepted++; } else { sourceStats.Rejects[reason] = ((!sourceStats.Rejects.TryGetValue(reason, out var value)) ? 1 : (value + 1)); } if (distance >= 0f) { sourceStats.DistanceSum += distance; sourceStats.DistanceCount++; } if (ratio >= 0f) { sourceStats.RatioSum += ratio; sourceStats.RatioCount++; } } public void WaveUsed(string areaKey, string sourceId) { Stats(areaKey, sourceId).WavesUsed++; } public void SourceEmpty(string areaKey, string sourceId) { SourceStats sourceStats = Stats(areaKey, sourceId); sourceStats.Rejects[RejectReason.SourceEmpty] = ((!sourceStats.Rejects.TryGetValue(RejectReason.SourceEmpty, out var value)) ? 1 : (value + 1)); } public void Reset() { _areas.Clear(); } public string Format(string areaKey) { string text = (areaKey ?? "").Trim(); if (text.Length == 0) { text = "(unknown)"; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("area ").Append(text).Append(" — anchor sources") .AppendLine(); if (!_areas.TryGetValue(text, out Dictionary<string, SourceStats> value)) { stringBuilder.Append(" (no candidates recorded here yet)"); return stringBuilder.ToString(); } stringBuilder.Append(" ").Append("source".PadRight(16)).Append("offered".PadLeft(8)) .Append("accept".PadLeft(8)) .Append("used".PadLeft(6)) .Append("avgDist".PadLeft(9)) .Append("avgRatio".PadLeft(10)) .Append(" rejects") .AppendLine(); for (int i = 0; i < _sources.Count; i++) { string text2 = _sources[i]; value.TryGetValue(text2, out var value2); if (value2 == null) { value2 = new SourceStats(); } stringBuilder.Append(" ").Append(text2.PadRight(16)).Append(value2.Offered.ToString(CultureInfo.InvariantCulture).PadLeft(8)) .Append(value2.Accepted.ToString(CultureInfo.InvariantCulture).PadLeft(8)) .Append(value2.WavesUsed.ToString(CultureInfo.InvariantCulture).PadLeft(6)) .Append(Avg(value2.DistanceSum, value2.DistanceCount).PadLeft(9)) .Append(Avg(value2.RatioSum, value2.RatioCount).PadLeft(10)) .Append(" ") .Append(FormatRejects(value2.Rejects)) .AppendLine(); } return stringBuilder.ToString().TrimEnd(Array.Empty<char>()); } public string FormatAll() { IReadOnlyList<string> areas = Areas; if (areas.Count == 0) { return "no candidates recorded in any area yet"; } StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < areas.Count; i++) { if (i > 0) { stringBuilder.AppendLine(); } stringBuilder.AppendLine(Format(areas[i])); } return stringBuilder.ToString().TrimEnd(Array.Empty<char>()); } private static string Avg(double sum, int count) { if (count != 0) { return (sum / (double)count).ToString("F1", CultureInfo.InvariantCulture); } return "-"; } private static string FormatRejects(Dictionary<RejectReason, int> rejects) { if (rejects.Count == 0) { return "-"; } List<KeyValuePair<RejectReason, int>> list = new List<KeyValuePair<RejectReason, int>>(rejects); list.Sort(delegate(KeyValuePair<RejectReason, int> a, KeyValuePair<RejectReason, int> b) { int num2 = b.Value.CompareTo(a.Value); return (num2 == 0) ? string.CompareOrdinal(a.Key.ToString(), b.Key.ToString()) : num2; }); StringBuilder stringBuilder = new StringBuilder(); for (int num = 0; num < list.Count; num++) { if (num > 0) { stringBuilder.Append(' '); } stringBuilder.Append(list[num].Key).Append('=').Append(list[num].Value.ToString(CultureInfo.InvariantCulture)); } return stringBuilder.ToString(); } private SourceStats Stats(string areaKey, string sourceId) { string key = (string.IsNullOrWhiteSpace(areaKey) ? "(unknown)" : areaKey.Trim()); string text = (string.IsNullOrWhiteSpace(sourceId) ? "(unknown)" : sourceId.Trim()); RegisterSource(text); if (!_areas.TryGetValue(key, out Dictionary<string, SourceStats> value)) { value = new Dictionary<string, SourceStats>(StringComparer.OrdinalIgnoreCase); _areas[key] = value; } if (!value.TryGetValue(text, out var value2)) { value2 = (value[text] = new SourceStats()); } return value2; } } public static class MerchantText { public const string UnknownDestination = "the next town"; public const string RaidToast = "Bandits are closing on the merchant!"; public static string Greeting(string destinationName) { return "Traveller. Keep your hands where I can see them and we'll get on fine. I'm bound for " + Dest(destinationName) + " — if you're buying, I'm selling."; } public static string GreetingRescued(string destinationName) { return "You again. I'd be feeding the crows if you hadn't stepped in back there. Whatever you need before " + Dest(destinationName) + ", the pack's open — and the prices are kinder."; } public static string RoadFlavour(string destinationName, string bearing) { string s = Dest(destinationName); string text = Clean(bearing); if (text.Length <= 0) { return Cap(s) + " is the other end of this road, and the road between is no friend to anyone. Bandits take the pack if the beasts don't take the mule. I walk it anyway — nobody pays for goods that stay in the warehouse."; } return Cap(s) + " lies " + text + " of here, and the road between is no friend to anyone. Bandits take the pack if the beasts don't take the mule. I walk it anyway — nobody pays for goods that stay in the warehouse."; } public static string Toast(string bearing) { string text = Clean(bearing); if (text.Length <= 0) { return "A merchant is on the road nearby."; } return "A merchant is on the road to the " + text + "."; } private static string Dest(string destinationName) { string text = Clean(destinationName); if (text.Length <= 0) { return "the next town"; } return text; } private static string Clean(string s) { return (s ?? "").Trim(); } private static string Cap(string s) { if (s.Length != 0) { return char.ToUpperInvariant(s[0]) + s.Substring(1); } return s; } } public static class ParkRule { public static bool ReadsAsFighting(bool characterInCombat, bool aiIsFighting, bool hasLockedTarget) { return ReadsAsFighting(characterInCombat, aiIsFighting, hasLockedTarget, aiFrozen: false); } public static bool ReadsAsFighting(bool characterInCombat, bool aiIsFighting, bool hasLockedTarget, bool aiFrozen) { if (!aiFrozen) { return characterInCombat || aiIsFighting || hasLockedTarget; } return false; } public static bool ShouldPark(bool alive, bool fighting, bool farFromEveryPlayer) { return ShouldPark(alive, fighting, farFromEveryPlayer, beyondMidFightRadius: false); } public static bool ShouldPark(bool alive, bool fighting, bool farFromEveryPlayer, bool beyondMidFightRadius) { return ShouldPark(alive, fighting, farFromEveryPlayer, beyondMidFightRadius, eventOwned: false); } public static bool ShouldPark(bool alive, bool fighting, bool farFromEveryPlayer, bool beyondMidFightRadius, bool eventOwned) { if (!alive || eventOwned || !farFromEveryPlayer) { return false; } return !fighting || beyondMidFightRadius; } public static float EffectiveMidFightRadius(float despawnRadius, float parkMidFightBeyond) { if (parkMidFightBeyond <= 0f) { return 0f; } if (!(parkMidFightBeyond < despawnRadius)) { return parkMidFightBeyond; } return despawnRadius; } } public sealed class PatrolLedger { private readonly List<string> _spawned = new List<string>(); private readonly HashSet<string> _spawnedSet = new HashSet<string>(StringComparer.Ordinal); private readonly HashSet<string> _dead = new HashSet<string>(StringComparer.Ordinal); private readonly HashSet<string> _helpedOn = new HashSet<string>(StringComparer.Ordinal); public int Spawned => _spawned.Count; public int DeadCount => _dead.Count; public bool Helped => _helpedOn.Count > 0; public int HelpedOn => _helpedOn.Count; public bool Paid { get; private set; } public IReadOnlyList<string> Uids => _spawned; public bool Note(string uid) { if (string.IsNullOrEmpty(uid) || _spawnedSet.Contains(uid)) { return false; } _spawnedSet.Add(uid); _spawned.Add(uid); return true; } public bool NoteDead(string uid) { if (string.IsNullOrEmpty(uid) || !_spawnedSet.Contains(uid)) { return false; } return _dead.Add(uid); } public bool NoteHelped(string uid) { if (string.IsNullOrEmpty(uid) || !_spawnedSet.Contains(uid)) { return false; } return _helpedOn.Add(uid); } public bool IsOurs(string uid) { if (!string.IsNullOrEmpty(uid)) { return _spawnedSet.Contains(uid); } return false; } public bool MarkPaid() { if (Paid) { return false; } Paid = true; return true; } public override string ToString() { return $"spawned={Spawned} dead={DeadCount} helped={Helped}(on {HelpedOn}) paid={Paid}"; } } public static class PatrolPlan { public static readonly string[] TrogSpecies = new string[7] { "Troglodyte", "Armored Troglodyte", "Mana Troglodyte", "Troglodyte Grenadier", "Troglodyte Knight", "Troglodyte Archmage", "That Annoying Troglodyte" }; public static readonly string[] QueenGameObjectNames = new string[2] { "EliteTroglodyteQueen", "TroglodyteQueen" }; public static bool IsQueenBody(string goName) { if (string.IsNullOrEmpty(goName)) { return false; } for (int i = 0; i < QueenGameObjectNames.Length; i++) { if (goName.IndexOf(QueenGameObjectNames[i], StringComparison.OrdinalIgnoreCase) >= 0) { return true; } } if (goName.IndexOf("queen", StringComparison.OrdinalIgnoreCase) >= 0) { return goName.IndexOf("trog", StringComparison.OrdinalIgnoreCase) >= 0; } return false; } public static string PickSpecies(IReadOnlyList<string> allowed, double roll01) { if (allowed == null || allowed.Count == 0) { return null; } if (roll01 < 0.0) { roll01 = 0.0; } if (roll01 >= 1.0) { roll01 = 0.999999; } return allowed[(int)Math.Floor(roll01 * (double)allowed.Count)]; } public static string SpeciesSummary(IReadOnlyList<string> perTrogSpecies) { if (perTrogSpecies == null || perTrogSpecies.Count == 0) { return ""; } HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (string perTrogSpecy in perTrogSpecies) { if (!string.IsNullOrEmpty(perTrogSpecy)) { hashSet.Add(perTrogSpecy); } } List<string> list = new List<string>(); string[] trogSpecies = TrogSpecies; foreach (string item in trogSpecies) { if (hashSet.Contains(item)) { list.Add(item); } } foreach (string perTrogSpecy2 in perTrogSpecies) { if (string.IsNullOrEmpty(perTrogSpecy2)) { continue; } bool flag = false; foreach (string item2 in list) { if (string.Equals(item2, perTrogSpecy2, StringComparison.OrdinalIgnoreCase)) { flag = true; break; } } if (!flag) { list.Add(perTrogSpecy2); } } return string.Join(", ", list); } public static List<string> FactionCoherent(IReadOnlyList<string> pool, Func<string, string> factionOf, double roll01, out string faction, out string why) { faction = null; why = ""; List<string> list = new List<string>(); if (pool == null || pool.Count == 0) { return list; } if (factionOf == null || pool.Count == 1) { list.AddRange(pool); if (pool.Count == 1 && factionOf != null) { faction = factionOf(pool[0]); } why = ((factionOf == null) ? "no faction resolver — cast not narrowed" : "single-species cast"); return list; } if (roll01 < 0.0) { roll01 = 0.0; } if (roll01 >= 1.0) { roll01 = 0.999999; } int num = (int)Math.Floor(roll01 * (double)pool.Count); if (num >= pool.Count) { num = pool.Count - 1; } string b = BucketKey(pool[num], factionOf); faction = factionOf(pool[num]); List<string> list2 = new List<string>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); foreach (string item in pool) { string text = BucketKey(item, factionOf); hashSet.Add(text); if (string.Equals(text, b, StringComparison.OrdinalIgnoreCase)) { list.Add(item); } else { list2.Add(item); } } why = ((hashSet.Count <= 1) ? ("the cast is already one faction (" + (faction ?? "unknown") + ")") : (string.Format("cast pinned to faction {0} ({1} of {2} species; ", faction ?? "unknown", list.Count, pool.Count) + string.Format("{0} factions in the pool) — dropped {1} ", hashSet.Count, string.Join(", ", list2.ToArray())) + "so the party cannot fight itself")); return list; } public static List<string> CoherentWarmCast(IReadOnlyList<string> warm, IReadOnlyList<string> showable, Func<string, string> factionOf, double roll01, int minVariety, out string faction, out string why, out int coldAdmitted) { coldAdmitted = 0; if (showable == null || showable.Count == 0) { faction = null; why = ""; return new List<string>(); } if (warm == null || warm.Count == 0) { List<string> list = FactionCoherent(showable, factionOf, roll01, out faction, out why); coldAdmitted = list.Count; why += " (nothing warm here — the whole cast is a cold fallback)"; return list; } if (factionOf == null) { faction = null; List<string> list2 = new List<string>(warm); coldAdmitted = TopUp(list2, showable, null, null, minVariety); why = "no faction resolver — cast not narrowed"; return list2; } if (minVariety < 1) { minVariety = 1; } List<string> list3 = new List<string>(); Dictionary<string, List<string>> dictionary = new Dictionary<string, List<string>>(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < warm.Count; i++) { string text = warm[i]; if (!string.IsNullOrEmpty(text)) { string text2 = BucketKey(text, factionOf); if (!dictionary.TryGetValue(text2, out var value)) { value = (dictionary[text2] = new List<string>()); list3.Add(text2); } if (!value.Contains(text)) { value.Add(text); } } } if (list3.Count == 0) { faction = null; why = ""; return new List<string>(); } int num = 0; for (int j = 0; j < list3.Count; j++) { if (dictionary[list3[j]].Count > num) { num = dictionary[list3[j]].Count; } } List<string> list5 = new List<string>(); List<string> list6 = new List<string>(); for (int k = 0; k < list3.Count; k++) { List<string> list7 = dictionary[list3[k]]; if (list7.Count != num) { continue; } list6.Add(list3[k]); foreach (string item in list7) { list5.Add(item); } } if (roll01 < 0.0) { roll01 = 0.0; } if (roll01 >= 1.0) { roll01 = 0.999999; } int num2 = (int)Math.Floor(roll01 * (double)list5.Count); if (num2 >= list5.Count) { num2 = list5.Count - 1; } string text3 = BucketKey(list5[num2], factionOf); faction = factionOf(list5[num2]); List<string> list8 = new List<string>(dictionary[text3]); int count = list8.Count; coldAdmitted = TopUp(list8, showable, factionOf, text3, minVariety); string arg = ((list6.Count > 1) ? $"tied at {num} warm with {list6.Count} factions — rolled" : $"the warmest of {list3.Count} faction(s) in the warm pool"); why = string.Format("cast pinned to faction {0} ({1}; {2} warm species", faction ?? "unknown", arg, count) + ((coldAdmitted > 0) ? $" + {coldAdmitted} cold from the SAME faction to reach the variety floor of {minVariety}" : "") + ") so the party cannot fight itself"; return list8; } private static int TopUp(List<string> cast, IReadOnlyList<string> showable, Func<string, string> factionOf, string wantKey, int minVariety) { int num = 0; for (int i = 0; i < showable.Count; i++) { if (cast.Count >= minVariety) { break; } string text = showable[i]; if (string.IsNullOrEmpty(text) || (factionOf != null && !string.Equals(BucketKey(text, factionOf), wantKey, StringComparison.OrdinalIgnoreCase))) { continue; } bool flag = false; foreach (string item in cast) { if (string.Equals(item, text, StringComparison.OrdinalIgnoreCase)) { flag = true; break; } } if (!flag) { cast.Add(text); num++; } } return num; } private static string BucketKey(string species, Func<string, string> factionOf) { string text = factionOf(species); if (!string.IsNullOrEmpty(text)) { return text; } return "?" + species; } public static int Count(int min, int max, double roll01) { if (min < 1) { min = 1; } if (max < min) { max = min; } if (roll01 < 0.0) { roll01 = 0.0; } if (roll01 >= 1.0) { roll01 = 0.999999; } return min + (int)Math.Floor(roll01 * (double)(max - min + 1)); } public static int Rate(int min, int max, double roll01) { if (min < 0) { min = 0; } if (max < min) { max = min; } if (roll01 < 0.0) { roll01 = 0.0; } if (roll01 >= 1.0) { roll01 = 0.999999; } return min + (int)Math.Floor(roll01 * (double)(max - min + 1)); } public static int Reward(int spawned, int deadNow, bool helped, int min, int max, double roll01) { if (!helped) { return 0; } if (spawned < 0) { spawned = 0; } if (deadNow < 0) { deadNow = 0; } if (deadNow > spawned) { deadNow = spawned; } if (deadNow == 0) { return 0; } return Rate(min, max, roll01) * deadNow; } public static string[] DealSpecies(IReadOnlyList<string> pool, int warmCount, int slots, Func<double> roll01) { if (pool == null || pool.Count == 0 || slots <= 0) { return new string[0]; } if (roll01 == null) { roll01 = () => 0.0; } if (warmCount < 0) { warmCount = 0; } if (warmCount > pool.Count) { warmCount = pool.Count; } List<string> list = new List<string>(slots); int num = ((warmCount < slots) ? warmCount : slots); if (num > 0) { List<string> list2 = new List<string>(num); for (int num2 = 0; num2 < warmCount; num2++) { list2.Add(pool[num2]); } for (int num3 = 0; num3 < num; num3++) { int index = num3 + RollIndex(roll01(), list2.Count - num3); string value = list2[num3]; list2[num3] = list2[index]; list2[index] = value; list.Add(list2[num3]); } } while (list.Count < slots) { list.Add(PickSpecies(pool, roll01())); } string[] array = list.ToArray(); for (int num4 = 0; num4 < array.Length - 1; num4++) { int num5 = num4 + RollIndex(roll01(), array.Length - num4); string text = array[num4]; array[num4] = array[num5]; array[num5] = text; } return array; } private static int RollIndex(double roll01, int count) { if (count <= 1) { return 0; } if (roll01 < 0.0) { roll01 = 0.0; } if (roll01 >= 1.0) { roll01 = 0.999999; } int num = (int)Math.Floor(roll01 * (double)count); if (num < count) { return num; } return count - 1; } } public enum PatrolPhase { Spawning, Fighting, Resolved, Leaving, Gone } public enum PatrolResult { None, GuardsWon, GuardsDead, Stalemate } public readonly struct PatrolSnapshot { public int GuardsAlive { get; } public int TrogsAlive { get; } public double SecondsFighting { get; } public PatrolSnapshot(int guardsAlive, int trogsAlive, double secondsFighting) { GuardsAlive = guardsAlive; TrogsAlive = trogsAlive; SecondsFighting = secondsFighting; } } public static class PatrolState { public static PatrolPhase Advance(PatrolPhase phase, PatrolSnapshot snap, double stalemateSeconds, out PatrolResult result) { result = PatrolResult.None; switch (phase) { case PatrolPhase.Spawning: if (snap.GuardsAlive <= 0) { result = PatrolResult.GuardsDead; return PatrolPhase.Resolved; } if (snap.TrogsAlive <= 0) { return PatrolPhase.Spawning; } return PatrolPhase.Fighting; case PatrolPhase.Fighting: if (snap.GuardsAlive <= 0) { result = PatrolResult.GuardsDead; return PatrolPhase.Resolved; } if (snap.TrogsAlive <= 0) { result = PatrolResult.GuardsWon; return PatrolPhase.Resolved; } if (stalemateSeconds > 0.0 && snap.SecondsFighting >= stalemateSeconds) { result = PatrolResult.Stalemate; return PatrolPhase.Resolved; } return PatrolPhase.Fighting; default: return phase; } } } public static class PatrolText { public const string GreetingNoHelp = "It's done. Keep to the road, and keep your eyes open — where there were these, there will be more."; public const string RewardChoice = "You mentioned a purse."; public const string AlreadyPaid = "You've had the purse. Try that again and I'll want it back."; public static string Greeting(string townName) { if (!Has(townName)) { return "Stand clear, traveller. There is trog work on this road today, and we are paid to do it."; } return "Stand clear, traveller. " + townName + " pays us to keep this stretch of road clear of trogs, and there is work in it today."; } public static string GreetingHelped(int dead) { if (dead > 0) { return "You stood with us, and " + Trogs(dead) + " won't be crawling out of that hole again. The town keeps a purse for that sort of thing."; } return "You stood with us. That is more than most on this road would have done."; } public static string RewardReply(int silver, int dead) { if (silver > 0) { return Silver(silver) + ", figured on " + Trogs(dead) + ". Count it if you like — I did."; } return "Bounty's paid per carcass, and there's none to count. Nothing owed, but the thanks stand."; } public static string Toast(string bearing) { string text = (bearing ?? "").Trim(); if (text.Length <= 0) { return "A town patrol is fighting troglodytes nearby."; } return "A town patrol is fighting troglodytes to the " + text + "."; } public static string Trogs(int n) { if (n != 1) { return $"{n} troglodytes"; } return "1 troglodyte"; } public static string Silver(int n) { return $"{n} silver"; } private static bool Has(string s) { if (!string.IsNullOrEmpty(s)) { return s.Trim().Length > 0; } return false; } } public static class PeerRepush { public static bool ShouldRepush(bool isMaster, bool directorArmed, int peerActor, int localActor) { if (!isMaster) { return false; } if (!directorArmed) { return false; } if (peerActor <= 0 || peerActor == localActor) { return false; } return true; } } public static class PlateauRule { public static bool Accept(bool corner0, bool corner1, bool corner2, bool corner3) { return corner0 && corner1 && corner2 && corner3; } public static bool Accept(float d0, float d1, float d2, float d3, float maxSpread) { if (d0 < 0f || d1 < 0f || d2 < 0f || d3 < 0f) { return false; } if (maxSpread <= 0f) { return true; } float num = Math.Min(Math.Min(d0, d1), Math.Min(d2, d3)); return Math.Max(Math.Max(d0, d1), Math.Max(d2, d3)) - num <= maxSpread; } } public static class PlayerBody { public static bool Holds(bool playerFaction, bool isAi, bool alive) { return playerFaction && !isAi && alive; } } public static class PrewarmSlate { public const int ReservedForOthers = 4; public const int FixedCastDepth = 2; public static int Budget(int lruCapacity, int fallback) { if (lruCapacity <= 0) { if (fallback >= 0) { return fallback; } return 0; } int num = lruCapacity - 4; if (num >= 1) { return num; } return 1; } public static List<string> Compose(IReadOnlyList<IReadOnlyList<string>> sources, int perSource, int lruCapacity, Func<string, bool> alreadyWarm, out string why, IReadOnlyList<int> depths = null) { if (sources == null || perSource <= 0) { why = "nothing to warm"; return new List<string>(); } List<IReadOnlyList<string>> list = new List<IReadOnlyList<string>>(sources.Count); int num = 0; for (int i = 0; i < sources.Count; i++) { IReadOnlyList<string> readOnlyList = sources[i]; List<string> list2 = new List<string>(); if (readOnlyList != null) { for (int j = 0; j < readOnlyList.Count; j++) { string text = (readOnlyList[j] ?? "").Trim(); if (text.Length != 0) { num++; if (alreadyWarm == null || !alreadyWarm(text)) { list2.Add(text); } } } } list.Add(list2); } int[] array = new int[list.Count]; for (int k = 0; k < list.Count; k++) { int num2 = ((depths != null && k < depths.Count) ? depths[k] : perSource); if (num2 <= 0) { num2 = perSource; } array[k] = ((num2 < perSource) ? num2 : perSource); } List<string> list3 = MergeWithDepths(list, array); int num3 = Budget(lruCapacity, list3.Count); int num4 = 0; if (list3.Count > num3) { num4 = list3.Count - num3; list3.RemoveRange(num3, num4); } why = $"{list3.Count} cold of {num} offered across {list.Count} card(s), " + string.Format("depths [{0}] of {1}, budget {2}", string.Join(",", Array.ConvertAll(array, (int c) => c.ToString())), perSource, num3) + ((lruCapacity > 0) ? $" (LRU {lruCapacity} less {4} reserved)" : " (LRU uncapped)") + ((num4 > 0) ? $" — {num4} dropped so the queue cannot evict its own head" : ""); return list3; } private static List<string> MergeWithDepths(IReadOnlyList<IReadOnlyList<string>> sources, int[] caps) { List<string> list = new List<string>(); HashSet<string> hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase); int num = 0; for (int i = 0; i < caps.Length; i++) { if (caps[i] > num) { num = caps[i]; } } for (int j = 0; j < num; j++) { for (int k = 0; k < sources.Count; k++) { if (j >= caps[k]) { continue; } IReadOnlyList<string> readOnlyList = sources[k]; if (readOnlyList != null && j < readOnlyList.Count) { string text = (readOnlyList[j] ?? "").Trim(); if (text.Length != 0 && hashSet.Add(text)) { list.Add(text); } } } } return list; } } public sealed class ColdNote { public string What { get; } public string Species { get; } public string Reason { get; } public int[] Actors { get; } public ColdNote(string? what, string? species, string? reason, int[]? actors) { What = (what ?? "encounter").Trim(); Species = (species ?? "").Trim(); Reason = (reason ?? "").Trim(); Actors = actors ?? new int[0]; } } public static class QuietRoads { public const double DefaultMinGapSeconds = 60.0; public const double Never = double.NegativeInfinity; public static string KeyFor(string? region, IReadOnlyList<ColdNote>? notes) { if (notes == null || notes.Count == 0) { return ""; } List<string> list = new List<string>(notes.Count); for (int i = 0; i < notes.Count; i++) { ColdNote coldNote = notes[i]; if (coldNote != null && coldNote.Species.Length != 0) { List<int> list2 = new List<int>(coldNote.Actors); list2.Sort(); list.Add(coldNote.Species.ToLowerInvariant() + "=" + coldNote.Reason + "[" + string.Join(",", list2.ConvertAll((int a) => a.ToString()).ToArray()) + "]"); } } if (list.Count == 0) { return ""; } list.Sort(StringComparer.Ordinal); return (region ?? "").Trim() + "|" + string.Join(";", list.ToArray()); } public static int[] PeersIn(IReadOnlyList<ColdNote>? notes) { List<int> list = new List<int>(); if (notes == null) { return list.ToArray(); } for (int i = 0; i < notes.Count; i++) { ColdNote coldNote = notes[i]; if (coldNote == null) { continue; } for (int j = 0; j < coldNote.Actors.Length; j++) { if (!list.Contains(coldNote.Actors[j])) { list.Add(coldNote.Actors[j]); } } } list.Sort(); return list.ToArray(); } public static List<string> SpeciesIn(IReadOnlyList<ColdNote>? notes) { List<string> list = new List<string>(); if (notes == null) { return list; } for (int i = 0; i < notes.Count; i++) { ColdNote coldNote = notes[i]; if (coldNote == null || coldNote.Species.Length == 0) { continue; } bool flag = false; for (int j = 0; j < list.Count; j++) { if (string.Equals(list[j], coldNote.Species, StringComparison.OrdinalIgnoreCase)) { flag = true; break; } } if (!flag) { list.Add(coldNote.Species); } } return list; } public static bool ShouldLog(string? key, string? lastKey, double now, double lastAt, double minGapSeconds) { if (string.IsNullOrEmpty(key)) { return false; } if (!string.Equals(key, lastKey ?? "", StringComparison.Ordinal)) { return true; } if (double.IsNaN(now) || double.IsInfinity(now)) { return false; } if (double.IsNaN(lastAt) || double.IsInfinity(lastAt)) { return true; } if (minGapSeconds <= 0.0) { return true; } return now - lastAt >= minGapSeconds; } public static bool ShouldToast(string? region, string? lastToastRegion) { string text = (region ?? "").Trim(); if (text.Length == 0) { return false; } return !string.Equals(text, (lastToastRegion ?? "").Trim(), StringComparison.Ordinal); } public static string Describe(string? region, IReadOnlyList<ColdNote>? notes) { if (notes == null || notes.Count == 0) { return ""; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.Append("co-op encounters refused in '").Append(string.IsNullOrEmpty(region) ? "unknown region" : region).Append("': "); bool flag = true; for (int i = 0; i < notes.Count; i++) { ColdNote coldNote = notes[i]; if (coldNote == null || coldNote.Species.Length == 0) { continue; } if (!flag) { stringBuilder.Append(", "); } flag = false; stringBuilder.Append(coldNote.Species).Append(" (").Append(coldNote.What) .Append("; ") .Append(coldNote.Reason); if (coldNote.Actors.Length != 0) { stringBuilder.Append("; actor").Append((coldNote.Actors.Length == 1) ? " " : "s ").Append(string.Join(",", Array.ConvertAll(coldNote.Actors, (int a) => a.ToString()))); } stringBuilder.Append(')'); } if (flag) { return ""; } int[] array = PeersIn(notes); stringBuilder.Append(". Cold peer").Append((array.Length == 1) ? " " : "s ").Append((array.Length == 0) ? "unknown" : string.Join(",", Array.ConvertAll(array, (int a) => a.ToString()))) .Append(" — 'skwarmdump' on the host prints each peer's row. ") .Append("THIS IS NOT A REGRESSION: the party is being asked to warm these species and the ") .Append("encounters come back when it can. [Coop] RoomWarmMode in cobalt.spawnkit.cfg governs ") .Append("how strict this is; [Coop] AnnounceQuietRoads in cobalt.dangerousroads.cfg shows the ") .Append("player a one-line toast."); return stringBuilder.ToString(); } public static string Toast(int speciesCount) { if (speciesCount > 0) { return $"The roads are quiet — a companion's world is still warming ({speciesCount} species)."; } return ""; } } public struct QuietFlush { public bool Log; public string Line; public bool Toast; public string ToastText; public int SpeciesCount; public List<string> Cleared; } public sealed class QuietRoadsLedger { private readonly Dictionary<string, ColdNote> _pending = new Dictionary<string, ColdNote>(StringComparer.OrdinalIgnoreCase); private string _region = ""; private string _lastKey = ""; private double _lastAt = double.NegativeInfinity; private string _lastToastRegion = ""; public double MinGapSeconds = 60.0; public int PendingCount => _pending.Count; public void Record(string? region, string? what, string? species, string? reason, int[]? actors) { string text = (region ?? "").Trim(); string text2 = (species ?? "").Trim(); if (text2.Length != 0) { if (!string.Equals(text, _region, StringComparison.Ordinal)) { _pending.Clear(); _region = text; _lastKey = ""; } _pending[text2] = new ColdNote(what, text2, reason, actors); } } public QuietFlush Flush(double now, Func<string, string, ColdNote?>? refresh, bool announce) { QuietFlush result = new QuietFlush { Line = "", ToastText = "", Cleared = new List<string>() }; if (refresh != null && _pending.Count > 0) { List<string> list = new List<string>(_pending.Keys); for (int i = 0; i < list.Count; i++) { ColdNote coldNote = _pending[list[i]]; ColdNote coldNote2 = null; bool flag = false; try { coldNote2 = refresh(coldNote.What, coldNote.Species); } catch { flag = true; } if (!flag) { if (coldNote2 == null) { _pending.Remove(list[i]); result.Cleared.Add(coldNote.Species); } else { _pending[list[i]] = coldNote2; } } } } List<ColdNote> notes = new List<ColdNote>(_pending.Values); string text = QuietRoads.KeyFor(_region, notes); if (!QuietRoads.ShouldLog(text, _lastKey, now, _lastAt, MinGapSeconds)) { if (text.Length == 0) { _lastKey = ""; } return result; } _lastKey = text; _lastAt = now; result.Log = true; result.Line = QuietRoads.Describe(_region, notes); result.SpeciesCount = QuietRoads.SpeciesIn(notes).Count; if (!announce) { return result; } if (!QuietRoads.ShouldToast(_region, _lastToastRegion)) { return result; } string text2 = QuietRoads.Toast(result.SpeciesCount); if (text2.Length == 0) { return result; } _lastToastRegion = _region; result.Toast = true; result.ToastText = text2; return result; } } public readonly struct RaidCandidate { public string Key { get; } public string Faction { get; } public RaidCandidate(string key, string faction) { Key = key; Faction = faction; } } public static class RaidPlan { public const string BanditFaction = "Bandits"; public const string MerchantFaction = "Merchants"; public const string PlayerFaction = "Player"; public static bool Roll(double roll01, float chance) { if (chance <= 0f) { return false; } if (chance >= 1f) { return true; } return roll01 < (double)chance; } public static int Count(int min, int max, double roll01) { if (min < 1) { min = 1; } if (max < min) { max = min; } if (roll01 < 0.0) { roll01 = 0.0; } if (roll01 >= 1.0) { roll01 = 0.999999; } return min + (int)Math.Floor(roll01 * (double)(max - min + 1)); } public static string PickSpecies(IReadOnlyList<RaidCandidate> roster, double roll01) { if (roster == null || roster.Count == 0) { return null; } List<string> list = new List<string>(); List<string> list2 = new List<string>(); List<string> list3 = new List<string>(); for (int i = 0; i < roster.Count; i++) { RaidCandidate raidCandidate = roster[i]; if (!string.IsNullOrEmpty(raidCandidate.Key)) { if (raidCandidate.Faction == null) { list3.Add(raidCandidate.Key); } else if (string.Equals(raidCandidate.Faction, "Bandits", StringComparison.OrdinalIgnoreCase)) { list.Add(raidCandidate.Key); } else if (!string.Equals(raidCandidate.Faction, "Merchants", StringComparison.OrdinalIgnoreCase) && !string.Equals(raidCandidate.Faction, "Player", StringComparison.OrdinalIgnoreCase)) { list2.Add(raidCandidate.Key); } } } List<string> list4 = ((list.Count > 0) ? list : ((list2.Count > 0) ? list2 : list3)); if (list4.Count == 0) { return null; } if (roll01 < 0.0) { roll01 = 0.0; } if (roll01 >= 1.0) { roll01 = 0.999999; } return list4[(int)Math.Floor(roll01 * (double)list4.Count)]; } public static bool ShouldTrigger(double playerDistance, float triggerMeters, double spawnedAt, double now, float delaySeconds, float farParkMeters = 0f) { if (triggerMeters > 0f && playerDistance >= 0.0 && playerDistance <= (double)triggerMeters) { return true; } if (delaySeconds > 0f && now - spawnedAt >= (double)delaySeconds) { if (farParkMeters > 0f && playerDistance > (double)farParkMeters) { return false; } return true; } return false; } public static int CapToRoom(int wanted, int maxOwnActive, int ownActive) { if (wanted < 0) { wanted = 0; } int num = maxOwnActive - ownActive; if (num < 0) { num = 0; } if (wanted >= num) { return num; } return wanted; } public static bool IsWon(int placed, int aliveRaiders) { if (placed > 0) { return aliveRaiders <= 0; } return false; } public static float Radius(float min, float max, double roll01) { if (min < 0f) { min = 0f; } if (max < min) { max = min; } if (roll01 < 0.0) { roll01 = 0.0; } if (roll01 > 1.0) { roll01 = 1.0; } return min + (float)(roll01 * (double)(max - min)); } } public enum RegionArmAction { Keep, Rearm, Disarm } public static class RegionArmGate { public static RegionArmAction Decide(bool isOverworld, string builtForScene, string activeScene, bool hasPlayer) { if (!isOverworld) { return RegionArmAction.Keep; } string text = builtForScene ?? ""; string b = activeScene ?? ""; if (text.Length > 0 && string.Equals(text, b, StringComparison.Ordinal)) { return RegionArmAction.Keep; } if (!hasPlayer) { return RegionArmAction.Disarm; } return RegionArmAction.Rearm; } } public enum GateVerdict { Ok, Disabled, NoArea, NotOverworld, TownOrCity } public enum ZoneKind { None, Overworld, City, Dungeon } public static class RegionGate { public static readonly int[] OverworldAreaIds = new int[6] { 101, 201, 301, 401, 501, 602 }; public static ZoneKind ZoneOf(GateVerdict verdict) { return verdict switch { GateVerdict.Ok => ZoneKind.Overworld, GateVerdict.TownOrCity => ZoneKind.City, GateVerdict.NotOverworld => ZoneKind.Dungeon, _ => ZoneKind.None, }; } public static bool IsOverworldArea(int areaId) { for (int i = 0; i < OverworldAreaIds.Length; i++) { if (OverworldAreaIds[i] == areaId) { return true; } } return false; } public static GateVerdict Evaluate(bool enabled, int? areaId, bool townOrCity) { if (!enabled) { return GateVerdict.Disabled; } if (!areaId.HasValue) { return GateVerdict.NoArea; } if (townOrCity) { return GateVerdict.TownOrCity; } if (!IsOverworldArea(areaId.Value)) { return GateVerdict.NotOverworld; } return GateVerdict.Ok; } } public enum RejectReason { None, OutOfBand, InSight, NoNavSample, NoPath, PathIncomplete, RatioTooHigh, Plateau, TooCloseToMember, SourceEmpty, Hazard, Roofed } public static class RoadChain { public const double DefaultHopMeters = 120.0; public const double DefaultMinAdvanceMeters = 10.0; public const int DefaultMaxWaypoints = 64; public static List<XZ> Chain(XZ spawn, XZ exit, IReadOnlyList<XZ> seeds, double hopMeters = 120.0, double minAdvanceMeters = 10.0, int maxWaypoints = 64) { List<XZ> list = new List<XZ>(); if (seeds == null || seeds.Count == 0 || hopMeters <= 0.0) { return list; } if (minAdvanceMeters < 0.0) { minAdvanceMeters = 0.0; } if (maxWaypoints < 1) { maxWaypoints = 1; } bool[] array = new bool[seeds.Count]; XZ xZ = spawn; double num = XZ.Distance(xZ, exit); while (list.Count < maxWaypoints && num > hopMeters) { int num2 = -1; double num3 = double.MaxValue; for (int i = 0; i < seeds.Count; i++) { if (!array[i]) { double num4 = XZ.Distance(xZ, seeds[i]); if (!(num4 > hopMeters) && !(num4 >= num3) && !(XZ.Distance(seeds[i], exit) > num - minAdvanceMeters)) { num2 = i; num3 = num4; } } } if (num2 < 0) { break; } array[num2] = true; xZ = seeds[num2]; num = XZ.Distance(xZ, exit); list.Add(xZ); } return list; } } public readonly struct XZ { public double X { get; } public double Z { get; } public XZ(double x, double z) { X = x; Z = z; } public static double Distance(XZ a, XZ b) { double num = b.X - a.X; double num2 = b.Z - a.Z; return Math.Sqrt(num * num + num2 * num2); } public override string ToString() { return $"({X:F1}, {Z:F1})"; } } public static class RoadRoute { public static int ChooseExit(XZ spawn, IReadOnlyList<XZ> exits, double minRouteMeters, double roll01) { if (exits == null || exits.Count == 0) { return -1; } List<int> list = new List<int>(); int result = 0; double num = -1.0; for (int i = 0; i < exits.Count; i++) { double num2 = XZ.Distance(spawn, exits[i]); if (num2 >= minRouteMeters) { list.Add(i); } if (num2 > num) { num = num2; result = i; } } if (list.Count == 0) { return result; } return list[IndexFor(roll01, list.Count)]; } public static List<XZ> ThreadSeeds(XZ spawn, XZ exit, IReadOnlyList<XZ> seeds, double corridorMeters) { List<(double, int, XZ)> list = new List<(double, int, XZ)>(); if (seeds == null || seeds.Count == 0) { return new List<XZ>(); } double num = exit.X - spawn.X; double num2 = exit.Z - spawn.Z; double num3 = num * num + num2 * num2; if (num3 <= 1E-09) { return new List<XZ>(); } double num4 = Math.Sqrt(num3); for (int i = 0; i < seeds.Count; i++) { double num5 = seeds[i].X - spawn.X; double num6 = seeds[i].Z - spawn.Z; double num7 = (num5 * num + num6 * num2) / num3; if (!(num7 <= 0.0) && !(num7 >= 1.0) && !(Math.Abs(num5 * num2 - num6 * num) / num4 > corridorMeters)) { list.Add((num7, i, seeds[i])); } } list.Sort(delegate((double T, int Index, XZ Seed) a, (double T, int Index, XZ Seed) b) { int num9 = a.T.CompareTo(b.T); return (num9 == 0) ? a.Index.CompareTo(b.Index) : num9; }); List<XZ> list2 = new List<XZ>(list.Count); for (int num8 = 0; num8 < list.Count; num8++) { list2.Add(list[num8].Item3); } return list2; } public static List<XZ> KeepReachable(XZ from, IReadOnlyList<XZ> seeds, Func<XZ, XZ, bool> canWalk, List<XZ> dropped = null) { List<XZ> list = new List<XZ>(); if (seeds == null || seeds.Count == 0) { return list; } if (canWalk == null) { list.AddRange(seeds); return list; } XZ arg = from; for (int i = 0; i < seeds.Count; i++) { if (canWalk(arg, seeds[i])) { list.Add(seeds[i]); arg = seeds[i]; } else { dropped?.Add(seeds[i]); } } return list; } public static List<XZ> Build(XZ spawn, XZ exit, IReadOnlyList<XZ> seeds, double corridorMeters) { List<XZ> list = ThreadSeeds(spawn, exit, seeds, corridorMeters); list.Add(exit); return list; } public static bool NextWaypoint(XZ pos, IReadOnlyList<XZ> route, double arriveRadius, ref int index, out XZ target) { target = default(XZ); if (route == null || route.Count == 0) { return false; } if (index < 0) { index = 0; } int num = route.Count - 1; if (index > num) { index = num; } for (int num2 = num; num2 >= index; num2--) { if (Arrived(pos, route[num2], arriveRadius)) { index = ((num2 + 1 > num) ? num : (num2 + 1)); break; } } target = route[index]; return true; } public static bool Arrived(XZ pos, XZ exit, double radius) { if (radius > 0.0) { return XZ.Distance(pos, exit) <= radius; } return false; } private static int IndexFor(double roll01, int count) { if (count <= 0) { return 0; } if (roll01 < 0.0) { roll01 = 0.0; } if (roll01 > 1.0) { roll01 = 1.0; } int num = (int)(roll01 * (double)count); if (num < count) { return num; } return count - 1; } } public enum SpeciesSource { DonorTable, SquadReserve, Nearby } public sealed class SpeciesCandidate { public string Key { get; } public SpeciesSource Source { get; } public bool Blocked { get; } public bool ExpeditionOnly { get; }
plugins/DangerousRoads.dll
Decompiled 2 weeks ago
The result has been truncated due to the large size, download it to view full contents!
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 AggroKit; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using CompanionKit; using CompanionKit.Core; using DangerousRoads.Aggro; using DangerousRoads.Anchors; using DangerousRoads.Core; using DangerousRoads.Events; using DangerousRoads.Placement; using DangerousRoads.RoadMerchant; using DangerousRoads.Stranger; using DangerousRoads.TownPatrol; using DonorKit; using ForgeKit; using HarmonyLib; using Microsoft.CodeAnalysis; using NetKit; using NetKit.Core; using NodeCanvas.DialogueTrees; using NodeCanvas.Framework; using SpawnKit; using SpawnKit.Core; using StoryKit; using StoryKit.Core; using StoryKit.Saves; using UnityEngine; using UnityEngine.AI; using UnityEngine.Events; 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.13.0")] [assembly: AssemblyInformationalVersion("0.1.13+06e79d2edf3a19848f2e4105dddc16417cd2c1fe")] [assembly: AssemblyProduct("DangerousRoads")] [assembly: AssemblyTitle("DangerousRoads")] [assembly: AssemblyMetadata("BuildStamp", "06e79d2e 2026-09-05")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.0.0")] [module: UnverifiableCode] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } } 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 EventDirector _events = new EventDirector(); private readonly List<string> _recentSpecies = new List<string>(); 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; private double _nextOwnCapLogAt; private double _nextQuietFlushAt; private readonly List<SpawnHandle> _activeScratch = new List<SpawnHandle>(); private int _lastOwnActive; private const string BLOCK_OWN_CAP = "own cap reached"; private const double OwnCapLogEverySeconds = 300.0; internal EventDirector Events => _events; internal MerchantCard Merchant { get; private set; } internal PatrolCard Patrol { get; private set; } internal StrangerCard Stranger { get; private set; } 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; private int CountOwnActive() { Spawner.Active("dangerousroads", _activeScratch); _lastOwnActive = _activeScratch.Count; return _lastOwnActive; } internal AmbushDirector(Plugin host) { //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_0048: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_005f: Expected O, but got Unknown _host = host; _wave = new WaveRunner(_anchors); _events.Register(new AmbushCard(_wave)); _events.Register(new BlankCard()); } internal void RegisterMerchant(MerchantCard card) { Merchant = card; _events.Register(card); } internal void RegisterPatrol(PatrolCard card) { Patrol = card; _events.Register(card); } internal void RegisterStranger(StrangerCard card) { Stranger = card; _events.Register(card); } internal void OnRegionReady(Character player, string sceneName) { //IL_007a: 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(); _events.OnRegionChanged(); ShieldWard.Forget(); if (!_region.IsActiveZone) { Disarm($"no zone to deal ({_region.LastVerdict})"); ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)("[AMBUSH] " + _region.Describe() + " — idle.")); } } else { ArmForRegion(player, sceneName); } } internal void OnRegionAbandoned(string sceneName, string why) { StandDown("player never became ready in '" + sceneName + "' (" + why + ")"); ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[AMBUSH] standing down — " + why + " in '" + sceneName + "'; the roster built for '" + _roster.BuiltForScene + "' is stale and will be rebuilt on the next region poll with a ready player.")); } } private void ArmForRegion(Character player, string sceneName) { //IL_0074: Unknown result type (might be due to invalid IL or missing references) //IL_0079: Unknown result type (might be due to invalid IL or missing references) //IL_00aa: Unknown result type (might be due to invalid IL or missing references) if (_region.IsOverworld) { _roster.Rebuild(player, sceneName); WarmComposition val = PrewarmComposeNow(); _prewarmer.Reset(val.Queue, val.Why); ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)("[ROSTER] warm requests — " + val.Explain())); } PushRoomWarm("region arm"); } _clock = AmbushClock.Arm((double)Time.time, DrConfig.FirstArmDelaySeconds.Value); _state = DirectorState.Armed; ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogMessage((object)(string.Format("{0} armed in {1} [{2} deck]; first draw in ", "[AMBUSH]", _region.Describe(), _region.Zone) + $"~{DrConfig.FirstArmDelaySeconds.Value:F0}s.")); } } internal void OnPeerSceneReady(int actor) { bool flag = _state != DirectorState.Idle && _state != DirectorState.Disabled && _region.IsOverworld; int num = ((PhotonNetwork.player != null) ? PhotonNetwork.player.ID : (-1)); if (PeerRepush.ShouldRepush(!PhotonNetwork.isNonMasterClientInRoom, flag, actor, num)) { PushRoomWarm($"peer {actor} scene-ready"); } } private WarmComposition PrewarmComposeNow() { WarmPlan val = WarmPlanNow(_roster.PrewarmKeys()); return WarmRequestPlan.Compose(val, DrConfig.PrewarmCount.Value, (Plugin.MaxCachedTemplates != null) ? Plugin.MaxCachedTemplates.Value : 0, (Func<string, bool>)Spawner.CanMintNow); } private WarmPlan WarmPlanNow(IReadOnlyList<string> roster) { return WarmRequestPlan.Order(roster, DrConfig.PrewarmCount.Value, (IReadOnlyList<WarmRequest>)_events.WarmRequests(Time.time)); } private List<string> WarmSlateNow() { WarmPlan val = WarmPlanNow(_roster.WarmPriority()); return WarmSlate.Merge(val.Sources, DrConfig.PrewarmCount.Value); } private void PushRoomWarm(string why) { List<string> list = WarmSlateNow(); if (list.Count == 0) { return; } int num = Spawner.RequestRoomWarm((IList<string>)list); if (num > 0) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)(string.Format("{0} asked the room to warm {1} species ({2}): ", "[ROSTER]", list.Count, why) + string.Format("{0} — {1} sk.want sent this sweep (the whole ", string.Join(", ", list.ToArray()), num) + "want book, not only these).")); } } } private void PushShrunkRoomWarm() { IReadOnlyList<ShrinkEntry> lastShrink = _roster.LastShrink; List<string> list = new List<string>(lastShrink.Count); for (int i = 0; i < lastShrink.Count; i++) { if (list.Count >= DrConfig.PrewarmCount.Value) { break; } list.Add(lastShrink[i].Species); } if (list.Count == 0) { return; } int num = Spawner.RequestRoomWarm((IList<string>)list); if (num > 0) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)(string.Format("{0} wave found {1} species warm here but cold in the ", "[ROSTER]", lastShrink.Count) + $"room; re-asked {list.Count} ({num} sk.want sent this sweep — the whole want book).")); } } } internal void Tick() { //IL_0082: 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_0090: Unknown result type (might be due to invalid IL or missing references) //IL_0095: Unknown result type (might be due to invalid IL or missing references) //IL_0193: 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) { _region.Poll(DrConfig.Enabled.Value); Character localPlayer = Plugin.LocalPlayer; if (_region.IsActiveZone && (Object)(object)localPlayer != (Object)null && Lifecycle.IsSanePosition(((Component)localPlayer).transform.position)) { Scene activeScene = SceneManager.GetActiveScene(); ArmForRegion(localPlayer, ((Scene)(ref activeScene)).name); } else { _state = DirectorState.Idle; } } if (num >= _nextRegionPollAt) { _nextRegionPollAt = num + 0.5; bool flag = _region.Poll(DrConfig.Enabled.Value); if (flag && !_region.IsActiveZone) { Disarm($"left every known zone ({_region.LastVerdict})"); } else if (flag || _state == DirectorState.Idle) { GuardStaleRoster(flag ? "area changed" : "idle poll"); } } _prewarmer.Tick(num); AutoDumpLedger(num); if (num >= _nextQuietFlushAt) { _nextQuietFlushAt = num + 1.0; RoomWarmGate.FlushIfPending(); } if (_region.IsActiveZone && !_wave.Running && !_events.Running && _state != DirectorState.Idle && AmbushClock.IsDue(_clock, (double)Time.time)) { TryWave(); } } private void GuardStaleRoster(string edge) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0049: Unknown result type (might be due to invalid IL or missing references) //IL_004e: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0053: Invalid comparison between Unknown and I4 //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Invalid comparison between Unknown and I4 Scene activeScene = SceneManager.GetActiveScene(); string name = ((Scene)(ref activeScene)).name; Character localPlayer = Plugin.LocalPlayer; bool flag = (Object)(object)localPlayer != (Object)null && Lifecycle.IsSanePosition(((Component)localPlayer).transform.position); RegionArmAction val = RegionArmGate.Decide(_region.IsOverworld, _roster.BuiltForScene, name, flag); if ((int)val != 1) { if ((int)val == 2 && _state != DirectorState.Idle) { StandDown(edge + " before the roster was rebuilt (built for '" + _roster.BuiltForScene + "', now in '" + name + "')"); ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[AMBUSH] " + edge + ": roster is stale (built for '" + _roster.BuiltForScene + "', now in '" + name + "') and no player is ready — standing down until one is.")); } } } else { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogMessage((object)("[AMBUSH] " + edge + ": roster was built for '" + _roster.BuiltForScene + "' but we stand in '" + name + "' — re-arming for this region.")); } Lifecycle.InvalidateWaits((object)Plugin.Instance); OnRegionReady(localPlayer, name); } } private void TryWave() { //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0089: Unknown result type (might be due to invalid IL or missing references) Character localPlayer = Plugin.LocalPlayer; CountOwnActive(); string text = SoftBlock(localPlayer); if (text != null) { SoftRetry(text); return; } WorldSnapshot world = WorldProbe.Capture(_region.LedgerKey, localPlayer, _region.Zone); string text2 = _events.Draw(() => Random.value, Time.time, world); if (text2 == null) { SoftRetry(_events.LastNullReason); } else { Fire(text2, localPlayer, forced: false, CardArgs.Empty); } } private void Fire(string cardId, Character player, bool forced, CardArgs args) { //IL_015e: 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) IEventCard card = _events.RunnerFor(cardId); if (card == null) { string text = "no runner for card '" + cardId + "'"; if (forced) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[EVENT] " + text + ".")); } } else { SoftRetry(text); } return; } bool ambush = string.Equals(cardId, "ambush", StringComparison.OrdinalIgnoreCase); List<SpeciesCandidate> list = null; if (ambush) { list = _roster.SpawnableNow(); if (_roster.LastShrink.Count > 0) { PushShrunkRoomWarm(); } if (list.Count == 0) { if (forced) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("[EVENT] forced '" + cardId + "': no warm species.")); } } else { SoftRetry("no warm species"); } return; } } EventContext obj = new EventContext { Player = player, LedgerKey = _region.LedgerKey }; IReadOnlyList<SpeciesCandidate> readOnlyList = list; obj.Spawnable = readOnlyList ?? Array.Empty<SpeciesCandidate>(); obj.RecentSpecies = _recentSpecies; obj.OwnActive = _lastOwnActive; obj.Forced = forced; obj.Args = args; EventContext ctx = obj; _state = DirectorState.WaveInFlight; _events.NoteRunning(cardId); bool flag = string.Equals(cardId, "blank", StringComparison.OrdinalIgnoreCase); if ((!ambush && !flag) || forced) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogMessage((object)("[EVENT] running card '" + cardId + "'" + (forced ? " (forced)" : "") + ".")); } } bool done = false; try { ((MonoBehaviour)_host).StartCoroutine(EventGuard.Wrap((Func<IEnumerator>)(() => card.Run(ctx, Complete)), (Func<bool>)(() => done), (Action<Exception>)delegate(Exception e) { ManualLogSource log5 = Plugin.Log; if (log5 != null) { log5.LogError((object)("[EVENT] card '" + cardId + "' threw " + e.GetType().Name + ": " + e.Message + "\n" + e.StackTrace)); } Complete(EventOutcome.Soft("card '" + cardId + "' threw " + e.GetType().Name)); }, (Action)delegate { ManualLogSource log5 = Plugin.Log; if (log5 != null) { log5.LogWarning((object)("[EVENT] card '" + cardId + "' ended without reporting an outcome.")); } Complete(EventOutcome.Soft("card '" + cardId + "' ended without outcome")); })); } catch (Exception ex) { ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogError((object)("[EVENT] could not start card '" + cardId + "': " + ex.GetType().Name + ": " + ex.Message + "\n" + ex.StackTrace)); } Complete(EventOutcome.Soft("card '" + cardId + "' could not start (" + ex.GetType().Name + ")")); } void Complete(EventOutcome outcome) { //IL_0143: Unknown result type (might be due to invalid IL or missing references) //IL_0148: 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_0101: Unknown result type (might be due to invalid IL or missing references) //IL_007e: Unknown result type (might be due to invalid IL or missing references) //IL_0083: Unknown result type (might be due to invalid IL or missing references) if (!done) { done = true; _events.NoteDone(outcome); WavePlan lastPlan; if (forced) { _state = DirectorState.Armed; if (outcome.Fired) { _events.MarkFired(cardId, Time.time); } if (outcome.Fired && ambush) { AmbushDirector ambushDirector = this; lastPlan = _wave.LastPlan; ambushDirector.RememberSpecies(((WavePlan)(ref lastPlan)).PrimarySpecies); } ManualLogSource log5 = Plugin.Log; if (log5 != null) { log5.LogMessage((object)(string.Format("{0} forced '{1}' done: {2}", "[EVENT]", cardId, outcome) + (outcome.Fired ? "; cooldown stamped" : "; cooldown NOT spent (refused)") + ".")); } } else { if (outcome.Fired) { if (ambush) { AmbushDirector ambushDirector2 = this; lastPlan = _wave.LastPlan; ambushDirector2.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.Detail); } if (_state == DirectorState.Cooldown) { _state = DirectorState.Armed; } } } } } internal void ForceEvent(Character player, string cardId, CardArgs args = default(CardArgs)) { //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_01bb: Unknown result type (might be due to invalid IL or missing references) if (_wave.Running || _events.Running) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[EVENT] a card is already running (" + (_events.Running ? _events.RunningId : "wave") + ").")); } } else { if (RefuseForced("card")) { return; } WorldSnapshot world = WorldProbe.Capture(_region.LedgerKey, player, _region.Zone); string text = ((cardId == null) ? _events.Draw(() => Random.value, Time.time, world, stamp: false) : _events.Force(cardId.Trim().ToLowerInvariant(), Time.time, world, stamp: false)); if (text == null) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("[EVENT] forced draw: " + _events.LastNullReason + " — nothing to run.")); } return; } if (((CardArgs)(ref args)).Positional.Count > 0) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogWarning((object)("[EVENT] ignoring bare token(s) '" + string.Join(" ", ((CardArgs)(ref args)).Positional) + "' — card arguments are key=value (did you mean at=here?).")); } } ManualLogSource log4 = Plugin.Log; if (log4 != null) { log4.LogMessage((object)("[EVENT] forced draw -> '" + text + "' (" + _events.LastDrawHow + ")" + ((((CardArgs)(ref args)).Count > 0) ? (" args: " + ((CardArgs)(ref args)).Describe()) : "") + ".")); } Fire(text, player, forced: true, args); } } private string SoftBlock(Character player) { if ((Object)(object)player == (Object)null) { return "no local player"; } if (!player.Alive) { return "player is dead"; } if (!Lifecycle.IsGameplayLive()) { return "gameplay not live (level loading, or still paused by the loader)"; } 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 (_lastOwnActive >= 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; if (why == "own cap reached") { double num = Time.unscaledTime; if (DrConfig.LogVerbose.Value || !(num < _nextOwnCapLogAt)) { _nextOwnCapLogAt = num + 300.0; ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)(string.Format("{0} soft block: {1} — {2}", "[AMBUSH]", why, _lastOwnActive) + $"/{DrConfig.MaxOwnActive.Value} alive. To raise it, edit [Wave] MaxOwnActive " + "in cobalt.dangerousroads.cfg — NOT [Spawner] MaxActiveSpawns " + $"(={Plugin.MaxActiveSpawns.Value}) in cobalt.spawnkit.cfg, which " + $"this check never consults. Retrying in {DrConfig.RetrySeconds.Value:F0}s.")); } } return; } bool flag = _blocks.CountOf(why) == 1; if (flag || DrConfig.LogVerbose.Value) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.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); } private void StandDown(string why) { Disarm(why); _state = DirectorState.Idle; } 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) { //IL_0064: Unknown result type (might be due to invalid IL or missing references) if (_wave.Running || _events.Running) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)"[AMBUSH] a wave is already running."); } } else { if (RefuseForced("wave")) { return; } if (!_region.IsOverworld) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)string.Format("{0} forced wave REFUSED: the roster belongs to an overworld region ({1}).", "[AMBUSH]", _region.LastVerdict)); } return; } _state = DirectorState.WaveInFlight; ((MonoBehaviour)_host).StartCoroutine(_wave.Run(player, _region.LedgerKey, _roster.SpawnableNow(), _recentSpecies, CountOwnActive(), (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 bool RefuseForced(string what) { //IL_0085: Unknown result type (might be due to invalid IL or missing references) if (PhotonNetwork.isNonMasterClientInRoom) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[AMBUSH] forced " + what + " REFUSED: this is a co-op GUEST. DangerousRoads runs on the host only (SpawnKit refuses NotMaster) — ask the host to run it.")); } return true; } if (!DrConfig.Enabled.Value) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogWarning((object)("[AMBUSH] forced " + what + " REFUSED: [General] Enabled=false in cobalt.dangerousroads.cfg. The kill-switch covers the forced path too — flip it and 'reloadcfg', then retry.")); } return true; } if (!_region.IsActiveZone) { ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.LogWarning((object)("[AMBUSH] forced " + what + " REFUSED: no known zone here " + $"({_region.LastVerdict}) — no deck is dealt outside a mapped area.")); } return true; } return false; } 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_00e0: Unknown result type (might be due to invalid IL or missing references) //IL_00e5: Unknown result type (might be due to invalid IL or missing references) //IL_00e9: Unknown result type (might be due to invalid IL or missing references) //IL_00ee: Unknown result type (might be due to invalid IL or missing references) //IL_010e: Unknown result type (might be due to invalid IL or missing references) StringBuilder stringBuilder = new StringBuilder(); Character localPlayer = Plugin.LocalPlayer; CharacterUI val = (((Object)(object)localPlayer != (Object)null) ? localPlayer.CharacterUI : null); if ((Object)(object)val == (Object)null) { return "no CharacterUI (no local player yet?)"; } UICompass componentInChildren = ((Component)val).GetComponentInChildren<UICompass>(true); stringBuilder.AppendLine("CharacterUI '" + ((Object)val).name + "' blips: " + CompassBlips.Describe()); if ((Object)(object)componentInChildren != (Object)null) { RectTransform component = ((Component)componentInChildren).GetComponent<RectTransform>(); 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<char>()); } private static void Walk(Transform t, int depth, int maxDepth, StringBuilder sb) { if (depth > maxDepth) { return; } List<string> list = new List<string>(); Component[] components = ((Component)t).GetComponents<Component>(); 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<RectTransform> Dots = new List<RectTransform>(); internal Transform Root; } private static readonly Dictionary<CharacterUI, Rig> _rigs = new Dictionary<CharacterUI, Rig>(); private static int _consecutiveErrors; private const int ErrorsBeforeDisable = 10; private static bool _disabled; private const float FriendlyScale = 1.4f; private static string _hostileHex; private static Color _hostileColor = Color.red; private static string _friendlyHex; private static Color _friendlyColor = Color.green; internal static bool? CompassFound { get; private set; } internal static int LiveBlips { get; private set; } internal static int LiveFriendlyBlips { 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<UICompass>(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<CharacterUI> list = null; foreach (KeyValuePair<CharacterUI, Rig> 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<CharacterUI>())).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<CharacterUI, Rig> rig in _rigs) { HideFrom(rig.Value, 0); } LiveBlips = 0; LiveFriendlyBlips = 0; } internal static void Tick(IReadOnlyList<Vector3> targets) { Tick(targets, null); } internal static void Tick(IReadOnlyList<Vector3> hostile, IReadOnlyList<Vector3> friendly) { //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_0069: 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_0085: 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_00f5: Unknown result type (might be due to invalid IL or missing references) //IL_00fa: Unknown result type (might be due to invalid IL or missing references) //IL_00fd: 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_0135: Unknown result type (might be due to invalid IL or missing references) //IL_013b: Unknown result type (might be due to invalid IL or missing references) bool value = DrConfig.ShowBlips.Value; bool flag = DrConfig.ShowMerchantBlip.Value || DrConfig.ShowFriendlyBlips.Value; if (_disabled || (!value && !flag)) { ClearAll(); return; } if (!value) { hostile = null; } if (!flag) { friendly = null; } try { Character localPlayer = Plugin.LocalPlayer; if ((Object)(object)localPlayer == (Object)null) { ClearAll(); return; } Vector3 position = ((Component)localPlayer).transform.position; float value2 = DrConfig.BlipRangeMeters.Value; Color color = HostileColor(DrConfig.BlipColor.Value); Color color2 = FriendlyColor(DrConfig.MerchantBlipColor.Value); int num = 0; int num2 = 0; foreach (KeyValuePair<CharacterUI, Rig> rig in _rigs) { Rig value3 = rig.Value; if ((Object)(object)value3.Compass == (Object)null || (Object)(object)value3.Root == (Object)null) { continue; } int num3 = 0; int num4 = 0; int num5 = 0; while (hostile != null && num5 < hostile.Count) { if (Place(value3, hostile[num5], position, value2, color, 1f, num3)) { num3++; } num5++; } int num6 = 0; while (friendly != null && num6 < friendly.Count) { if (Place(value3, friendly[num6], position, float.PositiveInfinity, color2, 1.4f, num3)) { num3++; num4++; } num6++; } HideFrom(value3, num3); if (num3 > num) { num = num3; } if (num4 > num2) { num2 = num4; } } LiveBlips = num; LiveFriendlyBlips = num2; _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 bool Place(Rig rig, Vector3 target, Vector3 origin, float range, Color color, float sizeMult, int index) { //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_0002: Unknown result type (might be due to invalid IL or missing references) //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_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_0056: Unknown result type (might be due to invalid IL or missing references) //IL_0057: 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_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_0064: 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_00a3: Unknown result type (might be due to invalid IL or missing references) //IL_00ab: Unknown result type (might be due to invalid IL or missing references) //IL_00b5: Unknown result type (might be due to invalid IL or missing references) Vector3 val = target - origin; val.y = 0f; if (((Vector3)(ref val)).sqrMagnitude < 0.01f) { return false; } if (!float.IsInfinity(range) && ((Vector3)(ref val)).sqrMagnitude > range * range) { return false; } Vector3 compassDir = rig.Compass.CompassDir; if (((Vector3)(ref compassDir)).sqrMagnitude < 0.001f) { return false; } float num = Vector3.Angle(compassDir, val) * Mathf.Sign(Vector3.Dot(Vector3.up, Vector3.Cross(compassDir, val))); Vector3 localPosition = default(Vector3); float num2 = default(float); if (!rig.Compass.IsVisibleOnCompass(num, ref localPosition, ref num2)) { return false; } RectTransform val2 = DotAt(rig, index, color); if ((Object)(object)val2 == (Object)null) { return false; } ((Transform)val2).localPosition = localPosition; ((Transform)val2).localScale = Vector3.one * (num2 * sizeMult); SetShown(val2, shown: true); return true; } 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<RectTransform>(); ((Transform)component).SetParent(rig.Root, false); component.sizeDelta = new Vector2(10f, 10f); Image component2 = val.GetComponent<Image>(); ((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<Image>(); 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_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) return ParseColor(hex, Color.red); } internal static Color ParseColor(string hex, Color fallback) { //IL_001a: 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 fallback; } return result; } private static Color HostileColor(string hex) { //IL_001f: 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_001a: Unknown result type (might be due to invalid IL or missing references) if (!string.Equals(hex, _hostileHex, StringComparison.Ordinal)) { _hostileHex = hex; _hostileColor = ParseColor(hex); } return _hostileColor; } private static Color FriendlyColor(string hex) { //IL_0024: 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_001a: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) if (!string.Equals(hex, _friendlyHex, StringComparison.Ordinal)) { _friendlyHex = hex; _friendlyColor = ParseColor(hex, Color.green); } return _friendlyColor; } 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 ({LiveBlips - LiveFriendlyBlips} hostile, {LiveFriendlyBlips} friendly)"; } 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<bool> Enabled; internal static ConfigEntry<float> MinIntervalSeconds; internal static ConfigEntry<float> MaxIntervalSeconds; internal static ConfigEntry<float> FirstArmDelaySeconds; internal static ConfigEntry<float> CooldownAfterWaveSeconds; internal static ConfigEntry<float> RetrySeconds; internal static ConfigEntry<int> MinCount; internal static ConfigEntry<int> MaxCount; internal static ConfigEntry<int> MaxOwnActive; internal static ConfigEntry<float> MemberSpacingMeters; internal static ConfigEntry<bool> SkipWhileInCombat; internal static ConfigEntry<float> CombatDeferMaxSeconds; internal static ConfigEntry<float> ClusterRadiusMeters; internal static ConfigEntry<bool> DeckEnabled; internal static ConfigEntry<string> AmbushWhen; internal static ConfigEntry<string> MerchantWhen; internal static ConfigEntry<string> PatrolWhen; internal static ConfigEntry<string> StrangerWhen; internal static ConfigEntry<string> SlumbushWhen; internal static ConfigEntry<float> BlankWeightOverworld; internal static ConfigEntry<float> BlankWeightCity; internal static ConfigEntry<float> BlankWeightDungeon; internal static ConfigEntry<float> SlumbushWeight; internal static ConfigEntry<float> SlumbushCooldownSeconds; internal static ConfigEntry<int> SlumbushDemandSilver; internal static ConfigEntry<int> SlumbushLootSilver; internal static ConfigEntry<float> SlumbushSpawnRadius; internal static ConfigEntry<float> SlumbushDecisionSeconds; internal static ConfigEntry<float> SlumbushLeaveSeconds; internal static ConfigEntry<float> SlumbushStalemateSeconds; internal static ConfigEntry<float> SlumbushReplySeconds; internal static ConfigEntry<float> SlumbushHealth; internal static ConfigEntry<float> SlumbushDamageMult; internal static ConfigEntry<float> SlumbushChanceToAttack; internal static ConfigEntry<float> AmbushWeight; internal static ConfigEntry<float> MerchantWeight; internal static ConfigEntry<float> MerchantCooldownSeconds; internal static ConfigEntry<float> MerchantHealth; internal static ConfigEntry<float> MerchantProtection; internal static ConfigEntry<float> MerchantDamageMult; internal static ConfigEntry<float> MerchantWalkSpeed; internal static ConfigEntry<float> MerchantExitRadius; internal static ConfigEntry<float> MerchantStuckSeconds; internal static ConfigEntry<float> MerchantShopDespawnDeferSeconds; internal static ConfigEntry<float> MerchantMinRouteMeters; internal static ConfigEntry<bool> MerchantPreferRoads; internal static ConfigEntry<float> MerchantRoadHopMeters; internal static ConfigEntry<bool> MerchantDefendsHimself; internal static ConfigEntry<float> MerchantGreetRadius; internal static ConfigEntry<float> MerchantGreetResumeSeconds; internal static ConfigEntry<bool> MerchantProtected; internal static ConfigEntry<float> MerchantRaidChance; internal static ConfigEntry<int> MerchantRaidMin; internal static ConfigEntry<int> MerchantRaidMax; internal static ConfigEntry<float> MerchantRaidTriggerMeters; internal static ConfigEntry<float> MerchantRaidDelaySeconds; internal static ConfigEntry<float> PatrolWeight; internal static ConfigEntry<float> PatrolCooldownSeconds; internal static ConfigEntry<float> StrangerWeight; internal static ConfigEntry<float> StrangerCooldownSeconds; internal static ConfigEntry<int> StrangerMobsMin; internal static ConfigEntry<int> StrangerMobsMax; internal static ConfigEntry<int> StrangerMaxStrangers; internal static ConfigEntry<float> StrangerStalemateSeconds; internal static ConfigEntry<bool> StrangerRewardEnabled; internal static ConfigEntry<int> PatrolGuardMin; internal static ConfigEntry<int> PatrolGuardMax; internal static ConfigEntry<int> PatrolTrogMin; internal static ConfigEntry<int> PatrolTrogMax; internal static ConfigEntry<float> PatrolGuardHealth; internal static ConfigEntry<float> PatrolGuardProtection; internal static ConfigEntry<float> PatrolGuardDamageMult; internal static ConfigEntry<float> PatrolGuardChanceToAttack; internal static ConfigEntry<bool> PatrolGuardProtected; internal static ConfigEntry<bool> PatrolWardOwnSpawns; internal static ConfigEntry<bool> PatrolDefendPlayer; internal static ConfigEntry<float> PatrolDefendRadiusMeters; internal static ConfigEntry<float> PatrolDefendLeashMeters; internal static ConfigEntry<int> PatrolRewardSilverMin; internal static ConfigEntry<int> PatrolRewardSilverMax; internal static ConfigEntry<float> PatrolTrogRingMin; internal static ConfigEntry<float> PatrolTrogRingMax; internal static ConfigEntry<float> PatrolGuardRingMeters; internal static ConfigEntry<float> PatrolStalemateSeconds; internal static ConfigEntry<float> PatrolLingerSeconds; internal static ConfigEntry<float> PatrolLeaveSpeed; internal static ConfigEntry<float> PatrolLeaveTimeoutSeconds; internal static ConfigEntry<float> PatrolRemoveRadiusMeters; internal static ConfigEntry<float> MinDistanceMeters; internal static ConfigEntry<float> MaxDistanceMeters; internal static ConfigEntry<float> PathLengthRatioMax; internal static ConfigEntry<bool> RequireOutOfSight; internal static ConfigEntry<bool> SightCheckAllPlayers; internal static ConfigEntry<string> SourceOrder; internal static ConfigEntry<bool> EnableInteractableAnchors; internal static ConfigEntry<int> MaxCandidatesPerWave; internal static ConfigEntry<float> PlateauProbeRadius; internal static ConfigEntry<bool> WarmOnly; internal static ConfigEntry<int> PrewarmCount; internal static ConfigEntry<float> PrewarmIntervalSeconds; internal static ConfigEntry<string> HarvestLoadingPriority; internal static ConfigEntry<string> ExtraBlocklist; internal static ConfigEntry<bool> FarCacheEnabled; internal static ConfigEntry<float> FarDespawnRadius; internal static ConfigEntry<float> FarRestoreRadius; internal static ConfigEntry<int> FarMaxEntries; internal static ConfigEntry<float> FarMaxAgeMinutes; internal static ConfigEntry<float> FarParkMidFightBeyond; internal static ConfigEntry<bool> ShowToast; internal static ConfigEntry<float> ToastMinGapSeconds; internal static ConfigEntry<bool> ToastBearing; internal static ConfigEntry<bool> AnnounceQuietRoads; internal static ConfigEntry<bool> ShowBlips; internal static ConfigEntry<float> BlipRangeMeters; internal static ConfigEntry<string> BlipColor; internal static ConfigEntry<bool> ShowMerchantBlip; internal static ConfigEntry<string> MerchantBlipColor; internal static ConfigEntry<bool> ShowFriendlyBlips; internal static ConfigEntry<bool> LogVerbose; internal static ConfigEntry<float> LedgerAutoDumpSeconds; internal static ConfigEntry<KeyboardShortcut> ForceWaveKey; internal static void Bind(ConfigFile cfg) { //IL_0e7f: Unknown result type (might be due to invalid IL or missing references) Enabled = cfg.Bind<bool>("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<float>("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<float>("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<float>("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<float>("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<float>("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<int>("Wave", "MinCount", 1, "Fewest creatures in a wave."); MaxCount = cfg.Bind<int>("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<int>("Wave", "MaxOwnActive", 12, "Cap on creatures this mod may have alive at once. THIS IS THE KEY THAT USUALLY STOPS SPAWNING: it is checked BEFORE SpawnKit's own cap, so if waves stall at a number BELOW [Spawner] MaxActiveSpawns in cobalt.spawnkit.cfg, raise this one — raising that one alone will change nothing. Keep it <= that cap: it is GLOBAL ACROSS ALL CONSUMERS, and exceeding it makes SpawnKit toast the player on every refusal. (Defaulted to 6 before 2026-08-02, which silently self-limited every install below SpawnKit's default of 8; an EXISTING cfg keeps the old 6 — BepInEx never migrates a changed default — so edit the file to pick this up.)"); MemberSpacingMeters = cfg.Bind<float>("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<bool>("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<float>("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<float>("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."); DeckEnabled = cfg.Bind<bool>("Events", "DeckEnabled", true, "Draw each due event from the deck (ambush, merchant, ...) instead of always running an ambush. false = the pre-deck mod exactly: every event is a hostile wave, no roll is pulled, the other weights below are ignored. Guards: a new card misbehaving in a live session — flip this + 'reloadcfg' and the old behaviour is back with no relaunch."); AmbushWhen = cfg.Bind<string>("Events", "AmbushWhen", "", "Ambush card. When this card may JOIN the deck — checked against the world before each draw; a card that fails sits out (no roll share, no cooldown) and 'roadsdeck' names why. Empty = always. Grammar: terms joined by '&' (all) and '|' (any), '!' negates: always | night | day | hour=20-6 (wraps midnight) | rain>0.5 | rain<0.2 | flag:<quest event UID> | faction:<name> | region:<area name substring>. Example: 'night & rain<0.5 | flag:SomeQuestUID'. A typo is logged and the card is treated as ALWAYS eligible rather than silently vanishing."); MerchantWhen = cfg.Bind<string>("Events", "MerchantWhen", "", "Merchant card. When this card may JOIN the deck — checked against the world before each draw; a card that fails sits out (no roll share, no cooldown) and 'roadsdeck' names why. Empty = always. Grammar: terms joined by '&' (all) and '|' (any), '!' negates: always | night | day | hour=20-6 (wraps midnight) | rain>0.5 | rain<0.2 | flag:<quest event UID> | faction:<name> | region:<area name substring>. Example: 'night & rain<0.5 | flag:SomeQuestUID'. A typo is logged and the card is treated as ALWAYS eligible rather than silently vanishing."); PatrolWhen = cfg.Bind<string>("Events", "PatrolWhen", "", "Patrol card. When this card may JOIN the deck — checked against the world before each draw; a card that fails sits out (no roll share, no cooldown) and 'roadsdeck' names why. Empty = always. Grammar: terms joined by '&' (all) and '|' (any), '!' negates: always | night | day | hour=20-6 (wraps midnight) | rain>0.5 | rain<0.2 | flag:<quest event UID> | faction:<name> | region:<area name substring>. Example: 'night & rain<0.5 | flag:SomeQuestUID'. A typo is logged and the card is treated as ALWAYS eligible rather than silently vanishing."); StrangerWhen = cfg.Bind<string>("Events", "StrangerWhen", "", "Stranger card. When this card may JOIN the deck — checked against the world before each draw; a card that fails sits out (no roll share, no cooldown) and 'roadsdeck' names why. Empty = always. Grammar: terms joined by '&' (all) and '|' (any), '!' negates: always | night | day | hour=20-6 (wraps midnight) | rain>0.5 | rain<0.2 | flag:<quest event UID> | faction:<name> | region:<area name substring>. Example: 'night & rain<0.5 | flag:SomeQuestUID'. A typo is logged and the card is treated as ALWAYS eligible rather than silently vanishing."); SlumbushWhen = cfg.Bind<string>("Events", "SlumbushWhen", "", "Slumbush card (Levant slum shakedown). When this card may JOIN the deck — checked against the world before each draw; a card that fails sits out (no roll share, no cooldown) and 'roadsdeck' names why. Empty = always. Grammar: terms joined by '&' (all) and '|' (any), '!' negates: always | night | day | hour=20-6 (wraps midnight) | rain>0.5 | rain<0.2 | flag:<quest event UID> | faction:<name> | region:<area name substring>. Example: 'night & rain<0.5 | flag:SomeQuestUID'. A typo is logged and the card is treated as ALWAYS eligible rather than silently vanishing. The card's own rule (zone:city & region:Levant) always applies underneath this one."); BlankWeightOverworld = cfg.Bind<float>("Events", "BlankWeightOverworld", 0f, "Weight of the BLANK card (an event that does nothing) in the overworld deck. 0 = the road deck exactly as before blanks existed. Raising it thins out road events without touching their relative shares."); BlankWeightCity = cfg.Bind<float>("Events", "BlankWeightCity", 0.97f, "Weight of the blank card in a TOWN/CITY deck. Cities are dealt a deck too now, but it is almost all blanks: with Slumbush at 0.03 this gives the shakedown ~3% of city draws. The road cards (ambush/merchant/patrol/stranger) never join a city deck."); BlankWeightDungeon = cfg.Bind<float>("Events", "BlankWeightDungeon", 1f, "Weight of the blank card in a DUNGEON deck. No other card joins a dungeon deck yet, so every dungeon draw is a blank — the seam is there for the first dungeon card."); SlumbushWeight = cfg.Bind<float>("Events", "SlumbushWeight", 0.03f, "Draw weight of the Slumbush card: in Levant, a shaking voice behind you wants 200 silver — pay, or fight 1–3 ragged thugs with iron. Only ever joins a city deck in Levant. With BlankWeightCity at 0.97 this is ~3% of city draws. 0 disables it."); SlumbushCooldownSeconds = cfg.Bind<float>("Events", "SlumbushCooldownSeconds", 1800f, "Shortest gap between two Slumbush draws, seconds of actual play, stamped when drawn."); SlumbushDemandSilver = cfg.Bind<int>("Slumbush", "DemandSilver", 200, "What the voice asks for. Paying removes exactly this from your inventory (bag first, then pouch — the vanilla RemoveMoney order). If you cannot cover it the thugs attack anyway."); SlumbushLootSilver = cfg.Bind<int>("Slumbush", "LootSilver", 60, "Silver stocked in ONE thug's pouch at spawn; loot his corpse for it. Paid thugs leave with it."); SlumbushSpawnRadius = cfg.Bind<float>("Slumbush", "SpawnRadius", 7f, "Metres behind the player the thugs appear (StoryKit RingSpawner: navmesh-snapped, collision-checked, spaced; blocked slots fall back to nearby angles/radii)."); SlumbushDecisionSeconds = cfg.Bind<float>("Slumbush", "DecisionSeconds", 60f, "Longest the player is held with the dialogue open before it counts as refusing. Real seconds."); SlumbushReplySeconds = cfg.Bind<float>("Slumbush", "ReplySeconds", 2.5f, "How long the thug's answer stays on screen after you choose, before the box is closed for you."); SlumbushLeaveSeconds = cfg.Bind<float>("Slumbush", "LeaveSeconds", 3f, "After a payment, how long the thugs stand before they are removed."); SlumbushStalemateSeconds = cfg.Bind<float>("Slumbush", "StalemateSeconds", 180f, "Longest a fight is watched; LIVING thugs still standing after this are removed. Corpses stay lootable."); SlumbushHealth = cfg.Bind<float>("Slumbush", "ThugHealth", 120f, "Each thug's max health."); SlumbushDamageMult = cfg.Bind<float>("Slumbush", "ThugDamageMult", 0.8f, "Multiplier on the damage a thug deals (1 = unchanged)."); SlumbushChanceToAttack = cfg.Bind<float>("Slumbush", "ThugChanceToAttack", 70f, "AI attack eagerness, percent."); AmbushWeight = cfg.Bind<float>("Events", "AmbushWeight", 1f, "Draw weight of the classic hostile wave. Weights are RELATIVE (share = weight / sum of eligible weights), so 1.0 here and 0.35 on the merchant is ~74% ambush. 0 disables the ambush — which, with the merchant also at 0 or absent, means NO event ever fires and the mod looks dead; 'roadsdeck' shows drawable=0 when that is the case."); MerchantWeight = cfg.Bind<float>("Events", "MerchantWeight", 0.35f, "Draw weight of the wandering-merchant card. Needs the merchant runner present in the build: a weight with no runner is logged once and EXCLUDED from the draw rather than silently eating a share of events. Guards: a quarter of events vanishing into a card that cannot run."); MerchantCooldownSeconds = cfg.Bind<float>("Events", "MerchantCooldownSeconds", 600f, "Shortest gap between two merchant draws, in seconds of ACTUAL PLAY (Time.time). The cooldown is stamped when the card is DRAWN, not when it succeeds, so a merchant that refuses (no exit, no body) is not redrawn every RetrySeconds. While it cools, its weight leaves the pool entirely: the ambush takes those draws, nothing falls through to 'no event'. Guards: a road with a trader on it every minute."); PatrolWeight = cfg.Bind<float>("Events", "PatrolWeight", 0.25f, "Draw weight of the town-guard patrol card (guards fighting troglodytes on the road). Relative, like the others: 1.0/0.35/0.25 is roughly 63%/22%/16%. It is the most expensive card in the deck — a patrol mints up to three StoryKit NPCs AND six trogs at once — which is why it sits below the merchant. 0 disables it; a weight with no runner is logged once and excluded from the draw."); PatrolCooldownSeconds = cfg.Bind<float>("Events", "PatrolCooldownSeconds", 900f, "Shortest gap between two patrol draws, in seconds of ACTUAL PLAY (Time.time). Longer than the merchant's: the guards hold spawn slots until they have walked out of render range, so back-to-back patrols would starve the ambush card of capacity. Stamped when the card is DRAWN, not when it succeeds."); StrangerWeight = cfg.Bind<float>("Events", "StrangerWeight", 0.25f, "Draw weight of the familiar-stranger card: one or two of YOUR OTHER saved characters are found on the road fighting this region's mobs. Save them and the survivor offers one item out of that character's real backpack — permanently moved to this one. Relative, like the others. 0 disables it. With no other saved character on this machine the card never fires (a soft block, logged once at boot)."); StrangerCooldownSeconds = cfg.Bind<float>("Events", "StrangerCooldownSeconds", 900f, "Shortest gap between two stranger draws, in seconds of ACTUAL PLAY (Time.time). Stamped when the card is DRAWN, not when it succeeds."); StrangerMobsMin = cfg.Bind<int>("Stranger", "MobsMin", 2, "Fewest mobs the strangers are fighting. Region-native species, exactly the ambush roster. Below two a single traveller handles it alone and there is no rescue."); StrangerMobsMax = cfg.Bind<int>("Stranger", "MobsMax", 4, "Most mobs. They count against [Wave] MaxOwnActive like wave members. An inverted band clamps to MobsMin."); StrangerMaxStrangers = cfg.Bind<int>("Stranger", "MaxStrangers", 2, "Most saved characters brought onto the road at once (1 or 2; more is clamped to 2 by the card — three look-alike bodies is a template mint the frame cannot afford). Never more than there are other characters on this machine."); StrangerStalemateSeconds = cfg.Bind<float>("Stranger", "StalemateSeconds", 90f, "After this long of Fighting the encounter resolves regardless (a mob stuck on a rock must not hold the strangers there forever). No rescue on a stalemate: the mobs are not dead. Live."); StrangerRewardEnabled = cfg.Bind<bool>("Stranger", "RewardEnabled", true, "false = the strangers say the rescue line but never open the backpack, and no save is ever written. The kill-switch for the save write-back while it is unverified."); MerchantHealth = cfg.Bind<float>("Merchant", "Health", 450f, "The merchant's max health (vanilla player ~100). He is meant to SURVIVE a bandit raid long enough for you to reach him — but not to shrug it off: at 100 a two-bandit raid kills him before the toast has faded and the rescue greeting is never seen, while a pool this side of 500 still leaves the rescue urgent. Applies to the next spawn."); MerchantProtection = cfg.Bind<float>("Merchant", "Protection", 20f, "Flat damage protection, all types. Same purpose as Health — a tank that is barely a threat. Applies to the next spawn."); MerchantDamageMult = cfg.Bind<float>("Merchant", "DamageMult", 0.25f, "Multiplier on the damage he deals (1 = unchanged). Low on purpose: if he can clear a raid alone the player has nothing to do, and 'rescued' never fires. Applies to the next spawn."); MerchantWalkSpeed = cfg.Bind<float>("Merchant", "WalkSpeed", 0.5f, "Wander-state speed modifier: a MULTIPLIER on the body's run speed (AISWander.SpeedModif), not m/s. Scale: vanilla wanderers use 0.3 (an amble), SideLoader's default is 1.1 (a jog — live 2026-08-22 he 'kept running from me'); 0.5 reads as a purposeful walk. Too high and he outruns a player who wants to trade; too low and a long route times out as 'stuck'. Live. (An EXISTING cfg keeps the old 1.1 — BepInEx never migrates a default.)"); MerchantExitRadius = cfg.Bind<float>("Merchant", "ExitRadius", 6f, "How close to the zone exit counts as arrived (meters, flat) — he despawns there. Too small and a gate whose trigger sits off the navmesh is never 'reached' and the walk ends as 'stuck' instead; too large and he vanishes in plain view short of the gate. 0 or less never arrives. Live."); MerchantStuckSeconds = cfg.Bind<float>("Merchant", "StuckSeconds", 90f, "Despawn after this long with no progress toward the current waypoint, OUTSIDE combat. Guards: a merchant wedged on a rock or pathing against a cliff standing there for the rest of the zone visit. Fights do not count as stuck. 0 = never give up. Live."); MerchantShopDespawnDeferSeconds = cfg.Bind<float>("Merchant", "DespawnDeferSeconds", 180f, "He never despawns while his shop panel or his dialogue is open — arrival at the exit and a 'stuck' give-up both WAIT (he stands still) until it closes. Guards: vanilla's Merchant.OnDestroy never runs OnQuitShopCleanup, so destroying him mid-trade leaves the shopper's UI up with a transaction that can never complete (no silver or goods are lost, but the panel is wedged). This is the ceiling on that wait: past it he leaves anyway with a warning, so a peer who wandered off with a panel open cannot pin him in the world for the session. 0 or less = wait for as long as the shop reads open. Live."); MerchantMinRouteMeters = cfg.Bind<float>("Merchant", "MinRouteMeters", 150f, "Prefer an exit at least this far from the spawn spot. Guards: a merchant who spawns beside a gate and despawns before the player can reach him. When no exit is that far, the farthest one is used. Applies to the next spawn."); MerchantPreferRoads = cfg.Bind<bool>("Merchant", "PreferRoads", true, "Route the walk seed-to-seed along the roadside spawn spots (the game's own caravanner points) instead of only through a straight spawn->exit corridor, so the merchant follows a road that CURVES rather than striking off cross-country. The chained route is still path-checked leg by leg; where the chain finds nothing the old corridor route is used. false = the pre-2026-09 corridor-only behaviour. Applies to the next spawn."); MerchantRoadHopMeters = cfg.Bind<float>("Merchant", "RoadHopMeters", 120f, "PreferRoads: the longest hop (meters, flat) between consecutive roadside seeds in the chained route. Too small and a sparse stretch of road breaks the chain (falls back to the corridor); too large and the chain can jump between parallel roads. Applies to the next spawn."); MerchantDefendsHimself = cfg.Bind<bool>("Merchant", "DefendsHimself", true, "true = he fights back against Bandits only (weakly, per DamageMult) and NEVER targets the player. false = fully passive; he targets nothing and just takes it. Either way he can be attacked — passive is for a session where his swings confuse the picture. Applies to the next spawn."); MerchantGreetRadius = cfg.Bind<float>("Merchant", "GreetRadius", 4f, "He stops walking and turns to face you while any player is within this many metres (or is in his dialogue). Guards: the live 2026-08-22 'he kept running from me even when I tried talking' — the talk prompt needs a body that holds still. Too large and he stands around for anyone passing on the road; 0 = never stops (dialogue still holds him). Live."); MerchantGreetResumeSeconds = cfg.Bind<float>("Merchant", "GreetResumeSeconds", 3f, "After the last player leaves GreetRadius (and nobody is in dialogue), how long he waits before walking on. Guards: a player stepping out of range to check their bag and back watching him set off and turn round. 0 = resumes the moment you step away. Live."); MerchantProtected = cfg.Bind<bool>("Merchant", "Protected", true, "The player SIDE cannot hurt him: your weapons, lock-on, your pet and its combat anchor and every Player-faction ally treat him as untargetable (AggroKit Protect), while the raid's bandits are explicitly re-allowed. Guards: the live 2026-08-22 session where the pet's anchor detected him as an enemy, dragged him into combat and he never walked again. false = anyone can hit him (the pre-fix behaviour; for a session that needs to damage him by hand). Applies to the next spawn."); MerchantRaidChance = cfg.Bind<float>("Merchant", "RaidChance", 0.75f, "Probability (0..1) that a merchant gets raided by bandits during his walk. Rolled once at his spawn. 0 = never (the phase-1 lone walker); 1 = always. Guards: a road where EVERY trader is a fight, which makes the rescue greeting routine instead of earned. Applies to the next spawn."); MerchantRaidMin = cfg.Bind<int>("Merchant", "RaidMin", 2, "Fewest raiders. Applies to the next spawn."); MerchantRaidMax = cfg.Bind<int>("Merchant", "RaidMax", 3, "Most raiders. Each one counts against [Wave] MaxOwnActive and SpawnKit's global cap like a wave member, so a raid that lands short of this is the cap, not a bug (`roadsmerchant status` says how many actually spawned). An inverted band clamps to RaidMin. Applies to the next spawn."); MerchantRaidTriggerMeters = cfg.Bind<float>("Merchant", "RaidTriggerMeters", 90f, "The raid springs when a player comes within this many metres of the merchant, so you WITNESS it rather than find a corpse. Keep it inside [FarCache] DespawnRadius and within earshot of the toast. 0 = never by proximity (the delay below still fires). Live."); MerchantRaidDelaySeconds = cfg.Bind<float>("Merchant", "RaidDelaySeconds", 120f, "The raid also springs this many seconds of play after his spawn whether or not anyone is near — a merchant the player never chases still gets his story, and the rescue greeting stays reachable on a later meeting. 0 = proximity only. Live."); PatrolGuardMin = cfg.Bind<int>("Patrol", "GuardMin", 2, "Fewest town guards in a patrol. Two is the floor at which the fight reads as a PATROL rather than one unlucky soldier — and at which the party can lose a member and still have somebody left to thank you. Applies to the next spawn."); PatrolGuardMax = cfg.Bind<int>("Patrol", "GuardMax", 3, "Most town guards in a patrol. Guards: a squad big enough to win without you, which would make the whole card a cutscene. An inverted band clamps to GuardMin."); PatrolTrogMin = cfg.Bind<int>("Patrol", "TrogMin", 4, "Fewest troglodytes the patrol is fighting. Below the guard count the guards are never in real danger and the reward row stops meaning anything."); PatrolTrogMax = cfg.Bind<int>("Patrol", "TrogMax", 6, "Most troglodytes. They count against [Wave] MaxOwnActive like wave members, so a patrol that lands short of this is the cap, not a bug ('roadspatrol status' says how many actually spawned). An inverted band clamps to TrogMin."); PatrolGuardHealth = cfg.Bind<float>("Patrol", "GuardHealth", 450f, "A guard's max health (vanilla player ~100). The card's whole point is that the guards are in REAL danger without you: high enough that they do not evaporate before you arrive, low enough that six trogs can take them. Applies to the next spawn."); PatrolGuardProtection = cfg.Bind<float>("Patrol", "GuardProtection", 12f, "Flat all-type damage protection on a guard — they are the ones in plate. Too high and the trogs plink harmlessly and every fight ends in the stalemate window. Next spawn."); PatrolGuardDamageMult = cfg.Bind<float>("Patrol", "GuardDamageMult", 0.9f, "Guard damage multiplier (1 = unchanged). Slightly under 1 so a three-guard party does not wipe six trogs before the player can land a hit and earn the reward. Next spawn."); PatrolGuardChanceToAttack = cfg.Bind<float>("Patrol", "GuardChanceToAttack", 85f, "AISCombatMelee.ChanceToAttack for a guard, in percent. High: a road detail that circles and blocks instead of swinging reads as broken, not as tactical. Next spawn."); PatrolGuardProtected = cfg.Bind<bool>("Patrol", "GuardProtected", true, "Shield the guards from the PLAYER side (AggroKit Protect) — weapons, lock-on, pets and their anchors. Guards the failure the road merchant already hit live: a pet's anchor detected the friendly NPC, dragged him into combat and he never did anything else. The trogs get explicit pairwise re-allows, so the fight itself is unaffected. false = the guards are ordinary characters and you can hit them. Applies at spawn."); PatrolWardOwnSpawns = cfg.Bind<bool>("Patrol", "WardOwnSpawns", true, "Kill switch for the ShieldWard pairing tick (2026-09-04): with GuardProtected on, every live DangerousRoads spawn is pair-re-allowed onto the shielded guards so it can fight back (the Marsh-Archer-at-38/300-hp bug). false = stop installing those pairs WITHOUT dropping the shields, so the player-side protection stays intact — the safe lever if the ward misbehaves live. The player is never re-allowed either way. Live (read every tick; flips with reloadcfg)."); PatrolDefendPlayer = cfg.Bind<bool>("Patrol", "DefendPlayer", false, "Let the patrol's guards step in against a hostile that is already fighting YOU (or your pet) close by, not just against their own troglodytes. The guards never START a fight: a candidate must already be locked onto a Player-faction body, must be in one of the hostile factions (Bandits, Mercs, Tuanosaurs, Hounds, CorruptionSpirit), and must not be on the [Species] blocklist (unique NPCs, bosses, the plot-faction Wolfgang family). Ships OFF for 0.1.8 — it is new the day before a release and a guard drawn off his trogs is a fight nobody has watched a hundred times yet. Live: read every tick, so `reloadcfg` turns it on mid-encounter."); PatrolDefendRadiusMeters = cfg.Bind<float>("Patrol", "DefendRadiusMeters", 18f, "How close to a PLAYER a hostile must be before the guards mind it, in metres. Measured to the nearest player, not to the guard — the point is protecting you. 18 matches TrogRingMax, so 'close' means inside the encounter's own footprint. Live."); PatrolDefendLeashMeters = cfg.Bind<float>("Patrol", "DefendLeashMeters", 35f, "How far from the patrol's centre a defended fight may travel before the guards let it go, in metres. A guard chasing a hyena over a ridge is worse than a guard who ignored it, and it strands the walk-away. 0 = no leash (not recommended). Live."); PatrolRewardSilverMin = cfg.Bind<int>("Patrol", "RewardSilverMin", 5, "Fewest silver per spawned-and-now-dead troglodyte, if you HELPED (any damage to any of the patrol's trogs counts — yours, your pet's, an arrow's). The rate is rolled ONCE per encounter, so the purse is a price and not a slot machine. Live."); PatrolRewardSilverMax = cfg.Bind<int>("Patrol", "RewardSilverMax", 6, "Most silver per dead troglodyte. With 4-6 trogs that is a 20-36 silver purse — a meaningful morning's pay, not an income. 0 on both = no reward row at all. Live."); PatrolTrogRingMin = cfg.Bind<float>("Patrol", "TrogRingMin", 12f, "Closest the troglodytes may be placed to the guards, in metres. Too close and they spawn inside the party with no charge for the player to see. Live."); PatrolTrogRingMax = cfg.Bind<float>("Patrol", "TrogRingMax", 18f, "Farthest the troglodytes may be placed. Beyond ~20 m they can lose the guards on the navmesh entirely and every fight ends in the stalemate window. Live."); PatrolGuardRingMeters = cfg.Bind<float>("Patrol", "GuardRingMeters", 4f, "Radius of the small ring the guards themselves stand on. Guards: three bodies minted on the same point and shoved apart by physics. Live."); PatrolStalemateSeconds = cfg.Bind<float>("Patrol", "StalemateSeconds", 90f, "How long a patrol may stay in Fighting before it resolves regardless. An archmage stuck on a rock and a guard who cannot path to it will plink at each other forever, and the guards would then never walk away or free their slots. 0 = never (a fight must then end by a wipe). Live."); PatrolLingerSeconds = cfg.Bind<float>("Patrol", "LingerSeconds", 20f, "How long the survivors stand around after the fight before they set off. Extended while you are talking to one — walking off mid-sentence is how the merchant's greet bug read. Live."); PatrolLeaveSpeed = cfg.Bind<float>("Patrol", "LeaveSpeed", 1.4f, "Walk speed for the guards' exit (the merchant's is 0.5 — these are soldiers going off duty, not a mule train). Live, read when the walk starts."); PatrolLeaveTimeoutSeconds = cfg.Bind<float>("Patrol", "LeaveTimeoutSeconds", 300f, "Give up on the walk after this long and remove the guards anyway — but ONLY once they are farther than RemoveRadiusMeters from every player, so a guard never blinks out in front of anyone. A guard wedged on terrain would otherwise hold a StoryKit spec, an AggroKit shield and a body for the rest of the session. Live."); PatrolRemoveRadiusMeters = cfg.Bind<float>("Patrol", "RemoveRadiusMeters", 120f, "Remove the guards once they are farther than this from EVERY player — the whole point of the walk-away is to give the capacity back. Keep it in step with [FarCache] DespawnRadius: below it and guards vanish while a player could still see them, far above it and they linger long past the point of interest. Live."); MinDistanceMeters = cfg.Bind<float>("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<float>("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<float>("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<bool>("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<bool>("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<string>("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<bool>("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<int>("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<float>("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<bool>("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<int>("Species", "PrewarmCount", 3, "How many of the region's cold species to warm in the background. Also bounds how many the host asks the ROOM to warm per push. SpawnKit keeps its own ceiling on that half — [Coop] RoomWarmRequestCap in BepInEx/config/cobalt.spawnkit.cfg, which caps species asked of ONE peer so a big roster cannot spend a guest's whole harvest budget in one breath — so the effective ask is the MINIMUM of the two, and raising this alone will not get more asked. Guards: an empty warm roster meaning the mod is silent for a whole region."); PrewarmIntervalSeconds = cfg.Bind<float>("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."); HarvestLoadingPriority = cfg.Bind<string>("Species", "HarvestLoadingPriority", "", "EXPERIMENTAL, off by default. Unity's Application.backgroundLoadingPriority is a main-thread millisecond budget for INTEGRATING an async load (Low~2ms, BelowNormal~4ms and the engine default, Normal~10ms, High~50ms), and this mod has never set it. Setting this to Low makes a background donor harvest take longer in wall clock in exchange for a possibly smaller worst frame. Empty (or 'unchanged' / an unrecognised value) leaves the engine exactly as it is, which is the shipped behaviour. Valid: Low, BelowNormal, Normal, High. It is set only for the width of one background prewarm and restored immediately afterwards — it is GLOBAL engine state that Outward's own zone loads read too, so it is never held across them. Guards: nothing; it exists so the stall can be A/B measured live ([ALIVE] worstMs against [HARVEST] donor scene loaded in Ns) without a rebuild."); ExtraBlocklist = cfg.Bind<string>("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."); FarCacheEnabled = cfg.Bind<bool>("FarCache", "Enabled", true, "Far-despawn + restore: a wave creature farther than DespawnRadius from EVERY player is silently despawned (no loot, no toast) and remembered — position, facing, health — then respawned in place when a player comes back within RestoreRadius. Guards: abandoned waves pinning [Wave] MaxOwnActive slots (and their share of SpawnKit's global cap) for the whole zone visit, which is what used to starve later waves."); FarDespawnRadius = cfg.Bind<float>("FarCache", "DespawnRadius", 120f, "Park a creature when every player is farther than this (meters, 3D). Keep it well above [Placement] MaxDistanceMeters' aggro tail and vanilla perception (~30-50m): despawning something the player is still fighting or watching reads as a vanish. A creature that is actually fighting is never parked regardless of distance — read from its AI's combat state and locked target, NOT from Character.InCombat, which is structurally always false for an AI body (fixed 2026-08-30; before that the exemption never fired and far fights were parked mid-swing)."); FarRestoreRadius = cfg.Bind<float>("FarCache", "RestoreRadius", 80f, "Respawn a parked creature when any player comes within this of its parked spot (meters). CLAMPED in code to at most 0.8 x DespawnRadius — the gap is the hysteresis that stops a boundary-straddling creature from park/restore flapping."); FarMaxEntries = cfg.Bind<int>("FarCache", "MaxEntries", 24, "Ledger cap; past it the OLDEST parked creature is forgotten. Bounds how much of the zone's history a long session drags around."); FarMaxAgeMinutes = cfg.Bind<float>("FarCache", "MaxAgeMinutes", 20f, "Forget a parked creature after this long (minutes; 0 = never). An ambush the player outran an hour ago waiting at the same spot forever reads as a haunting, not a restore."); FarParkMidFightBeyond = cfg.Bind<float>("FarCache", "ParkMidFightBeyond", 0f, "The one way to park a creature that is FIGHTING: a hard distance (meters, 3D) past which even an active fight is parked. 0 (the default) = never, which is the rule DespawnRadius describes. Why the knob exists: until 2026-08-30 the 'never park a fight' exemption never actually fired (it asked Character.InCombat, which is structurally always false for an AI body), so every far fight WAS parked mid-swing. Making the exemption real is a balance change — a fight you run away from now stays live and keeps holding a [Wave] MaxOwnActive slot until it disengages, so waves can feel more persistent and later ones can start later. Set this (try 2-3x DespawnRadius, e.g. 300) to get a bounded version of the old relief valve back. Values below DespawnRadius are raised to it — a fighting creature is never parked closer than a peaceful one."); if (FarParkMidFightBeyond.Value > 0f && FarParkMidFightBeyond.Value < FarDespawnRadius.Value) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)($"[FARCACHE] ParkMidFightBeyond={FarParkMidFightBeyond.Value:F0}m is below " + $"DespawnRadius={FarDespawnRadius.Value:F0}m — using {FarDespawnRadius.Value:F0}m (a fighting " + "creature is never parked closer than a peaceful one).")); } } ShowToast = cfg.Bind<bool>("Notify", "ShowToast", true, "Show an on-screen message when a wave arrives."); ToastMinGapSeconds = cfg.Bind<float>("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<bool>("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'."); AnnounceQuietRoads = cfg.Bind<bool>("Coop", "AnnounceQuietRoads", false, "HOST ONLY, and off by default. When a co-op encounter is refused because a peer cannot show the species yet (PR2's room-warm gate), show the player one toast per region saying so. Guards: 'the roads went quiet in multiplayer' being filed as a regression when it is the gate doing its job — the host log line ('co-op encounters refused in ...') names the species and the cold peers either way, and 'skwarmdump' prints each peer's row. Testers turn this on; it stays off for players because the honest answer to a quiet road is normally 'wait a moment'. How strict the gate itself is lives in SpawnKit: [Coop] RoomWarmMode in BepInEx/config/cobalt.spawnkit.cfg."); ShowBlips = cfg.Bind<bool>("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<float>("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<string>("Compass", "BlipColor", "#FF3B30", "Blip colour, #RRGGBB or #RRGGBBAA. An unparseable value falls back to red rather than to an invisible blip."); ShowMerchantBlip = cfg.Bind<bool>("Compass", "ShowMerchantBlip", true, "Mark the wandering merchant on the HUD compass while he is on the road — from the toast until he despawns, at ANY distance (BlipRangeMeters does not apply: he is somewhere to walk toward, not a nearby threat). Guards: a toast that says 'a merchant is on the road to the south' and no way to find him before he reaches the gate. Independent of ShowBlips."); MerchantBlipColor = cfg.Bind<string>("Compass", "MerchantBlipColor", "#34C759", "The merchant's compass dot colour, #RRGGBB or #RRGGBBAA; drawn 1.4x the size of a hostile blip so it reads as a destination, not a threat. An unparseable value falls back to green rather than to an invisible (or red, hostile-looking) dot."); ShowFriendlyBlips = cfg.Bind<bool>("Compass", "ShowFriendlyBlips", true, "Mark every OTHER friendly this mod's events put on the road — patrol guards, the familiar strangers (the traveller under attack) — with the same green dot the merchant gets (MerchantBlipColor, 1.4x size, any distance). A body blips while it is alive and enrolled in this mod's own protective shield roster (only player-side bodies ever are); the dot vanishes on death, despawn or region change. Its own key, like ShowMerchantBlip, because these are destinations to walk toward, not the nearby-threat radar ShowBlips governs. 'roadsblips' explains each body."); LogVerbose = cfg.Bind<bool>("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<float>("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<KeyboardShortcut>("Keys", "ForceWaveKey", new KeyboardShortcut((KeyCode)0, Array.Empty<KeyCode>()), "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 · " + $"[Events] deck={DeckEnabled.Value} blank(ow/city/dng)={BlankWeightOverworld.Value:F2}/{BlankWeightCity.Value:F2}/{BlankWeightDungeon.Value:F2} slumbush={SlumbushWeight.Value:F2} ambush={AmbushWeight.Value:F2} " + $"merchant={MerchantWeight.Value:F2}/{MerchantCooldownSeconds.Value:F0}s " + $"patrol={PatrolWeight.Value:F2}/{PatrolCooldownSeconds.Value:F0}s " + $"stranger={StrangerWeight.Value:F2}/{StrangerCooldownSeconds.Value:F0}s · " + $"[Merchant] hp={MerchantHealth.Value:F0} prot={MerchantProtection.Value:F0} " + $"dmg={MerchantDamageMult.Value:F2} speed={MerchantWalkSpeed.Value:F2} " + $"exit={MerchantExitRadius.Value:F0}m stuck={MerchantStuckSeconds.Value:F0}s " + $"despawnDefer={MerchantShopDespawnDeferSeconds.Value:F0}s " + $"minRoute={MerchantMinRouteMeters.Value:F0}m defends={MerchantDefendsHimself.Value} " + $"greet={MerchantGreetRadius.Value:F1}m/{MerchantGreetResumeSeconds.Value:F0}s protected={MerchantProtected.Value} " + $"raid={MerchantRaidChance.Value:F2}x{MerchantRaidMin.Value}-{MerchantRaidMax.Value} " + $"trigger={MerchantRaidTriggerMeters.Value:F0}m/{MerchantRaidDelaySeconds.Value:F0}s · " + $"[Patrol] guards={PatrolGuardMin.Value}-{PatrolGuardMax.Value} " + $"trogs={PatrolTrogMin.Value}-{PatrolTrogMax.Value} " + $"guardHp={PatrolGuardHealth.Value:F0} prot={PatrolGuardProtection.Value:F0} " + $"dmg={PatrolGuardDamageMult.Value:F2} atk={PatrolGuardChanceToAttack.Value:F0}% " + $"protected={PatrolGuardProtected.Value} " + $"defend={PatrolDefendPlayer.Value}@{PatrolDefendRadiusMeters.Value:F0}m/leash {PatrolDefendLeashMeters.Value:F0}m " + $"reward={PatrolRewardSilverMin.Value}-{PatrolRewardSilverMax.Value}/trog " + $"rings={PatrolGuardRingMeters.Value:F0}m/{PatrolTrogRingMin.Value:F0}-{PatrolTrogRingMax.Value:F0}m " + $"stalemate={PatrolStalemateSeconds.Value:F0}s linger={PatrolLingerSeconds.Value:F0}s " + $"leave={PatrolLeaveSpeed.Value:F2}/{PatrolLeaveTimeoutSeconds.Value:F0}s " + $"remove={PatrolRemoveRadiusMeters.Value:F0}m · " + $"[Stranger] mobs={StrangerMobsMin.Value}-{StrangerMobsMax.Value} " + $"max={StrangerMaxStrangers.Value} stalemate={StrangerStalemateSeconds.Value:F0}s " + $"reward={StrangerRewardEnabled.Value} · " + $"[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}' · " + $"[FarCache] enabled={FarCacheEnabled.Value} " + $"radii={FarDespawnRadius.Value:F0}/{FarCache.ClampRestoreRadius(FarDespawnRadius.Value, FarRestoreRadius.Value):F0}m " + $"cap={FarMaxEntries.Value} maxAge={FarMaxAgeMinutes.Value:F0}min " + "parkMidFightBeyond=" + ((FarParkMidFightBeyond.Value <= 0f) ? "never" : $"{ParkRule.EffectiveMidFightRadius(FarDespawnRadius.Value, FarParkMidFightBeyond.Value):F0}m") + " · " + $"[Compass] blips={ShowBlips.Value}/{BlipRangeMeters.Value:F0}m merchantBlip={ShowMerchantBlip.Value} friendlyBlips={ShowFriendlyBlips.Value} " + "merchantColor=" + MerchantBlipColor.Value + " · " + $"[Diag] verbose={LogVerbose.Value}"; } } internal static class FactionBook { internal static readonly SortedDictionary<string, string> Learned = new SortedDictionary<string, string>(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; } if (RosterFilter.IsBlocked(text, (IEnumerable<string>)null)) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)("[ROSTER] observed '" + text + "' = " + text2 + " but it is BLOCKLISTED — recorded for this session only, and deliberately NOT offered for paste-back into SpeciesFactions.txt (its faction is plot-driven; a committed row would be a snapshot masquerading as a fact).")); } return; } Learned[text] = text2; if (text3 == null) { ManualLogSource log2 = Plugin.Log; if (log2 != null) { log2.LogMessage((object)("[ROSTER] learned '" + text + "' = " + text2 + " (not in the shipped table).")); } return; } ManualLogSource log3 = Plugin.Log; if (log3 != null) { log3.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<AISquad> squads) { //IL_0066: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Invalid comparison between Unknown and I4 //IL_0077: 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 val = squads[i]; List<AISquadMember> list = (((Object)(object)val != (Object)null) ? val.Members : null); if (list == null) { continue; } for (int j = 0; j