Collect loot by daemon proximity
This commit is contained in:
parent
d173be4006
commit
61e0b0dbac
16 changed files with 254 additions and 367 deletions
|
|
@ -22,7 +22,7 @@ else if (args.Contains("--loadout", StringComparer.Ordinal))
|
|||
else if (args.Contains("--pickup", StringComparer.Ordinal))
|
||||
{
|
||||
checks.RunPickup();
|
||||
Console.WriteLine("Daemon Ymir pickup-contact smoke passed.");
|
||||
Console.WriteLine("Daemon pickup-proximity smoke passed.");
|
||||
}
|
||||
else if (args.Contains("--payload", StringComparer.Ordinal))
|
||||
{
|
||||
|
|
@ -95,8 +95,8 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
RunCheck(TractorRampsAndPullsThroughYmirWithoutTeleportingCargo);
|
||||
RunCheck(PickupIsCapacityCheckedExactlyOnceAndExpires);
|
||||
RunCheck(StationBodiesCannotConsumePickups);
|
||||
RunCheck(PickupShieldContactCollectsOrBounces);
|
||||
RunCheck(YmirRestartDoesNotReplayConsumedPickupContact);
|
||||
RunCheck(PickupProximityCollectsOrBounces);
|
||||
RunCheck(PickupRemovalSurvivesRestartWithoutReplay);
|
||||
}
|
||||
|
||||
public void RunPayload()
|
||||
|
|
@ -820,9 +820,9 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
TradePurchaseDerivesAcceptanceFromDaemonState,
|
||||
TradeSaleIsDaemonPricedSpatialAndAtomic,
|
||||
StationBodiesCannotConsumePickups,
|
||||
PickupShieldContactCollectsOrBounces,
|
||||
PickupProximityCollectsOrBounces,
|
||||
CultCacheRestartPreservesLoadoutAndBehaviorState,
|
||||
YmirRestartDoesNotReplayConsumedPickupContact,
|
||||
PickupRemovalSurvivesRestartWithoutReplay,
|
||||
ThermalCellsUseFossilConductionAndRadiation,
|
||||
EnergyNetworkSettlesReactorAfterConsumers,
|
||||
RadiatorPumpsHeatBeforeReactorSettlement,
|
||||
|
|
@ -8200,7 +8200,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
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 zone = new AetheriaRuntimeZoneSnapshotCommit { ZoneIndex = 0, Entities = [ship], DroppedPickups = [new AetheriaRuntimeDroppedPickupCommit { PickupIndex = 7, PositionX = AetheriaRuntimeTractorMechanics.CollectionDistance + 0.001, 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";
|
||||
|
|
@ -8209,55 +8209,25 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
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,
|
||||
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-pickup-outside-proximity.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");
|
||||
RequireEqual(1, zone.DroppedPickups.Count, "pickup outside the daemon collection distance must remain in the world");
|
||||
RequireEqual(0, CargoQuantity(ship, salvage.ItemKey), "outside 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");
|
||||
zone.DroppedPickups.Single().PositionX = AetheriaRuntimeTractorMechanics.CollectionDistance;
|
||||
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-pickup-proximity-dedup.cc"), run,
|
||||
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = new ScriptedWorldPhysics(), FrameId = 2, FixedDeltaSeconds = 0.1, SimulationTimeSeconds = 0.2, Catalog = catalog, BuildPublications = false });
|
||||
RequireEqual(0, zone.DroppedPickups.Count, "pickup at the 2D collection boundary must be consumed");
|
||||
RequireEqual(1, CargoQuantity(ship, salvage.ItemKey), "one pickup identity 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");
|
||||
"one consumed pickup identity 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");
|
||||
RequireEqual(0, run.PickupContactReceipts.Count,
|
||||
"proximity collection must not mint obsolete Ymir-contact receipts");
|
||||
var collectionFeedback = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
||||
new AetheriaRuntimeDaemonFrameDocument { FrameId = 3, Run = run },
|
||||
new AetheriaRuntimeDaemonHealthDocument(),
|
||||
|
|
@ -8266,11 +8236,11 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
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");
|
||||
"Eve feedback must publish the exact cargo delta committed by daemon proximity");
|
||||
|
||||
zone.DroppedPickups = [new AetheriaRuntimeDroppedPickupCommit { PickupIndex = 8, PositionX = 10, Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = salvage.ItemKey, Quantity = 1 }, LifetimeSeconds = 30 }];
|
||||
zone.DroppedPickups = [new AetheriaRuntimeDroppedPickupCommit { PickupIndex = 8, PositionX = 100, 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 });
|
||||
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = new ScriptedWorldPhysics(), FrameId = 3, 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");
|
||||
|
|
@ -8652,13 +8622,11 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
|
||||
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");
|
||||
"station cargo must remain unchanged because only ships collect by proximity");
|
||||
}
|
||||
|
||||
private static void PickupShieldContactCollectsOrBounces()
|
||||
private static void PickupProximityCollectsOrBounces()
|
||||
{
|
||||
var hull = CatalogItem("contact-hull"); hull.HullCapacity = PerformanceStat(1);
|
||||
var salvage = CatalogItem("contact-salvage"); salvage.Volume = 1;
|
||||
|
|
@ -8673,36 +8641,41 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
}
|
||||
var open = Scenario(false);
|
||||
using var openPhysics = NewPhysics();
|
||||
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-pickup-contact-open.cc"), open,
|
||||
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-pickup-proximity-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(0, open.Zones[0].DroppedPickups.Count, "2D ship proximity with capacity must collect pickup automatically");
|
||||
RequireEqual(1, CargoQuantity(open.Zones[0].Entities[0], salvage.ItemKey), "proximity 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");
|
||||
"proximity collection must emit one stable event");
|
||||
RequireEqual(0, open.PickupContactReceipts.Count,
|
||||
"proximity collection must not depend on a Box3D Begin fact");
|
||||
|
||||
var full = Scenario(true);
|
||||
using var fullPhysics = NewPhysics();
|
||||
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-pickup-contact-full.cc"), full,
|
||||
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-pickup-proximity-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");
|
||||
RequireEqual(1, full.Zones[0].DroppedPickups.Count, "full hold must leave the nearby pickup alive");
|
||||
Require(full.Zones[0].DroppedPickups[0].VelocityX > 20, "failed pickup must receive the fossil outward kick");
|
||||
var rejectedPickup = full.Zones[0].DroppedPickups[0];
|
||||
var rejectedShip = full.Zones[0].Entities[0];
|
||||
Require(Math.Sqrt(
|
||||
Math.Pow(rejectedPickup.PositionX - rejectedShip.PositionX, 2) +
|
||||
Math.Pow(rejectedPickup.PositionZ - rejectedShip.PositionZ, 2)) >
|
||||
AetheriaRuntimeTractorMechanics.CollectionDistance,
|
||||
"capacity rejection must move the pickup outside collection proximity before the next step");
|
||||
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,
|
||||
"capacity rejection feedback must preserve the pre-collection cargo quantity");
|
||||
RequireEqual(0, full.PickupContactReceipts.Count,
|
||||
"capacity rejection must not mint obsolete contact receipts");
|
||||
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-pickup-proximity-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");
|
||||
"the outward kick must prevent an immediate second rejection event");
|
||||
var feedback = AetheriaRuntimeDaemonGameSurfaceBuilder.Build(
|
||||
new AetheriaRuntimeDaemonFrameDocument { FrameId = 1, Run = full },
|
||||
new AetheriaRuntimeDaemonHealthDocument(),
|
||||
|
|
@ -9114,26 +9087,26 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
}
|
||||
}
|
||||
|
||||
private static void YmirRestartDoesNotReplayConsumedPickupContact()
|
||||
private static void PickupRemovalSurvivesRestartWithoutReplay()
|
||||
{
|
||||
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 hull = CatalogItem("restart-pickup-hull"); hull.HullCapacity = PerformanceStat(1);
|
||||
var salvage = CatalogItem("restart-pickup-salvage"); salvage.Volume = 1;
|
||||
var cargoBay = CatalogItem("restart-pickup-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))];
|
||||
ship.CargoContents = [Cargo()];
|
||||
var run = new AetheriaRuntimeRunCheckpointCommit
|
||||
{
|
||||
RunId = "ymir-restart-contact-smoke",
|
||||
RunId = "pickup-restart-proximity-smoke",
|
||||
CurrentZoneIndex = 0,
|
||||
CurrentEntityKey = "zone.0.entity.0",
|
||||
Zones = [new AetheriaRuntimeZoneSnapshotCommit
|
||||
|
|
@ -9169,10 +9142,15 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
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");
|
||||
RequireEqual(0, run.Zones[0].DroppedPickups.Count,
|
||||
"pre-restart proximity collection must remove the durable pickup row");
|
||||
RequireEqual(1, CargoQuantity(ship, salvage.ItemKey),
|
||||
"pre-restart proximity collection must commit cargo once");
|
||||
RequireEqual(1, run.GameEvents.Count(value =>
|
||||
value.Kind == "pickup.collected" && value.PickupIndex == 10),
|
||||
"pre-restart proximity collection must emit one stable event");
|
||||
RequireEqual(0, run.PickupContactReceipts.Count,
|
||||
"pre-restart proximity collection must not create contact receipts");
|
||||
|
||||
frame.Run = run;
|
||||
using var node = AetheriaStateNode.OpenAsync(statePath).GetAwaiter().GetResult();
|
||||
|
|
@ -9208,11 +9186,13 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
Catalog = catalog,
|
||||
BuildPublications = false
|
||||
});
|
||||
RequireEqual(1, durable.Run.PickupContactReceipts.Count,
|
||||
"restart must not replay an already consumed Box3D Begin fact");
|
||||
RequireEqual(0, durable.Run.Zones[0].DroppedPickups.Count,
|
||||
"restart must not resurrect the consumed pickup identity");
|
||||
RequireEqual(1, CargoQuantity(durable.Run.Zones[0].Entities[0], salvage.ItemKey),
|
||||
"restart must not duplicate committed pickup cargo");
|
||||
RequireEqual(1, durable.Run.GameEvents.Count(value =>
|
||||
value.Kind == "pickup.rejected" && value.PickupIndex == 10),
|
||||
"restart must not duplicate authoritative pickup rejection feedback");
|
||||
value.Kind == "pickup.collected" && value.PickupIndex == 10),
|
||||
"restart must not duplicate authoritative pickup collection feedback");
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
|
@ -9329,25 +9309,19 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
|
|||
}
|
||||
}
|
||||
|
||||
private sealed class ScriptedWorldPhysics(params AetheriaRuntimeWorldBeginContact[] contacts) : IAetheriaRuntimeWorldPhysics
|
||||
private sealed class ScriptedWorldPhysics : 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);
|
||||
double deltaSeconds) => new([], []);
|
||||
|
||||
public AetheriaRuntimePhysicalPayloadStep StepPhysicalPayloads(
|
||||
string runId,
|
||||
|
|
|
|||
|
|
@ -105,52 +105,6 @@ public sealed class AetheriaYmirWorldPhysics : IAetheriaRuntimeWorldPhysics, IDi
|
|||
}
|
||||
}
|
||||
|
||||
public AetheriaRuntimeWorldPickupStep ApplyPickupRejection(
|
||||
string runId,
|
||||
int zoneIndex,
|
||||
AetheriaRuntimeWorldBeginContact contact)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(contact);
|
||||
if (string.IsNullOrWhiteSpace(contact.FactId))
|
||||
throw new ArgumentException("Pickup rejection requires a Ymir Begin fact id.", nameof(contact));
|
||||
if (!_sessions.TryGetValue(new WorldKey(runId, zoneIndex), out var state))
|
||||
throw new InvalidOperationException($"No retained Ymir session owns run '{runId}' zone {zoneIndex}.");
|
||||
|
||||
var pickupId = PickupPrefix + contact.PickupIndex;
|
||||
var entityIndex = contact.EntityAIndex >= 0 ? contact.EntityAIndex : contact.EntityBIndex;
|
||||
var stableEntityId = contact.EntityAIndex >= 0 ? contact.EntityAId : contact.EntityBId;
|
||||
if (string.IsNullOrWhiteSpace(stableEntityId))
|
||||
throw new InvalidOperationException($"Ymir Begin fact '{contact.FactId}' has no stable entity identity.");
|
||||
var entityId = EntityPrefix + stableEntityId;
|
||||
var bodies = state.WorldSession.Snapshot().Bodies.ToDictionary(body => body.Id, StringComparer.Ordinal);
|
||||
if (!bodies.TryGetValue(pickupId, out var pickup) || !bodies.TryGetValue(entityId, out var entity))
|
||||
throw new InvalidOperationException($"Ymir Begin fact '{contact.FactId}' references a body outside its retained session.");
|
||||
|
||||
var normalSign = contact.EntityAIndex == entityIndex ? 1.0 : -1.0;
|
||||
var normal = Normalize(
|
||||
contact.NormalX * normalSign,
|
||||
contact.NormalZ * normalSign,
|
||||
pickup.Position.X >= entity.Position.X ? 1 : -1,
|
||||
0);
|
||||
RequireAccepted(state.WorldSession.SetVelocity(new YmirSetBodyVelocityCommand(
|
||||
new YmirCommandHeader($"aetheria:fact:{contact.FactId}:pickup-rejection", state.WorldSession.Info.Revision),
|
||||
pickupId,
|
||||
new Vec2(
|
||||
pickup.Velocity.X + (float)(normal.X * 25.0),
|
||||
pickup.Velocity.Y + (float)(normal.Y * 25.0)),
|
||||
pickup.AngularVelocity)));
|
||||
|
||||
var rejected = state.WorldSession.Snapshot().Bodies.Single(body => body.Id == pickupId);
|
||||
return new AetheriaRuntimeWorldPickupStep
|
||||
{
|
||||
PickupIndex = contact.PickupIndex,
|
||||
PositionX = rejected.Position.X,
|
||||
PositionZ = rejected.Position.Y,
|
||||
VelocityX = rejected.Velocity.X,
|
||||
VelocityZ = rejected.Velocity.Y
|
||||
};
|
||||
}
|
||||
|
||||
public AetheriaRuntimeWorldStep Step(
|
||||
string runId,
|
||||
long frameId,
|
||||
|
|
@ -404,10 +358,10 @@ public sealed class AetheriaYmirWorldPhysics : IAetheriaRuntimeWorldPhysics, IDi
|
|||
Header(session, frameId, simulationStepIndex, "configure", body.Id, ref commandOrdinal),
|
||||
body.Id, body.Radius, body.Mass, body.IsStatic, body.Restitution, body.IsKinematic)));
|
||||
|
||||
if (present.Direction != body.Direction)
|
||||
if (present.Position != body.Position || present.Direction != body.Direction)
|
||||
RequireAccepted(session.Teleport(new YmirTeleportBodyCommand(
|
||||
Header(session, frameId, simulationStepIndex, "direction", body.Id, ref commandOrdinal),
|
||||
body.Id, present.Position, body.Direction ?? new Vec2(0, 1))));
|
||||
Header(session, frameId, simulationStepIndex, "pose", body.Id, ref commandOrdinal),
|
||||
body.Id, body.Position, body.Direction ?? new Vec2(0, 1))));
|
||||
|
||||
if (!body.IsStatic && (present.Velocity != body.Velocity || present.AngularVelocity != body.AngularVelocity))
|
||||
RequireAccepted(session.SetVelocity(new YmirSetBodyVelocityCommand(
|
||||
|
|
@ -475,32 +429,7 @@ public sealed class AetheriaYmirWorldPhysics : IAetheriaRuntimeWorldPhysics, IDi
|
|||
PositionZ = body.Position.Y,
|
||||
VelocityX = body.Velocity.X,
|
||||
VelocityZ = body.Velocity.Y
|
||||
}).ToArray(),
|
||||
step.ContactFacts
|
||||
.Where(fact => fact.Kind == YmirContactFactKind.Begin &&
|
||||
(fact.BodyA.StartsWith(PickupPrefix, StringComparison.Ordinal) ||
|
||||
fact.BodyB.StartsWith(PickupPrefix, StringComparison.Ordinal)))
|
||||
.Select(fact =>
|
||||
{
|
||||
entitiesByBodyId.TryGetValue(fact.BodyA, out var entityA);
|
||||
entitiesByBodyId.TryGetValue(fact.BodyB, out var entityB);
|
||||
return new AetheriaRuntimeWorldBeginContact
|
||||
{
|
||||
FactId = fact.FactId,
|
||||
EntityAId = entityA?.EntityId ?? "",
|
||||
EntityBId = entityB?.EntityId ?? "",
|
||||
EntityAIndex = entityA?.EntityIndex ?? -1,
|
||||
EntityBIndex = entityB?.EntityIndex ?? -1,
|
||||
PickupIndex = Math.Max(ParseIndex(fact.BodyA, PickupPrefix), ParseIndex(fact.BodyB, PickupPrefix)),
|
||||
PointX = fact.Point?.X ?? 0,
|
||||
PointZ = fact.Point?.Y ?? 0,
|
||||
NormalX = fact.Normal?.X ?? 0,
|
||||
NormalZ = fact.Normal?.Y ?? 0,
|
||||
RelativeSpeed = fact.RelativeSpeed ?? 0
|
||||
};
|
||||
})
|
||||
.Where(contact => contact.PickupIndex >= 0 &&
|
||||
(contact.EntityAIndex >= 0 || contact.EntityBIndex >= 0))
|
||||
.ToArray());
|
||||
}
|
||||
|
||||
|
|
@ -544,7 +473,8 @@ public sealed class AetheriaYmirWorldPhysics : IAetheriaRuntimeWorldPhysics, IDi
|
|||
// The fossil moves entities directly and has no rigidbody collision response
|
||||
// between ships/stations. Keep their Ymir bodies for fields and queries, but
|
||||
// do not let invented circle radii hold a ship outside the 25-unit dock rule.
|
||||
// Pickups retain group zero, so ship/pickup Begin contacts still own collection.
|
||||
// Pickups retain group zero so Ymir can resolve their physical motion against ships.
|
||||
// The daemon independently owns collection through post-step XZ proximity.
|
||||
CollisionGroupIndex: -1);
|
||||
}).Concat(pickups.Select(pickup => new PhysicsBody(
|
||||
PickupPrefix + pickup.PickupIndex,
|
||||
|
|
|
|||
|
|
@ -632,7 +632,7 @@ Console.WriteLine("Inventory entity rename authority: UI sends daemon operations
|
|||
Console.WriteLine("Weapon group authority: UI sends daemon operations for assignments instead of checkpoint rewrites");
|
||||
Console.WriteLine("Action-bar binding authority: Unity owns local input bindings; activations send daemon operations");
|
||||
Console.WriteLine("Inventory transfer authority: UI transfer and drag/drop send daemon operations");
|
||||
Console.WriteLine("Loot pickup authority: daemon cargo collection occurs exactly once from an authenticated Ymir contact fact");
|
||||
Console.WriteLine("Loot pickup authority: daemon cargo collection occurs exactly once from ship/pickup XZ proximity");
|
||||
Console.WriteLine("Entity destruction authority: lethal daemon simulation commits exactly-once destruction and loot; clients cannot request erasure");
|
||||
Console.WriteLine("Dropped pickup state: daemon frames carry typed dropped-pickup snapshots and keyed live lowering");
|
||||
Console.WriteLine("Trade purchase authority: UI requests item and placement; daemon derives price, station, product kind, capacity, and ship creation");
|
||||
|
|
|
|||
|
|
@ -2481,8 +2481,16 @@ public class DaemonRuntimeDocumentTests
|
|||
}
|
||||
|
||||
[Test]
|
||||
public void TractorProximityCannotCollectCargoWithoutYmirContact()
|
||||
public void ShipProximityCollectsCargoWithoutTractorOrYmirContact()
|
||||
{
|
||||
var salvage = CatalogItem("raider-salvage", Array.Empty<AetheriaRuntimeBehaviorPayload>());
|
||||
salvage.Volume = 1;
|
||||
var cargoBay = CatalogItem("pickup-cargo-bay", Array.Empty<AetheriaRuntimeBehaviorPayload>());
|
||||
cargoBay.InteriorOccupiedCells = 1;
|
||||
var catalog = new AetheriaRuntimeCatalogSnapshot(
|
||||
new[] { salvage, cargoBay },
|
||||
Array.Empty<AetheriaRuntimeCorporation>(),
|
||||
Array.Empty<AetheriaRuntimeNameFile>());
|
||||
var pilot = new AetheriaRuntimeEntitySnapshotCommit
|
||||
{
|
||||
EntityIndex = 1,
|
||||
|
|
@ -2490,7 +2498,14 @@ public class DaemonRuntimeDocumentTests
|
|||
FactionKey = "player",
|
||||
IsActive = true,
|
||||
TargetEntityIndex = 6,
|
||||
TractorPower = 1,
|
||||
TractorPower = 0,
|
||||
CargoBays = new[]
|
||||
{
|
||||
new AetheriaRuntimeLoadoutItemSlotCommit
|
||||
{
|
||||
Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = cargoBay.ItemKey, Quantity = 1 }
|
||||
}
|
||||
},
|
||||
CargoContents = new[] { new AetheriaRuntimeCargoBayLoadoutCommit() }
|
||||
};
|
||||
var pickup = new AetheriaRuntimeDroppedPickupCommit
|
||||
|
|
@ -2521,11 +2536,12 @@ public class DaemonRuntimeDocumentTests
|
|||
new AetheriaRuntimeDaemonIntentState(),
|
||||
0.1,
|
||||
AetheriaRuntimeDaemonSimulationSettings.AetheriaDefault,
|
||||
new PassthroughWorldPhysics());
|
||||
new PassthroughWorldPhysics(),
|
||||
catalog);
|
||||
|
||||
Assert.IsFalse(pilot.CargoContents.SelectMany(bay => bay.Items).Any(slot => slot.Item.ItemKey == "raider-salvage"));
|
||||
Assert.AreEqual(1, run.Zones[0].DroppedPickups.Count,
|
||||
"targeting, tractor power, and proximity must not replace a Ymir Begin contact fact");
|
||||
Assert.IsTrue(pilot.CargoContents.SelectMany(bay => bay.Items).Any(slot => slot.Item.ItemKey == "raider-salvage"));
|
||||
Assert.AreEqual(0, run.Zones[0].DroppedPickups.Count,
|
||||
"daemon XZ proximity must own collection without tractor power or a Ymir contact fact");
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
@ -6634,12 +6650,6 @@ public class DaemonRuntimeDocumentTests
|
|||
|
||||
public void RetainWorlds(string runId, IReadOnlyList<int> zoneIndices) { }
|
||||
|
||||
public AetheriaRuntimeWorldPickupStep ApplyPickupRejection(
|
||||
string runId,
|
||||
int zoneIndex,
|
||||
AetheriaRuntimeWorldBeginContact contact) =>
|
||||
new() { PickupIndex = contact.PickupIndex };
|
||||
|
||||
public AetheriaRuntimeWorldStep Step(string runId, long frameId, int simulationStepIndex,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities, double deltaSeconds)
|
||||
|
|
@ -6651,7 +6661,7 @@ public class DaemonRuntimeDocumentTests
|
|||
VelocityX = entity.VelocityX, VelocityY = entity.VelocityY,
|
||||
DirectionX = entity.DirectionX, DirectionY = entity.DirectionY
|
||||
}).ToArray(),
|
||||
Array.Empty<AetheriaRuntimeWorldPickupStep>(), Array.Empty<AetheriaRuntimeWorldBeginContact>());
|
||||
Array.Empty<AetheriaRuntimeWorldPickupStep>());
|
||||
}
|
||||
|
||||
public AetheriaRuntimePhysicalPayloadStep StepPhysicalPayloads(
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
SetItemEnabled,
|
||||
SetItemOverrideShutdown,
|
||||
SetThermotoggleTargetTemperature,
|
||||
PickUpLoot, // Reserved serialized slot. Collection is owned by daemon interpretation of Ymir contacts.
|
||||
PickUpLoot, // Reserved serialized slot. Collection is owned by daemon ship/pickup proximity.
|
||||
RestoreLoadout,
|
||||
Dock,
|
||||
DockNearest,
|
||||
|
|
|
|||
|
|
@ -96,8 +96,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
var worldStep = StepWorldPhysics(
|
||||
run.RunId, frameId, simulationStepIndex, zone, entities, deltaSeconds, worldPhysics);
|
||||
ProjectEntityTerrainHeights(zone, entities, catalog, simulationTimeSeconds);
|
||||
ResolvePickupContacts(
|
||||
run, zone, entities, worldStep.BeginContacts, worldPhysics, catalog, frameId);
|
||||
ResolvePickupProximity(run, zone, entities, catalog, frameId);
|
||||
StepCombat(run, zone, entities, intents, deltaSeconds, settings, worldPhysics, catalog,
|
||||
frameId, simulationTimeSeconds, simulationStepIndex);
|
||||
AetheriaRuntimeVisibilitySimulation.StepZone(zone, entities, catalog, renderSettings);
|
||||
|
|
@ -432,91 +431,121 @@ namespace GameCult.Aetheria.State.Verse
|
|||
entity.PositionY = Resolve(entity, new HashSet<int>());
|
||||
}
|
||||
|
||||
private static void ResolvePickupContacts(
|
||||
private static void ResolvePickupProximity(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
IReadOnlyList<AetheriaRuntimeEntitySnapshotCommit> entities,
|
||||
IReadOnlyList<AetheriaRuntimeWorldBeginContact> beginContacts,
|
||||
IAetheriaRuntimeWorldPhysics worldPhysics,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog,
|
||||
long frameId)
|
||||
{
|
||||
var entitiesByIndex = entities
|
||||
.Where(value => value != null && value.IsActive)
|
||||
.ToDictionary(value => value.EntityIndex);
|
||||
foreach (var contact in (beginContacts ?? Array.Empty<AetheriaRuntimeWorldBeginContact>())
|
||||
.Where(value => value != null && value.PickupIndex >= 0 &&
|
||||
!string.IsNullOrWhiteSpace(value.FactId)))
|
||||
var attached = (entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
.Where(value => value != null)
|
||||
.SelectMany(value => value.ChildEntityIndices ?? Array.Empty<int>())
|
||||
.ToHashSet();
|
||||
var ships = (entities ?? Array.Empty<AetheriaRuntimeEntitySnapshotCommit>())
|
||||
.Where(value => value != null && value.IsActive && value.WormholeTransition == null &&
|
||||
!attached.Contains(value.EntityIndex) && IsPickupCollector(value, catalog))
|
||||
.Select(value => value!)
|
||||
.OrderBy(value => value.EntityIndex)
|
||||
.ToArray();
|
||||
var collectionDistanceSquared = AetheriaRuntimeTractorMechanics.CollectionDistance *
|
||||
AetheriaRuntimeTractorMechanics.CollectionDistance;
|
||||
|
||||
foreach (var pickup in (zone.DroppedPickups ?? Array.Empty<AetheriaRuntimeDroppedPickupCommit>())
|
||||
.Where(value => value != null && value.Item != null &&
|
||||
value.AgeSeconds < value.LifetimeSeconds)
|
||||
.Select(value => value!)
|
||||
.OrderBy(value => value.PickupIndex)
|
||||
.ToArray())
|
||||
{
|
||||
var entityIndex = contact.EntityAIndex >= 0 ? contact.EntityAIndex : contact.EntityBIndex;
|
||||
var contactEntityId = contact.EntityAIndex >= 0 ? contact.EntityAId : contact.EntityBId;
|
||||
entitiesByIndex.TryGetValue(entityIndex, out var entity);
|
||||
if (entity != null && !string.Equals(contactEntityId, entity.EntityId, StringComparison.Ordinal))
|
||||
throw new InvalidOperationException(
|
||||
$"Ymir fact '{contact.FactId}' entity identity '{contactEntityId}' does not match " +
|
||||
$"live entity {entityIndex} identity '{entity.EntityId}'.");
|
||||
var entityId = entity?.EntityId ?? contactEntityId;
|
||||
var priorReceipt = AetheriaRuntimePickupContactReceipts.Find(run, contact.FactId);
|
||||
if (priorReceipt != null)
|
||||
{
|
||||
if (priorReceipt.ZoneIndex != zone.ZoneIndex ||
|
||||
!string.Equals(priorReceipt.EntityId, entityId, StringComparison.Ordinal) ||
|
||||
priorReceipt.PickupIndex != contact.PickupIndex)
|
||||
throw new InvalidOperationException(
|
||||
$"Ymir fact id '{contact.FactId}' was reused for a different pickup contact.");
|
||||
var entity = ships
|
||||
.Select(value => new
|
||||
{
|
||||
Entity = value,
|
||||
DistanceSquared = DistanceSquared(value.PositionX, value.PositionZ,
|
||||
pickup.PositionX, pickup.PositionZ)
|
||||
})
|
||||
.Where(value => value.DistanceSquared <= collectionDistanceSquared)
|
||||
.OrderBy(value => value.DistanceSquared)
|
||||
.ThenBy(value => value.Entity.EntityIndex)
|
||||
.Select(value => value.Entity)
|
||||
.FirstOrDefault();
|
||||
if (entity == null)
|
||||
continue;
|
||||
}
|
||||
var pickup = (zone.DroppedPickups ?? Array.Empty<AetheriaRuntimeDroppedPickupCommit>())
|
||||
.FirstOrDefault(value => value != null && value.PickupIndex == contact.PickupIndex);
|
||||
var itemKey = pickup?.Item?.ItemKey ?? "";
|
||||
var quantity = Math.Max(1, pickup?.Item?.Quantity ?? 1);
|
||||
|
||||
var itemKey = pickup.Item.ItemKey ?? "";
|
||||
var quantity = Math.Max(1, pickup.Item.Quantity);
|
||||
var cargoQuantityBefore = AetheriaRuntimeCargoCapacityQueries.Quantity(entity);
|
||||
var result = entity == null
|
||||
? AetheriaRuntimePickupContactResult.Ignored
|
||||
: AetheriaRuntimePickupTransactions.ApplyContact(zone, entity, contact, catalog);
|
||||
var kind = result == AetheriaRuntimePickupContactResult.Collected
|
||||
var result = AetheriaRuntimePickupTransactions.ApplyProximity(
|
||||
zone, entity, pickup.PickupIndex, catalog);
|
||||
var kind = result == AetheriaRuntimePickupProximityResult.Collected
|
||||
? "pickup.collected"
|
||||
: result == AetheriaRuntimePickupContactResult.RejectedCapacity
|
||||
: result == AetheriaRuntimePickupProximityResult.RejectedCapacity
|
||||
? "pickup.rejected"
|
||||
: "pickup.ignored";
|
||||
if (result == AetheriaRuntimePickupContactResult.RejectedCapacity && pickup != null)
|
||||
{
|
||||
var rejected = worldPhysics.ApplyPickupRejection(run.RunId, zone.ZoneIndex, contact);
|
||||
pickup.PositionX = rejected.PositionX;
|
||||
pickup.PositionZ = rejected.PositionZ;
|
||||
pickup.VelocityX = rejected.VelocityX;
|
||||
pickup.VelocityZ = rejected.VelocityZ;
|
||||
}
|
||||
AetheriaRuntimePickupContactReceipts.Append(run, new AetheriaRuntimePickupContactReceiptCommit
|
||||
{
|
||||
FactId = contact.FactId,
|
||||
FrameId = frameId,
|
||||
ZoneIndex = zone.ZoneIndex,
|
||||
EntityIndex = entityIndex,
|
||||
EntityId = entityId,
|
||||
PickupIndex = contact.PickupIndex,
|
||||
Outcome = kind
|
||||
});
|
||||
if (result == AetheriaRuntimePickupContactResult.Ignored)
|
||||
if (result == AetheriaRuntimePickupProximityResult.RejectedCapacity)
|
||||
RejectPickupFromShip(pickup, entity);
|
||||
if (result == AetheriaRuntimePickupProximityResult.Ignored)
|
||||
continue;
|
||||
var eventId = result == AetheriaRuntimePickupProximityResult.Collected
|
||||
? $"zone:{zone.ZoneIndex}:pickup:{pickup.PickupIndex}:collected"
|
||||
: $"frame:{frameId}:zone:{zone.ZoneIndex}:pickup:{pickup.PickupIndex}:rejected:entity:{entity.EntityId}";
|
||||
AetheriaRuntimeGameEvents.Append(run, new AetheriaRuntimeGameEventCommit
|
||||
{
|
||||
EventId = $"ymir-fact:{contact.FactId}:{kind}",
|
||||
EventId = eventId,
|
||||
Kind = kind,
|
||||
FrameId = frameId,
|
||||
ZoneIndex = zone.ZoneIndex,
|
||||
TargetEntityIndex = entityIndex,
|
||||
PickupIndex = contact.PickupIndex,
|
||||
TargetEntityIndex = entity.EntityIndex,
|
||||
PickupIndex = pickup.PickupIndex,
|
||||
ItemKey = itemKey,
|
||||
ScalarValue = quantity,
|
||||
AuxiliaryValue = cargoQuantityBefore,
|
||||
Reason = result == AetheriaRuntimePickupContactResult.RejectedCapacity
|
||||
Reason = result == AetheriaRuntimePickupProximityResult.RejectedCapacity
|
||||
? "cargo-capacity"
|
||||
: ""
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsPickupCollector(
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
var hullType = catalog?.FindItem(entity.HullItemKey ?? "")?.HullType ?? "";
|
||||
return string.Equals(entity.Kind, "ship", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(hullType, "Ship", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static void RejectPickupFromShip(
|
||||
AetheriaRuntimeDroppedPickupCommit pickup,
|
||||
AetheriaRuntimeEntitySnapshotCommit entity)
|
||||
{
|
||||
var dx = pickup.PositionX - entity.PositionX;
|
||||
var dz = pickup.PositionZ - entity.PositionZ;
|
||||
var length = Math.Sqrt(dx * dx + dz * dz);
|
||||
if (length <= double.Epsilon)
|
||||
{
|
||||
dx = 1;
|
||||
dz = 0;
|
||||
length = 1;
|
||||
}
|
||||
var nx = dx / length;
|
||||
var nz = dz / length;
|
||||
var rejectedDistance = AetheriaRuntimeTractorMechanics.CollectionDistance + 0.001;
|
||||
pickup.PositionX = entity.PositionX + nx * rejectedDistance;
|
||||
pickup.PositionZ = entity.PositionZ + nz * rejectedDistance;
|
||||
pickup.VelocityX += nx * AetheriaRuntimeTractorMechanics.RejectionKick;
|
||||
pickup.VelocityZ += nz * AetheriaRuntimeTractorMechanics.RejectionKick;
|
||||
}
|
||||
|
||||
private static double DistanceSquared(double ax, double az, double bx, double bz)
|
||||
{
|
||||
var dx = ax - bx;
|
||||
var dz = az - bz;
|
||||
return dx * dx + dz * dz;
|
||||
}
|
||||
|
||||
private static void StepCombat(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
public static class AetheriaRuntimePickupContactReceipts
|
||||
{
|
||||
public static bool Contains(AetheriaRuntimeRunCheckpointCommit run, string factId)
|
||||
{
|
||||
return Find(run, factId) != null;
|
||||
}
|
||||
|
||||
public static AetheriaRuntimePickupContactReceiptCommit? Find(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
string factId) =>
|
||||
run == null || string.IsNullOrWhiteSpace(factId)
|
||||
? null
|
||||
: (run.PickupContactReceipts ?? Array.Empty<AetheriaRuntimePickupContactReceiptCommit>())
|
||||
.FirstOrDefault(value => value != null &&
|
||||
string.Equals(value.FactId, factId, StringComparison.Ordinal));
|
||||
|
||||
public static void Append(
|
||||
AetheriaRuntimeRunCheckpointCommit run,
|
||||
AetheriaRuntimePickupContactReceiptCommit receipt)
|
||||
{
|
||||
if (run == null || receipt == null || string.IsNullOrWhiteSpace(receipt.FactId))
|
||||
throw new ArgumentException("Pickup contact receipts require a Ymir fact id.", nameof(receipt));
|
||||
if (Contains(run, receipt.FactId))
|
||||
return;
|
||||
|
||||
run.PickupContactReceipts =
|
||||
(run.PickupContactReceipts ?? Array.Empty<AetheriaRuntimePickupContactReceiptCommit>())
|
||||
.Append(receipt)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 54a8cbe4b7d84d35955c2084fe0dd9e4
|
||||
|
|
@ -5,7 +5,7 @@ using System.Linq;
|
|||
|
||||
namespace GameCult.Aetheria.State.Verse
|
||||
{
|
||||
internal enum AetheriaRuntimePickupContactResult
|
||||
internal enum AetheriaRuntimePickupProximityResult
|
||||
{
|
||||
Ignored,
|
||||
Collected,
|
||||
|
|
@ -14,25 +14,23 @@ namespace GameCult.Aetheria.State.Verse
|
|||
|
||||
internal static class AetheriaRuntimePickupTransactions
|
||||
{
|
||||
public static AetheriaRuntimePickupContactResult ApplyContact(
|
||||
public static AetheriaRuntimePickupProximityResult ApplyProximity(
|
||||
AetheriaRuntimeZoneSnapshotCommit zone,
|
||||
AetheriaRuntimeEntitySnapshotCommit entity,
|
||||
AetheriaRuntimeWorldBeginContact contact,
|
||||
int pickupIndex,
|
||||
AetheriaRuntimeCatalogSnapshot? catalog)
|
||||
{
|
||||
if (zone == null || entity == null || contact == null || !entity.IsActive ||
|
||||
contact.PickupIndex < 0 ||
|
||||
(contact.EntityAIndex != entity.EntityIndex && contact.EntityBIndex != entity.EntityIndex))
|
||||
return AetheriaRuntimePickupContactResult.Ignored;
|
||||
if (zone == null || entity == null || !entity.IsActive || pickupIndex < 0)
|
||||
return AetheriaRuntimePickupProximityResult.Ignored;
|
||||
|
||||
var pickups = (zone.DroppedPickups ?? Array.Empty<AetheriaRuntimeDroppedPickupCommit>()).ToList();
|
||||
var index = pickups.FindIndex(pickup => pickup != null && pickup.PickupIndex == contact.PickupIndex);
|
||||
if (index < 0) return AetheriaRuntimePickupContactResult.Ignored;
|
||||
var index = pickups.FindIndex(pickup => pickup != null && pickup.PickupIndex == pickupIndex);
|
||||
if (index < 0) return AetheriaRuntimePickupProximityResult.Ignored;
|
||||
var pickup = pickups[index];
|
||||
var quantity = Math.Max(1, pickup.Item?.Quantity ?? 1);
|
||||
if (pickup.AgeSeconds >= pickup.LifetimeSeconds ||
|
||||
pickup.Item == null)
|
||||
return AetheriaRuntimePickupContactResult.Ignored;
|
||||
return AetheriaRuntimePickupProximityResult.Ignored;
|
||||
var cargoBayCount = entity.CargoBays?.Count ?? 0;
|
||||
var cargoIndex = Enumerable.Range(0, cargoBayCount)
|
||||
.Where(index => quantity <= AetheriaRuntimeCargoCapacityQueries.UnitsThatFit(
|
||||
|
|
@ -40,7 +38,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
.DefaultIfEmpty(-1)
|
||||
.First();
|
||||
if (cargoIndex < 0)
|
||||
return AetheriaRuntimePickupContactResult.RejectedCapacity;
|
||||
return AetheriaRuntimePickupProximityResult.RejectedCapacity;
|
||||
var bays = (entity.CargoContents ?? Array.Empty<AetheriaRuntimeCargoBayLoadoutCommit>()).ToList();
|
||||
while (bays.Count <= cargoIndex) bays.Add(new AetheriaRuntimeCargoBayLoadoutCommit());
|
||||
var slots = (bays[cargoIndex].Items ?? Array.Empty<AetheriaRuntimeLoadoutItemSlotCommit>()).ToList();
|
||||
|
|
@ -49,7 +47,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
else slots.Add(new AetheriaRuntimeLoadoutItemSlotCommit { Item = pickup.Item });
|
||||
bays[cargoIndex].Items = slots; entity.CargoContents = bays;
|
||||
pickups.RemoveAt(index); zone.DroppedPickups = pickups;
|
||||
return AetheriaRuntimePickupContactResult.Collected;
|
||||
return AetheriaRuntimePickupProximityResult.Collected;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -215,6 +215,8 @@ namespace GameCult.Aetheria.State.Verse
|
|||
Array.Empty<AetheriaRuntimeShotReceiptCommit>();
|
||||
|
||||
[Key(16)]
|
||||
// Reserved for checkpoint compatibility with the retired contact-gated loot path.
|
||||
// Proximity collection never reads or writes this history.
|
||||
public IReadOnlyList<AetheriaRuntimePickupContactReceiptCommit> PickupContactReceipts { get; set; } =
|
||||
Array.Empty<AetheriaRuntimePickupContactReceiptCommit>();
|
||||
|
||||
|
|
@ -564,6 +566,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
}
|
||||
|
||||
[MessagePackObject]
|
||||
// Serialized legacy row retained only so older checkpoints remain readable.
|
||||
public sealed class AetheriaRuntimePickupContactReceiptCommit
|
||||
{
|
||||
[Key(0)] public string FactId { get; set; } = "";
|
||||
|
|
|
|||
|
|
@ -7,5 +7,7 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public const double Radius = 25;
|
||||
public const double Traction = 25;
|
||||
public const double Distance = 75;
|
||||
public const double CollectionDistance = 25;
|
||||
public const double RejectionKick = 25;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,10 +9,6 @@ namespace GameCult.Aetheria.State.Verse
|
|||
{
|
||||
string ImplementationId { get; }
|
||||
void RetainWorlds(string runId, IReadOnlyList<int> zoneIndices);
|
||||
AetheriaRuntimeWorldPickupStep ApplyPickupRejection(
|
||||
string runId,
|
||||
int zoneIndex,
|
||||
AetheriaRuntimeWorldBeginContact contact);
|
||||
AetheriaRuntimeWorldStep Step(
|
||||
string runId,
|
||||
long frameId,
|
||||
|
|
@ -33,16 +29,13 @@ namespace GameCult.Aetheria.State.Verse
|
|||
{
|
||||
public AetheriaRuntimeWorldStep(
|
||||
IReadOnlyList<AetheriaRuntimeWorldBodyStep> bodies,
|
||||
IReadOnlyList<AetheriaRuntimeWorldPickupStep> pickups,
|
||||
IReadOnlyList<AetheriaRuntimeWorldBeginContact> beginContacts)
|
||||
IReadOnlyList<AetheriaRuntimeWorldPickupStep> pickups)
|
||||
{
|
||||
Bodies = bodies ?? Array.Empty<AetheriaRuntimeWorldBodyStep>();
|
||||
Pickups = pickups ?? Array.Empty<AetheriaRuntimeWorldPickupStep>();
|
||||
BeginContacts = beginContacts ?? Array.Empty<AetheriaRuntimeWorldBeginContact>();
|
||||
}
|
||||
public IReadOnlyList<AetheriaRuntimeWorldBodyStep> Bodies { get; }
|
||||
public IReadOnlyList<AetheriaRuntimeWorldPickupStep> Pickups { get; }
|
||||
public IReadOnlyList<AetheriaRuntimeWorldBeginContact> BeginContacts { get; }
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeWorldPickupStep
|
||||
|
|
@ -65,21 +58,6 @@ namespace GameCult.Aetheria.State.Verse
|
|||
public double DirectionY { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimeWorldBeginContact
|
||||
{
|
||||
public string FactId { get; set; } = "";
|
||||
public string EntityAId { get; set; } = "";
|
||||
public string EntityBId { get; set; } = "";
|
||||
public int EntityAIndex { get; set; } = -1;
|
||||
public int EntityBIndex { get; set; } = -1;
|
||||
public int PickupIndex { get; set; } = -1;
|
||||
public double PointX { get; set; }
|
||||
public double PointZ { get; set; }
|
||||
public double NormalX { get; set; }
|
||||
public double NormalZ { get; set; }
|
||||
public double RelativeSpeed { get; set; }
|
||||
}
|
||||
|
||||
public sealed class AetheriaRuntimePhysicalPayloadStep
|
||||
{
|
||||
public AetheriaRuntimePhysicalPayloadStep(
|
||||
|
|
|
|||
|
|
@ -175,10 +175,11 @@ Current Ymir control flow:
|
|||
marker. Startup restores the exact `(RunId, FrameId, ZoneIndex)` records
|
||||
before the client CultMesh host starts, and fails closed on incomplete active
|
||||
persistence.
|
||||
- Cargo collection accepts only typed Ymir `Begin` facts. Aetheria persists one
|
||||
pickup-contact receipt per Ymir `FactId` before exposing the resulting event,
|
||||
so duplicate fact delivery cannot add cargo twice. Proximity and client loot
|
||||
commands do not collect cargo.
|
||||
- Cargo collection runs after Ymir motion and accepts only live pickups within
|
||||
the fixed XZ distance of an active, undocked ship. The daemon removes the
|
||||
durable pickup row in the same transaction that inserts cargo, so restart or
|
||||
repeated proximity evaluation cannot add it twice. Ymir contact facts and
|
||||
client loot commands do not collect cargo.
|
||||
- Ordinary direct, constant, and charged weapon fire resolves through
|
||||
deterministic shot receipts. It does not create a Ymir body or derive damage
|
||||
from collision.
|
||||
|
|
|
|||
|
|
@ -1311,36 +1311,38 @@ the Terminus proof arena cannot award completion.
|
|||
proves every topology zone has bodies/orbits, every zone has a stellar
|
||||
primary, and the tutorial retains asteroid belts.
|
||||
|
||||
### Daemon Pickup Contact Transaction
|
||||
### Daemon Pickup Proximity Transaction
|
||||
|
||||
- Owner: the daemon simulation owns pickup collection by interpreting typed
|
||||
entity-pickup contact facts returned by embedded Ymir physics.
|
||||
- Inputs: the exact Ymir contact, canonical active entity and live pickup,
|
||||
catalog-backed cargo capacity, and current frame identity. Tractor power and
|
||||
range influence pickup motion only.
|
||||
- Owner: the daemon simulation owns pickup collection by comparing canonical
|
||||
active-ship and live-pickup XZ positions after the Ymir motion step.
|
||||
- Inputs: stable ship and pickup identities, their daemon-owned XZ positions,
|
||||
the fixed 25-unit collection distance, catalog-backed cargo capacity, and
|
||||
current frame identity. Tractor power influences pickup motion only.
|
||||
- Outputs: capacity permits one atomic cargo insertion, pickup removal, and
|
||||
`pickup.collected` event. Capacity refusal retains the pickup, applies the
|
||||
fossil 25-unit outward kick along the contact normal, and emits
|
||||
fossil 25-unit outward kick along the ship-to-pickup direction, moves the
|
||||
pickup just outside collection distance, and emits
|
||||
`pickup.rejected`.
|
||||
- Derived state: pickup transforms, tractor visuals, Eve world nodes, Unity
|
||||
colliders, and client selection are presentation or control projections and
|
||||
cannot admit collection.
|
||||
- Forbidden writers: the reserved `PickUpLoot` command, proximity/range scans,
|
||||
- Forbidden writers: the reserved `PickUpLoot` command, Ymir contact facts,
|
||||
Unity collision callbacks, and generic Eve lowerers cannot mutate cargo or
|
||||
despawn pickups.
|
||||
- Shared paths: destruction drops, generated pickups, and restored live pickup
|
||||
rows all enter the same next-tick Ymir body/contact path. Duplicate contact
|
||||
facts for one entity-pickup pair in a frame reduce once; a collected pickup
|
||||
identity cannot commit again.
|
||||
- Cut line: the command executor/builders and distance-based collection helper
|
||||
are deleted. The old enum ordinal remains reserved only to avoid shifting
|
||||
serialized command values and is rejected by the daemon.
|
||||
- Verification layer: daemon/Ymir smoke proves one commit per consumed fact,
|
||||
duplicate-fact suppression, collection, capacity refusal, and outward kick.
|
||||
The released EveUnity `0.3.63` warm rejection witness proves full player cargo
|
||||
remains `5 -> 5`, the pickup remains live, and distinct player recontacts
|
||||
arrive as distinct `ymir-fact:` feedback identities without a player
|
||||
collection event.
|
||||
rows all enter the same post-Ymir proximity pass. Pickups are ordered by
|
||||
stable index and choose the nearest eligible ship with entity-index tie
|
||||
breaking; removal of the durable pickup row makes a second commit impossible.
|
||||
A capacity-rejection displacement is committed to daemon world truth and the
|
||||
retained Ymir session reconciles that pose before its next integration step.
|
||||
- Cut line: the contact-receipt helper and pickup contact lowering are deleted.
|
||||
The old enum ordinal remains reserved only to avoid shifting serialized
|
||||
command values and is rejected by the daemon.
|
||||
- Verification layer: daemon smoke proves exact inside/outside boundary,
|
||||
tractor-independent collection, one commit per pickup identity, ship-only
|
||||
admission, capacity refusal, outward displacement/kick, and restart without
|
||||
resurrection or duplicate cargo. The prior released contact-gated witness is
|
||||
historical evidence only and must be rerun against the proximity owner.
|
||||
|
||||
### Daemon Gravity Field Contract
|
||||
|
||||
|
|
@ -2755,10 +2757,10 @@ First Aetheria surfaces to publish:
|
|||
button color, but the daemon owns `CurrentEntity`, `DockingBay.DockedShip`,
|
||||
and the typed checkpoint.
|
||||
- Done: delete client-owned loot pickup submission. Tractor power influences
|
||||
Ymir motion, but only an authenticated Ymir Begin contact fact can ask the
|
||||
daemon pickup transaction to mutate cargo or remove a pickup. Unity
|
||||
collision callbacks and the retired serialized command payload have no
|
||||
writer authority.
|
||||
Ymir motion; after that step the daemon alone compares live ship/pickup XZ
|
||||
positions and may mutate cargo or remove a pickup. Ymir contact facts,
|
||||
Unity collision callbacks, and the retired serialized command payload have
|
||||
no writer authority.
|
||||
- Done: route entity destruction through typed daemon operations.
|
||||
`EntityInstance` observes hull death and may spawn the local destruction
|
||||
effect, but the daemon owns equipment/cargo drop decisions, zone entity
|
||||
|
|
@ -3315,12 +3317,12 @@ instantiation, credits, dock assignment, current entity, and checkpoint. Docked
|
|||
current-ship selection follows the same rule: UI requests selection, and the
|
||||
daemon owns `CurrentEntity`, `DockingBay.DockedShip`, and checkpoint. Undocking
|
||||
preserves the fossil ship pose, velocity, and direction when the detached ship
|
||||
re-enters world physics. Loot pickup is contact-gated: the
|
||||
tractor applies force to loose bodies, Ymir owns typed Begin contact facts, and
|
||||
the daemon alone interprets each persisted fact identity into capacity
|
||||
validation, cargo storage or rejection bounce, pickup removal, feedback, and
|
||||
checkpoint. Targeting, proximity, client commands, and presentation collisions
|
||||
cannot decide inventory ownership. The portable Eve entity SoA publishes
|
||||
re-enters world physics. Loot pickup is proximity-gated: the tractor applies
|
||||
force to loose bodies through Ymir, then the daemon compares canonical XZ
|
||||
positions and owns capacity validation, cargo storage or rejection bounce,
|
||||
pickup removal, feedback, and checkpoint. Ymir contacts, targeting, client
|
||||
commands, and presentation collisions cannot decide inventory ownership. The
|
||||
portable Eve entity SoA publishes
|
||||
`inventory.cargo.quantity`, allowing generic clients and witnesses to observe
|
||||
the committed cargo delta without reading Aetheria internals. Entity destruction follows
|
||||
the same rule: instance code observes death, and the daemon owns drop
|
||||
|
|
@ -3434,8 +3436,8 @@ Docking ownership follows the fossil's entity and `ActionGameManager` path:
|
|||
first-eligible selection, weapon disarm, docked fire-command rejection,
|
||||
preserved pose/velocity/direction, exact receipt reason, exactly one accepted
|
||||
transition fact, no false event on rejected undock, Eve feedback projection,
|
||||
entity pass-through during home approach, and retained ship/pickup contact
|
||||
collection. A production-Ymir smoke advances an overlapping ship/station
|
||||
entity pass-through during home approach, and retained pickup motion followed
|
||||
by daemon proximity collection. A production-Ymir smoke advances an overlapping ship/station
|
||||
pair, proves the negative collision group leaves the ship at the authored
|
||||
24-unit pose, and then accepts docking through the ordinary daemon command.
|
||||
Simulation smoke also proves both docked attacker and docked target
|
||||
|
|
@ -3521,8 +3523,8 @@ Ymir restart ownership is deliberately private and asymmetric:
|
|||
- Cut line: the rejected design that embedded complete Ymir checkpoints in
|
||||
every public zone/frame is absent; no public persistence document types are
|
||||
registered.
|
||||
- Verification layer: daemon smoke tears down a live rejected-pickup contact,
|
||||
restores the exact durable frame into fresh Ymir sessions, and proves the
|
||||
next tick cannot duplicate its receipt, feedback event, or kick. Ymir unit
|
||||
- Verification layer: daemon smoke collects a live pickup, persists the public
|
||||
frame and private Ymir state, restores both into fresh sessions, and proves
|
||||
the next tick cannot resurrect the pickup or duplicate cargo or feedback. Ymir unit
|
||||
tests prove incremental suffix capture, exact chunk coverage, checksums, and
|
||||
replay verifier convergence.
|
||||
|
|
|
|||
|
|
@ -19,15 +19,15 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
|
|||
| Entity identity/loadout | `ServerShared/Entity*` | daemon | typed entity and inventory surfaces | EveUnity/Electron | daemon-owned hull identity, exact installed placement/rotation and instance state, cargo stacks, weapon membership/held state, equipment wear, executable behavior state, weapon chronology, active-effect state, and loadout-generation provenance survive a hard CultCache close/reopen through the durable latest-frame checkpoint. An advertised generic Eve equip operation now translates into the typed daemon transaction before that checkpoint; its accepted placement survives restart and its consumed station-cargo source does not resurrect. Generic Eve rebuilds weapon and scalar thermostat controls from that restored truth; legacy per-entity documents are bootstrap inputs only when no live frame exists | live client refit witness and multi-client convergence |
|
||||
| Loadout generation | map/faction availability, item cost, hull hardpoints and role constraints | daemon using canonical galaxy, faction knowledge/economy and typed catalog | generated loadout plus provenance/availability facts | Eve inspection and refit surfaces | daemon now uses the fossil candidate filter and seeded weighted selection: manufacturer must be present and allied, ally weight decays by home-zone graph distance, price uses the authored exponent, and hardpoints require matching semantic type, rotated footprint containment, and controller role. Prelude generation retains the fossil exception that allied catalog manufacturers need not own one of the six tutorial home zones; unknown manufacturers remain zero-weight. The April 2021 catalog fossil restores the distinct common `Longinus`, rare AetherDrive-compatible `LonginusX`, and `Djinni` rows with their original IDs, shapes, hardpoints, prices, drag, and prefabs, plus missing Thruster rows required to fill their authored mounts; typed `rarity:rare` tags apply a 0.05 generation multiplier to `LonginusX` and AetherDrive equipment; the generator owns one mutable CultMath stream, fixing the readonly-struct defensive-copy bug that restarted every weighted draw; placement rotation survives durable state and reconnect; generation fills the largest compatible footprint before economic weighting; cargo/capacitor plus station docking equipment must fit remaining hull space; stations pack faction-available inventory largest-first; ordinary ships begin without invented scenario cargo; every generated entity persists a source/selection receipt that reaches Eve as `loadout.item` semantics | multi-zone generated-world parity and exact root-to-zone stream sequencing against the fossil |
|
||||
| Flight intent | `Ship`, `Thruster`, `AetherDrive`, `VelocityConversion`, `VelocityLimit`, input paths | daemon actuator simulation + Ymir integration | analog local helm/look/tractor plus actuator state | generic Eve input and scene lowerers | direct faction-speed velocity ownership is deleted; daemon persists local strafe/forward helm and desired look, allocates ordinary directional thruster banks by authored rotation, derives torque from hull/item placement, compensates lateral torque, gates execution through the shared energy ledger, deposits equipment-cell heat, publishes plume visibility and actuator state, applies hull drag, then runs operational velocity conversion with the fossil exponential damp equation and clamps the result through evaluated velocity limits before giving velocity/direction to Ymir. The evaluated limit remains in durable behavior state for reconnect and Eve inspection. Autonomous tasks translate world travel into the same local helm axes and issue look commands instead of mutating heading. AetherDrive rotor/coupling simulation is additive and explicitly non-default: common `Longinus` and `Djinni` hulls use ordinary thrusters; the rare `LonginusX` replaces those mounts with the single rare `Traction` AetherDrive path | hardpoint-transform direction parity, exact cross-equipment behavior execution ordering, CultMath/float32 parity harness, live production-frequency evidence, generic thrust/turn feedback lowering, and live handling screenshots |
|
||||
| Collision | fossil entity code contains no ship/station collision gameplay callback; shield collision timing is presentation | Ymir | Ymir-owned contact facts for collidable pairs; immutable entity generations for presentation | visual interpolation only | Ymir world stepping owns entity poses, radial fields, ship/pickup Begin contacts, rejection kicks, and persistent physical-payload contacts. Ship and station bodies deliberately share one negative collision group: the fossil had no rigidbody entity response, and invented circle separation would make its 25-unit center-distance docking rule unreachable. Daemon smoke advances an overlapping ship/station pair, proves the ship remains at 24 units, then accepts docking. The released EveUnity scene sink consumes immutable presented generations and has no provider/command-transport dependency; the canonical Unity product contains no Aetheria or ServerShared gameplay assembly | upstream Box3D parity harness for any future solid-body pair and live interpolation timeline under packet delay |
|
||||
| Collision | fossil entity code contains no ship/station collision gameplay callback; shield collision timing is presentation | Ymir | Ymir-owned contact facts for collidable pairs; immutable entity generations for presentation | visual interpolation only | Ymir world stepping owns entity poses and radial fields. Loot collection and its capacity-rejection kick are daemon-owned XZ proximity consequences and do not consume contact facts. Ship and station bodies deliberately share one negative collision group: the fossil had no rigidbody entity response, and invented circle separation would make its 25-unit center-distance docking rule unreachable. Daemon smoke advances an overlapping ship/station pair, proves the ship remains at 24 units, then accepts docking. The released EveUnity scene sink consumes immutable presented generations and has no provider/command-transport dependency; the canonical Unity product contains no Aetheria or ServerShared gameplay assembly | upstream Box3D parity harness for any future solid-body pair and live interpolation timeline under packet delay |
|
||||
| Targeting/contacts | sensors + `ActionGameManager` | daemon | contact/target semantics | target HUD and indicators | daemon-owned selection now preserves the executable fossil exactly, including its oddly misnamed `Target Nearest` action selecting `MaxBy(distance)`, stable visible-contact tie order, and previous-from-no-selection choosing the second-to-last distance row when at least three contacts exist. Generic Eve nearest/next/previous/reticle/clear levers route to that owner; explicit scene targeting preserves both EveUnity's `sourceEntityId` and `targetEntityId` instead of falling back to the frame's current ship. Only daemon-visible contacts can be explicitly selected, and `target-not-visible` survives into the denied Eve receipt without disturbing the prior target. Every authored instant lock behavior evaluates on every daemon step independently of trigger/burst selection, so an idle weapon cannot retain a stale completed lock; acquisition and decay derive from facing, elapsed time, and observer-local contact confidence. Smoke proves progressive acquisition, exact target-transfer loss/start chronology, authored angular decay with one loss crossing, reacquisition without replaying start, invalid-target reset, and generic Eve projection of the prior/current lock values. `weapon.lock.started`, `weapon.lock.acquired`, and `weapon.lock.lost` publish stable one-time chronology with target, weapon identity, reason, and prior/current progress through the generic feedback stream | authored reticle/HUD parity; provider audio/effect binding is visual pull work |
|
||||
| Projectile flight | weapon/projectile classes | Eve presentation for ordinary tracers; Ymir for persistent physical payloads | receipt trajectory/effect facts or persistent payload state | native projectile visual | ordinary instant, charged, constant, GuidedWeapon, and Launcher execution creates no projectile body; each weapon state owns a stable shot sequence and receipts carry origin, endpoint, nominal duration, and effect identity for Eve presentation. Guided receipts additionally retain target-entity/look-direction mode, complete guidance/thrust/lift curves, evaluated thrust and top speed, and dodge frequency; these survive checkpoint restart and reach generic Eve shot facts. The fossil guided projectile only steered, split, faded, and selected hit visuals after the ordinary weapon event had committed damage, so the client path remains derived presentation rather than a Ymir gameplay body. `SpawnProjectile` and its alternate writer are deleted; Ymir projectile contacts are feedback-only for explicitly persisted payload rows and cannot write damage; Ymir.Core remains embedded for world integration, radial fields, circle contacts, pickups, and genuinely persistent/interceptable payloads | generic guided trajectory lowering, provider split/fade asset parameters, and removal of ordinary projectile terminology from runtime contracts |
|
||||
| Shield/armor/equipment/hull damage | energy-funded shield, schematic armor cells, equipment and scalar hull | daemon canonical damage transaction | aggregate/exact armor grid, entity status, and layer-aware shot/damage receipts | schematic, meters, impacts, damage VFX | owner-exists; wired; exclusive; projected: daemon initializes armor/maximum-armor grids from hull plus hardpoint catalog topology; direct and deployable damage share one `shield -> armor cell -> equipment -> scalar hull` transaction; direct impacts use the fossil's full-width/full-height normalized-UV scaling to select a cell or complete hardpoint footprint before orthogonal spread and contiguous 0.5-cell penetration that stops at the first hull gap, while splash payloads select the source-facing hull half; rotated equipment interception shares the refit/thermal/schematic footprint owner; the fossil penetration infinite-loop defect is fixed; damage type is typed/pass-through without resistances because the fossil never applied them; Eve exposes aggregate and exact armor state, and each direct-shot receipt owns stable per-cell coordinates plus armor/equipment/hull application for presentation; fake Ymir contacts and presentation cannot decide damage | authoritative authored collider-UV topology input, orthogonal-spread/depth fixtures, and generic shield/equipment effect lifecycle |
|
||||
| Thermal/energy | cell conduction, capacitor/reactor/radiator network, and cockpit thermal exposure | daemon | ship/equipment power, temperature, medical state, risk and death feedback | schematic/post effects | owner-exists; wired; exclusive; projected; generic timing and native asset delivery lowered: catalog hull and installed-item shapes derive cell mass/conductivity/topology; local temperature derives thermal performance, wear potential and online state; all consumers share capacitor-first/reactor-residual transactions; powered radiators pump before reactor settlement; reactor heat then enters the same cell network before conduction/radiation; cockpit-cell temperature drives the fossil nonlinear heatstroke/hypothermia accumulation, linear recovery, severe-risk crossings, and ordinary exactly-once destruction with a typed cause; Eve exposes cockpit, risk, heatstroke source-weight, phasing and death-transition facts; EveUnity generically resolves the original severe pulse plus live/reconnect-safe cause-specific death crossfade; Aetheria deterministically translates the five legacy profiles into URP profiles, persists every override as a real profile sub-asset, advertises them by semantic role through CultMesh CDN, and binds them in the canonical client through a weight-only native sink; rotated equipment topology, parent/child radiation, schematic composition, legacy SSR/AO/roundness equivalents, and live/reconnect death-fade parity remain incomplete | multi-consumer ordering, multi-radiator visibility, rotated topology, generic schematic meters, renderer-feature parity, and live/reconnect death-fade screenshots |
|
||||
| Weapons | ServerShared behaviors + Unity effects | daemon plus Ymir geometry/kinematics | action capability, weapon state, shot/damage receipts | effects/audio/HUD | instant and constant resource/timing lifecycles are daemon-owned; ordinary instant weapons remain press-edge driven, while authored `AutoWeapon` payloads use the persisted generic hold latch to retrigger on the fixed step after cooldown completion and stop structurally on release, including across CultCache restart. Charged weapons accept one semantic request, spool without requiring a solution, hold at full charge, commit automatically when a solution appears, and perform deterministic once-per-second malfunction hazard checks after the authored safe hold interval with increasing persisted risk; successful instant, automatic, constant, charged, deployable, and turret-controlled transactions publish authored transient visibility and consume the owning equipment's temperature-derived wear, with charged visibility applying its authored multiplier and turret wear remaining exact-weapon scoped; GuidedWeapon and Launcher retain ordinary authoritative shot resolution while publishing their complete fossil steering profile as derived presentation; generic Eve `weapon.state` exposes charge hold/risk and feedback exposes readiness, commit, refusal, and malfunction chronology; committed direct damage now enters the shared daemon armor-cell transaction | remaining impact-distribution parity, generic guided/effect lowering, and Unity effect reconciliation |
|
||||
| Destruction/drops | hull death, 25% equipment rolls, unconditional cargo drops, 25-unit launch, 30-second pickups | daemon | destruction identity, death/drop events, typed pickups | provider-advertised destruction effect and pickups | owner-exists; wired; exclusive; projected: direct and deployable lethal damage commit one deterministic destruction transaction, clear live references, retain only a non-rendered identity tombstone, and emit exact pickups/events; residual cell damage destroys installed equipment before hull, disables its behaviors immediately, and cockpit destruction kills with its fossil cause before scalar hull depletion; the reserved client `DestroyEntity` command is rejected; Aetheria advertises its original big explosion as `effect.feedback.entity.destroyed`, lowered generically by EveUnity | multiplayer reconnect chronology |
|
||||
| Tractor/scoop | analog tractor and physical timed pickups | daemon + Ymir | tractor power, pickup body and receipt | pickup world entity plus beam/interaction feedback | fossil 2/s power ramp and authored forward radius/traction/distance feed pickup bodies into the shared Ymir world; ship contact collects exactly once when capacity permits or applies the fossil 25-unit rejection kick; Aetheria maps pickup collision only against ship bodies, so stations and other world bodies cannot become cargo writers; 30s daemon lifetime; instant cargo teleport deleted; current-zone pickups occupy ordinary rows in the shared Eve entity SoA body with stable logical identity and provider-owned assets; `pilot.scoop` advertises a generic `button-hold.v1` scalar contract, the released client sends `1` on press and `0` on release through `SetTractorPower`, both receipts reconcile, and daemon power returns exactly to zero; the provider build removes the fossil's embedded tractor object from ship prefabs, and `beam.presentation` attaches the sole standalone effect to the SoA ship; released warm witnesses prove successful exactly-once Ymir collection and controlled-player `cargo-capacity` rejection with unchanged cargo | polish provider beam shape/attachment |
|
||||
| Dock/undock | wormhole-first interact; first eligible bay; component prerequisites | daemon + Ymir placement | dock commands/context | camera/UI transition | daemon docking now preserves the fossil transaction: stable zone order selects the first entity with a real empty bay inside the fixed 25-unit XZ center radius; undock checks installed Cockpit, Thruster-or-AetherDrive, Reactor, then empty bay cargo in source order; rejection reasons survive committed command facts into generic Eve receipts; success removes parentage without moving, stopping, or rotating the ship. Accepted transitions additionally publish retained `ship.docked` / `ship.undocked` feedback with exact actor, parent, bay, frame and position; rejected operations publish no false transition. Docking clears held-group and transient weapon state, docked actors reject later fire commands, and combat subtracts docked child identities from both attacker iteration and target lookup, so stale input, forged pulses, and turret controllers cannot fire through a bay. Ymir world entities retain gravity/query bodies but do not invent ship/station rigidbody collision response that would make the 25-unit rule unreachable; ship/pickup Begin contacts remain enabled. The live generic Unity witness also proves advertised actions, bay assignment, dock-parent SoA inclusion and sensor sharing, camera retargeting, controlled-subject hiding, movement suppression, and restoration on undock | reconnect, docked inventory/trade surfaces, and authored dock-camera blend/effect lowering |
|
||||
| Tractor/scoop | analog tractor and physical timed pickups | daemon + Ymir | tractor power, pickup body and receipt | pickup world entity plus beam/interaction feedback | fossil 2/s power ramp and authored forward radius/traction/distance feed pickup bodies into the shared Ymir world; after motion, the daemon collects the nearest live pickup within a fixed 25-unit XZ distance of an active undocked ship when capacity permits or moves it outside the boundary with the fossil 25-unit rejection kick; stable pickup removal makes collection exactly once and stations cannot become cargo writers; 30s daemon lifetime; instant cargo teleport deleted; current-zone pickups occupy ordinary rows in the shared Eve entity SoA body with stable logical identity and provider-owned assets; `pilot.scoop` advertises a generic `button-hold.v1` scalar contract, the released client sends `1` on press and `0` on release through `SetTractorPower`, both receipts reconcile, and daemon power returns exactly to zero; the provider build removes the fossil's embedded tractor object from ship prefabs, and `beam.presentation` attaches the sole standalone effect to the SoA ship | rerun released warm collection/rejection witness against proximity ownership; polish provider beam shape/attachment |
|
||||
| Dock/undock | wormhole-first interact; first eligible bay; component prerequisites | daemon + Ymir placement | dock commands/context | camera/UI transition | daemon docking now preserves the fossil transaction: stable zone order selects the first entity with a real empty bay inside the fixed 25-unit XZ center radius; undock checks installed Cockpit, Thruster-or-AetherDrive, Reactor, then empty bay cargo in source order; rejection reasons survive committed command facts into generic Eve receipts; success removes parentage without moving, stopping, or rotating the ship. Accepted transitions additionally publish retained `ship.docked` / `ship.undocked` feedback with exact actor, parent, bay, frame and position; rejected operations publish no false transition. Docking clears held-group and transient weapon state, docked actors reject later fire commands, and combat subtracts docked child identities from both attacker iteration and target lookup, so stale input, forged pulses, and turret controllers cannot fire through a bay. Ymir world entities retain gravity/query bodies but do not invent ship/station rigidbody collision response that would make the 25-unit rule unreachable. The live generic Unity witness also proves advertised actions, bay assignment, dock-parent SoA inclusion and sensor sharing, camera retargeting, controlled-subject hiding, movement suppression, and restoration on undock | reconnect, docked inventory/trade surfaces, and authored dock-camera blend/effect lowering |
|
||||
| Wormhole travel | `ActionGameManager.EnterWormhole` plus `Ship` enter/exit state | daemon fixed-step transition plus Ymir exclusion | current-entity transition state and feedback chronology | provider-selected generic transition lowering | command acceptance no longer moves an entity immediately; player interact, explicit travel, and agent routing derive the same canonical adjacent-zone wormhole and proximity gate, then persist four-second `entering`, authoritative transfer, four-second `exiting`, and one-frame `completed` phases. The daemon owns smootherstep XZ motion, the fossil 1000-unit visual-depth lever, deterministic 20-unit exit direction, 50-unit exit radius, target-adjacency discovery, and exactly-once start/transfer/completion events. Flight and every world-physics implementation receive no transitioning body; Ymir resumes on the tick after completion. An autonomous three-zone route now hard-flushes and closes CultCache during the first `entering` phase, reopens into a fresh run graph and Ymir owner, preserves the assigned task/carrier/phase, crosses both edges, emits exactly two start/transfer/completion triplets, and never clones the worker | generic provider transition effect/camera lowering and live multi-zone pilot witness |
|
||||
| Inventory/equipment | `Entity.ItemFits`, `TryEquip`, `TryUnequip`, `EquippedCargoBay.ItemFits`/`TryStore`, inventory panels + ServerShared | daemon atomic inventory transactions | typed inventory/equipment state, commands, exact receipts | generic UI | generated cargo and docking bays now occupy their typed collections instead of masquerading as ordinary equipment; docked equip/reposition/store validates the complete destination on cloned state before committing either side, uses authored hull interior and hardpoint geometry, forces hardpoint rotation, rejects occupied cells and deployed ships, preserves exact item instance fields, surviving equipment behavior/weapon state, and weapon-group membership, routes bay categories with aligned content/assignment arrays, and refuses to unequip nonempty/occupied bays. Cargo transfer now uses the same plan-first rule with typed bay interiors, rotated item footprints, deterministic free-cell search, exact stack maxima, multi-stack splitting, and no partial source/destination mutation on rejection; exact reasons reach generic Eve receipts. The generic Eve equip request-to-transaction-to-CultCache-restart path is covered, including source non-resurrection | live generic refit control lowering, reconnect after rejected moves, and full fossil inventory feedback parity |
|
||||
| Trade | trade UI/economy | daemon | stock, current value, purchase/sale commands | generic UI | daemon derives station, stock row, catalog item, current price, quantity, credits, target capacity, and hull-vs-cargo result; purchases ignore client opinions about price and destination kind. Commodity delivery now uses the same cloned spatial cargo plan as inventory transfer: it fills compatible partial stacks, splits the remainder at authored maximum stack size, finds deterministic free cells in the bay shape, and only then commits stock, cargo, and credits. The sequential daemon reducer serializes two client purchases for the same final stock in one frame: exactly one command debits/delivers, the other commits `trade-stock-unavailable`, and a hard CultCache close/reopen preserves exhausted stock, the single wallet debit, delivered cargo, and both generic Eve receipts. Docked sales identify only the source stack and quantity: the daemon derives station, instance-sensitive value, payout, deterministic typed stock placement, and commits cargo/credits atomically; generic Eve advertises available buy and sell actions. The fossil had no durable quote or expiry mechanic: its menu displayed current item value and mutated immediately, so Aetheria intentionally reprices at authoritative command execution rather than inventing quote authority. The generic Unity live witness receives an authoritative purchase receipt while paused | live sale receipt witness and full trade surface parity |
|
||||
|
|
@ -40,8 +40,8 @@ validation proves contract shape, not wiring, exclusivity, lowering, or parity.
|
|||
| HUD/schematic | ARPG HUD and `SchematicDisplay` | Eve composition from daemon facts | semantic status/components | EveUnity UI | provider-authored transparent cockpit is wired, projected, and lowered through generic UI Toolkit panes, metrics, and progress components for hull, shield, capacitor, temperature, cargo, weapon cooldown/groups, selected target, target lock, target shield, and target hull. The daemon now publishes the original spatial schematic semantics as a generic `ship.schematic` subtree for both self and selected target: explicit occupied hull cells with current/maximum armor, catalog-backed rotated equipment footprints, durability, thermal range/performance, online state, wear, and nested weapon ammo/cooldown/charge/reload facts. Refit, damage, and presentation share the same rotation geometry; Unity objects do not derive schematic truth | generic spatial schematic lowering, warning timelines, dynamic action state, and composited live capture |
|
||||
| Hit/lock/warnings | `ActionGameManager` feedback | daemon event facts | transient feedback stream | VFX/UI/audio | bounded deduplicated game-event ledger and Eve `feedback.stream` wired for pickup outcomes, projectile launch/impact, entity damage, alive-to-dead destruction, thermal-risk crossings, accepted dock/undock transitions, authoritative weapon reload chronology, lock start/acquired/lost chronology, and episode-deduplicated `weapon.fire.refused` facts for insufficient energy or missing ammunition; docking facts retain exact actor/parent/bay identity and rejected operations emit none; every lock transition carries authored weapon identity, target, reason, and prior/current progress, while reconnect priming prevents retained events from replaying as new feedback | broader negative transaction fixtures, provider lock/docking audio and effect roles, and broader runtime effect consumption |
|
||||
| Assets | `Assets/Resources` | Aetheria provider | asset catalog + CultMesh CDN | runtime cache/load | provider manifests name deterministic bundle-internal paths and now project each typed hull prefab through the same asset key in Eve scene and SoA identity state; the authoring build derives presentation-only prefabs, strips non-Eve gameplay scripts, externalizes embedded effects, preserves source material semantics, and verifies the loaded bundle ABI; released EveUnity `0.3.57` resolves Unity's lowercase bundle names against authored-case advertised keys. The released cold witness begins with an empty cache, transfers the 56,168,017-byte body through `cultmesh.content.v1`, atomically promotes its exact SHA-256 with no partial, selects `SharedFileMapping`, and lowers the shield-free hull while preserving pilot/map isolation | provider-to-client network zero-copy, shared-memory delivery, and GPU zero-copy remain open; the managed network session still copies and fragments payload bytes before verified promotion |
|
||||
| Save/resume | original persistence paths | daemon/CultCache | published current state | clients rehydrate | durable latest-frame checkpoints survive real CultCache directory-store close/reopen with complete loadout/behavior state, pending combat lock/trigger state, consumed Ymir contact chronology, active autonomous task ownership, and a mid-wormhole multi-zone route. Fresh Ymir sessions reconstruct ordinary motion from committed world truth; daemon-private contact journals prevent replay where reconstruction alone is insufficient. Explore tasks resume through the same scheduler movement commands, complete exactly once, release helm ownership, and reach the generic Eve commander surface; cross-zone restart preserves the transition phase and exactly-once event chronology without cloning the worker | live process-kill/relaunch witness and version/catalog migration policy |
|
||||
| Terminus loop | original game composition | daemon/Ymir/Eve | pilot surface | Aetheria.Unity | live minimum-loop proof: generic Unity discovers the provider, lowers the pilot world/assets, single-steps dock and undock while paused, buys daemon-priced station stock, single-steps Ymir salvage collection, resumes, moves, targets, fires, and presents authoritative receipts/shot outcome; the released full-session profile starts with no pickup, destroys a deterministic target, observes its canonical cargo drop, tractors it into a Ymir Begin contact, and proves exactly one `ymir-fact:` collection receipt plus cargo `0` to `1` | richer trade content, docking transitions, and sustained combat encounters |
|
||||
| Save/resume | original persistence paths | daemon/CultCache | published current state | clients rehydrate | durable latest-frame checkpoints survive real CultCache directory-store close/reopen with complete loadout/behavior state, pending combat lock/trigger state, consumed pickup removal/cargo chronology, active autonomous task ownership, and a mid-wormhole multi-zone route. Fresh Ymir sessions reconstruct ordinary motion from committed world truth; loot replay safety comes from the same atomic durable pickup removal and cargo insertion rather than a private contact journal. Explore tasks resume through the same scheduler movement commands, complete exactly once, release helm ownership, and reach the generic Eve commander surface; cross-zone restart preserves the transition phase and exactly-once event chronology without cloning the worker | live process-kill/relaunch witness and version/catalog migration policy |
|
||||
| Terminus loop | original game composition | daemon/Ymir/Eve | pilot surface | Aetheria.Unity | the prior released minimum/full-session witnesses prove generic provider discovery, world/assets, dock/undock, trade, movement, targeting, fire, destruction, tractor motion, and feedback. Their loot gate was contact-based and is now superseded: the live witness must prove tractor delivery into daemon XZ proximity, one stable `pickup.collected` event, durable pickup removal, and cargo `0` to `1` without a Ymir fact receipt | rerun released loop against proximity ownership; richer trade content, docking transitions, and sustained combat encounters |
|
||||
| Starbridge | original/shared architecture intent | one daemon session | pilot + commander surfaces | Unity + Electron | commander task board and autonomous explore execution wired; Electron Eve lowering and split-target proof missing | commander orders plus four pilots and autonomous workers |
|
||||
| Behavior engine | `ServerShared/Behaviors` composition | daemon | executable action/status projection | action bar and feedback | tick reconciles equipped catalog payloads into persistent behavior state; `Switch`, one-shot `Trigger`, normalized `Cooldown`, adjustable high/low-pass `Thermotoggle`, `EnergyDraw`, equipment `Heat`, atomic `ItemUsage`, and thermal `Wear` execute in authored group order and stop downstream effects on refusal; each row records whether the chain reached it. `Thruster`, rare `AetherDrive`, `Radiator`, `MiningTool`, and `ResourceScanner` pause their groups at the specialized owner, report the exact fossil success result, and resume common tails at most once. Invalid or out-of-range resource work therefore cannot consume trailing cargo or wear. Deferred tails now complete after the resource owners and before reactor settlement and thermal integration, preserving the fossil consumer-before-reactor order; radiator emission remains its independent always-update path. Cooldown projects generic progress and thermostat target is daemon-commanded state seeded from authored data. Equipped and timed-consumable `StatModifier` sources persist daemon-owned lifecycle state and every runtime stat reader applies the fossil `base × multiplier product + constant sum` rule through one query; legacy `Data` kind references and required-behavior filters are honored | remaining specialized return-value composition and canonical consumable-content proof |
|
||||
| Sensors/visibility | installed sensor sensitivity and angular curve, `Visibility`, `Reflector`, decaying signature sources, stellar light, ping state, and observer information thresholds | daemon | accumulated contacts, named visibility sources, and ping events | indicators/maps and provider environment lowering | every operational installed Sensor executes the fossil visibility/sensitivity/angle/distance information equation and decay; visibility is thresholded by the provider setting. Actor-scoped ping owns energy, cooldown, expansion radius, reconnect-stable crossed-target state, one-time information boost, and transient signature. Operational `Visibility` equipment publishes evaluated constant signature; committed weapons publish authored signature including charged-shot multiplication; transient weapon and ping sources decay by the fossil exponential rule. Reflectors multiply evaluated cross-section by the exponent-8 stellar `PowerPulse` after thermal settlement. Gameplay light and environment projection share resolved authored orbit/body centers and provider-owned sun radius inputs. Contact refresh no longer invents perfect information or overwrites emitted visibility; docked craft consume one daemon-derived union of craft and dock-parent contacts through both Eve rows and SoA bodies, with sensor-source provenance | active consumable visibility/reflector effects, complete source cardinality, classification timelines, and canonical catalog station-array data |
|
||||
|
|
@ -95,8 +95,9 @@ owner.
|
|||
|
||||
These are source-confirmed ownership failures, not feature backlog:
|
||||
|
||||
1. **Physics parity is incomplete.** Daemon entity, pickup, tractor, physical
|
||||
payload, and wormhole-exclusion paths use the embedded Ymir owner. The
|
||||
1. **Physics parity is incomplete.** Daemon entity, pickup motion, tractor,
|
||||
physical payload, and wormhole-exclusion paths use the embedded Ymir owner;
|
||||
pickup collection itself is an Aetheria XZ proximity rule. The
|
||||
entity/entity collision exclusion required by fossil docking is now proven,
|
||||
and the released Unity scene projection has no writeback dependency. The
|
||||
upstream Box3D parity harness and broader shape/contact coverage remain open.
|
||||
|
|
|
|||
|
|
@ -122,8 +122,8 @@ render channel, and an advertised activation action. EveUnity attaches and
|
|||
modulates that prefab; it does not raycast, apply force, infer contact, or move
|
||||
cargo. The provider build removes the fossil's embedded tractor object from
|
||||
ship presentation prefabs, so the standalone `beam.presentation` effect is the
|
||||
only live writer of tractor visuals. Ymir contact facts remain the only
|
||||
collection gate.
|
||||
only live writer of tractor visuals. The daemon's post-motion XZ proximity pass
|
||||
remains the only collection gate.
|
||||
|
||||
Projectile simulation and projectile presentation are separate. The daemon and
|
||||
Ymir own identity/pose/contact; presentation owns visual asset, trail, impact,
|
||||
|
|
|
|||
Loading…
Reference in a new issue