From 77602d5c0eb01b2e34bf1bb9820432e77c78c50a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Tue, 29 Sep 2026 16:57:31 +0200 Subject: [PATCH] Pin every CultMath MessagePack shape and decode a missing Color32 alpha as opaque Golden bytes now cover double2, double3, int3, int4, bool3, bool4, quaternion, Color32, Random, float3x3 and CultCellular. A missing Color32 alpha decodes as 255, the constructor default. A test proves a consumer resolver registered for the assembly wins over CultMathResolver. The redundant nil check is deleted; ReadArrayHeader already refuses nil. Co-Authored-By: Claude Sonnet 5 --- .../CultMathResolver.cs | 5 +- .../CultMathSerializationTests.cs | 58 +++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/GameCult.Caching.MessagePack/CultMathResolver.cs b/src/GameCult.Caching.MessagePack/CultMathResolver.cs index c854e12d..803b3873 100644 --- a/src/GameCult.Caching.MessagePack/CultMathResolver.cs +++ b/src/GameCult.Caching.MessagePack/CultMathResolver.cs @@ -11,7 +11,7 @@ namespace GameCult.Caching.MessagePack; /// The canonical MessagePack encoding of every public CultMath value type. Each type is an /// array of its components in declaration order: vectors and quaternions are flat scalar /// arrays, matrices are arrays of row vectors, and rect is [min, max]. Decoding -/// skips trailing elements and leaves missing ones at zero; nil is refused. The resolver is +/// skips trailing elements and leaves missing ones at zero (a missing Color32 alpha is 255); nil is refused. The resolver is /// part of the default chain, so a consumer /// registers nothing. /// @@ -49,7 +49,6 @@ public sealed class CultMathResolver : IFormatterResolver public T Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) { - if (reader.IsNil) throw new MessagePackSerializationException($"{typeof(T).Name} cannot be read from nil."); return read(ref reader); } } @@ -171,7 +170,7 @@ public sealed class CultMathResolver : IFormatterResolver Shape((ref MessagePackWriter w, quaternion v) => Put(ref w, stackalloc float[] { v.x, v.y, v.z, v.w }), (ref MessagePackReader r) => { Span s = stackalloc float[4]; Singles(ref r, s); return new quaternion(s[0], s[1], s[2], s[3]); }), Shape((ref MessagePackWriter w, Color32 v) => Put(ref w, stackalloc byte[] { v.r, v.g, v.b, v.a }), - (ref MessagePackReader r) => { Span s = stackalloc byte[4]; Bytes(ref r, s); return new Color32(s[0], s[1], s[2], s[3]); }), + (ref MessagePackReader r) => { Span s = stackalloc byte[4]; s[3] = 255; Bytes(ref r, s); return new Color32(s[0], s[1], s[2], s[3]); }), Shape((ref MessagePackWriter w, Random v) => { w.WriteArrayHeader(1); w.Write(v.state); }, (ref MessagePackReader r) => { diff --git a/tests/GameCult.Caching.Tests/CultMathSerializationTests.cs b/tests/GameCult.Caching.Tests/CultMathSerializationTests.cs index 76f95d34..fb6f1489 100644 --- a/tests/GameCult.Caching.Tests/CultMathSerializationTests.cs +++ b/tests/GameCult.Caching.Tests/CultMathSerializationTests.cs @@ -5,8 +5,11 @@ using FluentAssertions; using GameCult.Caching.MessagePack; using CultMath; using MessagePack; +using MessagePack.Formatters; using NUnit.Framework; +[assembly: CultCacheFormatterResolver(typeof(GameCult.Caching.Tests.CultMathSerializationTests.Float2Override))] + namespace GameCult.Caching.Tests { public sealed class CultMathSerializationTests @@ -106,6 +109,61 @@ namespace GameCult.Caching.Tests Hex(new float2x2(1f, 2f, 3f, 4f)).Should().Be("9292CA3F800000CA4000000092CA40400000CA40800000"); } + + [Test] + public void EveryShape_IsPinnedByteForByte() + { + Hex(new double2(1d, 2d)).Should().Be("92CB3FF0000000000000CB4000000000000000"); + Hex(new double3(1d, 2d, 3d)).Should().Be("93CB3FF0000000000000CB4000000000000000CB4008000000000000"); + Hex(new int3(1, -2, 3)).Should().Be("9301FE03"); + Hex(new int4(1, 2, 3, -1)).Should().Be("94010203FF"); + Hex(new bool3(true, false, true)).Should().Be("93C3C2C3"); + Hex(new bool4(false, true, true, false)).Should().Be("94C2C3C3C2"); + Hex(new quaternion(1f, 2f, 3f, 4f)).Should().Be("94CA3F800000CA40000000CA40400000CA40800000"); + Hex(new Color32(255, 128, 0, 7)).Should().Be("94CCFFCC800007"); + Hex(new CultMath.Random(12345u)).Should().Be("91CD3039"); + Hex(new float3x3(1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f, 9f)).Should().Be( + "93" + + "93CA3F800000CA40000000CA40400000" + + "93CA40800000CA40A00000CA40C00000" + + "93CA40E00000CA41000000CA41100000"); + Hex(new CultCellular(new float4(1f, 2f, 3f, 4f), new float4(5f, 6f, 7f, 8f), 0.25f)).Should().Be( + "93" + + "94CA3F800000CA40000000CA40400000CA40800000" + + "94CA40A00000CA40C00000CA40E00000CA41000000" + + "CA3E800000"); + } + + [Test] + public void Color32_MissingAlphaDecodesAsOpaque() + { + Decode("93CCFFCC8000").Should().Be(new Color32(255, 128, 0, 255)); + Decode("94CCFFCC800007").Should().Be(new Color32(255, 128, 0, 7)); + } + + [Test] + public void ConsumerResolvers_WinOverCultMathResolver() + { + var overridden = CultDocumentMessagePackSerialization.OptionsFor(typeof(Float2Override).Assembly); + + MessagePackSerializer.Serialize(new float2(1f, 2f), overridden).Should().Equal(0xC3); + Hex(new float2(1f, 2f)).Should().Be("92CA3F800000CA40000000"); + } + + public sealed class Float2Override : IFormatterResolver + { + public static readonly Float2Override Instance = new(); + + public IMessagePackFormatter? GetFormatter() => + typeof(T) == typeof(float2) ? (IMessagePackFormatter)(object)Formatter.Instance : null; + + private sealed class Formatter : IMessagePackFormatter + { + public static readonly Formatter Instance = new(); + public void Serialize(ref MessagePackWriter writer, float2 value, MessagePackSerializerOptions options) => writer.Write(true); + public float2 Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) => throw new NotSupportedException(); + } + } [Test] public void EveryPublicCultMathValueType_HasAFormatter() {