Compose exact reactive Eve surfaces

This commit is contained in:
Meta Vens 2026-07-19 07:35:16 +01:00
commit 6387879951
6 changed files with 194 additions and 7 deletions

View file

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("GameCult.Eve.UnityScene.Tests")]

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 68e2f1a625b94fc39dcc44d33e0cf318

View file

@ -55,6 +55,8 @@ namespace GameCult.Eve.UnityScene
private readonly List<ICultMeshBodyReadLease> _assetBodyLeases = new List<ICultMeshBodyReadLease>();
private readonly Dictionary<string, int> _renderChannelLayers = new Dictionary<string, int>(StringComparer.Ordinal);
private readonly ConcurrentQueue<object> _liveDocuments = new ConcurrentQueue<object>();
private readonly Dictionary<string, EveSurfaceDocument> _embeddedSurfaces =
new Dictionary<string, EveSurfaceDocument>(StringComparer.Ordinal);
private CultMeshNode? _node;
private CultMeshSnapshotSession? _snapshot;
private CultNetDocumentRegistry? _networkRegistry;
@ -66,6 +68,7 @@ namespace GameCult.Eve.UnityScene
private CultMeshVerifiedBodyMappingBroker? _assetBodyMappings;
private EveProviderAdvertisementDocument? _advertisement;
private EveAdvertisedSurface? _advertisedSurface;
private EveSurfaceDocument? _baseSurface;
private CultMeshBodyPublicationResolver? _bodyResolver;
private EveEntitySoaViewDocument? _latestEntityLayout;
private CultMeshBodyPublicationDocument? _latestEntityPublication;
@ -215,8 +218,13 @@ namespace GameCult.Eve.UnityScene
PublishEntityView(entityGeneration.View, entityGeneration.Publication);
else if (document is EveFieldsSplatsDocument fields)
FieldsSplatsAvailable?.Invoke(fields);
else if (document is EveSurfaceDocument surface)
PublishSurface(surface);
else if (document is PendingSurfaceDocument surface)
{
if (string.Equals(surface.RecordKey, _advertisedSurface?.RecordRef, StringComparison.Ordinal))
PublishBaseSurface(surface.Document);
else
PublishEmbeddedSurface(surface.RecordKey, surface.Document);
}
else if (document is EveInputCapabilityDocument inputCapability)
CurrentInputCapability = inputCapability;
else if (document is EveAssetCatalogDocument assetCatalog)
@ -392,6 +400,8 @@ namespace GameCult.Eve.UnityScene
_assetBodyMappings = null;
_networkRegistry = null;
_replicaShard = null;
_baseSurface = null;
_embeddedSurfaces.Clear();
_pendingCommandIds.Clear();
_publishedReceiptIds.Clear();
}
@ -723,11 +733,29 @@ namespace GameCult.Eve.UnityScene
.GetResult()
.FirstOrDefault()
?? throw new InvalidOperationException("The advertised Eve surface record was not available.");
PublishSurface(surface);
PublishBaseSurface(surface);
}
private void PublishSurface(EveSurfaceDocument surface)
private void PublishBaseSurface(EveSurfaceDocument surface)
{
_baseSurface = surface ?? throw new ArgumentNullException(nameof(surface));
PublishComposedSurface();
EnsureLiveSubscriptions();
}
private void PublishEmbeddedSurface(string recordKey, EveSurfaceDocument surface)
{
if (string.IsNullOrWhiteSpace(recordKey))
throw new InvalidOperationException("An embedded Eve surface update has no record identity.");
_embeddedSurfaces[recordKey] = surface ?? throw new ArgumentNullException(nameof(surface));
PublishComposedSurface();
}
private void PublishComposedSurface()
{
var surface = ComposeSurface(
_baseSurface ?? throw new InvalidOperationException("The base Eve surface is unavailable."),
_embeddedSurfaces);
var interaction = RequireWorldInteraction();
CurrentSurfaceDocument = new EveUnitySceneProviderSurfaceDocument(
surface,
@ -742,7 +770,50 @@ namespace GameCult.Eve.UnityScene
_advertisedSurface.RecordRef,
surface.Version);
SurfaceDocumentAvailable?.Invoke(CurrentSurfaceDocument);
EnsureLiveSubscriptions();
}
internal static EveSurfaceDocument ComposeSurface(
EveSurfaceDocument surface,
IReadOnlyDictionary<string, EveSurfaceDocument> embeddedSurfaces)
{
var root = ComposeComponent(surface.Surface.Root, embeddedSurfaces);
var version = embeddedSurfaces.Values.Select(value => value.Version)
.DefaultIfEmpty(surface.Version)
.Append(surface.Version)
.Max();
return new EveSurfaceDocument(
surface.Type,
surface.Schema,
surface.ProviderId,
surface.ProviderKind,
surface.Title,
version,
surface.UpdatedAtUtc,
new EveSurfaceTree(surface.Surface.Id, root, surface.Surface.Styles),
surface.Commands);
}
private static EveSurfaceComponent ComposeComponent(
EveSurfaceComponent component,
IReadOnlyDictionary<string, EveSurfaceDocument> embeddedSurfaces)
{
var children = component.Children.Select(child => ComposeComponent(child, embeddedSurfaces)).ToList();
foreach (var slot in component.EmbeddedDocuments ?? Array.Empty<EveEmbeddedDocumentSlot>())
{
if (!string.Equals(slot.SchemaId, EveSurfaceDocument.SchemaId, StringComparison.Ordinal) ||
!embeddedSurfaces.TryGetValue(slot.DocumentId, out var embedded))
continue;
children.Add(ComposeComponent(embedded.Surface.Root, embeddedSurfaces));
}
return new EveSurfaceComponent(
component.Id,
component.Kind,
component.Props,
children,
component.StateBindingRecords,
component.EmbeddedDocuments,
component.Layout,
component.Style);
}
private void EnsureLiveSubscriptions()
@ -828,6 +899,25 @@ namespace GameCult.Eve.UnityScene
schemaIds: new[] { EveSurfaceDocument.SchemaId },
includeSnapshot: false)
.GetAwaiter().GetResult();
var embeddedSlots = EnumerateEmbeddedDocuments(CurrentSurfaceDocument.SurfaceDocument.Surface.Root)
.Where(slot => string.Equals(slot.SchemaId, EveSurfaceDocument.SchemaId, StringComparison.Ordinal))
.Where(slot => !string.IsNullOrWhiteSpace(slot.DocumentId))
.GroupBy(slot => slot.DocumentId, StringComparer.Ordinal)
.Select(group => group.First())
.ToArray();
for (var index = 0; index < embeddedSlots.Length; index++)
{
var slot = embeddedSlots[index];
var initialEmbedded = _subscriptions.SubscribeAsync(
$"eve-unity-embedded-surface-{index}",
recordKeys: new[] { slot.DocumentId },
schemaIds: new[] { EveSurfaceDocument.SchemaId })
.GetAwaiter().GetResult()
.OfType<EveSurfaceDocument>()
.FirstOrDefault();
if (initialEmbedded != null)
PublishEmbeddedSurface(slot.DocumentId, initialEmbedded);
}
var inputCapabilityRef = FindComponentProp(
CurrentSurfaceDocument.SurfaceDocument.Surface.Root,
"inputCapability");
@ -880,11 +970,34 @@ namespace GameCult.Eve.UnityScene
QueueEntityView(entityView);
else if (change.Document is CultMeshBodyPublicationDocument publication)
QueueBodyPublication(publication);
else if (change.Document is EveSurfaceDocument or EveInputCapabilityDocument or
else if (change.Document is EveSurfaceDocument surface)
_liveDocuments.Enqueue(new PendingSurfaceDocument(change.RecordKey, surface));
else if (change.Document is EveInputCapabilityDocument or
EveAssetCatalogDocument or EveCommandReceiptDocument or EveFieldsSplatsDocument)
_liveDocuments.Enqueue(change.Document);
}
private static IEnumerable<EveEmbeddedDocumentSlot> EnumerateEmbeddedDocuments(EveSurfaceComponent component)
{
foreach (var slot in component.EmbeddedDocuments ?? Array.Empty<EveEmbeddedDocumentSlot>())
yield return slot;
foreach (var child in component.Children ?? Array.Empty<EveSurfaceComponent>())
foreach (var slot in EnumerateEmbeddedDocuments(child))
yield return slot;
}
private sealed class PendingSurfaceDocument
{
public PendingSurfaceDocument(string recordKey, EveSurfaceDocument document)
{
RecordKey = recordKey ?? "";
Document = document ?? throw new ArgumentNullException(nameof(document));
}
public string RecordKey { get; }
public EveSurfaceDocument Document { get; }
}
private static void TraceHotState(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("AETHERIA_TRACE_CLIENT_RUDP"), "1", StringComparison.Ordinal))

View file

@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GameCult.Eve.Surface;
using NUnit.Framework;
namespace GameCult.Eve.UnityScene.Tests
{
public sealed class EveUnityEmbeddedSurfaceCompositionTests
{
[Test]
public void ComposesAdvertisedReactiveSurfaceWithoutReplacingBaseTopology()
{
const string record = "eve:surface:test.reactive";
var slot = new EveEmbeddedDocumentSlot(
"reactive",
record,
EveSurfaceDocument.SchemaId,
"reactive-state");
var world = Component("world", "world.scene3d");
var mount = new EveSurfaceComponent(
"reactive-mount",
"layer.reactive",
EmptyProps(),
Array.Empty<EveSurfaceComponent>(),
Array.Empty<GameCult.Mesh.CultMeshStateBindingDescriptor>(),
new[] { slot });
var @base = Surface(4, Component("root", "surface", world, mount));
var fragment = Surface(12, Component(
"fragment",
"layer.reactive",
Component("shot", "shot.receipt")));
var composed = EveUnityCultMeshLiveProviderTransport.ComposeSurface(
@base,
new Dictionary<string, EveSurfaceDocument>(StringComparer.Ordinal)
{
[record] = fragment
});
Assert.That(composed.Version, Is.EqualTo(12));
Assert.That(composed.Surface.Root.Children[0].Id, Is.EqualTo("world"));
Assert.That(composed.Surface.Root.Children[1].Children.Single().Id, Is.EqualTo("fragment"));
Assert.That(composed.Surface.Root.Children[1].Children.Single().Children.Single().Kind,
Is.EqualTo("shot.receipt"));
}
private static EveSurfaceDocument Surface(long version, EveSurfaceComponent root) =>
new EveSurfaceDocument(
"provider",
"test",
"Test",
version,
"2026-07-19T00:00:00Z",
new EveSurfaceTree("test", root, Array.Empty<EveStyleToken>()),
Array.Empty<EveCommandTemplate>());
private static EveSurfaceComponent Component(
string id,
string kind,
params EveSurfaceComponent[] children) =>
new EveSurfaceComponent(id, kind, EmptyProps(), children);
private static IReadOnlyDictionary<string, string> EmptyProps() =>
new Dictionary<string, string>(StringComparer.Ordinal);
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8e721e8052744db6ba925b8a3831de70

View file

@ -1,7 +1,7 @@
{
"name": "org.gamecult.eve.unity-scene",
"displayName": "Eve Unity Scene Lowering",
"version": "0.3.81",
"version": "0.3.82",
"unity": "6000.0",
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
"dependencies": {