From 0479304d0b84aec8623dfea0a88897c4a5f98557 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 30 Sep 2026 01:56:03 +0200 Subject: [PATCH 1/5] 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], From ef8f76a0d4baa5483067c7f617f1d79ad26f4178 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 30 Sep 2026 02:15:47 +0200 Subject: [PATCH 2/5] Geometry: register the cache's CultRecordRef formatter with the MessagePack generator The MessagePack source generator emitted a broken formatter reference for the CultRecordRef reference members. Declaring CultRecordRefFormatter<> a known formatter makes the generated resolver defer to the cache's own. Co-Authored-By: Claude Sonnet 5.5 --- src/GameCult.Geometry/CultGeometryDocuments.cs | 4 ++++ src/GameCult.Geometry/GameCult.Geometry.csproj | 1 + 2 files changed, 5 insertions(+) diff --git a/src/GameCult.Geometry/CultGeometryDocuments.cs b/src/GameCult.Geometry/CultGeometryDocuments.cs index 0da8af90..5d9ddaa9 100644 --- a/src/GameCult.Geometry/CultGeometryDocuments.cs +++ b/src/GameCult.Geometry/CultGeometryDocuments.cs @@ -7,6 +7,10 @@ using GameCult.Caching; using MessagePack; using static GameCult.Geometry.CultGeometryDocuments; +// The generator emits a resolver for these [MessagePackObject] documents; without this it would try to +// generate a formatter for the CultRecordRef reference members instead of using the cache's. +[assembly: MessagePackKnownFormatter(typeof(CultRecordRefFormatter<>))] + namespace GameCult.Geometry { /// diff --git a/src/GameCult.Geometry/GameCult.Geometry.csproj b/src/GameCult.Geometry/GameCult.Geometry.csproj index c10c2ecf..c1588906 100644 --- a/src/GameCult.Geometry/GameCult.Geometry.csproj +++ b/src/GameCult.Geometry/GameCult.Geometry.csproj @@ -10,6 +10,7 @@ + From e35b0092042aecc902854cd4cfbcb8f48b0266fb Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 30 Sep 2026 02:15:56 +0200 Subject: [PATCH 3/5] Geometry: fix the Caching.MessagePack project reference path The previous commit lost its backslashes. Co-Authored-By: Claude Sonnet 5.5 --- src/GameCult.Geometry/GameCult.Geometry.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/GameCult.Geometry/GameCult.Geometry.csproj b/src/GameCult.Geometry/GameCult.Geometry.csproj index c1588906..cc66cba9 100644 --- a/src/GameCult.Geometry/GameCult.Geometry.csproj +++ b/src/GameCult.Geometry/GameCult.Geometry.csproj @@ -10,7 +10,7 @@ - + From 9ea134aca948ae864e51638526421caa966ee947 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 30 Sep 2026 02:25:27 +0200 Subject: [PATCH 4/5] Geometry: name the cache formatter on each CultRecordRef member The MessagePack source generator cannot resolve CultRecordRef on its own and emitted a broken formatter reference; MessagePackKnownFormatter on the open generic did not change that. An explicit MessagePackFormatter per member is the same formatter the cache resolver uses, so the wire stays a key string. Co-Authored-By: Claude Sonnet 5.5 --- src/GameCult.Geometry/CultGeometryDocuments.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/GameCult.Geometry/CultGeometryDocuments.cs b/src/GameCult.Geometry/CultGeometryDocuments.cs index 5d9ddaa9..1ce73bdb 100644 --- a/src/GameCult.Geometry/CultGeometryDocuments.cs +++ b/src/GameCult.Geometry/CultGeometryDocuments.cs @@ -7,10 +7,6 @@ using GameCult.Caching; using MessagePack; using static GameCult.Geometry.CultGeometryDocuments; -// The generator emits a resolver for these [MessagePackObject] documents; without this it would try to -// generate a formatter for the CultRecordRef reference members instead of using the cache's. -[assembly: MessagePackKnownFormatter(typeof(CultRecordRefFormatter<>))] - namespace GameCult.Geometry { /// @@ -157,6 +153,7 @@ namespace GameCult.Geometry public string RequestId { get; set; } = string.Empty; [Key(1)] + [MessagePackFormatter(typeof(CultRecordRefFormatter))] public CultRecordRef DomainKey { get; set; } [Key(2)] @@ -231,6 +228,7 @@ namespace GameCult.Geometry public string CutId { get; set; } = string.Empty; [Key(1)] + [MessagePackFormatter(typeof(CultRecordRefFormatter))] public CultRecordRef RequestKey { get; set; } [Key(2)] @@ -315,6 +313,7 @@ namespace GameCult.Geometry public string ChunkId { get; set; } = string.Empty; [Key(1)] + [MessagePackFormatter(typeof(CultRecordRefFormatter))] public CultRecordRef CutKey { get; set; } [Key(2)] From fd34d4573e757ba845e0c975c1197b814d7152bf Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 30 Sep 2026 02:30:43 +0200 Subject: [PATCH 5/5] Geometry tests: import System.Linq for the reference walk test Co-Authored-By: Claude Sonnet 5.5 --- tests/GameCult.Geometry.Tests/GeometryDocumentTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/GameCult.Geometry.Tests/GeometryDocumentTests.cs b/tests/GameCult.Geometry.Tests/GeometryDocumentTests.cs index 2576c295..b5f63775 100644 --- a/tests/GameCult.Geometry.Tests/GeometryDocumentTests.cs +++ b/tests/GameCult.Geometry.Tests/GeometryDocumentTests.cs @@ -10,6 +10,7 @@ using R3; using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text.Json; using System.Threading.Tasks;