using System; 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("1.0.0.0")] [assembly: AssemblyInformationalVersion("1.0.0+fb85873cdf7f439cbcb58ddab6d8ef85c9144594")] [assembly: AssemblyProduct("DangerousRoads.Core")] [assembly: AssemblyTitle("DangerousRoads.Core")] [assembly: AssemblyMetadata("BuildStamp", "fb85873 2026-08-02")] [assembly: AssemblyVersion("1.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 _counts = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly List _order = new List(); 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 list = new List(_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 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 sealed class FactionTable { private readonly Dictionary _byName = new Dictionary(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 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; } bool num = _byName.TryGetValue(text, out previous); _byName[text] = text2; if (num) { return !string.Equals(previous, text2, StringComparison.OrdinalIgnoreCase); } return true; } public List SameFaction(string species, IEnumerable pool) { List list = new List(); 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 ToLines() { List list = new List(_byName.Keys); list.Sort(StringComparer.OrdinalIgnoreCase); List list2 = new List(list.Count); foreach (string item in list) { list2.Add(item + "=" + _byName[item]); } return list2; } } 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 Rejects = new Dictionary(); } private readonly List _sources = new List(); private readonly Dictionary> _areas = new Dictionary>(StringComparer.OrdinalIgnoreCase); public IReadOnlyList Sources => _sources; public IReadOnlyList Areas { get { List list = new List(_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 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()); } public string FormatAll() { IReadOnlyList 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()); } 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 rejects) { if (rejects.Count == 0) { return "-"; } List> list = new List>(rejects); list.Sort(delegate(KeyValuePair a, KeyValuePair 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 value)) { value = new Dictionary(StringComparer.OrdinalIgnoreCase); _areas[key] = value; } if (!value.TryGetValue(text, out var value2)) { value2 = (value[text] = new SourceStats()); } return value2; } } 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 enum GateVerdict { Ok, Disabled, NoArea, NotOverworld, TownOrCity } public static class RegionGate { public static readonly int[] OverworldAreaIds = new int[6] { 101, 201, 301, 401, 501, 602 }; 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 } 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; } public bool Warm { get; } public SpeciesCandidate(string key, SpeciesSource source, bool blocked, bool expeditionOnly, bool warm) { Key = key; Source = source; Blocked = blocked; ExpeditionOnly = expeditionOnly; Warm = warm; } public override string ToString() { return string.Format("{0} ({1}{2}", Key, Source, Blocked ? ", blocked" : "") + (ExpeditionOnly ? ", expedition-only" : "") + (Warm ? ", warm" : ", cold") + ")"; } } public static class RosterFilter { public static readonly string[] DefaultBlocklist = new string[16] { "Ghost of Vanasse", "Luke the Pearlescent", "Scarlet Emissary", "Guardian of the Compass", "Josef", "Dorion", "Evangeline", "Calixa's Squire", "Simeon's Squire", "Concealed Knight", "Bonded Beastmaster", "Marsh Guardian", "Royal Manticore", "Hive Lord", "Immaculate", "Wolfgang" }; public static bool IsBlocked(string key, IEnumerable? extraBlocklist) { if (string.IsNullOrWhiteSpace(key)) { return true; } string[] defaultBlocklist = DefaultBlocklist; foreach (string needle in defaultBlocklist) { if (Contains(key, needle)) { return true; } } if (extraBlocklist != null) { foreach (string item in extraBlocklist) { if (!string.IsNullOrWhiteSpace(item) && Contains(key, item.Trim())) { return true; } } } return false; } private static bool Contains(string haystack, string needle) { if (needle.Length > 0) { return haystack.IndexOf(needle, StringComparison.OrdinalIgnoreCase) >= 0; } return false; } public static List Build(IEnumerable<(string key, SpeciesSource source)> raw, IEnumerable? extraBlocklist, Func isExpeditionOnly, Func canMintNow) { List list = new List(); if (raw == null) { return list; } HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var item3 in raw) { string item = item3.key; SpeciesSource item2 = item3.source; string text = (item ?? "").Trim(); if (text.Length != 0 && hashSet.Add(text)) { bool flag = IsBlocked(text, extraBlocklist); bool flag2 = !flag && isExpeditionOnly != null && isExpeditionOnly(text); bool warm = !flag && !flag2 && canMintNow != null && canMintNow(text); list.Add(new SpeciesCandidate(text, item2, flag, flag2, warm)); } } return list; } public static List Spawnable(IReadOnlyList? all, bool warmOnly) { List list = new List(); if (all == null) { return list; } for (int i = 0; i < all.Count; i++) { SpeciesCandidate speciesCandidate = all[i]; if (!speciesCandidate.Blocked && !speciesCandidate.ExpeditionOnly && (!warmOnly || speciesCandidate.Warm)) { list.Add(speciesCandidate); } } return list; } public static List PrewarmTargets(IReadOnlyList? all) { List list = new List(); if (all == null) { return list; } for (int i = 0; i < all.Count; i++) { SpeciesCandidate speciesCandidate = all[i]; if (!speciesCandidate.Blocked && !speciesCandidate.ExpeditionOnly && !speciesCandidate.Warm) { list.Add(speciesCandidate); } } return list; } } public static class SourceOrder { public static List Parse(string? csv, IReadOnlyList known, out List unknown) { unknown = new List(); List list = new List(); if (known == null) { return list; } if (!string.IsNullOrWhiteSpace(csv)) { HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); string[] array = csv.Split(new char[1] { ',' }); for (int i = 0; i < array.Length; i++) { string text = array[i].Trim(); if (text.Length == 0) { continue; } string text2 = null; for (int j = 0; j < known.Count; j++) { if (string.Equals(known[j], text, StringComparison.OrdinalIgnoreCase)) { text2 = known[j]; break; } } if (text2 == null) { unknown.Add(text); } else if (hashSet.Add(text2)) { list.Add(text2); } } } if (list.Count == 0) { for (int k = 0; k < known.Count; k++) { list.Add(known[k]); } } return list; } } public static class ThrottlePolicy { public const double Never = double.NegativeInfinity; public static bool Allow(double now, double lastAt, float minGap) { if (!(minGap <= 0f)) { return now - lastAt >= (double)minGap; } return true; } } public static class ToastText { public const string UnknownSpecies = "Something roams nearby."; public static string Wave(string? species, int count) { return Wave(species, count, null); } public static string Wave(string? species, int count, string? bearing) { if (count <= 0) { return ""; } string text = (species ?? "").Trim(); string text2 = (bearing ?? "").Trim(); if (text.Length == 0) { if (text2.Length <= 0) { return "Something roams nearby."; } return "Something roams the wilds to the " + text2 + "."; } if (text2.Length > 0) { if (count != 1) { return $"{count} {Pluralize(text)} roam the wilds to the {text2}."; } return "A " + text + " roams the wilds to the " + text2 + "."; } if (count != 1) { return $"{count} {Pluralize(text)} roam nearby."; } return "A " + text + " roams nearby."; } public static string Pluralize(string name) { if (string.IsNullOrEmpty(name)) { return name; } if (!EndsWithSibilant(name)) { return name + "s"; } return name + "es"; } private static bool EndsWithSibilant(string s) { if (s.EndsWith("ch", StringComparison.OrdinalIgnoreCase) || s.EndsWith("sh", StringComparison.OrdinalIgnoreCase)) { return true; } char c = char.ToLowerInvariant(s[s.Length - 1]); if (c != 's' && c != 'x') { return c == 'z'; } return true; } } public enum WaveRefusal { None, NoSpecies, CapReached, NoAnchor, Disarmed } public enum WaveOrigin { Squad, SameSpecies } public readonly struct WavePlan { private readonly string _faction; private readonly IReadOnlyList _members; public string Faction => _faction ?? ""; public IReadOnlyList Members => _members ?? Array.Empty(); public WaveOrigin Origin { get; } public WaveRefusal Refusal { get; } public bool IsUnset { get { if (_members != null) { if (_members.Count == 0) { return Refusal == WaveRefusal.None; } return false; } return true; } } public int Count => Members.Count; public bool Ok { get { if (Refusal == WaveRefusal.None) { return Members.Count > 0; } return false; } } public string PrimarySpecies { get { if (Members.Count == 0) { return ""; } string result = Members[0]; int num = 0; for (int i = 0; i < Members.Count; i++) { int num2 = 0; for (int j = 0; j < Members.Count; j++) { if (string.Equals(Members[i], Members[j], StringComparison.OrdinalIgnoreCase)) { num2++; } } if (num2 > num) { num = num2; result = Members[i]; } } return result; } } public bool IsMixed { get { for (int i = 1; i < Members.Count; i++) { if (!string.Equals(Members[0], Members[i], StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } } public WavePlan(string faction, IReadOnlyList members, WaveOrigin origin, WaveRefusal refusal) { _faction = faction ?? ""; _members = members ?? Array.Empty(); Origin = origin; Refusal = refusal; } public static WavePlan Refused(WaveRefusal why) { return new WavePlan("", Array.Empty(), WaveOrigin.SameSpecies, why); } public override string ToString() { if (!IsUnset) { if (!Ok) { return $"refused({Refusal})"; } return string.Format("{0}x [{1}] ", Count, string.Join(", ", new List(Members).ToArray())) + $"faction={Faction} via {Origin}"; } return "(no wave composed yet)"; } } public static class WavePlanner { public static WavePlan Compose(IReadOnlyList spawnable, IReadOnlyList squadRoster, FactionTable factions, double speciesRoll, double countRoll, int minCount, int maxCount, int ownActive, int maxOwnActive, IReadOnlyList recentSpecies, int recentPenaltyWindow, int availableSpots) { int num = maxOwnActive - ownActive; if (num <= 0) { return WavePlan.Refused(WaveRefusal.CapReached); } if (availableSpots <= 0) { return WavePlan.Refused(WaveRefusal.NoAnchor); } if (spawnable == null || spawnable.Count == 0) { return WavePlan.Refused(WaveRefusal.NoSpecies); } int num2 = RollCount(countRoll, minCount, maxCount); if (num2 > num) { num2 = num; } if (num2 > availableSpots) { num2 = availableSpots; } if (num2 <= 0) { return WavePlan.Refused(WaveRefusal.CapReached); } string faction; List list = SquadCohort(spawnable, squadRoster, factions, speciesRoll, out faction); if (list.Count >= 2) { List list2 = new List(num2); int num3 = IndexFor(speciesRoll, list.Count); for (int i = 0; i < num2; i++) { list2.Add(list[(num3 + i) % list.Count]); } return new WavePlan(faction, list2, WaveOrigin.Squad, WaveRefusal.None); } string text = PickSpecies(spawnable, speciesRoll, recentSpecies, recentPenaltyWindow); if (text.Length == 0) { return WavePlan.Refused(WaveRefusal.NoSpecies); } List list3 = new List(num2); for (int j = 0; j < num2; j++) { list3.Add(text); } return new WavePlan(factions?.FactionOf(text) ?? "", list3, WaveOrigin.SameSpecies, WaveRefusal.None); } public static List SquadCohort(IReadOnlyList spawnable, IReadOnlyList squadRoster, FactionTable factions, double roll01, out string faction) { faction = ""; List list = new List(); if (squadRoster == null || squadRoster.Count == 0 || factions == null) { return list; } List list2 = new List(); HashSet hashSet = new HashSet(StringComparer.OrdinalIgnoreCase); for (int i = 0; i < squadRoster.Count; i++) { string text = (squadRoster[i] ?? "").Trim(); if (text.Length == 0 || !hashSet.Add(text)) { continue; } for (int j = 0; j < spawnable.Count; j++) { if (string.Equals(spawnable[j].Key, text, StringComparison.OrdinalIgnoreCase)) { list2.Add(spawnable[j].Key); break; } } } if (list2.Count < 2) { return list; } string species = list2[IndexFor(roll01, list2.Count)]; string text2 = factions.FactionOf(species); if (string.IsNullOrEmpty(text2)) { return list; } for (int k = 0; k < list2.Count; k++) { if (string.Equals(factions.FactionOf(list2[k]), text2, StringComparison.OrdinalIgnoreCase)) { list.Add(list2[k]); } } if (list.Count < 2) { list.Clear(); return list; } faction = text2; return list; } public static int RollCount(double roll01, int minCount, int maxCount) { if (minCount < 0) { minCount = 0; } if (maxCount < minCount) { maxCount = minCount; } int count = maxCount - minCount + 1; return minCount + IndexFor(roll01, count); } 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; } private static string PickSpecies(IReadOnlyList spawnable, double roll01, IReadOnlyList recentSpecies, int recentPenaltyWindow) { List list = new List(spawnable.Count); if (recentSpecies != null && recentPenaltyWindow > 0) { int num = Math.Min(recentPenaltyWindow, recentSpecies.Count); for (int i = 0; i < spawnable.Count; i++) { bool flag = false; for (int j = 0; j < num; j++) { if (string.Equals(recentSpecies[j], spawnable[i].Key, StringComparison.OrdinalIgnoreCase)) { flag = true; break; } } if (!flag) { list.Add(spawnable[i]); } } } IReadOnlyList readOnlyList; if (list.Count <= 0) { readOnlyList = spawnable; } else { IReadOnlyList readOnlyList2 = list; readOnlyList = readOnlyList2; } IReadOnlyList readOnlyList3 = readOnlyList; if (readOnlyList3.Count != 0) { return readOnlyList3[IndexFor(roll01, readOnlyList3.Count)].Key; } return ""; } } }