Geometry: reference members are CultRecordRef<T>, not [CultReference] strings

e420410f (D11) refuses a reference member whose CLR shape the cache cannot
walk; many:false needs CultRecordRef<T>. 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<T> (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 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 01:56:03 +02:00
commit 0479304d0b
2 changed files with 28 additions and 11 deletions

View file

@ -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<CultGeometryDomainDocument> 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<CultGeometryBuildRequest> RequestKey { get; set; }
[Key(2)]
public string[] SelectedNodes { get; set; } = Array.Empty<string>();
@ -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<CultGeometrySelectedCutManifest> 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),

View file

@ -104,7 +104,7 @@ namespace GameCult.Geometry.Tests
var domainKey = CultGeometryDomainDocument.CreateRecordKey(domain);
var request = new CultGeometryBuildRequest
{
DomainKey = domainKey.Value,
DomainKey = new CultRecordRef<CultGeometryDomainDocument>(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<T> 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<CultGeometryBuildRequest>();
descriptor.DeclaredMembers.Should().Contain(m => m.MemberName == "DomainKey" && m.IsReference && !m.IsMany);
var chunk = SampleChunk();
var references = cache.Registry.GetRequired<CultGeometryChunkArtifact>().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<CultGeometrySelectedCutManifest>(new CultRecordKey("geometry:cut:test")),
SelectedCutId = "cut-test",
BoundsMin = [-1f, -1f, 0f],
BoundsMax = [1f, 1f, 1f],