Prove contact-gated cargo rejection

This commit is contained in:
Meta Vens 2026-07-16 22:26:29 +01:00
commit e73da9faf5
4 changed files with 238 additions and 67 deletions

View file

@ -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<string>(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;

View file

@ -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<string>(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;

View file

@ -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.

View file

@ -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
}