Partition provider assets for demand loading
This commit is contained in:
parent
028945aad6
commit
72ce713a19
10 changed files with 497 additions and 141 deletions
|
|
@ -15,6 +15,20 @@ using System.Collections.Concurrent;
|
|||
|
||||
var options = AetheriaDaemonHostOptions.Parse(args);
|
||||
var startedAtUtc = DateTimeOffset.UtcNow.ToString("O");
|
||||
var startup = Stopwatch.StartNew();
|
||||
var startupPhase = Stopwatch.StartNew();
|
||||
var traceStartupPhases = string.Equals(
|
||||
Environment.GetEnvironmentVariable("AETHERIA_TRACE_STARTUP_PHASES"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
void TraceStartup(string name)
|
||||
{
|
||||
if (traceStartupPhases)
|
||||
Console.WriteLine(
|
||||
$"Aetheria startup phase {name} took {startupPhase.Elapsed.TotalMilliseconds:0.###}ms " +
|
||||
$"(total {startup.Elapsed.TotalMilliseconds:0.###}ms).");
|
||||
startupPhase.Restart();
|
||||
}
|
||||
using var worldPhysics = new AetheriaYmirWorldPhysics();
|
||||
var traceClientTransport = string.Equals(
|
||||
Environment.GetEnvironmentVariable("AETHERIA_TRACE_CLIENT_TRANSPORT"),
|
||||
|
|
@ -32,25 +46,17 @@ await using var node = await AetheriaStateNode.OpenAsync(
|
|||
runtimeId: options.DaemonId,
|
||||
startServer: true,
|
||||
enableDurableShardLogs: false).ConfigureAwait(false);
|
||||
TraceStartup("open-state-node");
|
||||
using var discoveryHost = new AetheriaVerseDiscoveryHost(node);
|
||||
|
||||
await EnsureWorldDocumentAsync(node).ConfigureAwait(false);
|
||||
await EnsureTradeValuePolicyAsync(node, startedAtUtc).ConfigureAwait(false);
|
||||
await AetheriaDaemonNativeCatalog.EnsureAsync(node).ConfigureAwait(false);
|
||||
await node.FlushAsync().ConfigureAwait(false);
|
||||
await EnsurePlayableRunDocumentsAsync(node, options, startedAtUtc).ConfigureAwait(false);
|
||||
await EnsureGameSessionAsync(node, options, startedAtUtc).ConfigureAwait(false);
|
||||
var verseHost = await EnsureVerseHostSettingsAsync(node, options, startedAtUtc).ConfigureAwait(false);
|
||||
await EnsureVerseAuthorityPolicyAsync(node, options).ConfigureAwait(false);
|
||||
discoveryHost.Update(verseHost);
|
||||
await PublishRuntimeSessionAsync(node, options, startedAtUtc, "starting").ConfigureAwait(false);
|
||||
await PublishStateSurfacesAsync(node, options, startedAtUtc).ConfigureAwait(false);
|
||||
var latestFrame = await node.MutableDocument<AetheriaRuntimeDaemonFrameDocument>(AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest).ReadAsync().ConfigureAwait(false);
|
||||
using var physicsPersistence = await AetheriaYmirPersistenceCoordinator.OpenAsync(
|
||||
node,
|
||||
worldPhysics,
|
||||
latestFrame).ConfigureAwait(false);
|
||||
using var cultMeshClientHost = StartClientCultMeshHost(node, options, () => latestFrame);
|
||||
TraceStartup("latest-frame");
|
||||
var unityBundles = BuildUnityBundleArtifactSet(options);
|
||||
TraceStartup("provider-asset-bundles");
|
||||
using var cultMeshClientHost = StartClientCultMeshHost(node, options, unityBundles, () => latestFrame);
|
||||
TraceStartup("client-host");
|
||||
Console.WriteLine($"Aetheria client CultMesh endpoint: {cultMeshClientHost.ControlEndpoint}");
|
||||
Console.WriteLine($"Aetheria client CDN endpoint: {cultMeshClientHost.ContentEndpoint}");
|
||||
Console.WriteLine($"Aetheria daemon transport-ready in {startup.Elapsed.TotalMilliseconds:0.###}ms.");
|
||||
using var clientSubscriptions = new CultNetDatabaseSubscriptionServer(cultMeshClientHost.Control, node.Database);
|
||||
var managedViewportDemand = new AetheriaManagedViewportDemandState();
|
||||
clientSubscriptions.DemandChanged += managedViewportDemand.Observe;
|
||||
|
|
@ -64,11 +70,39 @@ using var bodyDemand = new CultMeshBodyDemandTracker(clientSubscriptions);
|
|||
using var soaPublisher = new AetheriaRuntimeDaemonSoaFramePublisher(
|
||||
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
||||
bodyDemand);
|
||||
|
||||
await EnsureWorldDocumentAsync(node).ConfigureAwait(false);
|
||||
TraceStartup("world-document");
|
||||
await EnsureTradeValuePolicyAsync(node, startedAtUtc).ConfigureAwait(false);
|
||||
TraceStartup("trade-policy");
|
||||
await AetheriaDaemonNativeCatalog.EnsureAsync(node).ConfigureAwait(false);
|
||||
TraceStartup("native-catalog");
|
||||
await node.FlushAsync().ConfigureAwait(false);
|
||||
TraceStartup("initial-flush");
|
||||
await EnsurePlayableRunDocumentsAsync(node, options, startedAtUtc).ConfigureAwait(false);
|
||||
TraceStartup("playable-run");
|
||||
await EnsureGameSessionAsync(node, options, startedAtUtc).ConfigureAwait(false);
|
||||
TraceStartup("game-session");
|
||||
var verseHost = await EnsureVerseHostSettingsAsync(node, options, startedAtUtc).ConfigureAwait(false);
|
||||
TraceStartup("verse-host");
|
||||
await EnsureVerseAuthorityPolicyAsync(node, options).ConfigureAwait(false);
|
||||
TraceStartup("authority-policy");
|
||||
discoveryHost.Update(verseHost);
|
||||
await PublishRuntimeSessionAsync(node, options, startedAtUtc, "starting").ConfigureAwait(false);
|
||||
TraceStartup("runtime-session");
|
||||
await PublishStateSurfacesAsync(node, options, startedAtUtc).ConfigureAwait(false);
|
||||
TraceStartup("state-surfaces");
|
||||
using var physicsPersistence = await AetheriaYmirPersistenceCoordinator.OpenAsync(
|
||||
node,
|
||||
worldPhysics,
|
||||
latestFrame).ConfigureAwait(false);
|
||||
TraceStartup("ymir-persistence");
|
||||
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);
|
||||
var firstTick = await TickAsync(node, options, unityBundles, worldPhysics, latestFrame, ingressState, buildPublications: false).ConfigureAwait(false);
|
||||
TraceStartup("first-tick");
|
||||
ThrowIfClientHostFaulted(cultMeshClientHost);
|
||||
latestFrame = firstTick.Frame;
|
||||
var firstPublication = PreparePublication(
|
||||
|
|
@ -81,14 +115,16 @@ await PublishPreparedDocumentsAsync(
|
|||
node,
|
||||
options,
|
||||
physicsPersistence,
|
||||
unityBundles,
|
||||
firstPublication,
|
||||
reactiveSurfaceState,
|
||||
publishTopology: true).ConfigureAwait(false);
|
||||
TraceStartup("first-publication");
|
||||
await PublishHotEntityStateAsync(node, soaPublisher, hotState, firstTick.Frame, ingressState.Catalog).ConfigureAwait(false);
|
||||
TraceStartup("hot-entity-state");
|
||||
nextApiPublicationUtc = DateTimeOffset.UtcNow.Add(options.ApiPublicationInterval);
|
||||
Console.WriteLine($"Aetheria Verse daemon published frame {firstTick.Frame.FrameId}.");
|
||||
Console.WriteLine($"Aetheria client CultMesh endpoint: {cultMeshClientHost.ControlEndpoint}");
|
||||
Console.WriteLine($"Aetheria client CDN endpoint: {cultMeshClientHost.ContentEndpoint}");
|
||||
Console.WriteLine($"Aetheria daemon playable-world-ready in {startup.Elapsed.TotalMilliseconds:0.###}ms.");
|
||||
|
||||
if (options.Once)
|
||||
{
|
||||
|
|
@ -119,7 +155,7 @@ while (!stopped.Task.IsCompleted)
|
|||
}
|
||||
|
||||
var buildPublications = DateTimeOffset.UtcNow >= nextApiPublicationUtc;
|
||||
var tick = await TickAsync(node, options, worldPhysics, latestFrame, ingressState, buildPublications: false).ConfigureAwait(false);
|
||||
var tick = await TickAsync(node, options, unityBundles, worldPhysics, latestFrame, ingressState, buildPublications: false).ConfigureAwait(false);
|
||||
ThrowIfClientHostFaulted(cultMeshClientHost);
|
||||
latestFrame = tick.Frame;
|
||||
await PublishHotEntityStateAsync(node, soaPublisher, hotState, tick.Frame, ingressState.Catalog).ConfigureAwait(false);
|
||||
|
|
@ -140,6 +176,7 @@ while (!stopped.Task.IsCompleted)
|
|||
node,
|
||||
options,
|
||||
physicsPersistence,
|
||||
unityBundles,
|
||||
publication,
|
||||
reactiveSurfaceState,
|
||||
publishTopology: false);
|
||||
|
|
@ -171,6 +208,7 @@ static void ThrowIfClientHostFaulted(AetheriaClientCultMeshHost host)
|
|||
static async Task<AetheriaRuntimeDaemonTickResult> TickAsync(
|
||||
AetheriaStateNode node,
|
||||
AetheriaDaemonHostOptions options,
|
||||
AetheriaUnityBundleArtifactSet unityBundles,
|
||||
IAetheriaRuntimeWorldPhysics worldPhysics,
|
||||
AetheriaRuntimeDaemonFrameDocument? currentFrame,
|
||||
AetheriaDaemonIngressState ingressState,
|
||||
|
|
@ -344,7 +382,7 @@ static async Task<AetheriaRuntimeDaemonTickResult> TickAsync(
|
|||
|
||||
if (buildPublications)
|
||||
{
|
||||
await PublishDaemonApiDocumentsAsync(node, options, result, publishTopology: true).ConfigureAwait(false);
|
||||
await PublishDaemonApiDocumentsAsync(node, options, unityBundles, result, publishTopology: true).ConfigureAwait(false);
|
||||
TracePhase("api-publication");
|
||||
await PublishStateSurfacesAsync(node, options, result.Frame.PublishedAtUtc).ConfigureAwait(false);
|
||||
TracePhase("state-surfaces");
|
||||
|
|
@ -393,6 +431,7 @@ static async Task PublishPreparedDocumentsAsync(
|
|||
AetheriaStateNode node,
|
||||
AetheriaDaemonHostOptions options,
|
||||
AetheriaYmirPersistenceCoordinator physicsPersistence,
|
||||
AetheriaUnityBundleArtifactSet unityBundles,
|
||||
AetheriaPreparedPublication prepared,
|
||||
AetheriaReactiveSurfacePublicationState reactiveSurfaceState,
|
||||
bool publishTopology)
|
||||
|
|
@ -401,6 +440,7 @@ static async Task PublishPreparedDocumentsAsync(
|
|||
await PublishDaemonApiDocumentsAsync(
|
||||
node,
|
||||
options,
|
||||
unityBundles,
|
||||
prepared.Publication,
|
||||
publishTopology,
|
||||
reactiveSurfaceState).ConfigureAwait(false);
|
||||
|
|
@ -630,15 +670,19 @@ static async Task<AetheriaRuntimeDaemonTickResult> ImportRemoteCommittedFactsAsy
|
|||
static AetheriaClientCultMeshHost StartClientCultMeshHost(
|
||||
AetheriaStateNode node,
|
||||
AetheriaDaemonHostOptions options,
|
||||
AetheriaUnityBundleArtifactSet unityBundles,
|
||||
Func<AetheriaRuntimeDaemonFrameDocument?> latestFrame)
|
||||
{
|
||||
var bundleCdnDocuments = BuildBundleCdnDocuments(node, options);
|
||||
var contentCache = new CultCache(CultMesh.CreateCultCacheDocumentRegistry(
|
||||
typeof(CultMeshCdnArtifactManifest),
|
||||
typeof(CultMeshCdnArtifactChunk)));
|
||||
var bundleCdnDocuments = BuildBundleCdnDocuments(node, contentCache, unityBundles, options.DaemonId);
|
||||
var server = new TcpFramedCultNetSchemaServer(new TcpListener(
|
||||
ParseBindAddress(options.ClientCultMeshHost),
|
||||
options.ClientCultMeshPort));
|
||||
var content = new CultMeshTcpContentServer(new TcpListener(
|
||||
ParseBindAddress(options.ClientCultMeshHost),
|
||||
options.ClientCultMeshContentPort), node.Cache);
|
||||
options.ClientCultMeshContentPort), contentCache);
|
||||
var advertisedEndpoint = $"cultnet+tcp://{options.ClientCultMeshAdvertiseHost}:{server.LocalEndPoint.Port}";
|
||||
var advertisedContentEndpoint =
|
||||
$"{CultMeshTcpContentTransportConnector.Scheme}://{options.ClientCultMeshAdvertiseHost}:{content.LocalEndPoint.Port}";
|
||||
|
|
@ -1092,6 +1136,7 @@ static AetheriaClientCultMeshHost StartClientCultMeshHost(
|
|||
return new AetheriaClientCultMeshHost(
|
||||
server,
|
||||
content,
|
||||
contentCache,
|
||||
advertisedEndpoint,
|
||||
advertisedContentEndpoint);
|
||||
}
|
||||
|
|
@ -1128,13 +1173,15 @@ static void InjectCultMeshCdnManifestSnapshots(
|
|||
|
||||
static IReadOnlyDictionary<string, CultNetRawDocumentRecord> BuildBundleCdnDocuments(
|
||||
AetheriaStateNode node,
|
||||
AetheriaDaemonHostOptions options)
|
||||
CultCache contentCache,
|
||||
AetheriaUnityBundleArtifactSet unityBundles,
|
||||
string daemonId)
|
||||
{
|
||||
var documents = new Dictionary<string, CultNetRawDocumentRecord>(StringComparer.Ordinal);
|
||||
foreach (var bundle in FindAssetBundles(options))
|
||||
foreach (var bundle in unityBundles.Bundles)
|
||||
{
|
||||
var artifact = PackAssetBundle(bundle.Path, bundle.Platform);
|
||||
CultMeshCdn.PublishAsync(node.Cache, artifact).GetAwaiter().GetResult();
|
||||
var artifact = bundle.Artifact;
|
||||
CultMeshCdn.PublishAsync(contentCache, artifact).GetAwaiter().GetResult();
|
||||
Add(artifact.ManifestKey, artifact.Manifest);
|
||||
}
|
||||
return documents;
|
||||
|
|
@ -1147,7 +1194,7 @@ static IReadOnlyDictionary<string, CultNetRawDocumentRecord> BuildBundleCdnDocum
|
|||
document,
|
||||
new CultNetDocumentMessageOptions
|
||||
{
|
||||
SourceRuntimeId = options.DaemonId,
|
||||
SourceRuntimeId = daemonId,
|
||||
SourceRole = "aetheria-cultmesh-cdn",
|
||||
Tags = ["aetheria", "cultmesh-cdn", "asset-bundle"]
|
||||
});
|
||||
|
|
@ -1596,6 +1643,7 @@ static async Task PublishHotEntityStateAsync(
|
|||
static async Task PublishDaemonApiDocumentsAsync(
|
||||
AetheriaStateNode node,
|
||||
AetheriaDaemonHostOptions options,
|
||||
AetheriaUnityBundleArtifactSet unityBundles,
|
||||
AetheriaRuntimeDaemonTickResult result,
|
||||
bool publishTopology,
|
||||
AetheriaReactiveSurfacePublicationState? reactiveSurfaceState = null)
|
||||
|
|
@ -1628,7 +1676,7 @@ static async Task PublishDaemonApiDocumentsAsync(
|
|||
.ReplaceAsync(result.AssetManifest)
|
||||
.ConfigureAwait(false);
|
||||
await node.MutableDocument<EveAssetCatalogDocument>(AetheriaRuntimeVerseRecordKeys.EveAssetCatalog)
|
||||
.ReplaceAsync(BuildCoreAssetCatalog(options, result.AssetManifest))
|
||||
.ReplaceAsync(BuildCoreAssetCatalog(unityBundles, result.AssetManifest))
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
if (result.StarbridgeSessionSummary != null)
|
||||
|
|
@ -2251,50 +2299,51 @@ static async Task PublishOdinSurfaceAnnouncementsAsync(
|
|||
}
|
||||
|
||||
static EveAssetCatalogDocument BuildCoreAssetCatalog(
|
||||
AetheriaDaemonHostOptions options,
|
||||
AetheriaUnityBundleArtifactSet unityBundles,
|
||||
AetheriaRuntimeAssetManifestDocument source)
|
||||
{
|
||||
var variants = FindAssetBundles(options).Select(bundle =>
|
||||
{
|
||||
var artifact = PackAssetBundle(bundle.Path, bundle.Platform);
|
||||
return new
|
||||
{
|
||||
bundle.Platform,
|
||||
Uri = artifact.ManifestKey.Value,
|
||||
Hash = $"sha256:{artifact.Manifest.ContentHash}",
|
||||
Size = artifact.Manifest.SizeBytes
|
||||
};
|
||||
}).ToArray();
|
||||
var variants = unityBundles.Bundles;
|
||||
|
||||
var assets = (source.Assets ?? Array.Empty<AetheriaRuntimeAssetManifestEntry>())
|
||||
.Where(entry => entry?.Ref != null &&
|
||||
(entry.Ref.Metadata.TryGetValue("unityAssetPath", out _) ||
|
||||
string.Equals(entry.Ref.Kind, AetheriaRuntimeAssetKinds.Prefab, StringComparison.Ordinal) &&
|
||||
entry.Ref.Metadata.TryGetValue("resourcesPath", out _)))
|
||||
.Select(entry =>
|
||||
{
|
||||
var unityAssetPath = entry.Ref.Metadata.TryGetValue("bundleAssetPath", out var bundleAssetPath)
|
||||
? bundleAssetPath
|
||||
: entry.Ref.Metadata.TryGetValue("unityAssetPath", out var explicitPath)
|
||||
? explicitPath
|
||||
: $"Assets/Resources/{entry.Ref.Metadata["resourcesPath"]}.prefab";
|
||||
var bundleName = AetheriaRuntimeAssets.ResolveUnityBundleName(entry);
|
||||
var entryVariants = variants
|
||||
.Where(bundle => string.Equals(bundle.BundleName, bundleName, StringComparison.Ordinal))
|
||||
.Select(bundle =>
|
||||
{
|
||||
var dependencyUris = bundle.Dependencies.Select(dependency => variants.Single(candidate =>
|
||||
string.Equals(candidate.Platform, bundle.Platform, StringComparison.Ordinal) &&
|
||||
string.Equals(candidate.BundleName, dependency, StringComparison.Ordinal)).Artifact.ManifestKey.Value)
|
||||
.ToArray();
|
||||
var unityAssetPath = ResolveUnityBundleAssetKey(entry, bundle.Assets);
|
||||
return new EveAssetVariant(
|
||||
"unity-scene",
|
||||
bundle.Platform,
|
||||
"unity-assetbundle",
|
||||
bundle.Artifact.ManifestKey.Value,
|
||||
$"sha256:{bundle.Artifact.Manifest.ContentHash}",
|
||||
bundle.Artifact.Manifest.SizeBytes,
|
||||
unityAssetPath,
|
||||
new Dictionary<string, string>(
|
||||
entry.Ref.Metadata
|
||||
.Where(pair => pair.Key.StartsWith("unity.", StringComparison.Ordinal))
|
||||
.Append(new KeyValuePair<string, string>("unity.bundleName", bundle.BundleName))
|
||||
.Append(new KeyValuePair<string, string>("unity.bundleDependencyUris", string.Join(";", dependencyUris)))
|
||||
.Append(new KeyValuePair<string, string>("renderChannel.map.unityLayer", "14")),
|
||||
StringComparer.Ordinal));
|
||||
})
|
||||
.ToArray();
|
||||
if (entryVariants.Length == 0)
|
||||
throw new InvalidOperationException(
|
||||
$"No built Unity bundle named '{bundleName}' contains advertised asset '{entry.Ref.AssetKey}'.");
|
||||
return new EveAssetCatalogEntry(
|
||||
entry.Ref.AssetKey,
|
||||
entry.Ref.Kind,
|
||||
variants.Select(bundle => new EveAssetVariant(
|
||||
"unity-scene",
|
||||
bundle.Platform,
|
||||
"unity-assetbundle",
|
||||
bundle.Uri,
|
||||
bundle.Hash,
|
||||
bundle.Size,
|
||||
unityAssetPath,
|
||||
new Dictionary<string, string>(
|
||||
entry.Ref.Metadata
|
||||
.Where(pair => pair.Key.StartsWith("unity.", StringComparison.Ordinal))
|
||||
.Append(new KeyValuePair<string, string>("renderChannel.map.unityLayer", "14")),
|
||||
StringComparer.Ordinal)))
|
||||
.ToArray(),
|
||||
entryVariants,
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["mimeType"] = entry.Ref.MimeType ?? "",
|
||||
|
|
@ -2322,28 +2371,104 @@ static long AssetCatalogVersion(string publishedAtUtc)
|
|||
: DateTimeOffset.UtcNow.UtcDateTime.Ticks;
|
||||
}
|
||||
|
||||
static IReadOnlyList<(string Path, string Platform)> FindAssetBundles(AetheriaDaemonHostOptions options)
|
||||
static string ResolveUnityBundleAssetKey(
|
||||
AetheriaRuntimeAssetManifestEntry entry,
|
||||
IReadOnlyList<string> bundleAssets)
|
||||
{
|
||||
const string bundleName = "aetheria-world";
|
||||
return Directory.Exists(options.AssetBundleRoot)
|
||||
? Directory.GetFiles(options.AssetBundleRoot, bundleName, SearchOption.AllDirectories)
|
||||
.OrderBy(path => path, StringComparer.Ordinal)
|
||||
.Select(path => (path, new DirectoryInfo(Path.GetDirectoryName(path)!).Name))
|
||||
.ToArray()
|
||||
: Array.Empty<(string, string)>();
|
||||
if (entry.Ref.Metadata.TryGetValue("bundleAssetPath", out var bundleAssetPath))
|
||||
return bundleAssetPath;
|
||||
if (entry.Ref.Metadata.TryGetValue("unityAssetPath", out var explicitPath))
|
||||
return explicitPath;
|
||||
if (!entry.Ref.Metadata.TryGetValue("resourcesPath", out var resourcesPath))
|
||||
throw new InvalidOperationException($"Asset '{entry.Ref.AssetKey}' has no Unity asset path.");
|
||||
|
||||
var expected = $"Assets/Resources/{resourcesPath}".Replace('\\', '/');
|
||||
var resolved = bundleAssets.FirstOrDefault(path => string.Equals(
|
||||
Path.ChangeExtension(path, null)?.Replace('\\', '/'),
|
||||
expected,
|
||||
StringComparison.OrdinalIgnoreCase));
|
||||
return resolved ?? throw new InvalidOperationException(
|
||||
$"Bundle asset for '{entry.Ref.AssetKey}' was not found below '{expected}'.");
|
||||
}
|
||||
|
||||
static CultMeshCdnArtifact PackAssetBundle(string path, string platform)
|
||||
static (IReadOnlyList<string> Assets, IReadOnlyList<string> Dependencies) ReadUnityBundleSidecar(string bundlePath)
|
||||
{
|
||||
var manifestPath = bundlePath + ".manifest";
|
||||
if (!File.Exists(manifestPath))
|
||||
throw new FileNotFoundException("Unity bundle sidecar manifest is missing.", manifestPath);
|
||||
var assets = new List<string>();
|
||||
var dependencies = new List<string>();
|
||||
var section = "";
|
||||
foreach (var line in File.ReadLines(manifestPath))
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
if (trimmed == "Assets:") { section = "assets"; continue; }
|
||||
if (trimmed.StartsWith("Dependencies:", StringComparison.Ordinal))
|
||||
{
|
||||
section = "dependencies";
|
||||
if (trimmed == "Dependencies: []") section = "";
|
||||
continue;
|
||||
}
|
||||
if (!trimmed.StartsWith("- ", StringComparison.Ordinal)) continue;
|
||||
if (section == "assets") assets.Add(trimmed.Substring(2));
|
||||
else if (section == "dependencies")
|
||||
dependencies.Add(Path.GetFileName(trimmed.Substring(2).Replace('\\', '/')));
|
||||
}
|
||||
return (assets, dependencies);
|
||||
}
|
||||
|
||||
static IReadOnlyList<(string Path, string Platform, string BundleName)> FindAssetBundles(AetheriaDaemonHostOptions options)
|
||||
{
|
||||
return Directory.Exists(options.AssetBundleRoot)
|
||||
? Directory.GetFiles(options.AssetBundleRoot, "aetheria-*", SearchOption.AllDirectories)
|
||||
.Where(path => !path.EndsWith(".manifest", StringComparison.OrdinalIgnoreCase))
|
||||
.OrderBy(path => path, StringComparer.Ordinal)
|
||||
.Select(path => (
|
||||
path,
|
||||
new DirectoryInfo(Path.GetDirectoryName(path)!).Name,
|
||||
Path.GetFileName(path)))
|
||||
.ToArray()
|
||||
: Array.Empty<(string, string, string)>();
|
||||
}
|
||||
|
||||
static AetheriaUnityBundleArtifactSet BuildUnityBundleArtifactSet(AetheriaDaemonHostOptions options)
|
||||
{
|
||||
var bundles = FindAssetBundles(options)
|
||||
.Select(bundle =>
|
||||
{
|
||||
var sidecar = ReadUnityBundleSidecar(bundle.Path);
|
||||
return new AetheriaUnityBundleArtifact(
|
||||
bundle.Path,
|
||||
bundle.Platform,
|
||||
bundle.BundleName,
|
||||
sidecar.Assets,
|
||||
sidecar.Dependencies,
|
||||
PackAssetBundle(bundle.Path, bundle.Platform, bundle.BundleName));
|
||||
})
|
||||
.ToArray();
|
||||
foreach (var bundle in bundles)
|
||||
foreach (var dependency in bundle.Dependencies)
|
||||
{
|
||||
if (!bundles.Any(candidate =>
|
||||
string.Equals(candidate.Platform, bundle.Platform, StringComparison.Ordinal) &&
|
||||
string.Equals(candidate.BundleName, dependency, StringComparison.Ordinal)))
|
||||
throw new InvalidOperationException(
|
||||
$"Unity bundle '{bundle.BundleName}' depends on missing bundle '{dependency}' for {bundle.Platform}.");
|
||||
}
|
||||
return new AetheriaUnityBundleArtifactSet(bundles);
|
||||
}
|
||||
|
||||
static CultMeshCdnArtifact PackAssetBundle(string path, string platform, string bundleName)
|
||||
{
|
||||
return CultMeshCdn.PackArtifact(
|
||||
$"aetheria/world/{platform}/aetheria-world",
|
||||
$"aetheria/bundles/{platform}/{bundleName}",
|
||||
File.ReadAllBytes(path),
|
||||
new CultMeshCdnPackOptions
|
||||
{
|
||||
Kind = CultMeshCdnArtifactKinds.Asset,
|
||||
Version = "1",
|
||||
MimeType = "application/vnd.unity.assetbundle",
|
||||
Tags = ["aetheria", "unity-scene", platform]
|
||||
Tags = ["aetheria", "unity-scene", platform, bundleName]
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -2588,6 +2713,13 @@ static async Task EnsurePlayableRunDocumentsAsync(
|
|||
AetheriaDaemonHostOptions options,
|
||||
string now)
|
||||
{
|
||||
if (options.UseTerminusFixture)
|
||||
{
|
||||
await AetheriaDaemonZoneGenerator.WritePlayableRunAsync(
|
||||
node, node.RuntimeCatalog().Latest(), now, options.TerminusScenario).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
var settings = await node.MutableDocument<AetheriaPlayerSettings>(AetheriaStateNode.PlayerSettingsKey).ReadAsync().ConfigureAwait(false);
|
||||
if (!string.IsNullOrWhiteSpace(settings?.ActiveRunKey))
|
||||
{
|
||||
|
|
@ -2596,12 +2728,6 @@ static async Task EnsurePlayableRunDocumentsAsync(
|
|||
return;
|
||||
}
|
||||
|
||||
if (options.UseTerminusFixture)
|
||||
{
|
||||
await AetheriaDaemonZoneGenerator.WritePlayableRunAsync(
|
||||
node, node.RuntimeCatalog().Latest(), now, options.TerminusScenario).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
await AetheriaDaemonRunFactory.WriteAsync(
|
||||
node, node.RuntimeCatalog().Latest(), now, now).ConfigureAwait(false);
|
||||
}
|
||||
|
|
@ -3355,28 +3481,68 @@ internal sealed class AetheriaDaemonIngressState
|
|||
}
|
||||
}
|
||||
|
||||
internal sealed class AetheriaUnityBundleArtifactSet
|
||||
{
|
||||
public AetheriaUnityBundleArtifactSet(IReadOnlyList<AetheriaUnityBundleArtifact> bundles)
|
||||
{
|
||||
Bundles = bundles ?? throw new ArgumentNullException(nameof(bundles));
|
||||
}
|
||||
|
||||
public IReadOnlyList<AetheriaUnityBundleArtifact> Bundles { get; }
|
||||
}
|
||||
|
||||
internal sealed class AetheriaUnityBundleArtifact
|
||||
{
|
||||
public AetheriaUnityBundleArtifact(
|
||||
string path,
|
||||
string platform,
|
||||
string bundleName,
|
||||
IReadOnlyList<string> assets,
|
||||
IReadOnlyList<string> dependencies,
|
||||
CultMeshCdnArtifact artifact)
|
||||
{
|
||||
Path = path;
|
||||
Platform = platform;
|
||||
BundleName = bundleName;
|
||||
Assets = assets;
|
||||
Dependencies = dependencies;
|
||||
Artifact = artifact;
|
||||
}
|
||||
|
||||
public string Path { get; }
|
||||
public string Platform { get; }
|
||||
public string BundleName { get; }
|
||||
public IReadOnlyList<string> Assets { get; }
|
||||
public IReadOnlyList<string> Dependencies { get; }
|
||||
public CultMeshCdnArtifact Artifact { get; }
|
||||
}
|
||||
|
||||
internal sealed class AetheriaClientCultMeshHost : IDisposable
|
||||
{
|
||||
public AetheriaClientCultMeshHost(
|
||||
TcpFramedCultNetSchemaServer control,
|
||||
CultMeshTcpContentServer content,
|
||||
CultCache contentCache,
|
||||
string controlEndpoint,
|
||||
string contentEndpoint)
|
||||
{
|
||||
Control = control ?? throw new ArgumentNullException(nameof(control));
|
||||
Content = content ?? throw new ArgumentNullException(nameof(content));
|
||||
ContentCache = contentCache ?? throw new ArgumentNullException(nameof(contentCache));
|
||||
ControlEndpoint = controlEndpoint ?? throw new ArgumentNullException(nameof(controlEndpoint));
|
||||
ContentEndpoint = contentEndpoint ?? throw new ArgumentNullException(nameof(contentEndpoint));
|
||||
}
|
||||
|
||||
public TcpFramedCultNetSchemaServer Control { get; }
|
||||
public CultMeshTcpContentServer Content { get; }
|
||||
private CultCache ContentCache { get; }
|
||||
public string ControlEndpoint { get; }
|
||||
public string ContentEndpoint { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Content.Dispose();
|
||||
ContentCache.Dispose();
|
||||
Control.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,8 @@
|
|||
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Mesh\GameCult.Mesh.csproj" />
|
||||
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Networking\GameCult.Networking.csproj" />
|
||||
<ProjectReference Include="..\..\CultMath\src\CultMath\CultMath.csproj" />
|
||||
<ProjectReference Include="$(EveUnityRoot)\packages\org.gamecult.eve.surface\GameCult.Eve.Surface.csproj" />
|
||||
<ProjectReference Include="$(EveUnityRoot)\packages\org.gamecult.eve.surface\GameCult.Eve.Surface.csproj"
|
||||
AdditionalProperties="CultLibRoot=$(CultLibRoot)" />
|
||||
<ProjectReference Include="..\..\EvePlugins\plugins\eve-plugin-fields\GameCult.Eve.PluginFields.csproj"
|
||||
AdditionalProperties="CultLibRoot=$(CultLibRoot)" />
|
||||
</ItemGroup>
|
||||
|
|
|
|||
|
|
@ -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.34",
|
||||
"org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.35",
|
||||
"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.94"
|
||||
"org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.96"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -127,11 +127,11 @@
|
|||
}
|
||||
},
|
||||
"org.gamecult.cultlib": {
|
||||
"version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.34",
|
||||
"version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.35",
|
||||
"depth": 0,
|
||||
"source": "git",
|
||||
"dependencies": {},
|
||||
"hash": "c148d891c1d8713285ae15b2f17b59c106fb9426"
|
||||
"hash": "36a822d91416d7fecd4f3e67259186e45cf7cdaa"
|
||||
},
|
||||
"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.94",
|
||||
"version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.96",
|
||||
"depth": 0,
|
||||
"source": "git",
|
||||
"dependencies": {
|
||||
"org.gamecult.cultlib": "1.0.34",
|
||||
"org.gamecult.cultlib": "1.0.35",
|
||||
"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": "0463ddff4e39c9cca14e632b7e46b6b0081c67cc"
|
||||
"hash": "3da246aba8ee24af1c2528fe1c81b41a51a41c64"
|
||||
},
|
||||
"com.unity.modules.assetbundle": {
|
||||
"version": "1.0.0",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ namespace Aetheria.Editor
|
|||
{
|
||||
public static class EveAssetBundleBuilder
|
||||
{
|
||||
public const string BundleName = "aetheria-world";
|
||||
private const string ProviderMaterialsRoot = "Assets/Generated/Eve/ProviderMaterials";
|
||||
|
||||
public static void BuildWindows()
|
||||
|
|
@ -23,7 +22,9 @@ namespace Aetheria.Editor
|
|||
public static void VerifyWindows()
|
||||
{
|
||||
var output = ResolveOutput(BuildTarget.StandaloneWindows64);
|
||||
VerifyBundle(Path.Combine(output, BundleName));
|
||||
VerifyBundles(output, Directory.GetFiles(output, "aetheria-*", SearchOption.TopDirectoryOnly)
|
||||
.Where(path => !path.EndsWith(".manifest", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(Path.GetFileName));
|
||||
}
|
||||
|
||||
private static void Build(BuildTarget target)
|
||||
|
|
@ -32,6 +33,8 @@ namespace Aetheria.Editor
|
|||
EveEnvironmentProfileMigrator.EnsureGenerated();
|
||||
var output = ResolveOutput(target);
|
||||
Directory.CreateDirectory(output);
|
||||
foreach (var stale in Directory.GetFiles(output, "*", SearchOption.TopDirectoryOnly))
|
||||
File.Delete(stale);
|
||||
|
||||
var catalog = AetheriaRuntimeCatalogStore.OpenReadOnly(
|
||||
Path.GetFullPath(Path.Combine("GameData", "aetheria-world.cc")));
|
||||
|
|
@ -41,18 +44,26 @@ namespace Aetheria.Editor
|
|||
BuildPresentationPrefab(entry);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
var assetNames = assets
|
||||
.Select(entry => entry.Ref.Metadata.TryGetValue("bundleAssetPath", out var presentationPath)
|
||||
? presentationPath
|
||||
: entry.Ref.Metadata.TryGetValue("unityAssetPath", out var explicitPath)
|
||||
? explicitPath
|
||||
: "")
|
||||
.Where(path => !string.IsNullOrWhiteSpace(path) && File.Exists(path))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(path => path, StringComparer.Ordinal)
|
||||
var bundleBuilds = assets
|
||||
.Select(entry => new
|
||||
{
|
||||
BundleName = AetheriaRuntimeAssets.ResolveUnityBundleName(entry),
|
||||
AssetPath = ResolveBundleAssetPath(entry)
|
||||
})
|
||||
.Where(asset => !string.IsNullOrWhiteSpace(asset.AssetPath) && File.Exists(asset.AssetPath))
|
||||
.GroupBy(asset => asset.BundleName, StringComparer.Ordinal)
|
||||
.Select(group => new AssetBundleBuild
|
||||
{
|
||||
assetBundleName = group.Key,
|
||||
assetNames = group.Select(asset => asset.AssetPath)
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(path => path, StringComparer.Ordinal)
|
||||
.ToArray()
|
||||
})
|
||||
.OrderBy(build => build.assetBundleName, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
if (assetNames.Length == 0)
|
||||
throw new InvalidOperationException("Aetheria Eve bundle has no provider Unity assets.");
|
||||
if (bundleBuilds.Length == 0)
|
||||
throw new InvalidOperationException("Aetheria Eve bundles have no provider Unity assets.");
|
||||
var authoredStardustMaterial = AssetDatabase.LoadAssetAtPath<Material>(
|
||||
"Assets/Shaders/Compute/Stardust/Stardust.mat");
|
||||
AetheriaStardustContinuityVerifier.VerifyTemporalDitherMaterial(
|
||||
|
|
@ -66,21 +77,15 @@ namespace Aetheria.Editor
|
|||
|
||||
var manifest = BuildPipeline.BuildAssetBundles(
|
||||
output,
|
||||
new[]
|
||||
{
|
||||
new AssetBundleBuild
|
||||
{
|
||||
assetBundleName = BundleName,
|
||||
assetNames = assetNames
|
||||
}
|
||||
},
|
||||
BuildAssetBundleOptions.None,
|
||||
bundleBuilds,
|
||||
BuildAssetBundleOptions.ChunkBasedCompression,
|
||||
target);
|
||||
if (manifest == null || !manifest.GetAllAssetBundles().Contains(BundleName, StringComparer.Ordinal))
|
||||
throw new InvalidOperationException("Unity did not emit the Aetheria Eve world bundle.");
|
||||
var expectedBundleNames = bundleBuilds.Select(build => build.assetBundleName).ToArray();
|
||||
if (manifest == null || expectedBundleNames.Except(manifest.GetAllAssetBundles(), StringComparer.Ordinal).Any())
|
||||
throw new InvalidOperationException("Unity did not emit every advertised Aetheria Eve bundle.");
|
||||
|
||||
VerifyBundle(Path.Combine(output, BundleName));
|
||||
Console.WriteLine($"Aetheria Eve AssetBundle: {Path.Combine(output, BundleName)}");
|
||||
VerifyBundles(output, expectedBundleNames);
|
||||
Console.WriteLine($"Aetheria Eve AssetBundles: {string.Join(", ", expectedBundleNames)}");
|
||||
}
|
||||
|
||||
private static string ResolveOutput(BuildTarget target)
|
||||
|
|
@ -91,6 +96,26 @@ namespace Aetheria.Editor
|
|||
: output;
|
||||
}
|
||||
|
||||
private static string ResolveBundleAssetPath(AetheriaRuntimeAssetManifestEntry entry)
|
||||
{
|
||||
if (entry.Ref.Metadata.TryGetValue("bundleAssetPath", out var presentationPath))
|
||||
return presentationPath;
|
||||
if (entry.Ref.Metadata.TryGetValue("unityAssetPath", out var explicitPath))
|
||||
return explicitPath;
|
||||
if (!entry.Ref.Metadata.TryGetValue("resourcesPath", out var resourcesPath))
|
||||
return "";
|
||||
|
||||
var expectedWithoutExtension = $"Assets/Resources/{resourcesPath}".Replace('\\', '/');
|
||||
var directory = Path.GetDirectoryName(expectedWithoutExtension)?.Replace('\\', '/') ?? "Assets/Resources";
|
||||
var fileName = Path.GetFileName(expectedWithoutExtension);
|
||||
return AssetDatabase.FindAssets(fileName, new[] { directory })
|
||||
.Select(AssetDatabase.GUIDToAssetPath)
|
||||
.FirstOrDefault(candidate => string.Equals(
|
||||
Path.ChangeExtension(candidate, null)?.Replace('\\', '/'),
|
||||
expectedWithoutExtension,
|
||||
StringComparison.OrdinalIgnoreCase)) ?? "";
|
||||
}
|
||||
|
||||
private static void BuildPresentationPrefab(AetheriaRuntimeAssetManifestEntry entry)
|
||||
{
|
||||
if (!entry.Ref.Metadata.TryGetValue("bundleAssetPath", out var outputPath))
|
||||
|
|
@ -604,44 +629,57 @@ namespace Aetheria.Editor
|
|||
$"Presentation prefab {assetPath} contains forbidden scripts:\n{string.Join("\n", violations)}");
|
||||
}
|
||||
|
||||
private static void VerifyBundle(string bundlePath)
|
||||
private static void VerifyBundles(string output, IEnumerable<string> bundleNames)
|
||||
{
|
||||
if (!File.Exists(bundlePath))
|
||||
throw new InvalidOperationException($"Aetheria Eve bundle does not exist: {bundlePath}");
|
||||
var bundle = AssetBundle.LoadFromFile(bundlePath);
|
||||
if (bundle == null)
|
||||
throw new InvalidOperationException($"Aetheria Eve bundle could not be loaded: {bundlePath}");
|
||||
var orderedNames = bundleNames
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.OrderBy(BundleLoadOrder)
|
||||
.ThenBy(name => name, StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
var bundles = new List<AssetBundle>();
|
||||
try
|
||||
{
|
||||
var assetNames = bundle.GetAllAssetNames();
|
||||
foreach (var assetPath in assetNames.Where(path => path.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)))
|
||||
foreach (var bundleName in orderedNames)
|
||||
{
|
||||
var bundlePath = Path.Combine(output, bundleName);
|
||||
if (!File.Exists(bundlePath))
|
||||
throw new InvalidOperationException($"Aetheria Eve bundle does not exist: {bundlePath}");
|
||||
var bundle = AssetBundle.LoadFromFile(bundlePath);
|
||||
if (bundle == null)
|
||||
throw new InvalidOperationException($"Aetheria Eve bundle could not be loaded: {bundlePath}");
|
||||
bundles.Add(bundle);
|
||||
}
|
||||
foreach (var bundle in bundles)
|
||||
foreach (var assetPath in bundle.GetAllAssetNames()
|
||||
.Where(path => path.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase)))
|
||||
VerifyPrefab(bundle.LoadAsset<GameObject>(assetPath), assetPath);
|
||||
var reflection = LoadAuthoredAsset<Cubemap>(bundle, assetNames, "Assets/Textures/studio2.hdr");
|
||||
var reflection = LoadAuthoredAsset<Cubemap>(bundles, "Assets/Textures/studio2.hdr");
|
||||
if (reflection == null)
|
||||
throw new InvalidOperationException("Aetheria Eve bundle has no provider reflection cubemap.");
|
||||
var postProcess = LoadAuthoredAsset<VolumeProfile>(bundle, assetNames, EveEnvironmentProfileMigrator.FlightProfilePath);
|
||||
throw new InvalidOperationException("Aetheria Eve bundles have no provider reflection cubemap.");
|
||||
var postProcess = LoadAuthoredAsset<VolumeProfile>(bundles, EveEnvironmentProfileMigrator.FlightProfilePath);
|
||||
VerifyFlightPostProcessProfile(postProcess);
|
||||
var gravityFog = LoadAuthoredAsset<Shader>(bundle, assetNames, "Assets/Shaders/Raymarching/CloudShader.shader");
|
||||
var gravityFog = LoadAuthoredAsset<Shader>(bundles, "Assets/Shaders/Raymarching/CloudShader.shader");
|
||||
if (gravityFog == null || !gravityFog.isSupported)
|
||||
throw new InvalidOperationException("Aetheria Eve bundle has no supported gravity-fog volume shader.");
|
||||
throw new InvalidOperationException("Aetheria Eve bundles have no supported gravity-fog volume shader.");
|
||||
AetheriaGravityFogVerifier.VerifyCameraProjection(
|
||||
gravityFog,
|
||||
requireAuthoredSource: false);
|
||||
var dither = LoadAuthoredAsset<Texture2D>(bundle, assetNames, "Assets/Resources/LDR_LLL1_0.png");
|
||||
var dither = LoadAuthoredAsset<Texture2D>(bundles, "Assets/Resources/LDR_LLL1_0.png");
|
||||
if (dither == null)
|
||||
throw new InvalidOperationException("Aetheria Eve bundle has no pre-generated volume dither texture.");
|
||||
var stardustCompute = LoadAuthoredAsset<ComputeShader>(bundle, assetNames,
|
||||
var stardustCompute = LoadAuthoredAsset<ComputeShader>(bundles,
|
||||
"Assets/Shaders/Compute/Stardust/Stardust.compute");
|
||||
if (stardustCompute == null || !stardustCompute.HasKernel("UpdateParticles"))
|
||||
throw new InvalidOperationException("Aetheria Eve bundle has no supported Stardust update program.");
|
||||
var stardustMaterial = LoadAuthoredAsset<Material>(bundle, assetNames,
|
||||
var stardustMaterial = LoadAuthoredAsset<Material>(bundles,
|
||||
"Assets/Shaders/Compute/Stardust/Stardust.mat");
|
||||
if (stardustMaterial == null || stardustMaterial.shader == null || !stardustMaterial.shader.isSupported)
|
||||
throw new InvalidOperationException("Aetheria Eve bundle has no supported Stardust render material.");
|
||||
AetheriaStardustContinuityVerifier.VerifyTemporalDitherMaterial(
|
||||
stardustMaterial,
|
||||
requireAuthoredSource: false);
|
||||
var stardustColors = LoadAuthoredAsset<Texture2D>(bundle, assetNames,
|
||||
var stardustColors = LoadAuthoredAsset<Texture2D>(bundles,
|
||||
"Assets/Resources/Gradients/blackbody.png");
|
||||
if (stardustColors == null)
|
||||
throw new InvalidOperationException("Aetheria Eve bundle has no pre-generated Stardust color texture.");
|
||||
|
|
@ -649,16 +687,29 @@ namespace Aetheria.Editor
|
|||
}
|
||||
finally
|
||||
{
|
||||
bundle.Unload(true);
|
||||
for (var index = bundles.Count - 1; index >= 0; index--)
|
||||
bundles[index].Unload(true);
|
||||
}
|
||||
}
|
||||
|
||||
private static T LoadAuthoredAsset<T>(AssetBundle bundle, IEnumerable<string> assetNames, string authoredPath)
|
||||
private static int BundleLoadOrder(string bundleName)
|
||||
{
|
||||
if (bundleName == AetheriaRuntimeAssets.UnityShaderBundle) return 0;
|
||||
if (bundleName == AetheriaRuntimeAssets.UnityCoreBundle) return 1;
|
||||
if (bundleName == AetheriaRuntimeAssets.UnityUiBundle) return 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
private static T LoadAuthoredAsset<T>(IEnumerable<AssetBundle> bundles, string authoredPath)
|
||||
where T : UnityEngine.Object
|
||||
{
|
||||
var nativePath = assetNames.FirstOrDefault(path =>
|
||||
string.Equals(path, authoredPath, StringComparison.OrdinalIgnoreCase));
|
||||
return string.IsNullOrWhiteSpace(nativePath) ? null : bundle.LoadAsset<T>(nativePath);
|
||||
foreach (var bundle in bundles)
|
||||
{
|
||||
var nativePath = bundle.GetAllAssetNames().FirstOrDefault(path =>
|
||||
string.Equals(path, authoredPath, StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrWhiteSpace(nativePath)) return bundle.LoadAsset<T>(nativePath);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void VerifyFlightPostProcessProfile(VolumeProfile profile)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using GameCult.Aetheria.State.Verse;
|
||||
using NUnit.Framework;
|
||||
|
||||
public sealed class AetheriaRuntimeAssetBundleLayoutTests
|
||||
{
|
||||
[Test]
|
||||
public void SharedPresentationAssetsHaveStableBundleOwners()
|
||||
{
|
||||
var manifest = AetheriaRuntimeAssets.ProjectManifest(null);
|
||||
|
||||
Assert.AreEqual(AetheriaRuntimeAssets.UnityShaderBundle, BundleFor("shader.environment.gravity-fog"));
|
||||
Assert.AreEqual(AetheriaRuntimeAssets.UnityShaderBundle, BundleFor("texture.environment.volume-dither"));
|
||||
Assert.AreEqual(AetheriaRuntimeAssets.UnityUiBundle, BundleFor("map.entity.player"));
|
||||
Assert.AreEqual(AetheriaRuntimeAssets.UnityUiBundle, BundleFor("font.ui.primary"));
|
||||
Assert.AreEqual(AetheriaRuntimeAssets.UnityCoreBundle, BundleFor("texture.environment.reflection"));
|
||||
Assert.AreEqual(AetheriaRuntimeAssets.UnityCoreBundle, BundleFor("texture.core.sun.surface-flow"));
|
||||
|
||||
string BundleFor(string assetKey)
|
||||
{
|
||||
var entry = manifest.Assets.Single(asset =>
|
||||
string.Equals(asset.Ref.AssetKey, assetKey, StringComparison.Ordinal));
|
||||
return AetheriaRuntimeAssets.ResolveUnityBundleName(entry);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void EntityContentIsPartitionedByAuthoredSource()
|
||||
{
|
||||
var manifest = AetheriaRuntimeAssets.ProjectManifest(null);
|
||||
var player = manifest.Assets.Single(asset => asset.Ref.AssetKey == "prefab.entity.player");
|
||||
var ship = manifest.Assets.Single(asset => asset.Ref.AssetKey == "prefab.entity.ship");
|
||||
var station = manifest.Assets.Single(asset => asset.Ref.AssetKey == "prefab.entity.station");
|
||||
|
||||
var playerBundle = AetheriaRuntimeAssets.ResolveUnityBundleName(player);
|
||||
Assert.AreEqual(playerBundle, AetheriaRuntimeAssets.ResolveUnityBundleName(ship));
|
||||
Assert.AreNotEqual(playerBundle, AetheriaRuntimeAssets.ResolveUnityBundleName(station));
|
||||
StringAssert.StartsWith("aetheria-content-", playerBundle);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 20642d787904fb94aa7ae63109f417ce
|
||||
|
|
@ -18,6 +18,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public const string Audio = "audio";
|
||||
public const string Prefab = "prefab";
|
||||
public const string VolumeProfile = "unity.volume-profile";
|
||||
public const string Font = "font";
|
||||
}
|
||||
|
||||
public static class AetheriaRuntimeAssetTransports
|
||||
|
|
|
|||
|
|
@ -9,6 +9,52 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public static class AetheriaRuntimeAssets
|
||||
{
|
||||
public const string PresentationHullItemTagPrefix = "presentation-hull-item:";
|
||||
public const string UnityShaderBundle = "aetheria-shaders";
|
||||
public const string UnityUiBundle = "aetheria-ui";
|
||||
public const string UnityCoreBundle = "aetheria-core";
|
||||
|
||||
public static string ResolveUnityBundleName(AetheriaRuntimeAssetManifestEntry entry)
|
||||
{
|
||||
if (entry?.Ref == null)
|
||||
throw new ArgumentNullException(nameof(entry));
|
||||
|
||||
var key = entry.Ref.AssetKey ?? "";
|
||||
if (key.StartsWith("shader.", StringComparison.Ordinal) ||
|
||||
key.StartsWith("compute.", StringComparison.Ordinal) ||
|
||||
key == "material.environment.stardust" ||
|
||||
key == "texture.environment.stardust-colors" ||
|
||||
key == "texture.environment.volume-dither" ||
|
||||
key == "profile.environment.flight")
|
||||
return UnityShaderBundle;
|
||||
|
||||
if (key.StartsWith("map.", StringComparison.Ordinal) ||
|
||||
key.StartsWith("material.sector-map.", StringComparison.Ordinal) ||
|
||||
key.StartsWith("inventory.", StringComparison.Ordinal) ||
|
||||
key.StartsWith("profile.thermal.", StringComparison.Ordinal) ||
|
||||
key == "profile.death" ||
|
||||
key.StartsWith("item.", StringComparison.Ordinal) && key.EndsWith(".icon", StringComparison.Ordinal) ||
|
||||
key == "font.ui.primary")
|
||||
return UnityUiBundle;
|
||||
|
||||
if (!string.Equals(entry.Ref.Kind, AetheriaRuntimeAssetKinds.Prefab, StringComparison.Ordinal))
|
||||
return UnityCoreBundle;
|
||||
|
||||
var source = entry.Ref.Metadata.TryGetValue("unityAssetPath", out var unityAssetPath)
|
||||
? unityAssetPath
|
||||
: entry.Ref.Metadata.TryGetValue("resourcesPath", out var resourcesPath)
|
||||
? resourcesPath
|
||||
: key;
|
||||
var stem = string.Concat((source ?? key).Trim().ToLowerInvariant().Select(character =>
|
||||
char.IsLetterOrDigit(character) ? character : '-'));
|
||||
while (stem.Contains("--")) stem = stem.Replace("--", "-");
|
||||
stem = stem.Trim('-');
|
||||
if (stem.StartsWith("assets-", StringComparison.Ordinal)) stem = stem.Substring("assets-".Length);
|
||||
if (stem.StartsWith("resources-", StringComparison.Ordinal)) stem = stem.Substring("resources-".Length);
|
||||
if (stem.EndsWith("-prefab", StringComparison.Ordinal)) stem = stem.Substring(0, stem.Length - "-prefab".Length);
|
||||
const int maximumStemLength = 96;
|
||||
if (stem.Length > maximumStemLength) stem = stem.Substring(stem.Length - maximumStemLength);
|
||||
return $"aetheria-content-{stem}";
|
||||
}
|
||||
|
||||
public static string ResolveEntityPrefabAssetRef(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
|
|
@ -42,6 +88,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
Add(entries, EnvironmentStardustComputeShader());
|
||||
Add(entries, EnvironmentStardustMaterial());
|
||||
Add(entries, EnvironmentStardustColorTexture());
|
||||
foreach (var coreAsset in CorePresentationResources()) Add(entries, coreAsset);
|
||||
Add(entries, UiPrimaryFont());
|
||||
foreach (var mapAsset in SectorMapAssets()) Add(entries, mapAsset);
|
||||
Add(entries, MapIcon("entity.player", "Player", "Sprites/Icons/Stroked/Ship"));
|
||||
Add(entries, MapIcon("entity.ship", "Ship", "Sprites/Icons/Stroked/Ship"));
|
||||
|
|
@ -467,6 +515,39 @@ namespace GameCult.Aetheria.State.Verse
|
|||
};
|
||||
}
|
||||
|
||||
public static IReadOnlyList<AetheriaRuntimeAssetManifestEntry> CorePresentationResources()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
MapPresentationAsset("texture.core.sun.surface-flow", AetheriaRuntimeAssetKinds.Texture,
|
||||
"Sun surface flow", "Assets/Resources/Sun Flow/sunsurface1offset.png", "core.sun.surface-flow"),
|
||||
MapPresentationAsset("texture.core.sun.albedo", AetheriaRuntimeAssetKinds.Texture,
|
||||
"Sun albedo", "Assets/Resources/Sun Albedo/sunsurface2map.png", "core.sun.albedo"),
|
||||
MapPresentationAsset("texture.core.fx.explosion-emission", AetheriaRuntimeAssetKinds.Texture,
|
||||
"Explosion emission", "Assets/Prefabs/Fire & Explosion Effects/Textures/ExplosionEmission.tif", "core.fx.explosion-emission"),
|
||||
MapPresentationAsset("texture.core.fx.smoke-puff", AetheriaRuntimeAssetKinds.Texture,
|
||||
"Smoke puff", "Assets/Prefabs/Fire & Explosion Effects/Textures/SmokePuff01.tif", "core.fx.smoke-puff"),
|
||||
MapPresentationAsset("texture.core.fx.explosion", AetheriaRuntimeAssetKinds.Texture,
|
||||
"Explosion", "Assets/Prefabs/Fire & Explosion Effects/Textures/Explosion.tif", "core.fx.explosion"),
|
||||
MapPresentationAsset("texture.core.fx.shockwave", AetheriaRuntimeAssetKinds.Texture,
|
||||
"Shockwave", "Assets/Prefabs/Fire & Explosion Effects/Textures/shockwave.tif", "core.fx.shockwave"),
|
||||
MapPresentationAsset("texture.core.fx.sphere-normal", AetheriaRuntimeAssetKinds.Texture,
|
||||
"Sphere normal", "Assets/Prefabs/Fire & Explosion Effects/Textures/SphereNormal.tif", "core.fx.sphere-normal"),
|
||||
MapPresentationAsset("texture.core.fx.debris", AetheriaRuntimeAssetKinds.Texture,
|
||||
"Debris", "Assets/Prefabs/Fire & Explosion Effects/Textures/Debris.png", "core.fx.debris")
|
||||
};
|
||||
}
|
||||
|
||||
public static AetheriaRuntimeAssetManifestEntry UiPrimaryFont()
|
||||
{
|
||||
return MapPresentationAsset(
|
||||
"font.ui.primary",
|
||||
AetheriaRuntimeAssetKinds.Font,
|
||||
"Primary UI font",
|
||||
"Assets/Fonts/Ubuntu/Ubuntu-L Small SDF.asset",
|
||||
"ui.font.primary");
|
||||
}
|
||||
|
||||
private static AetheriaRuntimeAssetManifestEntry MapIcon(
|
||||
string key,
|
||||
string label,
|
||||
|
|
|
|||
|
|
@ -1886,24 +1886,37 @@ edges with one worker and exactly two start/transfer/completion event triplets.
|
|||
|
||||
### Daemon Asset Content Delivery
|
||||
|
||||
- Owner: the Aetheria daemon owns bundle content and serves bytes only through
|
||||
the managed `cultmesh.content.v1` session hosted by CultMesh.
|
||||
- Inputs: provider bundle path, packed artifact manifest, content hash, byte
|
||||
length, negotiated chunk requests, and resumable client checkpoint.
|
||||
- Owner: Aetheria's runtime asset manifest owns each authored asset's Unity
|
||||
bundle assignment. The build emits `aetheria-shaders`, `aetheria-ui`, and
|
||||
`aetheria-core` shared bundles plus content bundles grouped by authored entity
|
||||
source. The daemon owns those artifacts and serves bytes only through the
|
||||
managed `cultmesh.content.v1` session hosted by CultMesh. EveUnity knows no
|
||||
Aetheria bundle taxonomy.
|
||||
- Inputs: provider bundle paths, Unity dependency manifests, packed artifact
|
||||
manifests, content hashes, byte lengths, negotiated chunk requests, and
|
||||
resumable client checkpoints.
|
||||
- Outputs: snapshot delivery publishes the typed artifact manifest only;
|
||||
content-session requests return bounded byte ranges which the generic runtime
|
||||
verifies and atomically promotes to a hash-named `.body`, then binds to a
|
||||
read-only mapped-body lease for Unity loading.
|
||||
catalog variants name their owning artifact and dependency artifact URIs.
|
||||
Publishing the catalog transfers no bundle. The generic runtime requests an
|
||||
artifact only when presentation first resolves one of its asset keys, loads
|
||||
dependencies first, verifies each body, atomically promotes it to a hash-named
|
||||
`.body`, and binds it to a read-only mapped-body lease for Unity loading.
|
||||
- Derived state: Unity `AssetBundle` instances, native prefab lookup, cache
|
||||
files, progress UI, and screenshots cannot publish or mutate provider bytes.
|
||||
- Forbidden writers: batched snapshot chunks and the former
|
||||
- Forbidden writers: a monolithic world bundle, eager catalog materialization,
|
||||
client-owned content categories, batched snapshot chunks, and the former
|
||||
`gamecult.cultmesh.cdn.asset_blob.v1` fallback are deleted. Eve surfaces and
|
||||
renderer code cannot smuggle bundle bytes through state records.
|
||||
- Shared paths: cold transfer, resume, and warm cache reuse all resolve the same
|
||||
advertised manifest and content hash through the generic runtime transport.
|
||||
- Shared paths: prefab, shader, texture, font, and profile resolution all use the
|
||||
same dependency-aware artifact loader. Cold transfer, resume, and warm cache
|
||||
reuse resolve the same advertised manifests and hashes through the generic
|
||||
runtime transport.
|
||||
- Cut line: CultMesh snapshots index manifests; managed content sessions own
|
||||
remote bytes, verification, and promotion. Mapped-body negotiation begins only
|
||||
at the returned final path and cannot map partials or reconstruct cache names.
|
||||
at first asset demand and at the returned final path; it cannot map partials or
|
||||
reconstruct cache names. Blender-authored native entity reconstruction remains
|
||||
the intended successor to Unity prefab content and is not simulated by this
|
||||
bundle partition.
|
||||
|
||||
- Owner: `Aetheria.State` owns the new typed state spine for durable state.
|
||||
The embedded `GameCult.Aetheria.State.Unity` package owns Unity's runtime
|
||||
|
|
|
|||
Loading…
Reference in a new issue