Harden Ymir contact consumption

This commit is contained in:
Meta Vens 2026-07-15 08:09:29 +01:00
commit 72128da11e
3 changed files with 38 additions and 8 deletions

View file

@ -2954,15 +2954,39 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
RequireEqual(1, zone.DroppedPickups.Count, "nearby pickup without a Ymir contact fact must remain in the world");
RequireEqual(0, CargoQuantity(ship, salvage.ItemKey), "proximity must not mutate cargo");
var forgedContact = new AetheriaRuntimeWorldBeginContact
{
FactId = "ymir-fact-forged-identity",
EntityAIndex = 0,
EntityAId = "another-entity",
PickupIndex = 7,
NormalX = 1
};
var rejectedForgedIdentity = false;
try
{
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-pickup-forged-identity.cc"), run,
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = new ScriptedWorldPhysics(forgedContact), FrameId = 2, FixedDeltaSeconds = 0.1, SimulationTimeSeconds = 0.2, Catalog = catalog, BuildPublications = false });
}
catch (InvalidOperationException)
{
rejectedForgedIdentity = true;
}
Require(rejectedForgedIdentity, "a Ymir contact whose stable identity disagrees with the live entity must fail closed");
RequireEqual(1, zone.DroppedPickups.Count, "forged contact identity must not remove the pickup");
RequireEqual(0, CargoQuantity(ship, salvage.ItemKey), "forged contact identity must not mutate cargo");
RequireEqual(0, run.PickupContactReceipts.Count, "forged contact identity must not mint a consumption receipt");
var contact = new AetheriaRuntimeWorldBeginContact
{
FactId = "ymir-fact-pickup-7",
EntityAIndex = 0,
EntityAId = ship.EntityId,
PickupIndex = 7,
NormalX = 1
};
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-pickup-contact-dedup.cc"), run,
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = new ScriptedWorldPhysics(contact, contact), FrameId = 2, FixedDeltaSeconds = 0.1, SimulationTimeSeconds = 0.2, Catalog = catalog, BuildPublications = false });
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = new ScriptedWorldPhysics(contact, contact), FrameId = 3, FixedDeltaSeconds = 0.1, SimulationTimeSeconds = 0.3, Catalog = catalog, BuildPublications = false });
RequireEqual(0, zone.DroppedPickups.Count, "one Ymir contact must consume the pickup");
RequireEqual(1, CargoQuantity(ship, salvage.ItemKey), "duplicate contact facts must commit cargo exactly once");
RequireEqual(1, run.GameEvents.Count(value => value.Kind == "pickup.collected" && value.PickupIndex == 7),
@ -2972,7 +2996,7 @@ internal sealed class AetheriaDaemonYmirSmokeChecks
zone.DroppedPickups = [new AetheriaRuntimeDroppedPickupCommit { PickupIndex = 8, PositionX = 10, Item = new AetheriaRuntimeLoadoutItemCommit { ItemKey = salvage.ItemKey, Quantity = 1 }, LifetimeSeconds = 30 }];
AetheriaRuntimeDaemonTickRunner.Tick(Path.Combine(Path.GetTempPath(), "aetheria-pickup-expiry-smoke.cc"), run,
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = new ScriptedWorldPhysics(), FrameId = 3, FixedDeltaSeconds = 30, SimulationTimeSeconds = 30, BuildPublications = false });
new AetheriaRuntimeDaemonTickOptions { WorldPhysics = new ScriptedWorldPhysics(), FrameId = 4, FixedDeltaSeconds = 30, SimulationTimeSeconds = 30, BuildPublications = false });
RequireEqual(0, zone.DroppedPickups.Count, "pickup must expire after the fossil thirty-second lifetime");
Require(run.GameEvents.Any(value => value.Kind == "pickup.expired" && value.PickupIndex == 8),
"daemon lifetime owner must emit authoritative pickup expiry event");

View file

@ -92,8 +92,9 @@ public sealed class AetheriaYmirWorldPhysics : IAetheriaRuntimeWorldPhysics, IDi
var key = new WorldKey(runId, zone.ZoneIndex);
if (_sessions.TryGetValue(key, out var existing) &&
existing.LastFrameId == frameId && existing.LastSimulationStepIndex == simulationStepIndex)
return existing.LastResult!;
existing.LastFrameId == frameId && existing.LastSimulationStepIndex == simulationStepIndex &&
existing.LastResult != null)
return existing.LastResult;
if (existing != null &&
(frameId < existing.LastFrameId ||
(frameId == existing.LastFrameId && simulationStepIndex < existing.LastSimulationStepIndex)))
@ -137,8 +138,9 @@ public sealed class AetheriaYmirWorldPhysics : IAetheriaRuntimeWorldPhysics, IDi
throw new InvalidOperationException(
$"Payload phase requires the retained world phase for run '{runId}' zone {zone.ZoneIndex} " +
$"at frame/step {frameId}/{simulationStepIndex}.");
if (state.LastPayloadFrameId == frameId && state.LastPayloadSimulationStepIndex == simulationStepIndex)
return state.LastPayloadResult!;
if (state.LastPayloadFrameId == frameId && state.LastPayloadSimulationStepIndex == simulationStepIndex &&
state.LastPayloadResult != null)
return state.LastPayloadResult;
if (deltaSeconds <= 0)
throw new ArgumentOutOfRangeException(nameof(deltaSeconds));

View file

@ -181,9 +181,13 @@ namespace GameCult.Aetheria.State.Verse
!string.IsNullOrWhiteSpace(value.FactId)))
{
var entityIndex = contact.EntityAIndex >= 0 ? contact.EntityAIndex : contact.EntityBIndex;
var contactEntityId = contact.EntityAIndex >= 0 ? contact.EntityAId : contact.EntityBId;
entitiesByIndex.TryGetValue(entityIndex, out var entity);
var entityId = entity?.EntityId ??
(contact.EntityAIndex >= 0 ? contact.EntityAId : contact.EntityBId);
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)
{