8704 lines
483 KiB
C#
8704 lines
483 KiB
C#
using Aetheria.State;
|
|
using Aetheria.State.Daemon;
|
|
using Aetheria.State.Documents;
|
|
using GameCult.Aetheria.State.Verse;
|
|
using GameCult.Caching;
|
|
using GameCult.Eve.PluginFields;
|
|
using GameCult.Eve.Surface;
|
|
using GameCult.Mesh;
|
|
using System.Globalization;
|
|
|
|
var checks = new AetheriaDaemonYmirSmokeChecks();
|
|
if (args.Contains("--gravity", StringComparer.Ordinal))
|
|
{
|
|
checks.RunGravity();
|
|
Console.WriteLine("Daemon Ymir gravity-sign smoke passed.");
|
|
}
|
|
else if (args.Contains("--loadout", StringComparer.Ordinal))
|
|
{
|
|
checks.RunLoadout();
|
|
Console.WriteLine("Daemon loadout hardpoint smoke passed.");
|
|
}
|
|
else if (args.Contains("--pickup", StringComparer.Ordinal))
|
|
{
|
|
checks.RunPickup();
|
|
Console.WriteLine("Daemon Ymir pickup-contact smoke passed.");
|
|
}
|
|
else if (args.Contains("--payload", StringComparer.Ordinal))
|
|
{
|
|
checks.RunPayload();
|
|
Console.WriteLine("Daemon retained Ymir payload smoke passed.");
|
|
}
|
|
else if (args.Contains("--tutorial-topology", StringComparer.Ordinal))
|
|
{
|
|
checks.RunTutorialTopology();
|
|
Console.WriteLine("Daemon tutorial topology smoke passed.");
|
|
}
|
|
else if (args.Contains("--zone-bodies", StringComparer.Ordinal))
|
|
{
|
|
checks.RunZoneBodies();
|
|
Console.WriteLine("Daemon generated zone-body smoke passed.");
|
|
}
|
|
else if (args.Contains("--tutorial-world", StringComparer.Ordinal))
|
|
{
|
|
checks.RunTutorialWorld();
|
|
Console.WriteLine("Daemon tutorial world-plan smoke passed.");
|
|
}
|
|
else if (args.Contains("--tutorial-materialization", StringComparer.Ordinal))
|
|
{
|
|
checks.RunTutorialMaterialization();
|
|
Console.WriteLine("Daemon tutorial entity-materialization smoke passed.");
|
|
}
|
|
else if (args.Contains("--tutorial-run", StringComparer.Ordinal))
|
|
{
|
|
checks.RunTutorialRun();
|
|
Console.WriteLine("Daemon persisted tutorial-run smoke passed.");
|
|
}
|
|
else if (args.Contains("--orbits", StringComparer.Ordinal))
|
|
{
|
|
checks.RunOrbits();
|
|
Console.WriteLine("Daemon dynamic-orbit smoke passed.");
|
|
}
|
|
else if (args.Contains("--patrol", StringComparer.Ordinal))
|
|
{
|
|
checks.RunPatrol();
|
|
Console.WriteLine("Daemon generated-agent patrol smoke passed.");
|
|
}
|
|
else
|
|
{
|
|
checks.Run();
|
|
Console.WriteLine("Daemon Ymir physical payload smoke passed.");
|
|
}
|
|
|
|
internal sealed class AetheriaDaemonYmirSmokeChecks
|
|
{
|
|
private static readonly List<AetheriaYmirWorldPhysics> OwnedPhysics = [];
|
|
|
|
public void RunGravity() => RunCheck(PositiveGravityDepthAttractsAndProjectsAsAWell);
|
|
|
|
public void RunLoadout() => DaemonLoadoutsRespectFactionAvailabilityAndHullRoles();
|
|
|
|
public void RunPickup()
|
|
{
|
|
RunCheck(RetainedWorldAdvancesEveryFixedSubstep);
|
|
RunCheck(StableEntityIdentitySurvivesCrossZoneReindex);
|
|
RunCheck(TractorRampsAndPullsThroughYmirWithoutTeleportingCargo);
|
|
RunCheck(PickupIsCapacityCheckedExactlyOnceAndExpires);
|
|
RunCheck(StationBodiesCannotConsumePickups);
|
|
RunCheck(PickupShieldContactCollectsOrBounces);
|
|
RunCheck(YmirRestartDoesNotReplayConsumedPickupContact);
|
|
}
|
|
|
|
public void RunPayload()
|
|
{
|
|
RunCheck(YmirMovesProjectileAndReportsStableContact);
|
|
RunCheck(PayloadQueryExcludesItsSource);
|
|
RunCheck(PayloadBodiesDoNotCollideWithEachOther);
|
|
RunCheck(OverlappingMineRemainsQueryableAfterArming);
|
|
}
|
|
|
|
public void RunTutorialTopology() => RunCheck(TutorialTopologyIsDeterministicConnectedAndRoleOwned);
|
|
public void RunZoneBodies() => RunCheck(GeneratedZoneBodiesPreserveFossilHierarchy);
|
|
public void RunTutorialWorld() => RunCheck(TutorialWorldMaterializesEveryTopologyZone);
|
|
public void RunTutorialMaterialization() => RunCheck(TutorialPopulationPreservesFossilEntityMechanics);
|
|
public void RunTutorialRun() => RunCheck(TutorialRunPersistsCanonicalWorldTruth);
|
|
public void RunOrbits() => RunCheck(GeneratedOrbitsAdvanceOrbitalWorldTruth);
|
|
public void RunPatrol()
|
|
{
|
|
RunCheck(SecurityZonesOwnFossilTrespasserHostility);
|
|
RunCheck(AgentPatrolsHistoricalOrbitCircuitThroughMovementCommands);
|
|
RunCheck(PatrolCombatPreemptsAndResumesTheSameCircuit);
|
|
}
|
|
|
|
private static void TutorialRunPersistsCanonicalWorldTruth()
|
|
{
|
|
var catalog = TutorialPopulationCatalog(out _);
|
|
var root = Path.Combine(Path.GetTempPath(), $"aetheria-tutorial-run-{Guid.NewGuid():N}");
|
|
var statePath = Path.Combine(root, "aetheria.cc");
|
|
Directory.CreateDirectory(root);
|
|
try
|
|
{
|
|
using var node = AetheriaStateNode.OpenAsync(statePath).GetAwaiter().GetResult();
|
|
var written = AetheriaDaemonTutorialRunWriter.WriteAsync(
|
|
node, catalog, "2026-07-17T00:00:00.0000000Z").GetAwaiter().GetResult();
|
|
var run = node.MutableDocument<AetheriaRunState>(new CultRecordKey(written.RunKey))
|
|
.ReadAsync().GetAwaiter().GetResult();
|
|
var settings = node.MutableDocument<AetheriaPlayerSettings>(AetheriaStateNode.PlayerSettingsKey)
|
|
.ReadAsync().GetAwaiter().GetResult();
|
|
Require(run != null && run.IsTutorial && run.ZoneKeys.Length == 64 &&
|
|
run.EntranceZoneIndex == run.CurrentZoneIndex && run.ExitZoneIndex == -1,
|
|
"persisted New Game truth must be the complete authored tutorial graph, not the Terminus witness fixture");
|
|
Require(run!.AgentTasks.Length > 0 && run.AgentTasks.All(task =>
|
|
task.TaskType == "patrol" && task.Status == "assigned" && task.TargetOrbitKeys.Length == 4),
|
|
"persisted tutorial agents must retain their assigned four-orbit fossil patrols");
|
|
Require(settings != null && settings.ActiveRunKey == written.RunKey && !settings.TutorialPassed,
|
|
"tutorial persistence must select the new run without falsely awarding completion");
|
|
Require(run!.CurrentEntityKey == written.CurrentEntityKey,
|
|
"the run and session handoff must share one canonical controlled-entity identity");
|
|
var entrance = node.MutableDocument<AetheriaZoneState>(
|
|
new CultRecordKey(run.ZoneKeys[run.EntranceZoneIndex]))
|
|
.ReadAsync().GetAwaiter().GetResult();
|
|
Require(entrance != null && entrance.AdjacentZoneIndices.Length > 0 &&
|
|
entrance.EntityKeys.Contains(run.CurrentEntityKey, StringComparer.Ordinal) &&
|
|
entrance.Bodies.Length > 0 && entrance.Orbits.Length >= entrance.Bodies.Length,
|
|
"the persisted entrance must contain the starter ship, celestial hierarchy, and derived wormhole adjacency");
|
|
var player = node.MutableDocument<AetheriaEntitySnapshot>(new CultRecordKey(run.CurrentEntityKey))
|
|
.ReadAsync().GetAwaiter().GetResult();
|
|
Require(player != null && player.Kind == "ship" && player.LoadoutGeneration != null,
|
|
"the persisted current entity must be a playable daemon-generated ship with loadout provenance");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
private static void TutorialPopulationPreservesFossilEntityMechanics()
|
|
{
|
|
var catalog = TutorialPopulationCatalog(out var factions);
|
|
var settings = AetheriaDaemonTutorialTopologySettings.Fossil;
|
|
var firstWorld = AetheriaDaemonTutorialWorldGenerator.Generate(factions, 0xA37E_2026u, settings);
|
|
var secondWorld = AetheriaDaemonTutorialWorldGenerator.Generate(factions, 0xA37E_2026u, settings);
|
|
var first = AetheriaDaemonTutorialWorldMaterializer.Materialize(firstWorld, factions, catalog);
|
|
var second = AetheriaDaemonTutorialWorldMaterializer.Materialize(secondWorld, factions, catalog);
|
|
|
|
RequireEqual(first.Topology.EntranceZoneIndex, first.PlayerZoneIndex,
|
|
"the starter ship must enter the authored tutorial entrance zone");
|
|
var entrance = first.Zones[first.PlayerZoneIndex];
|
|
Require(entrance.PlayerEntityIndex >= 0 &&
|
|
entrance.Entities[entrance.PlayerEntityIndex].Kind == "ship" &&
|
|
entrance.Entities[entrance.PlayerEntityIndex].FactionKey == factions[0].CorporationKey,
|
|
"the tutorial must materialize one protagonist starter ship as daemon world truth");
|
|
Require(first.Zones.Values.Any(zone => zone.Entities.Any(entity => entity.Kind == "station")) &&
|
|
first.Zones.Values.Any(zone => zone.Entities.Any(entity => entity.Kind == "turret")) &&
|
|
first.Zones.Values.Any(zone => zone.Entities.Any(entity =>
|
|
entity.Kind == "ship" && entity.AgentTaskCapabilities.Contains("attack", StringComparer.Ordinal))),
|
|
"fossil station, defensive turret, and hostile-agent population must all survive materialization");
|
|
Require(first.Zones.Values.SelectMany(zone => zone.Entities)
|
|
.Where(entity => entity.Kind == "ship" && entity.AgentTaskCapabilities.Contains("attack", StringComparer.Ordinal))
|
|
.All(entity => entity.Position.X != 0 || entity.Position.Z != 0),
|
|
"generated non-player ships must retain the fossil activation rule that distributes zero-pose ships through the zone");
|
|
Require(first.Zones.Values.All(zone =>
|
|
{
|
|
var orbitKeys = zone.Orbits.Select(orbit => orbit.OrbitKey).ToHashSet(StringComparer.Ordinal);
|
|
var agents = zone.Entities.Where(entity =>
|
|
entity.Kind == "ship" && entity.AgentTaskCapabilities.Contains("attack", StringComparer.Ordinal)).ToArray();
|
|
return zone.AgentTasks.Length == agents.Length &&
|
|
zone.AgentTasks.All(task => task.TaskType == "patrol" &&
|
|
task.Status == "assigned" && task.TargetOrbitKeys.Length == 4 &&
|
|
task.TargetOrbitKeys.Distinct(StringComparer.Ordinal).Count() == 4 &&
|
|
task.TargetOrbitKeys.All(orbitKeys.Contains)) &&
|
|
agents.All(entity => zone.AgentTasks.Any(task => task.TaskId == entity.AssignedAgentTaskId));
|
|
}), "every generated NPC must begin with one assigned four-orbit patrol owned by daemon task state");
|
|
Require(first.Zones.Values.SelectMany(zone => zone.Entities)
|
|
.Where(entity => !string.IsNullOrWhiteSpace(entity.OrbitKey))
|
|
.All(entity => entity.Position.X != 0 || entity.Position.Z != 0),
|
|
"orbital stations and turrets must enter world truth at their resolved orbit position, not at the player origin");
|
|
var player = first.Zones[first.PlayerZoneIndex].Entities[first.PlayerEntityIndex];
|
|
Require(player.Position.X == 0 && player.Position.Z == 0,
|
|
"the controlled starter ship alone must retain the tutorial entrance origin");
|
|
Require(first.Zones.Values.All(zone =>
|
|
{
|
|
var orbitKeys = zone.Orbits.Select(orbit => orbit.OrbitKey).ToHashSet(StringComparer.Ordinal);
|
|
return zone.Entities.Where(entity => entity.Kind is "station" or "turret")
|
|
.All(entity => !string.IsNullOrWhiteSpace(entity.OrbitKey) && orbitKeys.Contains(entity.OrbitKey));
|
|
}), "every orbital entity must reference an orbit owned by its canonical zone document");
|
|
Require(first.Zones.Values.SelectMany(zone => zone.Entities)
|
|
.Where(entity => entity.Kind is "station" or "turret" or "ship")
|
|
.All(entity => entity.LoadoutGeneration != null &&
|
|
entity.LoadoutGeneration.SourceZoneIndex >= 0 &&
|
|
entity.LoadoutGeneration.Selections.Any(selection => selection.Role == "hull")),
|
|
"every generated entity must retain its daemon loadout provenance receipt");
|
|
Require(first.Zones.OrderBy(pair => pair.Key)
|
|
.Select(pair => string.Join('|', pair.Value.Entities.Select(entity =>
|
|
$"{entity.Kind}:{entity.FactionKey}:{entity.HullItemKey}:{entity.OrbitKey}")))
|
|
.SequenceEqual(second.Zones.OrderBy(pair => pair.Key)
|
|
.Select(pair => string.Join('|', pair.Value.Entities.Select(entity =>
|
|
$"{entity.Kind}:{entity.FactionKey}:{entity.HullItemKey}:{entity.OrbitKey}")))),
|
|
"fixed topology, catalog, and post-body random state must reproduce entity population exactly");
|
|
}
|
|
|
|
private static AetheriaRuntimeCatalogSnapshot TutorialPopulationCatalog(
|
|
out AetheriaDaemonTutorialFactionInput[] factions)
|
|
{
|
|
var one = new[] { new AetheriaRuntimeShapeCell(0, 0) };
|
|
var hullCells = Enumerable.Range(0, 6).SelectMany(y => Enumerable.Range(0, 6)
|
|
.Select(x => new AetheriaRuntimeShapeCell(x, y))).ToArray();
|
|
// The recovered tutorial catalog includes allied manufacturers which do
|
|
// not own one of the six prelude home zones. Prelude availability must
|
|
// retain those authored items without inventing another faction home.
|
|
var manufacturer = "manufacturer.common";
|
|
|
|
AetheriaRuntimeCatalogItem Item(string key, string category, string hardpointType = "", params string[] behaviors)
|
|
{
|
|
var item = CatalogItem(key, behaviors.Select((kind, index) =>
|
|
new AetheriaRuntimeBehaviorPayload(index, kind, 0, Array.Empty<AetheriaRuntimeBehaviorField>())).ToArray());
|
|
item.Category = category;
|
|
item.ManufacturerKey = manufacturer;
|
|
item.Price = 10;
|
|
item.HardpointType = hardpointType;
|
|
item.ShapeWidth = 1;
|
|
item.ShapeHeight = 1;
|
|
item.OccupiedCells = 1;
|
|
item.ShapeCells = one;
|
|
item.BehaviorKinds = behaviors;
|
|
return item;
|
|
}
|
|
|
|
AetheriaRuntimeCatalogItem Hull(string key, string hullType, string controller)
|
|
{
|
|
var hull = Item(key, AetheriaRuntimeItemCategories.Hull, "Hull");
|
|
hull.HullType = hullType;
|
|
hull.ShapeWidth = 6;
|
|
hull.ShapeHeight = 6;
|
|
hull.OccupiedCells = hullCells.Length;
|
|
hull.ShapeCells = hullCells;
|
|
hull.Hardpoints =
|
|
[
|
|
new AetheriaRuntimeHardpoint("ControlModule", 0, 0, 1, 1, 1, one, "", "None", 0),
|
|
new AetheriaRuntimeHardpoint("Weapon", 1, 0, 1, 1, 1, one, "", "None", 0)
|
|
];
|
|
return hull;
|
|
}
|
|
|
|
var shipHull = Hull("tutorial-ship-hull", "Ship", "Cockpit");
|
|
var stationHull = Hull("tutorial-station-hull", "Station", "TurretController");
|
|
var cockpit = Item("tutorial-cockpit", AetheriaRuntimeItemCategories.Gear, "ControlModule", "Cockpit");
|
|
var turret = Item("tutorial-turret-controller", AetheriaRuntimeItemCategories.Gear, "ControlModule", "TurretController");
|
|
var weapon = Item("tutorial-weapon", AetheriaRuntimeItemCategories.Weapon, "Weapon", "LockWeapon");
|
|
var cargo = Item("tutorial-cargo", AetheriaRuntimeItemCategories.CargoBay, "Internal");
|
|
cargo.InteriorShapeWidth = 4;
|
|
cargo.InteriorShapeHeight = 4;
|
|
cargo.InteriorOccupiedCells = 16;
|
|
cargo.InteriorShapeCells = Enumerable.Range(0, 4).SelectMany(y => Enumerable.Range(0, 4)
|
|
.Select(x => new AetheriaRuntimeShapeCell(x, y))).ToArray();
|
|
var docking = Item("tutorial-docking", AetheriaRuntimeItemCategories.DockingBay, "Internal");
|
|
var reactor = Item("tutorial-reactor", AetheriaRuntimeItemCategories.Gear, "Internal", "Reactor");
|
|
var capacitor = Item("tutorial-capacitor", AetheriaRuntimeItemCategories.Gear, "Internal", "Capacitor");
|
|
var names = new[] { "Miss", "Zhe", "Luc", "Aero", "Finch", "Adras" };
|
|
var tutorialCorporations = names.Select(name => new AetheriaRuntimeCorporation(
|
|
$"faction.{name.ToLowerInvariant()}", name, name, "", $"names.{name}", "", 4, 1,
|
|
[new AetheriaRuntimeCorporationAllegiance(manufacturer, 1)])).ToArray();
|
|
var corporations = tutorialCorporations.Append(new AetheriaRuntimeCorporation(
|
|
manufacturer, "Common Manufacturer", "Common", "", "", "", 0, 0,
|
|
Array.Empty<AetheriaRuntimeCorporationAllegiance>())).ToArray();
|
|
factions = names.Select((name, index) => new AetheriaDaemonTutorialFactionInput(
|
|
tutorialCorporations[index].CorporationKey, name, name, 4)
|
|
{
|
|
TrainingNames = Enumerable.Range(0, 128).Select(value => $"{name}{value:D3}").ToArray()
|
|
}).ToArray();
|
|
var nameFiles = names.Select(name => new AetheriaRuntimeNameFile(
|
|
$"names.{name}", name, 128,
|
|
Enumerable.Range(0, 8).Select(value => $"{name}{value:D3}").ToArray(),
|
|
Enumerable.Range(0, 128).Select(value => $"{name}{value:D3}").ToArray())).ToArray();
|
|
return new AetheriaRuntimeCatalogSnapshot(
|
|
[shipHull, stationHull, cockpit, turret, weapon, cargo, docking, reactor, capacitor],
|
|
corporations,
|
|
nameFiles);
|
|
}
|
|
|
|
private static void TutorialWorldMaterializesEveryTopologyZone()
|
|
{
|
|
AetheriaDaemonTutorialFactionInput Faction(string shortName) =>
|
|
new($"faction.{shortName.ToLowerInvariant()}", shortName, shortName, 2)
|
|
{
|
|
TrainingNames = Enumerable.Range(0, 512).Select(index =>
|
|
$"{(char)('a' + index % 26)}{(char)('a' + index / 26 % 26)}{(char)('a' + index / 676 % 26)}{shortName.ToLowerInvariant()}")
|
|
.ToArray()
|
|
};
|
|
var factions = new[]
|
|
{
|
|
Faction("Miss"), Faction("Zhe"), Faction("Luc"),
|
|
Faction("Aero"), Faction("Finch"), Faction("Adras")
|
|
};
|
|
var world = AetheriaDaemonTutorialWorldGenerator.Generate(factions, 0xA37E_2026u);
|
|
|
|
RequireEqual(world.Topology.Zones.Count, world.Zones.Count,
|
|
"the daemon tutorial world plan must materialize every topology zone exactly once");
|
|
Require(world.Topology.Zones.All(zone => world.Zones.ContainsKey(zone.ZoneIndex)),
|
|
"tutorial topology identities must be the sole keys of generated zone plans");
|
|
Require(world.Zones.Values.All(zone => zone.Orbits.Length > 0 && zone.Bodies.Length > 0),
|
|
"no selectable tutorial zone may be published as an empty celestial shell");
|
|
Require(world.Zones.Values.All(zone =>
|
|
Math.Abs(zone.Mass - zone.HierarchyMass) <= Math.Max(0.01, zone.Mass * 0.00001)),
|
|
"every tutorial zone must conserve its authored mass independently");
|
|
Require(world.Zones.Values.SelectMany(zone => zone.Bodies).Any(body => body.Kind == "asteroid_belt"),
|
|
"the complete tutorial world must retain fossil asteroid-belt generation");
|
|
Require(world.Zones.Values.SelectMany(zone => zone.Bodies).Count(body => body.Kind == "sun") >= world.Zones.Count,
|
|
"every generated tutorial zone must contain at least one stellar primary");
|
|
}
|
|
|
|
private static void GeneratedZoneBodiesPreserveFossilHierarchy()
|
|
{
|
|
var zone = new AetheriaDaemonTutorialZoneTopology(
|
|
7,
|
|
"Morrowind",
|
|
0.42f,
|
|
0.63f,
|
|
new[] { 3, 8 },
|
|
new[] { "faction.miss" },
|
|
"faction.miss");
|
|
var first = AetheriaDaemonZoneBodyGenerator.Generate(0xA37E_2026u, zone, 1.25f);
|
|
var second = AetheriaDaemonZoneBodyGenerator.Generate(0xA37E_2026u, zone, 1.25f);
|
|
|
|
RequireEqual(first.GenerationSeed, second.GenerationSeed,
|
|
"daemon zone generation must derive one stable seed from galaxy and zone identity");
|
|
RequireNear(first.Radius, second.Radius, 0,
|
|
"fixed zone inputs must reproduce the authored radius curve exactly");
|
|
RequireNear(first.Mass, first.HierarchyMass, Math.Max(0.01, first.Mass * 0.00001),
|
|
"planet hierarchy normalization must conserve the generated zone mass");
|
|
Require(first.Radius >= 1000 && first.Radius <= 10000 && first.Mass >= 10000 && first.Mass <= 500000,
|
|
"zone radius and mass must remain inside the authored exponential-lerp bounds");
|
|
Require(first.Orbits.Length >= first.Bodies.Length && first.Orbits.Length > 1,
|
|
"rosette placeholders may own orbits without bodies, but every generated zone must retain its hierarchy");
|
|
var orbitKeys = first.Orbits.Select(orbit => orbit.OrbitKey).ToHashSet(StringComparer.Ordinal);
|
|
Require(first.Orbits.All(orbit => orbit.ParentOrbitKey.Length == 0 || orbitKeys.Contains(orbit.ParentOrbitKey)) &&
|
|
first.Bodies.All(body => orbitKeys.Contains(body.OrbitKey)),
|
|
"every generated parent and body orbit reference must resolve inside the daemon plan");
|
|
Require(first.Bodies.All(body => body.GravityInfluenceRadius > 0 && body.GravityWellDepth > 0 &&
|
|
body.GravityWaveRadius > 0 && body.GravityWaveDepth > 0),
|
|
"generated body mass must project the authored positive gravity and wave curves for Ymir and Eve");
|
|
Require(first.Bodies.Select(body => (body.BodyKey, body.Kind, body.Mass, body.OrbitKey, body.Asteroids.Length))
|
|
.SequenceEqual(second.Bodies.Select(body => (body.BodyKey, body.Kind, body.Mass, body.OrbitKey, body.Asteroids.Length))),
|
|
"body kinds, masses, identities, orbits, and asteroid cardinality must be deterministic");
|
|
Require(first.Bodies.Any(body => body.Kind == "sun") && first.Bodies.Any(body => body.Kind != "sun"),
|
|
"the fossil solar hierarchy must produce both a stellar primary and subordinate bodies");
|
|
}
|
|
|
|
private static void TutorialTopologyIsDeterministicConnectedAndRoleOwned()
|
|
{
|
|
AetheriaDaemonTutorialFactionInput Faction(
|
|
string selector,
|
|
string name,
|
|
string shortName,
|
|
int influence = 2) =>
|
|
new($"faction.{selector.ToLowerInvariant()}", name, shortName, influence)
|
|
{
|
|
TrainingNames = Enumerable.Range(0, 512).Select(index =>
|
|
$"{(char)('a' + index % 26)}{(char)('a' + index / 26 % 26)}{(char)('a' + index / 676 % 26)}{selector.ToLowerInvariant()}")
|
|
.ToArray()
|
|
};
|
|
var factions = new[]
|
|
{
|
|
Faction("Miss", "Miss Terri's Sugariffic Snack Company", "Miss Terri's"),
|
|
Faction("Zhe", "Zhestokost", "Zhestokost"),
|
|
Faction("Luc", "Lucent Media", "Lucent"),
|
|
Faction("Aero", "Aeronautics Unlimited", "AU"),
|
|
Faction("Finch", "Finch Cybernetics", "Finch"),
|
|
Faction("Adras", "Adrasteia", "Adrasteia")
|
|
};
|
|
var first = AetheriaDaemonTutorialTopologyGenerator.GenerateFossil(factions, 0xA37E_2026u);
|
|
var second = AetheriaDaemonTutorialTopologyGenerator.GenerateFossil(factions, 0xA37E_2026u);
|
|
RequireEqual(64, first.Zones.Count,
|
|
"the daemon tutorial must retain the authored 64-zone topology size");
|
|
Require(first.Zones.All(zone => !string.IsNullOrWhiteSpace(zone.Name)) &&
|
|
first.Zones.Select(zone => zone.Name).Distinct(StringComparer.Ordinal).Count() == first.Zones.Count,
|
|
"daemon tutorial naming must produce one stable unique catalog name per zone");
|
|
Require(first.Zones.Select(zone => (zone.X, zone.Y, string.Join(',', zone.AdjacentZoneIndices)))
|
|
.SequenceEqual(second.Zones.Select(zone => (zone.X, zone.Y, string.Join(',', zone.AdjacentZoneIndices)))),
|
|
"tutorial topology must be deterministic for a fixed daemon seed and density field");
|
|
RequireNear(first.NoisePosition, second.NoisePosition, 0,
|
|
"the daemon seed must reproduce the authored tutorial cloud field position");
|
|
Require(AetheriaDaemonTutorialTopologyGenerator.TutorialCloudDensity(0.5f, 0.5f, first.NoisePosition) >= 0.5f,
|
|
"tutorial cloud selection must retain the fossil's dense-center admission gate");
|
|
RequireEqual(6, first.HomeZoneByFactionKey.Count,
|
|
"every authored tutorial faction role must receive one daemon-owned home zone");
|
|
Require(first.HomeZoneByFactionKey["faction.miss"] != first.HomeZoneByFactionKey["faction.zhe"],
|
|
"tutorial protagonist and antagonist homes must not collapse onto one zone");
|
|
Require(first.Zones[first.EntranceZoneIndex].OwnerFactionKey.Length == 0,
|
|
"the tutorial entrance must retain the fossil's unowned-zone rule");
|
|
Require(first.DiscoveredZoneIndices.SequenceEqual(
|
|
new[] { first.EntranceZoneIndex }
|
|
.Concat(first.Zones[first.EntranceZoneIndex].AdjacentZoneIndices)
|
|
.Distinct()
|
|
.OrderBy(value => value)),
|
|
"initial tutorial discovery must contain exactly the entrance and its adjacent zones");
|
|
Require(first.Zones.All(zone => zone.AdjacentZoneIndices.Count > 0) &&
|
|
first.Zones.All(zone => zone.AdjacentZoneIndices.All(neighbor =>
|
|
first.Zones[neighbor].AdjacentZoneIndices.Contains(zone.ZoneIndex))),
|
|
"tutorial link pruning must preserve one connected undirected graph");
|
|
var reachable = new HashSet<int> { first.EntranceZoneIndex };
|
|
var queue = new Queue<int>();
|
|
queue.Enqueue(first.EntranceZoneIndex);
|
|
while (queue.Count > 0)
|
|
{
|
|
foreach (var neighbor in first.Zones[queue.Dequeue()].AdjacentZoneIndices)
|
|
if (reachable.Add(neighbor)) queue.Enqueue(neighbor);
|
|
}
|
|
RequireEqual(first.Zones.Count, reachable.Count,
|
|
"tutorial link elimination must never strand a zone outside the playable graph");
|
|
}
|
|
|
|
private static void RetainedWorldAdvancesEveryFixedSubstep()
|
|
{
|
|
var ship = Entity(0, 0, "player");
|
|
ship.VelocityX = 10;
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "retained-substep-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [ship] }]
|
|
};
|
|
using var physics = NewPhysics();
|
|
AetheriaRuntimeDaemonTickRunner.Tick(
|
|
Path.Combine(Path.GetTempPath(), "aetheria-retained-substeps.cc"),
|
|
run,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
WorldPhysics = physics,
|
|
FrameId = 1,
|
|
FixedDeltaSeconds = 0.1,
|
|
SimulationStepCount = 3,
|
|
SimulationTimeSeconds = 0.3,
|
|
BuildPublications = false
|
|
});
|
|
RequireNear(3, ship.PositionX, 0.05,
|
|
"one retained Box3D world must advance once for every fixed substep, not once per publication frame");
|
|
}
|
|
|
|
private static void GeneratedOrbitsAdvanceOrbitalWorldTruth()
|
|
{
|
|
var orbital = Entity(0, 0, "neutral");
|
|
orbital.Kind = "station";
|
|
orbital.OrbitKey = "orbit.station";
|
|
orbital.PositionX = 10;
|
|
var orbit = new AetheriaRuntimeOrbitSnapshotCommit
|
|
{
|
|
OrbitKey = orbital.OrbitKey,
|
|
Distance = 10,
|
|
Phase = 0,
|
|
Period = 10
|
|
};
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [orbital],
|
|
Orbits = [orbit]
|
|
};
|
|
|
|
AetheriaRuntimeOrbitSimulation.StepZone(zone, [orbital], 2);
|
|
|
|
RequireNear(0.2, orbit.Phase, 0.000001,
|
|
"the canonical orbit phase must advance by fixed delta divided by authored period");
|
|
var expectedX = Math.Cos(0.2 * Math.PI * 2) * 10;
|
|
var expectedZ = Math.Sin(0.2 * Math.PI * 2) * 10;
|
|
RequireNear(expectedX, orbital.PositionX, 0.000001,
|
|
"orbital entity X must derive from the same advanced orbit observed by render queries");
|
|
RequireNear(expectedZ, orbital.PositionZ, 0.000001,
|
|
"orbital entity Z must derive from the same advanced orbit observed by render queries");
|
|
RequireNear((expectedX - 10) / 2, orbital.VelocityX, 0.000001,
|
|
"orbital X velocity must be the fixed-step pose delta consumed by Ymir");
|
|
RequireNear(expectedZ / 2, orbital.VelocityY, 0.000001,
|
|
"orbital Z velocity must be the fixed-step pose delta consumed by Ymir");
|
|
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "dynamic-orbit-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [zone]
|
|
};
|
|
using var physics = NewPhysics();
|
|
AetheriaRuntimeDaemonSimulation.Step(
|
|
run,
|
|
new AetheriaRuntimeDaemonIntentState(),
|
|
0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
physics,
|
|
frameId: 1,
|
|
simulationTimeSeconds: 2.1);
|
|
var integratedPhase = 0.21;
|
|
RequireNear(integratedPhase, orbit.Phase, 0.000001,
|
|
"the complete daemon step must advance the canonical orbit before Ymir");
|
|
RequireNear(Math.Cos(integratedPhase * Math.PI * 2) * 10, orbital.PositionX, 0.05,
|
|
"Ymir must retain the daemon's advanced orbital X pose instead of restoring a stale body transform");
|
|
RequireNear(Math.Sin(integratedPhase * Math.PI * 2) * 10, orbital.PositionZ, 0.05,
|
|
"Ymir must retain the daemon's advanced orbital Z pose instead of restoring a stale body transform");
|
|
}
|
|
|
|
private static void StableEntityIdentitySurvivesCrossZoneReindex()
|
|
{
|
|
var departing = Entity(0, 0, "player");
|
|
departing.EntityId = "cross-zone.departing";
|
|
var survivor = Entity(1, 100, "player");
|
|
survivor.EntityId = "cross-zone.survivor";
|
|
var source = new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [departing, survivor]
|
|
};
|
|
var destination = new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 1, Entities = [] };
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "cross-zone-retained-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [source, destination]
|
|
};
|
|
using var physics = NewPhysics();
|
|
AetheriaRuntimeDaemonTickRunner.Tick(
|
|
Path.Combine(Path.GetTempPath(), "aetheria-cross-zone-before.cc"),
|
|
run,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
WorldPhysics = physics,
|
|
FrameId = 1,
|
|
FixedDeltaSeconds = 0.1,
|
|
SimulationTimeSeconds = 0.1,
|
|
BuildPublications = false
|
|
});
|
|
|
|
survivor.EntityIndex = 0;
|
|
departing.EntityIndex = 0;
|
|
departing.PositionX = 250;
|
|
source.Entities = [survivor];
|
|
destination.Entities = [departing];
|
|
run.CurrentZoneIndex = 1;
|
|
run.CurrentEntityKey = "zone.1.entity.0";
|
|
AetheriaRuntimeDaemonTickRunner.Tick(
|
|
Path.Combine(Path.GetTempPath(), "aetheria-cross-zone-after.cc"),
|
|
run,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
WorldPhysics = physics,
|
|
FrameId = 2,
|
|
FixedDeltaSeconds = 0.1,
|
|
SimulationTimeSeconds = 0.2,
|
|
BuildPublications = false
|
|
});
|
|
|
|
RequireNear(100, survivor.PositionX, 0.05,
|
|
"source-zone reindex must not give a survivor the departed entity's retained Box3D body");
|
|
RequireNear(250, departing.PositionX, 0.05,
|
|
"a cross-zone arrival must retain its stable identity while spawning in the destination session");
|
|
}
|
|
|
|
public void Run()
|
|
{
|
|
Action[] checks =
|
|
[
|
|
PositiveGravityDepthAttractsAndProjectsAsAWell,
|
|
TutorialTopologyIsDeterministicConnectedAndRoleOwned,
|
|
GeneratedZoneBodiesPreserveFossilHierarchy,
|
|
TutorialWorldMaterializesEveryTopologyZone,
|
|
GeneratedOrbitsAdvanceOrbitalWorldTruth,
|
|
VolumeSurfaceKeepsNativeShaderAbiInAssetVariant,
|
|
YmirMovesProjectileAndReportsStableContact,
|
|
CompressedTerminusStopsAtAttentionBoundary,
|
|
InstantWeaponRequestSurvivesLockAcquisition,
|
|
GuidedWeaponPublishesPresentationProfileWithoutPhysicalAuthority,
|
|
CombatLockSurvivesPublicFrameRestart,
|
|
ConstantWeaponRunsOnDaemonThroughYmirBeamContact,
|
|
ChargedWeaponCannotBypassChargeLifecycle,
|
|
ChargedWeaponHoldRiskMalfunctionsDeterministically,
|
|
DeployableWeaponRunsThroughYmirAndDetonatesOnDaemon,
|
|
DeployableRangeExpiryDetonatesAfterYmirMovement,
|
|
CanonicalCatalogPublishesRecoveredMineLauncher,
|
|
EnergyFundedShieldInterceptsDamageBeforeHull,
|
|
ArmorCellsResolveBeforeEquipmentAndHull,
|
|
PenetrationStopsAtFirstHullGap,
|
|
ShipSchematicProjectsDaemonTopologyAndEquipmentState,
|
|
EquipmentDestructionDisablesBehaviorAndCockpitKillsBeforeHull,
|
|
DestructionDropsLootExactlyOnce,
|
|
DaemonSimulationTreatsYmirHitAsPresentationOnly,
|
|
ProjectileContactCannotKill,
|
|
MissingWorldPhysicsOwnerCannotAdvanceShips,
|
|
DockingUsesRealBaysAndFossilUndockRules,
|
|
DockedEntitiesCannotAttackOrBeTargeted,
|
|
RefitIsAtomicAndUsesTypedPlacement,
|
|
CargoTransferUsesSpatialAtomicTransaction,
|
|
DockedRefitSurfacePublishesGenericCommands,
|
|
FossilVelocityBehaviorsRunInDaemonFlightStep,
|
|
BehaviorChainsGateAuthoritativeStatModifiers,
|
|
ResourceBehaviorsStopEquipmentChainsInAuthoredOrder,
|
|
ThermotoggleSeedsAndGatesFromDaemonOwnedTarget,
|
|
InputCapabilityPublishesExactBehaviorLevers,
|
|
InputCapabilityPublishesConsumableActionBarState,
|
|
InputCapabilityPublishesHeldWeaponGroupLever,
|
|
InputCapabilityPublishesNativeTargetAndInteractControls,
|
|
ReflectorUsesSharedStellarLightFieldWithoutAccumulating,
|
|
SensorPingIsActorScopedEnergyGatedAndReconnectable,
|
|
TurretControllerAcquiresAimsAndTriggersExactWeapons,
|
|
TractorRampsAndPullsThroughYmirWithoutTeleportingCargo,
|
|
PickupIsCapacityCheckedExactlyOnceAndExpires,
|
|
TradePurchaseDerivesAcceptanceFromDaemonState,
|
|
TradeSaleIsDaemonPricedSpatialAndAtomic,
|
|
StationBodiesCannotConsumePickups,
|
|
PickupShieldContactCollectsOrBounces,
|
|
CultCacheRestartPreservesLoadoutAndBehaviorState,
|
|
YmirRestartDoesNotReplayConsumedPickupContact,
|
|
ThermalCellsUseFossilConductionAndRadiation,
|
|
EnergyNetworkSettlesReactorAfterConsumers,
|
|
RadiatorPumpsHeatBeforeReactorSettlement,
|
|
EquipmentThermalPerformanceOwnsShutdownAndWear,
|
|
ThermalTopologyComesFromHullAndEquipment,
|
|
ThermalMedicalExposureUsesCockpitTemperature,
|
|
ThermalMedicalDeathUsesOrdinaryDestructionPath,
|
|
MultipleActorsUseTheSameMovementLever,
|
|
DirectionalThrustersOwnOrdinaryFlightFeedback,
|
|
UnpoweredThrustersCannotMoveOrAdvertisePlume,
|
|
SpecializedBehaviorSuccessGatesTrailingWear,
|
|
RareAetherDriveSpoolsOnlyOnModifiedHullFixture,
|
|
LookDirectionRejectsInvalidVectorsWithoutMutatingTheShip,
|
|
AgentClaimsAndCompletesExploreTaskThroughCommands,
|
|
AgentTaskSurvivesCultCacheRestart,
|
|
SchedulerAssignsHighestPriorityCompatibleTask,
|
|
SchedulerRequeuesTaskFromDeadAgent,
|
|
SchedulerCollapsesDuplicateAssignmentMarkers,
|
|
CancellingAgentTaskReleasesSchedulerControls,
|
|
SchedulerAssignsShortestGalaxyRoute,
|
|
WormholeTraversalUsesPersistedEnterTransferExitPhases,
|
|
AgentTraversesGalaxyRouteBeforeExecutingTask,
|
|
IdleAgentReturnsToCanonicalHomeAndDocks,
|
|
ControlledShipDoesNotReceiveAutonomousHelmCommands,
|
|
AttackAgentControlsOptimumRangeThroughMovementLever,
|
|
AttackTaskAdmissionRejectsImpossibleWork,
|
|
AgentCompletesAttackTaskThroughTargetFireAndYmir,
|
|
AgentCompletesHaulTaskThroughMovementAndCargoCommands,
|
|
RejectedHaulTransferDoesNotAdvanceTask,
|
|
AgentPatrolsHistoricalOrbitCircuitThroughMovementCommands,
|
|
TickReconcilesAndEvaluatesCatalogBehaviors,
|
|
DaemonLoadoutsRespectFactionAvailabilityAndHullRoles,
|
|
AgentMinesAsteroidThroughEquippedBehavior,
|
|
CargoCapacityComesFromHullAndCatalogVolumes,
|
|
AgentSurveysBodyIntoCorporationKnowledge,
|
|
AgentTowsStationIntoPersistentOrbit
|
|
];
|
|
foreach (var check in checks)
|
|
RunCheck(check);
|
|
}
|
|
|
|
private static void CompressedTerminusStopsAtAttentionBoundary()
|
|
{
|
|
var source = Entity(0, -100, "player");
|
|
var target = Entity(1, 0, "raider");
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "terminus-attention-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [source, target],
|
|
PhysicalPayloads = [new AetheriaRuntimePhysicalPayloadCommit
|
|
{
|
|
PayloadId = "attention-mine",
|
|
PayloadKind = "mine",
|
|
WeaponItemKey = "attention-charge",
|
|
SourceEntityIndex = source.EntityIndex,
|
|
PositionX = target.PositionX,
|
|
PositionZ = target.PositionZ,
|
|
LifetimeSeconds = 30,
|
|
BlastRadius = 200,
|
|
PayloadMagnitude = 10,
|
|
DamageType = "Kinetic",
|
|
TriggeredAtSeconds = 0,
|
|
DetonationDelaySeconds = 0,
|
|
Stationary = true,
|
|
Active = true
|
|
}]
|
|
}]
|
|
};
|
|
|
|
var tick = AetheriaRuntimeDaemonTickRunner.Tick(
|
|
Path.Combine(Path.GetTempPath(), "aetheria-terminus-attention-smoke.cc"),
|
|
run,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
FrameId = 7,
|
|
SimulationTimeSeconds = 0.4,
|
|
FixedDeltaSeconds = 0.1,
|
|
AdvanceSimulation = true,
|
|
SimulationStepCount = 4,
|
|
StopCompressedSimulationOnAttention = true,
|
|
WorldPhysics = NewPhysics(),
|
|
BuildPublications = false
|
|
});
|
|
|
|
RequireEqual(1, tick.SimulationStepsExecuted,
|
|
"compressed Terminus must stop on the internal step that produces a decision event");
|
|
RequireNear(0.1, tick.Frame.SimulationTimeSeconds, 0.000001,
|
|
"authoritative frame time must stop at the attention boundary instead of claiming the requested batch horizon");
|
|
Require(tick.AttentionInterruption != null &&
|
|
tick.AttentionInterruption.CauseKind == "entity.damaged" &&
|
|
Math.Abs(tick.AttentionInterruption.SimulationTimeSeconds - 0.1) < 0.000001,
|
|
"tick result must retain the exact event and simulation time that interrupted compression");
|
|
RequireNear(90, Stat(target, "hull"), 0.000001,
|
|
"the causal damage step must commit before compressed simulation pauses");
|
|
var interruption = run.GameEvents.Single(value => value.Kind == "simulation.interrupted");
|
|
Require(interruption.Reason == "entity.damaged" && interruption.ScalarValue == 4 &&
|
|
interruption.AuxiliaryValue == 1,
|
|
"daemon must publish requested versus executed steps with the attention cause");
|
|
tick.Frame.GameMode = "terminus";
|
|
tick.Frame.RequestedSimulationRate = 4;
|
|
tick.Frame.EffectiveSimulationRate = 0;
|
|
tick.Frame.SimulationStepsExecuted = tick.SimulationStepsExecuted;
|
|
tick.Frame.AttentionCauseKind = tick.AttentionInterruption?.CauseKind ?? "";
|
|
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
tick.Frame,
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
Require(Flatten(surface.Surface.Root).Any(node => node.Kind == "feedback.event" &&
|
|
node.Props["eventKind"] == "simulation.interrupted" &&
|
|
node.Props["reason"] == "entity.damaged" &&
|
|
node.Props["scalarValue"] == "4" && node.Props["auxiliaryValue"] == "1"),
|
|
"generic Eve feedback must expose why compressed world time stopped");
|
|
var world = Flatten(surface.Surface.Root).Single(node => node.Kind == "world.scene3d");
|
|
Require(world.Props["gameMode"] == "terminus" &&
|
|
world.Props["requestedSimulationRate"] == "4" &&
|
|
world.Props["effectiveSimulationRate"] == "0" &&
|
|
world.Props["simulationStepsExecuted"] == "1" &&
|
|
world.Props["attentionCauseKind"] == "entity.damaged",
|
|
"generic Eve world state must expose requested and effective clock state without owning it");
|
|
var restoredFrame = MessagePack.MessagePackSerializer.Deserialize<AetheriaRuntimeDaemonFrameDocument>(
|
|
MessagePack.MessagePackSerializer.Serialize(tick.Frame));
|
|
Require(restoredFrame.RequestedSimulationRate == 4 && restoredFrame.EffectiveSimulationRate == 0 &&
|
|
restoredFrame.SimulationStepsExecuted == 1 && restoredFrame.AttentionCauseKind == "entity.damaged",
|
|
"clock interruption state must survive authoritative frame serialization");
|
|
}
|
|
|
|
private static void BehaviorChainsGateAuthoritativeStatModifiers()
|
|
{
|
|
var thruster = CatalogItem("modifier-target", new AetheriaRuntimeBehaviorPayload(
|
|
0, "Thruster", 0,
|
|
[new AetheriaRuntimeBehaviorField(1, PerformanceStat(10))]));
|
|
var controls = CatalogItem("modifier-controls",
|
|
new AetheriaRuntimeBehaviorPayload(0, "Switch", 0, []),
|
|
new AetheriaRuntimeBehaviorPayload(1, "StatModifier", 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, StatReference("ThrusterData", "Thrust")),
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(2)),
|
|
new AetheriaRuntimeBehaviorField(3, StringValue("Multiplier"))
|
|
]),
|
|
new AetheriaRuntimeBehaviorPayload(2, "Trigger", 1, []),
|
|
new AetheriaRuntimeBehaviorPayload(3, "StatModifier", 1,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, StatReference("Thruster", "Thrust")),
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(5)),
|
|
new AetheriaRuntimeBehaviorField(3, StringValue("Constant"))
|
|
]));
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([thruster, controls], [], []);
|
|
var entity = Entity(0, 0, "player");
|
|
entity.Equipment =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = thruster.ItemKey, Enabled = true, Durability = 1, Quality = 1 } },
|
|
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = controls.ItemKey, Enabled = true, Durability = 1, Quality = 1 } }
|
|
];
|
|
var activate = new[]
|
|
{
|
|
new AetheriaRuntimeDaemonBehaviorIntent
|
|
{ ActorEntityKey = "zone.0.entity.0", EquipmentIndex = 1, BehaviorIndex = 0, Active = true },
|
|
new AetheriaRuntimeDaemonBehaviorIntent
|
|
{ ActorEntityKey = "zone.0.entity.0", EquipmentIndex = 1, BehaviorIndex = 2, Active = true }
|
|
};
|
|
|
|
AetheriaRuntimeBehaviorSimulation.Step(0, [entity],
|
|
[new AetheriaRuntimeDaemonBehaviorIntent
|
|
{ ActorEntityKey = "zone.1.entity.0", EquipmentIndex = 1, BehaviorIndex = 0, Active = true }],
|
|
catalog, 0.1);
|
|
var target = AetheriaRuntimeEquippedBehaviorQueries.FindOperational(entity, catalog, "Thruster").Single();
|
|
RequireNear(10, target.EvaluateStat(1), 0.000001,
|
|
"a behavior command for the same entity index in another zone must not cross-activate this ship");
|
|
|
|
AetheriaRuntimeBehaviorSimulation.Step(0, [entity], activate, catalog, 0.1);
|
|
RequireNear(25, target.EvaluateStat(1), 0.000001,
|
|
"switch and one-shot trigger groups must multiply then add through the shared daemon stat path");
|
|
var modifierStates = entity.BehaviorStates.Where(value => value.BehaviorKind == "StatModifier").ToArray();
|
|
Require(modifierStates.All(value => value.StatModifierApplied && value.StatModifierTargetStatCount == 1),
|
|
"applied modifier state must name the exact live target count");
|
|
|
|
AetheriaRuntimeBehaviorSimulation.Step(0, [entity], [], catalog, 0.1);
|
|
RequireNear(20, target.EvaluateStat(1), 0.000001,
|
|
"trigger modifiers must disappear on the next tick without another pull");
|
|
|
|
AetheriaRuntimeBehaviorSimulation.Step(0, [entity],
|
|
[new AetheriaRuntimeDaemonBehaviorIntent
|
|
{ ActorEntityKey = "zone.0.entity.0", EquipmentIndex = 1, BehaviorIndex = 0, Active = false }],
|
|
catalog, 0.1);
|
|
RequireNear(10, target.EvaluateStat(1), 0.000001,
|
|
"deactivating the switch must restore the unmodified authored stat exactly");
|
|
}
|
|
|
|
private static void ResourceBehaviorsStopEquipmentChainsInAuthoredOrder()
|
|
{
|
|
var targetItem = CatalogItem("resource-target", new AetheriaRuntimeBehaviorPayload(
|
|
0, "Thruster", 0, [new AetheriaRuntimeBehaviorField(1, PerformanceStat(10))]));
|
|
var controlItem = CatalogItem("resource-controls",
|
|
new AetheriaRuntimeBehaviorPayload(0, "Switch", 0, []),
|
|
new AetheriaRuntimeBehaviorPayload(1, "Cooldown", 0,
|
|
[new AetheriaRuntimeBehaviorField(1, PerformanceStat(1))]),
|
|
new AetheriaRuntimeBehaviorPayload(2, "EnergyDraw", 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, PerformanceStat(5)),
|
|
new AetheriaRuntimeBehaviorField(2, BoolValue(false))
|
|
]),
|
|
new AetheriaRuntimeBehaviorPayload(3, "Heat", 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, PerformanceStat(10)),
|
|
new AetheriaRuntimeBehaviorField(2, BoolValue(false))
|
|
]),
|
|
new AetheriaRuntimeBehaviorPayload(4, "ItemUsage", 0,
|
|
[new AetheriaRuntimeBehaviorField(1, ItemKeyValue("resource-ammo"))]),
|
|
new AetheriaRuntimeBehaviorPayload(5, "Wear", 0,
|
|
[new AetheriaRuntimeBehaviorField(1, BoolValue(true))]),
|
|
new AetheriaRuntimeBehaviorPayload(6, "StatModifier", 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, StatReference("Thruster", "Thrust")),
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(2)),
|
|
new AetheriaRuntimeBehaviorField(3, StringValue("Multiplier"))
|
|
]));
|
|
var capacitorItem = CatalogItem("resource-capacitor", CapacitorPayload(10, 1));
|
|
var ammoItem = CatalogItem("resource-ammo");
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
|
[targetItem, controlItem, capacitorItem, ammoItem], [], []);
|
|
var entity = Entity(0, 0, "player");
|
|
entity.Equipment =
|
|
[
|
|
Equipment(targetItem.ItemKey),
|
|
Equipment(controlItem.ItemKey),
|
|
Equipment(capacitorItem.ItemKey)
|
|
];
|
|
entity.CargoContents = [new AetheriaRuntimeCargoBayLoadoutCommit
|
|
{
|
|
Items = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{ Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = ammoItem.ItemKey, Quantity = 2 } }]
|
|
}];
|
|
AetheriaRuntimeThermalSimulation.EnsureTopology(entity, catalog);
|
|
AetheriaRuntimeThermalSimulation.EnsureState(entity);
|
|
AetheriaRuntimeThermalSimulation.UpdateEquipmentStates(entity, catalog, 0.1);
|
|
AetheriaRuntimeBehaviorStateProjector.EnsureEquipmentBehaviorStates(entity, catalog);
|
|
AetheriaRuntimeEnergySimulation.BeginTick(entity, catalog);
|
|
var capacitor = AetheriaRuntimeEquippedBehaviorQueries.Find(entity, catalog, "Capacitor").Single();
|
|
capacitor.State.CapacitorCharge = 10;
|
|
var controlEquipmentState = entity.EquipmentStates.Single(value => value.EquipmentIndex == 1);
|
|
controlEquipmentState.Wear = 2;
|
|
var initialTemperature = AetheriaRuntimeThermalSimulation.EquipmentTemperature(entity, catalog, 1);
|
|
var target = AetheriaRuntimeEquippedBehaviorQueries.FindOperational(entity, catalog, "Thruster").Single();
|
|
|
|
AetheriaRuntimeBehaviorSimulation.Step(0, [entity],
|
|
[new AetheriaRuntimeDaemonBehaviorIntent
|
|
{ ActorEntityKey = "zone.0.entity.0", EquipmentIndex = 1, BehaviorIndex = 0, Active = true }],
|
|
catalog, 0.1);
|
|
RequireNear(10, capacitor.State.CapacitorCharge, 0.000001,
|
|
"a newly constructed cooldown must stop its group before any resource transaction");
|
|
RequireEqual(2, CargoQuantity(entity, ammoItem.ItemKey),
|
|
"cooldown must stop item usage before cargo mutation");
|
|
|
|
AetheriaRuntimeBehaviorSimulation.Step(0, [entity], [], catalog, 0.1);
|
|
RequireNear(5, capacitor.State.CapacitorCharge, 0.000001,
|
|
"energy draw must debit the daemon capacitor exactly once when the cooldown opens");
|
|
RequireEqual(1, CargoQuantity(entity, ammoItem.ItemKey),
|
|
"item usage must remove exactly one authored cargo item");
|
|
RequireNear(0.8, entity.Equipment[1].Item.Durability, 0.000001,
|
|
"wear must use the thermal owner's pressure only after preceding gates succeed");
|
|
Require(AetheriaRuntimeThermalSimulation.EquipmentTemperature(entity, catalog, 1) > initialTemperature,
|
|
"heat must enter the source equipment's authoritative thermal cells");
|
|
RequireNear(20, target.EvaluateStat(1), 0.000001,
|
|
"downstream modifiers must apply only after every preceding resource gate succeeds");
|
|
RequireNear(0.9, entity.BehaviorProgress.Single(value =>
|
|
value.OwnerIndex == 1 && value.BehaviorKind == "Cooldown").Progress, 0.000001,
|
|
"cooldown state must project a generic Eve progress lever from the authoritative behavior row");
|
|
|
|
var cooldown = entity.BehaviorStates.Single(value =>
|
|
value.OwnerIndex == 1 && value.BehaviorKind == "Cooldown");
|
|
cooldown.CooldownProgress = -1;
|
|
capacitor.State.CapacitorCharge = 0;
|
|
var temperatureBeforeRefusal = AetheriaRuntimeThermalSimulation.EquipmentTemperature(entity, catalog, 1);
|
|
var durabilityBeforeRefusal = entity.Equipment[1].Item.Durability;
|
|
AetheriaRuntimeBehaviorSimulation.Step(0, [entity], [], catalog, 0.1);
|
|
RequireEqual(1, CargoQuantity(entity, ammoItem.ItemKey),
|
|
"an empty capacitor must stop the group before item usage");
|
|
RequireNear(durabilityBeforeRefusal, entity.Equipment[1].Item.Durability, 0.000001,
|
|
"an empty capacitor must stop the group before wear");
|
|
RequireNear(temperatureBeforeRefusal,
|
|
AetheriaRuntimeThermalSimulation.EquipmentTemperature(entity, catalog, 1), 0.000001,
|
|
"an empty capacitor must stop the group before heat");
|
|
RequireNear(10, target.EvaluateStat(1), 0.000001,
|
|
"an empty capacitor must remove a downstream modifier instead of leaving stale application state");
|
|
|
|
static AetheriaRuntimeLoadoutItemSlotCommit Equipment(string itemKey) => new()
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = itemKey, Enabled = true, Durability = 1, Quality = 1 }
|
|
};
|
|
}
|
|
|
|
private static void ThermotoggleSeedsAndGatesFromDaemonOwnedTarget()
|
|
{
|
|
var thermostat = CatalogItem("thermostat",
|
|
new AetheriaRuntimeBehaviorPayload(0, "Thermotoggle", 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, Number(300)),
|
|
new AetheriaRuntimeBehaviorField(2, BoolValue(false)),
|
|
new AetheriaRuntimeBehaviorField(3, BoolValue(true))
|
|
]),
|
|
new AetheriaRuntimeBehaviorPayload(1, "Radiator", 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, PerformanceStat(0)),
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(3, Number(200)),
|
|
new AetheriaRuntimeBehaviorField(4, PerformanceStat(0)),
|
|
new AetheriaRuntimeBehaviorField(5, PerformanceStat(0)),
|
|
new AetheriaRuntimeBehaviorField(6, PerformanceStat(10))
|
|
]),
|
|
new AetheriaRuntimeBehaviorPayload(2, "Heat", 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, PerformanceStat(10)),
|
|
new AetheriaRuntimeBehaviorField(2, BoolValue(false))
|
|
]),
|
|
new AetheriaRuntimeBehaviorPayload(3, "Wear", 0,
|
|
[new AetheriaRuntimeBehaviorField(1, BoolValue(true))]));
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([thermostat], [], []);
|
|
var entity = Entity(0, 0, "player");
|
|
entity.HeatsinksEnabled = true;
|
|
entity.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = thermostat.ItemKey, Enabled = true, Durability = 1, Quality = 1 }
|
|
}];
|
|
AetheriaRuntimeThermalSimulation.EnsureTopology(entity, catalog);
|
|
AetheriaRuntimeThermalSimulation.EnsureState(entity);
|
|
AetheriaRuntimeThermalSimulation.UpdateEquipmentStates(entity, catalog, 0.1);
|
|
entity.EquipmentStates.Single().Wear = 1;
|
|
AetheriaRuntimeBehaviorStateProjector.EnsureEquipmentBehaviorStates(entity, catalog);
|
|
var thermostatState = entity.BehaviorStates.Single(value => value.BehaviorKind == "Thermotoggle");
|
|
RequireNear(300, thermostatState.ThermotoggleTargetTemperature, 0.000001,
|
|
"new thermotoggle state must seed from the authored target instead of zero");
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "thermotoggle-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [entity] }]
|
|
};
|
|
var context = new AetheriaRuntimeDaemonOperationContext { Catalog = catalog };
|
|
|
|
SetTarget(200, 1);
|
|
var before = AetheriaRuntimeThermalSimulation.EquipmentTemperature(entity, catalog, 0);
|
|
AetheriaRuntimeBehaviorSimulation.Step(0, [entity], [], catalog, 0.1);
|
|
AetheriaRuntimeEnergySimulation.StepRadiators(entity, catalog, 0.1);
|
|
AetheriaRuntimeBehaviorSimulation.CompleteDeferredChains(entity, catalog, 0.1);
|
|
RequireNear(before, AetheriaRuntimeThermalSimulation.EquipmentTemperature(entity, catalog, 0), 0.000001,
|
|
"a low-pass thermotoggle above its target must stop downstream heat");
|
|
Require(!entity.BehaviorStates.Single(value => value.BehaviorKind == "Heat").ChainReached,
|
|
"a closed thermostat must keep downstream specialized and common behaviors unreachable");
|
|
Require(!entity.BehaviorStates.Single(value => value.BehaviorKind == "Radiator").ChainSucceeded,
|
|
"a closed thermostat must keep the radiator Execute path from succeeding");
|
|
RequireNear(0, entity.BehaviorStates.Single(value => value.BehaviorKind == "Radiator").PumpedHeat, 0.000001,
|
|
"a closed thermostat must not refresh Execute-owned radiator throughput");
|
|
RequireNear(1, entity.Equipment[0].Item.Durability, 0.000001,
|
|
"a closed thermostat must not apply trailing wear");
|
|
|
|
SetTarget(300, 2);
|
|
AetheriaRuntimeBehaviorSimulation.Step(0, [entity], [], catalog, 0.1);
|
|
AetheriaRuntimeEnergySimulation.StepRadiators(entity, catalog, 0.1);
|
|
AetheriaRuntimeBehaviorSimulation.CompleteDeferredChains(entity, catalog, 0.1);
|
|
Require(AetheriaRuntimeThermalSimulation.EquipmentTemperature(entity, catalog, 0) > before,
|
|
"a low-pass thermotoggle below its target must admit downstream heat");
|
|
Require(entity.BehaviorStates.Single(value => value.BehaviorKind == "Heat").ChainReached,
|
|
"an open thermostat must publish that the downstream behavior was reached this tick");
|
|
RequireNear(1, entity.BehaviorStates.Single(value => value.BehaviorKind == "Radiator").PumpedHeat, 0.000001,
|
|
"an open thermostat must admit the radiator subsystem through the same chain state");
|
|
RequireNear(0.9, entity.Equipment[0].Item.Durability, 0.000001,
|
|
"a successful radiator must apply its authored trailing wear exactly once");
|
|
|
|
var radiatorPayload = thermostat.BehaviorPayloads.Single(value => value.Kind == "Radiator");
|
|
radiatorPayload.Fields = radiatorPayload.Fields.Select(field => field.Key == 5
|
|
? new AetheriaRuntimeBehaviorField(5, PerformanceStat(10))
|
|
: field).ToArray();
|
|
var durabilityBeforeEnergyRefusal = entity.Equipment[0].Item.Durability;
|
|
AetheriaRuntimeBehaviorSimulation.Step(0, [entity], [], catalog, 0.1);
|
|
AetheriaRuntimeEnergySimulation.StepRadiators(entity, catalog, 0.1);
|
|
AetheriaRuntimeBehaviorSimulation.CompleteDeferredChains(entity, catalog, 0.1);
|
|
Require(!entity.BehaviorStates.Single(value => value.BehaviorKind == "Radiator").ChainSucceeded,
|
|
"an unfunded radiator must report its fossil Execute failure");
|
|
RequireNear(durabilityBeforeEnergyRefusal, entity.Equipment[0].Item.Durability, 0.000001,
|
|
"an unfunded radiator must stop the chain before trailing wear");
|
|
|
|
void SetTarget(double target, long sequence)
|
|
{
|
|
var command = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.SetThermotoggleTargetTemperature,
|
|
"smoke", "thermostat-session", sequence, "zone.0.entity.0");
|
|
command.TargetEntityKey = "zone.0.entity.0";
|
|
command.EquipmentIndex = 0;
|
|
command.BehaviorIndex = 0;
|
|
command.ScalarValue = target;
|
|
var result = AetheriaRuntimeDaemonOperations.Execute(run, [command], context);
|
|
RequireEqual(1, result.AppliedCommandIds.Count,
|
|
"an installed adjustable thermotoggle must accept its typed daemon command");
|
|
}
|
|
}
|
|
|
|
private static void InputCapabilityPublishesExactBehaviorLevers()
|
|
{
|
|
var controls = CatalogItem("behavior-controls",
|
|
new AetheriaRuntimeBehaviorPayload(0, "Thruster", 0, []),
|
|
new AetheriaRuntimeBehaviorPayload(1, "Switch", 1, []),
|
|
new AetheriaRuntimeBehaviorPayload(2, "Trigger", 2, []),
|
|
new AetheriaRuntimeBehaviorPayload(3, "Thermotoggle", 3,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, Number(300)),
|
|
new AetheriaRuntimeBehaviorField(2, BoolValue(false)),
|
|
new AetheriaRuntimeBehaviorField(3, BoolValue(true))
|
|
]),
|
|
new AetheriaRuntimeBehaviorPayload(4, "Thermotoggle", 4,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, Number(350)),
|
|
new AetheriaRuntimeBehaviorField(2, BoolValue(false)),
|
|
new AetheriaRuntimeBehaviorField(3, BoolValue(false))
|
|
]));
|
|
controls.Name = "Climate Control";
|
|
controls.ActionBarIcon = "Assets/Resources/Sprites/Icons/Stroked/Power.png";
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([controls], [], []);
|
|
var entity = Entity(0, 0, "player");
|
|
entity.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = controls.ItemKey, Enabled = true, Durability = 1, Quality = 1 }
|
|
}];
|
|
AetheriaRuntimeBehaviorStateProjector.EnsureEquipmentBehaviorStates(entity, catalog);
|
|
entity.EquipmentStates = [new AetheriaRuntimeEquipmentStateCommit
|
|
{ EquipmentIndex = 0, Online = true }];
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "behavior-input-smoke",
|
|
CurrentZoneIndex = 0,
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [entity] }]
|
|
};
|
|
run.CurrentEntityKey = run.EntityRecordKey(0, entity.EntityIndex);
|
|
var frame = AetheriaRuntimeDaemonFrameDocument.Create(run, "smoke", "behavior-input", 1, 0.1, 0.1);
|
|
var capability = AetheriaRuntimeInputCapabilityDocument.FromFrame(frame, catalog: catalog);
|
|
RequireEqual("aetheria", capability.ProviderId,
|
|
"input capability must retain provider identity instead of substituting daemon service identity");
|
|
var actions = capability.Actions.Where(value => value.Category == "equipment").ToArray();
|
|
RequireEqual(3, actions.Length,
|
|
"input capability must omit non-interactive and fixed behaviors instead of activating behavior zero blindly");
|
|
var switchAction = actions.Single(value => value.ActionId == "equipment.0.behavior.1");
|
|
var triggerAction = actions.Single(value => value.ActionId == "equipment.0.behavior.2");
|
|
var thermostatAction = actions.Single(value => value.ActionId == "equipment.0.behavior.3");
|
|
RequireEqual("button-hold.v1", switchAction.InputValue?.Model ?? "",
|
|
"switch controls must advertise the fossil press/release input model");
|
|
RequireEqual("active", triggerAction.InputValue?.PayloadKey ?? "",
|
|
"trigger controls must route their input value into the typed active field");
|
|
RequireEqual("scalar.v1", thermostatAction.InputValue?.Model ?? "",
|
|
"adjustable thermostats must advertise a runtime-neutral scalar lever");
|
|
RequireNear(300, thermostatAction.InputValue?.CurrentValue ?? double.NaN, 0.000001,
|
|
"thermostat controls must publish the daemon-owned current target as typed scalar state");
|
|
RequireEqual("kelvin", thermostatAction.InputValue?.Unit ?? "",
|
|
"thermostat controls must publish the scalar unit without hiding it in command payload strings");
|
|
Require(actions.All(value => value.ActionBar && value.IconRef == "item.behavior-controls.icon"),
|
|
"interactive equipment behaviors must expose provider-icon action-bar suggestions");
|
|
var portableThermostat = capability.ToEveDocument().Actions
|
|
.Single(value => value.ActionId == thermostatAction.ActionId);
|
|
RequireNear(300, portableThermostat.InputValue?.CurrentValue ?? double.NaN, 0.000001,
|
|
"the portable Eve document must retain typed scalar current state");
|
|
Require(portableThermostat.ActionBar && portableThermostat.IconRef == "item.behavior-controls.icon",
|
|
"the portable Eve action must retain the provider action-bar suggestion and asset key");
|
|
|
|
var switchRequest = new EveSurfaceCommandRequest(
|
|
capability.ProviderId,
|
|
AetheriaRuntimeDaemonGameSurfaceBuilder.PilotSurfaceId,
|
|
CultMesh.OperationInvocation(switchAction.Operation),
|
|
CultMesh.OperationPayload(
|
|
("entityId", run.CurrentEntityKey),
|
|
("equipmentIndex", "0"),
|
|
("behaviorIndex", "1"),
|
|
("active", "1")),
|
|
DateTimeOffset.UtcNow,
|
|
"behavior-input-smoke");
|
|
var switchTranslated = AetheriaRuntimeDaemonOperationsClient.TryCreateSurfaceCommandDocument(
|
|
switchRequest, frame, ".", "behavior-input-smoke", "behavior-input", out var switchCommand);
|
|
Require(switchTranslated &&
|
|
switchCommand?.Kind == AetheriaRuntimeDaemonCommandKinds.SetBehaviorActive &&
|
|
switchCommand.EquipmentIndex == 0 && switchCommand.BehaviorIndex == 1 &&
|
|
switchCommand.ScalarValue > 0.5,
|
|
$"the generic Eve action must translate into the exact typed switch command; accepted={switchTranslated}, kind={switchCommand?.Kind}, equipment={switchCommand?.EquipmentIndex}, behavior={switchCommand?.BehaviorIndex}, scalar={switchCommand?.ScalarValue}");
|
|
var switchResult = AetheriaRuntimeDaemonOperations.Execute(
|
|
run, [switchCommand!], new AetheriaRuntimeDaemonOperationContext { Catalog = catalog });
|
|
AetheriaRuntimeBehaviorSimulation.Step(0, [entity], switchResult.Intents.Behaviors, catalog, 0.1);
|
|
Require(entity.BehaviorStates.Single(value => value.BehaviorIndex == 1).SwitchActivated,
|
|
"the advertised switch lever must reach the daemon behavior owner");
|
|
|
|
var thermostatRequest = new EveSurfaceCommandRequest(
|
|
capability.ProviderId,
|
|
AetheriaRuntimeDaemonGameSurfaceBuilder.PilotSurfaceId,
|
|
CultMesh.OperationInvocation(thermostatAction.Operation),
|
|
CultMesh.OperationPayload(
|
|
("entityId", run.CurrentEntityKey),
|
|
("targetEntityKey", run.CurrentEntityKey),
|
|
("equipmentIndex", "0"),
|
|
("behaviorIndex", "3"),
|
|
("scalarValue", "425")),
|
|
DateTimeOffset.UtcNow,
|
|
"behavior-input-smoke");
|
|
Require(AetheriaRuntimeDaemonOperationsClient.TryCreateSurfaceCommandDocument(
|
|
thermostatRequest, frame, ".", "behavior-input-smoke", "behavior-input", out var thermostatCommand) &&
|
|
thermostatCommand?.Kind == AetheriaRuntimeDaemonCommandKinds.SetThermotoggleTargetTemperature,
|
|
"the generic Eve scalar lever must translate into the typed thermostat command");
|
|
var thermostatResult = AetheriaRuntimeDaemonOperations.Execute(
|
|
run, [thermostatCommand!], new AetheriaRuntimeDaemonOperationContext { Catalog = catalog });
|
|
RequireEqual(1, thermostatResult.AppliedCommandIds.Count,
|
|
"the translated thermostat command must pass daemon catalog authorization");
|
|
RequireNear(425, entity.BehaviorStates.Single(value => value.BehaviorIndex == 3)
|
|
.ThermotoggleTargetTemperature, 0.000001,
|
|
"the advertised scalar lever must mutate only daemon-owned thermostat state");
|
|
|
|
entity.EquipmentStates[0].Online = false;
|
|
var offline = AetheriaRuntimeInputCapabilityDocument.FromFrame(frame, catalog: catalog);
|
|
Require(offline.Actions.Where(value => value.Category == "equipment")
|
|
.All(value => value.Availability == "unavailable"),
|
|
"offline equipment must retain stable action identity while advertising current unavailability");
|
|
}
|
|
|
|
private static void InputCapabilityPublishesConsumableActionBarState()
|
|
{
|
|
var repairGel = CatalogItem("repair-gel");
|
|
repairGel.Name = "Repair Gel";
|
|
repairGel.Category = AetheriaRuntimeItemCategories.Consumable;
|
|
repairGel.Duration = 4;
|
|
repairGel.Stackable = false;
|
|
repairGel.ActionBarIcon = "Assets/Resources/Sprites/Icons/Stroked/Repair.png";
|
|
var ore = CatalogItem("ore");
|
|
ore.Category = AetheriaRuntimeItemCategories.SimpleCommodity;
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([repairGel, ore], [], []);
|
|
var entity = Entity(0, 0, "player");
|
|
entity.CargoContents =
|
|
[
|
|
new AetheriaRuntimeCargoBayLoadoutCommit
|
|
{
|
|
Items =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "repair-gel", Quantity = 3 }
|
|
},
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "ore", Quantity = 7 }
|
|
}
|
|
]
|
|
}
|
|
];
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "consumable-input-smoke",
|
|
CurrentZoneIndex = 0,
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [entity] }]
|
|
};
|
|
run.CurrentEntityKey = run.EntityRecordKey(0, entity.EntityIndex);
|
|
var frame = AetheriaRuntimeDaemonFrameDocument.Create(run, "smoke", "consumable-input", 1, 0.1, 0.1);
|
|
var capability = AetheriaRuntimeInputCapabilityDocument.FromFrame(frame, catalog: catalog);
|
|
var action = capability.Actions.Single(value => value.Category == "consumable");
|
|
RequireEqual("3", action.Payload["quantityRemaining"],
|
|
"consumable action bars must derive quantity from daemon cargo truth");
|
|
RequireEqual("0", action.Payload["fillValue"],
|
|
"inactive consumables must advertise an empty remaining-duration fill");
|
|
RequireEqual("remaining-ratio.v1", action.Payload["fillModel"],
|
|
"generic lowerers must receive the fossil action-bar fill semantics explicitly");
|
|
RequireEqual("available", action.Availability,
|
|
"a valid consumable in cargo must advertise current command availability");
|
|
Require(action.ActionBar && action.IconRef == "item.repair-gel.icon",
|
|
"consumables must suggest an action-bar lever using their provider-owned catalog icon");
|
|
Require(!capability.Actions.Any(value => value.Category == "consumable" && value.Payload["itemKey"] == "ore"),
|
|
"non-consumable cargo must not become an action-bar command");
|
|
|
|
entity.CargoContents = Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>();
|
|
entity.ActiveConsumables =
|
|
[
|
|
new AetheriaRuntimeActiveConsumableCommit
|
|
{
|
|
ItemKey = "repair-gel",
|
|
Duration = 4,
|
|
RemainingDuration = 1
|
|
}
|
|
];
|
|
var activeAction = AetheriaRuntimeInputCapabilityDocument.FromFrame(frame, catalog: catalog)
|
|
.ToEveDocument().Actions.Single(value => value.Category == "consumable");
|
|
RequireEqual("0", activeAction.Payload["quantityRemaining"],
|
|
"the action must survive depletion while its daemon-owned effect remains active");
|
|
RequireEqual("1", activeAction.Payload["activeEffectCount"],
|
|
"the portable Eve action must expose daemon-owned active effect state");
|
|
RequireEqual("1", activeAction.Payload["remainingDurationSeconds"],
|
|
"generic clients must receive exact remaining duration without querying a Unity effect object");
|
|
RequireEqual("4", activeAction.Payload["durationSeconds"],
|
|
"generic clients must receive exact authored consumable duration");
|
|
RequireEqual("0.25", activeAction.Payload["fillValue"],
|
|
"remaining duration must lower to the fossil action-bar ratio exactly");
|
|
RequireEqual("unavailable", activeAction.Availability,
|
|
"a depleted non-stackable active consumable must remain visible but reject activation");
|
|
}
|
|
|
|
private static void InputCapabilityPublishesHeldWeaponGroupLever()
|
|
{
|
|
var entity = Entity(0, 0, "player");
|
|
entity.WeaponGroups = [new[] { 0 }];
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "weapon-input-smoke",
|
|
CurrentZoneIndex = 0,
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [entity] }]
|
|
};
|
|
run.CurrentEntityKey = run.EntityRecordKey(0, entity.EntityIndex);
|
|
var frame = AetheriaRuntimeDaemonFrameDocument.Create(run, "smoke", "weapon-input", 1, 0.1, 0.1);
|
|
var capability = AetheriaRuntimeInputCapabilityDocument.FromFrame(frame);
|
|
var action = capability.Actions.Single(value => value.ActionId == "weapon-group.0.fire");
|
|
RequireEqual("aetheria.daemon.commands.SetWeaponGroupActive", action.Operation,
|
|
"weapon input must advertise the daemon's press/release owner instead of the one-shot compatibility command");
|
|
RequireEqual("button-hold.v1", action.InputValue?.Model ?? "",
|
|
"generic Eve clients must know that weapon firing is a held control");
|
|
RequireEqual("active", action.InputValue?.PayloadKey ?? "",
|
|
"weapon press/release values must route into the typed active field");
|
|
Require(action.ActionBar,
|
|
"weapon groups must remain suggested action-bar levers without making the client own firing");
|
|
|
|
AetheriaRuntimeDaemonCommandDocument Translate(string active)
|
|
{
|
|
var request = new EveSurfaceCommandRequest(
|
|
capability.ProviderId,
|
|
AetheriaRuntimeDaemonGameSurfaceBuilder.PilotSurfaceId,
|
|
CultMesh.OperationInvocation(action.Operation),
|
|
CultMesh.OperationPayload(
|
|
("entityId", run.CurrentEntityKey),
|
|
("weaponGroup", "0"),
|
|
("active", active)),
|
|
DateTimeOffset.UtcNow,
|
|
"weapon-input-smoke");
|
|
Require(AetheriaRuntimeDaemonOperationsClient.TryCreateSurfaceCommandDocument(
|
|
request, frame, ".", "weapon-input-smoke", "weapon-input", out var command) &&
|
|
command?.Kind == AetheriaRuntimeDaemonCommandKinds.SetWeaponGroupActive,
|
|
$"generic weapon input value {active} must translate into the typed held-fire command");
|
|
return command!;
|
|
}
|
|
|
|
var pressed = AetheriaRuntimeDaemonOperations.Execute(
|
|
run, [Translate("1")], new AetheriaRuntimeDaemonOperationContext());
|
|
RequireEqual(1, pressed.Intents.WeaponGroups.Count,
|
|
"weapon press must reach the daemon intent owner exactly once");
|
|
Require(pressed.Intents.WeaponGroups[0].Active,
|
|
"weapon press must activate the exact advertised group");
|
|
Require(entity.ActiveWeaponGroups.SequenceEqual(new[] { true }),
|
|
"weapon press must persist the daemon-owned held state beyond its command frame");
|
|
var heldCapability = AetheriaRuntimeInputCapabilityDocument.FromFrame(frame);
|
|
RequireEqual("1", heldCapability.Actions.Single(value => value.ActionId == "weapon-group.0.fire")
|
|
.Payload["currentValue"],
|
|
"the Eve lever must derive its current value from the daemon-owned held state");
|
|
var restartedRun = MessagePack.MessagePackSerializer.Deserialize<AetheriaRuntimeRunCheckpointCommit>(
|
|
MessagePack.MessagePackSerializer.Serialize(run));
|
|
var restartedFrame = AetheriaRuntimeDaemonFrameDocument.Create(
|
|
restartedRun, "smoke", "weapon-input-restart", 2, 0.2, 0.1);
|
|
Require(restartedRun.Zones[0].Entities[0].ActiveWeaponGroups.SequenceEqual(new[] { true }) &&
|
|
AetheriaRuntimeInputCapabilityDocument.FromFrame(restartedFrame).Actions
|
|
.Single(value => value.ActionId == "weapon-group.0.fire").Payload["currentValue"] == "1",
|
|
"daemon restart must retain held weapon-group truth and re-advertise it through Eve");
|
|
|
|
var released = AetheriaRuntimeDaemonOperations.Execute(
|
|
run, [Translate("0")], new AetheriaRuntimeDaemonOperationContext());
|
|
RequireEqual(1, released.Intents.WeaponGroups.Count,
|
|
"weapon release must reach the daemon intent owner exactly once");
|
|
Require(!released.Intents.WeaponGroups[0].Active,
|
|
"weapon release must deactivate the exact advertised group");
|
|
Require(entity.ActiveWeaponGroups.SequenceEqual(new[] { false }),
|
|
"weapon release must clear the daemon-owned held state");
|
|
}
|
|
|
|
private static void InputCapabilityPublishesNativeTargetAndInteractControls()
|
|
{
|
|
var actor = Entity(0, 0, "player");
|
|
var near = Entity(1, 0, "enemy");
|
|
near.PositionX = 4;
|
|
var middle = Entity(2, 0, "enemy");
|
|
middle.PositionX = 8;
|
|
var far = Entity(3, 0, "enemy");
|
|
far.PositionZ = 12;
|
|
var hidden = Entity(4, 0, "enemy");
|
|
hidden.PositionX = 2;
|
|
actor.Contacts =
|
|
[
|
|
new AetheriaRuntimeEntityContactCommit
|
|
{ TargetEntityIndex = near.EntityIndex, Visible = true, Hostile = true },
|
|
new AetheriaRuntimeEntityContactCommit
|
|
{ TargetEntityIndex = middle.EntityIndex, Visible = true, Hostile = true },
|
|
new AetheriaRuntimeEntityContactCommit
|
|
{ TargetEntityIndex = far.EntityIndex, Visible = true, Hostile = true }
|
|
];
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "native-input-smoke",
|
|
CurrentZoneIndex = 0,
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [actor, near, middle, far, hidden] }]
|
|
};
|
|
run.CurrentEntityKey = run.EntityRecordKey(0, actor.EntityIndex);
|
|
var frame = AetheriaRuntimeDaemonFrameDocument.Create(run, "smoke", "native-input", 1, 0.1, 0.1);
|
|
var capability = AetheriaRuntimeInputCapabilityDocument.FromFrame(frame);
|
|
var targeting = capability.Actions.Where(value => value.Category == "targeting").ToArray();
|
|
Require(targeting.Select(value => value.ActionId).ToHashSet(StringComparer.Ordinal).SetEquals(
|
|
["pilot.target-nearest", "pilot.target-previous", "pilot.target-next", "pilot.target-reticle", "pilot.target-clear"]),
|
|
"portable input must advertise every daemon target-selection lever");
|
|
Require(targeting.Where(value => value.ActionId != "pilot.target-clear")
|
|
.All(value => value.Availability == "available" &&
|
|
value.Payload["visibleHostileCount"] == "3" &&
|
|
value.Payload["currentTargetEntityIndex"] == "-1") &&
|
|
targeting.Single(value => value.ActionId == "pilot.target-clear").Availability == "unavailable",
|
|
"target controls must derive availability and current selection from daemon contact truth");
|
|
|
|
var keyboard = capability.DefaultProfiles.Single(value => value.DeviceClass == "keyboard-mouse");
|
|
string Control(string actionId) => keyboard.Bindings.Single(value => value.ActionId == actionId).Gesture.Controls.Single();
|
|
RequireEqual("keyboard.f", Control("pilot.interact"),
|
|
"the generic default profile must preserve the fossil interaction key");
|
|
RequireEqual("keyboard.t", Control("pilot.target-nearest"),
|
|
"the generic default profile must preserve the fossil nearest-target key");
|
|
RequireEqual("keyboard.y", Control("pilot.target-previous"),
|
|
"the generic default profile must preserve the fossil previous-target key");
|
|
RequireEqual("keyboard.u", Control("pilot.target-next"),
|
|
"the generic default profile must preserve the fossil next-target key");
|
|
RequireEqual("keyboard.r", Control("pilot.target-reticle"),
|
|
"the generic default profile must preserve the fossil reticle-target key");
|
|
Require(keyboard.Bindings.Where(value => value.ActionId == "pilot.interact" ||
|
|
value.ActionId.StartsWith("pilot.target-", StringComparison.Ordinal))
|
|
.All(value => !value.ActionBar),
|
|
"ordinary fossil shortcuts must not masquerade as action-bar slot suggestions");
|
|
var reticleAction = capability.ToEveDocument().Actions.Single(value => value.ActionId == "pilot.target-reticle");
|
|
RequireEqual("view-direction.v1", reticleAction.InputValue?.Model ?? "",
|
|
"reticle targeting must advertise the generic active-view direction model");
|
|
Require((reticleAction.InputValue?.PayloadKeys ?? Array.Empty<string>()).SequenceEqual(
|
|
["directionX", "directionY", "directionZ"]),
|
|
"reticle targeting must preserve the portable world-direction payload order");
|
|
|
|
AetheriaRuntimeDaemonCommandDocument Translate(AetheriaRuntimeInputActionDocument action)
|
|
{
|
|
var request = new EveSurfaceCommandRequest(
|
|
capability.ProviderId,
|
|
AetheriaRuntimeDaemonGameSurfaceBuilder.PilotSurfaceId,
|
|
CultMesh.OperationInvocation(action.Operation),
|
|
CultMesh.OperationPayload(("entityId", run.CurrentEntityKey)),
|
|
DateTimeOffset.UtcNow,
|
|
"native-input-smoke");
|
|
Require(AetheriaRuntimeDaemonOperationsClient.TryCreateSurfaceCommandDocument(
|
|
request, frame, ".", "native-input-smoke", "native-input", out var command),
|
|
$"advertised action {action.ActionId} must translate through the generic Eve command path");
|
|
return command!;
|
|
}
|
|
|
|
var previousCommand = Translate(targeting.Single(value => value.ActionId == "pilot.target-previous"));
|
|
var previousResult = AetheriaRuntimeDaemonOperations.Execute(
|
|
run, [previousCommand], new AetheriaRuntimeDaemonOperationContext());
|
|
RequireEqual(1, previousResult.AppliedCommandIds.Count,
|
|
"the advertised previous-target action must reach the daemon owner");
|
|
RequireEqual(middle.EntityIndex, actor.TargetEntityIndex,
|
|
"previous from no selection must preserve the fossil's second-to-last index arithmetic");
|
|
|
|
var clearCommand = Translate(targeting.Single(value => value.ActionId == "pilot.target-clear"));
|
|
var clearResult = AetheriaRuntimeDaemonOperations.Execute(
|
|
run, [clearCommand], new AetheriaRuntimeDaemonOperationContext());
|
|
RequireEqual(1, clearResult.AppliedCommandIds.Count,
|
|
"the generic clear-target action must return selection ownership to the daemon");
|
|
|
|
var nearestCommand = Translate(targeting.Single(value => value.ActionId == "pilot.target-nearest"));
|
|
RequireEqual(AetheriaRuntimeDaemonCommandKinds.TargetNearest, nearestCommand.Kind,
|
|
"nearest target action must preserve daemon command identity");
|
|
var result = AetheriaRuntimeDaemonOperations.Execute(
|
|
run, [nearestCommand], new AetheriaRuntimeDaemonOperationContext());
|
|
RequireEqual(1, result.AppliedCommandIds.Count,
|
|
"the advertised target action must reach the daemon owner");
|
|
RequireEqual(far.EntityIndex, actor.TargetEntityIndex,
|
|
"the historically misnamed nearest action must preserve the fossil's MaxBy-distance behavior");
|
|
var selected = AetheriaRuntimeInputCapabilityDocument.FromFrame(frame);
|
|
Require(selected.Actions.Single(value => value.ActionId == "pilot.target-clear").Availability == "available" &&
|
|
selected.Actions.Where(value => value.Category == "targeting")
|
|
.All(value => value.Payload["currentTargetEntityIndex"] == far.EntityIndex.ToString()),
|
|
"all portable target levers must refresh from the newly selected daemon target");
|
|
|
|
var clearAfterNearest = AetheriaRuntimeDaemonOperations.Execute(
|
|
run, [Translate(selected.Actions.Single(value => value.ActionId == "pilot.target-clear"))],
|
|
new AetheriaRuntimeDaemonOperationContext());
|
|
RequireEqual(1, clearAfterNearest.AppliedCommandIds.Count,
|
|
"generic Eve must clear the fossil-selected target before the independent reticle selection proof");
|
|
|
|
var reticleRequest = new EveSurfaceCommandRequest(
|
|
capability.ProviderId,
|
|
AetheriaRuntimeDaemonGameSurfaceBuilder.PilotSurfaceId,
|
|
CultMesh.OperationInvocation(reticleAction.Operation),
|
|
CultMesh.OperationPayload(
|
|
("entityId", run.CurrentEntityKey),
|
|
("directionX", "0"),
|
|
("directionY", "0"),
|
|
("directionZ", "1")),
|
|
DateTimeOffset.UtcNow,
|
|
"native-input-smoke");
|
|
Require(AetheriaRuntimeDaemonOperationsClient.TryCreateSurfaceCommandDocument(
|
|
reticleRequest, frame, ".", "native-input-smoke", "native-input", out var reticleCommand) &&
|
|
reticleCommand?.Kind == AetheriaRuntimeDaemonCommandKinds.TargetReticle,
|
|
"the generic Eve view-direction action must translate into the typed reticle command");
|
|
var reticleResult = AetheriaRuntimeDaemonOperations.Execute(
|
|
run, [reticleCommand!], new AetheriaRuntimeDaemonOperationContext());
|
|
RequireEqual(1, reticleResult.AppliedCommandIds.Count,
|
|
"the translated reticle action must reach the daemon target owner");
|
|
RequireEqual(far.EntityIndex, actor.TargetEntityIndex,
|
|
"reticle selection must rank visible hostile contacts from the advertised view ray inside the daemon");
|
|
|
|
EveSurfaceCommandRequest ExplicitTarget(string targetEntityKey) => new(
|
|
capability.ProviderId,
|
|
AetheriaRuntimeDaemonGameSurfaceBuilder.PilotSurfaceId,
|
|
CultMesh.OperationInvocation(AetheriaRuntimeDaemonSurfaceCommandCatalog.CommandName(
|
|
AetheriaRuntimeDaemonCommandKinds.SetTarget)),
|
|
CultMesh.OperationPayload(
|
|
("sourceEntityId", run.CurrentEntityKey),
|
|
("targetEntityId", targetEntityKey)),
|
|
DateTimeOffset.UtcNow,
|
|
"native-input-smoke");
|
|
|
|
Require(AetheriaRuntimeDaemonOperationsClient.TryCreateSurfaceCommandDocument(
|
|
ExplicitTarget(run.EntityRecordKey(0, near.EntityIndex)),
|
|
frame,
|
|
".",
|
|
"native-input-smoke",
|
|
"native-input",
|
|
out var explicitTarget) &&
|
|
explicitTarget?.ActorEntityKey == run.CurrentEntityKey &&
|
|
explicitTarget.TargetEntityKey == run.EntityRecordKey(0, near.EntityIndex),
|
|
"generic Eve explicit targeting must preserve both source and target entity identities");
|
|
var explicitResult = AetheriaRuntimeDaemonOperations.Execute(
|
|
run, [explicitTarget!], new AetheriaRuntimeDaemonOperationContext());
|
|
Require(explicitResult.AppliedCommandIds.Contains(explicitTarget!.CommandId) &&
|
|
actor.TargetEntityIndex == near.EntityIndex,
|
|
"a generic scene click must select the visible entity through daemon contact truth");
|
|
|
|
Require(AetheriaRuntimeDaemonOperationsClient.TryCreateSurfaceCommandDocument(
|
|
ExplicitTarget(run.EntityRecordKey(0, hidden.EntityIndex)),
|
|
frame,
|
|
".",
|
|
"native-input-smoke",
|
|
"native-input",
|
|
out var hiddenTarget),
|
|
"generic Eve must translate an explicit target attempt without deciding visibility client-side");
|
|
var hiddenResult = AetheriaRuntimeDaemonOperations.Execute(
|
|
run, [hiddenTarget!], new AetheriaRuntimeDaemonOperationContext());
|
|
RequireEqual(AetheriaRuntimeDaemonRejectionReasons.TargetNotVisible,
|
|
hiddenResult.RejectedCommandReasons[hiddenTarget!.CommandId],
|
|
"the daemon must reject a generic explicit target absent from authoritative visible contacts");
|
|
RequireEqual(near.EntityIndex, actor.TargetEntityIndex,
|
|
"a rejected explicit target must not disturb the prior valid selection");
|
|
var rejectedFrame = AetheriaRuntimeDaemonFrameDocument.Create(
|
|
run, "smoke", "native-input", 2, 0.2, 0.1);
|
|
rejectedFrame.RejectedCommandIds = hiddenResult.RejectedCommandIds;
|
|
rejectedFrame.RejectedCommandReasons = hiddenResult.RejectedCommandReasons;
|
|
var targetReceipt = AetheriaRuntimeDaemonReceiptProjector.Project(
|
|
AetheriaRuntimeCommittedCommandFactDocument.FromRejectedCommand(
|
|
rejectedFrame, hiddenTarget!, "aetheria.local"),
|
|
AetheriaRuntimeDaemonGameSurfaceBuilder.PilotSurfaceId);
|
|
Require(targetReceipt.State == "denied" &&
|
|
targetReceipt.Message.Contains(
|
|
AetheriaRuntimeDaemonRejectionReasons.TargetNotVisible,
|
|
StringComparison.Ordinal),
|
|
"generic Eve must receive the daemon's exact explicit-target rejection reason");
|
|
|
|
var interact = capability.Actions.Single(value => value.ActionId == "pilot.interact");
|
|
RequireEqual(AetheriaRuntimeDaemonCommandKinds.Interact, Translate(interact).Kind,
|
|
"the fossil interaction binding must reach the shared wormhole-first/then-dock daemon command");
|
|
}
|
|
|
|
private static void DockedRefitSurfacePublishesGenericCommands()
|
|
{
|
|
var hull = HullCatalogItem("surface-hull", 9, 7, 100);
|
|
var cargoBay = CatalogItem("surface-cargo-bay");
|
|
cargoBay.InteriorShapeWidth = 6;
|
|
cargoBay.InteriorShapeHeight = 4;
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([hull, cargoBay, CatalogItem("ore")], [], []);
|
|
var ship = Entity(0, 0, "player");
|
|
ship.Name = "Pilot";
|
|
ship.HullItemKey = hull.ItemKey;
|
|
ship.CargoBays = [ItemSlot(cargoBay.ItemKey)];
|
|
ship.CargoContents = [new AetheriaRuntimeCargoBayLoadoutCommit { Items = [ItemSlot("ore", 2, 3)] }];
|
|
var station = Entity(1, 0, "player");
|
|
station.Kind = "station";
|
|
station.Name = "Station";
|
|
station.DockingBayAssignments = [ship.EntityIndex];
|
|
station.CargoBays = [ItemSlot(cargoBay.ItemKey)];
|
|
station.CargoContents = [new AetheriaRuntimeCargoBayLoadoutCommit()];
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "generic-refit-surface-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [ship, station] }]
|
|
};
|
|
var frame = AetheriaRuntimeDaemonFrameDocument.Create(run, "smoke", "surface", 1, 0.02, 0.02);
|
|
var shipKey = run.EntityRecordKey(0, ship.EntityIndex);
|
|
var stationKey = run.EntityRecordKey(0, station.EntityIndex);
|
|
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
frame,
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("smoke"),
|
|
catalog: catalog);
|
|
var equipment = Find(surface.Surface.Root, "aetheria.daemon.game.refit.ship.equipment");
|
|
var shipCargo = Find(surface.Surface.Root, "aetheria.daemon.game.refit.ship.cargo.0");
|
|
var stationCargo = Find(surface.Surface.Root, "aetheria.daemon.game.refit.station.cargo.0");
|
|
Require(equipment != null && equipment.Kind == "inventory.grid" &&
|
|
equipment.Props["columns"] == "9" && equipment.Props["rows"] == "7",
|
|
"docked pilot surface must publish authored ship equipment geometry");
|
|
Require(shipCargo != null && stationCargo != null &&
|
|
shipCargo.Props["targetEntityKey"] == shipKey &&
|
|
stationCargo.Props["targetEntityKey"] == stationKey,
|
|
"docked pilot surface must publish both ship and station cargo targets");
|
|
Require(surface.Commands.Any(value => value.Command ==
|
|
AetheriaRuntimeDaemonSurfaceCommandCatalog.CommandName(AetheriaRuntimeDaemonCommandKinds.TransferCargoItem)) &&
|
|
surface.Commands.Any(value => value.Command ==
|
|
AetheriaRuntimeDaemonSurfaceCommandCatalog.CommandName(AetheriaRuntimeDaemonCommandKinds.EquipItem)) &&
|
|
surface.Commands.Any(value => value.Command ==
|
|
AetheriaRuntimeDaemonSurfaceCommandCatalog.CommandName(AetheriaRuntimeDaemonCommandKinds.StoreItem)),
|
|
"docked pilot surface must advertise all atomic refit operations");
|
|
|
|
var request = new EveSurfaceCommandRequest(
|
|
"aetheria",
|
|
AetheriaRuntimeDaemonGameSurfaceBuilder.PilotSurfaceId,
|
|
CultMesh.OperationInvocation(
|
|
AetheriaRuntimeDaemonSurfaceCommandCatalog.CommandName(AetheriaRuntimeDaemonCommandKinds.TransferCargoItem)),
|
|
CultMesh.OperationPayload(
|
|
("entityId", shipKey),
|
|
("originEntityKey", shipKey),
|
|
("originCargoIndex", "0"),
|
|
("destinationEntityKey", stationKey),
|
|
("destinationCargoIndex", "0"),
|
|
("itemKey", "ore"),
|
|
("quantity", "1"),
|
|
("sourceX", "2"),
|
|
("sourceY", "3"),
|
|
("destinationX", "0"),
|
|
("destinationY", "0"),
|
|
("hasDestinationPosition", "true")),
|
|
DateTimeOffset.UtcNow,
|
|
"generic-smoke");
|
|
Require(AetheriaRuntimeDaemonOperationsClient.TryCreateSurfaceCommandDocument(
|
|
request, frame, ".", "generic-smoke", "surface", out var translated) &&
|
|
translated?.CargoTransfer.OriginCargoIndex == 0 &&
|
|
translated.CargoTransfer.DestinationEntityKey == stationKey &&
|
|
translated.CargoTransfer.HasDestinationPosition,
|
|
"generic Eve inventory drop must translate into the exact typed daemon cargo command");
|
|
|
|
var unauthorized = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.TransferCargoItem,
|
|
"generic-smoke", "surface", 1, shipKey);
|
|
unauthorized.TextValue = "ore";
|
|
unauthorized.CargoTransfer.OriginEntityKey = stationKey;
|
|
unauthorized.CargoTransfer.DestinationEntityKey = stationKey;
|
|
unauthorized.CargoTransfer.OriginCargoIndex = 0;
|
|
unauthorized.CargoTransfer.DestinationCargoIndex = 0;
|
|
var rejected = AetheriaRuntimeDaemonOperations.Execute(run, [unauthorized],
|
|
new AetheriaRuntimeDaemonOperationContext { Catalog = catalog });
|
|
Require(rejected.RejectedCommandReasons[unauthorized.CommandId] ==
|
|
AetheriaRuntimeDaemonRejectionReasons.CargoAccessDenied,
|
|
"cargo transfer actor must own one endpoint even when the target is physically nearby");
|
|
|
|
station.DockingBayAssignments = [];
|
|
var undocked = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
frame,
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("smoke"),
|
|
catalog: catalog);
|
|
Require(Find(undocked.Surface.Root, "aetheria.daemon.game.refit") == null,
|
|
"undocked pilot surfaces must not expose station refit controls");
|
|
|
|
static AetheriaRuntimeSurfaceComponent? Find(AetheriaRuntimeSurfaceComponent component, string id) =>
|
|
component.Id == id
|
|
? component
|
|
: component.Children.Select(child => Find(child, id)).FirstOrDefault(value => value != null);
|
|
|
|
static AetheriaRuntimeLoadoutItemSlotCommit ItemSlot(string itemKey, int x = 0, int y = 0) => new()
|
|
{
|
|
X = x,
|
|
Y = y,
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{
|
|
ItemKey = itemKey,
|
|
Quantity = 1,
|
|
Quality = 1,
|
|
Durability = 1,
|
|
Enabled = true
|
|
}
|
|
};
|
|
}
|
|
|
|
private static void FossilVelocityBehaviorsRunInDaemonFlightStep()
|
|
{
|
|
var conversion = new AetheriaRuntimeBehaviorPayload(
|
|
0,
|
|
AetheriaRuntimeBehaviorKinds.VelocityConversion,
|
|
0,
|
|
[new AetheriaRuntimeBehaviorField(1, PerformanceStat(Math.Log(2)))]);
|
|
var limit = new AetheriaRuntimeBehaviorPayload(
|
|
1,
|
|
AetheriaRuntimeBehaviorKinds.VelocityLimit,
|
|
0,
|
|
[new AetheriaRuntimeBehaviorField(1, PerformanceStat(4))]);
|
|
var hull = HullCatalogItem("velocity-hull", 1, 1, 100);
|
|
var controller = CatalogItem("velocity-controller", conversion, limit);
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([hull, controller], [], []);
|
|
var ship = Entity(0, 0, "player");
|
|
ship.HullItemKey = hull.ItemKey;
|
|
ship.DirectionX = 1;
|
|
ship.DirectionY = 0;
|
|
ship.VelocityX = 0;
|
|
ship.VelocityY = 10;
|
|
ship.Equipment =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{
|
|
ItemKey = controller.ItemKey,
|
|
Quantity = 1,
|
|
Quality = 1,
|
|
Durability = 1,
|
|
Enabled = true
|
|
}
|
|
}
|
|
];
|
|
|
|
AetheriaRuntimeBehaviorStateProjector.EnsureEquipmentBehaviorStates(ship, catalog);
|
|
AetheriaRuntimeFlightSimulation.Step([ship], [], catalog, 1, 1, 1, 0, 1);
|
|
|
|
RequireNear(4, Math.Sqrt(ship.VelocityX * ship.VelocityX + ship.VelocityY * ship.VelocityY), 0.000001,
|
|
"VelocityLimit must clamp the post-conversion speed in the authoritative flight step");
|
|
RequireNear(ship.VelocityX, ship.VelocityY, 0.000001,
|
|
"VelocityConversion must exponentially align velocity toward the ship direction before limiting");
|
|
var limitState = ship.BehaviorStates.Single(state =>
|
|
state.BehaviorKind == AetheriaRuntimeBehaviorKinds.VelocityLimit);
|
|
RequireNear(4, limitState.VelocityLimit, 0.000001,
|
|
"VelocityLimit must publish its evaluated runtime value for Eve and reconnect");
|
|
}
|
|
|
|
private static void ReflectorUsesSharedStellarLightFieldWithoutAccumulating()
|
|
{
|
|
var reflector = new AetheriaRuntimeBehaviorPayload(
|
|
0,
|
|
AetheriaRuntimeBehaviorKinds.Reflector,
|
|
0,
|
|
[new AetheriaRuntimeBehaviorField(1, PerformanceStat(2))]);
|
|
var constantVisibility = new AetheriaRuntimeBehaviorPayload(
|
|
1,
|
|
AetheriaRuntimeBehaviorKinds.Visibility,
|
|
0,
|
|
[new AetheriaRuntimeBehaviorField(1, PerformanceStat(5))]);
|
|
var controller = CatalogItem("reflector-controller", reflector, constantVisibility);
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([controller], [], []);
|
|
var ship = Entity(0, 250, "player");
|
|
ship.Visibility = 3;
|
|
ship.Equipment =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{
|
|
ItemKey = controller.ItemKey,
|
|
Quantity = 1,
|
|
Quality = 1,
|
|
Durability = 1,
|
|
Enabled = true
|
|
}
|
|
}
|
|
];
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [ship],
|
|
Orbits =
|
|
[
|
|
new AetheriaRuntimeOrbitSnapshotCommit
|
|
{
|
|
OrbitKey = "sun-orbit",
|
|
FixedPositionX = 100
|
|
}
|
|
],
|
|
Bodies =
|
|
[
|
|
new AetheriaRuntimeBodySnapshotCommit
|
|
{
|
|
Kind = "sun",
|
|
Mass = 16,
|
|
OrbitKey = "sun-orbit",
|
|
SunVisual = new AetheriaRuntimeSunVisualCommit { LightRadiusMultiplier = 1 }
|
|
}
|
|
]
|
|
};
|
|
var settings = AetheriaRuntimeDaemonRenderSettings.AetheriaDefault;
|
|
var radius = settings.ResolveLightRadius(16);
|
|
var pulseX = Math.Min(1, 150 / radius * 2);
|
|
var expectedReflection = 2 * Math.Pow((pulseX + 1) * (1 - pulseX), 8);
|
|
|
|
AetheriaRuntimeVisibilitySimulation.StepZone(zone, [ship], catalog, settings);
|
|
AetheriaRuntimeVisibilitySimulation.StepZone(zone, [ship], catalog, settings);
|
|
|
|
RequireNear(3 + 5 + expectedReflection, ship.Visibility, 0.000001,
|
|
"Visibility and Reflector must publish their fossil sources without accumulating per tick");
|
|
var equipmentSource = ship.StatGrids.Single(grid => grid.Name == "equipment-visibility");
|
|
RequireNear(5, equipmentSource.Values.Single(), 0.000001,
|
|
"Operational Visibility equipment must publish its evaluated signature as an inspectable source");
|
|
var source = ship.StatGrids.Single(grid => grid.Name == "reflector-visibility");
|
|
RequireNear(expectedReflection, source.Values.Single(), 0.000001,
|
|
"Reflector visibility must remain an inspectable daemon-owned source for Eve and reconnect");
|
|
}
|
|
|
|
private static void SensorPingIsActorScopedEnergyGatedAndReconnectable()
|
|
{
|
|
var sensorItem = CatalogItem("sensor-ping-array", SensorPayload(
|
|
sensitivity: 0.01,
|
|
pingBoost: 1,
|
|
pingEnergy: 5,
|
|
pingVisibility: 4,
|
|
pingRange: 200,
|
|
pingCooldown: 1,
|
|
pingDuration: 1,
|
|
pingRadiusExponent: 1));
|
|
var capacitorItem = CatalogItem("sensor-ping-capacitor", CapacitorPayload(10, 1));
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([sensorItem, capacitorItem], [], []);
|
|
|
|
static AetheriaRuntimeEntitySnapshotCommit Observer(int index, string sensorKey, string capacitorKey)
|
|
{
|
|
var entity = Entity(index, 0, "player");
|
|
entity.Equipment =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = sensorKey, Quality = 1, Durability = 1, Enabled = true } },
|
|
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = capacitorKey, Quality = 1, Durability = 1, Enabled = true } }
|
|
];
|
|
entity.BehaviorStates =
|
|
[
|
|
new AetheriaRuntimeBehaviorStateCommit
|
|
{
|
|
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind,
|
|
OwnerIndex = 1,
|
|
BehaviorIndex = 0,
|
|
BehaviorKind = "Capacitor",
|
|
CapacitorCharge = 10,
|
|
CapacitorCapacity = 10,
|
|
CapacitorEfficiency = 1
|
|
}
|
|
];
|
|
return entity;
|
|
}
|
|
|
|
var actor = Observer(0, sensorItem.ItemKey, capacitorItem.ItemKey);
|
|
var other = Observer(1, sensorItem.ItemKey, capacitorItem.ItemKey);
|
|
var target = Entity(2, 40, "raider");
|
|
target.Visibility = 1;
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [actor, other, target] };
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "sensor-ping-smoke",
|
|
Zones = [zone]
|
|
};
|
|
|
|
AetheriaRuntimeSensorSimulation.StepZone(
|
|
run, zone, [actor, other, target], [], catalog, 0.25, 0.5, 0.1, 1);
|
|
AetheriaRuntimeSensorSimulation.StepZone(
|
|
run,
|
|
zone,
|
|
[actor, other, target],
|
|
[new AetheriaRuntimeDaemonSensorPingIntent { ActorEntityKey = "zone.0.entity.0" }],
|
|
catalog,
|
|
0.25,
|
|
0.5,
|
|
0.1,
|
|
2);
|
|
|
|
var sensor = actor.BehaviorStates.Single(state => state.BehaviorKind == AetheriaRuntimeBehaviorKinds.Sensor);
|
|
var otherSensor = other.BehaviorStates.Single(state => state.BehaviorKind == AetheriaRuntimeBehaviorKinds.Sensor);
|
|
Require(sensor.Pinging && !otherSensor.Pinging,
|
|
"sensor ping intent must retain actor identity instead of activating every sensor");
|
|
RequireEqual(2, sensor.PingedEntityCount,
|
|
"expanding ping must persist every crossed target index for reconnect-safe exactly-once boost");
|
|
Require(sensor.PingedEntityIndices.SequenceEqual(new[] { 1, 2 }),
|
|
"pinged target indices must survive in stable order rather than only an unverifiable count");
|
|
RequireNear(5, actor.BehaviorStates.Single(state => state.BehaviorKind == "Capacitor").CapacitorCharge,
|
|
0.000001, "accepted ping must consume its authored energy exactly once");
|
|
Require(actor.Contacts.Single(contact => contact.TargetEntityIndex == 2).Visible,
|
|
"ping boost must cross the daemon-owned detection threshold");
|
|
RequireNear(4, actor.StatGrids.Single(grid => grid.Name == "transient-visibility:sensor:0:0").Values.Single(),
|
|
0.000001, "ping must publish its authored transient visibility source");
|
|
RequireEqual(1, run.GameEvents.Count(value => value.Kind == "sensor.ping.started"),
|
|
"accepted ping must emit one provider-owned start event");
|
|
}
|
|
|
|
private static void TurretControllerAcquiresAimsAndTriggersExactWeapons()
|
|
{
|
|
var weaponPayload = new AetheriaRuntimeBehaviorPayload(
|
|
0,
|
|
AetheriaRuntimeBehaviorKinds.InstantWeapon,
|
|
0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(10)),
|
|
new AetheriaRuntimeBehaviorField(5, PerformanceStat(0)),
|
|
new AetheriaRuntimeBehaviorField(6, PerformanceStat(200)),
|
|
new AetheriaRuntimeBehaviorField(11, PerformanceStat(7)),
|
|
new AetheriaRuntimeBehaviorField(16, PerformanceStat(100)),
|
|
new AetheriaRuntimeBehaviorField(17, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(19, PerformanceStat(0.1)),
|
|
new AetheriaRuntimeBehaviorField(21, PerformanceStat(100)),
|
|
new AetheriaRuntimeBehaviorField(22, PerformanceStat(0)),
|
|
new AetheriaRuntimeBehaviorField(23, PerformanceStat(180)),
|
|
new AetheriaRuntimeBehaviorField(24, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(25, PerformanceStat(1))
|
|
]);
|
|
var weaponItem = WearableWeaponCatalogItem("turret-smoke-weapon", weaponPayload);
|
|
var controllerItem = CatalogItem(
|
|
"turret-smoke-controller",
|
|
new AetheriaRuntimeBehaviorPayload(0, "EnergyDraw", 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, PerformanceStat(5)),
|
|
new AetheriaRuntimeBehaviorField(2, BoolValue(false))
|
|
]),
|
|
new AetheriaRuntimeBehaviorPayload(1, AetheriaRuntimeBehaviorKinds.TurretController, 0, []));
|
|
var capacitorItem = CatalogItem("turret-smoke-capacitor", CapacitorPayload(20, 1));
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([controllerItem, weaponItem, capacitorItem], [], []);
|
|
|
|
var uncontrolled = Entity(0, 0, "raider");
|
|
uncontrolled.DirectionX = 1;
|
|
uncontrolled.WeaponGroups = [new[] { 0 }];
|
|
uncontrolled.TargetEntityIndex = 1;
|
|
uncontrolled.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = weaponItem.ItemKey, Quality = 0, Durability = 1, Enabled = true }
|
|
}];
|
|
uncontrolled.Contacts = [new AetheriaRuntimeEntityContactCommit
|
|
{ TargetEntityIndex = 1, InfoGathered = 1, Visible = true, Hostile = true }];
|
|
var uncontrolledTarget = Entity(1, 100, "player");
|
|
var uncontrolledZone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{ ZoneIndex = 0, Entities = [uncontrolled, uncontrolledTarget] };
|
|
var uncontrolledRun = new AetheriaRuntimeRunCheckpointCommit
|
|
{ RunId = "uncontrolled-hostile-smoke", Zones = [uncontrolledZone] };
|
|
using (var physics = NewPhysics())
|
|
{
|
|
for (var frame = 0; frame < 3; frame++)
|
|
AetheriaRuntimeDaemonSimulation.Step(
|
|
uncontrolledRun,
|
|
new AetheriaRuntimeDaemonIntentState(),
|
|
0.1,
|
|
new AetheriaRuntimeDaemonSimulationSettings(),
|
|
physics,
|
|
catalog,
|
|
frame,
|
|
frame * 0.1);
|
|
}
|
|
RequireEqual(0, uncontrolledRun.ShotReceipts.Count,
|
|
"non-player entities without a controller or command must not inherit the removed group-zero fire authority");
|
|
|
|
var turret = Entity(0, 0, "player");
|
|
turret.Kind = "station";
|
|
turret.DirectionX = 1;
|
|
turret.DirectionY = 0;
|
|
turret.Equipment =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = controllerItem.ItemKey, Quality = 1, Durability = 1, Enabled = true }
|
|
},
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Rotation = "None",
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = weaponItem.ItemKey, Quality = 0, Durability = 1, Enabled = true }
|
|
},
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Rotation = "Half",
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = weaponItem.ItemKey, Quality = 0, Durability = 1, Enabled = true }
|
|
},
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = capacitorItem.ItemKey, Quality = 1, Durability = 1, Enabled = true }
|
|
}
|
|
];
|
|
turret.WeaponGroups = [new[] { 1, 2 }];
|
|
turret.Contacts = [new AetheriaRuntimeEntityContactCommit
|
|
{ TargetEntityIndex = 1, InfoGathered = 1, Visible = true, Hostile = true }];
|
|
var target = Entity(1, 100, "raider");
|
|
target.VelocityY = 5;
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [turret, target] };
|
|
var run = new AetheriaRuntimeRunCheckpointCommit { RunId = "turret-controller-smoke", Zones = [zone] };
|
|
using (var physics = NewPhysics())
|
|
{
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
new AetheriaRuntimeDaemonSimulationSettings(), physics, catalog, 0, 0);
|
|
RequireEqual(-1, turret.TargetEntityIndex,
|
|
"an unfunded controller must remain unreachable behind its authored EnergyDraw gate");
|
|
RequireEqual(0, run.ShotReceipts.Count,
|
|
"an unfunded controller must not aim or trigger weapons behind the behavior chain");
|
|
|
|
var capacitor = turret.BehaviorStates.Single(state => state.BehaviorKind == "Capacitor");
|
|
capacitor.CapacitorCharge = 20;
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
new AetheriaRuntimeDaemonSimulationSettings(), physics, catalog, 1, 0.1);
|
|
RequireEqual(target.EntityIndex, turret.TargetEntityIndex,
|
|
"an idle turret controller must acquire the first visible hostile ship without firing on the acquisition tick");
|
|
RequireEqual(0, run.ShotReceipts.Count,
|
|
"target acquisition must not smuggle an extra trigger into the same fossil controller tick");
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
new AetheriaRuntimeDaemonSimulationSettings(), physics, catalog, 2, 0.2);
|
|
}
|
|
|
|
var controllerState = turret.BehaviorStates.Single(state =>
|
|
state.BehaviorKind == AetheriaRuntimeBehaviorKinds.TurretController);
|
|
RequireEqual(2, controllerState.TurretControllerWeaponCount,
|
|
"turret controller initialization must publish its exact operational weapon count");
|
|
Require(controllerState.TurretControllerPredictShots &&
|
|
Math.Abs(controllerState.TurretControllerShotSpeed - 100) < 0.000001 &&
|
|
turret.LookDirectionY > 0,
|
|
"projectile velocity must enable the fossil first-order intercept aim and persist its controller state");
|
|
Require(run.ShotReceipts.Count > 0 && run.ShotReceipts.All(receipt => receipt.WeaponOwnerIndex == 1),
|
|
"turret controller must trigger only the exact in-range aligned weapon behavior, not its group or reversed neighbor");
|
|
RequireNear(7, turret.StatGrids.Single(grid =>
|
|
grid.Name == "transient-visibility:weapon:equipment:1:0").Values.Single(), 0.000001,
|
|
"turret-triggered fire must publish the selected weapon's authored visibility through the shared shot transaction");
|
|
Require(turret.Equipment[1].Item!.Durability < 1 &&
|
|
Math.Abs(turret.Equipment[2].Item!.Durability - 1) < 0.000001,
|
|
"turret-triggered fire must wear only the exact weapon that committed a shot");
|
|
}
|
|
|
|
private static void DockingUsesRealBaysAndFossilUndockRules()
|
|
{
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
|
[
|
|
CatalogItem("dock-cockpit", new AetheriaRuntimeBehaviorPayload(0, "Cockpit", 0, [])),
|
|
CatalogItem("dock-thruster", new AetheriaRuntimeBehaviorPayload(0, "Thruster", 0, [])),
|
|
CatalogItem("dock-drive", new AetheriaRuntimeBehaviorPayload(0, "AetherDrive", 0, [])),
|
|
CatalogItem("dock-reactor", new AetheriaRuntimeBehaviorPayload(0, "Reactor", 0, []))
|
|
], [], []);
|
|
var context = new AetheriaRuntimeDaemonOperationContext { Catalog = catalog };
|
|
|
|
var physicsShip = Entity(0, 24, "player");
|
|
var physicsStation = Entity(1, 0, "player");
|
|
physicsStation.Kind = "station";
|
|
physicsStation.DockingBays =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "dock-bay", Quantity = 1 }
|
|
}
|
|
];
|
|
physicsStation.DockingBayAssignments = [-1];
|
|
physicsStation.DockingBayContents = [Cargo()];
|
|
var physicsZone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [physicsShip, physicsStation]
|
|
};
|
|
var physicsStep = NewPhysics().Step(
|
|
"dock-collision-exclusion-smoke",
|
|
1,
|
|
0,
|
|
physicsZone,
|
|
physicsZone.Entities,
|
|
0.1);
|
|
var steppedShip = physicsStep.Bodies.Single(body => body.EntityIndex == physicsShip.EntityIndex);
|
|
RequireNear(24, steppedShip.PositionX, 0.000001,
|
|
"Ymir must not invent ship/station collision separation that pushes the fossil docking radius out of reach");
|
|
physicsShip.PositionX = steppedShip.PositionX;
|
|
physicsShip.PositionZ = steppedShip.PositionZ;
|
|
var physicsRun = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "dock-collision-exclusion-smoke",
|
|
CurrentZoneIndex = 0,
|
|
Zones = [physicsZone]
|
|
};
|
|
physicsRun.CurrentEntityKey = physicsRun.EntityRecordKey(0, physicsShip.EntityIndex);
|
|
var physicsDock = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.DockNearest,
|
|
"pilot",
|
|
physicsRun.RunId,
|
|
1,
|
|
physicsRun.CurrentEntityKey);
|
|
var physicsDockResult = AetheriaRuntimeDaemonOperations.Execute(physicsRun, [physicsDock], context);
|
|
Require(physicsDockResult.AppliedCommandIds.Contains(physicsDock.CommandId, StringComparer.Ordinal) &&
|
|
physicsStation.DockingBayAssignments.Single() == physicsShip.EntityIndex,
|
|
"the daemon must still accept docking after Ymir advances the overlapping ship/station bodies: " +
|
|
string.Join(",", physicsDockResult.RejectedCommandReasons.Values));
|
|
|
|
static AetheriaRuntimeEntitySnapshotCommit DockTarget(int index, double x, bool hasBay)
|
|
{
|
|
var target = Entity(index, x, "station");
|
|
target.PositionZ = 0;
|
|
if (hasBay)
|
|
{
|
|
target.DockingBays = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "dock-bay" }
|
|
}];
|
|
target.DockingBayAssignments = [-1];
|
|
target.DockingBayContents = [Cargo()];
|
|
}
|
|
return target;
|
|
}
|
|
|
|
var actor = Entity(0, 0, "player");
|
|
actor.WeaponGroups = [new[] { 0 }];
|
|
actor.ActiveWeaponGroups = [true];
|
|
actor.WeaponStates = [new AetheriaRuntimeWeaponStateCommit
|
|
{
|
|
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind,
|
|
OwnerIndex = 0,
|
|
BehaviorIndex = 0,
|
|
BehaviorKind = AetheriaRuntimeBehaviorKinds.ChargedWeapon,
|
|
Firing = true,
|
|
TriggerPending = true,
|
|
BurstRemaining = 2,
|
|
Charging = true,
|
|
Charged = true,
|
|
Charge = 1,
|
|
ChargeHoldSeconds = 3,
|
|
ChargeRiskChecks = 2,
|
|
ChargeMalfunctionRisk = 0.5,
|
|
LockTargetEntityIndex = 4,
|
|
LockProgress = 1
|
|
}];
|
|
var ineligible = DockTarget(1, 5, false);
|
|
var outOfRange = DockTarget(2, 30, true);
|
|
var firstEligible = DockTarget(3, 20, true);
|
|
var nearerButLater = DockTarget(4, 10, true);
|
|
var selectionRun = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "dock-selection-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [actor, ineligible, outOfRange, firstEligible, nearerButLater]
|
|
}]
|
|
};
|
|
var dock = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.DockNearest,
|
|
"pilot",
|
|
"dock-selection-smoke",
|
|
0,
|
|
selectionRun.CurrentEntityKey);
|
|
dock.ScalarValue = 100;
|
|
var dockResult = AetheriaRuntimeDaemonOperations.Execute(selectionRun, [dock], context);
|
|
RequireEqual(1, dockResult.AppliedCommandIds.Count,
|
|
"DockNearest must accept the first entity with a real empty bay inside the fossil radius");
|
|
RequireEqual(actor.EntityIndex, firstEligible.DockingBayAssignments.Single(),
|
|
"docking must use the first eligible entity in stable zone order instead of the closest entity");
|
|
RequireEqual(-1, nearerButLater.DockingBayAssignments.Single(),
|
|
"a later closer station must not override the fossil first-eligible selection");
|
|
RequireEqual(0, ineligible.DockingBayAssignments.Count,
|
|
"docking must not manufacture a bay assignment on an entity with no docking-bay equipment");
|
|
RequireEqual(-1, outOfRange.DockingBayAssignments.Single(),
|
|
"a client scalar must not widen the daemon-owned fossil docking radius");
|
|
var dockedEvent = selectionRun.GameEvents.Single(value => value.Kind == "ship.docked");
|
|
Require(dockedEvent.SourceEntityIndex == actor.EntityIndex &&
|
|
dockedEvent.TargetEntityIndex == firstEligible.EntityIndex &&
|
|
dockedEvent.ScalarValue == 0 && dockedEvent.SubjectKey == selectionRun.CurrentEntityKey,
|
|
"accepted docking must publish one retained actor/parent/bay transition fact");
|
|
var disarmed = actor.WeaponStates.Single();
|
|
RequireEqual(0, actor.ActiveWeaponGroups.Count,
|
|
"docking must clear the daemon-owned held weapon-group latch");
|
|
Require(!disarmed.Firing && !disarmed.TriggerPending && disarmed.BurstRemaining == 0 &&
|
|
!disarmed.Charging && !disarmed.Charged && disarmed.Charge == 0 &&
|
|
disarmed.LockTargetEntityIndex < 0 && disarmed.LockProgress == 0,
|
|
"docking must cancel every transient weapon lifecycle before the ship enters its bay");
|
|
|
|
var dockedFire = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.SetWeaponGroupActive,
|
|
"pilot",
|
|
selectionRun.RunId,
|
|
1,
|
|
selectionRun.CurrentEntityKey);
|
|
dockedFire.CommandId = "docked-fire-forbidden";
|
|
dockedFire.WeaponGroup = 0;
|
|
dockedFire.ScalarValue = 1;
|
|
var dockedFireResult = AetheriaRuntimeDaemonOperations.Execute(selectionRun, [dockedFire], context);
|
|
Require(dockedFireResult.RejectedCommandIds.Contains(dockedFire.CommandId) &&
|
|
dockedFireResult.Intents.WeaponGroups.Count == 0 && actor.ActiveWeaponGroups.Count == 0,
|
|
"a docked actor must reject held-fire commands without rearming daemon state");
|
|
|
|
AetheriaRuntimeRunCheckpointCommit DockedRun(
|
|
IReadOnlyList<string> itemKeys,
|
|
bool bayHasCargo = false)
|
|
{
|
|
var ship = Entity(0, 7, "player");
|
|
ship.PositionZ = 9;
|
|
ship.VelocityX = 3;
|
|
ship.VelocityY = -4;
|
|
ship.DirectionX = 0.6;
|
|
ship.DirectionY = 0.8;
|
|
ship.Equipment = itemKeys.Select(itemKey => new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{
|
|
ItemKey = itemKey,
|
|
Durability = 1,
|
|
Enabled = true
|
|
}
|
|
}).ToArray();
|
|
var station = DockTarget(1, 7, true);
|
|
station.PositionZ = 9;
|
|
station.ChildEntityIndices = [0];
|
|
station.DockingBayAssignments = [0];
|
|
station.DockingBayContents = [bayHasCargo
|
|
? Cargo(("forgotten-cargo", 1, 0, 0))
|
|
: Cargo()];
|
|
return new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "undock-parity-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [ship, station] }]
|
|
};
|
|
}
|
|
|
|
AetheriaRuntimeDaemonOperationResult RejectUndock(
|
|
IReadOnlyList<string> itemKeys,
|
|
string expectedReason,
|
|
bool bayHasCargo = false)
|
|
{
|
|
var run = DockedRun(itemKeys, bayHasCargo);
|
|
var command = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.Undock,
|
|
"pilot",
|
|
"undock-parity-smoke",
|
|
0,
|
|
run.CurrentEntityKey);
|
|
command.CommandId = $"undock:{expectedReason}";
|
|
var result = AetheriaRuntimeDaemonOperations.Execute(run, [command], context);
|
|
RequireEqual(expectedReason, result.RejectedCommandReasons[command.CommandId],
|
|
"undock rejection must retain the exact daemon-owned fossil reason");
|
|
Require(run.Zones[0].Entities[1].DockingBayAssignments.Contains(0),
|
|
"a rejected undock must leave parentage and bay assignment untouched");
|
|
Require(!run.GameEvents.Any(value => value.Kind == "ship.undocked"),
|
|
"a rejected undock must not publish a false presentation transition");
|
|
return result;
|
|
}
|
|
|
|
RejectUndock(["dock-thruster", "dock-reactor"],
|
|
AetheriaRuntimeDaemonRejectionReasons.MissingCockpit);
|
|
RejectUndock(["dock-cockpit", "dock-reactor"],
|
|
AetheriaRuntimeDaemonRejectionReasons.MissingPropulsion);
|
|
RejectUndock(["dock-cockpit", "dock-thruster"],
|
|
AetheriaRuntimeDaemonRejectionReasons.MissingReactor);
|
|
var cargoRejected = RejectUndock(
|
|
["dock-cockpit", "dock-thruster", "dock-reactor"],
|
|
AetheriaRuntimeDaemonRejectionReasons.DockingBayCargoNotEmpty,
|
|
bayHasCargo: true);
|
|
|
|
var cargoFrame = AetheriaRuntimeDaemonFrameDocument.Create(
|
|
cargoRejected.Run, "daemon", "session", 4, 0.08, 0.02);
|
|
cargoFrame.RejectedCommandIds = cargoRejected.RejectedCommandIds;
|
|
cargoFrame.RejectedCommandReasons = cargoRejected.RejectedCommandReasons;
|
|
var cargoCommand = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.Undock,
|
|
"pilot",
|
|
"undock-parity-smoke",
|
|
0,
|
|
cargoRejected.Run.CurrentEntityKey);
|
|
cargoCommand.CommandId = "undock:docking-bay-cargo-not-empty";
|
|
var fact = AetheriaRuntimeCommittedCommandFactDocument.FromRejectedCommand(
|
|
cargoFrame, cargoCommand, "aetheria.local");
|
|
RequireEqual(AetheriaRuntimeDaemonRejectionReasons.DockingBayCargoNotEmpty, fact.RejectionReason,
|
|
"the committed command fact must carry the daemon-owned undock rejection reason");
|
|
var receipt = AetheriaRuntimeDaemonReceiptProjector.Project(
|
|
fact, AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId);
|
|
Require(receipt.State == "denied" &&
|
|
receipt.Message.Contains(AetheriaRuntimeDaemonRejectionReasons.DockingBayCargoNotEmpty,
|
|
StringComparison.Ordinal),
|
|
"the generic Eve receipt must expose the exact rejection reason without client reconstruction");
|
|
|
|
var successRun = DockedRun(["dock-cockpit", "dock-drive", "dock-reactor"]);
|
|
var successShip = successRun.Zones[0].Entities[0];
|
|
var poseBefore = (
|
|
successShip.PositionX,
|
|
successShip.PositionZ,
|
|
successShip.VelocityX,
|
|
successShip.VelocityY,
|
|
successShip.DirectionX,
|
|
successShip.DirectionY);
|
|
var undock = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.Undock,
|
|
"pilot",
|
|
"undock-parity-smoke",
|
|
0,
|
|
successRun.CurrentEntityKey);
|
|
var undockResult = AetheriaRuntimeDaemonOperations.Execute(successRun, [undock], context);
|
|
RequireEqual(1, undockResult.AppliedCommandIds.Count,
|
|
"AetherDrive must satisfy the fossil propulsion alternative for rare compatible ships");
|
|
RequireEqual(poseBefore, (
|
|
successShip.PositionX,
|
|
successShip.PositionZ,
|
|
successShip.VelocityX,
|
|
successShip.VelocityY,
|
|
successShip.DirectionX,
|
|
successShip.DirectionY),
|
|
"undocking must preserve the ship pose, velocity, and direction instead of applying an invented launch");
|
|
Require(!successRun.Zones[0].Entities[1].DockingBayAssignments.Contains(0),
|
|
"successful undocking must release the exact occupied bay");
|
|
var undockedEvent = successRun.GameEvents.Single(value => value.Kind == "ship.undocked");
|
|
Require(undockedEvent.SourceEntityIndex == successShip.EntityIndex &&
|
|
undockedEvent.TargetEntityIndex == successRun.Zones[0].Entities[1].EntityIndex &&
|
|
undockedEvent.ScalarValue == 0,
|
|
"accepted undocking must retain the released parent and bay after state removal");
|
|
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 1, Run = successRun },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"),
|
|
catalog: catalog);
|
|
Require(Flatten(surface.Surface.Root).Any(node => node.Kind == "feedback.event" &&
|
|
node.Props["eventKind"] == "ship.undocked" &&
|
|
node.Props["sourceEntityIndex"] == "0" && node.Props["targetEntityIndex"] == "1" &&
|
|
node.Props["scalarValue"] == "0"),
|
|
"generic Eve feedback must expose authoritative docking chronology and bay identity");
|
|
}
|
|
|
|
private static void DockedEntitiesCannotAttackOrBeTargeted()
|
|
{
|
|
var weaponPayload = new AetheriaRuntimeBehaviorPayload(
|
|
0,
|
|
AetheriaRuntimeBehaviorKinds.ConstantWeapon,
|
|
0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(10)),
|
|
new AetheriaRuntimeBehaviorField(6, PerformanceStat(200)),
|
|
new AetheriaRuntimeBehaviorField(9, PerformanceStat(0)),
|
|
new AetheriaRuntimeBehaviorField(10, PerformanceStat(0)),
|
|
new AetheriaRuntimeBehaviorField(11, PerformanceStat(0)),
|
|
new AetheriaRuntimeBehaviorField(13, Number(0)),
|
|
new AetheriaRuntimeBehaviorField(14, Number(1)),
|
|
new AetheriaRuntimeBehaviorField(17, Number(1))
|
|
]);
|
|
var weapon = CatalogItem("docked-combat-weapon", weaponPayload);
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([weapon], [], []);
|
|
|
|
static AetheriaRuntimeEntitySnapshotCommit ArmedShip(int index, double x, string faction, string itemKey)
|
|
{
|
|
var ship = Entity(index, x, faction);
|
|
ship.WeaponGroups = [new[] { 0 }];
|
|
ship.ActiveWeaponGroups = [true];
|
|
ship.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = itemKey, Quality = 1, Durability = 1, Enabled = true }
|
|
}];
|
|
return ship;
|
|
}
|
|
|
|
var dockedAttacker = ArmedShip(0, 0, "player", weapon.ItemKey);
|
|
dockedAttacker.TargetEntityIndex = 2;
|
|
var station = Entity(1, 0, "player");
|
|
station.ChildEntityIndices = [0];
|
|
station.DockingBayAssignments = [0];
|
|
var exposedTarget = Entity(2, 50, "raider");
|
|
var attackerRun = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "docked-attacker-smoke",
|
|
CurrentZoneIndex = 0,
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit
|
|
{ ZoneIndex = 0, Entities = [dockedAttacker, station, exposedTarget] }]
|
|
};
|
|
var forgedIntent = new AetheriaRuntimeDaemonIntentState();
|
|
forgedIntent.WeaponGroups.Add(new AetheriaRuntimeDaemonWeaponGroupIntent
|
|
{ ActorEntityKey = "zone.0.entity.0", WeaponGroup = 0, Fire = true, Active = true });
|
|
AetheriaRuntimeDaemonSimulation.Step(
|
|
attackerRun, forgedIntent, 0.1, new AetheriaRuntimeDaemonSimulationSettings(),
|
|
NewPhysics(), catalog, 0, 0);
|
|
RequireEqual(0, attackerRun.ShotReceipts.Count,
|
|
"combat must subtract docked attackers even when stale state and a forged pulse both request fire");
|
|
RequireNear(100, Stat(exposedTarget, "hull"), 0.000001,
|
|
"a docked child must not damage an exposed world entity");
|
|
|
|
var exposedAttacker = ArmedShip(0, 0, "player", weapon.ItemKey);
|
|
exposedAttacker.TargetEntityIndex = 1;
|
|
var dockedTarget = Entity(1, 50, "raider");
|
|
var hostileStation = Entity(2, 50, "raider");
|
|
hostileStation.ChildEntityIndices = [1];
|
|
hostileStation.DockingBayAssignments = [1];
|
|
var targetRun = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "docked-target-smoke",
|
|
CurrentZoneIndex = 0,
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit
|
|
{ ZoneIndex = 0, Entities = [exposedAttacker, dockedTarget, hostileStation] }]
|
|
};
|
|
AetheriaRuntimeDaemonSimulation.Step(
|
|
targetRun, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
new AetheriaRuntimeDaemonSimulationSettings(), NewPhysics(), catalog, 0, 0);
|
|
RequireEqual(0, targetRun.ShotReceipts.Count,
|
|
"combat target lookup must subtract docked children from world participants");
|
|
RequireNear(100, Stat(dockedTarget, "hull"), 0.000001,
|
|
"a docked child must not receive ordinary world combat damage through stale targeting");
|
|
}
|
|
|
|
private static void RefitIsAtomicAndUsesTypedPlacement()
|
|
{
|
|
static AetheriaRuntimeCatalogItem ShapedItem(
|
|
string key,
|
|
string category,
|
|
string hardpointType,
|
|
int width,
|
|
int height,
|
|
params (int X, int Y)[] cells)
|
|
{
|
|
var item = CatalogItem(key);
|
|
item.Category = category;
|
|
item.HardpointType = hardpointType;
|
|
item.ShapeWidth = width;
|
|
item.ShapeHeight = height;
|
|
item.OccupiedCells = cells.Length;
|
|
item.ShapeCells = cells.Select(cell => new AetheriaRuntimeShapeCell(cell.X, cell.Y)).ToArray();
|
|
return item;
|
|
}
|
|
|
|
var hull = HullCatalogItem("refit-hull", 4, 3, 1);
|
|
hull.Hardpoints =
|
|
[
|
|
new AetheriaRuntimeHardpoint(
|
|
"Weapon",
|
|
2,
|
|
0,
|
|
1,
|
|
2,
|
|
2,
|
|
[new AetheriaRuntimeShapeCell(0, 0), new AetheriaRuntimeShapeCell(0, 1)],
|
|
"",
|
|
"Clockwise",
|
|
0)
|
|
];
|
|
var weapon = ShapedItem(
|
|
"refit-weapon",
|
|
AetheriaRuntimeItemCategories.Weapon,
|
|
"Weapon",
|
|
2,
|
|
1,
|
|
(0, 0),
|
|
(1, 0));
|
|
var blocker = ShapedItem("refit-blocker", AetheriaRuntimeItemCategories.Gear, "", 1, 1, (0, 0));
|
|
var cargoBay = ShapedItem("refit-cargo-bay", AetheriaRuntimeItemCategories.CargoBay, "", 1, 1, (0, 0));
|
|
cargoBay.InteriorShapeWidth = 4;
|
|
cargoBay.InteriorShapeHeight = 3;
|
|
cargoBay.InteriorOccupiedCells = 12;
|
|
cargoBay.InteriorShapeCells = Enumerable.Range(0, 3)
|
|
.SelectMany(y => Enumerable.Range(0, 4).Select(x => new AetheriaRuntimeShapeCell(x, y)))
|
|
.ToArray();
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([hull, weapon, blocker, cargoBay], [], []);
|
|
|
|
var station = Entity(0, 0, "player");
|
|
station.Kind = "station";
|
|
station.CargoBays =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = cargoBay.ItemKey, Quantity = 1 }
|
|
}
|
|
];
|
|
station.CargoContents = [new AetheriaRuntimeCargoBayLoadoutCommit()];
|
|
station.DockingBays =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "station-refit-dock", Quantity = 1 }
|
|
}
|
|
];
|
|
station.DockingBayAssignments = [1];
|
|
station.DockingBayContents = [new AetheriaRuntimeCargoBayLoadoutCommit()];
|
|
station.ChildEntityIndices = [1];
|
|
|
|
var ship = Entity(1, 0, "player");
|
|
ship.HullItemKey = hull.ItemKey;
|
|
ship.Equipment =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = 0,
|
|
Y = 0,
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{
|
|
ItemKey = blocker.ItemKey,
|
|
Quality = 0.8,
|
|
Durability = 0.7,
|
|
Quantity = 1,
|
|
Enabled = false,
|
|
OverrideShutdown = true,
|
|
Temperature = 321
|
|
}
|
|
}
|
|
];
|
|
ship.CargoBays =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = 3,
|
|
Y = 2,
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = cargoBay.ItemKey, Quantity = 1 }
|
|
}
|
|
];
|
|
ship.CargoContents =
|
|
[
|
|
new AetheriaRuntimeCargoBayLoadoutCommit
|
|
{
|
|
Items =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = 0,
|
|
Y = 0,
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{
|
|
ItemKey = weapon.ItemKey,
|
|
Quality = 0.9,
|
|
Durability = 0.6,
|
|
Quantity = 1,
|
|
Enabled = true,
|
|
Temperature = 444
|
|
}
|
|
},
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = 1,
|
|
Y = 0,
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = blocker.ItemKey, Quantity = 1 }
|
|
},
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = 2,
|
|
Y = 0,
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = cargoBay.ItemKey, Quantity = 1 }
|
|
}
|
|
]
|
|
}
|
|
];
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "refit-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.1",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [station, ship] }]
|
|
};
|
|
var context = new AetheriaRuntimeDaemonOperationContext { Catalog = catalog };
|
|
|
|
AetheriaRuntimeDaemonCommandDocument Equip(
|
|
string id,
|
|
string sourceKind,
|
|
int sourceIndex,
|
|
string itemKey,
|
|
int sourceX,
|
|
int sourceY,
|
|
int destinationX,
|
|
int destinationY)
|
|
{
|
|
var command = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.EquipItem,
|
|
"pilot",
|
|
run.RunId,
|
|
1,
|
|
run.CurrentEntityKey);
|
|
command.CommandId = id;
|
|
command.TextValue = itemKey;
|
|
command.EquipmentTransfer = new AetheriaRuntimeEquipmentTransferCommand
|
|
{
|
|
SourceKind = sourceKind,
|
|
OriginEntityKey = run.CurrentEntityKey,
|
|
OriginIndex = sourceIndex,
|
|
SourceX = sourceX,
|
|
SourceY = sourceY,
|
|
DestinationEntityKey = run.CurrentEntityKey,
|
|
DestinationX = destinationX,
|
|
DestinationY = destinationY,
|
|
HasDestinationPosition = true
|
|
};
|
|
return command;
|
|
}
|
|
|
|
var equipWeapon = AetheriaRuntimeDaemonOperations.Execute(
|
|
run,
|
|
[Equip("refit-weapon", AetheriaRuntimeRefitSourceKinds.Cargo, 0, weapon.ItemKey, 0, 0, 2, 0)],
|
|
context);
|
|
Require(equipWeapon.AppliedCommandIds.Contains("refit-weapon"),
|
|
"a docked cargo weapon must equip through the daemon transaction");
|
|
var equippedWeapon = ship.Equipment.Single(slot => slot.Item.ItemKey == weapon.ItemKey);
|
|
RequireEqual("Clockwise", equippedWeapon.Rotation,
|
|
"hardpoint equipment must inherit the authored hardpoint rotation");
|
|
RequireNear(0.9, equippedWeapon.Item.Quality, 0.000001,
|
|
"refit must preserve the exact item instance quality");
|
|
RequireNear(444, equippedWeapon.Item.Temperature, 0.000001,
|
|
"refit must preserve the exact item instance temperature");
|
|
|
|
var rejectedSource = ship.CargoContents[0].Items.Single(slot => slot.Item.ItemKey == blocker.ItemKey);
|
|
var rejectedBefore = (
|
|
Count: ship.CargoContents[0].Items.Count,
|
|
X: rejectedSource.X,
|
|
Y: rejectedSource.Y,
|
|
Quality: rejectedSource.Item.Quality,
|
|
Durability: rejectedSource.Item.Durability,
|
|
Temperature: rejectedSource.Item.Temperature);
|
|
var overlap = AetheriaRuntimeDaemonOperations.Execute(
|
|
run,
|
|
[Equip("refit-overlap", AetheriaRuntimeRefitSourceKinds.Cargo, 0, blocker.ItemKey, 1, 0, 0, 0)],
|
|
context);
|
|
RequireEqual(AetheriaRuntimeDaemonRejectionReasons.RefitNoFit,
|
|
overlap.RejectedCommandReasons["refit-overlap"],
|
|
"occupied interior cells must reject with the daemon-owned fit reason");
|
|
var rejectedAfter = ship.CargoContents[0].Items.Single(slot => slot.Item.ItemKey == blocker.ItemKey);
|
|
RequireEqual(rejectedBefore.Count, ship.CargoContents[0].Items.Count,
|
|
"rejected refit must not remove the source item");
|
|
Require(rejectedBefore.X == rejectedAfter.X && rejectedBefore.Y == rejectedAfter.Y &&
|
|
rejectedBefore.Quality == rejectedAfter.Item.Quality &&
|
|
rejectedBefore.Durability == rejectedAfter.Item.Durability &&
|
|
rejectedBefore.Temperature == rejectedAfter.Item.Temperature,
|
|
"rejected refit must preserve every source instance field");
|
|
|
|
var equipBay = AetheriaRuntimeDaemonOperations.Execute(
|
|
run,
|
|
[Equip("refit-bay", AetheriaRuntimeRefitSourceKinds.Cargo, 0, cargoBay.ItemKey, 2, 0, 1, 2)],
|
|
context);
|
|
Require(equipBay.AppliedCommandIds.Contains("refit-bay") && ship.CargoBays.Count == 2 && ship.CargoContents.Count == 2,
|
|
"catalog cargo-bay category must route into aligned typed bay and contents collections");
|
|
|
|
ship.CargoContents[1].Items =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = blocker.ItemKey, Quantity = 1 }
|
|
}
|
|
];
|
|
var storeBay = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.StoreItem,
|
|
"pilot",
|
|
run.RunId,
|
|
1,
|
|
run.CurrentEntityKey);
|
|
storeBay.CommandId = "refit-nonempty-bay";
|
|
storeBay.TextValue = cargoBay.ItemKey;
|
|
storeBay.StoreItem = new AetheriaRuntimeStoreItemCommand
|
|
{
|
|
SourceKind = AetheriaRuntimeRefitSourceKinds.CargoBay,
|
|
OriginEntityKey = run.CurrentEntityKey,
|
|
SourceEquipmentIndex = 1,
|
|
DestinationEntityKey = "zone.0.entity.0",
|
|
DestinationCargoIndex = 0,
|
|
DestinationX = 0,
|
|
DestinationY = 0,
|
|
HasDestinationPosition = true
|
|
};
|
|
var nonempty = AetheriaRuntimeDaemonOperations.Execute(run, [storeBay], context);
|
|
RequireEqual(AetheriaRuntimeDaemonRejectionReasons.RefitBayNotEmpty,
|
|
nonempty.RejectedCommandReasons[storeBay.CommandId],
|
|
"a nonempty cargo bay must reject unequip before any mutation");
|
|
RequireEqual(2, ship.CargoBays.Count,
|
|
"nonempty bay rejection must preserve the installed bay");
|
|
|
|
var frame = AetheriaRuntimeDaemonFrameDocument.Create(run, "daemon", "session", 8, 0.16, 0.02);
|
|
frame.RejectedCommandIds = nonempty.RejectedCommandIds;
|
|
frame.RejectedCommandReasons = nonempty.RejectedCommandReasons;
|
|
var fact = AetheriaRuntimeCommittedCommandFactDocument.FromRejectedCommand(frame, storeBay, "aetheria.local");
|
|
var receipt = AetheriaRuntimeDaemonReceiptProjector.Project(
|
|
fact,
|
|
AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId);
|
|
RequireEqual(AetheriaRuntimeDaemonRejectionReasons.RefitBayNotEmpty, fact.RejectionReason,
|
|
"refit rejection reason must survive the committed command fact");
|
|
Require(receipt.State == "denied" &&
|
|
receipt.Message.Contains(AetheriaRuntimeDaemonRejectionReasons.RefitBayNotEmpty, StringComparison.Ordinal),
|
|
"generic Eve receipt must expose the exact refit rejection reason");
|
|
|
|
station.DockingBayAssignments = [-1];
|
|
station.ChildEntityIndices = [];
|
|
var deployed = AetheriaRuntimeDaemonOperations.Execute(
|
|
run,
|
|
[Equip("refit-deployed", AetheriaRuntimeRefitSourceKinds.Equipment, 0, blocker.ItemKey, int.MinValue, int.MinValue, 1, 1)],
|
|
context);
|
|
RequireEqual(AetheriaRuntimeDaemonRejectionReasons.RefitRequiresDocked,
|
|
deployed.RejectedCommandReasons["refit-deployed"],
|
|
"deployed ships must not refit even when the requested placement would fit");
|
|
Require(ship.Equipment.Any(slot => slot.Item.ItemKey == blocker.ItemKey),
|
|
"deployed refit rejection must preserve installed equipment");
|
|
}
|
|
|
|
private static void CargoTransferUsesSpatialAtomicTransaction()
|
|
{
|
|
var cargoBay = CatalogItem("grid-cargo-bay");
|
|
cargoBay.Category = AetheriaRuntimeItemCategories.CargoBay;
|
|
cargoBay.ShapeWidth = 1;
|
|
cargoBay.ShapeHeight = 1;
|
|
cargoBay.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
cargoBay.InteriorShapeWidth = 2;
|
|
cargoBay.InteriorShapeHeight = 1;
|
|
cargoBay.InteriorOccupiedCells = 2;
|
|
cargoBay.InteriorShapeCells = [new AetheriaRuntimeShapeCell(0, 0), new AetheriaRuntimeShapeCell(1, 0)];
|
|
var commodity = CatalogItem("grid-commodity");
|
|
commodity.Category = AetheriaRuntimeItemCategories.SimpleCommodity;
|
|
commodity.ShapeWidth = 1;
|
|
commodity.ShapeHeight = 1;
|
|
commodity.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
commodity.Stackable = true;
|
|
commodity.MaxStack = 10;
|
|
var blocker = CatalogItem("grid-blocker");
|
|
blocker.ShapeWidth = 1;
|
|
blocker.ShapeHeight = 1;
|
|
blocker.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([cargoBay, commodity, blocker], [], []);
|
|
|
|
AetheriaRuntimeEntitySnapshotCommit CargoEntity(int index, params AetheriaRuntimeLoadoutItemSlotCommit[] items)
|
|
{
|
|
var entity = Entity(index, 0, "player");
|
|
entity.CargoBays =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = cargoBay.ItemKey, Quantity = 1 }
|
|
}
|
|
];
|
|
entity.CargoContents = [new AetheriaRuntimeCargoBayLoadoutCommit { Items = items }];
|
|
return entity;
|
|
}
|
|
|
|
var source = CargoEntity(0, new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = 0,
|
|
Y = 0,
|
|
Rotation = "None",
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{
|
|
ItemKey = commodity.ItemKey,
|
|
Quantity = 4,
|
|
Quality = 0.75,
|
|
Durability = 0.5,
|
|
Temperature = 333
|
|
}
|
|
});
|
|
var destination = CargoEntity(
|
|
1,
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = 0,
|
|
Y = 0,
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = blocker.ItemKey, Quantity = 1 }
|
|
},
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = 1,
|
|
Y = 0,
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = commodity.ItemKey, Quantity = 8 }
|
|
});
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "cargo-grid-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [source, destination] }]
|
|
};
|
|
var context = new AetheriaRuntimeDaemonOperationContext { Catalog = catalog };
|
|
|
|
AetheriaRuntimeDaemonCommandDocument Transfer(string id, int quantity, int x, bool positioned = true)
|
|
{
|
|
var command = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.TransferCargoItem,
|
|
"pilot",
|
|
run.RunId,
|
|
0,
|
|
run.CurrentEntityKey);
|
|
command.CommandId = id;
|
|
command.TextValue = commodity.ItemKey;
|
|
command.CargoTransfer = new AetheriaRuntimeCargoTransferCommand
|
|
{
|
|
OriginEntityKey = "zone.0.entity.0",
|
|
OriginCargoIndex = 0,
|
|
DestinationEntityKey = "zone.0.entity.1",
|
|
DestinationCargoIndex = 0,
|
|
SourceX = 0,
|
|
SourceY = 0,
|
|
DestinationX = x,
|
|
DestinationY = 0,
|
|
HasDestinationPosition = positioned,
|
|
Quantity = quantity
|
|
};
|
|
return command;
|
|
}
|
|
|
|
var overfill = AetheriaRuntimeDaemonOperations.Execute(run, [Transfer("cargo-overfill", 3, 1)], context);
|
|
RequireEqual(AetheriaRuntimeDaemonRejectionReasons.CargoStackLimit,
|
|
overfill.RejectedCommandReasons["cargo-overfill"],
|
|
"an explicit commodity stack overfill must reject atomically");
|
|
RequireEqual(4, source.CargoContents[0].Items.Single().Item.Quantity,
|
|
"stack-limit rejection must not decrement the source stack");
|
|
RequireEqual(8, destination.CargoContents[0].Items.Single(slot => slot.Item.ItemKey == commodity.ItemKey).Item.Quantity,
|
|
"stack-limit rejection must not partially fill the destination stack");
|
|
|
|
var stacked = AetheriaRuntimeDaemonOperations.Execute(run, [Transfer("cargo-stack", 2, 1)], context);
|
|
Require(stacked.AppliedCommandIds.Contains("cargo-stack"),
|
|
"a complete explicit stack merge must commit");
|
|
RequireEqual(2, source.CargoContents[0].Items.Single().Item.Quantity,
|
|
"accepted stack merge must remove exactly the requested source quantity");
|
|
RequireEqual(10, destination.CargoContents[0].Items.Single(slot => slot.Item.ItemKey == commodity.ItemKey).Item.Quantity,
|
|
"accepted stack merge must fill the destination to the typed maximum");
|
|
|
|
var occupied = AetheriaRuntimeDaemonOperations.Execute(run, [Transfer("cargo-overlap", 1, 0)], context);
|
|
RequireEqual(AetheriaRuntimeDaemonRejectionReasons.CargoNoFit,
|
|
occupied.RejectedCommandReasons["cargo-overlap"],
|
|
"an occupied cell containing another item must reject spatial placement");
|
|
RequireEqual(2, source.CargoContents[0].Items.Single().Item.Quantity,
|
|
"spatial rejection must leave the source stack untouched");
|
|
|
|
destination.CargoContents[0].Items = [];
|
|
var placed = AetheriaRuntimeDaemonOperations.Execute(run, [Transfer("cargo-place", 2, 0, positioned: false)], context);
|
|
Require(placed.AppliedCommandIds.Contains("cargo-place") && source.CargoContents[0].Items.Count == 0,
|
|
"unpositioned transfer must commit only after finding the first authored interior cell");
|
|
var moved = destination.CargoContents[0].Items.Single();
|
|
Require(moved.X == 0 && moved.Y == 0 && moved.Item.Quantity == 2,
|
|
"unpositioned cargo placement must use deterministic interior order");
|
|
RequireNear(0.75, moved.Item.Quality, 0.000001,
|
|
"cargo transfer must preserve source instance quality");
|
|
RequireNear(333, moved.Item.Temperature, 0.000001,
|
|
"cargo transfer must preserve source instance temperature");
|
|
}
|
|
|
|
private static void VolumeSurfaceKeepsNativeShaderAbiInAssetVariant()
|
|
{
|
|
var player = Entity(0, 0, "player");
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "volume-program-ownership-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [player] }]
|
|
};
|
|
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 1, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
var volume = Flatten(surface.Surface.Root).Single(node => node.Kind == "field.volume3d");
|
|
var stardust = Flatten(surface.Surface.Root).Single(node => node.Kind == "field.particles3d");
|
|
var gravityField = Flatten(surface.Surface.Root).Single(node => node.Kind == "field.surface2d");
|
|
Require(volume.Props.Values.All(value => value == null || !value.Contains("_Nebula", StringComparison.Ordinal)) &&
|
|
volume.Props["layerBindings"].Contains("fog.surface_height=surfaceHeight", StringComparison.Ordinal) &&
|
|
volume.Props["layerTargetDescriptors"].Contains("fog.surface_height=2,2,false,bilinear", StringComparison.Ordinal) &&
|
|
volume.Props["layerTargetDescriptors"].Contains("fog.tint=0.5,0.5,true,trilinear", StringComparison.Ordinal) &&
|
|
volume.Props["viewportTextureScaleBindings"] == "ditherCoordinates=dither" &&
|
|
volume.Props["viewportAnchor"] == stardust.Props["viewportAnchor"] &&
|
|
volume.Props["span"] == stardust.Props["span"] &&
|
|
volume.Props["cellWorldSize"] == stardust.Props["cellWorldSize"] &&
|
|
volume.Props["gravityTexelsPerCell"] == stardust.Props["gravityTexelsPerCell"] &&
|
|
volume.Props["viewportSnapLayer"] == stardust.Props["viewportSnapLayer"] &&
|
|
volume.Props["viewportSnapTexels"] == stardust.Props["viewportSnapTexels"] &&
|
|
volume.Props["documentFloatBindings"] == "simulationTimeSeconds=flowScroll,0.025,0" &&
|
|
!volume.Props.ContainsKey("vectorParameters"),
|
|
"portable Eve volume surfaces must name logical ports rather than Unity shader properties");
|
|
Require(gravityField.Props["minX"] == "-768" &&
|
|
gravityField.Props["minY"] == "-768" &&
|
|
gravityField.Props["maxX"] == "768" &&
|
|
gravityField.Props["maxY"] == "768" &&
|
|
volume.Props["documentRef"] == gravityField.Props["renderSplatsDocumentId"],
|
|
"flight fog rasterization must share the exact 256-by-6 Stardust lattice viewport");
|
|
Require(stardust.Props["documentRef"] == gravityField.Props["renderSplatsDocumentId"] &&
|
|
stardust.Props["computeProgramAssetRef"] == "compute.environment.stardust" &&
|
|
stardust.Props["materialAssetRef"] == "material.environment.stardust" &&
|
|
stardust.Props["span"] == "256" &&
|
|
stardust.Props["threadGroupSize"] == "128" &&
|
|
stardust.Props["particleStrideBytes"] == "28" &&
|
|
stardust.Props["viewportAnchor"] == "active-camera.xz" &&
|
|
stardust.Props["layerBindings"] == "fog.surface_height=surfaceHeight;fog.tint=tint" &&
|
|
stardust.Props["materialAssetTextureBindings"] == "texture.environment.volume-dither=dither" &&
|
|
stardust.Props["materialViewportTextureScaleBindings"] == "ditherCoordinates=dither" &&
|
|
stardust.Props["materialRenderFrameIndexPort"] == "frameIndex" &&
|
|
stardust.Props["cellWorldSize"] == "6" &&
|
|
stardust.Props["gravityTexelsPerCell"] == "8" &&
|
|
stardust.Props["viewportSnapLayer"] == "fog.surface_height" &&
|
|
stardust.Props["viewportSnapTexels"] == "8" &&
|
|
stardust.Props["documentFloatBindings"] ==
|
|
"simulationTimeSeconds=time,1,0;simulationTimeSeconds=flowScroll,0.025,0" &&
|
|
stardust.Props["documentTimeVectorPort"] == "timeVector" &&
|
|
stardust.Props["floatParameters"].Contains(
|
|
"period=2;minimumSize=0.25;maximumSize=0.75;spacing=6;ceilingHeight=0;" +
|
|
"floorHeight=-10;minHeadroom=25;maxHeadroom=100;heightExponent=3",
|
|
StringComparison.Ordinal) &&
|
|
!stardust.Props["floatParameters"].Contains("patch", StringComparison.OrdinalIgnoreCase),
|
|
"portable Stardust semantics must preserve the grid/gravity lattice, dispatch, camera snap, time, and particle values exactly");
|
|
|
|
var shader = AetheriaRuntimeAssets.ProjectManifest(null).Assets.Single(asset =>
|
|
asset.Ref.AssetKey == "shader.environment.gravity-fog");
|
|
var postProcess = AetheriaRuntimeAssets.ProjectManifest(null).Assets.Single(asset =>
|
|
asset.Ref.AssetKey == "profile.environment.flight");
|
|
var stardustCompute = AetheriaRuntimeAssets.ProjectManifest(null).Assets.Single(asset =>
|
|
asset.Ref.AssetKey == "compute.environment.stardust");
|
|
var stardustMaterial = AetheriaRuntimeAssets.ProjectManifest(null).Assets.Single(asset =>
|
|
asset.Ref.AssetKey == "material.environment.stardust");
|
|
var stardustColors = AetheriaRuntimeAssets.ProjectManifest(null).Assets.Single(asset =>
|
|
asset.Ref.AssetKey == "texture.environment.stardust-colors");
|
|
Require(postProcess.Ref.Kind == AetheriaRuntimeAssetKinds.VolumeProfile &&
|
|
postProcess.Ref.Metadata["presentationRole"] == "environment.post-process.flight",
|
|
"provider asset catalog must carry the advertised flight post-process profile variant");
|
|
Require(shader.Ref.Metadata["unity.volume.texturePort.surfaceHeight"] == "_NebulaSurfaceHeight" &&
|
|
shader.Ref.Metadata["unity.volume.matrixPort.cameraToWorld"] == "_CamToWorld" &&
|
|
shader.Ref.Metadata["unity.volume.pass.raymarch"] == "0" &&
|
|
shader.Ref.Metadata["unity.volume.pass.temporal"] == "1" &&
|
|
shader.Ref.Metadata["unity.volume.pass.composite"] == "2" &&
|
|
shader.Ref.Metadata["unity.volume.texturePort.currentSample"] == "_UndersampleCloudTex" &&
|
|
shader.Ref.Metadata["unity.volume.texturePort.history"] == "_MainTex" &&
|
|
shader.Ref.Metadata["unity.volume.matrixPort.previousViewProjection"] == "_PrevVP" &&
|
|
shader.Ref.Metadata["unity.volume.floatPort.resetHistory"] == "_ResetHistory",
|
|
"provider asset metadata must own the concrete Unity volume-program ABI projected into runtime variants");
|
|
Require(stardustCompute.Ref.Kind == AetheriaRuntimeAssetKinds.ComputeShader &&
|
|
stardustCompute.Ref.Metadata["unity.particles.kernel.update"] == "UpdateParticles" &&
|
|
stardustCompute.Ref.Metadata["unity.particles.bufferPort.particles"] == "particles" &&
|
|
!stardustCompute.Ref.Metadata.ContainsKey("unity.particles.texturePort.patch") &&
|
|
!stardustCompute.Ref.Metadata.ContainsKey("unity.particles.texturePort.patchHeight") &&
|
|
!stardustCompute.Ref.Metadata.ContainsKey("unity.particles.floatPort.patchDensity") &&
|
|
!stardustCompute.Ref.Metadata.ContainsKey("unity.particles.floatPort.patchBlend") &&
|
|
stardustCompute.Ref.Metadata["unity.particles.vectorPort.viewportTransform"] == "_GridTransform" &&
|
|
stardustCompute.Ref.Metadata["unity.particles.vectorPort.timeVector"] == "_Time" &&
|
|
stardustCompute.Ref.Metadata["unity.particles.intPort.span"] == "span" &&
|
|
stardustMaterial.Ref.Metadata["unity.particles.bufferPort.particles"] == "particles" &&
|
|
stardustMaterial.Ref.Metadata["unity.particles.bufferPort.quadPoints"] == "quadPoints" &&
|
|
stardustMaterial.Ref.Metadata["unity.particles.texturePort.dither"] == "_DitheringTex" &&
|
|
stardustMaterial.Ref.Metadata["unity.particles.vectorPort.ditherCoordinates"] == "_DitheringCoords" &&
|
|
stardustMaterial.Ref.Metadata["unity.particles.intPort.frameIndex"] == "_FrameNumber" &&
|
|
stardustColors.Ref.Metadata["unityAssetPath"] == "Assets/Resources/Gradients/blackbody.png",
|
|
"provider asset metadata must own the exact Stardust compute/render ABI and authored color ramp");
|
|
var fogVolume = Flatten(surface.Surface.Root)
|
|
.Single(component => string.Equals(component.Kind, "field.volume3d", StringComparison.Ordinal));
|
|
Require(fogVolume.Props.TryGetValue("floatParameters", out var fogParameters) &&
|
|
fogParameters.Contains("compositeOpacity=1", StringComparison.Ordinal),
|
|
"the portable fog surface must retain the fossil cloud shader's full-strength composite default");
|
|
}
|
|
|
|
private static AetheriaYmirWorldPhysics NewPhysics()
|
|
{
|
|
AetheriaYmirWorldPhysics physics = new();
|
|
OwnedPhysics.Add(physics);
|
|
return physics;
|
|
}
|
|
|
|
private static void RunCheck(Action check)
|
|
{
|
|
try
|
|
{
|
|
check();
|
|
}
|
|
finally
|
|
{
|
|
foreach (var physics in OwnedPhysics)
|
|
physics.Dispose();
|
|
OwnedPhysics.Clear();
|
|
}
|
|
}
|
|
|
|
private static void PositiveGravityDepthAttractsAndProjectsAsAWell()
|
|
{
|
|
var body = new AetheriaRuntimeBodySnapshotCommit
|
|
{
|
|
BodyKey = "gravity-owner",
|
|
Kind = "gas_giant",
|
|
GravityInfluenceCenterX = 0,
|
|
GravityInfluenceCenterZ = 0,
|
|
GravityInfluenceRadius = 100,
|
|
GravityWellDepth = 20,
|
|
GravityWaveRadius = 80,
|
|
GravityWaveDepth = 3,
|
|
GravityWaveSpeed = 2,
|
|
GravityWaveFrequency = 6
|
|
};
|
|
var actor = Entity(0, 10, "player");
|
|
actor.PositionZ = 0;
|
|
actor.VelocityX = 0;
|
|
actor.VelocityY = 0;
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Bodies = [body],
|
|
Entities = [actor],
|
|
GravityTerrainRadius = 100,
|
|
GravityTerrainDepth = 5
|
|
};
|
|
|
|
using var physics = NewPhysics();
|
|
physics.RetainWorlds("gravity-sign-smoke", [zone.ZoneIndex]);
|
|
var step = physics.Step("gravity-sign-smoke", 1, 0, zone, zone.Entities, 0.1);
|
|
var steppedActor = step.Bodies.Single(value => value.EntityIndex == actor.EntityIndex);
|
|
Require(steppedActor.VelocityX < 0,
|
|
"positive authored gravity depth must accelerate a body toward the well center through Ymir");
|
|
Require(AetheriaRuntimeDaemonRenderQueries.EvaluateGravityTerrainHeight(zone, 10, 0, 0) < 0,
|
|
"the same positive gravity-depth magnitude must project as a negative terrain well");
|
|
var splats = AetheriaRuntimeGameDocuments.RenderSplatsViewport(
|
|
new AetheriaRuntimeDaemonFrameDocument
|
|
{
|
|
Run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "gravity-sign-smoke",
|
|
CurrentZoneIndex = 0,
|
|
Zones = [zone]
|
|
}
|
|
},
|
|
new AetheriaRuntimeViewportBounds { MinX = -20, MinY = -20, MaxX = 20, MaxY = 20 });
|
|
var bodySplatIndex = splats.Splats.SourceKey
|
|
.Select((sourceKey, index) => (sourceKey, index))
|
|
.Single(value => value.sourceKey == body.BodyKey).index;
|
|
var terrainSplatIndex = splats.Splats.SourceKey
|
|
.Select((sourceKey, index) => (sourceKey, index))
|
|
.Single(value => value.sourceKey == "environment.gravity_terrain").index;
|
|
var fogSurfaceSplatIndex = splats.Splats.SourceKey
|
|
.Select((sourceKey, index) => (sourceKey, index))
|
|
.Single(value => value.sourceKey == "environment.gravity_terrain:fog.surface_height").index;
|
|
var gravityWaveIndex = splats.Splats.SourceKey
|
|
.Select((sourceKey, index) => (sourceKey, index))
|
|
.Single(value => value.sourceKey == body.BodyKey + ":gravity.wave").index;
|
|
var fogSurfaceWaveIndex = splats.Splats.SourceKey
|
|
.Select((sourceKey, index) => (sourceKey, index))
|
|
.Single(value => value.sourceKey == body.BodyKey + ":fog.surface_height.wave").index;
|
|
var fogPatchWaveIndex = splats.Splats.SourceKey
|
|
.Select((sourceKey, index) => (sourceKey, index))
|
|
.Single(value => value.sourceKey == body.BodyKey + ":fog.patch_height.wave").index;
|
|
Require(splats.Splats.ValueR[bodySplatIndex] < 0,
|
|
"render-splat projection must convert positive canonical body depth into a negative gravity-height value");
|
|
Require(splats.Splats.Falloff[terrainSplatIndex] == EveFieldsSplatFalloffs.PowerPulse &&
|
|
splats.Splats.CenterX[terrainSplatIndex] == 0 &&
|
|
splats.Splats.CenterY[terrainSplatIndex] == 0 &&
|
|
splats.Splats.HalfExtentX[terrainSplatIndex] == zone.GravityTerrainRadius &&
|
|
splats.Splats.FalloffScale[terrainSplatIndex] == 1,
|
|
"zone gravity terrain must remain the fossil radial PowerPulse rather than a viewport-sized solid slab");
|
|
Require(splats.Splats.Falloff[bodySplatIndex] == EveFieldsSplatFalloffs.PowerPulse &&
|
|
splats.Splats.FalloffScale[bodySplatIndex] == 2 &&
|
|
splats.Splats.FalloffExponent[bodySplatIndex] == body.GravityDepthExponent,
|
|
"body gravity wells must publish the fossil radial PowerPulse scale and exponent");
|
|
Require(splats.Splats.LayerIndex[fogSurfaceSplatIndex] != splats.Splats.LayerIndex[terrainSplatIndex] &&
|
|
splats.Splats.ValueR[fogSurfaceSplatIndex] == -splats.Splats.ValueR[terrainSplatIndex] &&
|
|
splats.Splats.ValueR[fogSurfaceSplatIndex] > 0 &&
|
|
splats.Splats.Falloff[fogSurfaceSplatIndex] == EveFieldsSplatFalloffs.PowerPulse,
|
|
"the fog surface must project positive fossil PowerBrush depth without changing negative gameplay terrain height");
|
|
foreach (var waveIndex in new[] { gravityWaveIndex, fogSurfaceWaveIndex, fogPatchWaveIndex })
|
|
{
|
|
Require(splats.Splats.SourceKind[waveIndex] == EveFieldsSplatSourceKinds.AnimatedRadialCosine &&
|
|
splats.Splats.HalfExtentX[waveIndex] == body.GravityWaveRadius * 0.5 &&
|
|
splats.Splats.FrequencyX[waveIndex] == body.GravityWaveFrequency &&
|
|
splats.Splats.FrequencyY[waveIndex] == 1.25 &&
|
|
splats.Splats.AnimationSpeed[waveIndex] == body.GravityWaveSpeed &&
|
|
splats.Splats.Falloff[waveIndex] == EveFieldsSplatFalloffs.PowerPulse &&
|
|
splats.Splats.FalloffExponent[waveIndex] == 8,
|
|
"gravity, fog surface, and fog patch height must share the exact fossil radial-wave source");
|
|
}
|
|
var patchLayerIndex = splats.Layers
|
|
.Select((layer, index) => (layer, index))
|
|
.Single(value => value.layer.LayerKey == EveFieldsSplatLayerKeys.FogPatch).index;
|
|
var patchLayer = splats.Layers[patchLayerIndex];
|
|
var patchMicroIndex = splats.Splats.SourceKey
|
|
.Select((sourceKey, index) => (sourceKey, index))
|
|
.Single(value => value.sourceKey == "environment.fog_patch.depth_micro").index;
|
|
var patchMacroIndex = splats.Splats.SourceKey
|
|
.Select((sourceKey, index) => (sourceKey, index))
|
|
.Single(value => value.sourceKey == "environment.fog_patch.depth_macro").index;
|
|
var ambientTintIndex = splats.Splats.SourceKey
|
|
.Select((sourceKey, index) => (sourceKey, index))
|
|
.Single(value => value.sourceKey == "environment.fog_tint.ambient").index;
|
|
Require(patchLayer.BlendMode == EveFieldsSplatBlendModes.Add,
|
|
"fossil patch depth brushes must add instead of taking the maximum");
|
|
Require(splats.Splats.SourceKind[patchMicroIndex] == EveFieldsSplatSourceKinds.AnimatedSimplexNoise &&
|
|
splats.Splats.SourceFlags[patchMicroIndex] == EveFieldsSplatSourceFlags.AbsoluteValue &&
|
|
splats.Splats.FrequencyX[patchMicroIndex] == 0.005 &&
|
|
splats.Splats.AnimationSpeed[patchMicroIndex] == 0.025 &&
|
|
splats.Splats.HalfExtentX[patchMicroIndex] == 1500,
|
|
"fog micro-patch must retain the fossil world-space animated absolute-simplex brush");
|
|
Require(splats.Splats.SourceKind[patchMacroIndex] == EveFieldsSplatSourceKinds.AnimatedCellNoiseB &&
|
|
splats.Splats.FrequencyX[patchMacroIndex] == 0.001 &&
|
|
splats.Splats.AnimationSpeed[patchMacroIndex] == 0.025 &&
|
|
splats.Splats.ValueR[patchMacroIndex] == 20,
|
|
"fog macro-patch must retain the fossil moving cellular-B brush");
|
|
Require(splats.Splats.ValueR[ambientTintIndex] > 0 &&
|
|
splats.Splats.ValueB[ambientTintIndex] > splats.Splats.ValueG[ambientTintIndex] &&
|
|
splats.Splats.ValueG[ambientTintIndex] > splats.Splats.ValueR[ambientTintIndex],
|
|
"fog tint must include the fossil ambient blue brush before local body lights");
|
|
Require(!splats.Splats.SourceKey.Contains("environment.fog_patch") &&
|
|
!splats.Splats.SourceKey.Contains("environment.fog_patch_height"),
|
|
"the viewport-local placeholder fog producers must remain deleted");
|
|
var ordinaryPlanetSplats = AetheriaRuntimeGameDocuments.RenderSplatsViewport(
|
|
new AetheriaRuntimeDaemonFrameDocument
|
|
{
|
|
Run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
CurrentZoneIndex = 0,
|
|
Zones =
|
|
[
|
|
new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Bodies =
|
|
[
|
|
new AetheriaRuntimeBodySnapshotCommit
|
|
{
|
|
BodyKey = "ordinary-planet",
|
|
Kind = "planet",
|
|
GravityInfluenceRadius = 20,
|
|
GravityWaveRadius = 20,
|
|
GravityWaveDepth = 4
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
},
|
|
new AetheriaRuntimeViewportBounds { MinX = -30, MinY = -30, MaxX = 30, MaxY = 30 });
|
|
Require(!ordinaryPlanetSplats.Splats.SourceKey.Any(key =>
|
|
key.StartsWith("ordinary-planet:", StringComparison.Ordinal) &&
|
|
key.EndsWith(".wave", StringComparison.Ordinal)),
|
|
"ordinary planets must not acquire gas-giant gravity-wave visuals merely because compatibility rows contain wave values");
|
|
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "gravity-height-smoke",
|
|
CurrentZoneIndex = zone.ZoneIndex,
|
|
Zones = [zone]
|
|
};
|
|
AetheriaRuntimeDaemonSimulation.Step(
|
|
run,
|
|
new AetheriaRuntimeDaemonIntentState(),
|
|
0.1,
|
|
new AetheriaRuntimeDaemonSimulationSettings(),
|
|
physics,
|
|
simulationTimeSeconds: 0.1);
|
|
RequireNear(
|
|
AetheriaRuntimeDaemonRenderQueries.EvaluateGravityTerrainHeight(
|
|
zone, actor.PositionX, actor.PositionZ, 0.1),
|
|
actor.PositionY,
|
|
0.0001,
|
|
"daemon entities must retain the fossil terrain-height projection while Ymir owns XZ physics");
|
|
}
|
|
|
|
private static void ConstantWeaponRunsOnDaemonThroughYmirBeamContact()
|
|
{
|
|
var source = Entity(0, 0, "player");
|
|
source.DirectionX = 1;
|
|
source.DirectionY = 0;
|
|
source.TargetEntityIndex = 2;
|
|
source.WeaponGroups = [new[] { 0 }];
|
|
source.Equipment =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "test-beam", Quality = 0, Durability = 1, Enabled = true } },
|
|
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "test-beam-capacitor", Quality = 1, Durability = 1, Enabled = true } }
|
|
];
|
|
source.BehaviorStates = [new AetheriaRuntimeBehaviorStateCommit
|
|
{
|
|
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind, OwnerIndex = 1, BehaviorIndex = 0,
|
|
BehaviorKind = "Capacitor", CapacitorCharge = 10, CapacitorCapacity = 10, CapacitorEfficiency = 1
|
|
}];
|
|
source.CargoContents = [Cargo(("beam-ammo", 1, 0, 0))];
|
|
var blocker = Entity(1, 40, "neutral");
|
|
var selectedTarget = Entity(2, 100, "raider");
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [source, blocker, selectedTarget] };
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "constant-weapon-smoke", CurrentZoneIndex = 0,
|
|
CurrentEntityKey = AetheriaRuntimeRunCheckpointCommit.EntityRecordKey("constant-weapon-smoke", 0, 0),
|
|
Zones = [zone]
|
|
};
|
|
var payload = new AetheriaRuntimeBehaviorPayload(0, AetheriaRuntimeBehaviorKinds.ConstantWeapon, 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(10)),
|
|
new AetheriaRuntimeBehaviorField(6, PerformanceStat(150)),
|
|
new AetheriaRuntimeBehaviorField(9, PerformanceStat(2)),
|
|
new AetheriaRuntimeBehaviorField(10, PerformanceStat(3)),
|
|
new AetheriaRuntimeBehaviorField(11, PerformanceStat(6)),
|
|
new AetheriaRuntimeBehaviorField(12, new AetheriaRuntimeBehaviorValue(
|
|
"item-key", "", 0, false, "", "beam-ammo", [], [])),
|
|
new AetheriaRuntimeBehaviorField(13, Number(3)),
|
|
new AetheriaRuntimeBehaviorField(14, Number(0.3)),
|
|
new AetheriaRuntimeBehaviorField(17, Number(0.15))
|
|
]);
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
|
[WearableWeaponCatalogItem("test-beam", payload), CatalogItem("test-beam-capacitor", CapacitorPayload(10, 1))], [], []);
|
|
var press = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.SetWeaponGroupActive,
|
|
"pilot",
|
|
run.RunId,
|
|
0,
|
|
run.CurrentEntityKey);
|
|
press.CommandId = "constant-weapon-press";
|
|
press.WeaponGroup = 0;
|
|
press.ScalarValue = 1;
|
|
var pressed = AetheriaRuntimeDaemonOperations.Execute(
|
|
run, [press], new AetheriaRuntimeDaemonOperationContext());
|
|
for (var frame = 0; frame < 7; frame++)
|
|
AetheriaRuntimeDaemonSimulation.Step(run,
|
|
frame == 0 ? pressed.Intents : new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
new AetheriaRuntimeDaemonSimulationSettings(),
|
|
NewPhysics(), catalog, frame, frame * 0.1);
|
|
|
|
var state = source.WeaponStates.Single(value => value.BehaviorKind == AetheriaRuntimeBehaviorKinds.ConstantWeapon);
|
|
Require(run.GameEvents.Count(value => value.Kind == "weapon.firing.started") == 1,
|
|
"held constant fire must publish one start transition");
|
|
Require(run.GameEvents.Count(value => value.Kind == "weapon.reload.started") == 1 &&
|
|
run.GameEvents.Count(value => value.Kind == "weapon.firing.stopped") == 1,
|
|
"empty constant weapon magazine must start reload and stop the held effect once");
|
|
Require(state.Reloading && !state.Firing,
|
|
"constant weapon reload must remain authoritative persisted state");
|
|
Require(Math.Abs(Stat(blocker, "hull") - 100) < 0.000001 && Stat(selectedTarget, "hull") < 100,
|
|
"constant weapon must resolve against its firing solution rather than a presentation collider");
|
|
Require(run.ShotReceipts.Any(value => value.TargetEntityIndex == selectedTarget.EntityIndex &&
|
|
value.WeaponItemKey == "test-beam" && value.Hit),
|
|
"daemon must publish accepted continuous shot receipts with authored beam identity");
|
|
Require(CargoQuantity(source, "beam-ammo") == 0,
|
|
"constant weapon reload must consume its reserve cargo through the shared transaction");
|
|
Require(source.BehaviorStates.Single(value => value.BehaviorKind == "Capacitor").CapacitorCharge < 10,
|
|
"constant weapon must pay elapsed-time energy through canonical capacitor state");
|
|
RequireNear(6, source.StatGrids.Single(grid =>
|
|
grid.Name == "transient-visibility:weapon:equipment:0:0").Values.Single(), 0.000001,
|
|
"constant fire must refresh its authored weapon visibility while the beam is committed");
|
|
Require(source.Equipment[0].Item!.Durability < 1,
|
|
"constant fire must apply elapsed-time wear only while its beam transaction commits");
|
|
for (var frame = 7; frame < 11; frame++)
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
new AetheriaRuntimeDaemonSimulationSettings(),
|
|
NewPhysics(), catalog, frame, frame * 0.1);
|
|
var release = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.SetWeaponGroupActive,
|
|
"pilot",
|
|
run.RunId,
|
|
11,
|
|
run.CurrentEntityKey);
|
|
release.CommandId = "constant-weapon-release";
|
|
release.WeaponGroup = 0;
|
|
release.ScalarValue = 0;
|
|
var released = AetheriaRuntimeDaemonOperations.Execute(
|
|
run, [release], new AetheriaRuntimeDaemonOperationContext());
|
|
AetheriaRuntimeDaemonSimulation.Step(run, released.Intents, 0.1,
|
|
new AetheriaRuntimeDaemonSimulationSettings(),
|
|
NewPhysics(), catalog, 11, 1.1);
|
|
Require(!state.Firing && run.GameEvents.Count(value => value.Kind == "weapon.firing.stopped") == 2,
|
|
"one release command must stop daemon-latched constant fire and publish its transition");
|
|
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 11, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
var weaponNodes = Flatten(surface.Surface.Root).Where(node => node.Kind == "weapon.state").ToArray();
|
|
Require(weaponNodes.Any(node =>
|
|
node.Props["behaviorKind"] == AetheriaRuntimeBehaviorKinds.ConstantWeapon &&
|
|
node.Props["itemKey"] == "test-beam" && node.Props["firing"] == "false"),
|
|
$"Eve world entity must project recoverable generic ConstantWeapon state; nodes={string.Join(";", weaponNodes.Select(node => string.Join(",", node.Props.Select(pair => $"{pair.Key}={pair.Value}"))))}");
|
|
Require(Flatten(surface.Surface.Root).Any(node => node.Kind == "feedback.event" &&
|
|
node.Props["eventKind"] == "weapon.firing.started" && node.Props["itemKey"] == "test-beam"),
|
|
"Eve feedback stream must project continuous weapon transition chronology");
|
|
var cockpit = Flatten(surface.Surface.Root).Single(node => node.Id == "aetheria.daemon.game.cockpit");
|
|
Require(cockpit.Kind == "pane" && cockpit.Props["role"] == "pilot.cockpit" &&
|
|
cockpit.Layout["position"] == "absolute" && cockpit.Style["background"] == "transparent",
|
|
"pilot surface must publish a transparent provider-authored cockpit overlay");
|
|
Require(Flatten(cockpit).Count(node => node.Kind == "progress") >= 7 &&
|
|
Flatten(cockpit).Any(node => node.Id == "aetheria.daemon.game.cockpit.capacitor") &&
|
|
Flatten(cockpit).Any(node => node.Id == "aetheria.daemon.game.cockpit.targetLock" &&
|
|
node.Props["label"] == "TARGET LOCK") &&
|
|
Flatten(cockpit).Any(node => node.Id == "aetheria.daemon.game.cockpit.targetHull"),
|
|
"pilot cockpit must expose native ship and target instrumentation through generic Eve progress components");
|
|
Require(Flatten(surface.Surface.Root).Where(node =>
|
|
node.Id is "aetheria.daemon.game.frame" or "aetheria.daemon.game.player" or "aetheria.daemon.game.commands")
|
|
.All(node => node.Layout.TryGetValue("display", out var display) && display == "none"),
|
|
"operator diagnostics must remain published state without becoming pilot-camera UI");
|
|
}
|
|
|
|
private static void ChargedWeaponCannotBypassChargeLifecycle()
|
|
{
|
|
var source = Entity(0, 0, "player");
|
|
source.DirectionX = 1;
|
|
source.TargetEntityIndex = -1;
|
|
source.WeaponGroups = [new[] { 0 }];
|
|
source.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "test-charged", Quality = 0, Durability = 1, Enabled = true }
|
|
}];
|
|
var target = Entity(1, 80, "raider");
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [source, target] };
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "charged-weapon-admission-smoke", CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0", Zones = [zone]
|
|
};
|
|
var payload = new AetheriaRuntimeBehaviorPayload(0, AetheriaRuntimeBehaviorKinds.ChargedWeapon, 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(20)),
|
|
new AetheriaRuntimeBehaviorField(6, PerformanceStat(150)),
|
|
new AetheriaRuntimeBehaviorField(11, PerformanceStat(5)),
|
|
new AetheriaRuntimeBehaviorField(15, PerformanceStat(1000000000)),
|
|
new AetheriaRuntimeBehaviorField(16, PerformanceStat(200)),
|
|
new AetheriaRuntimeBehaviorField(17, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(19, PerformanceStat(0.5)),
|
|
new AetheriaRuntimeBehaviorField(21, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(24, new AetheriaRuntimeBehaviorValue("bool", "", 0, true, "", "", [], [])),
|
|
new AetheriaRuntimeBehaviorField(27, Number(2)),
|
|
new AetheriaRuntimeBehaviorField(30, Number(3))
|
|
]);
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
|
[WearableWeaponCatalogItem("test-charged", payload)], [], []);
|
|
var intents = new AetheriaRuntimeDaemonIntentState();
|
|
intents.WeaponGroups.Add(new AetheriaRuntimeDaemonWeaponGroupIntent
|
|
{
|
|
ActorEntityKey = "zone.0.entity.0", WeaponGroup = 0, Fire = true, Active = true
|
|
});
|
|
AetheriaRuntimeDaemonSimulation.Step(run, intents, 0.1,
|
|
new AetheriaRuntimeDaemonSimulationSettings(),
|
|
NewPhysics(),
|
|
catalog, 0, 0);
|
|
|
|
Require(zone.PhysicalPayloads.Count == 0 && !run.GameEvents.Any(value => value.Kind == "shot.committed"),
|
|
"pressing an authored ChargedWeapon must not bypass charging through the instant weapon resolver");
|
|
Require(Math.Abs(Stat(target, "hull") - 100) < 0.000001,
|
|
"charged weapon admission must not apply damage before a daemon-owned release commits the shot");
|
|
var state = source.WeaponStates.Single(value => value.BehaviorKind == AetheriaRuntimeBehaviorKinds.ChargedWeapon);
|
|
for (var frame = 1; frame < 11; frame++)
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
new AetheriaRuntimeDaemonSimulationSettings(),
|
|
NewPhysics(),
|
|
catalog, frame, frame * 0.1);
|
|
Require(state.Charging && state.Charged && state.Charge >= 1 && state.ChargeHoldSeconds > 0,
|
|
"one semantic request must precharge without a firing solution and enter persisted hold state");
|
|
Require(!run.GameEvents.Any(value => value.Kind == "shot.committed"),
|
|
"ready charged weapon must hold rather than invent a firing solution");
|
|
source.TargetEntityIndex = target.EntityIndex;
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
new AetheriaRuntimeDaemonSimulationSettings(),
|
|
NewPhysics(),
|
|
catalog, 11, 1.1);
|
|
Require(run.GameEvents.Count(value => value.Kind == "weapon.charge.committed") == 1 &&
|
|
run.GameEvents.Any(value => value.Kind == "shot.committed" &&
|
|
value.ItemKey == "test-charged" && Math.Abs(value.ScalarValue - 40) < 0.000001),
|
|
"stored full charge must commit automatically when a firing solution becomes available");
|
|
RequireNear(15, source.StatGrids.Single(grid =>
|
|
grid.Name == "transient-visibility:weapon:equipment:0:0").Values.Single(), 0.000001,
|
|
"charged fire must publish base visibility multiplied by its committed charge modifier");
|
|
Require(source.Equipment[0].Item!.Durability < 1,
|
|
"charged hold must not suppress wear when the stored shot finally commits");
|
|
var miss = run.ShotReceipts.Single(value => value.WeaponItemKey == "test-charged");
|
|
var missDx = miss.EndpointX - target.PositionX;
|
|
var missDz = miss.EndpointZ - target.PositionZ;
|
|
Require(!miss.Hit && miss.AppliedDamage == 0 && miss.HitRoll > miss.HitProbability,
|
|
"extreme authored dispersion must deterministically resolve a miss without damage");
|
|
RequireEqual("none", miss.ImpactKind, "a miss must not advertise a fabricated impact response");
|
|
RequireEqual("bolt", miss.PresentationKind, "instant weapon receipts must advertise a runtime-neutral travelling effect");
|
|
Require(Math.Sqrt(missDx * missDx + missDz * missDz) > 20 &&
|
|
miss.ImpactAngleRoll >= 0 && miss.ImpactAngleRoll < 1 &&
|
|
miss.ImpactRadiusRoll >= 0 && miss.ImpactRadiusRoll < 1,
|
|
"miss receipt must carry independent named impact rolls and an endpoint outside the target silhouette");
|
|
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 11, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
Require(Flatten(surface.Surface.Root).Any(node => node.Kind == "weapon.state" &&
|
|
node.Props.ContainsKey("chargeMalfunctionRisk")),
|
|
"Eve must expose charged hold duration and malfunction risk generically");
|
|
Require(Flatten(surface.Surface.Root).Any(node => node.Kind == "feedback.event" &&
|
|
node.Props["eventKind"] == "weapon.charge.committed"),
|
|
"Eve feedback must expose charged solution commit chronology");
|
|
}
|
|
|
|
private static void InstantWeaponRequestSurvivesLockAcquisition()
|
|
{
|
|
var source = Entity(0, 0, "player");
|
|
source.DirectionX = 1;
|
|
source.VelocityX = 1;
|
|
source.TargetEntityIndex = 1;
|
|
source.Contacts = [new AetheriaRuntimeEntityContactCommit
|
|
{
|
|
TargetEntityIndex = 1, InfoGathered = 1, Hostile = true, Visible = true
|
|
}];
|
|
source.WeaponGroups = [new[] { 0 }];
|
|
source.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "test-instant", Quality = 0, Durability = 1, Enabled = true }
|
|
}];
|
|
var target = Entity(1, 80, "raider");
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [source, target],
|
|
Bodies =
|
|
[
|
|
new AetheriaRuntimeBodySnapshotCommit
|
|
{
|
|
Kind = "sun",
|
|
Mass = 16,
|
|
SunVisual = new AetheriaRuntimeSunVisualCommit
|
|
{
|
|
LightColorX = 0.5,
|
|
LightColorY = 0.75,
|
|
LightColorZ = 1,
|
|
LightRadiusMultiplier = 1
|
|
}
|
|
}
|
|
]
|
|
};
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "instant-trigger-smoke", CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0", Zones = [zone]
|
|
};
|
|
var payload = new AetheriaRuntimeBehaviorPayload(0, AetheriaRuntimeBehaviorKinds.InstantWeapon, 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(20)),
|
|
new AetheriaRuntimeBehaviorField(6, PerformanceStat(150)),
|
|
new AetheriaRuntimeBehaviorField(11, PerformanceStat(8)),
|
|
new AetheriaRuntimeBehaviorField(15, PerformanceStat(0)),
|
|
new AetheriaRuntimeBehaviorField(16, PerformanceStat(200)),
|
|
new AetheriaRuntimeBehaviorField(17, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(19, PerformanceStat(0.5)),
|
|
new AetheriaRuntimeBehaviorField(23, PerformanceStat(45)),
|
|
new AetheriaRuntimeBehaviorField(24, PerformanceStat(0)),
|
|
new AetheriaRuntimeBehaviorField(25, PerformanceStat(1))
|
|
]);
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([WearableWeaponCatalogItem("test-instant", payload)], [], []);
|
|
RequireNear(45,
|
|
AetheriaRuntimeEquippedBehaviorQueries.Find(source, catalog, AetheriaRuntimeBehaviorKinds.InstantWeapon)
|
|
.Single().EvaluateStat(23),
|
|
0.000001,
|
|
"lock-transition fixture must retain its authored 45-degree acquisition cone");
|
|
var fire = new AetheriaRuntimeDaemonIntentState();
|
|
fire.WeaponGroups.Add(new AetheriaRuntimeDaemonWeaponGroupIntent
|
|
{
|
|
ActorEntityKey = "zone.0.entity.0", WeaponGroup = 0, Fire = true, Active = true
|
|
});
|
|
|
|
AetheriaRuntimeDaemonSimulation.Step(run, fire, 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), catalog, 0, 0);
|
|
for (var frame = 1; frame < 12 && run.ShotReceipts.Count == 0; frame++)
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), catalog, frame, frame * 0.1);
|
|
|
|
Require(run.ShotReceipts.Count == 1 && run.ShotReceipts[0].SourceEntityIndex == source.EntityIndex,
|
|
$"one instant weapon request must remain pending until daemon lock acquisition commits its shot; " +
|
|
$"receipts={run.ShotReceipts.Count}, direction={source.DirectionX},{source.DirectionY}, contacts={string.Join(";", (source.Contacts ?? []).Select(value => $"{value.TargetEntityIndex}:{value.InfoGathered}"))}, states={string.Join(";", (source.WeaponStates ?? []).Select(value => $"{value.BehaviorKind}:pending={value.TriggerPending}:lock={value.LockProgress}:target={value.LockTargetEntityIndex}:burst={value.BurstRemaining}"))}");
|
|
Require(run.GameEvents.Count(value => value.Kind == "weapon.lock.started" &&
|
|
value.SourceEntityIndex == 0 && value.TargetEntityIndex == 1) == 1 &&
|
|
run.GameEvents.Count(value => value.Kind == "weapon.lock.acquired" &&
|
|
value.SourceEntityIndex == 0 && value.TargetEntityIndex == 1) == 1,
|
|
"daemon lock acquisition must publish one start and one completed transition instead of client-timed feedback");
|
|
var state = source.WeaponStates.Single(value => value.BehaviorKind == AetheriaRuntimeBehaviorKinds.InstantWeapon);
|
|
Require(!state.TriggerPending,
|
|
"instant weapon request must clear after the committed burst begins");
|
|
var weaponVisibility = source.StatGrids.Single(grid =>
|
|
grid.Name == "transient-visibility:weapon:equipment:0:0");
|
|
RequireNear(8, weaponVisibility.Values.Single(), 0.000001,
|
|
"committed instant shot must publish its authored visibility as a named transient source");
|
|
Require(source.Equipment[0].Item!.Durability < 1,
|
|
"committed instant shot must apply one round of thermal wear to its owning equipment");
|
|
AetheriaRuntimeVisibilitySimulation.BeginTick([source], 1, 0.5);
|
|
RequireNear(8 * Math.Exp(-0.5), weaponVisibility.Values.Single(), 0.000001,
|
|
"weapon visibility must use the fossil exponential source decay rather than vanish with the effect");
|
|
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 12, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
var combat = Flatten(surface.Surface.Root).Single(node => node.Kind == "combat.presentation");
|
|
var world = Flatten(surface.Surface.Root).Single(node => node.Kind == "world.scene3d");
|
|
var aim = Flatten(surface.Surface.Root).Single(node => node.Kind == "aim.presentation");
|
|
Require(!string.IsNullOrWhiteSpace(world.Props["lookCommand"]) &&
|
|
world.Props["lookModel"] == "planar-yaw.v1" &&
|
|
world.Props["lookSensitivityRadians"] == "-0.001",
|
|
"playable world must advertise the daemon-owned continuous look command and fossil pointer response");
|
|
Require(world.Props["cameraRig"] == "perspective.entity-forward-follow.v1" &&
|
|
world.Props["cameraLookAt"] == "aim.convergence-point.v1" &&
|
|
world.Props["cameraDistance"] == "30" &&
|
|
world.Props["cameraVerticalFieldOfViewDegrees"] == "60" &&
|
|
world.Props["cameraTargetScreenX"] == "0.64" &&
|
|
world.Props["cameraTargetScreenY"] == "0.19" &&
|
|
world.Props["cameraPositionDamping"] == "0" &&
|
|
world.Props["cameraNearClipPlane"] == "1" &&
|
|
world.Props["cameraFarClipPlane"] == "4096",
|
|
"undocked world must translate the authored ARPG Third Person Rig into bottom-origin Eve viewport coordinates");
|
|
Require(!world.Props.ContainsKey("skyboxAssetRef") &&
|
|
world.Props["ambientLightColor"] == "0.5,0.75,1" &&
|
|
world.Props["reflectionAssetRef"] == "texture.environment.reflection" &&
|
|
world.Props["postProcessProfileAssetRef"] == "profile.environment.flight" &&
|
|
world.Props["colorGradingSpace"] == "hdr-before-tonemap.v1" &&
|
|
world.Props["exposureMode"] == "histogram.v1" &&
|
|
world.Props["exposureLowPercent"] == "47.37294" &&
|
|
world.Props["exposureHighPercent"] == "99" &&
|
|
world.Props["exposureMinimumEv"] == "-3" &&
|
|
world.Props["exposureMaximumEv"] == "0.3" &&
|
|
world.Props["exposureKeyValue"] == "0.5" &&
|
|
world.Props["exposureAdaptation"] == "progressive" &&
|
|
world.Props["exposureSpeedUp"] == "2" &&
|
|
world.Props["exposureSpeedDown"] == "1" &&
|
|
world.Props["cameraReconstruction"] == "temporal-reprojection.v1" &&
|
|
world.Props["temporalQuality"] == "high" &&
|
|
world.Props["temporalHistoryBlend"] == "0.99" &&
|
|
world.Props["temporalJitterScale"] == "0.1" &&
|
|
world.Props["temporalSharpening"] == "0" &&
|
|
world.Props["reflectionIntensity"] == "1" &&
|
|
world.Props["ambientLightIntensity"] == "1.46" &&
|
|
!world.Props.ContainsKey("keyLightDirection") &&
|
|
!world.Props.ContainsKey("keyLightColor") &&
|
|
!world.Props.ContainsKey("keyLightIntensity"),
|
|
"playable world must light from stellar tint, use the studio HDRI only for reflections, and leave the raymarch pass in charge of the frame");
|
|
Require(aim.Props["controlledEntityIndex"] == "0" &&
|
|
aim.Props["convergenceTargetEntityId"] == run.EntityRecordKey(0, 1) &&
|
|
aim.Props["minimumConvergenceDistance"] == "50",
|
|
"Eve aim presentation must bind the controlled body direction to the fossil convergence semantic");
|
|
Require(combat.Props["controlledEntityIndex"] == "0" &&
|
|
combat.Props["selectedTargetEntityIndex"] == "1" &&
|
|
combat.Props["targetVisible"] == "true" &&
|
|
combat.Props["targetHostile"] == "true" &&
|
|
double.Parse(combat.Props["lockProgress"], CultureInfo.InvariantCulture) > 0.99,
|
|
"Eve combat presentation must expose daemon-owned selection, contact, and completed lock without duplicating body transforms");
|
|
var cockpitLock = Flatten(surface.Surface.Root).Single(node =>
|
|
node.Id == "aetheria.daemon.game.cockpit.targetLock");
|
|
Require(cockpitLock.Kind == "progress" && cockpitLock.Props["label"] == "TARGET LOCK" &&
|
|
double.Parse(cockpitLock.Props["value"], CultureInfo.InvariantCulture) > 0.99,
|
|
"provider cockpit must project the same completed daemon lock through a generic Eve progress component");
|
|
Require(combat.Props["hitMarkerDurationSeconds"] == "0.25" &&
|
|
combat.Props["radialFillMinimum"] == "0.25" &&
|
|
combat.Props["radialFillMaximum"] == "0.75",
|
|
"Eve combat presentation must preserve the fossil's presentation timing and radial meter semantics");
|
|
|
|
var secondTarget = Entity(2, 100, "raider");
|
|
zone.Entities = [source, target, secondTarget];
|
|
source.TargetEntityIndex = secondTarget.EntityIndex;
|
|
source.Contacts =
|
|
[
|
|
new AetheriaRuntimeEntityContactCommit
|
|
{ TargetEntityIndex = 1, InfoGathered = 1, Hostile = true, Visible = true },
|
|
new AetheriaRuntimeEntityContactCommit
|
|
{ TargetEntityIndex = 2, InfoGathered = 1, Hostile = true, Visible = true }
|
|
];
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), catalog, 13, 1.3);
|
|
Require(state.LockTargetEntityIndex == 2 && state.LockProgress > 0 && state.LockProgress < 0.99 &&
|
|
run.GameEvents.Count(value => value.Kind == "weapon.lock.lost" &&
|
|
value.TargetEntityIndex == 1 && value.Reason == "target-changed") == 1 &&
|
|
run.GameEvents.Count(value => value.Kind == "weapon.lock.started" &&
|
|
value.TargetEntityIndex == 2) == 1 &&
|
|
run.GameEvents.Count(value => value.Kind == "weapon.lock.acquired" &&
|
|
value.TargetEntityIndex == 2) == 0,
|
|
$"idle authored lock behavior must tick every daemon step and transfer chronology exactly once on target change; " +
|
|
$"target={state.LockTargetEntityIndex} progress={state.LockProgress:0.###} events={string.Join('|', run.GameEvents.Where(value => value.Kind.StartsWith("weapon.lock.", StringComparison.Ordinal)).Select(value => $"{value.Kind}:{value.TargetEntityIndex}:{value.Reason}:{value.AuxiliaryValue:0.###}->{value.ScalarValue:0.###}"))}");
|
|
|
|
for (var frame = 14; frame <= 17; frame++)
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), catalog, frame, frame * 0.1);
|
|
Require(state.LockProgress > 0.99 &&
|
|
run.GameEvents.Count(value => value.Kind == "weapon.lock.started" &&
|
|
value.TargetEntityIndex == 2) == 1 &&
|
|
run.GameEvents.Count(value => value.Kind == "weapon.lock.acquired" &&
|
|
value.TargetEntityIndex == 2) == 1,
|
|
"idle authored lock behavior must continue progressive acquisition without duplicating transition events");
|
|
|
|
source.DirectionX = -1;
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), catalog, 18, 1.8);
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), catalog, 19, 1.9);
|
|
Require(state.LockProgress < 0.99 &&
|
|
run.GameEvents.Count(value => value.Kind == "weapon.lock.lost" &&
|
|
value.TargetEntityIndex == 2 && value.Reason == "angle") == 1,
|
|
$"crossing the authored lock angle must publish one loss transition while continued decay remains silent; " +
|
|
$"direction={source.DirectionX},{source.DirectionY} source={source.PositionX:0.###},{source.PositionZ:0.###} target={secondTarget.PositionX:0.###},{secondTarget.PositionZ:0.###} progress={state.LockProgress:0.###} events={string.Join('|', run.GameEvents.Where(value => value.Kind.StartsWith("weapon.lock.", StringComparison.Ordinal)).Select(value => $"{value.Kind}:{value.TargetEntityIndex}:{value.Reason}:{value.AuxiliaryValue:0.###}->{value.ScalarValue:0.###}"))}");
|
|
|
|
source.DirectionX = 1;
|
|
for (var frame = 20; frame <= 21; frame++)
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), catalog, frame, frame * 0.1);
|
|
Require(state.LockProgress > 0.99 &&
|
|
run.GameEvents.Count(value => value.Kind == "weapon.lock.acquired" &&
|
|
value.TargetEntityIndex == 2) == 2,
|
|
"restored facing must reacquire through a second completed transition without replaying lock start");
|
|
|
|
zone.Entities = [source, target];
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), catalog, 22, 2.2);
|
|
Require(state.LockTargetEntityIndex == -1 && state.LockProgress == 0 &&
|
|
run.ShotReceipts.Count == 1 &&
|
|
run.GameEvents.Count(value => value.Kind == "weapon.lock.lost" &&
|
|
value.TargetEntityIndex == 2 && value.Reason == "target-invalid") == 1,
|
|
$"an invalid target must clear an idle weapon's authoritative lock and publish one loss transition; " +
|
|
$"target={state.LockTargetEntityIndex} progress={state.LockProgress:0.###} shots={run.ShotReceipts.Count} events={string.Join('|', run.GameEvents.Where(value => value.Kind.StartsWith("weapon.lock.", StringComparison.Ordinal)).Select(value => $"{value.Kind}:{value.TargetEntityIndex}:{value.Reason}:{value.AuxiliaryValue:0.###}->{value.ScalarValue:0.###}"))}");
|
|
var resetSurface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 22, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
Require(Flatten(resetSurface.Surface.Root).Any(node => node.Kind == "feedback.event" &&
|
|
node.Props["eventKind"] == "weapon.lock.lost" &&
|
|
node.Props["targetEntityIndex"] == "2" &&
|
|
node.Props["reason"] == "target-invalid" &&
|
|
double.Parse(node.Props["auxiliaryValue"], CultureInfo.InvariantCulture) > 0.99),
|
|
"Eve feedback must project the daemon lock-loss transition and its prior completed progress");
|
|
}
|
|
|
|
private static void GuidedWeaponPublishesPresentationProfileWithoutPhysicalAuthority()
|
|
{
|
|
var source = Entity(0, 0, "player");
|
|
source.DirectionX = 1;
|
|
source.TargetEntityIndex = 1;
|
|
source.Contacts = [new AetheriaRuntimeEntityContactCommit
|
|
{
|
|
TargetEntityIndex = 1, InfoGathered = 1, Hostile = true, Visible = true
|
|
}];
|
|
source.WeaponGroups = [new[] { 0 }];
|
|
source.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "test-launcher", Quality = 1, Durability = 1, Enabled = true }
|
|
}];
|
|
var target = Entity(1, 80, "raider");
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [source, target] };
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "guided-presentation-smoke", CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0", Zones = [zone]
|
|
};
|
|
var guidance = Curve(
|
|
new AetheriaRuntimeCurveKey(0, 0.25, 0, 0),
|
|
new AetheriaRuntimeCurveKey(1, 1, 0, 0));
|
|
var thrust = Curve(
|
|
new AetheriaRuntimeCurveKey(0, 1, 0, 0),
|
|
new AetheriaRuntimeCurveKey(1, 0.5, 0, 0));
|
|
var lift = Curve(
|
|
new AetheriaRuntimeCurveKey(0, 0, 0, 0),
|
|
new AetheriaRuntimeCurveKey(1, 0.75, 0, 0));
|
|
var payload = new AetheriaRuntimeBehaviorPayload(0, AetheriaRuntimeBehaviorKinds.Launcher, 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(10)),
|
|
new AetheriaRuntimeBehaviorField(6, PerformanceStat(150)),
|
|
new AetheriaRuntimeBehaviorField(16, PerformanceStat(20)),
|
|
new AetheriaRuntimeBehaviorField(17, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(19, PerformanceStat(0.1)),
|
|
new AetheriaRuntimeBehaviorField(21, PerformanceStat(100)),
|
|
new AetheriaRuntimeBehaviorField(22, PerformanceStat(0)),
|
|
new AetheriaRuntimeBehaviorField(23, PerformanceStat(180)),
|
|
new AetheriaRuntimeBehaviorField(24, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(25, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(26, guidance),
|
|
new AetheriaRuntimeBehaviorField(27, thrust),
|
|
new AetheriaRuntimeBehaviorField(28, lift),
|
|
new AetheriaRuntimeBehaviorField(29, PerformanceStat(5)),
|
|
new AetheriaRuntimeBehaviorField(30, Number(2)),
|
|
new AetheriaRuntimeBehaviorField(31, PerformanceStat(40))
|
|
]);
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([CatalogItem("test-launcher", payload)], [], []);
|
|
var fire = new AetheriaRuntimeDaemonIntentState();
|
|
fire.WeaponGroups.Add(new AetheriaRuntimeDaemonWeaponGroupIntent
|
|
{ ActorEntityKey = run.CurrentEntityKey, WeaponGroup = 0, Fire = true, Active = true });
|
|
|
|
for (var frame = 0; frame < 4 && run.ShotReceipts.Count == 0; frame++)
|
|
AetheriaRuntimeDaemonSimulation.Step(run,
|
|
frame == 0 ? fire : new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), catalog, frame, frame * 0.1);
|
|
|
|
RequireEqual(0, zone.PhysicalPayloads.Count,
|
|
"fossil guided presentation must not be promoted into a damage-owning Ymir body");
|
|
var receipt = run.ShotReceipts.Single(value => value.WeaponItemKey == "test-launcher");
|
|
RequireEqual("guided", receipt.PresentationKind,
|
|
"Launcher receipts must select the generic guided presentation path");
|
|
RequireEqual("target-entity", receipt.GuidanceMode,
|
|
"Launcher guidance must retain its fossil target-entity mode");
|
|
RequireEqual(2, receipt.GuidanceCurve.Count,
|
|
"guided receipt must retain the complete authored guidance curve");
|
|
RequireNear(5, receipt.GuidanceThrust, 0.000001,
|
|
"guided receipt must retain evaluated authored thrust");
|
|
RequireNear(40, receipt.GuidanceTopSpeed, 0.000001,
|
|
"guided receipt must retain evaluated authored missile velocity");
|
|
RequireNear(2, receipt.GuidanceDodgeFrequency, 0.000001,
|
|
"guided receipt must retain authored dodge frequency");
|
|
var restored = MessagePack.MessagePackSerializer.Deserialize<AetheriaRuntimeRunCheckpointCommit>(
|
|
MessagePack.MessagePackSerializer.Serialize(run));
|
|
RequireEqual("target-entity", restored.ShotReceipts.Single().GuidanceMode,
|
|
"guided presentation levers must survive daemon checkpoint restart");
|
|
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 4, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
var shot = Flatten(surface.Surface.Root).Single(node =>
|
|
node.Kind == "shot.receipt" && node.Props.TryGetValue("itemKey", out var itemKey) &&
|
|
itemKey == "test-launcher");
|
|
Require(shot.Props["guidanceMode"] == "target-entity" &&
|
|
shot.Props["guidanceCurve"] == "0,0.25,0,0;1,1,0,0" &&
|
|
shot.Props["guidanceThrustCurve"] == "0,1,0,0;1,0.5,0,0" &&
|
|
shot.Props["guidanceLiftCurve"] == "0,0,0,0;1,0.75,0,0",
|
|
"generic Eve shot facts must expose the authored guidance curves without provider internals");
|
|
}
|
|
|
|
private static void DeployableWeaponRunsThroughYmirAndDetonatesOnDaemon()
|
|
{
|
|
var source = Entity(0, 0, "player");
|
|
source.DirectionX = 1;
|
|
source.WeaponGroups = [new[] { 0 }];
|
|
source.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "test-mine-layer", Quality = 0, Durability = 1, Enabled = true }
|
|
}];
|
|
var target = Entity(1, 50, "neutral");
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [source, target] };
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "deployable-smoke", CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0", Zones = [zone]
|
|
};
|
|
var payload = new AetheriaRuntimeBehaviorPayload(0, AetheriaRuntimeBehaviorKinds.DeployableWeapon, 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(35)),
|
|
new AetheriaRuntimeBehaviorField(6, PerformanceStat(100)),
|
|
new AetheriaRuntimeBehaviorField(11, PerformanceStat(4)),
|
|
new AetheriaRuntimeBehaviorField(16, PerformanceStat(10)),
|
|
new AetheriaRuntimeBehaviorField(17, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(19, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(26, Number(0.2)),
|
|
new AetheriaRuntimeBehaviorField(27, Number(5)),
|
|
new AetheriaRuntimeBehaviorField(28, PerformanceStat(30)),
|
|
new AetheriaRuntimeBehaviorField(29, Number(0.2)),
|
|
new AetheriaRuntimeBehaviorField(30, PerformanceStat(50))
|
|
]);
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
|
[WearableWeaponCatalogItem("test-mine-layer", payload)], [], []);
|
|
var fire = new AetheriaRuntimeDaemonIntentState();
|
|
fire.WeaponGroups.Add(new AetheriaRuntimeDaemonWeaponGroupIntent
|
|
{ ActorEntityKey = "zone.0.entity.0", WeaponGroup = 0, Fire = true, Active = true });
|
|
|
|
AetheriaRuntimeDaemonSimulation.Step(run, fire, 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), catalog, 1, 0.1);
|
|
RequireEqual(1, zone.PhysicalPayloads.Count, "deployable fire must create one persistent physical payload");
|
|
Require(!run.ShotReceipts.Any(), "deployable fire must not masquerade as an ordinary shot receipt");
|
|
RequireNear(4, source.StatGrids.Single(grid =>
|
|
grid.Name == "transient-visibility:weapon:equipment:0:0").Values.Single(), 0.000001,
|
|
"successful deployment must publish the launcher's authored weapon visibility");
|
|
Require(source.Equipment[0].Item!.Durability < 1,
|
|
"successful deployment must apply one round of thermal wear to its launcher");
|
|
var mine = zone.PhysicalPayloads.Single();
|
|
Require(mine.PositionX > 2 && mine.TriggeredAtSeconds < 0,
|
|
"Ymir must advance the unarmed mine without inventing a trigger");
|
|
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), catalog, 2, 0.2);
|
|
mine = zone.PhysicalPayloads.Single();
|
|
Require(mine.Stationary && mine.TriggeredAtSeconds >= 0 &&
|
|
run.GameEvents.Count(value => value.Kind == "deployable.triggered") == 1,
|
|
"an armed Ymir contact must become one daemon-owned trigger transition");
|
|
RequireNear(100, Stat(target, "hull"), 0.000001,
|
|
"trigger contact must not bypass the authored detonation delay");
|
|
|
|
using var soaCache = new CultCache();
|
|
using var soaPublisher = new AetheriaRuntimeDaemonSoaFramePublisher(soaCache, producerEpoch: 1);
|
|
var soaFrame = soaPublisher.BuildCurrentZoneEntities(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 2, Run = run });
|
|
var mineIdentity = soaFrame.View.Identities.Single(identity =>
|
|
identity.EntityId == $"{run.RunId}:zone:0:physical-payload:{mine.PayloadId}");
|
|
Require(mineIdentity.Kind == "physical-payload" && mineIdentity.AssetRef == "prefab.entity.mine" &&
|
|
!mineIdentity.Selectable && mineIdentity.EntityIndex < -1,
|
|
"the playable SoA must carry the retained Ymir mine through its provider-owned asset identity");
|
|
|
|
var objects = AetheriaRuntimeGameDocuments.ObjectsViewport(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 2, Run = run },
|
|
new AetheriaRuntimeViewportBounds { MinX = -1000, MinY = -1000, MaxX = 1000, MaxY = 1000 });
|
|
var mineObject = objects.Objects.Single(value => value.EntityKey == mine.PayloadId);
|
|
RequireEqual("projectile", mineObject.Kind,
|
|
"the viewport projection must expose the retained physical payload without reviving world.scene3d as a transform authority");
|
|
Require(mineObject.IsActive && mineObject.IconAsset != null,
|
|
"the viewport projection must expose an active provider-authored presentation for the retained mine body");
|
|
Require(mine.Stationary && mine.TriggeredAtSeconds >= 0,
|
|
"daemon state must retain Ymir-derived motion and trigger state independently of presentation");
|
|
RequireNear(35, mine.PayloadMagnitude, 0.000001, "deployable must retain authored payload magnitude");
|
|
RequireNear(50, mine.BlastRadius, 0.000001, "deployable must retain authored blast radius");
|
|
Require(Math.Abs(target.PositionX - mine.PositionX) <= mine.BlastRadius,
|
|
$"triggered target must remain inside authored blast radius (mine {mine.PositionX}, target {target.PositionX})");
|
|
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.21,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), catalog, 3, 0.41);
|
|
RequireEqual(0, zone.PhysicalPayloads.Count, "detonated mine must leave canonical payload state");
|
|
RequireNear(65, Stat(target, "hull"), 0.000001,
|
|
"Aetheria must apply authored blast damage after the delay");
|
|
Require(run.GameEvents.Count(value => value.Kind == "deployable.detonated") == 1,
|
|
"daemon must publish one authoritative detonation event");
|
|
}
|
|
|
|
private static void DeployableRangeExpiryDetonatesAfterYmirMovement()
|
|
{
|
|
var source = Entity(0, -50, "player");
|
|
var target = Entity(1, 35, "neutral");
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [source, target],
|
|
PhysicalPayloads = [new AetheriaRuntimePhysicalPayloadCommit
|
|
{
|
|
PayloadId = "range-expiry-mine",
|
|
PayloadKind = "mine",
|
|
WeaponItemKey = "canonical-mine",
|
|
SourceEntityIndex = source.EntityIndex,
|
|
PositionX = 5,
|
|
VelocityX = 20,
|
|
Radius = 1,
|
|
ActivationDelaySeconds = 10,
|
|
LifetimeSeconds = 30,
|
|
MaximumSourceDistance = 6,
|
|
BlastRadius = 50,
|
|
PayloadMagnitude = 15,
|
|
TriggeredAtSeconds = -1,
|
|
Active = true
|
|
}]
|
|
};
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "range-expiry-smoke", CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0", Zones = [zone]
|
|
};
|
|
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), frameId: 4,
|
|
simulationTimeSeconds: 0.1);
|
|
|
|
RequireEqual(0, zone.PhysicalPayloads.Count,
|
|
"mine crossing source-relative range after Ymir movement must detonate in the same tick");
|
|
RequireNear(85, Stat(target, "hull"), 0.000001,
|
|
"range expiry must preserve the fossil splash consequence");
|
|
Require(run.GameEvents.Any(value => value.Kind == "deployable.expired" &&
|
|
value.Reason == "range" && value.ItemKey == "canonical-mine"),
|
|
"daemon must explain range-triggered expiry with stable item identity");
|
|
Require(run.GameEvents.Any(value => value.Kind == "deployable.detonated" &&
|
|
value.Reason == "range" && value.ItemKey == "canonical-mine"),
|
|
"range expiry must flow through the shared detonation transaction");
|
|
}
|
|
|
|
private static void CanonicalCatalogPublishesRecoveredMineLauncher()
|
|
{
|
|
var catalog = AetheriaRuntimeCatalogStore.OpenReadOnly(
|
|
Path.Combine(Directory.GetCurrentDirectory(), "GameData", "aetheria-world.cc"));
|
|
var drives = catalog.Items
|
|
.Where(item => item.BehaviorKinds.Contains(AetheriaRuntimeBehaviorKinds.AetherDrive))
|
|
.ToArray();
|
|
var driveHulls = catalog.Items
|
|
.Where(item => item.Hardpoints.Any(point => point.Type == "AetherDrive"))
|
|
.ToArray();
|
|
Require(drives.Length == 1 && drives[0].Name == "Traction" &&
|
|
drives[0].HardpointType == "AetherDrive" && drives[0].Price == 250000 &&
|
|
drives[0].Tags.Contains("rarity:rare", StringComparer.Ordinal),
|
|
"canonical AetherDrive must remain the singular expensive Traction retrofit");
|
|
Require(driveHulls.Length == 1 && driveHulls[0].Name == "LonginusX" &&
|
|
driveHulls[0].Price == 7500000 &&
|
|
driveHulls[0].Tags.Contains("rarity:rare", StringComparer.Ordinal),
|
|
"canonical AetherDrive compatibility must remain confined to the rare modified LonginusX hull");
|
|
var ordinaryShipHulls = catalog.Items.Where(item => item.HullType == "Ship" &&
|
|
item.Hardpoints.All(point => point.Type != "AetherDrive") &&
|
|
!item.Tags.Contains("rarity:rare", StringComparer.Ordinal))
|
|
.ToArray();
|
|
Require(ordinaryShipHulls.Any(item => item.Name == "Longinus") &&
|
|
ordinaryShipHulls.Any(item => item.Name == "Djinni"),
|
|
"the recovered common Longinus and Djinni must own ordinary ship generation");
|
|
var mine = catalog.Items.Single(item =>
|
|
item.ItemKey == "aetheria.item_definition:legacy:e78d3670-3ac6-4d9b-834c-1da4228ac311");
|
|
RequireEqual("Mine Launcher", mine.Name, "recovered item must retain fossil identity");
|
|
RequireEqual("Mine", mine.WeaponType, "recovered item must publish normalized semantic weapon type");
|
|
var behavior = mine.BehaviorPayloads.Single(value =>
|
|
value.Kind == AetheriaRuntimeBehaviorKinds.DeployableWeapon);
|
|
RequireNear(2, behavior.Fields.Single(field => field.Key == 26).Value.NumberValue, 0.000001,
|
|
"catalog mine must retain two-second activation delay");
|
|
RequireNear(30, behavior.Fields.Single(field => field.Key == 27).Value.NumberValue, 0.000001,
|
|
"catalog mine must retain thirty-second lifetime");
|
|
var manifest = AetheriaRuntimeAssets.ProjectManifest(catalog);
|
|
Require(!manifest.Assets.Any(value => value.Ref.AssetKey == "material.environment.skybox"),
|
|
"provider catalog must not retain the obsolete skybox authority when raymarched fog owns the frame");
|
|
var reflection = manifest.Assets.Single(value => value.Ref.AssetKey == "texture.environment.reflection");
|
|
RequireEqual(AetheriaRuntimeAssetKinds.Texture, reflection.Ref.Kind,
|
|
"provider must type the pre-generated environment map as a texture asset");
|
|
RequireEqual("environment.reflection", reflection.Ref.Metadata["presentationRole"],
|
|
"provider must advertise the generic reflection presentation role");
|
|
RequireEqual("Assets/Textures/studio2.hdr", reflection.Ref.Metadata["unityAssetPath"],
|
|
"provider bundle must own the pre-generated reflection map without an importer plugin");
|
|
var asset = manifest.Assets.Single(value => value.Ref.AssetKey == "prefab.entity.mine");
|
|
RequireEqual("physical-payload.mine", asset.Ref.Metadata["presentationRole"],
|
|
"provider must advertise the script-free mine presentation role");
|
|
RequireEqual("0.25", asset.Ref.Metadata["triggeredPulseSeconds"],
|
|
"mine presentation must retain the fossil triggered pulse cadence");
|
|
|
|
var rareHull = driveHulls.Single();
|
|
var rareHullAssetKey = AetheriaRuntimeAssets.HullPrefabAssetKey(rareHull.ItemKey);
|
|
var rareHullAsset = manifest.Assets.Single(value => value.Ref.AssetKey == rareHullAssetKey);
|
|
var expectedHullResourcePath = rareHull.HullPrefab.Replace('\\', '/');
|
|
const string resourcesPrefix = "Assets/Resources/";
|
|
if (expectedHullResourcePath.StartsWith(resourcesPrefix, StringComparison.OrdinalIgnoreCase))
|
|
expectedHullResourcePath = expectedHullResourcePath[resourcesPrefix.Length..];
|
|
if (expectedHullResourcePath.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
|
|
expectedHullResourcePath = expectedHullResourcePath[..^".prefab".Length];
|
|
RequireEqual("entity.hull", rareHullAsset.Ref.Metadata["presentationRole"],
|
|
"provider manifest must type catalog hull prefabs as generic hull presentation assets");
|
|
RequireEqual(expectedHullResourcePath, rareHullAsset.Ref.Metadata["resourcesPath"],
|
|
"provider hull asset must retain the catalog-authored prefab instead of a faction fallback");
|
|
|
|
var rareShip = Entity(0, 0, "player");
|
|
rareShip.HullItemKey = rareHull.ItemKey;
|
|
var rareRun = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "rare-hull-presentation-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [rareShip] }]
|
|
};
|
|
var rareFrame = new AetheriaRuntimeDaemonFrameDocument { FrameId = 1, Run = rareRun };
|
|
var rareSurface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
rareFrame,
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"),
|
|
catalog: catalog);
|
|
var rareSurfaceEntity = Flatten(rareSurface.Surface.Root).Single(node =>
|
|
node.Id == "aetheria.daemon.game.world.entity.0");
|
|
RequireEqual(rareHullAssetKey, rareSurfaceEntity.Props["assetRef"],
|
|
"Eve scene projection must select the typed hull asset rather than the player Djinni fallback");
|
|
using var rareSoaCache = new CultCache();
|
|
using var rareSoaPublisher = new AetheriaRuntimeDaemonSoaFramePublisher(rareSoaCache, producerEpoch: 1);
|
|
var rareSoa = rareSoaPublisher.BuildCurrentZoneEntities(rareFrame, catalog);
|
|
RequireEqual(rareHullAssetKey, rareSoa.View.Identities.Single().AssetRef,
|
|
"SoA identity and Eve scene projection must share the catalog-owned hull asset key");
|
|
}
|
|
|
|
private static void EnergyFundedShieldInterceptsDamageBeforeHull()
|
|
{
|
|
var source = Entity(0, -50, "player");
|
|
var target = Entity(1, 0, "neutral");
|
|
target.Equipment =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "test-energy-shield", Quality = 1, Durability = 1, Enabled = true } },
|
|
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "test-shield-capacitor", Quality = 1, Durability = 1, Enabled = true } }
|
|
];
|
|
target.BehaviorStates = [new AetheriaRuntimeBehaviorStateCommit
|
|
{
|
|
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind, OwnerIndex = 1, BehaviorIndex = 0,
|
|
BehaviorKind = "Capacitor", CapacitorCharge = 100, CapacitorCapacity = 100,
|
|
CapacitorEfficiency = 1
|
|
}];
|
|
var shield = new AetheriaRuntimeBehaviorPayload(0, "Shield", 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, PerformanceStat(2)),
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(3))
|
|
]);
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
|
[CatalogItem("test-energy-shield", shield), CatalogItem("test-shield-capacitor", CapacitorPayload(100, 1))], [], []);
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [source, target] };
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "energy-shield-smoke", CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0", Zones = [zone]
|
|
};
|
|
|
|
void AddDetonatingPayload(string id, double damage)
|
|
{
|
|
zone.PhysicalPayloads = [new AetheriaRuntimePhysicalPayloadCommit
|
|
{
|
|
PayloadId = id, PayloadKind = "mine", WeaponItemKey = "test-blast",
|
|
SourceEntityIndex = source.EntityIndex, PositionX = target.PositionX,
|
|
LifetimeSeconds = 30, BlastRadius = 10, PayloadMagnitude = damage,
|
|
TriggeredAtSeconds = 0, DetonationDelaySeconds = 0, Stationary = true, Active = true
|
|
}];
|
|
}
|
|
|
|
AddDetonatingPayload("shielded-blast", 20);
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), catalog, 1, 0.1);
|
|
var capacitor = target.BehaviorStates.Single(value => value.BehaviorKind == "Capacitor");
|
|
RequireNear(100, Stat(target, "hull"), 0.000001,
|
|
"funded shield must intercept damage before hull");
|
|
RequireNear(40, capacitor.CapacitorCharge, 0.000001,
|
|
"shield must consume damage multiplied by authored energy usage");
|
|
RequireNear(40.0 / 3.0, Stat(target, "shield"), 0.000001,
|
|
"legacy shield meter must be derived from remaining funded absorption, not own hit points");
|
|
Require(run.GameEvents.Any(value => value.Kind == "shield.absorbed" &&
|
|
Math.Abs(value.ScalarValue - 20) < 0.000001 &&
|
|
Math.Abs(value.AuxiliaryValue - 10) < 0.000001),
|
|
"shield outcome must publish absorbed damage and authored heat contribution");
|
|
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 1, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
Require(Flatten(surface.Surface.Root).Any(node => node.Kind == "feedback.event" &&
|
|
node.Props["eventKind"] == "shield.absorbed" &&
|
|
node.Props["scalarValue"] == "20" && node.Props["auxiliaryValue"] == "10"),
|
|
"Eve feedback must expose exact shield absorption and heat facts");
|
|
var shieldAsset = AetheriaRuntimeAssets.ProjectManifest(catalog).Assets.Single(value =>
|
|
value.Ref.Metadata.TryGetValue("presentationRole", out var role) && role == "effect.impact.shield");
|
|
RequireEqual("prefab.effect.impact.shield", shieldAsset.Ref.AssetKey,
|
|
"provider must advertise the authored shield visual by semantic impact role");
|
|
|
|
capacitor.CapacitorCharge = 0;
|
|
AddDetonatingPayload("unfunded-blast", 20);
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), catalog, 2, 0.2);
|
|
RequireNear(80, Stat(target, "hull"), 0.000001,
|
|
"unfunded shield must not manufacture absorption");
|
|
}
|
|
|
|
private static void ArmorCellsResolveBeforeEquipmentAndHull()
|
|
{
|
|
var source = Entity(0, -100, "player");
|
|
var target = Entity(1, 0, "neutral");
|
|
target.HullItemKey = "test-cell-hull";
|
|
target.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = 0, Y = 0, Rotation = "Clockwise",
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "test-cell-gear", Quality = 1, Durability = 2, Enabled = true }
|
|
}];
|
|
var hull = HullCatalogItem("test-cell-hull", 3, 3, 5);
|
|
var gear = CatalogItem("test-cell-gear");
|
|
gear.ShapeWidth = 2;
|
|
gear.ShapeHeight = 1;
|
|
gear.ShapeCells = [new AetheriaRuntimeShapeCell(1, 0)];
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([hull, gear], [], []);
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{ ZoneIndex = 0, Entities = [source, target], PhysicalPayloads = [new AetheriaRuntimePhysicalPayloadCommit
|
|
{
|
|
PayloadId = "armor-blast", PayloadKind = "mine", WeaponItemKey = "test-cannon",
|
|
SourceEntityIndex = source.EntityIndex, PositionX = target.PositionX, PositionZ = target.PositionZ,
|
|
LifetimeSeconds = 30, BlastRadius = 200, PayloadMagnitude = 20,
|
|
DamageType = "Corrosive", Penetration = 0, DamageSpread = 0,
|
|
TriggeredAtSeconds = 0, DetonationDelaySeconds = 0, Stationary = true, Active = true
|
|
}] };
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{ RunId = "armor-cell-smoke", CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0", Zones = [zone] };
|
|
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), catalog, 1, 0.1);
|
|
|
|
var armor = target.StatGrids.Single(value => value.Name == "armor");
|
|
RequireNear(0, armor.Values[3], 0.000001, "source-facing splash cells must absorb damage first");
|
|
RequireNear(1.0 / 3.0, target.Equipment.Single().Item.Durability, 0.000001,
|
|
"rotated installed equipment at the impact cell must receive residual damage second");
|
|
RequireNear(100 - (10.0 / 3.0), Stat(target, "hull"), 0.000001,
|
|
"only residual damage after armor and equipment may reach hull durability");
|
|
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 1, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
var targetNode = Flatten(surface.Surface.Root).Single(node =>
|
|
node.Id == "aetheria.daemon.game.world.entity.1");
|
|
RequireEqual("entity.presentation", targetNode.Kind,
|
|
"Eve semantic entity facts must not become a second body-transform authority");
|
|
RequireEqual("30", targetNode.Props["armor"],
|
|
"Eve world projection must expose aggregate armor as derived presentation state");
|
|
RequireEqual("0,5,5,0,5,5,0,5,5", targetNode.Props["armorGrid"],
|
|
"Eve world projection must expose the exact daemon-owned schematic grid");
|
|
}
|
|
|
|
private static void PenetrationStopsAtFirstHullGap()
|
|
{
|
|
var source = Entity(0, 0, "player");
|
|
source.DirectionX = 1;
|
|
source.VelocityX = 1;
|
|
source.TargetEntityIndex = 1;
|
|
source.Contacts = [new AetheriaRuntimeEntityContactCommit
|
|
{
|
|
TargetEntityIndex = 1, InfoGathered = 1, Hostile = true, Visible = true
|
|
}];
|
|
source.WeaponGroups = [new[] { 0 }];
|
|
source.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "gap-penetrator", Quality = 1, Durability = 10, Enabled = true }
|
|
}];
|
|
var target = Entity(1, 80, "raider");
|
|
target.HullItemKey = "disconnected-hull";
|
|
var hull = HullCatalogItem("disconnected-hull", 4, 1, 0);
|
|
hull.ShapeCells =
|
|
[
|
|
new AetheriaRuntimeShapeCell(0, 0),
|
|
new AetheriaRuntimeShapeCell(1, 0),
|
|
new AetheriaRuntimeShapeCell(3, 0)
|
|
];
|
|
var payload = new AetheriaRuntimeBehaviorPayload(0, AetheriaRuntimeBehaviorKinds.InstantWeapon, 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(10)),
|
|
new AetheriaRuntimeBehaviorField(3, PerformanceStat(3)),
|
|
new AetheriaRuntimeBehaviorField(6, PerformanceStat(150)),
|
|
new AetheriaRuntimeBehaviorField(16, PerformanceStat(200)),
|
|
new AetheriaRuntimeBehaviorField(17, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(19, PerformanceStat(0.1)),
|
|
new AetheriaRuntimeBehaviorField(21, PerformanceStat(100)),
|
|
new AetheriaRuntimeBehaviorField(23, PerformanceStat(180)),
|
|
new AetheriaRuntimeBehaviorField(24, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(25, PerformanceStat(1))
|
|
]);
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
|
[WearableWeaponCatalogItem("gap-penetrator", payload), hull], [], []);
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "penetration-gap-smoke", GenerationSeed = 91, CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [source, target] }]
|
|
};
|
|
var fire = new AetheriaRuntimeDaemonIntentState();
|
|
fire.WeaponGroups.Add(new AetheriaRuntimeDaemonWeaponGroupIntent
|
|
{ ActorEntityKey = run.CurrentEntityKey, WeaponGroup = 0, Fire = true, Active = true });
|
|
|
|
for (var frame = 0; frame < 12 && run.ShotReceipts.Count == 0; frame++)
|
|
AetheriaRuntimeDaemonSimulation.Step(run,
|
|
frame == 0 ? fire : new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), catalog, frame, frame * 0.1);
|
|
|
|
var receipt = run.ShotReceipts.Single(value => value.WeaponItemKey == "gap-penetrator");
|
|
Require(receipt.Hit && receipt.DamageCells.Count == 1 &&
|
|
receipt.DamageCells[0].X == 1 && receipt.DamageCells[0].Y == 0,
|
|
$"impact UV must scale by the full fossil schematic dimensions and penetration must stop " +
|
|
$"at the first empty hull sample instead of damaging a disconnected island; " +
|
|
$"endpoint={receipt.EndpointX:0.###},{receipt.EndpointZ:0.###} " +
|
|
$"cells={string.Join(';', receipt.DamageCells.Select(cell => $"{cell.X},{cell.Y}"))}");
|
|
RequireNear(90, Stat(target, "hull"), 0.000001,
|
|
"a disconnected hull island must not divide or receive penetration damage beyond a gap");
|
|
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = receipt.FrameId, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
var shot = Flatten(surface.Surface.Root).Single(node =>
|
|
node.Kind == "shot.receipt" && node.Props["itemKey"] == "gap-penetrator");
|
|
RequireEqual("1", shot.Props["damageCellCount"],
|
|
"generic Eve shot receipts must expose the daemon's contiguous penetration result");
|
|
var damageCell = shot.Children.Single(node => node.Kind == "shot.damage-cell");
|
|
Require(damageCell.Props["x"] == "1" && damageCell.Props["y"] == "0" &&
|
|
damageCell.Props["armorAppliedDamage"] == "0" &&
|
|
damageCell.Props["equipmentIndex"] == "-1" &&
|
|
damageCell.Props["equipmentAppliedDamage"] == "0" &&
|
|
damageCell.Props["hullAppliedDamage"] == "10",
|
|
"generic Eve must retain the exact per-cell layer outcome without reconstructing damage");
|
|
}
|
|
|
|
private static void ShipSchematicProjectsDaemonTopologyAndEquipmentState()
|
|
{
|
|
var player = Entity(0, -50, "player");
|
|
player.TargetEntityIndex = 1;
|
|
var target = Entity(1, 0, "raider");
|
|
var observed = Entity(2, 50, "neutral");
|
|
target.HullItemKey = "schematic-hull";
|
|
target.StatGrids =
|
|
[
|
|
Grid("hull", 100), Grid("shield", 0), Grid("heat", 400),
|
|
Grid("armor", 3, 2, 10, 8, 6, 4, 2, 0),
|
|
Grid("maximumArmor", 3, 2, 10, 10, 10, 10, 10, 10)
|
|
];
|
|
target.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = 1, Y = 0, Rotation = "Clockwise",
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{
|
|
ItemKey = "schematic-launcher", Quality = 1, Durability = 5,
|
|
Temperature = 400, Enabled = true
|
|
}
|
|
}];
|
|
target.EquipmentStates = [new AetheriaRuntimeEquipmentStateCommit
|
|
{
|
|
EquipmentIndex = 0, Temperature = 400, ThermalPerformance = 0.75,
|
|
DurabilityPerformance = 0.5, Wear = 0.02, Online = true,
|
|
ThermalOnline = true, DurabilityOnline = true
|
|
}];
|
|
target.WeaponStates = [new AetheriaRuntimeWeaponStateCommit
|
|
{
|
|
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind,
|
|
OwnerIndex = 0, BehaviorIndex = 0, BehaviorKind = AetheriaRuntimeBehaviorKinds.Launcher,
|
|
Ammo = 3, CooldownProgress = 0.4, CoolingDown = true
|
|
}];
|
|
var hull = HullCatalogItem("schematic-hull", 3, 2, 10);
|
|
var launcher = CatalogItem("schematic-launcher", new AetheriaRuntimeBehaviorPayload(
|
|
0, AetheriaRuntimeBehaviorKinds.Launcher, 0, []));
|
|
launcher.ShapeWidth = 2;
|
|
launcher.ShapeHeight = 1;
|
|
launcher.OccupiedCells = 2;
|
|
launcher.ShapeCells =
|
|
[
|
|
new AetheriaRuntimeShapeCell(0, 0),
|
|
new AetheriaRuntimeShapeCell(1, 0)
|
|
];
|
|
launcher.Durability = 10;
|
|
launcher.MinimumTemperature = 200;
|
|
launcher.MaximumTemperature = 600;
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([hull, launcher], [], []);
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [player, target, observed] };
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "schematic-surface-smoke", CurrentZoneIndex = 0,
|
|
CurrentEntityKey = AetheriaRuntimeRunCheckpointCommit.EntityRecordKey(
|
|
"schematic-surface-smoke", 0, player.EntityIndex),
|
|
Zones = [zone]
|
|
};
|
|
|
|
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 1, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"),
|
|
catalog: catalog);
|
|
var schematic = Flatten(surface.Surface.Root).Single(node =>
|
|
node.Id == "aetheria.daemon.game.world.entity.1.schematic");
|
|
Require(schematic.Kind == "ship.schematic" && schematic.Props["role"] == "target" &&
|
|
schematic.Props["width"] == "3" && schematic.Props["height"] == "2",
|
|
"selected target must publish its daemon-owned hull topology as a generic ship schematic");
|
|
var hullCells = Flatten(schematic).Where(node => node.Kind == "schematic.hull-cell").ToArray();
|
|
RequireEqual(6, hullCells.Length,
|
|
"schematic must publish every occupied hull cell rather than a renderer-invented rectangle");
|
|
var damagedCell = hullCells.Single(node => node.Props["x"] == "1" && node.Props["y"] == "0");
|
|
Require(damagedCell.Props["armor"] == "8" && damagedCell.Props["maximumArmor"] == "10" &&
|
|
damagedCell.Props["armorRatio"] == "0.8",
|
|
"schematic cell must expose exact current and maximum daemon armor");
|
|
var item = Flatten(schematic).Single(node => node.Kind == "schematic.item");
|
|
Require(item.Props["itemKey"] == "schematic-launcher" && item.Props["durabilityRatio"] == "0.5" &&
|
|
item.Props["temperatureRatio"] == "0.5" && item.Props["thermalPerformance"] == "0.75" &&
|
|
item.Props["online"] == "true",
|
|
"schematic item must expose daemon-derived durability and thermal state");
|
|
var itemCells = Flatten(item).Where(node => node.Kind == "schematic.item-cell")
|
|
.Select(node => $"{node.Props["x"]},{node.Props["y"]}").ToArray();
|
|
RequireEqual("1,0;1,1", string.Join(";", itemCells),
|
|
"schematic footprint must use the same clockwise placement geometry as refit and damage");
|
|
var weapon = Flatten(item).Single(node => node.Kind == "schematic.weapon");
|
|
Require(weapon.Props["ammo"] == "3" && weapon.Props["cooldownProgress"] == "0.4",
|
|
"schematic weapon row must expose authoritative ammo and cooldown levers");
|
|
Require(Flatten(surface.Surface.Root).Single(node =>
|
|
node.Id == "aetheria.daemon.game.world.entity.0.schematic").Props["role"] == "self",
|
|
"controlled entity must publish the same schematic contract with a self role");
|
|
Require(!Flatten(surface.Surface.Root).Any(node =>
|
|
node.Id == "aetheria.daemon.game.world.entity.2.schematic"),
|
|
"unselected observed entities must not bloat the live surface with unused schematic trees");
|
|
}
|
|
|
|
private static void EquipmentDestructionDisablesBehaviorAndCockpitKillsBeforeHull()
|
|
{
|
|
var source = Entity(0, -100, "player");
|
|
var target = Entity(1, 0, "neutral");
|
|
target.HullItemKey = "equipment-damage-hull";
|
|
target.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = 0, Y = 0,
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "damageable-weapon", Quality = 1, Durability = 1, Enabled = true }
|
|
}];
|
|
target.WeaponStates = [new AetheriaRuntimeWeaponStateCommit
|
|
{
|
|
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind,
|
|
OwnerIndex = 0, BehaviorIndex = 0, BehaviorKind = "InstantWeapon",
|
|
Firing = true, TriggerPending = true, BurstRemaining = 2,
|
|
Charging = true, Charged = true, Charge = 1,
|
|
LockProgress = 1, LockTargetEntityIndex = source.EntityIndex
|
|
}];
|
|
var hull = HullCatalogItem("equipment-damage-hull", 1, 1, 0);
|
|
var weapon = CatalogItem("damageable-weapon", new AetheriaRuntimeBehaviorPayload(
|
|
0, "InstantWeapon", 0, []));
|
|
weapon.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([hull, weapon], [], []);
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0, Entities = [source, target], PhysicalPayloads = [new AetheriaRuntimePhysicalPayloadCommit
|
|
{
|
|
PayloadId = "equipment-destruction", PayloadKind = "mine", WeaponItemKey = "test-cannon",
|
|
SourceEntityIndex = source.EntityIndex, PositionX = target.PositionX, PositionZ = target.PositionZ,
|
|
LifetimeSeconds = 30, BlastRadius = 200, PayloadMagnitude = 1,
|
|
TriggeredAtSeconds = 0, DetonationDelaySeconds = 0, Stationary = true, Active = true
|
|
}]
|
|
};
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "equipment-destruction-smoke", CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0", Zones = [zone]
|
|
};
|
|
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault, NewPhysics(), catalog, 1, 0.1);
|
|
|
|
Require(target.IsActive && Math.Abs(Stat(target, "hull") - 100) < 0.000001,
|
|
"destroying ordinary equipment must not manufacture hull damage or destroy the entity");
|
|
RequireNear(0, target.Equipment.Single().Item.Durability, 0.000001,
|
|
"equipment damage must durably cross into the fossil destroyed state");
|
|
RequireEqual(1, AetheriaRuntimeEquippedBehaviorQueries.Find(target, catalog, "InstantWeapon").Count,
|
|
"destroyed equipment must remain installed as durable loadout state");
|
|
RequireEqual(0, AetheriaRuntimeEquippedBehaviorQueries.FindOperational(target, catalog, "InstantWeapon").Count,
|
|
"destroyed equipment behavior must stop contributing immediately");
|
|
var destroyedWeaponState = target.WeaponStates.Single();
|
|
Require(!destroyedWeaponState.Firing && !destroyedWeaponState.TriggerPending &&
|
|
!destroyedWeaponState.Charging && !destroyedWeaponState.Charged &&
|
|
destroyedWeaponState.BurstRemaining == 0 && destroyedWeaponState.LockTargetEntityIndex < 0,
|
|
"destroyed weapon state must deactivate atomically instead of publishing a ghost firing effect");
|
|
RequireEqual(1, run.GameEvents.Count(value => value.Kind == "equipment.destroyed" &&
|
|
value.TargetEntityIndex == target.EntityIndex && value.ItemKey == "damageable-weapon"),
|
|
"the durability crossing must publish exactly one generic equipment destruction fact");
|
|
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 1, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
Require(Flatten(surface.Surface.Root).Any(node => node.Kind == "feedback.event" &&
|
|
node.Props["eventKind"] == "equipment.destroyed" &&
|
|
node.Props["itemKey"] == "damageable-weapon"),
|
|
"generic Eve clients must receive equipment destruction through provider feedback facts");
|
|
|
|
var cockpitTarget = Entity(2, 0, "neutral");
|
|
cockpitTarget.HullItemKey = "cockpit-damage-hull";
|
|
cockpitTarget.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = 0, Y = 0,
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "damageable-cockpit", Quality = 1, Durability = 1, Enabled = true }
|
|
}];
|
|
var cockpitHull = HullCatalogItem("cockpit-damage-hull", 1, 1, 0);
|
|
var cockpit = CatalogItem("damageable-cockpit", new AetheriaRuntimeBehaviorPayload(0, "Cockpit", 0, []));
|
|
cockpit.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
var cockpitCatalog = new AetheriaRuntimeCatalogSnapshot([cockpitHull, cockpit], [], []);
|
|
var cockpitZone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0, Entities = [source, cockpitTarget], PhysicalPayloads = [new AetheriaRuntimePhysicalPayloadCommit
|
|
{
|
|
PayloadId = "cockpit-destruction", PayloadKind = "mine", WeaponItemKey = "test-cannon",
|
|
SourceEntityIndex = source.EntityIndex, PositionX = cockpitTarget.PositionX, PositionZ = cockpitTarget.PositionZ,
|
|
LifetimeSeconds = 30, BlastRadius = 200, PayloadMagnitude = 1,
|
|
TriggeredAtSeconds = 0, DetonationDelaySeconds = 0, Stationary = true, Active = true
|
|
}]
|
|
};
|
|
var cockpitRun = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "cockpit-destruction-smoke", CurrentZoneIndex = 0,
|
|
CurrentEntityKey = AetheriaRuntimeRunCheckpointCommit.EntityRecordKey(
|
|
"cockpit-destruction-smoke", 0, cockpitTarget.EntityIndex),
|
|
Zones = [cockpitZone]
|
|
};
|
|
|
|
AetheriaRuntimeDaemonSimulation.Step(cockpitRun, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault, NewPhysics(), cockpitCatalog, 1, 0.1);
|
|
|
|
Require(!cockpitTarget.IsActive && cockpitTarget.CauseOfDeath == "cockpit-destroyed",
|
|
"cockpit durability crossing must use the fossil cockpit death path before hull depletion");
|
|
RequireNear(100, Stat(cockpitTarget, "hull"), 0.000001,
|
|
"cockpit destruction must not counterfeit scalar hull depletion");
|
|
var chronology = cockpitRun.GameEvents.Where(value =>
|
|
value.Kind == "equipment.destroyed" || value.Kind == "entity.destroyed")
|
|
.Select(value => value.Kind).ToArray();
|
|
RequireEqual("equipment.destroyed,entity.destroyed", string.Join(",", chronology),
|
|
"equipment destruction feedback must precede the cause-specific entity destruction fact");
|
|
RequireEqual(AetheriaRuntimeRunLifecycle.Failed, cockpitRun.LifecyclePhase,
|
|
"destruction of the controlled entity must end the daemon-owned run");
|
|
RequireEqual("cockpit-destroyed", cockpitRun.TerminalReason,
|
|
"the run terminal reason must preserve the fossil cause of death");
|
|
RequireEqual(1L, cockpitRun.TerminalFrameId,
|
|
"the run terminal frame must be committed with the lethal transaction");
|
|
RequireEqual("", cockpitRun.CurrentEntityKey,
|
|
"the failed run must release its controlled entity after recording the terminal cause");
|
|
RequireEqual(1, cockpitRun.GameEvents.Count(value => value.Kind == "run.failed" &&
|
|
value.Reason == "cockpit-destroyed"),
|
|
"the lethal transaction must publish one durable run failure fact");
|
|
|
|
var restartedRun = MessagePack.MessagePackSerializer.Deserialize<AetheriaRuntimeRunCheckpointCommit>(
|
|
MessagePack.MessagePackSerializer.Serialize(cockpitRun));
|
|
restartedRun.Zones[0].Entities[0].VelocityX = 10;
|
|
var sourcePosition = restartedRun.Zones[0].Entities[0].PositionX;
|
|
var rejectedAfterFailure = MovementCommand("move-after-run-failure", "zone.0.entity.0", 1, 0);
|
|
using var restartedPhysics = NewPhysics();
|
|
var restarted = AetheriaRuntimeDaemonTickRunner.Tick(
|
|
Path.Combine(Path.GetTempPath(), "aetheria-failed-run-restart.cc"),
|
|
restartedRun,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
FrameId = 2,
|
|
FixedDeltaSeconds = 0.1,
|
|
SimulationTimeSeconds = 0.2,
|
|
ObservedCommands = [rejectedAfterFailure],
|
|
WorldPhysics = restartedPhysics,
|
|
BuildPublications = false
|
|
});
|
|
Require(restarted.OperationResult.RejectedCommandIds.Contains("move-after-run-failure"),
|
|
"a terminal run must reject later gameplay commands after restart");
|
|
RequireNear(sourcePosition, restartedRun.Zones[0].Entities[0].PositionX, 0.000001,
|
|
"a terminal run must not advance surviving world bodies after restart");
|
|
RequireEqual(1, restartedRun.GameEvents.Count(value => value.Kind == "run.failed"),
|
|
"restart must not replay the run failure transition");
|
|
|
|
var failedSurface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
restarted.Frame,
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
var failedWorld = Flatten(failedSurface.Surface.Root)
|
|
.Single(node => node.Id == "aetheria.daemon.game.world");
|
|
RequireEqual("terminal", failedWorld.Props["presentationMode"],
|
|
"the generic Eve world must receive a terminal presentation lever");
|
|
RequireEqual("false", failedWorld.Props["movementEnabled"],
|
|
"the generic Eve world must not advertise movement after run failure");
|
|
RequireEqual("cockpit-destroyed", failedWorld.Props["runTerminalReason"],
|
|
"the generic Eve world must receive the authoritative terminal cause");
|
|
Require(AetheriaRuntimeStateRefResolver.TryResolveDaemonStateRef(
|
|
restarted.Frame,
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"),
|
|
AetheriaRuntimeDaemonStateRefs.CurrentRunLifecycle,
|
|
out var projectedLifecycle) &&
|
|
projectedLifecycle == AetheriaRuntimeRunLifecycle.Failed,
|
|
"Eve state bindings must resolve the daemon-owned run lifecycle without client inference");
|
|
}
|
|
|
|
private static void DestructionDropsLootExactlyOnce()
|
|
{
|
|
var source = Entity(0, -100, "player");
|
|
var target = Entity(1, 0, "raider");
|
|
target.HullItemKey = "drop-hull";
|
|
target.StatGrids = [Grid("hull", 1), Grid("shield", 0), Grid("heat", 0)];
|
|
target.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "drop-gear", Quality = 0.8, Durability = 0.6, Quantity = 1, Enabled = true }
|
|
}];
|
|
target.CargoContents = [Cargo(("drop-cargo", 4, 0, 0))];
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0, Entities = [source, target],
|
|
PhysicalPayloads = [new AetheriaRuntimePhysicalPayloadCommit
|
|
{
|
|
PayloadId = "destruction-mine", PayloadKind = "mine", WeaponItemKey = "destruction-charge",
|
|
SourceEntityIndex = 0, PositionX = 0, PositionZ = 0, BlastRadius = 200,
|
|
PayloadMagnitude = 20, TriggeredAtSeconds = 0, DetonationDelaySeconds = 0,
|
|
LifetimeSeconds = 30, Active = true, Stationary = true
|
|
}]
|
|
};
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "destruction-smoke", GenerationSeed = 29, CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0", Zones = [zone]
|
|
};
|
|
var settings = LootSettings(1);
|
|
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1, settings,
|
|
NewPhysics(), null, 1, 0.1);
|
|
|
|
Require(!target.IsActive && target.DestroyedFrameId == 1 &&
|
|
!string.IsNullOrWhiteSpace(target.DestructionId),
|
|
"lethal daemon damage must commit one durable destruction identity and deactivate the entity");
|
|
RequireEqual(2, zone.DroppedPickups.Count,
|
|
"guaranteed fossil loot roll must drop non-hull equipment and every cargo stack");
|
|
Require(!zone.DroppedPickups.Any(value => value.Item.ItemKey == "drop-hull"),
|
|
"the equipped hull must remain excluded from fossil destruction loot");
|
|
RequireEqual(4, zone.DroppedPickups.Single(value => value.Item.ItemKey == "drop-cargo").Item.Quantity,
|
|
"cargo drops must preserve exact stack quantity");
|
|
Require(zone.DroppedPickups.All(value => Math.Abs(Math.Sqrt(
|
|
value.VelocityX * value.VelocityX + value.VelocityY * value.VelocityY +
|
|
value.VelocityZ * value.VelocityZ) - 25) < 0.000001 &&
|
|
Math.Abs(value.LifetimeSeconds - 30) < 0.000001),
|
|
"destruction pickups must retain fossil launch speed and lifetime");
|
|
RequireEqual(2, run.GameEvents.Count(value => value.Kind == "pickup.dropped"),
|
|
"each durable pickup must publish one drop event");
|
|
RequireEqual(1, run.GameEvents.Count(value => value.Kind == "entity.destroyed"),
|
|
"destruction chronology must publish exactly once");
|
|
RequireEqual(AetheriaRuntimeRunLifecycle.Active, run.LifecyclePhase,
|
|
"destroying a non-controlled entity must not counterfeit a run failure");
|
|
|
|
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 1, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
Require(!Flatten(surface.Surface.Root).Any(node => node.Id == "aetheria.daemon.game.world.entity.1"),
|
|
"Eve must not project a destroyed tombstone as an active world entity");
|
|
var destructionAsset = AetheriaRuntimeAssets.ProjectManifest(null).Assets.Single(value =>
|
|
value.Ref.Metadata.TryGetValue("presentationRole", out var role) &&
|
|
role == "effect.feedback.entity.destroyed");
|
|
RequireEqual("prefab.effect.entity.destroyed", destructionAsset.Ref.AssetKey,
|
|
"provider must advertise the original destruction effect by semantic feedback role");
|
|
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1, settings,
|
|
NewPhysics(), null, 2, 0.2);
|
|
RequireEqual(2, zone.DroppedPickups.Count,
|
|
"later ticks must not duplicate an already committed destruction transaction");
|
|
RequireEqual(1, run.GameEvents.Count(value => value.Kind == "entity.destroyed"),
|
|
"later ticks must not duplicate destruction chronology");
|
|
}
|
|
|
|
private static void ChargedWeaponHoldRiskMalfunctionsDeterministically()
|
|
{
|
|
var source = Entity(0, 0, "player");
|
|
source.WeaponGroups = [new[] { 0 }];
|
|
var item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "risk-charger", Quality = 1, Durability = 1, Enabled = true };
|
|
source.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit { Item = item }];
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "charged-risk-smoke", GenerationSeed = 17,
|
|
CurrentZoneIndex = 0, CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [source] }]
|
|
};
|
|
var payload = new AetheriaRuntimeBehaviorPayload(0, AetheriaRuntimeBehaviorKinds.ChargedWeapon, 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(20)),
|
|
new AetheriaRuntimeBehaviorField(6, PerformanceStat(150)),
|
|
new AetheriaRuntimeBehaviorField(17, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(19, PerformanceStat(0.5)),
|
|
new AetheriaRuntimeBehaviorField(21, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(25, Number(1)),
|
|
new AetheriaRuntimeBehaviorField(26, Number(0.25))
|
|
]);
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([CatalogItem(item.ItemKey, payload)], [], []);
|
|
for (var frame = 0; frame < 21; frame++)
|
|
{
|
|
var intents = new AetheriaRuntimeDaemonIntentState();
|
|
if (frame == 0) intents.WeaponGroups.Add(new AetheriaRuntimeDaemonWeaponGroupIntent
|
|
{ ActorEntityKey = "zone.0.entity.0", WeaponGroup = 0, Fire = true, Active = true });
|
|
AetheriaRuntimeDaemonSimulation.Step(run, intents, 0.1,
|
|
new AetheriaRuntimeDaemonSimulationSettings(),
|
|
NewPhysics(), catalog, frame, frame * 0.1);
|
|
}
|
|
var state = source.WeaponStates.Single(value => value.BehaviorKind == AetheriaRuntimeBehaviorKinds.ChargedWeapon);
|
|
Require(!state.Charging && !state.Charged && state.CoolingDown && state.ChargeRiskChecks == 1 &&
|
|
Math.Abs(state.ChargeMalfunctionRisk - 1) < 0.000001,
|
|
"held full charge must persist its risk check and fail when authored risk reaches certainty");
|
|
RequireNear(0.75, item.Durability, 0.000001,
|
|
"charged malfunction must damage canonical equipped-item durability");
|
|
Require(run.GameEvents.Count(value => value.Kind == "weapon.charge.malfunctioned" &&
|
|
value.SubjectKey == "hold-risk" && value.ItemKey == item.ItemKey) == 1,
|
|
"charged hold risk must emit one authoritative malfunction event");
|
|
Require(!run.GameEvents.Any(value => value.Kind == "shot.committed"),
|
|
"malfunction without a firing solution must never leak a projectile");
|
|
}
|
|
|
|
private static void AgentTowsStationIntoPersistentOrbit()
|
|
{
|
|
var tug = Entity(0, 0, "workers");
|
|
var station = Entity(1, 0, "workers");
|
|
station.Kind = "station";
|
|
tug.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Tow];
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0, Entities = [tug, station],
|
|
Orbits = [new AetheriaRuntimeOrbitSnapshotCommit { OrbitKey = "parent-orbit", FixedPositionX = 20 }],
|
|
Bodies = [new AetheriaRuntimeBodySnapshotCommit { BodyKey = "parent-body", Kind = "planet", OrbitKey = "parent-orbit" }]
|
|
};
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
CurrentZoneIndex = 0, CurrentEntityKey = "", Zones = [zone],
|
|
AgentTasks = [new AetheriaRuntimeAgentTaskCommit { TaskId = "tow-1", CorporationKey = "workers", TaskType = AetheriaRuntimeAgentTaskTypes.Tow, ZoneIndex = 0, Status = AetheriaRuntimeAgentTaskStatuses.Queued, TargetEntityIndex = 1, OrbitParentKey = "parent-body", OrbitDistance = 20, CompletionRadius = 5 }]
|
|
};
|
|
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-tow-attach-smoke.cc"), run,
|
|
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = NewPhysics(), FrameId = 1, FixedDeltaSeconds = 0.1, SimulationTimeSeconds = 1, BuildPublications = false });
|
|
Require(run.Zones[0].Entities[0].ChildEntityIndices.Contains(1), "tow pickup must attach station to tug parentage");
|
|
RequireEqual("", run.Zones[0].Entities[1].OrbitKey, "attached station must no longer own an orbit");
|
|
|
|
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-tow-detach-smoke.cc"), run,
|
|
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = NewPhysics(), FrameId = 2, FixedDeltaSeconds = 0.1, SimulationTimeSeconds = 2, BuildPublications = false });
|
|
Require(!run.Zones[0].Entities[0].ChildEntityIndices.Contains(1), "tow delivery must detach station parentage");
|
|
Require(!string.IsNullOrWhiteSpace(run.Zones[0].Entities[1].OrbitKey), "delivered station must own a persistent orbit");
|
|
Require(run.Zones[0].Orbits.Any(orbit => orbit.OrbitKey == run.Zones[0].Entities[1].OrbitKey && orbit.ParentOrbitKey == "parent-orbit" && Math.Abs(orbit.Distance - 20) < 0.001),
|
|
"delivered orbit must preserve requested parent and radius");
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Completed, run.AgentTasks[0].Status, "tow task must complete only after detach applies");
|
|
}
|
|
|
|
private static void AgentSurveysBodyIntoCorporationKnowledge()
|
|
{
|
|
var surveyor = Entity(0, 0, "workers");
|
|
surveyor.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Explore];
|
|
surveyor.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "scanner", Enabled = true } }];
|
|
var scanner = CatalogItem("scanner", new AetheriaRuntimeBehaviorPayload(0, "ResourceScanner", 0,
|
|
[new AetheriaRuntimeBehaviorField(1, PerformanceStat(100)), new AetheriaRuntimeBehaviorField(2, PerformanceStat(4)), new AetheriaRuntimeBehaviorField(3, PerformanceStat(0.5))]));
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([scanner], [], []);
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
CurrentZoneIndex = 0, CurrentEntityKey = "",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0, Entities = [surveyor],
|
|
Orbits = [new AetheriaRuntimeOrbitSnapshotCommit { OrbitKey = "survey-orbit", FixedPositionX = 10 }],
|
|
Bodies = [new AetheriaRuntimeBodySnapshotCommit { BodyKey = "survey-world", Kind = "planet", OrbitKey = "survey-orbit" }]
|
|
}],
|
|
AgentTasks = [new AetheriaRuntimeAgentTaskCommit { TaskId = "survey-1", CorporationKey = "workers", TaskType = AetheriaRuntimeAgentTaskTypes.Explore, ZoneIndex = 0, Status = AetheriaRuntimeAgentTaskStatuses.Queued, TargetBodyKeys = ["survey-world"] }]
|
|
};
|
|
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-survey-smoke.cc"), run,
|
|
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = NewPhysics(), FrameId = 1, FixedDeltaSeconds = 1, SimulationTimeSeconds = 1, Catalog = catalog, BuildPublications = false });
|
|
var knowledge = run.CorporationSurveys.Single();
|
|
RequireEqual("workers", knowledge.CorporationKey, "survey knowledge must belong to the agent corporation");
|
|
RequireEqual("survey-world", knowledge.BodyKey, "survey knowledge must identify the scanned body");
|
|
RequireNear(4, knowledge.DensityFloor, 0.000001, "survey must publish the scanner minimum density");
|
|
|
|
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-survey-complete-smoke.cc"), run,
|
|
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = NewPhysics(), FrameId = 2, FixedDeltaSeconds = 1, SimulationTimeSeconds = 2, Catalog = catalog, BuildPublications = false });
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Completed, run.AgentTasks[0].Status,
|
|
"survey order must complete when corporation knowledge satisfies the scanner threshold");
|
|
}
|
|
|
|
private static void CargoCapacityComesFromHullAndCatalogVolumes()
|
|
{
|
|
var hull = CatalogItem("hauler-hull");
|
|
hull.HullCapacity = PerformanceStat(20);
|
|
var gear = CatalogItem("scanner");
|
|
gear.Volume = 3;
|
|
var cargoBay = CatalogItem("hauler-cargo");
|
|
cargoBay.Volume = 0;
|
|
cargoBay.InteriorOccupiedCells = 16;
|
|
var ore = CatalogItem("ore");
|
|
ore.Volume = 2;
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([hull, gear, cargoBay, ore], [], []);
|
|
var entity = Entity(0, 0, "workers");
|
|
entity.HullItemKey = hull.ItemKey;
|
|
entity.Equipment =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = gear.ItemKey } }
|
|
];
|
|
entity.CargoBays = [new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = cargoBay.ItemKey } }];
|
|
entity.CargoContents = [new AetheriaRuntimeCargoBayLoadoutCommit { Items = [new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = ore.ItemKey, Quantity = 4 } }] }];
|
|
|
|
RequireNear(20, AetheriaRuntimeCargoCapacityQueries.Capacity(entity, catalog), 0.000001,
|
|
"cargo capacity must evaluate the authored hull performance stat");
|
|
RequireNear(11, AetheriaRuntimeCargoCapacityQueries.Occupied(entity, catalog), 0.000001,
|
|
"equipment and stacked cargo must consume catalog volume");
|
|
RequireEqual(4, AetheriaRuntimeCargoCapacityQueries.UnitsThatFit(entity, catalog, ore.ItemKey),
|
|
"the installed cargo bay's remaining volume must determine whole units that fit");
|
|
}
|
|
|
|
private static void AgentMinesAsteroidThroughEquippedBehavior()
|
|
{
|
|
var miner = Entity(0, 0, "workers");
|
|
var home = Entity(1, 0, "workers");
|
|
miner.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Mine];
|
|
miner.Equipment =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "mining-tool", Enabled = true, Quality = 1, Durability = 1 } }
|
|
];
|
|
miner.CargoBays = [new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "miner-cargo", Enabled = true, Quality = 1, Durability = 1 } }];
|
|
home.CargoBays = [new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "home-cargo", Enabled = true, Quality = 1, Durability = 1 } }];
|
|
var minerHull = CatalogItem("miner-hull");
|
|
minerHull.HullCapacity = PerformanceStat(3);
|
|
var homeHull = CatalogItem("home-hull");
|
|
homeHull.HullCapacity = PerformanceStat(100);
|
|
var iron = CatalogItem("iron");
|
|
iron.SimpleCommodityCategory = "ore";
|
|
iron.Category = AetheriaRuntimeItemCategories.SimpleCommodity;
|
|
iron.ShapeWidth = 1;
|
|
iron.ShapeHeight = 1;
|
|
iron.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
iron.Stackable = true;
|
|
iron.MaxStack = 100;
|
|
iron.Volume = 1;
|
|
var minerCargo = CatalogItem("miner-cargo");
|
|
minerCargo.Category = AetheriaRuntimeItemCategories.CargoBay;
|
|
minerCargo.ShapeWidth = 1;
|
|
minerCargo.ShapeHeight = 1;
|
|
minerCargo.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
minerCargo.InteriorShapeWidth = 2;
|
|
minerCargo.InteriorShapeHeight = 1;
|
|
minerCargo.InteriorOccupiedCells = 2;
|
|
minerCargo.InteriorShapeCells = [new AetheriaRuntimeShapeCell(0, 0), new AetheriaRuntimeShapeCell(1, 0)];
|
|
var homeCargo = CatalogItem("home-cargo");
|
|
homeCargo.Category = AetheriaRuntimeItemCategories.CargoBay;
|
|
homeCargo.ShapeWidth = 1;
|
|
homeCargo.ShapeHeight = 1;
|
|
homeCargo.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
homeCargo.InteriorShapeWidth = 10;
|
|
homeCargo.InteriorShapeHeight = 10;
|
|
homeCargo.InteriorOccupiedCells = 100;
|
|
homeCargo.InteriorShapeCells = Enumerable.Range(0, 10)
|
|
.SelectMany(y => Enumerable.Range(0, 10).Select(x => new AetheriaRuntimeShapeCell(x, y)))
|
|
.ToArray();
|
|
miner.HullItemKey = minerHull.ItemKey;
|
|
home.HullItemKey = homeHull.ItemKey;
|
|
miner.CargoContents = [new AetheriaRuntimeCargoBayLoadoutCommit()];
|
|
home.CargoContents = [new AetheriaRuntimeCargoBayLoadoutCommit()];
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
|
[minerHull, homeHull, iron, minerCargo, homeCargo, CatalogItem("mining-tool", new AetheriaRuntimeBehaviorPayload(0, "MiningTool", 0,
|
|
[new AetheriaRuntimeBehaviorField(1, PerformanceStat(1000)), new AetheriaRuntimeBehaviorField(2, PerformanceStat(1000000000)), new AetheriaRuntimeBehaviorField(3, PerformanceStat(2)), new AetheriaRuntimeBehaviorField(4, PerformanceStat(1000))]))],
|
|
[], []);
|
|
var asteroid = new AetheriaRuntimeAsteroidCommit { Distance = 0, Size = 6 };
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [miner, home],
|
|
Bodies = [new AetheriaRuntimeBodySnapshotCommit { BodyKey = "belt", Kind = "asteroid_belt", Asteroids = [asteroid], Resources = [new AetheriaRuntimeBodyResourceCommit { ItemKey = "iron", Amount = 1 }] }]
|
|
};
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
GenerationSeed = 7,
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "",
|
|
Zones = [zone],
|
|
AgentTasks = [new AetheriaRuntimeAgentTaskCommit { TaskId = "mine-1", CorporationKey = "workers", TaskType = AetheriaRuntimeAgentTaskTypes.Mine, ZoneIndex = 0, OriginEntityIndex = 1, CompletionRadius = 10, Status = AetheriaRuntimeAgentTaskStatuses.Queued, TargetBodyKeys = ["belt"] }]
|
|
};
|
|
|
|
var result = AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-mining-smoke.cc"), run,
|
|
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = NewPhysics(), FrameId = 1, FixedDeltaSeconds = 1, SimulationTimeSeconds = 1, Catalog = catalog, BuildPublications = false });
|
|
|
|
Require(run.AgentTasks[0].Phase == "mining", "mining task must activate its equipped tool");
|
|
Require(result.OperationResult.Intents.Behaviors.Count == 1,
|
|
$"mining behavior command must become daemon intent (count={result.OperationResult.Intents.Behaviors.Count}, applied={string.Join(',', result.Frame.AppliedCommandIds)}, rejected={string.Join(',', result.Frame.RejectedCommandIds)})");
|
|
var committedMiner = run.Zones[0].Entities.Single(entity => entity.EntityIndex == 0);
|
|
var committedAsteroid = run.Zones[0].Bodies[0].Asteroids[0];
|
|
Require(committedAsteroid.RespawnTimer > 0, $"mining damage must deplete the asteroid and start historical respawn (damage={committedAsteroid.Damage}, respawn={committedAsteroid.RespawnTimer})");
|
|
Require(committedMiner.CargoContents.SelectMany(bay => bay.Items).Any(slot => slot.Item.ItemKey == "iron" && slot.Item.Quantity > 0),
|
|
"historical mining yield must enter daemon-owned cargo");
|
|
|
|
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-mining-offload-smoke.cc"), run,
|
|
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = NewPhysics(), FrameId = 2, FixedDeltaSeconds = 1, SimulationTimeSeconds = 2, Catalog = catalog, BuildPublications = false });
|
|
Require(!run.Zones[0].Entities[0].CargoContents.SelectMany(bay => bay.Items).Any(),
|
|
"full miner must offload through the shared cargo transfer command");
|
|
Require(run.Zones[0].Entities[1].CargoContents.SelectMany(bay => bay.Items).Any(slot => slot.Item.ItemKey == "iron" && slot.Item.Quantity == 1),
|
|
"home storage must receive the mined commodity");
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Assigned, run.AgentTasks[0].Status,
|
|
"miner must remain assigned after a successful offload");
|
|
}
|
|
|
|
private static void TickReconcilesAndEvaluatesCatalogBehaviors()
|
|
{
|
|
var entity = Entity(0, 0, "workers");
|
|
entity.Equipment =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{
|
|
ItemKey = "mining-tool",
|
|
Quality = 1,
|
|
Durability = 1,
|
|
Enabled = true
|
|
}
|
|
}
|
|
];
|
|
entity.BehaviorStates = Array.Empty<AetheriaRuntimeBehaviorStateCommit>();
|
|
var payload = new AetheriaRuntimeBehaviorPayload(
|
|
0,
|
|
"MiningTool",
|
|
0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, PerformanceStat(12)),
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(0.8)),
|
|
new AetheriaRuntimeBehaviorField(3, PerformanceStat(2)),
|
|
new AetheriaRuntimeBehaviorField(4, PerformanceStat(50))
|
|
]);
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
|
[CatalogItem("mining-tool", payload)],
|
|
Array.Empty<AetheriaRuntimeCorporation>(),
|
|
Array.Empty<AetheriaRuntimeNameFile>());
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "behavior-query-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [entity] }]
|
|
};
|
|
|
|
AetheriaRuntimeDaemonTickRunner.Tick(
|
|
Path.Combine(Path.GetTempPath(), "aetheria-behavior-query-smoke.cc"),
|
|
run,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
WorldPhysics = NewPhysics(),
|
|
FrameId = 1,
|
|
FixedDeltaSeconds = 0,
|
|
Catalog = catalog,
|
|
BuildPublications = false
|
|
});
|
|
|
|
var behavior = AetheriaRuntimeEquippedBehaviorQueries.Find(entity, catalog, "MiningTool").Single();
|
|
RequireEqual("MiningTool", behavior.State.BehaviorKind,
|
|
"tick must reconcile equipped catalog payloads into persistent behavior state");
|
|
RequireNear(12, behavior.EvaluateStat(1), 0.000001, "behavior query must evaluate mining damage from catalog");
|
|
RequireNear(50, behavior.EvaluateStat(4), 0.000001, "behavior query must evaluate mining range from catalog");
|
|
}
|
|
|
|
private static void DaemonLoadoutsRespectFactionAvailabilityAndHullRoles()
|
|
{
|
|
var cells4 = Enumerable.Range(0, 4).SelectMany(y => Enumerable.Range(0, 4)
|
|
.Select(x => new AetheriaRuntimeShapeCell(x, y))).ToArray();
|
|
var one = new[] { new AetheriaRuntimeShapeCell(0, 0) };
|
|
var cells2 = Enumerable.Range(0, 2).SelectMany(y => Enumerable.Range(0, 2)
|
|
.Select(x => new AetheriaRuntimeShapeCell(x, y))).ToArray();
|
|
|
|
AetheriaRuntimeCatalogItem Item(string key, string category, string manufacturer, int price,
|
|
string hardpointType = "", params string[] behaviors)
|
|
{
|
|
var item = CatalogItem(key, behaviors.Select((kind, index) =>
|
|
new AetheriaRuntimeBehaviorPayload(index, kind, 0, Array.Empty<AetheriaRuntimeBehaviorField>())).ToArray());
|
|
item.Category = category; item.ManufacturerKey = manufacturer; item.Price = price;
|
|
item.HardpointType = hardpointType; item.ShapeWidth = 1; item.ShapeHeight = 1;
|
|
item.OccupiedCells = 1; item.ShapeCells = one; item.BehaviorKinds = behaviors;
|
|
return item;
|
|
}
|
|
|
|
var availableHull = Item("available-hull", AetheriaRuntimeItemCategories.Hull, "forge", 100);
|
|
availableHull.HardpointType = "Hull";
|
|
availableHull.HullType = "Ship"; availableHull.ShapeWidth = 4; availableHull.ShapeHeight = 4;
|
|
availableHull.OccupiedCells = 16; availableHull.ShapeCells = cells4;
|
|
availableHull.Hardpoints =
|
|
[
|
|
new AetheriaRuntimeHardpoint("ControlModule", 0, 0, 1, 1, 1, one, "", "None", 0),
|
|
new AetheriaRuntimeHardpoint("Weapon", 1, 0, 1, 1, 1, one, "", "Clockwise", 0),
|
|
new AetheriaRuntimeHardpoint("Sensors", 2, 0, 1, 1, 1, one, "", "None", 0)
|
|
];
|
|
var unavailableHull = Item("cheap-foreign-hull", AetheriaRuntimeItemCategories.Hull, "foreign", 1);
|
|
unavailableHull.HardpointType = "Hull";
|
|
unavailableHull.HullType = "Ship"; unavailableHull.ShapeWidth = 4; unavailableHull.ShapeHeight = 4;
|
|
unavailableHull.OccupiedCells = 16; unavailableHull.ShapeCells = cells4;
|
|
var stationHull = Item("station-hull", AetheriaRuntimeItemCategories.Hull, "forge", 120);
|
|
stationHull.HardpointType = "Hull"; stationHull.HullType = "Station";
|
|
stationHull.ShapeWidth = 4; stationHull.ShapeHeight = 4; stationHull.OccupiedCells = 16;
|
|
stationHull.ShapeCells = cells4;
|
|
stationHull.Hardpoints =
|
|
[
|
|
new AetheriaRuntimeHardpoint("ControlModule", 0, 0, 1, 1, 1, one, "", "None", 0),
|
|
new AetheriaRuntimeHardpoint("Sensors", 1, 1, 2, 2, 4, cells2, "", "None", 0)
|
|
];
|
|
var cockpit = Item("cockpit", AetheriaRuntimeItemCategories.Gear, "forge", 20, "ControlModule", "Cockpit");
|
|
var wrongController = Item("cheap-turret-controller", AetheriaRuntimeItemCategories.Gear, "forge", 1,
|
|
"ControlModule", "TurretController");
|
|
var weapon = Item("cannon", AetheriaRuntimeItemCategories.Weapon, "forge", 40, "Weapon", "LockWeapon");
|
|
var shipSensor = Item("ship-sensor", AetheriaRuntimeItemCategories.Gear, "forge", 15, "Sensors", "Sensor");
|
|
var stationSensor = Item("station-sensor", AetheriaRuntimeItemCategories.Gear, "forge", 80, "Sensors", "Sensor");
|
|
stationSensor.ShapeWidth = 2; stationSensor.ShapeHeight = 2;
|
|
stationSensor.OccupiedCells = 4; stationSensor.ShapeCells = cells2;
|
|
var cargo = Item("cargo-bay", AetheriaRuntimeItemCategories.CargoBay, "forge", 30);
|
|
cargo.HardpointType = "Internal";
|
|
cargo.InteriorShapeWidth = 3; cargo.InteriorShapeHeight = 3; cargo.InteriorOccupiedCells = 9;
|
|
cargo.InteriorShapeCells = Enumerable.Range(0, 3).SelectMany(y => Enumerable.Range(0, 3)
|
|
.Select(x => new AetheriaRuntimeShapeCell(x, y))).ToArray();
|
|
var docking = Item("docking-bay", AetheriaRuntimeItemCategories.DockingBay, "forge", 35);
|
|
docking.HardpointType = "Internal";
|
|
var capacitor = Item("capacitor", AetheriaRuntimeItemCategories.Gear, "forge", 25, "", "Capacitor");
|
|
capacitor.HardpointType = "Internal";
|
|
var faction = new AetheriaRuntimeCorporation("forge", "Forge", "F", "", "", "", 1, 1,
|
|
[new AetheriaRuntimeCorporationAllegiance("forge", 1)]);
|
|
var foreign = new AetheriaRuntimeCorporation("foreign", "Foreign", "X", "", "", "", 1, 2,
|
|
[new AetheriaRuntimeCorporationAllegiance("foreign", 1), new AetheriaRuntimeCorporationAllegiance("forge", 0.5)]);
|
|
var reactor = Item("reactor", AetheriaRuntimeItemCategories.Gear, "forge", 25, "", "Reactor");
|
|
reactor.HardpointType = "Internal";
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
|
[availableHull, unavailableHull, stationHull, cockpit, wrongController, weapon, shipSensor, stationSensor, cargo, docking, capacitor, reactor],
|
|
[faction, foreign], Array.Empty<AetheriaRuntimeNameFile>());
|
|
var fallbackCatalog = new AetheriaRuntimeCatalogSnapshot(
|
|
[availableHull, unavailableHull, stationHull, cockpit, wrongController, weapon, shipSensor, cargo, docking, capacitor, reactor],
|
|
[faction, foreign], Array.Empty<AetheriaRuntimeNameFile>());
|
|
var homes = new Dictionary<string, int> { ["forge"] = 0, ["foreign"] = 1 };
|
|
var adjacency = new Dictionary<int, IReadOnlyList<int>> { [0] = [1], [1] = [0] };
|
|
|
|
var rareHull = Item("rare-hull", AetheriaRuntimeItemCategories.Hull, "forge", 100);
|
|
rareHull.HardpointType = availableHull.HardpointType;
|
|
rareHull.HullType = availableHull.HullType;
|
|
rareHull.ShapeWidth = availableHull.ShapeWidth;
|
|
rareHull.ShapeHeight = availableHull.ShapeHeight;
|
|
rareHull.OccupiedCells = availableHull.OccupiedCells;
|
|
rareHull.ShapeCells = availableHull.ShapeCells;
|
|
rareHull.Hardpoints = availableHull.Hardpoints;
|
|
rareHull.Tags = ["rarity:rare"];
|
|
var rarityCatalog = new AetheriaRuntimeCatalogSnapshot(
|
|
[availableHull, rareHull, cockpit, wrongController, weapon, shipSensor, cargo, capacitor, reactor],
|
|
[faction], Array.Empty<AetheriaRuntimeNameFile>());
|
|
|
|
var first = new AetheriaDaemonLoadoutGenerator(catalog, 42, 0, homes, adjacency).Build("ship", "forge");
|
|
var second = new AetheriaDaemonLoadoutGenerator(catalog, 42, 0, homes, adjacency).Build("ship", "forge");
|
|
var station = new AetheriaDaemonLoadoutGenerator(catalog, 84, 0, homes, adjacency).Build("station", "forge");
|
|
var fallbackStation = new AetheriaDaemonLoadoutGenerator(fallbackCatalog, 84, 0, homes, adjacency)
|
|
.Build("station", "forge");
|
|
var uninterruptedForge = new AetheriaDaemonLoadoutGenerator(catalog, 101, 0, homes, adjacency);
|
|
var interleavedForge = new AetheriaDaemonLoadoutGenerator(catalog, 101, 0, homes, adjacency);
|
|
var foreignStream = new AetheriaDaemonLoadoutGenerator(catalog, 202, 1, homes, adjacency);
|
|
var uninterruptedSequence = new[]
|
|
{
|
|
uninterruptedForge.Build("ship", "forge"),
|
|
uninterruptedForge.Build("ship", "forge")
|
|
};
|
|
var interleavedFirst = interleavedForge.Build("ship", "forge");
|
|
foreignStream.Build("ship", "foreign");
|
|
var interleavedSecond = interleavedForge.Build("ship", "forge");
|
|
var rarityGenerator = new AetheriaDaemonLoadoutGenerator(
|
|
rarityCatalog, 9001, 0, homes, adjacency);
|
|
var rareSelections = Enumerable.Range(0, 2000)
|
|
.Select(_ => rarityGenerator.Build("ship", "forge").HullItemKey)
|
|
.Count(key => key == "rare-hull");
|
|
|
|
RequireEqual("available-hull", first.HullItemKey,
|
|
"loadout generation must exclude manufacturers outside the faction allegiance graph");
|
|
Require(first.Equipment.Any(value => value.ItemKey == "cockpit") &&
|
|
first.Equipment.All(value => value.ItemKey != "cheap-turret-controller"),
|
|
"ship control hardpoints must require the cockpit role even when the wrong controller is cheaper");
|
|
Require(first.Equipment.Any(value => value.ItemKey == "cannon") &&
|
|
first.CargoBays.Any(value => value.ItemKey == "cargo-bay") &&
|
|
first.Equipment.Any(value => value.ItemKey == "capacitor") &&
|
|
first.Equipment.Any(value => value.ItemKey == "ship-sensor") &&
|
|
first.Equipment.All(value => value.ItemKey != "station-sensor"),
|
|
"generated ships must fit hardpoint equipment and reject station-sized sensor gear");
|
|
RequireEqual("Clockwise", first.Equipment.Single(value => value.ItemKey == "cannon").Rotation,
|
|
"generated equipment must preserve the placement rotation used by hardpoint fitting");
|
|
Require(first.HullItemKey == second.HullItemKey &&
|
|
first.Equipment.Select(value => value.ItemKey).SequenceEqual(second.Equipment.Select(value => value.ItemKey)),
|
|
"same seed, map, faction and catalog must produce the same loadout");
|
|
Require(first.Cargo.Length == 0 && first.Receipt.Selections.All(value => value.Role != "cargo"),
|
|
"ordinary generated ships must not acquire cargo from non-canonical scenario aliases");
|
|
Require(first.Receipt.Seed == 42 && first.Receipt.SourceZoneIndex == 0 &&
|
|
first.Receipt.AvailabilityFactionKey == "forge" &&
|
|
first.Receipt.Selections.Any(value => value.Role == "hull" && value.ItemKey == "available-hull" &&
|
|
value.ManufacturerKey == "forge" && value.ManufacturerDistance == 1 && value.Allegiance == 1),
|
|
"generated loadouts must carry daemon-authored source and selection provenance");
|
|
Require(rareSelections > 0 && rareSelections < 200,
|
|
$"rare catalog candidates must remain possible without becoming a default loadout path (selected {rareSelections}/2000)");
|
|
Require(station.DockingBays.Any(value => value.ItemKey == "docking-bay") &&
|
|
station.CargoBays.Any(value => value.ItemKey == "cargo-bay") &&
|
|
station.Equipment.Any(value => value.ItemKey == "capacitor") &&
|
|
station.Equipment.Any(value => value.ItemKey == "cheap-turret-controller") &&
|
|
station.Equipment.Any(value => value.ItemKey == "station-sensor") &&
|
|
station.Equipment.All(value => value.ItemKey != "ship-sensor"),
|
|
"station generation must fill a large sensor hardpoint with the largest available compatible array");
|
|
Require(fallbackStation.Equipment.Any(value => value.ItemKey == "ship-sensor"),
|
|
"a smaller sensor must remain a valid fallback when it fits inside a larger same-type hardpoint");
|
|
Require(station.Cargo.Length > 0 && station.Cargo.Length <= cargo.InteriorOccupiedCells &&
|
|
station.Cargo.All(value => value.Item.ItemKey != "cheap-foreign-hull" &&
|
|
catalog.FindItem(value.Item.ItemKey) != null) &&
|
|
station.Receipt.Selections.Where(value => value.Role == "cargo")
|
|
.Select(value => value.ItemKey)
|
|
.SequenceEqual(station.Cargo.Select(value => value.Item.ItemKey)),
|
|
"station inventory draws must use canonical catalog keys, respect cargo capacity, exclude unavailable manufacturers, and preserve exact provenance");
|
|
Require(LoadoutKeys(uninterruptedSequence[0]).SequenceEqual(LoadoutKeys(interleavedFirst)) &&
|
|
LoadoutKeys(uninterruptedSequence[1]).SequenceEqual(LoadoutKeys(interleavedSecond)),
|
|
"generation in another faction stream must not perturb this faction's continuing loadout sequence");
|
|
}
|
|
|
|
private static IEnumerable<string> LoadoutKeys(AetheriaDaemonLoadout loadout) =>
|
|
new[] { loadout.HullItemKey }
|
|
.Concat(loadout.Equipment.Select(value => value.ItemKey))
|
|
.Concat(loadout.CargoBays.Select(value => value.ItemKey))
|
|
.Concat(loadout.DockingBays.Select(value => value.ItemKey))
|
|
.Concat(loadout.Cargo.Select(value => value.Item.ItemKey));
|
|
|
|
private static AetheriaRuntimeBehaviorValue PerformanceStat(double value) => new(
|
|
"performance-stat",
|
|
"",
|
|
0,
|
|
false,
|
|
"",
|
|
"",
|
|
[Number(value), Number(value), Number(0), Number(0), Number(0)],
|
|
Array.Empty<AetheriaRuntimeBehaviorMapEntry>());
|
|
|
|
private static AetheriaRuntimeBehaviorValue Number(double value) => new(
|
|
"number",
|
|
"",
|
|
value,
|
|
false,
|
|
"",
|
|
"",
|
|
Array.Empty<AetheriaRuntimeBehaviorValue>(),
|
|
Array.Empty<AetheriaRuntimeBehaviorMapEntry>());
|
|
|
|
private static AetheriaRuntimeBehaviorValue StringValue(string value) => new(
|
|
"string", value, 0, false, "", "",
|
|
Array.Empty<AetheriaRuntimeBehaviorValue>(),
|
|
Array.Empty<AetheriaRuntimeBehaviorMapEntry>());
|
|
|
|
private static AetheriaRuntimeBehaviorValue BoolValue(bool value) => new(
|
|
"bool", "", 0, value, "", "",
|
|
Array.Empty<AetheriaRuntimeBehaviorValue>(),
|
|
Array.Empty<AetheriaRuntimeBehaviorMapEntry>());
|
|
|
|
private static AetheriaRuntimeBehaviorValue ItemKeyValue(string value) => new(
|
|
"item-key", "", 0, false, "", value,
|
|
Array.Empty<AetheriaRuntimeBehaviorValue>(),
|
|
Array.Empty<AetheriaRuntimeBehaviorMapEntry>());
|
|
|
|
private static AetheriaRuntimeBehaviorValue StatReference(string target, string stat) => new(
|
|
"stat-reference", "", 0, false, "", "",
|
|
[StringValue(""), StringValue(target), StringValue(stat)],
|
|
Array.Empty<AetheriaRuntimeBehaviorMapEntry>());
|
|
|
|
private static AetheriaRuntimeBehaviorValue Vector(params double[] values) => new(
|
|
"float3",
|
|
"",
|
|
0,
|
|
false,
|
|
"",
|
|
"",
|
|
values.Select(Number).ToArray(),
|
|
Array.Empty<AetheriaRuntimeBehaviorMapEntry>());
|
|
|
|
private static AetheriaRuntimeBehaviorValue Curve(params AetheriaRuntimeCurveKey[] keys) => new(
|
|
"bezier-curve",
|
|
"",
|
|
0,
|
|
false,
|
|
"",
|
|
"",
|
|
[new AetheriaRuntimeBehaviorValue(
|
|
"array",
|
|
"",
|
|
0,
|
|
false,
|
|
"",
|
|
"",
|
|
keys.Select(key => Vector(key.Time, key.Value, key.InTangent, key.OutTangent)).ToArray(),
|
|
Array.Empty<AetheriaRuntimeBehaviorMapEntry>())],
|
|
Array.Empty<AetheriaRuntimeBehaviorMapEntry>());
|
|
|
|
private static AetheriaRuntimeBehaviorPayload AetherDrivePayload(
|
|
double couplingEfficiency = 1,
|
|
double energyDraw = 0) => new(
|
|
0,
|
|
AetheriaRuntimeBehaviorKinds.AetherDrive,
|
|
0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, Vector(100, 100, 100)),
|
|
new AetheriaRuntimeBehaviorField(2, Vector(10, 10, 0.1)),
|
|
new AetheriaRuntimeBehaviorField(3, PerformanceStat(100)),
|
|
new AetheriaRuntimeBehaviorField(4, Vector(10, 10, 10)),
|
|
new AetheriaRuntimeBehaviorField(5, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(6, PerformanceStat(couplingEfficiency)),
|
|
new AetheriaRuntimeBehaviorField(7, PerformanceStat(10000)),
|
|
new AetheriaRuntimeBehaviorField(8, Curve(
|
|
new AetheriaRuntimeCurveKey(0, 1, 0, 0),
|
|
new AetheriaRuntimeCurveKey(1, 1, 0, 0))),
|
|
new AetheriaRuntimeBehaviorField(9, PerformanceStat(energyDraw)),
|
|
new AetheriaRuntimeBehaviorField(10, PerformanceStat(0))
|
|
]);
|
|
|
|
private static AetheriaRuntimeBehaviorPayload ThrusterPayload() => new(
|
|
0,
|
|
AetheriaRuntimeBehaviorKinds.Thruster,
|
|
0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, PerformanceStat(90)),
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(2)),
|
|
new AetheriaRuntimeBehaviorField(3, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(4, PerformanceStat(0))
|
|
]);
|
|
|
|
private static AetheriaRuntimeCatalogSnapshot EquipThrusterBank(
|
|
IEnumerable<AetheriaRuntimeEntitySnapshotCommit> entities,
|
|
params AetheriaRuntimeCatalogItem[] additionalItems)
|
|
{
|
|
var hull = HullCatalogItem("smoke-standard-thruster-hull", 5, 5, 0);
|
|
hull.HullDrag = 1;
|
|
var thruster = CatalogItem("smoke-directional-thruster", ThrusterPayload());
|
|
var placements = new[]
|
|
{
|
|
(0, 2, "None"), (4, 2, "None"),
|
|
(0, 1, "Half"), (4, 1, "Half"),
|
|
(2, 0, "CounterClockwise"), (2, 4, "CounterClockwise"),
|
|
(1, 0, "Clockwise"), (1, 4, "Clockwise")
|
|
};
|
|
foreach (var entity in entities.Where(entity => entity.Kind == "ship"))
|
|
{
|
|
if (string.IsNullOrWhiteSpace(entity.HullItemKey))
|
|
entity.HullItemKey = hull.ItemKey;
|
|
entity.Equipment = (entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
|
.Concat(placements.Select(value => new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = value.Item1,
|
|
Y = value.Item2,
|
|
Rotation = value.Item3,
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{
|
|
ItemKey = thruster.ItemKey,
|
|
Quantity = 1,
|
|
Durability = 1,
|
|
Enabled = true
|
|
}
|
|
}))
|
|
.ToArray();
|
|
}
|
|
return new AetheriaRuntimeCatalogSnapshot(
|
|
additionalItems.Concat([hull, thruster]).ToArray(), [], []);
|
|
}
|
|
|
|
private static AetheriaRuntimeCatalogSnapshot EquipAetherDrive(
|
|
IEnumerable<AetheriaRuntimeEntitySnapshotCommit> entities,
|
|
params AetheriaRuntimeCatalogItem[] additionalItems)
|
|
{
|
|
var hull = HullCatalogItem("smoke-rare-aether-drive-hull", 3, 3, 0);
|
|
hull.Hardpoints =
|
|
[
|
|
new AetheriaRuntimeHardpoint(
|
|
"AetherDrive", 1, 1, 1, 1, 1,
|
|
[new AetheriaRuntimeShapeCell(0, 0)], "", "None", 0)
|
|
];
|
|
var drive = CatalogItem("smoke-aether-drive", AetherDrivePayload());
|
|
drive.HardpointType = "AetherDrive";
|
|
foreach (var entity in entities.Where(entity => entity.Kind == "ship"))
|
|
{
|
|
if (string.IsNullOrWhiteSpace(entity.HullItemKey))
|
|
entity.HullItemKey = hull.ItemKey;
|
|
entity.Equipment = (entity.Equipment ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>())
|
|
.Concat([new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = 1,
|
|
Y = 1,
|
|
Rotation = "None",
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{
|
|
ItemKey = drive.ItemKey,
|
|
Quantity = 1,
|
|
Durability = 1,
|
|
Enabled = true
|
|
}
|
|
}])
|
|
.ToArray();
|
|
}
|
|
return new AetheriaRuntimeCatalogSnapshot(
|
|
additionalItems.Concat([hull, drive]).ToArray(), [], []);
|
|
}
|
|
|
|
private static AetheriaRuntimeBehaviorPayload CapacitorPayload(double capacity, double efficiency) => new(
|
|
0, "Capacitor", 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, PerformanceStat(capacity)),
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(efficiency))
|
|
]);
|
|
|
|
private static AetheriaRuntimeBehaviorPayload SensorPayload(
|
|
double sensitivity = 1,
|
|
double pingBoost = 1,
|
|
double pingEnergy = 0,
|
|
double pingVisibility = 3,
|
|
double pingRange = 200,
|
|
double pingCooldown = 1,
|
|
double pingDuration = 2,
|
|
double pingRadiusExponent = 0.5) => new(
|
|
0, AetheriaRuntimeBehaviorKinds.Sensor, 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(3, PerformanceStat(sensitivity)),
|
|
new AetheriaRuntimeBehaviorField(4, Curve(
|
|
new AetheriaRuntimeCurveKey(0, 1, 0, 0),
|
|
new AetheriaRuntimeCurveKey(1, 1, 0, 0))),
|
|
new AetheriaRuntimeBehaviorField(5, PerformanceStat(pingBoost)),
|
|
new AetheriaRuntimeBehaviorField(6, PerformanceStat(pingEnergy)),
|
|
new AetheriaRuntimeBehaviorField(7, PerformanceStat(pingVisibility)),
|
|
new AetheriaRuntimeBehaviorField(8, PerformanceStat(pingRange)),
|
|
new AetheriaRuntimeBehaviorField(9, PerformanceStat(pingCooldown)),
|
|
new AetheriaRuntimeBehaviorField(10, Number(pingDuration)),
|
|
new AetheriaRuntimeBehaviorField(11, Number(pingRadiusExponent))
|
|
]);
|
|
|
|
private static AetheriaRuntimeCatalogItem CatalogItem(string itemKey, params AetheriaRuntimeBehaviorPayload[] payloads) => new(
|
|
itemKey, itemKey, "equipment", "", "", 0, 1, 1, 1, 1,
|
|
1, 1, 1, Array.Empty<AetheriaRuntimeShapeCell>(),
|
|
0, 0, 0, Array.Empty<AetheriaRuntimeShapeCell>(),
|
|
Array.Empty<AetheriaRuntimeHardpoint>(), payloads,
|
|
"utility", "", payloads.Select(payload => payload.Kind).ToArray(),
|
|
1, false, 0, 1, "", "", "", "", "",
|
|
0, 1000, Array.Empty<AetheriaRuntimeCurveKey>(), "", 1, 0, 0, 0, false,
|
|
0, 0, "", Array.Empty<AetheriaRuntimeAudioStat>(), Array.Empty<AetheriaRuntimeCurveKey>(), "", "");
|
|
|
|
private static AetheriaRuntimeCatalogItem WearableWeaponCatalogItem(
|
|
string itemKey, params AetheriaRuntimeBehaviorPayload[] payloads)
|
|
{
|
|
var item = CatalogItem(itemKey, payloads);
|
|
item.Durability = 1;
|
|
item.ThermalResilience = 1;
|
|
item.MinimumTemperature = 0;
|
|
item.MaximumTemperature = 1;
|
|
item.ThermalPerformanceCurveKeys =
|
|
[
|
|
new AetheriaRuntimeCurveKey(0, 0.9, 0, 0),
|
|
new AetheriaRuntimeCurveKey(1, 0.9, 0, 0)
|
|
];
|
|
return item;
|
|
}
|
|
|
|
private static AetheriaRuntimeCatalogItem HullCatalogItem(string itemKey, int width, int height, double armor)
|
|
{
|
|
var cells = Enumerable.Range(0, height)
|
|
.SelectMany(y => Enumerable.Range(0, width).Select(x => new AetheriaRuntimeShapeCell(x, y)))
|
|
.ToArray();
|
|
return new AetheriaRuntimeCatalogItem(
|
|
itemKey, itemKey, "hull", "", "", 0, 1, 1, 1, 1,
|
|
width, height, cells.Length, cells,
|
|
width, height, cells.Length, cells,
|
|
Array.Empty<AetheriaRuntimeHardpoint>(), Array.Empty<AetheriaRuntimeBehaviorPayload>(),
|
|
"", "ship", Array.Empty<string>(),
|
|
1, false, 0, 1, "", "", "", "", "",
|
|
0, 1000, Array.Empty<AetheriaRuntimeCurveKey>(), "", 1, 0, armor, 0, false,
|
|
0, 0, "", Array.Empty<AetheriaRuntimeAudioStat>(), Array.Empty<AetheriaRuntimeCurveKey>(), "", "");
|
|
}
|
|
|
|
private static AetheriaRuntimeDaemonSimulationSettings LootSettings(double probability)
|
|
{
|
|
var value = AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault;
|
|
return new AetheriaRuntimeDaemonSimulationSettings(
|
|
value.PawnSpeed, value.RaiderSpeed, value.AttackRange, value.AttackHoldRatio,
|
|
value.PawnProjectileDamage, value.RaiderProjectileDamage, value.WeaponCooldownSeconds,
|
|
value.ProjectileSpeed, value.ProjectileRadius, value.ProjectileLifetimeSeconds,
|
|
value.ProjectileSpawnOffset, value.ProjectileHeatScale, value.HeatDissipationPerSecond,
|
|
value.StationSensorRange, value.EntitySensorRange, value.PlayerStationHull,
|
|
value.HostileStationHull, value.PlayerEntityHull, value.RaiderEntityHull,
|
|
value.StationShield, value.EntityShield, value.WeaponLockSpeed,
|
|
value.WeaponLockSensorImpact, value.WeaponLockAngleDegrees,
|
|
value.WeaponLockDirectionImpact, value.WeaponLockDecayPerSecond,
|
|
probability, value.LootDropVelocity, value.PickupLifetimeSeconds);
|
|
}
|
|
|
|
private static void AgentPatrolsHistoricalOrbitCircuitThroughMovementCommands()
|
|
{
|
|
var agent = Entity(0, 0, "workers");
|
|
agent.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Patrol];
|
|
var catalog = EquipThrusterBank([agent]);
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "agent-patrol-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "",
|
|
Zones =
|
|
[
|
|
new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [agent],
|
|
Orbits =
|
|
[
|
|
new AetheriaRuntimeOrbitSnapshotCommit { OrbitKey = "orbit:east", FixedPositionX = 20 },
|
|
new AetheriaRuntimeOrbitSnapshotCommit { OrbitKey = "orbit:west", FixedPositionX = -20 }
|
|
],
|
|
Bodies =
|
|
[
|
|
new AetheriaRuntimeBodySnapshotCommit { BodyKey = "body:east", OrbitKey = "orbit:east", Kind = "planet" },
|
|
new AetheriaRuntimeBodySnapshotCommit { BodyKey = "body:west", OrbitKey = "orbit:west", Kind = "planet" }
|
|
]
|
|
}
|
|
]
|
|
};
|
|
var issue = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.IssueAgentTask,
|
|
"commander-smoke",
|
|
"starbridge-smoke",
|
|
0,
|
|
"");
|
|
issue.CommandId = "issue-patrol";
|
|
issue.AgentTask = new AetheriaRuntimeAgentTaskCommand
|
|
{
|
|
TaskId = "patrol-orbits",
|
|
CorporationKey = "workers",
|
|
TaskType = AetheriaRuntimeAgentTaskTypes.Patrol,
|
|
Priority = 10,
|
|
ZoneIndex = 0,
|
|
CompletionRadius = 5,
|
|
TargetOrbitKeys = ["orbit:east", "orbit:west"]
|
|
};
|
|
var sawWestLeg = false;
|
|
var sawReturnToEast = false;
|
|
var physics = NewPhysics();
|
|
for (var frame = 0; frame < 100; frame++)
|
|
{
|
|
var tick = AetheriaRuntimeDaemonTickRunner.Tick(
|
|
Path.Combine(Path.GetTempPath(), "aetheria-agent-patrol-smoke.cc"),
|
|
run,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
WorldPhysics = physics,
|
|
Catalog = catalog,
|
|
FrameId = frame,
|
|
FixedDeltaSeconds = 0.1,
|
|
SimulationTimeSeconds = frame * 0.1,
|
|
ObservedCommands = frame == 0 ? [issue] : Array.Empty<AetheriaRuntimeDaemonCommandDocument>(),
|
|
BuildPublications = false
|
|
});
|
|
Require(tick.OperationResult.AppliedCommandIds.Any(id => id.EndsWith(":move", StringComparison.Ordinal)),
|
|
"patrol controller must drive the shared movement command every tick");
|
|
var cursor = run.AgentTasks.Single().CircuitIndex;
|
|
sawWestLeg |= cursor == 1;
|
|
sawReturnToEast |= sawWestLeg && cursor == 0;
|
|
if (sawReturnToEast)
|
|
break;
|
|
}
|
|
|
|
Require(sawWestLeg && sawReturnToEast, "patrol must advance through and wrap its authored orbit circuit");
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Assigned, run.AgentTasks.Single().Status,
|
|
"patrol is persistent work and must remain assigned after one circuit");
|
|
}
|
|
|
|
private static void SecurityZonesOwnFossilTrespasserHostility()
|
|
{
|
|
var guard = Entity(0, 0, "owners");
|
|
guard.SecurityLevel = 2;
|
|
guard.SecurityRadius = 100;
|
|
var visitor = Entity(1, 10, "visitors");
|
|
guard.Contacts = [new AetheriaRuntimeEntityContactCommit { TargetEntityIndex = 1, InfoGathered = 1 }];
|
|
visitor.Contacts = [new AetheriaRuntimeEntityContactCommit { TargetEntityIndex = 0, InfoGathered = 1 }];
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
OwnerFactionIndex = 0,
|
|
Entities = [guard, visitor]
|
|
};
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
CurrentEntityKey = "zone.0.entity.1",
|
|
FactionRelationships =
|
|
[
|
|
new AetheriaRuntimeFactionRelationshipCommit
|
|
{
|
|
FactionKey = "owners", Relationship = "Neutral"
|
|
}
|
|
],
|
|
Zones = [zone]
|
|
};
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([], [new AetheriaRuntimeCorporation(
|
|
"owners", "Owners", "Owners", "", "", "", 4, 0,
|
|
Array.Empty<AetheriaRuntimeCorporationAllegiance>())], []);
|
|
|
|
AetheriaRuntimeSensorSimulation.StepZone(
|
|
run, zone, [guard, visitor], null, catalog, 0.1, 0, 0.5, 1);
|
|
Require(guard.Contacts.Single().Hostile && visitor.Contacts.Single().Hostile,
|
|
"a neutral visitor inside an owner critical-security field must be mutually hostile");
|
|
|
|
visitor.PositionX = 1000;
|
|
AetheriaRuntimeSensorSimulation.StepZone(
|
|
run, zone, [guard, visitor], null, catalog, 0.1, 0, 0.5, 2);
|
|
Require(!guard.Contacts.Single().Hostile && !visitor.Contacts.Single().Hostile,
|
|
"the same neutral visitor outside security fields must cease being hostile");
|
|
}
|
|
|
|
private static void PatrolCombatPreemptsAndResumesTheSameCircuit()
|
|
{
|
|
var patrol = Entity(0, 0, "owners");
|
|
patrol.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Patrol];
|
|
patrol.WeaponGroups = [new[] { 0 }];
|
|
patrol.Equipment =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{
|
|
ItemKey = "patrol-cannon", Quality = 1, Durability = 1, Enabled = true
|
|
}
|
|
}
|
|
];
|
|
patrol.Contacts =
|
|
[
|
|
new AetheriaRuntimeEntityContactCommit
|
|
{
|
|
TargetEntityIndex = 1, InfoGathered = 1, Visible = true, Hostile = true
|
|
}
|
|
];
|
|
var hostile = Entity(1, 0, "visitors");
|
|
hostile.PositionZ = 50;
|
|
var weapon = new AetheriaRuntimeBehaviorPayload(0, AetheriaRuntimeBehaviorKinds.InstantWeapon, 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(10)),
|
|
new AetheriaRuntimeBehaviorField(5, PerformanceStat(0)),
|
|
new AetheriaRuntimeBehaviorField(6, PerformanceStat(150)),
|
|
new AetheriaRuntimeBehaviorField(16, PerformanceStat(200)),
|
|
new AetheriaRuntimeBehaviorField(19, PerformanceStat(0.5))
|
|
]);
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
|
[WearableWeaponCatalogItem("patrol-cannon", weapon)], [], []);
|
|
var task = new AetheriaRuntimeAgentTaskCommit
|
|
{
|
|
TaskId = "patrol-combat",
|
|
CorporationKey = "owners",
|
|
TaskType = AetheriaRuntimeAgentTaskTypes.Patrol,
|
|
Status = AetheriaRuntimeAgentTaskStatuses.Assigned,
|
|
ZoneIndex = 0,
|
|
AssignedEntityIndex = 0,
|
|
CompletionRadius = 5,
|
|
TargetOrbitKeys = ["orbit:east", "orbit:west"],
|
|
CircuitIndex = 1
|
|
};
|
|
patrol.AssignedAgentTaskId = task.TaskId;
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "patrol-combat-smoke",
|
|
CurrentEntityKey = "",
|
|
AgentTasks = [task],
|
|
Zones =
|
|
[
|
|
new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [patrol, hostile],
|
|
Orbits =
|
|
[
|
|
new AetheriaRuntimeOrbitSnapshotCommit { OrbitKey = "orbit:east", FixedPositionX = 20 },
|
|
new AetheriaRuntimeOrbitSnapshotCommit { OrbitKey = "orbit:west", FixedPositionX = -20 }
|
|
]
|
|
}
|
|
]
|
|
};
|
|
|
|
var combatCommands = AetheriaRuntimeAgentScheduler.AssignAndPlan(
|
|
run, 1, catalog, 0.1, AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault);
|
|
Require(combatCommands.Any(command =>
|
|
command.Kind == AetheriaRuntimeDaemonCommandKinds.SetTarget &&
|
|
AetheriaRuntimeRunCheckpointCommit.TryParseEntityKey(
|
|
command.TargetEntityKey, out var zoneIndex, out var entityIndex) &&
|
|
zoneIndex == 0 && entityIndex == 1),
|
|
"a visible hostile ship must pre-empt patrol movement through the shared target command");
|
|
Require(combatCommands.Any(command => command.Kind == AetheriaRuntimeDaemonCommandKinds.FireWeaponGroup),
|
|
"an aligned in-range patrol agent must use the shared weapon command");
|
|
RequireEqual(1, task.CircuitIndex,
|
|
"opportunistic combat must not replace or advance the patrol circuit");
|
|
|
|
patrol.Contacts = Array.Empty<AetheriaRuntimeEntityContactCommit>();
|
|
var resumed = AetheriaRuntimeAgentScheduler.AssignAndPlan(
|
|
run, 2, catalog, 0.2, AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault);
|
|
Require(resumed.Any(command => command.Kind == AetheriaRuntimeDaemonCommandKinds.SetMoveVector),
|
|
"loss of hostile visibility must resume the same patrol circuit");
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Assigned, task.Status,
|
|
"opportunistic combat must never complete or fail the patrol task");
|
|
}
|
|
|
|
private static void RejectedHaulTransferDoesNotAdvanceTask()
|
|
{
|
|
var origin = Entity(0, 0, "workers");
|
|
origin.CargoContents = [Cargo(("ore", 2, 0, 0))];
|
|
var agent = Entity(1, 0, "workers");
|
|
agent.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Haul];
|
|
agent.CargoContents = Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>();
|
|
var destination = Entity(2, 20, "workers");
|
|
destination.CargoContents = [Cargo()];
|
|
var task = new AetheriaRuntimeAgentTaskCommit
|
|
{
|
|
TaskId = "rejected-haul",
|
|
CorporationKey = "workers",
|
|
TaskType = AetheriaRuntimeAgentTaskTypes.Haul,
|
|
Priority = 1,
|
|
ZoneIndex = 0,
|
|
OriginEntityIndex = 0,
|
|
TargetEntityIndex = 2,
|
|
ItemKey = "ore",
|
|
RequestedQuantity = 1,
|
|
CompletionRadius = 5,
|
|
Phase = "pickup"
|
|
};
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "rejected-haul-smoke",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [origin, agent, destination] }],
|
|
AgentTasks = [task]
|
|
};
|
|
|
|
var planned = AetheriaRuntimeAgentScheduler.AssignAndPlan(run, 1);
|
|
var reduced = AetheriaRuntimeDaemonOperations.Execute(run, planned);
|
|
AetheriaRuntimeAgentScheduler.Reconcile(run, 1, reduced.AppliedCommandIds, reduced.RejectedCommandIds);
|
|
|
|
Require(reduced.RejectedCommandIds.Any(id => id.EndsWith(":pickup", StringComparison.Ordinal)),
|
|
"invalid pickup must be rejected by the normal cargo reducer");
|
|
RequireEqual("pickup", task.Phase, "rejected pickup must not advance the haul task");
|
|
RequireEqual(0, task.PendingQuantity, "rejected pickup must clear pending transfer state");
|
|
RequireEqual(2, CargoQuantity(origin, "ore"), "rejected pickup must leave origin cargo untouched");
|
|
}
|
|
|
|
private static void AgentCompletesHaulTaskThroughMovementAndCargoCommands()
|
|
{
|
|
var cargoBay = CatalogItem("haul-cargo-bay");
|
|
cargoBay.Category = AetheriaRuntimeItemCategories.CargoBay;
|
|
cargoBay.ShapeWidth = 1;
|
|
cargoBay.ShapeHeight = 1;
|
|
cargoBay.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
cargoBay.InteriorShapeWidth = 4;
|
|
cargoBay.InteriorShapeHeight = 4;
|
|
cargoBay.InteriorOccupiedCells = 16;
|
|
cargoBay.InteriorShapeCells = Enumerable.Range(0, 4)
|
|
.SelectMany(y => Enumerable.Range(0, 4).Select(x => new AetheriaRuntimeShapeCell(x, y)))
|
|
.ToArray();
|
|
var ore = CatalogItem("ore");
|
|
ore.Category = AetheriaRuntimeItemCategories.SimpleCommodity;
|
|
ore.ShapeWidth = 1;
|
|
ore.ShapeHeight = 1;
|
|
ore.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
ore.Stackable = true;
|
|
ore.MaxStack = 100;
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([cargoBay, ore], [], []);
|
|
AetheriaRuntimeLoadoutItemSlotCommit InstalledCargoBay() => new()
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = cargoBay.ItemKey, Quantity = 1 }
|
|
};
|
|
var origin = Entity(0, 0, "workers");
|
|
origin.Kind = "station";
|
|
origin.CargoBays = [InstalledCargoBay()];
|
|
origin.CargoContents = [Cargo(("ore", 5, 2, 3))];
|
|
var agent = Entity(1, 0, "workers");
|
|
agent.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Haul];
|
|
agent.CargoBays = [InstalledCargoBay()];
|
|
agent.CargoContents = [Cargo()];
|
|
var destination = Entity(2, 50, "workers");
|
|
destination.Kind = "station";
|
|
destination.CargoBays = [InstalledCargoBay()];
|
|
destination.CargoContents = [Cargo()];
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "agent-haul-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [origin, agent, destination] }]
|
|
};
|
|
var issue = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.IssueAgentTask,
|
|
"commander-smoke",
|
|
"starbridge-smoke",
|
|
0,
|
|
"");
|
|
issue.CommandId = "issue-haul";
|
|
issue.AgentTask = new AetheriaRuntimeAgentTaskCommand
|
|
{
|
|
TaskId = "haul-ore",
|
|
CorporationKey = "workers",
|
|
TaskType = AetheriaRuntimeAgentTaskTypes.Haul,
|
|
Priority = 40,
|
|
ZoneIndex = 0,
|
|
OriginEntityIndex = 0,
|
|
TargetEntityIndex = 2,
|
|
ItemKey = "ore",
|
|
Quantity = 3,
|
|
CompletionRadius = 5
|
|
};
|
|
var sawPickup = false;
|
|
var sawDelivery = false;
|
|
for (var frame = 0; frame < 30; frame++)
|
|
{
|
|
var tick = AetheriaRuntimeDaemonTickRunner.Tick(
|
|
Path.Combine(Path.GetTempPath(), "aetheria-agent-haul-smoke.cc"),
|
|
run,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
WorldPhysics = NewPhysics(),
|
|
FrameId = frame,
|
|
FixedDeltaSeconds = 0.1,
|
|
SimulationTimeSeconds = frame * 0.1,
|
|
ObservedCommands = frame == 0 ? [issue] : Array.Empty<AetheriaRuntimeDaemonCommandDocument>(),
|
|
Catalog = catalog,
|
|
BuildPublications = false
|
|
});
|
|
sawPickup |= tick.OperationResult.AppliedCommandIds.Any(id => id.EndsWith(":pickup", StringComparison.Ordinal));
|
|
sawDelivery |= tick.OperationResult.AppliedCommandIds.Any(id => id.EndsWith(":delivery", StringComparison.Ordinal));
|
|
if (string.Equals(run.AgentTasks.Single().Status, AetheriaRuntimeAgentTaskStatuses.Completed, StringComparison.Ordinal))
|
|
break;
|
|
}
|
|
|
|
Require(sawPickup && sawDelivery, "haul task must use accepted pickup and delivery cargo commands");
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Completed, run.AgentTasks.Single().Status,
|
|
"haul task must complete only after accepted delivery");
|
|
RequireEqual(2, CargoQuantity(origin, "ore"), "origin must retain the unrequested stack quantity");
|
|
RequireEqual(3, CargoQuantity(destination, "ore"), "destination must receive exactly the requested quantity");
|
|
RequireEqual(0, CargoQuantity(agent, "ore"), "hauler must finish with no in-transit cargo");
|
|
}
|
|
|
|
private static AetheriaRuntimeCargoBayLoadoutCommit Cargo(params (string ItemKey, int Quantity, int X, int Y)[] items) => new()
|
|
{
|
|
Items = items.Select(item => new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = item.X,
|
|
Y = item.Y,
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = item.ItemKey, Quantity = item.Quantity }
|
|
}).ToArray()
|
|
};
|
|
|
|
private static int CargoQuantity(AetheriaRuntimeEntitySnapshotCommit entity, string itemKey) =>
|
|
(entity.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>())
|
|
.SelectMany(bay => bay.Items)
|
|
.Where(slot => string.Equals(slot.Item.ItemKey, itemKey, StringComparison.Ordinal))
|
|
.Sum(slot => slot.Item.Quantity);
|
|
|
|
private static void AgentCompletesAttackTaskThroughTargetFireAndYmir()
|
|
{
|
|
var agent = Entity(0, 0, "player");
|
|
agent.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Attack];
|
|
agent.WeaponGroups = [new[] { 0 }];
|
|
agent.Equipment =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{
|
|
ItemKey = "test-lock-cannon", Quality = 1, Durability = 1, Enabled = true
|
|
}
|
|
},
|
|
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "test-attack-capacitor", Quality = 1, Durability = 1, Enabled = true } },
|
|
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "test-attack-sensor", Quality = 1, Durability = 1, Enabled = true } }
|
|
];
|
|
agent.BehaviorStates =
|
|
[
|
|
new AetheriaRuntimeBehaviorStateCommit
|
|
{
|
|
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind, OwnerIndex = 1, BehaviorIndex = 0,
|
|
BehaviorKind = "Capacitor", CapacitorCharge = 50, CapacitorCapacity = 50, CapacitorEfficiency = 1
|
|
}
|
|
];
|
|
agent.CargoContents = [Cargo(("test-ammo", 2, 0, 0))];
|
|
var weaponPayload = new AetheriaRuntimeBehaviorPayload(
|
|
0,
|
|
"LockWeapon",
|
|
0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, Number(1)),
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(7)),
|
|
new AetheriaRuntimeBehaviorField(3, PerformanceStat(1.5)),
|
|
new AetheriaRuntimeBehaviorField(4, PerformanceStat(2)),
|
|
new AetheriaRuntimeBehaviorField(6, PerformanceStat(145)),
|
|
new AetheriaRuntimeBehaviorField(9, PerformanceStat(5)),
|
|
new AetheriaRuntimeBehaviorField(10, PerformanceStat(3)),
|
|
new AetheriaRuntimeBehaviorField(12, new AetheriaRuntimeBehaviorValue(
|
|
"item-key", "", 0, false, "", "test-ammo",
|
|
Array.Empty<AetheriaRuntimeBehaviorValue>(), Array.Empty<AetheriaRuntimeBehaviorMapEntry>())),
|
|
new AetheriaRuntimeBehaviorField(13, Number(3)),
|
|
new AetheriaRuntimeBehaviorField(14, Number(0.3)),
|
|
new AetheriaRuntimeBehaviorField(16, PerformanceStat(330)),
|
|
new AetheriaRuntimeBehaviorField(17, PerformanceStat(3)),
|
|
new AetheriaRuntimeBehaviorField(18, PerformanceStat(0.3)),
|
|
new AetheriaRuntimeBehaviorField(19, PerformanceStat(0.2)),
|
|
new AetheriaRuntimeBehaviorField(21, PerformanceStat(2)),
|
|
new AetheriaRuntimeBehaviorField(22, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(23, PerformanceStat(45)),
|
|
new AetheriaRuntimeBehaviorField(24, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(25, PerformanceStat(1))
|
|
]);
|
|
var catalog = EquipThrusterBank(
|
|
[agent],
|
|
CatalogItem("test-lock-cannon", weaponPayload),
|
|
CatalogItem("test-attack-capacitor", CapacitorPayload(50, 1)),
|
|
CatalogItem("test-attack-sensor", SensorPayload()));
|
|
var target = Entity(1, 105, "raider");
|
|
target.Kind = "station";
|
|
target.Visibility = 10;
|
|
target.StatGrids = [Grid("hull", 14), Grid("shield", 0), Grid("heat", 0)];
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "agent-attack-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [agent, target] }]
|
|
};
|
|
var issue = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.IssueAgentTask,
|
|
"commander-smoke",
|
|
"starbridge-smoke",
|
|
0,
|
|
"");
|
|
issue.CommandId = "issue-attack";
|
|
issue.AgentTask = new AetheriaRuntimeAgentTaskCommand
|
|
{
|
|
TaskId = "attack-raider",
|
|
CorporationKey = "player",
|
|
TaskType = AetheriaRuntimeAgentTaskTypes.Attack,
|
|
Priority = 100,
|
|
ZoneIndex = 0,
|
|
TargetEntityIndex = 1,
|
|
CompletionRadius = 25,
|
|
WeaponGroup = 0
|
|
};
|
|
var sawTargetCommand = false;
|
|
var sawFireCommand = false;
|
|
var sawPartialLock = false;
|
|
var lockTrace = new List<double>();
|
|
var attackTrace = new List<string>();
|
|
var firstLaunchFrame = -1;
|
|
var physics = NewPhysics();
|
|
for (var frame = 0; frame < 60; frame++)
|
|
{
|
|
var tick = AetheriaRuntimeDaemonTickRunner.Tick(
|
|
Path.Combine(Path.GetTempPath(), "aetheria-agent-attack-smoke.cc"),
|
|
run,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
WorldPhysics = physics,
|
|
FrameId = frame,
|
|
FixedDeltaSeconds = 0.1,
|
|
SimulationTimeSeconds = frame * 0.1,
|
|
ObservedCommands = frame == 0 ? [issue] : Array.Empty<AetheriaRuntimeDaemonCommandDocument>(),
|
|
Catalog = catalog,
|
|
BuildPublications = false
|
|
});
|
|
sawTargetCommand |= tick.OperationResult.AppliedCommandIds.Any(id => id.EndsWith(":target", StringComparison.Ordinal));
|
|
sawFireCommand |= tick.OperationResult.AppliedCommandIds.Any(id => id.Contains(":fire-", StringComparison.Ordinal));
|
|
var weapon = (agent.WeaponStates ?? Array.Empty<AetheriaRuntimeWeaponStateCommit>())
|
|
.Single(value => value.OwnerKind == AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind);
|
|
lockTrace.Add(weapon.LockProgress);
|
|
attackTrace.Add($"{frame}:{agent.TargetEntityIndex}:{agent.PositionX:0.##},{agent.PositionZ:0.##}:{agent.DirectionX:0.##},{agent.DirectionY:0.##}:{weapon.LockTargetEntityIndex}");
|
|
sawPartialLock |= weapon.LockProgress > 0 && weapon.LockProgress < 0.99;
|
|
if (firstLaunchFrame < 0 && run.GameEvents.Any(value => value.Kind == "shot.committed"))
|
|
firstLaunchFrame = frame;
|
|
if (string.Equals(run.AgentTasks.Single().Status, AetheriaRuntimeAgentTaskStatuses.Completed, StringComparison.Ordinal))
|
|
break;
|
|
}
|
|
|
|
Require(sawTargetCommand, "attack agent must target through the shared target command");
|
|
Require(sawFireCommand, "attack agent must fire through the shared weapon-group command");
|
|
Require(sawPartialLock,
|
|
$"attack agent must acquire a persisted partial weapon lock before firing; locks={string.Join(',', lockTrace.Select(value => value.ToString("0.###", CultureInfo.InvariantCulture)))} states={string.Join('|', attackTrace)}");
|
|
Require(firstLaunchFrame > 1, "accepted fire intent must not bypass progressive weapon lock acquisition");
|
|
Require(run.GameEvents.Any(value => value.Kind == "shot.committed" && value.SourceEntityIndex == agent.EntityIndex),
|
|
"accepted fire control must emit authoritative shot commitment chronology");
|
|
Require(run.GameEvents.Any(value => value.Kind == "shot.committed" &&
|
|
value.ItemKey == "test-lock-cannon" && Math.Abs(value.ScalarValue - (7.0 / 3.0)) < 0.000001),
|
|
"daemon combat must divide authored damage across the configured burst rounds");
|
|
Require((agent.WeaponStates ?? Array.Empty<AetheriaRuntimeWeaponStateCommit>()).Any(value => value.OwnerKind == AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind &&
|
|
value.OwnerIndex == 0 && value.BehaviorKind == "LockWeapon"),
|
|
"weapon progress must belong to the equipped authored behavior instead of a synthetic entity weapon");
|
|
var authoredWeapon = agent.WeaponStates.Single(value =>
|
|
value.OwnerKind == AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind);
|
|
RequireEqual(0, authoredWeapon.Ammo,
|
|
"two three-round authored bursts must consume both complete magazines");
|
|
RequireNear(40, agent.BehaviorStates.Single(value => value.BehaviorKind == "Capacitor").CapacitorCharge,
|
|
0.000001, "two authored bursts must drain exactly their combined energy from canonical capacitor state");
|
|
RequireEqual(1, CargoQuantity(agent, "test-ammo"),
|
|
"one empty magazine must consume exactly one reserve cargo commodity before reload");
|
|
Require(run.GameEvents.Count(value => value.Kind == "shot.committed" && value.ItemKey == "test-lock-cannon") >= 6,
|
|
"two authored bursts must commit all six due rounds through the shot resolver");
|
|
Require(run.ShotReceipts.Count(value => value.WeaponItemKey == "test-lock-cannon") >= 6 &&
|
|
run.ShotReceipts.All(value => value.Hit && value.HullAppliedDamage >= 0 &&
|
|
value.HullAppliedDamage <= value.NominalDamage && value.ShieldAbsorbedDamage == 0 &&
|
|
value.DamageType == "Corrosive" && Math.Abs(value.Penetration - 1.5) < 0.000001 &&
|
|
Math.Abs(value.DamageSpread - 2) < 0.000001),
|
|
"authored rounds must retain damage type, penetration, spread, and exact damage receipts before presentation travel");
|
|
Require(run.ShotReceipts.Select(value => value.ShotId).Distinct(StringComparer.Ordinal).Count() ==
|
|
run.ShotReceipts.Count,
|
|
"every burst round must own a stable independent shot identity");
|
|
Require(run.GameEvents.Count(value => value.Kind == "weapon.reload.started") == 1 &&
|
|
run.GameEvents.Count(value => value.Kind == "weapon.reload.completed") == 1,
|
|
"empty magazine transition must publish one authoritative reload start and completion pair");
|
|
var reloadStarted = run.GameEvents.Single(value => value.Kind == "weapon.reload.started").FrameId;
|
|
var reloadCompleted = run.GameEvents.Single(value => value.Kind == "weapon.reload.completed").FrameId;
|
|
Require(reloadCompleted > reloadStarted && !run.GameEvents.Any(value =>
|
|
value.Kind == "shot.committed" && value.FrameId > reloadStarted && value.FrameId < reloadCompleted),
|
|
"reload interval must be a shot-free authoritative timeline, not a cosmetic client delay");
|
|
Require(!target.IsActive,
|
|
$"attack task must end through daemon shot resolution before presentation projectile contacts; hull={Stat(target, "hull"):0.###} " +
|
|
$"agent={agent.PositionX:0.###},{agent.PositionZ:0.###} target={target.PositionX:0.###},{target.PositionZ:0.###} " +
|
|
$"projectiles={run.Zones[0].PhysicalPayloads.Count} task={run.AgentTasks.Single().Status}");
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Completed, run.AgentTasks.Single().Status,
|
|
"attack task must complete when its target dies");
|
|
var shotSurface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 60, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
Require(Flatten(shotSurface.Surface.Root).Any(node => node.Kind == "shot.receipt" &&
|
|
node.Props["itemKey"] == "test-lock-cannon" && node.Props["outcome"] == "hit" &&
|
|
node.Props["presentationKind"] == "bolt" &&
|
|
node.Props.ContainsKey("presentationIntensity") &&
|
|
node.Props.ContainsKey("impactKind")),
|
|
"Eve must project inspectable deterministic shot receipts without reconstructing combat");
|
|
}
|
|
|
|
private static void AttackTaskAdmissionRejectsImpossibleWork()
|
|
{
|
|
var agent = Entity(0, 0, "player");
|
|
agent.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Attack];
|
|
agent.WeaponGroups = [new[] { 0 }];
|
|
var hostile = Entity(1, 100, "raider");
|
|
var friendly = Entity(2, 100, "player");
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "attack-admission-smoke",
|
|
CurrentEntityKey = "",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [agent, hostile, friendly] }]
|
|
};
|
|
|
|
AetheriaRuntimeDaemonCommandDocument Command(string id, int targetIndex, int weaponGroup)
|
|
{
|
|
var command = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.IssueAgentTask, "commander-smoke", "starbridge-smoke", 0, "");
|
|
command.CommandId = id;
|
|
command.AgentTask = new AetheriaRuntimeAgentTaskCommand
|
|
{
|
|
TaskId = id,
|
|
CorporationKey = "player",
|
|
TaskType = AetheriaRuntimeAgentTaskTypes.Attack,
|
|
ZoneIndex = 0,
|
|
TargetEntityIndex = targetIndex,
|
|
WeaponGroup = weaponGroup
|
|
};
|
|
return command;
|
|
}
|
|
|
|
var result = AetheriaRuntimeDaemonOperations.Execute(run,
|
|
[
|
|
Command("missing-target", 99, 0),
|
|
Command("friendly-target", 2, 0),
|
|
Command("missing-group", 1, 1),
|
|
Command("valid-attack", 1, 0)
|
|
]);
|
|
|
|
Require(result.RejectedCommandIds.SequenceEqual(new[] { "missing-target", "friendly-target", "missing-group" }),
|
|
"attack admission must reject missing/friendly targets and unavailable weapon groups");
|
|
Require(result.AppliedCommandIds.SequenceEqual(new[] { "valid-attack" }),
|
|
"attack admission must accept executable hostile work");
|
|
Require(run.AgentTasks.Count == 1 && run.AgentTasks[0].TaskId == "valid-attack",
|
|
"rejected attack orders must not enter the durable corporation queue");
|
|
}
|
|
|
|
private static void AttackAgentControlsOptimumRangeThroughMovementLever()
|
|
{
|
|
var closing = AttackPlanAtDistance(250, facingTarget: false);
|
|
var closingMove = closing.Commands.Single(command => command.Kind == AetheriaRuntimeDaemonCommandKinds.SetMoveVector);
|
|
Require(closingMove.DirectionX > 0.5 && closingMove.DirectionY < -0.5 && closingMove.ScalarValue > 0.99,
|
|
"loadout-derived long-range attack must blend closing motion with the fossil target-relative strafe");
|
|
|
|
var retreating = AttackPlanAtDistance(50, facingTarget: false);
|
|
var retreatingMove = retreating.Commands.Single(command => command.Kind == AetheriaRuntimeDaemonCommandKinds.SetMoveVector);
|
|
Require(retreatingMove.DirectionX < -0.5 && retreatingMove.DirectionY < -0.5,
|
|
"loadout-derived short-range attack must blend opening motion with the fossil target-relative strafe");
|
|
|
|
var broadside = AttackPlanAtDistance(150, facingTarget: false);
|
|
Require(!broadside.Commands.Any(command => command.Kind == AetheriaRuntimeDaemonCommandKinds.FireWeaponGroup),
|
|
"an attack agent must not fire a selected group through a hardpoint facing away from its aim");
|
|
|
|
var firing = AttackPlanAtDistance(150, facingTarget: true, targetVelocityZ: 8);
|
|
RequireEqual(1, firing.Task.WeaponGroup,
|
|
"agent combat must select the highest current range-DPS group from operational equipment");
|
|
var fire = firing.Commands.Single(command => command.Kind == AetheriaRuntimeDaemonCommandKinds.FireWeaponGroup);
|
|
RequireEqual(1, fire.WeaponGroup,
|
|
"the daemon must fire the derived group instead of the task's stale authored group");
|
|
var look = firing.Commands.Single(command => command.Kind == AetheriaRuntimeDaemonCommandKinds.SetLookDirection);
|
|
Require(look.DirectionX > 0.9 && look.PositionZ > 0.05,
|
|
"projectile agent aim must lead target velocity through CultMath first-order intercept semantics");
|
|
}
|
|
|
|
private static (IReadOnlyList<AetheriaRuntimeDaemonCommandDocument> Commands, AetheriaRuntimeAgentTaskCommit Task)
|
|
AttackPlanAtDistance(double distance, bool facingTarget, double targetVelocityZ = 0)
|
|
{
|
|
var agent = Entity(0, 0, "workers");
|
|
agent.DirectionX = facingTarget ? 1 : 0;
|
|
agent.DirectionY = facingTarget ? 0 : 1;
|
|
agent.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Attack];
|
|
agent.Equipment =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Rotation = "None",
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "agent-short-weapon", Quality = 1, Durability = 1, Enabled = true }
|
|
},
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Rotation = "None",
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "agent-long-weapon", Quality = 1, Durability = 1, Enabled = true }
|
|
}
|
|
];
|
|
agent.WeaponGroups = [new[] { 0 }, new[] { 1 }];
|
|
var target = Entity(1, distance, "raiders");
|
|
target.DirectionY = 1;
|
|
target.VelocityY = targetVelocityZ;
|
|
var task = new AetheriaRuntimeAgentTaskCommit
|
|
{
|
|
TaskId = "range-control",
|
|
CorporationKey = "workers",
|
|
TaskType = AetheriaRuntimeAgentTaskTypes.Attack,
|
|
ZoneIndex = 0,
|
|
TargetEntityIndex = 1,
|
|
WeaponGroup = 0
|
|
};
|
|
AetheriaRuntimeBehaviorPayload Weapon(double damage, double minimumRange, double range, double velocity) =>
|
|
new(0, AetheriaRuntimeBehaviorKinds.InstantWeapon, 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(damage)),
|
|
new AetheriaRuntimeBehaviorField(5, PerformanceStat(minimumRange)),
|
|
new AetheriaRuntimeBehaviorField(6, PerformanceStat(range)),
|
|
new AetheriaRuntimeBehaviorField(16, PerformanceStat(velocity)),
|
|
new AetheriaRuntimeBehaviorField(17, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(19, PerformanceStat(1))
|
|
]);
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
|
[
|
|
CatalogItem("agent-short-weapon", Weapon(10, 5, 100, 80)),
|
|
CatalogItem("agent-long-weapon", Weapon(40, 25, 200, 100))
|
|
], [], []);
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
GenerationSeed = 123,
|
|
RunId = "agent-range-control-smoke",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [agent, target] }],
|
|
AgentTasks = [task]
|
|
};
|
|
|
|
return (AetheriaRuntimeAgentScheduler.AssignAndPlan(run, 1, catalog), task);
|
|
}
|
|
|
|
private static void SchedulerAssignsHighestPriorityCompatibleTask()
|
|
{
|
|
var agent = Entity(0, 0, "workers");
|
|
agent.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Explore];
|
|
var low = AgentTask("low", 1);
|
|
var high = AgentTask("high", 99);
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "agent-priority-smoke",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [agent] }],
|
|
AgentTasks = [low, high]
|
|
};
|
|
|
|
AetheriaRuntimeAgentScheduler.AssignAndPlan(run, 4);
|
|
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Assigned, high.Status,
|
|
"highest priority compatible task must claim the available agent");
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Queued, low.Status,
|
|
"lower priority task must remain queued when no controller remains");
|
|
RequireEqual("high", agent.AssignedAgentTaskId, "agent assignment must point at the selected task");
|
|
}
|
|
|
|
private static void SchedulerRequeuesTaskFromDeadAgent()
|
|
{
|
|
var dead = Entity(0, 0, "workers");
|
|
dead.IsActive = false;
|
|
dead.AssignedAgentTaskId = "recover-work";
|
|
var replacement = Entity(1, 0, "workers");
|
|
replacement.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Explore];
|
|
var task = new AetheriaRuntimeAgentTaskCommit
|
|
{
|
|
TaskId = "recover-work",
|
|
CorporationKey = "workers",
|
|
TaskType = AetheriaRuntimeAgentTaskTypes.Explore,
|
|
ZoneIndex = 0,
|
|
Status = AetheriaRuntimeAgentTaskStatuses.Assigned,
|
|
AssignedEntityIndex = 0,
|
|
TargetPositionX = 100,
|
|
DeliveredQuantity = 3,
|
|
RequestedQuantity = 5
|
|
};
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "agent-recovery-smoke",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [dead, replacement] }],
|
|
AgentTasks = [task]
|
|
};
|
|
|
|
AetheriaRuntimeAgentScheduler.AssignAndPlan(run, 8);
|
|
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Assigned, task.Status,
|
|
"work abandoned by an inactive agent must return to the corporation queue and be reassigned");
|
|
RequireEqual(replacement.EntityIndex, task.AssignedEntityIndex,
|
|
"compatible active replacement must receive abandoned work");
|
|
RequireEqual(3, task.DeliveredQuantity,
|
|
"reassignment must preserve authoritative task progress");
|
|
Require(string.IsNullOrWhiteSpace(dead.AssignedAgentTaskId),
|
|
"inactive agent must not retain reservation authority");
|
|
}
|
|
|
|
private static void SchedulerCollapsesDuplicateAssignmentMarkers()
|
|
{
|
|
var first = Entity(0, 0, "workers");
|
|
first.AssignedAgentTaskId = "single-owner";
|
|
var duplicate = Entity(1, 0, "workers");
|
|
duplicate.AssignedAgentTaskId = "single-owner";
|
|
duplicate.HelmForward = 1;
|
|
duplicate.TargetEntityIndex = 7;
|
|
var task = AgentTask("single-owner", 1);
|
|
task.Status = AetheriaRuntimeAgentTaskStatuses.Assigned;
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "agent-single-owner-smoke",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [first, duplicate] }],
|
|
AgentTasks = [task]
|
|
};
|
|
|
|
AetheriaRuntimeAgentScheduler.AssignAndPlan(run, 9);
|
|
|
|
RequireEqual(1, run.Zones.SelectMany(zone => zone.Entities)
|
|
.Count(entity => string.Equals(entity.AssignedAgentTaskId, task.TaskId, StringComparison.Ordinal)),
|
|
"one task must have exactly one active carrier after reconciliation");
|
|
RequireEqual(first.EntityIndex, task.AssignedEntityIndex,
|
|
"duplicate assignment reconciliation must be deterministic");
|
|
RequireNear(0, duplicate.HelmForward, 0.000001,
|
|
"duplicate assignment reconciliation must release the losing scheduler controls");
|
|
RequireEqual(-1, duplicate.TargetEntityIndex,
|
|
"duplicate assignment reconciliation must release the losing scheduler target");
|
|
}
|
|
|
|
private static void CancellingAgentTaskReleasesSchedulerControls()
|
|
{
|
|
var agent = Entity(0, 0, "workers");
|
|
agent.AssignedAgentTaskId = "tow-station";
|
|
agent.HelmStrafe = 0.75;
|
|
agent.HelmForward = 1;
|
|
agent.TargetEntityIndex = 2;
|
|
agent.ChildEntityIndices = [2];
|
|
var station = Entity(2, 0, "workers");
|
|
station.Kind = "station";
|
|
var task = new AetheriaRuntimeAgentTaskCommit
|
|
{
|
|
TaskId = "tow-station",
|
|
CorporationKey = "workers",
|
|
TaskType = AetheriaRuntimeAgentTaskTypes.Tow,
|
|
Status = AetheriaRuntimeAgentTaskStatuses.Assigned,
|
|
AssignedEntityIndex = 0,
|
|
TargetEntityIndex = 2,
|
|
PendingQuantity = 4,
|
|
Phase = "delivery"
|
|
};
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "agent-cancel-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "",
|
|
AgentTasks = [task],
|
|
Zones =
|
|
[
|
|
new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [agent, Entity(1, 0, "workers"), station]
|
|
}
|
|
]
|
|
};
|
|
var cancel = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.CancelAgentTask,
|
|
"commander-smoke",
|
|
"starbridge-smoke",
|
|
12,
|
|
"");
|
|
cancel.CommandId = "cancel-tow-station";
|
|
cancel.AgentTask = new AetheriaRuntimeAgentTaskCommand { TaskId = task.TaskId };
|
|
|
|
var result = AetheriaRuntimeDaemonOperations.Execute(
|
|
run,
|
|
[cancel],
|
|
new AetheriaRuntimeDaemonOperationContext());
|
|
|
|
Require(result.AppliedCommandIds.Contains(cancel.CommandId, StringComparer.Ordinal),
|
|
"task cancellation must commit through the daemon command boundary");
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Cancelled, task.Status,
|
|
"cancelled task must become terminal immediately");
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Cancelled, task.Phase,
|
|
"cancelled task must not retain a counterfeit in-progress phase");
|
|
RequireEqual(-1, task.AssignedEntityIndex,
|
|
"cancelled task must release its assigned agent identity");
|
|
RequireEqual(0, task.PendingQuantity,
|
|
"cancelled transfer work must not retain pending quantity");
|
|
RequireEqual("", agent.AssignedAgentTaskId,
|
|
"cancelled agent must release the durable assignment marker");
|
|
RequireNear(0, agent.HelmStrafe, 0.000001,
|
|
"cancelled agent must release scheduler-owned strafe input");
|
|
RequireNear(0, agent.HelmForward, 0.000001,
|
|
"cancelled agent must release scheduler-owned forward input");
|
|
RequireEqual(-1, agent.TargetEntityIndex,
|
|
"cancelled agent must release scheduler-owned targeting");
|
|
Require(!agent.ChildEntityIndices.Contains(station.EntityIndex),
|
|
"cancelling an active tow must release the station at its authoritative world position");
|
|
RequireEqual(0, AetheriaRuntimeAgentScheduler.AssignAndPlan(run, 13).Count,
|
|
"cancelled work must not produce another scheduler command on the next frame");
|
|
}
|
|
|
|
private static AetheriaRuntimeAgentTaskCommit AgentTask(string id, int priority) => new()
|
|
{
|
|
TaskId = id,
|
|
CorporationKey = "workers",
|
|
TaskType = AetheriaRuntimeAgentTaskTypes.Explore,
|
|
Priority = priority,
|
|
ZoneIndex = 0,
|
|
TargetPositionX = 100,
|
|
CompletionRadius = 5
|
|
};
|
|
|
|
private static void SchedulerAssignsShortestGalaxyRoute()
|
|
{
|
|
var distant = Entity(0, 0, "workers");
|
|
distant.EntityId = "worker.route.distant";
|
|
distant.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Explore];
|
|
var nearby = Entity(0, 0, "workers");
|
|
nearby.EntityId = "worker.route.nearby";
|
|
nearby.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Explore];
|
|
var task = AgentTask("route-choice", 10);
|
|
task.ZoneIndex = 2;
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "agent-route-choice-smoke",
|
|
Zones =
|
|
[
|
|
new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, PositionX = 0, GravityTerrainRadius = 100, AdjacentZoneIndices = [1], Entities = [distant] },
|
|
new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 1, PositionX = 100, GravityTerrainRadius = 100, AdjacentZoneIndices = [0, 2], Entities = Array.Empty<AetheriaRuntimeEntitySnapshotCommit>() },
|
|
new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 2, PositionX = 200, GravityTerrainRadius = 100, AdjacentZoneIndices = [1, 3], Entities = Array.Empty<AetheriaRuntimeEntitySnapshotCommit>() },
|
|
new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 3, PositionX = 300, GravityTerrainRadius = 100, AdjacentZoneIndices = [2], Entities = [nearby] }
|
|
],
|
|
AgentTasks = [task]
|
|
};
|
|
|
|
AetheriaRuntimeAgentScheduler.AssignAndPlan(run, 1);
|
|
|
|
RequireEqual("route-choice", nearby.AssignedAgentTaskId,
|
|
"scheduler must reserve the controller with the shortest galaxy route to the task zone");
|
|
Require(string.IsNullOrWhiteSpace(distant.AssignedAgentTaskId),
|
|
"longer-route controller must remain available");
|
|
}
|
|
|
|
private static void AgentTraversesGalaxyRouteBeforeExecutingTask()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "aetheria-agent-route-restart-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(root);
|
|
var statePath = Path.Combine(root, "world.cc");
|
|
try
|
|
{
|
|
var agent = Entity(0, 0, "workers");
|
|
agent.EntityId = "worker.cross-zone.stable";
|
|
agent.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Explore];
|
|
var catalog = EquipThrusterBank([agent]);
|
|
var task = AgentTask("cross-zone", 10);
|
|
task.ZoneIndex = 2;
|
|
task.TargetPositionX = 0;
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "agent-route-travel-smoke",
|
|
Zones =
|
|
[
|
|
new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, PositionX = 0, GravityTerrainRadius = 100, AdjacentZoneIndices = [1], Entities = [agent] },
|
|
new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 1, PositionX = 100, GravityTerrainRadius = 100, AdjacentZoneIndices = [0, 2], Entities = Array.Empty<AetheriaRuntimeEntitySnapshotCommit>() },
|
|
new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 2, PositionX = 200, GravityTerrainRadius = 100, AdjacentZoneIndices = [1], Entities = Array.Empty<AetheriaRuntimeEntitySnapshotCommit>() }
|
|
],
|
|
AgentTasks = [task]
|
|
};
|
|
var appliedTravelCommands = 0;
|
|
var appliedApproachCommands = 0;
|
|
var restarted = false;
|
|
var physics = NewPhysics();
|
|
for (var frameId = 0; frameId < 400; frameId++)
|
|
{
|
|
var tick = AetheriaRuntimeDaemonTickRunner.Tick(
|
|
statePath,
|
|
run,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
WorldPhysics = physics,
|
|
Catalog = catalog,
|
|
FrameId = frameId,
|
|
FixedDeltaSeconds = 0.25,
|
|
SimulationTimeSeconds = frameId * 0.25,
|
|
BuildPublications = false
|
|
});
|
|
appliedTravelCommands += tick.OperationResult.AppliedCommandIds.Count(id => id.EndsWith(":travel", StringComparison.Ordinal));
|
|
appliedApproachCommands += tick.OperationResult.AppliedCommandIds.Count(id => id.EndsWith(":travel-approach", StringComparison.Ordinal));
|
|
agent = run.Zones.SelectMany(zone => zone.Entities)
|
|
.Single(entity => entity.EntityId == "worker.cross-zone.stable");
|
|
|
|
if (!restarted && string.Equals(agent.WormholeTransition?.Phase, "entering", StringComparison.Ordinal))
|
|
{
|
|
var eventCountsBefore = (
|
|
Started: run.GameEvents.Count(value => value.Kind == "wormhole.enter.started"),
|
|
Transferred: run.GameEvents.Count(value => value.Kind == "wormhole.transferred"),
|
|
Completed: run.GameEvents.Count(value => value.Kind == "wormhole.exit.completed"));
|
|
var durableFrame = new AetheriaRuntimeDaemonFrameDocument
|
|
{
|
|
FrameId = frameId,
|
|
FixedDeltaSeconds = 0.25,
|
|
SimulationTimeSeconds = frameId * 0.25,
|
|
Run = run
|
|
};
|
|
using (var node = AetheriaStateNode.OpenAsync(statePath).GetAwaiter().GetResult())
|
|
{
|
|
node.MutableDocument<AetheriaRuntimeDaemonFrameDocument>(
|
|
AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest)
|
|
.ReplaceAsync(durableFrame).GetAwaiter().GetResult();
|
|
node.FlushAsync(soft: false).GetAwaiter().GetResult();
|
|
}
|
|
using (var node = AetheriaStateNode.OpenAsync(statePath).GetAwaiter().GetResult())
|
|
{
|
|
durableFrame = node.MutableDocument<AetheriaRuntimeDaemonFrameDocument>(
|
|
AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest)
|
|
.ReadAsync().GetAwaiter().GetResult()
|
|
?? throw new InvalidOperationException("Durable cross-zone agent frame is missing.");
|
|
}
|
|
|
|
run = durableFrame.Run;
|
|
task = run.AgentTasks.Single();
|
|
agent = run.Zones.SelectMany(zone => zone.Entities)
|
|
.Single(entity => entity.EntityId == "worker.cross-zone.stable");
|
|
physics.Dispose();
|
|
physics = NewPhysics();
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Assigned, task.Status,
|
|
"cross-zone restart must preserve the assigned task during wormhole entry");
|
|
RequireEqual(task.TaskId, agent.AssignedAgentTaskId,
|
|
"cross-zone restart must preserve the task carrier edge");
|
|
RequireEqual("entering", agent.WormholeTransition?.Phase ?? "",
|
|
"cross-zone restart must preserve the exact wormhole phase");
|
|
Require(eventCountsBefore == (
|
|
run.GameEvents.Count(value => value.Kind == "wormhole.enter.started"),
|
|
run.GameEvents.Count(value => value.Kind == "wormhole.transferred"),
|
|
run.GameEvents.Count(value => value.Kind == "wormhole.exit.completed")),
|
|
"CultCache reopen must not replay wormhole chronology before the next daemon tick");
|
|
restarted = true;
|
|
}
|
|
|
|
if (string.Equals(task.Status, AetheriaRuntimeAgentTaskStatuses.Completed, StringComparison.Ordinal))
|
|
break;
|
|
}
|
|
|
|
Require(restarted,
|
|
"cross-zone task proof must hard-restart while the worker is inside the wormhole timeline");
|
|
Require(appliedApproachCommands > 0,
|
|
"agent must approach wormholes through shared movement commands and Ymir before transition");
|
|
RequireEqual(2, appliedTravelCommands,
|
|
"agent must traverse each galaxy edge through the shared wormhole command boundary");
|
|
RequireEqual(2, run.GameEvents.Count(value => value.Kind == "wormhole.enter.started"),
|
|
"two route edges must publish exactly two retained entry events across restart");
|
|
RequireEqual(2, run.GameEvents.Count(value => value.Kind == "wormhole.transferred"),
|
|
"two route edges must publish exactly two retained transfer events across restart");
|
|
RequireEqual(2, run.GameEvents.Count(value => value.Kind == "wormhole.exit.completed"),
|
|
"two route edges must publish exactly two retained completion events across restart");
|
|
RequireEqual(1, run.Zones.Sum(zone => zone.Entities.Count),
|
|
"cross-zone restart must not clone the durable worker into another zone");
|
|
Require(run.Zones.Single(zone => zone.ZoneIndex == 2).Entities.Count == 1,
|
|
"assigned agent must arrive in the task zone without a parallel teleport owner");
|
|
RequireEqual("worker.cross-zone.stable", run.Zones.Single(zone => zone.ZoneIndex == 2).Entities.Single().EntityId,
|
|
"zone transfer must preserve stable entity identity while projection indices change");
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Completed, task.Status,
|
|
"agent must execute the task only after arriving in its destination zone");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
private static void WormholeTraversalUsesPersistedEnterTransferExitPhases()
|
|
{
|
|
var ship = Entity(0, 75, "player");
|
|
ship.EntityId = "wormhole-traveler";
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "wormhole-transition-smoke",
|
|
GenerationSeed = 77,
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
DiscoveredZoneIndices = [0],
|
|
Zones =
|
|
[
|
|
new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0, PositionX = 0, GravityTerrainRadius = 100,
|
|
AdjacentZoneIndices = [1], Entities = [ship]
|
|
},
|
|
new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 1, PositionX = 100, GravityTerrainRadius = 100,
|
|
AdjacentZoneIndices = [0, 2], Entities = Array.Empty<AetheriaRuntimeEntitySnapshotCommit>()
|
|
},
|
|
new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 2, PositionX = 200, GravityTerrainRadius = 100,
|
|
AdjacentZoneIndices = [1], Entities = Array.Empty<AetheriaRuntimeEntitySnapshotCommit>()
|
|
}
|
|
]
|
|
};
|
|
AetheriaRuntimeDaemonCommandDocument Travel(string id)
|
|
{
|
|
var command = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.EnterWormhole,
|
|
"pilot",
|
|
id,
|
|
0,
|
|
run.CurrentEntityKey);
|
|
command.CommandId = id;
|
|
command.TargetZoneIndex = 1;
|
|
return command;
|
|
}
|
|
var physics = NewPhysics();
|
|
AetheriaRuntimeDaemonTickResult Tick(int frame, params AetheriaRuntimeDaemonCommandDocument[] commands) =>
|
|
AetheriaRuntimeDaemonTickRunner.Tick(
|
|
Path.Combine(Path.GetTempPath(), "aetheria-wormhole-transition-smoke.cc"),
|
|
run,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
WorldPhysics = physics,
|
|
FrameId = frame,
|
|
FixedDeltaSeconds = 1,
|
|
SimulationTimeSeconds = frame + 1,
|
|
ObservedCommands = commands,
|
|
BuildPublications = false
|
|
});
|
|
|
|
var entering = Tick(0, Travel("wormhole-start"), Travel("wormhole-same-batch"));
|
|
Require(entering.OperationResult.RejectedCommandIds.Any(id => id.Contains("wormhole-same-batch", StringComparison.Ordinal)),
|
|
"one command batch must not admit two competing transition intents for the same entity");
|
|
RequireEqual(0, run.CurrentZoneIndex,
|
|
"wormhole command acceptance must not splice the ship into the destination immediately");
|
|
RequireEqual("entering", ship.WormholeTransition?.Phase ?? "",
|
|
"daemon must persist the entering phase on the authoritative entity");
|
|
Require(Math.Abs((ship.WormholeTransition?.Progress ?? 0) - 0.25) < 0.0001,
|
|
"fossil four-second entry timing must advance from fixed daemon time");
|
|
Require(Math.Abs((ship.WormholeTransition?.VisualDepthOffset ?? 0) + 250) < 0.0001,
|
|
"transition must expose the fossil depth lever without making the client own motion");
|
|
var enteringView = AetheriaRuntimeGameDocuments.CurrentEntity(entering.Frame).WormholeTransition;
|
|
RequireEqual("entering", enteringView?.Phase ?? "",
|
|
"current-entity projection must expose the same transition state to Eve consumers");
|
|
|
|
run = MessagePack.MessagePackSerializer.Deserialize<AetheriaRuntimeRunCheckpointCommit>(
|
|
MessagePack.MessagePackSerializer.Serialize(run));
|
|
ship = run.Zones.Single(zone => zone.ZoneIndex == 0).Entities.Single();
|
|
physics.Dispose();
|
|
physics = NewPhysics();
|
|
RequireEqual("entering", ship.WormholeTransition?.Phase ?? "",
|
|
"daemon restart must retain the exact mid-entry phase instead of repairing it from client state");
|
|
|
|
var duplicate = Tick(1, Travel("wormhole-duplicate"));
|
|
Require(duplicate.OperationResult.RejectedCommandIds.Any(id => id.Contains("wormhole-duplicate", StringComparison.Ordinal)),
|
|
"an entity already inside a wormhole transition must reject a second traversal owner");
|
|
Tick(2);
|
|
Tick(3);
|
|
RequireEqual(1, run.CurrentZoneIndex,
|
|
"zone ownership must transfer only after the entering phase completes");
|
|
var arrived = run.Zones.Single(zone => zone.ZoneIndex == 1).Entities.Single();
|
|
RequireEqual("exiting", arrived.WormholeTransition?.Phase ?? "",
|
|
"transferred ship must persist a distinct exiting phase");
|
|
Require(run.DiscoveredZoneIndices.Contains(2),
|
|
"arrival must reveal the destination adjacency using the fossil discovery rule");
|
|
|
|
Tick(4);
|
|
Tick(5);
|
|
Tick(6);
|
|
Tick(7);
|
|
RequireEqual("completed", arrived.WormholeTransition?.Phase ?? "",
|
|
"the completion frame must preserve a presentation lever before normal physics resumes");
|
|
Require(Math.Abs(Math.Sqrt(arrived.VelocityX * arrived.VelocityX + arrived.VelocityY * arrived.VelocityY) - 20) < 0.0001,
|
|
"completed exit must restore the fossil twenty-unit launch velocity");
|
|
Require(Math.Abs(Math.Sqrt(Math.Pow(arrived.PositionX + 75, 2) + Math.Pow(arrived.PositionZ, 2)) - 50) < 0.001,
|
|
"completed exit must place the ship fifty units from the destination wormhole");
|
|
Tick(8);
|
|
Require(arrived.WormholeTransition == null,
|
|
"the tick after completion must release input and Ymir instead of leaving a permanent transition flag");
|
|
RequireEqual(1, run.GameEvents.Count(value => value.Kind == "wormhole.enter.started"),
|
|
"entry feedback must have stable exactly-once chronology");
|
|
RequireEqual(1, run.GameEvents.Count(value => value.Kind == "wormhole.transferred"),
|
|
"transfer feedback must have stable exactly-once chronology");
|
|
RequireEqual(1, run.GameEvents.Count(value => value.Kind == "wormhole.exit.completed"),
|
|
"exit feedback must have stable exactly-once chronology");
|
|
}
|
|
|
|
private static void IdleAgentReturnsToCanonicalHomeAndDocks()
|
|
{
|
|
var worker = Entity(0, 0, "workers");
|
|
worker.EntityId = "worker.return-home";
|
|
worker.HomeEntityId = "station.home";
|
|
worker.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Haul];
|
|
var home = Entity(0, 60, "workers");
|
|
home.EntityId = "station.home";
|
|
home.Kind = "station";
|
|
home.DockingBays = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "station-docking-bay" }
|
|
}];
|
|
home.DockingBayAssignments = [-1];
|
|
home.DockingBayContents = [Cargo()];
|
|
var catalog = EquipThrusterBank([worker]);
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "agent-return-home-smoke",
|
|
Zones =
|
|
[
|
|
new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, PositionX = 0, GravityTerrainRadius = 100, AdjacentZoneIndices = [1], Entities = [worker] },
|
|
new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 1, PositionX = 100, GravityTerrainRadius = 100, AdjacentZoneIndices = [0, 2], Entities = Array.Empty<AetheriaRuntimeEntitySnapshotCommit>() },
|
|
new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 2, PositionX = 200, GravityTerrainRadius = 100, AdjacentZoneIndices = [1], Entities = [home] }
|
|
]
|
|
};
|
|
var sawApproach = false;
|
|
var travelCount = 0;
|
|
var sawDock = false;
|
|
var rejectedHomeCommands = new List<string>();
|
|
var minimumHomeDistance = double.PositiveInfinity;
|
|
var maximumHomeSpeed = 0.0;
|
|
var physics = NewPhysics();
|
|
for (var frame = 0; frame < 500; frame++)
|
|
{
|
|
var tick = AetheriaRuntimeDaemonTickRunner.Tick(
|
|
Path.Combine(Path.GetTempPath(), "aetheria-agent-return-home-smoke.cc"),
|
|
run,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
WorldPhysics = physics,
|
|
Catalog = catalog,
|
|
FrameId = frame,
|
|
FixedDeltaSeconds = 0.25,
|
|
SimulationTimeSeconds = frame * 0.25,
|
|
BuildPublications = false
|
|
});
|
|
sawApproach |= tick.OperationResult.AppliedCommandIds.Any(id => id.Contains(":home-approach", StringComparison.Ordinal));
|
|
travelCount += tick.OperationResult.AppliedCommandIds.Count(id => id.EndsWith(":home-travel", StringComparison.Ordinal));
|
|
sawDock |= tick.OperationResult.AppliedCommandIds.Any(id => id.EndsWith(":home-dock", StringComparison.Ordinal));
|
|
rejectedHomeCommands.AddRange(tick.OperationResult.RejectedCommandIds.Where(id => id.Contains(":home-", StringComparison.Ordinal)));
|
|
var currentHomeZone = run.Zones.Single(zone => zone.ZoneIndex == 2);
|
|
var currentWorker = currentHomeZone.Entities.FirstOrDefault(entity => entity.EntityId == "worker.return-home");
|
|
var currentHome = currentHomeZone.Entities.FirstOrDefault(entity => entity.EntityId == "station.home");
|
|
if (currentWorker != null && currentHome != null)
|
|
{
|
|
minimumHomeDistance = Math.Min(minimumHomeDistance, Math.Sqrt(
|
|
Math.Pow(currentWorker.PositionX - currentHome.PositionX, 2) +
|
|
Math.Pow(currentWorker.PositionZ - currentHome.PositionZ, 2)));
|
|
maximumHomeSpeed = Math.Max(maximumHomeSpeed, Math.Sqrt(
|
|
currentWorker.VelocityX * currentWorker.VelocityX +
|
|
currentWorker.VelocityY * currentWorker.VelocityY));
|
|
}
|
|
if (sawDock)
|
|
break;
|
|
}
|
|
|
|
var homeZone = run.Zones.Single(zone => zone.ZoneIndex == 2);
|
|
var arrivedWorker = homeZone.Entities.Single(entity => entity.EntityId == "worker.return-home");
|
|
var arrivedHome = homeZone.Entities.Single(entity => entity.EntityId == "station.home");
|
|
Require(sawApproach, "idle worker must approach its route and home through shared movement commands");
|
|
RequireEqual(2, travelCount, "idle worker must traverse the shortest route to its canonical home");
|
|
Require(sawDock,
|
|
$"idle worker must dock through the daemon docking command; worker={arrivedWorker.PositionX:0.###},{arrivedWorker.PositionZ:0.###} home={arrivedHome.PositionX:0.###},{arrivedHome.PositionZ:0.###} minDistance={minimumHomeDistance:0.###} maxSpeed={maximumHomeSpeed:0.###} rejected={string.Join(",", rejectedHomeCommands)}");
|
|
Require(arrivedHome.DockingBayAssignments.Contains(arrivedWorker.EntityIndex),
|
|
"home docking parentage must be the authoritative completion fact");
|
|
|
|
var settled = AetheriaRuntimeDaemonTickRunner.Tick(
|
|
Path.Combine(Path.GetTempPath(), "aetheria-agent-return-home-settled-smoke.cc"),
|
|
run,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
WorldPhysics = physics, Catalog = catalog, FrameId = 501,
|
|
FixedDeltaSeconds = 0.25, SimulationTimeSeconds = 125.25, BuildPublications = false
|
|
});
|
|
Require(!settled.OperationResult.AppliedCommandIds.Any(id => id.Contains(":home-", StringComparison.Ordinal)),
|
|
"docked worker must not emit a perpetual homecoming repair loop");
|
|
}
|
|
|
|
private static void ControlledShipDoesNotReceiveAutonomousHelmCommands()
|
|
{
|
|
var controlled = Entity(0, 100, "workers");
|
|
controlled.EntityId = "controlled-worker";
|
|
controlled.HomeEntityId = "controlled-home";
|
|
controlled.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Explore];
|
|
var home = Entity(1, 0, "workers");
|
|
home.EntityId = "controlled-home";
|
|
home.Kind = "station";
|
|
var task = AgentTask("controlled-task", 10);
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "controlled-agent-authority-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [controlled, home] }],
|
|
AgentTasks = [task]
|
|
};
|
|
|
|
var commands = AetheriaRuntimeAgentScheduler.AssignAndPlan(run, 1);
|
|
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Queued, task.Status,
|
|
"manual helm owner must not be conscripted into autonomous corporation work");
|
|
Require(commands.Count == 0,
|
|
"controlled ship must emit no task or return-home commands without delegated helm authority");
|
|
}
|
|
|
|
private static void AgentClaimsAndCompletesExploreTaskThroughCommands()
|
|
{
|
|
var agent = Entity(0, 0, "workers");
|
|
agent.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Explore];
|
|
var catalog = EquipThrusterBank([agent]);
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "agent-task-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [agent] }]
|
|
};
|
|
var issue = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.IssueAgentTask,
|
|
"commander-smoke",
|
|
"starbridge-smoke",
|
|
0,
|
|
"");
|
|
issue.CommandId = "issue-explore";
|
|
issue.AgentTask = new AetheriaRuntimeAgentTaskCommand
|
|
{
|
|
TaskId = "explore-east",
|
|
CorporationKey = "workers",
|
|
TaskType = AetheriaRuntimeAgentTaskTypes.Explore,
|
|
Priority = 50,
|
|
ZoneIndex = 0,
|
|
TargetPositionX = 50,
|
|
TargetPositionZ = 0,
|
|
CompletionRadius = 5
|
|
};
|
|
var completed = false;
|
|
var physics = NewPhysics();
|
|
for (var frame = 0; frame <= 60; frame++)
|
|
{
|
|
var tick = AetheriaRuntimeDaemonTickRunner.Tick(
|
|
Path.Combine(Path.GetTempPath(), "aetheria-agent-task-smoke.cc"),
|
|
run,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
WorldPhysics = physics,
|
|
FrameId = frame,
|
|
FixedDeltaSeconds = 0.1,
|
|
SimulationTimeSeconds = frame * 0.1,
|
|
ObservedCommands = frame == 0 ? [issue] : Array.Empty<AetheriaRuntimeDaemonCommandDocument>(),
|
|
Catalog = catalog,
|
|
BuildPublications = false
|
|
});
|
|
if (frame == 0)
|
|
{
|
|
Require(tick.OperationResult.AppliedCommandIds.Contains("issue-explore"),
|
|
"commander task command must enter the normal tick reducer");
|
|
Require(tick.OperationResult.AppliedCommandIds.Any(id => id.Contains(":explore-east:0:move", StringComparison.Ordinal)),
|
|
"agent movement must return through the same command receipts");
|
|
}
|
|
completed = string.Equals(run.AgentTasks.Single().Status, AetheriaRuntimeAgentTaskStatuses.Completed, StringComparison.Ordinal);
|
|
if (completed)
|
|
break;
|
|
}
|
|
|
|
Require(completed,
|
|
$"agent must complete an explore task without direct position mutation; position={agent.PositionX:0.###},{agent.PositionZ:0.###} velocity={agent.VelocityX:0.###},{agent.VelocityY:0.###}");
|
|
Require(string.IsNullOrWhiteSpace(agent.AssignedAgentTaskId), "completed task must release the agent");
|
|
Require(agent.PositionX >= 45, "agent must reach the task through repeated movement commands");
|
|
|
|
var frameDocument = new AetheriaRuntimeDaemonFrameDocument { FrameId = 21, Run = run };
|
|
var commander = AetheriaRuntimeDaemonGameSurfaceBuilder.BuildCommander(
|
|
frameDocument,
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("agent-task-smoke"));
|
|
var taskNode = Flatten(commander.Surface.Root)
|
|
.Single(component => string.Equals(component.Id, "aetheria.starbridge.commander.tasks.explore_east", StringComparison.Ordinal));
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Completed, taskNode.Props["status"],
|
|
"commander Eve surface must publish authoritative task status");
|
|
Require(commander.Commands.Any(command => string.Equals(command.Command, "aetheria.daemon.issue_agent_task", StringComparison.Ordinal)),
|
|
"commander Eve surface must advertise task issue command");
|
|
var roster = Flatten(commander.Surface.Root)
|
|
.Single(component => string.Equals(component.Kind, "agent.roster", StringComparison.Ordinal));
|
|
var worker = Flatten(roster)
|
|
.Single(component => string.Equals(component.Kind, "agent.item", StringComparison.Ordinal));
|
|
Require(!string.IsNullOrWhiteSpace(worker.Props["entityId"]),
|
|
"commander Eve roster must expose stable provider-owned worker identity");
|
|
Require(worker.Props["capabilities"].Split(',').Contains(AetheriaRuntimeAgentTaskTypes.Explore),
|
|
"commander Eve roster must expose provider-owned capability claims");
|
|
RequireEqual("idle", worker.Props["status"],
|
|
"commander Eve roster must publish generic status for runtime-independent lowering");
|
|
Require(worker.Props["badges"].Split(',').Contains(AetheriaRuntimeAgentTaskTypes.Explore),
|
|
"commander Eve roster must publish capability badges through the generic item contract");
|
|
RequireEqual("", worker.Props["assignedTaskId"],
|
|
"completed worker assignment must be visible as released on the Eve roster");
|
|
RequireEqual("false", worker.Props["controlled"],
|
|
"commander Eve roster must distinguish autonomous workers from manual helm authority");
|
|
}
|
|
|
|
private static void AgentTaskSurvivesCultCacheRestart()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "aetheria-agent-restart-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(root);
|
|
var statePath = Path.Combine(root, "world.cc");
|
|
try
|
|
{
|
|
var agent = Entity(0, 0, "workers");
|
|
agent.AgentTaskCapabilities = [AetheriaRuntimeAgentTaskTypes.Explore];
|
|
var catalog = EquipThrusterBank([agent]);
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "agent-task-restart-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [agent] }]
|
|
};
|
|
var issue = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.IssueAgentTask,
|
|
"commander-smoke",
|
|
run.RunId,
|
|
0,
|
|
"");
|
|
issue.CommandId = "issue-restart-explore";
|
|
issue.AgentTask = new AetheriaRuntimeAgentTaskCommand
|
|
{
|
|
TaskId = "restart-explore-east",
|
|
CorporationKey = "workers",
|
|
TaskType = AetheriaRuntimeAgentTaskTypes.Explore,
|
|
Priority = 50,
|
|
ZoneIndex = 0,
|
|
TargetPositionX = 80,
|
|
TargetPositionZ = 0,
|
|
CompletionRadius = 5
|
|
};
|
|
|
|
var physics = NewPhysics();
|
|
for (var frameId = 0; frameId <= 8; frameId++)
|
|
AetheriaRuntimeDaemonTickRunner.Tick(
|
|
statePath,
|
|
run,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
WorldPhysics = physics,
|
|
FrameId = frameId,
|
|
FixedDeltaSeconds = 0.1,
|
|
SimulationTimeSeconds = frameId * 0.1,
|
|
ObservedCommands = frameId == 0
|
|
? [issue]
|
|
: Array.Empty<AetheriaRuntimeDaemonCommandDocument>(),
|
|
Catalog = catalog,
|
|
BuildPublications = false
|
|
});
|
|
|
|
var taskBeforeRestart = run.AgentTasks.Single();
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Assigned, taskBeforeRestart.Status,
|
|
"restart fixture must persist an actively assigned task rather than a queued or completed shell");
|
|
RequireEqual(taskBeforeRestart.TaskId, agent.AssignedAgentTaskId,
|
|
"restart fixture must persist the scheduler's single agent assignment");
|
|
Require(agent.PositionX > 0 && agent.PositionX < 75,
|
|
"restart fixture must stop during commanded Ymir travel");
|
|
var positionBeforeRestart = agent.PositionX;
|
|
var frame = new AetheriaRuntimeDaemonFrameDocument
|
|
{
|
|
FrameId = 8,
|
|
FixedDeltaSeconds = 0.1,
|
|
SimulationTimeSeconds = 0.8,
|
|
Run = run
|
|
};
|
|
using (var node = AetheriaStateNode.OpenAsync(statePath).GetAwaiter().GetResult())
|
|
{
|
|
node.MutableDocument<AetheriaRuntimeDaemonFrameDocument>(
|
|
AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest)
|
|
.ReplaceAsync(frame).GetAwaiter().GetResult();
|
|
node.FlushAsync(soft: false).GetAwaiter().GetResult();
|
|
}
|
|
|
|
using (var node = AetheriaStateNode.OpenAsync(statePath).GetAwaiter().GetResult())
|
|
{
|
|
frame = node.MutableDocument<AetheriaRuntimeDaemonFrameDocument>(
|
|
AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest)
|
|
.ReadAsync().GetAwaiter().GetResult()
|
|
?? throw new InvalidOperationException("Durable agent-task frame is missing.");
|
|
}
|
|
run = frame.Run;
|
|
agent = run.Zones.Single().Entities.Single();
|
|
var restoredTask = run.AgentTasks.Single();
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Assigned, restoredTask.Status,
|
|
"CultCache restart must preserve the active task status");
|
|
RequireEqual(restoredTask.TaskId, agent.AssignedAgentTaskId,
|
|
"CultCache restart must preserve the task-to-agent ownership edge");
|
|
RequireNear(positionBeforeRestart, agent.PositionX, 0.000001,
|
|
"CultCache restart must preserve the last daemon/Ymir committed pose");
|
|
|
|
physics = NewPhysics();
|
|
var completed = false;
|
|
for (var frameId = 9; frameId <= 100; frameId++)
|
|
{
|
|
AetheriaRuntimeDaemonTickRunner.Tick(
|
|
statePath,
|
|
run,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
WorldPhysics = physics,
|
|
FrameId = frameId,
|
|
FixedDeltaSeconds = 0.1,
|
|
SimulationTimeSeconds = frameId * 0.1,
|
|
Catalog = catalog,
|
|
BuildPublications = false
|
|
});
|
|
completed = string.Equals(
|
|
run.AgentTasks.Single().Status,
|
|
AetheriaRuntimeAgentTaskStatuses.Completed,
|
|
StringComparison.Ordinal);
|
|
if (completed)
|
|
break;
|
|
}
|
|
|
|
Require(completed && agent.PositionX >= 75,
|
|
"restored task must reconstruct Ymir from committed world truth and finish through scheduler movement commands");
|
|
RequireEqual(1, run.AgentTasks.Count,
|
|
"restart must not duplicate the durable task when the scheduler resumes");
|
|
RequireEqual("", agent.AssignedAgentTaskId,
|
|
"completed restored task must release scheduler control exactly once");
|
|
var commander = AetheriaRuntimeDaemonGameSurfaceBuilder.BuildCommander(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 100, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("agent-task-restart-smoke"));
|
|
var taskNode = Flatten(commander.Surface.Root).Single(node =>
|
|
node.Id == "aetheria.starbridge.commander.tasks.restart_explore_east");
|
|
RequireEqual(AetheriaRuntimeAgentTaskStatuses.Completed, taskNode.Props["status"],
|
|
"generic Eve commander reconnect must observe the restored task's authoritative completion");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
private static void MultipleActorsUseTheSameMovementLever()
|
|
{
|
|
var player = Entity(0, 0, "player");
|
|
var agent = Entity(1, 200, "worker");
|
|
var catalog = EquipThrusterBank([player, agent]);
|
|
player.TargetEntityIndex = agent.EntityIndex;
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "shared-lever-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [player, agent] }]
|
|
};
|
|
var commands = new[]
|
|
{
|
|
MovementCommand("player-move", "zone.0.entity.0", 1, 0),
|
|
MovementCommand("agent-move", "zone.0.entity.1", 0, 1)
|
|
};
|
|
var operation = AetheriaRuntimeDaemonOperations.Execute(run, commands);
|
|
|
|
RequireEqual(2, operation.Intents.Movements.Count, "movement intent must retain one lever position per actor");
|
|
var physics = NewPhysics();
|
|
for (var step = 0; step < 2; step++)
|
|
AetheriaRuntimeDaemonSimulation.Step(
|
|
run,
|
|
operation.Intents,
|
|
0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
physics,
|
|
catalog,
|
|
step + 1);
|
|
Require(player.VelocityX > 0 && Math.Abs(player.VelocityY) < Math.Abs(player.VelocityX) * 0.1,
|
|
$"player command must drive its actor through the shared movement lever; velocity={player.VelocityX:0.###},{player.VelocityY:0.###}");
|
|
Require(agent.VelocityY > 0 && Math.Abs(agent.VelocityX) < Math.Abs(agent.VelocityY) * 0.1,
|
|
$"agent command must drive its actor through the shared movement lever; velocity={agent.VelocityX:0.###},{agent.VelocityY:0.###}");
|
|
|
|
var release = MovementCommand("player-release", "zone.0.entity.0", 0, 0);
|
|
release.ScalarValue = 0;
|
|
var releaseOperation = AetheriaRuntimeDaemonOperations.Execute(run, [release]);
|
|
AetheriaRuntimeDaemonSimulation.Step(
|
|
run,
|
|
releaseOperation.Intents,
|
|
0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
physics,
|
|
catalog,
|
|
3);
|
|
RequireEqual(agent.EntityIndex, player.TargetEntityIndex,
|
|
"releasing the movement lever must not steal targeting authority");
|
|
}
|
|
|
|
private static void DirectionalThrustersOwnOrdinaryFlightFeedback()
|
|
{
|
|
var ship = Entity(0, 0, "player");
|
|
var catalog = EquipThrusterBank([ship]);
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "ordinary-thruster-flight-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [ship] }]
|
|
};
|
|
var operation = AetheriaRuntimeDaemonOperations.Execute(
|
|
run, [MovementCommand("forward-thrust", "zone.0.entity.0", 0, 1)]);
|
|
var physics = NewPhysics();
|
|
AetheriaRuntimeDaemonSimulation.Step(
|
|
run, operation.Intents, 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
physics, catalog, 1);
|
|
|
|
var thrusterStates = ship.BehaviorStates
|
|
.Where(value => value.BehaviorKind == AetheriaRuntimeBehaviorKinds.Thruster)
|
|
.ToArray();
|
|
RequireEqual(8, thrusterStates.Length,
|
|
"ordinary hull fixture must fly through its directional thruster bank");
|
|
Require(thrusterStates.Count(value => value.ThrusterAxis > 0.99) == 2 &&
|
|
thrusterStates.Where(value => value.ThrusterAxis > 0.99)
|
|
.All(value => ship.Equipment[value.OwnerIndex].Rotation == "Half"),
|
|
"forward helm must fire only the reversed forward-thrust pair when no turn is requested");
|
|
Require(thrusterStates.Where(value => value.ThrusterAxis > 0.99)
|
|
.All(value => value.ThrusterThrust > 0 && Math.Abs(value.ThrusterTorque) > 0.5),
|
|
"thruster state must expose evaluated thrust and placement-derived torque");
|
|
Require(ship.VelocityY > 0 && Math.Abs(ship.VelocityX) < Math.Abs(ship.VelocityY) * 0.1,
|
|
"ordinary thrust must become Ymir-integrated forward velocity");
|
|
Require(ship.Visibility > 1.9,
|
|
"successful thruster execution must publish its maximum plume visibility");
|
|
Require(thrusterStates.Where(value => value.ThrusterAxis > 0.99)
|
|
.Any(value => AetheriaRuntimeThermalSimulation.EquipmentTemperature(
|
|
ship, catalog, value.OwnerIndex) > ship.EquipmentStates
|
|
.Single(state => state.EquipmentIndex == value.OwnerIndex).Temperature),
|
|
"successful thruster execution must deposit authored heat into daemon equipment cells");
|
|
Require(!ship.BehaviorStates.Any(value => value.BehaviorKind == AetheriaRuntimeBehaviorKinds.AetherDrive),
|
|
"ordinary flight must not manufacture an AetherDrive behavior");
|
|
}
|
|
|
|
private static void UnpoweredThrustersCannotMoveOrAdvertisePlume()
|
|
{
|
|
var ship = Entity(0, 0, "player");
|
|
var catalog = EquipThrusterBank([ship]);
|
|
var thruster = catalog.Items.Single(value => value.ItemKey == "smoke-directional-thruster");
|
|
thruster.BehaviorPayloads.Single().Fields = thruster.BehaviorPayloads.Single().Fields
|
|
.Select(field => field.Key == 4
|
|
? new AetheriaRuntimeBehaviorField(4, PerformanceStat(10))
|
|
: field)
|
|
.ToArray();
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "unpowered-thruster-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [ship] }]
|
|
};
|
|
var operation = AetheriaRuntimeDaemonOperations.Execute(
|
|
run, [MovementCommand("unpowered-forward", "zone.0.entity.0", 0, 1)]);
|
|
AetheriaRuntimeDaemonSimulation.Step(
|
|
run, operation.Intents, 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), catalog, 1);
|
|
|
|
RequireNear(0, ship.VelocityX, 0.000001,
|
|
"an unfunded thruster bank must not create lateral velocity");
|
|
RequireNear(0, ship.VelocityY, 0.000001,
|
|
"an unfunded thruster bank must not create forward velocity");
|
|
RequireNear(0, ship.Visibility, 0.000001,
|
|
"an unfunded thruster bank must not advertise a plume that did not execute");
|
|
}
|
|
|
|
private static void SpecializedBehaviorSuccessGatesTrailingWear()
|
|
{
|
|
var ship = Entity(0, 0, "player");
|
|
var catalog = EquipThrusterBank([ship]);
|
|
var thrusterItem = catalog.Items.Single(value => value.ItemKey == "smoke-directional-thruster");
|
|
thrusterItem.BehaviorPayloads =
|
|
[
|
|
ThrusterPayload(),
|
|
new AetheriaRuntimeBehaviorPayload(1, "Wear", 0,
|
|
[new AetheriaRuntimeBehaviorField(1, BoolValue(true))])
|
|
];
|
|
AetheriaRuntimeThermalSimulation.EnsureTopology(ship, catalog);
|
|
AetheriaRuntimeThermalSimulation.EnsureState(ship);
|
|
AetheriaRuntimeThermalSimulation.UpdateEquipmentStates(ship, catalog, 0.1);
|
|
foreach (var state in ship.EquipmentStates)
|
|
state.Wear = 1;
|
|
|
|
AetheriaRuntimeEnergySimulation.BeginTick(ship, catalog);
|
|
AetheriaRuntimeBehaviorSimulation.Step(0, [ship], [], catalog, 0.1);
|
|
AetheriaRuntimeFlightSimulation.Step([ship], [], catalog, 0.1, 1, 1, 0.001, 1);
|
|
AetheriaRuntimeBehaviorSimulation.CompleteDeferredChains(ship, catalog, 0.1);
|
|
Require(ship.Equipment.All(slot => Math.Abs(slot.Item.Durability - 1) < 0.000001),
|
|
"idle thrusters must return false and leave every trailing wear behavior unreachable");
|
|
|
|
ship.HelmForward = 1;
|
|
AetheriaRuntimeBehaviorSimulation.Step(0, [ship], [], catalog, 0.1);
|
|
AetheriaRuntimeFlightSimulation.Step([ship], [], catalog, 0.1, 1, 1, 0.001, 1);
|
|
AetheriaRuntimeBehaviorSimulation.CompleteDeferredChains(ship, catalog, 0.1);
|
|
RequireEqual(2, ship.Equipment.Count(slot => slot.Item.Durability < 1),
|
|
"only the two successful forward thrusters may resume into trailing wear");
|
|
Require(ship.Equipment.Where(slot => slot.Item.Durability < 1)
|
|
.All(slot => Math.Abs(slot.Item.Durability - 0.9) < 0.000001),
|
|
"each successful specialized behavior must resume its tail exactly once");
|
|
AetheriaRuntimeBehaviorSimulation.CompleteDeferredChains(ship, catalog, 0.1);
|
|
Require(ship.Equipment.Where(slot => slot.Item.Durability < 1)
|
|
.All(slot => Math.Abs(slot.Item.Durability - 0.9) < 0.000001),
|
|
"replaying deferred completion in the same tick must not apply trailing wear twice");
|
|
}
|
|
|
|
private static void RareAetherDriveSpoolsOnlyOnModifiedHullFixture()
|
|
{
|
|
var ship = Entity(0, 0, "player");
|
|
var catalog = EquipAetherDrive([ship]);
|
|
var hull = catalog.FindItem(ship.HullItemKey)!;
|
|
var drive = catalog.Items.Single(value => value.ItemKey == "smoke-aether-drive");
|
|
Require(drive.HardpointType == "AetherDrive" &&
|
|
hull.Hardpoints.Count(value => value.Type == drive.HardpointType) == 1,
|
|
"rare AetherDrive fixture must use an explicit modified-hull hardpoint instead of the ordinary hull path");
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "rare-aether-drive-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [ship] }]
|
|
};
|
|
var operation = AetheriaRuntimeDaemonOperations.Execute(
|
|
run, [MovementCommand("rare-drive-forward", "zone.0.entity.0", 0, 1)]);
|
|
var physics = NewPhysics();
|
|
for (var frame = 1; frame <= 2; frame++)
|
|
AetheriaRuntimeDaemonSimulation.Step(
|
|
run, operation.Intents, 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
physics, catalog, frame);
|
|
|
|
var state = ship.BehaviorStates.Single(value =>
|
|
value.BehaviorKind == AetheriaRuntimeBehaviorKinds.AetherDrive);
|
|
Require(state.AetherDriveRpmX > 0 && state.AetherDriveMaximumRpm > 0,
|
|
"rare installed drive must persist rotor spool state");
|
|
Require(ship.VelocityY > 0,
|
|
"rare installed drive must contribute force only after its rotor has spooled");
|
|
Require(!ship.BehaviorStates.Any(value => value.BehaviorKind == AetheriaRuntimeBehaviorKinds.Thruster),
|
|
"isolated AetherDrive proof must not conceal its force behind ordinary thrusters");
|
|
}
|
|
|
|
private static void LookDirectionRejectsInvalidVectorsWithoutMutatingTheShip()
|
|
{
|
|
var player = Entity(0, 0, "player");
|
|
player.DirectionX = 0.6;
|
|
player.DirectionY = 0.8;
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "invalid-look-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [player] }]
|
|
};
|
|
|
|
AetheriaRuntimeDaemonCommandDocument Look(string id, double x, double z)
|
|
{
|
|
var command = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.SetLookDirection,
|
|
"look-smoke",
|
|
"look-session",
|
|
0,
|
|
"zone.0.entity.0");
|
|
command.CommandId = id;
|
|
command.DirectionX = x;
|
|
command.PositionZ = z;
|
|
return command;
|
|
}
|
|
|
|
var operation = AetheriaRuntimeDaemonOperations.Execute(run,
|
|
[
|
|
Look("zero-look", 0, 0),
|
|
Look("nan-look", double.NaN, 1),
|
|
Look("infinite-look", 1, double.PositiveInfinity)
|
|
]);
|
|
|
|
Require(operation.RejectedCommandIds.SequenceEqual(new[] { "zero-look", "nan-look", "infinite-look" }),
|
|
"zero and non-finite look intents must be rejected at the daemon authority boundary");
|
|
RequireNear(0.6, player.DirectionX, 0.000001,
|
|
"rejected look intents must not mutate the authoritative ship direction X");
|
|
RequireNear(0.8, player.DirectionY, 0.000001,
|
|
"rejected look intents must not mutate the authoritative ship direction Z");
|
|
}
|
|
|
|
private static AetheriaRuntimeDaemonCommandDocument MovementCommand(
|
|
string commandId,
|
|
string actor,
|
|
double x,
|
|
double y)
|
|
{
|
|
var command = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.SetMoveVector,
|
|
"shared-control-smoke",
|
|
"shared-control-session",
|
|
0,
|
|
actor);
|
|
command.CommandId = commandId;
|
|
command.DirectionX = x;
|
|
command.DirectionY = y;
|
|
command.ScalarValue = 1;
|
|
return command;
|
|
}
|
|
|
|
private static void EnergyNetworkSettlesReactorAfterConsumers()
|
|
{
|
|
var entity = Entity(0, 0, "player");
|
|
entity.Equipment =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "ledger-capacitor", Quality = 1, Durability = 1, Enabled = true } },
|
|
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "ledger-reactor", Quality = 1, Durability = 1, Enabled = true } }
|
|
];
|
|
entity.BehaviorStates =
|
|
[
|
|
new AetheriaRuntimeBehaviorStateCommit
|
|
{
|
|
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind,
|
|
OwnerIndex = 0, BehaviorIndex = 0, BehaviorKind = "Capacitor",
|
|
CapacitorCharge = 2, CapacitorCapacity = 10, CapacitorEfficiency = 0.5
|
|
},
|
|
new AetheriaRuntimeBehaviorStateCommit
|
|
{
|
|
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind,
|
|
OwnerIndex = 1, BehaviorIndex = 0, BehaviorKind = "Reactor"
|
|
}
|
|
];
|
|
entity.StatGrids =
|
|
[
|
|
Grid("hull", 100), Grid("shield", 0), Grid("heat", 280),
|
|
Grid("temperature", 280), Grid("thermal-mass", 1), Grid("conductivity", 1)
|
|
];
|
|
var capacitorItem = CatalogItem("ledger-capacitor", CapacitorPayload(10, 0.5));
|
|
capacitorItem.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
var reactorPayload = new AetheriaRuntimeBehaviorPayload(0, "Reactor", 100,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, PerformanceStat(4)),
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(2)),
|
|
new AetheriaRuntimeBehaviorField(3, PerformanceStat(0.5)),
|
|
new AetheriaRuntimeBehaviorField(4, PerformanceStat(2))
|
|
]);
|
|
var reactorItem = CatalogItem("ledger-reactor", reactorPayload);
|
|
reactorItem.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([capacitorItem, reactorItem], [], []);
|
|
|
|
AetheriaRuntimeEnergySimulation.BeginTick(entity, catalog);
|
|
Require(AetheriaRuntimeEnergySimulation.TryConsume(entity, catalog, 5),
|
|
"online reactor must accept residual demand after capacitors drain");
|
|
var capacitor = entity.BehaviorStates.Single(value => value.BehaviorKind == "Capacitor");
|
|
var reactor = entity.BehaviorStates.Single(value => value.BehaviorKind == "Reactor");
|
|
RequireNear(0, capacitor.CapacitorCharge, 0.000001,
|
|
"consumers must drain capacitors before adding reactor draw");
|
|
RequireNear(3, reactor.ReactorDraw, 0.000001,
|
|
"residual demand must accumulate on the last-running reactor");
|
|
|
|
AetheriaRuntimeEnergySimulation.SettleReactors(entity, catalog, 1);
|
|
RequireNear(1, capacitor.CapacitorCharge, 0.000001,
|
|
"baseline reactor surplus must refill non-full capacitors after demand settles");
|
|
RequireNear(0, reactor.ReactorDraw, 0.000001,
|
|
"reactor settlement must consume and reset the per-tick demand ledger");
|
|
RequireNear(283.5, GridValue(entity, "temperature", 0), 0.000001,
|
|
"capacitor discharge/refill losses and reactor baseline heat must reach canonical item cells");
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "energy-ledger-smoke", CurrentZoneIndex = 0, CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [entity] }]
|
|
};
|
|
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 1, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
var worldEntity = Flatten(surface.Surface.Root).Single(node =>
|
|
node.Id == "aetheria.daemon.game.world.entity.0");
|
|
RequireEqual("1", worldEntity.Props["capacitorCharge"],
|
|
"Eve must expose the settled canonical capacitor charge");
|
|
RequireEqual("0", worldEntity.Props["reactorDraw"],
|
|
"Eve must expose the reset post-settlement reactor ledger");
|
|
}
|
|
|
|
private static void RadiatorPumpsHeatBeforeReactorSettlement()
|
|
{
|
|
var entity = Entity(0, 0, "player");
|
|
entity.HeatsinksEnabled = true;
|
|
entity.Equipment =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "radiator-capacitor", Quality = 1, Durability = 1, Enabled = true } },
|
|
new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "radiator", Quality = 1, Durability = 1, Enabled = true } }
|
|
];
|
|
entity.BehaviorStates =
|
|
[
|
|
new AetheriaRuntimeBehaviorStateCommit
|
|
{
|
|
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind,
|
|
OwnerIndex = 0, BehaviorIndex = 0, BehaviorKind = "Capacitor", CapacitorCharge = 10
|
|
},
|
|
new AetheriaRuntimeBehaviorStateCommit
|
|
{
|
|
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind,
|
|
OwnerIndex = 1, BehaviorIndex = 0, BehaviorKind = "Radiator", RadiatorTemperature = 280
|
|
}
|
|
];
|
|
entity.StatGrids =
|
|
[
|
|
Grid("hull", 100), Grid("shield", 0), Grid("heat", 400),
|
|
Grid("temperature", 400), Grid("thermal-mass", 1), Grid("conductivity", 1)
|
|
];
|
|
var capacitor = CatalogItem("radiator-capacitor", CapacitorPayload(10, 1));
|
|
capacitor.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
var radiatorPayload = new AetheriaRuntimeBehaviorPayload(0, "Radiator", 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, PerformanceStat(0)),
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(3, Number(300)),
|
|
new AetheriaRuntimeBehaviorField(4, PerformanceStat(0.5)),
|
|
new AetheriaRuntimeBehaviorField(5, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(6, PerformanceStat(10))
|
|
]);
|
|
var radiator = CatalogItem("radiator", radiatorPayload);
|
|
radiator.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([capacitor, radiator], [], []);
|
|
|
|
AetheriaRuntimeEnergySimulation.BeginTick(entity, catalog);
|
|
AetheriaRuntimeBehaviorSimulation.Step(0, [entity], [], catalog, 1);
|
|
AetheriaRuntimeEnergySimulation.StepRadiators(entity, catalog, 1);
|
|
|
|
RequireNear(9, entity.BehaviorStates.Single(value => value.BehaviorKind == "Capacitor").CapacitorCharge,
|
|
0.000001, "radiator pump must consume authored energy before reactor settlement");
|
|
RequireNear(300.5, GridValue(entity, "temperature", 0), 0.000001,
|
|
"radiator must remove pumped heat and return authored waste heat to its item cells");
|
|
RequireNear(290, entity.BehaviorStates.Single(value => value.BehaviorKind == "Radiator").RadiatorTemperature,
|
|
0.000001, "pumped heat must accumulate in radiator thermal mass before radiation");
|
|
}
|
|
|
|
private static void EquipmentThermalPerformanceOwnsShutdownAndWear()
|
|
{
|
|
var entity = Entity(0, 0, "player");
|
|
entity.ShutdownPerformance = 0.25;
|
|
var item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "thermal-wear-item", Quality = 0, Durability = 10, Enabled = true };
|
|
entity.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit { Item = item }];
|
|
entity.StatGrids =
|
|
[
|
|
Grid("hull", 100), Grid("shield", 0), Grid("heat", 300),
|
|
Grid("temperature", 300), Grid("thermal-mass", 1), Grid("conductivity", 1)
|
|
];
|
|
var wearPayload = new AetheriaRuntimeBehaviorPayload(0, "Wear", 0,
|
|
[new AetheriaRuntimeBehaviorField(1, new AetheriaRuntimeBehaviorValue(
|
|
"bool", "", 0, true, "", "", [], []))]);
|
|
var typed = CatalogItem("thermal-wear-item", wearPayload);
|
|
typed.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
typed.Durability = 10;
|
|
typed.ThermalResilience = 2;
|
|
typed.MinimumTemperature = 200;
|
|
typed.MaximumTemperature = 400;
|
|
typed.ThermalPerformanceCurveKeys =
|
|
[
|
|
new AetheriaRuntimeCurveKey(0, 1, 0, 0),
|
|
new AetheriaRuntimeCurveKey(1, 0, 0, 0)
|
|
];
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([typed], [], []);
|
|
|
|
AetheriaRuntimeThermalSimulation.UpdateEquipmentStates(entity, catalog, 1);
|
|
var state = entity.EquipmentStates.Single();
|
|
RequireNear(0.5, state.ThermalPerformance, 0.000001,
|
|
"item thermal performance must come from mean occupied-cell temperature and authored curve");
|
|
Require(state.ThermalOnline && state.Online,
|
|
"item above shutdown performance with positive durability must remain online");
|
|
AetheriaRuntimeBehaviorSimulation.Step(0, [entity], [], catalog, 1);
|
|
RequireNear(10 - state.Wear, item.Durability, 0.000001,
|
|
"generic Wear behavior must apply the computed thermal wear potential at its authored cadence");
|
|
|
|
var temperature = entity.StatGrids.Single(value => value.Name == "temperature");
|
|
temperature.Values = [380];
|
|
item.OverrideShutdown = false;
|
|
entity.OverrideShutdown = true;
|
|
AetheriaRuntimeThermalSimulation.UpdateEquipmentStates(entity, catalog, 1);
|
|
state = entity.EquipmentStates.Single();
|
|
Require(!state.ThermalOnline && !state.Online,
|
|
"entity override alone must not bypass item thermal shutdown");
|
|
item.OverrideShutdown = true;
|
|
AetheriaRuntimeThermalSimulation.UpdateEquipmentStates(entity, catalog, 1);
|
|
Require(entity.EquipmentStates.Single().ThermalOnline,
|
|
"thermal shutdown bypass must require both entity-wide and item-local override");
|
|
}
|
|
|
|
private static void ThermalTopologyComesFromHullAndEquipment()
|
|
{
|
|
var entity = Entity(0, 0, "player");
|
|
entity.HullItemKey = "thermal-hull";
|
|
entity.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = 0, Y = 0, Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "thermal-gear", Quality = 1, Durability = 1, Enabled = true }
|
|
}];
|
|
entity.CargoBays = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = 1, Y = 0, Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "thermal-cargo-bay", Quality = 1, Durability = 1, Enabled = true }
|
|
}];
|
|
var hull = HullCatalogItem("thermal-hull", 2, 1, 0);
|
|
hull.Mass = 10;
|
|
hull.SpecificHeat = 2;
|
|
hull.Conductivity = 4;
|
|
hull.InteriorShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
var gear = CatalogItem("thermal-gear");
|
|
gear.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
gear.Mass = 4;
|
|
gear.SpecificHeat = 5;
|
|
gear.Conductivity = 3;
|
|
var cargoBay = CatalogItem("thermal-cargo-bay");
|
|
cargoBay.Category = AetheriaRuntimeItemCategories.CargoBay;
|
|
cargoBay.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
cargoBay.Mass = 6;
|
|
cargoBay.SpecificHeat = 2;
|
|
cargoBay.Conductivity = 2;
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([hull, gear, cargoBay], [], []);
|
|
|
|
AetheriaRuntimeThermalSimulation.EnsureTopology(entity, catalog);
|
|
|
|
var mass = entity.StatGrids.Single(value => value.Name == "thermal-mass");
|
|
var conductivity = entity.StatGrids.Single(value => value.Name == "conductivity");
|
|
RequireNear(30, mass.Values[0], 0.000001,
|
|
"occupied cell thermal mass must include its proportional equipment mass");
|
|
RequireNear(22, mass.Values[1], 0.000001,
|
|
"typed cargo bays must contribute thermal mass at their authored hull cells");
|
|
RequireNear(3, conductivity.Values[0], 0.000001,
|
|
"occupied cell conductivity must come from installed equipment");
|
|
RequireNear(280, entity.StatGrids.Single(value => value.Name == "temperature").Values[1], 0.000001,
|
|
"authored hull topology must initialize each occupied cell at the fossil temperature");
|
|
}
|
|
|
|
private static void ThermalMedicalExposureUsesCockpitTemperature()
|
|
{
|
|
var entity = Entity(0, 0, "player");
|
|
entity.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "medical-cockpit", Quality = 1, Durability = 1, Enabled = true }
|
|
}];
|
|
entity.StatGrids =
|
|
[
|
|
Grid("hull", 100), Grid("temperature", 340),
|
|
Grid("thermal-mass", 1), Grid("conductivity", 1)
|
|
];
|
|
var cockpit = CatalogItem("medical-cockpit",
|
|
new AetheriaRuntimeBehaviorPayload(0, "Cockpit", 0, []));
|
|
cockpit.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([cockpit], [], []);
|
|
var settings = AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault;
|
|
|
|
var heat = AetheriaRuntimeThermalMedicalSimulation.Step(entity, catalog, 300, settings);
|
|
RequireNear(0.3, entity.Heatstroke, 0.000001,
|
|
"cockpit heat must accumulate nonlinear heatstroke at the fossil rate");
|
|
Require(heat.HeatstrokeRiskCrossed,
|
|
"heatstroke must emit one upward severe-risk crossing");
|
|
RequireNear(340, Stat(entity, "cockpit-temperature"), 0.000001,
|
|
"cockpit cell temperature must become an authoritative Eve-readable fact");
|
|
|
|
entity.StatGrids.Single(value => value.Name == "temperature").Values = [280];
|
|
AetheriaRuntimeThermalMedicalSimulation.Step(entity, catalog, 1, settings);
|
|
RequireNear(0.1, entity.Heatstroke, 0.000001,
|
|
"heatstroke must recover linearly below the authored threshold");
|
|
|
|
entity.StatGrids.Single(value => value.Name == "temperature").Values = [263];
|
|
var cold = AetheriaRuntimeThermalMedicalSimulation.Step(entity, catalog, 300, settings);
|
|
RequireNear(0.3, entity.Hypothermia, 0.000001,
|
|
"cockpit cold must accumulate nonlinear hypothermia at the fossil rate");
|
|
Require(cold.HypothermiaRiskCrossed,
|
|
"hypothermia must use its own exposure for the intended severe-risk crossing");
|
|
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "thermal-medical-surface", CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [entity] }]
|
|
};
|
|
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 1, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
var worldEntity = Flatten(surface.Surface.Root).Single(node =>
|
|
node.Id == "aetheria.daemon.game.world.entity.0");
|
|
RequireEqual("263", worldEntity.Props["cockpitTemperature"],
|
|
"Eve must publish canonical cockpit temperature rather than deriving it from effects");
|
|
RequireEqual("true", worldEntity.Props["hypothermiaRisk"],
|
|
"Eve must publish the current severe cold-risk state");
|
|
Require(worldEntity.Props.ContainsKey("heatstrokePostWeight") &&
|
|
worldEntity.Props.ContainsKey("severeHeatstrokeWeight"),
|
|
"Eve must publish the exact source weights used by the original heatstroke presentation");
|
|
RequireEqual("5", worldEntity.Props["heatstrokePhasingFrequency"],
|
|
"Eve must publish the provider-authored severe heatstroke pulse frequency");
|
|
RequireEqual("1", worldEntity.Props["deathTransitionSeconds"],
|
|
"Eve must publish the original thermal death crossfade duration");
|
|
}
|
|
|
|
private static void ThermalMedicalDeathUsesOrdinaryDestructionPath()
|
|
{
|
|
var entity = Entity(0, 0, "player");
|
|
entity.EntityId = "thermal-pilot";
|
|
entity.Equipment = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "lethal-cockpit", Quality = 1, Durability = 1, Enabled = true }
|
|
}];
|
|
entity.StatGrids =
|
|
[
|
|
Grid("hull", 100), Grid("temperature", 700),
|
|
Grid("thermal-mass", 1), Grid("conductivity", 1)
|
|
];
|
|
var cockpit = CatalogItem("lethal-cockpit",
|
|
new AetheriaRuntimeBehaviorPayload(0, "Cockpit", 0, []));
|
|
cockpit.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([cockpit], [], []);
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [entity] };
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "thermal-death", CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0", Zones = [zone]
|
|
};
|
|
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics(), catalog, frameId: 17);
|
|
|
|
RequireEqual("heatstroke", entity.CauseOfDeath,
|
|
"lethal cockpit heat must persist the typed cause of death");
|
|
Require(!entity.IsActive && !string.IsNullOrWhiteSpace(entity.DestructionId),
|
|
"thermal death must enter the ordinary exactly-once destruction path");
|
|
var death = run.GameEvents.Single(value => value.Kind == "entity.destroyed");
|
|
RequireEqual("heatstroke", death.Reason,
|
|
"Eve feedback must carry the thermal cause needed for the original death transition");
|
|
Require(run.GameEvents.Any(value => value.Kind == "pilot.heatstroke.risk"),
|
|
"a lethal step crossing the severe threshold must publish risk before death");
|
|
}
|
|
|
|
private static void ThermalCellsUseFossilConductionAndRadiation()
|
|
{
|
|
var entity = Entity(7, 0, "player");
|
|
entity.StatGrids =
|
|
[
|
|
Grid(AetheriaRuntimeThermalSimulation.TemperatureGrid, 2, 1, 300, 280),
|
|
Grid(AetheriaRuntimeThermalSimulation.ThermalMassGrid, 2, 1, 1, 1),
|
|
Grid(AetheriaRuntimeThermalSimulation.ConductivityGrid, 2, 1, 1, 1)
|
|
];
|
|
|
|
AetheriaRuntimeThermalSimulation.AddHeat(entity, 20);
|
|
RequireNear(310, GridValue(entity, AetheriaRuntimeThermalSimulation.TemperatureGrid, 0), 0.000001,
|
|
"heat energy must be divided across cells and thermal mass");
|
|
RequireNear(290, GridValue(entity, AetheriaRuntimeThermalSimulation.TemperatureGrid, 1), 0.000001,
|
|
"heat energy must be divided across cells and thermal mass");
|
|
|
|
AetheriaRuntimeThermalSimulation.Step(entity, 0.1);
|
|
var expectedHot = (310 / 0.01 + 290) / 101;
|
|
expectedHot -= Math.Pow(expectedHot, 3) * 0.00000001 * 0.1;
|
|
var expectedCool = (290 / 0.01 + 310) / 101;
|
|
expectedCool -= Math.Pow(expectedCool, 3) * 0.00000001 * 0.1;
|
|
RequireNear(expectedHot, GridValue(entity, AetheriaRuntimeThermalSimulation.TemperatureGrid, 0), 0.000001,
|
|
"hot cell must follow fossil conduction and radiation");
|
|
RequireNear(expectedCool, GridValue(entity, AetheriaRuntimeThermalSimulation.TemperatureGrid, 1), 0.000001,
|
|
"cool cell must follow fossil conduction and radiation");
|
|
RequireNear((expectedHot + expectedCool) / 2, Stat(entity, "heat"), 0.000001,
|
|
"legacy heat scalar must be derived from cell temperature");
|
|
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "thermal-projection-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.7",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [entity] }]
|
|
};
|
|
var document = AetheriaRuntimeGameDocuments.CurrentEntity(new AetheriaRuntimeDaemonFrameDocument { Run = run });
|
|
RequireNear(Stat(entity, "heat"), document.Hud.MeanTemperature, 0.000001,
|
|
"Eve current-entity state must publish mean temperature");
|
|
RequireNear(GridValue(entity, AetheriaRuntimeThermalSimulation.TemperatureGrid, 0), document.Hud.MaximumTemperature, 0.000001,
|
|
"Eve current-entity state must publish maximum temperature");
|
|
Require(document.Hud.ThermalVisibility > 0, "Eve current-entity state must publish thermal visibility");
|
|
}
|
|
|
|
private static void YmirMovesProjectileAndReportsStableContact()
|
|
{
|
|
var (run, zone, target) = Scenario();
|
|
using var physics = NewPhysics();
|
|
physics.RetainWorlds(run.RunId, [zone.ZoneIndex]);
|
|
physics.Step(run.RunId, 0, 0, zone, zone.Entities, 0.1);
|
|
var step = physics.StepPhysicalPayloads(run.RunId, 0, 0, zone, zone.Entities, 0.1);
|
|
|
|
RequireEqual("ymir.box3d.retained-session.v1", physics.ImplementationId, "zone physics must identify its retained Ymir implementation");
|
|
RequireEqual(0, step.PhysicalPayloads.Count, "contacted projectile must not survive");
|
|
RequireEqual(1, step.Hits.Count, "Ymir must report one projectile contact");
|
|
RequireEqual(target.EntityIndex, step.Hits[0].TargetEntityIndex, "contact must resolve the daemon entity");
|
|
RequireEqual("aetheria.physical-payload.smoke-projectile", step.Hits[0].PhysicalPayloadBodyId, "physical payload body id must be stable");
|
|
RequireEqual("aetheria.daemon.entity.smoke.entity.2", step.Hits[0].TargetBodyId, "entity body id must be stable");
|
|
}
|
|
|
|
private static void PayloadQueryExcludesItsSource()
|
|
{
|
|
var source = Entity(1, 0, "player");
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [source],
|
|
PhysicalPayloads =
|
|
[
|
|
new AetheriaRuntimePhysicalPayloadCommit
|
|
{
|
|
PayloadId = "source-exclusion",
|
|
SourceEntityIndex = source.EntityIndex,
|
|
PositionX = 0,
|
|
PositionZ = 0,
|
|
VelocityX = 10,
|
|
Radius = 1,
|
|
LifetimeSeconds = 5,
|
|
Active = true
|
|
}
|
|
]
|
|
};
|
|
using var physics = NewPhysics();
|
|
physics.RetainWorlds("source-exclusion-smoke", [zone.ZoneIndex]);
|
|
physics.Step("source-exclusion-smoke", 1, 0, zone, zone.Entities, 0.1);
|
|
var step = physics.StepPhysicalPayloads("source-exclusion-smoke", 1, 0, zone, zone.Entities, 0.1);
|
|
|
|
RequireEqual(0, step.Hits.Count, "a payload query must not hit its source body");
|
|
RequireEqual(1, step.PhysicalPayloads.Count, "source exclusion must leave the payload alive");
|
|
}
|
|
|
|
private static void PayloadBodiesDoNotCollideWithEachOther()
|
|
{
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [],
|
|
PhysicalPayloads =
|
|
[
|
|
new AetheriaRuntimePhysicalPayloadCommit
|
|
{
|
|
PayloadId = "payload-left",
|
|
SourceEntityIndex = -1,
|
|
PositionX = -5,
|
|
VelocityX = 100,
|
|
Radius = 1,
|
|
LifetimeSeconds = 5,
|
|
Active = true
|
|
},
|
|
new AetheriaRuntimePhysicalPayloadCommit
|
|
{
|
|
PayloadId = "payload-right",
|
|
SourceEntityIndex = -1,
|
|
PositionX = 5,
|
|
VelocityX = -100,
|
|
Radius = 1,
|
|
LifetimeSeconds = 5,
|
|
Active = true
|
|
}
|
|
]
|
|
};
|
|
using var physics = NewPhysics();
|
|
physics.RetainWorlds("payload-isolation-smoke", [zone.ZoneIndex]);
|
|
physics.Step("payload-isolation-smoke", 2, 0, zone, zone.Entities, 0.1);
|
|
var step = physics.StepPhysicalPayloads("payload-isolation-smoke", 2, 0, zone, zone.Entities, 0.1);
|
|
|
|
RequireEqual(0, step.Hits.Count, "payload bodies must not manufacture world-query hits against each other");
|
|
RequireEqual(2, step.PhysicalPayloads.Count, "payload bodies must survive crossing each other");
|
|
Require(step.PhysicalPayloads.Single(payload => payload.PayloadId == "payload-left").VelocityX > 0,
|
|
"payload-to-payload response must not reverse the left payload");
|
|
Require(step.PhysicalPayloads.Single(payload => payload.PayloadId == "payload-right").VelocityX < 0,
|
|
"payload-to-payload response must not reverse the right payload");
|
|
}
|
|
|
|
private static void OverlappingMineRemainsQueryableAfterArming()
|
|
{
|
|
var target = Entity(2, 0, "enemy");
|
|
var mine = new AetheriaRuntimePhysicalPayloadCommit
|
|
{
|
|
PayloadId = "arming-overlap",
|
|
PayloadKind = "mine",
|
|
SourceEntityIndex = 1,
|
|
PositionX = 0,
|
|
PositionZ = 0,
|
|
Radius = 1,
|
|
TriggerRadius = 25,
|
|
ActivationDelaySeconds = 1,
|
|
LifetimeSeconds = 10,
|
|
Stationary = false,
|
|
Active = true
|
|
};
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [target],
|
|
PhysicalPayloads = [mine]
|
|
};
|
|
using var physics = NewPhysics();
|
|
physics.RetainWorlds("mine-arming-overlap-smoke", [zone.ZoneIndex]);
|
|
physics.Step("mine-arming-overlap-smoke", 1, 0, zone, zone.Entities, 0.5);
|
|
var unarmed = physics.StepPhysicalPayloads("mine-arming-overlap-smoke", 1, 0, zone, zone.Entities, 0.5);
|
|
RequireEqual(1, unarmed.Hits.Count, "an unarmed overlap may be observed without granting Ymir trigger authority");
|
|
|
|
mine.AgeSeconds = mine.ActivationDelaySeconds;
|
|
mine.Radius = mine.TriggerRadius;
|
|
zone.PhysicalPayloads = unarmed.PhysicalPayloads;
|
|
physics.Step("mine-arming-overlap-smoke", 2, 0, zone, zone.Entities, 0.5);
|
|
var armed = physics.StepPhysicalPayloads("mine-arming-overlap-smoke", 2, 0, zone, zone.Entities, 0.5);
|
|
RequireEqual(1, armed.Hits.Count,
|
|
"a retained mine already overlapping at activation must still yield an eligible Box3D overlap fact");
|
|
RequireEqual(target.EntityIndex, armed.Hits[0].TargetEntityIndex,
|
|
"the armed overlap fact must preserve the target identity");
|
|
}
|
|
|
|
private static void DaemonSimulationTreatsYmirHitAsPresentationOnly()
|
|
{
|
|
var (run, _, target) = Scenario();
|
|
|
|
AetheriaRuntimeDaemonSimulation.Step(
|
|
run,
|
|
new AetheriaRuntimeDaemonIntentState(),
|
|
0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
NewPhysics());
|
|
|
|
RequireEqual(100.0, Stat(target, "hull"), "presentation projectile contact must not write combat damage");
|
|
RequireEqual(0, run.Zones[0].PhysicalPayloads.Count, "spent projectile must leave daemon state");
|
|
RequireEqual(1, run.GameEvents.Count(value => value.Kind == "physical-payload.contact" && value.SubjectKey == "smoke-projectile"),
|
|
"Ymir contact must emit one projectile-identity impact event");
|
|
RequireEqual(0, run.GameEvents.Count(value => value.Kind == "entity.damaged" && value.TargetEntityIndex == target.EntityIndex),
|
|
"presentation projectile contact must not manufacture a damage event");
|
|
var feedback = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 0, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
Require(Flatten(feedback.Surface.Root).Any(node => node.Kind == "feedback.event" && node.Props["eventKind"] == "physical-payload.contact" && node.Props["subjectKey"] == "smoke-projectile"),
|
|
"Eve feedback must project authoritative projectile impact identity");
|
|
}
|
|
|
|
private static void ProjectileContactCannotKill()
|
|
{
|
|
var (run, _, target) = Scenario();
|
|
target.StatGrids.Single(grid => grid.Name == "hull").Values = [5];
|
|
AetheriaRuntimeDaemonSimulation.Step(run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault, NewPhysics(), frameId: 9);
|
|
Require(target.IsActive && Math.Abs(Stat(target, "hull") - 5) < 0.000001,
|
|
"even nominally lethal presentation contact must leave canonical target state untouched");
|
|
RequireEqual(0, run.GameEvents.Count(value => value.Kind == "entity.destroyed" && value.TargetEntityIndex == target.EntityIndex),
|
|
"presentation contact must not manufacture a destruction event");
|
|
}
|
|
|
|
private static void MissingWorldPhysicsOwnerCannotAdvanceShips()
|
|
{
|
|
var entity = Entity(0, 0, "player");
|
|
entity.VelocityX = 10;
|
|
var run = new AetheriaRuntimeRunCheckpointCommit { CurrentZoneIndex = 0, CurrentEntityKey = "zone.0.entity.0", Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [entity] }] };
|
|
try
|
|
{
|
|
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-missing-world-physics.cc"), run,
|
|
new AetheriaRuntimeDaemonTickOptions { FrameId = 1, FixedDeltaSeconds = 0.1, BuildPublications = false });
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
RequireNear(0, entity.PositionX, 0.000001, "ship must not advance without Ymir world authority");
|
|
return;
|
|
}
|
|
throw new InvalidOperationException("daemon advanced a ship without an authoritative world physics owner");
|
|
}
|
|
|
|
private static void TractorRampsAndPullsThroughYmirWithoutTeleportingCargo()
|
|
{
|
|
var ship = Entity(0, 0, "player");
|
|
ship.DirectionX = 1; ship.DirectionY = 0;
|
|
var pickup = new AetheriaRuntimeDroppedPickupCommit { PickupIndex = 3, PositionX = 60, Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "salvage", Quantity = 1 }, LifetimeSeconds = 30 };
|
|
var run = new AetheriaRuntimeRunCheckpointCommit { CurrentZoneIndex = 0, CurrentEntityKey = "zone.0.entity.0", Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [ship], DroppedPickups = [pickup] }] };
|
|
var command = AetheriaRuntimeDaemonCommandDocument.Create(AetheriaRuntimeDaemonCommandKinds.SetTractorPower, "pilot", "tractor-smoke", 0, "zone.0.entity.0");
|
|
command.CommandId = "tractor-on"; command.ScalarValue = 1;
|
|
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-tractor-smoke.cc"), run,
|
|
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = NewPhysics(), FrameId = 1, FixedDeltaSeconds = 0.25, SimulationTimeSeconds = 0.25, ObservedCommands = [command], BuildPublications = false });
|
|
RequireNear(0.5, ship.TractorPower, 0.000001, "tractor power must use the fossil two-per-second ramp");
|
|
Require(pickup.VelocityX < 0 && pickup.PositionX < 60, "Ymir must pull a pickup inside the forward tractor volume toward the ship");
|
|
RequireEqual(1, pickup.Item.Quantity, "tractor force must not consume the pickup item");
|
|
RequireEqual(0, CargoQuantity(ship, "salvage"), "scooping must remain a separate capacity-checked transaction");
|
|
Require(pickup.AgeSeconds > 0 && pickup.AgeSeconds < pickup.LifetimeSeconds,
|
|
"daemon-owned pickup lifetime must advance independently of its SoA presentation");
|
|
var release = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.SetTractorPower,
|
|
"pilot",
|
|
"tractor-smoke",
|
|
1,
|
|
"zone.0.entity.0");
|
|
release.CommandId = "tractor-off";
|
|
release.ScalarValue = 0;
|
|
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-tractor-release-smoke.cc"), run,
|
|
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = NewPhysics(), FrameId = 2, FixedDeltaSeconds = 0.25, SimulationTimeSeconds = 0.5, ObservedCommands = [release], BuildPublications = false });
|
|
RequireNear(0, ship.TractorTargetPower, 0.000001, "released tractor input must set daemon target power to zero");
|
|
RequireNear(0, ship.TractorPower, 0.000001, "daemon must ramp released tractor power back to zero at the fossil rate");
|
|
var inputCapability = AetheriaRuntimeInputCapabilityDocument
|
|
.FromFrame(new AetheriaRuntimeDaemonFrameDocument { FrameId = 2, Run = run })
|
|
.ToEveDocument();
|
|
var scoopAction = inputCapability.Actions.Single(action => action.ActionId == "pilot.scoop");
|
|
Require(scoopAction.InputValue?.Model == "button-hold.v1" &&
|
|
scoopAction.InputValue.PayloadKey == "scalarValue" &&
|
|
scoopAction.Payload["scalarValue"] == "0",
|
|
"tractor input must advertise press/release scalar ownership instead of a one-shot on command");
|
|
ship.TractorPower = 0.5;
|
|
ship.TractorTargetPower = 1;
|
|
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 1, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
var beam = Flatten(surface.Surface.Root).Single(node => node.Kind == "beam.presentation");
|
|
Require(beam.Props["sourceEntityId"] == run.EntityRecordKey(0, ship.EntityIndex) &&
|
|
beam.Props["assetRole"] == "effect.beam.tractor" &&
|
|
beam.Props["directionMode"] == "source-forward.v1" &&
|
|
beam.Props["activationActionId"] == "pilot.scoop" &&
|
|
beam.Props["power"] == "0.5" && beam.Props["radius"] == "25" &&
|
|
beam.Props["maximumDistance"] == "75",
|
|
"Eve beam presentation must project the same daemon tractor power and fossil volume without owning force or contact");
|
|
|
|
double PulledVelocity(double power, int frameId)
|
|
{
|
|
var actor = Entity(0, 0, "player");
|
|
actor.DirectionX = 1;
|
|
actor.DirectionY = 0;
|
|
actor.TractorPower = power;
|
|
actor.TractorTargetPower = power;
|
|
var cargo = new AetheriaRuntimeDroppedPickupCommit
|
|
{
|
|
PickupIndex = 30 + frameId,
|
|
PositionX = 60,
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = "salvage", Quantity = 1 },
|
|
LifetimeSeconds = 30
|
|
};
|
|
var scenario = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = $"tractor-power-{frameId}",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [actor], DroppedPickups = [cargo] }]
|
|
};
|
|
AetheriaRuntimeDaemonTickRunner.Tick(
|
|
Path.Combine(Path.GetTempPath(), $"aetheria-tractor-power-{frameId}.cc"),
|
|
scenario,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
WorldPhysics = NewPhysics(), FrameId = frameId, FixedDeltaSeconds = 0.1,
|
|
SimulationTimeSeconds = frameId * 0.1, BuildPublications = false
|
|
});
|
|
return cargo.VelocityX;
|
|
}
|
|
|
|
RequireNear(PulledVelocity(0.02, 20), PulledVelocity(1, 21), 0.000001,
|
|
"tractor power above the fossil threshold must gate full traction rather than scale force");
|
|
var pickupAsset = AetheriaRuntimeAssets.ProjectManifest(null).Assets.Single(asset => asset.Ref.AssetKey == "prefab.entity.pickup");
|
|
Require(pickupAsset.Ref.Metadata.TryGetValue("unityAssetPath", out var pickupPath) && pickupPath == "Assets/Prefabs/RPG/Pickups/Tetrahedron.prefab",
|
|
"provider asset manifest must advertise the pickup visual used by Eve");
|
|
var assets = AetheriaRuntimeAssets.ProjectManifest(null).Assets;
|
|
var playerAsset = assets.Single(asset => asset.Ref.AssetKey == "prefab.entity.player");
|
|
RequireEqual("player", playerAsset.Ref.Metadata["presentationRole"],
|
|
"entity prefab fallback must use a specific presentation role instead of the generic world tag");
|
|
var boltEffect = assets.Single(asset => asset.Ref.AssetKey == "prefab.effect.shot.bolt");
|
|
RequireEqual("effect.shot.bolt", boltEffect.Ref.Metadata["presentationRole"],
|
|
"provider manifest must bind semantic shot feedback to its graduated EveUnity effect");
|
|
var tractorEffect = assets.Single(asset => asset.Ref.AssetKey == "prefab.effect.beam.tractor");
|
|
RequireEqual("effect.beam.tractor", tractorEffect.Ref.Metadata["presentationRole"],
|
|
"provider manifest must bind the stripped fossil tractor prefab to the generic beam role");
|
|
var thermalProfiles = assets.Where(asset =>
|
|
string.Equals(asset.Ref.Kind, AetheriaRuntimeAssetKinds.VolumeProfile, StringComparison.Ordinal) &&
|
|
asset.Ref.Metadata.TryGetValue("presentationRole", out var role) &&
|
|
(role.StartsWith("post.thermal.", StringComparison.Ordinal) ||
|
|
string.Equals(role, "post.death", StringComparison.Ordinal))).ToArray();
|
|
RequireEqual(5, thermalProfiles.Length,
|
|
"provider manifest must advertise every original thermal and death volume profile");
|
|
Require(thermalProfiles.All(asset =>
|
|
asset.Ref.Metadata.ContainsKey("unityAssetPath") &&
|
|
asset.Ref.Metadata.ContainsKey("presentationRole")),
|
|
"thermal profiles must cross the CDN boundary with exact Unity paths and semantic roles");
|
|
}
|
|
|
|
private static void PickupIsCapacityCheckedExactlyOnceAndExpires()
|
|
{
|
|
var hull = CatalogItem("pickup-hull"); hull.HullCapacity = PerformanceStat(1);
|
|
var salvage = CatalogItem("salvage"); salvage.Volume = 1;
|
|
var cargoBay = CatalogItem("pickup-cargo-bay"); cargoBay.InteriorOccupiedCells = 1;
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([hull, salvage, cargoBay], [], []);
|
|
var ship = Entity(0, 0, "player"); ship.HullItemKey = hull.ItemKey; ship.CargoContents = [Cargo()];
|
|
ship.CargoBays = [new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = cargoBay.ItemKey, Quantity = 1 } }];
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [ship], DroppedPickups = [new AetheriaRuntimeDroppedPickupCommit { PickupIndex = 7, PositionX = 10, Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = salvage.ItemKey, Quantity = 1 }, LifetimeSeconds = 30 }] };
|
|
var run = new AetheriaRuntimeRunCheckpointCommit { CurrentZoneIndex = 0, CurrentEntityKey = "zone.0.entity.0", Zones = [zone] };
|
|
var forbiddenCommand = AetheriaRuntimeDaemonCommandDocument.Create(AetheriaRuntimeDaemonCommandKinds.PickUpLoot, "pilot", "pickup-smoke", 0, "zone.0.entity.0");
|
|
forbiddenCommand.CommandId = "pickup-command-forbidden";
|
|
forbiddenCommand.TargetEntityKey = "zone.0.entity.0";
|
|
var forbidden = AetheriaRuntimeDaemonOperations.Execute(run, [forbiddenCommand], new AetheriaRuntimeDaemonOperationContext { Catalog = catalog });
|
|
Require(forbidden.RejectedCommandIds.Contains(forbiddenCommand.CommandId),
|
|
"client pickup commands must not own cargo collection");
|
|
|
|
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-pickup-no-contact.cc"), run,
|
|
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = new ScriptedWorldPhysics(), FrameId = 1, FixedDeltaSeconds = 0.1, SimulationTimeSeconds = 0.1, Catalog = catalog, BuildPublications = false });
|
|
RequireEqual(1, zone.DroppedPickups.Count, "nearby pickup without a Ymir contact fact must remain in the world");
|
|
RequireEqual(0, CargoQuantity(ship, salvage.ItemKey), "proximity must not mutate cargo");
|
|
|
|
var forgedContact = new AetheriaRuntimeWorldBeginContact
|
|
{
|
|
FactId = "ymir-fact-forged-identity",
|
|
EntityAIndex = 0,
|
|
EntityAId = "another-entity",
|
|
PickupIndex = 7,
|
|
NormalX = 1
|
|
};
|
|
var rejectedForgedIdentity = false;
|
|
try
|
|
{
|
|
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-pickup-forged-identity.cc"), run,
|
|
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = new ScriptedWorldPhysics(forgedContact), FrameId = 2, FixedDeltaSeconds = 0.1, SimulationTimeSeconds = 0.2, Catalog = catalog, BuildPublications = false });
|
|
}
|
|
catch (InvalidOperationException)
|
|
{
|
|
rejectedForgedIdentity = true;
|
|
}
|
|
Require(rejectedForgedIdentity, "a Ymir contact whose stable identity disagrees with the live entity must fail closed");
|
|
RequireEqual(1, zone.DroppedPickups.Count, "forged contact identity must not remove the pickup");
|
|
RequireEqual(0, CargoQuantity(ship, salvage.ItemKey), "forged contact identity must not mutate cargo");
|
|
RequireEqual(0, run.PickupContactReceipts.Count, "forged contact identity must not mint a consumption receipt");
|
|
|
|
var contact = new AetheriaRuntimeWorldBeginContact
|
|
{
|
|
FactId = "ymir-fact-pickup-7",
|
|
EntityAIndex = 0,
|
|
EntityAId = ship.EntityId,
|
|
PickupIndex = 7,
|
|
NormalX = 1
|
|
};
|
|
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-pickup-contact-dedup.cc"), run,
|
|
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = new ScriptedWorldPhysics(contact, contact), FrameId = 3, FixedDeltaSeconds = 0.1, SimulationTimeSeconds = 0.3, Catalog = catalog, BuildPublications = false });
|
|
RequireEqual(0, zone.DroppedPickups.Count, "one Ymir contact must consume the pickup");
|
|
RequireEqual(1, CargoQuantity(ship, salvage.ItemKey), "duplicate contact facts must commit cargo exactly once");
|
|
RequireEqual(1, run.GameEvents.Count(value => value.Kind == "pickup.collected" && value.PickupIndex == 7),
|
|
"duplicate contact facts must emit one collection event");
|
|
var collectionEvent = run.GameEvents.Single(value => value.Kind == "pickup.collected" && value.PickupIndex == 7);
|
|
RequireNear(1, collectionEvent.ScalarValue, 0.000001,
|
|
"collection feedback must publish the committed cargo delta");
|
|
RequireNear(0, collectionEvent.AuxiliaryValue, 0.000001,
|
|
"collection feedback must publish cargo quantity before the transaction");
|
|
RequireEqual(1, run.PickupContactReceipts.Count(value => value.FactId == contact.FactId),
|
|
"duplicate delivery of one Ymir fact must persist one Aetheria consumption receipt");
|
|
var collectionFeedback = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 3, Run = run },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
Require(Flatten(collectionFeedback.Surface.Root).Any(node => node.Kind == "feedback.event" &&
|
|
node.Props["eventKind"] == "pickup.collected" && node.Props["pickupIndex"] == "7" &&
|
|
node.Props["itemKey"] == salvage.ItemKey && node.Props["scalarValue"] == "1" &&
|
|
node.Props["cargoQuantityBefore"] == "0" && node.Props["cargoQuantityAfter"] == "1"),
|
|
"Eve feedback must publish the exact cargo delta committed by one Ymir contact fact");
|
|
|
|
zone.DroppedPickups = [new AetheriaRuntimeDroppedPickupCommit { PickupIndex = 8, PositionX = 10, Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = salvage.ItemKey, Quantity = 1 }, LifetimeSeconds = 30 }];
|
|
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-pickup-expiry-smoke.cc"), run,
|
|
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = new ScriptedWorldPhysics(), FrameId = 4, FixedDeltaSeconds = 30, SimulationTimeSeconds = 30, BuildPublications = false });
|
|
RequireEqual(0, zone.DroppedPickups.Count, "pickup must expire after the fossil thirty-second lifetime");
|
|
Require(run.GameEvents.Any(value => value.Kind == "pickup.expired" && value.PickupIndex == 8),
|
|
"daemon lifetime owner must emit authoritative pickup expiry event");
|
|
}
|
|
|
|
private static void TradePurchaseDerivesAcceptanceFromDaemonState()
|
|
{
|
|
var ore = CatalogItem("trade-ore");
|
|
ore.Price = 40;
|
|
ore.Volume = 1;
|
|
ore.Stackable = true;
|
|
ore.MaxStack = 4;
|
|
ore.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
var cargoBay = CatalogItem("trade-cargo-bay");
|
|
cargoBay.InteriorShapeWidth = 3;
|
|
cargoBay.InteriorShapeHeight = 1;
|
|
cargoBay.InteriorOccupiedCells = 3;
|
|
cargoBay.InteriorShapeCells =
|
|
[
|
|
new AetheriaRuntimeShapeCell(0, 0),
|
|
new AetheriaRuntimeShapeCell(1, 0),
|
|
new AetheriaRuntimeShapeCell(2, 0)
|
|
];
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([ore, cargoBay], [], []);
|
|
var station = Entity(0, 0, "station");
|
|
station.Kind = "station";
|
|
station.CargoContents = [Cargo((ore.ItemKey, 4, 2, 3), (ore.ItemKey, 4, 3, 3))];
|
|
station.DockingBayAssignments = [1];
|
|
station.ChildEntityIndices = [1];
|
|
var ship = Entity(1, 0, "player");
|
|
ship.CargoBays = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = cargoBay.ItemKey, Quantity = 1 }
|
|
}];
|
|
ship.CargoContents = [Cargo((ore.ItemKey, 3, 0, 0))];
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [station, ship] };
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "trade-authority-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.1",
|
|
Credits = 5000,
|
|
Zones = [zone]
|
|
};
|
|
var command = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.TradePurchase,
|
|
"pilot",
|
|
"trade-authority-smoke",
|
|
1,
|
|
run.CurrentEntityKey);
|
|
command.TradePurchase.ItemKey = ore.ItemKey;
|
|
command.TradePurchase.Quantity = 4;
|
|
command.TradePurchase.StationCargoIndex = 0;
|
|
command.TradePurchase.TargetCargoIndex = 0;
|
|
command.TradePurchase.SourceX = 2;
|
|
command.TradePurchase.SourceY = 3;
|
|
command.TradePurchase.PurchaseKind = "docked_ship";
|
|
command.TradePurchase.UnitPrice = -999;
|
|
command.TradePurchase.TotalPrice = -999;
|
|
command.TradePurchase.StationEntityKey = "forged-station";
|
|
command.TradePurchase.TargetEntityKey = "forged-target";
|
|
command.TradePurchase.CreatesDockedShip = true;
|
|
|
|
Require(AetheriaRuntimeRunCheckpointCommit.TryParseEntityKey(run.CurrentEntityKey, out _, out _),
|
|
"trade smoke current entity key must be daemon-resolvable");
|
|
RequireEqual(0, AetheriaRuntimeCargoCapacityQueries.UnitsThatFit(ship, catalog, ore.ItemKey, 0),
|
|
"the old volume-only capacity proxy must expose why it cannot own spatial stack placement");
|
|
var daemonUnitPrice = AetheriaRuntimeDaemonTradeItemQueries.TradeItemValue(
|
|
ore,
|
|
station.CargoContents[0].Items[0].Item,
|
|
catalog.TradeValueSettings).Price;
|
|
Require(daemonUnitPrice > 0, "trade smoke daemon catalog price must be positive");
|
|
|
|
var result = AetheriaRuntimeDaemonOperations.Execute(
|
|
run,
|
|
[command],
|
|
new AetheriaRuntimeDaemonOperationContext { Catalog = catalog });
|
|
|
|
Require(result.AppliedCommandIds.Contains(command.CommandId),
|
|
"daemon must accept a valid commodity purchase regardless of forged compatibility opinions");
|
|
RequireEqual(5000 - daemonUnitPrice * 4, run.Credits,
|
|
"daemon catalog price must own trade credit mutation");
|
|
RequireEqual(7, CargoQuantity(ship, ore.ItemKey), "daemon cargo capacity and placement must own delivery");
|
|
Require(ship.CargoContents.Single().Items
|
|
.Where(slot => slot.Item.ItemKey == ore.ItemKey)
|
|
.Select(slot => (slot.X, slot.Y, slot.Item.Quantity))
|
|
.SequenceEqual([(0, 0, 4), (1, 0, 3)]),
|
|
"a purchase must fill an existing stack before placing the remainder in the first authored free cell");
|
|
RequireEqual(2, zone.Entities.Count, "forged ship-creation opinion must not materialize an entity");
|
|
|
|
AetheriaRuntimeDaemonCommandDocument Contender(string clientId, string commandId)
|
|
{
|
|
var contender = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.TradePurchase,
|
|
clientId,
|
|
run.RunId,
|
|
2,
|
|
run.CurrentEntityKey);
|
|
contender.CommandId = commandId;
|
|
contender.TradePurchase.ItemKey = ore.ItemKey;
|
|
contender.TradePurchase.Quantity = 4;
|
|
contender.TradePurchase.StationCargoIndex = 0;
|
|
contender.TradePurchase.TargetCargoIndex = 0;
|
|
contender.TradePurchase.SourceX = 3;
|
|
contender.TradePurchase.SourceY = 3;
|
|
return contender;
|
|
}
|
|
|
|
var firstClient = Contender("pilot-a", "trade-contested-a");
|
|
var secondClient = Contender("pilot-b", "trade-contested-b");
|
|
var creditsBeforeContest = run.Credits;
|
|
var contested = AetheriaRuntimeDaemonOperations.Execute(
|
|
run,
|
|
[firstClient, secondClient],
|
|
new AetheriaRuntimeDaemonOperationContext { Catalog = catalog });
|
|
|
|
Require(contested.AppliedCommandIds.SequenceEqual([firstClient.CommandId]),
|
|
"the daemon batch reducer must serialize contested stock to exactly one winner");
|
|
Require(contested.RejectedCommandIds.SequenceEqual([secondClient.CommandId]),
|
|
"the command observing exhausted stock must be rejected exactly once");
|
|
RequireEqual(AetheriaRuntimeDaemonRejectionReasons.TradeStockUnavailable,
|
|
contested.RejectedCommandReasons[secondClient.CommandId],
|
|
"contested stock must retain the exact daemon-owned rejection reason");
|
|
RequireEqual(creditsBeforeContest - daemonUnitPrice * 4, run.Credits,
|
|
"contested stock must debit the shared run wallet exactly once");
|
|
RequireEqual(11, CargoQuantity(ship, ore.ItemKey),
|
|
"contested stock must deliver exactly one winning stack quantity");
|
|
RequireEqual(0, CargoQuantity(station, ore.ItemKey),
|
|
"the winning purchase must exhaust authoritative station stock");
|
|
|
|
var frame = AetheriaRuntimeDaemonFrameDocument.Create(
|
|
run, "daemon", "trade-contest", 2, 0.04, 0.02);
|
|
frame.AppliedCommandIds = contested.AppliedCommandIds;
|
|
frame.RejectedCommandIds = contested.RejectedCommandIds;
|
|
frame.RejectedCommandReasons = contested.RejectedCommandReasons;
|
|
var winnerFact = AetheriaRuntimeCommittedCommandFactDocument.FromAppliedCommand(
|
|
frame, firstClient, "aetheria.local");
|
|
var loserFact = AetheriaRuntimeCommittedCommandFactDocument.FromRejectedCommand(
|
|
frame, secondClient, "aetheria.local");
|
|
var winnerReceipt = AetheriaRuntimeDaemonReceiptProjector.Project(
|
|
winnerFact, AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId);
|
|
var loserReceipt = AetheriaRuntimeDaemonReceiptProjector.Project(
|
|
loserFact, AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId);
|
|
RequireEqual("reconciled", winnerReceipt.State,
|
|
"the winning client must receive an authoritative generic Eve receipt");
|
|
Require(loserReceipt.State == "denied" &&
|
|
loserReceipt.Message.Contains(
|
|
AetheriaRuntimeDaemonRejectionReasons.TradeStockUnavailable,
|
|
StringComparison.Ordinal),
|
|
"the losing client must receive the exact contested-stock reason through generic Eve");
|
|
|
|
var root = Path.Combine(Path.GetTempPath(), "aetheria-trade-contest-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(root);
|
|
var statePath = Path.Combine(root, "world.cc");
|
|
try
|
|
{
|
|
using (var node = AetheriaStateNode.OpenAsync(statePath).GetAwaiter().GetResult())
|
|
{
|
|
node.MutableDocument<AetheriaRuntimeDaemonFrameDocument>(
|
|
AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest)
|
|
.ReplaceAsync(frame).GetAwaiter().GetResult();
|
|
node.MutableDocument<AetheriaRuntimeCommittedCommandFactDocument>(
|
|
new CultRecordKey(AetheriaRuntimeCommittedCommandFactDocument.CreateRecordKey(winnerFact.FactId)))
|
|
.ReplaceAsync(winnerFact).GetAwaiter().GetResult();
|
|
node.MutableDocument<AetheriaRuntimeCommittedCommandFactDocument>(
|
|
new CultRecordKey(AetheriaRuntimeCommittedCommandFactDocument.CreateRecordKey(loserFact.FactId)))
|
|
.ReplaceAsync(loserFact).GetAwaiter().GetResult();
|
|
node.FlushAsync(soft: false).GetAwaiter().GetResult();
|
|
}
|
|
|
|
using (var node = AetheriaStateNode.OpenAsync(statePath).GetAwaiter().GetResult())
|
|
{
|
|
var durableFrame = node.MutableDocument<AetheriaRuntimeDaemonFrameDocument>(
|
|
AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest)
|
|
.ReadAsync().GetAwaiter().GetResult()
|
|
?? throw new InvalidOperationException("Durable contested-trade frame is missing.");
|
|
var durableWinner = node.MutableDocument<AetheriaRuntimeCommittedCommandFactDocument>(
|
|
new CultRecordKey(AetheriaRuntimeCommittedCommandFactDocument.CreateRecordKey(winnerFact.FactId)))
|
|
.ReadAsync().GetAwaiter().GetResult()
|
|
?? throw new InvalidOperationException("Durable winning trade fact is missing.");
|
|
var durableLoser = node.MutableDocument<AetheriaRuntimeCommittedCommandFactDocument>(
|
|
new CultRecordKey(AetheriaRuntimeCommittedCommandFactDocument.CreateRecordKey(loserFact.FactId)))
|
|
.ReadAsync().GetAwaiter().GetResult()
|
|
?? throw new InvalidOperationException("Durable rejected trade fact is missing.");
|
|
var durableStation = durableFrame.Run.Zones.Single().Entities.Single(entity => entity.EntityIndex == 0);
|
|
var durableShip = durableFrame.Run.Zones.Single().Entities.Single(entity => entity.EntityIndex == 1);
|
|
RequireEqual(run.Credits, durableFrame.Run.Credits,
|
|
"CultCache reconnect must preserve the single authoritative debit");
|
|
RequireEqual(0, CargoQuantity(durableStation, ore.ItemKey),
|
|
"CultCache reconnect must not resurrect contested station stock");
|
|
RequireEqual(11, CargoQuantity(durableShip, ore.ItemKey),
|
|
"CultCache reconnect must preserve the winning cargo delivery exactly once");
|
|
RequireEqual("reconciled", AetheriaRuntimeDaemonReceiptProjector.Project(
|
|
durableWinner, AetheriaRuntimeDaemonGameSurfaceBuilder.SurfaceId).State,
|
|
"the winning generic Eve receipt must survive reconnect");
|
|
RequireEqual(AetheriaRuntimeDaemonRejectionReasons.TradeStockUnavailable,
|
|
durableLoser.RejectionReason,
|
|
"the losing generic Eve receipt reason must survive reconnect");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
private static void TradeSaleIsDaemonPricedSpatialAndAtomic()
|
|
{
|
|
var ore = CatalogItem("sale-ore");
|
|
ore.Price = 40;
|
|
ore.Stackable = true;
|
|
ore.MaxStack = 10;
|
|
ore.ShapeWidth = 1;
|
|
ore.ShapeHeight = 1;
|
|
ore.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
var cargoBay = CatalogItem("sale-cargo-bay");
|
|
cargoBay.InteriorShapeWidth = 2;
|
|
cargoBay.InteriorShapeHeight = 1;
|
|
cargoBay.InteriorShapeCells =
|
|
[
|
|
new AetheriaRuntimeShapeCell(0, 0),
|
|
new AetheriaRuntimeShapeCell(1, 0)
|
|
];
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([ore, cargoBay], [], []);
|
|
|
|
var station = Entity(0, 0, "station");
|
|
station.Kind = "station";
|
|
station.CargoBays = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = cargoBay.ItemKey, Quantity = 1 }
|
|
}];
|
|
station.CargoContents = [Cargo((ore.ItemKey, 8, 0, 0))];
|
|
station.DockingBayAssignments = [1];
|
|
|
|
var ship = Entity(1, 0, "player");
|
|
ship.CargoBays = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = cargoBay.ItemKey, Quantity = 1 }
|
|
}];
|
|
ship.CargoContents = [new AetheriaRuntimeCargoBayLoadoutCommit
|
|
{
|
|
Items = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = 0,
|
|
Y = 0,
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{
|
|
ItemKey = ore.ItemKey,
|
|
Quantity = 5,
|
|
Quality = 0.75,
|
|
Temperature = 333
|
|
}
|
|
}]
|
|
}];
|
|
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "trade-sale-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.1",
|
|
Credits = 100,
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [station, ship] }]
|
|
};
|
|
var sale = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.TradeSale,
|
|
"pilot",
|
|
"trade-sale-smoke",
|
|
1,
|
|
run.CurrentEntityKey);
|
|
sale.TradeSale.ItemKey = ore.ItemKey;
|
|
sale.TradeSale.Quantity = 3;
|
|
sale.TradeSale.SourceCargoIndex = 0;
|
|
sale.TradeSale.SourceX = 0;
|
|
sale.TradeSale.SourceY = 0;
|
|
|
|
var unitPrice = AetheriaRuntimeDaemonTradeItemQueries.TradeItemValue(
|
|
ore,
|
|
ship.CargoContents[0].Items[0].Item,
|
|
catalog.TradeValueSettings).Price;
|
|
var result = AetheriaRuntimeDaemonOperations.Execute(
|
|
run,
|
|
[sale],
|
|
new AetheriaRuntimeDaemonOperationContext { Catalog = catalog });
|
|
|
|
Require(result.AppliedCommandIds.Contains(sale.CommandId),
|
|
"valid docked sale must commit through the daemon economy transaction");
|
|
RequireEqual(100 + unitPrice * 3, run.Credits,
|
|
"sale payout must be derived from the authoritative source instance and catalog");
|
|
RequireEqual(2, CargoQuantity(ship, ore.ItemKey),
|
|
"sale must remove exactly the accepted quantity from seller cargo");
|
|
RequireEqual(11, CargoQuantity(station, ore.ItemKey),
|
|
"station stock must receive the exact sold quantity");
|
|
RequireEqual(10, station.CargoContents[0].Items[0].Item.Quantity,
|
|
"station stock must fill the first compatible stack before allocating a new cell");
|
|
var allocated = station.CargoContents[0].Items.Single(slot => slot.X == 1 && slot.Y == 0);
|
|
RequireEqual(1, allocated.Item.Quantity,
|
|
"station stock overflow must use the first free authored cargo cell");
|
|
RequireNear(0.75, allocated.Item.Quality, 0.000001,
|
|
"sale placement must preserve source instance quality");
|
|
RequireNear(333, allocated.Item.Temperature, 0.000001,
|
|
"sale placement must preserve source instance temperature");
|
|
|
|
station.CargoContents[0].Items[1].Item.Quantity = 10;
|
|
var rejectedSale = AetheriaRuntimeDaemonCommandDocument.Create(
|
|
AetheriaRuntimeDaemonCommandKinds.TradeSale,
|
|
"pilot",
|
|
"trade-sale-smoke",
|
|
2,
|
|
run.CurrentEntityKey);
|
|
rejectedSale.TradeSale.ItemKey = ore.ItemKey;
|
|
rejectedSale.TradeSale.Quantity = 1;
|
|
rejectedSale.TradeSale.SourceCargoIndex = 0;
|
|
rejectedSale.TradeSale.SourceX = 0;
|
|
rejectedSale.TradeSale.SourceY = 0;
|
|
var creditsBeforeRejection = run.Credits;
|
|
result = AetheriaRuntimeDaemonOperations.Execute(
|
|
run,
|
|
[rejectedSale],
|
|
new AetheriaRuntimeDaemonOperationContext { Catalog = catalog });
|
|
|
|
Require(result.RejectedCommandIds.Contains(rejectedSale.CommandId),
|
|
"station stock without spatial capacity must reject the sale");
|
|
RequireEqual(AetheriaRuntimeDaemonRejectionReasons.TradeStationNoFit,
|
|
result.RejectedCommandReasons[rejectedSale.CommandId],
|
|
"full station stock must expose the exact trade rejection reason");
|
|
RequireEqual(creditsBeforeRejection, run.Credits,
|
|
"rejected sale must not credit the seller");
|
|
RequireEqual(2, CargoQuantity(ship, ore.ItemKey),
|
|
"rejected sale must not remove seller cargo");
|
|
}
|
|
|
|
private static void StationBodiesCannotConsumePickups()
|
|
{
|
|
var hull = CatalogItem("station-filter-hull"); hull.HullCapacity = PerformanceStat(1);
|
|
var salvage = CatalogItem("station-filter-salvage"); salvage.Volume = 1;
|
|
var cargoBay = CatalogItem("station-filter-cargo-bay"); cargoBay.InteriorOccupiedCells = 1;
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([hull, salvage, cargoBay], [], []);
|
|
var station = Entity(0, 0, "player");
|
|
station.Kind = "station";
|
|
station.HullItemKey = hull.ItemKey;
|
|
station.CargoBays = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = cargoBay.ItemKey, Quantity = 1 }
|
|
}];
|
|
station.CargoContents = [Cargo()];
|
|
var ship = Entity(1, 200, "player");
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.1",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [station, ship],
|
|
DroppedPickups = [new AetheriaRuntimeDroppedPickupCommit
|
|
{
|
|
PickupIndex = 9,
|
|
PositionX = 0,
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = salvage.ItemKey, Quantity = 1 },
|
|
LifetimeSeconds = 30
|
|
}]
|
|
}]
|
|
};
|
|
|
|
using var physics = NewPhysics();
|
|
AetheriaRuntimeDaemonTickRunner.Tick(
|
|
Path.Combine(Path.GetTempPath(), "aetheria-pickup-station-filter.cc"),
|
|
run,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
WorldPhysics = physics,
|
|
FrameId = 1,
|
|
FixedDeltaSeconds = 0.1,
|
|
SimulationTimeSeconds = 0.1,
|
|
Catalog = catalog,
|
|
BuildPublications = false
|
|
});
|
|
|
|
RequireEqual(1, run.Zones[0].DroppedPickups.Count,
|
|
"station world bodies must not consume ship-scoop pickups");
|
|
RequireEqual(0, run.PickupContactReceipts.Count,
|
|
"Ymir collision filtering must not publish station/pickup Begin facts");
|
|
RequireEqual(0, CargoQuantity(station, salvage.ItemKey),
|
|
"station cargo must remain unchanged without a ship shield contact");
|
|
}
|
|
|
|
private static void PickupShieldContactCollectsOrBounces()
|
|
{
|
|
var hull = CatalogItem("contact-hull"); hull.HullCapacity = PerformanceStat(1);
|
|
var salvage = CatalogItem("contact-salvage"); salvage.Volume = 1;
|
|
var cargoBay = CatalogItem("contact-cargo-bay"); cargoBay.InteriorOccupiedCells = 1;
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([hull, salvage, cargoBay], [], []);
|
|
AetheriaRuntimeRunCheckpointCommit Scenario(bool full)
|
|
{
|
|
var ship = Entity(0, 0, "player"); ship.HullItemKey = hull.ItemKey;
|
|
ship.CargoBays = [new AetheriaRuntimeLoadoutItemSlotCommit { Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = cargoBay.ItemKey, Quantity = 1 } }];
|
|
ship.CargoContents = [full ? Cargo((salvage.ItemKey, 1, 0, 0)) : Cargo()];
|
|
return new AetheriaRuntimeRunCheckpointCommit { CurrentZoneIndex = 0, CurrentEntityKey = "zone.0.entity.0", Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [ship], DroppedPickups = [new AetheriaRuntimeDroppedPickupCommit { PickupIndex = 10, PositionX = 20, Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = salvage.ItemKey, Quantity = 1 }, LifetimeSeconds = 30 }] }] };
|
|
}
|
|
var open = Scenario(false);
|
|
using var openPhysics = NewPhysics();
|
|
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-pickup-contact-open.cc"), open,
|
|
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = openPhysics, FrameId = 1, FixedDeltaSeconds = 0.1, SimulationTimeSeconds = 0.1, Catalog = catalog, BuildPublications = false });
|
|
RequireEqual(0, open.Zones[0].DroppedPickups.Count, "shield contact with capacity must collect pickup automatically");
|
|
RequireEqual(1, CargoQuantity(open.Zones[0].Entities[0], salvage.ItemKey), "contact collection must commit cargo once");
|
|
RequireEqual(1, open.GameEvents.Count(value => value.Kind == "pickup.collected" && value.PickupIndex == 10),
|
|
"contact collection must emit one stable event");
|
|
RequireEqual(1, open.PickupContactReceipts.Count,
|
|
"a real Box3D Begin fact must persist one cargo-consumption receipt");
|
|
|
|
var full = Scenario(true);
|
|
using var fullPhysics = NewPhysics();
|
|
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-pickup-contact-full.cc"), full,
|
|
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = fullPhysics, FrameId = 1, FixedDeltaSeconds = 0.1, SimulationTimeSeconds = 0.1, Catalog = catalog, BuildPublications = false });
|
|
RequireEqual(1, full.Zones[0].DroppedPickups.Count, "full hold must leave contacted pickup alive");
|
|
Require(full.Zones[0].DroppedPickups[0].VelocityX > 20, "failed pickup must receive the fossil outward kick");
|
|
RequireEqual(1, full.GameEvents.Count(value => value.Kind == "pickup.rejected" && value.PickupIndex == 10),
|
|
"capacity rejection must emit one stable event");
|
|
var rejectionEvent = full.GameEvents.Single(value => value.Kind == "pickup.rejected" && value.PickupIndex == 10);
|
|
RequireEqual("cargo-capacity", rejectionEvent.Reason,
|
|
"capacity rejection feedback must name the provider-owned reason");
|
|
RequireNear(1, rejectionEvent.AuxiliaryValue, 0.000001,
|
|
"capacity rejection feedback must preserve the pre-contact cargo quantity");
|
|
RequireEqual(1, full.PickupContactReceipts.Count,
|
|
"capacity rejection must consume its Box3D Begin fact exactly once");
|
|
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-pickup-contact-full-second.cc"), full,
|
|
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = fullPhysics, FrameId = 2, FixedDeltaSeconds = 0.1, SimulationTimeSeconds = 0.2, Catalog = catalog, BuildPublications = false });
|
|
RequireEqual(1, full.PickupContactReceipts.Count,
|
|
"persistent or separating contact must not replay an already consumed Begin fact");
|
|
RequireEqual(1, full.GameEvents.Count(value => value.Kind == "pickup.rejected" && value.PickupIndex == 10),
|
|
"a consumed rejection fact must not emit a second gameplay event");
|
|
var feedback = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
new AetheriaRuntimeDaemonFrameDocument { FrameId = 1, Run = full },
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("daemon"));
|
|
Require(Flatten(feedback.Surface.Root).Any(node => node.Kind == "feedback.event" &&
|
|
node.Props["eventKind"] == "pickup.rejected" && node.Props["pickupIndex"] == "10" &&
|
|
node.Props["reason"] == "cargo-capacity" &&
|
|
node.Props["cargoQuantityBefore"] == "1" && node.Props["cargoQuantityAfter"] == "1"),
|
|
"Eve surface must project authoritative pickup feedback chronology");
|
|
}
|
|
|
|
private static (AetheriaRuntimeRunCheckpointCommit Run, AetheriaRuntimeZoneSnapshotCommit Zone, AetheriaRuntimeEntitySnapshotCommit Target) Scenario()
|
|
{
|
|
var source = Entity(1, -100, "player");
|
|
var target = Entity(2, 30, "enemy");
|
|
var zone = new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [source, target],
|
|
PhysicalPayloads =
|
|
[
|
|
new AetheriaRuntimePhysicalPayloadCommit
|
|
{
|
|
PayloadId = "smoke-projectile",
|
|
SourceEntityIndex = 1,
|
|
TargetEntityIndex = 2,
|
|
PositionX = 0,
|
|
PositionZ = 0,
|
|
VelocityX = 100,
|
|
VelocityY = 0,
|
|
Radius = 1,
|
|
ContactMagnitude = 12,
|
|
LifetimeSeconds = 5,
|
|
Active = true
|
|
}
|
|
]
|
|
};
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "ymir-projectile-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.1",
|
|
Zones = [zone]
|
|
};
|
|
return (run, zone, target);
|
|
}
|
|
|
|
private static AetheriaRuntimeEntitySnapshotCommit Entity(int index, double x, string faction) => new()
|
|
{
|
|
EntityIndex = index,
|
|
EntityId = $"smoke.entity.{index}",
|
|
Kind = "ship",
|
|
FactionKey = faction,
|
|
PositionX = x,
|
|
PositionZ = 0,
|
|
TargetEntityIndex = -1,
|
|
IsActive = true,
|
|
StatGrids = [Grid("hull", 100), Grid("shield", 0), Grid("heat", 0)]
|
|
};
|
|
|
|
private static AetheriaRuntimeEntityStatGridCommit Grid(string name, double value) => new()
|
|
{
|
|
Name = name,
|
|
Width = 1,
|
|
Height = 1,
|
|
Values = [value]
|
|
};
|
|
|
|
private static AetheriaRuntimeEntityStatGridCommit Grid(string name, int width, int height, params double[] values) => new()
|
|
{
|
|
Name = name,
|
|
Width = width,
|
|
Height = height,
|
|
Values = values
|
|
};
|
|
|
|
private static double GridValue(AetheriaRuntimeEntitySnapshotCommit entity, string name, int index) =>
|
|
entity.StatGrids.Single(grid => string.Equals(grid.Name, name, StringComparison.OrdinalIgnoreCase)).Values[index];
|
|
|
|
private static IEnumerable<AetheriaRuntimeSurfaceComponent> Flatten(AetheriaRuntimeSurfaceComponent root)
|
|
{
|
|
yield return root;
|
|
foreach (var child in root.Children)
|
|
foreach (var descendant in Flatten(child))
|
|
yield return descendant;
|
|
}
|
|
|
|
private static double Stat(AetheriaRuntimeEntitySnapshotCommit entity, string name) =>
|
|
entity.StatGrids.Single(grid => string.Equals(grid.Name, name, StringComparison.OrdinalIgnoreCase)).Values[0];
|
|
|
|
private static void RequireEqual<T>(T expected, T actual, string message)
|
|
{
|
|
if (!EqualityComparer<T>.Default.Equals(expected, actual))
|
|
throw new InvalidOperationException($"{message}. Expected {expected}; actual {actual}.");
|
|
}
|
|
|
|
private static void Require(bool condition, string message)
|
|
{
|
|
if (!condition)
|
|
throw new InvalidOperationException(message);
|
|
}
|
|
|
|
private static void RequireNear(double expected, double actual, double tolerance, string message)
|
|
{
|
|
if (Math.Abs(expected - actual) > tolerance)
|
|
throw new InvalidOperationException($"{message}. Expected {expected}; actual {actual}.");
|
|
}
|
|
|
|
private static void CultCacheRestartPreservesLoadoutAndBehaviorState()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "aetheria-loadout-restart-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(root);
|
|
var statePath = Path.Combine(root, "world.cc");
|
|
try
|
|
{
|
|
var controller = CatalogItem("reconnect-controller",
|
|
new AetheriaRuntimeBehaviorPayload(0, "Thermotoggle", 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(1, Number(300)),
|
|
new AetheriaRuntimeBehaviorField(2, BoolValue(false)),
|
|
new AetheriaRuntimeBehaviorField(3, BoolValue(true))
|
|
]));
|
|
controller.Name = "Reconnect Controller";
|
|
var hull = HullCatalogItem("reconnect-hull", 9, 7, 100);
|
|
var cargoBay = CatalogItem("reconnect-cargo-bay");
|
|
cargoBay.InteriorShapeWidth = 6;
|
|
cargoBay.InteriorShapeHeight = 4;
|
|
var addedModule = CatalogItem("reconnect-added-module");
|
|
addedModule.ShapeWidth = 1;
|
|
addedModule.ShapeHeight = 1;
|
|
addedModule.OccupiedCells = 1;
|
|
addedModule.ShapeCells = [new AetheriaRuntimeShapeCell(0, 0)];
|
|
addedModule.HardpointType = "";
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
|
[hull, cargoBay, controller, addedModule], [], []);
|
|
var ship = Entity(0, 0, "player");
|
|
ship.HullItemKey = hull.ItemKey;
|
|
ship.Equipment =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
X = 3,
|
|
Y = 4,
|
|
Rotation = "CounterClockwise",
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{
|
|
ItemKey = controller.ItemKey,
|
|
Quality = 0.73,
|
|
Durability = 0.61,
|
|
Quantity = 1,
|
|
Enabled = true,
|
|
OverrideShutdown = true,
|
|
Temperature = 417
|
|
}
|
|
}
|
|
];
|
|
ship.CargoBays =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{
|
|
ItemKey = cargoBay.ItemKey,
|
|
Quantity = 1,
|
|
Quality = 1,
|
|
Durability = 1,
|
|
Enabled = true
|
|
}
|
|
}
|
|
];
|
|
ship.CargoContents = [Cargo(("reconnect-salvage", 7, 2, 1))];
|
|
ship.WeaponGroups = [new[] { 0 }];
|
|
ship.ActiveWeaponGroups = [true];
|
|
ship.EquipmentStates =
|
|
[
|
|
new AetheriaRuntimeEquipmentStateCommit
|
|
{
|
|
EquipmentIndex = 0,
|
|
Temperature = 417,
|
|
PreviousTemperature = 410,
|
|
ThermalPerformance = 0.8,
|
|
DurabilityPerformance = 0.61,
|
|
Wear = 0.12,
|
|
ThermalOnline = true,
|
|
DurabilityOnline = true,
|
|
Online = true
|
|
}
|
|
];
|
|
ship.BehaviorStates =
|
|
[
|
|
new AetheriaRuntimeBehaviorStateCommit
|
|
{
|
|
OwnerKind = AetheriaRuntimeBehaviorStateProjector.EquipmentOwnerKind,
|
|
OwnerIndex = 0,
|
|
BehaviorIndex = 0,
|
|
BehaviorKind = "Thermotoggle",
|
|
ThermotoggleTargetTemperature = 412,
|
|
ChainReached = true,
|
|
ChainSucceeded = true,
|
|
ChainCompleted = true
|
|
}
|
|
];
|
|
ship.WeaponStates =
|
|
[
|
|
new AetheriaRuntimeWeaponStateCommit
|
|
{
|
|
OwnerKind = "equipment",
|
|
OwnerIndex = 0,
|
|
BehaviorIndex = 1,
|
|
BehaviorKind = AetheriaRuntimeBehaviorKinds.InstantWeapon,
|
|
Ammo = 9,
|
|
CooldownProgress = 0.37,
|
|
LockProgress = 0.82,
|
|
LockTargetEntityIndex = 4,
|
|
ShotSequence = 23
|
|
}
|
|
];
|
|
ship.ActiveConsumables =
|
|
[
|
|
new AetheriaRuntimeActiveConsumableCommit
|
|
{
|
|
EffectId = "effect:reconnect-tonic:1",
|
|
ItemKey = "reconnect-tonic",
|
|
Quality = 0.67,
|
|
Duration = 30,
|
|
RemainingDuration = 12,
|
|
BehaviorStates =
|
|
[
|
|
new AetheriaRuntimeConsumableBehaviorStateCommit
|
|
{
|
|
BehaviorId = "tonic-modifier",
|
|
BehaviorIndex = 0,
|
|
BehaviorKind = AetheriaRuntimeBehaviorKinds.StatModifier,
|
|
ScalarState = 0.42,
|
|
StatModifierApplied = true,
|
|
StatModifierExecuted = true,
|
|
StatModifierTargetStatCount = 3
|
|
}
|
|
]
|
|
}
|
|
];
|
|
ship.LoadoutGeneration = new AetheriaRuntimeLoadoutGenerationReceiptCommit
|
|
{
|
|
Seed = 0xA37E,
|
|
SourceZoneIndex = 2,
|
|
AvailabilityFactionKey = "player",
|
|
PriceExponent = 0.25,
|
|
Selections =
|
|
[
|
|
new AetheriaRuntimeLoadoutGenerationSelectionCommit
|
|
{
|
|
Role = "controller",
|
|
ItemKey = controller.ItemKey,
|
|
ManufacturerKey = "player",
|
|
Price = 1200,
|
|
ManufacturerDistance = 1,
|
|
Allegiance = 0.9
|
|
}
|
|
]
|
|
};
|
|
var station = Entity(1, 0, "player");
|
|
station.Kind = "station";
|
|
station.DockingBayAssignments = [ship.EntityIndex];
|
|
station.CargoBays =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{
|
|
ItemKey = cargoBay.ItemKey,
|
|
Quantity = 1,
|
|
Quality = 1,
|
|
Durability = 1,
|
|
Enabled = true
|
|
}
|
|
}
|
|
];
|
|
station.CargoContents =
|
|
[
|
|
Cargo((addedModule.ItemKey, 1, 0, 0))
|
|
];
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "loadout-restart-smoke",
|
|
CurrentZoneIndex = 0,
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [ship, station] }]
|
|
};
|
|
run.CurrentEntityKey = run.EntityRecordKey(0, ship.EntityIndex);
|
|
var frame = AetheriaRuntimeDaemonFrameDocument.Create(
|
|
run, "smoke", "loadout-restart", 41, 4.1, 0.1);
|
|
var shipKey = run.EntityRecordKey(0, ship.EntityIndex);
|
|
var stationKey = run.EntityRecordKey(0, station.EntityIndex);
|
|
var surface = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
|
frame,
|
|
new AetheriaRuntimeDaemonHealthDocument(),
|
|
AetheriaRuntimeDaemonCommandBoundaryDocument.Create("smoke"),
|
|
catalog: catalog);
|
|
var equipOperation = AetheriaRuntimeDaemonSurfaceCommandCatalog.CommandName(
|
|
AetheriaRuntimeDaemonCommandKinds.EquipItem);
|
|
Require(surface.Commands.Any(command => command.Command == equipOperation),
|
|
"docked Eve surface must advertise the generic equip operation before reconnect");
|
|
var request = new EveSurfaceCommandRequest(
|
|
"aetheria",
|
|
AetheriaRuntimeDaemonGameSurfaceBuilder.PilotSurfaceId,
|
|
CultMesh.OperationInvocation(equipOperation),
|
|
CultMesh.OperationPayload(
|
|
("entityId", shipKey),
|
|
("sourceKind", AetheriaRuntimeRefitSourceKinds.Cargo),
|
|
("originEntityKey", stationKey),
|
|
("originIndex", "0"),
|
|
("destinationEntityKey", shipKey),
|
|
("itemKey", addedModule.ItemKey),
|
|
("sourceX", "0"),
|
|
("sourceY", "0"),
|
|
("destinationX", "5"),
|
|
("destinationY", "2"),
|
|
("hasDestinationPosition", "true")),
|
|
DateTimeOffset.UtcNow,
|
|
"loadout-restart-smoke");
|
|
Require(AetheriaRuntimeDaemonOperationsClient.TryCreateSurfaceCommandDocument(
|
|
request,
|
|
frame,
|
|
".",
|
|
"loadout-restart-smoke",
|
|
"refit",
|
|
out var equipCommand) && equipCommand != null,
|
|
"generic Eve equip request must translate into a typed daemon command");
|
|
var refit = AetheriaRuntimeDaemonOperations.Execute(
|
|
run,
|
|
[equipCommand!],
|
|
new AetheriaRuntimeDaemonOperationContext { Catalog = catalog });
|
|
Require(refit.AppliedCommandIds.Contains(equipCommand!.CommandId, StringComparer.Ordinal),
|
|
"generic Eve equip command must commit through the atomic daemon refit transaction: " +
|
|
string.Join(",", refit.RejectedCommandReasons.Values));
|
|
Require(ship.Equipment.Any(slot => slot.Item.ItemKey == addedModule.ItemKey &&
|
|
slot.X == 5 && slot.Y == 2),
|
|
"accepted generic refit must place the exact module in daemon world truth");
|
|
RequireEqual(0, station.CargoContents.Single().Items.Count,
|
|
"accepted generic refit must remove the exact source item atomically");
|
|
|
|
using (var node = AetheriaStateNode.OpenAsync(statePath).GetAwaiter().GetResult())
|
|
{
|
|
node.MutableDocument<AetheriaRuntimeDaemonFrameDocument>(
|
|
AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest)
|
|
.ReplaceAsync(frame).GetAwaiter().GetResult();
|
|
node.FlushAsync(soft: false).GetAwaiter().GetResult();
|
|
}
|
|
|
|
using (var node = AetheriaStateNode.OpenAsync(statePath).GetAwaiter().GetResult())
|
|
{
|
|
var durable = node.MutableDocument<AetheriaRuntimeDaemonFrameDocument>(
|
|
AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest)
|
|
.ReadAsync().GetAwaiter().GetResult()
|
|
?? throw new InvalidOperationException("Durable loadout restart frame is missing.");
|
|
var restored = durable.Run.Zones.Single().Entities.Single(entity => entity.EntityIndex == ship.EntityIndex);
|
|
var restoredStation = durable.Run.Zones.Single().Entities.Single(entity => entity.EntityIndex == station.EntityIndex);
|
|
var installed = restored.Equipment.Single(slot => slot.Item.ItemKey == controller.ItemKey);
|
|
RequireEqual("reconnect-hull", restored.HullItemKey,
|
|
"CultCache restart must preserve daemon-owned hull identity");
|
|
Require(installed.X == 3 && installed.Y == 4 && installed.Rotation == "CounterClockwise",
|
|
"CultCache restart must preserve exact equipment placement and rotation");
|
|
RequireNear(0.73, installed.Item.Quality, 0.000001,
|
|
"CultCache restart must preserve installed item quality");
|
|
RequireNear(0.61, installed.Item.Durability, 0.000001,
|
|
"CultCache restart must preserve installed item durability");
|
|
Require(installed.Item.OverrideShutdown && installed.Item.Temperature == 417,
|
|
"CultCache restart must preserve per-instance thermal controls");
|
|
RequireEqual(7, restored.CargoContents.Single().Items.Single().Item.Quantity,
|
|
"CultCache restart must preserve cargo stack quantity");
|
|
Require(restored.WeaponGroups.Single().SequenceEqual(new[] { 0 }) &&
|
|
restored.ActiveWeaponGroups.SequenceEqual(new[] { true }),
|
|
"CultCache restart must preserve weapon membership and held state");
|
|
RequireNear(0.12, restored.EquipmentStates.Single().Wear, 0.000001,
|
|
"CultCache restart must preserve equipment wear state");
|
|
RequireNear(412, restored.BehaviorStates.Single().ThermotoggleTargetTemperature, 0.000001,
|
|
"CultCache restart must preserve executable behavior state");
|
|
Require(restored.BehaviorStates.Single().ChainCompleted,
|
|
"CultCache restart must preserve specialized chain chronology");
|
|
Require(restored.WeaponStates.Single().ShotSequence == 23 &&
|
|
restored.WeaponStates.Single().Ammo == 9,
|
|
"CultCache restart must preserve weapon chronology and ammunition");
|
|
var consumableState = restored.ActiveConsumables.Single().BehaviorStates.Single();
|
|
Require(consumableState.StatModifierApplied && consumableState.StatModifierExecuted &&
|
|
consumableState.StatModifierTargetStatCount == 3,
|
|
"CultCache restart must preserve active consumable execution state");
|
|
Require(restored.LoadoutGeneration?.Selections.Single().ItemKey == controller.ItemKey,
|
|
"CultCache restart must preserve generated-loadout provenance");
|
|
Require(restored.Equipment.Any(slot => slot.Item.ItemKey == addedModule.ItemKey &&
|
|
slot.X == 5 && slot.Y == 2),
|
|
"CultCache restart must preserve the generic Eve refit's accepted placement");
|
|
RequireEqual(0, restoredStation.CargoContents.Single().Items.Count,
|
|
"CultCache restart must not resurrect the generic refit's consumed source item");
|
|
|
|
var capability = AetheriaRuntimeInputCapabilityDocument.FromFrame(
|
|
durable, catalog: catalog);
|
|
RequireEqual("1", capability.Actions.Single(action =>
|
|
action.ActionId == "weapon-group.0.fire").Payload["currentValue"],
|
|
"generic Eve reconnect must observe the daemon-owned held weapon state");
|
|
RequireNear(412, capability.Actions.Single(action =>
|
|
action.ActionId == "equipment.0.behavior.0").InputValue?.CurrentValue ?? double.NaN,
|
|
0.000001,
|
|
"generic Eve reconnect must observe the daemon-owned thermostat target");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
private static void YmirRestartDoesNotReplayConsumedPickupContact()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "aetheria-ymir-restart-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(root);
|
|
var statePath = Path.Combine(root, "world.cc");
|
|
try
|
|
{
|
|
var hull = CatalogItem("restart-contact-hull"); hull.HullCapacity = PerformanceStat(1);
|
|
var salvage = CatalogItem("restart-contact-salvage"); salvage.Volume = 1;
|
|
var cargoBay = CatalogItem("restart-contact-cargo-bay"); cargoBay.InteriorOccupiedCells = 1;
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([hull, salvage, cargoBay], [], []);
|
|
var ship = Entity(0, 0, "player"); ship.HullItemKey = hull.ItemKey;
|
|
ship.CargoBays = [new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = cargoBay.ItemKey, Quantity = 1 }
|
|
}];
|
|
ship.CargoContents = [Cargo((salvage.ItemKey, 1, 0, 0))];
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "ymir-restart-contact-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit
|
|
{
|
|
ZoneIndex = 0,
|
|
Entities = [ship],
|
|
DroppedPickups = [new AetheriaRuntimeDroppedPickupCommit
|
|
{
|
|
PickupIndex = 10,
|
|
PositionX = 20,
|
|
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = salvage.ItemKey, Quantity = 1 },
|
|
LifetimeSeconds = 30
|
|
}]
|
|
}]
|
|
};
|
|
var frame = new AetheriaRuntimeDaemonFrameDocument
|
|
{
|
|
FrameId = 1,
|
|
FixedDeltaSeconds = 0.1,
|
|
SimulationTimeSeconds = 0.1,
|
|
Run = run
|
|
};
|
|
|
|
using (var physics = new AetheriaYmirWorldPhysics())
|
|
{
|
|
AetheriaRuntimeDaemonTickRunner.Tick(statePath, run,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
WorldPhysics = physics,
|
|
FrameId = 1,
|
|
FixedDeltaSeconds = 0.1,
|
|
SimulationTimeSeconds = 0.1,
|
|
Catalog = catalog,
|
|
BuildPublications = false
|
|
});
|
|
RequireEqual(1, run.PickupContactReceipts.Count,
|
|
"pre-restart rejection must consume exactly one Box3D Begin fact");
|
|
var rejectedVelocity = run.Zones[0].DroppedPickups.Single().VelocityX;
|
|
Require(rejectedVelocity > 20, "pre-restart rejection must persist the outward kick in world truth");
|
|
|
|
frame.Run = run;
|
|
using var node = AetheriaStateNode.OpenAsync(statePath).GetAwaiter().GetResult();
|
|
using var persistence = AetheriaYmirPersistenceCoordinator.OpenAsync(node, physics, null)
|
|
.GetAwaiter().GetResult();
|
|
var capture = persistence.Capture(frame);
|
|
persistence.PersistPrivateAsync(capture).GetAwaiter().GetResult();
|
|
node.MutableDocument<AetheriaRuntimeDaemonFrameDocument>(AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest)
|
|
.ReplaceAsync(frame).GetAwaiter().GetResult();
|
|
node.FlushAsync(soft: false).GetAwaiter().GetResult();
|
|
persistence.ActivateAsync().GetAwaiter().GetResult();
|
|
Require(!AetheriaDocumentRegistry.DocumentTypes.Contains(typeof(AetheriaYmirResumeDocument)),
|
|
"daemon-private Ymir resume types must not enter the public Aetheria registry");
|
|
Require(!node.Cache.AllEntries.Any(value => value is AetheriaYmirResumeDocument or AetheriaYmirJournalChunkDocument),
|
|
"public CultMesh cache must not enumerate daemon-private Ymir restart material");
|
|
}
|
|
|
|
using (var node = AetheriaStateNode.OpenAsync(statePath).GetAwaiter().GetResult())
|
|
{
|
|
var durable = node.MutableDocument<AetheriaRuntimeDaemonFrameDocument>(AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest)
|
|
.ReadAsync().GetAwaiter().GetResult()
|
|
?? throw new InvalidOperationException("Durable restart smoke frame is missing.");
|
|
using var restoredPhysics = new AetheriaYmirWorldPhysics();
|
|
using var persistence = AetheriaYmirPersistenceCoordinator.OpenAsync(node, restoredPhysics, durable)
|
|
.GetAwaiter().GetResult();
|
|
AetheriaRuntimeDaemonTickRunner.Tick(statePath, durable.Run,
|
|
new AetheriaRuntimeDaemonTickOptions
|
|
{
|
|
WorldPhysics = restoredPhysics,
|
|
FrameId = 2,
|
|
FixedDeltaSeconds = 0.1,
|
|
SimulationTimeSeconds = 0.2,
|
|
Catalog = catalog,
|
|
BuildPublications = false
|
|
});
|
|
RequireEqual(1, durable.Run.PickupContactReceipts.Count,
|
|
"restart must not replay an already consumed Box3D Begin fact");
|
|
RequireEqual(1, durable.Run.GameEvents.Count(value =>
|
|
value.Kind == "pickup.rejected" && value.PickupIndex == 10),
|
|
"restart must not duplicate authoritative pickup rejection feedback");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
private static void CombatLockSurvivesPublicFrameRestart()
|
|
{
|
|
var root = Path.Combine(Path.GetTempPath(), "aetheria-combat-restart-" + Guid.NewGuid().ToString("N"));
|
|
Directory.CreateDirectory(root);
|
|
var statePath = Path.Combine(root, "world.cc");
|
|
try
|
|
{
|
|
var source = Entity(0, 0, "player");
|
|
source.DirectionX = 1;
|
|
source.TargetEntityIndex = 1;
|
|
source.Contacts =
|
|
[
|
|
new AetheriaRuntimeEntityContactCommit
|
|
{ TargetEntityIndex = 1, InfoGathered = 1, Hostile = true, Visible = true }
|
|
];
|
|
source.WeaponGroups = [new[] { 0 }];
|
|
source.Equipment =
|
|
[
|
|
new AetheriaRuntimeLoadoutItemSlotCommit
|
|
{
|
|
Item = new AetheriaRuntimeLoadoutItemCommit
|
|
{ ItemKey = "restart-instant", Quality = 1, Durability = 1, Enabled = true }
|
|
}
|
|
];
|
|
var target = Entity(1, 80, "raider");
|
|
var run = new AetheriaRuntimeRunCheckpointCommit
|
|
{
|
|
RunId = "combat-restart-smoke",
|
|
CurrentZoneIndex = 0,
|
|
CurrentEntityKey = "zone.0.entity.0",
|
|
Zones = [new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [source, target] }]
|
|
};
|
|
var payload = new AetheriaRuntimeBehaviorPayload(0, AetheriaRuntimeBehaviorKinds.InstantWeapon, 0,
|
|
[
|
|
new AetheriaRuntimeBehaviorField(2, PerformanceStat(20)),
|
|
new AetheriaRuntimeBehaviorField(6, PerformanceStat(150)),
|
|
new AetheriaRuntimeBehaviorField(15, PerformanceStat(0)),
|
|
new AetheriaRuntimeBehaviorField(16, PerformanceStat(200)),
|
|
new AetheriaRuntimeBehaviorField(17, PerformanceStat(1)),
|
|
new AetheriaRuntimeBehaviorField(19, PerformanceStat(0.5)),
|
|
new AetheriaRuntimeBehaviorField(23, PerformanceStat(45)),
|
|
new AetheriaRuntimeBehaviorField(24, PerformanceStat(0)),
|
|
new AetheriaRuntimeBehaviorField(25, PerformanceStat(1))
|
|
]);
|
|
var catalog = new AetheriaRuntimeCatalogSnapshot([CatalogItem("restart-instant", payload)], [], []);
|
|
var fire = new AetheriaRuntimeDaemonIntentState();
|
|
fire.WeaponGroups.Add(new AetheriaRuntimeDaemonWeaponGroupIntent
|
|
{
|
|
ActorEntityKey = "zone.0.entity.0", WeaponGroup = 0, Fire = true, Active = true
|
|
});
|
|
|
|
using (var physics = new AetheriaYmirWorldPhysics())
|
|
AetheriaRuntimeDaemonSimulation.Step(run, fire, 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
physics, catalog, 1, 0.1);
|
|
var state = source.WeaponStates.Single(value =>
|
|
value.BehaviorKind == AetheriaRuntimeBehaviorKinds.InstantWeapon);
|
|
Require(state.TriggerPending && state.LockProgress > 0 && state.LockProgress < 0.99 &&
|
|
run.ShotReceipts.Count == 0,
|
|
"combat restart fixture must stop with one pending trigger and a partial daemon lock");
|
|
var partialProgress = state.LockProgress;
|
|
var frame = new AetheriaRuntimeDaemonFrameDocument
|
|
{
|
|
FrameId = 1,
|
|
FixedDeltaSeconds = 0.1,
|
|
SimulationTimeSeconds = 0.1,
|
|
Run = run
|
|
};
|
|
using (var node = AetheriaStateNode.OpenAsync(statePath).GetAwaiter().GetResult())
|
|
{
|
|
node.MutableDocument<AetheriaRuntimeDaemonFrameDocument>(AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest)
|
|
.ReplaceAsync(frame).GetAwaiter().GetResult();
|
|
node.FlushAsync(soft: false).GetAwaiter().GetResult();
|
|
}
|
|
|
|
using (var node = AetheriaStateNode.OpenAsync(statePath).GetAwaiter().GetResult())
|
|
{
|
|
var durable = node.MutableDocument<AetheriaRuntimeDaemonFrameDocument>(AetheriaRuntimeVerseRecordKeys.DaemonFrameLatest)
|
|
.ReadAsync().GetAwaiter().GetResult()
|
|
?? throw new InvalidOperationException("Durable combat restart frame is missing.");
|
|
var restoredSource = durable.Run.Zones[0].Entities[0];
|
|
var restoredState = restoredSource.WeaponStates.Single(value =>
|
|
value.BehaviorKind == AetheriaRuntimeBehaviorKinds.InstantWeapon);
|
|
Require(restoredState.TriggerPending && Math.Abs(restoredState.LockProgress - partialProgress) < 0.000001,
|
|
"public frame restart must retain the pending trigger and exact partial lock progress");
|
|
using var restoredPhysics = new AetheriaYmirWorldPhysics();
|
|
for (var frameId = 2; frameId < 12 && durable.Run.ShotReceipts.Count == 0; frameId++)
|
|
AetheriaRuntimeDaemonSimulation.Step(durable.Run, new AetheriaRuntimeDaemonIntentState(), 0.1,
|
|
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
|
restoredPhysics, catalog, frameId, frameId * 0.1);
|
|
RequireEqual(1, durable.Run.ShotReceipts.Count,
|
|
"restart must complete one pending instant-weapon shot, not lose or duplicate it");
|
|
RequireEqual(1, durable.Run.GameEvents.Count(value => value.Kind == "weapon.lock.started"),
|
|
"restart must not replay the pre-restart lock-start transition");
|
|
RequireEqual(1, durable.Run.GameEvents.Count(value => value.Kind == "weapon.lock.acquired"),
|
|
"restart must publish one completed lock transition");
|
|
RequireEqual(1, durable.Run.GameEvents.Count(value => value.Kind == "shot.committed"),
|
|
"restart must publish one committed-shot transition");
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(root))
|
|
Directory.Delete(root, recursive: true);
|
|
}
|
|
}
|
|
|
|
private sealed class ScriptedWorldPhysics(params AetheriaRuntimeWorldBeginContact[] contacts) : IAetheriaRuntimeWorldPhysics
|
|
{
|
|
public string ImplementationId => "smoke.scripted-world";
|
|
|
|
public void RetainWorlds(string runId, IReadOnlyList<int> zoneIndices) { }
|
|
|
|
public AetheriaRuntimeWorldPickupStep ApplyPickupRejection(
|
|
string runId,
|
|
int zoneIndex,
|
|
AetheriaRuntimeWorldBeginContact contact) =>
|
|
new() { PickupIndex = contact.PickupIndex, VelocityX = contact.NormalX * 25, VelocityZ = contact.NormalZ * 25 };
|
|
|
|
public AetheriaRuntimeWorldStep Step(
|
|
string runId,
|
|
long frameId,
|
|
int simulationStepIndex,
|
|
AetheriaRuntimeZoneSnapshotCommit zone,
|
|
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
|
|
double deltaSeconds) => new([], [], contacts);
|
|
|
|
public AetheriaRuntimePhysicalPayloadStep StepPhysicalPayloads(
|
|
string runId,
|
|
long frameId,
|
|
int simulationStepIndex,
|
|
AetheriaRuntimeZoneSnapshotCommit zone,
|
|
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
|
|
double deltaSeconds) => new(zone.PhysicalPayloads, []);
|
|
}
|
|
}
|