Index runtime catalog item lookup

This commit is contained in:
Meta Vens 2026-07-19 09:18:25 +01:00
commit ee3fb2092b
2 changed files with 47 additions and 2 deletions

View file

@ -101,6 +101,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
RunCheck(StableEntityIdentitySurvivesCrossZoneReindex);
RunCheck(TractorRampsAndPullsThroughYmirWithoutTeleportingCargo);
RunCheck(PickupProximityPrecedesYmirCollisionShell);
RunCheck(CatalogItemIndexPreservesSnapshotLookupSemantics);
RunCheck(PickupIsCapacityCheckedExactlyOnceAndExpires);
RunCheck(StationBodiesCannotConsumePickups);
RunCheck(PickupProximityCollectsOrBounces);
@ -8286,6 +8287,22 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
"a proximity-collected pickup must leave authoritative world state exactly once");
}
private static void CatalogItemIndexPreservesSnapshotLookupSemantics()
{
var first = CatalogItem("Indexed-Item");
var duplicate = CatalogItem("indexed-item");
var catalog = new AetheriaRuntimeCatalogSnapshot([first, duplicate], [], []);
Require(ReferenceEquals(first, catalog.FindItem("INDEXED-ITEM")),
"the derived catalog index must preserve first case-insensitive match semantics");
var replacement = CatalogItem("replacement-item");
catalog.Items = [replacement];
Require(catalog.FindItem("indexed-item") == null &&
ReferenceEquals(replacement, catalog.FindItem("REPLACEMENT-ITEM")),
"replacing snapshot items must invalidate the derived lookup index");
}
private static void PickupIsCapacityCheckedExactlyOnceAndExpires()
{
var hull = CatalogItem("pickup-hull"); hull.HullCapacity = PerformanceStat(1);

View file

@ -12,6 +12,9 @@ namespace GameCult.Aetheria.State.Verse
[MessagePackObject]
public sealed class AetheriaRuntimeCatalogSnapshot
{
private IReadOnlyList<AetheriaRuntimeCatalogItem> _items = Array.Empty<AetheriaRuntimeCatalogItem>();
private Dictionary<string, AetheriaRuntimeCatalogItem>? _itemsByKey;
public const string SchemaId = "gamecult.aetheria.runtime_catalog.v1";
public const string DocumentName = "aetheria.runtime_catalog";
@ -36,7 +39,15 @@ namespace GameCult.Aetheria.State.Verse
public string CatalogId { get; set; } = DocumentName;
[Key(1)]
public IReadOnlyList<AetheriaRuntimeCatalogItem> Items { get; set; } = Array.Empty<AetheriaRuntimeCatalogItem>();
public IReadOnlyList<AetheriaRuntimeCatalogItem> Items
{
get => _items;
set
{
_items = value ?? Array.Empty<AetheriaRuntimeCatalogItem>();
_itemsByKey = null;
}
}
[IgnoreMember]
public IReadOnlyList<AetheriaRuntimeCatalogItem> TradeItems =>
@ -57,7 +68,11 @@ namespace GameCult.Aetheria.State.Verse
public AetheriaRuntimeCatalogItem? FindItem(string itemKey)
{
return TryGet(Items, itemKey, item => item.ItemKey);
if (string.IsNullOrWhiteSpace(itemKey))
return null;
_itemsByKey ??= BuildItemIndex(Items);
return _itemsByKey.TryGetValue(itemKey, out var item) ? item : null;
}
public AetheriaRuntimeCatalogItem? FindItem<T>(T? item, Func<T, string?> itemKey) where T : class
@ -108,6 +123,19 @@ namespace GameCult.Aetheria.State.Verse
? null
: values.FirstOrDefault(value => string.Equals(getKey(value), key, StringComparison.OrdinalIgnoreCase));
}
private static Dictionary<string, AetheriaRuntimeCatalogItem> BuildItemIndex(
IEnumerable<AetheriaRuntimeCatalogItem> items)
{
var index = new Dictionary<string, AetheriaRuntimeCatalogItem>(StringComparer.OrdinalIgnoreCase);
foreach (var item in items ?? Enumerable.Empty<AetheriaRuntimeCatalogItem>())
{
if (item == null || string.IsNullOrWhiteSpace(item.ItemKey))
continue;
index.TryAdd(item.ItemKey, item);
}
return index;
}
}
[CultDocument("gamecult.aetheria.loadout_templates", "gamecult.aetheria.loadout_templates.v1")]