Read legacy Eve provider advertisements

This commit is contained in:
Meta Vens 2026-07-13 17:21:04 +01:00
commit 7da47f04db
2 changed files with 223 additions and 0 deletions

View file

@ -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<EveAdvertisedCommand> Commands { get; }
}
/// <summary>
/// Reads canonical provider advertisements and the original Aetheria-expanded v1 layout.
/// </summary>
public sealed class EveProviderAdvertisementCompatibilityFormatter :
IMessagePackFormatter<EveProviderAdvertisementDocument?>
{
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<EveProviderFreshness>(options).Serialize(ref writer, value.Freshness, options);
Formatter<IReadOnlyList<string>>(options).Serialize(ref writer, value.Schemas, options);
Formatter<IReadOnlyList<EveProviderWitness>>(options).Serialize(ref writer, value.Witnesses, options);
Formatter<IReadOnlyList<EveAdvertisedSurface>>(options).Serialize(ref writer, value.Surfaces, options);
Formatter<IReadOnlyList<EveAdvertisedCommand>>(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<EveProviderFreshness>(options).Deserialize(ref reader, options) : null;
var schemas = fields > 9 ? Formatter<IReadOnlyList<string>>(options).Deserialize(ref reader, options) : null;
var witnesses = fields > 10 ? Formatter<IReadOnlyList<EveProviderWitness>>(options).Deserialize(ref reader, options) : null;
var surfaces = fields > 11 ? Formatter<IReadOnlyList<EveAdvertisedSurface>>(options).Deserialize(ref reader, options) : null;
var commands = fields > 12 ? Formatter<IReadOnlyList<EveAdvertisedCommand>>(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<string>(),
witnesses ?? Array.Empty<EveProviderWitness>(),
surfaces ?? Array.Empty<EveAdvertisedSurface>(),
commands ?? Array.Empty<EveAdvertisedCommand>());
}
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<EveProviderFreshness>(options).Deserialize(ref reader, options)
?? new EveProviderFreshness("unknown", "", 0);
var schemas = Formatter<IReadOnlyList<string>>(options).Deserialize(ref reader, options)
?? Array.Empty<string>();
var witnesses = Formatter<IReadOnlyList<EveProviderWitness>>(options).Deserialize(ref reader, options)
?? Array.Empty<EveProviderWitness>();
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<T> Formatter<T>(MessagePackSerializerOptions options) =>
options.Resolver.GetFormatter<T>()
?? 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
{

View file

@ -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<EveProviderAdvertisementDocument>(
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<EveProviderWitness>(),
Array.Empty<EveAdvertisedSurface>(), Array.Empty<EveAdvertisedCommand>());
var bytes = MessagePackSerializer.Serialize(document, Options);
var reader = new MessagePackReader(bytes);
Assert.That(reader.ReadArrayHeader(), Is.EqualTo(13));
Assert.That(MessagePackSerializer.Deserialize<EveProviderAdvertisementDocument>(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<byte>();
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();
}
}