1991 lines
114 KiB
C#
1991 lines
114 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using GameCult.Caching;
|
|
using GameCult.Caching.MessagePack;
|
|
using GameCult.Eve.Surface;
|
|
using GameCult.Eve.UnityUIToolkit;
|
|
using GameCult.Eve.UnityScene;
|
|
using GameCult.Eve.UnityScene.Fields;
|
|
using GameCult.Mesh;
|
|
using GameCult.Mesh.Quic.Native;
|
|
using NUnit.Framework;
|
|
using UnityEngine;
|
|
using UnityEngine.Rendering;
|
|
using UnityEngine.Rendering.Universal;
|
|
using UnityEngine.TestTools;
|
|
using UnityEngine.UIElements;
|
|
using Stopwatch = System.Diagnostics.Stopwatch;
|
|
|
|
namespace GameCult.EveUnity.GenericClient.PlayModeTests
|
|
{
|
|
public sealed class GenericWorldCaptureTests
|
|
{
|
|
[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"));
|
|
}
|
|
|
|
[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);
|
|
}
|
|
}
|
|
|
|
[UnityTest]
|
|
[Timeout(300000)]
|
|
public IEnumerator GenericCultMeshClientLowersAndMovesAdvertisedWorld()
|
|
{
|
|
var rendezvousEndpoint = Environment.GetEnvironmentVariable("EVEUNITY_RENDEZVOUS_ENDPOINT");
|
|
if (string.IsNullOrWhiteSpace(rendezvousEndpoint))
|
|
Assert.Ignore("EVEUNITY_RENDEZVOUS_ENDPOINT is not configured for the live provider witness.");
|
|
|
|
var providerId = Environment.GetEnvironmentVariable("EVEUNITY_PROVIDER_ID") ?? "";
|
|
var surfaceId = Environment.GetEnvironmentVariable("EVEUNITY_SURFACE_ID") ?? "";
|
|
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");
|
|
var mapCapturePath = Environment.GetEnvironmentVariable("EVEUNITY_AETHERIA_MAP_CAPTURE_PATH") ??
|
|
Path.Combine(Application.temporaryCachePath, "aetheria-daemon-map.png");
|
|
var root = new GameObject("Generic Eve World Projection");
|
|
var cameraObject = new GameObject("Generic Eve Capture Camera");
|
|
var mapCameraObject = new GameObject("Generic Eve Map Camera");
|
|
RenderTexture target = null;
|
|
Texture2D pixels = null;
|
|
EveUnityCultMeshPlayableWorldProvider provider = null;
|
|
EveUnityPlayableWorldClientHost host = null;
|
|
EveUnityPlayableWorldRuntime runtime = null;
|
|
WitnessReceipt movementReceipt = null;
|
|
WitnessReceipt lookReceipt = null;
|
|
WitnessReceipt focusReceipt = null;
|
|
WitnessReceipt tractorReceipt = null;
|
|
WitnessReceipt tractorReleaseReceipt = null;
|
|
WitnessReceipt actionReceipt = null;
|
|
WitnessReceipt tradePurchaseReceipt = null;
|
|
WitnessReceipt tradeSaleReceipt = null;
|
|
bool tradeRoundTrip = false;
|
|
string tradeItemKey = "";
|
|
EveUnityShotReceipt observedShot = null;
|
|
EveUnityCombatPresentation observedCombat = null;
|
|
float shieldRatioBefore = 0f;
|
|
float hullRatioBefore = 0f;
|
|
long initialVersion = 0;
|
|
float movementDistance = 0f;
|
|
float aimDotDistance = 0f;
|
|
int cockpitProgressCount = 0;
|
|
float tractorBeamPower = 0f;
|
|
float tractorReleasedPower = 1f;
|
|
int tractorBeamParticleSystemCount = 0;
|
|
EveUnityFeedbackEvent pickupCollection = null;
|
|
EveUnityFeedbackEvent pickupRejection = null;
|
|
int pickupCollectionEventCount = 0;
|
|
int pickupRejectionEventCount = 0;
|
|
var pickupRejectionEventIds = new HashSet<string>(StringComparer.Ordinal);
|
|
int initialPickupEntityCount = 0;
|
|
int finalPickupEntityCount = -1;
|
|
bool destructionPickupObserved = false;
|
|
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);
|
|
var gameplayScenario = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_GAMEPLAY_SCENARIO") ??
|
|
"released-client-proof";
|
|
Assert.That(gameplayScenario,
|
|
Is.EqualTo("released-client-proof").Or.EqualTo("cargo-capacity-rejection-proof"),
|
|
"The live witness must select an explicit daemon-owned gameplay scenario.");
|
|
var expectCargoRejection = string.Equals(
|
|
gameplayScenario,
|
|
"cargo-capacity-rejection-proof",
|
|
StringComparison.Ordinal);
|
|
EveUnityAimPresentationRenderer aimRenderer = null;
|
|
EveUnityBeamPresentation beamPresentation = null;
|
|
EveUnityBeamPresentationRenderer beamRenderer = null;
|
|
EveUnityShotTrajectoryRenderer trajectories = null;
|
|
|
|
try
|
|
{
|
|
var providerReadyPath = Environment.GetEnvironmentVariable("EVEUNITY_PROVIDER_READY_PATH");
|
|
if (!string.IsNullOrWhiteSpace(providerReadyPath))
|
|
File.WriteAllText(providerReadyPath, DateTimeOffset.UtcNow.ToString("O"));
|
|
provider = root.AddComponent<EveUnityCultMeshPlayableWorldProvider>();
|
|
provider.Configure(
|
|
rendezvousEndpoint,
|
|
replicaPath,
|
|
providerId,
|
|
surfaceId,
|
|
requiredSurfaceKind: "interactive-world",
|
|
clientRuntimeId: $"eve-unity-test-{Guid.NewGuid():N}");
|
|
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);
|
|
}
|
|
var publicationDeadline = Time.realtimeSinceStartup + 30f;
|
|
while (true)
|
|
{
|
|
var published = false;
|
|
try
|
|
{
|
|
provider.Refresh();
|
|
published = true;
|
|
}
|
|
catch (InvalidOperationException) when (Time.realtimeSinceStartup < publicationDeadline)
|
|
{
|
|
}
|
|
catch (TimeoutException) when (Time.realtimeSinceStartup < publicationDeadline)
|
|
{
|
|
}
|
|
if (published) break;
|
|
yield return new WaitForSecondsRealtime(0.1f);
|
|
}
|
|
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);
|
|
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);
|
|
Assert.That(provider.CurrentAssetBodyTransportKind, Is.Null,
|
|
"Publishing the provider catalog eagerly transferred an asset bundle.");
|
|
host = root.AddComponent<EveUnityPlayableWorldClientHost>();
|
|
host.Configure(
|
|
root.transform,
|
|
provider,
|
|
provider,
|
|
provider,
|
|
provider,
|
|
provider);
|
|
host.Connect();
|
|
var loweredSurface = new EveUiToolkitSurfaceLowerer().Lower(
|
|
provider.CurrentDocument.SurfaceDocument,
|
|
provider.Submit);
|
|
var cockpit = loweredSurface.Q<VisualElement>("aetheria.daemon.game.cockpit");
|
|
Assert.That(cockpit, Is.Not.Null, "The provider did not publish a generic cockpit overlay.");
|
|
cockpitProgressCount = cockpit.Query<ProgressBar>().ToList().Count;
|
|
Assert.That(cockpitProgressCount, Is.GreaterThanOrEqualTo(7));
|
|
Assert.That(loweredSurface.Q<VisualElement>("aetheria.daemon.game.frame").style.display.value,
|
|
Is.EqualTo(DisplayStyle.None), "Daemon diagnostics leaked into the pilot UI.");
|
|
trajectories = root.GetComponent<EveUnityShotTrajectoryRenderer>();
|
|
Assert.That(trajectories, Is.Not.Null);
|
|
host.ShotAvailable += shot =>
|
|
{
|
|
observedShot = shot;
|
|
maximumTrajectoryCount = Math.Max(maximumTrajectoryCount, trajectories.ActiveTrajectoryCount);
|
|
};
|
|
runtime = host.Runtime;
|
|
Assert.That(runtime, Is.Not.Null);
|
|
Assert.That(runtime.ActiveWorld.PlayerEntityId, Is.Not.Empty);
|
|
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));
|
|
initialPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity =>
|
|
string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal));
|
|
if (requireSessionGameplay)
|
|
Assert.That(initialPickupEntityCount, Is.Zero,
|
|
"The witness scenario must obtain salvage from daemon-owned destruction, not a boot-seeded pickup.");
|
|
var playerId = runtime.ActiveWorld.PlayerEntityId;
|
|
var playerFact = host.PresentedEntities.CurrentGeneration.Entities
|
|
.Single(entity => entity.EntityId == playerId);
|
|
runtime.FeedbackAvailable += feedback =>
|
|
{
|
|
if (feedback.TargetEntityIndex != playerFact.SourceIndex) return;
|
|
if (string.Equals(feedback.Kind, "pickup.collected", StringComparison.Ordinal))
|
|
{
|
|
pickupCollectionEventCount++;
|
|
pickupCollection = feedback;
|
|
}
|
|
else if (string.Equals(feedback.Kind, "pickup.rejected", StringComparison.Ordinal))
|
|
{
|
|
pickupRejectionEventCount++;
|
|
pickupRejectionEventIds.Add(feedback.EventId);
|
|
pickupRejection = feedback;
|
|
}
|
|
};
|
|
|
|
var playerPrefab = provider.ResolvePrefab(new EveUnityPlayableWorldAssetBinding(
|
|
playerFact.AssetRef,
|
|
playerFact.EntityKind,
|
|
"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.");
|
|
var marker = FindMarker(root, playerId);
|
|
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.");
|
|
|
|
if (requireSessionGameplay)
|
|
{
|
|
var dockVersion = runtime.ActiveVersion;
|
|
var dock = host.SubmitAdvertisedActionIntent(playerId, "pilot.dock");
|
|
var dockDeadline = Time.realtimeSinceStartup + 12f;
|
|
while (Time.realtimeSinceStartup < dockDeadline &&
|
|
(runtime.LastReceipt == null || runtime.LastReceipt.CommandId != dock.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.buy.", StringComparison.Ordinal) &&
|
|
string.Equals(candidate.Availability, "available", StringComparison.OrdinalIgnoreCase))))
|
|
{
|
|
yield return new WaitForSecondsRealtime(0.1f);
|
|
runtime.Refresh();
|
|
}
|
|
AssertReconciledProviderReceipt(runtime.LastReceipt, dock.CommandId,
|
|
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);
|
|
}
|
|
|
|
var initialPosition = marker.transform.position;
|
|
initialVersion = runtime.ActiveVersion;
|
|
var request = runtime.SubmitMoveVectorIntent(playerId, 1f, 0f, 1f);
|
|
|
|
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();
|
|
}
|
|
|
|
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);
|
|
|
|
if (requireSessionGameplay)
|
|
{
|
|
var movementVersion = runtime.ActiveVersion;
|
|
var focus = runtime.SubmitFocusIntent(playerId);
|
|
var focusDeadline = Time.realtimeSinceStartup + 12f;
|
|
while (Time.realtimeSinceStartup < focusDeadline &&
|
|
(runtime.LastReceipt == null || runtime.LastReceipt.CommandId != focus.CommandId ||
|
|
!string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) ||
|
|
runtime.ActiveVersion < runtime.LastReceipt.SourceVersion ||
|
|
string.IsNullOrWhiteSpace(EveUnityCombatPresentation.Find(runtime.ActiveProjection)?.SelectedTargetEntityId)))
|
|
{
|
|
yield return new WaitForSecondsRealtime(0.1f);
|
|
runtime.Refresh();
|
|
}
|
|
AssertReconciledProviderReceipt(
|
|
runtime.LastReceipt,
|
|
focus.CommandId,
|
|
provider.Selection.ProviderId,
|
|
provider.Selection.SurfaceId,
|
|
movementVersion);
|
|
Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion));
|
|
focusReceipt = WitnessReceipt.From("targeting", runtime.LastReceipt);
|
|
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();
|
|
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))
|
|
{
|
|
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);
|
|
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.");
|
|
Assert.That(beamPresentation.ActivationActionId, Is.Not.Empty,
|
|
"The beam presentation does not name an advertised generic action.");
|
|
var tractorVersion = runtime.ActiveVersion;
|
|
var tractor = host.SubmitAdvertisedActionValueIntent(playerId, beamPresentation.ActivationActionId, 1f);
|
|
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 ||
|
|
EveUnityBeamPresentation.FindAll(runtime.ActiveProjection).Single().Power < 0.99f))
|
|
{
|
|
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}'.");
|
|
beamRenderer = root.GetComponent<EveUnityBeamPresentationRenderer>();
|
|
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);
|
|
Assert.That(tractorBeamPower, Is.GreaterThanOrEqualTo(0.99f),
|
|
"Held input did not reach full daemon-authored tractor power before capture.");
|
|
tractorBeamParticleSystemCount = beamRenderer.ActiveParticleSystemCount;
|
|
Assert.That(tractorBeamParticleSystemCount, Is.GreaterThan(0),
|
|
"The generic beam lowerer did not instantiate the provider-owned effect prefab.");
|
|
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);
|
|
|
|
var combatRenderer = root.GetComponent<EveUnityCombatPresentationRenderer>();
|
|
Assert.That(combatRenderer, Is.Not.Null);
|
|
var focusVersion = runtime.ActiveVersion;
|
|
var inputDriver = root.GetComponent<EveUnityPlayableWorldInputDriver>() ??
|
|
root.AddComponent<EveUnityPlayableWorldInputDriver>();
|
|
inputDriver.Host = host;
|
|
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);
|
|
Assert.That(action, Is.Not.Null,
|
|
"The advertised input capability did not bind a primary action for this generic client.");
|
|
var actionDeadline = Time.realtimeSinceStartup + 12f;
|
|
while (Time.realtimeSinceStartup < actionDeadline &&
|
|
(runtime.LastReceipt == null || runtime.LastReceipt.CommandId != action!.CommandId ||
|
|
!string.Equals(runtime.LastReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) ||
|
|
runtime.ActiveVersion < runtime.LastReceipt.SourceVersion))
|
|
{
|
|
yield return new WaitForSecondsRealtime(0.1f);
|
|
runtime.Refresh();
|
|
combatRenderer.RefreshNow();
|
|
hitMarkerObserved |= combatRenderer.HitMarkerVisible;
|
|
}
|
|
AssertReconciledProviderReceipt(
|
|
runtime.LastReceipt,
|
|
action!.CommandId,
|
|
provider.Selection.ProviderId,
|
|
provider.Selection.SurfaceId,
|
|
focusVersion);
|
|
Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(runtime.LastReceipt.SourceVersion));
|
|
actionReceipt = WitnessReceipt.From("action", runtime.LastReceipt);
|
|
|
|
var shotDeadline = Time.realtimeSinceStartup + 20f;
|
|
while (Time.realtimeSinceStartup < shotDeadline &&
|
|
(observedShot == null ||
|
|
host.PresentedEntities.CurrentGeneration.Entities.Any(entity =>
|
|
entity.EntityId == targetEntityId) ||
|
|
(!destructionPickupObserved && pickupCollection == null && pickupRejection == null)))
|
|
{
|
|
yield return new WaitForSecondsRealtime(0.05f);
|
|
runtime.Refresh();
|
|
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;
|
|
}
|
|
Assert.That(observedShot, Is.Not.Null,
|
|
"A reconciled fire request never reached daemon-owned lock acquisition and shot resolution.");
|
|
Assert.That(host.PresentedEntities.CurrentGeneration.Entities.Any(entity =>
|
|
entity.EntityId == targetEntityId), Is.False,
|
|
"The low-hull witness raider was not destroyed by daemon combat.");
|
|
Assert.That(destructionPickupObserved || pickupCollection != null || pickupRejection != null, Is.True,
|
|
"Daemon destruction did not publish its guaranteed cargo as a pickup.");
|
|
// 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;
|
|
var nextTractorAimAt = 0f;
|
|
while (Time.realtimeSinceStartup < collectionDeadline &&
|
|
(expectCargoRejection
|
|
? pickupRejection == null
|
|
: pickupCollection == null || host.PresentedEntities.CurrentGeneration.Entities.Any(entity =>
|
|
string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal))))
|
|
{
|
|
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);
|
|
nextTractorAimAt = Time.realtimeSinceStartup + 0.5f;
|
|
}
|
|
finalPickupEntityCount = host.PresentedEntities.CurrentGeneration.Entities.Count(entity =>
|
|
string.Equals(entity.EntityKind, "pickup", StringComparison.Ordinal));
|
|
if (expectCargoRejection)
|
|
{
|
|
Assert.That(pickupRejection, Is.Not.Null,
|
|
"Full cargo did not reject destruction salvage at daemon pickup proximity.");
|
|
Assert.That(pickupRejectionEventCount, Is.EqualTo(1),
|
|
"The first destruction-loot proximity attempt must emit one rejection event.");
|
|
Assert.That(pickupRejectionEventIds.Count, Is.EqualTo(pickupRejectionEventCount),
|
|
"One daemon proximity attempt emitted duplicate player rejection feedback.");
|
|
Assert.That(pickupCollectionEventCount, Is.Zero,
|
|
"Capacity-rejected salvage was also collected.");
|
|
Assert.That(pickupRejection.ItemKey, Is.Not.Empty);
|
|
Assert.That(pickupRejection.EventId, Does.StartWith("frame:"),
|
|
"Capacity feedback must retain its daemon frame/zone/pickup attempt identity.");
|
|
Assert.That(pickupRejection.Reason, Is.EqualTo("cargo-capacity"));
|
|
Assert.That(pickupRejection.ScalarValue, Is.EqualTo(1));
|
|
Assert.That(pickupRejection.CargoQuantityAfter,
|
|
Is.EqualTo(pickupRejection.CargoQuantityBefore));
|
|
Assert.That(finalPickupEntityCount, Is.EqualTo(1),
|
|
"Capacity-rejected destruction loot disappeared from provider state.");
|
|
}
|
|
else
|
|
{
|
|
Assert.That(pickupCollection, Is.Not.Null,
|
|
"Destruction-created salvage never reached cargo through daemon pickup proximity.");
|
|
Assert.That(pickupCollectionEventCount, Is.EqualTo(1),
|
|
"One durable destruction pickup must emit one collection event.");
|
|
Assert.That(pickupRejectionEventCount, Is.Zero,
|
|
"Collectible destruction salvage was also rejected.");
|
|
Assert.That(pickupCollection.ItemKey, Is.Not.Empty,
|
|
"Destruction loot must retain its canonical typed item identity.");
|
|
Assert.That(pickupCollection.EventId, Does.StartWith("zone:"),
|
|
"Collection feedback must retain its stable daemon zone/pickup identity.");
|
|
Assert.That(pickupCollection.ScalarValue, Is.EqualTo(1));
|
|
Assert.That(pickupCollection.CargoQuantityBefore, Is.Zero);
|
|
Assert.That(pickupCollection.CargoQuantityAfter, Is.EqualTo(1));
|
|
Assert.That(finalPickupEntityCount, Is.Zero,
|
|
"Collected destruction loot remained in the provider-authored entity generation.");
|
|
}
|
|
observedCombat = EveUnityCombatPresentation.Find(runtime.ActiveProjection);
|
|
Assert.That(observedShot.TargetEntityIndex, Is.EqualTo(targetEntityIndex));
|
|
Assert.That(observedShot.LockQuality, Is.GreaterThanOrEqualTo(0.99));
|
|
Assert.That(maximumTrajectoryCount, Is.GreaterThan(0));
|
|
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))
|
|
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);
|
|
|
|
var camera = cameraObject.AddComponent<Camera>();
|
|
camera.clearFlags = CameraClearFlags.SolidColor;
|
|
camera.backgroundColor = new Color(0.035f, 0.055f, 0.08f, 1f);
|
|
camera.cullingMask = -1;
|
|
var playerMarker = FindMarker(root, playerId);
|
|
var playerRenderers = playerMarker.GetComponentsInChildren<Renderer>();
|
|
Assert.That(playerRenderers, Is.Not.Empty, "The provider-authored player prefab has no renderable visual.");
|
|
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.");
|
|
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.");
|
|
}
|
|
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.");
|
|
|
|
const int captureWidth = 1280;
|
|
const int captureHeight = 720;
|
|
target = new RenderTexture(captureWidth, captureHeight, 24, RenderTextureFormat.ARGB32);
|
|
camera.targetTexture = target;
|
|
var cameraRig = cameraObject.AddComponent<EveUnityPlayableWorldCameraRig>();
|
|
cameraRig.Host = host;
|
|
cameraRig.CameraTransform = camera.transform;
|
|
cameraRig.RenderPolicySource = provider;
|
|
Assert.That(cameraRig.ApplyRig(0f), Is.True);
|
|
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.");
|
|
var reflection = provider.ResolveAsset(new EveUnityPlayableWorldAssetBinding(
|
|
runtime.ActiveWorld.ReflectionAssetRef, "", "provider-asset-ref"), typeof(Cubemap)) as Cubemap;
|
|
Assert.That(runtime.ActiveWorld.SkyboxAssetRef, Is.Empty,
|
|
"Aetheria's raymarch owns the frame; its reflection HDRI must not become a skybox-lighting authority.");
|
|
Assert.That(runtime.ActiveWorld.ReflectionAssetRef, Is.Not.Empty);
|
|
Assert.That(reflection, Is.Not.Null);
|
|
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));
|
|
Assert.That(RenderSettings.customReflectionTexture, Is.SameAs(reflection));
|
|
var environmentPresentation = RenderSettings.ambientMode == AmbientMode.Flat &&
|
|
RenderSettings.customReflectionTexture == reflection;
|
|
aimRenderer.RefreshNow();
|
|
Assert.That(aimRenderer.ViewDotVisible, Is.True);
|
|
aimDotDistance = Vector3.Distance(playerMarker.transform.position, aimRenderer.ViewDotPosition);
|
|
Assert.That(aimDotDistance, Is.GreaterThanOrEqualTo(49.9f));
|
|
Assert.That(camera.cullingMask & (1 << mapLayer), Is.Zero);
|
|
Assert.That(runtime.ActiveWorld.CameraRig, Is.EqualTo("perspective.entity-forward-follow.v1"));
|
|
Assert.That(runtime.ActiveWorld.CameraLookAt, Is.EqualTo("aim.convergence-point.v1"));
|
|
Assert.That(runtime.ActiveWorld.CameraDistance, Is.EqualTo(30f).Within(0.001f));
|
|
Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f));
|
|
Assert.That(runtime.ActiveWorld.CameraPositionDamping, Is.EqualTo(0f).Within(0.001f));
|
|
var playerViewport = camera.WorldToViewportPoint(playerMarker.transform.position);
|
|
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));
|
|
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);
|
|
Assert.That(texturedPlayerMaterialCount, Is.GreaterThanOrEqualTo(8),
|
|
"The provider bundle must retain its advertised native material textures.");
|
|
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.");
|
|
|
|
var mapCamera = mapCameraObject.AddComponent<Camera>();
|
|
mapCamera.cullingMask = 1 << mapLayer;
|
|
Assert.That(mapCamera.cullingMask, Is.EqualTo(1 << mapLayer));
|
|
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;
|
|
var temporalSettlingElapsed = Stopwatch.StartNew();
|
|
var initialCompositeCount = captureFieldVolume.CompositeCount;
|
|
var temporalSettlingDeadline = Time.realtimeSinceStartup + 20f;
|
|
while (captureFieldVolume.CompositeCount - initialCompositeCount < temporalSettlingFrames &&
|
|
Time.realtimeSinceStartup < temporalSettlingDeadline)
|
|
{
|
|
camera.Render();
|
|
yield return null;
|
|
}
|
|
Assert.That(captureFieldVolume.CompositeCount - initialCompositeCount,
|
|
Is.GreaterThanOrEqualTo(temporalSettlingFrames),
|
|
"The pilot capture ran before the advertised temporal reconstruction had settled.");
|
|
Debug.Log($"EveUnity witness phase temporal-settling-128-frames took {temporalSettlingElapsed.Elapsed.TotalMilliseconds:0.###}ms.");
|
|
var pilotCaptureElapsed = Stopwatch.StartNew();
|
|
yield return null;
|
|
RenderTexture.active = target;
|
|
pixels = new Texture2D(captureWidth, captureHeight, TextureFormat.RGB24, false);
|
|
pixels.ReadPixels(new Rect(0, 0, captureWidth, captureHeight), 0, 0);
|
|
pixels.Apply();
|
|
var pilotChangedPixels = CountChangedPixels(pixels, camera.backgroundColor);
|
|
MeasureLuminance(pixels, out var pilotAverageLuminance, out var pilotBrightPixelCount);
|
|
Assert.That(pilotChangedPixels, Is.GreaterThan(25), "The pilot camera rendered no provider-authored world pixels.");
|
|
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(capturePath)));
|
|
File.WriteAllBytes(capturePath, pixels.EncodeToPNG());
|
|
Assert.That(new FileInfo(capturePath).Length, Is.GreaterThan(1024));
|
|
WriteFieldLayerCaptures(root, host.Runtime.ActiveWorld, capturePath);
|
|
WriteFieldCloudCapture(root, capturePath);
|
|
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.");
|
|
Debug.Log($"EveUnity witness phase pilot-capture-and-renderer-probes took {pilotCaptureElapsed.Elapsed.TotalMilliseconds:0.###}ms.");
|
|
|
|
var mapCaptureElapsed = Stopwatch.StartNew();
|
|
mapRenderers = mapRenderers.Where(renderer => renderer != null).ToList();
|
|
Assert.That(mapRenderers, Is.Not.Empty,
|
|
"All advertised map-channel renderers were destroyed before map capture.");
|
|
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;
|
|
var fieldParticlesForMap = root.GetComponent<EveUnityFieldsParticleRenderer>();
|
|
var particleDrawsBeforeMap = fieldParticlesForMap?.DrawCount ?? 0;
|
|
mapCamera.Render();
|
|
var particleMapCameraIsolated = fieldParticlesForMap != null &&
|
|
fieldParticlesForMap.DrawCount == particleDrawsBeforeMap;
|
|
Assert.That(particleMapCameraIsolated, Is.True,
|
|
"The map camera executed the pilot-only field-particle pass.");
|
|
RenderTexture.active = target;
|
|
pixels.ReadPixels(new Rect(0, 0, captureWidth, captureHeight), 0, 0);
|
|
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));
|
|
Debug.Log($"EveUnity witness phase map-capture took {mapCaptureElapsed.Elapsed.TotalMilliseconds:0.###}ms.");
|
|
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);
|
|
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.");
|
|
var fieldParticles = fieldParticlesForMap;
|
|
Assert.That(fieldParticles, Is.Not.Null,
|
|
"The generic client did not install its field-particle lowerer.");
|
|
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);
|
|
Assert.That(fieldParticles.ProgramReady, Is.True,
|
|
"The generic field-particle lowerer rejected the advertised provider ABI or assets.");
|
|
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.");
|
|
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));
|
|
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));
|
|
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);
|
|
WriteWitnessFacts(
|
|
witnessProfile,
|
|
gameplayScenario,
|
|
provider.CurrentAssetBodyTransportKind?.ToString() ?? "",
|
|
initialVersion,
|
|
runtime.ActiveVersion,
|
|
movementDistance,
|
|
playerRenderers.Length,
|
|
pilotChangedPixels,
|
|
mapRenderers.Count,
|
|
mapChangedPixels,
|
|
(camera.cullingMask & (1 << mapLayer)) == 0,
|
|
(mapCamera.cullingMask & (1 << mapLayer)) != 0,
|
|
environmentPresentation,
|
|
fieldVolume.PresentedFrameId,
|
|
fieldVolume.PresentedLayerCount,
|
|
fieldVolume.CompositeCount,
|
|
fieldParticles.PresentedFrameId,
|
|
fieldParticles.ParticleCount,
|
|
fieldParticles.DispatchCount,
|
|
fieldParticles.DrawCount,
|
|
particleMapCameraIsolated,
|
|
fieldParticles.LastGridCenter,
|
|
movementReceipt,
|
|
lookReceipt,
|
|
focusReceipt,
|
|
tractorReceipt,
|
|
tractorReleaseReceipt,
|
|
actionReceipt,
|
|
tradePurchaseReceipt,
|
|
tradeSaleReceipt,
|
|
tradeRoundTrip,
|
|
tradeItemKey,
|
|
observedShot,
|
|
shieldRatioBefore,
|
|
observedCombat?.ShieldRatio ?? shieldRatioBefore,
|
|
hullRatioBefore,
|
|
observedCombat?.HullRatio ?? hullRatioBefore,
|
|
(float)(observedShot?.LockQuality ?? 0),
|
|
aimDotDistance,
|
|
playerViewportWidth,
|
|
playerViewportHeight,
|
|
texturedPlayerMaterialCount,
|
|
directionalLightIntensity,
|
|
pilotAverageLuminance,
|
|
pilotBrightPixelCount,
|
|
cockpitProgressCount,
|
|
tractorBeamPower,
|
|
tractorReleasedPower,
|
|
tractorBeamParticleSystemCount,
|
|
initialPickupEntityCount,
|
|
finalPickupEntityCount,
|
|
pickupCollectionEventCount,
|
|
pickupRejectionEventCount,
|
|
destructionPickupObserved,
|
|
pickupCollection,
|
|
pickupRejection,
|
|
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)),
|
|
playerRendererFacts,
|
|
root.GetComponentsInChildren<EveUnityPlayableWorldEntityMarker>(includeInactive: true)
|
|
.Select(marker => PresentedEntityFact.From(camera, marker))
|
|
.ToArray());
|
|
}
|
|
finally
|
|
{
|
|
RenderTexture.active = null;
|
|
var camera = cameraObject.GetComponent<Camera>();
|
|
if (camera != null) camera.targetTexture = null;
|
|
var mapCamera = mapCameraObject.GetComponent<Camera>();
|
|
if (mapCamera != null) mapCamera.targetTexture = null;
|
|
if (target != null) UnityEngine.Object.DestroyImmediate(target);
|
|
if (pixels != null) UnityEngine.Object.DestroyImmediate(pixels);
|
|
host?.Disconnect();
|
|
UnityEngine.Object.DestroyImmediate(mapCameraObject);
|
|
UnityEngine.Object.DestroyImmediate(cameraObject);
|
|
UnityEngine.Object.DestroyImmediate(root);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
private static void WriteWitnessFacts(
|
|
string witnessProfile,
|
|
string gameplayScenario,
|
|
string assetBodyTransport,
|
|
long initialVersion,
|
|
long finalVersion,
|
|
float movementDistance,
|
|
int playerRendererCount,
|
|
int pilotChangedPixels,
|
|
int mapChannelRendererCount,
|
|
int mapChangedPixels,
|
|
bool pilotCameraExcludesMapChannel,
|
|
bool mapCameraIncludesMapChannel,
|
|
bool environmentPresentation,
|
|
long fieldVolumeFrameId,
|
|
int fieldVolumeLayerCount,
|
|
long fieldVolumeCompositeCount,
|
|
long fieldParticleFrameId,
|
|
int fieldParticleCount,
|
|
int fieldParticleDispatchCount,
|
|
int fieldParticleDrawCount,
|
|
bool fieldParticleMapCameraIsolated,
|
|
Vector2 fieldParticleGridCenter,
|
|
WitnessReceipt movement,
|
|
WitnessReceipt look,
|
|
WitnessReceipt targeting,
|
|
WitnessReceipt tractor,
|
|
WitnessReceipt tractorRelease,
|
|
WitnessReceipt action,
|
|
WitnessReceipt tradePurchase,
|
|
WitnessReceipt tradeSale,
|
|
bool tradeRoundTrip,
|
|
string tradeItemKey,
|
|
EveUnityShotReceipt shot,
|
|
float shieldRatioBefore,
|
|
float shieldRatioAfter,
|
|
float hullRatioBefore,
|
|
float hullRatioAfter,
|
|
float lockProgress,
|
|
float aimDotDistance,
|
|
float playerViewportWidth,
|
|
float playerViewportHeight,
|
|
int texturedPlayerMaterialCount,
|
|
float directionalLightIntensity,
|
|
float pilotAverageLuminance,
|
|
int pilotBrightPixelCount,
|
|
int cockpitProgressCount,
|
|
float tractorBeamPower,
|
|
float tractorReleasedPower,
|
|
int tractorBeamParticleSystemCount,
|
|
int initialPickupEntityCount,
|
|
int finalPickupEntityCount,
|
|
int pickupCollectionEventCount,
|
|
int pickupRejectionEventCount,
|
|
bool destructionPickupObserved,
|
|
EveUnityFeedbackEvent pickupCollection,
|
|
EveUnityFeedbackEvent pickupRejection,
|
|
Vector3 cameraPosition,
|
|
Vector3 cameraForward,
|
|
Vector3 playerPosition,
|
|
Vector3 aimPosition,
|
|
float fieldViewportMinX,
|
|
float fieldViewportMinY,
|
|
float fieldViewportMaxX,
|
|
float fieldViewportMaxY,
|
|
int fieldSplatCount,
|
|
RendererFact[] playerRendererFacts,
|
|
PresentedEntityFact[] presentedEntityFacts)
|
|
{
|
|
var path = Environment.GetEnvironmentVariable("EVEUNITY_WITNESS_FACTS_PATH");
|
|
if (string.IsNullOrWhiteSpace(path)) return;
|
|
var pickupEvent = pickupCollection ?? pickupRejection;
|
|
var document = new WitnessFacts
|
|
{
|
|
witnessProfile = witnessProfile,
|
|
gameplayScenario = gameplayScenario,
|
|
providerAdvertisement = true,
|
|
providerAssets = true,
|
|
assetBodyTransport = assetBodyTransport,
|
|
environmentPresentation = environmentPresentation,
|
|
ambientProbeEnergy = AmbientProbeEnergy(),
|
|
ambientMode = RenderSettings.ambientMode.ToString(),
|
|
ambientLight = RenderSettings.ambientLight,
|
|
reflectionTexture = RenderSettings.customReflectionTexture != null
|
|
? RenderSettings.customReflectionTexture.name
|
|
: "",
|
|
fieldVolumeFrameId = fieldVolumeFrameId,
|
|
fieldVolumeLayerCount = fieldVolumeLayerCount,
|
|
fieldVolumeCompositeCount = fieldVolumeCompositeCount,
|
|
fieldParticleFrameId = fieldParticleFrameId,
|
|
fieldParticleCount = fieldParticleCount,
|
|
fieldParticleDispatchCount = fieldParticleDispatchCount,
|
|
fieldParticleDrawCount = fieldParticleDrawCount,
|
|
fieldParticleMapCameraIsolated = fieldParticleMapCameraIsolated,
|
|
fieldParticleGridCenter = fieldParticleGridCenter,
|
|
movement = movement != null,
|
|
aimPresentation = look != null && aimDotDistance >= 49.9f,
|
|
targeting = targeting != null,
|
|
beamPresentation = tractor != null && tractorBeamPower > 0.01f && tractorBeamParticleSystemCount > 0,
|
|
beamRelease = tractorRelease != null,
|
|
action = action != null,
|
|
tradeRoundTrip = tradeRoundTrip && tradePurchase != null && tradeSale != null,
|
|
tradeItemKey = tradeItemKey ?? "",
|
|
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,
|
|
initialVersion = initialVersion,
|
|
finalVersion = finalVersion,
|
|
movementDistance = movementDistance,
|
|
aimDotDistance = aimDotDistance,
|
|
playerRendererCount = playerRendererCount,
|
|
playerViewportWidth = playerViewportWidth,
|
|
playerViewportHeight = playerViewportHeight,
|
|
texturedPlayerMaterialCount = texturedPlayerMaterialCount,
|
|
directionalLightIntensity = directionalLightIntensity,
|
|
pilotChangedPixels = pilotChangedPixels,
|
|
pilotAverageLuminance = pilotAverageLuminance,
|
|
pilotBrightPixelCount = pilotBrightPixelCount,
|
|
cockpitProgressCount = cockpitProgressCount,
|
|
tractorBeamPower = tractorBeamPower,
|
|
tractorReleasedPower = tractorReleasedPower,
|
|
tractorBeamParticleSystemCount = tractorBeamParticleSystemCount,
|
|
pickupCollection = pickupCollection != null && pickupCollectionEventCount == 1 &&
|
|
pickupCollection.CargoQuantityAfter - pickupCollection.CargoQuantityBefore == pickupCollection.ScalarValue,
|
|
pickupRejection = pickupRejection != null && pickupRejectionEventCount >= 1 &&
|
|
pickupCollectionEventCount == 0 &&
|
|
string.Equals(pickupRejection.Reason, "cargo-capacity", StringComparison.Ordinal) &&
|
|
pickupRejection.CargoQuantityAfter == pickupRejection.CargoQuantityBefore,
|
|
destructionLoot = pickupEvent != null && !string.IsNullOrWhiteSpace(pickupEvent.EventId) &&
|
|
!string.IsNullOrWhiteSpace(pickupEvent.ItemKey) && pickupEvent.ScalarValue > 0,
|
|
initialPickupEntityCount = initialPickupEntityCount,
|
|
finalPickupEntityCount = finalPickupEntityCount,
|
|
pickupCollectionEventCount = pickupCollectionEventCount,
|
|
pickupRejectionEventCount = pickupRejectionEventCount,
|
|
pickupRejectionEventId = pickupRejection?.EventId ?? "",
|
|
pickupRejectionReason = pickupRejection?.Reason ?? "",
|
|
cargoQuantityBeforeRejection = (float)(pickupRejection?.CargoQuantityBefore ?? 0),
|
|
cargoQuantityAfterRejection = (float)(pickupRejection?.CargoQuantityAfter ?? 0),
|
|
pickupEventId = pickupEvent?.EventId ?? "",
|
|
pickupItemKey = pickupEvent?.ItemKey ?? "",
|
|
pickupQuantity = (float)(pickupEvent?.ScalarValue ?? 0),
|
|
cargoQuantityBeforePickup = (float)(pickupEvent?.CargoQuantityBefore ?? 0),
|
|
cargoQuantityAfterPickup = (float)(pickupEvent?.CargoQuantityAfter ?? 0),
|
|
cameraPosition = cameraPosition,
|
|
cameraForward = cameraForward,
|
|
playerPosition = playerPosition,
|
|
aimPosition = aimPosition,
|
|
fieldViewportMinX = fieldViewportMinX,
|
|
fieldViewportMinY = fieldViewportMinY,
|
|
fieldViewportMaxX = fieldViewportMaxX,
|
|
fieldViewportMaxY = fieldViewportMaxY,
|
|
fieldSplatCount = fieldSplatCount,
|
|
playerRenderers = playerRendererFacts,
|
|
presentedEntities = presentedEntityFacts,
|
|
mapChannelRendererCount = mapChannelRendererCount,
|
|
mapChangedPixels = mapChangedPixels,
|
|
pilotCameraExcludesMapChannel = pilotCameraExcludesMapChannel,
|
|
mapCameraIncludesMapChannel = mapCameraIncludesMapChannel,
|
|
receipts = new[] { movement, look, targeting, tractor, tractorRelease, action, tradePurchase, tradeSale }
|
|
};
|
|
Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path)));
|
|
File.WriteAllText(path, JsonUtility.ToJson(document, true));
|
|
}
|
|
|
|
[Serializable]
|
|
private sealed class WitnessFacts
|
|
{
|
|
public string witnessProfile;
|
|
public string gameplayScenario;
|
|
public bool providerAdvertisement;
|
|
public bool providerAssets;
|
|
public string assetBodyTransport;
|
|
public bool environmentPresentation;
|
|
public float ambientProbeEnergy;
|
|
public string ambientMode;
|
|
public Color ambientLight;
|
|
public string reflectionTexture;
|
|
public long fieldVolumeFrameId;
|
|
public int fieldVolumeLayerCount;
|
|
public long fieldVolumeCompositeCount;
|
|
public long fieldParticleFrameId;
|
|
public int fieldParticleCount;
|
|
public int fieldParticleDispatchCount;
|
|
public int fieldParticleDrawCount;
|
|
public bool fieldParticleMapCameraIsolated;
|
|
public Vector2 fieldParticleGridCenter;
|
|
public bool movement;
|
|
public bool aimPresentation;
|
|
public bool targeting;
|
|
public bool beamPresentation;
|
|
public bool beamRelease;
|
|
public bool action;
|
|
public bool tradeRoundTrip;
|
|
public string tradeItemKey;
|
|
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;
|
|
public long initialVersion;
|
|
public long finalVersion;
|
|
public float movementDistance;
|
|
public float aimDotDistance;
|
|
public int playerRendererCount;
|
|
public float playerViewportWidth;
|
|
public float playerViewportHeight;
|
|
public int texturedPlayerMaterialCount;
|
|
public float directionalLightIntensity;
|
|
public int pilotChangedPixels;
|
|
public float pilotAverageLuminance;
|
|
public int pilotBrightPixelCount;
|
|
public int cockpitProgressCount;
|
|
public float tractorBeamPower;
|
|
public float tractorReleasedPower;
|
|
public int tractorBeamParticleSystemCount;
|
|
public bool pickupCollection;
|
|
public bool pickupRejection;
|
|
public bool destructionLoot;
|
|
public int initialPickupEntityCount;
|
|
public int finalPickupEntityCount;
|
|
public int pickupCollectionEventCount;
|
|
public int pickupRejectionEventCount;
|
|
public string pickupRejectionEventId;
|
|
public string pickupRejectionReason;
|
|
public float cargoQuantityBeforeRejection;
|
|
public float cargoQuantityAfterRejection;
|
|
public string pickupEventId;
|
|
public string pickupItemKey;
|
|
public float pickupQuantity;
|
|
public float cargoQuantityBeforePickup;
|
|
public float cargoQuantityAfterPickup;
|
|
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;
|
|
public RendererFact[] playerRenderers;
|
|
public PresentedEntityFact[] presentedEntities;
|
|
public int mapChannelRendererCount;
|
|
public int mapChangedPixels;
|
|
public bool pilotCameraExcludesMapChannel;
|
|
public bool mapCameraIncludesMapChannel;
|
|
public WitnessReceipt[] receipts;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
[Serializable]
|
|
private sealed class WitnessReceipt
|
|
{
|
|
public string commandKind;
|
|
public string commandId;
|
|
public string state;
|
|
public string receiptId;
|
|
public string ownerRepo;
|
|
public string authority;
|
|
public string providerId;
|
|
public string surfaceId;
|
|
public bool isProviderOwned;
|
|
public long sourceVersion;
|
|
|
|
public static WitnessReceipt From(string commandKind, EveUnitySceneCommandReceipt receipt)
|
|
{
|
|
return new WitnessReceipt
|
|
{
|
|
commandKind = commandKind,
|
|
commandId = receipt.CommandId,
|
|
state = receipt.State,
|
|
receiptId = receipt.ReceiptId,
|
|
ownerRepo = receipt.OwnerRepo,
|
|
authority = receipt.Authority,
|
|
providerId = receipt.ProviderId,
|
|
surfaceId = receipt.SurfaceId,
|
|
isProviderOwned = receipt.IsProviderOwned,
|
|
sourceVersion = receipt.SourceVersion
|
|
};
|
|
}
|
|
}
|
|
|
|
[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()
|
|
};
|
|
}
|
|
}
|
|
|
|
[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
|
|
};
|
|
}
|
|
}
|
|
|
|
[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
|
|
};
|
|
}
|
|
|
|
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));
|
|
}
|
|
|
|
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}'.");
|
|
}
|
|
|
|
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;
|
|
camera.Render();
|
|
RenderTexture.active = target;
|
|
pixels.ReadPixels(new Rect(0, 0, target.width, target.height), 0, 0);
|
|
pixels.Apply();
|
|
var baseline = pixels.GetPixels();
|
|
for (var index = 0; index < measuredRenderers.Count; index++)
|
|
{
|
|
var fact = facts[index];
|
|
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;
|
|
}
|
|
measuredRenderers[index].enabled = true;
|
|
camera.Render();
|
|
RenderTexture.active = target;
|
|
pixels.ReadPixels(new Rect(0, 0, target.width, target.height), 0, 0);
|
|
pixels.Apply();
|
|
MeasureDifference(
|
|
pixels.GetPixels(),
|
|
baseline,
|
|
out fact.renderedPixelCount,
|
|
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;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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;
|
|
File.WriteAllBytes(
|
|
Path.Combine(directory, $"field-{layerKey}.exr"),
|
|
floatCopy.EncodeToEXR(Texture2D.EXRFlags.OutputAsFloat));
|
|
UnityEngine.Object.DestroyImmediate(floatCopy);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void WriteFieldCloudCapture(GameObject root, string capturePath)
|
|
{
|
|
var renderer = root.GetComponent<EveUnityFieldsVolumeRenderer>();
|
|
if (renderer == null || string.IsNullOrWhiteSpace(capturePath)) return;
|
|
var raymarchField = typeof(EveUnityFieldsVolumeRenderer).GetField(
|
|
"_raymarchTexture",
|
|
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
|
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);
|
|
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);
|
|
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"));
|
|
WriteVolumeMaterialFacts(
|
|
materialField?.GetValue(renderer) as Material,
|
|
metadataField?.GetValue(renderer) as IReadOnlyDictionary<string, string>,
|
|
Path.Combine(directory, "field-volume-material.txt"));
|
|
RenderTexture.active = previous;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
private static void WriteFloatTexture(RenderTexture texture, string path)
|
|
{
|
|
if (texture == null || string.IsNullOrWhiteSpace(path)) return;
|
|
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;
|
|
File.WriteAllBytes(path, copy.EncodeToEXR(Texture2D.EXRFlags.OutputAsFloat));
|
|
UnityEngine.Object.DestroyImmediate(copy);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|