Import typed behavior payloads

This commit is contained in:
Meta Vens 2026-06-13 10:01:20 +01:00
commit 21a319a67d
151 changed files with 610 additions and 293 deletions

Binary file not shown.

View file

@ -313,7 +313,14 @@ internal static class LegacyCatalogReader
var shape = ReadShape(payload, 5);
var interiorShape = ReadInteriorShape(unionKey, payload, shape);
var hardpoints = unionKey == 3 ? ReadHardpoints(payload, 23) : [];
var behaviorKinds = ReadBehaviorKinds(payload, unionKey == 31 || unionKey is 2 or 3 or 29 or 30 ? 11 : 10);
var behaviorKey = unionKey == 31 || unionKey is 2 or 3 or 29 or 30 ? 11 : 10;
var behaviorPayloads = ReadBehaviorPayloads(payload, behaviorKey);
var behaviorKinds = behaviorPayloads
.Select(behavior => behavior.Kind)
.Where(kind => !string.IsNullOrWhiteSpace(kind))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(kind => kind, StringComparer.OrdinalIgnoreCase)
.ToArray();
var summary = new AetheriaLegacyCatalogEntrySummary
{
LegacyId = legacyId,
@ -345,10 +352,11 @@ internal static class LegacyCatalogReader
InteriorOccupiedCells = interiorShape.OccupiedCells,
InteriorShapeCells = interiorShape.Cells,
Hardpoints = hardpoints,
BehaviorPayloads = behaviorPayloads,
HardpointType = GetHardpointType(unionKey, payload),
HullType = unionKey == 3 ? GetEnumName(payload, 25, HullTypes) : "",
BehaviorKinds = behaviorKinds,
BehaviorCount = behaviorKinds.Length,
BehaviorCount = behaviorPayloads.Length,
MaxStack = unionKey == 0 ? GetInt(payload, 9) : 0,
Stackable = false,
Duration = 0,
@ -640,7 +648,7 @@ internal static class LegacyCatalogReader
};
}
private static string[] ReadBehaviorKinds(IReadOnlyDictionary<int, object?> payload, int key)
private static AetheriaBehaviorPayload[] ReadBehaviorPayloads(IReadOnlyDictionary<int, object?> payload, int key)
{
if (!payload.TryGetValue(key, out var value) || value is not object?[] behaviors)
{
@ -648,18 +656,17 @@ internal static class LegacyCatalogReader
}
return behaviors
.Select(ReadBehaviorKind)
.Where(kind => !string.IsNullOrWhiteSpace(kind))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(kind => kind, StringComparer.OrdinalIgnoreCase)
.Select(ReadBehaviorPayload)
.Where(behavior => behavior != null)
.Cast<AetheriaBehaviorPayload>()
.ToArray();
}
private static string ReadBehaviorKind(object? value)
private static AetheriaBehaviorPayload? ReadBehaviorPayload(object? value)
{
if (value is not object?[] union || union.Length < 1)
if (value is not object?[] union || union.Length < 2)
{
return "";
return null;
}
var unionKey = union[0] switch
@ -668,7 +675,74 @@ internal static class LegacyCatalogReader
int intValue => intValue,
_ => -1
};
return BehaviorUnionNames.TryGetValue(unionKey, out var name) ? name : $"behavior:{unionKey}";
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 },
byte[] bytes when bytes.Length == 16 => new AetheriaBehaviorValue
{
Kind = "legacy-id",
LegacyIdValue = new Guid(bytes).ToString("D")
},
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() ?? ""
}
};
}
private static Dictionary<int, string> CreateEnumMap(string[] names)

View file

@ -62,6 +62,20 @@ if (!catalog.FindItemsByBehavior(behaviorKind).Any())
throw new InvalidOperationException($"Runtime catalog behavior lookup failed for {behaviorKind}.");
}
var behaviorHost = catalog.Items.FirstOrDefault(item => item.BehaviorPayloads.Count > 0)
?? throw new InvalidOperationException("Runtime catalog has no typed behavior payloads.");
if (!behaviorHost.BehaviorPayloads.All(payload => behaviorHost.BehaviorKinds.Contains(payload.Kind)))
{
throw new InvalidOperationException(
$"Runtime behavior payload kind index mismatch for {behaviorHost.Name}.");
}
var behaviorPayload = behaviorHost.BehaviorPayloads.First();
if (behaviorPayload.Fields.Count == 0)
{
throw new InvalidOperationException($"Runtime behavior payload has no fields for {behaviorHost.Name}.");
}
var equipment = catalog.EquipmentItems.First();
if (!catalog.FindItemsByHardpoint(equipment.HardpointType).Any())
{
@ -85,5 +99,6 @@ Console.WriteLine($"Aetheria Unity runtime catalog smoke passed: {statePath}");
Console.WriteLine($"Items/trade/equipment: {catalog.Items.Count}/{catalog.TradeItems.Count}/{catalog.EquipmentItems.Count}");
Console.WriteLine($"Shape mask sample: {shaped.Name} {shaped.ShapeWidth}x{shaped.ShapeHeight}/{shaped.ShapeCells.Count}");
Console.WriteLine($"Interior/hardpoint sample: {interior.Name} {interior.InteriorShapeCells.Count}; {hardpointHost.Name} {hardpoint.Type} {hardpoint.ShapeCells.Count}");
Console.WriteLine($"Behavior payload sample: {behaviorHost.Name} {behaviorPayload.Kind}/{behaviorPayload.Fields.Count}");
Console.WriteLine($"Behavior sample: {behaviorKind}");
Console.WriteLine($"Eve surface: {surface.Surface.Id}");

View file

@ -113,6 +113,7 @@ public sealed class AetheriaRuntimeCatalogItem
int interiorOccupiedCells,
IReadOnlyList<AetheriaRuntimeShapeCell> interiorShapeCells,
IReadOnlyList<AetheriaRuntimeHardpoint> hardpoints,
IReadOnlyList<AetheriaRuntimeBehaviorPayload> behaviorPayloads,
string hardpointType,
string hullType,
IReadOnlyList<string> behaviorKinds,
@ -141,6 +142,7 @@ public sealed class AetheriaRuntimeCatalogItem
InteriorOccupiedCells = interiorOccupiedCells;
InteriorShapeCells = interiorShapeCells;
Hardpoints = hardpoints;
BehaviorPayloads = behaviorPayloads;
HardpointType = hardpointType;
HullType = hullType;
BehaviorKinds = behaviorKinds;
@ -170,6 +172,7 @@ public sealed class AetheriaRuntimeCatalogItem
public int InteriorOccupiedCells { get; }
public IReadOnlyList<AetheriaRuntimeShapeCell> InteriorShapeCells { get; }
public IReadOnlyList<AetheriaRuntimeHardpoint> Hardpoints { get; }
public IReadOnlyList<AetheriaRuntimeBehaviorPayload> BehaviorPayloads { get; }
public string HardpointType { get; }
public string HullType { get; }
public IReadOnlyList<string> BehaviorKinds { get; }
@ -201,6 +204,7 @@ public sealed class AetheriaRuntimeCatalogItem
item.InteriorOccupiedCells,
item.InteriorShapeCells.Select(AetheriaRuntimeShapeCell.FromState).ToArray(),
item.Hardpoints.Select(AetheriaRuntimeHardpoint.FromState).ToArray(),
item.BehaviorPayloads.Select(AetheriaRuntimeBehaviorPayload.FromState).ToArray(),
item.HardpointType,
item.HullType,
item.BehaviorKinds,
@ -232,6 +236,121 @@ public sealed class AetheriaRuntimeShapeCell
}
}
public sealed class AetheriaRuntimeBehaviorPayload
{
public AetheriaRuntimeBehaviorPayload(
int unionKey,
string kind,
int group,
IReadOnlyList<AetheriaRuntimeBehaviorField> fields)
{
UnionKey = unionKey;
Kind = kind;
Group = group;
Fields = fields;
}
public int UnionKey { get; }
public string Kind { get; }
public int Group { get; }
public IReadOnlyList<AetheriaRuntimeBehaviorField> Fields { get; }
public static AetheriaRuntimeBehaviorPayload FromState(AetheriaBehaviorPayload payload)
{
return new AetheriaRuntimeBehaviorPayload(
payload.UnionKey,
payload.Kind,
payload.Group,
payload.Fields.Select(AetheriaRuntimeBehaviorField.FromState).ToArray());
}
}
public sealed class AetheriaRuntimeBehaviorField
{
public AetheriaRuntimeBehaviorField(int key, AetheriaRuntimeBehaviorValue value)
{
Key = key;
Value = value;
}
public int Key { get; }
public AetheriaRuntimeBehaviorValue Value { get; }
public static AetheriaRuntimeBehaviorField FromState(AetheriaBehaviorField field)
{
return new AetheriaRuntimeBehaviorField(field.Key, AetheriaRuntimeBehaviorValue.FromState(field.Value));
}
}
public sealed class AetheriaRuntimeBehaviorMapEntry
{
public AetheriaRuntimeBehaviorMapEntry(string key, AetheriaRuntimeBehaviorValue value)
{
Key = key;
Value = value;
}
public string Key { get; }
public AetheriaRuntimeBehaviorValue Value { get; }
public static AetheriaRuntimeBehaviorMapEntry FromState(AetheriaBehaviorMapEntry entry)
{
return new AetheriaRuntimeBehaviorMapEntry(entry.Key, AetheriaRuntimeBehaviorValue.FromState(entry.Value));
}
}
public sealed class AetheriaRuntimeBehaviorValue
{
public AetheriaRuntimeBehaviorValue(
string kind,
string stringValue,
double numberValue,
bool boolValue,
string legacyIdValue,
IReadOnlyList<AetheriaRuntimeBehaviorValue> children,
IReadOnlyList<AetheriaRuntimeBehaviorMapEntry> mapEntries)
{
Kind = kind;
StringValue = stringValue;
NumberValue = numberValue;
BoolValue = boolValue;
LegacyIdValue = legacyIdValue;
Children = children;
MapEntries = mapEntries;
}
public string Kind { get; }
public string StringValue { get; }
public double NumberValue { get; }
public bool BoolValue { get; }
public string LegacyIdValue { get; }
public IReadOnlyList<AetheriaRuntimeBehaviorValue> Children { get; }
public IReadOnlyList<AetheriaRuntimeBehaviorMapEntry> MapEntries { get; }
public static AetheriaRuntimeBehaviorValue FromState(AetheriaBehaviorValue value)
{
return new AetheriaRuntimeBehaviorValue(
value.Kind,
value.StringValue,
value.NumberValue,
value.BoolValue,
value.LegacyIdValue,
value.Children.Select(FromState).ToArray(),
value.MapEntries.Select(AetheriaRuntimeBehaviorMapEntry.FromState).ToArray());
}
}
public sealed class AetheriaRuntimeHardpoint
{
public AetheriaRuntimeHardpoint(

View file

@ -48,6 +48,13 @@ var interiorShapeItems = items.Count(item => item.InteriorShapeCells.Length > 0)
var hardpointHostItems = items.Count(item => item.Hardpoints.Length > 0);
var hardpointCount = items.Sum(item => item.Hardpoints.Length);
var behaviorItems = items.Count(item => item.BehaviorCount > 0 && item.BehaviorKinds.Length > 0);
var behaviorPayloadItems = items.Count(item => item.BehaviorPayloads.Length > 0);
var behaviorPayloadCount = items.Sum(item => item.BehaviorPayloads.Length);
var behaviorFieldCount = items.SelectMany(item => item.BehaviorPayloads).Sum(behavior => behavior.Fields.Length);
var behaviorLegacyRefCount = items
.SelectMany(item => item.BehaviorPayloads)
.SelectMany(behavior => behavior.Fields)
.Count(field => ContainsBehaviorValueKind(field.Value, "legacy-id"));
var hardpointItems = items.Count(item => !string.IsNullOrWhiteSpace(item.HardpointType));
var hullItems = items.Count(item => !string.IsNullOrWhiteSpace(item.HullType));
var weaponItems = items.Count(item =>
@ -148,6 +155,29 @@ if (behaviorItems == 0)
throw new InvalidOperationException("Typed item definitions did not import any behavior fingerprints.");
}
if (behaviorPayloadItems != behaviorItems || behaviorPayloadCount == 0 || behaviorFieldCount == 0)
{
throw new InvalidOperationException(
$"Typed behavior payload import mismatch: payloadItems={behaviorPayloadItems}, behaviorItems={behaviorItems}, payloads={behaviorPayloadCount}, fields={behaviorFieldCount}.");
}
foreach (var item in items.Where(item => item.BehaviorPayloads.Length > 0))
{
if (item.BehaviorPayloads.Length != item.BehaviorCount)
{
throw new InvalidOperationException(
$"Typed item behavior payload count mismatch for {item.Name}: payloads={item.BehaviorPayloads.Length}, count={item.BehaviorCount}.");
}
foreach (var behavior in item.BehaviorPayloads)
{
if (string.IsNullOrWhiteSpace(behavior.Kind) || behavior.UnionKey < 0 || behavior.Fields.Length == 0)
{
throw new InvalidOperationException($"Typed item behavior payload is incomplete for {item.Name}.");
}
}
}
if (hardpointItems == 0)
{
throw new InvalidOperationException("Typed item definitions did not import any hardpoint types.");
@ -256,6 +286,7 @@ Console.WriteLine($"Item definitions: {items.Length}");
Console.WriteLine($"Priced/manufactured/shaped items: {pricedItems}/{manufacturedItems}/{shapedItems}");
Console.WriteLine($"Shape masks: {shapedMaskItems}");
Console.WriteLine($"Interior masks/hardpoint hosts/hardpoints: {interiorShapeItems}/{hardpointHostItems}/{hardpointCount}");
Console.WriteLine($"Behavior payload items/payloads/fields/legacy refs: {behaviorPayloadItems}/{behaviorPayloadCount}/{behaviorFieldCount}/{behaviorLegacyRefCount}");
Console.WriteLine($"Behavior/hardpoint/hull/weapon items: {behaviorItems}/{hardpointItems}/{hullItems}/{weaponItems}");
Console.WriteLine($"Typed catalog trade items: {tradeItems.Length}");
Console.WriteLine($"Eve catalog surface: {surface.Surface.Id} ({surface.Surface.Root.Children.Length} root children)");
@ -278,6 +309,13 @@ static void RequireCount(AetheriaMigrationLedger ledger, string documentType, in
}
}
static bool ContainsBehaviorValueKind(AetheriaBehaviorValue value, string kind)
{
return string.Equals(value.Kind, kind, StringComparison.OrdinalIgnoreCase) ||
value.Children.Any(child => ContainsBehaviorValueKind(child, kind)) ||
value.MapEntries.Any(entry => ContainsBehaviorValueKind(entry.Value, kind));
}
static async Task RequireLegacyLookupAsync<T>(
string legacyId,
Func<Task<T?>> lookup,

View file

@ -105,6 +105,9 @@ public sealed class AetheriaItemDefinition
[Key(30)]
public AetheriaItemHardpoint[] Hardpoints { get; set; } = [];
[Key(31)]
public AetheriaBehaviorPayload[] BehaviorPayloads { get; set; } = [];
}
[MessagePackObject]
@ -150,3 +153,64 @@ public sealed class AetheriaItemHardpoint
[Key(9)]
public double Armor { get; set; }
}
[MessagePackObject]
public sealed class AetheriaBehaviorPayload
{
[Key(0)]
public int UnionKey { get; set; }
[Key(1)]
public string Kind { get; set; } = "";
[Key(2)]
public int Group { get; set; }
[Key(3)]
public AetheriaBehaviorField[] Fields { get; set; } = [];
}
[MessagePackObject]
public sealed class AetheriaBehaviorField
{
[Key(0)]
public int Key { get; set; }
[Key(1)]
public AetheriaBehaviorValue Value { get; set; } = new();
}
[MessagePackObject]
public sealed class AetheriaBehaviorMapEntry
{
[Key(0)]
public string Key { get; set; } = "";
[Key(1)]
public AetheriaBehaviorValue Value { get; set; } = new();
}
[MessagePackObject]
public sealed class AetheriaBehaviorValue
{
[Key(0)]
public string Kind { get; set; } = "nil";
[Key(1)]
public string StringValue { get; set; } = "";
[Key(2)]
public double NumberValue { get; set; }
[Key(3)]
public bool BoolValue { get; set; }
[Key(4)]
public string LegacyIdValue { get; set; } = "";
[Key(5)]
public AetheriaBehaviorValue[] Children { get; set; } = [];
[Key(6)]
public AetheriaBehaviorMapEntry[] MapEntries { get; set; } = [];
}

View file

@ -48,15 +48,14 @@ into `Aetheria.State`. The current checked-in catalog maps to 115 item
definitions, 12 factions, and 12 name files. Typed item definitions include
legacy manufacturer IDs, price, shape dimensions, occupied cell counts, full
shape-cell masks, interior shape masks for hull/cargo equipment, hull hardpoint
definitions, and
stable equipment facets: hardpoint type, hull type, behavior kind fingerprints,
stack size, durability, and weapon range/caliber/type/fire/modifier
classifications.
definitions, behavior kind fingerprints, typed recursive behavior payloads, and
stable equipment facets: hardpoint type, hull type, stack size, durability, and
weapon range/caliber/type/fire/modifier classifications.
Typed corporation documents include the legacy short name, the true description
field from key 3, name-file and boss-hull legacy IDs, influence distance,
allegiance count, and music bank IDs. Runtime object graphs, full behavior
payloads, and simulation state remain legacy until dedicated typed documents
exist.
allegiance count, and music bank IDs. Runtime object graphs, typed behavior
factory construction, and simulation state remain legacy until dedicated typed
runtime documents exist.
Legacy GUID references that are `Guid.Empty` are imported as absent references,
not as resolvable catalog links.
@ -77,10 +76,10 @@ It opens `aetheria-world.cc`, emits immutable catalog read models for trade,
equipment, behavior, hardpoint, manufacturer, corporation, and name-file
queries, exposes typed item shape masks for layout and fitting consumers, and
exposes typed interior masks and hardpoints for equipment/cargo layout
consumers, and can read the published Eve catalog surface. It does not
deserialize legacy `DatabaseEntry` objects and does not write state. Unity can
use this as the first package boundary once CultLib/Eve runtime packaging is
available.
consumers, exposes typed behavior payload read models, and can read the
published Eve catalog surface. It does not deserialize legacy `DatabaseEntry`
objects and does not write state. Unity can use this as the first package
boundary once CultLib/Eve runtime packaging is available.
`Aetheria.State.Verify` opens a materialized state file and checks that the
typed migration ledger matches the actual item, corporation, and name-file
@ -88,9 +87,12 @@ records in the `.cc` store. It also verifies the canonical legacy-ID lookup API
for one migrated record of each catalog kind and checks that expanded item and
corporation catalog facts survived materialization. The verifier also exercises
the typed catalog snapshot query surface, shape-mask invariants, and the
published Eve catalog surface. Legacy content currently includes at least one
overhanging hardpoint; the verifier preserves the payload and checks hardpoint
local masks rather than silently converting content repair into migration.
typed behavior payload tree, and the published Eve catalog surface. The current
catalog materializes 65 behavior payloads, 661 behavior fields, and 18
behavior-side legacy-ID references. Legacy content currently includes at least
one overhanging hardpoint; the verifier preserves the payload and checks
hardpoint local masks rather than silently converting content repair into
migration.
Use it after import when `GameData/aetheria-world.cc` changes.
`Aetheria.State.Unity.Smoke` opens the materialized state through the runtime

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more