Split Aetheria client transport planes

This commit is contained in:
Meta Vens 2026-07-19 19:53:08 +01:00
commit 028945aad6
9 changed files with 148 additions and 126 deletions

View file

@ -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");

View file

@ -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<object?>();
@ -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<AetheriaRuntimeDaemonTickResult> TickAsync(
@ -639,27 +627,23 @@ static async Task<AetheriaRuntimeDaemonTickResult> ImportRemoteCommittedFactsAsy
return import.Tick;
}
static RudpCultNetSchemaServer StartClientCultMeshHost(
static AetheriaClientCultMeshHost StartClientCultMeshHost(
AetheriaStateNode node,
AetheriaDaemonHostOptions options,
Func<AetheriaRuntimeDaemonFrameDocument?> latestFrame,
CultMeshNetworkBodyStore networkBodies)
Func<AetheriaRuntimeDaemonFrameDocument?> 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<CultMeshVerseCatalogRequestMessage>((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<CultNetSnapshotRequestMessage>(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<EveSurfaceDocument?> 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<EveSurfaceDocument>(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<string> PeerCultMeshEndpoints { get; init; } = Array.Empty<string>();
@ -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),

View file

@ -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<CultMeshBodyPublicationDocument>(soaPublication.Body.RecordKey)", StringComparison.Ordinal) ||
daemonHostProgram.IndexOf("MutableDocument<CultMeshBodyPublicationDocument>(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<EveEntitySoaViewDocument>(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<AetheriaRuntimeDaemonSoaViewDocument>(AetheriaRuntimeVerseRecordKeys.DaemonSoaViewLatest)",
"PublishDaemonApiDocumentsAsync(node, options, result",
"PublishHotEntityStateAsync(",
"node.MutableDocument<EveEntitySoaViewDocument>(AetheriaRuntimeVerseRecordKeys.EveEntitySoaViewLatest)",
"node.MutableDocument<AetheriaRuntimeDaemonProviderAdvertisementDocument>(AetheriaRuntimeVerseRecordKeys.DaemonProviderAdvertisement)",
"node.MutableDocument<AetheriaRuntimeDaemonHealthDocument>(AetheriaRuntimeVerseRecordKeys.DaemonHealth)",
"node.MutableDocument<AetheriaRuntimeDaemonCommandBoundaryDocument>(AetheriaRuntimeVerseRecordKeys.DaemonCommandBoundary)",
@ -12258,7 +12305,6 @@ static void RequireDaemonVersePublication(string root)
"node.MutableDocument<EveSurfaceDocument>(AetheriaRuntimeVerseRecordKeys.DaemonEditorTuiSurface)",
"node.MutableDocument<AetheriaRuntimeStarbridgeSessionSummaryDocument>(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.");
}
}

View file

@ -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"
}
}

View file

@ -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",

View file

@ -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",

View file

@ -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<byte> 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<string, int> _syntheticEntityIndices =
new Dictionary<string, int>(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<AetheriaRuntimeZoneSnapshotCommit>())
@ -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<CultMeshBodyDescriptor>();
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);

View file

@ -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",

View file

@ -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)"