2026-06-13 05:53:18 +01:00
using System.Security.Cryptography ;
using Aetheria.State ;
using Aetheria.State.Documents ;
using Aetheria.State.Migration ;
2026-06-13 06:15:55 +01:00
using GameCult.Caching ;
using MessagePack ;
2026-06-13 05:53:18 +01:00
var root = args . Length > 0 ? Path . GetFullPath ( args [ 0 ] ) : Directory . GetCurrentDirectory ( ) ;
var statePath = args . Length > 1
? Path . GetFullPath ( args [ 1 ] )
: AetheriaStatePaths . ResolveDefaultStatePath ( root ) ;
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 ( )
: [ ] ;
2026-06-13 06:15:55 +01:00
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 ( ) ;
2026-06-13 05:53:18 +01:00
await using var node = await AetheriaStateNode . OpenAsync ( statePath , "aetheria-legacy-catalog-import" ) ;
await node . PutLegacyCatalogQuarantineAsync ( new AetheriaLegacyCatalogQuarantine
{
RootPath = root ,
CapturedAtUtc = capturedAtUtc ,
CatalogFile = catalog . RelativePath ,
CatalogFingerprint = catalog . Fingerprint ,
CatalogBytes = catalog . Bytes ,
NameFiles = nameFiles ,
Notes =
[
2026-06-13 06:15:55 +01:00
"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."
2026-06-13 05:53:18 +01:00
]
} ) ;
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
2026-06-13 06:15:55 +01:00
} ,
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
2026-06-13 05:53:18 +01:00
}
] ,
2026-06-13 06:15:55 +01:00
LegacyCatalogEntries = entries . Select ( entry = > entry . Summary ) . ToArray ( ) ,
2026-06-13 05:53:18 +01:00
Notes =
[
2026-06-13 06:15:55 +01:00
"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." ,
2026-06-13 05:53:18 +01:00
$"State path: {statePath}"
]
} ) ;
2026-06-13 06:15:55 +01:00
foreach ( var item in itemDefinitions )
{
await node . PutItemDefinitionAsync ( new CultRecordKey ( $"legacy:item:{item.LegacyId}" ) , item ) ;
}
foreach ( var corporation in corporations )
{
await node . PutCorporationAsync ( new CultRecordKey ( $"legacy:faction:{corporation.LegacyId}" ) , corporation ) ;
}
foreach ( var nameFile in parsedNameFiles )
{
await node . PutNameFileAsync ( new CultRecordKey ( $"legacy:name-file:{nameFile.LegacyId}" ) , nameFile ) ;
}
2026-06-13 05:53:18 +01:00
await node . FlushAsync ( ) ;
2026-06-13 06:15:55 +01:00
Console . WriteLine ( $"Aetheria legacy catalog mapped into typed state: {statePath}" ) ;
2026-06-13 05:53:18 +01:00
Console . WriteLine ( $"Catalog: {catalog.RelativePath} {catalog.Bytes} bytes {catalog.Fingerprint}" ) ;
Console . WriteLine ( $"Name files: {nameFiles.Length}" ) ;
2026-06-13 06:15:55 +01:00
Console . WriteLine ( $"Mapped items: {itemDefinitions.Length}" ) ;
Console . WriteLine ( $"Mapped factions: {corporations.Length}" ) ;
Console . WriteLine ( $"Mapped name files: {parsedNameFiles.Length}" ) ;
2026-06-13 05:53:18 +01:00
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
} ;
}
2026-06-13 06:15:55 +01:00
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 ) ;