CultLib/src/GameCult.Caching.MessagePack/CultRecordRefFormatter.cs
Meta Vens a92381e589 Write an unset CultRecordRef as "" in every position
452f928 made nil the unset wire form, including dictionary keys: a
Dictionary<CultRecordRef<H>, float> holding the empty key encoded it as C0.
Python msgpack refuses nil map keys under strict_map_key, its default, so
cultcache-py cannot decode such a record. Nil cannot be the one canonical
form; "" can, and every runtime reads it.

The writer always writes "". The reader still takes nil as unset, because
452f928/b85a828 stores and Aetheria's Cut 9 import wrote nil; such a record
converts to "" on its next save and is byte-stable after that. The
store-composition contract names "" for value and key position, requires
readers to accept nil, and tells other runtimes to write "".

The tests pin payload bytes, so each claim is consequential: an unset ref
writes "", reloads through a fresh cache and resaves byte-identical; a
nil-written legacy payload reads as unset and resaves as ""; a ref-keyed
dictionary with an empty key writes "", reads back unset and resaves
byte-identical. The nil writer fails every byte assertion, a reader taking
nil as a set key fails the legacy pair, and a reader taking "" as a set key
fails the read-back assertions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 21:34:23 +02:00

18 lines
858 B
C#

using MessagePack;
using MessagePack.Formatters;
namespace GameCult.Caching;
// An unset reference (the empty key) is "" on the wire in every position, map keys included (other runtimes refuse nil
// map keys), and default(CultRecordRef<T>) in memory. Nil, written by 1.0.58 stores, reads as unset and saves as "".
public sealed class CultRecordRefFormatter<T> : IMessagePackFormatter<CultRecordRef<T>>
{
public void Serialize(ref MessagePackWriter writer, CultRecordRef<T> value, MessagePackSerializerOptions options) =>
writer.Write(value.Key.Value);
public CultRecordRef<T> Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{
var key = reader.TryReadNil() ? null : reader.ReadString();
return string.IsNullOrEmpty(key) ? default : new CultRecordRef<T>(new CultRecordKey(key!));
}
}