Decompiled source of Falkenpass v0.3.1
plugins/Falkenpass/Falkenpass.dll
Decompiled 5 days 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.IO; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security.Cryptography; using System.Text; using BepInEx; using HarmonyLib; using Microsoft.CodeAnalysis; using NGS.MeshFusionPro; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Pathfinding; using TMPro; using Thronefall.SaveServices; using UnityEngine; using UnityEngine.Events; using UnityEngine.Rendering; using UnityEngine.SceneManagement; using UnityEngine.UI; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("Falkenpass")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.3.1.0")] [assembly: AssemblyInformationalVersion("0.3.1")] [assembly: AssemblyProduct("Falkenpass")] [assembly: AssemblyTitle("Falkenpass")] [assembly: AssemblyVersion("0.3.1.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [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 Falkenpass { internal static class AdaptiveSpawnLab { internal static IEnumerator Run() { if (!Plugin.Lab || !Plugin.Ready || !Object.op_Implicit((Object)(object)AdaptiveSpawns.Instance) || BalanceLab.Running || EnemySpawner.instance.Wavenumber != -1 || MapBuilder.KeptBuildings.Any((BuildSlot s) => s.Level > 0)) { throw new InvalidOperationException("Adaptive fixture requires a fresh, idle lab map."); } StringBuilder text = new StringBuilder("Adaptive ground-spawn fixture: explicit native purchases and two extra spawns; not balance or human play evidence.\n"); int failures = 0; int checks = 0; AdaptiveSpawns adaptive = AdaptiveSpawns.Instance; adaptive.BeginDay(); Dictionary<string, Vector3> baseline = MapBuilder.RouteSpawns.ToDictionary((KeyValuePair<string, Transform> p) => p.Key, (KeyValuePair<string, Transform> p) => p.Value.position); MapLayout.Route[] routes = MapLayout.Routes; foreach (MapLayout.Route route in routes) { Check(Mathf.Abs(adaptive.PlannedDistances[route.Key] - route.Length / 2f) < 0.01f, route.Key + " starts halfway along its original winding route"); Check(MapBuilder.AuthoredWaves.SelectMany((Wave w) => w.spawns).First((Spawn s) => (Object)(object)s.spawnLine == (Object)(object)MapBuilder.RouteSpawns[route.Key]).spawnLine == MapBuilder.RouteSpawns[route.Key], route.Key + " native waves share the adaptive line"); } FieldInfo markerField = AccessTools.Field(typeof(EnemySpawner), "screenMarkersEnemySpawns"); Component[] markers = ((IEnumerable)markerField.GetValue(EnemySpawner.instance)).Cast<Component>().ToArray(); Dictionary<Component, int> numbers = markers.ToDictionary((Component m) => m, (Component m) => ((ScreenMarker)m).Number); Check(markers.Any(delegate(Component m) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) //IL_001b: Unknown result type (might be due to invalid IL or missing references) //IL_0020: Unknown result type (might be due to invalid IL or missing references) Vector3 val6 = m.transform.position - baseline["west-low"]; return ((Vector3)(ref val6)).sqrMagnitude < 0.01f; }), "native first-wave forecast initially matches west-low"); BuildSlot val = MapBuilder.KeptBuildings.First((BuildSlot s) => s.buildingName == "Castle Center"); BuildSlot house = (from s in MapBuilder.KeptBuildings where s.buildingName == "House" && ((Component)s).transform.position.x < -140f && ((Component)s).transform.position.z < -60f orderby ((Component)s).transform.position.x select s).First(); PlayerInteraction.instance.AddCoin(50); while (val.Level < 2) { int level = val.Level; LabCommands.Buy(val, nativeLifecycle: true); if (val.Level == level) { throw new InvalidOperationException("Fixture castle purchase did not advance."); } } yield return null; LabCommands.Buy(house, nativeLifecycle: true); yield return null; adaptive.Replan(); Vector3 advanced = MapBuilder.RouteSpawns["west-low"].position; Check(house.Level > 0 && house.BuildingParent.activeInHierarchy, "one native forward house is living and constructed"); Vector3 val2 = advanced - baseline["west-low"]; Check(((Vector3)(ref val2)).sqrMagnitude > 1f, "forward construction pushes its own west-low spawn outward"); foreach (KeyValuePair<string, Vector3> item4 in baseline.Where((KeyValuePair<string, Vector3> p) => p.Key != "west-low")) { val2 = MapBuilder.RouteSpawns[item4.Key].position - item4.Value; Check(((Vector3)(ref val2)).sqrMagnitude < 0.001f, item4.Key + " remains unchanged by non-overlapping branch progress"); } Check(markers.Length == ((IEnumerable)markerField.GetValue(EnemySpawner.instance)).Cast<object>().Count() && numbers.All((KeyValuePair<Component, int> p) => Object.op_Implicit((Object)(object)p.Key) && ((ScreenMarker)p.Key).Number == p.Value), "replanning preserves marker identity, count and enemy numbers without duplicates"); Check(markers.Any(delegate(Component m) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) Vector3 val6 = m.transform.position - advanced; return ((Vector3)(ref val6)).sqrMagnitude < 0.01f; }), "existing native forecast moves to the actual advanced spawn midpoint"); routes = MapLayout.Routes; foreach (MapLayout.Route route2 in routes) { Transform val3 = MapBuilder.RouteSpawns[route2.Key]; Vector3[] array = (Vector3[])(object)new Vector3[3] { AdaptiveSpawns.Center(val3), val3.GetChild(0).position, val3.GetChild(val3.childCount - 1).position }; foreach (Vector3 val4 in array) { Check(MapBuilder.Contains(new Vector2(val4.x, val4.z)), route2.Key + " center/endpoint stays inside authored floor"); NNInfo nearest = AstarPath.active.GetNearest(val4, EnemySpawner.instance.groundConstraint); NNInfo nearest2 = AstarPath.active.GetNearest(val4, EnemySpawner.instance.groundConstraintLarge); Check(nearest.node != null && Vector3.Distance(val4, nearest.position) < 0.15f && nearest2.node != null && Vector3.Distance(val4, nearest2.position) < 0.15f, route2.Key + " center/endpoint matches native S and L ground surfaces"); } } PathfindMovementEnemy[] before = Object.FindObjectsOfType<PathfindMovementEnemy>(); GameObject item = MapBuilder.EnemyPrefabs["E Melee"]; GameObject item2 = MapBuilder.EnemyPrefabs.Values.First(delegate(GameObject p) { //IL_001b: Unknown result type (might be due to invalid IL or missing references) PathfindMovementEnemy componentInChildren = p.GetComponentInChildren<PathfindMovementEnemy>(true); if (componentInChildren != null) { ref GraphMask graphMask2 = ref ((Component)componentInChildren).GetComponent<Seeker>().graphMask; object obj2 = GraphMask.FromGraphName("Enemy Units L"); return ((object)Unsafe.As<GraphMask, GraphMask>(ref graphMask2)/*cast due to .constrained prefix*/).Equals(obj2); } return false; }); (GameObject prefab, string graph)[] probes = new(GameObject, string)[2] { (item, "Enemy Units S"), (item2, "Enemy Units L") }; (GameObject, string)[] array2 = probes; for (int num = 0; num < array2.Length; num++) { Spawn val5 = new Spawn(array2[num].Item1, MapBuilder.RouteSpawns["west-low"], 1, 0.5f, 0, 0f, false); val5.Reset(true); val5.Update(1f); } yield return null; PathfindMovementEnemy[] array3 = Object.FindObjectsOfType<PathfindMovementEnemy>().Except(before).ToArray(); Check(array3.Length == 2, "two extra native S/L enemies actually spawned"); PathfindMovementEnemy[] array4 = array3; foreach (PathfindMovementEnemy enemy in array4) { string item3 = probes.First(((GameObject prefab, string graph) p) => ((Object)enemy).name.StartsWith(((Object)p.prefab).name, StringComparison.Ordinal)).graph; ref GraphMask graphMask = ref ((Component)enemy).GetComponent<Seeker>().graphMask; object obj = GraphMask.FromGraphName(item3); Check(((object)Unsafe.As<GraphMask, GraphMask>(ref graphMask)/*cast due to .constrained prefix*/).Equals(obj), ((Object)enemy).name + " uses expected native " + item3 + " graph"); Check(Vector3.Distance(((Component)enemy).transform.position, enemy.GetNearestGroundPosition(((Component)enemy).transform.position)) < 0.15f, ((Object)enemy).name + " native movement remains snapped to its ground surface"); Object.Destroy((Object)(object)((Component)enemy).gameObject); } yield return null; int wavenumber = EnemySpawner.instance.Wavenumber; EnemySpawner.instance.StartSpawning(); EnemySpawner.instance.StopSpawnAfterWaveAndReset(); AccessTools.Field(typeof(EnemySpawner), "wavenumber").SetValue(EnemySpawner.instance, wavenumber); Check(adaptive.Frozen, "native StartSpawning hook freezes the night's plan before Spawn.Update"); Hp health = house.BuildingParent.GetComponent<Hp>(); health.MakeInvulerable(false); health.TakeDamage(float.MaxValue, (TaggedObject)null, false, false); yield return null; Check(health.HpValue <= 0f, "native fixture house is actually destroyed after disabling daytime invulnerability"); adaptive.Replan(); val2 = MapBuilder.RouteSpawns["west-low"].position - advanced; Check(((Vector3)(ref val2)).sqrMagnitude < 0.001f, "destruction leaves a frozen night's line unchanged"); adaptive.BeginDay(); val2 = MapBuilder.RouteSpawns["west-low"].position - baseline["west-low"]; Check(((Vector3)(ref val2)).sqrMagnitude < 0.001f, "next daytime planning retreats after native destruction"); health.Revive(true, 1f); adaptive.Replan(); val2 = MapBuilder.RouteSpawns["west-low"].position - advanced; Check(((Vector3)(ref val2)).sqrMagnitude < 0.001f, "native repair restores the branch's forward plan"); string[] array5 = new string[2] { "air-north", "air-south" }; foreach (string text2 in array5) { val2 = MapBuilder.RouteSpawns[text2].position - baseline[text2]; Check(((Vector3)(ref val2)).sqrMagnitude < 0.001f, text2 + " flying approach remains original"); } text.AppendLine("SUMMARY checks=" + checks + " failures=" + failures + "; funded buildings remain for this disposable fixture."); File.WriteAllText(Path.Combine(Plugin.DataRoot, "adaptive-spawn-test.txt"), text.ToString()); Plugin.Instance.Log("Adaptive spawn fixture: " + checks + " checks, " + failures + " failures."); void Check(bool valid, string label) { checks++; if (!valid) { failures++; } text.AppendLine((valid ? "PASS " : "FAIL ") + label); } } } internal sealed class AdaptiveSpawns : MonoBehaviour { private sealed class SiteBinding { internal BuildSlot Slot; internal string Route; internal float Progress; internal float Footprint; internal Hp Health; internal AutoAttack[] Attacks; } internal readonly struct Influence { internal readonly string Route; internal readonly Vector2 Position; internal readonly float Progress; internal readonly float Protection; internal readonly float Vision; internal Influence(string route, Vector2 position, float progress, float protection, float vision) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) Route = route; Position = position; Progress = progress; Protection = protection; Vision = vision; } } [HarmonyPatch(typeof(EnemySpawner), "PlaceMarkersForNextWave")] private static class PreviewPlan { private static void Prefix() { if (Plugin.ActiveMap && Object.op_Implicit((Object)(object)Instance) && !Instance.Frozen) { Instance.Replan(); } } } [HarmonyPatch(typeof(EnemySpawner), "StartSpawning")] private static class NightPlan { private static void Prefix() { if (Plugin.ActiveMap && Object.op_Implicit((Object)(object)Instance)) { Instance.FreezeForNight(); } } } [HarmonyPatch(typeof(EnemySpawner), "OnStartOfTheDay")] private static class DayPlan { private static void Prefix() { if (Plugin.ActiveMap && Object.op_Implicit((Object)(object)Instance)) { Instance.BeginDay(); } } } internal const float MinimumBuffer = 24f; internal const float AttackMargin = 6f; private const float LineHalfWidth = 2f; internal readonly Dictionary<string, float> PlannedDistances = new Dictionary<string, float>(StringComparer.Ordinal); internal readonly Dictionary<string, bool> SafetyLimited = new Dictionary<string, bool>(StringComparer.Ordinal); private readonly Dictionary<string, Transform> lines = new Dictionary<string, Transform>(StringComparer.Ordinal); private readonly List<SiteBinding> sites = new List<SiteBinding>(); private readonly Dictionary<string, Vector3> originalPositions = new Dictionary<string, Vector3>(StringComparer.Ordinal); private readonly List<Influence> influences = new List<Influence>(); private float nextPlan; private static readonly Dictionary<string, string> TerritoryRoutes = new Dictionary<string, string>(StringComparer.Ordinal) { ["Pinewatch Farm"] = "west-high", ["Sunstone Farm"] = "east-high", ["Raven Lookout"] = "west-ridge", ["Crown Lookout"] = "east-ridge", ["Granite Siegehold"] = "west-low", ["Ashen Trading Post"] = "east-low" }; internal static AdaptiveSpawns Instance { get; private set; } internal bool Frozen { get; private set; } internal static void Install(GameObject root, IReadOnlyDictionary<string, Transform> spawns) { //IL_0032: Unknown result type (might be due to invalid IL or missing references) //IL_0038: 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_00db: Unknown result type (might be due to invalid IL or missing references) //IL_00f1: Unknown result type (might be due to invalid IL or missing references) //IL_00fb: Unknown result type (might be due to invalid IL or missing references) //IL_0100: Unknown result type (might be due to invalid IL or missing references) //IL_0160: Unknown result type (might be due to invalid IL or missing references) //IL_01a4: Unknown result type (might be due to invalid IL or missing references) //IL_01ae: Unknown result type (might be due to invalid IL or missing references) //IL_01c9: Unknown result type (might be due to invalid IL or missing references) //IL_01da: Unknown result type (might be due to invalid IL or missing references) //IL_01df: Unknown result type (might be due to invalid IL or missing references) //IL_01e4: Unknown result type (might be due to invalid IL or missing references) //IL_01e9: Unknown result type (might be due to invalid IL or missing references) //IL_01ef: Unknown result type (might be due to invalid IL or missing references) //IL_01f4: Unknown result type (might be due to invalid IL or missing references) //IL_01fb: Unknown result type (might be due to invalid IL or missing references) //IL_0202: Unknown result type (might be due to invalid IL or missing references) AdaptiveSpawns adaptiveSpawns = (Instance = root.AddComponent<AdaptiveSpawns>()); MapLayout.Route[] routes = MapLayout.Routes; foreach (MapLayout.Route route in routes) { Transform val = spawns[route.Key]; if (((Component)val).gameObject.scene != root.scene) { throw new InvalidOperationException("Adaptive spawn belongs to a different map scene: " + route.Key); } adaptiveSpawns.lines.Add(route.Key, val); adaptiveSpawns.originalPositions.Add(route.Key, val.position); } FieldInfo fieldInfo = AccessTools.Field(typeof(BuildSlot), "mainCollider"); foreach (BuildSlot slot in MapBuilder.KeptBuildings) { Vector2 position = new Vector2(((Component)slot).transform.position.x, ((Component)slot).transform.position.z); MapLayout.Site site = MapLayout.Sites.First(delegate(MapLayout.Site s) { //IL_0019: 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) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_0029: Unknown result type (might be due to invalid IL or missing references) if (s.Building == slot.buildingName) { Vector2 val4 = s.Position - position; return ((Vector2)(ref val4)).sqrMagnitude < 0.05f; } return false; }); TerritoryRoutes.TryGetValue(site.Territory, out var key); float progress = ((key != null) ? Project(MapLayout.Routes.First((MapLayout.Route r) => r.Key == key), position) : 0f); float num = 0f; object? value = fieldInfo.GetValue(slot); BoxCollider val2 = (BoxCollider)((value is BoxCollider) ? value : null); if (val2 != null) { for (int num2 = -1; num2 <= 1; num2 += 2) { for (int num3 = -1; num3 <= 1; num3 += 2) { Vector3 val3 = ((Component)val2).transform.TransformPoint(val2.center + new Vector3((float)num2 * val2.size.x / 2f, 0f, (float)num3 * val2.size.z / 2f)); num = Mathf.Max(num, Vector2.Distance(position, new Vector2(val3.x, val3.z))); } } } adaptiveSpawns.sites.Add(new SiteBinding { Slot = slot, Route = key, Progress = progress, Footprint = num, Health = (Object.op_Implicit((Object)(object)slot.BuildingParent) ? slot.BuildingParent.GetComponent<Hp>() : null), Attacks = (Object.op_Implicit((Object)(object)slot.BuildingParent) ? slot.BuildingParent.GetComponentsInChildren<AutoAttack>(true) : Array.Empty<AutoAttack>()) }); } adaptiveSpawns.Replan(); } internal static bool Living(BuildSlot slot, Hp health) { if (Object.op_Implicit((Object)(object)slot) && slot.Level > 0 && ((Component)slot).gameObject.activeInHierarchy && Object.op_Implicit((Object)(object)slot.BuildingParent) && slot.BuildingParent.activeInHierarchy) { if (Object.op_Implicit((Object)(object)health)) { if (health.HpValue > 0f) { return !health.KnockedOut; } return false; } return true; } return false; } internal List<Influence> ReadFrontier() { //IL_0115: Unknown result type (might be due to invalid IL or missing references) //IL_012a: Unknown result type (might be due to invalid IL or missing references) //IL_0134: Unknown result type (might be due to invalid IL or missing references) influences.Clear(); foreach (SiteBinding site in sites) { if (!Living(site.Slot, site.Health)) { continue; } float num = 0f; AutoAttack[] attacks = site.Attacks; foreach (AutoAttack val in attacks) { if (!Object.op_Implicit((Object)(object)val) || !((Behaviour)val).isActiveAndEnabled) { continue; } foreach (TargetPriority targetPriority in val.targetPriorities) { num = Mathf.Max(num, targetPriority.range); } } float protection = Mathf.Max(24f + site.Footprint, num + 6f) + 2f; float vision = (Object.op_Implicit((Object)(object)FogOfWar.Instance) ? (FogOfWar.Instance.BuildingVisionRadius(site.Slot) + 2f) : 0f); influences.Add(new Influence(site.Route, new Vector2(((Component)site.Slot).transform.position.x, ((Component)site.Slot).transform.position.z), site.Progress, protection, vision)); } return influences; } private void Update() { //IL_0020: Unknown result type (might be due to invalid IL or missing references) if (Plugin.ActiveMap && Object.op_Implicit((Object)(object)DayNightCycle.Instance) && !Frozen && (int)DayNightCycle.Instance.CurrentTimestate == 0 && !(Time.unscaledTime < nextPlan)) { nextPlan = Time.unscaledTime + 0.25f; Replan(); } } internal void BeginDay() { Frozen = false; Replan(); } internal void FreezeForNight() { if (!Frozen) { Replan(); Frozen = true; } } internal void Replan() { if (!Frozen) { Apply(ReadFrontier()); } } internal void Apply(IReadOnlyList<Influence> frontier) { //IL_0028: Unknown result type (might be due to invalid IL or missing references) //IL_002d: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0049: 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_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0067: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_007a: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: 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) //IL_00b7: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) //IL_00be: Unknown result type (might be due to invalid IL or missing references) //IL_00ce: Unknown result type (might be due to invalid IL or missing references) //IL_00d0: Unknown result type (might be due to invalid IL or missing references) if (Frozen) { return; } MapLayout.Route[] routes = MapLayout.Routes; foreach (MapLayout.Route route in routes) { bool limited; float num = Choose(route, frontier, out limited); Vector2 inward; Vector2 val = At(route, num, out inward); Transform obj = lines[route.Key]; Vector3 val2 = Center(obj); obj.position = new Vector3(val.x, 0f, val.y); obj.rotation = Quaternion.LookRotation(new Vector3(inward.x, 0f, inward.y)); PlannedDistances[route.Key] = num; SafetyLimited[route.Key] = limited; Vector3 val3 = Center(obj); Vector3 val4 = val3 - val2; if (((Vector3)(ref val4)).sqrMagnitude > 0.0001f) { MoveMarkers(val2, val3); } } } internal static float Choose(MapLayout.Route route, IReadOnlyList<Influence> frontier, out bool limited) { //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) //IL_00a2: Unknown result type (might be due to invalid IL or missing references) //IL_00a6: Unknown result type (might be due to invalid IL or missing references) float length = route.Length; float num = length * 0.5f; foreach (Influence item in frontier) { if (item.Route == route.Key) { num = Mathf.Max(num, item.Progress + item.Protection); } } num = Mathf.Min(num, length); float result = num; float num2 = float.NegativeInfinity; bool flag = false; float num3 = num; while (true) { Vector2 inward; Vector2 val = At(route, num3, out inward); float num4 = float.PositiveInfinity; bool flag2 = false; foreach (Influence item2 in frontier) { float num5 = Vector2.Distance(val, item2.Position); num4 = Mathf.Min(num4, num5 - item2.Protection); flag2 |= item2.Vision > 0f && num5 < item2.Vision; } if (num4 >= 0f) { if (!flag) { result = num3; flag = true; } if (!flag2) { limited = false; return num3; } } else if (!flag && num4 > num2) { result = num3; num2 = num4; } if (num3 >= length) { break; } num3 = Mathf.Min(length, num3 + 2f); } limited = true; return result; } internal static float Project(MapLayout.Route route, Vector2 position) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) //IL_002e: Unknown result type (might be due to invalid IL or missing references) //IL_0033: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_0050: Unknown result type (might be due to invalid IL or missing references) //IL_0055: Unknown result type (might be due to invalid IL or missing references) //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) //IL_0078: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0086: Unknown result type (might be due to invalid IL or missing references) //IL_008b: Unknown result type (might be due to invalid IL or missing references) float num = 0f; float result = 0f; float num2 = float.PositiveInfinity; for (int i = 0; i < route.Points.Length - 1; i++) { Vector2 val = route.Points[i + 1] - route.Points[i]; float magnitude = ((Vector2)(ref val)).magnitude; float num3 = Mathf.Clamp01(Vector2.Dot(position - route.Points[i], val) / ((Vector2)(ref val)).sqrMagnitude); Vector2 val2 = position - (route.Points[i] + val * num3); float sqrMagnitude = ((Vector2)(ref val2)).sqrMagnitude; if (sqrMagnitude < num2) { num2 = sqrMagnitude; result = num + magnitude * num3; } num += magnitude; } return result; } internal static Vector2 At(MapLayout.Route route, float distance, out Vector2 inward) { //IL_000d: Unknown result type (might be due to invalid IL or missing references) //IL_0019: Unknown result type (might be due to invalid IL or missing references) //IL_001e: 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_0089: Unknown result type (might be due to invalid IL or missing references) //IL_008e: 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_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_004a: 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_005b: 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_0069: Unknown result type (might be due to invalid IL or missing references) for (int i = 0; i < route.Points.Length - 1; i++) { Vector2 val = route.Points[i + 1] - route.Points[i]; float magnitude = ((Vector2)(ref val)).magnitude; if (distance <= magnitude || i == route.Points.Length - 2) { inward = -((Vector2)(ref val)).normalized; return route.Points[i] + val * Mathf.Clamp01(distance / magnitude); } distance -= magnitude; } inward = Vector2.left; return route.Points[route.Points.Length - 1]; } internal static Vector3 Center(Transform line) { //IL_0017: Unknown result type (might be due to invalid IL or missing references) //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_002f: Unknown result type (might be due to invalid IL or missing references) //IL_0039: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) if (line.childCount <= 0) { return line.position; } return (line.GetChild(0).position + line.GetChild(line.childCount - 1).position) * 0.5f; } private static void MoveMarkers(Vector3 old, Vector3 current) { //IL_0053: Unknown result type (might be due to invalid IL or missing references) //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_005e: Unknown result type (might be due to invalid IL or missing references) //IL_0073: Unknown result type (might be due to invalid IL or missing references) if (!Object.op_Implicit((Object)(object)EnemySpawner.instance) || !(AccessTools.Field(typeof(EnemySpawner), "screenMarkersEnemySpawns").GetValue(EnemySpawner.instance) is IEnumerable enumerable)) { return; } foreach (object item in enumerable) { Component val = (Component)((item is Component) ? item : null); if (val != null) { Vector3 val2 = val.transform.position - old; if (((Vector3)(ref val2)).sqrMagnitude < 0.01f) { val.transform.position = current; } } } } internal string Describe() { //IL_005e: 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_008f: Unknown result type (might be due to invalid IL or missing references) //IL_0094: Unknown result type (might be due to invalid IL or missing references) StringBuilder stringBuilder = new StringBuilder("Adaptive ground spawn plan frozen=" + Frozen + "\n"); MapLayout.Route[] routes = MapLayout.Routes; foreach (MapLayout.Route route in routes) { stringBuilder.AppendLine(route.Key + " original=" + ((object)originalPositions[route.Key]/*cast due to .constrained prefix*/).ToString() + " current=" + ((object)Center(lines[route.Key])/*cast due to .constrained prefix*/).ToString() + " arc=" + PlannedDistances[route.Key].ToString("F1") + "/" + route.Length.ToString("F1") + " boundedRoadSafetyLimit=" + SafetyLimited[route.Key]); } return stringBuilder.ToString(); } private void OnDestroy() { if ((Object)(object)Instance == (Object)(object)this) { Instance = null; } } } internal static class BalanceLab { [HarmonyPatch] private static class LabButtonInput { private static bool Prepare() { return Plugin.Lab; } internal static IEnumerable<MethodBase> TargetMethods() { Type playerType = AccessTools.Field(typeof(PlayerInteraction), "input").FieldType; string[] array = new string[8] { "GetButton", "GetButtonDown", "GetButtonUp", "GetButtonRepeating", "GetNegativeButton", "GetNegativeButtonDown", "GetNegativeButtonUp", "GetNegativeButtonRepeating" }; foreach (string name in array) { Type[] array2 = new Type[2] { typeof(string), typeof(int) }; foreach (Type type in array2) { yield return AccessTools.Method(playerType, name, new Type[1] { type }, (Type[])null) ?? throw new MissingMethodException(playerType.FullName, name); } } } private static bool Prefix(ref bool __result) { if (!QuarantineInput) { return true; } __result = false; return false; } } [HarmonyPatch(typeof(UIFrame), "HandleMouseNavigation")] private static class LabMouseInput { private static bool Prepare() { return Plugin.Lab; } private static bool Prefix() { return !QuarantineInput; } } [HarmonyPatch(typeof(ChoiceManager), "PresentChoices")] private static class NativeLabChoice { private static bool Prefix(List<Choice> _availableChoices, BuildSlot originBuildSlot, Action<Choice> _onCompleteFunction) { if (!Plugin.Lab || (Object)(object)originBuildSlot != (Object)(object)purchaseOrigin || purchaseChoice == null || !_availableChoices.Contains(purchaseChoice)) { return true; } _onCompleteFunction(purchaseChoice); return false; } } private sealed class TacticalDeployment { private sealed class Order { internal PathfindMovementPlayerunit Unit; internal BuildSlot Source; internal int Side; } private readonly List<Order> orders = new List<Order>(); private readonly bool[] fellBack = new bool[2]; internal bool Ready { get { if (orders.Count > 0) { return orders.All((Order o) => Object.op_Implicit((Object)(object)o.Unit) && Object.op_Implicit((Object)(object)o.Unit.hp) && o.Unit.hp.HpValue > 0f && o.Unit.HasReachedHomePositionAlready && Vector3.Distance(((Component)o.Unit).transform.position, o.Unit.HomePosition) <= 4f); } return false; } } internal TacticalDeployment(StringBuilder report) { //IL_0075: Unknown result type (might be due to invalid IL or missing references) //IL_020b: Unknown result type (might be due to invalid IL or missing references) //IL_0245: Unknown result type (might be due to invalid IL or missing references) //IL_0270: Unknown result type (might be due to invalid IL or missing references) string value; Vector3 destination = default(Vector3); foreach (BuildSlot source in MapBuilder.KeptBuildings.Where((BuildSlot b) => b.Level > 0 && (b.buildingName == "Barracks" || b.buildingName == "Archery Range") && purchaseTerritories.TryGetValue(b, out value) && (value == "Westwatch" || value == "Eastwatch"))) { int side = ((!(((Component)source).transform.position.x < 0f)) ? 1 : 0); MapLayout.Territory territory = MapLayout.Territories.First((MapLayout.Territory t) => t.Name == ((side == 0) ? "Pinewatch Farm" : "Sunstone Farm")); bool flag = source.buildingName == "Archery Range"; PathfindMovementPlayerunit[] array = (from h in (from r in ((Component)source).GetComponentsInChildren<UnitRespawnerForBuildings>(true) where ((Behaviour)r).isActiveAndEnabled && (Object)(object)r.myBuildSlot == (Object)(object)source select r).SelectMany((UnitRespawnerForBuildings r) => r.units) where Object.op_Implicit((Object)(object)h) && ((Component)h).gameObject.activeInHierarchy && h.HpValue > 0f select ((Component)h).GetComponent<PathfindMovementPlayerunit>() into u where Object.op_Implicit((Object)(object)u) select u).Distinct().ToArray(); report.AppendLine("GARRISON_ORDER source=" + ((Object)source).name.Replace(' ', '_') + " paidTier=" + source.Level + " destination=" + territory.Name.Replace(' ', '_') + " units=" + array.Length + " reason=public-high-road-warning"); for (int num = 0; num < array.Length; num++) { ((Vector3)(ref destination))..ctor(territory.Center.x + (float)(((side != 0) ? 1 : (-1)) * (flag ? 8 : 15)) + ((float)(num % 4) - 1.5f) * 0.8f, 0f, territory.Center.y + (float)(flag ? 8 : 18) + (float)(num / 4) * 0.8f); Command(array[num], destination); orders.Add(new Order { Unit = array[num], Source = source, Side = side }); } } if (orders.Count == 0 || orders.All((Order o) => o.Side == 0) || orders.All((Order o) => o.Side == 1)) { throw new InvalidOperationException("The paid manifest must supply active staging troops on both sides for this deployment comparison."); } } private static void Command(PathfindMovementPlayerunit unit, Vector3 destination) { //IL_0009: Unknown result type (might be due to invalid IL or missing references) //IL_000a: Unknown result type (might be due to invalid IL or missing references) unit.FollowPlayer(false); unit.HomePosition = unit.GetNearestGroundPosition(destination); unit.HasReachedHomePositionAlready = false; unit.HoldPosition = true; unit.OriginalPathRequest(); } internal void Describe(StringBuilder report, string phase, float elapsed) { //IL_0132: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_0157: Unknown result type (might be due to invalid IL or missing references) //IL_015c: Unknown result type (might be due to invalid IL or missing references) //IL_017d: Unknown result type (might be due to invalid IL or missing references) //IL_0182: Unknown result type (might be due to invalid IL or missing references) report.AppendLine("GARRISON phase=" + phase + " elapsed=" + elapsed.ToString("F1", CultureInfo.InvariantCulture) + " ready=" + Ready + " arrived=" + orders.Count((Order o) => Object.op_Implicit((Object)(object)o.Unit) && o.Unit.HasReachedHomePositionAlready && Vector3.Distance(((Component)o.Unit).transform.position, o.Unit.HomePosition) <= 4f) + "/" + orders.Count); if (phase != "staged") { return; } foreach (Order order in orders) { report.AppendLine("GARRISON_UNIT source=" + ((Object)order.Source).name.Replace(' ', '_') + " unit=" + ((Object)order.Unit).name.Replace(' ', '_') + " actual=" + ((object)((Component)order.Unit).transform.position/*cast due to .constrained prefix*/).ToString() + " commanded=" + ((object)order.Unit.HomePosition/*cast due to .constrained prefix*/).ToString() + " original=" + ((object)order.Unit.HomePositionOriginal/*cast due to .constrained prefix*/).ToString() + " arrived=" + order.Unit.HasReachedHomePositionAlready + " hp=" + order.Unit.hp.HpValue.ToString("F1", CultureInfo.InvariantCulture)); } } internal void ObserveVisibleBreach(StringBuilder report, float elapsed) { //IL_007b: 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_0146: Unknown result type (might be due to invalid IL or missing references) //IL_01f2: Unknown result type (might be due to invalid IL or missing references) //IL_01f7: Unknown result type (might be due to invalid IL or missing references) TaggedObject[] source = TagManager.instance.EnemyUnits.Where((TaggedObject e) => Object.op_Implicit((Object)(object)e) && Object.op_Implicit((Object)(object)e.Hp) && e.Hp.HpValue > 0f && !e.Tags.Contains((ETag)6) && Object.op_Implicit((Object)(object)FogOfWar.Instance) && FogOfWar.Instance.IsCurrentlyVisible(((Component)e).transform.position)).ToArray(); for (int side = 0; side < 2; side++) { if (fellBack[side]) { continue; } Vector2 gate = MapBuilder.GateCenters[side]; TaggedObject val = ((IEnumerable<TaggedObject>)source).FirstOrDefault((Func<TaggedObject, bool>)((TaggedObject e) => ((!(((Component)e).transform.position.x < 0f)) ? 1 : 0) == side && Mathf.Abs(((Component)e).transform.position.x) < 96f && Mathf.Abs(((Component)e).transform.position.z - gate.y) < 24f)); if (!Object.op_Implicit((Object)(object)val)) { continue; } fellBack[side] = true; int num = 0; foreach (Order item in orders.Where((Order o) => o.Side == side && Object.op_Implicit((Object)(object)o.Unit) && ((Behaviour)o.Unit).isActiveAndEnabled && Object.op_Implicit((Object)(object)o.Unit.hp) && o.Unit.hp.HpValue > 0f)) { Command(item.Unit, new Vector3(gate.x + (float)((side == 0) ? 7 : (-7)), 0f, gate.y + (float)(num % 5 - 2) * 0.8f)); num++; } report.AppendLine("GARRISON_FALLBACK elapsed=" + elapsed.ToString("F1", CultureInfo.InvariantCulture) + " side=" + ((side == 0) ? "west" : "east") + " units=" + num + " reason=visible-ground-breach threat=" + ((Object)val).name.Replace(' ', '_') + " position=" + ((object)((Component)val).transform.position/*cast due to .constrained prefix*/).ToString()); } } } [HarmonyPatch(typeof(AutoAttack), "OnAttack")] private static class GateAssaultAttackObserver { private static bool Prepare() { return Plugin.Lab; } private static void Postfix(AutoAttack __instance, TaggedObject target) { if (!((Object)(object)__instance != (Object)(object)gateAssaultAttack) && Object.op_Implicit((Object)(object)gateAssaultAttack)) { gateAssaultLastTarget = target; gateAssaultAttackCount++; if (Object.op_Implicit((Object)(object)target) && (Object)(object)((Component)target).GetComponentInParent<BuildSlot>() == (Object)(object)gateAssaultSlot) { gateAssaultGateAttackCount++; } } } } private static BuildSlot purchaseOrigin; private static Choice purchaseChoice; private static readonly Dictionary<BuildSlot, string> purchaseTerritories = new Dictionary<BuildSlot, string>(); private static AutoAttack gateAssaultAttack; private static TaggedObject gateAssaultLastTarget; private static int gateAssaultAttackCount; private static int gateAssaultGateAttackCount; private static BuildSlot gateAssaultSlot; internal static bool Running { get; private set; } internal static bool QuarantineInput { get { if (Plugin.Lab) { if (!Running && !Object.op_Implicit((Object)(object)BalancePilot.Current)) { return CampaignRestartLab.Running; } return true; } return false; } } internal static void VerifyInputQuarantine(StringBuilder report) { int num = LabButtonInput.TargetMethods().Count((MethodBase method) => Harmony.GetPatchInfo(method)?.Prefixes.Any((Patch p) => p.PatchMethod.DeclaringType == typeof(LabButtonInput)) ?? false); bool flag = Harmony.GetPatchInfo((MethodBase)AccessTools.Method(typeof(UIFrame), "HandleMouseNavigation", (Type[])null, (Type[])null))?.Prefixes.Any((Patch p) => p.PatchMethod.DeclaringType == typeof(LabMouseInput)) ?? false; report.AppendLine("INPUT quarantine=" + QuarantineInput + " nativeButtonHooks=" + num + "/16 mouseNavigationHook=" + flag + " nativeMovementAndWeaponUpdates=retained nativeCoinFetch=retained"); if (!QuarantineInput || num != 16 || !flag) { throw new InvalidOperationException("Lab input quarantine is incomplete; do not run a funded campaign."); } } private static bool ValidateWave(StringBuilder report, int night, string phase, int expectedIndex, bool completed = false, int? authoredCount = null) { //IL_0173: Unknown result type (might be due to invalid IL or missing references) //IL_0178: Unknown result type (might be due to invalid IL or missing references) int wavenumber = EnemySpawner.instance.Wavenumber; int num = authoredCount ?? CampaignBalance.Nights[night - 1].Count; int totalEnemiesSpawnedThisWave = EnemySpawner.totalEnemiesSpawnedThisWave; bool flag = expectedIndex == night - 1 && (totalEnemiesSpawnedThisWave > num || (completed && totalEnemiesSpawnedThisWave != num)); if (wavenumber >= 0 && wavenumber < EnemySpawner.instance.waves.Count && !CampaignBalance.ObjectivesValid(EnemySpawner.instance.waves[wavenumber])) { report.AppendLine("RESULT stopped-at-night=" + night + " reason=authored-objective-binding-mismatch phase=" + phase + " " + CampaignBalance.DescribeObjectives(EnemySpawner.instance.waves[wavenumber])); return false; } if (wavenumber == expectedIndex && !flag) { return true; } report.AppendLine("RESULT stopped-at-night=" + night + " reason=native-wave-desynchronization phase=" + phase + " expectedNativeWave=" + expectedIndex + " actualNativeWave=" + wavenumber + " expectedSpawns=" + num + " actualSpawns=" + totalEnemiesSpawnedThisWave + " completed=" + completed + " daytime=" + ((object)DayNightCycle.Instance.CurrentTimestate/*cast due to .constrained prefix*/).ToString() + " simulatedTime=" + Time.time.ToString("F3", CultureInfo.InvariantCulture)); return false; } internal static string DescribeEconomy() { StringBuilder stringBuilder = new StringBuilder("Falkenpass native economy and combat snapshot\n"); stringBuilder.AppendLine("wave=" + EnemySpawner.instance.Wavenumber + " gold=" + PlayerInteraction.instance.Balance + " trueGold=" + PlayerInteraction.instance.TrueBalance); stringBuilder.AppendLine("loadout=" + string.Join(",", PerkManager.instance.CurrentlyEquipped.Select((Equippable p) => ((Object)p).name))); foreach (IGrouping<string, BuildSlot> item in from b in MapBuilder.KeptBuildings group b by b.buildingName) { stringBuilder.AppendLine("BUILDING " + item.Key + " sites=" + item.Count()); foreach (BuildSlot item2 in item) { Hp val = (Object.op_Implicit((Object)(object)item2.BuildingParent) ? item2.BuildingParent.GetComponent<Hp>() : null); stringBuilder.AppendLine("SITE " + ((Object)item2).name + " level=" + item2.Level + " income=" + item2.GoldIncome + " hp=" + (Object.op_Implicit((Object)(object)val) ? val.maxHp.ToString("F1", CultureInfo.InvariantCulture) : "none")); for (int num = 0; num < item2.upgrades.Count; num++) { Upgrade val2 = item2.upgrades[num]; stringBuilder.AppendLine(" TIER " + (num + 1) + " cost=" + val2.cost + " disabled=" + val2.disableInThisMode + " tooltip=" + val2.upgradeTooltip); for (int num2 = 0; num2 < val2.upgradeBranches.Count; num2++) { UpgradeBranch val3 = val2.upgradeBranches[num2]; stringBuilder.AppendLine(" BRANCH " + num2 + " name=" + val3.choiceDetails?.name + " incomeDelta=" + val3.goldIncomeChange + " hpDelta=" + val3.hpChange + " tooltip=" + val3.choiceDetails?.tooltip); foreach (GameObject item3 in val3.objectsToActivate.Where((GameObject o) => Object.op_Implicit((Object)(object)o))) { stringBuilder.AppendLine(" activates=" + ((Object)item3).name + " components=" + string.Join(",", (from c in item3.GetComponentsInChildren<Component>(true) where Object.op_Implicit((Object)(object)c) select ((object)c).GetType().Name).Distinct())); } } } AutoAttack[] componentsInChildren = ((Component)item2).GetComponentsInChildren<AutoAttack>(true); foreach (AutoAttack attack in componentsInChildren) { DescribeAttack(stringBuilder, attack); } } } foreach (GameObject item4 in (from s in EnemySpawner.instance.waves.SelectMany((Wave w) => w.spawns) select s.enemyPrefab).Distinct()) { Hp componentInChildren = item4.GetComponentInChildren<Hp>(true); PathfindMovementEnemy componentInChildren2 = item4.GetComponentInChildren<PathfindMovementEnemy>(true); stringBuilder.AppendLine("ENEMY " + ((Object)item4).name + " hp=" + (Object.op_Implicit((Object)(object)componentInChildren) ? componentInChildren.maxHp : 0f) + " speed=" + (Object.op_Implicit((Object)(object)componentInChildren2) ? componentInChildren2.movementSpeed : 0f)); AutoAttack[] componentsInChildren = item4.GetComponentsInChildren<AutoAttack>(true); foreach (AutoAttack attack2 in componentsInChildren) { DescribeAttack(stringBuilder, attack2); } } EternalTrialEnemySet[] array = Resources.FindObjectsOfTypeAll<EternalTrialEnemySet>(); foreach (EternalTrialEnemySet val4 in array) { foreach (EternalTrialEnemy item5 in val4.enemies.Where((EternalTrialEnemy e) => Object.op_Implicit((Object)(object)e.enemyPrefab))) { Hp componentInChildren3 = item5.enemyPrefab.GetComponentInChildren<Hp>(true); stringBuilder.AppendLine("CATALOGUE set=" + ((Object)val4).name + " enemy=" + ((Object)item5.enemyPrefab).name + " flying=" + item5.flying + " nativeDifficulty=" + item5.difficultyValue + " hp=" + (Object.op_Implicit((Object)(object)componentInChildren3) ? componentInChildren3.maxHp : 0f)); } } return stringBuilder.ToString(); } private static void DescribeAttack(StringBuilder report, AutoAttack attack) { AutoAttackTower val = (AutoAttackTower)(object)((attack is AutoAttackTower) ? attack : null); report.AppendLine(" ATTACK " + ((Object)attack).name + " type=" + ((object)attack).GetType().Name + " active=" + ((Behaviour)attack).isActiveAndEnabled + " cooldown=" + attack.cooldownDuration + " damageMulti=" + attack.DamageMultiplyer + " ranges=" + string.Join(",", attack.targetPriorities.Select((TargetPriority p) => p.range)) + " projectiles=" + ((!Object.op_Implicit((Object)(object)val)) ? 1 : val.simultaneousProjectiles) + " weapon=" + (Object.op_Implicit((Object)(object)attack.weapon) ? ((Object)attack.weapon).name : "none")); if (!Object.op_Implicit((Object)(object)attack.weapon)) { return; } foreach (DamageModifyer item in attack.weapon.directDamage) { report.AppendLine(" DIRECT add=" + item.damageAdded + " multi=" + item.damageMultiplyer + " tags=" + string.Join(",", item.requiredTags)); } foreach (DamageModifyer item2 in attack.weapon.splashDamage) { report.AppendLine(" SPLASH add=" + item2.damageAdded + " multi=" + item2.damageMultiplyer + " tags=" + string.Join(",", item2.requiredTags)); } } internal static void SaveEconomy() { if (!Plugin.Lab) { throw new InvalidOperationException("Balance inspection is limited to the isolated lab profile."); } File.WriteAllText(Path.Combine(Plugin.DataRoot, "balance-economy.txt"), DescribeEconomy()); } internal static IEnumerator VerifyExtensionComposition() { if (!Plugin.Lab || Running || (int)DayNightCycle.Instance.CurrentTimestate != 0) { throw new InvalidOperationException("Upgrade composition requires an idle daytime lab."); } StringBuilder report = new StringBuilder("UPGRADE MECHANICS PROBE: exact fixture funding; not economic or combat-balance evidence\n"); Running = true; try { VerifyInputQuarantine(report); BuildSlot castle = MapBuilder.KeptBuildings.First((BuildSlot b) => b.buildingName == "Castle Center"); BuildSlot[] towers = MapBuilder.KeptBuildings.Where((BuildSlot b) => b.buildingName == "Defense Tower").Take(6).ToArray(); if (towers.Length != 6 || !Object.op_Implicit((Object)(object)((Component)castle).GetComponent<ExtendedBuildingState>())) { throw new InvalidOperationException("Install extended upgrades before running their probe."); } int num = ((IEnumerable<BuildSlot>)(object)new BuildSlot[1] { castle }).Concat(towers).Sum((BuildSlot b) => b.upgrades.Skip(b.Level).Sum((Upgrade u) => u.cost)); int num2 = Math.Max(0, num - PlayerInteraction.instance.Balance); PlayerInteraction.instance.AddCoin(num2); report.AppendLine("fixtureBudget=" + num + " explicitFixtureGrant=" + num2); while (castle.CanBeUpgraded) { if (!Buy(castle, 0, report)) { throw new InvalidOperationException("Could not buy a castle fixture tier."); } yield return null; } for (int fixture = 0; fixture < towers.Length; fixture++) { int role = fixture % 3; bool healing = fixture >= 3; BuildSlot slot = towers[fixture]; ExtendedBuildingState state = ((Component)slot).GetComponent<ExtendedBuildingState>(); while (slot.Level < state.NativeTierCount) { int num3 = 0; if (healing && slot.Level == 2) { num3 = slot.upgrades[slot.Level].upgradeBranches.FindIndex((UpgradeBranch b) => b.choiceDetails.name.IndexOf("Healing", StringComparison.OrdinalIgnoreCase) >= 0); } if (num3 < 0 || !Buy(slot, num3, report)) { throw new InvalidOperationException("Could not buy a native tower fixture tier."); } yield return null; } AutoAttackTower attack = ((Component)slot).GetComponentInChildren<AutoAttackTower>(true); if (!Object.op_Implicit((Object)(object)attack)) { throw new InvalidOperationException("A tower fixture has no native attack."); } report.AppendLine("SOURCE colocated=" + Object.op_Implicit((Object)(object)((Component)attack).GetComponent<TaggedObject>()) + " resolved=" + Object.op_Implicit((Object)(object)ExtendedUpgrades.AttackSource(attack))); float originalCooldown = ((AutoAttack)attack).cooldownDuration; float originalDamage = ((AutoAttack)attack).DamageMultiplyer; int originalTargets = attack.simultaneousProjectiles; float originalRange = ((AutoAttack)attack).targetPriorities.Max((TargetPriority p) => p.range); Weapon nativeWeapon = ((AutoAttack)attack).weapon; ETargetingMode nativeMode = attack.targetingMode; float[] baselineHits = MeasureRoleHits(attack, healing); if (!Buy(slot, role, report)) { throw new InvalidOperationException("Could not buy a specialization fixture."); } yield return null; CheckRoleHits(report, attack, healing, role, masterwork: false, baselineHits); if (!Buy(slot, 0, report)) { throw new InvalidOperationException("Could not buy a masterwork fixture."); } yield return null; CheckRoleHits(report, attack, healing, role, masterwork: true, baselineHits); CheckAcquisition(report, attack, healing, role); Check(report, ExtendedUpgrades.RoleOf(slot) == (ExtendedUpgrades.TowerRole)(role + 1) && ExtendedUpgrades.IsMastered(slot), "Native paid choices retain selected role and masterwork"); Check(report, ExtendedUpgrades.IsHealing(slot) == healing && (Object)(object)((AutoAttack)attack).weapon == (Object)(object)nativeWeapon && attack.targetingMode == nativeMode, "Specialization preserves native weapon and healing/damage mode"); if (healing) { Check(report, ((AutoAttack)attack).targetPriorities.All((TargetPriority p) => p.mustHaveTags.Contains((ETag)0) && p.mayNotHaveTags.Contains((ETag)8) && !p.mustHaveTags.Contains((ETag)1)), "Healing roles keep allied living-unit targeting and exclude buildings"); string[] array = new string[3] { "Triage battery", "Field hospital", "Signal sanctuary" }; Check(report, slot.upgrades[state.NativeTierCount].upgradeBranches[role].choiceDetails.name == "Falkenpass/" + array[role] && slot.upgrades[state.NativeTierCount + 1].upgradeBranches[0].choiceDetails.name == "Falkenpass/Healing masterwork", "Healing roles expose accurate native choice labels"); } float expectedCooldown = originalCooldown * role switch { 1 => 1.2f, 0 => 0.85f, _ => 1f, }; float expectedDamage = originalDamage * ((role == 0) ? 0.8f : 1f); int expectedTargets = originalTargets + role switch { 2 => 3, 0 => 4, _ => 0, }; float expectedRange = originalRange * role switch { 2 => 1.3f, 1 => 1.2f, _ => 1f, }; Check(report, Mathf.Approximately(((AutoAttack)attack).cooldownDuration, expectedCooldown) && Mathf.Approximately(((AutoAttack)attack).DamageMultiplyer, expectedDamage) && attack.simultaneousProjectiles == expectedTargets && Mathf.Approximately(((AutoAttack)attack).targetPriorities.Max((TargetPriority p) => p.range), expectedRange), "Role composes with native tower effects: " + state.Role.ToString() + " healing=" + healing); Hp hp = slot.BuildingParent.GetComponent<Hp>(); float maxHp = hp.maxHp; hp.MakeInvulerable(false); hp.TakeDamage(float.MaxValue, (TaggedObject)null, false, false); yield return null; Check(report, hp.HpValue <= 0f, "Tower enters native destroyed state"); hp.Revive(true, 1f); yield return null; state.Refresh(); state.Refresh(); Check(report, hp.HpValue > 0f && Mathf.Approximately(hp.maxHp, maxHp) && Mathf.Approximately(((AutoAttack)attack).cooldownDuration, expectedCooldown) && Mathf.Approximately(((AutoAttack)attack).DamageMultiplyer, expectedDamage) && attack.simultaneousProjectiles == expectedTargets && Mathf.Approximately(((AutoAttack)attack).targetPriorities.Max((TargetPriority p) => p.range), expectedRange), "Native repair and repeated refresh preserve effects without stacking"); Check(report, ExtendedUpgrades.BonusSight(slot) == (float)((role == 2) ? 36 : 0), "Only beacon role receives extra live sight"); DescribeAttack(report, (AutoAttack)(object)attack); } report.AppendLine("This probe covers three damaging and three healing specializations, including native impact damage/healing against explicit temporary tagged fixtures. It does not validate projectile travel, every native branch, save/load, UI layout, or campaign difficulty."); } finally { Running = false; File.WriteAllText(Path.Combine(Plugin.DataRoot, "balance-upgrade-composition.txt"), report.ToString()); } } private static void Check(StringBuilder report, bool condition, string label) { report.AppendLine((condition ? "PASS " : "FAIL ") + label); } private unsafe static void CheckAcquisition(StringBuilder report, AutoAttackTower attack, bool healing, int role) { //IL_0013: Unknown result type (might be due to invalid IL or missing references) //IL_0018: Unknown result type (might be due to invalid IL or missing references) //IL_001f: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) //IL_004b: Unknown result type (might be due to invalid IL or missing references) //IL_048d: Unknown result type (might be due to invalid IL or missing references) //IL_0499: Unknown result type (might be due to invalid IL or missing references) //IL_049a: Unknown result type (might be due to invalid IL or missing references) //IL_03cb: Unknown result type (might be due to invalid IL or missing references) //IL_03d3: Unknown result type (might be due to invalid IL or missing references) //IL_03d5: Unknown result type (might be due to invalid IL or missing references) Vector3 position = ((Component)attack).transform.position; ETargetingMode targetingMode = attack.targetingMode; List<Hp> fixtures = new List<Hp>(); ((Component)attack).transform.position = new Vector3(10000f, 10000f, 10000f); try { if (healing) { Hp val = Target("wounded troop", 3f, (ETag[])(object)new ETag[2] { default(ETag), (ETag)7 }); Check(report, Exactly((Hp[])(object)new Hp[1] { val }), "Native acquisition singleton wounded ally receives one pulse: role=" + role); Hp val2 = Target("wounded rider", 1f, (ETag[])(object)new ETag[2] { default(ETag), (ETag)2 }); Hp[] expected = (Hp[])(object)((attack.simultaneousProjectiles <= 1) ? new Hp[1] { val } : new Hp[2] { val, val2 }); Check(report, Exactly(expected), "Native acquisition fills distinct wounded allies up to native capacity=" + attack.simultaneousProjectiles + " role=" + role); if (role == 1) { Check(report, (Object)(object)Acquire().FirstOrDefault() == (Object)(object)val.TaggedObj, "Hospital prioritizes wounded troops before the rider"); } Target("healthy troop", 0.5f, (ETag[])(object)new ETag[2] { default(ETag), (ETag)7 }).SetHpToMaxHp(); Target("wounded building", 0.25f, (ETag[])(object)new ETag[2] { default(ETag), (ETag)8 }); Check(report, Exactly(expected), "Native acquisition excludes full-health allies and buildings: role=" + role); ((Component)val2).gameObject.SetActive(false); Check(report, Exactly((Hp[])(object)new Hp[1] { val }), "Native acquisition clears stale fallback healing targets: role=" + role); return; } Hp obj; switch (role) { default: obj = Target("infantry", 4f, (ETag[])(object)new ETag[1] { (ETag)1 }); break; case 2: obj = Target("flyer", 4f, (ETag[])(object)new ETag[2] { (ETag)1, (ETag)6 }); break; case 1: { ETag[] array = new ETag[3]; RuntimeHelpers.InitializeArray(array, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/); obj = Target("ram", 4f, (ETag[])(object)array); break; } } Hp val3 = obj; Check(report, Exactly((Hp[])(object)new Hp[1] { val3 }), "Native acquisition singleton enemy receives one shot: role=" + role); Hp val4 = Target("fallback infantry", 1f, (ETag[])(object)new ETag[1] { (ETag)1 }); Check(report, Exactly((Hp[])(object)new Hp[2] { val3, val4 }), "Native acquisition fills distinct priority and fallback targets: role=" + role); if (role != 0) { Check(report, (Object)(object)Acquire().FirstOrDefault() == (Object)(object)val3.TaggedObj, "Native acquisition prefers declared enemy role before closer infantry: role=" + role); } if (role == 1) { Hp val5 = Target("large only", 2f, (ETag[])(object)new ETag[2] { (ETag)1, (ETag)41 }); Hp val6 = Target("siege only", 5f, (ETag[])(object)new ETag[2] { (ETag)1, (ETag)9 }); Check(report, Exactly((Hp[])(object)new Hp[2] { val3, val6 }), "Siege acquisition includes siege-only and combined-tag enemies before large-only enemies"); ((Component)val3).gameObject.SetActive(false); ((Component)val6).gameObject.SetActive(false); Check(report, Exactly((Hp[])(object)new Hp[2] { val5, val4 }), "Large-only enemy retains priority and leaves a distinct fallback slot"); ((Component)val5).gameObject.SetActive(false); ((Component)val3).gameObject.SetActive(true); } ETargetingMode[] array2 = new ETargetingMode[4]; RuntimeHelpers.InitializeArray(array2, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/); ETargetingMode[] array3 = (ETargetingMode[])(object)array2; for (int i = 0; i < array3.Length; i++) { ETargetingMode targetingMode2 = array3[i]; attack.targetingMode = targetingMode2; Check(report, Exactly((Hp[])(object)new Hp[2] { val3, val4 }), "Native acquisition distinct targets with mode=" + ((object)(*(ETargetingMode*)(&targetingMode2))/*cast due to .constrained prefix*/).ToString() + " role=" + role); ((Component)val4).gameObject.SetActive(false); Check(report, Exactly((Hp[])(object)new Hp[1] { val3 }), "Native acquisition clears scratch buffer with mode=" + ((object)(*(ETargetingMode*)(&targetingMode2))/*cast due to .constrained prefix*/).ToString() + " role=" + role); ((Component)val4).gameObject.SetActive(true); } } finally { ((Component)attack).transform.position = position; attack.targetingMode = targetingMode; foreach (Hp item in fixtures) { ((Component)item).gameObject.SetActive(false); Object.Destroy((Object)(object)((Component)item).gameObject); } attack.FindAutoAttackTargets(); } TaggedObject[] Acquire() { attack.FindAutoAttackTargets(); return ((List<TaggedObject>)AccessTools.Field(typeof(AutoAttackTower), "attackTargets").GetValue(attack)).ToArray(); } bool Exactly(params Hp[] array4) { TaggedObject[] actual = Acquire(); if (actual.Length == array4.Length && actual.Distinct().Count() == actual.Length) { return array4.All((Hp h) => actual.Contains(h.TaggedObj)); } return false; } Hp Target(string name, float distance, params ETag[] tags) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Unknown result type (might be due to invalid IL or missing references) //IL_0025: Unknown result type (might be due to invalid IL or missing references) //IL_0036: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0041: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_0081: Unknown result type (might be due to invalid IL or missing references) //IL_0088: Unknown result type (might be due to invalid IL or missing references) GameObject val7 = new GameObject("Falkenpass acquisition fixture " + name); val7.transform.SetParent(MapBuilder.Root.transform); val7.transform.position = ((Component)attack).transform.position + Vector3.forward * distance; Hp val8 = val7.AddComponent<Hp>(); val8.maxHp = 20000f; val8.Revive(false, 1f); val8.SetHpTo(10000f); foreach (ETag val9 in tags) { val8.TaggedObj.AddTag(val9); } fixtures.Add(val8); return val8; } } private static float[] MeasureRoleHits(AutoAttackTower attack, bool healing) { if (!healing) { float[] obj = new float[5] { MeasureNativeHit(attack, (ETag)1), MeasureNativeHit(attack, (ETag)1, (ETag)41), MeasureNativeHit(attack, (ETag)1, (ETag)9), 0f, 0f }; ETag[] array = new ETag[3]; RuntimeHelpers.InitializeArray(array, (RuntimeFieldHandle)/*OpCode not supported: LdMemberToken*/); obj[3] = MeasureNativeHit(attack, (ETag[])(object)array); obj[4] = MeasureNativeHit(attack, (ETag)1, (ETag)6); return obj; } return new float[1] { MeasureNativeHit(attack, default(ETag), (ETag)7) }; } private static float MeasureNativeHit(AutoAttackTower attack, params ETag[] tags) { //IL_0005: Unknown result type (might be due to invalid IL or missing references) //IL_000b: Expected O, but got Unknown //IL_002c: Unknown result type (might be due to invalid IL or missing references) //IL_0068: Unknown result type (might be due to invalid IL or missing references) //IL_0070: Unknown result type (might be due to invalid IL or missing references) GameObject val = new GameObject("Falkenpass impact mechanics fixture"); val.transform.SetParent(MapBuilder.Root.transform); val.transform.position = ((Component)attack).transform.position; try { Hp val2 = val.AddComponent<Hp>(); val2.maxHp = 20000f; val2.Revive(false, 1f); val2.SetHpTo(10000f); foreach (ETag val3 in tags) { val2.TaggedObj.AddTag(val3); } ((AutoAttack)attack).weapon.DealDamage(val2, ((AutoAttack)attack).DamageMultiplyer, ExtendedUpgrades.AttackSource(attack), false); return 10000f - val2.HpValue; } finally { val.SetActive(false); Object.Destroy((Object)(object)val); } } private static void CheckRoleHits(StringBuilder report, AutoAttackTower attack, bool healing, int role, bool masterwork, float[] before) { float[] array = MeasureRoleHits(attack, healing); for (int i = 0; i < before.Length; i++) { float num = ((role == 0) ? 0.8f : 1f); if (healing && role == 1) { num = (masterwork ? 2.5f : 1.75f); } else if (!healing && role == 1 && i >= 1 && i <= 3) { num = (masterwork ? 4f : 2.5f); } else if (!healing && role == 2 && i == 4) { num = (masterwork ? 3 : 2); } bool flag = (healing ? (before[i] < 0f && array[i] < 0f) : (before[i] > 0f && array[i] > 0f)); Check(report, flag && Mathf.Abs(array[i] - before[i] * num) < 0.02f, "Native impact role=" + role + " healing=" + healing + " masterwork=" + masterwork + " target=" + i + " before=" + before[i] + " after=" + array[i] + " expectedFactor=" + num); } } internal static bool Buy(BuildSlot slot, int branch, StringBuilder ledger = null) { //IL_0029: Unknown result type (might be due to invalid IL or missing references) if (!Plugin.Lab || !Object.op_Implicit((Object)(object)slot) || !((Component)slot).gameObject.activeInHierarchy || !slot.CanBeUpgraded || (int)DayNightCycle.Instance.CurrentTimestate != 0) { return false; } int nextUpgradeOrBuildCost = slot.NextUpgradeOrBuildCost; int level = slot.Level; if (PlayerInteraction.instance.Balance < nextUpgradeOrBuildCost) { return false; } List<UpgradeBranch> upgradeBranches = slot.upgrades[level].upgradeBranches; branch = Mathf.Clamp(branch, 0, upgradeBranches.Count - 1); if (!upgradeBranches[branch].choiceDetails.CanBePicked || upgradeBranches[branch].choiceDetails.disabledInThisMode) { return false; } purchaseOrigin = slot; purchaseChoice = upgradeBranches[branch].choiceDetails; try { slot.TryToBuildOrUpgradeAndPay(PlayerInteraction.instance, true); } finally { purchaseOrigin = null; purchaseChoice = null; } if (slot.Level != level + 1) { return false; } PlayerInteraction.instance.SpendCoins(nextUpgradeOrBuildCost); if (Object.op_Implicit((Object)(object)slot.buildingInteractor)) { slot.buildingInteractor.MarkAsHarvested(); slot.buildingInteractor.UpdateInteractionState(false, (InteractionState)0); } ((Component)slot).GetComponent<ExtendedBuildingState>()?.Refresh(); ledger?.AppendLine("BUY night=" + (EnemySpawner.instance.Wavenumber + 2) + " site=" + ((Object)slot).name + " territory=" + (purchaseTerritories.TryGetValue(slot, out var value) ? value : "fixture") + " tier=" + slot.Level + " branch=" + branch + " choice=" + upgradeBranches[branch].choiceDetails.name + " cost=" + nextUpgradeOrBuildCost + " bank=" + PlayerInteraction.instance.Balance); return true; } internal static IEnumerator RunCampaign(bool activePilot, bool lateUpgrades, int seed = 417) { if (!Plugin.Lab || Running) { throw new InvalidOperationException("Campaign probes require an idle isolated lab."); } if (EnemySpawner.instance.Wavenumber != -1 || MapBuilder.KeptBuildings.Any((BuildSlot b) => b.Level != 0) || PlayerInteraction.instance.Balance != 12) { throw new InvalidOperationException("A funded campaign requires a fresh map and its actual starting gold."); } Running = true; Random.InitState(seed); string label = "funded-" + (activePilot ? "pilot" : "idle") + (lateUpgrades ? "-extended" : "-native") + "-" + seed; StringBuilder ledger = new StringBuilder("FUNDED NATIVE CAMPAIGN 30-night stepped enemy ramp; regular tentacles from night ten\n"); ledger.AppendLine("Automatic purchase planner; " + (activePilot ? "native-input scripted combat pilot v8 (isolated input, strict native wave/count checks, native paths, visible castle response and artillery interception, paid purchase harvest bookkeeping)" : "no active player combat; isolated input and strict native wave/count checks") + ". This is not a human skill estimate."); ledger.AppendLine("No granted coins, enemy deletion, healing, invulnerability, teleports, or skipped waves. Troop home orders are scripted."); ledger.AppendLine("seed=" + seed + " loadout=" + string.Join(",", PerkManager.instance.CurrentlyEquipped.Select((Equippable p) => ((Object)p).name))); int earned = 0; int spent = 0; UnityAction<int> gain = delegate(int n) { earned += n; }; UnityAction<int> spend = delegate(int n) { spent += n; }; PlayerInteraction player = PlayerInteraction.instance; player.onBalanceGain.AddListener(gain); player.onBalanceSpend.AddListener(spend); BalancePilot pilot = MapBuilder.Root.AddComponent<BalancePilot>(); pilot.CombatEnabled = activePilot; BuildSlot val = MapBuilder.KeptBuildings.First((BuildSlot b) => b.buildingName == "Castle Center"); Hp castleHp = val.BuildingParent.GetComponent<Hp>(); float previousScale = Time.timeScale; try { VerifyInputQuarantine(ledger); Time.timeScale = 4f; for (int night = 1; night <= 30; night++) { float coinDeadline = Time.time + 90f; while ((CoinSpawner.AllCoinsLeftToBeSpawned > 0 || player.TrueBalance > player.Balance || !DayNightCycle.Instance.AfterSunrise) && Time.time < coinDeadline) { if (!ValidateWave(ledger, night, "dawn", night - 2)) { yield break; } yield return null; } if (!ValidateWave(ledger, night, "before-purchases", night - 2)) { yield break; } if ((int)DayNightCycle.Instance.CurrentTimestate != 0 || !DayNightCycle.Instance.AfterSunrise || (int)LocalGamestate.Instance.CurrentState != 1) { ledger.AppendLine("RESULT stopped-before-night=" + night + " reason=native-dawn-not-ready"); yield break; } yield return PurchaseDay(night, lateUpgrades, ledger); if (!ValidateWave(ledger, night, "after-purchases", night - 2)) { yield break; } OrderGarrisons(); pilot.PrepareForNight(night); float prepareDeadline = Time.time + 45f; while (!pilot.Prepared && Time.time < prepareDeadline) { if (!ValidateWave(ledger, night, "preparation", night - 2)) { yield break; } yield return null; } yield return (object)new WaitForSeconds(6f); if (!ValidateWave(ledger, night, "before-dusk", night - 2)) { yield break; } ledger.AppendLine("PREPARE night=" + night + " ready=" + pilot.Prepared + " " + pilot.Describe()); if (!pilot.Prepared) { ledger.AppendLine("RESULT stopped-before-night=" + night + " reason=pilot-navigation-preparation; inspect native paths and collision"); yield break; } int invested = spent; string text = "DUSK night=" + night + " bank=" + player.Balance + " earnedSinceStart=" + earned + " spent=" + spent + " income=" + MapBuilder.KeptBuildings.Sum((BuildSlot b) => b.GoldIncome) + " built=" + MapBuilder.KeptBuildings.Count((BuildSlot b) => b.Level > 0) + "/" + MapBuilder.KeptBuildings.Count + " maxed=" + MapBuilder.KeptBuildings.Count((BuildSlot b) => b.Level == b.upgrades.Count) + " extendedTowers=" + MapBuilder.KeptBuildings.Count((BuildSlot b) => ExtendedUpgrades.RoleOf(b) != ExtendedUpgrades.TowerRole.None) + " masterworks=" + MapBuilder.KeptBuildings.Count(ExtendedUpgrades.IsMastered); if (night == 30) { SavePurchaseManifest(label, earned, spent); } float minimumCastle = 1f; float minimumPlayer = 1f; float begin = Time.time; float minimumGate = 1f; float minimumWestGate = 1f; float minimumEastGate = 1f; int peakEnemies = 0; HashSet<BuildSlot> destroyed = new HashSet<BuildSlot>(); DayNightCycle.Instance.SwitchToNight(); if (!ValidateWave(ledger, night, "dusk", night - 1)) { yield break; } ledger.AppendLine(text + " nativeWave=" + EnemySpawner.instance.Wavenumber + " expectedSpawns=" + CampaignBalance.Nights[night - 1].Count + " " + DescribeTreasury()); SaveRun(label, ledger); float deadline = Time.realtimeSinceStartup + 480f; float nextPilotSample = Time.time; while ((int)DayNightCycle.Instance.CurrentTimestate != 1 && Time.realtimeSinceStartup < deadline && (int)LocalGamestate.Instance.CurrentState == 1) { if (!ValidateWave(ledger, night, "await-night", night - 1)) { yield break; } yield return null; } while ((int)DayNightCycle.Instance.CurrentTimestate == 1 && Time.realtimeSinceStartup < deadline && (int)LocalGamestate.Instance.CurrentState == 1) { if (!ValidateWave(ledger, night, "night", night - 1)) { yield break; } if (Time.time >= nextPilotSample) { ledger.AppendLine("PILOT night=" + night + " " + pilot.Describe()); AppendFortress(ledger, night, begin, castleHp, destroyed); SaveRun(label, ledger); nextPilotSample = Time.time + 10f; } minimumCastle = Mathf.Min(minimumCastle, castleHp.HpPercentage); minimumPlayer = Mathf.Min(minimumPlayer, PlayerMovement.instance.Hp.HpPercentage); foreach (BuildSlot item in MapBuilder.KeptGates.Where((BuildSlot g) => g.Level > 0)) { minimumGate = Mathf.Min(minimumGate, item.BuildingParent.GetComponent<Hp>().HpPercentage); if (((Component)item).transform.position.x < 0f) { minimumWestGate = Mathf.Min(minimumWestGate, item.BuildingParent.GetComponent<Hp>().HpPercentage); } else { minimumEastGate = Mathf.Min(minimumEastGate, item.BuildingParent.GetComponent<Hp>().HpPercentage); } } peakEnemies = Mathf.Max(peakEnemies, EnemySpawner.instance.NumberOfEnemiesOnTheMap); foreach (BuildSlot item2 in MapBuilder.KeptBuildings.Where((BuildSlot b) => b.Level > 0)) { Hp component = item2.BuildingParent.GetComponent<Hp>(); if (Object.op_Implicit((Object)(object)component) && component.HpValue <= 0f && destroyed.Add(item2)) { AppendLoss(ledger, night, begin, item2); } } yield return null; } bool flag = ((int)DayNightCycle.Instance.CurrentTimestate == 0 && EnemySpawner.instance.NumberOfEnemiesOnTheMap == 0 && castleHp.HpValue > 0f) || (night == 30 && (int)LocalGamestate.Instance.CurrentState == 2); if (!ValidateWave(ledger, night, "completion", night - 1, flag)) { yield break; } ledger.AppendLine("NIGHT night=" + night + " completed=" + flag + " state=" + ((object)LocalGamestate.Instance.CurrentState/*cast due to .constrained prefix*/).ToString() + " simulatedSeconds=" + (Time.time - begin).ToString("F1", CultureInfo.InvariantCulture) + " minimumCastle=" + minimumCastle.ToString("F4", CultureInfo.InvariantCulture) + " minimumGate=" + minimumGate.ToString("F4", CultureInfo.InvariantCulture) + " minimumWestGate=" + minimumWestGate.ToString("F4", CultureInfo.InvariantCulture) + " minimumEastGate=" + minimumEastGate.ToString("F4", CultureInfo.InvariantCulture) + " minimumPlayer=" + minimumPlayer.ToString("F4", CultureInfo.InvariantCulture) + " lostStructures=" + destroyed.Count + " destroyed=" + string.Join(";", destroyed.Select((BuildSlot s) => ((Object)s).name)) + " peakEnemies=" + peakEnemies + " remaining=" + EnemySpawner.instance.NumberOfEnemiesOnTheMap + " nativeSpawns=" + EnemySpawner.totalEnemiesSpawnedThisWave + " attacks=" + pilot.ActiveAttacks + " invested=" + invested); SaveRun(label, ledger); if (!flag) { ledger.AppendLine("RESULT stopped-at-night=" + night + " reason=" + ((Time.realtimeSinceStartup >= deadline) ? "timeout; inspect routes and living enemies" : "defeat")); yield break; } } float victoryDeadline = Time.realtimeSinceStartup + 30f; while ((int)LocalGamestate.Instance.CurrentState == 1 && Time.realtimeSinceStartup < victoryDeadline) { if (!ValidateWave(ledger, 30, "await-victory", 29, completed: true)) { yield break; } yield return null; } ledger.AppendLine("RESULT nativeVictory=" + ((int)LocalGamestate.Instance.CurrentState == 2) + " earned=" + earned + " spent=" + spent + " bank=" + player.Balance); } finally { Time.timeScale = previousScale; player.onBalanceGain.RemoveListener(gain); player.onBalanceSpend.RemoveListener(spend); Object.Destroy((Object)(object)pilot); Running = false; SaveRun(label, ledger); Plugin.Instance.Log("Balance run stopped: " + label); } } private static void SaveRun(string label, StringBuilder ledger) { File.WriteAllText(Path.Combine(Plugin.DataRoot, "balance-" + label + ".txt"), ledger.ToString()); } private static void AppendFortress(StringBuilder report, int night, float begin, Hp castle, HashSet<BuildSlot> lost) { //IL_0069: Unknown result type (might be due to invalid IL or missing references) float num = 1f; float num2 = 1f; foreach (BuildSlot item in MapBuilder.KeptGates.Where((BuildSlot g) => g.Level > 0)) { float hpPercentage = item.BuildingParent.GetComponent<Hp>().HpPercentage; if (((Component)item).transform.position.x < 0f) { num = hpPercentage; } else { num2 = hpPercentage; } } float num3 = (from e in TagManager.instance.EnemyUnits where Object.op_Implicit((Object)(object)e) && Object.op_Implicit((Object)(object)e.Hp) && e.Hp.HpValue > 0f select Vector3.Distance(((Component)e).transform.position, ((Component)castle).transform.position)).DefaultIfEmpty(-1f).Min(); report.AppendLine("FORTRESS night=" + night + " elapsed=" + (Time.time - begin).ToString("F1", CultureInfo.InvariantCulture) + " castle=" + castle.HpPercentage.ToString("F4", CultureInfo.InvariantCulture) + " westGate=" + num.ToString("F4", CultureInfo.InvariantCulture) + " eastGate=" + num2.ToString("F4", CultureInfo.InvariantCulture) + " livingEnemies=" + EnemySpawner.instance.NumberOfEnemiesOnTheMap + " nativeSpawns=" + EnemySpawner.totalEnemiesSpawnedThisWave + " closestEnemyToKeep=" + num3.ToString("F1", CultureInfo.InvariantCulture) + " lost=" + lost.Count + " lostTowers=" + lost.Count((BuildSlot b) => b.buildingName == "Defense Tower") + " lossByTerritory=" + string.Join(";", from b in lost group b by (!purchaseTerritories.TryGetValue(b, out var value)) ? "unknown" : value into g select g.Key.Replace(' ', '_') + ":" + g.Count())); foreach (var item2 in from e in TagManager.instance.EnemyUnits where Object.op_Implicit((Object)(object)e) && Object.op_Implicit((Object)(object)e.Hp) && e.Hp.HpValue > 0f && (((Object)e).name.StartsWith("E Thing", StringComparison.Ordinal) || ((Object)e).name.StartsWith("E Windboat", StringComparison.Ordinal) || ((Object)e).name.StartsWith("E Catapult", StringComparison.Ordinal) || ((Object)e).name.StartsWith("E Ram", StringComparison.Ordinal) || ((Object)e).name.StartsWith("E Ogre", StringComparison.Ordinal)) group e by new { Side = ((((Component)e).transform.position.x < 0f) ? "west" : "east"), Type = ((Object)e).name.Replace("(Clone)", "").Trim().Replace(' ', '_'), Elite = e.Hp.Elite, KeepAssault = CampaignBalance.IsKeepAssault(e) }) { IEnumerable<string> values = from g in item2.Select(delegate(TaggedObject e) { PathfindMovementEnemy component = ((Component)e).GetComponent<PathfindMovementEnemy>(); return (component == null) ? null : component.SeekToTargetObj; }).GroupBy(delegate(TaggedObject t) { if (!Object.op_Implicit((Object)(object)t)) { return "none"; } BuildSlot componentInParent = ((Component)t).GetComponentInParent<BuildSlot>(); return (((componentInParent != null) ? ((Object)componentInParent).name : null) ?? ((Object)t).name).Replace(' ', '_'); }) select g.Key + ":" + g.Count(); report.AppendLine("HEAVY night=" + night + " elapsed=" + (Time.time - begin).ToString("F1", CultureInfo.InvariantCulture) + " side=" + item2.Key.Side + " type=" + item2.Key.Type + " elite=" + item2.Key.Elite + " keepAssault=" + item2.Key.KeepAssault + " alive=" + item2.Count() + " remainingHp=" + item2.Sum((TaggedObject e) => e.Hp.HpValue).ToString("F1", CultureInfo.InvariantCulture) + " closestKeep=" + item2.Min((TaggedObject e) => Vector3.Distance(((Component)e).transform.position, ((Component)castle).transform.position)).ToString("F1", CultureInfo.InvariantCulture) + " nativeMovementTargets=" + string.Join(";", values)); } report.AppendLine("OBJECTIVES night=" + night + " " + CampaignBalance.DescribeObjectives(EnemySpawner.instance.waves[night - 1])); } private static void AppendLoss(StringBuilder report, int night, float begin, BuildSlot slot) { //IL_0066: Unknown result type (might be due to invalid IL or missing references) string value; string text = (purchaseTerritories.TryGetValue(slot, out value) ? value : "unknown"); report.AppendLine("LOSS night=" + night + " elapsed=" + (Time.time - begin).ToString("F1", CultureInfo.InvariantCulture) + " side=" + ((((Component)slot).transform.position.x < 0f) ? "west" : "east") + " territory=" + text.Replace(' ', '_') + " type=" + slot.buildingName.Replace(' ', '_') + " tier=" + slot.Level + " role=" + ExtendedUpgrades.RoleOf(slot).ToString() + " masterwork=" + ExtendedUpgrades.IsMastered(slot) + " site=" + ((Object)slot).name); } private static string DescribeFortressHealth() { IEnumerable<BuildSlot> source = ((IEnumerable<BuildSlot>)(object)new BuildSlot[1] { MapBuilder.KeptBuildings.First((BuildSlot b) => b.buildingName == "Castle Center") }).Concat(MapBuilder.KeptGates); return "FORTRESS_HEALTH " + string.Join(" ", source.Select(delegate(BuildSlot b) { //IL_002c: Unknown result type (might be due to invalid IL or missing references) Hp component = b.BuildingParent.GetComponent<Hp>(); return ((b.buildingName == "Castle Center") ? "castle" : ((((Component)b).transform.position.x < 0f) ? "westGate" : "eastGate")) + "=" + component.HpValue.ToString("F1", CultureInfo.InvariantCulture) + "/" + component.maxHp.ToString("F1", CultureInfo.InvariantCulture) + ":tier" + b.Level; })); } private static string DescribeTreasury() { BuildSlot obj = MapBuilder.KeptBuildings.First((BuildSlot b) => b.buildingName == "Castle Center"); List<string> list = new List<string>(); foreach (EmpireUpgrade item in from e in ((Component)obj).GetComponentsInChildren<EmpireUpgrade>(true) where ((Behaviour)e).isActiveAndEnabled select e) { float num = (float)AccessTools.Field(typeof(EmpireUpgrade), "currentlyAppliedMultiplyer").GetValue(item); int num2 = (int)AccessTools.Field(typeof(EmpireUpgrade), "goldToDoublePower").GetValue(item); float num3 = (float)AccessTools.Field(typeof(EmpireUpgrade), "towerDamageMultiplyer").GetValue(item); list.Add(((Object)item).name.Replace(' ', '_') + ":actual=" + num.ToString("F4", CultureInfo.InvariantCulture) + ":goldToDouble=" + num2 + ":nativeTower=" + num3.ToString("F4", CultureInfo.InvariantCulture)); } BlacksmithUpgrades instance = BlacksmithUpgrades.instance; return "treasuryBank=" + PlayerInteraction.instance.Balance + " empire=" + ((list.Count == 0) ? "none" : string.Join(";", list)) + " actualGlobalMeleeDamage=" + instance.meleeDamage.ToString("F4", CultureInfo.InvariantCulture) + " actualGlobalRangedDamage=" + instance.rangedDamage.ToString("F4", CultureInfo.InvariantCulture) + " actualGlobalMeleeResistance=" + instance.meleeResistance.ToString("F4", CultureInfo.InvariantCulture) + " actualGlobalRangedResistance=" + instance.rangedResistance.ToString("F4", CultureInfo.InvariantCulture); } private static void SavePurchaseManifest(string label, int earned, int spent) { StringBuilder stringBuilder = new StringBuilder("# Measured funded night-30 purchase manifest\n"); stringBuilder.AppendLine("# earned=" + earned + " spent=" + spent + " bank=" + PlayerInteraction.instance.Balance); foreach (BuildSlot keptBuilding in MapBuilder.KeptBuildings) { stringBuilder.AppendLine(((Object)keptBuilding).name + "|" + string.Join(",", keptBuilding.AppliedUpgrades)); } File.WriteAllText(Path.Combine(Plugin.DataRoot, "balance-" + label + "-night30-plan.txt"), stringBuilder.ToString()); } private static string GateAssaultTarget(TaggedObject target) { if (Object.op_Implicit((Object)(object)target)) { BuildSlot componentInParent = ((Component)target).GetComponentInParent<BuildSlot>(); return (((componentInParent != null) ? ((Object)componentInParent).name : null) ?? ((Object)target).name).Replace(' ', '_'); } return "none"; } internal static IEnumerator GateAssault() { if (!Plugin.Lab || Running || EnemySpawner.instance.Wavenumber != -1 || MapBuilder.KeptBuildings.Any((BuildSlot b) => b.Level != 0)) { throw new InvalidOperationException("Gate assault requires a fresh isolated lab map."); } StringBuilder report = new StringBuilder("NATIVE GATE ASSAULT: mechanics fixture, not a funded campaign or difficulty result\n"); report.AppendLine("Six finite one-enemy waves; ordinary native HP/damage/movement/weapons; simulationScale=1; no hero combat, troops, or towers. Castle1 and both Gate1 are paid from the actual starting twelve gold. Test enemies are removed after passage or a logged timeout so native dawn can repair the gate."); Wave[] originals = EnemySpawner.instance.waves.Take(6).ToArray(); List<GameObject> spawnLines = new List<GameObject>(); List<GameObject> testEnemies = new List<GameObject>(); float previousScale = Time.timeScale; BalancePilot pilot = null; Running = true; try { VerifyInputQuarantine(report); Time.timeScale = 1f; RefreshPurchaseTerritories(); if (!Buy(MapBuilder.KeptBuildings.First((BuildSlot b) => b.buildingName == "Castle Center"), 0, report)) { throw new InvalidOperationException("Unable to pay native castle1 cost."); } foreach (GateDefense.GateSpan span2 in GateDefense.Instance.Spans) { if (!Buy(span2.Slot, 0, report)) { throw new InvalidOperationException("Unable to pay native gate1 cost."); } } yield return null; pilot = MapBuilder.Root.AddComponent<BalancePilot>(); pilot.CombatEnabled = false; yield return null; GateDefense.Instance.FlushNavigation(); report.AppendLine("SETUP bank=" + PlayerInteraction.instance.Balance + " " + DescribeFortressHealth()); report.AppendLine(GateDefense.Instance.Describe()); report.AppendLine(GateDefense.Instance.NavigationProbe()); SaveRun("gate-assault", report); int waveIndex = 0; string[] array = new string[3] { "E Thing", "E Ram", "E Catapult" }; foreach (string enemyName in array) { foreach (GateDefense.GateSpan span in GateDefense.Instance.Spans.OrderBy((GateDefense.GateSpan s) => s.Center.x)) { BuildSlot slot = span.Slot; Hp health = span.Health; if ((int)DayNightCycle.Instance.CurrentTimestate != 0 || !DayNightCycle.Instance.AfterSunrise || EnemySpawner.instance.Wavenumber != waveIndex - 1 || !span.Alive || health.HpPercentage < 0.999f) { throw new InvalidOperationException("Gate case requires native dawn, exact wave index, and a repaired gate."); } GateDefense.Instance.FlushNavigation(); GameObject val = new GameObject("GateAssault " + enemyName + " " + ((Object)slot).name); spawnLines.Add(val); val.transform.SetParent(MapBuilder.Root.transform, false); float side = ((!(span.Center.x < 0f)) ? 1 : (-1)); val.transform.position = span.Center + Vector3.right * side * 35f; Spawn spawn = new Spawn(MapBuilder.EnemyPrefabs[enemyName], val.transform, 1, 1f, 0, 0f, false); EnemySpawner.instance.waves[waveIndex] = new Wave(spawn) { difficultyMulti = 1f, warningText = "Lab: ordinary native gate assault" }; EnemySpawner.instance.Refresh(); TaggedObject taggedGate = slot.BuildingParent.GetComponent<TaggedObject>(); string[] obj = new string[16] { "CASE wave=", waveIndex.ToString(), " enemy=", enemyName.Replace(' ', '_'), " gate=", ((Object)slot).name.Replace(' ', '_'), " spawn=", null, null, null, null, null, null, null, null, null }; Vector3 val2 = val.transform.position; obj[7] = ((Vector3)(ref val2)).ToString("F2"); obj[8] = " hp="; obj[9] = health.HpValue.ToString(); obj[10] = "/"; obj[11] = health.maxHp.ToString(); obj[12] = " tags="; obj[13] = string.Join(",", taggedGate.Tags); obj[14] = " tagCollider="; obj[15] = (Object.op_Implicit((Object)(object)taggedGate.colliderForBigOjectsToMeasureDistance) ? ((Object)taggedGate.colliderForBigOjectsToMeasureDistance).name : "none"); report.AppendLine(string.Concat(obj)); Check(report, taggedGate.Tags.Contains((ETag)12) && taggedGate.Tags.Contains((ETag)8) && taggedGate.Tags.Contains((ETag)0) && taggedGate.Tags.Contains((ETag)10), "Living native gate has wall/building/player/alive tags"); gateAssaultSlot = slot; gateAssaultAttack = null; gateAssaultLastTarget = null; gateAssaultGateAttackCount = 0; gateAssaultAttackCount = 0; DayNightCycle.Instance.SwitchToNight(); if (EnemySpawner.instance.Wavenumber != waveIndex) { throw new InvalidOperationException("Gate diagnostic native dusk index mismatch."); } float begin = Time.time; float nextSample = begin; float deadline = Time.realtimeSinceStartup + 120f; GameObject enemy = null; PathfindMovementEnemy movement = null; Hp enemyHealth = null; float minimumGate = health.HpValue; bool breached = false; bool passed = false; while (Time.realtimeSinceStartup < deadline && (int)LocalGamestate.Instance.CurrentState == 1) { if (EnemySpawner.instance.Wavenumber != waveIndex || EnemySpawner.totalEnemiesSpawnedThisWave > 1) { throw new InvalidOperationException("Gate diagnostic wave identity/count mismatch."); } if (!Object.op_Implicit((Object)(object)enemy)) { TaggedObject val3 = ((IEnumerable<TaggedObject>)TagManager.instance.EnemyUnits).FirstOrDefault((Func<TaggedObject, bool>)((TaggedObject e) => Object.op_Implicit((Object)(object)e) && Object.op_Implicit((Object)(object)e.Hp) && e.Hp.HpValue > 0f)); if (Object.op_Implicit((Object)(object)val3)) { enemy = ((Component)val3).gameObject; testEnemies.Add(enemy); movement = enemy.GetComponent<PathfindMovementEnemy>(); enemyHealth = val3.Hp; gateAssaultAttack = enemy.GetComponent<AutoAttack>(); if (!Object.op_Implicit((Object)(object)movement) || !Object.op_Implicit((Object)(object)gateAssaultAttack)) { throw new InvalidOperationException("Expected native movement and attack on diagnostic enemy."); } report.AppendLine("NATIVE enemyHp=" + enemyHealth.HpValue + "/" + enemyHealth.maxHp + " movementSpeed=" + movement.movementSpeed + " backupGraph=" + movement.backupMovementGraph); DescribeAttack(report, gateAssaultAttack); foreach (TargetPriority targetPriority in movement.targetPriorities) { report.AppendLine("MOVE_PRIORITY must=" + string.Join(",", targetPriority.mustHaveTags) + " excluded=" + string.Join(",", targetPriority.mayNotHaveTags)); } foreach (TargetPriority targetPriority2 in gateAssaultAttack.targetPriorities) { report.AppendLine("ATTACK_PRIORITY must=" + string.Join(",", targetPriority2.mustHaveTags) + " excluded=" + string.Join(",", targetPriority2.mayNotHaveTags) + " range=" + targetPriority2.range); } } } minimumGate = Mathf.Min(minimumGate, health.HpValue); breached |= health.HpValue <= 0f; if (Object.op_Implicit((Object)(object)enemy)) { passed = breached && (enemy.transform.position.x - span.Center.x) * side < -4f; if (Time.time >= nextSample || passed) { Vector3 val4 = (Object.op_Implicit((Object)(object)taggedGate.colliderForBigOjectsToMeasureDistance) ? taggedGate.colliderForBigOjectsToMeasureDistance.ClosestPoint(enemy.transform.position) : ((Component)taggedGate).transform.position); Vector3 val5 = ((movement.Path.Count > 0) ? movement.Path[movement.Path.Count - 1] : enemy.transform.position); string[] obj2 = new string[32] { "ASSAULT wave=", waveIndex.ToString(), " elapsed=", (Time.time - begin).ToString("F1"), " position=", null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null }; val2 = enemy.transform.position; obj2[5] = ((Vector3)(ref val2)).ToString("F2"); obj2[6] = " hp="; obj2[7] = enemyHealth.HpValue.ToString("F1"); obj2[8] = " gateHp="; obj2[9] = health.HpValue.ToString("F1"); obj2[10] = " colliderDistance="; obj2[11] = Vector3.Distance(enemy.transform.position, val4).ToString("F2"); obj2[12] = " colliderClosest="; obj2[13] = ((Vector3)(ref val4)).ToString("F2"); obj2[14] = " moveTarget="; obj2[15] = GateAssaultTarget(movement.SeekToTargetObj); obj2[16] = " snappedTarget="; val2 = movement.SeekToTargetPosSnappedtoNavmesh; obj2[17] = ((Vector3)(ref val2)).ToString("F2"); obj2[18] = " pathEnd="; obj2[19] = ((Vector3)(ref val5)).ToString("F2"); obj2[20] = " pathPoints="; obj2[21] = movement.Path.Count.ToString(); obj2[22] = " nativeAttacks="; obj2[23] = gateAssaultAttackCount.ToString(); obj2[24] = " gateAttacks="; obj2[25] = gateAssaultGateAttackCount.ToString(); obj2[26] = " lastAttackTarget="; obj2[27] = GateAssaultTarget(gateAssaultLastTarget); obj2[28] = " breached="; obj2[29] = breached.ToString(); obj2[30] = " passed="; obj2[31] = passed.ToString(); report.AppendLine(string.Concat(obj2)); SaveRun("gate-assault", report); nextSample = Time.time + 2f; } } if (passed) { break; } yield return null; } Check(report, EnemySpawner.totalEnemiesSpawnedThisWave == 1 && spawn.SpawnedUnits == 1 && spawn.Finished, "Exactly one ordinary authored native spawn wave=" + waveIndex); Check(report, gateAssaultGateAttackCount > 0 && minimumGate < health.maxHp, "Actual native weapon attacks damage the gate wave=" + waveIndex); Check(report, breached, "Native weapon destroys the closed gate wave=" + waveIndex); Check(report, passed, "Native movement crosses destroyed gate without teleport wave=" + waveIndex); report.AppendLine("CASE_RESULT wave=" + waveIndex + " passed=" + passed + " elapsed=" + (Time.time - begin).ToString("F1") + " attacks=" + gateAssaultAttackCount + " gateAttacks=" + gateAssaultGateAttackCount + " minimumGateHp=" + minimumGate.ToString("F1") + " cleanup=remove-test-enemy-after-observation"); gateAssaultAttack = null; if (Object.op_Implicit((Object)(object)enemy)) { Object.Destroy((Object)(object)enemy); } SaveRun("gate-assault", report); float dawnDeadline = Time.realtimeSinceStartup + 20f; while (((int)DayNightCycle.Instance.CurrentTimestate != 0 || !DayNightCycle.Instance.AfterSunrise) && Time.realtimeSinceStartup < dawnDeadline) { yield return null; } if ((int)DayNightCycle.Instance.CurrentTimestate != 0 || !DayNightCycle.Instance.AfterSunrise) { throw new InvalidOperationException("Native dawn did not complete after gate diagnostic cleanup."); } waveIndex++; } } report.AppendLine("RESULT cases=" + waveIndex + " failures=" + report.ToString().Split('\n').Count((string l) => l.StartsWith("FAIL ", StringComparison.Ordinal)) + " " + pilot.Describe()); } finally { gateAssaultAttack = null; gateAssaultLastTarget = null; gateAssaultSlot = null; foreach (GameObject item in testEnemies) { if (Object.op_Implicit((Object)(object)item)) { Object.Destroy((Object)(object)item); } } for (int num2 = 0; num2 < originals.Length; num2++) { EnemySpawner.instance.waves[num2] = originals[num2]; } foreach (GameObject item2 in spawnLines) { if (Object.op_Implicit((Object)(object)item2)) { Object.Destroy((Object)(object)item2); } } if (Object.op_Implicit((Object)(object)pilot)) { Object.Destroy((Object)(object)pilot); } SaveRun("gate-assault", report); Time.timeScale = previousScale; Running = false; } } internal static IEnumerator ReplayFinale(string manifestName, bool activePilot, bool lateUpgrades, int seed = 417, bool baselineCadence = false, float simulationScale = 4f, int comparisonCandidate = 0, bool tacticalGarrisons = false) { if (!Plugin.Lab || Running || EnemySpawner.instance.Wavenumber != -1 || MapBuilder.KeptBuildings.Any((BuildSlot b) => b.Level != 0)) { throw new InvalidOperationException("A finale comparison requires a fresh isolated lab map."); } if (Path.GetFileName(manifestName) != manifestName || !manifestName.StartsWith("balance-funded-", StringComparison.Ordinal) || !manifestName.EndsWith("-night30-plan.txt", StringComparison.Ordinal)) { throw new InvalidOperationException("Use a measured funded campaign manifest from this lab profile."); } if (float.IsNaN(simulationScale) || float.IsInfinity(simulationScale) || simulationScale <= 0f || simulationScale > 4f) { throw new ArgumentOutOfRangeException("simulationScale", "Use a positive simulation scale no greater than the calibrated lab scale of four."); } if (comparisonCandidate != 0 && comparisonCandidate != 2 && comparisonCandidate != 3 && comparisonCandidate != 4 && comparisonCandidate != 5 && comparisonCandidate != 6) { throw new ArgumentOutOfRangeException("comparisonCandidate", "Select current (0) or preserved candidate 2, 3, 4, 5, or 6."); } if (baselineCadence && comparisonCandidate != 0 && comparisonCandidate != 2) { throw new ArgumentException("Select one preserved en