Split Aetheria client transport planes
This commit is contained in:
parent
908e83b83a
commit
028945aad6
9 changed files with 148 additions and 126 deletions
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
|
|
|
|||
Loading…
Reference in a new issue