2026-06-21 09:37:54 +01:00
|
|
|
extern alias PackageUnity;
|
|
|
|
|
|
2026-06-13 09:15:59 +01:00
|
|
|
using Aetheria.State;
|
|
|
|
|
using Aetheria.State.Unity;
|
2026-06-21 09:37:54 +01:00
|
|
|
using PackageUnity::GameCult.Aetheria.State.Unity;
|
2026-06-13 09:15:59 +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);
|
|
|
|
|
|
|
|
|
|
await using var client = await AetheriaRuntimeCatalogClient.OpenAsync(statePath);
|
|
|
|
|
var catalog = client.ReadCatalog();
|
2026-06-13 12:24:27 +01:00
|
|
|
var packageCatalog = AetheriaRuntimeCatalogStore.OpenReadOnly(statePath);
|
2026-06-13 16:15:03 +01:00
|
|
|
var packageSurfaces = AetheriaRuntimeCatalogStore.ReadEveSurfaces(statePath);
|
2026-06-13 09:15:59 +01:00
|
|
|
var surface = await client.ReadCatalogSurfaceAsync();
|
2026-06-13 15:27:12 +01:00
|
|
|
var commitSmokeDirectory = Path.Combine(Path.GetTempPath(), "aetheria-state-unity-smoke", Guid.NewGuid().ToString("N"));
|
|
|
|
|
var commitSmokeStatePath = Path.Combine(commitSmokeDirectory, "aetheria-world.cc");
|
|
|
|
|
Directory.CreateDirectory(commitSmokeDirectory);
|
2026-06-13 09:15:59 +01:00
|
|
|
|
|
|
|
|
if (catalog.Items.Count != 115)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException($"Expected 115 runtime catalog items, found {catalog.Items.Count}.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 12:24:27 +01:00
|
|
|
if (packageCatalog.Items.Count != catalog.Items.Count ||
|
|
|
|
|
packageCatalog.Corporations.Count != catalog.Corporations.Count ||
|
|
|
|
|
packageCatalog.NameFiles.Count != catalog.NameFiles.Count)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"Package catalog store mismatch: {packageCatalog.Items.Count}/{packageCatalog.Corporations.Count}/{packageCatalog.NameFiles.Count} " +
|
|
|
|
|
$"!= {catalog.Items.Count}/{catalog.Corporations.Count}/{catalog.NameFiles.Count}.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 09:15:59 +01:00
|
|
|
if (catalog.TradeItems.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("Runtime catalog has no trade items.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (catalog.EquipmentItems.Count == 0)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("Runtime catalog has no equipment items.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 09:32:24 +01:00
|
|
|
var shaped = catalog.Items.FirstOrDefault(item => item.ShapeCells.Count > 0)
|
|
|
|
|
?? throw new InvalidOperationException("Runtime catalog has no typed shape masks.");
|
2026-06-16 10:40:39 +01:00
|
|
|
if (string.IsNullOrWhiteSpace(shaped.ItemKey))
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException($"Runtime shape sample has no typed item key: {shaped.Name}.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var packageShaped = packageCatalog.FindItem(shaped.ItemKey);
|
2026-06-13 12:24:27 +01:00
|
|
|
if (packageShaped == null ||
|
|
|
|
|
packageShaped.Name != shaped.Name ||
|
|
|
|
|
packageShaped.ShapeCells.Count != shaped.ShapeCells.Count)
|
|
|
|
|
{
|
2026-06-16 10:40:39 +01:00
|
|
|
throw new InvalidOperationException($"Package catalog store did not read the expected typed item payload for {shaped.ItemKey}.");
|
2026-06-13 12:24:27 +01:00
|
|
|
}
|
|
|
|
|
|
2026-06-13 09:32:24 +01:00
|
|
|
if (shaped.ShapeCells.Count != shaped.OccupiedCells)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"Runtime shape mask count mismatch for {shaped.Name}: cells={shaped.ShapeCells.Count}, occupied={shaped.OccupiedCells}.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (shaped.ShapeCells.Any(cell => cell.X < 0 || cell.Y < 0 || cell.X >= shaped.ShapeWidth || cell.Y >= shaped.ShapeHeight))
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException($"Runtime shape mask has out-of-bounds cells for {shaped.Name}.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 09:47:25 +01:00
|
|
|
var interior = catalog.Items.FirstOrDefault(item => item.InteriorShapeCells.Count > 0)
|
|
|
|
|
?? throw new InvalidOperationException("Runtime catalog has no typed interior shape masks.");
|
|
|
|
|
if (interior.InteriorShapeCells.Count != interior.InteriorOccupiedCells)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"Runtime interior shape mask count mismatch for {interior.Name}: cells={interior.InteriorShapeCells.Count}, occupied={interior.InteriorOccupiedCells}.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var hardpointHost = catalog.Items.FirstOrDefault(item => item.Hardpoints.Count > 0)
|
|
|
|
|
?? throw new InvalidOperationException("Runtime catalog has no typed hardpoints.");
|
|
|
|
|
var hardpoint = hardpointHost.Hardpoints.First();
|
|
|
|
|
if (hardpoint.ShapeCells.Count != hardpoint.OccupiedCells)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"Runtime hardpoint shape count mismatch for {hardpointHost.Name}: cells={hardpoint.ShapeCells.Count}, occupied={hardpoint.OccupiedCells}.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 09:15:59 +01:00
|
|
|
var behaviorKind = catalog.Items.SelectMany(item => item.BehaviorKinds).FirstOrDefault()
|
|
|
|
|
?? throw new InvalidOperationException("Runtime catalog has no behavior kind fingerprints.");
|
|
|
|
|
if (!catalog.FindItemsByBehavior(behaviorKind).Any())
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException($"Runtime catalog behavior lookup failed for {behaviorKind}.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 10:01:20 +01:00
|
|
|
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}.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-15 16:10:13 +01:00
|
|
|
var behaviorItemRefsMissingItemKeys = packageCatalog.Items
|
|
|
|
|
.SelectMany(item => item.BehaviorPayloads)
|
|
|
|
|
.Sum(CountRequiredBehaviorItemRefsMissingItemKeys);
|
|
|
|
|
if (behaviorItemRefsMissingItemKeys > 0)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException(
|
|
|
|
|
$"Runtime behavior payload item refs missing item-key projections: {behaviorItemRefsMissingItemKeys}.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 09:15:59 +01:00
|
|
|
var equipment = catalog.EquipmentItems.First();
|
|
|
|
|
if (!catalog.FindItemsByHardpoint(equipment.HardpointType).Any())
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException($"Runtime catalog hardpoint lookup failed for {equipment.HardpointType}.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 12:59:33 +01:00
|
|
|
var manufactured = catalog.Items.FirstOrDefault(item => !string.IsNullOrWhiteSpace(item.ManufacturerKey))
|
2026-06-13 09:15:59 +01:00
|
|
|
?? throw new InvalidOperationException("Runtime catalog has no manufactured item.");
|
2026-06-15 17:25:40 +01:00
|
|
|
if (string.IsNullOrWhiteSpace(manufactured.ManufacturerKey) ||
|
|
|
|
|
catalog.FindCorporation(manufactured.ManufacturerKey) == null ||
|
|
|
|
|
catalog.GetManufacturer(manufactured) == null)
|
2026-06-13 09:15:59 +01:00
|
|
|
{
|
2026-06-15 17:25:40 +01:00
|
|
|
throw new InvalidOperationException($"Runtime catalog typed manufacturer lookup failed for {manufactured.Name}.");
|
2026-06-13 09:15:59 +01:00
|
|
|
}
|
|
|
|
|
|
2026-06-16 12:59:33 +01:00
|
|
|
var packageCorporation = packageCatalog.Corporations.FirstOrDefault(corporation => !string.IsNullOrWhiteSpace(corporation.GeonameFileKey));
|
2026-06-13 13:18:03 +01:00
|
|
|
if (packageCorporation == null)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("Package catalog store did not read any corporation name-file links.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var packageNameFile = packageCatalog.GetNameFile(packageCorporation);
|
2026-06-15 17:25:40 +01:00
|
|
|
if (string.IsNullOrWhiteSpace(packageCorporation.GeonameFileKey) ||
|
|
|
|
|
packageCatalog.FindNameFile(packageCorporation.GeonameFileKey) == null ||
|
|
|
|
|
packageNameFile == null ||
|
|
|
|
|
string.IsNullOrWhiteSpace(packageNameFile.NameFileKey) ||
|
|
|
|
|
packageNameFile.Names.Count == 0 ||
|
|
|
|
|
packageNameFile.Names.Count != packageNameFile.NameCount)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("Package catalog store did not read typed corporation/name-file links with full names.");
|
2026-06-13 12:24:27 +01:00
|
|
|
}
|
|
|
|
|
|
2026-06-13 13:18:03 +01:00
|
|
|
if (packageCorporation.Allegiances.Count == 0 || packageCorporation.Allegiances.Count != packageCorporation.AllegianceCount)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("Package catalog store did not read corporation allegiance edges.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 09:15:59 +01:00
|
|
|
if (surface?.Schema != "gamecult.eve.surface.v1" ||
|
|
|
|
|
surface.Surface.Id != AetheriaCatalogSurfaceProjector.SurfaceId)
|
|
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("Runtime catalog client did not read the typed Eve surface.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 16:15:03 +01:00
|
|
|
var packageSurface = packageSurfaces.FirstOrDefault(candidate => candidate.Surface.Id == AetheriaCatalogSurfaceProjector.SurfaceId);
|
|
|
|
|
if (packageSurface == null ||
|
|
|
|
|
packageSurface.Schema != "gamecult.eve.surface.v1" ||
|
|
|
|
|
packageSurface.ProviderId != "aetheria" ||
|
|
|
|
|
packageSurface.Surface.Root.Kind != "surface" ||
|
|
|
|
|
packageSurface.Surface.Root.Children.Count == 0 ||
|
2026-06-21 09:37:54 +01:00
|
|
|
packageSurface.Commands.All(command => command.Command != AetheriaRuntimeCatalogCommands.Refresh))
|
2026-06-13 16:15:03 +01:00
|
|
|
{
|
|
|
|
|
throw new InvalidOperationException("Package runtime store did not read the typed Eve surface contract from CultCache.");
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-13 15:27:12 +01:00
|
|
|
try
|
|
|
|
|
{
|
2026-06-21 09:37:54 +01:00
|
|
|
var eveCommand = AetheriaRuntimeEveCommands.SubmitCatalogCommand(
|
2026-06-13 15:27:12 +01:00
|
|
|
commitSmokeStatePath,
|
2026-06-21 09:37:54 +01:00
|
|
|
AetheriaRuntimeCatalogCommands.Refresh,
|
|
|
|
|
"aetheria-state-unity-smoke");
|
|
|
|
|
await using var eveCommandNode = await AetheriaStateNode.OpenAsync(
|
2026-06-14 13:46:08 +01:00
|
|
|
commitSmokeStatePath,
|
2026-06-21 09:37:54 +01:00
|
|
|
"aetheria-unity-runtime-eve-command-smoke");
|
|
|
|
|
await eveCommandNode.SubmitEveCommandAsync(new GameCult.Aetheria.State.Unity.AetheriaRuntimeEveCommandDocument
|
|
|
|
|
{
|
|
|
|
|
Schema = eveCommand.Schema,
|
|
|
|
|
CommandId = eveCommand.CommandId,
|
|
|
|
|
ProviderId = eveCommand.ProviderId,
|
|
|
|
|
SurfaceId = eveCommand.SurfaceId,
|
|
|
|
|
Command = eveCommand.Command,
|
|
|
|
|
IssuedAtUtc = eveCommand.IssuedAtUtc,
|
|
|
|
|
ClientId = eveCommand.ClientId,
|
|
|
|
|
PlayerSettings = new GameCult.Aetheria.State.Unity.AetheriaRuntimePlayerSettingsCommandBody
|
2026-06-14 13:46:08 +01:00
|
|
|
{
|
2026-06-21 09:37:54 +01:00
|
|
|
PlayerName = eveCommand.PlayerSettings.PlayerName
|
|
|
|
|
},
|
|
|
|
|
InputSettings = new GameCult.Aetheria.State.Unity.AetheriaRuntimeInputSettingsCommandBody
|
2026-06-13 15:37:05 +01:00
|
|
|
{
|
2026-06-21 09:37:54 +01:00
|
|
|
ActionName = eveCommand.InputSettings.ActionName,
|
|
|
|
|
BindingIndex = eveCommand.InputSettings.BindingIndex,
|
|
|
|
|
InputSystemPath = eveCommand.InputSettings.InputSystemPath,
|
|
|
|
|
Enabled = eveCommand.InputSettings.Enabled
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
var eveSubmitted = eveCommandNode.ReadObservedEveCommands();
|
|
|
|
|
if (eveSubmitted.Count != 1 ||
|
|
|
|
|
eveSubmitted[0].Schema != AetheriaRuntimeEveCommandClient.CommandSchema ||
|
|
|
|
|
eveSubmitted[0].CommandId != eveCommand.CommandId ||
|
|
|
|
|
eveSubmitted[0].ProviderId != "aetheria" ||
|
|
|
|
|
eveSubmitted[0].SurfaceId != AetheriaRuntimeCatalogCommands.SurfaceId ||
|
|
|
|
|
eveSubmitted[0].Command != AetheriaRuntimeCatalogCommands.Refresh ||
|
|
|
|
|
eveSubmitted[0].PlayerSettings == null ||
|
|
|
|
|
eveSubmitted[0].InputSettings == null)
|
2026-06-13 16:44:24 +01:00
|
|
|
{
|
2026-06-21 09:37:54 +01:00
|
|
|
throw new InvalidOperationException("Runtime Eve command state record did not preserve typed command envelopes separately from state commits.");
|
2026-06-13 16:44:24 +01:00
|
|
|
}
|
2026-06-13 15:27:12 +01:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
2026-06-13 22:26:43 +01:00
|
|
|
if (Environment.GetEnvironmentVariable("AETHERIA_KEEP_SMOKE_STATE") == "1")
|
|
|
|
|
Console.Error.WriteLine($"Kept smoke state: {commitSmokeDirectory}");
|
|
|
|
|
else
|
|
|
|
|
Directory.Delete(commitSmokeDirectory, true);
|
2026-06-13 15:27:12 +01:00
|
|
|
}
|
|
|
|
|
|
2026-06-13 09:15:59 +01:00
|
|
|
Console.WriteLine($"Aetheria Unity runtime catalog smoke passed: {statePath}");
|
|
|
|
|
Console.WriteLine($"Items/trade/equipment: {catalog.Items.Count}/{catalog.TradeItems.Count}/{catalog.EquipmentItems.Count}");
|
2026-06-13 09:32:24 +01:00
|
|
|
Console.WriteLine($"Shape mask sample: {shaped.Name} {shaped.ShapeWidth}x{shaped.ShapeHeight}/{shaped.ShapeCells.Count}");
|
2026-06-13 09:47:25 +01:00
|
|
|
Console.WriteLine($"Interior/hardpoint sample: {interior.Name} {interior.InteriorShapeCells.Count}; {hardpointHost.Name} {hardpoint.Type} {hardpoint.ShapeCells.Count}");
|
2026-06-13 10:01:20 +01:00
|
|
|
Console.WriteLine($"Behavior payload sample: {behaviorHost.Name} {behaviorPayload.Kind}/{behaviorPayload.Fields.Count}");
|
2026-06-13 09:15:59 +01:00
|
|
|
Console.WriteLine($"Behavior sample: {behaviorKind}");
|
|
|
|
|
Console.WriteLine($"Eve surface: {surface.Surface.Id}");
|
2026-06-13 16:15:03 +01:00
|
|
|
Console.WriteLine($"Package Eve surfaces: {packageSurfaces.Count}");
|
2026-06-21 09:37:54 +01:00
|
|
|
Console.WriteLine("Runtime Eve command smoke: surface command document is a typed state record");
|
2026-06-16 17:44:48 +01:00
|
|
|
|
2026-06-15 16:10:13 +01:00
|
|
|
static int CountRequiredBehaviorItemRefsMissingItemKeys(AetheriaRuntimeBehaviorPayload payload)
|
|
|
|
|
{
|
|
|
|
|
return payload.Fields.Count(field =>
|
|
|
|
|
IsBehaviorItemRefField(payload.Kind, field.Key) &&
|
|
|
|
|
IsNonEmptyLegacyRefMissingItemKey(field.Value));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool IsBehaviorItemRefField(string behaviorKind, int fieldKey)
|
|
|
|
|
{
|
|
|
|
|
return (string.Equals(behaviorKind, "ItemUsage", StringComparison.OrdinalIgnoreCase) && fieldKey == 1) ||
|
|
|
|
|
(IsWeaponBehavior(behaviorKind) && fieldKey == 12);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool IsWeaponBehavior(string behaviorKind)
|
|
|
|
|
{
|
|
|
|
|
return behaviorKind is "GuidedWeapon" or "InstantWeapon" or "ConstantWeapon" or "ChargedWeapon" or "AutoWeapon" or "LockWeapon";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool IsNonEmptyLegacyRefMissingItemKey(AetheriaRuntimeBehaviorValue value)
|
|
|
|
|
{
|
|
|
|
|
return string.Equals(value.Kind, "legacy-id", StringComparison.OrdinalIgnoreCase) &&
|
|
|
|
|
!IsEmptyLegacyId(value.LegacyIdValue) &&
|
|
|
|
|
string.IsNullOrWhiteSpace(value.ItemKeyValue);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static bool IsEmptyLegacyId(string legacyId)
|
|
|
|
|
{
|
|
|
|
|
return string.IsNullOrWhiteSpace(legacyId) ||
|
|
|
|
|
(Guid.TryParse(legacyId, out var parsed) && parsed == Guid.Empty);
|
|
|
|
|
}
|