Partition provider assets for demand loading

This commit is contained in:
Meta Vens 2026-07-19 21:51:49 +01:00
commit 72ce713a19
10 changed files with 497 additions and 141 deletions

View file

@ -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();
}
}

View file

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

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

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

View file

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

View file

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

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 20642d787904fb94aa7ae63109f417ce

View file

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

View file

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

View file

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