From 7da47f04db9042c3ba3e1911f7c88ff9f254fc33 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 17:21:04 +0100 Subject: [PATCH] Read legacy Eve provider advertisements --- .../Runtime/EveProviderDocuments.cs | 146 ++++++++++++++++++ ...eSurfaceSerializationCompatibilityTests.cs | 77 +++++++++ 2 files changed, 223 insertions(+) diff --git a/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs b/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs index e9bb1da..874ace8 100644 --- a/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs +++ b/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using GameCult.Caching; using MessagePack; +using MessagePack.Formatters; #nullable enable @@ -9,6 +10,7 @@ namespace GameCult.Eve.Surface { [CultDocument("gamecult.eve.provider_advertisement", SchemaId)] [MessagePackObject] + [MessagePackFormatter(typeof(EveProviderAdvertisementCompatibilityFormatter))] public sealed class EveProviderAdvertisementDocument { public const string SchemaId = "gamecult.eve.provider_advertisement.v1"; @@ -76,6 +78,150 @@ namespace GameCult.Eve.Surface [Key(12)] public IReadOnlyList Commands { get; } } + /// + /// Reads canonical provider advertisements and the original Aetheria-expanded v1 layout. + /// + public sealed class EveProviderAdvertisementCompatibilityFormatter : + IMessagePackFormatter + { + private const int CurrentFieldCount = 13; + private const int LegacyFieldCount = 16; + + public void Serialize( + ref MessagePackWriter writer, + EveProviderAdvertisementDocument? value, + MessagePackSerializerOptions options) + { + if (value == null) { writer.WriteNil(); return; } + writer.WriteArrayHeader(CurrentFieldCount); + writer.Write(value.Schema); + writer.Write(value.ProviderId); + writer.Write(value.ServiceId); + writer.Write(value.VerseId); + writer.Write(value.Title); + writer.Write(value.Kind); + writer.Write(value.CultMeshAddress); + writer.Write(value.UpdatedAtUtc); + Formatter(options).Serialize(ref writer, value.Freshness, options); + Formatter>(options).Serialize(ref writer, value.Schemas, options); + Formatter>(options).Serialize(ref writer, value.Witnesses, options); + Formatter>(options).Serialize(ref writer, value.Surfaces, options); + Formatter>(options).Serialize(ref writer, value.Commands, options); + } + + public EveProviderAdvertisementDocument? Deserialize( + ref MessagePackReader reader, + MessagePackSerializerOptions options) + { + if (reader.TryReadNil()) return null; + if (reader.NextMessagePackType != MessagePackType.Array) + throw new MessagePackSerializationException("Eve provider advertisement must be an array."); + + options.Security.DepthStep(ref reader); + try + { + var fields = reader.ReadArrayHeader(); + if (fields == LegacyFieldCount) + return ReadLegacy(ref reader, options); + + var schema = ReadString(ref reader, fields, 0); + var providerId = ReadString(ref reader, fields, 1); + var serviceId = ReadString(ref reader, fields, 2); + var verseId = ReadString(ref reader, fields, 3); + var title = ReadString(ref reader, fields, 4); + var kind = ReadString(ref reader, fields, 5); + var address = ReadString(ref reader, fields, 6); + var updatedAtUtc = ReadString(ref reader, fields, 7); + var freshness = fields > 8 ? Formatter(options).Deserialize(ref reader, options) : null; + var schemas = fields > 9 ? Formatter>(options).Deserialize(ref reader, options) : null; + var witnesses = fields > 10 ? Formatter>(options).Deserialize(ref reader, options) : null; + var surfaces = fields > 11 ? Formatter>(options).Deserialize(ref reader, options) : null; + var commands = fields > 12 ? Formatter>(options).Deserialize(ref reader, options) : null; + for (var index = CurrentFieldCount; index < fields; index++) reader.Skip(); + return new EveProviderAdvertisementDocument( + schema, providerId, serviceId, verseId, title, kind, address, updatedAtUtc, + freshness ?? new EveProviderFreshness("unknown", "", 0), + schemas ?? Array.Empty(), + witnesses ?? Array.Empty(), + surfaces ?? Array.Empty(), + commands ?? Array.Empty()); + } + finally { reader.Depth--; } + } + + private static EveProviderAdvertisementDocument ReadLegacy( + ref MessagePackReader reader, + MessagePackSerializerOptions options) + { + var schema = ReadString(ref reader); + var providerId = ReadString(ref reader); + var serviceId = ReadString(ref reader); + var verseId = ReadString(ref reader); + reader.Skip(); // RootVerse remains provider-owned settings, not portable advertisement state. + reader.Skip(); // CanonicalService + reader.Skip(); // LocatedService + var address = ReadString(ref reader); + var title = ReadString(ref reader); + var kind = ReadString(ref reader); + var updatedAtUtc = ReadString(ref reader); + var freshness = Formatter(options).Deserialize(ref reader, options) + ?? new EveProviderFreshness("unknown", "", 0); + var schemas = Formatter>(options).Deserialize(ref reader, options) + ?? Array.Empty(); + var witnesses = Formatter>(options).Deserialize(ref reader, options) + ?? Array.Empty(); + var surfaces = ReadLegacySurfaces(ref reader); + var commands = ReadLegacyCommands(ref reader); + return new EveProviderAdvertisementDocument( + schema, providerId, serviceId, verseId, title, kind, address, updatedAtUtc, + freshness, schemas, witnesses, surfaces, commands); + } + + private static EveAdvertisedSurface[] ReadLegacySurfaces(ref MessagePackReader reader) + { + var count = reader.ReadArrayHeader(); + var values = new EveAdvertisedSurface[count]; + for (var index = 0; index < count; index++) + { + var fields = reader.ReadArrayHeader(); + var schema = ReadString(ref reader, fields, 0); + var surfaceId = ReadString(ref reader, fields, 1); + var recordRef = ReadString(ref reader, fields, 2); + var transport = ReadString(ref reader, fields, 3); + var status = ReadString(ref reader, fields, 4); + for (var field = 5; field < fields; field++) reader.Skip(); + values[index] = new EveAdvertisedSurface( + surfaceId, schema, recordRef, transport, status, "", null); + } + return values; + } + + private static EveAdvertisedCommand[] ReadLegacyCommands(ref MessagePackReader reader) + { + var count = reader.ReadArrayHeader(); + var values = new EveAdvertisedCommand[count]; + for (var index = 0; index < count; index++) + { + var fields = reader.ReadArrayHeader(); + var command = ReadString(ref reader, fields, 0); + var transport = ReadString(ref reader, fields, 1); + var summary = ReadString(ref reader, fields, 2); + for (var field = 3; field < fields; field++) reader.Skip(); + values[index] = new EveAdvertisedCommand(command, "", transport, summary); + } + return values; + } + + private static IMessagePackFormatter Formatter(MessagePackSerializerOptions options) => + options.Resolver.GetFormatter() + ?? throw new MessagePackSerializationException($"No formatter is registered for {typeof(T).FullName}."); + + private static string ReadString(ref MessagePackReader reader) => reader.ReadString() ?? ""; + private static string ReadString(ref MessagePackReader reader, int fields, int index) => + index < fields ? ReadString(ref reader) : ""; + } + + [MessagePackObject] public sealed class EveProviderFreshness { diff --git a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs index b198b2e..da482df 100644 --- a/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs +++ b/tests/GameCult.Eve.Surface.Serialization.Tests/EveSurfaceSerializationCompatibilityTests.cs @@ -70,6 +70,44 @@ public sealed class EveSurfaceSerializationCompatibilityTests Assert.That(roundTrip.Commands[0].Command, Is.EqualTo("current.execute")); } + [Test] + public void LegacyExpandedProviderAdvertisementDeserializesAsCanonicalContract() + { + var document = MessagePackSerializer.Deserialize( + WriteLegacyProviderAdvertisement(), + Options); + + Assert.Multiple(() => + { + Assert.That(document.ProviderId, Is.EqualTo("aetheria")); + Assert.That(document.ServiceId, Is.EqualTo("aetheria-daemon")); + Assert.That(document.VerseId, Is.EqualTo("aetheria.local")); + Assert.That(document.Title, Is.EqualTo("Aetheria")); + Assert.That(document.CultMeshAddress, Is.EqualTo("cultmesh://aetheria.local/aetheria")); + Assert.That(document.Surfaces[0].SurfaceId, Is.EqualTo("aetheria.pilot")); + Assert.That(document.Surfaces[0].Schema, Is.EqualTo(EveSurfaceDocument.SchemaId)); + Assert.That(document.Commands[0].Command, Is.EqualTo("aetheria.pilot.move")); + Assert.That(document.Commands[0].SurfaceId, Is.Empty); + }); + } + + [Test] + public void CurrentProviderAdvertisementSerializationWritesCanonicalThirteenFields() + { + var document = new EveProviderAdvertisementDocument( + "provider", "service", "verse", "Title", "game.runtime", "cultmesh://verse/provider", + "2026-07-13T14:00:00Z", new EveProviderFreshness("fresh", "2026-07-13T14:00:00Z", 15000), + new[] { EveSurfaceDocument.SchemaId }, Array.Empty(), + Array.Empty(), Array.Empty()); + + var bytes = MessagePackSerializer.Serialize(document, Options); + var reader = new MessagePackReader(bytes); + + Assert.That(reader.ReadArrayHeader(), Is.EqualTo(13)); + Assert.That(MessagePackSerializer.Deserialize(bytes, Options).Title, + Is.EqualTo("Title")); + } + private static EveSurfaceDocument CreateCurrentDocument() => new( "current-provider", "current-kind", @@ -126,4 +164,43 @@ public sealed class EveSurfaceSerializationCompatibilityTests writer.Flush(); return buffer.WrittenSpan.ToArray(); } + + private static byte[] WriteLegacyProviderAdvertisement() + { + var buffer = new ArrayBufferWriter(); + var writer = new MessagePackWriter(buffer); + writer.WriteArrayHeader(16); + writer.Write(EveProviderAdvertisementDocument.SchemaId); + writer.Write("aetheria"); + writer.Write("aetheria-daemon"); + writer.Write("aetheria.local"); + writer.Write("asgard"); + writer.Write("aetheria"); + writer.Write("aetheria.local/aetheria"); + writer.Write("cultmesh://aetheria.local/aetheria"); + writer.Write("Aetheria"); + writer.Write("game.runtime"); + writer.Write("2026-07-13T14:00:00Z"); + writer.WriteArrayHeader(3); + writer.Write("fresh"); + writer.Write("2026-07-13T14:00:00Z"); + writer.Write(15000); + writer.WriteArrayHeader(1); + writer.Write(EveSurfaceDocument.SchemaId); + writer.WriteArrayHeader(0); + writer.WriteArrayHeader(1); + writer.WriteArrayHeader(5); + writer.Write(EveSurfaceDocument.SchemaId); + writer.Write("aetheria.pilot"); + writer.Write("eve:surface:aetheria.pilot"); + writer.Write("cultmesh"); + writer.Write("available"); + writer.WriteArrayHeader(1); + writer.WriteArrayHeader(3); + writer.Write("aetheria.pilot.move"); + writer.Write("cultmesh"); + writer.Write("Move the pilot ship"); + writer.Flush(); + return buffer.WrittenSpan.ToArray(); + } }