From e73da9faf5611bd9ba43b7469fe50cb1df58fa38 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Thu, 16 Jul 2026 22:26:29 +0100 Subject: [PATCH] Prove contact-gated cargo rejection --- .../PlayMode/GenericWorldCaptureTests.cs | 131 ++++++++++++++---- .../PlayMode/GenericWorldCaptureTests.cs | 131 ++++++++++++++---- docs/aetheria-released-package-witness.md | 15 +- scripts/run-aetheria-daemon-world-witness.ps1 | 32 ++++- 4 files changed, 240 insertions(+), 69 deletions(-) diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index f98cfd1..152bac1 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -210,7 +210,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float tractorReleasedPower = 1f; int tractorBeamParticleSystemCount = 0; EveUnityFeedbackEvent pickupCollection = null; + EveUnityFeedbackEvent pickupRejection = null; int pickupCollectionEventCount = 0; + int pickupRejectionEventCount = 0; + var pickupRejectionEventIds = new HashSet(StringComparer.Ordinal); int initialPickupEntityCount = 0; int finalPickupEntityCount = -1; bool destructionPickupObserved = false; @@ -225,6 +228,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests witnessProfile, "full-session-gameplay", StringComparison.Ordinal); + var gameplayScenario = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_GAMEPLAY_SCENARIO") ?? + "released-client-proof"; + Assert.That(gameplayScenario, + Is.EqualTo("released-client-proof").Or.EqualTo("cargo-capacity-rejection-proof"), + "The live witness must select an explicit daemon-owned gameplay scenario."); + var expectCargoRejection = string.Equals( + gameplayScenario, + "cargo-capacity-rejection-proof", + StringComparison.Ordinal); EveUnityAimPresentationRenderer aimRenderer = null; EveUnityBeamPresentation beamPresentation = null; EveUnityBeamPresentationRenderer beamRenderer = null; @@ -311,16 +323,25 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests if (requireSessionGameplay) Assert.That(initialPickupEntityCount, Is.Zero, "The witness scenario must obtain salvage from daemon-owned destruction, not a boot-seeded pickup."); - runtime.FeedbackAvailable += feedback => - { - if (!string.Equals(feedback.Kind, "pickup.collected", StringComparison.Ordinal)) return; - pickupCollectionEventCount++; - pickupCollection = feedback; - }; - var playerId = runtime.ActiveWorld.PlayerEntityId; var playerFact = host.PresentedEntities.CurrentGeneration.Entities .Single(entity => entity.EntityId == playerId); + runtime.FeedbackAvailable += feedback => + { + if (feedback.TargetEntityIndex != playerFact.SourceIndex) return; + if (string.Equals(feedback.Kind, "pickup.collected", StringComparison.Ordinal)) + { + pickupCollectionEventCount++; + pickupCollection = feedback; + } + else if (string.Equals(feedback.Kind, "pickup.rejected", StringComparison.Ordinal)) + { + pickupRejectionEventCount++; + pickupRejectionEventIds.Add(feedback.EventId); + pickupRejection = feedback; + } + }; + var playerPrefab = provider.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( playerFact.AssetRef, playerFact.EntityKind, @@ -529,7 +550,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests (observedShot == null || host.PresentedEntities.CurrentGeneration.Entities.Any(entity => entity.EntityId == targetEntityId) || - (!destructionPickupObserved && pickupCollection == null))) + (!destructionPickupObserved && pickupCollection == null && pickupRejection == null))) { yield return new WaitForSecondsRealtime(0.05f); runtime.Refresh(); @@ -544,11 +565,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(host.PresentedEntities.CurrentGeneration.Entities.Any(entity => entity.EntityId == targetEntityId), Is.False, "The low-hull witness raider was not destroyed by daemon combat."); - Assert.That(destructionPickupObserved || pickupCollection != null, Is.True, + Assert.That(destructionPickupObserved || pickupCollection != null || pickupRejection != null, Is.True, "Daemon destruction did not publish its guaranteed cargo as a pickup."); var collectionDeadline = Time.realtimeSinceStartup + 15f; var nextTractorAimAt = 0f; - while (Time.realtimeSinceStartup < collectionDeadline && pickupCollection == null) + while (Time.realtimeSinceStartup < collectionDeadline && + (expectCargoRejection ? pickupRejection == null : pickupCollection == null)) { yield return new WaitForSecondsRealtime(0.05f); runtime.Refresh(); @@ -571,19 +593,44 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } finalPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity => string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); - Assert.That(pickupCollection, Is.Not.Null, - "Destruction-created salvage never reached cargo through a Ymir contact fact."); - Assert.That(pickupCollectionEventCount, Is.EqualTo(1), - "One destruction-loot Ymir contact transaction must emit one collection event."); - Assert.That(pickupCollection.ItemKey, Is.Not.Empty, - "Destruction loot must retain its canonical typed item identity."); - Assert.That(pickupCollection.EventId, Does.StartWith("ymir-fact:"), - "Collection feedback must retain the consumed Ymir fact identity."); - Assert.That(pickupCollection.ScalarValue, Is.EqualTo(1)); - Assert.That(pickupCollection.CargoQuantityBefore, Is.Zero); - Assert.That(pickupCollection.CargoQuantityAfter, Is.EqualTo(1)); - Assert.That(finalPickupEntityCount, Is.Zero, - "Collected destruction loot remained in the provider-authored entity generation."); + if (expectCargoRejection) + { + Assert.That(pickupRejection, Is.Not.Null, + "Full cargo did not reject destruction salvage from its Ymir contact fact."); + Assert.That(pickupRejectionEventCount, Is.EqualTo(1), + "The first destruction-loot Ymir contact transaction must emit one rejection event."); + Assert.That(pickupRejectionEventIds.Count, Is.EqualTo(pickupRejectionEventCount), + "One Ymir fact identity emitted duplicate player rejection feedback."); + Assert.That(pickupCollectionEventCount, Is.Zero, + "Capacity-rejected salvage was also collected."); + Assert.That(pickupRejection.ItemKey, Is.Not.Empty); + Assert.That(pickupRejection.EventId, Does.StartWith("ymir-fact:"), + "Rejection feedback must retain the consumed Ymir fact identity."); + Assert.That(pickupRejection.Reason, Is.EqualTo("cargo-capacity")); + Assert.That(pickupRejection.ScalarValue, Is.EqualTo(1)); + Assert.That(pickupRejection.CargoQuantityAfter, + Is.EqualTo(pickupRejection.CargoQuantityBefore)); + Assert.That(finalPickupEntityCount, Is.EqualTo(1), + "Capacity-rejected destruction loot disappeared from provider state."); + } + else + { + Assert.That(pickupCollection, Is.Not.Null, + "Destruction-created salvage never reached cargo through a Ymir contact fact."); + Assert.That(pickupCollectionEventCount, Is.EqualTo(1), + "One destruction-loot Ymir contact transaction must emit one collection event."); + Assert.That(pickupRejectionEventCount, Is.Zero, + "Collectible destruction salvage was also rejected."); + Assert.That(pickupCollection.ItemKey, Is.Not.Empty, + "Destruction loot must retain its canonical typed item identity."); + Assert.That(pickupCollection.EventId, Does.StartWith("ymir-fact:"), + "Collection feedback must retain the consumed Ymir fact identity."); + Assert.That(pickupCollection.ScalarValue, Is.EqualTo(1)); + Assert.That(pickupCollection.CargoQuantityBefore, Is.Zero); + Assert.That(pickupCollection.CargoQuantityAfter, Is.EqualTo(1)); + Assert.That(finalPickupEntityCount, Is.Zero, + "Collected destruction loot remained in the provider-authored entity generation."); + } observedCombat = EveUnityCombatPresentation.Find(runtime.ActiveProjection); Assert.That(observedShot.TargetEntityIndex, Is.EqualTo(targetEntityIndex)); Assert.That(observedShot.LockQuality, Is.GreaterThanOrEqualTo(0.99)); @@ -869,6 +916,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(fieldSplats, Is.Not.Null); WriteWitnessFacts( witnessProfile, + gameplayScenario, provider.CurrentAssetBodyTransportKind?.ToString() ?? "", initialVersion, runtime.ActiveVersion, @@ -915,8 +963,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests initialPickupEntityCount, finalPickupEntityCount, pickupCollectionEventCount, + pickupRejectionEventCount, destructionPickupObserved, pickupCollection, + pickupRejection, camera.transform.position, camera.transform.forward, playerMarker.transform.position, @@ -949,6 +999,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests private static void WriteWitnessFacts( string witnessProfile, + string gameplayScenario, string assetBodyTransport, long initialVersion, long finalVersion, @@ -995,8 +1046,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests int initialPickupEntityCount, int finalPickupEntityCount, int pickupCollectionEventCount, + int pickupRejectionEventCount, bool destructionPickupObserved, EveUnityFeedbackEvent pickupCollection, + EveUnityFeedbackEvent pickupRejection, Vector3 cameraPosition, Vector3 cameraForward, Vector3 playerPosition, @@ -1011,9 +1064,11 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); if (string.IsNullOrWhiteSpace(path)) return; + var pickupEvent = pickupCollection ?? pickupRejection; var document = new WitnessFacts { witnessProfile = witnessProfile, + gameplayScenario = gameplayScenario, providerAdvertisement = true, providerAssets = true, assetBodyTransport = assetBodyTransport, @@ -1068,16 +1123,25 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamParticleSystemCount = tractorBeamParticleSystemCount, pickupCollection = pickupCollection != null && pickupCollectionEventCount == 1 && pickupCollection.CargoQuantityAfter - pickupCollection.CargoQuantityBefore == pickupCollection.ScalarValue, - destructionLoot = pickupCollection != null && - pickupCollection.EventId.StartsWith("ymir-fact:", StringComparison.Ordinal), + pickupRejection = pickupRejection != null && pickupRejectionEventCount >= 1 && + pickupCollectionEventCount == 0 && + string.Equals(pickupRejection.Reason, "cargo-capacity", StringComparison.Ordinal) && + pickupRejection.CargoQuantityAfter == pickupRejection.CargoQuantityBefore, + destructionLoot = (pickupCollection?.EventId ?? pickupRejection?.EventId ?? "") + .StartsWith("ymir-fact:", StringComparison.Ordinal), initialPickupEntityCount = initialPickupEntityCount, finalPickupEntityCount = finalPickupEntityCount, pickupCollectionEventCount = pickupCollectionEventCount, - pickupEventId = pickupCollection?.EventId ?? "", - pickupItemKey = pickupCollection?.ItemKey ?? "", - pickupQuantity = (float)(pickupCollection?.ScalarValue ?? 0), - cargoQuantityBeforePickup = (float)(pickupCollection?.CargoQuantityBefore ?? 0), - cargoQuantityAfterPickup = (float)(pickupCollection?.CargoQuantityAfter ?? 0), + pickupRejectionEventCount = pickupRejectionEventCount, + pickupRejectionEventId = pickupRejection?.EventId ?? "", + pickupRejectionReason = pickupRejection?.Reason ?? "", + cargoQuantityBeforeRejection = (float)(pickupRejection?.CargoQuantityBefore ?? 0), + cargoQuantityAfterRejection = (float)(pickupRejection?.CargoQuantityAfter ?? 0), + pickupEventId = pickupEvent?.EventId ?? "", + pickupItemKey = pickupEvent?.ItemKey ?? "", + pickupQuantity = (float)(pickupEvent?.ScalarValue ?? 0), + cargoQuantityBeforePickup = (float)(pickupEvent?.CargoQuantityBefore ?? 0), + cargoQuantityAfterPickup = (float)(pickupEvent?.CargoQuantityAfter ?? 0), cameraPosition = cameraPosition, cameraForward = cameraForward, playerPosition = playerPosition, @@ -1103,6 +1167,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests private sealed class WitnessFacts { public string witnessProfile; + public string gameplayScenario; public bool providerAdvertisement; public bool providerAssets; public string assetBodyTransport; @@ -1154,10 +1219,16 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float tractorReleasedPower; public int tractorBeamParticleSystemCount; public bool pickupCollection; + public bool pickupRejection; public bool destructionLoot; public int initialPickupEntityCount; public int finalPickupEntityCount; public int pickupCollectionEventCount; + public int pickupRejectionEventCount; + public string pickupRejectionEventId; + public string pickupRejectionReason; + public float cargoQuantityBeforeRejection; + public float cargoQuantityAfterRejection; public string pickupEventId; public string pickupItemKey; public float pickupQuantity; diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index f98cfd1..152bac1 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -210,7 +210,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float tractorReleasedPower = 1f; int tractorBeamParticleSystemCount = 0; EveUnityFeedbackEvent pickupCollection = null; + EveUnityFeedbackEvent pickupRejection = null; int pickupCollectionEventCount = 0; + int pickupRejectionEventCount = 0; + var pickupRejectionEventIds = new HashSet(StringComparer.Ordinal); int initialPickupEntityCount = 0; int finalPickupEntityCount = -1; bool destructionPickupObserved = false; @@ -225,6 +228,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests witnessProfile, "full-session-gameplay", StringComparison.Ordinal); + var gameplayScenario = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_GAMEPLAY_SCENARIO") ?? + "released-client-proof"; + Assert.That(gameplayScenario, + Is.EqualTo("released-client-proof").Or.EqualTo("cargo-capacity-rejection-proof"), + "The live witness must select an explicit daemon-owned gameplay scenario."); + var expectCargoRejection = string.Equals( + gameplayScenario, + "cargo-capacity-rejection-proof", + StringComparison.Ordinal); EveUnityAimPresentationRenderer aimRenderer = null; EveUnityBeamPresentation beamPresentation = null; EveUnityBeamPresentationRenderer beamRenderer = null; @@ -311,16 +323,25 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests if (requireSessionGameplay) Assert.That(initialPickupEntityCount, Is.Zero, "The witness scenario must obtain salvage from daemon-owned destruction, not a boot-seeded pickup."); - runtime.FeedbackAvailable += feedback => - { - if (!string.Equals(feedback.Kind, "pickup.collected", StringComparison.Ordinal)) return; - pickupCollectionEventCount++; - pickupCollection = feedback; - }; - var playerId = runtime.ActiveWorld.PlayerEntityId; var playerFact = host.PresentedEntities.CurrentGeneration.Entities .Single(entity => entity.EntityId == playerId); + runtime.FeedbackAvailable += feedback => + { + if (feedback.TargetEntityIndex != playerFact.SourceIndex) return; + if (string.Equals(feedback.Kind, "pickup.collected", StringComparison.Ordinal)) + { + pickupCollectionEventCount++; + pickupCollection = feedback; + } + else if (string.Equals(feedback.Kind, "pickup.rejected", StringComparison.Ordinal)) + { + pickupRejectionEventCount++; + pickupRejectionEventIds.Add(feedback.EventId); + pickupRejection = feedback; + } + }; + var playerPrefab = provider.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( playerFact.AssetRef, playerFact.EntityKind, @@ -529,7 +550,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests (observedShot == null || host.PresentedEntities.CurrentGeneration.Entities.Any(entity => entity.EntityId == targetEntityId) || - (!destructionPickupObserved && pickupCollection == null))) + (!destructionPickupObserved && pickupCollection == null && pickupRejection == null))) { yield return new WaitForSecondsRealtime(0.05f); runtime.Refresh(); @@ -544,11 +565,12 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(host.PresentedEntities.CurrentGeneration.Entities.Any(entity => entity.EntityId == targetEntityId), Is.False, "The low-hull witness raider was not destroyed by daemon combat."); - Assert.That(destructionPickupObserved || pickupCollection != null, Is.True, + Assert.That(destructionPickupObserved || pickupCollection != null || pickupRejection != null, Is.True, "Daemon destruction did not publish its guaranteed cargo as a pickup."); var collectionDeadline = Time.realtimeSinceStartup + 15f; var nextTractorAimAt = 0f; - while (Time.realtimeSinceStartup < collectionDeadline && pickupCollection == null) + while (Time.realtimeSinceStartup < collectionDeadline && + (expectCargoRejection ? pickupRejection == null : pickupCollection == null)) { yield return new WaitForSecondsRealtime(0.05f); runtime.Refresh(); @@ -571,19 +593,44 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } finalPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity => string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); - Assert.That(pickupCollection, Is.Not.Null, - "Destruction-created salvage never reached cargo through a Ymir contact fact."); - Assert.That(pickupCollectionEventCount, Is.EqualTo(1), - "One destruction-loot Ymir contact transaction must emit one collection event."); - Assert.That(pickupCollection.ItemKey, Is.Not.Empty, - "Destruction loot must retain its canonical typed item identity."); - Assert.That(pickupCollection.EventId, Does.StartWith("ymir-fact:"), - "Collection feedback must retain the consumed Ymir fact identity."); - Assert.That(pickupCollection.ScalarValue, Is.EqualTo(1)); - Assert.That(pickupCollection.CargoQuantityBefore, Is.Zero); - Assert.That(pickupCollection.CargoQuantityAfter, Is.EqualTo(1)); - Assert.That(finalPickupEntityCount, Is.Zero, - "Collected destruction loot remained in the provider-authored entity generation."); + if (expectCargoRejection) + { + Assert.That(pickupRejection, Is.Not.Null, + "Full cargo did not reject destruction salvage from its Ymir contact fact."); + Assert.That(pickupRejectionEventCount, Is.EqualTo(1), + "The first destruction-loot Ymir contact transaction must emit one rejection event."); + Assert.That(pickupRejectionEventIds.Count, Is.EqualTo(pickupRejectionEventCount), + "One Ymir fact identity emitted duplicate player rejection feedback."); + Assert.That(pickupCollectionEventCount, Is.Zero, + "Capacity-rejected salvage was also collected."); + Assert.That(pickupRejection.ItemKey, Is.Not.Empty); + Assert.That(pickupRejection.EventId, Does.StartWith("ymir-fact:"), + "Rejection feedback must retain the consumed Ymir fact identity."); + Assert.That(pickupRejection.Reason, Is.EqualTo("cargo-capacity")); + Assert.That(pickupRejection.ScalarValue, Is.EqualTo(1)); + Assert.That(pickupRejection.CargoQuantityAfter, + Is.EqualTo(pickupRejection.CargoQuantityBefore)); + Assert.That(finalPickupEntityCount, Is.EqualTo(1), + "Capacity-rejected destruction loot disappeared from provider state."); + } + else + { + Assert.That(pickupCollection, Is.Not.Null, + "Destruction-created salvage never reached cargo through a Ymir contact fact."); + Assert.That(pickupCollectionEventCount, Is.EqualTo(1), + "One destruction-loot Ymir contact transaction must emit one collection event."); + Assert.That(pickupRejectionEventCount, Is.Zero, + "Collectible destruction salvage was also rejected."); + Assert.That(pickupCollection.ItemKey, Is.Not.Empty, + "Destruction loot must retain its canonical typed item identity."); + Assert.That(pickupCollection.EventId, Does.StartWith("ymir-fact:"), + "Collection feedback must retain the consumed Ymir fact identity."); + Assert.That(pickupCollection.ScalarValue, Is.EqualTo(1)); + Assert.That(pickupCollection.CargoQuantityBefore, Is.Zero); + Assert.That(pickupCollection.CargoQuantityAfter, Is.EqualTo(1)); + Assert.That(finalPickupEntityCount, Is.Zero, + "Collected destruction loot remained in the provider-authored entity generation."); + } observedCombat = EveUnityCombatPresentation.Find(runtime.ActiveProjection); Assert.That(observedShot.TargetEntityIndex, Is.EqualTo(targetEntityIndex)); Assert.That(observedShot.LockQuality, Is.GreaterThanOrEqualTo(0.99)); @@ -869,6 +916,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests Assert.That(fieldSplats, Is.Not.Null); WriteWitnessFacts( witnessProfile, + gameplayScenario, provider.CurrentAssetBodyTransportKind?.ToString() ?? "", initialVersion, runtime.ActiveVersion, @@ -915,8 +963,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests initialPickupEntityCount, finalPickupEntityCount, pickupCollectionEventCount, + pickupRejectionEventCount, destructionPickupObserved, pickupCollection, + pickupRejection, camera.transform.position, camera.transform.forward, playerMarker.transform.position, @@ -949,6 +999,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests private static void WriteWitnessFacts( string witnessProfile, + string gameplayScenario, string assetBodyTransport, long initialVersion, long finalVersion, @@ -995,8 +1046,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests int initialPickupEntityCount, int finalPickupEntityCount, int pickupCollectionEventCount, + int pickupRejectionEventCount, bool destructionPickupObserved, EveUnityFeedbackEvent pickupCollection, + EveUnityFeedbackEvent pickupRejection, Vector3 cameraPosition, Vector3 cameraForward, Vector3 playerPosition, @@ -1011,9 +1064,11 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); if (string.IsNullOrWhiteSpace(path)) return; + var pickupEvent = pickupCollection ?? pickupRejection; var document = new WitnessFacts { witnessProfile = witnessProfile, + gameplayScenario = gameplayScenario, providerAdvertisement = true, providerAssets = true, assetBodyTransport = assetBodyTransport, @@ -1068,16 +1123,25 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamParticleSystemCount = tractorBeamParticleSystemCount, pickupCollection = pickupCollection != null && pickupCollectionEventCount == 1 && pickupCollection.CargoQuantityAfter - pickupCollection.CargoQuantityBefore == pickupCollection.ScalarValue, - destructionLoot = pickupCollection != null && - pickupCollection.EventId.StartsWith("ymir-fact:", StringComparison.Ordinal), + pickupRejection = pickupRejection != null && pickupRejectionEventCount >= 1 && + pickupCollectionEventCount == 0 && + string.Equals(pickupRejection.Reason, "cargo-capacity", StringComparison.Ordinal) && + pickupRejection.CargoQuantityAfter == pickupRejection.CargoQuantityBefore, + destructionLoot = (pickupCollection?.EventId ?? pickupRejection?.EventId ?? "") + .StartsWith("ymir-fact:", StringComparison.Ordinal), initialPickupEntityCount = initialPickupEntityCount, finalPickupEntityCount = finalPickupEntityCount, pickupCollectionEventCount = pickupCollectionEventCount, - pickupEventId = pickupCollection?.EventId ?? "", - pickupItemKey = pickupCollection?.ItemKey ?? "", - pickupQuantity = (float)(pickupCollection?.ScalarValue ?? 0), - cargoQuantityBeforePickup = (float)(pickupCollection?.CargoQuantityBefore ?? 0), - cargoQuantityAfterPickup = (float)(pickupCollection?.CargoQuantityAfter ?? 0), + pickupRejectionEventCount = pickupRejectionEventCount, + pickupRejectionEventId = pickupRejection?.EventId ?? "", + pickupRejectionReason = pickupRejection?.Reason ?? "", + cargoQuantityBeforeRejection = (float)(pickupRejection?.CargoQuantityBefore ?? 0), + cargoQuantityAfterRejection = (float)(pickupRejection?.CargoQuantityAfter ?? 0), + pickupEventId = pickupEvent?.EventId ?? "", + pickupItemKey = pickupEvent?.ItemKey ?? "", + pickupQuantity = (float)(pickupEvent?.ScalarValue ?? 0), + cargoQuantityBeforePickup = (float)(pickupEvent?.CargoQuantityBefore ?? 0), + cargoQuantityAfterPickup = (float)(pickupEvent?.CargoQuantityAfter ?? 0), cameraPosition = cameraPosition, cameraForward = cameraForward, playerPosition = playerPosition, @@ -1103,6 +1167,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests private sealed class WitnessFacts { public string witnessProfile; + public string gameplayScenario; public bool providerAdvertisement; public bool providerAssets; public string assetBodyTransport; @@ -1154,10 +1219,16 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float tractorReleasedPower; public int tractorBeamParticleSystemCount; public bool pickupCollection; + public bool pickupRejection; public bool destructionLoot; public int initialPickupEntityCount; public int finalPickupEntityCount; public int pickupCollectionEventCount; + public int pickupRejectionEventCount; + public string pickupRejectionEventId; + public string pickupRejectionReason; + public float cargoQuantityBeforeRejection; + public float cargoQuantityAfterRejection; public string pickupEventId; public string pickupItemKey; public float pickupQuantity; diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index a71e860..daaec94 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -84,6 +84,17 @@ The remaining visual controls (fog parameters, stellar tint, ambient intensity, histogram exposure, grading, and temporal settings) remain provider-owned levers; this proof does not claim a final art grade. +The released `0.3.63` warm rejection witness in +`artifacts/aetheria-daemon-cargo-rejection-0363-warm` selects the daemon-owned +`cargo-capacity-rejection-proof` scenario. The controlled ship begins with all +generated cargo bays filled from typed item volume and bay capacity. Combat +creates the salvage pickup normally; player Ymir Begin contacts emit +`pickup.rejected` with `cargo-capacity`, preserve cargo at `5` items, and leave +the pickup live. The final run observed two later player recontacts with two +distinct `ymir-fact:` identities and no player collection event. The generic +witness correlates feedback through advertised SoA `SourceIndex` and feedback +`TargetEntityIndex`; events from other ships are not attributed to the pilot. + Current released-package evidence is in `artifacts/aetheria-daemon-mapped-body-cold`; despite that retained directory name, `runtime-witness.warm.json` is the current passing run. The warm PlayMode test @@ -251,8 +262,8 @@ changes cargo from `0` to `1`, and emits one `pickup.collected` feedback event whose identity begins with `ymir-fact:`. Aetheria's Ymir body mapping excludes stations and non-ship world bodies from pickup collision facts, so ambient station contact cannot become a cargo writer. Capacity rejection and -its `cargo-capacity` reason are covered by the daemon smoke, not claimed as a -released-client live scenario here. +its `cargo-capacity` reason are also proven by the released-client live rejection +scenario described above. Effect tuning remains provider art work; the runtime must not compensate with an Aetheria-specific beam. diff --git a/scripts/run-aetheria-daemon-world-witness.ps1 b/scripts/run-aetheria-daemon-world-witness.ps1 index 86fb3a0..0f7c4ed 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -9,6 +9,8 @@ param( [string] $OutputDirectory = "artifacts\aetheria-daemon", [ValidateSet("auto", "cold", "warm")] [string] $CacheState = "auto", + [ValidateSet("released-client-proof", "cargo-capacity-rejection-proof")] + [string] $GameplayScenario = "released-client-proof", [switch] $SkipAssetBundleBuild ) @@ -168,6 +170,7 @@ $daemonArguments = @( "--client-cultmesh-port", $Port, "--tick-interval-ms", 20, "--fixed-delta-ms", 20, + "--terminus-scenario", $GameplayScenario, "--no-odin-announcements" ) $env:AETHERIA_TRACE_EVE_SNAPSHOTS = "1" @@ -183,6 +186,7 @@ $env:EVEUNITY_ASSET_CACHE_PATH = $assetCachePath $env:EVEUNITY_WITNESS_FACTS_PATH = $factsPath $env:EVEUNITY_PROVIDER_READY_PATH = $providerReadyPath $env:EVEUNITY_WITNESS_PROFILE = $witnessProfile +$env:EVEUNITY_WITNESS_GAMEPLAY_SCENARIO = $GameplayScenario $arguments = @( "-batchmode", "-projectPath", $projectRoot, "-runTests", "-testPlatform", "PlayMode", @@ -271,11 +275,21 @@ try { @($presentedCelestials | Where-Object { $_.intersectsPilotFrustum }).Count -le 0) { throw "Live witness did not prove provider-owned celestial bodies and asteroids reached the generic scene and intersected the pilot frustum. bodies=$($presentedBodies.Count) asteroids=$($presentedAsteroids.Count) invalid=$($invalidCelestials.Count)" } - if ($witnessProfile -eq "full-session-gameplay" -and - (-not $facts.combatPresentation -or [string]::IsNullOrWhiteSpace($facts.shotId) -or - [double]$facts.lockProgress -le 0.99 -or -not $facts.pickupCollection -or - -not $facts.destructionLoot -or [int]$facts.pickupCollectionEventCount -ne 1)) { - throw "Full-session witness did not prove combat and exactly-once destruction-loot Ymir contact collection." + if ($witnessProfile -eq "full-session-gameplay") { + if (-not $facts.combatPresentation -or [string]::IsNullOrWhiteSpace($facts.shotId) -or + [double]$facts.lockProgress -le 0.99 -or -not $facts.destructionLoot) { + throw "Full-session witness did not prove combat and Ymir-contact destruction loot." + } + if ($GameplayScenario -eq "cargo-capacity-rejection-proof") { + if ($facts.gameplayScenario -ne $GameplayScenario -or -not $facts.pickupRejection -or + [int]$facts.pickupRejectionEventCount -lt 1 -or [int]$facts.pickupCollectionEventCount -ne 0 -or + $facts.pickupRejectionReason -ne "cargo-capacity" -or + [double]$facts.cargoQuantityBeforeRejection -ne [double]$facts.cargoQuantityAfterRejection) { + throw "Full-session rejection witness did not prove player cargo-capacity refusal from distinct Ymir contact facts." + } + } elseif (-not $facts.pickupCollection -or [int]$facts.pickupCollectionEventCount -ne 1) { + throw "Full-session collection witness did not prove exactly-once player destruction-loot collection from a Ymir contact fact." + } } $releaseLock = Get-Content -LiteralPath (Join-Path $repoRoot "ReleaseConsumerProject\Packages\packages-lock.json") -Raw | ConvertFrom-Json $releasedPackageClient = $projectRoot -eq "ReleaseConsumerProject" @@ -379,7 +393,11 @@ try { } ) authority = if ($witnessProfile -eq "full-session-gameplay") { - "released-generic-runtime-observes-provider-assets-authoritative-gameplay-receipts-ymir-contact-collection-and-camera-channel-separation" + if ($GameplayScenario -eq "cargo-capacity-rejection-proof") { + "released-generic-runtime-observes-provider-assets-authoritative-gameplay-receipts-ymir-contact-capacity-rejection-and-camera-channel-separation" + } else { + "released-generic-runtime-observes-provider-assets-authoritative-gameplay-receipts-ymir-contact-collection-and-camera-channel-separation" + } } else { "released-generic-runtime-cold-loads-provider-assets-lowers-playable-world-and-preserves-camera-channel-separation" } @@ -396,6 +414,6 @@ try { finally { if ($null -ne $unity -and -not $unity.HasExited) { Stop-Process -Id $unity.Id -Force -ErrorAction SilentlyContinue } if ($null -ne $daemon -and -not $daemon.HasExited) { Stop-Process -Id $daemon.Id -Force } - Remove-Item Env:EVEUNITY_RENDEZVOUS_ENDPOINT, Env:EVEUNITY_PROVIDER_ENDPOINT, Env:EVEUNITY_PROVIDER_ID, Env:EVEUNITY_SURFACE_ID, Env:EVEUNITY_REPLICA_PATH, Env:EVEUNITY_AETHERIA_CAPTURE_PATH, Env:EVEUNITY_AETHERIA_MAP_CAPTURE_PATH, Env:EVEUNITY_DISABLE_AUTO_LAUNCHER, Env:EVEUNITY_ASSET_CACHE_PATH, Env:EVEUNITY_WITNESS_FACTS_PATH, Env:EVEUNITY_PROVIDER_READY_PATH, Env:EVEUNITY_WITNESS_PROFILE -ErrorAction SilentlyContinue + Remove-Item Env:EVEUNITY_RENDEZVOUS_ENDPOINT, Env:EVEUNITY_PROVIDER_ENDPOINT, Env:EVEUNITY_PROVIDER_ID, Env:EVEUNITY_SURFACE_ID, Env:EVEUNITY_REPLICA_PATH, Env:EVEUNITY_AETHERIA_CAPTURE_PATH, Env:EVEUNITY_AETHERIA_MAP_CAPTURE_PATH, Env:EVEUNITY_DISABLE_AUTO_LAUNCHER, Env:EVEUNITY_ASSET_CACHE_PATH, Env:EVEUNITY_WITNESS_FACTS_PATH, Env:EVEUNITY_PROVIDER_READY_PATH, Env:EVEUNITY_WITNESS_PROFILE, Env:EVEUNITY_WITNESS_GAMEPLAY_SCENARIO -ErrorAction SilentlyContinue Remove-Item Env:AETHERIA_TRACE_EVE_SNAPSHOTS -ErrorAction SilentlyContinue }