From 0479304d0b84aec8623dfea0a88897c4a5f98557 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 30 Sep 2026 01:56:03 +0200 Subject: [PATCH] Geometry: reference members are CultRecordRef, not [CultReference] strings e420410f (D11) refuses a reference member whose CLR shape the cache cannot walk; many:false needs CultRecordRef. CultGeometryBuildRequest.DomainKey, CultGeometrySelectedCutManifest.RequestKey and CultGeometryChunkArtifact.CutKey were strings marked [CultReference], so registering the geometry catalog threw and 7 geometry tests failed on main. All three are record keys of another document, so the members become CultRecordRef (the attribute is dropped, the type declares the reference). The msgpack formatter writes a CultRecordRef as its key string, so the wire bytes and the fingerprint inputs are unchanged. Co-Authored-By: Claude Sonnet 5.5 --- .../CultGeometryDocuments.cs | 15 +++++------- .../GeometryDocumentTests.cs | 24 +++++++++++++++++-- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/GameCult.Geometry/CultGeometryDocuments.cs b/src/GameCult.Geometry/CultGeometryDocuments.cs index 306b5849..0da8af90 100644 --- a/src/GameCult.Geometry/CultGeometryDocuments.cs +++ b/src/GameCult.Geometry/CultGeometryDocuments.cs @@ -153,8 +153,7 @@ namespace GameCult.Geometry public string RequestId { get; set; } = string.Empty; [Key(1)] - [CultReference(typeof(CultGeometryDomainDocument))] - public string DomainKey { get; set; } = string.Empty; + public CultRecordRef DomainKey { get; set; } [Key(2)] [CultIndex] @@ -200,7 +199,7 @@ namespace GameCult.Geometry { if (request == null) throw new ArgumentNullException(nameof(request)); return new CultRecordKey("geometry:request:" + StableHash( - request.DomainKey, + request.DomainKey.Key.Value, request.WorkerGroup, StableVector(request.CameraPosition), StableVector(request.FrustumMin), @@ -228,8 +227,7 @@ namespace GameCult.Geometry public string CutId { get; set; } = string.Empty; [Key(1)] - [CultReference(typeof(CultGeometryBuildRequest))] - public string RequestKey { get; set; } = string.Empty; + public CultRecordRef RequestKey { get; set; } [Key(2)] public string[] SelectedNodes { get; set; } = Array.Empty(); @@ -247,7 +245,7 @@ namespace GameCult.Geometry { if (cut == null) throw new ArgumentNullException(nameof(cut)); return new CultRecordKey("geometry:cut:" + StableHash( - cut.RequestKey, + cut.RequestKey.Key.Value, cut.CutId, StableArray(cut.SelectedNodes), StableArray(cut.DeferredChildRequests), @@ -313,8 +311,7 @@ namespace GameCult.Geometry public string ChunkId { get; set; } = string.Empty; [Key(1)] - [CultReference(typeof(CultGeometrySelectedCutManifest))] - public string CutKey { get; set; } = string.Empty; + public CultRecordRef CutKey { get; set; } [Key(2)] [CultIndex] @@ -357,7 +354,7 @@ namespace GameCult.Geometry { if (chunk == null) throw new ArgumentNullException(nameof(chunk)); return new CultRecordKey("geometry:chunk:" + StableHash( - chunk.CutKey, + chunk.CutKey.Key.Value, chunk.ChunkId, chunk.SelectedCutId, StableArray(chunk.SourceDomainKeys), diff --git a/tests/GameCult.Geometry.Tests/GeometryDocumentTests.cs b/tests/GameCult.Geometry.Tests/GeometryDocumentTests.cs index 8741f891..2576c295 100644 --- a/tests/GameCult.Geometry.Tests/GeometryDocumentTests.cs +++ b/tests/GameCult.Geometry.Tests/GeometryDocumentTests.cs @@ -104,7 +104,7 @@ namespace GameCult.Geometry.Tests var domainKey = CultGeometryDomainDocument.CreateRecordKey(domain); var request = new CultGeometryBuildRequest { - DomainKey = domainKey.Value, + DomainKey = new CultRecordRef(domainKey), WorkerGroup = "ragnarok-column-workers", CameraPosition = [36f, -42f, 30f], FrustumMin = [-75f, -75f, -30f], @@ -129,6 +129,26 @@ namespace GameCult.Geometry.Tests first.Value.Should().StartWith("geometry:request:"); } + // The geometry documents reference each other by record key. The reference members are + // CultRecordRef so the cache can walk them, and they stay a bare msgpack string on the wire. + [Test] + public void GeometryReferences_AreWalkableByTheCache_AndStayStringsOnTheWire() + { + var cache = new CultCache(); + var descriptor = cache.Registry.GetRequired(); + descriptor.DeclaredMembers.Should().Contain(m => m.MemberName == "DomainKey" && m.IsReference && !m.IsMany); + + var chunk = SampleChunk(); + var references = cache.Registry.GetRequired().ReferencesOf(chunk, "CutKey").ToArray(); + references.Should().ContainSingle().Which.Target.Value.Should().Be("geometry:cut:test"); + + var payload = CultDocumentMessagePackSerialization.SerializeUntyped(chunk, typeof(CultGeometryChunkArtifact)); + var reader = new MessagePackReader(payload); + reader.ReadArrayHeader(); + reader.ReadString().Should().Be(chunk.ChunkId); + reader.ReadString().Should().Be("geometry:cut:test"); + } + [Test] public void ChunkArtifact_RoundTrips_ThroughCultCacheMessagePack() { @@ -382,7 +402,7 @@ namespace GameCult.Geometry.Tests return new CultGeometryChunkArtifact { ChunkId = "chunk/ragnarok-column/column-00", - CutKey = "geometry:cut:test", + CutKey = new CultRecordRef(new CultRecordKey("geometry:cut:test")), SelectedCutId = "cut-test", BoundsMin = [-1f, -1f, 0f], BoundsMax = [1f, 1f, 1f],