2026-07-10 03:34:56 +01:00
using System ;
using System.Collections ;
using System.Collections.Generic ;
using System.IO ;
2026-07-14 03:59:45 +01:00
using System.Linq ;
2026-07-19 23:30:44 +01:00
using System.Threading ;
using System.Threading.Tasks ;
2026-07-10 04:03:09 +01:00
using GameCult.Caching ;
using GameCult.Caching.MessagePack ;
2026-07-10 03:34:56 +01:00
using GameCult.Eve.Surface ;
2026-07-15 09:49:31 +01:00
using GameCult.Eve.UnityUIToolkit ;
2026-07-10 03:34:56 +01:00
using GameCult.Eve.UnityScene ;
2026-07-15 17:31:02 +01:00
using GameCult.Eve.UnityScene.Fields ;
2026-07-16 14:46:49 +01:00
using GameCult.Mesh ;
2026-07-19 23:30:44 +01:00
using GameCult.Mesh.Quic.Native ;
2026-07-10 03:34:56 +01:00
using NUnit.Framework ;
using UnityEngine ;
2026-07-16 19:01:09 +01:00
using UnityEngine.Rendering ;
2026-07-16 17:53:40 +01:00
using UnityEngine.Rendering.Universal ;
2026-07-10 03:34:56 +01:00
using UnityEngine.TestTools ;
2026-07-15 09:49:31 +01:00
using UnityEngine.UIElements ;
2026-07-19 21:51:30 +01:00
using Stopwatch = System . Diagnostics . Stopwatch ;
2026-07-10 03:34:56 +01:00
namespace GameCult.EveUnity.GenericClient.PlayModeTests
{
public sealed class GenericWorldCaptureTests
{
2026-07-10 04:03:09 +01:00
[Test]
public void CoreProviderDocumentsRoundTripThroughCultCacheSerialization ( )
{
var advertisement = new EveProviderAdvertisementDocument (
"eve.world-smoke" ,
"eve-world-smoke" ,
"eve.local" ,
"Generic world" ,
"game.daemon" ,
"127.0.0.1:7777" ,
"2026-07-10T00:00:00Z" ,
new EveProviderFreshness ( "fresh" , "2026-07-10T00:00:00Z" , 5000 ) ,
new [ ] { EveSurfaceDocument . SchemaId , EveCommandReceiptDocument . SchemaId } ,
Array . Empty < EveProviderWitness > ( ) ,
new [ ]
{
new EveAdvertisedSurface (
"eve.world-smoke.surface" ,
EveSurfaceDocument . SchemaId ,
"surface:eve.world-smoke" ,
"cultmesh-record" ,
"active" ,
"interactive-world" ,
new EveWorldInteractionAdvertisement (
"provider-authored-world-surface" ,
Array . Empty < string > ( ) ,
"world.commands" ,
"commands:eve.world-smoke" ,
EveCommandReceiptDocument . SchemaId ,
"receipts:eve.world-smoke" ,
"assets:eve.world-smoke" ,
new [ ] { "unity-scene" } ,
"provider-owns-world-state-command-acceptance-and-receipts" ) )
} ,
Array . Empty < EveAdvertisedCommand > ( ) ) ;
var payload = CultDocumentMessagePackSerialization . Serialize ( advertisement ) ;
var decoded = CultDocumentMessagePackSerialization . Deserialize < EveProviderAdvertisementDocument > ( payload ) ;
Assert . That ( decoded . ProviderId , Is . EqualTo ( "eve.world-smoke" ) ) ;
Assert . That ( decoded . Surfaces . Count , Is . EqualTo ( 1 ) ) ;
Assert . That ( decoded . Surfaces [ 0 ] . WorldInteraction . CommandRecordRef , Is . EqualTo ( "commands:eve.world-smoke" ) ) ;
}
2026-07-10 03:34:56 +01:00
[UnityTest]
public IEnumerator GenericClientRendersWorldSmokeAndEmitsCommand ( )
{
var hostObject = new GameObject ( "EveUnity Generic Client" ) ;
var sceneRoot = new GameObject ( "World Projection" ) ;
var provider = hostObject . AddComponent < WorldSmokeProvider > ( ) ;
var host = hostObject . AddComponent < EveUnityPlayableWorldClientHost > ( ) ;
var cameraObject = new GameObject ( "Capture Camera" ) ;
var lightObject = new GameObject ( "World Light" ) ;
var ground = GameObject . CreatePrimitive ( PrimitiveType . Plane ) ;
RenderTexture target = null ;
Texture2D pixels = null ;
try
{
provider . CurrentDocument = WorldDocument ( ) ;
host . Configure ( sceneRoot . transform , provider , provider ) ;
var presentation = host . Connect ( ) ;
Assert . That ( presentation . ActiveEntities , Is . EqualTo ( 3 ) ) ;
Assert . That (
sceneRoot . GetComponentsInChildren < EveUnityPlayableWorldEntityMarker > ( ) ,
Has . Length . EqualTo ( 3 ) ) ;
foreach ( var marker in sceneRoot . GetComponentsInChildren < EveUnityPlayableWorldEntityMarker > ( ) )
{
var renderer = marker . GetComponent < Renderer > ( ) ;
renderer . material . color = marker . Controllable
? new Color ( 0.18f , 0.75f , 1f )
: marker . EntityKind = = "enemy"
? new Color ( 1f , 0.25f , 0.2f )
: new Color ( 1f , 0.78f , 0.15f ) ;
}
ground . name = "Projection Ground" ;
ground . transform . position = new Vector3 ( 0f , - 0.65f , 3f ) ;
ground . transform . localScale = new Vector3 ( 2.2f , 1f , 2.2f ) ;
ground . GetComponent < Renderer > ( ) . material . color = new Color ( 0.12f , 0.16f , 0.18f ) ;
host . SubmitMoveVectorIntent ( "player" , 0f , 1f ) ;
Assert . That ( provider . Submitted , Has . Count . EqualTo ( 1 ) ) ;
Assert . That ( provider . Submitted [ 0 ] . ProviderId , Is . EqualTo ( "eve.world-smoke" ) ) ;
var light = lightObject . AddComponent < Light > ( ) ;
light . type = LightType . Directional ;
light . intensity = 1.4f ;
lightObject . transform . rotation = Quaternion . Euler ( 45f , - 35f , 0f ) ;
var camera = cameraObject . AddComponent < Camera > ( ) ;
camera . clearFlags = CameraClearFlags . SolidColor ;
camera . backgroundColor = new Color ( 0.035f , 0.055f , 0.08f , 1f ) ;
camera . transform . position = new Vector3 ( 10f , 8f , - 12f ) ;
camera . transform . LookAt ( new Vector3 ( 0f , 1f , 3f ) ) ;
target = new RenderTexture ( 640 , 360 , 24 , RenderTextureFormat . ARGB32 ) ;
camera . targetTexture = target ;
yield return null ;
camera . Render ( ) ;
RenderTexture . active = target ;
pixels = new Texture2D ( 640 , 360 , TextureFormat . RGB24 , false ) ;
pixels . ReadPixels ( new Rect ( 0 , 0 , 640 , 360 ) , 0 , 0 ) ;
pixels . Apply ( ) ;
var changedPixels = 0 ;
var background = camera . backgroundColor ;
foreach ( var pixel in pixels . GetPixels ( ) )
{
if ( Mathf . Abs ( pixel . r - background . r ) +
Mathf . Abs ( pixel . g - background . g ) +
Mathf . Abs ( pixel . b - background . b ) > 0.08f )
changedPixels + + ;
}
Assert . That ( changedPixels , Is . GreaterThan ( 500 ) ) ;
var capturePath = Environment . GetEnvironmentVariable ( "EVEUNITY_CAPTURE_PATH" ) ;
if ( string . IsNullOrWhiteSpace ( capturePath ) )
{
capturePath = Path . GetFullPath ( Path . Combine (
Application . dataPath ,
".." ,
".." ,
"artifacts" ,
"playmode" ,
"world-smoke.png" ) ) ;
}
Directory . CreateDirectory ( Path . GetDirectoryName ( capturePath ) ) ;
File . WriteAllBytes ( capturePath , pixels . EncodeToPNG ( ) ) ;
Assert . That ( new FileInfo ( capturePath ) . Length , Is . GreaterThan ( 1024 ) ) ;
}
finally
{
RenderTexture . active = null ;
var camera = cameraObject . GetComponent < Camera > ( ) ;
if ( camera ! = null ) camera . targetTexture = null ;
if ( target ! = null ) UnityEngine . Object . DestroyImmediate ( target ) ;
if ( pixels ! = null ) UnityEngine . Object . DestroyImmediate ( pixels ) ;
UnityEngine . Object . DestroyImmediate ( lightObject ) ;
UnityEngine . Object . DestroyImmediate ( cameraObject ) ;
UnityEngine . Object . DestroyImmediate ( ground ) ;
UnityEngine . Object . DestroyImmediate ( sceneRoot ) ;
UnityEngine . Object . DestroyImmediate ( hostObject ) ;
}
}
2026-07-10 04:39:16 +01:00
[UnityTest]
2026-07-10 06:37:33 +01:00
[Timeout(300000)]
2026-07-10 04:39:16 +01:00
public IEnumerator GenericCultMeshClientLowersAndMovesAdvertisedWorld ( )
{
2026-07-10 11:04:26 +01:00
var rendezvousEndpoint = Environment . GetEnvironmentVariable ( "EVEUNITY_RENDEZVOUS_ENDPOINT" ) ;
if ( string . IsNullOrWhiteSpace ( rendezvousEndpoint ) )
Assert . Ignore ( "EVEUNITY_RENDEZVOUS_ENDPOINT is not configured for the live provider witness." ) ;
2026-07-10 04:39:16 +01:00
2026-07-10 11:04:26 +01:00
var providerId = Environment . GetEnvironmentVariable ( "EVEUNITY_PROVIDER_ID" ) ? ? "" ;
var surfaceId = Environment . GetEnvironmentVariable ( "EVEUNITY_SURFACE_ID" ) ? ? "" ;
2026-07-10 04:39:16 +01:00
var replicaPath = Environment . GetEnvironmentVariable ( "EVEUNITY_REPLICA_PATH" ) ? ?
Path . Combine ( Application . temporaryCachePath , $"eve-unity-{Guid.NewGuid():N}.cc" ) ;
var capturePath = Environment . GetEnvironmentVariable ( "EVEUNITY_AETHERIA_CAPTURE_PATH" ) ? ?
Path . Combine ( Application . temporaryCachePath , "aetheria-daemon-world.png" ) ;
2026-07-14 03:59:45 +01:00
var mapCapturePath = Environment . GetEnvironmentVariable ( "EVEUNITY_AETHERIA_MAP_CAPTURE_PATH" ) ? ?
Path . Combine ( Application . temporaryCachePath , "aetheria-daemon-map.png" ) ;
2026-07-10 04:39:16 +01:00
var root = new GameObject ( "Generic Eve World Projection" ) ;
var cameraObject = new GameObject ( "Generic Eve Capture Camera" ) ;
2026-07-14 03:59:45 +01:00
var mapCameraObject = new GameObject ( "Generic Eve Map Camera" ) ;
2026-07-10 04:39:16 +01:00
RenderTexture target = null ;
Texture2D pixels = null ;
2026-07-10 14:13:53 +01:00
EveUnityCultMeshPlayableWorldProvider provider = null ;
2026-07-14 03:59:45 +01:00
EveUnityPlayableWorldClientHost host = null ;
2026-07-10 04:39:16 +01:00
EveUnityPlayableWorldRuntime runtime = null ;
2026-07-10 07:14:44 +01:00
WitnessReceipt movementReceipt = null ;
2026-07-15 06:14:49 +01:00
WitnessReceipt lookReceipt = null ;
2026-07-10 07:14:44 +01:00
WitnessReceipt focusReceipt = null ;
2026-07-15 11:04:11 +01:00
WitnessReceipt tractorReceipt = null ;
2026-07-15 11:14:22 +01:00
WitnessReceipt tractorReleaseReceipt = null ;
2026-07-10 07:14:44 +01:00
WitnessReceipt actionReceipt = null ;
2026-07-19 16:01:21 +01:00
WitnessReceipt tradePurchaseReceipt = null ;
WitnessReceipt tradeSaleReceipt = null ;
bool tradeRoundTrip = false ;
string tradeItemKey = "" ;
2026-07-15 03:21:28 +01:00
EveUnityShotReceipt observedShot = null ;
EveUnityCombatPresentation observedCombat = null ;
float shieldRatioBefore = 0f ;
float hullRatioBefore = 0f ;
2026-07-10 07:14:44 +01:00
long initialVersion = 0 ;
float movementDistance = 0f ;
2026-07-15 06:14:49 +01:00
float aimDotDistance = 0f ;
2026-07-15 09:49:31 +01:00
int cockpitProgressCount = 0 ;
2026-07-15 11:04:11 +01:00
float tractorBeamPower = 0f ;
2026-07-15 11:14:22 +01:00
float tractorReleasedPower = 1f ;
2026-07-15 11:04:11 +01:00
int tractorBeamParticleSystemCount = 0 ;
2026-07-15 11:45:50 +01:00
EveUnityFeedbackEvent pickupCollection = null ;
2026-07-16 22:26:29 +01:00
EveUnityFeedbackEvent pickupRejection = null ;
2026-07-15 11:45:50 +01:00
int pickupCollectionEventCount = 0 ;
2026-07-16 22:26:29 +01:00
int pickupRejectionEventCount = 0 ;
var pickupRejectionEventIds = new HashSet < string > ( StringComparer . Ordinal ) ;
2026-07-15 11:45:50 +01:00
int initialPickupEntityCount = 0 ;
int finalPickupEntityCount = - 1 ;
2026-07-16 03:34:47 +01:00
bool destructionPickupObserved = false ;
int maximumTrajectoryCount = 0 ;
bool hitMarkerObserved = false ;
var witnessProfile = Environment . GetEnvironmentVariable ( "EVEUNITY_WITNESS_PROFILE" ) ? ?
"full-session-gameplay" ;
Assert . That ( witnessProfile ,
Is . EqualTo ( "cold-start-lowering" ) . Or . EqualTo ( "full-session-gameplay" ) ,
"The witness profile must name an explicit proof boundary." ) ;
var requireSessionGameplay = string . Equals (
witnessProfile ,
"full-session-gameplay" ,
StringComparison . Ordinal ) ;
2026-07-16 22:26:29 +01:00
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 ) ;
2026-07-16 03:34:47 +01:00
EveUnityAimPresentationRenderer aimRenderer = null ;
EveUnityBeamPresentation beamPresentation = null ;
EveUnityBeamPresentationRenderer beamRenderer = null ;
2026-07-16 04:41:06 +01:00
EveUnityShotTrajectoryRenderer trajectories = null ;
2026-07-10 04:39:16 +01:00
try
{
2026-07-15 16:36:32 +01:00
var providerReadyPath = Environment . GetEnvironmentVariable ( "EVEUNITY_PROVIDER_READY_PATH" ) ;
if ( ! string . IsNullOrWhiteSpace ( providerReadyPath ) )
File . WriteAllText ( providerReadyPath , DateTimeOffset . UtcNow . ToString ( "O" ) ) ;
2026-07-10 14:13:53 +01:00
provider = root . AddComponent < EveUnityCultMeshPlayableWorldProvider > ( ) ;
provider . Configure (
2026-07-10 11:04:26 +01:00
rendezvousEndpoint ,
2026-07-10 14:13:53 +01:00
replicaPath ,
2026-07-10 11:04:26 +01:00
providerId ,
surfaceId ,
2026-07-10 14:13:53 +01:00
requiredSurfaceKind : "interactive-world" ,
clientRuntimeId : $"eve-unity-test-{Guid.NewGuid():N}" ) ;
2026-07-19 00:02:51 +01:00
var preparationDeadline = Time . realtimeSinceStartup + 30f ;
while ( true )
{
var preparation = provider . PrepareAsync ( ) ;
while ( ! preparation . IsCompleted & & Time . realtimeSinceStartup < preparationDeadline )
yield return new WaitForSecondsRealtime ( 0.1f ) ;
if ( ! preparation . IsCompleted )
throw new TimeoutException ( "Timed out preparing the advertised Eve provider." ) ;
var prepared = false ;
try
{
preparation . GetAwaiter ( ) . GetResult ( ) ;
prepared = true ;
}
catch ( InvalidOperationException ) when ( Time . realtimeSinceStartup < preparationDeadline )
{
}
catch ( TimeoutException ) when ( Time . realtimeSinceStartup < preparationDeadline )
{
}
if ( prepared ) break ;
yield return new WaitForSecondsRealtime ( 0.1f ) ;
}
2026-07-15 16:36:32 +01:00
var publicationDeadline = Time . realtimeSinceStartup + 30f ;
2026-07-10 04:39:16 +01:00
while ( true )
{
var published = false ;
try
{
2026-07-10 14:13:53 +01:00
provider . Refresh ( ) ;
2026-07-10 04:39:16 +01:00
published = true ;
}
catch ( InvalidOperationException ) when ( Time . realtimeSinceStartup < publicationDeadline )
{
}
2026-07-15 16:36:32 +01:00
catch ( TimeoutException ) when ( Time . realtimeSinceStartup < publicationDeadline )
{
}
2026-07-10 04:39:16 +01:00
if ( published ) break ;
yield return new WaitForSecondsRealtime ( 0.1f ) ;
}
2026-07-10 14:13:53 +01:00
Assert . That ( provider . Selection , Is . Not . Null ) ;
Assert . That ( provider . Selection . VerseId , Is . Not . Empty ) ;
Assert . That ( provider . Selection . ProviderId , Is . Not . Empty ) ;
Assert . That ( provider . Selection . SurfaceId , Is . Not . Empty ) ;
2026-07-19 23:30:44 +01:00
var quicProof = ReceiveAdvertisedRealtimeFrameAsync (
rendezvousEndpoint ,
provider . Selection . VerseId ) ;
var quicDeadline = Time . realtimeSinceStartup + 15f ;
while ( ! quicProof . IsCompleted & & Time . realtimeSinceStartup < quicDeadline )
yield return new WaitForSecondsRealtime ( 0.05f ) ;
if ( ! quicProof . IsCompleted )
throw new TimeoutException ( "Timed out receiving advertised CultMesh QUIC state in Unity." ) ;
var quicFrame = quicProof . GetAwaiter ( ) . GetResult ( ) ;
Assert . That ( quicFrame . Delivery , Is . EqualTo ( CultMeshRealtimeDelivery . LatestOnly ) ) ;
Assert . That ( quicFrame . BodyId , Is . Not . Empty ) ;
Assert . That ( quicFrame . SchemaId , Is . Not . Empty ) ;
2026-07-19 21:51:30 +01:00
Assert . That ( provider . CurrentAssetBodyTransportKind , Is . Null ,
"Publishing the provider catalog eagerly transferred an asset bundle." ) ;
2026-07-14 03:59:45 +01:00
host = root . AddComponent < EveUnityPlayableWorldClientHost > ( ) ;
host . Configure (
2026-07-10 05:05:27 +01:00
root . transform ,
2026-07-10 14:13:53 +01:00
provider ,
provider ,
2026-07-14 03:59:45 +01:00
provider ,
2026-07-10 14:13:53 +01:00
provider ,
provider ) ;
2026-07-14 03:59:45 +01:00
host . Connect ( ) ;
2026-07-20 05:22:52 +01:00
var mountedOverlay = root . GetComponent < EveUnityUiToolkitOverlay > ( ) ;
Assert . That ( mountedOverlay , Is . Not . Null ) ;
var cockpit = mountedOverlay . Root . Q < VisualElement > ( "aetheria.daemon.game.cockpit" ) ;
2026-07-15 09:49:31 +01:00
Assert . That ( cockpit , Is . Not . Null , "The provider did not publish a generic cockpit overlay." ) ;
cockpitProgressCount = cockpit . Query < ProgressBar > ( ) . ToList ( ) . Count ;
2026-07-15 11:04:11 +01:00
Assert . That ( cockpitProgressCount , Is . GreaterThanOrEqualTo ( 7 ) ) ;
2026-07-20 05:22:52 +01:00
Assert . That ( mountedOverlay . Root . Q < VisualElement > ( "aetheria.daemon.game.frame" ) . style . display . value ,
Is . EqualTo ( DisplayStyle . None ) , "Daemon diagnostics leaked into the mounted pilot UI." ) ;
Assert . That ( mountedOverlay . Root . Q < VisualElement > ( "aetheria.daemon.game.main_menu" ) . style . display . value ,
Is . EqualTo ( DisplayStyle . None ) , "The closed main menu leaked into the mounted pilot UI." ) ;
Assert . That ( provider . CurrentDocument . SurfaceDocument . Surface . Root . Children
. Single ( component = > component . Id = = "aetheria.daemon.game.frame" ) . Layout [ "display" ] ,
Is . EqualTo ( "none" ) , "Daemon diagnostics leaked into the pilot UI." ) ;
2026-07-16 04:41:06 +01:00
trajectories = root . GetComponent < EveUnityShotTrajectoryRenderer > ( ) ;
Assert . That ( trajectories , Is . Not . Null ) ;
host . ShotAvailable + = shot = >
{
observedShot = shot ;
maximumTrajectoryCount = Math . Max ( maximumTrajectoryCount , trajectories . ActiveTrajectoryCount ) ;
} ;
2026-07-14 03:59:45 +01:00
runtime = host . Runtime ;
Assert . That ( runtime , Is . Not . Null ) ;
2026-07-10 04:39:16 +01:00
Assert . That ( runtime . ActiveWorld . PlayerEntityId , Is . Not . Empty ) ;
2026-07-14 03:59:45 +01:00
var entityDeadline = Time . realtimeSinceStartup + 12f ;
while ( Time . realtimeSinceStartup < entityDeadline & &
( host . PresentedEntities ? . CurrentGeneration = = null | |
host . PresentedEntities . CurrentGeneration . Entities . Count = = 0 ) )
{
yield return new WaitForSecondsRealtime ( 0.1f ) ;
host . Refresh ( ) ;
}
Assert . That ( host . PresentedEntities ? . CurrentGeneration , Is . Not . Null ) ;
Assert . That ( host . PresentedEntities . CurrentGeneration . Entities . Count , Is . GreaterThan ( 0 ) ) ;
2026-07-15 11:45:50 +01:00
initialPickupEntityCount = host . PresentedEntities . CurrentGeneration . Entities . Count ( entity = >
string . Equals ( entity . EntityKind , "pickup" , StringComparison . Ordinal ) ) ;
2026-07-16 03:34:47 +01:00
if ( requireSessionGameplay )
Assert . That ( initialPickupEntityCount , Is . Zero ,
"The witness scenario must obtain salvage from daemon-owned destruction, not a boot-seeded pickup." ) ;
2026-07-16 22:26:29 +01:00
var playerId = runtime . ActiveWorld . PlayerEntityId ;
var playerFact = host . PresentedEntities . CurrentGeneration . Entities
. Single ( entity = > entity . EntityId = = playerId ) ;
2026-07-15 11:45:50 +01:00
runtime . FeedbackAvailable + = feedback = >
{
2026-07-16 22:26:29 +01:00
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 ;
}
2026-07-15 11:45:50 +01:00
} ;
2026-07-10 04:39:16 +01:00
var marker = FindMarker ( root , playerId ) ;
2026-07-19 16:01:21 +01:00
if ( requireSessionGameplay )
{
2026-07-20 00:22:32 +01:00
EveUnitySceneCommandReceipt observedDockReceipt = null ;
string dockCommandId = null ;
runtime . ReceiptAvailable + = receipt = >
{
if ( receipt . CommandId = = dockCommandId )
observedDockReceipt = receipt ;
} ;
host . SubmitAdvertisedActionIntent ( playerId , "simulation.step" ) ;
2026-07-19 16:01:21 +01:00
var dockVersion = runtime . ActiveVersion ;
var dock = host . SubmitAdvertisedActionIntent ( playerId , "pilot.dock" ) ;
2026-07-20 00:22:32 +01:00
dockCommandId = dock . CommandId ;
2026-07-19 16:01:21 +01:00
var dockDeadline = Time . realtimeSinceStartup + 12f ;
while ( Time . realtimeSinceStartup < dockDeadline & &
2026-07-20 00:22:32 +01:00
( observedDockReceipt = = null | |
! string . Equals ( observedDockReceipt . State , "reconciled" , StringComparison . OrdinalIgnoreCase ) | |
runtime . ActiveVersion < observedDockReceipt . SourceVersion | |
2026-07-19 16:01:21 +01:00
! ( host . InputCapability ? . Actions ? ? Array . Empty < EveInputActionDocument > ( ) ) . Any ( candidate = >
candidate ! = null & &
candidate . ActionId . StartsWith ( "trade.buy." , StringComparison . Ordinal ) & &
string . Equals ( candidate . Availability , "available" , StringComparison . OrdinalIgnoreCase ) ) ) )
{
yield return new WaitForSecondsRealtime ( 0.1f ) ;
runtime . Refresh ( ) ;
}
2026-07-20 00:22:32 +01:00
AssertReconciledProviderReceipt ( observedDockReceipt , dock . CommandId ,
2026-07-19 16:01:21 +01:00
provider . Selection . ProviderId , provider . Selection . SurfaceId , dockVersion ) ;
var buyAction = ( host . InputCapability ? . Actions ? ? Array . Empty < EveInputActionDocument > ( ) )
. FirstOrDefault ( candidate = > candidate ! = null & &
candidate . ActionId . StartsWith ( "trade.buy." , StringComparison . Ordinal ) & &
string . Equals ( candidate . Availability , "available" , StringComparison . OrdinalIgnoreCase ) ) ;
Assert . That ( buyAction , Is . Not . Null ,
"Docking did not expose an available provider-authored purchase action." ) ;
Assert . That ( buyAction . Payload . TryGetValue ( "itemKey" , out tradeItemKey ) , Is . True ) ;
Assert . That ( tradeItemKey , Is . Not . Empty ) ;
var purchaseVersion = runtime . ActiveVersion ;
var purchase = host . SubmitAdvertisedActionIntent ( playerId , buyAction . ActionId ) ;
var purchaseDeadline = Time . realtimeSinceStartup + 12f ;
while ( Time . realtimeSinceStartup < purchaseDeadline & &
( runtime . LastReceipt = = null | | runtime . LastReceipt . CommandId ! = purchase . CommandId | |
! string . Equals ( runtime . LastReceipt . State , "reconciled" , StringComparison . OrdinalIgnoreCase ) | |
runtime . ActiveVersion < runtime . LastReceipt . SourceVersion | |
! ( host . InputCapability ? . Actions ? ? Array . Empty < EveInputActionDocument > ( ) ) . Any ( candidate = >
candidate ! = null & &
candidate . ActionId . StartsWith ( "trade.sell." , StringComparison . Ordinal ) & &
candidate . Payload . TryGetValue ( "itemKey" , out var itemKey ) & &
string . Equals ( itemKey , tradeItemKey , StringComparison . Ordinal ) & &
string . Equals ( candidate . Availability , "available" , StringComparison . OrdinalIgnoreCase ) ) ) )
{
yield return new WaitForSecondsRealtime ( 0.1f ) ;
runtime . Refresh ( ) ;
}
AssertReconciledProviderReceipt ( runtime . LastReceipt , purchase . CommandId ,
provider . Selection . ProviderId , provider . Selection . SurfaceId , purchaseVersion ) ;
tradePurchaseReceipt = WitnessReceipt . From ( "trade-purchase" , runtime . LastReceipt ) ;
var sellAction = ( host . InputCapability ? . Actions ? ? Array . Empty < EveInputActionDocument > ( ) )
. FirstOrDefault ( candidate = > candidate ! = null & &
candidate . ActionId . StartsWith ( "trade.sell." , StringComparison . Ordinal ) & &
candidate . Payload . TryGetValue ( "itemKey" , out var itemKey ) & &
string . Equals ( itemKey , tradeItemKey , StringComparison . Ordinal ) & &
string . Equals ( candidate . Availability , "available" , StringComparison . OrdinalIgnoreCase ) ) ;
Assert . That ( sellAction , Is . Not . Null ,
"The authoritative purchase did not expose the purchased cargo as an available sale action." ) ;
var saleCapabilityVersion = host . InputCapability . Version ;
var saleVersion = runtime . ActiveVersion ;
var sale = host . SubmitAdvertisedActionIntent ( playerId , sellAction . ActionId ) ;
var saleDeadline = Time . realtimeSinceStartup + 12f ;
while ( Time . realtimeSinceStartup < saleDeadline & &
( runtime . LastReceipt = = null | | runtime . LastReceipt . CommandId ! = sale . CommandId | |
! string . Equals ( runtime . LastReceipt . State , "reconciled" , StringComparison . OrdinalIgnoreCase ) | |
runtime . ActiveVersion < runtime . LastReceipt . SourceVersion | |
host . InputCapability . Version < = saleCapabilityVersion | |
( host . InputCapability . Actions ? ? Array . Empty < EveInputActionDocument > ( ) ) . Any ( candidate = >
candidate ! = null & &
candidate . ActionId . StartsWith ( "trade.sell." , StringComparison . Ordinal ) & &
candidate . Payload . TryGetValue ( "itemKey" , out var itemKey ) & &
string . Equals ( itemKey , tradeItemKey , StringComparison . Ordinal ) ) ) )
{
yield return new WaitForSecondsRealtime ( 0.1f ) ;
runtime . Refresh ( ) ;
}
AssertReconciledProviderReceipt ( runtime . LastReceipt , sale . CommandId ,
provider . Selection . ProviderId , provider . Selection . SurfaceId , saleVersion ) ;
tradeSaleReceipt = WitnessReceipt . From ( "trade-sale" , runtime . LastReceipt ) ;
tradeRoundTrip = ! ( host . InputCapability . Actions ? ? Array . Empty < EveInputActionDocument > ( ) ) . Any ( candidate = >
candidate ! = null & &
candidate . ActionId . StartsWith ( "trade.sell." , StringComparison . Ordinal ) & &
candidate . Payload . TryGetValue ( "itemKey" , out var itemKey ) & &
string . Equals ( itemKey , tradeItemKey , StringComparison . Ordinal ) ) ;
Assert . That ( tradeRoundTrip , Is . True ,
"The sold unit remained in the daemon-authored cargo action catalog." ) ;
var resumeVersion = runtime . ActiveVersion ;
var resume = host . SubmitAdvertisedActionIntent ( playerId , "simulation.rate.realtime" ) ;
var resumeDeadline = Time . realtimeSinceStartup + 12f ;
while ( Time . realtimeSinceStartup < resumeDeadline & &
( runtime . LastReceipt = = null | | runtime . LastReceipt . CommandId ! = resume . CommandId | |
! string . Equals ( runtime . LastReceipt . State , "reconciled" , StringComparison . OrdinalIgnoreCase ) | |
runtime . ActiveVersion < runtime . LastReceipt . SourceVersion ) )
{
yield return new WaitForSecondsRealtime ( 0.1f ) ;
runtime . Refresh ( ) ;
}
AssertReconciledProviderReceipt ( runtime . LastReceipt , resume . CommandId ,
provider . Selection . ProviderId , provider . Selection . SurfaceId , resumeVersion ) ;
var undockVersion = runtime . ActiveVersion ;
var undock = host . SubmitAdvertisedActionIntent ( playerId , "pilot.undock" ) ;
var undockDeadline = Time . realtimeSinceStartup + 12f ;
while ( Time . realtimeSinceStartup < undockDeadline & &
( runtime . LastReceipt = = null | | runtime . LastReceipt . CommandId ! = undock . CommandId | |
! string . Equals ( runtime . LastReceipt . State , "reconciled" , StringComparison . OrdinalIgnoreCase ) | |
runtime . ActiveVersion < runtime . LastReceipt . SourceVersion | |
( host . InputCapability ? . Actions ? ? Array . Empty < EveInputActionDocument > ( ) ) . Any ( candidate = >
candidate ! = null & & candidate . ActionId . StartsWith ( "trade." , StringComparison . Ordinal ) ) ) )
{
yield return new WaitForSecondsRealtime ( 0.1f ) ;
runtime . Refresh ( ) ;
}
AssertReconciledProviderReceipt ( runtime . LastReceipt , undock . CommandId ,
provider . Selection . ProviderId , provider . Selection . SurfaceId , undockVersion ) ;
}
2026-07-20 00:22:32 +01:00
var playerPrefab = provider . ResolvePrefab ( new EveUnityPlayableWorldAssetBinding (
playerFact . AssetRef ,
playerFact . EntityKind ,
"provider-asset-ref" ) ) ;
Assert . That ( playerPrefab , Is . Not . Null ,
$"Provider asset catalog did not resolve player AssetRef '{playerFact.AssetRef}'." ) ;
Assert . That ( provider . CurrentAssetBodyTransportKind ,
Is . EqualTo ( CultMeshBodyTransportKind . SharedFileMapping ) ,
"The first requested provider bundle was not negotiated as a mapped body." ) ;
Assert . That (
marker . GetComponentsInChildren < Transform > ( includeInactive : true ) . Length ,
Is . GreaterThan ( 1 ) ,
"The generic client substituted its primitive fallback instead of the provider-authored AssetBundle prefab." ) ;
2026-07-10 07:14:44 +01:00
initialVersion = runtime . ActiveVersion ;
2026-07-20 04:18:13 +01:00
if ( requireSessionGameplay )
2026-07-10 04:39:16 +01:00
{
2026-07-20 04:18:13 +01:00
var initialPosition = marker . transform . position ;
var request = runtime . SubmitMoveVectorIntent ( playerId , 1f , 0f , 1f ) ;
2026-07-10 04:39:16 +01:00
2026-07-20 04:18:13 +01:00
var deadline = Time . realtimeSinceStartup + 12f ;
while ( Time . realtimeSinceStartup < deadline & &
( runtime . LastReceipt = = null | | runtime . LastReceipt . CommandId ! = request . CommandId | |
! string . Equals ( runtime . LastReceipt . State , "reconciled" , StringComparison . OrdinalIgnoreCase ) | |
runtime . ActiveVersion < = initialVersion | |
Vector3 . Distance ( FindMarker ( root , playerId ) . transform . position , initialPosition ) < 0.01f ) )
{
yield return new WaitForSecondsRealtime ( 0.1f ) ;
runtime . Refresh ( ) ;
}
2026-07-10 04:39:16 +01:00
2026-07-20 04:18:13 +01:00
AssertReconciledProviderReceipt (
runtime . LastReceipt ,
request . CommandId ,
provider . Selection . ProviderId ,
provider . Selection . SurfaceId ,
initialVersion ) ;
Assert . That ( runtime . ActiveVersion , Is . GreaterThan ( initialVersion ) ) ;
Assert . That ( runtime . ActiveVersion , Is . GreaterThanOrEqualTo ( runtime . LastReceipt . SourceVersion ) ) ;
movementDistance = Vector3 . Distance ( FindMarker ( root , playerId ) . transform . position , initialPosition ) ;
Assert . That ( movementDistance , Is . GreaterThan ( 0.01f ) ) ;
movementReceipt = WitnessReceipt . From ( "movement" , runtime . LastReceipt ) ;
var releaseVersion = runtime . ActiveVersion ;
var release = runtime . SubmitMoveVectorIntent ( playerId , 0f , 0f , 0f ) ;
var releaseDeadline = Time . realtimeSinceStartup + 12f ;
while ( Time . realtimeSinceStartup < releaseDeadline & &
( runtime . LastReceipt = = null | | runtime . LastReceipt . CommandId ! = release . CommandId | |
! string . Equals ( runtime . LastReceipt . State , "reconciled" , StringComparison . OrdinalIgnoreCase ) ) )
{
yield return new WaitForSecondsRealtime ( 0.1f ) ;
runtime . Refresh ( ) ;
}
AssertReconciledProviderReceipt ( runtime . LastReceipt , release . CommandId ,
provider . Selection . ProviderId , provider . Selection . SurfaceId , releaseVersion ) ;
2026-07-15 03:21:28 +01:00
}
2026-07-16 03:34:47 +01:00
if ( requireSessionGameplay )
{
2026-07-10 06:37:33 +01:00
var movementVersion = runtime . ActiveVersion ;
2026-07-15 03:21:28 +01:00
var focus = runtime . SubmitFocusIntent ( playerId ) ;
2026-07-10 06:37:33 +01:00
var focusDeadline = Time . realtimeSinceStartup + 12f ;
while ( Time . realtimeSinceStartup < focusDeadline & &
( runtime . LastReceipt = = null | | runtime . LastReceipt . CommandId ! = focus . CommandId | |
2026-07-14 16:26:38 +01:00
! string . Equals ( runtime . LastReceipt . State , "reconciled" , StringComparison . OrdinalIgnoreCase ) | |
2026-07-15 03:21:28 +01:00
runtime . ActiveVersion < runtime . LastReceipt . SourceVersion | |
string . IsNullOrWhiteSpace ( EveUnityCombatPresentation . Find ( runtime . ActiveProjection ) ? . SelectedTargetEntityId ) ) )
2026-07-10 06:37:33 +01:00
{
yield return new WaitForSecondsRealtime ( 0.1f ) ;
runtime . Refresh ( ) ;
}
2026-07-14 16:26:38 +01:00
AssertReconciledProviderReceipt (
runtime . LastReceipt ,
focus . CommandId ,
provider . Selection . ProviderId ,
provider . Selection . SurfaceId ,
movementVersion ) ;
Assert . That ( runtime . ActiveVersion , Is . GreaterThanOrEqualTo ( runtime . LastReceipt . SourceVersion ) ) ;
2026-07-10 07:14:44 +01:00
focusReceipt = WitnessReceipt . From ( "targeting" , runtime . LastReceipt ) ;
2026-07-15 03:21:28 +01:00
observedCombat = EveUnityCombatPresentation . Find ( runtime . ActiveProjection ) ;
Assert . That ( observedCombat , Is . Not . Null ) ;
Assert . That ( observedCombat . TargetVisible , Is . True ) ;
Assert . That ( observedCombat . TargetHostile , Is . True ) ;
var targetEntityId = observedCombat . SelectedTargetEntityId ;
var targetEntityIndex = observedCombat . SelectedTargetEntityIndex ;
Assert . That ( targetEntityId , Is . Not . Empty ) ;
Assert . That ( host . PresentedEntities . CurrentGeneration . Entities . Any ( entity = > entity . EntityId = = targetEntityId ) , Is . True ,
"The provider-selected combat target is absent from the advertised SoA generation." ) ;
shieldRatioBefore = observedCombat . ShieldRatio ;
hullRatioBefore = observedCombat . HullRatio ;
var targetFact = host . PresentedEntities . CurrentGeneration . Entities
. Single ( entity = > entity . EntityId = = targetEntityId ) ;
playerFact = host . PresentedEntities . CurrentGeneration . Entities
. Single ( entity = > entity . EntityId = = playerId ) ;
var aim = targetFact . Position - playerFact . Position ;
aim . y = 0 ;
Assert . That ( aim . sqrMagnitude , Is . GreaterThan ( 0.0001f ) ) ;
aim . Normalize ( ) ;
2026-07-15 06:14:49 +01:00
var lookVersion = runtime . ActiveVersion ;
var look = runtime . SubmitLookDirectionIntent ( playerId , aim . x , 0f , aim . z ) ;
var lookDeadline = Time . realtimeSinceStartup + 12f ;
while ( Time . realtimeSinceStartup < lookDeadline & &
( runtime . LastReceipt = = null | | runtime . LastReceipt . CommandId ! = look . CommandId | |
! string . Equals ( runtime . LastReceipt . State , "reconciled" , StringComparison . OrdinalIgnoreCase ) | |
runtime . ActiveVersion < runtime . LastReceipt . SourceVersion | |
Vector3 . Dot ( FindMarker ( root , playerId ) . transform . forward , aim ) < 0.99f ) )
2026-07-15 03:21:28 +01:00
{
yield return new WaitForSecondsRealtime ( 0.1f ) ;
runtime . Refresh ( ) ;
}
2026-07-15 06:14:49 +01:00
AssertReconciledProviderReceipt ( runtime . LastReceipt , look . CommandId ,
provider . Selection . ProviderId , provider . Selection . SurfaceId , lookVersion ) ;
Assert . That ( Vector3 . Dot ( FindMarker ( root , playerId ) . transform . forward , aim ) , Is . GreaterThan ( 0.99f ) ) ;
lookReceipt = WitnessReceipt . From ( "look" , runtime . LastReceipt ) ;
2026-07-16 03:34:47 +01:00
aimRenderer = root . GetComponent < EveUnityAimPresentationRenderer > ( ) ;
2026-07-15 06:14:49 +01:00
Assert . That ( aimRenderer , Is . Not . Null ) ;
2026-07-10 06:37:33 +01:00
2026-07-16 03:34:47 +01:00
beamPresentation = EveUnityBeamPresentation . FindAll ( runtime . ActiveProjection ) . SingleOrDefault ( ) ;
2026-07-15 11:04:11 +01:00
Assert . That ( beamPresentation , Is . Not . Null ,
"The provider did not advertise its continuous beam presentation." ) ;
Assert . That ( beamPresentation . ActivationActionId , Is . Not . Empty ,
"The beam presentation does not name an advertised generic action." ) ;
var tractorVersion = runtime . ActiveVersion ;
2026-07-15 11:14:22 +01:00
var tractor = host . SubmitAdvertisedActionValueIntent ( playerId , beamPresentation . ActivationActionId , 1f ) ;
2026-07-15 11:04:11 +01:00
var tractorDeadline = Time . realtimeSinceStartup + 12f ;
while ( Time . realtimeSinceStartup < tractorDeadline & &
( runtime . LastReceipt = = null | | runtime . LastReceipt . CommandId ! = tractor . CommandId | |
! string . Equals ( runtime . LastReceipt . State , "reconciled" , StringComparison . OrdinalIgnoreCase ) | |
runtime . ActiveVersion < runtime . LastReceipt . SourceVersion | |
2026-07-15 11:38:52 +01:00
EveUnityBeamPresentation . FindAll ( runtime . ActiveProjection ) . Single ( ) . Power < 0.99f ) )
2026-07-15 11:04:11 +01:00
{
yield return new WaitForSecondsRealtime ( 0.1f ) ;
runtime . Refresh ( ) ;
}
AssertReconciledProviderReceipt ( runtime . LastReceipt , tractor . CommandId ,
provider . Selection . ProviderId , provider . Selection . SurfaceId , tractorVersion ) ;
tractorReceipt = WitnessReceipt . From ( "beam" , runtime . LastReceipt ) ;
Assert . That ( beamPresentation . SourceEntityId , Is . EqualTo ( playerId ) ,
"The daemon beam source does not identify the presented player entity." ) ;
Assert . That ( host . PresentedEntities . TryGetByEntityId ( beamPresentation . SourceEntityId , out _ ) , Is . True ,
"The generic beam lowerer cannot resolve the advertised source entity." ) ;
Assert . That ( provider . ResolvePrefab ( new EveUnityPlayableWorldAssetBinding (
beamPresentation . AssetRole , beamPresentation . AssetRole , "provider-asset-ref" ) ) , Is . Not . Null ,
$"The provider asset catalog did not resolve beam role '{beamPresentation.AssetRole}'." ) ;
2026-07-16 03:34:47 +01:00
beamRenderer = root . GetComponent < EveUnityBeamPresentationRenderer > ( ) ;
2026-07-15 11:04:11 +01:00
Assert . That ( beamRenderer , Is . Not . Null ) ;
beamRenderer . RefreshNow ( ) ;
Assert . That ( beamRenderer . ActiveBeamCount , Is . EqualTo ( 1 ) ) ;
Assert . That ( beamRenderer . TryGetPower ( beamPresentation . Id , out tractorBeamPower ) , Is . True ) ;
2026-07-15 11:38:52 +01:00
Assert . That ( tractorBeamPower , Is . GreaterThanOrEqualTo ( 0.99f ) ,
"Held input did not reach full daemon-authored tractor power before capture." ) ;
2026-07-15 11:04:11 +01:00
tractorBeamParticleSystemCount = beamRenderer . ActiveParticleSystemCount ;
Assert . That ( tractorBeamParticleSystemCount , Is . GreaterThan ( 0 ) ,
"The generic beam lowerer did not instantiate the provider-owned effect prefab." ) ;
2026-07-15 20:07:35 +01:00
targetFact = host . PresentedEntities . CurrentGeneration . Entities
. Single ( entity = > entity . EntityId = = targetEntityId ) ;
playerFact = host . PresentedEntities . CurrentGeneration . Entities
. Single ( entity = > entity . EntityId = = playerId ) ;
aim = targetFact . Position - playerFact . Position ;
aim . y = 0 ;
Assert . That ( aim . sqrMagnitude , Is . GreaterThan ( 0.0001f ) ) ;
aim . Normalize ( ) ;
lookVersion = runtime . ActiveVersion ;
look = runtime . SubmitLookDirectionIntent ( playerId , aim . x , 0f , aim . z ) ;
lookDeadline = Time . realtimeSinceStartup + 12f ;
while ( Time . realtimeSinceStartup < lookDeadline & &
( runtime . LastReceipt = = null | | runtime . LastReceipt . CommandId ! = look . CommandId | |
! string . Equals ( runtime . LastReceipt . State , "reconciled" , StringComparison . OrdinalIgnoreCase ) | |
runtime . ActiveVersion < runtime . LastReceipt . SourceVersion | |
Vector3 . Dot ( FindMarker ( root , playerId ) . transform . forward , aim ) < 0.99f ) )
{
yield return new WaitForSecondsRealtime ( 0.1f ) ;
runtime . Refresh ( ) ;
}
AssertReconciledProviderReceipt ( runtime . LastReceipt , look . CommandId ,
provider . Selection . ProviderId , provider . Selection . SurfaceId , lookVersion ) ;
Assert . That ( Vector3 . Dot ( FindMarker ( root , playerId ) . transform . forward , aim ) , Is . GreaterThan ( 0.99f ) ) ;
lookReceipt = WitnessReceipt . From ( "look" , runtime . LastReceipt ) ;
2026-07-16 16:25:09 +01:00
var combatRenderer = root . GetComponent < EveUnityCombatPresentationRenderer > ( ) ;
Assert . That ( combatRenderer , Is . Not . Null ) ;
2026-07-10 06:37:33 +01:00
var focusVersion = runtime . ActiveVersion ;
2026-07-15 03:21:28 +01:00
var inputDriver = root . GetComponent < EveUnityPlayableWorldInputDriver > ( ) ? ?
root . AddComponent < EveUnityPlayableWorldInputDriver > ( ) ;
inputDriver . Host = host ;
2026-07-17 13:57:34 +01:00
var primaryActionId = EveUnityPlayableWorldInputDriver . ResolvePrimaryActionId ( host . InputCapability ) ;
Assert . That ( primaryActionId , Is . Not . Empty ,
"The advertised input capability did not bind a primary action for this generic client." ) ;
var primaryAction = EveUnityAdvertisedInputAction . Resolve ( host . InputCapability , primaryActionId ) ;
Assert . That ( primaryAction . IsButtonHold , Is . True ,
"The witness primary action must be driven through its advertised held-input semantics." ) ;
var action = inputDriver . SubmitActionValue ( primaryActionId , 1f ) ;
2026-07-15 03:21:28 +01:00
Assert . That ( action , Is . Not . Null ,
"The advertised input capability did not bind a primary action for this generic client." ) ;
2026-07-10 06:37:33 +01:00
var actionDeadline = Time . realtimeSinceStartup + 12f ;
while ( Time . realtimeSinceStartup < actionDeadline & &
2026-07-15 03:21:28 +01:00
( runtime . LastReceipt = = null | | runtime . LastReceipt . CommandId ! = action ! . CommandId | |
2026-07-14 16:26:38 +01:00
! string . Equals ( runtime . LastReceipt . State , "reconciled" , StringComparison . OrdinalIgnoreCase ) | |
2026-07-10 06:37:33 +01:00
runtime . ActiveVersion < runtime . LastReceipt . SourceVersion ) )
{
yield return new WaitForSecondsRealtime ( 0.1f ) ;
runtime . Refresh ( ) ;
2026-07-16 16:25:09 +01:00
combatRenderer . RefreshNow ( ) ;
hitMarkerObserved | = combatRenderer . HitMarkerVisible ;
2026-07-10 06:37:33 +01:00
}
2026-07-14 16:26:38 +01:00
AssertReconciledProviderReceipt (
runtime . LastReceipt ,
2026-07-15 03:21:28 +01:00
action ! . CommandId ,
2026-07-14 16:26:38 +01:00
provider . Selection . ProviderId ,
provider . Selection . SurfaceId ,
focusVersion ) ;
2026-07-10 06:37:33 +01:00
Assert . That ( runtime . ActiveVersion , Is . GreaterThanOrEqualTo ( runtime . LastReceipt . SourceVersion ) ) ;
2026-07-10 07:14:44 +01:00
actionReceipt = WitnessReceipt . From ( "action" , runtime . LastReceipt ) ;
2026-07-10 06:37:33 +01:00
2026-07-16 03:34:47 +01:00
var shotDeadline = Time . realtimeSinceStartup + 20f ;
while ( Time . realtimeSinceStartup < shotDeadline & &
( observedShot = = null | |
host . PresentedEntities . CurrentGeneration . Entities . Any ( entity = >
entity . EntityId = = targetEntityId ) | |
2026-07-16 22:26:29 +01:00
( ! destructionPickupObserved & & pickupCollection = = null & & pickupRejection = = null ) ) )
2026-07-15 03:21:28 +01:00
{
yield return new WaitForSecondsRealtime ( 0.05f ) ;
runtime . Refresh ( ) ;
2026-07-16 03:34:47 +01:00
destructionPickupObserved | = host . PresentedEntities . CurrentGeneration . Entities . Any ( entity = >
string . Equals ( entity . EntityKind , "pickup" , StringComparison . Ordinal ) ) ;
maximumTrajectoryCount = Math . Max ( maximumTrajectoryCount , trajectories . ActiveTrajectoryCount ) ;
combatRenderer . RefreshNow ( ) ;
hitMarkerObserved | = combatRenderer . HitMarkerVisible ;
2026-07-15 03:21:28 +01:00
}
Assert . That ( observedShot , Is . Not . Null ,
"A reconciled fire request never reached daemon-owned lock acquisition and shot resolution." ) ;
2026-07-16 03:34:47 +01:00
Assert . That ( host . PresentedEntities . CurrentGeneration . Entities . Any ( entity = >
entity . EntityId = = targetEntityId ) , Is . False ,
"The low-hull witness raider was not destroyed by daemon combat." ) ;
2026-07-16 22:26:29 +01:00
Assert . That ( destructionPickupObserved | | pickupCollection ! = null | | pickupRejection ! = null , Is . True ,
2026-07-16 03:34:47 +01:00
"Daemon destruction did not publish its guaranteed cargo as a pickup." ) ;
2026-07-19 00:33:07 +01:00
// The provider advances a fixed-step simulation while also publishing the large SoA witness.
// Allow enough wall time for the tractor to cover its advertised volume without making the
// generic client manufacture ship motion or loot authority.
var collectionDeadline = Time . realtimeSinceStartup + 45f ;
2026-07-16 03:34:47 +01:00
var nextTractorAimAt = 0f ;
2026-07-16 22:26:29 +01:00
while ( Time . realtimeSinceStartup < collectionDeadline & &
2026-07-19 02:22:46 +01:00
( expectCargoRejection
? pickupRejection = = null
: pickupCollection = = null | | host . PresentedEntities . CurrentGeneration . Entities . Any ( entity = >
string . Equals ( entity . EntityKind , "pickup" , StringComparison . Ordinal ) ) ) )
2026-07-16 03:34:47 +01:00
{
yield return new WaitForSecondsRealtime ( 0.05f ) ;
runtime . Refresh ( ) ;
maximumTrajectoryCount = Math . Max ( maximumTrajectoryCount , trajectories . ActiveTrajectoryCount ) ;
combatRenderer . RefreshNow ( ) ;
hitMarkerObserved | = combatRenderer . HitMarkerVisible ;
var pickupFact = host . PresentedEntities . CurrentGeneration . Entities . FirstOrDefault ( entity = >
string . Equals ( entity . EntityKind , "pickup" , StringComparison . Ordinal ) ) ;
if ( pickupFact = = null | | Time . realtimeSinceStartup < nextTractorAimAt )
continue ;
playerFact = host . PresentedEntities . CurrentGeneration . Entities
. Single ( entity = > entity . EntityId = = playerId ) ;
var tractorAim = pickupFact . Position - playerFact . Position ;
tractorAim . y = 0 ;
if ( tractorAim . sqrMagnitude < = 0.0001f )
continue ;
tractorAim . Normalize ( ) ;
runtime . SubmitLookDirectionIntent ( playerId , tractorAim . x , 0f , tractorAim . z ) ;
2026-07-19 00:33:07 +01:00
nextTractorAimAt = Time . realtimeSinceStartup + 0.5f ;
2026-07-16 03:34:47 +01:00
}
finalPickupEntityCount = host . PresentedEntities . CurrentGeneration . Entities . Count ( entity = >
string . Equals ( entity . EntityKind , "pickup" , StringComparison . Ordinal ) ) ;
2026-07-16 22:26:29 +01:00
if ( expectCargoRejection )
{
Assert . That ( pickupRejection , Is . Not . Null ,
2026-07-17 13:57:34 +01:00
"Full cargo did not reject destruction salvage at daemon pickup proximity." ) ;
2026-07-16 22:26:29 +01:00
Assert . That ( pickupRejectionEventCount , Is . EqualTo ( 1 ) ,
2026-07-17 13:57:34 +01:00
"The first destruction-loot proximity attempt must emit one rejection event." ) ;
2026-07-16 22:26:29 +01:00
Assert . That ( pickupRejectionEventIds . Count , Is . EqualTo ( pickupRejectionEventCount ) ,
2026-07-17 13:57:34 +01:00
"One daemon proximity attempt emitted duplicate player rejection feedback." ) ;
2026-07-16 22:26:29 +01:00
Assert . That ( pickupCollectionEventCount , Is . Zero ,
"Capacity-rejected salvage was also collected." ) ;
Assert . That ( pickupRejection . ItemKey , Is . Not . Empty ) ;
2026-07-17 13:57:34 +01:00
Assert . That ( pickupRejection . EventId , Does . StartWith ( "frame:" ) ,
"Capacity feedback must retain its daemon frame/zone/pickup attempt identity." ) ;
2026-07-16 22:26:29 +01:00
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 ,
2026-07-17 13:57:34 +01:00
"Destruction-created salvage never reached cargo through daemon pickup proximity." ) ;
2026-07-16 22:26:29 +01:00
Assert . That ( pickupCollectionEventCount , Is . EqualTo ( 1 ) ,
2026-07-17 13:57:34 +01:00
"One durable destruction pickup must emit one collection event." ) ;
2026-07-16 22:26:29 +01:00
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." ) ;
2026-07-17 13:57:34 +01:00
Assert . That ( pickupCollection . EventId , Does . StartWith ( "zone:" ) ,
"Collection feedback must retain its stable daemon zone/pickup identity." ) ;
2026-07-16 22:26:29 +01:00
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." ) ;
}
2026-07-15 03:21:28 +01:00
observedCombat = EveUnityCombatPresentation . Find ( runtime . ActiveProjection ) ;
Assert . That ( observedShot . TargetEntityIndex , Is . EqualTo ( targetEntityIndex ) ) ;
2026-07-15 22:32:26 +01:00
Assert . That ( observedShot . LockQuality , Is . GreaterThanOrEqualTo ( 0.99 ) ) ;
2026-07-16 03:34:47 +01:00
Assert . That ( maximumTrajectoryCount , Is . GreaterThan ( 0 ) ) ;
2026-07-15 03:21:28 +01:00
combatRenderer . RefreshNow ( ) ;
if ( observedCombat ! = null & & ! string . IsNullOrWhiteSpace ( observedCombat . SelectedTargetEntityId ) )
{
Assert . That ( combatRenderer . ReticleVisible , Is . True ) ;
Assert . That ( combatRenderer . LockVisible , Is . True ) ;
}
if ( observedShot . Hit & & ( observedShot . AppliedDamage > 0 | | observedShot . ShieldAbsorbedDamage > 0 ) )
2026-07-16 03:34:47 +01:00
Assert . That ( hitMarkerObserved , Is . True ) ;
}
aimRenderer ? ? = root . GetComponent < EveUnityAimPresentationRenderer > ( ) ;
Assert . That ( aimRenderer , Is . Not . Null ) ;
beamPresentation ? ? = EveUnityBeamPresentation . FindAll ( runtime . ActiveProjection ) . SingleOrDefault ( ) ;
Assert . That ( beamPresentation , Is . Not . Null ,
"The provider did not advertise its continuous beam presentation." ) ;
beamRenderer ? ? = root . GetComponent < EveUnityBeamPresentationRenderer > ( ) ;
finalPickupEntityCount = Math . Max ( 0 , finalPickupEntityCount ) ;
2026-07-15 03:21:28 +01:00
2026-07-10 04:39:16 +01:00
var camera = cameraObject . AddComponent < Camera > ( ) ;
camera . clearFlags = CameraClearFlags . SolidColor ;
camera . backgroundColor = new Color ( 0.035f , 0.055f , 0.08f , 1f ) ;
2026-07-14 03:59:45 +01:00
camera . cullingMask = - 1 ;
2026-07-10 05:05:27 +01:00
var playerMarker = FindMarker ( root , playerId ) ;
2026-07-10 06:37:33 +01:00
var playerRenderers = playerMarker . GetComponentsInChildren < Renderer > ( ) ;
Assert . That ( playerRenderers , Is . Not . Empty , "The provider-authored player prefab has no renderable visual." ) ;
2026-07-16 19:01:09 +01:00
Assert . That ( playerRenderers . Any ( renderer = > renderer . name = = "Shield Visual" ) , Is . False ,
"The provider hull retained an always-on shield envelope instead of using receipt-driven shield feedback." ) ;
2026-07-15 04:58:50 +01:00
Assert . That ( playerMarker . GetComponentsInChildren < Collider > ( ) , Is . Empty ,
"The provider-authored player presentation retained gameplay colliders." ) ;
Assert . That ( playerMarker . GetComponentsInChildren < Rigidbody > ( ) , Is . Empty ,
"The provider-authored player presentation retained gameplay rigid bodies." ) ;
foreach ( var renderer in playerRenderers )
foreach ( var material in renderer . sharedMaterials )
{
Assert . That ( material , Is . Not . Null , $"Provider renderer {renderer.name} has a null material." ) ;
Assert . That ( material . shader , Is . Not . Null , $"Provider material {material.name} has no shader." ) ;
Assert . That ( material . shader . isSupported , Is . True ,
$"Provider material {material.name} uses unsupported shader {material.shader.name}." ) ;
Assert . That ( material . shader . name , Does . Not . Contain ( "FallbackError" ) ,
$"Provider material {material.name} resolved to Unity's error shader." ) ;
}
2026-07-14 03:59:45 +01:00
Assert . That ( provider . TryGetRenderChannelLayer ( "map" , out var mapLayer ) , Is . True ) ;
var mapRenderers = new List < Renderer > ( ) ;
foreach ( var renderer in root . GetComponentsInChildren < Renderer > ( includeInactive : true ) )
{
if ( renderer . gameObject . layer = = mapLayer )
mapRenderers . Add ( renderer ) ;
}
Assert . That ( mapRenderers , Is . Not . Empty , "The live provider prefab has no renderer assigned to its advertised map channel." ) ;
2026-07-16 20:40:40 +01:00
const int captureWidth = 1280 ;
const int captureHeight = 720 ;
target = new RenderTexture ( captureWidth , captureHeight , 24 , RenderTextureFormat . ARGB32 ) ;
2026-07-15 05:20:46 +01:00
camera . targetTexture = target ;
2026-07-14 03:59:45 +01:00
var cameraRig = cameraObject . AddComponent < EveUnityPlayableWorldCameraRig > ( ) ;
cameraRig . Host = host ;
cameraRig . CameraTransform = camera . transform ;
cameraRig . RenderPolicySource = provider ;
Assert . That ( cameraRig . ApplyRig ( 0f ) , Is . True ) ;
2026-07-16 20:57:34 +01:00
var activePipelineAsset = GraphicsSettings . currentRenderPipeline as UniversalRenderPipelineAsset ;
Assert . That ( activePipelineAsset , Is . Not . Null ,
"The generic client has no active URP asset for the advertised grading space." ) ;
Assert . That ( activePipelineAsset . colorGradingMode , Is . EqualTo ( ColorGradingMode . HighDynamicRange ) ,
"The advertised HDR-before-tonemap grading space did not reach the active URP pipeline." ) ;
2026-07-15 06:44:09 +01:00
var reflection = provider . ResolveAsset ( new EveUnityPlayableWorldAssetBinding (
runtime . ActiveWorld . ReflectionAssetRef , "" , "provider-asset-ref" ) , typeof ( Cubemap ) ) as Cubemap ;
2026-07-16 19:01:09 +01:00
Assert . That ( runtime . ActiveWorld . SkyboxAssetRef , Is . Empty ,
"Aetheria's raymarch owns the frame; its reflection HDRI must not become a skybox-lighting authority." ) ;
2026-07-15 06:44:09 +01:00
Assert . That ( runtime . ActiveWorld . ReflectionAssetRef , Is . Not . Empty ) ;
Assert . That ( reflection , Is . Not . Null ) ;
2026-07-16 19:01:09 +01:00
Assert . That ( RenderSettings . ambientMode , Is . EqualTo ( AmbientMode . Flat ) ) ;
Assert . That ( RenderSettings . ambientLight . r ,
Is . EqualTo ( runtime . ActiveWorld . AmbientLightR * runtime . ActiveWorld . AmbientLightIntensity ) . Within ( 0.001f ) ) ;
Assert . That ( RenderSettings . ambientLight . g ,
Is . EqualTo ( runtime . ActiveWorld . AmbientLightG * runtime . ActiveWorld . AmbientLightIntensity ) . Within ( 0.001f ) ) ;
Assert . That ( RenderSettings . ambientLight . b ,
Is . EqualTo ( runtime . ActiveWorld . AmbientLightB * runtime . ActiveWorld . AmbientLightIntensity ) . Within ( 0.001f ) ) ;
2026-07-15 06:44:09 +01:00
Assert . That ( RenderSettings . customReflectionTexture , Is . SameAs ( reflection ) ) ;
2026-07-16 19:01:09 +01:00
var environmentPresentation = RenderSettings . ambientMode = = AmbientMode . Flat & &
2026-07-15 06:44:09 +01:00
RenderSettings . customReflectionTexture = = reflection ;
2026-07-15 06:14:49 +01:00
aimRenderer . RefreshNow ( ) ;
Assert . That ( aimRenderer . ViewDotVisible , Is . True ) ;
aimDotDistance = Vector3 . Distance ( playerMarker . transform . position , aimRenderer . ViewDotPosition ) ;
Assert . That ( aimDotDistance , Is . GreaterThanOrEqualTo ( 49.9f ) ) ;
2026-07-14 03:59:45 +01:00
Assert . That ( camera . cullingMask & ( 1 < < mapLayer ) , Is . Zero ) ;
2026-07-15 21:40:16 +01:00
Assert . That ( runtime . ActiveWorld . CameraRig , Is . EqualTo ( "perspective.entity-forward-follow.v1" ) ) ;
2026-07-15 22:32:26 +01:00
Assert . That ( runtime . ActiveWorld . CameraLookAt , Is . EqualTo ( "aim.convergence-point.v1" ) ) ;
2026-07-15 21:40:16 +01:00
Assert . That ( runtime . ActiveWorld . CameraDistance , Is . EqualTo ( 30f ) . Within ( 0.001f ) ) ;
2026-07-15 05:20:46 +01:00
Assert . That ( camera . fieldOfView , Is . EqualTo ( 60f ) . Within ( 0.001f ) ) ;
2026-07-15 21:40:16 +01:00
Assert . That ( runtime . ActiveWorld . CameraPositionDamping , Is . EqualTo ( 0f ) . Within ( 0.001f ) ) ;
2026-07-15 05:20:46 +01:00
var playerViewport = camera . WorldToViewportPoint ( playerMarker . transform . position ) ;
2026-07-16 02:23:22 +01:00
Assert . That ( runtime . ActiveWorld . CameraTargetScreenX , Is . EqualTo ( 0.64f ) . Within ( 0.001f ) ) ;
Assert . That ( runtime . ActiveWorld . CameraTargetScreenY , Is . EqualTo ( 0.19f ) . Within ( 0.001f ) ) ;
Assert . That ( playerViewport . x , Is . EqualTo ( runtime . ActiveWorld . CameraTargetScreenX ) . Within ( 0.01f ) ) ;
Assert . That ( playerViewport . y , Is . EqualTo ( runtime . ActiveWorld . CameraTargetScreenY ) . Within ( 0.01f ) ) ;
2026-07-15 09:08:44 +01:00
MeasureViewportCoverage ( camera , playerRenderers , out var playerViewportWidth , out var playerViewportHeight ) ;
var playerRendererFacts = playerRenderers
. Select ( renderer = > RendererFact . From ( camera , renderer ) )
. ToArray ( ) ;
var texturedPlayerMaterialCount = playerRenderers
. SelectMany ( renderer = > renderer . sharedMaterials )
. Where ( material = > material ! = null )
. Distinct ( )
. Count ( HasAnyTexture ) ;
2026-07-20 04:18:13 +01:00
var expectedTexturedPlayerMaterialCount = requireSessionGameplay ? 8 : 7 ;
Assert . That ( texturedPlayerMaterialCount , Is . GreaterThanOrEqualTo ( expectedTexturedPlayerMaterialCount ) ,
2026-07-15 09:27:48 +01:00
"The provider bundle must retain its advertised native material textures." ) ;
2026-07-15 09:08:44 +01:00
var directionalLightIntensity = UnityEngine . Object . FindObjectsOfType < Light > ( )
. Where ( light = > light ! = null & & light . enabled & & light . type = = LightType . Directional )
. Sum ( light = > light . intensity ) ;
Assert . That ( directionalLightIntensity ,
Is . EqualTo ( runtime . ActiveWorld . KeyLightIntensity ) . Within ( 0.001f ) ,
"The live witness contains directional lighting not owned by the advertised provider surface." ) ;
2026-07-14 03:59:45 +01:00
var mapCamera = mapCameraObject . AddComponent < Camera > ( ) ;
mapCamera . cullingMask = 1 < < mapLayer ;
2026-07-14 16:26:38 +01:00
Assert . That ( mapCamera . cullingMask , Is . EqualTo ( 1 < < mapLayer ) ) ;
2026-07-16 20:40:40 +01:00
var captureFieldVolume = root . GetComponent < EveUnityFieldsVolumeRenderer > ( ) ;
Assert . That ( captureFieldVolume , Is . Not . Null ,
"The generic client did not install its Fields volume lowerer before temporal settling." ) ;
const int temporalSettlingFrames = 128 ;
2026-07-19 21:51:30 +01:00
var temporalSettlingElapsed = Stopwatch . StartNew ( ) ;
2026-07-16 20:40:40 +01:00
var initialCompositeCount = captureFieldVolume . CompositeCount ;
var temporalSettlingDeadline = Time . realtimeSinceStartup + 20f ;
while ( captureFieldVolume . CompositeCount - initialCompositeCount < temporalSettlingFrames & &
Time . realtimeSinceStartup < temporalSettlingDeadline )
2026-07-19 00:33:07 +01:00
{
camera . Render ( ) ;
2026-07-16 20:40:40 +01:00
yield return null ;
2026-07-19 00:33:07 +01:00
}
2026-07-16 20:40:40 +01:00
Assert . That ( captureFieldVolume . CompositeCount - initialCompositeCount ,
Is . GreaterThanOrEqualTo ( temporalSettlingFrames ) ,
"The pilot capture ran before the advertised temporal reconstruction had settled." ) ;
2026-07-20 05:22:52 +01:00
Assert . That ( cockpit . resolvedStyle . display , Is . Not . EqualTo ( DisplayStyle . None ) ) ;
Assert . That ( cockpit . worldBound . width , Is . GreaterThan ( 0 ) ,
"The mounted provider cockpit has no visible screen layout." ) ;
2026-07-19 21:51:30 +01:00
Debug . Log ( $"EveUnity witness phase temporal-settling-128-frames took {temporalSettlingElapsed.Elapsed.TotalMilliseconds:0.###}ms." ) ;
var pilotCaptureElapsed = Stopwatch . StartNew ( ) ;
2026-07-16 21:58:52 +01:00
yield return null ;
2026-07-10 04:39:16 +01:00
RenderTexture . active = target ;
2026-07-16 20:40:40 +01:00
pixels = new Texture2D ( captureWidth , captureHeight , TextureFormat . RGB24 , false ) ;
pixels . ReadPixels ( new Rect ( 0 , 0 , captureWidth , captureHeight ) , 0 , 0 ) ;
2026-07-10 04:39:16 +01:00
pixels . Apply ( ) ;
2026-07-14 16:26:38 +01:00
var pilotChangedPixels = CountChangedPixels ( pixels , camera . backgroundColor ) ;
2026-07-15 09:08:44 +01:00
MeasureLuminance ( pixels , out var pilotAverageLuminance , out var pilotBrightPixelCount ) ;
2026-07-14 16:26:38 +01:00
Assert . That ( pilotChangedPixels , Is . GreaterThan ( 25 ) , "The pilot camera rendered no provider-authored world pixels." ) ;
2026-07-10 04:39:16 +01:00
Directory . CreateDirectory ( Path . GetDirectoryName ( Path . GetFullPath ( capturePath ) ) ) ;
File . WriteAllBytes ( capturePath , pixels . EncodeToPNG ( ) ) ;
Assert . That ( new FileInfo ( capturePath ) . Length , Is . GreaterThan ( 1024 ) ) ;
2026-07-15 20:07:35 +01:00
WriteFieldLayerCaptures ( root , host . Runtime . ActiveWorld , capturePath ) ;
WriteFieldCloudCapture ( root , capturePath ) ;
2026-07-15 09:08:44 +01:00
MeasureRendererOutputs (
camera ,
target ,
pixels ,
root . GetComponentsInChildren < Renderer > ( includeInactive : true ) ,
playerRenderers ,
playerRendererFacts ) ;
var playerMapRendererFacts = playerRendererFacts
. Where ( fact = > fact . layer = = mapLayer )
. ToArray ( ) ;
Assert . That ( playerMapRendererFacts , Is . Not . Empty ,
"The provider-authored player prefab has no renderer on its advertised map channel." ) ;
Assert . That ( playerMapRendererFacts . Sum ( fact = > fact . renderedPixelCount ) , Is . Zero ,
"A provider-prefab map-channel renderer leaked into the pilot camera." ) ;
2026-07-19 21:51:30 +01:00
Debug . Log ( $"EveUnity witness phase pilot-capture-and-renderer-probes took {pilotCaptureElapsed.Elapsed.TotalMilliseconds:0.###}ms." ) ;
2026-07-14 03:59:45 +01:00
2026-07-19 21:51:30 +01:00
var mapCaptureElapsed = Stopwatch . StartNew ( ) ;
2026-07-15 03:21:28 +01:00
mapRenderers = mapRenderers . Where ( renderer = > renderer ! = null ) . ToList ( ) ;
Assert . That ( mapRenderers , Is . Not . Empty ,
"All advertised map-channel renderers were destroyed before map capture." ) ;
2026-07-14 03:59:45 +01:00
var mapBounds = mapRenderers [ 0 ] . bounds ;
foreach ( var renderer in mapRenderers )
mapBounds . Encapsulate ( renderer . bounds ) ;
mapCamera . clearFlags = CameraClearFlags . SolidColor ;
mapCamera . backgroundColor = camera . backgroundColor ;
mapCamera . orthographic = true ;
mapCamera . orthographicSize = Mathf . Max ( 0.1f , mapBounds . extents . x , mapBounds . extents . z ) * 1.25f ;
mapCamera . nearClipPlane = 0.01f ;
mapCamera . farClipPlane = Mathf . Max ( 10f , mapBounds . size . magnitude * 4f ) ;
mapCamera . transform . position = mapBounds . center + Vector3 . up * ( mapCamera . farClipPlane * 0.25f ) ;
mapCamera . transform . rotation = Quaternion . Euler ( 90f , 0f , 0f ) ;
mapCamera . targetTexture = target ;
2026-07-16 12:23:00 +01:00
var fieldParticlesForMap = root . GetComponent < EveUnityFieldsParticleRenderer > ( ) ;
var particleDrawsBeforeMap = fieldParticlesForMap ? . DrawCount ? ? 0 ;
2026-07-14 03:59:45 +01:00
mapCamera . Render ( ) ;
2026-07-16 12:23:00 +01:00
var particleMapCameraIsolated = fieldParticlesForMap ! = null & &
fieldParticlesForMap . DrawCount = = particleDrawsBeforeMap ;
Assert . That ( particleMapCameraIsolated , Is . True ,
"The map camera executed the pilot-only field-particle pass." ) ;
2026-07-14 03:59:45 +01:00
RenderTexture . active = target ;
2026-07-16 20:40:40 +01:00
pixels . ReadPixels ( new Rect ( 0 , 0 , captureWidth , captureHeight ) , 0 , 0 ) ;
2026-07-14 03:59:45 +01:00
pixels . Apply ( ) ;
var mapChangedPixels = CountChangedPixels ( pixels , mapCamera . backgroundColor ) ;
Assert . That ( mapChangedPixels , Is . GreaterThan ( 25 ) , "The map-only camera rendered no provider-authored map glyph pixels." ) ;
Directory . CreateDirectory ( Path . GetDirectoryName ( Path . GetFullPath ( mapCapturePath ) ) ) ;
File . WriteAllBytes ( mapCapturePath , pixels . EncodeToPNG ( ) ) ;
Assert . That ( new FileInfo ( mapCapturePath ) . Length , Is . GreaterThan ( 1024 ) ) ;
2026-07-19 21:51:30 +01:00
Debug . Log ( $"EveUnity witness phase map-capture took {mapCaptureElapsed.Elapsed.TotalMilliseconds:0.###}ms." ) ;
2026-07-20 04:18:13 +01:00
if ( requireSessionGameplay )
2026-07-15 11:14:22 +01:00
{
2026-07-20 04:18:13 +01:00
var tractorReleaseVersion = runtime . ActiveVersion ;
var tractorRelease = host . SubmitAdvertisedActionValueIntent (
playerId ,
beamPresentation . ActivationActionId ,
0f ) ;
var tractorReleaseDeadline = Time . realtimeSinceStartup + 12f ;
while ( Time . realtimeSinceStartup < tractorReleaseDeadline & &
( runtime . LastReceipt = = null | | runtime . LastReceipt . CommandId ! = tractorRelease . CommandId | |
! string . Equals ( runtime . LastReceipt . State , "reconciled" , StringComparison . OrdinalIgnoreCase ) | |
runtime . ActiveVersion < runtime . LastReceipt . SourceVersion ) )
{
yield return new WaitForSecondsRealtime ( 0.1f ) ;
runtime . Refresh ( ) ;
}
AssertReconciledProviderReceipt ( runtime . LastReceipt , tractorRelease . CommandId ,
provider . Selection . ProviderId , provider . Selection . SurfaceId , tractorReleaseVersion ) ;
tractorReleaseReceipt = WitnessReceipt . From ( "beam-release" , runtime . LastReceipt ) ;
beamRenderer . RefreshNow ( ) ;
Assert . That ( beamRenderer . TryGetPower ( beamPresentation . Id , out tractorReleasedPower ) , Is . True ) ;
2026-07-15 11:14:22 +01:00
}
2026-07-15 17:31:02 +01:00
var fieldVolume = root . GetComponent < EveUnityFieldsVolumeRenderer > ( ) ;
Assert . That ( fieldVolume , Is . Not . Null , "The generic client did not install its Fields volume lowerer." ) ;
Assert . That ( fieldVolume . PresentedFrameId , Is . GreaterThanOrEqualTo ( 0 ) ,
"The advertised Fields splat document never reached the generic volume lowerer." ) ;
Assert . That ( fieldVolume . PresentedLayerCount , Is . GreaterThan ( 0 ) ,
"The generic volume lowerer did not rasterize any advertised Fields layers." ) ;
Assert . That ( fieldVolume . CompositeCount , Is . GreaterThan ( 0 ) ,
"The generic volume lowerer never composited into the pilot camera." ) ;
2026-07-16 12:23:00 +01:00
var fieldParticles = fieldParticlesForMap ;
Assert . That ( fieldParticles , Is . Not . Null ,
"The generic client did not install its field-particle lowerer." ) ;
2026-07-16 12:37:49 +01:00
var particleProjection = host . ActiveWorld . FieldParticles . Single ( ) ;
var particleComputeBinding = new EveUnityPlayableWorldAssetBinding (
particleProjection . ComputeProgramAssetRef , "" , "provider-asset-ref" ) ;
var particleMaterialBinding = new EveUnityPlayableWorldAssetBinding (
particleProjection . MaterialAssetRef , "" , "provider-asset-ref" ) ;
var particleCompute = host . NativeAssetProvider . ResolveAsset (
particleComputeBinding , typeof ( ComputeShader ) ) as ComputeShader ;
var particleMaterial = host . NativeAssetProvider . ResolveAsset (
particleMaterialBinding , typeof ( Material ) ) as Material ;
Assert . That ( particleCompute , Is . Not . Null ,
$"Provider asset '{particleProjection.ComputeProgramAssetRef}' did not lower as a ComputeShader." ) ;
Assert . That ( particleMaterial , Is . Not . Null ,
$"Provider asset '{particleProjection.MaterialAssetRef}' did not lower as a Material." ) ;
var particleMetadata = host . NativeAssetProvider as IEveUnityNativeAssetMetadataProvider ;
Assert . That ( particleMetadata , Is . Not . Null ) ;
Assert . That ( particleMetadata . TryResolveAssetMetadata (
particleComputeBinding , out var particleComputeMetadata ) , Is . True ) ;
Assert . That ( particleMetadata . TryResolveAssetMetadata (
particleMaterialBinding , out var particleMaterialMetadata ) , Is . True ) ;
Assert . That ( EveUnityFieldsParticleRenderer . TryValidateProgramMetadata (
particleComputeMetadata , particleMaterialMetadata , out var particleAbiError ) , Is . True ,
particleAbiError ) ;
2026-07-16 12:23:00 +01:00
Assert . That ( fieldParticles . ProgramReady , Is . True ,
"The generic field-particle lowerer rejected the advertised provider ABI or assets." ) ;
2026-07-16 18:17:02 +01:00
var liveParticleMaterial = typeof ( EveUnityFieldsParticleRenderer )
. GetField ( "_material" , System . Reflection . BindingFlags . Instance | System . Reflection . BindingFlags . NonPublic )
? . GetValue ( fieldParticles ) as Material ;
Assert . That ( liveParticleMaterial , Is . Not . Null ) ;
var ditherTextureProperty = particleMaterialMetadata [ "unity.particles.texturePort.dither" ] ;
var ditherCoordinatesProperty = particleMaterialMetadata [ "unity.particles.vectorPort.ditherCoordinates" ] ;
var renderFrameProperty = particleMaterialMetadata [ "unity.particles.intPort.frameIndex" ] ;
var liveDitherTexture = liveParticleMaterial . GetTexture ( ditherTextureProperty ) ;
Assert . That ( liveDitherTexture , Is . Not . Null ,
"The generic lowerer did not bind the advertised provider dither texture." ) ;
Assert . That ( liveParticleMaterial . GetVector ( ditherCoordinatesProperty ) . x ,
Is . EqualTo ( ( float ) camera . pixelWidth / liveDitherTexture . width ) . Within ( 0.0001f ) ) ;
Assert . That ( liveParticleMaterial . GetVector ( ditherCoordinatesProperty ) . y ,
Is . EqualTo ( ( float ) camera . pixelHeight / liveDitherTexture . height ) . Within ( 0.0001f ) ) ;
Assert . That ( liveParticleMaterial . GetInt ( renderFrameProperty ) , Is . GreaterThan ( 0 ) ,
"The generic lowerer did not advance the advertised render-frame port." ) ;
2026-07-16 12:23:00 +01:00
Assert . That ( fieldParticles . PresentedFrameId , Is . GreaterThanOrEqualTo ( 0 ) ) ;
Assert . That ( fieldParticles . ParticleCount , Is . EqualTo ( 65536 ) ) ;
Assert . That ( fieldParticles . DispatchCount , Is . GreaterThan ( 0 ) ) ;
Assert . That ( fieldParticles . DrawCount , Is . GreaterThan ( 0 ) ) ;
2026-07-16 17:53:40 +01:00
Assert . That ( fieldVolume . LastGridCenter , Is . EqualTo ( fieldParticles . LastGridCenter ) ,
"Fog and stateless particles were lowered against different world-grid frames." ) ;
var pilotCameraData = camera . GetUniversalAdditionalCameraData ( ) ;
Assert . That ( pilotCameraData . antialiasing , Is . EqualTo ( AntialiasingMode . TemporalAntiAliasing ) ,
"The advertised temporal reconstruction contract did not reach the pilot camera." ) ;
Assert . That ( pilotCameraData . taaSettings . baseBlendFactor , Is . EqualTo ( 0.99f ) . Within ( 0.0001f ) ) ;
Assert . That ( pilotCameraData . taaSettings . jitterScale , Is . EqualTo ( 0.1f ) . Within ( 0.0001f ) ) ;
2026-07-15 22:32:26 +01:00
var fieldDocument = typeof ( EveUnityFieldsVolumeRenderer )
. GetField ( "_document" , System . Reflection . BindingFlags . Instance | System . Reflection . BindingFlags . NonPublic )
? . GetValue ( fieldVolume ) ;
Assert . That ( fieldDocument , Is . Not . Null , "The volume lowerer did not retain its presented Fields document." ) ;
var fieldViewport = fieldDocument . GetType ( ) . GetProperty ( "Viewport" ) ? . GetValue ( fieldDocument ) ;
var fieldSplats = fieldDocument . GetType ( ) . GetProperty ( "Splats" ) ? . GetValue ( fieldDocument ) ;
Assert . That ( fieldViewport , Is . Not . Null ) ;
Assert . That ( fieldSplats , Is . Not . Null ) ;
2026-07-10 07:14:44 +01:00
WriteWitnessFacts (
2026-07-16 03:34:47 +01:00
witnessProfile ,
2026-07-16 22:26:29 +01:00
gameplayScenario ,
2026-07-16 14:46:49 +01:00
provider . CurrentAssetBodyTransportKind ? . ToString ( ) ? ? "" ,
2026-07-10 07:14:44 +01:00
initialVersion ,
runtime . ActiveVersion ,
movementDistance ,
2026-07-14 16:26:38 +01:00
playerRenderers . Length ,
pilotChangedPixels ,
2026-07-14 03:59:45 +01:00
mapRenderers . Count ,
mapChangedPixels ,
( camera . cullingMask & ( 1 < < mapLayer ) ) = = 0 ,
( mapCamera . cullingMask & ( 1 < < mapLayer ) ) ! = 0 ,
2026-07-15 06:44:09 +01:00
environmentPresentation ,
2026-07-15 17:31:02 +01:00
fieldVolume . PresentedFrameId ,
fieldVolume . PresentedLayerCount ,
fieldVolume . CompositeCount ,
2026-07-16 12:23:00 +01:00
fieldParticles . PresentedFrameId ,
fieldParticles . ParticleCount ,
fieldParticles . DispatchCount ,
fieldParticles . DrawCount ,
particleMapCameraIsolated ,
fieldParticles . LastGridCenter ,
2026-07-10 07:14:44 +01:00
movementReceipt ,
2026-07-15 06:14:49 +01:00
lookReceipt ,
2026-07-10 07:14:44 +01:00
focusReceipt ,
2026-07-15 11:04:11 +01:00
tractorReceipt ,
2026-07-15 11:14:22 +01:00
tractorReleaseReceipt ,
2026-07-15 03:21:28 +01:00
actionReceipt ,
2026-07-19 16:01:21 +01:00
tradePurchaseReceipt ,
tradeSaleReceipt ,
tradeRoundTrip ,
tradeItemKey ,
2026-07-15 03:21:28 +01:00
observedShot ,
shieldRatioBefore ,
observedCombat ? . ShieldRatio ? ? shieldRatioBefore ,
hullRatioBefore ,
observedCombat ? . HullRatio ? ? hullRatioBefore ,
2026-07-16 03:34:47 +01:00
( float ) ( observedShot ? . LockQuality ? ? 0 ) ,
2026-07-15 09:08:44 +01:00
aimDotDistance ,
playerViewportWidth ,
playerViewportHeight ,
texturedPlayerMaterialCount ,
directionalLightIntensity ,
pilotAverageLuminance ,
pilotBrightPixelCount ,
2026-07-15 09:49:31 +01:00
cockpitProgressCount ,
2026-07-15 11:04:11 +01:00
tractorBeamPower ,
2026-07-15 11:14:22 +01:00
tractorReleasedPower ,
2026-07-15 11:04:11 +01:00
tractorBeamParticleSystemCount ,
2026-07-15 11:45:50 +01:00
initialPickupEntityCount ,
finalPickupEntityCount ,
pickupCollectionEventCount ,
2026-07-16 22:26:29 +01:00
pickupRejectionEventCount ,
2026-07-16 03:34:47 +01:00
destructionPickupObserved ,
2026-07-15 11:45:50 +01:00
pickupCollection ,
2026-07-16 22:26:29 +01:00
pickupRejection ,
2026-07-15 22:32:26 +01:00
camera . transform . position ,
camera . transform . forward ,
playerMarker . transform . position ,
aimRenderer . ViewDotPosition ,
Convert . ToSingle ( fieldViewport . GetType ( ) . GetProperty ( "MinX" ) ? . GetValue ( fieldViewport ) ) ,
Convert . ToSingle ( fieldViewport . GetType ( ) . GetProperty ( "MinY" ) ? . GetValue ( fieldViewport ) ) ,
Convert . ToSingle ( fieldViewport . GetType ( ) . GetProperty ( "MaxX" ) ? . GetValue ( fieldViewport ) ) ,
Convert . ToSingle ( fieldViewport . GetType ( ) . GetProperty ( "MaxY" ) ? . GetValue ( fieldViewport ) ) ,
Convert . ToInt32 ( fieldSplats . GetType ( ) . GetProperty ( "Count" ) ? . GetValue ( fieldSplats ) ) ,
2026-07-16 04:41:06 +01:00
playerRendererFacts ,
root . GetComponentsInChildren < EveUnityPlayableWorldEntityMarker > ( includeInactive : true )
. Select ( marker = > PresentedEntityFact . From ( camera , marker ) )
. ToArray ( ) ) ;
2026-07-10 04:39:16 +01:00
}
finally
{
RenderTexture . active = null ;
var camera = cameraObject . GetComponent < Camera > ( ) ;
if ( camera ! = null ) camera . targetTexture = null ;
2026-07-14 03:59:45 +01:00
var mapCamera = mapCameraObject . GetComponent < Camera > ( ) ;
if ( mapCamera ! = null ) mapCamera . targetTexture = null ;
2026-07-10 04:39:16 +01:00
if ( target ! = null ) UnityEngine . Object . DestroyImmediate ( target ) ;
if ( pixels ! = null ) UnityEngine . Object . DestroyImmediate ( pixels ) ;
2026-07-14 03:59:45 +01:00
host ? . Disconnect ( ) ;
UnityEngine . Object . DestroyImmediate ( mapCameraObject ) ;
2026-07-10 04:39:16 +01:00
UnityEngine . Object . DestroyImmediate ( cameraObject ) ;
UnityEngine . Object . DestroyImmediate ( root ) ;
}
}
2026-07-19 23:30:44 +01:00
private static async Task < CultMeshRealtimeFrame > ReceiveAdvertisedRealtimeFrameAsync (
string rendezvousEndpoint ,
string verseId )
{
using var timeout = new CancellationTokenSource ( TimeSpan . FromSeconds ( 15 ) ) ;
using var client = new CultMeshClient ( new CultMeshClientOptions
{
RendezvousEndpoints = new [ ] { rendezvousEndpoint } ,
RealtimeConnectors = new ICultMeshRealtimeTransportConnector [ ]
{
new CultMeshNativeQuicRealtimeTransportConnector ( )
}
} ) ;
using var session = await client . ConnectRealtimeAsync ( verseId , timeout . Token ) ;
return await session . ReceiveAsync ( timeout . Token ) ;
}
2026-07-10 07:14:44 +01:00
private static void WriteWitnessFacts (
2026-07-16 03:34:47 +01:00
string witnessProfile ,
2026-07-16 22:26:29 +01:00
string gameplayScenario ,
2026-07-16 14:46:49 +01:00
string assetBodyTransport ,
2026-07-10 07:14:44 +01:00
long initialVersion ,
long finalVersion ,
float movementDistance ,
2026-07-14 16:26:38 +01:00
int playerRendererCount ,
int pilotChangedPixels ,
2026-07-14 03:59:45 +01:00
int mapChannelRendererCount ,
int mapChangedPixels ,
bool pilotCameraExcludesMapChannel ,
bool mapCameraIncludesMapChannel ,
2026-07-15 06:44:09 +01:00
bool environmentPresentation ,
2026-07-15 17:31:02 +01:00
long fieldVolumeFrameId ,
int fieldVolumeLayerCount ,
long fieldVolumeCompositeCount ,
2026-07-16 12:23:00 +01:00
long fieldParticleFrameId ,
int fieldParticleCount ,
int fieldParticleDispatchCount ,
int fieldParticleDrawCount ,
bool fieldParticleMapCameraIsolated ,
Vector2 fieldParticleGridCenter ,
2026-07-10 07:14:44 +01:00
WitnessReceipt movement ,
2026-07-15 06:14:49 +01:00
WitnessReceipt look ,
2026-07-10 07:14:44 +01:00
WitnessReceipt targeting ,
2026-07-15 11:04:11 +01:00
WitnessReceipt tractor ,
2026-07-15 11:14:22 +01:00
WitnessReceipt tractorRelease ,
2026-07-15 03:21:28 +01:00
WitnessReceipt action ,
2026-07-19 16:01:21 +01:00
WitnessReceipt tradePurchase ,
WitnessReceipt tradeSale ,
bool tradeRoundTrip ,
string tradeItemKey ,
2026-07-15 03:21:28 +01:00
EveUnityShotReceipt shot ,
float shieldRatioBefore ,
float shieldRatioAfter ,
float hullRatioBefore ,
float hullRatioAfter ,
2026-07-15 06:14:49 +01:00
float lockProgress ,
2026-07-15 09:08:44 +01:00
float aimDotDistance ,
float playerViewportWidth ,
float playerViewportHeight ,
int texturedPlayerMaterialCount ,
float directionalLightIntensity ,
float pilotAverageLuminance ,
int pilotBrightPixelCount ,
2026-07-15 09:49:31 +01:00
int cockpitProgressCount ,
2026-07-15 11:04:11 +01:00
float tractorBeamPower ,
2026-07-15 11:14:22 +01:00
float tractorReleasedPower ,
2026-07-15 11:04:11 +01:00
int tractorBeamParticleSystemCount ,
2026-07-15 11:45:50 +01:00
int initialPickupEntityCount ,
int finalPickupEntityCount ,
int pickupCollectionEventCount ,
2026-07-16 22:26:29 +01:00
int pickupRejectionEventCount ,
2026-07-16 03:34:47 +01:00
bool destructionPickupObserved ,
2026-07-15 11:45:50 +01:00
EveUnityFeedbackEvent pickupCollection ,
2026-07-16 22:26:29 +01:00
EveUnityFeedbackEvent pickupRejection ,
2026-07-15 22:32:26 +01:00
Vector3 cameraPosition ,
Vector3 cameraForward ,
Vector3 playerPosition ,
Vector3 aimPosition ,
float fieldViewportMinX ,
float fieldViewportMinY ,
float fieldViewportMaxX ,
float fieldViewportMaxY ,
int fieldSplatCount ,
2026-07-16 04:41:06 +01:00
RendererFact [ ] playerRendererFacts ,
PresentedEntityFact [ ] presentedEntityFacts )
2026-07-10 07:14:44 +01:00
{
var path = Environment . GetEnvironmentVariable ( "EVEUNITY_WITNESS_FACTS_PATH" ) ;
if ( string . IsNullOrWhiteSpace ( path ) ) return ;
2026-07-16 22:26:29 +01:00
var pickupEvent = pickupCollection ? ? pickupRejection ;
2026-07-10 07:14:44 +01:00
var document = new WitnessFacts
{
2026-07-16 03:34:47 +01:00
witnessProfile = witnessProfile ,
2026-07-16 22:26:29 +01:00
gameplayScenario = gameplayScenario ,
2026-07-10 07:14:44 +01:00
providerAdvertisement = true ,
providerAssets = true ,
2026-07-16 14:46:49 +01:00
assetBodyTransport = assetBodyTransport ,
2026-07-15 06:44:09 +01:00
environmentPresentation = environmentPresentation ,
2026-07-16 19:01:09 +01:00
ambientProbeEnergy = AmbientProbeEnergy ( ) ,
ambientMode = RenderSettings . ambientMode . ToString ( ) ,
ambientLight = RenderSettings . ambientLight ,
reflectionTexture = RenderSettings . customReflectionTexture ! = null
? RenderSettings . customReflectionTexture . name
: "" ,
2026-07-15 17:31:02 +01:00
fieldVolumeFrameId = fieldVolumeFrameId ,
fieldVolumeLayerCount = fieldVolumeLayerCount ,
fieldVolumeCompositeCount = fieldVolumeCompositeCount ,
2026-07-16 12:23:00 +01:00
fieldParticleFrameId = fieldParticleFrameId ,
fieldParticleCount = fieldParticleCount ,
fieldParticleDispatchCount = fieldParticleDispatchCount ,
fieldParticleDrawCount = fieldParticleDrawCount ,
fieldParticleMapCameraIsolated = fieldParticleMapCameraIsolated ,
fieldParticleGridCenter = fieldParticleGridCenter ,
2026-07-10 07:14:44 +01:00
movement = movement ! = null ,
2026-07-15 06:14:49 +01:00
aimPresentation = look ! = null & & aimDotDistance > = 49.9f ,
2026-07-10 07:14:44 +01:00
targeting = targeting ! = null ,
2026-07-15 11:04:11 +01:00
beamPresentation = tractor ! = null & & tractorBeamPower > 0.01f & & tractorBeamParticleSystemCount > 0 ,
2026-07-19 13:14:07 +01:00
beamRelease = tractorRelease ! = null ,
2026-07-10 07:14:44 +01:00
action = action ! = null ,
2026-07-19 16:01:21 +01:00
tradeRoundTrip = tradeRoundTrip & & tradePurchase ! = null & & tradeSale ! = null ,
tradeItemKey = tradeItemKey ? ? "" ,
2026-07-15 03:21:28 +01:00
combatPresentation = shot ! = null ,
shotId = shot ? . ShotId ? ? "" ,
shotHit = shot ? . Hit ? ? false ,
impactKind = shot ? . ImpactKind ? ? "" ,
appliedDamage = ( float ) ( shot ? . AppliedDamage ? ? 0 ) ,
shieldAbsorbedDamage = ( float ) ( shot ? . ShieldAbsorbedDamage ? ? 0 ) ,
shieldRatioBefore = shieldRatioBefore ,
shieldRatioAfter = shieldRatioAfter ,
hullRatioBefore = hullRatioBefore ,
hullRatioAfter = hullRatioAfter ,
lockProgress = lockProgress ,
2026-07-10 07:14:44 +01:00
initialVersion = initialVersion ,
finalVersion = finalVersion ,
movementDistance = movementDistance ,
2026-07-15 06:14:49 +01:00
aimDotDistance = aimDotDistance ,
2026-07-14 16:26:38 +01:00
playerRendererCount = playerRendererCount ,
2026-07-15 09:08:44 +01:00
playerViewportWidth = playerViewportWidth ,
playerViewportHeight = playerViewportHeight ,
texturedPlayerMaterialCount = texturedPlayerMaterialCount ,
directionalLightIntensity = directionalLightIntensity ,
2026-07-14 16:26:38 +01:00
pilotChangedPixels = pilotChangedPixels ,
2026-07-15 09:08:44 +01:00
pilotAverageLuminance = pilotAverageLuminance ,
pilotBrightPixelCount = pilotBrightPixelCount ,
2026-07-15 09:49:31 +01:00
cockpitProgressCount = cockpitProgressCount ,
2026-07-15 11:04:11 +01:00
tractorBeamPower = tractorBeamPower ,
2026-07-15 11:14:22 +01:00
tractorReleasedPower = tractorReleasedPower ,
2026-07-15 11:04:11 +01:00
tractorBeamParticleSystemCount = tractorBeamParticleSystemCount ,
2026-07-15 11:45:50 +01:00
pickupCollection = pickupCollection ! = null & & pickupCollectionEventCount = = 1 & &
pickupCollection . CargoQuantityAfter - pickupCollection . CargoQuantityBefore = = pickupCollection . ScalarValue ,
2026-07-16 22:26:29 +01:00
pickupRejection = pickupRejection ! = null & & pickupRejectionEventCount > = 1 & &
pickupCollectionEventCount = = 0 & &
string . Equals ( pickupRejection . Reason , "cargo-capacity" , StringComparison . Ordinal ) & &
pickupRejection . CargoQuantityAfter = = pickupRejection . CargoQuantityBefore ,
2026-07-17 13:57:34 +01:00
destructionLoot = pickupEvent ! = null & & ! string . IsNullOrWhiteSpace ( pickupEvent . EventId ) & &
! string . IsNullOrWhiteSpace ( pickupEvent . ItemKey ) & & pickupEvent . ScalarValue > 0 ,
2026-07-15 11:45:50 +01:00
initialPickupEntityCount = initialPickupEntityCount ,
finalPickupEntityCount = finalPickupEntityCount ,
pickupCollectionEventCount = pickupCollectionEventCount ,
2026-07-16 22:26:29 +01:00
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 ) ,
2026-07-15 22:32:26 +01:00
cameraPosition = cameraPosition ,
cameraForward = cameraForward ,
playerPosition = playerPosition ,
aimPosition = aimPosition ,
fieldViewportMinX = fieldViewportMinX ,
fieldViewportMinY = fieldViewportMinY ,
fieldViewportMaxX = fieldViewportMaxX ,
fieldViewportMaxY = fieldViewportMaxY ,
fieldSplatCount = fieldSplatCount ,
2026-07-15 09:08:44 +01:00
playerRenderers = playerRendererFacts ,
2026-07-16 04:41:06 +01:00
presentedEntities = presentedEntityFacts ,
2026-07-14 03:59:45 +01:00
mapChannelRendererCount = mapChannelRendererCount ,
mapChangedPixels = mapChangedPixels ,
pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel ,
mapCameraIncludesMapChannel = mapCameraIncludesMapChannel ,
2026-07-19 16:01:21 +01:00
receipts = new [ ] { movement , look , targeting , tractor , tractorRelease , action , tradePurchase , tradeSale }
2026-07-10 07:14:44 +01:00
} ;
Directory . CreateDirectory ( Path . GetDirectoryName ( Path . GetFullPath ( path ) ) ) ;
File . WriteAllText ( path , JsonUtility . ToJson ( document , true ) ) ;
}
[Serializable]
private sealed class WitnessFacts
{
2026-07-16 03:34:47 +01:00
public string witnessProfile ;
2026-07-16 22:26:29 +01:00
public string gameplayScenario ;
2026-07-10 07:14:44 +01:00
public bool providerAdvertisement ;
public bool providerAssets ;
2026-07-16 14:46:49 +01:00
public string assetBodyTransport ;
2026-07-15 06:44:09 +01:00
public bool environmentPresentation ;
2026-07-16 19:01:09 +01:00
public float ambientProbeEnergy ;
public string ambientMode ;
public Color ambientLight ;
public string reflectionTexture ;
2026-07-15 17:31:02 +01:00
public long fieldVolumeFrameId ;
public int fieldVolumeLayerCount ;
public long fieldVolumeCompositeCount ;
2026-07-16 12:23:00 +01:00
public long fieldParticleFrameId ;
public int fieldParticleCount ;
public int fieldParticleDispatchCount ;
public int fieldParticleDrawCount ;
public bool fieldParticleMapCameraIsolated ;
public Vector2 fieldParticleGridCenter ;
2026-07-10 07:14:44 +01:00
public bool movement ;
2026-07-15 06:14:49 +01:00
public bool aimPresentation ;
2026-07-10 07:14:44 +01:00
public bool targeting ;
2026-07-15 11:04:11 +01:00
public bool beamPresentation ;
2026-07-15 11:14:22 +01:00
public bool beamRelease ;
2026-07-10 07:14:44 +01:00
public bool action ;
2026-07-19 16:01:21 +01:00
public bool tradeRoundTrip ;
public string tradeItemKey ;
2026-07-15 03:21:28 +01:00
public bool combatPresentation ;
public string shotId ;
public bool shotHit ;
public string impactKind ;
public float appliedDamage ;
public float shieldAbsorbedDamage ;
public float shieldRatioBefore ;
public float shieldRatioAfter ;
public float hullRatioBefore ;
public float hullRatioAfter ;
public float lockProgress ;
2026-07-10 07:14:44 +01:00
public long initialVersion ;
public long finalVersion ;
public float movementDistance ;
2026-07-15 06:14:49 +01:00
public float aimDotDistance ;
2026-07-14 16:26:38 +01:00
public int playerRendererCount ;
2026-07-15 09:08:44 +01:00
public float playerViewportWidth ;
public float playerViewportHeight ;
public int texturedPlayerMaterialCount ;
public float directionalLightIntensity ;
2026-07-14 16:26:38 +01:00
public int pilotChangedPixels ;
2026-07-15 09:08:44 +01:00
public float pilotAverageLuminance ;
public int pilotBrightPixelCount ;
2026-07-15 09:49:31 +01:00
public int cockpitProgressCount ;
2026-07-15 11:04:11 +01:00
public float tractorBeamPower ;
2026-07-15 11:14:22 +01:00
public float tractorReleasedPower ;
2026-07-15 11:04:11 +01:00
public int tractorBeamParticleSystemCount ;
2026-07-15 11:45:50 +01:00
public bool pickupCollection ;
2026-07-16 22:26:29 +01:00
public bool pickupRejection ;
2026-07-16 03:34:47 +01:00
public bool destructionLoot ;
2026-07-15 11:45:50 +01:00
public int initialPickupEntityCount ;
public int finalPickupEntityCount ;
public int pickupCollectionEventCount ;
2026-07-16 22:26:29 +01:00
public int pickupRejectionEventCount ;
public string pickupRejectionEventId ;
public string pickupRejectionReason ;
public float cargoQuantityBeforeRejection ;
public float cargoQuantityAfterRejection ;
2026-07-16 03:34:47 +01:00
public string pickupEventId ;
2026-07-15 11:45:50 +01:00
public string pickupItemKey ;
public float pickupQuantity ;
public float cargoQuantityBeforePickup ;
public float cargoQuantityAfterPickup ;
2026-07-15 22:32:26 +01:00
public Vector3 cameraPosition ;
public Vector3 cameraForward ;
public Vector3 playerPosition ;
public Vector3 aimPosition ;
public float fieldViewportMinX ;
public float fieldViewportMinY ;
public float fieldViewportMaxX ;
public float fieldViewportMaxY ;
public int fieldSplatCount ;
2026-07-15 09:08:44 +01:00
public RendererFact [ ] playerRenderers ;
2026-07-16 04:41:06 +01:00
public PresentedEntityFact [ ] presentedEntities ;
2026-07-14 03:59:45 +01:00
public int mapChannelRendererCount ;
public int mapChangedPixels ;
public bool pilotCameraExcludesMapChannel ;
public bool mapCameraIncludesMapChannel ;
2026-07-10 07:14:44 +01:00
public WitnessReceipt [ ] receipts ;
}
2026-07-16 19:01:09 +01:00
private static float AmbientProbeEnergy ( )
{
var probe = RenderSettings . ambientProbe ;
var energy = 0f ;
for ( var channel = 0 ; channel < 3 ; channel + + )
for ( var coefficient = 0 ; coefficient < 9 ; coefficient + + )
energy + = Mathf . Abs ( probe [ channel , coefficient ] ) ;
return energy ;
}
2026-07-10 07:14:44 +01:00
[Serializable]
private sealed class WitnessReceipt
{
public string commandKind ;
public string commandId ;
public string state ;
2026-07-14 16:26:38 +01:00
public string receiptId ;
public string ownerRepo ;
public string authority ;
public string providerId ;
public string surfaceId ;
public bool isProviderOwned ;
2026-07-10 07:14:44 +01:00
public long sourceVersion ;
public static WitnessReceipt From ( string commandKind , EveUnitySceneCommandReceipt receipt )
{
return new WitnessReceipt
{
commandKind = commandKind ,
commandId = receipt . CommandId ,
state = receipt . State ,
2026-07-14 16:26:38 +01:00
receiptId = receipt . ReceiptId ,
ownerRepo = receipt . OwnerRepo ,
authority = receipt . Authority ,
providerId = receipt . ProviderId ,
surfaceId = receipt . SurfaceId ,
isProviderOwned = receipt . IsProviderOwned ,
2026-07-10 07:14:44 +01:00
sourceVersion = receipt . SourceVersion
} ;
}
}
2026-07-15 09:08:44 +01:00
[Serializable]
private sealed class RendererFact
{
public string name ;
public string rendererType ;
public bool enabled ;
public bool activeInHierarchy ;
public int layer ;
public string boundsSize ;
public float viewportWidth ;
public float viewportHeight ;
public int renderedPixelCount ;
public float renderedAverageLuminance ;
public int renderedBrightPixelCount ;
public MaterialFact [ ] materials ;
public static RendererFact From ( Camera camera , Renderer renderer )
{
MeasureViewportCoverage ( camera , new [ ] { renderer } , out var width , out var height ) ;
return new RendererFact
{
name = renderer . name ,
rendererType = renderer . GetType ( ) . Name ,
enabled = renderer . enabled ,
activeInHierarchy = renderer . gameObject . activeInHierarchy ,
layer = renderer . gameObject . layer ,
boundsSize = FormatVector ( renderer . bounds . size ) ,
viewportWidth = width ,
viewportHeight = height ,
materials = renderer . sharedMaterials
. Where ( material = > material ! = null )
. Select ( MaterialFact . From )
. ToArray ( )
} ;
}
}
2026-07-16 04:41:06 +01:00
[Serializable]
private sealed class PresentedEntityFact
{
public string entityId ;
public string entityKind ;
public string label ;
public string assetRef ;
public string worldPosition ;
public string worldScale ;
public int rendererCount ;
public int enabledRendererCount ;
public bool intersectsPilotFrustum ;
public float viewportWidth ;
public float viewportHeight ;
public static PresentedEntityFact From ( Camera camera , EveUnityPlayableWorldEntityMarker marker )
{
var renderers = marker . GetComponentsInChildren < Renderer > ( includeInactive : true ) ;
MeasureViewportCoverage ( camera , renderers , out var width , out var height ) ;
var planes = GeometryUtility . CalculateFrustumPlanes ( camera ) ;
return new PresentedEntityFact
{
entityId = marker . EntityId ,
entityKind = marker . EntityKind ,
label = marker . Label ,
assetRef = marker . AssetRef ,
worldPosition = FormatVector ( marker . transform . position ) ,
worldScale = FormatVector ( marker . transform . lossyScale ) ,
rendererCount = renderers . Length ,
enabledRendererCount = renderers . Count ( renderer = > renderer . enabled & & renderer . gameObject . activeInHierarchy ) ,
intersectsPilotFrustum = renderers . Any ( renderer = >
renderer . enabled & & renderer . gameObject . activeInHierarchy & &
GeometryUtility . TestPlanesAABB ( planes , renderer . bounds ) ) ,
viewportWidth = width ,
viewportHeight = height
} ;
}
}
2026-07-15 09:08:44 +01:00
[Serializable]
private sealed class MaterialFact
{
public string name ;
public string shader ;
public bool shaderSupported ;
public string baseColor ;
public float surface ;
public int textureCount ;
public int renderQueue ;
public static MaterialFact From ( Material material ) = > new MaterialFact
{
name = material . name ,
shader = material . shader ? . name ? ? "" ,
shaderSupported = material . shader ! = null & & material . shader . isSupported ,
baseColor = material . HasProperty ( "_BaseColor" )
? FormatColor ( material . GetColor ( "_BaseColor" ) )
: "" ,
surface = material . HasProperty ( "_Surface" ) ? material . GetFloat ( "_Surface" ) : - 1f ,
textureCount = material . GetTexturePropertyNames ( ) . Count ( name = > material . GetTexture ( name ) ! = null ) ,
renderQueue = material . renderQueue
} ;
}
2026-07-14 16:26:38 +01:00
private static void AssertReconciledProviderReceipt (
EveUnitySceneCommandReceipt receipt ,
string commandId ,
string providerId ,
string surfaceId ,
long minimumSourceVersion )
{
Assert . That ( receipt , Is . Not . Null ) ;
Assert . That ( receipt . CommandId , Is . EqualTo ( commandId ) ) ;
Assert . That ( receipt . State , Is . EqualTo ( "reconciled" ) , receipt . Message ) ;
Assert . That ( receipt . IsProviderOwned , Is . True ) ;
Assert . That ( receipt . ProviderId , Is . EqualTo ( providerId ) ) ;
Assert . That ( receipt . SurfaceId , Is . EqualTo ( surfaceId ) ) ;
Assert . That ( receipt . ReceiptId , Is . Not . Empty ) ;
Assert . That ( receipt . OwnerRepo , Is . Not . Empty ) ;
Assert . That ( receipt . Authority , Is . Not . Empty ) ;
Assert . That ( receipt . SourceVersion , Is . GreaterThan ( minimumSourceVersion ) ) ;
}
2026-07-10 04:39:16 +01:00
private static EveUnityPlayableWorldEntityMarker FindMarker ( GameObject root , string entityId )
{
foreach ( var marker in root . GetComponentsInChildren < EveUnityPlayableWorldEntityMarker > ( ) )
{
if ( string . Equals ( marker . EntityId , entityId , StringComparison . Ordinal ) )
return marker ;
}
throw new InvalidOperationException ( $"Projected world has no entity marker '{entityId}'." ) ;
}
2026-07-15 09:08:44 +01:00
private static bool HasAnyTexture ( Material material )
{
foreach ( var propertyName in material . GetTexturePropertyNames ( ) )
{
if ( material . GetTexture ( propertyName ) ! = null )
return true ;
}
return false ;
}
private static string FormatVector ( Vector3 value ) = >
$"{value.x:R},{value.y:R},{value.z:R}" ;
private static string FormatColor ( Color value ) = >
$"{value.r:R},{value.g:R},{value.b:R},{value.a:R}" ;
private static void MeasureViewportCoverage (
Camera camera ,
IReadOnlyList < Renderer > renderers ,
out float width ,
out float height )
{
var min = new Vector2 ( float . PositiveInfinity , float . PositiveInfinity ) ;
var max = new Vector2 ( float . NegativeInfinity , float . NegativeInfinity ) ;
foreach ( var renderer in renderers )
{
var bounds = renderer . bounds ;
for ( var x = - 1 ; x < = 1 ; x + = 2 )
for ( var y = - 1 ; y < = 1 ; y + = 2 )
for ( var z = - 1 ; z < = 1 ; z + = 2 )
{
var point = bounds . center + Vector3 . Scale ( bounds . extents , new Vector3 ( x , y , z ) ) ;
var viewport = camera . WorldToViewportPoint ( point ) ;
if ( viewport . z < = 0f ) continue ;
min = Vector2 . Min ( min , viewport ) ;
max = Vector2 . Max ( max , viewport ) ;
}
}
width = float . IsInfinity ( min . x ) ? 0f : Mathf . Max ( 0f , max . x - min . x ) ;
height = float . IsInfinity ( min . y ) ? 0f : Mathf . Max ( 0f , max . y - min . y ) ;
}
private static void MeasureLuminance (
Texture2D texture ,
out float average ,
out int brightPixelCount )
{
double total = 0 ;
brightPixelCount = 0 ;
var pixels = texture . GetPixels32 ( ) ;
foreach ( var pixel in pixels )
{
var luminance = ( 0.2126 * pixel . r + 0.7152 * pixel . g + 0.0722 * pixel . b ) / 255.0 ;
total + = luminance ;
if ( luminance > = 0.2 )
brightPixelCount + + ;
}
average = pixels . Length = = 0 ? 0f : ( float ) ( total / pixels . Length ) ;
}
private static void MeasureRendererOutputs (
Camera camera ,
RenderTexture target ,
Texture2D pixels ,
IReadOnlyList < Renderer > allRenderers ,
IReadOnlyList < Renderer > measuredRenderers ,
IReadOnlyList < RendererFact > facts )
{
var enabled = allRenderers . Select ( renderer = > renderer . enabled ) . ToArray ( ) ;
var clearFlags = camera . clearFlags ;
var background = camera . backgroundColor ;
try
{
foreach ( var renderer in allRenderers )
renderer . enabled = false ;
camera . clearFlags = CameraClearFlags . SolidColor ;
camera . backgroundColor = Color . black ;
2026-07-15 20:07:35 +01:00
camera . Render ( ) ;
RenderTexture . active = target ;
pixels . ReadPixels ( new Rect ( 0 , 0 , target . width , target . height ) , 0 , 0 ) ;
pixels . Apply ( ) ;
var baseline = pixels . GetPixels ( ) ;
2026-07-15 09:08:44 +01:00
for ( var index = 0 ; index < measuredRenderers . Count ; index + + )
{
var fact = facts [ index ] ;
2026-07-16 02:23:22 +01:00
var rendererLayer = measuredRenderers [ index ] . gameObject . layer ;
if ( rendererLayer > = 0 & & rendererLayer < 32 & &
( camera . cullingMask & ( 1 < < rendererLayer ) ) = = 0 )
{
fact . renderedPixelCount = 0 ;
fact . renderedAverageLuminance = 0 ;
fact . renderedBrightPixelCount = 0 ;
continue ;
}
2026-07-15 09:08:44 +01:00
measuredRenderers [ index ] . enabled = true ;
camera . Render ( ) ;
RenderTexture . active = target ;
pixels . ReadPixels ( new Rect ( 0 , 0 , target . width , target . height ) , 0 , 0 ) ;
pixels . Apply ( ) ;
2026-07-15 20:07:35 +01:00
MeasureDifference (
pixels . GetPixels ( ) ,
baseline ,
out fact . renderedPixelCount ,
2026-07-15 09:08:44 +01:00
out fact . renderedAverageLuminance ,
out fact . renderedBrightPixelCount ) ;
measuredRenderers [ index ] . enabled = false ;
}
}
finally
{
for ( var index = 0 ; index < allRenderers . Count ; index + + )
allRenderers [ index ] . enabled = enabled [ index ] ;
camera . clearFlags = clearFlags ;
camera . backgroundColor = background ;
}
}
2026-07-15 20:07:35 +01:00
private static void MeasureDifference (
IReadOnlyList < Color > actual ,
IReadOnlyList < Color > baseline ,
out int changedPixelCount ,
out float averageLuminance ,
out int brightPixelCount )
{
changedPixelCount = 0 ;
brightPixelCount = 0 ;
double total = 0 ;
var count = Math . Min ( actual . Count , baseline . Count ) ;
for ( var index = 0 ; index < count ; index + + )
{
var delta = new Color (
Mathf . Abs ( actual [ index ] . r - baseline [ index ] . r ) ,
Mathf . Abs ( actual [ index ] . g - baseline [ index ] . g ) ,
Mathf . Abs ( actual [ index ] . b - baseline [ index ] . b ) ) ;
var magnitude = delta . r + delta . g + delta . b ;
if ( magnitude > 0.08f ) changedPixelCount + + ;
var luminance = delta . r * 0.2126f + delta . g * 0.7152f + delta . b * 0.0722f ;
total + = luminance ;
if ( luminance > = 0.2f ) brightPixelCount + + ;
}
averageLuminance = count = = 0 ? 0f : ( float ) ( total / count ) ;
}
2026-07-14 03:59:45 +01:00
private static int CountChangedPixels ( Texture2D texture , Color background )
{
var changed = 0 ;
foreach ( var pixel in texture . GetPixels ( ) )
{
if ( Mathf . Abs ( pixel . r - background . r ) +
Mathf . Abs ( pixel . g - background . g ) +
Mathf . Abs ( pixel . b - background . b ) > 0.08f )
changed + + ;
}
return changed ;
}
2026-07-15 20:07:35 +01:00
private static void WriteFieldLayerCaptures ( GameObject root , EveUnityPlayableWorldProjection world , string capturePath )
{
var renderer = root . GetComponent < EveFieldsSplatLayerRenderer > ( ) ;
if ( renderer = = null | | world = = null | | string . IsNullOrWhiteSpace ( capturePath ) ) return ;
var directory = Path . GetDirectoryName ( Path . GetFullPath ( capturePath ) ) ;
foreach ( var field in world . FieldVolumes )
{
if ( ! field . Props . TryGetValue ( "layerBindings" , out var bindings ) ) continue ;
foreach ( var pair in ( bindings ? ? "" ) . Split ( ';' ) )
{
var separator = pair . IndexOf ( '=' ) ;
if ( separator < = 0 ) continue ;
var layerKey = pair . Substring ( 0 , separator ) . Trim ( ) ;
if ( ! renderer . TryGetTexture ( layerKey , out var texture ) | | texture = = null ) continue ;
var previous = RenderTexture . active ;
RenderTexture . active = texture ;
var copy = new Texture2D ( texture . width , texture . height , TextureFormat . RGBA32 , false , true ) ;
copy . ReadPixels ( new Rect ( 0 , 0 , texture . width , texture . height ) , 0 , 0 ) ;
copy . Apply ( ) ;
RenderTexture . active = previous ;
File . WriteAllBytes ( Path . Combine ( directory , $"field-{layerKey}.png" ) , copy . EncodeToPNG ( ) ) ;
UnityEngine . Object . DestroyImmediate ( copy ) ;
RenderTexture . active = texture ;
var floatCopy = new Texture2D ( texture . width , texture . height , TextureFormat . RGBAFloat , false , true ) ;
floatCopy . ReadPixels ( new Rect ( 0 , 0 , texture . width , texture . height ) , 0 , 0 ) ;
floatCopy . Apply ( ) ;
RenderTexture . active = previous ;
2026-07-16 01:54:09 +01:00
File . WriteAllBytes (
Path . Combine ( directory , $"field-{layerKey}.exr" ) ,
floatCopy . EncodeToEXR ( Texture2D . EXRFlags . OutputAsFloat ) ) ;
2026-07-15 20:07:35 +01:00
UnityEngine . Object . DestroyImmediate ( floatCopy ) ;
}
}
}
private static void WriteFieldCloudCapture ( GameObject root , string capturePath )
{
var renderer = root . GetComponent < EveUnityFieldsVolumeRenderer > ( ) ;
2026-07-15 21:15:06 +01:00
if ( renderer = = null | | string . IsNullOrWhiteSpace ( capturePath ) ) return ;
var raymarchField = typeof ( EveUnityFieldsVolumeRenderer ) . GetField (
"_raymarchTexture" ,
2026-07-15 20:07:35 +01:00
System . Reflection . BindingFlags . Instance | System . Reflection . BindingFlags . NonPublic ) ;
2026-07-15 21:15:06 +01:00
var historyTexturesField = typeof ( EveUnityFieldsVolumeRenderer ) . GetField (
"_historyTextures" ,
System . Reflection . BindingFlags . Instance | System . Reflection . BindingFlags . NonPublic ) ;
var historyIndexField = typeof ( EveUnityFieldsVolumeRenderer ) . GetField (
"_historyTextureIndex" ,
System . Reflection . BindingFlags . Instance | System . Reflection . BindingFlags . NonPublic ) ;
2026-07-16 01:54:09 +01:00
var materialField = typeof ( EveUnityFieldsVolumeRenderer ) . GetField (
"_material" ,
System . Reflection . BindingFlags . Instance | System . Reflection . BindingFlags . NonPublic ) ;
var metadataField = typeof ( EveUnityFieldsVolumeRenderer ) . GetField (
"_programMetadata" ,
System . Reflection . BindingFlags . Instance | System . Reflection . BindingFlags . NonPublic ) ;
2026-07-15 21:15:06 +01:00
var raymarchTexture = raymarchField ? . GetValue ( renderer ) as RenderTexture ;
var historyTextures = historyTexturesField ? . GetValue ( renderer ) as RenderTexture [ ] ;
var historyIndex = historyIndexField ? . GetValue ( renderer ) is int value ? value : - 1 ;
var previous = RenderTexture . active ;
var directory = Path . GetDirectoryName ( Path . GetFullPath ( capturePath ) ) ;
WriteFloatTexture ( raymarchTexture , Path . Combine ( directory , "field-volume-raymarch.exr" ) ) ;
var historyTexture = historyIndex > = 0 & & historyTextures ! = null & & historyIndex < historyTextures . Length
? historyTextures [ historyIndex ]
: null ;
WriteFloatTexture ( historyTexture , Path . Combine ( directory , "field-volume-history.exr" ) ) ;
WriteFloatTexture ( historyTexture ? ? raymarchTexture , Path . Combine ( directory , "field-volume-cloud.exr" ) ) ;
2026-07-16 01:54:09 +01:00
WriteVolumeMaterialFacts (
materialField ? . GetValue ( renderer ) as Material ,
metadataField ? . GetValue ( renderer ) as IReadOnlyDictionary < string , string > ,
Path . Combine ( directory , "field-volume-material.txt" ) ) ;
2026-07-15 21:15:06 +01:00
RenderTexture . active = previous ;
}
2026-07-16 01:54:09 +01:00
private static void WriteVolumeMaterialFacts (
Material material ,
IReadOnlyDictionary < string , string > metadata ,
string path )
{
if ( material = = null | | metadata = = null ) return ;
var facts = new List < string > ( ) ;
foreach ( var binding in metadata . OrderBy ( value = > value . Key , StringComparer . Ordinal ) )
{
if ( binding . Key . Contains ( ".floatPort." , StringComparison . Ordinal ) )
facts . Add ( $"{binding.Key}={binding.Value}:{material.GetFloat(binding.Value):R}" ) ;
else if ( binding . Key . Contains ( ".vectorPort." , StringComparison . Ordinal ) )
facts . Add ( $"{binding.Key}={binding.Value}:{material.GetVector(binding.Value)}" ) ;
else if ( binding . Key . Contains ( ".texturePort." , StringComparison . Ordinal ) )
{
var texture = material . HasProperty ( binding . Value ) ? material . GetTexture ( binding . Value ) : null ;
facts . Add ( $"{binding.Key}={binding.Value}:{texture?.name ?? " < none > "}:{texture?.width ?? 0}x{texture?.height ?? 0}" ) ;
}
}
File . WriteAllLines ( path , facts ) ;
}
2026-07-15 21:15:06 +01:00
private static void WriteFloatTexture ( RenderTexture texture , string path )
{
if ( texture = = null | | string . IsNullOrWhiteSpace ( path ) ) return ;
2026-07-15 20:07:35 +01:00
var previous = RenderTexture . active ;
RenderTexture . active = texture ;
var copy = new Texture2D ( texture . width , texture . height , TextureFormat . RGBAFloat , false , true ) ;
copy . ReadPixels ( new Rect ( 0 , 0 , texture . width , texture . height ) , 0 , 0 ) ;
copy . Apply ( ) ;
RenderTexture . active = previous ;
2026-07-16 01:54:09 +01:00
File . WriteAllBytes ( path , copy . EncodeToEXR ( Texture2D . EXRFlags . OutputAsFloat ) ) ;
2026-07-15 20:07:35 +01:00
UnityEngine . Object . DestroyImmediate ( copy ) ;
}
2026-07-10 03:34:56 +01:00
private static EveUnitySceneProviderSurfaceDocument WorldDocument ( )
{
var entities = new [ ]
{
Entity ( "player-node" , "player" , "player" , "Vanguard" , "0,0,0" , "1.2" , true ) ,
Entity ( "enemy-node" , "enemy" , "enemy" , "Raider" , "3,0,4" , "1" , false ) ,
Entity ( "landmark-node" , "landmark" , "landmark" , "Beacon" , "-4,0,6" , "1.8" , false )
} ;
var document = new EveSurfaceDocument (
"eve.world-smoke" ,
"game.runtime" ,
"Generic interactive world" ,
1 ,
"2026-07-10T00:00:00Z" ,
new EveSurfaceTree (
"eve.world-smoke.surface" ,
new EveSurfaceComponent (
"world.root" ,
"surface" ,
Props ( ) ,
new [ ]
{
new EveSurfaceComponent (
"world.scene" ,
"world.scene3d" ,
Props (
( "playerEntityId" , "player" ) ,
( "movementCommand" , "world.move" ) ,
( "inputProfile" , "third-person" ) ,
( "cameraRig" , "third-person-orbit" ) ) ,
entities )
} ) ,
Array . Empty < EveStyleToken > ( ) ) ,
Array . Empty < EveCommandTemplate > ( ) ) ;
return new EveUnitySceneProviderSurfaceDocument (
document ,
new EveUnitySceneProviderSurfaceAdvertisement (
"eve.world-smoke.surface" ,
"interactive-world" ,
new EveUnitySceneWorldInteraction (
"provider-authored-world-surface" ,
"world.commands" ,
"gamecult.eve.command_receipt.v1" ,
"provider-owns-world-state-command-acceptance-and-receipts" ) ) ,
"cultmesh://eve.world-smoke/surfaces/world" ,
1 ) ;
}
private static EveSurfaceComponent Entity (
string nodeId ,
string entityId ,
string entityKind ,
string label ,
string position ,
string radius ,
bool controllable )
{
return new EveSurfaceComponent (
nodeId ,
"world.entity3d" ,
Props (
( "entityId" , entityId ) ,
( "entityKind" , entityKind ) ,
( "label" , label ) ,
( "position" , position ) ,
( "radius" , radius ) ,
( "assetRef" , $"cultmesh://eve.world-smoke/assets/{entityKind}" ) ,
( "selectable" , "true" ) ,
( "controllable" , controllable ? "true" : "false" ) ,
( "moveCommand" , controllable ? "world.move" : "" ) ) ,
Array . Empty < EveSurfaceComponent > ( ) ) ;
}
private static Dictionary < string , string > Props ( params ( string Key , string Value ) [ ] values )
{
var props = new Dictionary < string , string > ( StringComparer . Ordinal ) ;
foreach ( var value in values ) props [ value . Key ] = value . Value ;
return props ;
}
private sealed class WorldSmokeProvider : MonoBehaviour ,
IEveUnitySceneProviderSurfaceDocumentSource ,
IEveUnitySceneCommandSink
{
public EveUnitySceneProviderSurfaceDocument CurrentDocument { get ; set ; }
public string SinkKind = > "world-smoke-command-sink" ;
public List < EveSurfaceCommandRequest > Submitted { get ; } = new List < EveSurfaceCommandRequest > ( ) ;
public event Action < EveUnitySceneProviderSurfaceDocument > DocumentAvailable ;
public void Submit ( EveSurfaceCommandRequest request ) = > Submitted . Add ( request ) ;
}
}
}