2026-06-13 05:53:18 +01:00
using System.Security.Cryptography ;
using Aetheria.State ;
using Aetheria.State.Documents ;
using Aetheria.State.Migration ;
2026-07-12 02:25:40 +01:00
using GameCult.Aetheria.State.Verse ;
2026-06-13 06:15:55 +01:00
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 ) ;
2026-06-13 06:52:19 +01:00
var sourceRoot = Path . GetRelativePath ( Directory . GetCurrentDirectory ( ) , root ) . Replace ( '\\' , '/' ) ;
var outputStatePath = Path . GetRelativePath ( root , statePath ) . Replace ( '\\' , '/' ) ;
2026-06-13 05:53:18 +01:00
var gameData = Path . Combine ( root , "GameData" ) ;
var catalogPath = Path . Combine ( gameData , "AetherDB.msgpack" ) ;
2026-07-12 02:25:40 +01:00
var supplementalCatalogPath = Path . Combine ( gameData , "Legacy" , "AetherDB.2021-03-05.msgpack" ) ;
2026-07-16 17:27:01 +01:00
var recoveredHullCatalogPath = Path . Combine ( gameData , "Legacy" , "AetherDB.2021-04-14.msgpack" ) ;
2026-06-13 05:53:18 +01:00
var nameFilesRoot = Path . Combine ( gameData , "NameFile" ) ;
var capturedAtUtc = DateTimeOffset . UtcNow . ToString ( "O" ) ;
var catalog = CaptureFile ( root , catalogPath ) ;
2026-07-12 02:25:40 +01:00
var supplementalCatalog = CaptureFile ( root , supplementalCatalogPath ) ;
2026-07-16 17:27:01 +01:00
var recoveredHullCatalog = CaptureFile ( root , recoveredHullCatalogPath ) ;
2026-06-13 05:53:18 +01:00
var nameFiles = Directory . Exists ( nameFilesRoot )
? Directory . EnumerateFiles ( nameFilesRoot , "*.msgpack" , SearchOption . TopDirectoryOnly )
. OrderBy ( path = > path , StringComparer . OrdinalIgnoreCase )
. Select ( path = > CaptureFile ( root , path ) )
. ToArray ( )
: [ ] ;
2026-07-12 02:25:40 +01:00
var currentEntries = LegacyCatalogReader . Read ( catalogPath ) ;
var currentLegacyIds = currentEntries . Select ( entry = > entry . Summary . LegacyId ) . ToHashSet ( StringComparer . Ordinal ) ;
var supplementalMineEntries = LegacyCatalogReader . Read ( supplementalCatalogPath )
. Where ( entry = > entry . ItemDefinition ! = null & & IsMineItem ( entry . ItemDefinition ) )
. Where ( entry = > ! currentLegacyIds . Contains ( entry . Summary . LegacyId ) )
. ToArray ( ) ;
2026-07-16 17:27:01 +01:00
var recoveredGameplayEntries = LegacyCatalogReader . Read (
recoveredHullCatalogPath ,
LegacyCatalogLayout . April2021 )
. Where ( entry = > entry . ItemDefinition ! = null & & IsRecoveredGameplayItem ( entry . ItemDefinition ) )
. Where ( entry = > ! currentLegacyIds . Contains ( entry . Summary . LegacyId ) )
. ToArray ( ) ;
var entries = currentEntries
. Concat ( supplementalMineEntries )
. Concat ( recoveredGameplayEntries )
. ToArray ( ) ;
2026-06-13 06:15:55 +01:00
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 )
2026-07-16 17:27:01 +01:00
. Select ( entry = > ProjectCatalogRarity ( ProjectStationSensorMount ( ProjectDeployableWeapon ( entry . ItemDefinition ! ) ) ) )
2026-06-13 06:15:55 +01:00
. 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
2026-07-12 02:25:40 +01:00
static AetheriaItemDefinition ProjectDeployableWeapon ( AetheriaItemDefinition item )
{
if ( ! IsMineItem ( item ) )
return item ;
item . WeaponType = "Mine" ;
item . BehaviorPayloads = ( item . BehaviorPayloads ? ? [ ] )
. Select ( behavior = > string . Equals ( behavior . Kind , "InstantWeapon" , StringComparison . Ordinal )
? new AetheriaBehaviorPayload
{
UnionKey = behavior . UnionKey ,
Kind = "DeployableWeapon" ,
Group = behavior . Group ,
2026-07-13 16:19:35 +01:00
BehaviorId = behavior . BehaviorId ,
2026-07-12 02:25:40 +01:00
Fields = behavior . Fields
. Where ( field = > field . Key < 26 | | field . Key > 30 )
. Concat ( [
NumberBehaviorField ( 26 , 2 ) ,
NumberBehaviorField ( 27 , 30 ) ,
PerformanceBehaviorField ( 28 , 25 ) ,
NumberBehaviorField ( 29 , 2 ) ,
PerformanceBehaviorField ( 30 , 25 )
] )
. OrderBy ( field = > field . Key )
. ToArray ( )
}
: behavior )
. ToArray ( ) ;
item . BehaviorKinds = item . BehaviorPayloads
. Select ( behavior = > behavior . Kind )
. Distinct ( StringComparer . Ordinal )
. OrderBy ( kind = > kind , StringComparer . Ordinal )
. ToArray ( ) ;
item . BehaviorCount = item . BehaviorPayloads . Length ;
item . Tags = ( item . Tags ? ? [ ] )
. Where ( tag = > ! string . Equals ( tag , "behavior:InstantWeapon" , StringComparison . Ordinal ) )
. Concat ( [ "behavior:DeployableWeapon" ] )
. Distinct ( StringComparer . Ordinal )
. ToArray ( ) ;
return item ;
}
2026-07-13 16:53:25 +01:00
static AetheriaItemDefinition ProjectStationSensorMount ( AetheriaItemDefinition item )
{
const string stationHullLegacyId = "82efc0a5-1ba5-4ff3-a281-b2e6e247521d" ;
if ( ! string . Equals ( item . LegacyId , stationHullLegacyId , StringComparison . OrdinalIgnoreCase ) | |
( item . Hardpoints ? ? [ ] ) . Any ( hardpoint = > string . Equals ( hardpoint . Type , "Sensors" , StringComparison . Ordinal ) ) )
{
return item ;
}
const int positionX = 5 ;
const int positionY = 1 ;
var mountCells = new [ ]
{
new AetheriaShapeCell { X = 0 , Y = 0 } ,
new AetheriaShapeCell { X = 1 , Y = 0 } ,
new AetheriaShapeCell { X = 0 , Y = 1 } ,
new AetheriaShapeCell { X = 1 , Y = 1 }
} ;
var hullCells = ( item . ShapeCells ? ? [ ] )
. Select ( cell = > ( cell . X , cell . Y ) )
. ToHashSet ( ) ;
if ( mountCells . Any ( cell = > ! hullCells . Contains ( ( positionX + cell . X , positionY + cell . Y ) ) ) )
{
throw new InvalidDataException ( "Recovered station sensor mount does not fit the decoded station hull shape." ) ;
}
item . Hardpoints = ( item . Hardpoints ? ? [ ] )
. Append ( new AetheriaItemHardpoint
{
Type = "Sensors" ,
PositionX = positionX ,
PositionY = positionY ,
ShapeWidth = 2 ,
ShapeHeight = 2 ,
OccupiedCells = 4 ,
ShapeCells = mountCells ,
Rotation = "None"
} )
. ToArray ( ) ;
return item ;
}
2026-07-12 02:25:40 +01:00
static bool IsMineItem ( AetheriaItemDefinition item )
{
var hasMineEffect = ( item . BehaviorPayloads ? ? [ ] ) . Any ( behavior = >
string . Equals ( behavior . Kind , "InstantWeapon" , StringComparison . Ordinal ) & &
behavior . Fields . Any ( field = > field . Key = = 8 & &
field . Value . StringValue . EndsWith ( "/Mine Launcher.prefab" , StringComparison . OrdinalIgnoreCase ) ) ) ;
return string . Equals ( item . WeaponType , "Mine" , StringComparison . Ordinal ) | | hasMineEffect ;
}
2026-07-16 17:27:01 +01:00
static bool IsRecoveredGameplayItem ( AetheriaItemDefinition item )
{
return string . Equals ( item . LegacyId , "c4b20032-8cd3-4206-807f-97b296797425" , StringComparison . OrdinalIgnoreCase ) | |
string . Equals ( item . LegacyId , "bb5d9f73-612f-4060-8fb4-f72d6b93b616" , StringComparison . OrdinalIgnoreCase ) | |
string . Equals ( item . HardpointType , "Thruster" , StringComparison . Ordinal ) ;
}
static AetheriaItemDefinition ProjectCatalogRarity ( AetheriaItemDefinition item )
{
var tags = ( item . Tags ? ? [ ] ) . AsEnumerable ( ) ;
if ( IsRecoveredGameplayItem ( item ) )
tags = tags . Append ( "provenance:recovered:2021-04-14" ) ;
var isRareHull = string . Equals ( item . Name , "LonginusX" , StringComparison . Ordinal ) & &
string . Equals ( item . HullType , "Ship" , StringComparison . Ordinal ) ;
var isRareDrive = ( item . BehaviorKinds ? ? [ ] ) . Contains ( "AetherDrive" , StringComparer . Ordinal ) ;
if ( isRareHull | | isRareDrive )
tags = tags . Append ( "rarity:rare" ) ;
item . Tags = tags
. Distinct ( StringComparer . Ordinal )
. ToArray ( ) ;
return item ;
}
2026-07-12 02:25:40 +01:00
static AetheriaBehaviorField NumberBehaviorField ( int key , double value ) = > new ( )
{
Key = key ,
Value = new AetheriaBehaviorValue { Kind = "number" , NumberValue = value }
} ;
static AetheriaBehaviorField PerformanceBehaviorField ( int key , double value ) = > new ( )
{
Key = key ,
Value = new AetheriaBehaviorValue
{
Kind = "performance-stat" ,
Children =
[
new AetheriaBehaviorValue { Kind = "number" , NumberValue = value } ,
new AetheriaBehaviorValue { Kind = "number" , NumberValue = value } ,
new AetheriaBehaviorValue { Kind = "number" } ,
new AetheriaBehaviorValue { Kind = "number" } ,
new AetheriaBehaviorValue { Kind = "number" }
]
}
} ;
if ( args . Any ( argument = > string . Equals ( argument , "--merge-mine" , StringComparison . Ordinal ) ) )
{
var mine = itemDefinitions . SingleOrDefault ( item = > string . Equals ( item . WeaponType , "Mine" , StringComparison . Ordinal ) )
? ? throw new InvalidDataException ( "Supplemental catalog did not contain the recovered Mine Launcher item." ) ;
await using var mergeNode = await AetheriaStateNode . OpenAsync ( statePath , "aetheria-mine-catalog-merge" ) ;
await mergeNode . MutableDocument < AetheriaItemDefinition > ( AetheriaCatalogKeys . ItemDefinitionFromLegacyId ( mine . LegacyId ) )
. ReplaceAsync ( mine ) ;
await mergeNode . FlushAsync ( ) ;
var merged = await mergeNode . RuntimeCatalog ( ) . LatestAsync ( ) . ConfigureAwait ( false ) ;
var mergedMine = merged . FindItem ( mine . ItemKey ) ;
if ( mergedMine = = null | | ! mergedMine . BehaviorPayloads . Any ( behavior = >
string . Equals ( behavior . Kind , "DeployableWeapon" , StringComparison . Ordinal ) ) )
throw new InvalidDataException ( "Mine Launcher merge did not become visible through the runtime catalog." ) ;
Console . WriteLine ( $"Merged Mine Launcher into typed state: {statePath}" ) ;
Console . WriteLine ( $"Item: {mine.ItemKey}" ) ;
return ;
}
2026-07-13 16:53:25 +01:00
if ( args . Any ( argument = > string . Equals ( argument , "--merge-station-sensor" , StringComparison . Ordinal ) ) )
{
const string stationHullLegacyId = "82efc0a5-1ba5-4ff3-a281-b2e6e247521d" ;
var stationHull = itemDefinitions . Single ( item = >
string . Equals ( item . LegacyId , stationHullLegacyId , StringComparison . OrdinalIgnoreCase ) ) ;
await using var mergeNode = await AetheriaStateNode . OpenAsync ( statePath , "aetheria-station-sensor-catalog-merge" ) ;
await mergeNode . MutableDocument < AetheriaItemDefinition > ( AetheriaCatalogKeys . ItemDefinitionFromLegacyId ( stationHull . LegacyId ) )
. ReplaceAsync ( stationHull ) ;
await mergeNode . FlushAsync ( ) ;
var merged = await mergeNode . RuntimeCatalog ( ) . LatestAsync ( ) . ConfigureAwait ( false ) ;
var mergedHull = merged . FindItem ( stationHull . ItemKey ) ;
if ( mergedHull = = null | | ! ( mergedHull . Hardpoints ? ? [ ] ) . Any ( hardpoint = >
string . Equals ( hardpoint . Type , "Sensors" , StringComparison . Ordinal ) & &
hardpoint . OccupiedCells = = 4 ) )
{
throw new InvalidDataException ( "Station sensor hardpoint merge did not become visible through the runtime catalog." ) ;
}
Console . WriteLine ( $"Merged station sensor hardpoint into typed state: {statePath}" ) ;
Console . WriteLine ( $"Hull: {stationHull.ItemKey}" ) ;
return ;
}
2026-06-13 10:29:12 +01:00
ResetMaterializedStateOutput ( statePath ) ;
2026-07-16 13:58:45 +01:00
await using var node = await AetheriaStateNode . OpenAsync (
statePath ,
"aetheria-legacy-catalog-import" ,
2026-07-19 00:34:11 +01:00
useDirectoryStore : true ) ;
2026-06-13 05:53:18 +01:00
2026-06-29 03:41:19 +01:00
await node . MutableDocument < AetheriaLegacyCatalogQuarantine > ( AetheriaStateNode . LegacyCatalogQuarantineKey )
. ReplaceAsync ( new AetheriaLegacyCatalogQuarantine
2026-06-13 05:53:18 +01:00
{
2026-06-13 06:52:19 +01:00
RootPath = sourceRoot ,
2026-06-13 05:53:18 +01:00
CapturedAtUtc = capturedAtUtc ,
CatalogFile = catalog . RelativePath ,
CatalogFingerprint = catalog . Fingerprint ,
CatalogBytes = catalog . Bytes ,
NameFiles = nameFiles ,
2026-07-16 17:27:01 +01:00
SupplementalCatalogFiles = new [ ] { supplementalCatalog , recoveredHullCatalog }
. Where ( file = > file . Bytes > 0 )
. ToArray ( ) ,
2026-06-13 05:53:18 +01:00
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." ,
2026-07-12 02:25:40 +01:00
"AetherDB.msgpack and NameFile/*.msgpack remain migration inputs until Unity runtime reads typed Aetheria catalog documents directly." ,
2026-07-16 17:27:01 +01:00
"The 2021-03-05 supplemental catalog contributes only the missing Mine Launcher item and remains quarantined provenance, not runtime authority." ,
"The 2021-04-14 catalog contributes only the missing common Longinus and Djinni hull rows plus missing Thruster equipment required to outfit their authored mounts. It preserves the post-drive catalog's distinct Longinus/LonginusX identities after the later modular database lost the common rows."
2026-06-13 05:53:18 +01:00
]
} ) ;
2026-06-29 03:41:19 +01:00
await node . MutableDocument < AetheriaMigrationLedger > ( AetheriaStateNode . MigrationLedgerKey )
. ReplaceAsync ( new AetheriaMigrationLedger
2026-06-13 05:53:18 +01:00
{
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
{
2026-06-13 13:18:03 +01:00
DocumentType = "aetheria.corporation.v2" ,
2026-06-13 06:15:55 +01:00
Count = corporations . Length
} ,
new AetheriaMigrationCount
{
2026-06-13 12:46:34 +01:00
DocumentType = "aetheria.name_file.v2" ,
2026-06-13 06:15:55 +01:00
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-07-16 17:27:01 +01:00
"Recovered common Longinus and Djinni hulls and their missing Thruster equipment come from the fingerprinted 2021-04-14 catalog; LonginusX and AetherDrive items are explicitly tagged as rare generation candidates." ,
2026-06-13 06:52:19 +01:00
$"State path: {outputStatePath}"
2026-06-13 05:53:18 +01:00
]
} ) ;
2026-06-13 06:15:55 +01:00
foreach ( var item in itemDefinitions )
{
2026-06-29 03:41:19 +01:00
await node . MutableDocument < AetheriaItemDefinition > ( AetheriaCatalogKeys . ItemDefinitionFromLegacyId ( item . LegacyId ) )
. ReplaceAsync ( item ) ;
2026-06-13 06:15:55 +01:00
}
foreach ( var corporation in corporations )
{
2026-06-29 03:41:19 +01:00
await node . MutableDocument < AetheriaCorporation > ( AetheriaCatalogKeys . CorporationFromLegacyId ( corporation . LegacyId ) )
. ReplaceAsync ( corporation ) ;
2026-06-13 06:15:55 +01:00
}
foreach ( var nameFile in parsedNameFiles )
{
2026-06-29 03:41:19 +01:00
await node . MutableDocument < AetheriaNameFile > ( AetheriaCatalogKeys . NameFileFromLegacyId ( nameFile . LegacyId ) )
. ReplaceAsync ( nameFile ) ;
2026-06-13 06:15:55 +01:00
}
2026-06-29 03:41:19 +01:00
await node . FlushAsync ( ) ;
2026-06-28 04:22:59 +01:00
var runtimeCatalog = await node . RuntimeCatalog ( ) . LatestAsync ( ) . ConfigureAwait ( false ) ;
2026-07-14 00:50:13 +01:00
var importedCorporationKeys = runtimeCatalog . Corporations
. Select ( corporation = > corporation . CorporationKey )
. Where ( key = > ! string . IsNullOrWhiteSpace ( key ) )
. Distinct ( StringComparer . Ordinal )
. ToArray ( ) ;
if ( importedCorporationKeys . Length ! = corporations . Length )
{
throw new InvalidDataException (
$"Runtime catalog projected {importedCorporationKeys.Length} corporation keys from {corporations.Length} typed corporation documents." ) ;
}
2026-07-12 02:25:40 +01:00
var runtimeMines = runtimeCatalog . Items
. Where ( item = > string . Equals ( item . WeaponType , "Mine" , StringComparison . Ordinal ) )
. ToArray ( ) ;
if ( runtimeMines . Any ( item = > ! item . BehaviorPayloads . Any ( behavior = >
string . Equals ( behavior . Kind , "DeployableWeapon" , StringComparison . Ordinal ) & &
HasNumber ( behavior , 26 , 2 ) & &
HasNumber ( behavior , 27 , 30 ) & &
HasPerformanceStat ( behavior , 28 , 25 ) & &
HasNumber ( behavior , 29 , 2 ) & &
HasPerformanceStat ( behavior , 30 , 25 ) ) ) )
{
var diagnostic = string . Join ( "; " , runtimeMines . Select ( item = >
$"{item.Name} [{string.Join(" , ", item.BehaviorPayloads.Select(behavior => behavior.Kind + " : " + string.Join(" / ", behavior.Fields.Select(field => field.Key))))}]" ) ) ;
throw new InvalidDataException ( $"Mine catalog import did not preserve the canonical deployable lifecycle: {diagnostic}" ) ;
}
2026-06-29 03:41:19 +01:00
await node . MutableDocument < AetheriaTradeValuePolicy > ( AetheriaStateNode . TradeValuePolicyKey )
. ReplaceAsync ( AetheriaRuntimeStateMapper . ToTradeValuePolicy (
runtimeCatalog . TradeValueSettings ,
capturedAtUtc ) ) ;
await node . FlushAsync ( ) ;
await node . CatalogSurface ( ) . LatestAsync ( ) . ConfigureAwait ( false ) ;
2026-06-13 08:37:10 +01:00
2026-06-13 05:53:18 +01:00
await node . FlushAsync ( ) ;
2026-07-18 23:32:12 +01:00
await using ( var reopenedNode = await AetheriaStateNode . OpenAsync (
statePath ,
"aetheria-legacy-catalog-import-reopen-proof" ,
useDirectoryStore : true ) )
{
var reopenedCatalog = await reopenedNode . RuntimeCatalog ( ) . LatestAsync ( ) . ConfigureAwait ( false ) ;
var reopenedCorporationKeys = reopenedCatalog . Corporations
. Select ( corporation = > corporation . CorporationKey )
. Where ( key = > ! string . IsNullOrWhiteSpace ( key ) )
. Distinct ( StringComparer . Ordinal )
. ToArray ( ) ;
if ( reopenedCorporationKeys . Length ! = importedCorporationKeys . Length )
{
throw new InvalidDataException (
$"Reopened runtime catalog projected {reopenedCorporationKeys.Length} corporation keys after the importer flushed {importedCorporationKeys.Length}." ) ;
}
}
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}" ) ;
2026-07-14 00:50:13 +01:00
Console . WriteLine ( $"Runtime corporation keys: {string.Join(" , ", importedCorporationKeys)}" ) ;
2026-06-13 06:15:55 +01:00
Console . WriteLine ( $"Mapped name files: {parsedNameFiles.Length}" ) ;
2026-07-12 02:25:40 +01:00
Console . WriteLine ( $"Mapped mine weapons: {string.Join(" , ", runtimeMines.Select(item => item.Name + " ( " + item.ItemKey + " ) "))}" ) ;
static bool HasNumber ( AetheriaRuntimeBehaviorPayload behavior , int key , double expected ) = >
behavior . Fields . Any ( field = > field . Key = = key & & Math . Abs ( field . Value . NumberValue - expected ) < 0.000001 ) ;
static bool HasPerformanceStat ( AetheriaRuntimeBehaviorPayload behavior , int key , double expected )
{
var field = behavior . Fields . FirstOrDefault ( candidate = > candidate . Key = = key ) ;
return field ? . Value . Children . Count > = 2 & &
Math . Abs ( field . Value . Children [ 0 ] . NumberValue - expected ) < 0.000001 & &
Math . Abs ( field . Value . Children [ 1 ] . NumberValue - expected ) < 0.000001 ;
}
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
2026-06-13 10:29:12 +01:00
static void ResetMaterializedStateOutput ( string statePath )
{
DeleteFileIfExists ( statePath ) ;
DeleteDirectoryIfExists ( $"{statePath}.records" ) ;
var meshDirectory = Path . ChangeExtension ( statePath , ".cultmesh" ) ;
if ( ! string . Equals ( meshDirectory , statePath , StringComparison . OrdinalIgnoreCase ) )
{
DeleteDirectoryIfExists ( meshDirectory ) ;
}
}
static void DeleteFileIfExists ( string path )
{
if ( File . Exists ( path ) )
{
File . Delete ( path ) ;
}
}
static void DeleteDirectoryIfExists ( string path )
{
if ( Directory . Exists ( path ) )
{
Directory . Delete ( path , recursive : true ) ;
}
}
2026-07-16 17:27:01 +01:00
internal enum LegacyCatalogLayout
{
Current ,
April2021
}
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"
} ;
2026-06-15 20:33:32 +01:00
private static readonly Dictionary < int , string > ItemCategories = new ( )
{
[0] = "simple-commodity" ,
[1] = "compound-commodity" ,
[2] = "gear" ,
[3] = "hull" ,
[29] = "cargo-bay" ,
[30] = "docking-bay" ,
[31] = "weapon"
} ;
2026-06-13 06:15:55 +01:00
private static readonly HashSet < int > ItemUnionKeys = [ 0 , 1 , 2 , 3 , 29 , 30 , 31 ] ;
2026-06-13 09:01:13 +01:00
private static readonly Dictionary < int , string > HardpointTypes = CreateEnumMap ( [
"Hull" ,
"Tool" ,
"Thermal" ,
"Thruster" ,
"WarpDrive" ,
"Reactor" ,
"Radiator" ,
"Shield" ,
"Sensors" ,
"Energy" ,
"Ballistic" ,
"Launcher" ,
"ControlModule" ,
"AetherDrive"
] ) ;
2026-06-13 09:47:25 +01:00
private static readonly Dictionary < int , string > ItemRotations = CreateEnumMap ( [
"None" ,
"CounterClockwise" ,
"Reversed" ,
"Clockwise"
] ) ;
2026-06-13 09:01:13 +01:00
private static readonly Dictionary < int , string > HullTypes = CreateEnumMap ( [ "Ship" , "Station" , "Turret" ] ) ;
2026-06-14 03:52:20 +01:00
private static readonly Dictionary < int , string > SimpleCommodityCategories = CreateEnumMap ( [
"Minerals" ,
"Metals" ,
"Alloys" ,
"Compounds" ,
"Organics" ,
"Ammo" ,
"Consumer"
] ) ;
private static readonly Dictionary < int , string > CompoundCommodityCategories = CreateEnumMap ( [
"Wearables" ,
"Consumables" ,
"Luxuries" ,
"Tools" ,
"Manufacturing" ,
"Assemblies"
] ) ;
2026-06-13 09:01:13 +01:00
private static readonly Dictionary < int , string > WeaponRanges = CreateEnumMap ( [ "Melee" , "Short" , "Medium" , "Long" ] ) ;
private static readonly Dictionary < int , string > WeaponCalibers = CreateEnumMap ( [ "Small" , "Medium" , "Large" , "ExtraLarge" ] ) ;
private static readonly Dictionary < int , string > WeaponTypes = CreateEnumMap ( [
"ElectromagneticallyPropelled" ,
"ExplosivelyPropelled" ,
"Laser" ,
"Electrostatic" ,
"ParticleProjection" ,
"Missile" ,
"MicroMissile" ,
"SplitMissile" ,
"Mine" ,
"Jet"
] ) ;
private static readonly Dictionary < int , string > WeaponFireTypeFlags = new ( )
{
[1 << 0] = "Direct" ,
[1 << 1] = "Guided" ,
[1 << 2] = "Seeking" ,
[1 << 3] = "Continuous" ,
[1 << 4] = "Charged"
} ;
private static readonly Dictionary < int , string > WeaponModifierFlags = new ( )
{
[1 << 0] = "Airburst" ,
[1 << 1] = "Incendiary" ,
[1 << 2] = "ArmorPenetrating" ,
[1 << 3] = "NegativeEntropy" ,
[1 << 4] = "RapidFire" ,
[1 << 5] = "Burst" ,
[1 << 6] = "Cluster"
} ;
private static readonly Dictionary < int , string > BehaviorUnionNames = new ( )
{
[0] = "GuidedWeapon" ,
[1] = "Launcher" ,
[2] = "Reactor" ,
[3] = "Radiator" ,
[4] = "StatModifier" ,
[5] = "Sensor" ,
[6] = "Reflector" ,
[7] = "Shield" ,
[8] = "Thruster" ,
[9] = "Wear" ,
[10] = "VelocityConversion" ,
[11] = "VelocityLimit" ,
[12] = "AetherDrive" ,
[15] = "Cooldown" ,
[16] = "Heat" ,
[18] = "ItemUsage" ,
[20] = "Switch" ,
[21] = "Trigger" ,
[22] = "Visibility" ,
[23] = "Thermotoggle" ,
[24] = "EnergyDraw" ,
[26] = "MiningTool" ,
[28] = "ResourceScanner" ,
[31] = "Capacitor" ,
[32] = "Cockpit" ,
[33] = "HeatStorage" ,
[34] = "TurretController" ,
[35] = "InstantWeapon" ,
[36] = "ConstantWeapon" ,
[37] = "ChargedWeapon" ,
[38] = "AutoWeapon"
} ;
2026-06-13 06:15:55 +01:00
2026-07-16 17:27:01 +01:00
public static IReadOnlyList < LegacyCatalogEntry > Read (
string catalogPath ,
LegacyCatalogLayout layout = LegacyCatalogLayout . Current )
2026-06-13 06:15:55 +01:00
{
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 + + )
{
2026-07-16 17:27:01 +01:00
entries . Add ( ReadEntry ( ref reader , layout ) ) ;
2026-06-13 06:15:55 +01:00
}
return entries ;
}
public static LegacyCatalogEntry ReadSingle ( string path )
{
var bytes = File . ReadAllBytes ( path ) ;
var reader = new MessagePackReader ( bytes ) ;
2026-07-16 17:27:01 +01:00
return ReadEntry ( ref reader , LegacyCatalogLayout . Current ) ;
2026-06-13 06:15:55 +01:00
}
2026-07-16 17:27:01 +01:00
private static LegacyCatalogEntry ReadEntry (
ref MessagePackReader reader ,
LegacyCatalogLayout layout )
2026-06-13 06:15:55 +01:00
{
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 ) ;
2026-06-13 08:09:41 +01:00
var shape = ReadShape ( payload , 5 ) ;
2026-06-13 09:47:25 +01:00
var interiorShape = ReadInteriorShape ( unionKey , payload , shape ) ;
2026-06-14 05:39:15 +01:00
var dockingMaxSize = unionKey = = 30 ? ReadPoint ( payload , 25 ) : PointFacts . Empty ;
2026-07-16 17:27:01 +01:00
var hardpoints = unionKey = = 3
? ReadHardpoints ( payload , layout = = LegacyCatalogLayout . April2021 ? 20 : 23 )
: [ ] ;
2026-06-13 10:01:20 +01:00
var behaviorKey = unionKey = = 31 | | unionKey is 2 or 3 or 29 or 30 ? 11 : 10 ;
var behaviorPayloads = ReadBehaviorPayloads ( payload , behaviorKey ) ;
2026-06-15 20:33:32 +01:00
var itemCategory = GetItemCategory ( unionKey ) ;
var isConsumable = itemCategory = = "consumable" ;
2026-06-13 10:01:20 +01:00
var behaviorKinds = behaviorPayloads
. Select ( behavior = > behavior . Kind )
. Where ( kind = > ! string . IsNullOrWhiteSpace ( kind ) )
. Distinct ( StringComparer . OrdinalIgnoreCase )
. OrderBy ( kind = > kind , StringComparer . OrdinalIgnoreCase )
. ToArray ( ) ;
2026-06-13 06:15:55 +01:00
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 ,
2026-06-15 20:33:32 +01:00
Category = itemCategory ,
2026-06-13 06:15:55 +01:00
LegacyId = legacyId ,
Description = description ,
Mass = GetDouble ( payload , 4 ) ,
2026-06-14 04:35:51 +01:00
SpecificHeat = GetPositiveDoubleOrDefault ( payload , 6 , 1 ) ,
2026-06-14 05:08:39 +01:00
Conductivity = GetPositiveDoubleOrDefault ( payload , 7 , 1 ) ,
2026-06-13 08:09:41 +01:00
Volume = shape . OccupiedCells ,
2026-06-13 08:24:37 +01:00
ManufacturerLegacyId = GetOptionalGuid ( payload , 3 ) ,
2026-06-13 08:09:41 +01:00
Price = GetInt ( payload , 8 ) ,
ShapeWidth = shape . Width ,
ShapeHeight = shape . Height ,
OccupiedCells = shape . OccupiedCells ,
2026-06-13 09:32:24 +01:00
ShapeCells = shape . Cells ,
2026-06-13 09:47:25 +01:00
InteriorShapeWidth = interiorShape . Width ,
InteriorShapeHeight = interiorShape . Height ,
InteriorOccupiedCells = interiorShape . OccupiedCells ,
InteriorShapeCells = interiorShape . Cells ,
Hardpoints = hardpoints ,
2026-06-13 10:01:20 +01:00
BehaviorPayloads = behaviorPayloads ,
2026-07-16 17:27:01 +01:00
HardpointType = GetHardpointType ( unionKey , payload , layout ) ,
HullType = unionKey = = 3
? GetEnumName ( payload , layout = = LegacyCatalogLayout . April2021 ? 22 : 25 , HullTypes )
: "" ,
2026-06-13 09:01:13 +01:00
BehaviorKinds = behaviorKinds ,
2026-06-13 10:01:20 +01:00
BehaviorCount = behaviorPayloads . Length ,
2026-06-13 09:01:13 +01:00
MaxStack = unionKey = = 0 ? GetInt ( payload , 9 ) : 0 ,
2026-06-15 20:33:32 +01:00
Stackable = isConsumable & & GetBool ( payload , 11 ) ,
Duration = isConsumable ? GetDouble ( payload , 12 ) : 0 ,
2026-06-13 09:01:13 +01:00
Durability = unionKey is 2 or 3 or 29 or 30 or 31 ? GetDouble ( payload , 12 ) : 0 ,
2026-06-14 03:05:37 +01:00
MinimumTemperature = unionKey is 2 or 3 or 29 or 30 or 31 ? GetDouble ( payload , 13 ) : 0 ,
MaximumTemperature = unionKey is 2 or 3 or 29 or 30 or 31 ? GetDouble ( payload , 14 ) : 0 ,
2026-06-14 03:20:13 +01:00
ThermalPerformanceCurveKeys = unionKey is 2 or 3 or 29 or 30 or 31 ? ReadCurveKeys ( payload , 17 ) : [ ] ,
2026-06-14 06:57:53 +01:00
ThermalResilience = unionKey is 2 or 3 or 29 or 30 or 31 ? GetPositiveDoubleOrDefault ( payload , 18 , 1 ) : 1 ,
2026-07-16 17:27:01 +01:00
AudioStats = layout = = LegacyCatalogLayout . Current & & unionKey is 2 or 3 or 29 or 30 or 31
? ReadAudioStats ( payload , 22 )
: [ ] ,
2026-06-15 20:33:32 +01:00
EffectivenessCurveKeys = isConsumable ? ReadCurveKeys ( payload , 14 ) : [ ] ,
2026-07-16 17:27:01 +01:00
HullPrefab = unionKey = = 3
? GetString ( payload , layout = = LegacyCatalogLayout . April2021 ? 21 : 24 )
: "" ,
HullGridOffset = unionKey = = 3
? GetDouble ( payload , layout = = LegacyCatalogLayout . April2021 ? 23 : 26 )
: 0 ,
HullArmor = unionKey = = 3
? GetDouble ( payload , layout = = LegacyCatalogLayout . April2021 ? 24 : 27 )
: 0 ,
HullDrag = unionKey = = 3
? GetDouble ( payload , layout = = LegacyCatalogLayout . April2021 ? 25 : 28 )
: 0 ,
HullCanTow = layout = = LegacyCatalogLayout . Current & & unionKey = = 3 & & GetBool ( payload , 29 ) ,
2026-06-14 05:39:15 +01:00
DockingMaxSizeX = dockingMaxSize . X ,
DockingMaxSizeY = dockingMaxSize . Y ,
2026-07-16 17:27:01 +01:00
ActionBarIcon = layout = = LegacyCatalogLayout . Current & & unionKey is 2 or 3 or 29 or 30 or 31
? GetString ( payload , 20 )
: "" ,
2026-06-14 03:52:20 +01:00
SimpleCommodityCategory = unionKey = = 0 ? GetEnumName ( payload , 10 , SimpleCommodityCategories ) : "" ,
CompoundCommodityCategory = unionKey = = 1 ? GetEnumName ( payload , 11 , CompoundCommodityCategories ) : "" ,
2026-06-13 09:01:13 +01:00
WeaponRange = unionKey = = 31 ? GetEnumName ( payload , 24 , WeaponRanges ) : "" ,
WeaponCaliber = unionKey = = 31 ? GetEnumName ( payload , 25 , WeaponCalibers ) : "" ,
WeaponType = unionKey = = 31 ? GetEnumName ( payload , 26 , WeaponTypes ) : "" ,
WeaponFireTypes = unionKey = = 31 ? GetFlags ( payload , 27 , WeaponFireTypeFlags ) : "" ,
WeaponModifiers = unionKey = = 31 ? GetFlags ( payload , 28 , WeaponModifierFlags ) : "" ,
Tags = [ unionName , "legacy-catalog" , . . behaviorKinds . Select ( kind = > $"behavior:{kind}" ) ]
2026-06-13 06:15:55 +01:00
}
: null ,
unionKey = = 13
? new AetheriaCorporation
{
Name = name ,
LegacyId = legacyId ,
2026-06-13 08:09:41 +01:00
ShortName = GetString ( payload , 2 ) ,
Description = GetString ( payload , 3 ) ,
2026-06-13 08:24:37 +01:00
GeonameFileLegacyId = GetOptionalGuid ( payload , 9 ) ,
BossHullLegacyId = GetOptionalGuid ( payload , 10 ) ,
2026-06-13 08:09:41 +01:00
InfluenceDistance = GetInt ( payload , 11 ) ,
AllegianceCount = GetMapCount ( payload , 12 ) ,
OverworldMusic = GetUInt ( payload , 13 ) ,
CombatMusic = GetUInt ( payload , 14 ) ,
2026-06-13 13:18:03 +01:00
BossMusic = GetUInt ( payload , 15 ) ,
Allegiances = ReadAllegiances ( payload , 12 )
2026-06-13 06:15:55 +01:00
}
: null ,
unionKey = = 9
? new AetheriaNameFile
{
Name = name ,
LegacyId = legacyId ,
NameCount = GetArrayLength ( payload , 2 ) ,
2026-06-13 12:46:34 +01:00
SampleNames = GetStringArraySample ( payload , 2 , 8 ) ,
Names = GetStringArray ( payload , 2 )
2026-06-13 06:15:55 +01:00
}
: 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 ) ;
2026-06-13 13:18:03 +01:00
values [ FormatMapKey ( key ) ] = ReadValue ( ref reader ) ;
2026-06-13 06:15:55 +01:00
}
return values ;
}
2026-06-13 13:18:03 +01:00
private static string FormatMapKey ( object? key )
{
return key switch
{
byte [ ] bytes when bytes . Length = = 16 = > new Guid ( bytes ) . ToString ( "D" ) ,
string text = > text ,
_ = > key ? . ToString ( ) ? ? ""
} ;
}
2026-06-13 06:15:55 +01:00
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" )
: "" ;
}
2026-06-13 08:24:37 +01:00
private static string GetOptionalGuid ( IReadOnlyDictionary < int , object? > payload , int key )
{
var legacyId = GetGuid ( payload , key ) ;
return Guid . TryParse ( legacyId , out var guid ) & & guid = = Guid . Empty ? "" : legacyId ;
}
2026-06-13 06:15:55 +01:00
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
} ;
}
2026-06-14 04:35:51 +01:00
private static double GetPositiveDoubleOrDefault ( IReadOnlyDictionary < int , object? > payload , int key , double fallback )
{
var value = GetDouble ( payload , key ) ;
return value > 0 ? value : fallback ;
}
2026-06-13 08:09:41 +01:00
private static int GetInt ( IReadOnlyDictionary < int , object? > payload , int key )
{
if ( ! payload . TryGetValue ( key , out var value ) | | value = = null )
{
return 0 ;
}
return value switch
{
long integerValue = > checked ( ( int ) integerValue ) ,
int integerValue = > integerValue ,
double doubleValue = > checked ( ( int ) doubleValue ) ,
float floatValue = > checked ( ( int ) floatValue ) ,
_ = > 0
} ;
}
2026-06-13 09:01:13 +01:00
private static bool GetBool ( IReadOnlyDictionary < int , object? > payload , int key )
{
return payload . TryGetValue ( key , out var value ) & & value is true ;
}
2026-06-13 08:09:41 +01:00
private static uint GetUInt ( IReadOnlyDictionary < int , object? > payload , int key )
{
if ( ! payload . TryGetValue ( key , out var value ) | | value = = null )
{
return 0 ;
}
return value switch
{
long integerValue when integerValue > = 0 = > checked ( ( uint ) integerValue ) ,
int integerValue when integerValue > = 0 = > checked ( ( uint ) integerValue ) ,
double doubleValue when doubleValue > = 0 = > checked ( ( uint ) doubleValue ) ,
float floatValue when floatValue > = 0 = > checked ( ( uint ) floatValue ) ,
_ = > 0
} ;
}
private static int GetMapCount ( IReadOnlyDictionary < int , object? > payload , int key )
{
return payload . TryGetValue ( key , out var value ) & & value is Dictionary < string , object? > map ? map . Count : 0 ;
}
2026-06-13 13:18:03 +01:00
private static AetheriaCorporationAllegiance [ ] ReadAllegiances ( IReadOnlyDictionary < int , object? > payload , int key )
{
if ( ! payload . TryGetValue ( key , out var value ) | | value is not Dictionary < string , object? > map )
{
return [ ] ;
}
return map
. Select ( entry = > new AetheriaCorporationAllegiance
{
CorporationLegacyId = NormalizeLegacyId ( entry . Key ) ,
Weight = ReadDoubleValue ( entry . Value )
} )
. Where ( allegiance = > ! string . IsNullOrWhiteSpace ( allegiance . CorporationLegacyId ) )
. ToArray ( ) ;
}
private static string NormalizeLegacyId ( string legacyId )
{
return Guid . TryParse ( legacyId , out var parsed ) & & parsed ! = Guid . Empty
? parsed . ToString ( "D" )
: "" ;
}
2026-06-13 06:15:55 +01:00
private static int GetArrayLength ( IReadOnlyDictionary < int , object? > payload , int key )
{
return payload . TryGetValue ( key , out var value ) & & value is object? [ ] array ? array . Length : 0 ;
}
2026-07-16 17:27:01 +01:00
private static string GetHardpointType (
int unionKey ,
IReadOnlyDictionary < int , object? > payload ,
LegacyCatalogLayout layout )
2026-06-13 09:01:13 +01:00
{
return unionKey switch
{
3 = > "Hull" ,
29 or 30 = > "Tool" ,
2026-07-16 17:27:01 +01:00
2 or 31 = > GetEnumName (
payload ,
layout = = LegacyCatalogLayout . April2021 ? 20 : 23 ,
HardpointTypes ) ,
2026-06-13 09:01:13 +01:00
_ = > ""
} ;
}
2026-06-15 20:33:32 +01:00
private static string GetItemCategory ( int unionKey )
{
return ItemCategories . TryGetValue ( unionKey , out var category )
? category
: $"legacy-union:{unionKey}" ;
}
2026-06-13 09:01:13 +01:00
private static string GetEnumName (
IReadOnlyDictionary < int , object? > payload ,
int key ,
IReadOnlyDictionary < int , string > names )
{
var value = GetInt ( payload , key ) ;
return names . TryGetValue ( value , out var name ) ? name : "" ;
}
private static string GetFlags (
IReadOnlyDictionary < int , object? > payload ,
int key ,
IReadOnlyDictionary < int , string > names )
{
var value = GetInt ( payload , key ) ;
if ( value = = 0 )
{
return "None" ;
}
var flags = names
. Where ( entry = > ( value & entry . Key ) = = entry . Key )
. Select ( entry = > entry . Value )
. ToArray ( ) ;
return flags . Length = = 0 ? "" : string . Join ( "|" , flags ) ;
}
2026-06-13 09:47:25 +01:00
private static AetheriaItemHardpoint [ ] ReadHardpoints ( IReadOnlyDictionary < int , object? > payload , int key )
{
if ( ! payload . TryGetValue ( key , out var value ) | | value is not object? [ ] hardpoints )
{
return [ ] ;
}
return hardpoints
. Select ( ReadHardpoint )
. Where ( hardpoint = > hardpoint ! = null )
. Cast < AetheriaItemHardpoint > ( )
. ToArray ( ) ;
}
private static AetheriaItemHardpoint ? ReadHardpoint ( object? value )
{
if ( value is not object? [ ] fields )
{
return null ;
}
var position = ReadPoint ( fields . ElementAtOrDefault ( 1 ) ) ;
var shape = ReadShapeValue ( fields . ElementAtOrDefault ( 2 ) ) ;
return new AetheriaItemHardpoint
{
Type = ReadEnumName ( fields . ElementAtOrDefault ( 0 ) , HardpointTypes ) ,
PositionX = position . X ,
PositionY = position . Y ,
ShapeWidth = shape . Width ,
ShapeHeight = shape . Height ,
OccupiedCells = shape . OccupiedCells ,
ShapeCells = shape . Cells ,
Transform = ReadStringValue ( fields . ElementAtOrDefault ( 3 ) ) ,
Rotation = ReadEnumName ( fields . ElementAtOrDefault ( 4 ) , ItemRotations ) ,
Armor = ReadDoubleValue ( fields . ElementAtOrDefault ( 5 ) )
} ;
}
2026-06-14 03:20:13 +01:00
private static AetheriaCurveKey [ ] ReadCurveKeys ( IReadOnlyDictionary < int , object? > payload , int key )
{
if ( ! payload . TryGetValue ( key , out var value ) )
{
return [ ] ;
}
return ExtractCurveKeyTuples ( value )
. Select ( tuple = > new AetheriaCurveKey
{
Time = ReadDoubleValue ( tuple . ElementAtOrDefault ( 0 ) ) ,
Value = ReadDoubleValue ( tuple . ElementAtOrDefault ( 1 ) ) ,
InTangent = ReadDoubleValue ( tuple . ElementAtOrDefault ( 2 ) ) ,
OutTangent = ReadDoubleValue ( tuple . ElementAtOrDefault ( 3 ) )
} )
. OrderBy ( curveKey = > curveKey . Time )
. ToArray ( ) ;
}
2026-06-14 07:08:06 +01:00
private static AetheriaItemAudioStat [ ] ReadAudioStats ( IReadOnlyDictionary < int , object? > payload , int key )
{
if ( ! payload . TryGetValue ( key , out var value ) | | value is not object? [ ] audioStats )
{
return [ ] ;
}
return audioStats
. Select ( ReadAudioStat )
. Where ( audioStat = > audioStat ! = null )
. Cast < AetheriaItemAudioStat > ( )
. ToArray ( ) ;
}
private static AetheriaItemAudioStat ? ReadAudioStat ( object? value )
{
if ( value is not object? [ ] fields )
{
return null ;
}
return new AetheriaItemAudioStat
{
Parameter = ReadUIntValue ( fields . ElementAtOrDefault ( 0 ) ) ,
Stat = ReadPerformanceStat ( fields . ElementAtOrDefault ( 1 ) )
} ;
}
private static AetheriaItemPerformanceStat ReadPerformanceStat ( object? value )
{
if ( value is not object? [ ] fields )
{
return new AetheriaItemPerformanceStat ( ) ;
}
return new AetheriaItemPerformanceStat
{
Min = ReadDoubleValue ( fields . ElementAtOrDefault ( 0 ) ) ,
Max = ReadDoubleValue ( fields . ElementAtOrDefault ( 1 ) ) ,
HeatExponentMultiplier = ReadDoubleValue ( fields . ElementAtOrDefault ( 2 ) ) ,
DurabilityExponentMultiplier = ReadDoubleValue ( fields . ElementAtOrDefault ( 3 ) ) ,
QualityExponent = ReadDoubleValue ( fields . ElementAtOrDefault ( 4 ) )
} ;
}
2026-06-14 03:20:13 +01:00
private static IEnumerable < object? [ ] > ExtractCurveKeyTuples ( object? value )
{
if ( value is not object? [ ] values )
{
return [ ] ;
}
if ( LooksLikeCurveKey ( values ) )
{
return [ values ] ;
}
var directKeys = values
. OfType < object? [ ] > ( )
. Where ( LooksLikeCurveKey )
. ToArray ( ) ;
if ( directKeys . Length > 0 )
{
return directKeys ;
}
return values
. OfType < object? [ ] > ( )
. SelectMany ( ExtractCurveKeyTuples )
. ToArray ( ) ;
}
private static bool LooksLikeCurveKey ( object? [ ] values )
{
return values . Length > = 4 & &
IsNumber ( values [ 0 ] ) & &
IsNumber ( values [ 1 ] ) & &
IsNumber ( values [ 2 ] ) & &
IsNumber ( values [ 3 ] ) ;
}
private static bool IsNumber ( object? value )
{
return value is byte or sbyte or short or ushort or int or uint or long or ulong or float or double or decimal ;
}
2026-06-13 10:01:20 +01:00
private static AetheriaBehaviorPayload [ ] ReadBehaviorPayloads ( IReadOnlyDictionary < int , object? > payload , int key )
2026-06-13 09:01:13 +01:00
{
if ( ! payload . TryGetValue ( key , out var value ) | | value is not object? [ ] behaviors )
{
return [ ] ;
}
2026-07-13 16:19:35 +01:00
var result = behaviors
2026-06-13 10:01:20 +01:00
. Select ( ReadBehaviorPayload )
. Where ( behavior = > behavior ! = null )
. Cast < AetheriaBehaviorPayload > ( )
2026-06-13 09:01:13 +01:00
. ToArray ( ) ;
2026-07-13 16:19:35 +01:00
for ( var index = 0 ; index < result . Length ; index + + )
if ( string . IsNullOrWhiteSpace ( result [ index ] . BehaviorId ) )
result [ index ] . BehaviorId = $"behavior:{index}:{result[index].Kind}" ;
return result ;
2026-06-13 09:01:13 +01:00
}
2026-06-13 10:01:20 +01:00
private static AetheriaBehaviorPayload ? ReadBehaviorPayload ( object? value )
2026-06-13 09:01:13 +01:00
{
2026-06-13 10:01:20 +01:00
if ( value is not object? [ ] union | | union . Length < 2 )
2026-06-13 09:01:13 +01:00
{
2026-06-13 10:01:20 +01:00
return null ;
2026-06-13 09:01:13 +01:00
}
var unionKey = union [ 0 ] switch
{
long longValue = > checked ( ( int ) longValue ) ,
int intValue = > intValue ,
_ = > - 1
} ;
2026-06-13 10:01:20 +01:00
var fields = union [ 1 ] is object? [ ] payload
? payload
. Select ( ( fieldValue , index ) = > new AetheriaBehaviorField
{
Key = index ,
Value = ReadBehaviorValue ( fieldValue )
} )
. ToArray ( )
: [ ] ;
return new AetheriaBehaviorPayload
{
UnionKey = unionKey ,
Kind = BehaviorUnionNames . TryGetValue ( unionKey , out var name ) ? name : $"behavior:{unionKey}" ,
Group = fields . FirstOrDefault ( field = > field . Key = = 0 ) ? . Value . Kind = = "number"
? checked ( ( int ) fields [ 0 ] . Value . NumberValue )
: 0 ,
Fields = fields
} ;
}
private static AetheriaBehaviorValue ReadBehaviorValue ( object? value )
{
if ( value = = null )
{
return new AetheriaBehaviorValue { Kind = "nil" } ;
}
return value switch
{
bool boolValue = > new AetheriaBehaviorValue { Kind = "bool" , BoolValue = boolValue } ,
string stringValue = > new AetheriaBehaviorValue { Kind = "string" , StringValue = stringValue } ,
2026-06-15 16:10:13 +01:00
byte [ ] bytes when bytes . Length = = 16 = > ReadLegacyBehaviorId ( bytes ) ,
2026-06-13 10:01:20 +01:00
byte [ ] bytes = > new AetheriaBehaviorValue
{
Kind = "binary" ,
StringValue = Convert . ToHexString ( bytes ) . ToLowerInvariant ( )
} ,
object? [ ] array = > new AetheriaBehaviorValue
{
Kind = "array" ,
Children = array . Select ( ReadBehaviorValue ) . ToArray ( )
} ,
Dictionary < string , object? > map = > new AetheriaBehaviorValue
{
Kind = "map" ,
MapEntries = map
. OrderBy ( entry = > entry . Key , StringComparer . Ordinal )
. Select ( entry = > new AetheriaBehaviorMapEntry
{
Key = entry . Key ,
Value = ReadBehaviorValue ( entry . Value )
} )
. ToArray ( )
} ,
double doubleValue = > new AetheriaBehaviorValue { Kind = "number" , NumberValue = doubleValue } ,
float floatValue = > new AetheriaBehaviorValue { Kind = "number" , NumberValue = floatValue } ,
long integerValue = > new AetheriaBehaviorValue { Kind = "number" , NumberValue = integerValue } ,
int integerValue = > new AetheriaBehaviorValue { Kind = "number" , NumberValue = integerValue } ,
_ = > new AetheriaBehaviorValue
{
Kind = "unknown" ,
StringValue = value . ToString ( ) ? ? ""
}
2026-06-15 16:10:13 +01:00
} ;
}
private static AetheriaBehaviorValue ReadLegacyBehaviorId ( byte [ ] bytes )
{
var legacyId = new Guid ( bytes ) ;
return new AetheriaBehaviorValue
{
Kind = "legacy-id" ,
LegacyIdValue = legacyId . ToString ( "D" ) ,
ItemKeyValue = ProjectLegacyItemKey ( legacyId )
2026-06-13 10:01:20 +01:00
} ;
2026-06-13 09:01:13 +01:00
}
2026-06-15 16:10:13 +01:00
private static string ProjectLegacyItemKey ( Guid legacyItemId )
{
return legacyItemId = = Guid . Empty ? "" : $"aetheria.item_definition:legacy:{legacyItemId:D}" ;
}
2026-06-13 09:01:13 +01:00
private static Dictionary < int , string > CreateEnumMap ( string [ ] names )
{
return names
. Select ( ( name , index ) = > new { name , index } )
. ToDictionary ( entry = > entry . index , entry = > entry . name ) ;
}
2026-06-13 06:15:55 +01:00
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 ( )
: [ ] ;
}
2026-06-13 08:09:41 +01:00
2026-06-13 12:46:34 +01:00
private static string [ ] GetStringArray ( IReadOnlyDictionary < int , object? > payload , int key )
{
return payload . TryGetValue ( key , out var value ) & & value is object? [ ] array
? array . OfType < string > ( ) . ToArray ( )
: [ ] ;
}
2026-06-13 08:09:41 +01:00
private static ShapeFacts ReadShape ( IReadOnlyDictionary < int , object? > payload , int key )
{
if ( ! payload . TryGetValue ( key , out var value ) | | value = = null )
{
2026-06-13 09:32:24 +01:00
return ShapeFacts . Empty ;
2026-06-13 08:09:41 +01:00
}
2026-06-13 09:47:25 +01:00
return ReadShapeValue ( value ) ;
}
private static ShapeFacts ReadInteriorShape (
int unionKey ,
IReadOnlyDictionary < int , object? > payload ,
ShapeFacts itemShape )
{
return unionKey switch
{
3 = > itemShape . Shrink ( ) ,
29 or 30 = > ReadShape ( payload , 24 ) ,
_ = > ShapeFacts . Empty
} ;
}
private static ShapeFacts ReadShapeValue ( object? value )
{
if ( value = = null )
{
return ShapeFacts . Empty ;
}
2026-06-13 08:09:41 +01:00
var matrix = UnwrapShapeMatrix ( value ) ;
if ( matrix is not object? [ ] columns )
{
2026-06-13 09:32:24 +01:00
return ShapeFacts . Empty ;
2026-06-13 08:09:41 +01:00
}
2026-07-13 16:53:25 +01:00
if ( TryReadRectangularShape ( columns , out var rectangularShape ) )
{
return rectangularShape ;
}
2026-06-13 08:09:41 +01:00
var width = columns . Length ;
var height = 0 ;
2026-06-13 09:32:24 +01:00
var cellsByRow = new List < AetheriaShapeCell > ( ) ;
for ( var x = 0 ; x < columns . Length ; x + + )
2026-06-13 08:09:41 +01:00
{
2026-06-13 09:32:24 +01:00
var column = columns [ x ] ;
2026-06-13 08:09:41 +01:00
if ( column is not object? [ ] cells )
{
if ( column is bool occupiedCell )
{
height = Math . Max ( height , 1 ) ;
if ( occupiedCell )
{
2026-06-13 09:32:24 +01:00
cellsByRow . Add ( new AetheriaShapeCell { X = x , Y = 0 } ) ;
2026-06-13 08:09:41 +01:00
}
}
continue ;
}
height = Math . Max ( height , cells . Length ) ;
2026-06-13 09:32:24 +01:00
for ( var y = 0 ; y < cells . Length ; y + + )
{
if ( cells [ y ] is true )
{
cellsByRow . Add ( new AetheriaShapeCell { X = x , Y = y } ) ;
}
}
2026-06-13 08:09:41 +01:00
}
2026-06-13 09:32:24 +01:00
return new ShapeFacts (
width ,
height ,
cellsByRow
. OrderBy ( cell = > cell . Y )
. ThenBy ( cell = > cell . X )
. ToArray ( ) ) ;
2026-06-13 08:09:41 +01:00
}
2026-07-13 16:53:25 +01:00
private static bool TryReadRectangularShape ( object? [ ] envelope , out ShapeFacts shape )
{
shape = ShapeFacts . Empty ;
if ( envelope . Length ! = 3 | |
envelope [ 2 ] is not Array flattened )
{
return false ;
}
var width = ReadIntValue ( envelope [ 0 ] ) ;
var height = ReadIntValue ( envelope [ 1 ] ) ;
if ( width < = 0 | | height < = 0 | | flattened . Length ! = width * height )
{
return false ;
}
var cells = new List < AetheriaShapeCell > ( ) ;
for ( var x = 0 ; x < width ; x + + )
for ( var y = 0 ; y < height ; y + + )
{
if ( flattened . GetValue ( x * height + y ) is true )
{
cells . Add ( new AetheriaShapeCell { X = x , Y = y } ) ;
}
}
shape = new ShapeFacts ( width , height , cells
. OrderBy ( cell = > cell . Y )
. ThenBy ( cell = > cell . X )
. ToArray ( ) ) ;
return true ;
}
2026-06-13 08:09:41 +01:00
private static object? UnwrapShapeMatrix ( object? value )
{
return value is object? [ ] shapeObject & & shapeObject . Length = = 1 ? shapeObject [ 0 ] : value ;
}
2026-06-13 09:47:25 +01:00
private static PointFacts ReadPoint ( object? value )
{
if ( value is not object? [ ] coordinates )
{
return PointFacts . Empty ;
}
return new PointFacts (
ReadIntValue ( coordinates . ElementAtOrDefault ( 0 ) ) ,
ReadIntValue ( coordinates . ElementAtOrDefault ( 1 ) ) ) ;
}
2026-06-14 05:39:15 +01:00
private static PointFacts ReadPoint ( IReadOnlyDictionary < int , object? > payload , int key )
{
return payload . TryGetValue ( key , out var value )
? ReadPoint ( value )
: PointFacts . Empty ;
}
2026-06-13 09:47:25 +01:00
private static string ReadEnumName ( object? value , IReadOnlyDictionary < int , string > names )
{
var index = ReadIntValue ( value ) ;
return names . TryGetValue ( index , out var name ) ? name : "" ;
}
private static int ReadIntValue ( object? value )
{
return value switch
{
long integerValue = > checked ( ( int ) integerValue ) ,
int integerValue = > integerValue ,
double doubleValue = > checked ( ( int ) doubleValue ) ,
float floatValue = > checked ( ( int ) floatValue ) ,
_ = > 0
} ;
}
2026-06-14 07:08:06 +01:00
private static uint ReadUIntValue ( object? value )
{
return value switch
{
long integerValue when integerValue > = 0 = > checked ( ( uint ) integerValue ) ,
int integerValue when integerValue > = 0 = > checked ( ( uint ) integerValue ) ,
double doubleValue when doubleValue > = 0 = > checked ( ( uint ) doubleValue ) ,
float floatValue when floatValue > = 0 = > checked ( ( uint ) floatValue ) ,
_ = > 0
} ;
}
2026-06-13 09:47:25 +01:00
private static double ReadDoubleValue ( object? value )
{
return value switch
{
double doubleValue = > doubleValue ,
float floatValue = > floatValue ,
long integerValue = > integerValue ,
int integerValue = > integerValue ,
_ = > 0
} ;
}
private static string ReadStringValue ( object? value )
{
return value as string ? ? "" ;
}
2026-06-13 06:15:55 +01:00
}
internal sealed record LegacyCatalogEntry (
AetheriaLegacyCatalogEntrySummary Summary ,
AetheriaItemDefinition ? ItemDefinition ,
AetheriaCorporation ? Corporation ,
AetheriaNameFile ? NameFile ) ;
2026-06-13 08:09:41 +01:00
2026-06-13 09:32:24 +01:00
internal readonly record struct ShapeFacts ( int Width , int Height , AetheriaShapeCell [ ] Cells )
{
public static ShapeFacts Empty { get ; } = new ( 0 , 0 , [ ] ) ;
public int OccupiedCells = > Cells . Length ;
2026-06-13 09:47:25 +01:00
public ShapeFacts Shrink ( )
{
if ( Width = = 0 | | Height = = 0 | | Cells . Length = = 0 )
{
return Empty ;
}
var occupied = Cells
. Select ( cell = > ( cell . X , cell . Y ) )
. ToHashSet ( ) ;
var interior = Cells
. Where ( cell = >
IsOccupied ( occupied , cell . X - 1 , cell . Y - 1 ) & &
IsOccupied ( occupied , cell . X , cell . Y - 1 ) & &
IsOccupied ( occupied , cell . X + 1 , cell . Y - 1 ) & &
IsOccupied ( occupied , cell . X - 1 , cell . Y ) & &
IsOccupied ( occupied , cell . X + 1 , cell . Y ) & &
IsOccupied ( occupied , cell . X - 1 , cell . Y + 1 ) & &
IsOccupied ( occupied , cell . X , cell . Y + 1 ) & &
IsOccupied ( occupied , cell . X + 1 , cell . Y + 1 ) )
. Select ( cell = > new AetheriaShapeCell { X = cell . X , Y = cell . Y } )
. ToArray ( ) ;
return new ShapeFacts ( Width , Height , interior ) ;
}
private static bool IsOccupied ( IReadOnlySet < ( int X , int Y ) > occupied , int x , int y )
{
return occupied . Contains ( ( x , y ) ) ;
}
}
internal readonly record struct PointFacts ( int X , int Y )
{
public static PointFacts Empty { get ; } = new ( 0 , 0 ) ;
2026-06-13 09:32:24 +01:00
}