Read legacy Eve provider advertisements
This commit is contained in:
parent
511782ca42
commit
7da47f04db
2 changed files with 223 additions and 0 deletions
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue