diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index ab1981f..27875c4 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -205,9 +205,16 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float tractorBeamPower = 0f; float tractorReleasedPower = 1f; int tractorBeamParticleSystemCount = 0; + EveUnityFeedbackEvent pickupCollection = null; + int pickupCollectionEventCount = 0; + int initialPickupEntityCount = 0; + int finalPickupEntityCount = -1; try { + var providerReadyPath = Environment.GetEnvironmentVariable("EVEUNITY_PROVIDER_READY_PATH"); + if (!string.IsNullOrWhiteSpace(providerReadyPath)) + File.WriteAllText(providerReadyPath, DateTimeOffset.UtcNow.ToString("O")); provider = root.AddComponent(); provider.Configure( rendezvousEndpoint, @@ -216,7 +223,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests surfaceId, requiredSurfaceKind: "interactive-world", clientRuntimeId: $"eve-unity-test-{Guid.NewGuid():N}"); - var publicationDeadline = Time.realtimeSinceStartup + 10f; + var publicationDeadline = Time.realtimeSinceStartup + 30f; while (true) { var published = false; @@ -228,6 +235,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests catch (InvalidOperationException) when (Time.realtimeSinceStartup < publicationDeadline) { } + catch (TimeoutException) when (Time.realtimeSinceStartup < publicationDeadline) + { + } if (published) break; yield return new WaitForSecondsRealtime(0.1f); } @@ -267,6 +277,16 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests } Assert.That(host.PresentedEntities?.CurrentGeneration, Is.Not.Null); Assert.That(host.PresentedEntities.CurrentGeneration.Entities.Count, Is.GreaterThan(0)); + initialPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity => + string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); + Assert.That(initialPickupEntityCount, Is.GreaterThan(0), + "The daemon fixture did not publish its physical starter pickup through the entity SoA."); + 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 @@ -418,6 +438,27 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamParticleSystemCount = beamRenderer.ActiveParticleSystemCount; Assert.That(tractorBeamParticleSystemCount, Is.GreaterThan(0), "The generic beam lowerer did not instantiate the provider-owned effect prefab."); + var pickupDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < pickupDeadline && + (pickupCollection == null || + host.PresentedEntities.CurrentGeneration.Entities.Any(entity => + string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)))) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + finalPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity => + string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal)); + Assert.That(pickupCollection, Is.Not.Null, + "The released client did not observe provider-owned feedback for Ymir contact collection."); + Assert.That(pickupCollectionEventCount, Is.EqualTo(1), + "One Ymir contact transaction must emit one client-visible collection event."); + Assert.That(pickupCollection.ItemKey, Is.EqualTo("scrap-metal")); + Assert.That(pickupCollection.ScalarValue, Is.EqualTo(1)); + Assert.That(pickupCollection.CargoQuantityBefore, Is.Zero); + Assert.That(pickupCollection.CargoQuantityAfter, Is.EqualTo(1)); + Assert.That(finalPickupEntityCount, Is.EqualTo(initialPickupEntityCount - 1), + "Collected pickup remained in the provider-authored entity generation."); var focusVersion = runtime.ActiveVersion; var inputDriver = root.GetComponent() ?? @@ -659,6 +700,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamPower, tractorReleasedPower, tractorBeamParticleSystemCount, + initialPickupEntityCount, + finalPickupEntityCount, + pickupCollectionEventCount, + pickupCollection, playerRendererFacts); } finally @@ -711,6 +756,10 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests float tractorBeamPower, float tractorReleasedPower, int tractorBeamParticleSystemCount, + int initialPickupEntityCount, + int finalPickupEntityCount, + int pickupCollectionEventCount, + EveUnityFeedbackEvent pickupCollection, RendererFact[] playerRendererFacts) { var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH"); @@ -753,6 +802,15 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests tractorBeamPower = tractorBeamPower, tractorReleasedPower = tractorReleasedPower, tractorBeamParticleSystemCount = tractorBeamParticleSystemCount, + pickupCollection = pickupCollection != null && pickupCollectionEventCount == 1 && + pickupCollection.CargoQuantityAfter - pickupCollection.CargoQuantityBefore == pickupCollection.ScalarValue, + initialPickupEntityCount = initialPickupEntityCount, + finalPickupEntityCount = finalPickupEntityCount, + pickupCollectionEventCount = pickupCollectionEventCount, + pickupItemKey = pickupCollection?.ItemKey ?? "", + pickupQuantity = (float)(pickupCollection?.ScalarValue ?? 0), + cargoQuantityBeforePickup = (float)(pickupCollection?.CargoQuantityBefore ?? 0), + cargoQuantityAfterPickup = (float)(pickupCollection?.CargoQuantityAfter ?? 0), playerRenderers = playerRendererFacts, mapChannelRendererCount = mapChannelRendererCount, mapChangedPixels = mapChangedPixels, @@ -803,6 +861,14 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests public float tractorBeamPower; public float tractorReleasedPower; public int tractorBeamParticleSystemCount; + public bool pickupCollection; + public int initialPickupEntityCount; + public int finalPickupEntityCount; + public int pickupCollectionEventCount; + public string pickupItemKey; + public float pickupQuantity; + public float cargoQuantityBeforePickup; + public float cargoQuantityAfterPickup; public RendererFact[] playerRenderers; public int mapChannelRendererCount; public int mapChangedPixels; diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index df32b8b..0eca353 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -9,7 +9,7 @@ "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.13", "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#eveunity-surface-v0.2.2", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.31", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.32", "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1" }, "testables": [ diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index a4b2b92..fbe0822 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -142,7 +142,7 @@ "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.31", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#eveunity-scene-v0.3.32", "depth": 0, "source": "git", "dependencies": { @@ -152,7 +152,7 @@ "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0" }, - "hash": "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" + "hash": "817912a041ac1911e1e27a4b88617754d90ad4b7" }, "org.gamecult.eve.unity-uitoolkit": { "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.1", diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index e9b71e2..27875c4 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -212,6 +212,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests try { + var providerReadyPath = Environment.GetEnvironmentVariable("EVEUNITY_PROVIDER_READY_PATH"); + if (!string.IsNullOrWhiteSpace(providerReadyPath)) + File.WriteAllText(providerReadyPath, DateTimeOffset.UtcNow.ToString("O")); provider = root.AddComponent(); provider.Configure( rendezvousEndpoint, @@ -220,7 +223,7 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests surfaceId, requiredSurfaceKind: "interactive-world", clientRuntimeId: $"eve-unity-test-{Guid.NewGuid():N}"); - var publicationDeadline = Time.realtimeSinceStartup + 10f; + var publicationDeadline = Time.realtimeSinceStartup + 30f; while (true) { var published = false; @@ -232,6 +235,9 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests catch (InvalidOperationException) when (Time.realtimeSinceStartup < publicationDeadline) { } + catch (TimeoutException) when (Time.realtimeSinceStartup < publicationDeadline) + { + } if (published) break; yield return new WaitForSecondsRealtime(0.1f); } diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md index ec3f4f0..475d987 100644 --- a/docs/aetheria-released-package-witness.md +++ b/docs/aetheria-released-package-witness.md @@ -4,8 +4,8 @@ The warm-cache released-package witness passes with: -- `org.gamecult.eve.unity-scene` `0.3.31`, commit - `140e1bd963a0033e66777a3b2c5fe6e9c97dfe32`; +- `org.gamecult.eve.unity-scene` `0.3.32`, commit + `817912a041ac1911e1e27a4b88617754d90ad4b7`; - `org.gamecult.eve.surface` `0.2.2`, from the same commit; - `org.gamecult.eve.unity-uitoolkit` `0.1.1`, commit `4d0cbe0185bdc4fc65eb63503a7c5cb578539669`; @@ -16,7 +16,8 @@ The warm-cache released-package witness passes with: Evidence is in `artifacts/aetheria-daemon-native-0326-warm`. The PlayMode test passed and recorded provider-owned reconciled movement, look, targeting, -tractor press, tractor release, and action receipts. The daemon-owned look direction reached the SoA body rotation, +tractor press, tractor release, contact-gated cargo collection, and action +receipts. The daemon-owned look direction reached the SoA body rotation, and the generic aim presentation rendered at the advertised 50-unit minimum convergence distance. The advertised skybox material and reflection cubemap resolved from the provider bundle as their required native types and became the @@ -45,16 +46,20 @@ daemon lock progress `1.0`; Unity does not manufacture or smooth that value. Camera-channel facts: - provider-authored player renderers: `13`, including the lowered tractor effect; -- pilot changed pixels: `230,105`; -- pilot average luminance: `0.004590`; -- pilot bright pixels: `1,620`; +- pilot changed pixels: `230,302`; +- pilot average luminance: `0.002843`; +- pilot bright pixels: `1,040`; - map-channel renderers: `11`; -- map changed pixels: `5,187`; +- map changed pixels: `3,921`; - native cockpit progress bars: `7`; - daemon tractor power at capture: `1.0` after the held input completed its authored ramp; - daemon tractor power after the advertised release: `0`; - provider tractor particle systems: `1`; +- pickup entities before/after collection: `1` / `0`; +- provider `pickup.collected` events: exactly `1`; +- collected item and quantity: `scrap-metal`, `1`; +- authoritative cargo quantity before/after: `0` / `1`; - the pilot camera excludes the advertised map layer; - the player prefab's embedded layer-14 map icon contributes exactly `0` pilot pixels; @@ -86,6 +91,14 @@ build removes the fossil's embedded tractor object from player/ship prefabs, so the standalone `beam.presentation` prefab is the only tractor renderer. Its cyan/yellow dotted band is visible in the pilot capture and absent from the map capture. +The same released run begins with one provider-owned pickup in the entity SoA. +Held tractor input produces a Ymir Begin contact fact; the daemon consumes that +fact once, removes the pickup once, changes cargo from `0` to `1`, and emits one +`pickup.collected` feedback event carrying the exact delta. 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. 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 b4890ec..b412222 100644 --- a/scripts/run-aetheria-daemon-world-witness.ps1 +++ b/scripts/run-aetheria-daemon-world-witness.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = "Stop" -$expectedEveUnityCommit = "140e1bd963a0033e66777a3b2c5fe6e9c97dfe32" +$expectedEveUnityCommit = "817912a041ac1911e1e27a4b88617754d90ad4b7" $expectedEveUnityUiToolkitCommit = "4d0cbe0185bdc4fc65eb63503a7c5cb578539669" $expectedCultLibCommit = "feb5c71513e71d681699f462fe3682b3168c6f73" $repoRoot = Split-Path -Parent $PSScriptRoot @@ -26,6 +26,7 @@ $daemonLogPath = Join-Path $outputRoot "aetheria-daemon.log" $capturePath = Join-Path $outputRoot "aetheria-daemon-world.png" $mapCapturePath = Join-Path $outputRoot "aetheria-daemon-map.png" $factsPath = Join-Path $outputRoot "witness-facts.json" +$providerReadyPath = Join-Path $outputRoot "provider-ready.txt" $witnessPath = Join-Path $outputRoot "runtime-witness.json" $replicaPath = Join-Path $outputRoot "eve-unity-replica.cc" $assetCachePath = Join-Path $outputRoot "asset-cache" @@ -44,7 +45,7 @@ if ((Get-FileHash -LiteralPath $canonicalWitnessTest -Algorithm SHA256).Hash -ne } New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null -foreach ($priorArtifact in @($resultsPath, $capturePath, $mapCapturePath, $factsPath, $witnessPath)) { +foreach ($priorArtifact in @($resultsPath, $capturePath, $mapCapturePath, $factsPath, $providerReadyPath, $witnessPath)) { if (Test-Path -LiteralPath $priorArtifact) { Remove-Item -LiteralPath $priorArtifact -Force } } if ($CacheState -eq "cold" -and (Test-Path -LiteralPath $assetCachePath)) { @@ -132,6 +133,37 @@ $daemonArguments = @( ) $env:AETHERIA_TRACE_EVE_SNAPSHOTS = "1" $env:AETHERIA_TRACE_CLIENT_RUDP = "1" +$env:EVEUNITY_RENDEZVOUS_ENDPOINT = "rudp://127.0.0.1:$Port" +$env:EVEUNITY_PROVIDER_ID = "aetheria" +Remove-Item Env:EVEUNITY_SURFACE_ID -ErrorAction SilentlyContinue +$env:EVEUNITY_REPLICA_PATH = $replicaPath +$env:EVEUNITY_AETHERIA_CAPTURE_PATH = $capturePath +$env:EVEUNITY_AETHERIA_MAP_CAPTURE_PATH = $mapCapturePath +$env:EVEUNITY_DISABLE_AUTO_LAUNCHER = "1" +$env:EVEUNITY_ASSET_CACHE_PATH = $assetCachePath +$env:EVEUNITY_WITNESS_FACTS_PATH = $factsPath +$env:EVEUNITY_PROVIDER_READY_PATH = $providerReadyPath +$arguments = @( + "-batchmode", "-projectPath", $projectRoot, + "-runTests", "-testPlatform", "PlayMode", + "-assemblyNames", "GameCult.EveUnity.GenericClient.PlayModeTests", + "-testFilter", "GenericCultMeshClientLowersAndMovesAdvertisedWorld", + "-testResults", $resultsPath, "-logFile", $unityLogPath +) +$unity = Start-Process -FilePath $UnityExe -ArgumentList $arguments -WorkingDirectory $repoRoot -PassThru -WindowStyle Hidden +Write-Host "Unity witness PID: $($unity.Id)" +Write-Host "Unity log: $unityLogPath" +$clientReady = $false +for ($attempt = 0; $attempt -lt 240; $attempt++) { + if ($unity.HasExited) { throw "Unity exited before reaching provider connection. See $unityLogPath" } + if (Test-Path -LiteralPath $providerReadyPath) { $clientReady = $true; break } + Start-Sleep -Milliseconds 500 +} +if (-not $clientReady) { + Stop-Process -Id $unity.Id -Force -ErrorAction SilentlyContinue + throw "Unity did not reach provider connection within 120 seconds. See $unityLogPath" +} + $daemon = Start-Process -FilePath "dotnet" -ArgumentList $daemonArguments -PassThru -WindowStyle Hidden ` -RedirectStandardOutput $daemonLogPath -RedirectStandardError (Join-Path $outputRoot "aetheria-daemon.error.log") Write-Host "Aetheria daemon PID: $($daemon.Id)" @@ -148,23 +180,6 @@ try { } if (-not $ready) { throw "Aetheria daemon did not open CultMesh port $Port within 120 seconds. See $daemonLogPath" } - $env:EVEUNITY_RENDEZVOUS_ENDPOINT = "rudp://127.0.0.1:$Port" - $env:EVEUNITY_PROVIDER_ID = "aetheria" - Remove-Item Env:EVEUNITY_SURFACE_ID -ErrorAction SilentlyContinue - $env:EVEUNITY_REPLICA_PATH = $replicaPath - $env:EVEUNITY_AETHERIA_CAPTURE_PATH = $capturePath - $env:EVEUNITY_AETHERIA_MAP_CAPTURE_PATH = $mapCapturePath - $env:EVEUNITY_DISABLE_AUTO_LAUNCHER = "1" - $env:EVEUNITY_ASSET_CACHE_PATH = $assetCachePath - $env:EVEUNITY_WITNESS_FACTS_PATH = $factsPath - $arguments = @( - "-batchmode", "-projectPath", $projectRoot, - "-runTests", "-testPlatform", "PlayMode", - "-assemblyNames", "GameCult.EveUnity.GenericClient.PlayModeTests", - "-testFilter", "GenericCultMeshClientLowersAndMovesAdvertisedWorld", - "-testResults", $resultsPath, "-logFile", $unityLogPath - ) - $unity = Start-Process -FilePath $UnityExe -ArgumentList $arguments -WorkingDirectory $repoRoot -PassThru -WindowStyle Hidden if (-not $unity.WaitForExit(300000)) { Stop-Process -Id $unity.Id -Force -ErrorAction SilentlyContinue throw "Unity witness exceeded 300 seconds. See $unityLogPath" @@ -296,7 +311,8 @@ try { Write-Host "Cache-state witness: $stateWitnessPath" } 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 -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 -ErrorAction SilentlyContinue Remove-Item Env:AETHERIA_TRACE_EVE_SNAPSHOTS -ErrorAction SilentlyContinue }