using System; using System.Collections; using System.IO; using System.Linq; using GameCult.Caching; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using float2 = CultMath.float2; using float3 = CultMath.float3; // Batch proof that the presenters read the drawn pose through the clock: DrawnPoseTests pins the pose maths headlessly, // and this is what pins that each presenter passes ActionGameManager.Clock.Lead into it. A zone is loaded into a fresh // ZoneRenderer with the pickup and mine prefabs ARPG.unity's ZoneRenderer names; a loose Projectile is launched on the // zone. The zone is stepped once, then the clock is set to leads 0, 0.008 and 0.015 s with no step, and each presenter's // own Update places its transform. A body is drawn at its last step plus its step velocity times the lead, spelled here // from the body's fields, so a presenter that reads lead 0 (or half the lead) sits on the step and fails. // // Reached: ItemPickup, MineInstance, Projectile.Draw. Not reached: ZoneRenderer.Update (Lower needs a ComputeBuffer, a // GPU device) and the ActionGameManager markers (they need the ARPG scene running); a Lower split so its record fill is // pure, or a run with a GPU and ARPG.unity in play mode, would reach them. // // Unity -batchmode -nographics -projectPath -executeMethod Aetheria.EditorTools.DrawnPoseWiringVerify.Run -logFile // // Do not pass -quit: Run enters play mode (a domain reload), the check runs there, and the Editor exits itself, 0 on // success and 1 on any failure. namespace Aetheria.EditorTools { [InitializeOnLoad] public static class DrawnPoseWiringVerify { private const string Tag = "[DrawnPoseWiringVerify]"; private const string Flag = "DrawnPoseWiringVerify.running"; private const string Wiring = "DrawnPoseWiringVerify.wiring"; private const double Budget = 300; // real seconds private const float RoundSpeed = 100f, Tolerance = 1e-3f; private static IEnumerator _check; private static double _started; private static string _work; static DrawnPoseWiringVerify() { if (SessionState.GetBool(Flag, false)) EditorApplication.playModeStateChanged += OnPlayMode; } public static void Run() { try { SessionState.SetString(Wiring, ReadWiring()); SessionState.SetBool(Flag, true); EditorApplication.EnterPlaymode(); } catch (Exception exception) { Debug.LogException(exception); EditorApplication.Exit(1); } } private static void OnPlayMode(PlayModeStateChange change) { if (change != PlayModeStateChange.EnteredPlayMode) return; _work = Path.Combine(Path.GetTempPath(), $"drawn-pose-wiring-{Guid.NewGuid():N}"); Directory.CreateDirectory(_work); _started = Time.realtimeSinceStartupAsDouble; _check = Check(); EditorApplication.update += Step; } private static void Step() { try { if (Time.realtimeSinceStartupAsDouble - _started > Budget) throw new InvalidOperationException($"The check did not finish in {Budget} seconds."); if (_check.MoveNext()) return; Finish(0); } catch (Exception exception) { Debug.LogException(exception); Finish(1); } } private static void Finish(int code) { EditorApplication.update -= Step; SessionState.SetBool(Flag, false); try { if (Directory.Exists(_work)) Directory.Delete(_work, true); } catch (Exception) { /* a temp directory */ } EditorApplication.Exit(code); } private static void Require(bool condition, string what) { if (!condition) throw new InvalidOperationException(what); } private static IEnumerator Check() { var settings = AssetDatabase.LoadAssetAtPath("Assets/Resources/Settings.asset")?.GameplaySettings ?? throw new InvalidOperationException("No Settings.asset."); settings.FloaterLayerHeight = -1.5f; // nonzero, so a presenter that drops the floater layer fails var wired = SessionState.GetString(Wiring, "").Split('|').Select(entry => entry.Split('=')).ToDictionary(pair => pair[0], pair => pair[1]); var pickupPrefab = AssetDatabase.LoadAssetAtPath(wired["SimpleCommodityPickup"])?.GetComponent(); var minePrefab = AssetDatabase.LoadAssetAtPath(wired["MinePrefab"])?.GetComponent(); Require(pickupPrefab != null, "ZoneRenderer.SimpleCommodityPickup is not an ItemPickup in ARPG.unity."); Require(minePrefab != null, "ZoneRenderer.MinePrefab is not a MineInstance in ARPG.unity."); var oneCell = new Shape(1, 1); oneCell[new CultMath.int2(0, 0)] = true; var cache = AetheriaStores.Open(Path.Combine(_work, "wiring.cc"), catalogWritable: true); cache.Upsert(new VerseGrammar { Revision = 1 }); cache.Upsert(new SimpleCommodityData { Name = "Wiring Ore", Shape = oneCell, MaxStack = 100 }); cache.FlushAsync().Wait(); var ledger = new ProvenanceLedger(); var items = new ItemManager(cache, ledger, settings, _ => { }); var zone = new Zone(items, new PlanetSettings(), new ZonePack(), null, null); var rootObject = new GameObject("Wiring Zone Root"); var rendererObject = new GameObject("Wiring Zone Renderer"); rendererObject.SetActive(false); // inactive: ZoneRenderer.Update needs a GPU and is not part of this check var renderer = rendererObject.AddComponent(); renderer.ZoneRoot = rootObject.transform; renderer.SimpleCommodityPickup = pickupPrefab; renderer.MinePrefab = minePrefab; renderer.ItemManager = items; renderer.SectorBrushes = new GameObject("Wiring Brushes").transform; renderer.AmbientLight = AmbientEdgeVerify.NewAmbientLight(); renderer.SlimeGravityCamera = new GameObject("Wiring Gravity Camera").AddComponent(); var slimeObject = new GameObject("Wiring Slime"); slimeObject.SetActive(false); renderer.SlimeRenderer = slimeObject.AddComponent(); renderer.LoadZone(zone); var ore = new SimpleCommodity { Data = items.ItemData.RefOf(items.ItemData.GetByName("Wiring Ore")), Quantity = 7 }; var floating = zone.Release(ore, new float2(3, 4), new float2(10, 0)); var mine = new Mine { Body = new KinematicBody { Position = new float2(-20, 8), Velocity = new float2(0, 14) }, ArmingDelay = 1e6f, FuseDelay = 1f, Lifetime = 1e6f, BlastRadius = 1f }; zone.Lay(mine); // A round on its frozen line, no target and no outcome known: drawn at FireOrigin + direction * speed * (T - FireTime). var shot = new PendingShot { ShotId = 1, FireOrigin = new float3(0, 0, 0), FireTime = 0f, TravelDirection = new float2(1, 0), Speed = RoundSpeed, MaxRange = 1e6f, FireRange = 1e6f, CommitTime = 1e5f, ArrivalTime = 2e5f }; var projectile = new GameObject("Wiring Round").AddComponent(); projectile.Launch(zone, shot, new Vector3(0, 0, 0)); yield return null; zone.Update(.5f); var pickup = rootObject.GetComponentsInChildren(true).Single(p => p.Item == ore); var mineInstance = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None).Single(); var pickupVelocity = floating.Body.TotalVelocity; var mineVelocity = mine.Body.TotalVelocity; Require(Mathf.Abs(pickupVelocity.x) > 1f && Mathf.Abs(mineVelocity.y) > 1f, "The fixture's bodies stopped: a lead would not move them."); Vector2 baseline = default; foreach (var leadSeconds in new[] { 0f, .008f, .015f }) { ActionGameManager.Clock.Reset(); if (leadSeconds > 0) ActionGameManager.Clock.Advance(leadSeconds, _ => throw new InvalidOperationException("a step ran")); var lead = ActionGameManager.Clock.Lead; Require(Mathf.Abs(lead - leadSeconds) < 1e-6f, $"The clock's lead is {lead}, not {leadSeconds}."); yield return null; var seen = pickup.transform.position; var want = floating.Body.Position + pickupVelocity * lead; Require(Mathf.Abs(seen.x - want.x) < Tolerance && Mathf.Abs(seen.z - want.y) < Tolerance, $"ItemPickup at lead {lead}: drawn x/z {seen.x},{seen.z}, the step plus velocity times lead {want.x},{want.y}."); seen = mineInstance.transform.position; want = mine.Body.Position + mineVelocity * lead; Require(Mathf.Abs(seen.x - want.x) < Tolerance && Mathf.Abs(seen.z - want.y) < Tolerance, $"MineInstance at lead {lead}: drawn x/z {seen.x},{seen.z}, the step plus velocity times lead {want.x},{want.y}."); Require(Mathf.Abs(seen.y - DrawAhead.FloaterHeight(zone, settings, new float2(want.x, want.y), lead)) < Tolerance, $"MineInstance y {seen.y} is not on the floater layer."); // The round's line is a function of the drawn time: it moves RoundSpeed * lead along its direction between leads. seen = projectile.transform.position; if (leadSeconds == 0f) baseline = new Vector2(seen.x, seen.z); var moved = new Vector2(seen.x, seen.z) - baseline; Require(Mathf.Abs(moved.x - RoundSpeed * lead) < Tolerance && Mathf.Abs(moved.y) < Tolerance, $"Projectile at lead {lead}: drawn {moved.x},{moved.y} past its lead-0 pose, expected {RoundSpeed * lead},0."); } ActionGameManager.Clock.Reset(); Debug.Log($"{Tag} ok: ItemPickup, MineInstance and Projectile draw at the clock's lead (0, 0.008, 0.015 s)"); } // What ARPG.unity's ZoneRenderer names in the pickup and mine fields, read from the saved scene before the play session // (a scene cannot be opened inside one) and carried across the domain reload as asset paths. private static string ReadWiring() { var scene = EditorSceneManager.OpenScene("Assets/Scenes/ARPG.unity", OpenSceneMode.Additive); try { var found = UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Include, FindObjectsSortMode.None) .Where(r => r.gameObject.scene == scene).ToArray(); Require(found.Length == 1, $"ARPG.unity has {found.Length} ZoneRenderers."); var wired = found[0]; return string.Join("|", new (string, UnityEngine.Object)[] { ("SimpleCommodityPickup", wired.SimpleCommodityPickup), ("MinePrefab", wired.MinePrefab) }.Select(f => f.Item1 + "=" + (f.Item2 == null ? "" : AssetDatabase.GetAssetPath(f.Item2)))); } finally { EditorSceneManager.CloseScene(scene, true); } } } }