diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 9269482..31c58b6 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -248,6 +248,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var playerId = runtime.ActiveWorld.PlayerEntityId; var playerFact = host.PresentedEntities.CurrentGeneration.Entities .Single(entity => entity.EntityId == playerId); + var targetEntityId = host.PresentedEntities.CurrentGeneration.Entities + .Where(entity => entity.EntityId != playerId) + .Select(entity => entity.EntityId) + .FirstOrDefault(); + Assert.That(targetEntityId, Is.Not.Null.And.Not.Empty, + "The advertised live generation has no explicit targeting candidate."); var playerPrefab = provider.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( playerFact.AssetRef, playerFact.EntityKind, @@ -285,7 +291,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests movementReceipt = WitnessReceipt.From("movement", runtime.LastReceipt); var movementVersion = runtime.ActiveVersion; - var focus = runtime.SubmitFocusIntent(playerId); + var focus = runtime.SubmitTargetIntent(playerId, targetEntityId); var focusDeadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < focusDeadline && (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != focus.CommandId || diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index d5dc99b..31c58b6 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; using GameCult.Caching; using GameCult.Caching.MessagePack; using GameCult.Eve.Surface; @@ -175,12 +176,16 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests 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 mapCapturePath = Environment.GetEnvironmentVariable("EVEUNITY_AETHERIA_MAP_CAPTURE_PATH") ?? + Path.Combine(Application.temporaryCachePath, "aetheria-daemon-map.png"); var root = new GameObject("Generic Eve World Projection"); var cameraObject = new GameObject("Generic Eve Capture Camera"); + var mapCameraObject = new GameObject("Generic Eve Map Camera"); var lightObject = new GameObject("Generic Eve World Light"); RenderTexture target = null; Texture2D pixels = null; EveUnityCultMeshPlayableWorldProvider provider = null; + EveUnityPlayableWorldClientHost host = null; EveUnityPlayableWorldRuntime runtime = null; WitnessReceipt movementReceipt = null; WitnessReceipt focusReceipt = null; @@ -217,18 +222,44 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(provider.Selection.VerseId, Is.Not.Empty); Assert.That(provider.Selection.ProviderId, Is.Not.Empty); Assert.That(provider.Selection.SurfaceId, Is.Not.Empty); - runtime = EveUnityPlayableWorldRuntime.CreateForGameObjectScene( + host = root.AddComponent(); + host.Configure( root.transform, provider, provider, - null, + provider, provider, provider); - var initial = runtime.Connect(); - Assert.That(initial.ActiveEntities, Is.GreaterThan(0)); + host.Connect(); + runtime = host.Runtime; + Assert.That(runtime, Is.Not.Null); Assert.That(runtime.ActiveWorld.PlayerEntityId, Is.Not.Empty); + var entityDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < entityDeadline && + (host.PresentedEntities?.CurrentGeneration == null || + host.PresentedEntities.CurrentGeneration.Entities.Count == 0)) + { + yield return new WaitForSecondsRealtime(0.1f); + host.Refresh(); + } + Assert.That(host.PresentedEntities?.CurrentGeneration, Is.Not.Null); + Assert.That(host.PresentedEntities.CurrentGeneration.Entities.Count, Is.GreaterThan(0)); var playerId = runtime.ActiveWorld.PlayerEntityId; + var playerFact = host.PresentedEntities.CurrentGeneration.Entities + .Single(entity => entity.EntityId == playerId); + var targetEntityId = host.PresentedEntities.CurrentGeneration.Entities + .Where(entity => entity.EntityId != playerId) + .Select(entity => entity.EntityId) + .FirstOrDefault(); + Assert.That(targetEntityId, Is.Not.Null.And.Not.Empty, + "The advertised live generation has no explicit targeting candidate."); + var playerPrefab = provider.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( + playerFact.AssetRef, + playerFact.EntityKind, + "provider-asset-ref")); + Assert.That(playerPrefab, Is.Not.Null, + $"Provider asset catalog did not resolve player AssetRef '{playerFact.AssetRef}'."); var marker = FindMarker(root, playerId); Assert.That( marker.GetComponentsInChildren(includeInactive: true).Length, @@ -260,7 +291,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests movementReceipt = WitnessReceipt.From("movement", runtime.LastReceipt); var movementVersion = runtime.ActiveVersion; - var focus = runtime.SubmitFocusIntent(playerId); + var focus = runtime.SubmitTargetIntent(playerId, targetEntityId); var focusDeadline = Time.realtimeSinceStartup + 12f; while (Time.realtimeSinceStartup < focusDeadline && (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != focus.CommandId || @@ -302,6 +333,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var camera = cameraObject.AddComponent(); camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(0.035f, 0.055f, 0.08f, 1f); + camera.cullingMask = -1; foreach (var entityMarker in root.GetComponentsInChildren()) { foreach (var renderer in entityMarker.GetComponentsInChildren()) @@ -316,15 +348,25 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests 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); + Assert.That(provider.TryGetRenderChannelLayer("map", out var mapLayer), Is.True); + var mapRenderers = new List(); + foreach (var renderer in root.GetComponentsInChildren(includeInactive: true)) + { + if (renderer.gameObject.layer == mapLayer) + mapRenderers.Add(renderer); + } + Assert.That(mapRenderers, Is.Not.Empty, "The live provider prefab has no renderer assigned to its advertised map channel."); + + var cameraRig = cameraObject.AddComponent(); + cameraRig.Host = host; + cameraRig.CameraTransform = camera.transform; + cameraRig.RenderPolicySource = provider; + Assert.That(cameraRig.ApplyRig(0f), Is.True); + Assert.That(camera.cullingMask & (1 << mapLayer), Is.Zero); + + var mapCamera = mapCameraObject.AddComponent(); + mapCamera.cullingMask = 1 << mapLayer; + Assert.That(mapCamera.cullingMask & (1 << mapLayer), Is.Not.Zero); target = new RenderTexture(640, 360, 24, RenderTextureFormat.ARGB32); camera.targetTexture = target; yield return null; @@ -336,10 +378,36 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(capturePath))); File.WriteAllBytes(capturePath, pixels.EncodeToPNG()); Assert.That(new FileInfo(capturePath).Length, Is.GreaterThan(1024)); + + var mapBounds = mapRenderers[0].bounds; + foreach (var renderer in mapRenderers) + mapBounds.Encapsulate(renderer.bounds); + mapCamera.clearFlags = CameraClearFlags.SolidColor; + mapCamera.backgroundColor = camera.backgroundColor; + mapCamera.orthographic = true; + mapCamera.orthographicSize = Mathf.Max(0.1f, mapBounds.extents.x, mapBounds.extents.z) * 1.25f; + mapCamera.nearClipPlane = 0.01f; + mapCamera.farClipPlane = Mathf.Max(10f, mapBounds.size.magnitude * 4f); + mapCamera.transform.position = mapBounds.center + Vector3.up * (mapCamera.farClipPlane * 0.25f); + mapCamera.transform.rotation = Quaternion.Euler(90f, 0f, 0f); + mapCamera.targetTexture = target; + mapCamera.Render(); + RenderTexture.active = target; + pixels.ReadPixels(new Rect(0, 0, 640, 360), 0, 0); + pixels.Apply(); + var mapChangedPixels = CountChangedPixels(pixels, mapCamera.backgroundColor); + Assert.That(mapChangedPixels, Is.GreaterThan(25), "The map-only camera rendered no provider-authored map glyph pixels."); + Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(mapCapturePath))); + File.WriteAllBytes(mapCapturePath, pixels.EncodeToPNG()); + Assert.That(new FileInfo(mapCapturePath).Length, Is.GreaterThan(1024)); WriteWitnessFacts( initialVersion, runtime.ActiveVersion, movementDistance, + mapRenderers.Count, + mapChangedPixels, + (camera.cullingMask & (1 << mapLayer)) == 0, + (mapCamera.cullingMask & (1 << mapLayer)) != 0, movementReceipt, focusReceipt, actionReceipt); @@ -349,10 +417,13 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests RenderTexture.active = null; var camera = cameraObject.GetComponent(); if (camera != null) camera.targetTexture = null; + var mapCamera = mapCameraObject.GetComponent(); + if (mapCamera != null) mapCamera.targetTexture = null; if (target != null) UnityEngine.Object.DestroyImmediate(target); if (pixels != null) UnityEngine.Object.DestroyImmediate(pixels); - runtime?.Dispose(); + host?.Disconnect(); UnityEngine.Object.DestroyImmediate(lightObject); + UnityEngine.Object.DestroyImmediate(mapCameraObject); UnityEngine.Object.DestroyImmediate(cameraObject); UnityEngine.Object.DestroyImmediate(root); } @@ -362,6 +433,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests long initialVersion, long finalVersion, float movementDistance, + int mapChannelRendererCount, + int mapChangedPixels, + bool pilotCameraExcludesMapChannel, + bool mapCameraIncludesMapChannel, WitnessReceipt movement, WitnessReceipt targeting, WitnessReceipt action) @@ -378,6 +453,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests initialVersion = initialVersion, finalVersion = finalVersion, movementDistance = movementDistance, + mapChannelRendererCount = mapChannelRendererCount, + mapChangedPixels = mapChangedPixels, + pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel, + mapCameraIncludesMapChannel = mapCameraIncludesMapChannel, receipts = new[] { movement, targeting, action } }; Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))); @@ -395,6 +474,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public long initialVersion; public long finalVersion; public float movementDistance; + public int mapChannelRendererCount; + public int mapChangedPixels; + public bool pilotCameraExcludesMapChannel; + public bool mapCameraIncludesMapChannel; public WitnessReceipt[] receipts; } @@ -428,6 +511,19 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests throw new InvalidOperationException($"Projected world has no entity marker '{entityId}'."); } + private static int CountChangedPixels(Texture2D texture, Color background) + { + var changed = 0; + foreach (var pixel in texture.GetPixels()) + { + if (Mathf.Abs(pixel.r - background.r) + + Mathf.Abs(pixel.g - background.g) + + Mathf.Abs(pixel.b - background.b) > 0.08f) + changed++; + } + return changed; + } + private static EveUnitySceneProviderSurfaceDocument WorldDocument() { var entities = new[] diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index 83dadd9..f520e89 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -1,11 +1,11 @@ # Aetheria released-package witness -The warm-cache witness passes against `org.gamecult.eve.unity-scene` `0.3.17` -at commit `979eee3f8e327c904d25d2d3091a9a6db977842d` and -`org.gamecult.cultlib` `1.0.12` at commit -`26b4d1eadc4d0b1fe7a5ffb29a64373576544447`. +The warm-cache witness passes against `org.gamecult.eve.unity-scene` `0.3.18` +at commit `ed21479e467faea8ff624f2d2f5a7d2fecf913f4` and +`org.gamecult.cultlib` `1.0.13` at commit +`feb5c71513e71d681699f462fe3682b3168c6f73`. -Evidence is in `artifacts/render-channel-witness-content-warm`: +Evidence is in `artifacts/render-channel-witness-content-drain-cold-2`: - `results.xml`: one passing PlayMode witness. - `aetheria-daemon-world.png`: provider-authored 3D pilot view. @@ -13,14 +13,29 @@ Evidence is in `artifacts/render-channel-witness-content-warm`: - `witness-facts.json`: movement, targeting, action, receipts, and camera-mask facts. - `runtime-witness.warm.json`: released package commits and screenshot hashes. -The warm cache was seeded from the previously hash-verified bundle and renamed -to CultMesh's content-addressed `.body` cache shape. This proves released-client -loading and lowering, not cold delivery. +The warm run reused the body promoted by the preceding cold content transfer. +It reconciled movement, explicit targeting, and action receipts. The pilot +camera excluded all seven renderers assigned to the provider's `map` channel; +the map-only camera rendered 2,664 changed glyph pixels. Visual inspection +confirms provider-authored ships in the pilot view without map glyphs, and only +the colored provider-authored glyphs in the map view. -The cold CDN path remains failed. The 2026-07-14 run in -`artifacts/render-channel-witness-content-cold` used the managed -`cultmesh.content.v1` session and created the resumable 13,006,384-byte partial -body, but transferred only about 5.3 MB before the fixed 300-second witness -deadline. Snapshot chunk batching is no longer involved. The remaining fault is -the sequential one-chunk request path over RUDP; increasing the witness timeout -is not an accepted fix. +Cold content delivery now succeeds through the managed `cultmesh.content.v1` +session under the unchanged 300-second deadline. The run in +`artifacts/render-channel-witness-content-drain-cold-3` began with an empty +asset cache and atomically promoted: + +- path: `asset-cache/0d5b575a18ecb2e8f4f1c309a7fa009262bdbb2ff671c14a4bf610d5052685dd.body` +- bytes: `13,006,384` +- SHA-256: `0d5b575a18ecb2e8f4f1c309a7fa009262bdbb2ff671c14a4bf610d5052685dd` +- partial files after completion: zero + +The full cold gameplay witness is not yet a pass. During the roughly 19 seconds +of authoritative simulation needed to deliver and load the body, the witness +world can advance until its initial non-player targeting candidates are gone. +The released client then correctly reports a generation containing no explicit +target and the witness stops before screenshots. The warm run proves the +generic gameplay and camera contracts; the cold run proves content delivery. +Closing the combined cold proof requires an explicit readiness/lifecycle +contract or a stable witness-world scenario. A renderer fallback or a larger +timeout would conceal the ownership problem and is not accepted.