Hydrate only Aetheria daemon working state

This commit is contained in:
Meta Vens 2026-07-20 01:52:05 +01:00
commit b4b16867ee
9 changed files with 341 additions and 15 deletions

View file

@ -1,4 +1,5 @@
using System.Globalization;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
using Aetheria.State.Documents;
@ -40,12 +41,32 @@ public sealed class AetheriaYmirPersistenceCoordinator : IDisposable
AetheriaYmirWorldPhysics physics,
AetheriaRuntimeDaemonFrameDocument? frame)
{
var trace = string.Equals(
Environment.GetEnvironmentVariable("AETHERIA_TRACE_STARTUP_PHASES"),
"1",
StringComparison.Ordinal);
var phase = Stopwatch.StartNew();
void Trace(string name)
{
if (trace)
Console.WriteLine($"Aetheria Ymir restore phase {name} took {phase.Elapsed.TotalMilliseconds:0.###}ms.");
phase.Restart();
}
ArgumentNullException.ThrowIfNull(node);
ArgumentNullException.ThrowIfNull(physics);
var registry = CultMesh.CreateCultCacheDocumentRegistry(
typeof(AetheriaYmirPersistenceMarkerDocument),
typeof(AetheriaYmirJournalChunkDocument),
typeof(AetheriaYmirResumeDocument));
var ownedSchemaIds = registry.AllDescriptors
.Select(descriptor => descriptor.SchemaId)
.ToHashSet(StringComparer.Ordinal);
var currentResumeKeys = frame?.Run == null || string.IsNullOrWhiteSpace(frame.Run.RunId)
? new HashSet<string>(StringComparer.Ordinal)
: (frame.Run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
.Where(zone => zone != null)
.Select(zone => ResumeRecordKey(frame.Run.RunId, zone.ZoneIndex, frame.FrameId))
.ToHashSet(StringComparer.Ordinal);
var cache = await CultCacheMessagePack.OpenAsync(
PrivateStatePath(node.StatePath),
new CultCacheOpenOptions
@ -53,9 +74,14 @@ public sealed class AetheriaYmirPersistenceCoordinator : IDisposable
Registry = registry,
PullOnOpen = true,
UseDirectoryStore = true,
DirectoryStoreHydrationFilter = record =>
ownedSchemaIds.Contains(record.SchemaId) &&
(string.Equals(record.Key, AetheriaYmirPersistenceMarkerDocument.RecordKey, StringComparison.Ordinal) ||
currentResumeKeys.Contains(record.Key)),
FlushOnDispose = true,
StoreFlushOnDispose = true
}).ConfigureAwait(false);
Trace("open-private-cache");
try
{
var activated = cache.Get<AetheriaYmirPersistenceMarkerDocument>(
@ -72,14 +98,24 @@ public sealed class AetheriaYmirPersistenceCoordinator : IDisposable
.Select(zone => cache.Get<AetheriaYmirResumeDocument>(
new CultRecordKey(ResumeRecordKey(frame.Run.RunId, zone.ZoneIndex, frame.FrameId))))
.ToArray();
Trace("resolve-resumes");
if (resumes.All(value => value == null) && !activated)
return coordinator;
if (resumes.Any(value => value == null))
throw new InvalidOperationException(
$"Aetheria frame {frame.FrameId} has incomplete daemon-private Ymir resume state.");
var journalRanges = zones.Zip(resumes, (zone, resume) =>
JournalRanges(frame.Run.RunId, zone.ZoneIndex, resume!))
.SelectMany(value => value)
.ToArray();
await cache.PullBackingStoreRecordsAsync(metadata =>
journalRanges.Any(range => range.Contains(metadata.Key))).ConfigureAwait(false);
Trace("hydrate-journals");
foreach (var pair in zones.Zip(resumes, (zone, resume) => (zone, resume: resume!)))
coordinator.RestoreZone(frame.Run.RunId, frame.FrameId, pair.zone.ZoneIndex, pair.resume);
Trace("restore-zones");
return coordinator;
}
catch
@ -320,9 +356,43 @@ public sealed class AetheriaYmirPersistenceCoordinator : IDisposable
long firstEntryIndex) =>
$"private:aetheria.ymir.journal.v1:{RunKey(runId)}:{zoneIndex}:{channel}:{generation}:{firstEntryIndex:D20}";
private static JournalRange[] JournalRanges(
string runId,
int zoneIndex,
AetheriaYmirResumeDocument resume)
{
var world = YmirSessionCheckpointCodec.DecodeResumeDescriptor(resume.WorldDescriptorPayload);
var ranges = new List<JournalRange>
{
new(JournalRecordPrefix(runId, zoneIndex, WorldChannel, world.SessionGeneration), world.JournalEntryCount)
};
if (resume.PayloadDescriptorPayload.Length > 0)
{
var payload = YmirSessionCheckpointCodec.DecodeResumeDescriptor(resume.PayloadDescriptorPayload);
ranges.Add(new JournalRange(
JournalRecordPrefix(runId, zoneIndex, PayloadChannel, payload.SessionGeneration),
payload.JournalEntryCount));
}
return ranges.ToArray();
}
private static string JournalRecordPrefix(
string runId,
int zoneIndex,
string channel,
string generation) =>
$"private:aetheria.ymir.journal.v1:{RunKey(runId)}:{zoneIndex}:{channel}:{generation}:";
private static string RunKey(string runId) =>
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(runId))).ToLowerInvariant();
private readonly record struct CursorKey(string RunId, int ZoneIndex, string Channel);
private readonly record struct Cursor(string Generation, long EntryCount);
private readonly record struct JournalRange(string Prefix, long EntryCount)
{
public bool Contains(string key) =>
key.StartsWith(Prefix, StringComparison.Ordinal) &&
long.TryParse(key.AsSpan(Prefix.Length), NumberStyles.None, CultureInfo.InvariantCulture, out var firstEntryIndex) &&
firstEntryIndex < EntryCount;
}
}

View file

@ -48,7 +48,8 @@ await using var node = await AetheriaStateNode.OpenAsync(
options.StatePath,
runtimeId: options.DaemonId,
startServer: true,
enableDurableShardLogs: false).ConfigureAwait(false);
enableDurableShardLogs: false,
hydrationProfile: AetheriaStateHydrationProfile.DaemonBoot).ConfigureAwait(false);
TraceStartup("open-state-node");
using var discoveryHost = new AetheriaVerseDiscoveryHost(node);
var latestFrame = await node.MutableDocument<AetheriaRuntimeDaemonFrameDocument>(AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest).ReadAsync().ConfigureAwait(false);
@ -87,7 +88,10 @@ var nativeCatalogChanged = await AetheriaDaemonNativeCatalog.EnsureAsync(node).C
TraceStartup("native-catalog");
await node.FlushAsync().ConfigureAwait(false);
TraceStartup("initial-flush");
if (persistedRuntimeCatalog == null || tradePolicyChanged || nativeCatalogChanged)
if (persistedRuntimeCatalog == null ||
string.IsNullOrWhiteSpace(persistedRuntimeCatalog.NameCorpusRecordKey) ||
tradePolicyChanged ||
nativeCatalogChanged)
await node.RefreshRuntimeCatalogAsync().ConfigureAwait(false);
TraceStartup("runtime-catalog");
await EnsurePlayableRunDocumentsAsync(node, options, startedAtUtc).ConfigureAwait(false);
@ -2002,7 +2006,9 @@ static async Task PublishDaemonMenuSurfacesAsync(
catalog,
frame.Run?.RunId ?? frame.SessionId,
"cultmesh://aetheria/assets");
var loadoutTemplates = AetheriaRuntimeCatalogStore.ReadLoadoutTemplates(node.StatePath);
var loadoutTemplates = node.Cache.GetAll<AetheriaLoadoutTemplate>()
.Select(ToLoadoutTemplateSnapshot)
.ToArray();
var playerSettings = await ReadRuntimePlayerSettingsAsync(node).ConfigureAwait(false);
var currentEntity = AetheriaRuntimeGameDocuments.CurrentEntity(frame);
var stationRefit = AetheriaRuntimeGameDocuments.StationRefit(frame, loadoutTemplates, catalog);
@ -2871,7 +2877,10 @@ static async Task EnsurePlayableRunDocumentsAsync(
}
await AetheriaDaemonRunFactory.WriteAsync(
node, node.RuntimeCatalog().Latest(), now, now).ConfigureAwait(false);
node,
await node.RuntimeCatalogForGenerationAsync().ConfigureAwait(false),
now,
now).ConfigureAwait(false);
}
static async Task EnsureGameSessionAsync(
@ -2933,7 +2942,10 @@ static async Task<bool> ApplyRequestedNewGameSessionAsync(
var now = DateTimeOffset.UtcNow.ToString("O");
var written = await AetheriaDaemonRunFactory.WriteAsync(
node, node.RuntimeCatalog().Latest(), now, menu.LastCommandId).ConfigureAwait(false);
node,
await node.RuntimeCatalogForGenerationAsync().ConfigureAwait(false),
now,
menu.LastCommandId).ConfigureAwait(false);
session.Mode = written.SessionMode;
session.SessionId = options.SessionId;
session.RunId = written.RunId;
@ -3476,6 +3488,65 @@ static AetheriaRuntimeLoadoutItemCommit ToLoadoutItemCommit(AetheriaLoadoutItem?
};
}
static AetheriaRuntimeLoadoutTemplateSnapshot ToLoadoutTemplateSnapshot(AetheriaLoadoutTemplate template)
{
return new AetheriaRuntimeLoadoutTemplateSnapshot(
template.Name ?? "",
template.OwnerPlayerKey ?? "",
ToEntityLoadoutSnapshot(template.RootEntity),
template.CreatedAtUtc ?? "",
template.UpdatedAtUtc ?? "");
}
static AetheriaRuntimeEntityLoadoutSnapshot ToEntityLoadoutSnapshot(AetheriaEntityLoadout? entity)
{
entity ??= new AetheriaEntityLoadout();
return new AetheriaRuntimeEntityLoadoutSnapshot(
entity.Name ?? "",
entity.Kind ?? "",
entity.FactionKey ?? "",
ToLoadoutItemSnapshot(entity.Hull),
ToSlotSnapshots(entity.Equipment),
ToSlotSnapshots(entity.CargoBays),
ToSlotSnapshots(entity.DockingBays),
ToCargoSnapshots(entity.CargoContents),
ToCargoSnapshots(entity.DockingBayContents),
(entity.DockingBayAssignments ?? []).ToArray(),
(entity.WeaponGroups ?? []).Select(group => (IReadOnlyList<int>)group.ToArray()).ToArray(),
(entity.Children ?? []).Select(ToEntityLoadoutSnapshot).ToArray());
}
static AetheriaRuntimeLoadoutItemSnapshot ToLoadoutItemSnapshot(AetheriaLoadoutItem? item)
{
item ??= new AetheriaLoadoutItem();
return new AetheriaRuntimeLoadoutItemSnapshot(
item.ItemKey ?? "",
item.Quality,
item.Durability,
item.Quantity,
item.Enabled,
item.OverrideShutdown,
item.Temperature);
}
static AetheriaRuntimeLoadoutItemSlotSnapshot[] ToSlotSnapshots(IEnumerable<AetheriaLoadoutItemSlot>? slots)
{
return (slots ?? [])
.Select(slot => new AetheriaRuntimeLoadoutItemSlotSnapshot(
slot.Position?.X ?? 0,
slot.Position?.Y ?? 0,
ToLoadoutItemSnapshot(slot.Item),
slot.Rotation ?? "None"))
.ToArray();
}
static AetheriaRuntimeCargoBayLoadoutSnapshot[] ToCargoSnapshots(IEnumerable<AetheriaCargoBayLoadout>? bays)
{
return (bays ?? [])
.Select(bay => new AetheriaRuntimeCargoBayLoadoutSnapshot(ToSlotSnapshots(bay.Items)))
.ToArray();
}
internal sealed record AetheriaPreparedPublication(
AetheriaRuntimeDaemonTickResult Publication,
IReadOnlyList<AetheriaYmirZonePersistenceCapture> Physics,

View file

@ -33,6 +33,7 @@ public static class AetheriaDocumentRegistry
typeof(AetheriaLegacyCatalogQuarantine),
typeof(AetheriaTradeValuePolicy),
typeof(AetheriaRuntimeCatalogSnapshot),
typeof(AetheriaRuntimeNameCorpusSnapshot),
typeof(AetheriaPlayerSettings),
typeof(AetheriaLoadoutTemplate),
typeof(AetheriaRunState),

View file

@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Aetheria.State.Documents;
using GameCult.Aetheria.State.Verse;
@ -9,13 +11,22 @@ using GameCult.Mesh;
using GameCult.Networking;
using R3;
using EveSurfaceDocument = GameCult.Eve.Surface.EveSurfaceDocument;
using EveSurfaceCommandRequest = GameCult.Eve.Surface.EveSurfaceCommandRequest;
using EveCommandReceiptDocument = GameCult.Eve.Surface.EveCommandReceiptDocument;
namespace Aetheria.State;
public enum AetheriaStateHydrationProfile
{
All,
DaemonBoot
}
public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
{
private readonly CultMeshNode _node;
private CultMeshDocumentHandle<AetheriaRuntimeCatalogSnapshot>? _runtimeCatalog;
private CultMeshDocumentHandle<AetheriaRuntimeNameCorpusSnapshot>? _runtimeNameCorpus;
private CultMeshDocumentHandle<EveSurfaceDocument>? _catalogSurface;
private AetheriaStateNode(string statePath, string runtimeId, CultMeshNode node)
@ -42,7 +53,8 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
string runtimeId = "aetheria-local",
bool startServer = false,
bool enableDurableShardLogs = true,
bool useDirectoryStore = true)
bool useDirectoryStore = true,
AetheriaStateHydrationProfile hydrationProfile = AetheriaStateHydrationProfile.All)
{
if (string.IsNullOrWhiteSpace(statePath))
{
@ -52,6 +64,7 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(statePath)) ?? ".");
var cacheRegistry = AetheriaDocumentRegistry.CreateCultCacheRegistry();
var hydrationFilter = CreateHydrationFilter(hydrationProfile, cacheRegistry);
var node = await CultMesh.CreateNodeAsync(
statePath,
new CultMeshNodeOptions
@ -63,7 +76,8 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
Registry = cacheRegistry,
PullOnOpen = true,
StoreFlushOnDispose = true,
UseDirectoryStore = useDirectoryStore
UseDirectoryStore = useDirectoryStore,
DirectoryStoreHydrationFilter = hydrationFilter
},
DatabaseOptions = new CultNetDatabaseOptions
{
@ -75,6 +89,51 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
return new AetheriaStateNode(statePath, runtimeId, node);
}
private static Func<CultPersistedRecordMetadata, bool>? CreateHydrationFilter(
AetheriaStateHydrationProfile profile,
CultDocumentRegistry registry)
{
if (profile == AetheriaStateHydrationProfile.All)
return null;
if (profile != AetheriaStateHydrationProfile.DaemonBoot)
throw new ArgumentOutOfRangeException(nameof(profile), profile, "Unknown Aetheria state hydration profile.");
var schemaTypes = new[]
{
typeof(AetheriaItemDefinition),
typeof(AetheriaLoadoutTemplate),
typeof(AetheriaRunState),
typeof(AetheriaZoneState),
typeof(AetheriaEntitySnapshot),
typeof(AetheriaRuntimeAuthorityLeaseDocument),
typeof(AetheriaRuntimeDaemonCommandDocument),
typeof(AetheriaRuntimeCommittedCommandFactDocument),
typeof(AetheriaRuntimeEveCommandDocument),
typeof(EveSurfaceCommandRequest),
typeof(EveCommandReceiptDocument)
};
var schemaIds = schemaTypes
.Select(registry.GetRequired)
.SelectMany(descriptor => descriptor.ToCatalogEntry().CompatibleSchemaIds.Append(descriptor.SchemaId))
.ToHashSet(StringComparer.Ordinal);
var recordKeys = new HashSet<string>(StringComparer.Ordinal)
{
WorldKey.ToString(),
RuntimeCatalogKey.ToString(),
TradeValuePolicyKey.ToString(),
PlayerSettingsKey.ToString(),
VerseHostSettingsKey.ToString(),
GameSessionStateKey.ToString(),
MainMenuStateKey.ToString(),
EveCommandAcceptanceStatusKey.ToString(),
AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest.ToString(),
AetheriaRuntimeVerseRecordKeys.VerseAuthorityPolicy.ToString(),
AetheriaRuntimeVerseRecordKeys.StarbridgeScenarioLatest.ToString(),
AetheriaRuntimeVerseRecordKeys.StarbridgeSessionLatest.ToString()
};
return record => recordKeys.Contains(record.Key) || schemaIds.Contains(record.SchemaId);
}
public CultMeshDocumentHandle<TDocument> Document<TDocument>(CultRecordKey key)
where TDocument : class
{
@ -127,6 +186,50 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
routeHint: new CultMeshRouteHint(CultMeshLocalityKind.SharedMemory, "Aetheria typed catalog state"));
}
public CultMeshDocumentHandle<AetheriaRuntimeNameCorpusSnapshot> RuntimeNameCorpus()
{
return _runtimeNameCorpus ??= CultMesh.Document(
"aetheria.catalog.runtime-names",
CultMesh.Verse("aetheria.local", RuntimeId),
async _ => await Database.GetAsync<AetheriaRuntimeNameCorpusSnapshot>(RuntimeNameCorpusKey)
.ConfigureAwait(false)
?? throw new InvalidDataException("The compiled Aetheria runtime name corpus is missing."),
_ => Database.WatchRecord<AetheriaRuntimeNameCorpusSnapshot>(RuntimeNameCorpusKey)
.Where(change => change.Document != null)
.Select(change => change.Document!),
sources: new[]
{
CultMesh.ProjectionSource(
RuntimeNameCorpusKey.ToString(),
AetheriaRuntimeNameCorpusSnapshot.SchemaId,
"managed Aetheria runtime name corpus")
},
routeHint: new CultMeshRouteHint(
CultMeshLocalityKind.SharedMemory,
"Aetheria typed name generation corpus"));
}
public async Task<AetheriaRuntimeCatalogSnapshot> RuntimeCatalogForGenerationAsync()
{
var catalog = RuntimeCatalog().Latest()
?? throw new InvalidDataException("The compiled Aetheria runtime catalog is missing.");
if (Cache.Get<AetheriaRuntimeNameCorpusSnapshot>(RuntimeNameCorpusKey) == null)
{
await Cache.PullBackingStoreRecordsAsync(metadata =>
string.Equals(metadata.Key, RuntimeNameCorpusKey.ToString(), StringComparison.Ordinal)).ConfigureAwait(false);
}
var corpus = await RuntimeNameCorpus().LatestAsync().ConfigureAwait(false);
return new AetheriaRuntimeCatalogSnapshot(
catalog.Items.ToArray(),
catalog.Corporations.ToArray(),
corpus.NameFiles.ToArray(),
catalog.TradeValueSettings)
{
CatalogId = catalog.CatalogId,
NameCorpusRecordKey = RuntimeNameCorpusKey.ToString()
};
}
public CultMeshDocumentHandle<EveSurfaceDocument> CatalogSurface()
{
var catalog = RuntimeCatalog();
@ -157,6 +260,21 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
public async Task<AetheriaRuntimeCatalogSnapshot> RefreshRuntimeCatalogAsync()
{
var snapshot = AetheriaRuntimeCatalogStore.OpenReadOnly(StatePath);
await Database.PutAsync(
RuntimeNameCorpusKey,
new AetheriaRuntimeNameCorpusSnapshot
{
NameFiles = snapshot.NameFiles.ToArray()
}).ConfigureAwait(false);
snapshot.NameFiles = snapshot.NameFiles
.Select(nameFile => new AetheriaRuntimeNameFile(
nameFile.NameFileKey,
nameFile.Name,
nameFile.NameCount,
nameFile.SampleNames.ToArray(),
Array.Empty<string>()))
.ToArray();
snapshot.NameCorpusRecordKey = RuntimeNameCorpusKey.ToString();
await Database.PutAsync(RuntimeCatalogKey, snapshot).ConfigureAwait(false);
return snapshot;
}
@ -252,6 +370,9 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
public static CultRecordKey RuntimeCatalogKey { get; } =
new("global:aetheria.runtime_catalog.v1");
public static CultRecordKey RuntimeNameCorpusKey { get; } =
new("global:aetheria.runtime_name_corpus.v1");
public static CultRecordKey OperationsSurfaceKey { get; } =
new(AetheriaEveSurfaceDocuments.OperationsSurfaceKey);

View file

@ -7,7 +7,7 @@
"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.39",
"org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.40",
"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.99"

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.39",
"version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.40",
"depth": 0,
"source": "git",
"dependencies": {},
"hash": "54e2e69ff7f4f5e0a1316bb047c315e65ed2a185"
"hash": "cce542d82467a80519803c04f0c4e43c7e105fe8"
},
"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",
@ -156,7 +156,7 @@
"depth": 0,
"source": "git",
"dependencies": {
"org.gamecult.cultlib": "1.0.39",
"org.gamecult.cultlib": "1.0.40",
"org.gamecult.eve.plugin-fields": "0.2.3",
"org.gamecult.eve.surface": "0.2.4",
"com.unity.mathematics": "1.3.2",

View file

@ -66,6 +66,9 @@ namespace GameCult.Aetheria.State.Verse
[Key(4)]
public AetheriaRuntimeTradeValueSettings TradeValueSettings { get; set; } = AetheriaRuntimeTradeValueSettings.Default;
[Key(5)]
public string NameCorpusRecordKey { get; set; } = string.Empty;
public AetheriaRuntimeCatalogItem? FindItem(string itemKey)
{
if (string.IsNullOrWhiteSpace(itemKey))
@ -138,6 +141,21 @@ namespace GameCult.Aetheria.State.Verse
}
}
[CultDocument("gamecult.aetheria.runtime_name_corpus", "gamecult.aetheria.runtime_name_corpus.v1")]
[MessagePackObject]
public sealed class AetheriaRuntimeNameCorpusSnapshot
{
public const string SchemaId = "gamecult.aetheria.runtime_name_corpus.v1";
public const string DocumentName = "aetheria.runtime_name_corpus";
[Key(0)]
[CultName]
public string CorpusId { get; set; } = DocumentName;
[Key(1)]
public IReadOnlyList<AetheriaRuntimeNameFile> NameFiles { get; set; } = Array.Empty<AetheriaRuntimeNameFile>();
}
[CultDocument("gamecult.aetheria.loadout_templates", "gamecult.aetheria.loadout_templates.v1")]
[MessagePackObject]
public sealed class AetheriaRuntimeLoadoutTemplatesDocument

View file

@ -232,13 +232,18 @@ namespace GameCult.Aetheria.State.Verse
var records = new List<PersistedRecord>();
var reader = new MessagePackReader(File.ReadAllBytes(stateFilePath));
var snapshotFields = reader.ReadArrayHeader();
if (snapshotFields > 0) reader.Skip();
var formatVersion = snapshotFields > 0 ? ReadString(ref reader) : "";
if (snapshotFields > 1) reader.Skip();
if (snapshotFields > 2)
{
var recordCount = reader.ReadArrayHeader();
for (var index = 0; index < recordCount; index++)
records.Add(ReadPersistedRecord(ref reader));
{
if (string.Equals(formatVersion, "cultcache.store.v2.directory-indexed", StringComparison.Ordinal))
reader.Skip();
else
records.Add(ReadPersistedRecord(ref reader));
}
}
var recordDirectory = stateFilePath + ".records";

View file

@ -1,6 +1,6 @@
# Aetheria Current Codebase Model
Date: 2026-07-19
Date: 2026-07-20
This is a modeling pass over the live codebase as it exists now. It is not the
ideal architecture; it is the current control-flow map, with migration pressure
@ -37,7 +37,10 @@ Current daemon control flow:
1. Parse daemon options: state path, daemon id, session id, Verse id, CultMesh
address, tick interval, fixed delta.
2. Open `AetheriaStateNode` with `startServer: true`.
2. Open `AetheriaStateNode` with `startServer: true` and the daemon boot
hydration profile. The indexed CultCache manifest selects the control-plane
schemas and exact singleton records required to start; authored name files,
the compiled name corpus, asset bodies, and unrelated history remain cold.
3. Start `AetheriaVerseDiscoveryHost`.
4. Ensure world, Verse host settings, runtime session, compatibility surfaces,
and first daemon tick.
@ -67,6 +70,43 @@ intent, docking, loot, trade, loadout, and action-bar operations, but it is not
yet a complete standalone simulation engine for every behavior that still lives
under Unity `Assets/Scripts/ServerShared/Behaviors`.
### Daemon boot working set
- **Owner:** `AetheriaStateNode` owns the daemon boot record selection;
`DirectoryMessagePackBackingStore` owns key-to-page discovery and durable
retention. A service registry contains only Aetheria's explicitly declared
document types.
- **Inputs:** the indexed manifest, exact singleton boot keys, and the schema
identities for live commands, receipts, item/loadout definitions, and
current run/zone/entity state.
- **Outputs:** a compact live CultCache working set. The runtime catalog core
contains items, corporations, trade settings, and name-file summaries; the
full name corpus is a separate typed cold record loaded explicitly only when
generation needs it.
- **Derived state:** the catalog Eve surface and ordinary daemon ticks use the
compact core. Authored name files remain migration/compiler inputs, not boot
dependencies.
- **Forbidden writers:** daemon boot must not rescan record pages, reread legacy
name files, or let global assembly discovery expand its registry. A filtered
flush must not erase cold records.
- **Shared paths:** first import writes the indexed representation; daemon boot
selects its working set from that index; new-run generation uses CultCache's
selective pull primitive to hydrate the name corpus through the same open
store.
- **Cut line:** the monolithic runtime catalog no longer embeds twelve megabytes
of names, and menu bootstrap no longer rescans loadout pages from disk.
- **Verification layer:** backing-store tests hold an unrelated cold page under
an exclusive lock while selectively hydrating another record. Startup phase
traces report state open, catalog, Ymir restore, and client publication costs.
The remaining restart bottleneck is inside Ymir reconstruction, not CultCache
I/O: the current resume format validates state by replaying its entire
generation journal. Indexed hydration reduced journal page loading to tens of
milliseconds, but replay remains proportional to session lifetime. Ymir needs
a bounded baseline checkpoint plus journal tail before daemon restart cost is
independent of uptime; increasing timeouts or hiding the phase is not an
acceptable substitute.
## Materialized Import And Publication Ownership
### Catalog import