2026-07-09 19:20:09 +01:00
|
|
|
using System;
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
using System.Linq;
|
|
|
|
|
using GameCult.Aetheria.State.Verse;
|
|
|
|
|
using GameCult.Eve.Surface;
|
|
|
|
|
using GameCult.Eve.UnityScene;
|
|
|
|
|
using GameCult.Mesh;
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
using EveSurfaceDocument = GameCult.Eve.Surface.EveSurfaceDocument;
|
|
|
|
|
|
|
|
|
|
#nullable enable
|
|
|
|
|
|
|
|
|
|
namespace GameCult.Aetheria.EveRuntime
|
|
|
|
|
{
|
|
|
|
|
public sealed class AetheriaEveUnitySceneProviderBridge :
|
2026-07-09 21:52:45 +01:00
|
|
|
IEveUnitySceneLiveProviderTransport,
|
2026-07-09 19:20:09 +01:00
|
|
|
IEveUnitySceneProviderSurfaceDocumentSource,
|
|
|
|
|
IEveUnityPlayableWorldAssetManifestDocumentSource,
|
|
|
|
|
IEveUnitySceneCommandSink,
|
2026-07-09 20:21:43 +01:00
|
|
|
IEveUnitySceneCommandReceiptSource,
|
2026-07-09 19:34:22 +01:00
|
|
|
IEveUnityProviderRefreshSource,
|
2026-07-09 19:20:09 +01:00
|
|
|
IDisposable
|
|
|
|
|
{
|
|
|
|
|
private readonly string _stateFilePathOverride;
|
|
|
|
|
private readonly string _surfaceId;
|
|
|
|
|
private readonly string _runtimeId;
|
2026-07-10 03:07:26 +01:00
|
|
|
private readonly string _cultMeshEndpoint;
|
2026-07-09 19:20:09 +01:00
|
|
|
private AetheriaRuntimeStateBootReport? _stateBoot;
|
|
|
|
|
private AetheriaClientState? _runtimeState;
|
2026-07-10 03:07:26 +01:00
|
|
|
private AetheriaClient? _remoteClient;
|
2026-07-10 03:15:17 +01:00
|
|
|
private readonly AetheriaEveUnityRemoteReceiptTracker _remoteReceipts =
|
|
|
|
|
new AetheriaEveUnityRemoteReceiptTracker();
|
2026-07-09 19:20:09 +01:00
|
|
|
private event Action<EveUnityPlayableWorldAssetManifestDocument>? AssetManifestDocumentAvailable;
|
|
|
|
|
|
|
|
|
|
public AetheriaEveUnitySceneProviderBridge(
|
|
|
|
|
string stateFilePathOverride = "",
|
|
|
|
|
string surfaceId = AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId,
|
2026-07-10 03:07:26 +01:00
|
|
|
string runtimeId = "unity-scene",
|
|
|
|
|
string cultMeshEndpoint = "")
|
2026-07-09 19:20:09 +01:00
|
|
|
{
|
|
|
|
|
_stateFilePathOverride = stateFilePathOverride ?? "";
|
|
|
|
|
_surfaceId = string.IsNullOrWhiteSpace(surfaceId)
|
|
|
|
|
? AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId
|
|
|
|
|
: surfaceId;
|
|
|
|
|
_runtimeId = string.IsNullOrWhiteSpace(runtimeId) ? "unity-scene" : runtimeId;
|
2026-07-10 03:07:26 +01:00
|
|
|
_cultMeshEndpoint = cultMeshEndpoint?.Trim() ?? "";
|
2026-07-09 19:20:09 +01:00
|
|
|
CurrentDocument = CreateSurfaceDocument(AetheriaRuntimeSurfaceDocuments.EmptySurface(_surfaceId), 0);
|
|
|
|
|
CurrentDocumentManifest = new EveUnityPlayableWorldAssetManifestDocument(
|
|
|
|
|
AetheriaRuntimeVerseRecordKeys.DaemonAssetManifest.ToString(),
|
|
|
|
|
Array.Empty<EveUnityPlayableWorldAssetManifestDocumentEntry>(),
|
|
|
|
|
"aetheria.daemon");
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-10 03:07:26 +01:00
|
|
|
public string TransportKind => IsRemote
|
|
|
|
|
? "aetheria-remote-cultmesh-replica"
|
|
|
|
|
: "aetheria-local-cultmesh-replica";
|
2026-07-09 21:52:45 +01:00
|
|
|
|
|
|
|
|
public string SurfacePointer => CurrentDocument.SourcePointer;
|
|
|
|
|
|
|
|
|
|
public string AssetManifestPointer => CurrentDocumentManifest.ManifestRef;
|
|
|
|
|
|
2026-07-09 19:20:09 +01:00
|
|
|
public string SinkKind => "aetheria-daemon-command-boundary";
|
|
|
|
|
|
2026-07-09 19:34:22 +01:00
|
|
|
public string ManifestRef => CurrentDocumentManifest.ManifestRef;
|
|
|
|
|
|
2026-07-09 19:20:09 +01:00
|
|
|
public EveUnitySceneProviderSurfaceDocument CurrentDocument { get; private set; }
|
|
|
|
|
|
2026-07-09 21:52:45 +01:00
|
|
|
EveUnitySceneProviderSurfaceDocument IEveUnitySceneLiveProviderTransport.CurrentSurfaceDocument =>
|
|
|
|
|
CurrentDocument;
|
|
|
|
|
|
2026-07-09 19:20:09 +01:00
|
|
|
EveUnityPlayableWorldAssetManifestDocument IEveUnityPlayableWorldAssetManifestDocumentSource.CurrentDocument =>
|
|
|
|
|
CurrentDocumentManifest;
|
|
|
|
|
|
2026-07-09 21:52:45 +01:00
|
|
|
EveUnityPlayableWorldAssetManifestDocument IEveUnitySceneLiveProviderTransport.CurrentAssetManifestDocument =>
|
|
|
|
|
CurrentDocumentManifest;
|
|
|
|
|
|
2026-07-09 19:20:09 +01:00
|
|
|
public EveUnityPlayableWorldAssetManifestDocument CurrentDocumentManifest { get; private set; }
|
|
|
|
|
|
|
|
|
|
public event Action<EveUnitySceneProviderSurfaceDocument>? DocumentAvailable;
|
|
|
|
|
|
2026-07-09 20:21:43 +01:00
|
|
|
public event Action<EveUnitySceneCommandReceipt>? ReceiptAvailable;
|
|
|
|
|
|
2026-07-09 21:52:45 +01:00
|
|
|
event Action<EveUnitySceneProviderSurfaceDocument> IEveUnitySceneLiveProviderTransport.SurfaceDocumentAvailable
|
|
|
|
|
{
|
|
|
|
|
add => DocumentAvailable += value;
|
|
|
|
|
remove => DocumentAvailable -= value;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 19:20:09 +01:00
|
|
|
event Action<EveUnityPlayableWorldAssetManifestDocument> IEveUnityPlayableWorldAssetManifestDocumentSource.DocumentAvailable
|
|
|
|
|
{
|
|
|
|
|
add => AssetManifestDocumentAvailable += value;
|
|
|
|
|
remove => AssetManifestDocumentAvailable -= value;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 21:52:45 +01:00
|
|
|
event Action<EveUnityPlayableWorldAssetManifestDocument> IEveUnitySceneLiveProviderTransport.AssetManifestDocumentAvailable
|
|
|
|
|
{
|
|
|
|
|
add => AssetManifestDocumentAvailable += value;
|
|
|
|
|
remove => AssetManifestDocumentAvailable -= value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
event Action<EveUnitySceneCommandReceipt> IEveUnitySceneLiveProviderTransport.CommandReceiptAvailable
|
|
|
|
|
{
|
|
|
|
|
add => ReceiptAvailable += value;
|
|
|
|
|
remove => ReceiptAvailable -= value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Connect()
|
|
|
|
|
{
|
|
|
|
|
Refresh();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Disconnect()
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 19:20:09 +01:00
|
|
|
public void Refresh()
|
|
|
|
|
{
|
2026-07-10 03:07:26 +01:00
|
|
|
if (IsRemote)
|
|
|
|
|
{
|
|
|
|
|
var remote = ResolveRemoteClient();
|
|
|
|
|
remote.RefreshRemoteAsync().GetAwaiter().GetResult();
|
|
|
|
|
RefreshFromRuntimeState(remote.State, null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 19:20:09 +01:00
|
|
|
var stateBoot = ResolveStateBoot();
|
|
|
|
|
if (!stateBoot.SupportsLocalStateFileRead || !stateBoot.StateFileExists)
|
|
|
|
|
return;
|
|
|
|
|
|
2026-07-10 03:07:26 +01:00
|
|
|
RefreshFromRuntimeState(ResolveRuntimeState(stateBoot), stateBoot);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void RefreshFromRuntimeState(
|
|
|
|
|
AetheriaClientState runtimeState,
|
|
|
|
|
AetheriaRuntimeStateBootReport? stateBoot)
|
|
|
|
|
{
|
2026-07-09 19:20:09 +01:00
|
|
|
var surface = ReadSurface(runtimeState, stateBoot);
|
|
|
|
|
if (surface != null)
|
|
|
|
|
{
|
2026-07-09 22:05:03 +01:00
|
|
|
CurrentDocument = CreateSurfaceDocument(
|
|
|
|
|
surface,
|
|
|
|
|
surface.Version,
|
|
|
|
|
ReadAdvertisedSurface(runtimeState));
|
2026-07-09 19:20:09 +01:00
|
|
|
DocumentAvailable?.Invoke(CurrentDocument);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
CurrentDocumentManifest = ReadAssetManifest(runtimeState);
|
|
|
|
|
AssetManifestDocumentAvailable?.Invoke(CurrentDocumentManifest);
|
2026-07-10 03:15:17 +01:00
|
|
|
ReconcileRemoteReceipts();
|
2026-07-09 19:20:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Submit(EveSurfaceCommandRequest request)
|
|
|
|
|
{
|
|
|
|
|
if (request == null) throw new ArgumentNullException(nameof(request));
|
|
|
|
|
var clientId = string.IsNullOrWhiteSpace(request.ClientId) ? _runtimeId : request.ClientId;
|
2026-07-10 03:07:26 +01:00
|
|
|
if (IsRemote)
|
|
|
|
|
{
|
|
|
|
|
var remote = ResolveRemoteClient();
|
|
|
|
|
if (!remote.Control.TrySubmitSurfaceCommand(request, out var submitted))
|
|
|
|
|
throw new InvalidOperationException($"Aetheria daemon did not advertise command {request.Operation?.OperationId ?? "unknown"}.");
|
|
|
|
|
|
|
|
|
|
Debug.Log(
|
|
|
|
|
$"Forwarded Aetheria operation to remote CultMesh authority: {submitted!.Kind} {submitted.CommandId}");
|
2026-07-10 03:15:17 +01:00
|
|
|
_remoteReceipts.Track(submitted.CommandId, request);
|
2026-07-10 03:07:26 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 19:20:09 +01:00
|
|
|
var stateBoot = ResolveStateBoot();
|
|
|
|
|
var control = AetheriaEveRuntimeUnityHooks.RequireControl(stateBoot, clientId);
|
|
|
|
|
if (control.TrySubmitSurfaceCommand(request, out var daemonEnvelope))
|
|
|
|
|
{
|
|
|
|
|
Debug.Log(
|
|
|
|
|
$"Submitted Aetheria daemon operation from Eve Unity scene: {daemonEnvelope!.Kind} {daemonEnvelope.CommandId}");
|
2026-07-09 20:21:43 +01:00
|
|
|
ReceiptAvailable?.Invoke(ToReceipt(request, daemonEnvelope));
|
2026-07-09 19:20:09 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var envelope = AetheriaEveRuntimeUnityHooks
|
|
|
|
|
.RequireUi(stateBoot, _runtimeId)
|
|
|
|
|
.SurfaceCommandAsync(request, clientId)
|
|
|
|
|
.GetAwaiter()
|
|
|
|
|
.GetResult();
|
|
|
|
|
Debug.Log($"Submitted Eve Unity scene operation for CultMesh bridge: {envelope.OperationId}");
|
2026-07-09 20:21:43 +01:00
|
|
|
ReceiptAvailable?.Invoke(ToReceipt(request, envelope));
|
2026-07-09 19:20:09 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-09 21:52:45 +01:00
|
|
|
public void SubmitCommand(EveSurfaceCommandRequest request)
|
|
|
|
|
{
|
|
|
|
|
Submit(request);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 19:20:09 +01:00
|
|
|
public void Dispose()
|
|
|
|
|
{
|
2026-07-10 03:07:26 +01:00
|
|
|
_remoteClient?.Dispose();
|
|
|
|
|
_remoteClient = null;
|
2026-07-10 03:15:17 +01:00
|
|
|
_remoteReceipts.Clear();
|
2026-07-10 03:07:26 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private bool IsRemote => !string.IsNullOrWhiteSpace(_cultMeshEndpoint);
|
|
|
|
|
|
|
|
|
|
private AetheriaClient ResolveRemoteClient()
|
|
|
|
|
{
|
|
|
|
|
if (_remoteClient != null)
|
|
|
|
|
return _remoteClient;
|
|
|
|
|
|
|
|
|
|
var replicaPath = string.IsNullOrWhiteSpace(_stateFilePathOverride)
|
|
|
|
|
? System.IO.Path.Combine(Application.persistentDataPath, "aetheria-remote-replica.cc")
|
|
|
|
|
: _stateFilePathOverride;
|
|
|
|
|
_remoteClient = AetheriaClient
|
|
|
|
|
.OpenRemoteAsync(
|
|
|
|
|
replicaPath,
|
|
|
|
|
_cultMeshEndpoint,
|
|
|
|
|
_runtimeId,
|
|
|
|
|
sessionId: "eve-unity",
|
|
|
|
|
synchronizeOnOpen: false)
|
|
|
|
|
.GetAwaiter()
|
|
|
|
|
.GetResult();
|
|
|
|
|
return _remoteClient;
|
2026-07-09 19:20:09 +01:00
|
|
|
}
|
|
|
|
|
|
2026-07-10 03:15:17 +01:00
|
|
|
private void ReconcileRemoteReceipts()
|
|
|
|
|
{
|
|
|
|
|
if (!IsRemote || !_remoteReceipts.HasPending || _remoteClient == null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
foreach (var receipt in _remoteReceipts.Reconcile(
|
|
|
|
|
_remoteClient.CommittedCommandFacts(),
|
|
|
|
|
CurrentDocument.Version))
|
|
|
|
|
ReceiptAvailable?.Invoke(receipt);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 19:20:09 +01:00
|
|
|
private AetheriaRuntimeStateBootReport ResolveStateBoot()
|
|
|
|
|
{
|
2026-07-09 19:34:22 +01:00
|
|
|
var stateBoot = AetheriaEveRuntimeUnityHooks.RequireStateBoot(_stateFilePathOverride);
|
|
|
|
|
_stateBoot = stateBoot;
|
|
|
|
|
return stateBoot;
|
2026-07-09 19:20:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private AetheriaClientState ResolveRuntimeState(AetheriaRuntimeStateBootReport stateBoot)
|
|
|
|
|
{
|
2026-07-09 19:34:22 +01:00
|
|
|
var runtimeState = AetheriaEveRuntimeUnityHooks.RequireRuntimeState(stateBoot, _runtimeId);
|
|
|
|
|
_runtimeState = runtimeState;
|
|
|
|
|
return runtimeState;
|
2026-07-09 19:20:09 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private EveSurfaceDocument? ReadSurface(
|
|
|
|
|
AetheriaClientState runtimeState,
|
2026-07-10 03:07:26 +01:00
|
|
|
AetheriaRuntimeStateBootReport? stateBoot)
|
2026-07-09 19:20:09 +01:00
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var handle = runtimeState.EveSurfaceDocument(_surfaceId);
|
|
|
|
|
if (handle == null)
|
|
|
|
|
return null;
|
|
|
|
|
|
2026-07-10 03:07:26 +01:00
|
|
|
var resolver = stateBoot == null
|
|
|
|
|
? null
|
|
|
|
|
: AetheriaEveRuntimeUnityHooks.TryCreateStateRefResolver(stateBoot.Value, _runtimeId);
|
2026-07-09 21:05:06 +01:00
|
|
|
return AetheriaRuntimeSurfaceDocuments.ToEveSurfaceDocument(handle.Latest(), resolver);
|
2026-07-09 19:20:09 +01:00
|
|
|
}
|
|
|
|
|
catch (KeyNotFoundException)
|
|
|
|
|
{
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private EveUnityPlayableWorldAssetManifestDocument ReadAssetManifest(AetheriaClientState runtimeState)
|
|
|
|
|
{
|
2026-07-09 22:22:03 +01:00
|
|
|
var manifest = runtimeState.AssetManifest.Latest();
|
2026-07-09 19:20:09 +01:00
|
|
|
return ToUnityPlayableWorldAssetManifest(manifest);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 22:05:03 +01:00
|
|
|
private AetheriaRuntimeEveSurfaceAdvertisement? ReadAdvertisedSurface(AetheriaClientState runtimeState)
|
|
|
|
|
{
|
|
|
|
|
var provider = runtimeState.ProviderAdvertisement.Latest();
|
|
|
|
|
return (provider.EveSurfaces ?? Array.Empty<AetheriaRuntimeEveSurfaceAdvertisement>())
|
|
|
|
|
.FirstOrDefault(surface => surface != null && string.Equals(surface.SurfaceId, _surfaceId, StringComparison.Ordinal))
|
|
|
|
|
?? AetheriaRuntimeEveSurfaceCatalog.Find(_surfaceId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 19:20:09 +01:00
|
|
|
private EveUnitySceneProviderSurfaceDocument CreateSurfaceDocument(
|
|
|
|
|
EveSurfaceDocument surface,
|
2026-07-09 22:05:03 +01:00
|
|
|
long version,
|
|
|
|
|
AetheriaRuntimeEveSurfaceAdvertisement? advertisedSurface = null)
|
2026-07-09 19:20:09 +01:00
|
|
|
{
|
2026-07-09 22:05:03 +01:00
|
|
|
advertisedSurface ??= AetheriaRuntimeEveSurfaceCatalog.Find(_surfaceId);
|
|
|
|
|
var worldInteraction = advertisedSurface?.WorldInteraction;
|
2026-07-09 19:20:09 +01:00
|
|
|
return new EveUnitySceneProviderSurfaceDocument(
|
|
|
|
|
surface,
|
|
|
|
|
new EveUnitySceneProviderSurfaceAdvertisement(
|
|
|
|
|
_surfaceId,
|
2026-07-09 22:05:03 +01:00
|
|
|
advertisedSurface?.SurfaceKind ?? "",
|
2026-07-09 19:20:09 +01:00
|
|
|
new EveUnitySceneWorldInteraction(
|
2026-07-09 22:05:03 +01:00
|
|
|
worldInteraction?.ProjectionKind ?? "",
|
|
|
|
|
worldInteraction?.CommandBoundary ?? "",
|
|
|
|
|
worldInteraction?.ReceiptSchema ?? "",
|
|
|
|
|
worldInteraction?.Ownership ?? "")),
|
|
|
|
|
SourcePointer(advertisedSurface),
|
2026-07-09 19:20:09 +01:00
|
|
|
version);
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 22:05:03 +01:00
|
|
|
private string SourcePointer(AetheriaRuntimeEveSurfaceAdvertisement? advertisedSurface)
|
2026-07-09 19:20:09 +01:00
|
|
|
{
|
2026-07-09 22:05:03 +01:00
|
|
|
if (!string.IsNullOrWhiteSpace(advertisedSurface?.RecordRef))
|
|
|
|
|
return advertisedSurface.RecordRef;
|
|
|
|
|
|
2026-07-09 19:20:09 +01:00
|
|
|
var path = _stateBoot?.StateFilePath ?? _stateFilePathOverride;
|
|
|
|
|
return string.IsNullOrWhiteSpace(path)
|
|
|
|
|
? AetheriaRuntimeVerseRecordKeys.DaemonGameSurface.ToString()
|
|
|
|
|
: $"{path}#{_surfaceId}";
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 20:21:43 +01:00
|
|
|
private static EveUnitySceneCommandReceipt ToReceipt(
|
|
|
|
|
EveSurfaceCommandRequest request,
|
|
|
|
|
AetheriaRuntimeDaemonCommandEnvelope envelope)
|
|
|
|
|
{
|
|
|
|
|
if (request == null) throw new ArgumentNullException(nameof(request));
|
|
|
|
|
if (envelope == null) throw new ArgumentNullException(nameof(envelope));
|
|
|
|
|
|
|
|
|
|
return new EveUnitySceneCommandReceipt(
|
|
|
|
|
string.IsNullOrWhiteSpace(envelope.CommandId) ? envelope.OperationId : envelope.CommandId,
|
|
|
|
|
envelope.OperationId,
|
|
|
|
|
envelope.CommandId,
|
|
|
|
|
envelope.Accepted ? "accepted" : "denied",
|
|
|
|
|
"Aetheria",
|
|
|
|
|
"aetheria-daemon-command-boundary",
|
|
|
|
|
request.ReceiptSchema,
|
|
|
|
|
request.ProviderId,
|
|
|
|
|
request.SurfaceId,
|
|
|
|
|
envelope.Diagnostic ?? "",
|
|
|
|
|
ParseIssuedAt(envelope.IssuedAtUtc, request.IssuedAt));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static EveUnitySceneCommandReceipt ToReceipt(
|
|
|
|
|
EveSurfaceCommandRequest request,
|
|
|
|
|
CultMeshOperationReceipt receipt)
|
|
|
|
|
{
|
|
|
|
|
if (request == null) throw new ArgumentNullException(nameof(request));
|
|
|
|
|
if (receipt == null) throw new ArgumentNullException(nameof(receipt));
|
|
|
|
|
|
|
|
|
|
var commandId = request.Operation?.IdempotencyKey ?? "";
|
|
|
|
|
return new EveUnitySceneCommandReceipt(
|
|
|
|
|
string.IsNullOrWhiteSpace(commandId) ? receipt.OperationId : commandId,
|
|
|
|
|
receipt.OperationId,
|
|
|
|
|
commandId,
|
|
|
|
|
receipt.Accepted ? "accepted" : "denied",
|
|
|
|
|
"Aetheria",
|
|
|
|
|
"aetheria-eve-command-boundary",
|
|
|
|
|
request.ReceiptSchema,
|
|
|
|
|
request.ProviderId,
|
|
|
|
|
request.SurfaceId,
|
|
|
|
|
receipt.Diagnostic ?? "",
|
|
|
|
|
request.IssuedAt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static DateTimeOffset ParseIssuedAt(string value, DateTimeOffset fallback)
|
|
|
|
|
{
|
|
|
|
|
return DateTimeOffset.TryParse(value, out var parsed) ? parsed : fallback;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 19:20:09 +01:00
|
|
|
private static EveUnityPlayableWorldAssetManifestDocument ToUnityPlayableWorldAssetManifest(
|
|
|
|
|
AetheriaRuntimeAssetManifestDocument manifest)
|
|
|
|
|
{
|
|
|
|
|
var entries = (manifest.Assets ?? Array.Empty<AetheriaRuntimeAssetManifestEntry>())
|
|
|
|
|
.Where(entry => entry?.Ref != null && !string.IsNullOrWhiteSpace(entry.Ref.AssetKey))
|
|
|
|
|
.Select(entry => new EveUnityPlayableWorldAssetManifestDocumentEntry(
|
|
|
|
|
entry.Ref.AssetKey,
|
|
|
|
|
EntityKind(entry),
|
|
|
|
|
ResourcePath(entry.Ref),
|
|
|
|
|
entry.Ref.AssetKey,
|
|
|
|
|
"provider-asset-ref"))
|
|
|
|
|
.ToArray();
|
|
|
|
|
|
|
|
|
|
return new EveUnityPlayableWorldAssetManifestDocument(
|
|
|
|
|
AetheriaRuntimeVerseRecordKeys.DaemonAssetManifest.ToString(),
|
|
|
|
|
entries,
|
|
|
|
|
"aetheria.daemon");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string EntityKind(AetheriaRuntimeAssetManifestEntry entry)
|
|
|
|
|
{
|
|
|
|
|
return (entry.Tags ?? Array.Empty<string>())
|
|
|
|
|
.FirstOrDefault(tag => !string.IsNullOrWhiteSpace(tag) && !string.Equals(tag, "map", StringComparison.OrdinalIgnoreCase) && !string.Equals(tag, "icon", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
?? "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static string ResourcePath(AetheriaRuntimeAssetRef asset)
|
|
|
|
|
{
|
|
|
|
|
if (asset.Metadata != null && asset.Metadata.TryGetValue("resourcesPath", out var resourcesPath))
|
|
|
|
|
return resourcesPath ?? "";
|
|
|
|
|
|
|
|
|
|
return string.IsNullOrWhiteSpace(asset.AssetKey)
|
|
|
|
|
? ""
|
|
|
|
|
: asset.AssetKey.Replace('.', '/');
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-10 03:15:17 +01:00
|
|
|
|
|
|
|
|
public sealed class AetheriaEveUnityRemoteReceiptTracker
|
|
|
|
|
{
|
|
|
|
|
private readonly Dictionary<string, EveSurfaceCommandRequest> _pending =
|
|
|
|
|
new Dictionary<string, EveSurfaceCommandRequest>(StringComparer.Ordinal);
|
|
|
|
|
|
|
|
|
|
public bool HasPending => _pending.Count > 0;
|
|
|
|
|
|
|
|
|
|
public void Track(string commandId, EveSurfaceCommandRequest request)
|
|
|
|
|
{
|
|
|
|
|
if (string.IsNullOrWhiteSpace(commandId))
|
|
|
|
|
throw new ArgumentException("Remote command id must be non-empty.", nameof(commandId));
|
|
|
|
|
_pending[commandId] = request ?? throw new ArgumentNullException(nameof(request));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public IReadOnlyList<EveUnitySceneCommandReceipt> Reconcile(
|
|
|
|
|
IReadOnlyList<AetheriaRuntimeCommittedCommandFactDocument> facts,
|
|
|
|
|
long observedSurfaceVersion)
|
|
|
|
|
{
|
|
|
|
|
if (facts == null) throw new ArgumentNullException(nameof(facts));
|
|
|
|
|
var receipts = new List<EveUnitySceneCommandReceipt>();
|
|
|
|
|
var byCommandId = facts
|
|
|
|
|
.Where(fact => fact != null && !string.IsNullOrWhiteSpace(fact.CommandId))
|
|
|
|
|
.GroupBy(fact => fact.CommandId, StringComparer.Ordinal)
|
|
|
|
|
.ToDictionary(
|
|
|
|
|
group => group.Key,
|
|
|
|
|
group => group.OrderByDescending(fact => fact.SourceFrameId).First(),
|
|
|
|
|
StringComparer.Ordinal);
|
|
|
|
|
foreach (var commandId in _pending.Keys.ToArray())
|
|
|
|
|
{
|
|
|
|
|
if (!byCommandId.TryGetValue(commandId, out var fact) ||
|
|
|
|
|
observedSurfaceVersion < fact.SourceFrameId)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
var request = _pending[commandId];
|
|
|
|
|
_pending.Remove(commandId);
|
|
|
|
|
var accepted = string.Equals(
|
|
|
|
|
fact.Outcome,
|
|
|
|
|
AetheriaRuntimeCommandFactOutcomes.Applied,
|
|
|
|
|
StringComparison.Ordinal);
|
|
|
|
|
receipts.Add(new EveUnitySceneCommandReceipt(
|
|
|
|
|
fact.FactId,
|
|
|
|
|
request.Operation?.OperationId ?? "",
|
|
|
|
|
commandId,
|
|
|
|
|
accepted ? "accepted" : "denied",
|
|
|
|
|
"Aetheria",
|
|
|
|
|
fact.SourceDaemonId,
|
|
|
|
|
AetheriaRuntimeDaemonSchemas.CommittedCommandFact,
|
|
|
|
|
request.ProviderId,
|
|
|
|
|
request.SurfaceId,
|
|
|
|
|
accepted ? "Command applied by authoritative daemon." : "Command rejected by authoritative daemon.",
|
2026-07-10 04:03:11 +01:00
|
|
|
DateTimeOffset.TryParse(fact.CommittedAtUtc, out var committedAt) ? committedAt : request.IssuedAt,
|
|
|
|
|
fact.SourceFrameId));
|
2026-07-10 03:15:17 +01:00
|
|
|
}
|
|
|
|
|
return receipts;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public void Clear()
|
|
|
|
|
{
|
|
|
|
|
_pending.Clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-09 19:20:09 +01:00
|
|
|
}
|