Add generic CultMesh world transport
This commit is contained in:
parent
2cd2f13bd1
commit
3a2052f1f4
10 changed files with 583 additions and 2 deletions
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
"depth": 0,
|
||||
"source": "local",
|
||||
"dependencies": {
|
||||
"org.gamecult.cultlib": "1.0.3",
|
||||
"org.gamecult.eve.surface": "0.1.0"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 47197212764bfe04192e7b6a7e6f96a0
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c11a1170aa79f19448d38816e0b4080a
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9455c0bf8c92c1f4b80f60572332c88e
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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": [
|
||||
|
|
|
|||
89
scripts/run-aetheria-daemon-world-witness.ps1
Normal file
89
scripts/run-aetheria-daemon-world-witness.ps1
Normal 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
|
||||
}
|
||||
Reference in a new issue