From ee3fb2092b8cf6d24a175e91d925b7986bdb2bee Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 19 Jul 2026 09:18:25 +0100 Subject: [PATCH] Index runtime catalog item lookup --- Aetheria.State.Daemon.Smoke/Program.cs | 17 ++++++++++ .../Runtime/AetheriaRuntimeCatalogSnapshot.cs | 32 +++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/Aetheria.State.Daemon.Smoke/Program.cs b/Aetheria.State.Daemon.Smoke/Program.cs index 947a83719..e91329703 100644 --- a/Aetheria.State.Daemon.Smoke/Program.cs +++ b/Aetheria.State.Daemon.Smoke/Program.cs @@ -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); diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs index d69f9733c..8ec213e3d 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogSnapshot.cs @@ -12,6 +12,9 @@ namespace GameCult.Aetheria.State.Verse [MessagePackObject] public sealed class AetheriaRuntimeCatalogSnapshot { + private IReadOnlyList _items = Array.Empty(); + private Dictionary? _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 Items { get; set; } = Array.Empty(); + public IReadOnlyList Items + { + get => _items; + set + { + _items = value ?? Array.Empty(); + _itemsByKey = null; + } + } [IgnoreMember] public IReadOnlyList 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? item, Func 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 BuildItemIndex( + IEnumerable items) + { + var index = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var item in items ?? Enumerable.Empty()) + { + 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")]