| Filename | Latest commit message | Latest commit date |
|---|---|---|
docs/cultnet-selection-cut.md section 6/7/10: the reflection loop at CultNetDocumentRegistry.cs:325-342 exists only because nothing outside GameCult.Caching can read a declared index value, reference, or numeric member (Members/IndexAccessors are internal, no InternalsVisibleTo to Networking). This adds the one public door instead: CultDocumentMemberView, CultDocumentDescriptor.DeclaredMembers/TryGetIndexValue/TryGetIndexNumber/ ReferencesOf, and CultDocumentRegistry.ResolveTargetLeaves (D9, computed live against the registry, never cached or persisted). Two registration refusals land beside DiscoverMembers' existing DuplicateSlotMessage/HiddenMemberMessage: D10 renames the ArgumentException ToDictionary already threw for two differently-named members sharing one index alias into a rejection that names the schema, both members and the alias; D11 refuses a reference member whose CLR shape the cache cannot walk (many:false needs CultRecordRef<T>, many:true needs IEnumerable<CultRecordRef<T>> or IDictionary<CultRecordRef<T>, V>) instead of silently answering it as unreadable later. GameCult.Networking is untouched; the evaluator that will read this surface does not exist yet (commit 1). Verification: dotnet test tests/GameCult.Caching.Tests - 188 passed (176 existing + 12 new), 0 failed. New tests: TryGetIndexValue/TryGetIndexNumber reading an inherited alias; ReferencesOf over a single/many-list/many-dictionary reference, including two dictionary entries with different payload bytes (S19); ResolveTargetLeaves against an abstract target with no [CultDocument] of its own (D9), including a leaf registered after an earlier descriptor was built; IsNumeric agreement for a nullable numeric and a decimal member (S22); the two new registration refusals, dynamically emitted so registry discovery by attribute scan cannot see the rejected shapes. scripts/mutate-cultnet-selection-caching.ps1: a dedicated dotnet/NUnit mutation runner (the shared tools/eureka-mutations.ps1 harness only speaks cargo; scripts/mutate-cultmesh.mjs is the QUIC campaign's file and this campaign does not edit it, per the map's section 10). Byte-exact restore verified by SHA-256, a sidecar for crash recovery, M0 no-op control, one anchor match required per entry. Six entries (D8/S22, D10, D11 - revert and loosening each) all KILLED; final restore verified green. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> |
||
| .. | ||
| Contracts | ||
| CultAssetGuidKey.cs | ||
| CultCache.cs | ||
| CultDocumentContracts.cs | ||
| CultInspectorAttributes.cs | ||
| CultInspectorModel.cs | ||
| CultManagedDocument.cs | ||
| CultSchemaTypeNames.cs | ||
| GameCult.Caching.csproj | ||
| README.md | ||
| ReflectionExtensions.cs | ||
GameCult.Caching
GameCult.Caching is an attribute-driven typed cache with a canonical
single-file MessagePack store. Domain models stay clean. The cache owns record
keys, schema ids, timestamps, and persistence metadata instead of smearing
that sludge across every class.
Authoring Model
Cacheable models are plain classes annotated with cache intent.
using GameCult.Caching;
using MessagePack;
[CultDocument("gamecult.item_data", "gamecult.item_data.v1")]
public sealed class ItemData
{
[Key(0)]
[CultName]
public string Name = string.Empty;
[Key(1)]
[CultIndex]
public string Category = string.Empty;
[Key(2)]
public int Value;
}
Available cache attributes:
CultDocument(schemaName, schemaVersion)CultNameCultIndex(alias?)CultGlobalCultReference(targetType?, many: false)
CultDocumentRegistry builds every document descriptor by reflection over
these attributes and MessagePack's [Key]/[IgnoreMember]; it is the only
descriptor authority.
Main Concepts
Record keys and handles
CultCache gives each stored document a cache-owned CultRecordKey. You work
with typed handles instead of forcing storage identity into the domain object.
var cache = new CultCache();
var handle = await cache.AddAsync(new ItemData
{
Name = "Potion",
Category = "Consumable",
Value = 50
});
var potion = cache.Get<ItemData>(handle.Key);
Name and index lookups
Name and index lookups are driven from attributes, not manual registration.
var byName = cache.GetByName<ItemData>("Potion");
var byIndex = cache.GetByIndex<ItemData>("Category", "Consumable");
Explicit references
References stay explicit in the domain. The cache will resolve them on demand, but it will not quietly build an entire haunted object graph behind your back.
[CultDocument("gamecult.quest_data", "gamecult.quest_data.v1")]
public sealed class QuestData
{
[Key(0)] public string Title = string.Empty;
[Key(1)] [CultReference] public CultRecordRef<ItemData> Reward;
}
Persistence Shape
CultCache persistence is store-shaped, not entry-shaped:
- store header
- embedded schema catalog
- records
Each persisted record carries:
keyschemaIdstoredAtpayload
The domain payload stays free of storage metadata.
Backing Store
CultCache now has one sanctioned persistence format:
SingleFileMessagePackBackingStore
It persists the whole store as:
- store format version
- embedded schema catalog
- persisted records
Each persisted record carries:
keyschemaIdstoredAtpayload
Happy Path
using GameCult.Caching;
using GameCult.Caching.MessagePack;
var cache = await CultCacheMessagePack.OpenAsync("Data.msgpack");
var handle = await cache.UpsertAsync(new ItemData
{
Name = "Potion",
Category = "Consumable",
Value = 50
});
await cache.FlushAsync();
If you want the explicit assembly rite, the lower-level CultCache +
SingleFileMessagePackBackingStore path is still there. The helper is just the
blessed front door.
Managed Documents And SoA Storage
Gameplay code keeps authoring normal typed documents. CultCache owns the record identity, reactive presentation, and CPU-local structure-of-arrays table behind that POCO surface.
[CultDocument("game.player_transform", "game.player_transform.v1")]
public sealed class PlayerTransform
{
[CultName] public string Name = string.Empty;
public float PositionX;
public float PositionY;
public int Health;
}
var player = cache.Document<PlayerTransform>(new CultRecordKey("player:alice"));
await player.ReplaceAsync(new PlayerTransform
{
Name = "Alice",
PositionX = 4,
PositionY = 8,
Health = 100
});
using var subscription = player.Watch().Subscribe(current => Render(current));
player.Value!.Health = 90;
await player.CommitAsync();
var table = cache.Soa<PlayerTransform>();
Span<float> x = table.Column<float>(nameof(PlayerTransform.PositionX)).Span;
Span<float> y = table.Column<float>(nameof(PlayerTransform.PositionY)).Span;
Span<int> health = table.Column<int>(nameof(PlayerTransform.Health)).Span;
for (var row = 0; row < table.Count; row++)
{
x[row] += 0.016f;
}
Document<T>() is the standard presentation. Soa<T>() exposes the
cache-owned physical table for hot loops and diagnostics; it is not a second
domain model callers have to maintain.
Persistence discipline
Persistence stays manual by default. Mutations mark the cache and attached backing stores dirty; nothing silently flushes every object write just because that felt convenient in the moment.
Useful surfaces:
CultCacheMessagePack.OpenAsync(path)cache.IsDirtystore.IsDirtycache.Document<T>(key)cache.Soa<T>()cache.UpsertAsync(document)cache.FlushAllBackingStores()cache.FlushAsync()cache.FlushBackingStore(store)cache.PrepareForReloadOrShutdown()cache.PrepareForReloadOrShutdownAsync()cache.FlushAttachedStoresOnDisposestore.FlushOnDispose
This is built for callers like Aquarium that want durable runtime state while still choosing when to pay the write cost.
Single-file MessagePack remains a single-writer format. The atomic replace path protects against partial writes, not competing writers trampling each other.
Schema migration diagnostics
CultCache does not quietly fall back from exact schemaId resolution to
"close enough, probably." When a persisted schema drifts, the cache now emits a
typed migration report describing:
- exact vs compatible-drift resolution
- ignored extra slots
- defaulted missing slots
- warning codes/messages
Compatibility rules and canonical fixture receipts live in
Contracts/cultcache-schema-compatibility.md.