402 lines
13 KiB
C#
402 lines
13 KiB
C#
using System.Security.Cryptography;
|
|
using Aetheria.State;
|
|
using Aetheria.State.Documents;
|
|
using Aetheria.State.Migration;
|
|
using MessagePack;
|
|
|
|
var root = args.Length > 0 ? Path.GetFullPath(args[0]) : Directory.GetCurrentDirectory();
|
|
var statePath = args.Length > 1
|
|
? Path.GetFullPath(args[1])
|
|
: AetheriaStatePaths.ResolveDefaultStatePath(root);
|
|
var sourceRoot = Path.GetRelativePath(Directory.GetCurrentDirectory(), root).Replace('\\', '/');
|
|
var outputStatePath = Path.GetRelativePath(root, statePath).Replace('\\', '/');
|
|
|
|
var gameData = Path.Combine(root, "GameData");
|
|
var catalogPath = Path.Combine(gameData, "AetherDB.msgpack");
|
|
var nameFilesRoot = Path.Combine(gameData, "NameFile");
|
|
var capturedAtUtc = DateTimeOffset.UtcNow.ToString("O");
|
|
|
|
var catalog = CaptureFile(root, catalogPath);
|
|
var nameFiles = Directory.Exists(nameFilesRoot)
|
|
? Directory.EnumerateFiles(nameFilesRoot, "*.msgpack", SearchOption.TopDirectoryOnly)
|
|
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
|
.Select(path => CaptureFile(root, path))
|
|
.ToArray()
|
|
: [];
|
|
var entries = LegacyCatalogReader.Read(catalogPath);
|
|
var nameFileEntries = Directory.Exists(nameFilesRoot)
|
|
? Directory.EnumerateFiles(nameFilesRoot, "*.msgpack", SearchOption.TopDirectoryOnly)
|
|
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
|
.Select(LegacyCatalogReader.ReadSingle)
|
|
.ToArray()
|
|
: [];
|
|
var itemDefinitions = entries
|
|
.Where(entry => entry.ItemDefinition != null)
|
|
.Select(entry => entry.ItemDefinition!)
|
|
.ToArray();
|
|
var corporations = entries
|
|
.Where(entry => entry.Corporation != null)
|
|
.Select(entry => entry.Corporation!)
|
|
.ToArray();
|
|
var parsedNameFiles = entries.Concat(nameFileEntries)
|
|
.Where(entry => entry.NameFile != null)
|
|
.Select(entry => entry.NameFile!)
|
|
.ToArray();
|
|
|
|
await using var node = await AetheriaStateNode.OpenAsync(statePath, "aetheria-legacy-catalog-import");
|
|
|
|
await node.PutLegacyCatalogQuarantineAsync(new AetheriaLegacyCatalogQuarantine
|
|
{
|
|
RootPath = sourceRoot,
|
|
CapturedAtUtc = capturedAtUtc,
|
|
CatalogFile = catalog.RelativePath,
|
|
CatalogFingerprint = catalog.Fingerprint,
|
|
CatalogBytes = catalog.Bytes,
|
|
NameFiles = nameFiles,
|
|
Notes =
|
|
[
|
|
"This document quarantines legacy catalog file facts and records the bounded raw payload mapping pass. It does not grant old MessagePack files runtime state authority.",
|
|
"AetherDB.msgpack and NameFile/*.msgpack remain migration inputs until Unity runtime reads typed Aetheria catalog documents directly."
|
|
]
|
|
});
|
|
|
|
await node.PutMigrationLedgerAsync(new AetheriaMigrationLedger
|
|
{
|
|
Source = LegacyMigrationBoundary.LegacyGameDataFile,
|
|
SourceFingerprint = catalog.Fingerprint,
|
|
LastMigrationAtUtc = capturedAtUtc,
|
|
Counts =
|
|
[
|
|
new AetheriaMigrationCount
|
|
{
|
|
DocumentType = "aetheria.legacy_catalog_quarantine.v1",
|
|
Count = 1
|
|
},
|
|
new AetheriaMigrationCount
|
|
{
|
|
DocumentType = "legacy.catalog_file",
|
|
Count = catalog.Bytes > 0 ? 1 : 0
|
|
},
|
|
new AetheriaMigrationCount
|
|
{
|
|
DocumentType = "legacy.name_file",
|
|
Count = nameFiles.Length
|
|
},
|
|
new AetheriaMigrationCount
|
|
{
|
|
DocumentType = "aetheria.item_definition.v1",
|
|
Count = itemDefinitions.Length
|
|
},
|
|
new AetheriaMigrationCount
|
|
{
|
|
DocumentType = "aetheria.corporation.v1",
|
|
Count = corporations.Length
|
|
},
|
|
new AetheriaMigrationCount
|
|
{
|
|
DocumentType = "aetheria.name_file.v1",
|
|
Count = parsedNameFiles.Length
|
|
}
|
|
],
|
|
LegacyCatalogEntries = entries.Select(entry => entry.Summary).ToArray(),
|
|
Notes =
|
|
[
|
|
"Legacy catalog quarantine captured file fingerprints and mapped stable old MessagePack union payload fields into typed CultCache documents.",
|
|
"The mapper intentionally reads only stable scalar/catalog fields. Runtime object graphs, behavior payloads, and Unity-specific shapes remain legacy until dedicated typed documents exist.",
|
|
$"State path: {outputStatePath}"
|
|
]
|
|
});
|
|
|
|
foreach (var item in itemDefinitions)
|
|
{
|
|
await node.PutLegacyItemDefinitionAsync(item);
|
|
}
|
|
|
|
foreach (var corporation in corporations)
|
|
{
|
|
await node.PutLegacyCorporationAsync(corporation);
|
|
}
|
|
|
|
foreach (var nameFile in parsedNameFiles)
|
|
{
|
|
await node.PutLegacyNameFileAsync(nameFile);
|
|
}
|
|
|
|
await node.FlushAsync();
|
|
|
|
Console.WriteLine($"Aetheria legacy catalog mapped into typed state: {statePath}");
|
|
Console.WriteLine($"Catalog: {catalog.RelativePath} {catalog.Bytes} bytes {catalog.Fingerprint}");
|
|
Console.WriteLine($"Name files: {nameFiles.Length}");
|
|
Console.WriteLine($"Mapped items: {itemDefinitions.Length}");
|
|
Console.WriteLine($"Mapped factions: {corporations.Length}");
|
|
Console.WriteLine($"Mapped name files: {parsedNameFiles.Length}");
|
|
|
|
static AetheriaLegacyCatalogFile CaptureFile(string root, string path)
|
|
{
|
|
if (!File.Exists(path))
|
|
{
|
|
return new AetheriaLegacyCatalogFile
|
|
{
|
|
RelativePath = Path.GetRelativePath(root, path).Replace('\\', '/'),
|
|
Fingerprint = "missing",
|
|
Bytes = 0
|
|
};
|
|
}
|
|
|
|
using var stream = File.OpenRead(path);
|
|
var hash = SHA256.HashData(stream);
|
|
return new AetheriaLegacyCatalogFile
|
|
{
|
|
RelativePath = Path.GetRelativePath(root, path).Replace('\\', '/'),
|
|
Fingerprint = Convert.ToHexString(hash).ToLowerInvariant(),
|
|
Bytes = stream.Length
|
|
};
|
|
}
|
|
|
|
internal static class LegacyCatalogReader
|
|
{
|
|
private static readonly Dictionary<int, string> UnionNames = new()
|
|
{
|
|
[0] = "SimpleCommodityData",
|
|
[1] = "CompoundCommodityData",
|
|
[2] = "GearData",
|
|
[3] = "HullData",
|
|
[8] = "GalaxyMapLayerData",
|
|
[9] = "NameFile",
|
|
[11] = "PlayerData",
|
|
[13] = "Faction",
|
|
[15] = "OrbitData",
|
|
[16] = "BodyData",
|
|
[17] = "PersonalityAttribute",
|
|
[25] = "AsteroidBeltData",
|
|
[26] = "GasGiantData",
|
|
[27] = "SunData",
|
|
[28] = "PlanetData",
|
|
[29] = "CargoBayData",
|
|
[30] = "DockingBayData",
|
|
[31] = "WeaponItemData"
|
|
};
|
|
|
|
private static readonly HashSet<int> ItemUnionKeys = [0, 1, 2, 3, 29, 30, 31];
|
|
|
|
public static IReadOnlyList<LegacyCatalogEntry> Read(string catalogPath)
|
|
{
|
|
if (!File.Exists(catalogPath))
|
|
{
|
|
return [];
|
|
}
|
|
|
|
var bytes = File.ReadAllBytes(catalogPath);
|
|
var reader = new MessagePackReader(bytes);
|
|
var count = reader.ReadArrayHeader();
|
|
var entries = new List<LegacyCatalogEntry>(count);
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
entries.Add(ReadEntry(ref reader));
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
public static LegacyCatalogEntry ReadSingle(string path)
|
|
{
|
|
var bytes = File.ReadAllBytes(path);
|
|
var reader = new MessagePackReader(bytes);
|
|
return ReadEntry(ref reader);
|
|
}
|
|
|
|
private static LegacyCatalogEntry ReadEntry(ref MessagePackReader reader)
|
|
{
|
|
var unionHeader = reader.ReadArrayHeader();
|
|
if (unionHeader != 2)
|
|
{
|
|
throw new InvalidDataException($"Unexpected legacy catalog union header length: {unionHeader}.");
|
|
}
|
|
|
|
var unionKey = reader.ReadInt32();
|
|
var unionName = UnionNames.TryGetValue(unionKey, out var knownName) ? knownName : $"union:{unionKey}";
|
|
var payload = ReadArrayPayload(ref reader);
|
|
var legacyId = GetGuid(payload, 0);
|
|
var name = GetString(payload, 1);
|
|
var description = GetString(payload, 2);
|
|
var summary = new AetheriaLegacyCatalogEntrySummary
|
|
{
|
|
LegacyId = legacyId,
|
|
UnionKey = unionKey,
|
|
DocumentKind = unionName,
|
|
Name = name,
|
|
Note = "Mapped from stable legacy MessagePack array fields."
|
|
};
|
|
|
|
return new LegacyCatalogEntry(
|
|
summary,
|
|
ItemUnionKeys.Contains(unionKey)
|
|
? new AetheriaItemDefinition
|
|
{
|
|
Name = name,
|
|
Category = unionName,
|
|
LegacyId = legacyId,
|
|
Description = description,
|
|
Mass = GetDouble(payload, 4),
|
|
Volume = GetArrayLength(payload, 5),
|
|
Tags = [unionName, "legacy-catalog"]
|
|
}
|
|
: null,
|
|
unionKey == 13
|
|
? new AetheriaCorporation
|
|
{
|
|
Name = name,
|
|
LegacyId = legacyId,
|
|
Description = description
|
|
}
|
|
: null,
|
|
unionKey == 9
|
|
? new AetheriaNameFile
|
|
{
|
|
Name = name,
|
|
LegacyId = legacyId,
|
|
NameCount = GetArrayLength(payload, 2),
|
|
SampleNames = GetStringArraySample(payload, 2, 8)
|
|
}
|
|
: null);
|
|
}
|
|
|
|
private static Dictionary<int, object?> ReadArrayPayload(ref MessagePackReader reader)
|
|
{
|
|
var length = reader.ReadArrayHeader();
|
|
var values = new Dictionary<int, object?>(length);
|
|
for (var index = 0; index < length; index++)
|
|
{
|
|
values[index] = ReadValue(ref reader);
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
private static object? ReadValue(ref MessagePackReader reader)
|
|
{
|
|
if (reader.NextMessagePackType == MessagePackType.Nil)
|
|
{
|
|
reader.ReadNil();
|
|
return null;
|
|
}
|
|
|
|
return reader.NextMessagePackType switch
|
|
{
|
|
MessagePackType.Integer => ReadInteger(ref reader),
|
|
MessagePackType.Boolean => reader.ReadBoolean(),
|
|
MessagePackType.Float => reader.ReadDouble(),
|
|
MessagePackType.String => reader.ReadString(),
|
|
MessagePackType.Binary => ReadBinary(ref reader),
|
|
MessagePackType.Array => ReadNestedArray(ref reader),
|
|
MessagePackType.Map => ReadNestedMap(ref reader),
|
|
_ => SkipValue(ref reader)
|
|
};
|
|
}
|
|
|
|
private static long ReadInteger(ref MessagePackReader reader)
|
|
{
|
|
return reader.NextCode >= MessagePackCode.MinNegativeFixInt || reader.NextCode == MessagePackCode.Int8 ||
|
|
reader.NextCode == MessagePackCode.Int16 || reader.NextCode == MessagePackCode.Int32 ||
|
|
reader.NextCode == MessagePackCode.Int64
|
|
? reader.ReadInt64()
|
|
: (long) reader.ReadUInt64();
|
|
}
|
|
|
|
private static object?[] ReadNestedArray(ref MessagePackReader reader)
|
|
{
|
|
var length = reader.ReadArrayHeader();
|
|
var values = new object?[length];
|
|
for (var index = 0; index < length; index++)
|
|
{
|
|
values[index] = ReadValue(ref reader);
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
private static byte[]? ReadBinary(ref MessagePackReader reader)
|
|
{
|
|
var bytes = reader.ReadBytes();
|
|
if (bytes == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var result = new byte[bytes.Value.Length];
|
|
var offset = 0;
|
|
foreach (var segment in bytes.Value)
|
|
{
|
|
segment.Span.CopyTo(result.AsSpan(offset));
|
|
offset += segment.Length;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static Dictionary<string, object?> ReadNestedMap(ref MessagePackReader reader)
|
|
{
|
|
var length = reader.ReadMapHeader();
|
|
var values = new Dictionary<string, object?>(length);
|
|
for (var index = 0; index < length; index++)
|
|
{
|
|
var key = ReadValue(ref reader);
|
|
values[key?.ToString() ?? ""] = ReadValue(ref reader);
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
private static object? SkipValue(ref MessagePackReader reader)
|
|
{
|
|
reader.Skip();
|
|
return null;
|
|
}
|
|
|
|
private static string GetGuid(IReadOnlyDictionary<int, object?> payload, int key)
|
|
{
|
|
return payload.TryGetValue(key, out var value) && value is byte[] bytes && bytes.Length == 16
|
|
? new Guid(bytes).ToString("D")
|
|
: "";
|
|
}
|
|
|
|
private static string GetString(IReadOnlyDictionary<int, object?> payload, int key)
|
|
{
|
|
return payload.TryGetValue(key, out var value) ? value as string ?? "" : "";
|
|
}
|
|
|
|
private static double GetDouble(IReadOnlyDictionary<int, object?> payload, int key)
|
|
{
|
|
if (!payload.TryGetValue(key, out var value) || value == null)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return value switch
|
|
{
|
|
double doubleValue => doubleValue,
|
|
float floatValue => floatValue,
|
|
long integerValue => integerValue,
|
|
int integerValue => integerValue,
|
|
_ => 0
|
|
};
|
|
}
|
|
|
|
private static int GetArrayLength(IReadOnlyDictionary<int, object?> payload, int key)
|
|
{
|
|
return payload.TryGetValue(key, out var value) && value is object?[] array ? array.Length : 0;
|
|
}
|
|
|
|
private static string[] GetStringArraySample(IReadOnlyDictionary<int, object?> payload, int key, int max)
|
|
{
|
|
return payload.TryGetValue(key, out var value) && value is object?[] array
|
|
? array.OfType<string>().Take(max).ToArray()
|
|
: [];
|
|
}
|
|
}
|
|
|
|
internal sealed record LegacyCatalogEntry(
|
|
AetheriaLegacyCatalogEntrySummary Summary,
|
|
AetheriaItemDefinition? ItemDefinition,
|
|
AetheriaCorporation? Corporation,
|
|
AetheriaNameFile? NameFile);
|