diff --git a/ReleaseConsumerProject/.gitignore b/ReleaseConsumerProject/.gitignore index c71a5ef..920adf8 100644 --- a/ReleaseConsumerProject/.gitignore +++ b/ReleaseConsumerProject/.gitignore @@ -2,3 +2,4 @@ /Logs/ /Temp/ /UserSettings/ +/ProjectSettings/SceneTemplateSettings.json diff --git a/ReleaseConsumerProject/Assets/Tests.meta b/ReleaseConsumerProject/Assets/Tests.meta new file mode 100644 index 0000000..8d744a0 --- /dev/null +++ b/ReleaseConsumerProject/Assets/Tests.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3d6288e2387a67e42ad07e936418361f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode.meta b/ReleaseConsumerProject/Assets/Tests/PlayMode.meta new file mode 100644 index 0000000..35060c9 --- /dev/null +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 32e9c9abd53632a4fafee545ef702846 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef b/ReleaseConsumerProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef new file mode 100644 index 0000000..4d21cb3 --- /dev/null +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef @@ -0,0 +1,23 @@ +{ + "name": "GameCult.EveUnity.GenericClient.PlayModeTests", + "rootNamespace": "GameCult.EveUnity.GenericClient.PlayModeTests", + "references": [ + "GameCult.CultLib", + "GameCult.Eve.Surface", + "GameCult.Eve.UnityScene", + "GameCult.Eve.UnityScene.CultMesh" + ], + "includePlatforms": [], + "excludePlatforms": [], + "overrideReferences": true, + "precompiledReferences": [ + "GameCult.Caching.dll", + "GameCult.Caching.MessagePack.dll", + "GameCult.Mesh.dll", + "GameCult.Networking.dll" + ], + "autoReferenced": false, + "optionalUnityReferences": [ + "TestAssemblies" + ] +} diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef.meta b/ReleaseConsumerProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef.meta new file mode 100644 index 0000000..c323ab7 --- /dev/null +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: c4c5786b87ba5dc42baf6e34c3a1de03 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs new file mode 100644 index 0000000..e87d133 --- /dev/null +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -0,0 +1,529 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using GameCult.Caching; +using GameCult.Caching.MessagePack; +using GameCult.Eve.Surface; +using GameCult.Eve.UnityScene; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; + +namespace GameCult.EveUnity.GenericClient.PlayModeTests +{ + public sealed class GenericWorldCaptureTests + { + [Test] + public void CoreProviderDocumentsRoundTripThroughCultCacheSerialization() + { + var advertisement = new EveProviderAdvertisementDocument( + "eve.world-smoke", + "eve-world-smoke", + "eve.local", + "Generic world", + "game.daemon", + "127.0.0.1:7777", + "2026-07-10T00:00:00Z", + new EveProviderFreshness("fresh", "2026-07-10T00:00:00Z", 5000), + new[] { EveSurfaceDocument.SchemaId, EveCommandReceiptDocument.SchemaId }, + Array.Empty(), + new[] + { + new EveAdvertisedSurface( + "eve.world-smoke.surface", + EveSurfaceDocument.SchemaId, + "surface:eve.world-smoke", + "cultmesh-record", + "active", + "interactive-world", + new EveWorldInteractionAdvertisement( + "provider-authored-world-surface", + Array.Empty(), + "world.commands", + "commands:eve.world-smoke", + EveCommandReceiptDocument.SchemaId, + "receipts:eve.world-smoke", + "assets:eve.world-smoke", + new[] { "unity-scene" }, + "provider-owns-world-state-command-acceptance-and-receipts")) + }, + Array.Empty()); + + var payload = CultDocumentMessagePackSerialization.Serialize(advertisement); + var decoded = CultDocumentMessagePackSerialization.Deserialize(payload); + + Assert.That(decoded.ProviderId, Is.EqualTo("eve.world-smoke")); + Assert.That(decoded.Surfaces.Count, Is.EqualTo(1)); + Assert.That(decoded.Surfaces[0].WorldInteraction.CommandRecordRef, Is.EqualTo("commands:eve.world-smoke")); + } + + [UnityTest] + public IEnumerator GenericClientRendersWorldSmokeAndEmitsCommand() + { + var hostObject = new GameObject("EveUnity Generic Client"); + var sceneRoot = new GameObject("World Projection"); + var provider = hostObject.AddComponent(); + var host = hostObject.AddComponent(); + var cameraObject = new GameObject("Capture Camera"); + var lightObject = new GameObject("World Light"); + var ground = GameObject.CreatePrimitive(PrimitiveType.Plane); + RenderTexture target = null; + Texture2D pixels = null; + + try + { + provider.CurrentDocument = WorldDocument(); + host.Configure(sceneRoot.transform, provider, provider); + var presentation = host.Connect(); + + Assert.That(presentation.ActiveEntities, Is.EqualTo(3)); + Assert.That( + sceneRoot.GetComponentsInChildren(), + Has.Length.EqualTo(3)); + foreach (var marker in sceneRoot.GetComponentsInChildren()) + { + var renderer = marker.GetComponent(); + renderer.material.color = marker.Controllable + ? new Color(0.18f, 0.75f, 1f) + : marker.EntityKind == "enemy" + ? new Color(1f, 0.25f, 0.2f) + : new Color(1f, 0.78f, 0.15f); + } + + ground.name = "Projection Ground"; + ground.transform.position = new Vector3(0f, -0.65f, 3f); + ground.transform.localScale = new Vector3(2.2f, 1f, 2.2f); + ground.GetComponent().material.color = new Color(0.12f, 0.16f, 0.18f); + + host.SubmitMoveVectorIntent("player", 0f, 1f); + Assert.That(provider.Submitted, Has.Count.EqualTo(1)); + Assert.That(provider.Submitted[0].ProviderId, Is.EqualTo("eve.world-smoke")); + + var light = lightObject.AddComponent(); + light.type = LightType.Directional; + light.intensity = 1.4f; + lightObject.transform.rotation = Quaternion.Euler(45f, -35f, 0f); + + var camera = cameraObject.AddComponent(); + camera.clearFlags = CameraClearFlags.SolidColor; + camera.backgroundColor = new Color(0.035f, 0.055f, 0.08f, 1f); + camera.transform.position = new Vector3(10f, 8f, -12f); + camera.transform.LookAt(new Vector3(0f, 1f, 3f)); + target = new RenderTexture(640, 360, 24, RenderTextureFormat.ARGB32); + camera.targetTexture = target; + + yield return null; + camera.Render(); + RenderTexture.active = target; + pixels = new Texture2D(640, 360, TextureFormat.RGB24, false); + pixels.ReadPixels(new Rect(0, 0, 640, 360), 0, 0); + pixels.Apply(); + + var changedPixels = 0; + var background = camera.backgroundColor; + foreach (var pixel in pixels.GetPixels()) + { + if (Mathf.Abs(pixel.r - background.r) + + Mathf.Abs(pixel.g - background.g) + + Mathf.Abs(pixel.b - background.b) > 0.08f) + changedPixels++; + } + Assert.That(changedPixels, Is.GreaterThan(500)); + + var capturePath = Environment.GetEnvironmentVariable("EVEUNITY_CAPTURE_PATH"); + if (string.IsNullOrWhiteSpace(capturePath)) + { + capturePath = Path.GetFullPath(Path.Combine( + Application.dataPath, + "..", + "..", + "artifacts", + "playmode", + "world-smoke.png")); + } + Directory.CreateDirectory(Path.GetDirectoryName(capturePath)); + File.WriteAllBytes(capturePath, pixels.EncodeToPNG()); + Assert.That(new FileInfo(capturePath).Length, Is.GreaterThan(1024)); + } + finally + { + RenderTexture.active = null; + var camera = cameraObject.GetComponent(); + if (camera != null) camera.targetTexture = null; + if (target != null) UnityEngine.Object.DestroyImmediate(target); + if (pixels != null) UnityEngine.Object.DestroyImmediate(pixels); + UnityEngine.Object.DestroyImmediate(lightObject); + UnityEngine.Object.DestroyImmediate(cameraObject); + UnityEngine.Object.DestroyImmediate(ground); + UnityEngine.Object.DestroyImmediate(sceneRoot); + UnityEngine.Object.DestroyImmediate(hostObject); + } + } + + [UnityTest] + [Timeout(300000)] + public IEnumerator GenericCultMeshClientLowersAndMovesAdvertisedWorld() + { + var rendezvousEndpoint = Environment.GetEnvironmentVariable("EVEUNITY_RENDEZVOUS_ENDPOINT"); + if (string.IsNullOrWhiteSpace(rendezvousEndpoint)) + Assert.Ignore("EVEUNITY_RENDEZVOUS_ENDPOINT is not configured for the live provider witness."); + + var providerId = Environment.GetEnvironmentVariable("EVEUNITY_PROVIDER_ID") ?? ""; + var surfaceId = Environment.GetEnvironmentVariable("EVEUNITY_SURFACE_ID") ?? ""; + var replicaPath = Environment.GetEnvironmentVariable("EVEUNITY_REPLICA_PATH") ?? + Path.Combine(Application.temporaryCachePath, $"eve-unity-{Guid.NewGuid():N}.cc"); + var capturePath = Environment.GetEnvironmentVariable("EVEUNITY_AETHERIA_CAPTURE_PATH") ?? + Path.Combine(Application.temporaryCachePath, "aetheria-daemon-world.png"); + var root = new GameObject("Generic Eve World Projection"); + var cameraObject = new GameObject("Generic Eve Capture Camera"); + var lightObject = new GameObject("Generic Eve World Light"); + RenderTexture target = null; + Texture2D pixels = null; + EveUnityCultMeshLiveProviderTransport transport = null; + EveUnitySceneLiveProviderBridge bridge = null; + EveUnityPlayableWorldRuntime runtime = null; + WitnessReceipt movementReceipt = null; + WitnessReceipt focusReceipt = null; + WitnessReceipt actionReceipt = null; + long initialVersion = 0; + float movementDistance = 0f; + + try + { + var selection = new EveUnityCultMeshProviderDiscovery().Discover( + rendezvousEndpoint, + providerId, + surfaceId, + "interactive-world"); + Assert.That(selection.VerseId, Is.Not.Empty); + Assert.That(selection.ProviderId, Is.Not.Empty); + Assert.That(selection.SurfaceId, Is.Not.Empty); + transport = new EveUnityCultMeshLiveProviderTransport( + replicaPath, + selection.Endpoint, + selection.ProviderId, + selection.SurfaceId, + $"eve-unity-test-{Guid.NewGuid():N}"); + var publicationDeadline = Time.realtimeSinceStartup + 10f; + while (true) + { + var published = false; + try + { + transport.Refresh(); + published = true; + } + catch (InvalidOperationException) when (Time.realtimeSinceStartup < publicationDeadline) + { + } + if (published) break; + yield return new WaitForSecondsRealtime(0.1f); + } + bridge = new EveUnitySceneLiveProviderBridge(transport); + bridge.Connect(); + runtime = EveUnityPlayableWorldRuntime.CreateForGameObjectScene( + root.transform, + bridge, + bridge, + assetManifestDocuments: null, + receiptSource: bridge, + fallbackAssetProvider: transport); + var initial = runtime.Connect(); + Assert.That(initial.ActiveEntities, Is.GreaterThan(0)); + Assert.That(runtime.ActiveWorld.PlayerEntityId, Is.Not.Empty); + + var playerId = runtime.ActiveWorld.PlayerEntityId; + var marker = FindMarker(root, playerId); + Assert.That( + marker.GetComponentsInChildren(includeInactive: true).Length, + Is.GreaterThan(1), + "The generic client substituted its primitive fallback instead of the provider-authored AssetBundle prefab."); + var initialPosition = marker.transform.position; + initialVersion = runtime.ActiveVersion; + var request = runtime.SubmitMoveVectorIntent(playerId, 1f, 0f, 1f); + + var deadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < deadline && + (runtime.LastReceipt == null || runtime.ActiveVersion <= initialVersion || + Vector3.Distance(FindMarker(root, playerId).transform.position, initialPosition) < 0.01f)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + + Assert.That(runtime.LastReceipt, Is.Not.Null); + Assert.That(runtime.LastReceipt.CommandId, Is.EqualTo(request.CommandId)); + Assert.That( + runtime.LastReceipt.State, + Is.EqualTo("accepted").Or.EqualTo("reconciled"), + runtime.LastReceipt.Message); + Assert.That(runtime.LastReceipt.SourceVersion, Is.GreaterThan(initialVersion)); + Assert.That(runtime.ActiveVersion, Is.GreaterThan(initialVersion)); + movementDistance = Vector3.Distance(FindMarker(root, playerId).transform.position, initialPosition); + Assert.That(movementDistance, Is.GreaterThan(0.01f)); + movementReceipt = WitnessReceipt.From("movement", runtime.LastReceipt); + + var movementVersion = runtime.ActiveVersion; + var focus = runtime.SubmitFocusIntent(playerId); + var focusDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < focusDeadline && + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != focus.CommandId || + runtime.ActiveVersion < runtime.LastReceipt.SourceVersion)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + Assert.That(runtime.LastReceipt, Is.Not.Null); + Assert.That(runtime.LastReceipt.CommandId, Is.EqualTo(focus.CommandId)); + Assert.That(runtime.LastReceipt.State, Is.EqualTo("accepted").Or.EqualTo("reconciled")); + Assert.That(runtime.LastReceipt.SourceVersion, Is.GreaterThan(movementVersion)); + focusReceipt = WitnessReceipt.From("targeting", runtime.LastReceipt); + + var focusVersion = runtime.ActiveVersion; + var action = runtime.SubmitActionIntent(playerId, "0"); + var actionDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < actionDeadline && + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != action.CommandId || + runtime.ActiveVersion < runtime.LastReceipt.SourceVersion)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + Assert.That(runtime.LastReceipt, Is.Not.Null); + Assert.That(runtime.LastReceipt.CommandId, Is.EqualTo(action.CommandId)); + Assert.That( + runtime.LastReceipt.State, + Is.EqualTo("accepted").Or.EqualTo("reconciled"), + runtime.LastReceipt.Message); + Assert.That(runtime.LastReceipt.SourceVersion, Is.GreaterThan(focusVersion)); + Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion)); + actionReceipt = WitnessReceipt.From("action", runtime.LastReceipt); + + var light = lightObject.AddComponent(); + light.type = LightType.Directional; + light.intensity = 1.4f; + lightObject.transform.rotation = Quaternion.Euler(45f, -35f, 0f); + var camera = cameraObject.AddComponent(); + camera.clearFlags = CameraClearFlags.SolidColor; + camera.backgroundColor = new Color(0.035f, 0.055f, 0.08f, 1f); + foreach (var entityMarker in root.GetComponentsInChildren()) + { + foreach (var renderer in entityMarker.GetComponentsInChildren()) + { + renderer.material.color = entityMarker.Controllable + ? new Color(0.18f, 0.75f, 1f) + : entityMarker.EntityKind == "enemy" + ? new Color(1f, 0.25f, 0.2f) + : new Color(1f, 0.78f, 0.15f); + } + } + var playerMarker = FindMarker(root, playerId); + var playerRenderers = playerMarker.GetComponentsInChildren(); + Assert.That(playerRenderers, Is.Not.Empty, "The provider-authored player prefab has no renderable visual."); + var playerBounds = playerRenderers[0].bounds; + foreach (var renderer in playerRenderers) + playerBounds.Encapsulate(renderer.bounds); + var halfFovRadians = camera.fieldOfView * 0.5f * Mathf.Deg2Rad; + var cameraDistance = Mathf.Max(8f, playerBounds.extents.magnitude / Mathf.Tan(halfFovRadians) * 1.35f); + camera.nearClipPlane = Mathf.Max(0.01f, cameraDistance * 0.001f); + camera.farClipPlane = Mathf.Max(1000f, cameraDistance + playerBounds.size.magnitude * 4f); + camera.transform.position = playerBounds.center + new Vector3(1f, 0.65f, -1.2f).normalized * cameraDistance; + camera.transform.LookAt(playerBounds.center); + target = new RenderTexture(640, 360, 24, RenderTextureFormat.ARGB32); + camera.targetTexture = target; + yield return null; + camera.Render(); + RenderTexture.active = target; + pixels = new Texture2D(640, 360, TextureFormat.RGB24, false); + pixels.ReadPixels(new Rect(0, 0, 640, 360), 0, 0); + pixels.Apply(); + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(capturePath))); + File.WriteAllBytes(capturePath, pixels.EncodeToPNG()); + Assert.That(new FileInfo(capturePath).Length, Is.GreaterThan(1024)); + WriteWitnessFacts( + initialVersion, + runtime.ActiveVersion, + movementDistance, + movementReceipt, + focusReceipt, + actionReceipt); + } + finally + { + RenderTexture.active = null; + var camera = cameraObject.GetComponent(); + if (camera != null) camera.targetTexture = null; + if (target != null) UnityEngine.Object.DestroyImmediate(target); + if (pixels != null) UnityEngine.Object.DestroyImmediate(pixels); + runtime?.Dispose(); + bridge?.Dispose(); + transport?.Dispose(); + UnityEngine.Object.DestroyImmediate(lightObject); + UnityEngine.Object.DestroyImmediate(cameraObject); + UnityEngine.Object.DestroyImmediate(root); + } + } + + private static void WriteWitnessFacts( + long initialVersion, + long finalVersion, + float movementDistance, + WitnessReceipt movement, + WitnessReceipt targeting, + WitnessReceipt action) + { + var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); + if (string.IsNullOrWhiteSpace(path)) return; + var document = new WitnessFacts + { + providerAdvertisement = true, + providerAssets = true, + movement = movement != null, + targeting = targeting != null, + action = action != null, + initialVersion = initialVersion, + finalVersion = finalVersion, + movementDistance = movementDistance, + receipts = new[] { movement, targeting, action } + }; + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))); + File.WriteAllText(path, JsonUtility.ToJson(document, true)); + } + + [Serializable] + private sealed class WitnessFacts + { + public bool providerAdvertisement; + public bool providerAssets; + public bool movement; + public bool targeting; + public bool action; + public long initialVersion; + public long finalVersion; + public float movementDistance; + public WitnessReceipt[] receipts; + } + + [Serializable] + private sealed class WitnessReceipt + { + public string commandKind; + public string commandId; + public string state; + public long sourceVersion; + + public static WitnessReceipt From(string commandKind, EveUnitySceneCommandReceipt receipt) + { + return new WitnessReceipt + { + commandKind = commandKind, + commandId = receipt.CommandId, + state = receipt.State, + sourceVersion = receipt.SourceVersion + }; + } + } + + private static EveUnityPlayableWorldEntityMarker FindMarker(GameObject root, string entityId) + { + foreach (var marker in root.GetComponentsInChildren()) + { + if (string.Equals(marker.EntityId, entityId, StringComparison.Ordinal)) + return marker; + } + throw new InvalidOperationException($"Projected world has no entity marker '{entityId}'."); + } + + private static EveUnitySceneProviderSurfaceDocument WorldDocument() + { + var entities = new[] + { + Entity("player-node", "player", "player", "Vanguard", "0,0,0", "1.2", true), + Entity("enemy-node", "enemy", "enemy", "Raider", "3,0,4", "1", false), + Entity("landmark-node", "landmark", "landmark", "Beacon", "-4,0,6", "1.8", false) + }; + var document = new EveSurfaceDocument( + "eve.world-smoke", + "game.runtime", + "Generic interactive world", + 1, + "2026-07-10T00:00:00Z", + new EveSurfaceTree( + "eve.world-smoke.surface", + new EveSurfaceComponent( + "world.root", + "surface", + Props(), + new[] + { + new EveSurfaceComponent( + "world.scene", + "world.scene3d", + Props( + ("playerEntityId", "player"), + ("movementCommand", "world.move"), + ("inputProfile", "third-person"), + ("cameraRig", "third-person-orbit")), + entities) + }), + Array.Empty()), + Array.Empty()); + return new EveUnitySceneProviderSurfaceDocument( + document, + new EveUnitySceneProviderSurfaceAdvertisement( + "eve.world-smoke.surface", + "interactive-world", + new EveUnitySceneWorldInteraction( + "provider-authored-world-surface", + "world.commands", + "gamecult.eve.command_receipt.v1", + "provider-owns-world-state-command-acceptance-and-receipts")), + "cultmesh://eve.world-smoke/surfaces/world", + 1); + } + + private static EveSurfaceComponent Entity( + string nodeId, + string entityId, + string entityKind, + string label, + string position, + string radius, + bool controllable) + { + return new EveSurfaceComponent( + nodeId, + "world.entity3d", + Props( + ("entityId", entityId), + ("entityKind", entityKind), + ("label", label), + ("position", position), + ("radius", radius), + ("assetRef", $"cultmesh://eve.world-smoke/assets/{entityKind}"), + ("selectable", "true"), + ("controllable", controllable ? "true" : "false"), + ("moveCommand", controllable ? "world.move" : "")), + Array.Empty()); + } + + private static Dictionary Props(params (string Key, string Value)[] values) + { + var props = new Dictionary(StringComparer.Ordinal); + foreach (var value in values) props[value.Key] = value.Value; + return props; + } + + private sealed class WorldSmokeProvider : MonoBehaviour, + IEveUnitySceneProviderSurfaceDocumentSource, + IEveUnitySceneCommandSink + { + public EveUnitySceneProviderSurfaceDocument CurrentDocument { get; set; } + public string SinkKind => "world-smoke-command-sink"; + public List Submitted { get; } = new List(); + public event Action DocumentAvailable; + public void Submit(EveSurfaceCommandRequest request) => Submitted.Add(request); + } + } +} diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs.meta b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs.meta new file mode 100644 index 0000000..b94baff --- /dev/null +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 5f46c1904f53cf34d8fcf59d0bdfe5fb \ No newline at end of file diff --git a/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json b/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json index 0e540b5..f82d5ff 100644 --- a/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json +++ b/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json @@ -170,7 +170,7 @@ }, "capture": { "ownerRepo": "EveUnity", - "status": "partial-world-smoke-playmode-capture", + "status": "released-aetheria-playmode-capture", "captureContract": { "ownerRepo": "EveUnity", "runtimeId": "unity-scene", @@ -197,17 +197,18 @@ "evidencePaths": [ "scripts/build-capture-request.ps1", "scripts/run-playmode-capture.ps1", - "TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs", - "artifacts/playmode/world-smoke.png" + "ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs", + "scripts/run-aetheria-daemon-world-witness.ps1", + "artifacts/aetheria-daemon/aetheria-daemon-world.png" ], "pendingProofs": [ - "Attach the Aetheria daemon PlayMode PNG; the generic world-smoke control is captured by EveUnity." + ] } }, "capture": { - "status": "partial-world-smoke-playmode-capture", - "note": "The generic world-smoke PlayMode PNG passes; the separately running Aetheria daemon capture remains active lifecycle work.", + "status": "released-aetheria-playmode-capture", + "note": "The released generic EveUnity client discovers and captures the separately running Aetheria daemon world.", "artifacts": [ { "kind": "png", @@ -215,6 +216,13 @@ "path": "artifacts/playmode/world-smoke.png", "providerId": "eve.world-smoke", "surfaceId": "eve.world-smoke.surface" + }, + { + "kind": "png", + "schema": "gamecult.eve.runtime_frame_capture.v1", + "path": "artifacts/aetheria-daemon/aetheria-daemon-world.png", + "providerId": "aetheria.daemon", + "surfaceId": "aetheria.game" } ] } diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 92779d6..306c224 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -1,7 +1,6 @@ param( [string] $UnityExe = "C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Unity.exe", [string] $AetheriaRoot = "E:\Projects\Aetheria", - [string] $CultLibRoot = "E:\Projects\CultLib", [int] $Port = 3076, [string] $OutputDirectory = "artifacts\aetheria-daemon", [ValidateSet("auto", "cold", "warm")] @@ -11,7 +10,7 @@ param( $ErrorActionPreference = "Stop" $repoRoot = Split-Path -Parent $PSScriptRoot -$projectRoot = Join-Path $repoRoot "TestProject" +$projectRoot = "ReleaseConsumerProject" $outputRoot = if ([IO.Path]::IsPathRooted($OutputDirectory)) { $OutputDirectory } else { Join-Path $repoRoot $OutputDirectory } $resultsPath = Join-Path $outputRoot "results.xml" $unityLogPath = Join-Path $outputRoot "unity.log" @@ -25,7 +24,7 @@ $assetCachePath = Join-Path $outputRoot "asset-cache" $statePath = Join-Path $outputRoot "aetheria-witness-state.cc" $daemonProject = Join-Path $AetheriaRoot "Aetheria.State.Daemon\Aetheria.State.Daemon.csproj" -foreach ($required in @($UnityExe, $projectRoot, $daemonProject, (Join-Path $CultLibRoot "scripts\build-unity-package.ps1"))) { +foreach ($required in @($UnityExe, (Join-Path $repoRoot $projectRoot), $daemonProject)) { if (-not (Test-Path -LiteralPath $required)) { throw "Required world witness path not found: $required" } } @@ -53,15 +52,12 @@ foreach ($ephemeralPath in @( } if (Test-Path -LiteralPath $resolved) { Remove-Item -LiteralPath $resolved -Recurse -Force } } -powershell -ExecutionPolicy Bypass -File (Join-Path $CultLibRoot "scripts\build-unity-package.ps1") -if ($LASTEXITCODE -ne 0) { throw "CultLib Unity package build failed with exit code $LASTEXITCODE" } - if (-not $SkipAssetBundleBuild) { $bundleBuilder = Start-Process -FilePath $UnityExe -ArgumentList @( - "-batchmode", "-quit", "-projectPath", $AetheriaRoot, + "-batchmode", "-quit", "-projectPath", ".", "-executeMethod", "Aetheria.Editor.EveAssetBundleBuilder.BuildWindows", "-logFile", $bundleBuildLogPath - ) -PassThru -WindowStyle Hidden + ) -WorkingDirectory $AetheriaRoot -PassThru -WindowStyle Hidden Write-Host "AssetBundle builder PID: $($bundleBuilder.Id)" Write-Host "AssetBundle build log: $bundleBuildLogPath" Write-Host "Poll: Get-Content '$bundleBuildLogPath' -Tail 20" @@ -117,7 +113,7 @@ try { "-testFilter", "GenericCultMeshClientLowersAndMovesAdvertisedWorld", "-testResults", $resultsPath, "-logFile", $unityLogPath ) - $unity = Start-Process -FilePath $UnityExe -ArgumentList $arguments -PassThru -WindowStyle Hidden + $unity = Start-Process -FilePath $UnityExe -ArgumentList $arguments -WorkingDirectory $repoRoot -PassThru -WindowStyle Hidden if (-not $unity.WaitForExit(300000)) { Stop-Process -Id $unity.Id -Force -ErrorAction SilentlyContinue throw "Unity witness exceeded 300 seconds. See $unityLogPath" @@ -129,6 +125,11 @@ try { if (-not (Test-Path $capturePath) -or (Get-Item $capturePath).Length -lt 1024) { throw "Live world capture is missing: $capturePath" } if (-not (Test-Path $factsPath)) { throw "Live world witness facts are missing: $factsPath" } $facts = Get-Content -LiteralPath $factsPath -Raw | ConvertFrom-Json + $releaseLock = Get-Content -LiteralPath (Join-Path $repoRoot "ReleaseConsumerProject\Packages\packages-lock.json") -Raw | ConvertFrom-Json + $facts | Add-Member -NotePropertyName releasedPackageClient -NotePropertyValue $true + $facts | Add-Member -NotePropertyName clientProject -NotePropertyValue "ReleaseConsumerProject" + $facts | Add-Member -NotePropertyName eveUnityPackageCommit -NotePropertyValue $releaseLock.dependencies.'org.gamecult.eve.unity-scene'.hash + $facts | Add-Member -NotePropertyName cultLibPackageCommit -NotePropertyValue $releaseLock.dependencies.'org.gamecult.cultlib'.hash $capture = Get-Item -LiteralPath $capturePath $resultsArtifact = Get-Item -LiteralPath $resultsPath $durationMs = [Math]::Round(([DateTimeOffset]::UtcNow - $witnessStartedAt).TotalMilliseconds, 3) @@ -172,15 +173,18 @@ try { sizeBytes = $resultsArtifact.Length } ) - authority = "runtime-observes-provider-advertisement-assets-command-receipts-and-republished-surface-versions" + authority = "released-generic-runtime-observes-provider-advertisement-assets-command-receipts-and-republished-surface-versions" } $witness | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $witnessPath -Encoding UTF8 + $stateWitnessPath = Join-Path $outputRoot "runtime-witness.$observedCacheState.json" + $witness | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $stateWitnessPath -Encoding UTF8 Write-Host "Aetheria daemon world witness: passed" Write-Host "Capture: $capturePath" Write-Host "Runtime witness: $witnessPath" + Write-Host "Cache-state witness: $stateWitnessPath" } finally { if ($null -ne $daemon -and -not $daemon.HasExited) { Stop-Process -Id $daemon.Id -Force } - Remove-Item Env:EVEUNITY_PROVIDER_ENDPOINT, Env:EVEUNITY_PROVIDER_ID, Env:EVEUNITY_SURFACE_ID, Env:EVEUNITY_REPLICA_PATH, Env:EVEUNITY_AETHERIA_CAPTURE_PATH, Env:EVEUNITY_ASSET_CACHE_PATH, Env:EVEUNITY_WITNESS_FACTS_PATH -ErrorAction SilentlyContinue + Remove-Item Env:EVEUNITY_RENDEZVOUS_ENDPOINT, Env:EVEUNITY_PROVIDER_ENDPOINT, Env:EVEUNITY_PROVIDER_ID, Env:EVEUNITY_SURFACE_ID, Env:EVEUNITY_REPLICA_PATH, Env:EVEUNITY_AETHERIA_CAPTURE_PATH, Env:EVEUNITY_ASSET_CACHE_PATH, Env:EVEUNITY_WITNESS_FACTS_PATH -ErrorAction SilentlyContinue Remove-Item Env:AETHERIA_TRACE_EVE_SNAPSHOTS -ErrorAction SilentlyContinue }