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] public IEnumerator GenericCultMeshClientLowersAndMovesAdvertisedWorld() { var endpoint = Environment.GetEnvironmentVariable("EVEUNITY_PROVIDER_ENDPOINT"); if (string.IsNullOrWhiteSpace(endpoint)) Assert.Ignore("EVEUNITY_PROVIDER_ENDPOINT is not configured for the live provider witness."); var providerId = Environment.GetEnvironmentVariable("EVEUNITY_PROVIDER_ID") ?? "aetheria.daemon"; var surfaceId = Environment.GetEnvironmentVariable("EVEUNITY_SURFACE_ID") ?? "aetheria.game"; 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; try { transport = new EveUnityCultMeshLiveProviderTransport( replicaPath, endpoint, providerId, 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; var 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)); Assert.That(Vector3.Distance(FindMarker(root, playerId).transform.position, initialPosition), Is.GreaterThan(0.01f)); 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 playerRenderer = playerMarker.GetComponentInChildren(); Assert.That(playerRenderer, Is.Not.Null, "The provider-authored player prefab has no renderable visual."); var playerBounds = playerRenderer.bounds; var cameraDistance = Mathf.Max(8f, playerBounds.extents.magnitude * 4f); camera.transform.position = playerBounds.center + new Vector3(1f, 0.8f, -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)); } 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 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); } } }