Decompiled source of Falkenpass v0.3.1

plugins/Falkenpass/Falkenpass.dll

Decompiled 5 days ago
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