Add generic CultMesh world transport

This commit is contained in:
Meta Vens 2026-07-10 04:39:16 +01:00
commit 3a2052f1f4
10 changed files with 583 additions and 2 deletions

View file

@ -4,14 +4,17 @@
"references": [
"GameCult.CultLib",
"GameCult.Eve.Surface",
"GameCult.Eve.UnityScene"
"GameCult.Eve.UnityScene",
"GameCult.Eve.UnityScene.CultMesh"
],
"includePlatforms": [],
"excludePlatforms": [],
"overrideReferences": true,
"precompiledReferences": [
"GameCult.Caching.dll",
"GameCult.Caching.MessagePack.dll"
"GameCult.Caching.MessagePack.dll",
"GameCult.Mesh.dll",
"GameCult.Networking.dll"
],
"autoReferenced": false,
"optionalUnityReferences": [

View file

@ -161,6 +161,123 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests
}
}
[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, bridge, bridge);
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);
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.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>();
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(14f, 12f, -16f);
camera.transform.LookAt(FindMarker(root, playerId).transform.position);
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<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 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[]

View file

@ -35,6 +35,7 @@
"depth": 0,
"source": "local",
"dependencies": {
"org.gamecult.cultlib": "1.0.3",
"org.gamecult.eve.surface": "0.1.0"
}
},

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 47197212764bfe04192e7b6a7e6f96a0
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,331 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using GameCult.Caching;
using GameCult.Caching.MessagePack;
using GameCult.Eve.Surface;
using GameCult.Mesh;
using GameCult.Networking;
#nullable enable
namespace GameCult.Eve.UnityScene
{
public sealed class EveUnityCultMeshLiveProviderTransport :
IEveUnitySceneLiveProviderTransport,
IDisposable
{
private const string RemoteShardId = "provider";
private static readonly Type[] WireDocumentTypes =
{
typeof(EveProviderAdvertisementDocument),
typeof(EveSurfaceDocument),
typeof(EveSurfaceCommandRequest),
typeof(EveCommandReceiptDocument)
};
private readonly string _replicaPath;
private readonly string _endpoint;
private readonly string _providerId;
private readonly string _surfaceId;
private readonly string _runtimeId;
private readonly HashSet<string> _pendingCommandIds = new HashSet<string>(StringComparer.Ordinal);
private readonly HashSet<string> _publishedReceiptIds = new HashSet<string>(StringComparer.Ordinal);
private CultMeshNode? _node;
private CultMeshSnapshotEndpoint? _snapshot;
private CultNetDocumentRegistry? _networkRegistry;
private CultNetShardDescriptor? _replicaShard;
private EveProviderAdvertisementDocument? _advertisement;
private EveAdvertisedSurface? _advertisedSurface;
public EveUnityCultMeshLiveProviderTransport(
string replicaPath,
string endpoint,
string providerId,
string surfaceId,
string runtimeId = "eve-unity")
{
_replicaPath = string.IsNullOrWhiteSpace(replicaPath)
? throw new ArgumentException("Replica path must be non-empty.", nameof(replicaPath))
: Path.GetFullPath(replicaPath);
_endpoint = string.IsNullOrWhiteSpace(endpoint)
? throw new ArgumentException("CultMesh endpoint must be non-empty.", nameof(endpoint))
: endpoint.Trim();
_providerId = string.IsNullOrWhiteSpace(providerId)
? throw new ArgumentException("Provider id must be non-empty.", nameof(providerId))
: providerId.Trim();
_surfaceId = string.IsNullOrWhiteSpace(surfaceId)
? throw new ArgumentException("Surface id must be non-empty.", nameof(surfaceId))
: surfaceId.Trim();
_runtimeId = string.IsNullOrWhiteSpace(runtimeId) ? "eve-unity" : runtimeId.Trim();
CurrentSurfaceDocument = EmptySurfaceDocument();
CurrentAssetManifestDocument = EmptyAssetManifest();
}
public string TransportKind => "eve-cultmesh-remote-replica";
public string SurfacePointer => CurrentSurfaceDocument.SourcePointer;
public string AssetManifestPointer => CurrentAssetManifestDocument.ManifestRef;
public EveUnitySceneProviderSurfaceDocument CurrentSurfaceDocument { get; private set; }
public EveUnityPlayableWorldAssetManifestDocument CurrentAssetManifestDocument { get; private set; }
public event Action<EveUnitySceneProviderSurfaceDocument>? SurfaceDocumentAvailable;
public event Action<EveUnityPlayableWorldAssetManifestDocument>? AssetManifestDocumentAvailable;
public event Action<EveUnitySceneCommandReceipt>? CommandReceiptAvailable;
public void Connect()
{
EnsureOpen();
Refresh();
}
public void Disconnect()
{
}
public void Refresh()
{
EnsureOpen();
ResolveAdvertisement();
RefreshSurface();
RefreshReceipts();
}
public void SubmitCommand(EveSurfaceCommandRequest request)
{
if (request == null) throw new ArgumentNullException(nameof(request));
EnsureOpen();
ResolveAdvertisement();
var interaction = RequireWorldInteraction();
if (string.IsNullOrWhiteSpace(interaction.CommandRecordRef))
throw new InvalidOperationException("The provider advertisement does not publish a command record reference.");
if (!string.Equals(request.ProviderId, _providerId, StringComparison.Ordinal) ||
!string.Equals(request.SurfaceId, _surfaceId, StringComparison.Ordinal))
{
throw new InvalidOperationException("The command does not target the connected provider surface.");
}
var commandId = request.CommandId;
if (string.IsNullOrWhiteSpace(commandId))
throw new InvalidOperationException("Eve command invocations require an idempotency key.");
var recordKey = ChildRecordKey(interaction.CommandRecordRef, commandId);
var message = _networkRegistry!.CreateRawDocumentPutMessage(
$"eve-unity-{commandId}",
new CultRecordHandle<EveSurfaceCommandRequest>(new CultRecordKey(recordKey)),
request,
new CultNetDocumentMessageOptions
{
SourceRuntimeId = _runtimeId,
SourceRole = "eve-unity"
});
new CultNetSchemaWriteForwarder(new CultNetSchemaWriteForwarderOptions())
.ForwardPutAsync(_replicaShard!, message)
.GetAwaiter()
.GetResult();
_pendingCommandIds.Add(commandId);
}
public void Dispose()
{
_node?.Dispose();
_node = null;
_snapshot = null;
_networkRegistry = null;
_replicaShard = null;
_pendingCommandIds.Clear();
_publishedReceiptIds.Clear();
}
private void EnsureOpen()
{
if (_node != null)
return;
var cacheRegistry = CultMesh.CreateCultCacheDocumentRegistry(WireDocumentTypes);
_networkRegistry = CultMesh.CreateCultNetDocumentRegistry(WireDocumentTypes, cacheRegistry);
_replicaShard = new CultNetShardDescriptor(
RemoteShardId,
_providerId,
epoch: 1,
isPrimary: false,
schemaIds: WireDocumentTypes.Select(type => cacheRegistry.GetRequired(type).SchemaId),
primaryEndpoints: new[] { _endpoint });
_node = CultMesh.CreateNodeAsync(
_replicaPath,
new CultMeshNodeOptions
{
StartServer = false,
EnableDurableShardLogs = true,
CacheOptions = new CultCacheOpenOptions
{
Registry = cacheRegistry,
PullOnOpen = false,
StoreFlushOnDispose = true,
UseDirectoryStore = true
},
DatabaseOptions = new CultNetDatabaseOptions
{
RuntimeId = _runtimeId,
Shards = new[] { _replicaShard },
DocumentRegistry = _networkRegistry
}
})
.GetAwaiter()
.GetResult();
_snapshot = CultMesh.SnapshotEndpoint(
_endpoint,
new CultMeshSnapshotEndpointOptions
{
Context = CultMesh.Verse("eve.remote", _runtimeId).Context,
DocumentRegistry = _networkRegistry,
Request = new CultMeshSnapshotRequestOptions
{
ShardId = RemoteShardId,
ShardEpoch = 1,
ConnectTimeout = TimeSpan.FromSeconds(5),
ResponseTimeout = TimeSpan.FromSeconds(10),
MessageIdPrefix = "eve-unity",
RudpRuntimeId = _runtimeId,
RudpMaxFragmentBytes = 1200
}
});
}
private void ResolveAdvertisement()
{
var documents = _snapshot!
.FetchDocumentsAsync<EveProviderAdvertisementDocument>()
.GetAwaiter()
.GetResult();
_advertisement = documents.FirstOrDefault(document =>
string.Equals(document.ProviderId, _providerId, StringComparison.Ordinal))
?? throw new InvalidOperationException($"Provider '{_providerId}' did not publish an Eve advertisement.");
_advertisedSurface = _advertisement.Surfaces.FirstOrDefault(surface =>
string.Equals(surface.SurfaceId, _surfaceId, StringComparison.Ordinal))
?? throw new InvalidOperationException($"Provider '{_providerId}' did not advertise surface '{_surfaceId}'.");
if (!string.Equals(_advertisedSurface.Transport, "cultmesh-record", StringComparison.Ordinal))
throw new InvalidOperationException("EveUnity CultMesh transport requires a cultmesh-record surface advertisement.");
if (string.IsNullOrWhiteSpace(_advertisedSurface.RecordRef))
throw new InvalidOperationException("The provider surface advertisement does not publish a record reference.");
}
private void RefreshSurface()
{
var surface = _snapshot!
.FetchDocumentsAsync<EveSurfaceDocument>(
recordKeys: new[] { _advertisedSurface!.RecordRef },
schemaIds: new[] { EveSurfaceDocument.SchemaId })
.GetAwaiter()
.GetResult()
.FirstOrDefault()
?? throw new InvalidOperationException("The advertised Eve surface record was not available.");
var interaction = RequireWorldInteraction();
CurrentSurfaceDocument = new EveUnitySceneProviderSurfaceDocument(
surface,
new EveUnitySceneProviderSurfaceAdvertisement(
_advertisedSurface.SurfaceId,
_advertisedSurface.SurfaceKind,
new EveUnitySceneWorldInteraction(
interaction.ProjectionKind,
interaction.CommandBoundary,
interaction.ReceiptSchema,
interaction.Ownership)),
_advertisedSurface.RecordRef,
surface.Version);
SurfaceDocumentAvailable?.Invoke(CurrentSurfaceDocument);
AssetManifestDocumentAvailable?.Invoke(CurrentAssetManifestDocument);
}
private void RefreshReceipts()
{
if (_pendingCommandIds.Count == 0)
return;
var receipts = _snapshot!
.FetchDocumentsAsync<EveCommandReceiptDocument>()
.GetAwaiter()
.GetResult();
foreach (var receipt in receipts
.Where(receipt => _pendingCommandIds.Contains(receipt.CommandId))
.OrderBy(receipt => receipt.SourceVersion))
{
if (!_publishedReceiptIds.Add(receipt.ReceiptId))
continue;
CommandReceiptAvailable?.Invoke(new EveUnitySceneCommandReceipt(
receipt.ReceiptId,
receipt.Command,
receipt.CommandId,
receipt.State,
receipt.OwnerRepo,
receipt.Authority,
receipt.Schema,
receipt.ProviderId,
receipt.SurfaceId,
receipt.Message,
DateTimeOffset.TryParse(receipt.IssuedAtUtc, out var issuedAt) ? issuedAt : null,
receipt.SourceVersion));
if (string.Equals(receipt.State, "accepted", StringComparison.OrdinalIgnoreCase) ||
string.Equals(receipt.State, "denied", StringComparison.OrdinalIgnoreCase) ||
string.Equals(receipt.State, "reconciled", StringComparison.OrdinalIgnoreCase))
{
_pendingCommandIds.Remove(receipt.CommandId);
}
}
}
private EveWorldInteractionAdvertisement RequireWorldInteraction()
{
return _advertisedSurface?.WorldInteraction
?? throw new InvalidOperationException("The advertised surface has no interactive-world contract.");
}
private EveUnitySceneProviderSurfaceDocument EmptySurfaceDocument()
{
var surface = new EveSurfaceDocument(
_providerId,
"",
"",
0,
"",
new EveSurfaceTree(
_surfaceId,
new EveSurfaceComponent(
"root",
"surface",
new Dictionary<string, string>(),
Array.Empty<EveSurfaceComponent>()),
Array.Empty<EveStyleToken>()),
Array.Empty<EveCommandTemplate>());
return new EveUnitySceneProviderSurfaceDocument(
surface,
new EveUnitySceneProviderSurfaceAdvertisement(
_surfaceId,
"interactive-world",
new EveUnitySceneWorldInteraction("", "", EveCommandReceiptDocument.SchemaId, "")),
"",
0);
}
private EveUnityPlayableWorldAssetManifestDocument EmptyAssetManifest()
{
return new EveUnityPlayableWorldAssetManifestDocument(
"",
Array.Empty<EveUnityPlayableWorldAssetManifestDocumentEntry>(),
_providerId);
}
private static string ChildRecordKey(string parent, string child)
{
return $"{parent.TrimEnd(':', '/')}:{child}";
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c11a1170aa79f19448d38816e0b4080a

View file

@ -0,0 +1,22 @@
{
"name": "GameCult.Eve.UnityScene.CultMesh",
"rootNamespace": "GameCult.Eve.UnityScene",
"references": [
"GameCult.Eve.Surface",
"GameCult.Eve.UnityScene"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"GameCult.Caching.dll",
"GameCult.Caching.MessagePack.dll",
"GameCult.Mesh.dll",
"GameCult.Networking.dll"
],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 9455c0bf8c92c1f4b80f60572332c88e
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -5,6 +5,7 @@
"unity": "2021.3",
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
"dependencies": {
"org.gamecult.cultlib": "1.0.3",
"org.gamecult.eve.surface": "0.1.0"
},
"keywords": [

View file

@ -0,0 +1,89 @@
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"
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$projectRoot = Join-Path $repoRoot "TestProject"
$outputRoot = if ([IO.Path]::IsPathRooted($OutputDirectory)) { $OutputDirectory } else { Join-Path $repoRoot $OutputDirectory }
$resultsPath = Join-Path $outputRoot "results.xml"
$unityLogPath = Join-Path $outputRoot "unity.log"
$daemonLogPath = Join-Path $outputRoot "aetheria-daemon.log"
$capturePath = Join-Path $outputRoot "aetheria-daemon-world.png"
$replicaPath = Join-Path $outputRoot "eve-unity-replica.cc"
$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"))) {
if (-not (Test-Path -LiteralPath $required)) { throw "Required world witness path not found: $required" }
}
New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null
foreach ($ephemeralPath in @($replicaPath, $statePath)) {
$resolved = [IO.Path]::GetFullPath($ephemeralPath)
if (-not $resolved.StartsWith([IO.Path]::GetFullPath($outputRoot), [StringComparison]::OrdinalIgnoreCase)) {
throw "Witness cleanup escaped its artifact directory: $resolved"
}
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" }
$daemonArguments = @(
"run", "--project", $daemonProject, "--",
"--root", $AetheriaRoot,
"--state", $statePath,
"--client-cultmesh-host", "127.0.0.1",
"--client-cultmesh-advertise-host", "127.0.0.1",
"--client-cultmesh-port", $Port,
"--no-odin-announcements"
)
$daemon = Start-Process -FilePath "dotnet" -ArgumentList $daemonArguments -PassThru -WindowStyle Hidden `
-RedirectStandardOutput $daemonLogPath -RedirectStandardError (Join-Path $outputRoot "aetheria-daemon.error.log")
Write-Host "Aetheria daemon PID: $($daemon.Id)"
Write-Host "Daemon log: $daemonLogPath"
Write-Host "Poll: Select-String -Path '$daemonLogPath' -Pattern 'Aetheria client CultMesh endpoint'"
try {
$ready = $false
for ($attempt = 0; $attempt -lt 60; $attempt++) {
if ($daemon.HasExited) { throw "Aetheria daemon exited before publishing CultMesh. See $daemonLogPath" }
if ((Test-Path $daemonLogPath) -and
(Select-String -Path $daemonLogPath -Pattern "Aetheria client CultMesh endpoint: rudp://127.0.0.1:$Port" -Quiet)) { $ready = $true; break }
Start-Sleep -Milliseconds 500
}
if (-not $ready) { throw "Aetheria daemon did not open CultMesh port $Port. See $daemonLogPath" }
$env:EVEUNITY_PROVIDER_ENDPOINT = "rudp://127.0.0.1:$Port"
$env:EVEUNITY_PROVIDER_ID = "aetheria.daemon"
$env:EVEUNITY_SURFACE_ID = "aetheria.game"
$env:EVEUNITY_REPLICA_PATH = $replicaPath
$env:EVEUNITY_AETHERIA_CAPTURE_PATH = $capturePath
$arguments = @(
"-batchmode", "-projectPath", $projectRoot,
"-runTests", "-testPlatform", "PlayMode",
"-assemblyNames", "GameCult.EveUnity.GenericClient.PlayModeTests",
"-testFilter", "GenericCultMeshClientLowersAndMovesAdvertisedWorld",
"-testResults", $resultsPath, "-logFile", $unityLogPath
)
$unity = Start-Process -FilePath $UnityExe -ArgumentList $arguments -PassThru -WindowStyle Hidden
if (-not $unity.WaitForExit(90000)) {
Stop-Process -Id $unity.Id -Force -ErrorAction SilentlyContinue
throw "Unity witness exceeded 90 seconds. See $unityLogPath"
}
if ($unity.ExitCode -ne 0) { Get-Content $unityLogPath -Tail 160; throw "Unity witness failed with exit code $($unity.ExitCode)" }
[xml] $results = Get-Content $resultsPath -Raw
$run = $results.SelectSingleNode("//test-run")
if ($null -eq $run -or [int]$run.passed -ne 1 -or [int]$run.failed -ne 0) { throw "Live world witness did not pass: $resultsPath" }
if (-not (Test-Path $capturePath) -or (Get-Item $capturePath).Length -lt 1024) { throw "Live world capture is missing: $capturePath" }
Write-Host "Aetheria daemon world witness: passed"
Write-Host "Capture: $capturePath"
}
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 -ErrorAction SilentlyContinue
}