Prove released client against Aetheria daemon
This commit is contained in:
parent
d92db35600
commit
9700446b44
9 changed files with 607 additions and 17 deletions
1
ReleaseConsumerProject/.gitignore
vendored
1
ReleaseConsumerProject/.gitignore
vendored
|
|
@ -2,3 +2,4 @@
|
|||
/Logs/
|
||||
/Temp/
|
||||
/UserSettings/
|
||||
/ProjectSettings/SceneTemplateSettings.json
|
||||
|
|
|
|||
8
ReleaseConsumerProject/Assets/Tests.meta
Normal file
8
ReleaseConsumerProject/Assets/Tests.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3d6288e2387a67e42ad07e936418361f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
ReleaseConsumerProject/Assets/Tests/PlayMode.meta
Normal file
8
ReleaseConsumerProject/Assets/Tests/PlayMode.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 32e9c9abd53632a4fafee545ef702846
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c4c5786b87ba5dc42baf6e34c3a1de03
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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<EveProviderWitness>(),
|
||||
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<string>(),
|
||||
"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<EveAdvertisedCommand>());
|
||||
|
||||
var payload = CultDocumentMessagePackSerialization.Serialize(advertisement);
|
||||
var decoded = CultDocumentMessagePackSerialization.Deserialize<EveProviderAdvertisementDocument>(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<WorldSmokeProvider>();
|
||||
var host = hostObject.AddComponent<EveUnityPlayableWorldClientHost>();
|
||||
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<EveUnityPlayableWorldEntityMarker>(),
|
||||
Has.Length.EqualTo(3));
|
||||
foreach (var marker in sceneRoot.GetComponentsInChildren<EveUnityPlayableWorldEntityMarker>())
|
||||
{
|
||||
var renderer = marker.GetComponent<Renderer>();
|
||||
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<Renderer>().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>();
|
||||
light.type = LightType.Directional;
|
||||
light.intensity = 1.4f;
|
||||
lightObject.transform.rotation = Quaternion.Euler(45f, -35f, 0f);
|
||||
|
||||
var camera = cameraObject.AddComponent<Camera>();
|
||||
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<Camera>();
|
||||
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<Transform>(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>();
|
||||
light.type = LightType.Directional;
|
||||
light.intensity = 1.4f;
|
||||
lightObject.transform.rotation = Quaternion.Euler(45f, -35f, 0f);
|
||||
var camera = cameraObject.AddComponent<Camera>();
|
||||
camera.clearFlags = CameraClearFlags.SolidColor;
|
||||
camera.backgroundColor = new Color(0.035f, 0.055f, 0.08f, 1f);
|
||||
foreach (var entityMarker in root.GetComponentsInChildren<EveUnityPlayableWorldEntityMarker>())
|
||||
{
|
||||
foreach (var renderer in entityMarker.GetComponentsInChildren<Renderer>())
|
||||
{
|
||||
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<Renderer>();
|
||||
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<Camera>();
|
||||
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<EveUnityPlayableWorldEntityMarker>())
|
||||
{
|
||||
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<EveStyleToken>()),
|
||||
Array.Empty<EveCommandTemplate>());
|
||||
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<EveSurfaceComponent>());
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> Props(params (string Key, string Value)[] values)
|
||||
{
|
||||
var props = new Dictionary<string, string>(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<EveSurfaceCommandRequest> Submitted { get; } = new List<EveSurfaceCommandRequest>();
|
||||
public event Action<EveUnitySceneProviderSurfaceDocument> DocumentAvailable;
|
||||
public void Submit(EveSurfaceCommandRequest request) => Submitted.Add(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5f46c1904f53cf34d8fcf59d0bdfe5fb
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue