Add typed catalog snapshot queries

This commit is contained in:
Meta Vens 2026-06-13 08:24:37 +01:00
commit 6272408db2
148 changed files with 363 additions and 229 deletions

Binary file not shown.

View file

@ -240,7 +240,7 @@ internal static class LegacyCatalogReader
Description = description,
Mass = GetDouble(payload, 4),
Volume = shape.OccupiedCells,
ManufacturerLegacyId = GetGuid(payload, 3),
ManufacturerLegacyId = GetOptionalGuid(payload, 3),
Price = GetInt(payload, 8),
ShapeWidth = shape.Width,
ShapeHeight = shape.Height,
@ -255,8 +255,8 @@ internal static class LegacyCatalogReader
LegacyId = legacyId,
ShortName = GetString(payload, 2),
Description = GetString(payload, 3),
GeonameFileLegacyId = GetGuid(payload, 9),
BossHullLegacyId = GetGuid(payload, 10),
GeonameFileLegacyId = GetOptionalGuid(payload, 9),
BossHullLegacyId = GetOptionalGuid(payload, 10),
InfluenceDistance = GetInt(payload, 11),
AllegianceCount = GetMapCount(payload, 12),
OverworldMusic = GetUInt(payload, 13),
@ -374,6 +374,12 @@ internal static class LegacyCatalogReader
: "";
}
private static string GetOptionalGuid(IReadOnlyDictionary<int, object?> payload, int key)
{
var legacyId = GetGuid(payload, key);
return Guid.TryParse(legacyId, out var guid) && guid == Guid.Empty ? "" : legacyId;
}
private static string GetString(IReadOnlyDictionary<int, object?> payload, int key)
{
return payload.TryGetValue(key, out var value) ? value as string ?? "" : "";

View file

@ -16,6 +16,7 @@ var quarantine = await node.GetLegacyCatalogQuarantineAsync()
var items = node.Cache.GetAll<AetheriaItemDefinition>().ToArray();
var corporations = node.Cache.GetAll<AetheriaCorporation>().ToArray();
var nameFiles = node.Cache.GetAll<AetheriaNameFile>().ToArray();
var catalog = node.ReadCatalogSnapshot();
RequireCount(ledger, "aetheria.item_definition.v1", items.Length);
RequireCount(ledger, "aetheria.corporation.v1", corporations.Length);
@ -67,6 +68,30 @@ if (corporationNameLinks == 0)
throw new InvalidOperationException("Typed corporations did not import geoname file legacy IDs.");
}
var tradeItems = catalog.TradeItems.ToArray();
if (tradeItems.Length != pricedItems)
{
throw new InvalidOperationException(
$"Typed catalog trade item query mismatch: query={tradeItems.Length}, priced={pricedItems}.");
}
var manufacturedItem = items.FirstOrDefault(item => !string.IsNullOrWhiteSpace(item.ManufacturerLegacyId))
?? throw new InvalidOperationException("Cannot verify typed catalog manufacturer lookup: no manufactured item.");
if (catalog.GetManufacturer(manufacturedItem) == null)
{
throw new InvalidOperationException(
$"Typed catalog manufacturer lookup failed for item {manufacturedItem.Name}.");
}
var corporationWithNames = corporations.FirstOrDefault(corporation =>
!string.IsNullOrWhiteSpace(corporation.GeonameFileLegacyId))
?? throw new InvalidOperationException("Cannot verify typed catalog name-file lookup: no linked corporation.");
if (catalog.GetNameFile(corporationWithNames) == null)
{
throw new InvalidOperationException(
$"Typed catalog name-file lookup failed for corporation {corporationWithNames.Name}.");
}
await RequireLegacyLookupAsync(
items[0].LegacyId,
() => node.GetItemDefinitionByLegacyIdAsync(items[0].LegacyId),
@ -89,6 +114,7 @@ Console.WriteLine($"Aetheria typed state verify passed: {statePath}");
Console.WriteLine($"Catalog fingerprint: {quarantine.CatalogFingerprint}");
Console.WriteLine($"Item definitions: {items.Length}");
Console.WriteLine($"Priced/manufactured/shaped items: {pricedItems}/{manufacturedItems}/{shapedItems}");
Console.WriteLine($"Typed catalog trade items: {tradeItems.Length}");
Console.WriteLine($"Corporations: {corporations.Length}");
Console.WriteLine($"Described/geoname-linked corporations: {describedCorporations}/{corporationNameLinks}");
Console.WriteLine($"Name files: {nameFiles.Length}");

View file

@ -0,0 +1,80 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Aetheria.State.Documents;
namespace Aetheria.State;
public sealed class AetheriaCatalogSnapshot
{
private readonly Dictionary<string, AetheriaItemDefinition> _itemsByLegacyId;
private readonly Dictionary<string, AetheriaCorporation> _corporationsByLegacyId;
private readonly Dictionary<string, AetheriaNameFile> _nameFilesByLegacyId;
public AetheriaCatalogSnapshot(
IEnumerable<AetheriaItemDefinition> items,
IEnumerable<AetheriaCorporation> corporations,
IEnumerable<AetheriaNameFile> nameFiles)
{
Items = items.OrderBy(item => item.Name, StringComparer.OrdinalIgnoreCase).ToArray();
Corporations = corporations.OrderBy(corporation => corporation.Name, StringComparer.OrdinalIgnoreCase).ToArray();
NameFiles = nameFiles.OrderBy(nameFile => nameFile.Name, StringComparer.OrdinalIgnoreCase).ToArray();
_itemsByLegacyId = Items
.Where(item => !string.IsNullOrWhiteSpace(item.LegacyId))
.ToDictionary(item => item.LegacyId, StringComparer.OrdinalIgnoreCase);
_corporationsByLegacyId = Corporations
.Where(corporation => !string.IsNullOrWhiteSpace(corporation.LegacyId))
.ToDictionary(corporation => corporation.LegacyId, StringComparer.OrdinalIgnoreCase);
_nameFilesByLegacyId = NameFiles
.Where(nameFile => !string.IsNullOrWhiteSpace(nameFile.LegacyId))
.ToDictionary(nameFile => nameFile.LegacyId, StringComparer.OrdinalIgnoreCase);
}
public IReadOnlyList<AetheriaItemDefinition> Items { get; }
public IReadOnlyList<AetheriaCorporation> Corporations { get; }
public IReadOnlyList<AetheriaNameFile> NameFiles { get; }
public IEnumerable<AetheriaItemDefinition> TradeItems =>
Items.Where(item => item.Price > 0).OrderBy(item => item.Name, StringComparer.OrdinalIgnoreCase);
public AetheriaItemDefinition? FindItemByLegacyId(string legacyId)
{
return TryGet(_itemsByLegacyId, legacyId);
}
public AetheriaCorporation? FindCorporationByLegacyId(string legacyId)
{
return TryGet(_corporationsByLegacyId, legacyId);
}
public AetheriaNameFile? FindNameFileByLegacyId(string legacyId)
{
return TryGet(_nameFilesByLegacyId, legacyId);
}
public AetheriaCorporation? FindCorporationByNamePrefix(string namePrefix)
{
return string.IsNullOrWhiteSpace(namePrefix)
? null
: Corporations.FirstOrDefault(corporation =>
corporation.Name.StartsWith(namePrefix, StringComparison.InvariantCultureIgnoreCase));
}
public AetheriaCorporation? GetManufacturer(AetheriaItemDefinition item)
{
return FindCorporationByLegacyId(item.ManufacturerLegacyId);
}
public AetheriaNameFile? GetNameFile(AetheriaCorporation corporation)
{
return FindNameFileByLegacyId(corporation.GeonameFileLegacyId);
}
private static T? TryGet<T>(IReadOnlyDictionary<string, T> dictionary, string key) where T : class
{
return string.IsNullOrWhiteSpace(key) ? null : dictionary.TryGetValue(key, out var value) ? value : null;
}
}

View file

@ -155,6 +155,14 @@ public sealed class AetheriaStateNode : IAsyncDisposable, IDisposable
return GetNameFileAsync(AetheriaCatalogKeys.NameFileFromLegacyId(legacyId));
}
public AetheriaCatalogSnapshot ReadCatalogSnapshot()
{
return new AetheriaCatalogSnapshot(
Cache.GetAll<AetheriaItemDefinition>(),
Cache.GetAll<AetheriaCorporation>(),
Cache.GetAll<AetheriaNameFile>());
}
public Task<CultRecordHandle<AetheriaPlayerSettings>> PutPlayerSettingsAsync(AetheriaPlayerSettings settings)
{
return Database.PutAsync(new CultRecordKey("global:aetheria.player_settings.v1"), settings);

View file

@ -52,12 +52,20 @@ field from key 3, name-file and boss-hull legacy IDs, influence distance,
allegiance count, and music bank IDs. Runtime object graphs, behaviors, full
Unity shape masks, and simulation state remain legacy until dedicated typed
documents exist.
Legacy GUID references that are `Guid.Empty` are imported as absent references,
not as resolvable catalog links.
`AetheriaCatalogSnapshot` is the typed read surface over materialized catalog
documents. It exposes trade items, legacy-ID lookup, manufacturer lookup,
corporation prefix lookup, and corporation name-file lookup without reading
`DatabaseEntry` objects.
`Aetheria.State.Verify` opens a materialized state file and checks that the
typed migration ledger matches the actual item, corporation, and name-file
records in the `.cc` store. It also verifies the canonical legacy-ID lookup API
for one migrated record of each catalog kind and checks that expanded item and
corporation catalog facts survived materialization. Use it after import when
corporation catalog facts survived materialization. The verifier also exercises
the typed catalog snapshot query surface. Use it after import when
`GameData/aetheria-world.cc` changes.
Current rebuild notes:

Some files were not shown because too many files have changed in this diff Show more