CultLib/src/GameCult.Caching.MessagePack
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Meta Vens 64b86e0a27 write-ownership-cs: a write owns a record by the written type; one-batch retype is eviction then add (squash of hands/variants-write-ownership a4eae905..8c7252ea)
A write owns the record at its key when the written type declares the
record's schema id, or reads an older undeclared id of its own schema name
whole (ruling own-entry-reads-whole); every other registered type's record
is refused, in every cache, with or without a backing store
(cache-refuses-retype).

Removing a record and adding one of another type in a single batch is
staged as the held record's eviction then the add, so validators, Claim,
the store judges and the change journal see what they see staged apart
(one-batch-retype-is-eviction-then-add); Watch<old> gets Removed and
Watch<new> Added. A one-batch retype across home stores is refused by the
batch-spans rule and must be staged apart. A variant rebase changes type
exactly where a plain write may (variant-rebase-follows-plain-write).
Store kinds outside the library get one protected per-key judge
(store-kind-per-key-judge), exercised by tests/GameCult.Caching.OutsideStoreTests.

Spec variants:cut_spec:cut-write-ownership-cs.r1; reports h1-h5;
Soul s5 clears the merge (verdict variants:verdict:cut-write-ownership-cs.s5).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:22:20 +01:00
..
buildTransitive Assume CultRecordRef<T> formattable for MessagePack's source generator consumers 2026-10-27 23:59:13 +01:00
CultCacheFormatterResolverAttribute.cs Own serialization options by the document's assembly 2026-09-13 21:12:53 +02:00
CultCacheMessagePack.cs CultCache: list elements are written by their [Key] layout; registration and the codec refuse otherwise 2026-10-14 21:52:36 +02:00
CultDocumentMessagePackSerialization.cs Cacheless single-file API: WriteDocument encodes the declared type, the registry is the call's argument, DecodeDocument moves to CultCodec, ReadStoreSnapshot's gate is pinned 2026-10-23 01:38:16 +02:00
CultMathExprResolver.cs ExprGraph resolver: nil is nil, decode refuses non-canonical encodings, pin nested shape, nil-name and depth rules 2026-10-14 07:54:32 +02:00
CultMathJson.cs CultMathJson writes CultPhasor as cosine/sine and still reads cos/sin 2026-10-17 19:28:09 +02:00
CultMathResolver.cs CultMathJson writes CultPhasor as cosine/sine and still reads cos/sin 2026-10-17 19:28:09 +02:00
CultMessagePackSecurity.cs C# reference: one payload nesting bound (256) on read and write, nil arm slots refused, bounds vectors 2026-10-11 08:33:15 +02:00
CultPayloadDepth.cs C# reference: one payload nesting bound (256) on read and write, nil arm slots refused, bounds vectors 2026-10-11 08:33:15 +02:00
CultRecordRefFormatter.cs Write an unset CultRecordRef as "" in every position 2026-09-14 21:34:23 +02:00
CultUnionGuard.cs C# reference: one payload nesting bound (256) on read and write, nil arm slots refused, bounds vectors 2026-10-11 08:33:15 +02:00
DirectoryMessagePackBackingStore.cs write-ownership-cs: a write owns a record by the written type; one-batch retype is eviction then add (squash of hands/variants-write-ownership a4eae905..8c7252ea) 2026-10-28 08:22:20 +01:00
GameCult.Caching.MessagePack.csproj Assume CultRecordRef<T> formattable for MessagePack's source generator consumers 2026-10-27 23:59:13 +01:00
README.md Assume CultRecordRef<T> formattable for MessagePack's source generator consumers 2026-10-27 23:59:13 +01:00

GameCult.Caching.MessagePack

GameCult.Caching.MessagePack provides the canonical CultCache persistence format for the attribute-first GameCult.Caching stack.

Included Types

  • SingleFileMessagePackBackingStore
  • CultDocumentMessagePackSerialization
  • CultDocumentResolver
  • CultRecordRefFormatter<T>
  • CultMathResolver: the canonical component-array encoding of every public CultMath value type, part of the default resolver chain
  • CultMathExprResolver: the canonical encoding of CultMath.Expr.ExprGraph, decoded through its validator, part of the default resolver chain

Cult document payloads serialize through MessagePackSerializer with the options of the document's assembly (CultDocumentMessagePackSerialization.OptionsFor), after the reflective CultDocumentRegistry has accepted the document shape. The store snapshot format is written by hand against MessagePackWriter/MessagePackReader.

What It Does

  • stores whole CultCache snapshots in MessagePack
  • serializes attributed document payloads as MessagePack [Key(n)] arrays
  • keeps explicit CultRecordRef<T> values compact on disk/wire
  • preserves the cache/store split where metadata lives outside the domain model

Example Model

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)] public int Value;
}

Typical Usage

using GameCult.Caching;
using GameCult.Caching.MessagePack;

var cache = new CultCache();
var store = new SingleFileMessagePackBackingStore("Data.msgpack");

cache.AddBackingStore(store);
await cache.PullAllBackingStoresAsync();

Notes

  • Payload bytes are MessagePack. Store metadata and schema catalogs are also persisted through hand-written MessagePack array layouts in this package.
  • The raw CultNet document lane should treat these payload bytes as already blessed, not decode and re-encode them for sport.

MessagePack source generator

NuGet consumers get [assembly: MessagePackAssumedFormattable(typeof(CultRecordRef<>))] from the package, so a CultRecordRef<T> member or dictionary key compiles with MessagePack's source generator on and is formatted by CultLib's resolver; no per-member [MessagePackFormatter] is needed. Unity and ProjectReference consumers that turn the generator on write that line themselves (or import buildTransitive/GameCult.Caching.MessagePack.targets).

CultMath Value Shapes

Every public CultMath value type has one MessagePack shape (CultMathResolver, in the default chain) and one JSON shape (CultMathJson, added to a consumer's options with new JsonSerializerOptions().AddCultMathConverters(); CultLib owns no JSON options of its own). MessagePack decoding skips extra elements, leaves missing ones at zero (Color32 alpha at 255) and refuses nil. JSON reading skips unknown properties (UnmappedMemberHandling.Disallow refuses them), leaves missing ones at zero, honours PropertyNameCaseInsensitive, lets a repeated property replace the whole earlier value (refused when AllowDuplicateProperties is false), and throws JsonException for a wrong token or an out-of-range number. Numbers follow JsonSerializerOptions.NumberHandling as System.Text.Json's own float does: "NaN", "Infinity" and "-Infinity" are written under AllowNamedFloatingPointLiterals or WriteAsString and read under AllowNamedFloatingPointLiterals or AllowReadingFromString; quoted numbers read under AllowReadingFromString with no surrounding whitespace. Every type is also a dictionary key: its flattened components joined by commas, invariant culture ("1,2", "true,false", matrices row by row, rect as "minX,minY,maxX,maxY"), and only that exact spelling reads back. Two intended divergences from System.Text.Json: a bare number beyond a component's range (1e40 as a float) is refused where System.Text.Json reads infinity, and a refusal on write is a JsonException where System.Text.Json throws ArgumentException. A consumer resolver or converter registered ahead of these wins.

CultMath.Expr.ExprGraph is a class, not a value type, and has its own shape (CultMathExprResolver, in the default chain): [version, inputs, params, outputs, nodes], with a param [name, default float bits], an output [name, node] and a node [op name, operands, payload]. Decoding runs the graph's one validator and refuses a broken graph whole (an ExprGraphException naming the rule), so a consumer [CultDocument] record with an ExprGraph field needs no registration. A nil graph is nil, as in every reference slot; decoding refuses any encoding but the canonical one.

Type MessagePack JSON
float2, double2, int2, bool2 [x, y] {"x":..,"y":..}
float3, double3, int3, bool3 [x, y, z] {"x":..,"y":..,"z":..}
float4, int4, bool4, quaternion [x, y, z, w] {"x":..,"y":..,"z":..,"w":..}
Color32 [r, g, b, a] as uint8 {"r":..,"g":..,"b":..,"a":..}
Random [state] {"state":..}
float2x2, float3x3 array of row vectors (row-major) array of row objects
rect [min, max] of float2 {"min":{..},"max":{..}}
CultCellular [nearest, edge, id] {"nearest":{..},"edge":{..},"id":..}
CultPhasor [cosine, sine] of float4 {"cosine":{..},"sine":{..}} (reads legacy cos/sin)
CultSiteCells [nearest, second, nearestSite, secondSite] {"nearest":{..},"second":{..},"nearestSite":..,"secondSite":..}

Floats are MessagePack float32 (0xCA), doubles float64 (0xCB), integers the shortest MessagePack integer.