From 028945aad60c2be323b1e324b792e16f2ea11494 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 19 Jul 2026 19:53:08 +0100 Subject: [PATCH] Split Aetheria client transport planes --- Aetheria.State.Daemon.Smoke/Program.cs | 6 +- Aetheria.State.Daemon/Program.cs | 143 +++++++++--------- Aetheria.State.Verify/Program.cs | 72 +++++++-- Aetheria.Unity/Packages/manifest.json | 4 +- Aetheria.Unity/Packages/packages-lock.json | 10 +- Packages/manifest.json | 2 +- .../AetheriaRuntimeDaemonSoaFramePublisher.cs | 27 +--- Packages/packages-lock.json | 6 +- scripts/run-aetheria-unity.ps1 | 4 +- 9 files changed, 148 insertions(+), 126 deletions(-) diff --git a/Aetheria.State.Daemon.Smoke/Program.cs b/Aetheria.State.Daemon.Smoke/Program.cs index 9f5bf2a4a..e64a8e040 100644 --- a/Aetheria.State.Daemon.Smoke/Program.cs +++ b/Aetheria.State.Daemon.Smoke/Program.cs @@ -4455,8 +4455,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks RequireNear(100, Stat(target, "hull"), 0.000001, "trigger contact must not bypass the authored detonation delay"); - using var soaNetworkBodies = new CultMeshNetworkBodyStore(); - using var soaPublisher = new AetheriaRuntimeDaemonSoaFramePublisher(soaNetworkBodies, producerEpoch: 1); + using var soaPublisher = new AetheriaRuntimeDaemonSoaFramePublisher(producerEpoch: 1); using var soaFrame = soaPublisher.BuildCurrentZoneEntities( new AetheriaRuntimeDaemonFrameDocument { FrameId = 2, Run = run }); var mineIdentity = soaFrame.View.Identities.Single(identity => @@ -4641,8 +4640,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks node.Id == "aetheria.daemon.game.world.entity.0"); RequireEqual(rareHullAssetKey, rareSurfaceEntity.Props["assetRef"], "Eve scene projection must select the typed hull asset rather than the player Djinni fallback"); - using var rareSoaNetworkBodies = new CultMeshNetworkBodyStore(); - using var rareSoaPublisher = new AetheriaRuntimeDaemonSoaFramePublisher(rareSoaNetworkBodies, producerEpoch: 1); + using var rareSoaPublisher = new AetheriaRuntimeDaemonSoaFramePublisher(producerEpoch: 1); var rareSoa = rareSoaPublisher.BuildCurrentZoneEntities(rareFrame, catalog); RequireEqual(rareHullAssetKey, rareSoa.View.Identities.Single().AssetRef, "SoA identity and Eve scene projection must share the catalog-owned hull asset key"); diff --git a/Aetheria.State.Daemon/Program.cs b/Aetheria.State.Daemon/Program.cs index 4e8c5903d..ce71df4a5 100644 --- a/Aetheria.State.Daemon/Program.cs +++ b/Aetheria.State.Daemon/Program.cs @@ -16,8 +16,8 @@ using System.Collections.Concurrent; var options = AetheriaDaemonHostOptions.Parse(args); var startedAtUtc = DateTimeOffset.UtcNow.ToString("O"); using var worldPhysics = new AetheriaYmirWorldPhysics(); -var traceClientRudp = string.Equals( - Environment.GetEnvironmentVariable("AETHERIA_TRACE_CLIENT_RUDP"), +var traceClientTransport = string.Equals( + Environment.GetEnvironmentVariable("AETHERIA_TRACE_CLIENT_TRANSPORT"), "1", StringComparison.Ordinal); @@ -50,12 +50,11 @@ using var physicsPersistence = await AetheriaYmirPersistenceCoordinator.OpenAsyn node, worldPhysics, latestFrame).ConfigureAwait(false); -using var networkBodies = new CultMeshNetworkBodyStore(); -using var cultMeshRudpHost = StartClientCultMeshHost(node, options, () => latestFrame, networkBodies); -using var clientSubscriptions = new CultNetDatabaseSubscriptionServer(cultMeshRudpHost, node.Database); +using var cultMeshClientHost = StartClientCultMeshHost(node, options, () => latestFrame); +using var clientSubscriptions = new CultNetDatabaseSubscriptionServer(cultMeshClientHost.Control, node.Database); var managedViewportDemand = new AetheriaManagedViewportDemandState(); clientSubscriptions.DemandChanged += managedViewportDemand.Observe; -if (traceClientRudp) +if (traceClientTransport) clientSubscriptions.DemandChanged += demand => Console.WriteLine( $"CultMesh state demand consumer={demand.ConsumerRuntimeId} subscription={demand.SubscriptionId} " + $"active={demand.Active} sameMachine={demand.SameMachine} " + @@ -63,17 +62,14 @@ if (traceClientRudp) $"bodies=[{string.Join(",", demand.BodyIds)}] transports=[{string.Join(",", demand.SupportedBodyTransports)}]"); using var bodyDemand = new CultMeshBodyDemandTracker(clientSubscriptions); using var soaPublisher = new AetheriaRuntimeDaemonSoaFramePublisher( - networkBodies, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(), bodyDemand); -using var clientPumpCancellation = new CancellationTokenSource(); -var clientPump = RunClientCultMeshPumpAsync(cultMeshRudpHost, clientPumpCancellation.Token); var nextApiPublicationUtc = DateTimeOffset.UtcNow; var ingressState = new AetheriaDaemonIngressState(); var hotState = new AetheriaHotEntityPublicationState(); var reactiveSurfaceState = new AetheriaReactiveSurfacePublicationState(); var firstTick = await TickAsync(node, options, worldPhysics, latestFrame, ingressState, buildPublications: false).ConfigureAwait(false); -ThrowIfClientPumpFaulted(clientPump); +ThrowIfClientHostFaulted(cultMeshClientHost); latestFrame = firstTick.Frame; var firstPublication = PreparePublication( node.StatePath, @@ -91,13 +87,12 @@ await PublishPreparedDocumentsAsync( await PublishHotEntityStateAsync(node, soaPublisher, hotState, firstTick.Frame, ingressState.Catalog).ConfigureAwait(false); nextApiPublicationUtc = DateTimeOffset.UtcNow.Add(options.ApiPublicationInterval); Console.WriteLine($"Aetheria Verse daemon published frame {firstTick.Frame.FrameId}."); -Console.WriteLine($"Aetheria client CultMesh endpoint: rudp://{options.ClientCultMeshAdvertiseHost}:{cultMeshRudpHost.LocalEndPoint.Port}"); +Console.WriteLine($"Aetheria client CultMesh endpoint: {cultMeshClientHost.ControlEndpoint}"); +Console.WriteLine($"Aetheria client CDN endpoint: {cultMeshClientHost.ContentEndpoint}"); if (options.Once) { await PublishRuntimeSessionAsync(node, options, startedAtUtc, "completed").ConfigureAwait(false); - clientPumpCancellation.Cancel(); - await clientPump.ConfigureAwait(false); return; } var stopped = new TaskCompletionSource(); @@ -114,7 +109,7 @@ var nextTickUtc = DateTimeOffset.UtcNow.Add(options.TickInterval); Task publicationTask = Task.CompletedTask; while (!stopped.Task.IsCompleted) { - ThrowIfClientPumpFaulted(clientPump); + ThrowIfClientHostFaulted(cultMeshClientHost); var delay = nextTickUtc - DateTimeOffset.UtcNow; if (delay > TimeSpan.Zero) { @@ -125,7 +120,7 @@ while (!stopped.Task.IsCompleted) var buildPublications = DateTimeOffset.UtcNow >= nextApiPublicationUtc; var tick = await TickAsync(node, options, worldPhysics, latestFrame, ingressState, buildPublications: false).ConfigureAwait(false); - ThrowIfClientPumpFaulted(clientPump); + ThrowIfClientHostFaulted(cultMeshClientHost); latestFrame = tick.Frame; await PublishHotEntityStateAsync(node, soaPublisher, hotState, tick.Frame, ingressState.Catalog).ConfigureAwait(false); nextTickUtc += options.TickInterval; @@ -151,33 +146,26 @@ while (!stopped.Task.IsCompleted) await PublishDemandedManagedViewportsAsync(node, options, tick.Frame, managedViewportDemand).ConfigureAwait(false); nextApiPublicationUtc = DateTimeOffset.UtcNow.Add(options.ApiPublicationInterval); } - if (tick.Frame.FrameId % (traceClientRudp ? 10 : 120) == 0) + if (tick.Frame.FrameId % (traceClientTransport ? 10 : 120) == 0) { - var clientStats = cultMeshRudpHost.Stats; - var pendingReliable = string.Join(",", cultMeshRudpHost.Peers.Select( - peer => $"{peer.RemoteEndPoint}:{peer.PendingReliablePacketCount}")); Console.WriteLine( $"Aetheria Verse daemon published frame {tick.Frame.FrameId} at {tick.Frame.SimulationTimeSeconds:0.00}s; " + - $"client peers={cultMeshRudpHost.Peers.Count} pending=[{pendingReliable}] " + - $"rx={clientStats.BytesReceived} tx={clientStats.BytesSent}."); + $"control peers={cultMeshClientHost.Control.PeerCount} " + + $"cdn chunks={cultMeshClientHost.Content.ChunkRequestsServed}."); } } await PublishRuntimeSessionAsync(node, options, startedAtUtc, "stopping").ConfigureAwait(false); await publicationTask.ConfigureAwait(false); -clientPumpCancellation.Cancel(); -await clientPump.ConfigureAwait(false); Console.WriteLine("Aetheria Verse daemon stopping."); -static void ThrowIfClientPumpFaulted(Task clientPump) +static void ThrowIfClientHostFaulted(AetheriaClientCultMeshHost host) { - if (!clientPump.IsCompleted) + if (!host.Control.BackgroundFailure.IsCompleted) return; - - if (clientPump.IsFaulted) - throw new InvalidOperationException("Aetheria client CultMesh pump faulted.", clientPump.Exception); - - throw new InvalidOperationException("Aetheria client CultMesh pump stopped unexpectedly."); + throw new InvalidOperationException( + "Aetheria client CultMesh TCP control host faulted.", + host.Control.BackgroundFailure.GetAwaiter().GetResult()); } static async Task TickAsync( @@ -639,27 +627,23 @@ static async Task ImportRemoteCommittedFactsAsy return import.Tick; } -static RudpCultNetSchemaServer StartClientCultMeshHost( +static AetheriaClientCultMeshHost StartClientCultMeshHost( AetheriaStateNode node, AetheriaDaemonHostOptions options, - Func latestFrame, - CultMeshNetworkBodyStore networkBodies) + Func latestFrame) { var bundleCdnDocuments = BuildBundleCdnDocuments(node, options); - var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - socket.Bind(new IPEndPoint(ParseBindAddress(options.ClientCultMeshHost), options.ClientCultMeshPort)); - var server = new RudpCultNetSchemaServer(new RudpCultNetSchemaServerOptions - { - RuntimeId = $"{options.DaemonId}.client", - Socket = socket, - ConnectionId = 0x43554c54, - TransportId = "aetheria-client-rudp", - MaxFragmentBytes = 1024, - MaxPendingReliablePackets = 8192 - }); - _ = new CultMeshContentServer(server, node.Cache); - _ = new CultMeshBodyServer(server, networkBodies); - var advertisedEndpoint = $"rudp://{options.ClientCultMeshAdvertiseHost}:{((IPEndPoint)socket.LocalEndPoint!).Port}"; + var server = new TcpFramedCultNetSchemaServer(new TcpListener( + ParseBindAddress(options.ClientCultMeshHost), + options.ClientCultMeshPort)); + var content = new CultMeshTcpContentServer(new TcpListener( + ParseBindAddress(options.ClientCultMeshHost), + options.ClientCultMeshContentPort), node.Cache); + var advertisedEndpoint = $"cultnet+tcp://{options.ClientCultMeshAdvertiseHost}:{server.LocalEndPoint.Port}"; + var advertisedContentEndpoint = + $"{CultMeshTcpContentTransportConnector.Scheme}://{options.ClientCultMeshAdvertiseHost}:{content.LocalEndPoint.Port}"; + server.PeerFailed += (endpoint, error) => Console.Error.WriteLine( + $"Aetheria client CultMesh peer {endpoint} failed: {error.GetType().Name}: {error.Message}"); server.OnCultNet((request, peer) => { var descriptor = new CultMeshVerseDescriptor( @@ -669,7 +653,7 @@ static RudpCultNetSchemaServer StartClientCultMeshHost( new CultMeshVerseCompatibility( "cultmesh.v0", CultMeshVerseDescriptor.ComputeRulesHash("aetheria", "runtime-world.v1")), - discoveryEndpoints: new[] { advertisedEndpoint }, + discoveryEndpoints: new[] { advertisedEndpoint, advertisedContentEndpoint }, authorityRuntimeIds: new[] { options.DaemonId }, description: "Aetheria provider Verse"); peer.SendCultNet(new CultMeshVerseCatalogResponseMessage @@ -677,6 +661,7 @@ static RudpCultNetSchemaServer StartClientCultMeshHost( MessageId = string.IsNullOrWhiteSpace(request.MessageId) ? Guid.NewGuid().ToString("N") : request.MessageId, Verses = new[] { descriptor.ToMessage() } }); + return Task.CompletedTask; }); server.OnCultNet(async (request, peer) => { @@ -1104,7 +1089,11 @@ static RudpCultNetSchemaServer StartClientCultMeshHost( await node.Database.ApplyPutAsync(message).ConfigureAwait(false); }); - return server; + return new AetheriaClientCultMeshHost( + server, + content, + advertisedEndpoint, + advertisedContentEndpoint); } static void InjectCultMeshCdnManifestSnapshots( @@ -1286,25 +1275,6 @@ static async Task ReadPortableSurfaceAsync(AetheriaStateNod return providerSurface == null ? null : AetheriaRuntimeSurfaceDocuments.ToEveSurfaceDocument(providerSurface); } -static async Task RunClientCultMeshPumpAsync( - RudpCultNetSchemaServer server, - CancellationToken cancellationToken) -{ - try - { - while (!cancellationToken.IsCancellationRequested) - { - var poll = await server.PollAvailableAsync(256).ConfigureAwait(false); - server.PollResends(); - if (poll.TransportItemsConsumed == 0) - await Task.Delay(1, cancellationToken).ConfigureAwait(false); - } - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - } -} - static IPAddress ParseBindAddress(string host) { if (string.IsNullOrWhiteSpace(host) || @@ -1603,11 +1573,9 @@ static async Task PublishHotEntityStateAsync( var publication = await publisher.PublishAsync(hotFrame).ConfigureAwait(false); var layoutChanged = !state.Matches(publication.View); - var publishesRemoteGeneration = publication.Body.Representations.Any( - value => value.TransportKind == CultMeshBodyTransportKind.Network); - if (layoutChanged || publishesRemoteGeneration) + if (layoutChanged) { - if (layoutChanged && string.Equals(Environment.GetEnvironmentVariable("AETHERIA_TRACE_CLIENT_RUDP"), "1", StringComparison.Ordinal)) + if (layoutChanged && string.Equals(Environment.GetEnvironmentVariable("AETHERIA_TRACE_CLIENT_TRANSPORT"), "1", StringComparison.Ordinal)) Console.WriteLine( $"CultMesh entity layout changed sequence={publication.View.Sequence} bytes={MessagePackSerializer.Serialize(publication.View).Length} " + $"identities={publication.View.Identities.Length} representations=[{string.Join(",", publication.Body.Representations.Select(value => value.TransportKind))}]"); @@ -1702,7 +1670,7 @@ static async Task PublishDaemonApiDocumentsAsync( activeMainMenuSurfaceId, inputCatalog); var portableGameSurface = AetheriaRuntimeSurfaceDocuments.ToPortableSurface(gameSurface); - if (string.Equals(Environment.GetEnvironmentVariable("AETHERIA_TRACE_CLIENT_RUDP"), "1", StringComparison.Ordinal)) + if (string.Equals(Environment.GetEnvironmentVariable("AETHERIA_TRACE_CLIENT_TRANSPORT"), "1", StringComparison.Ordinal)) Console.WriteLine($"Eve game topology bytes={MessagePackSerializer.Serialize(portableGameSurface).Length}"); await node.MutableDocument(AetheriaRuntimeVerseRecordKeys.DaemonGameSurface) .ReplaceAsync(portableGameSurface) @@ -3387,6 +3355,32 @@ internal sealed class AetheriaDaemonIngressState } } +internal sealed class AetheriaClientCultMeshHost : IDisposable +{ + public AetheriaClientCultMeshHost( + TcpFramedCultNetSchemaServer control, + CultMeshTcpContentServer content, + string controlEndpoint, + string contentEndpoint) + { + Control = control ?? throw new ArgumentNullException(nameof(control)); + Content = content ?? throw new ArgumentNullException(nameof(content)); + ControlEndpoint = controlEndpoint ?? throw new ArgumentNullException(nameof(controlEndpoint)); + ContentEndpoint = contentEndpoint ?? throw new ArgumentNullException(nameof(contentEndpoint)); + } + + public TcpFramedCultNetSchemaServer Control { get; } + public CultMeshTcpContentServer Content { get; } + public string ControlEndpoint { get; } + public string ContentEndpoint { get; } + + public void Dispose() + { + Content.Dispose(); + Control.Dispose(); + } +} + internal sealed class AetheriaDaemonHostOptions { public string StatePath { get; init; } = ""; @@ -3397,6 +3391,7 @@ internal sealed class AetheriaDaemonHostOptions public string ClientCultMeshHost { get; init; } = "127.0.0.1"; public string ClientCultMeshAdvertiseHost { get; init; } = "127.0.0.1"; public int ClientCultMeshPort { get; init; } = 3076; + public int ClientCultMeshContentPort { get; init; } public string AetheriaResourcesRoot { get; init; } = ""; public string AssetBundleRoot { get; init; } = ""; public IReadOnlyList PeerCultMeshEndpoints { get; init; } = Array.Empty(); @@ -3429,6 +3424,7 @@ internal sealed class AetheriaDaemonHostOptions var clientCultMeshHost = ReadOption(args, "--client-cultmesh-host"); var clientCultMeshAdvertiseHost = ReadOption(args, "--client-cultmesh-advertise-host"); var clientCultMeshPort = ReadNonNegativeInt(args, "--client-cultmesh-port") ?? 3076; + var clientCultMeshContentPort = ReadNonNegativeInt(args, "--client-cultmesh-content-port") ?? 0; var aetheriaResourcesRoot = ReadOption(args, "--aetheria-resources-root"); var assetBundleRoot = ReadOption(args, "--asset-bundle-root"); RejectRemovedOption(args, "--rts-cultmesh-port", "--client-cultmesh-port"); @@ -3459,6 +3455,7 @@ internal sealed class AetheriaDaemonHostOptions ? (string.IsNullOrWhiteSpace(clientCultMeshHost) || clientCultMeshHost == "0.0.0.0" || clientCultMeshHost == "*" ? "127.0.0.1" : clientCultMeshHost) : clientCultMeshAdvertiseHost, ClientCultMeshPort = clientCultMeshPort, + ClientCultMeshContentPort = clientCultMeshContentPort, AetheriaResourcesRoot = string.IsNullOrWhiteSpace(aetheriaResourcesRoot) ? Path.GetFullPath(Path.Combine(root, "Assets", "Resources")) : Path.GetFullPath(aetheriaResourcesRoot), diff --git a/Aetheria.State.Verify/Program.cs b/Aetheria.State.Verify/Program.cs index 70eb36ddc..2d5e72f5c 100644 --- a/Aetheria.State.Verify/Program.cs +++ b/Aetheria.State.Verify/Program.cs @@ -19,6 +19,7 @@ File.Copy(statePath, verificationStatePath, overwrite: true); RequireGameplaySourcePurity(root); RequireManagedContentOnlyAssetDelivery(root); +RequireClientTransportPlaneOwnership(root); RequirePackageSerializerBoundary(root); RequireSharedEvePackagesImportedFromEveRepo(root); RequireSharedRuntimeSurfaceCommandsUseCultMeshTransport(root); @@ -853,10 +854,10 @@ static void RequireSharedEvePackagesImportedFromEveRepo(string root) } const string requiredAuthoringCultLib = - "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.16"; + "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.34"; if (!authoringManifest.Contains(requiredAuthoringCultLib, StringComparison.Ordinal) || !authoringPackageLock.Contains(requiredAuthoringCultLib, StringComparison.Ordinal) || - !authoringPackageLock.Contains("69caf2d027c49227e62ffa24452fc7d717f86ca5", StringComparison.Ordinal)) + !authoringPackageLock.Contains("c148d891c1d8713285ae15b2f17b59c106fb9426", StringComparison.Ordinal)) { throw new InvalidOperationException( "The fossil asset-authoring Unity project is not pinned to the released CultLib body required by the generic scene package."); @@ -1248,6 +1249,49 @@ static void RequireRuntimeCatalogKeyOnlyLookups(string root) } } +static void RequireClientTransportPlaneOwnership(string root) +{ + var daemonPath = Path.Combine(root, "Aetheria.State.Daemon", "Program.cs"); + var launcherPath = Path.Combine(root, "scripts", "run-aetheria-unity.ps1"); + var daemon = File.Exists(daemonPath) + ? File.ReadAllText(daemonPath) + : throw new InvalidOperationException("Cannot verify client transport ownership; daemon Program.cs is missing."); + var launcher = File.Exists(launcherPath) + ? File.ReadAllText(launcherPath) + : throw new InvalidOperationException("Cannot verify client transport ownership; Unity launcher is missing."); + var required = new[] + { + "new TcpFramedCultNetSchemaServer(", + "new CultMeshTcpContentServer(", + "CultMeshTcpContentTransportConnector.Scheme", + "discoveryEndpoints: new[] { advertisedEndpoint, advertisedContentEndpoint }", + "new CultNetDatabaseSubscriptionServer(cultMeshClientHost.Control, node.Database)", + "cultnet+tcp://127.0.0.1:$Port" + }; + var combined = daemon + "\n" + launcher; + var missing = required.Where(symbol => !combined.Contains(symbol, StringComparison.Ordinal)).ToArray(); + if (missing.Length > 0) + throw new InvalidOperationException( + "Aetheria client transport planes lost their TCP control/content ownership: " + string.Join(", ", missing)); + + var forbidden = new[] + { + "RudpCultNetSchemaServer", + "new CultMeshContentServer(", + "new CultMeshBodyServer(", + "RunClientCultMeshPumpAsync", + "AETHERIA_TRACE_CLIENT_RUDP", + "rudp://127.0.0.1:$Port" + }; + var present = forbidden.Where(symbol => combined.Contains(symbol, StringComparison.Ordinal)).ToArray(); + if (present.Length > 0) + throw new InvalidOperationException( + "Aetheria reintroduced RUDP or schema-carried bulk state into the client boundary: " + + string.Join(", ", present)); + + Console.WriteLine("Client transport ownership: TCP control and CDN are separate; RUDP and schema-carried bodies are absent"); +} + static void RequireDaemonPlayableRunGenerationAuthority(string root) { var daemonProgramPath = Path.Combine(root, "Aetheria.State.Daemon", "Program.cs"); @@ -11742,7 +11786,6 @@ static void RequireDaemonVersePublication(string root) "public sealed class AetheriaRuntimeDaemonSoaFramePublisher : IDisposable", "BuildCurrentZoneEntities(", "new CultMeshFrameBodyPublisher(", - "new CultMeshNetworkBodyPublisher(", "new CultMeshBodyPublicationHandle(BodyId, publication.ProducerEpoch, publication.Sequence)", ".Validate(publication)", "view.Buffers[0].BufferId, publication.BodyId", @@ -11768,7 +11811,9 @@ static void RequireDaemonVersePublication(string root) { "MemoryMappedFile", "RetainedMappedBuffer", - "CreateLocation(" + "CreateLocation(", + "CultMeshNetworkBodyStore", + "PublishNetwork" }; var forbiddenSoaPublisherHits = forbiddenSoaPublisherSymbols .Where(symbol => daemonSoaFramePublisher.Contains(symbol, StringComparison.Ordinal)) @@ -11797,8 +11842,10 @@ static void RequireDaemonVersePublication(string root) "Eve SoA layout projection still owns transport material or producer epoch: " + string.Join(", ", forbiddenProjectionHits)); } - if (!daemonHostProgram.Contains("MutableDocument(soaPublication.Body.RecordKey)", StringComparison.Ordinal) || - daemonHostProgram.IndexOf("MutableDocument(soaPublication.Body.RecordKey)", StringComparison.Ordinal) > + const string bodyPublicationWrite = + "CultMeshBodyPublicationDocument.CreateLatestRecordKey(publication.Body.BodyId)"; + if (!daemonHostProgram.Contains(bodyPublicationWrite, StringComparison.Ordinal) || + daemonHostProgram.IndexOf(bodyPublicationWrite, StringComparison.Ordinal) > daemonHostProgram.IndexOf("MutableDocument(AetheriaRuntimeVerseRecordKeys.EveEntitySoaViewLatest)", StringComparison.Ordinal)) { throw new InvalidOperationException("CultMesh body publication must precede the Eve entity SoA layout record."); @@ -12157,7 +12204,6 @@ static void RequireDaemonVersePublication(string root) "CultMeshAddress", "BuildPublications", "AetheriaRuntimeDaemonCommandBoundaryDocument.Create", - "soaPublisher.BuildCurrentZoneEntities(frame, catalog)", "AetheriaRuntimeDaemonProviderAdvertisementDocument.Create", "AetheriaRuntimeStarbridgeDocuments.SessionSummary(", "StarbridgeScenario", @@ -12246,8 +12292,9 @@ static void RequireDaemonVersePublication(string root) "StarbridgeScenario = starbridgeScenario", "StarbridgeSession = starbridgeSession", "BuildPublications = buildPublications", - "PublishDaemonApiDocumentsAsync(node, options, soaPublisher, result", - "node.MutableDocument(AetheriaRuntimeVerseRecordKeys.DaemonSoaViewLatest)", + "PublishDaemonApiDocumentsAsync(node, options, result", + "PublishHotEntityStateAsync(", + "node.MutableDocument(AetheriaRuntimeVerseRecordKeys.EveEntitySoaViewLatest)", "node.MutableDocument(AetheriaRuntimeVerseRecordKeys.DaemonProviderAdvertisement)", "node.MutableDocument(AetheriaRuntimeVerseRecordKeys.DaemonHealth)", "node.MutableDocument(AetheriaRuntimeVerseRecordKeys.DaemonCommandBoundary)", @@ -12258,7 +12305,6 @@ static void RequireDaemonVersePublication(string root) "node.MutableDocument(AetheriaRuntimeVerseRecordKeys.DaemonEditorTuiSurface)", "node.MutableDocument(AetheriaRuntimeVerseRecordKeys.StarbridgeSessionSummary)", ".ReplaceAsync(result.Frame)", - ".ReplaceAsync(result.SoaView)", ".ReplaceAsync(result.Health)", ".ReplaceAsync(result.AssetManifest)", ".ReplaceAsync(result.StarbridgeSessionSummary)", @@ -12394,7 +12440,7 @@ static void RequireDaemonVersePublication(string root) } if (!daemonTickBlock.Contains("if (buildPublications)", StringComparison.Ordinal) || - !daemonTickBlock.Contains("PublishDaemonApiDocumentsAsync(node, options, soaPublisher, result", StringComparison.Ordinal)) + !daemonTickBlock.Contains("PublishDaemonApiDocumentsAsync(node, options, result", StringComparison.Ordinal)) { throw new InvalidOperationException( "Daemon ticks must publish API cadence documents through managed AetheriaStateNode pointers."); @@ -21176,9 +21222,9 @@ static void RequireManagedContentOnlyAssetDelivery(string root) } if (!daemon.Contains("InjectCultMeshCdnManifestSnapshots", StringComparison.Ordinal) || - !daemon.Contains("CultMeshContentServer", StringComparison.Ordinal)) + !daemon.Contains("new CultMeshTcpContentServer(", StringComparison.Ordinal)) { throw new InvalidOperationException( - "Daemon asset delivery must publish snapshot manifests and host managed CultMesh content sessions."); + "Daemon asset delivery must publish snapshot manifests and host the dedicated TCP content plane."); } } diff --git a/Aetheria.Unity/Packages/manifest.json b/Aetheria.Unity/Packages/manifest.json index 023a3845e..79de95757 100644 --- a/Aetheria.Unity/Packages/manifest.json +++ b/Aetheria.Unity/Packages/manifest.json @@ -7,9 +7,9 @@ "com.unity.modules.physics": "1.0.0", "com.unity.modules.uielements": "1.0.0", "com.unity.render-pipelines.universal": "17.4.0", - "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.32", + "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.34", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.4", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.91" + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.94" } } diff --git a/Aetheria.Unity/Packages/packages-lock.json b/Aetheria.Unity/Packages/packages-lock.json index f1e783cb5..f60868577 100644 --- a/Aetheria.Unity/Packages/packages-lock.json +++ b/Aetheria.Unity/Packages/packages-lock.json @@ -127,11 +127,11 @@ } }, "org.gamecult.cultlib": { - "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.32", + "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.34", "depth": 0, "source": "git", "dependencies": {}, - "hash": "d018bb1fb5a8ce73da6f5579e2f475750d901d8b" + "hash": "c148d891c1d8713285ae15b2f17b59c106fb9426" }, "org.gamecult.eve.plugin-fields": { "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", @@ -152,18 +152,18 @@ "hash": "e08fa08335f99e9edddeb706912eecfad07cb281" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.91", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.94", "depth": 0, "source": "git", "dependencies": { - "org.gamecult.cultlib": "1.0.32", + "org.gamecult.cultlib": "1.0.34", "org.gamecult.eve.plugin-fields": "0.2.3", "org.gamecult.eve.surface": "0.2.4", "com.unity.mathematics": "1.3.2", "com.unity.render-pipelines.universal": "17.4.0", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "c93f09342f4c14e0607a47038df6b8e2e99cbf03" + "hash": "0463ddff4e39c9cca14e632b7e46b6b0081c67cc" }, "com.unity.modules.assetbundle": { "version": "1.0.0", diff --git a/Packages/manifest.json b/Packages/manifest.json index 5c8984fdf..40b44fa76 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -21,7 +21,7 @@ "org.gamecult.aetheria.eve-runtime": "file:org.gamecult.aetheria.eve-runtime", "org.gamecult.aetheria.state": "file:org.gamecult.aetheria.state", "org.gamecult.cultmath": "file:../../CultMath", - "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.23", + "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.34", "org.gamecult.eve.surface": "file:../../EveUnity/packages/org.gamecult.eve.surface", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", "org.gamecult.eve.unity-scene": "file:../../EveUnity/packages/org.gamecult.eve.unity-scene", diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaFramePublisher.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaFramePublisher.cs index 714042316..226323b74 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaFramePublisher.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeDaemonSoaFramePublisher.cs @@ -17,22 +17,15 @@ namespace GameCult.Aetheria.State.Verse public AetheriaRuntimeDaemonSoaFrame( AetheriaRuntimeDaemonSoaViewDocument view, CultMeshFrameBodyWriteLease write, - int byteLength, - bool publishSharedMemory, - bool publishNetwork) + int byteLength) { View = view ?? throw new ArgumentNullException(nameof(view)); _write = write ?? throw new ArgumentNullException(nameof(write)); ByteLength = byteLength; - PublishSharedMemory = publishSharedMemory; - PublishNetwork = publishNetwork; } public AetheriaRuntimeDaemonSoaViewDocument View { get; } public int ByteLength { get; } - public bool PublishSharedMemory { get; } - public bool PublishNetwork { get; } - public ReadOnlySpan Span => (_write ?? throw new ObjectDisposedException(nameof(AetheriaRuntimeDaemonSoaFrame))).Span[..ByteLength]; public CultMeshBodyDescriptor Commit(DateTimeOffset nowUtc) { @@ -81,18 +74,15 @@ namespace GameCult.Aetheria.State.Verse private const string EntityProxyMaterialUri = "cultmesh://aetheria/assets/daemon/entity_proxy/material"; private readonly CultMeshFrameBodyPublisher _localPublisher; - private readonly CultMeshNetworkBodyStore _networkBodies; private readonly CultMeshBodyDemandTracker? _demand; private readonly Dictionary _syntheticEntityIndices = new Dictionary(StringComparer.Ordinal); private int _nextSyntheticEntityIndex = -2; public AetheriaRuntimeDaemonSoaFramePublisher( - CultMeshNetworkBodyStore networkBodies, long producerEpoch, CultMeshBodyDemandTracker? demand = null) { - _networkBodies = networkBodies ?? throw new ArgumentNullException(nameof(networkBodies)); _localPublisher = new CultMeshFrameBodyPublisher( BodyId, BodySchemaId, LayoutVersion, Capacity, producerEpoch, checked((int)EntityHotSlabLayout.Create(Capacity).TotalByteLength), @@ -107,10 +97,8 @@ namespace GameCult.Aetheria.State.Verse if (frame == null) throw new ArgumentNullException(nameof(frame)); var demand = _demand?.Plan(BodyId); - if (demand != null && !demand.HasConsumers) + if (demand != null && (!demand.HasConsumers || !demand.RequiresSharedMemory)) return null; - var publishSharedMemory = demand?.RequiresSharedMemory ?? true; - var publishNetwork = demand?.RequiresNetwork ?? true; var run = frame.Run ?? new AetheriaRuntimeRunCheckpointCommit(); var zone = (run.Zones ?? Array.Empty()) @@ -284,9 +272,7 @@ namespace GameCult.Aetheria.State.Verse return new AetheriaRuntimeDaemonSoaFrame( view, write, - checked((int)layout.TotalByteLength), - publishSharedMemory, - publishNetwork); + checked((int)layout.TotalByteLength)); } catch { @@ -299,7 +285,6 @@ namespace GameCult.Aetheria.State.Verse { if (frame == null) throw new ArgumentNullException(nameof(frame)); var now = DateTimeOffset.UtcNow; - var networkBytes = frame.PublishNetwork ? frame.Span.ToArray() : null; var local = frame.Commit(now); var generation = new CultMeshBodyGeneration { @@ -313,10 +298,6 @@ namespace GameCult.Aetheria.State.Verse Synchronization = local.Synchronization, LeaseExpiresAtUnixMs = local.LeaseExpiresAtUnixMs }; - var representations = new List(); - if (frame.PublishSharedMemory) representations.Add(local); - if (frame.PublishNetwork) - representations.Add(_networkBodies.PublishOwned(generation, networkBytes!)); var publication = new CultMeshBodyPublicationDocument { BodyId = BodyId, @@ -329,7 +310,7 @@ namespace GameCult.Aetheria.State.Verse Sequence = local.Sequence, Synchronization = local.Synchronization, LivenessExpiresAtUnixMs = local.LeaseExpiresAtUnixMs, - Representations = representations.ToArray() + Representations = new[] { local } }; new CultMeshBodyPublicationHandle(BodyId, publication.ProducerEpoch, publication.Sequence) .Validate(publication); diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 2285dbd12..337ed607f 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -313,11 +313,11 @@ } }, "org.gamecult.cultlib": { - "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.23", + "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.34", "depth": 0, "source": "git", "dependencies": {}, - "hash": "743fa3889ebef752178ce9982dc7c9c031cc22e9" + "hash": "c148d891c1d8713285ae15b2f17b59c106fb9426" }, "org.gamecult.cultmath": { "version": "file:../../CultMath", @@ -347,7 +347,7 @@ "depth": 0, "source": "local", "dependencies": { - "org.gamecult.cultlib": "1.0.23", + "org.gamecult.cultlib": "1.0.34", "org.gamecult.eve.plugin-fields": "0.2.3", "org.gamecult.eve.surface": "0.2.4", "com.unity.mathematics": "1.3.2", diff --git a/scripts/run-aetheria-unity.ps1 b/scripts/run-aetheria-unity.ps1 index 8daa0f2b5..f4be04d09 100644 --- a/scripts/run-aetheria-unity.ps1 +++ b/scripts/run-aetheria-unity.ps1 @@ -65,11 +65,11 @@ try { $ready = $false for ($i = 0; $i -lt 60; $i++) { if ($daemon.HasExited) { throw "Aetheria daemon exited. See $daemonLog" } - if ((Test-Path $daemonLog) -and (Select-String $daemonLog -Pattern "Aetheria client CultMesh endpoint: rudp://127.0.0.1:$Port" -Quiet)) { $ready = $true; break } + if ((Test-Path $daemonLog) -and (Select-String $daemonLog -Pattern "Aetheria client CultMesh endpoint: cultnet\+tcp://127.0.0.1:$Port" -Quiet)) { $ready = $true; break } Start-Sleep -Milliseconds 500 } if (-not $ready) { throw "Aetheria daemon did not publish its Eve endpoint." } - $env:EVEUNITY_RENDEZVOUS_ENDPOINT = "rudp://127.0.0.1:$Port" + $env:EVEUNITY_RENDEZVOUS_ENDPOINT = "cultnet+tcp://127.0.0.1:$Port" $env:EVEUNITY_SURFACE_ID = "aetheria.pilot" $client = Start-Process $clientExe -ArgumentList "-force-d3d11" -PassThru Write-Host "Client PID: $($client.Id)"