Merge Geometry references as CultRecordRef<T>

DomainKey, RequestKey and CutKey were plain strings marked [CultReference];
since e420410f (D11) the cache refuses a reference it cannot walk, and
every cache that registered the geometry catalog threw (7 tests red since
2026-09-22). The wire is unchanged. Geometry gains a reference to
GameCult.Caching.MessagePack for the formatter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-09-30 02:49:38 +02:00
commit a6720b620f
3 changed files with 33 additions and 11 deletions

View file

@ -153,8 +153,8 @@ namespace GameCult.Geometry
public string RequestId { get; set; } = string.Empty;
[Key(1)]
[CultReference(typeof(CultGeometryDomainDocument))]
public string DomainKey { get; set; } = string.Empty;
[MessagePackFormatter(typeof(CultRecordRefFormatter<CultGeometryDomainDocument>))]
public CultRecordRef<CultGeometryDomainDocument> DomainKey { get; set; }
[Key(2)]
[CultIndex]
@ -200,7 +200,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 +228,8 @@ namespace GameCult.Geometry
public string CutId { get; set; } = string.Empty;
[Key(1)]
[CultReference(typeof(CultGeometryBuildRequest))]
public string RequestKey { get; set; } = string.Empty;
[MessagePackFormatter(typeof(CultRecordRefFormatter<CultGeometryBuildRequest>))]
public CultRecordRef<CultGeometryBuildRequest> RequestKey { get; set; }
[Key(2)]
public string[] SelectedNodes { get; set; } = Array.Empty<string>();
@ -247,7 +247,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 +313,8 @@ namespace GameCult.Geometry
public string ChunkId { get; set; } = string.Empty;
[Key(1)]
[CultReference(typeof(CultGeometrySelectedCutManifest))]
public string CutKey { get; set; } = string.Empty;
[MessagePackFormatter(typeof(CultRecordRefFormatter<CultGeometrySelectedCutManifest>))]
public CultRecordRef<CultGeometrySelectedCutManifest> CutKey { get; set; }
[Key(2)]
[CultIndex]
@ -357,7 +357,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

@ -10,6 +10,7 @@
<ItemGroup>
<ProjectReference Include="..\..\packages\cultmath\src\CultMath\CultMath.csproj" />
<ProjectReference Include="..\GameCult.Caching\GameCult.Caching.csproj" />
<ProjectReference Include="..\GameCult.Caching.MessagePack\GameCult.Caching.MessagePack.csproj" />
<PackageReference Include="MessagePack" Version="3.1.7" />
<PackageReference Include="System.Text.Json" Version="9.0.10" />
</ItemGroup>

View file

@ -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;
@ -104,7 +105,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 +130,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 +403,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],