diff --git a/.gitignore b/.gitignore index 975deb4..2c9c34e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,3 @@ TestProject/UserSettings/ obj/ *.csproj *.sln -packages/org.gamecult.eve.surface/bin~/ -packages/org.gamecult.eve.surface/obj~/ -packages/org.gamecult.eve.surface/Directory.Build.props.meta diff --git a/README.md b/README.md index 2f76872..3e36f56 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,7 @@ assembly is part of the client. `EveUnityCultMeshPlayableWorldProvider` is the shipped composition root. Its provider, surface, and Verse filters default to empty, so a client can select by -the generic `interactive-world` contract alone. After selection, Unity retains -the rendezvous address, Verse ID, and stable provider identity only. `CultMeshClient` owns -physical route discovery, reconnection, typed live-resource leases, command -submission, and content/body sessions. +the generic `interactive-world` contract alone. ```powershell powershell -ExecutionPolicy Bypass -File .\scripts\run-package-tests.ps1 diff --git a/ReleaseConsumerProject/Assets/DefaultVolumeProfile.asset b/ReleaseConsumerProject/Assets/DefaultVolumeProfile.asset deleted file mode 100644 index 5393bfe..0000000 --- a/ReleaseConsumerProject/Assets/DefaultVolumeProfile.asset +++ /dev/null @@ -1,15 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!114 &11400000 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: d7fd9488000d3734a9e00ee676215985, type: 3} - m_Name: DefaultVolumeProfile - m_EditorClassIdentifier: Unity.RenderPipelines.Core.Runtime::UnityEngine.Rendering.VolumeProfile - components: [] diff --git a/ReleaseConsumerProject/Assets/DefaultVolumeProfile.asset.meta b/ReleaseConsumerProject/Assets/DefaultVolumeProfile.asset.meta deleted file mode 100644 index 408ba9a..0000000 --- a/ReleaseConsumerProject/Assets/DefaultVolumeProfile.asset.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 5b8b49b0bf6036a4a823021e69f80309 -NativeFormatImporter: - externalObjects: {} - mainObjectFileID: 11400000 - userData: - assetBundleName: - assetBundleVariant: diff --git a/ReleaseConsumerProject/Assets/Editor.meta b/ReleaseConsumerProject/Assets/Editor.meta deleted file mode 100644 index 1d8dc57..0000000 --- a/ReleaseConsumerProject/Assets/Editor.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 42bb4a36b1c94282ba7a4cad2b073bbc -folderAsset: yes diff --git a/ReleaseConsumerProject/Assets/Editor/GenericEveUnityBuild.cs b/ReleaseConsumerProject/Assets/Editor/GenericEveUnityBuild.cs deleted file mode 100644 index 3c6f3db..0000000 --- a/ReleaseConsumerProject/Assets/Editor/GenericEveUnityBuild.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.IO; -using UnityEditor; -using UnityEditor.SceneManagement; -using UnityEngine; -using UnityEngine.SceneManagement; - -public static class GenericEveUnityBuild -{ - public static void BuildWindows() - { - const string scenePath = "Assets/GenericEveUnity.unity"; - var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); - new GameObject("Generic EveUnity Client").AddComponent(); - EditorSceneManager.SaveScene(scene, scenePath); - Directory.CreateDirectory("Build/Windows"); - var report = BuildPipeline.BuildPlayer(new[] { scenePath }, "Build/Windows/EveUnity.exe", BuildTarget.StandaloneWindows64, BuildOptions.Development); - if (report.summary.result != UnityEditor.Build.Reporting.BuildResult.Succeeded) - throw new System.InvalidOperationException("Generic EveUnity Windows build failed: " + report.summary.result); - } -} diff --git a/ReleaseConsumerProject/Assets/Editor/GenericEveUnityBuild.cs.meta b/ReleaseConsumerProject/Assets/Editor/GenericEveUnityBuild.cs.meta deleted file mode 100644 index 9683f45..0000000 --- a/ReleaseConsumerProject/Assets/Editor/GenericEveUnityBuild.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 8fcd5fd63f21489eb5a70306d22ed4a4 diff --git a/ReleaseConsumerProject/Assets/Editor/GenericEveUnityRenderPipeline.cs b/ReleaseConsumerProject/Assets/Editor/GenericEveUnityRenderPipeline.cs deleted file mode 100644 index 8431736..0000000 --- a/ReleaseConsumerProject/Assets/Editor/GenericEveUnityRenderPipeline.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Linq; -using GameCult.Eve.UnityScene; -using UnityEditor; -using UnityEngine; -using UnityEngine.Rendering; -using UnityEngine.Rendering.Universal; - -public static class GenericEveUnityRenderPipeline -{ - private const string PostProcessDataGuid = "41439944d30ece34e96484bdb6645b55"; - private const string RendererPath = "Assets/GenericEveUnityRenderer.asset"; - private const string PipelinePath = "Assets/GenericEveUnityRenderPipeline.asset"; - - public static void Configure() - { - var renderer = AssetDatabase.LoadAssetAtPath(RendererPath); - if (renderer == null) - { - renderer = ScriptableObject.CreateInstance(); - AssetDatabase.CreateAsset(renderer, RendererPath); - } - - renderer.postProcessData = AssetDatabase.LoadAssetAtPath( - AssetDatabase.GUIDToAssetPath(PostProcessDataGuid)); - if (!renderer.rendererFeatures.OfType().Any()) - { - var feature = ScriptableObject.CreateInstance(); - feature.name = "Eve Unity World Effects"; - AssetDatabase.AddObjectToAsset(feature, renderer); - renderer.rendererFeatures.Add(feature); - EditorUtility.SetDirty(feature); - } - renderer.SetDirty(); - EditorUtility.SetDirty(renderer); - - var pipeline = AssetDatabase.LoadAssetAtPath(PipelinePath); - if (pipeline == null) - { - pipeline = UniversalRenderPipelineAsset.Create(renderer); - AssetDatabase.CreateAsset(pipeline, PipelinePath); - } - - GraphicsSettings.defaultRenderPipeline = pipeline; - QualitySettings.renderPipeline = pipeline; - EditorUtility.SetDirty(pipeline); - AssetDatabase.SaveAssetIfDirty(renderer); - AssetDatabase.SaveAssetIfDirty(pipeline); - AssetDatabase.SaveAssets(); - } -} diff --git a/ReleaseConsumerProject/Assets/Editor/GenericEveUnityRenderPipeline.cs.meta b/ReleaseConsumerProject/Assets/Editor/GenericEveUnityRenderPipeline.cs.meta deleted file mode 100644 index 4b499ca..0000000 --- a/ReleaseConsumerProject/Assets/Editor/GenericEveUnityRenderPipeline.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: c07b26ad876001846ba5ea9d3743352e \ No newline at end of file diff --git a/ReleaseConsumerProject/Assets/GenericEveUnityLauncher.cs b/ReleaseConsumerProject/Assets/GenericEveUnityLauncher.cs deleted file mode 100644 index 0ae0bb7..0000000 --- a/ReleaseConsumerProject/Assets/GenericEveUnityLauncher.cs +++ /dev/null @@ -1,100 +0,0 @@ -using System; -using System.Collections; -using System.IO; -using GameCult.Eve.UnityUIToolkit; -using GameCult.Eve.UnityScene; -using UnityEngine; -using UnityEngine.UIElements; - -public sealed class GenericEveUnityLauncher : MonoBehaviour -{ - private EveUnityCultMeshPlayableWorldProvider _provider; - private EveUnityPlayableWorldClientBootstrap _bootstrap; - private EveUiToolkitSurfaceLowerer _surfaceLowerer; - private UIDocument _surfaceDocument; - private PanelSettings _panelSettings; - - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] - private static void StartClient() - { - if (string.Equals(Environment.GetEnvironmentVariable("EVEUNITY_DISABLE_AUTO_LAUNCHER"), "1", StringComparison.Ordinal)) - return; - if (FindObjectOfType() != null) return; - new GameObject("Generic EveUnity Client").AddComponent(); - } - - private void Awake() - { - Application.runInBackground = true; - var endpoint = Environment.GetEnvironmentVariable("EVEUNITY_RENDEZVOUS_ENDPOINT") ?? "rudp://127.0.0.1:3076"; - var surface = Environment.GetEnvironmentVariable("EVEUNITY_SURFACE_ID") ?? "aetheria.pilot"; - var replica = Path.Combine(Application.persistentDataPath, "eve-unity-client.cc"); - - var cameraObject = new GameObject("Main Camera") { tag = "MainCamera" }; - var camera = cameraObject.AddComponent(); - camera.clearFlags = CameraClearFlags.SolidColor; - camera.backgroundColor = new Color(0.025f, 0.035f, 0.055f); - camera.nearClipPlane = 0.05f; - camera.farClipPlane = 10000f; - - _provider = gameObject.AddComponent(); - _provider.Configure(endpoint, replica, surfaceId: surface, requiredSurfaceKind: "interactive-world", clientRuntimeId: "eve-unity-interactive"); - _provider.DocumentAvailable += OnSurfaceAvailable; - _surfaceLowerer = new EveUiToolkitSurfaceLowerer(); - _panelSettings = ScriptableObject.CreateInstance(); - _panelSettings.scaleMode = PanelScaleMode.ScaleWithScreenSize; - _panelSettings.referenceResolution = new Vector2Int(1920, 1080); - _panelSettings.sortingOrder = 20; - _surfaceDocument = gameObject.AddComponent(); - _surfaceDocument.panelSettings = _panelSettings; - _bootstrap = gameObject.AddComponent(); - _bootstrap.ConfigureProvider(_provider); - StartCoroutine(Connect()); - } - - private IEnumerator Connect() - { - while (true) - { - var connected = false; - var preparation = _provider.PrepareAsync(); - while (!preparation.IsCompleted) - yield return null; - try - { - preparation.GetAwaiter().GetResult(); - var presentation = _bootstrap.Mount(); - LowerSurface(_provider.CurrentDocument); - Debug.Log($"Connected generic Eve client: {_provider.Selection?.ProviderId} / {_provider.Selection?.SurfaceId} / {presentation.ActiveEntities} entities"); - connected = true; - } - catch (Exception ex) - { - Debug.Log("Waiting for Eve provider: " + ex.Message); - } - if (connected) yield break; - yield return new WaitForSecondsRealtime(0.5f); - } - } - - private void OnSurfaceAvailable(EveUnitySceneProviderSurfaceDocument document) - { - LowerSurface(document); - } - - private void LowerSurface(EveUnitySceneProviderSurfaceDocument document) - { - if (_surfaceDocument == null || _surfaceLowerer == null || document == null) return; - var root = _surfaceDocument.rootVisualElement; - root.Clear(); - var lowered = _surfaceLowerer.Lower(document.SurfaceDocument, _provider.Submit); - lowered.style.flexGrow = 1; - root.Add(lowered); - } - - private void OnDestroy() - { - if (_provider != null) _provider.DocumentAvailable -= OnSurfaceAvailable; - if (_panelSettings != null) Destroy(_panelSettings); - } -} diff --git a/ReleaseConsumerProject/Assets/GenericEveUnityLauncher.cs.meta b/ReleaseConsumerProject/Assets/GenericEveUnityLauncher.cs.meta deleted file mode 100644 index dde265b..0000000 --- a/ReleaseConsumerProject/Assets/GenericEveUnityLauncher.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 5c2c8060cff846cda741810156b0558a diff --git a/ReleaseConsumerProject/Assets/GenericEveUnityRenderPipeline.asset b/ReleaseConsumerProject/Assets/GenericEveUnityRenderPipeline.asset deleted file mode 100644 index e93f394..0000000 --- a/ReleaseConsumerProject/Assets/GenericEveUnityRenderPipeline.asset +++ /dev/null @@ -1,143 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!114 &11400000 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: bf2edee5c58d82540a51f03df9d42094, type: 3} - m_Name: GenericEveUnityRenderPipeline - m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalRenderPipelineAsset - k_AssetVersion: 13 - k_AssetPreviousVersion: 13 - m_RendererType: 1 - m_RendererData: {fileID: 0} - m_RendererDataList: - - {fileID: 11400000, guid: cbe5d2831ab649e42b9fb94644e029a9, type: 2} - m_DefaultRendererIndex: 0 - m_RequireDepthTexture: 0 - m_RequireOpaqueTexture: 0 - m_OpaqueDownsampling: 1 - m_SupportsTerrainHoles: 1 - m_SupportsHDR: 1 - m_HDRColorBufferPrecision: 0 - m_MSAA: 1 - m_RenderScale: 1 - m_UpscalingFilter: 0 - m_FsrOverrideSharpness: 0 - m_FsrSharpness: 0.92 - m_EnableLODCrossFade: 1 - m_LODCrossFadeDitheringType: 1 - m_ShEvalMode: 0 - m_LightProbeSystem: 0 - m_ProbeVolumeMemoryBudget: 1024 - m_ProbeVolumeBlendingMemoryBudget: 256 - m_SupportProbeVolumeGPUStreaming: 0 - m_SupportProbeVolumeDiskStreaming: 0 - m_SupportProbeVolumeScenarios: 0 - m_SupportProbeVolumeScenarioBlending: 0 - m_ProbeVolumeSHBands: 1 - m_MainLightRenderingMode: 1 - m_MainLightShadowsSupported: 1 - m_MainLightShadowmapResolution: 2048 - m_AdditionalLightsRenderingMode: 1 - m_AdditionalLightsPerObjectLimit: 4 - m_AdditionalLightShadowsSupported: 0 - m_AdditionalLightsShadowmapResolution: 2048 - m_AdditionalLightsShadowResolutionTierLow: 256 - m_AdditionalLightsShadowResolutionTierMedium: 512 - m_AdditionalLightsShadowResolutionTierHigh: 1024 - m_ReflectionProbeBlending: 0 - m_ReflectionProbeBoxProjection: 0 - m_ReflectionProbeAtlas: 1 - m_ShadowDistance: 50 - m_ShadowCascadeCount: 1 - m_Cascade2Split: 0.25 - m_Cascade3Split: {x: 0.1, y: 0.3} - m_Cascade4Split: {x: 0.067, y: 0.2, z: 0.467} - m_CascadeBorder: 0.2 - m_ShadowDepthBias: 1 - m_ShadowNormalBias: 1 - m_AnyShadowsSupported: 1 - m_SoftShadowsSupported: 0 - m_ConservativeEnclosingSphere: 1 - m_NumIterationsEnclosingSphere: 64 - m_SoftShadowQuality: 2 - m_AdditionalLightsCookieResolution: 2048 - m_AdditionalLightsCookieFormat: 3 - m_UseSRPBatcher: 1 - m_SupportsDynamicBatching: 0 - m_MixedLightingSupported: 1 - m_SupportsLightCookies: 1 - m_SupportsLightLayers: 0 - m_DebugLevel: 0 - m_StoreActionsOptimization: 0 - m_UseAdaptivePerformance: 1 - m_ColorGradingMode: 0 - m_ColorGradingLutSize: 32 - m_AllowPostProcessAlphaOutput: 0 - m_UseFastSRGBLinearConversion: 0 - m_SupportDataDrivenLensFlare: 1 - m_SupportScreenSpaceLensFlare: 1 - m_GPUResidentDrawerMode: 0 - m_SmallMeshScreenPercentage: 0 - m_GPUResidentDrawerEnableOcclusionCullingInCameras: 0 - m_ShadowType: 1 - m_LocalShadowsSupported: 0 - m_LocalShadowsAtlasResolution: 256 - m_MaxPixelLights: 0 - m_ShadowAtlasResolution: 256 - m_VolumeFrameworkUpdateMode: 0 - m_VolumeProfile: {fileID: 0} - apvScenesData: - obsoleteSceneBounds: - m_Keys: [] - m_Values: [] - obsoleteHasProbeVolumes: - m_Keys: [] - m_Values: - m_PrefilteringModeMainLightShadows: 1 - m_PrefilteringModeAdditionalLight: 4 - m_PrefilteringModeAdditionalLightShadows: 1 - m_PrefilterXRKeywords: 0 - m_PrefilteringModeForwardPlus: 1 - m_PrefilteringModeDeferredRendering: 1 - m_PrefilteringModeScreenSpaceOcclusion: 1 - m_PrefilterDebugKeywords: 0 - m_PrefilterWriteRenderingLayers: 0 - m_PrefilterHDROutput: 0 - m_PrefilterAlphaOutput: 0 - m_PrefilterSSAODepthNormals: 0 - m_PrefilterSSAOSourceDepthLow: 0 - m_PrefilterSSAOSourceDepthMedium: 0 - m_PrefilterSSAOSourceDepthHigh: 0 - m_PrefilterSSAOInterleaved: 0 - m_PrefilterSSAOBlueNoise: 0 - m_PrefilterSSAOSampleCountLow: 0 - m_PrefilterSSAOSampleCountMedium: 0 - m_PrefilterSSAOSampleCountHigh: 0 - m_PrefilterDBufferMRT1: 0 - m_PrefilterDBufferMRT2: 0 - m_PrefilterDBufferMRT3: 0 - m_PrefilterSoftShadowsQualityLow: 0 - m_PrefilterSoftShadowsQualityMedium: 0 - m_PrefilterSoftShadowsQualityHigh: 0 - m_PrefilterSoftShadows: 0 - m_PrefilterScreenCoord: 0 - m_PrefilterScreenSpaceIrradiance: 0 - m_PrefilterNativeRenderPass: 0 - m_PrefilterUseLegacyLightmaps: 0 - m_PrefilterBicubicLightmapSampling: 0 - m_PrefilterReflectionProbeRotation: 0 - m_PrefilterReflectionProbeBlending: 0 - m_PrefilterReflectionProbeBoxProjection: 0 - m_PrefilterReflectionProbeAtlas: 0 - m_ShaderVariantLogLevel: 0 - m_ShadowCascades: 0 - m_Textures: - blueNoise64LTex: {fileID: 2800000, guid: e3d24661c1e055f45a7560c033dbb837, type: 3} - bayerMatrixTex: {fileID: 2800000, guid: f9ee4ed84c1d10c49aabb9b210b0fc44, type: 3} diff --git a/ReleaseConsumerProject/Assets/GenericEveUnityRenderPipeline.asset.meta b/ReleaseConsumerProject/Assets/GenericEveUnityRenderPipeline.asset.meta deleted file mode 100644 index 266335c..0000000 --- a/ReleaseConsumerProject/Assets/GenericEveUnityRenderPipeline.asset.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 43f32b840bd83ce47af920dadd59d4b3 -NativeFormatImporter: - externalObjects: {} - mainObjectFileID: 11400000 - userData: - assetBundleName: - assetBundleVariant: diff --git a/ReleaseConsumerProject/Assets/GenericEveUnityRenderer.asset b/ReleaseConsumerProject/Assets/GenericEveUnityRenderer.asset deleted file mode 100644 index d63c193..0000000 --- a/ReleaseConsumerProject/Assets/GenericEveUnityRenderer.asset +++ /dev/null @@ -1,69 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!114 &11400000 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: de640fe3d0db1804a85f9fc8f5cadab6, type: 3} - m_Name: GenericEveUnityRenderer - m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalRendererData - debugShaders: - debugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7, type: 3} - hdrDebugViewPS: {fileID: 4800000, guid: 573620ae32aec764abd4d728906d2587, type: 3} - probeVolumeSamplingDebugComputeShader: {fileID: 7200000, guid: 53626a513ea68ce47b59dc1299fe3959, type: 3} - probeVolumeResources: - probeVolumeDebugShader: {fileID: 0} - probeVolumeFragmentationDebugShader: {fileID: 0} - probeVolumeOffsetDebugShader: {fileID: 0} - probeVolumeSamplingDebugShader: {fileID: 0} - probeSamplingDebugMesh: {fileID: 0} - probeSamplingDebugTexture: {fileID: 0} - probeVolumeBlendStatesCS: {fileID: 0} - m_RendererFeatures: - - {fileID: 7891961340342901043} - m_RendererFeatureMap: - m_UseNativeRenderPass: 0 - postProcessData: {fileID: 11400000, guid: 41439944d30ece34e96484bdb6645b55, type: 2} - m_AssetVersion: 3 - m_PrepassLayerMask: - serializedVersion: 2 - m_Bits: 4294967295 - m_OpaqueLayerMask: - serializedVersion: 2 - m_Bits: 4294967295 - m_TransparentLayerMask: - serializedVersion: 2 - m_Bits: 4294967295 - m_DefaultStencilState: - overrideStencilState: 0 - stencilReference: 0 - stencilCompareFunction: 8 - passOperation: 2 - failOperation: 0 - zFailOperation: 0 - m_ShadowTransparentReceive: 1 - m_RenderingMode: 0 - m_DepthPrimingMode: 0 - m_CopyDepthMode: 1 - m_DepthAttachmentFormat: 0 - m_DepthTextureFormat: 0 - m_AccurateGbufferNormals: 0 - m_IntermediateTextureMode: 1 ---- !u!114 &7891961340342901043 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 340385338d304b5aaf069562d4e7b457, type: 3} - m_Name: Eve Unity World Effects - m_EditorClassIdentifier: GameCult.Eve.UnityScene::GameCult.Eve.UnityScene.EveUnityRendererFeature - m_Active: 1 diff --git a/ReleaseConsumerProject/Assets/GenericEveUnityRenderer.asset.meta b/ReleaseConsumerProject/Assets/GenericEveUnityRenderer.asset.meta deleted file mode 100644 index 5837417..0000000 --- a/ReleaseConsumerProject/Assets/GenericEveUnityRenderer.asset.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: cbe5d2831ab649e42b9fb94644e029a9 -NativeFormatImporter: - externalObjects: {} - mainObjectFileID: 11400000 - userData: - assetBundleName: - assetBundleVariant: diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef b/ReleaseConsumerProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef index e7db487..4d21cb3 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef @@ -4,10 +4,8 @@ "references": [ "GameCult.CultLib", "GameCult.Eve.Surface", - "GameCult.Eve.UnityUIToolkit", "GameCult.Eve.UnityScene", - "GameCult.Eve.UnityScene.CultMesh", - "Unity.RenderPipelines.Universal.Runtime" + "GameCult.Eve.UnityScene.CultMesh" ], "includePlatforms": [], "excludePlatforms": [], @@ -16,7 +14,6 @@ "GameCult.Caching.dll", "GameCult.Caching.MessagePack.dll", "GameCult.Mesh.dll", - "GameCult.Mesh.Quic.Native.dll", "GameCult.Networking.dll" ], "autoReferenced": false, diff --git a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 8d8cef4..2a00a34 100644 --- a/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/ReleaseConsumerProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -3,23 +3,13 @@ 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 { @@ -182,110 +172,44 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var providerId = Environment.GetEnvironmentVariable("EVEUNITY_PROVIDER_ID") ?? ""; var surfaceId = Environment.GetEnvironmentVariable("EVEUNITY_SURFACE_ID") ?? ""; - var cacheDirectory = Environment.GetEnvironmentVariable("EVEUNITY_CACHE_DIRECTORY") ?? - Path.Combine(Application.temporaryCachePath, $"eve-unity-{Guid.NewGuid():N}"); + 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"); + var lightObject = new GameObject("Generic Eve World Light"); 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; + WitnessReceipt tradeReceipt = null; + EveUnityShotReceipt combatShot = null; + string combatActionId = ""; + string collectedPickupId = ""; + bool pickupCollected = false; + bool docked = false; + bool undocked = false; + string stationObservedHostileId = ""; 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(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; + int cargoQuantityBeforePickup = 0; + int cargoQuantityAfterPickup = 0; + var commandReceipts = new Dictionary(StringComparer.Ordinal); try { - var providerReadyPath = Environment.GetEnvironmentVariable("EVEUNITY_PROVIDER_READY_PATH"); - if (!string.IsNullOrWhiteSpace(providerReadyPath)) - File.WriteAllText(providerReadyPath, DateTimeOffset.UtcNow.ToString("O")); provider = root.AddComponent(); provider.Configure( rendezvousEndpoint, - cacheDirectory, + 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) { @@ -298,9 +222,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests catch (InvalidOperationException) when (Time.realtimeSinceStartup < publicationDeadline) { } - catch (TimeoutException) when (Time.realtimeSinceStartup < publicationDeadline) - { - } if (published) break; yield return new WaitForSecondsRealtime(0.1f); } @@ -308,1084 +229,528 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests 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, - provider.Selection.AuthorityRuntimeId); - 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(); - host.Configure( + runtime = EveUnityPlayableWorldRuntime.CreateForGameObjectScene( root.transform, provider, provider, - provider, + null, provider, provider); - host.Connect(); - var loweredSurface = new EveUiToolkitSurfaceLowerer().Lower( - provider.CurrentDocument.SurfaceDocument, - provider.Submit); - var cockpit = loweredSurface.Q("aetheria.daemon.game.cockpit"); - Assert.That(cockpit, Is.Not.Null, "The provider did not publish a generic cockpit overlay."); - cockpitProgressCount = cockpit.Query().ToList().Count; - Assert.That(cockpitProgressCount, Is.GreaterThanOrEqualTo(7)); - Assert.That(loweredSurface.Q("aetheria.daemon.game.frame").style.display.value, - Is.EqualTo(DisplayStyle.None), "Daemon diagnostics leaked into the pilot UI."); - trajectories = root.GetComponent(); - 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); + runtime.ReceiptAvailable += receipt => commandReceipts[receipt.CommandId] = receipt; + var initial = runtime.Connect(); var entityDeadline = Time.realtimeSinceStartup + 12f; - while (Time.realtimeSinceStartup < entityDeadline && - (host.PresentedEntities?.CurrentGeneration == null || - host.PresentedEntities.CurrentGeneration.Entities.Count == 0)) + while (initial.ActiveEntities == 0 && Time.realtimeSinceStartup < entityDeadline) { yield return new WaitForSecondsRealtime(0.1f); - host.Refresh(); + initial = runtime.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."); + Assert.That(initial.ActiveEntities, Is.GreaterThan(0)); + Assert.That(runtime.ActiveWorld.PlayerEntityId, Is.Not.Empty); + 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 marker = FindMarker(root, playerId); - - if (requireSessionGameplay) - { - EveUnitySceneCommandReceipt observedDockReceipt = null; - string dockCommandId = null; - runtime.ReceiptAvailable += receipt => - { - if (receipt.CommandId == dockCommandId) - observedDockReceipt = receipt; - }; - host.SubmitAdvertisedActionIntent(playerId, "simulation.step"); - var dockVersion = runtime.ActiveVersion; - var dock = host.SubmitAdvertisedActionIntent(playerId, "pilot.dock"); - dockCommandId = dock.CommandId; - var dockDeadline = Time.realtimeSinceStartup + 12f; - while (Time.realtimeSinceStartup < dockDeadline && - (observedDockReceipt == null || - !string.Equals(observedDockReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || - runtime.ActiveVersion < observedDockReceipt.SourceVersion || - !(host.InputCapability?.Actions ?? Array.Empty()).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(observedDockReceipt, dock.CommandId, - provider.Selection.ProviderId, provider.Selection.SurfaceId, dockVersion); - var buyAction = (host.InputCapability?.Actions ?? Array.Empty()) - .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()).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()) - .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()).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()).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()).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 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 seededPickup = root.GetComponentsInChildren() + .SingleOrDefault(candidate => string.Equals(candidate.EntityKind, "pickup", StringComparison.Ordinal)); + Assert.That(seededPickup, Is.Not.Null, "The daemon-authored salvage pickup was not lowered through the Eve entity view."); + collectedPickupId = seededPickup.EntityId; + Assert.That( + seededPickup.GetComponentsInChildren(includeInactive: true).Length, + Is.GreaterThan(1), + "The generic client did not instantiate the provider-authored pickup asset."); + var dockAction = provider.CurrentInputCapability.Actions + .Single(candidate => string.Equals(candidate.ActionId, "pilot.dock", StringComparison.Ordinal)); + var undockAction = provider.CurrentInputCapability.Actions + .Single(candidate => string.Equals(candidate.ActionId, "pilot.undock", StringComparison.Ordinal)); + var realtimeAction = provider.CurrentInputCapability?.Actions + .FirstOrDefault(action => string.Equals(action.ActionId, "simulation.rate.realtime", StringComparison.Ordinal)); + Assert.That(realtimeAction, Is.Not.Null, "Terminus did not advertise its user-controlled simulation clock."); + var pauseAction = provider.CurrentInputCapability.Actions + .Single(candidate => string.Equals(candidate.ActionId, "simulation.pause", StringComparison.Ordinal)); + var scoopAction = provider.CurrentInputCapability.Actions + .Single(candidate => string.Equals(candidate.ActionId, "pilot.scoop", StringComparison.Ordinal)); + var stepAction = provider.CurrentInputCapability.Actions + .Single(candidate => string.Equals(candidate.ActionId, "simulation.step", StringComparison.Ordinal)); Assert.That( marker.GetComponentsInChildren(includeInactive: true).Length, Is.GreaterThan(1), "The generic client substituted its primitive fallback instead of the provider-authored AssetBundle prefab."); + runtime.SubmitMoveVectorIntent(playerId, 0f, 0f, 0f); + var dock = runtime.SubmitActionIntent(playerId, dockAction.ActionId); + var resumeDock = runtime.SubmitActionIntent(playerId, stepAction.ActionId); + var dockDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < dockDeadline && + (!commandReceipts.TryGetValue(dock.CommandId, out var observedDock) || + runtime.ActiveVersion < observedDock.SourceVersion || + !string.Equals(runtime.ActiveWorld.PresentationMode, "docked", StringComparison.Ordinal))) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + Assert.That(commandReceipts.TryGetValue(resumeDock.CommandId, out var resumeDockReceipt), Is.True); + Assert.That(resumeDockReceipt.State, Is.EqualTo("accepted").Or.EqualTo("reconciled"), resumeDockReceipt.Message); + Assert.That(commandReceipts.TryGetValue(dock.CommandId, out var dockReceipt), Is.True); + Assert.That(dockReceipt.State, Is.EqualTo("accepted").Or.EqualTo("reconciled"), dockReceipt.Message); + Assert.That(runtime.ActiveWorld.PresentationMode, Is.EqualTo("docked")); + Assert.That(runtime.ActiveWorld.CameraTargetEntityId, Is.Not.Empty.And.Not.EqualTo(playerId)); + Assert.That(FindMarker(root, runtime.ActiveWorld.CameraTargetEntityId), Is.Not.Null, + "The daemon did not include the authoritative dock parent in the docked SoA view."); + stationObservedHostileId = provider.CurrentEntityView.Identities + .FirstOrDefault(identity => string.Equals(identity.Faction, "raider", StringComparison.Ordinal))?.EntityId ?? ""; + Assert.That(stationObservedHostileId, Is.Not.Empty, + "The docked pilot did not inherit the station's sensor-derived hostile contacts."); + Assert.That(runtime.ActiveWorld.SubjectVisible, Is.False); + Assert.That(runtime.ActiveWorld.MovementEnabled, Is.False); + Assert.That(FindMarker(root, playerId, includeInactive: true).gameObject.activeSelf, Is.False); + docked = true; - initialVersion = runtime.ActiveVersion; - if (requireSessionGameplay) + var servicePause = runtime.SubmitActionIntent(playerId, pauseAction.ActionId); + var servicePauseDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < servicePauseDeadline && + (!commandReceipts.TryGetValue(servicePause.CommandId, out var observedServicePause) || + runtime.ActiveVersion < observedServicePause.SourceVersion)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + Assert.That(commandReceipts.TryGetValue(servicePause.CommandId, out var servicePauseReceipt), Is.True); + Assert.That(servicePauseReceipt.State, Is.EqualTo("accepted").Or.EqualTo("reconciled"), servicePauseReceipt.Message); + + var tradeAction = provider.CurrentInputCapability.Actions + .FirstOrDefault(candidate => + string.Equals(candidate.Category, "trade", StringComparison.Ordinal) && + string.Equals(candidate.Availability, "available", StringComparison.OrdinalIgnoreCase)); + Assert.That(tradeAction, Is.Not.Null, + "The docked pilot input capability did not advertise an available daemon-authored station stock action. Actions: " + + string.Join("; ", provider.CurrentInputCapability.Actions.Select(candidate => + $"{candidate.ActionId}[category={candidate.Category},availability={candidate.Availability},label={candidate.Label}]"))); + var trade = runtime.SubmitActionIntent(playerId, tradeAction.ActionId); + var tradeDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < tradeDeadline && + (!commandReceipts.TryGetValue(trade.CommandId, out var observedTrade) || + runtime.ActiveVersion < observedTrade.SourceVersion)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + Assert.That(commandReceipts.TryGetValue(trade.CommandId, out var tradeCommandReceipt), Is.True); + Assert.That( + tradeCommandReceipt.State, + Is.EqualTo("accepted").Or.EqualTo("reconciled"), + tradeCommandReceipt.Message); + tradeReceipt = WitnessReceipt.From("trade", tradeCommandReceipt); + + var undock = runtime.SubmitActionIntent(playerId, undockAction.ActionId); + var resumeDeparture = runtime.SubmitActionIntent(playerId, stepAction.ActionId); + var undockDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < undockDeadline && + (!commandReceipts.TryGetValue(undock.CommandId, out var observedUndock) || + runtime.ActiveVersion < observedUndock.SourceVersion || + !string.Equals(runtime.ActiveWorld.PresentationMode, "world", StringComparison.Ordinal))) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + Assert.That(commandReceipts.TryGetValue(resumeDeparture.CommandId, out var resumeDepartureReceipt), Is.True); + Assert.That(resumeDepartureReceipt.State, Is.EqualTo("accepted").Or.EqualTo("reconciled"), resumeDepartureReceipt.Message); + Assert.That(commandReceipts.TryGetValue(undock.CommandId, out var undockReceipt), Is.True); + Assert.That(undockReceipt.State, Is.EqualTo("accepted").Or.EqualTo("reconciled"), undockReceipt.Message); + Assert.That(runtime.ActiveWorld.PresentationMode, Is.EqualTo("world")); + Assert.That(runtime.ActiveWorld.CameraTargetEntityId, Is.EqualTo(playerId)); + Assert.That(runtime.ActiveWorld.SubjectVisible, Is.True); + Assert.That(runtime.ActiveWorld.MovementEnabled, Is.True); + Assert.That(FindMarker(root, playerId).gameObject.activeSelf, Is.True); + undocked = true; + var undockPosition = marker.transform.position; + + var pause = runtime.SubmitActionIntent(playerId, pauseAction.ActionId); + var pauseDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < pauseDeadline && + (!commandReceipts.TryGetValue(pause.CommandId, out var observedPause) || + runtime.ActiveVersion < observedPause.SourceVersion)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + Assert.That(commandReceipts.TryGetValue(pause.CommandId, out var pauseReceipt), Is.True); + Assert.That(pauseReceipt.State, Is.EqualTo("accepted").Or.EqualTo("reconciled"), pauseReceipt.Message); + + cargoQuantityBeforePickup = ReadInt32EntitySemantic( + provider.CurrentEntityView, + playerId, + "inventory.cargo.quantity"); + + var scoop = runtime.SubmitActionIntent(playerId, scoopAction.ActionId); + var initialScoopDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < initialScoopDeadline && + !commandReceipts.TryGetValue(scoop.CommandId, out _)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + Assert.That(commandReceipts.TryGetValue(scoop.CommandId, out var initialScoopReceipt), Is.True); + Assert.That(initialScoopReceipt.State, Is.EqualTo("accepted").Or.EqualTo("reconciled"), initialScoopReceipt.Message); + var resumeScoop = runtime.SubmitActionIntent(playerId, realtimeAction.ActionId); + var resumeScoopDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < resumeScoopDeadline && + (!commandReceipts.TryGetValue(resumeScoop.CommandId, out var observedResumeScoop) || + runtime.ActiveVersion < observedResumeScoop.SourceVersion)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + Assert.That(commandReceipts.TryGetValue(resumeScoop.CommandId, out var resumeScoopReceipt), Is.True); + Assert.That(resumeScoopReceipt.State, Is.EqualTo("accepted").Or.EqualTo("reconciled"), resumeScoopReceipt.Message); + var collectionDeadline = Time.realtimeSinceStartup + 25f; + while (Time.realtimeSinceStartup < collectionDeadline && + root.GetComponentsInChildren() + .Any(candidate => string.Equals(candidate.EntityId, collectedPickupId, StringComparison.Ordinal))) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + pickupCollected = !root.GetComponentsInChildren() + .Any(candidate => string.Equals(candidate.EntityId, collectedPickupId, StringComparison.Ordinal)); + var uncollectedPickup = root.GetComponentsInChildren() + .FirstOrDefault(candidate => string.Equals(candidate.EntityId, collectedPickupId, StringComparison.Ordinal)); + var currentCargoQuantity = ReadInt32EntitySemantic( + provider.CurrentEntityView, + playerId, + "inventory.cargo.quantity"); + Assert.That(pickupCollected, Is.True, + uncollectedPickup == null + ? "The pilot bot did not collect the daemon-authored salvage." + : $"The pilot bot did not collect the daemon-authored salvage. " + + $"player={FindMarker(root, playerId).transform.position}, " + + $"pickup={uncollectedPickup.transform.position}, " + + $"distance={Vector3.Distance(FindMarker(root, playerId).transform.position, uncollectedPickup.transform.position):0.###}, " + + $"cargo={currentCargoQuantity}."); + cargoQuantityAfterPickup = ReadInt32EntitySemantic( + provider.CurrentEntityView, + playerId, + "inventory.cargo.quantity"); + Assert.That(cargoQuantityAfterPickup, Is.EqualTo(cargoQuantityBeforePickup + 1), + "The pickup left the world without an authoritative cargo-unit delta in the portable Eve SoA view."); + + var combatStartVersion = runtime.ActiveVersion; + var visibleMarkers = root.GetComponentsInChildren(); + var hostileTargets = visibleMarkers + .Where(candidate => candidate.Selectable && !candidate.Controllable && + string.Equals(candidate.EntityKind, "ship", StringComparison.OrdinalIgnoreCase) && + !string.Equals(candidate.Faction, marker.Faction, StringComparison.Ordinal)) + .OrderBy(candidate => Vector3.Distance(candidate.transform.position, marker.transform.position)) + .ToArray(); + Assert.That(hostileTargets, Is.Not.Empty, + "No hostile contact was available from the ship's own sensors immediately after undocking. " + + $"The station-only contact was {stationObservedHostileId}. Current entities: " + + string.Join(", ", visibleMarkers.Select(candidate => + $"{candidate.EntityId}[kind={candidate.EntityKind},faction={candidate.Faction},selectable={candidate.Selectable}]"))); + var targetEntityId = hostileTargets[0].EntityId; + var focus = runtime.SubmitTargetIntent(playerId, targetEntityId); + var focusVersion = runtime.ActiveVersion; + var aimVector = hostileTargets[0].transform.position - marker.transform.position; + var aim = runtime.SubmitMoveVectorIntent( + playerId, + aimVector.x, + aimVector.z, + 0.1f); + var trajectoryRenderer = root.AddComponent(); + runtime.ShotAvailable += shot => + { + var playerIdentity = provider.CurrentEntityView?.Identities + .FirstOrDefault(identity => string.Equals(identity.EntityId, playerId, StringComparison.Ordinal)); + if (playerIdentity != null && shot.SourceEntityIndex == playerIdentity.EntityIndex) + { + combatShot = shot; + trajectoryRenderer.Present(shot); + } + }; + Assert.That(provider.CurrentInputCapability, Is.Not.Null, "The pilot world did not advertise a typed input capability document."); + Assert.That(provider.CurrentInputCapability.Actions, Is.Not.Empty, "The pilot input capability advertised no available actions."); + var advertisedAction = provider.CurrentInputCapability.Actions + .First(candidate => string.Equals(candidate.Category, "weapon-group", StringComparison.Ordinal) && + string.Equals(candidate.Availability, "available", StringComparison.OrdinalIgnoreCase)); + combatActionId = advertisedAction.ActionId; + var action = runtime.SubmitActionIntent(playerId, advertisedAction.ActionId); + var resumeCombat = runtime.SubmitActionIntent(playerId, realtimeAction.ActionId); + var resumeCombatDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < resumeCombatDeadline && + (!commandReceipts.TryGetValue(resumeCombat.CommandId, out var observedResumeCombat) || + runtime.ActiveVersion < observedResumeCombat.SourceVersion)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + Assert.That(commandReceipts.TryGetValue(resumeCombat.CommandId, out var resumeCombatReceipt), Is.True); + Assert.That(resumeCombatReceipt.State, Is.EqualTo("accepted").Or.EqualTo("reconciled"), resumeCombatReceipt.Message); + var actionDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < actionDeadline && + (!commandReceipts.TryGetValue(focus.CommandId, out var observedFocus) || + runtime.ActiveVersion < observedFocus.SourceVersion || + !commandReceipts.TryGetValue(aim.CommandId, out var observedAim) || + runtime.ActiveVersion < observedAim.SourceVersion || + !commandReceipts.TryGetValue(action.CommandId, out var observedAction) || + runtime.ActiveVersion < observedAction.SourceVersion || + Vector3.Distance(FindMarker(root, playerId).transform.position, undockPosition) < 0.01f || + combatShot == null)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + Assert.That(commandReceipts.TryGetValue(focus.CommandId, out var focusCommandReceipt), Is.True); + Assert.That( + focusCommandReceipt.State, + Is.EqualTo("accepted").Or.EqualTo("reconciled"), + focusCommandReceipt.Message); + Assert.That(focusCommandReceipt.SourceVersion, Is.GreaterThan(combatStartVersion)); + focusReceipt = WitnessReceipt.From("targeting", focusCommandReceipt); + Assert.That(commandReceipts.TryGetValue(aim.CommandId, out var aimCommandReceipt), Is.True); + Assert.That( + aimCommandReceipt.State, + Is.EqualTo("accepted").Or.EqualTo("reconciled"), + aimCommandReceipt.Message); + movementDistance = Vector3.Distance(FindMarker(root, playerId).transform.position, undockPosition); + movementReceipt = WitnessReceipt.From("movement", aimCommandReceipt); + Assert.That(movementDistance, Is.GreaterThan(0.01f)); + Assert.That(commandReceipts.TryGetValue(action.CommandId, out var actionCommandReceipt), Is.True); + Assert.That( + actionCommandReceipt.State, + Is.EqualTo("accepted").Or.EqualTo("reconciled"), + actionCommandReceipt.Message); + Assert.That(actionCommandReceipt.SourceVersion, Is.GreaterThan(focusVersion)); + Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(actionCommandReceipt.SourceVersion)); + Assert.That(combatShot, Is.Not.Null, + $"The daemon accepted fire but published no shot receipt through Eve. Weapon state: {DescribeWeaponStates(provider)}"); + Assert.That(combatShot.ShotId, Is.Not.Empty); + var playerEntityIndex = provider.CurrentEntityView.Identities + .Single(identity => string.Equals(identity.EntityId, playerId, StringComparison.Ordinal)).EntityIndex; + Assert.That(combatShot.SourceEntityIndex, Is.EqualTo(playerEntityIndex)); + Assert.That(combatShot.TargetEntityIndex, Is.GreaterThanOrEqualTo(0)); + Assert.That(combatShot.PresentationKind, Is.Not.Empty); + Assert.That(trajectoryRenderer.ActiveTrajectoryCount, Is.GreaterThan(0)); + actionReceipt = WitnessReceipt.From("action", actionCommandReceipt); + + if (!pickupCollected) + { + var retryScoopAction = provider.CurrentInputCapability.Actions + .Single(candidate => string.Equals(candidate.ActionId, "pilot.scoop", StringComparison.Ordinal)); + var retryScoop = runtime.SubmitActionIntent(playerId, retryScoopAction.ActionId); + var scoopDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < scoopDeadline && + (!commandReceipts.TryGetValue(retryScoop.CommandId, out var observedScoop) || + runtime.ActiveVersion < observedScoop.SourceVersion)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + Assert.That(commandReceipts.TryGetValue(retryScoop.CommandId, out var scoopCommandReceipt), Is.True); + Assert.That( + scoopCommandReceipt.State, + Is.EqualTo("accepted").Or.EqualTo("reconciled"), + scoopCommandReceipt.Message); + + pickupCollected = !root.GetComponentsInChildren() + .Any(candidate => string.Equals(candidate.EntityId, collectedPickupId, StringComparison.Ordinal)); + if (pickupCollected) + { + movementDistance = Vector3.Distance(marker.transform.position, undockPosition); + movementReceipt = WitnessReceipt.From("movement", aimCommandReceipt); + } + else { var initialPosition = marker.transform.position; - var request = runtime.SubmitMoveVectorIntent(playerId, 1f, 0f, 1f); + initialVersion = runtime.ActiveVersion; + var currentPickup = root.GetComponentsInChildren() + .Single(candidate => string.Equals(candidate.EntityId, collectedPickupId, StringComparison.Ordinal)); + var pickupVector = currentPickup.transform.position - marker.transform.position; + var request = runtime.SubmitMoveVectorIntent(playerId, pickupVector.x, pickupVector.z, 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 || + (!commandReceipts.TryGetValue(request.CommandId, out var observedMovement) || + runtime.ActiveVersion < observedMovement.SourceVersion || 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)); + Assert.That(commandReceipts.TryGetValue(request.CommandId, out var movementCommandReceipt), Is.True); + Assert.That( + movementCommandReceipt.State, + Is.EqualTo("accepted").Or.EqualTo("reconciled"), + movementCommandReceipt.Message); + Assert.That(movementCommandReceipt.SourceVersion, Is.GreaterThan(initialVersion)); + Assert.That(runtime.ActiveVersion, Is.GreaterThanOrEqualTo(movementCommandReceipt.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))) + var pickupDeadline = Time.realtimeSinceStartup + 20f; + var nextSteeringUpdate = 0f; + while (Time.realtimeSinceStartup < pickupDeadline && + root.GetComponentsInChildren() + .Any(candidate => string.Equals(candidate.EntityId, collectedPickupId, StringComparison.Ordinal))) { + if (Time.realtimeSinceStartup >= nextSteeringUpdate) + { + var currentPlayer = FindMarker(root, playerId); + var observedPickup = root.GetComponentsInChildren() + .FirstOrDefault(candidate => string.Equals(candidate.EntityId, collectedPickupId, StringComparison.Ordinal)); + if (observedPickup != null) + { + var correction = observedPickup.transform.position - currentPlayer.transform.position; + runtime.SubmitMoveVectorIntent(playerId, correction.x, correction.z, 1f); + } + nextSteeringUpdate = Time.realtimeSinceStartup + 0.25f; + } yield return new WaitForSecondsRealtime(0.1f); runtime.Refresh(); } - AssertReconciledProviderReceipt(runtime.LastReceipt, release.CommandId, - provider.Selection.ProviderId, provider.Selection.SurfaceId, releaseVersion); + pickupCollected = !root.GetComponentsInChildren() + .Any(candidate => string.Equals(candidate.EntityId, collectedPickupId, StringComparison.Ordinal)); + movementReceipt = WitnessReceipt.From("movement", movementCommandReceipt); + } + Assert.That(movementDistance, Is.GreaterThan(0.01f)); + var remainingPickup = root.GetComponentsInChildren() + .FirstOrDefault(candidate => string.Equals(candidate.EntityId, collectedPickupId, StringComparison.Ordinal)); + Assert.That( + pickupCollected, + Is.True, + remainingPickup == null + ? "Ymir/daemon simulation retained a collected pickup outside the active Eve entity view." + : $"Ymir/daemon simulation produced no pickup contact. " + + $"player={marker.transform.position}, pickup={remainingPickup.transform.position}, " + + $"distance={Vector3.Distance(marker.transform.position, remainingPickup.transform.position):0.###}."); } - 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))) + var stop = runtime.SubmitMoveVectorIntent(playerId, 0f, 0f, 0f); + var stopDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < stopDeadline && + (!commandReceipts.TryGetValue(stop.CommandId, out var observedStop) || + runtime.ActiveVersion < observedStop.SourceVersion)) { 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(); - 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(); - 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(); - Assert.That(combatRenderer, Is.Not.Null); - var focusVersion = runtime.ActiveVersion; - var inputDriver = root.GetComponent() ?? - root.AddComponent(); - 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(); - 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(); - finalPickupEntityCount = Math.Max(0, finalPickupEntityCount); + Assert.That(commandReceipts.TryGetValue(stop.CommandId, out var stopCommandReceipt), Is.True); + Assert.That( + stopCommandReceipt.State, + Is.EqualTo("accepted").Or.EqualTo("reconciled"), + stopCommandReceipt.Message); + var light = lightObject.AddComponent(); + light.type = LightType.Directional; + light.intensity = 1.4f; + lightObject.transform.rotation = Quaternion.Euler(45f, -35f, 0f); var camera = cameraObject.AddComponent(); camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(0.035f, 0.055f, 0.08f, 1f); - camera.cullingMask = -1; + foreach (var entityMarker in root.GetComponentsInChildren()) + { + foreach (var renderer in entityMarker.GetComponentsInChildren()) + { + renderer.material.color = entityMarker.Controllable + ? new Color(0.18f, 0.75f, 1f) + : entityMarker.EntityKind == "enemy" + ? new Color(1f, 0.25f, 0.2f) + : new Color(1f, 0.78f, 0.15f); + } + } var playerMarker = FindMarker(root, playerId); var playerRenderers = playerMarker.GetComponentsInChildren(); 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(), Is.Empty, - "The provider-authored player presentation retained gameplay colliders."); - Assert.That(playerMarker.GetComponentsInChildren(), Is.Empty, - "The provider-authored player presentation retained gameplay rigid bodies."); + var playerBounds = playerRenderers[0].bounds; 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(); - foreach (var renderer in root.GetComponentsInChildren(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); + playerBounds.Encapsulate(renderer.bounds); + var halfFovRadians = camera.fieldOfView * 0.5f * Mathf.Deg2Rad; + var cameraDistance = Mathf.Max(8f, playerBounds.extents.magnitude / Mathf.Tan(halfFovRadians) * 1.35f); + camera.nearClipPlane = Mathf.Max(0.01f, cameraDistance * 0.001f); + camera.farClipPlane = Mathf.Max(1000f, cameraDistance + playerBounds.size.magnitude * 4f); + camera.transform.position = playerBounds.center + new Vector3(1f, 0.65f, -1.2f).normalized * cameraDistance; + camera.transform.LookAt(playerBounds.center); + target = new RenderTexture(640, 360, 24, RenderTextureFormat.ARGB32); camera.targetTexture = target; - var cameraRig = cameraObject.AddComponent(); - 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); - var expectedTexturedPlayerMaterialCount = requireSessionGameplay ? 8 : 7; - Assert.That(texturedPlayerMaterialCount, Is.GreaterThanOrEqualTo(expectedTexturedPlayerMaterialCount), - "The provider bundle must retain its advertised native material textures."); - var directionalLightIntensity = UnityEngine.Object.FindObjectsOfType() - .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(); - mapCamera.cullingMask = 1 << mapLayer; - Assert.That(mapCamera.cullingMask, Is.EqualTo(1 << mapLayer)); - var captureFieldVolume = root.GetComponent(); - 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; + camera.Render(); RenderTexture.active = target; - pixels = new Texture2D(captureWidth, captureHeight, TextureFormat.RGB24, false); - pixels.ReadPixels(new Rect(0, 0, captureWidth, captureHeight), 0, 0); + pixels = new Texture2D(640, 360, TextureFormat.RGB24, false); + pixels.ReadPixels(new Rect(0, 0, 640, 360), 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(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(); - 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."); - if (requireSessionGameplay) - { - 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(); - 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(includeInactive: true) - .Select(marker => PresentedEntityFact.From(camera, marker)) - .ToArray()); + tradeReceipt, + combatActionId, + combatShot, + collectedPickupId, + pickupCollected, + cargoQuantityBeforePickup, + cargoQuantityAfterPickup, + docked, + undocked); } finally { RenderTexture.active = null; var camera = cameraObject.GetComponent(); if (camera != null) camera.targetTexture = null; - var mapCamera = mapCameraObject.GetComponent(); - 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); + runtime?.Dispose(); + UnityEngine.Object.DestroyImmediate(lightObject); UnityEngine.Object.DestroyImmediate(cameraObject); UnityEngine.Object.DestroyImmediate(root); } } - private static async Task ReceiveAdvertisedRealtimeFrameAsync( - string rendezvousEndpoint, - string verseId, - string authorityRuntimeId) - { - 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( - new CultMeshSessionTarget(verseId, authorityRuntimeId), - 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) + WitnessReceipt trade, + string combatActionId, + EveUnityShotReceipt combatShot, + string collectedPickupId, + bool pickupCollected, + int cargoQuantityBeforePickup, + int cargoQuantityAfterPickup, + bool docked, + bool undocked) { 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, + trade = trade != null, + combatShot = combatShot != null, + combatActionId = combatActionId ?? "", + shotId = combatShot?.ShotId ?? "", + shotOutcome = combatShot?.Outcome ?? "", + shotPresentationKind = combatShot?.PresentationKind ?? "", + shotHit = combatShot?.Hit ?? false, + pickupVisible = !string.IsNullOrWhiteSpace(collectedPickupId), + pickupCollected = pickupCollected, + cargoQuantityBeforePickup = cargoQuantityBeforePickup, + cargoQuantityAfterPickup = cargoQuantityAfterPickup, + docked = docked, + undocked = undocked, + pickupId = collectedPickupId ?? "", 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 } + receipts = new[] { movement, targeting, action, trade } }; Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))); File.WriteAllText(path, JsonUtility.ToJson(document, true)); @@ -1394,102 +759,52 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests [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 bool trade; + public bool combatShot; + public string combatActionId; public string shotId; + public string shotOutcome; + public string shotPresentationKind; 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 bool pickupVisible; + public bool pickupCollected; + public int cargoQuantityBeforePickup; + public int cargoQuantityAfterPickup; + public bool docked; + public bool undocked; + public string pickupId; 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() + private static int ReadInt32EntitySemantic( + EveEntitySoaViewDocument document, + string entityId, + string semantic) { - 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; + Assert.That(document, Is.Not.Null, "The provider did not publish an Eve entity SoA view."); + var entityIndex = document.Identities + .Single(identity => string.Equals(identity.EntityId, entityId, StringComparison.Ordinal)) + .EntityIndex; + using var view = EveUnityEntitySoaView.Open(document); + for (var row = 0; row < view.EntityCount; row++) + { + if (!view.TryReadInt32("entity.index", row, out var rowEntityIndex) || rowEntityIndex != entityIndex) + continue; + Assert.That(view.TryReadInt32(semantic, row, out var value), Is.True, + $"The Eve entity SoA view did not expose int32 semantic '{semantic}'."); + return value; + } + + Assert.Fail($"The Eve entity SoA view did not contain entity '{entityId}' ({entityIndex})."); + return 0; } [Serializable] @@ -1498,12 +813,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests 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) @@ -1513,140 +822,17 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests 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 + private static EveUnityPlayableWorldEntityMarker FindMarker( + GameObject root, + string entityId, + bool includeInactive = false) { - 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(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()) + foreach (var marker in root.GetComponentsInChildren(includeInactive)) { if (string.Equals(marker.EntityId, entityId, StringComparison.Ordinal)) return marker; @@ -1654,268 +840,32 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests throw new InvalidOperationException($"Projected world has no entity marker '{entityId}'."); } - private static bool HasAnyTexture(Material material) + private static string DescribeWeaponStates(EveUnityCultMeshPlayableWorldProvider provider) { - 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 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) + return string.Join("; ", Flatten(provider.CurrentDocument.SurfaceDocument.Surface.Root) + .Where(node => string.Equals(node.Kind, "weapon.state", StringComparison.Ordinal)) + .Select(node => string.Join(",", new[] { - 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); + Prop(node, "behaviorKind"), + $"pending={Prop(node, "triggerPending")}", + $"firing={Prop(node, "firing")}", + $"charging={Prop(node, "charging")}", + $"charged={Prop(node, "charged")}", + $"lock={Prop(node, "lockProgress")}", + $"target={Prop(node, "targetEntityId")}", + $"refusal={Prop(node, "lastRefusalReason")}" + }))); } - 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 string Prop(EveSurfaceComponent node, string key) => + node.Props.TryGetValue(key, out var value) ? value : ""; - private static void MeasureRendererOutputs( - Camera camera, - RenderTexture target, - Texture2D pixels, - IReadOnlyList allRenderers, - IReadOnlyList measuredRenderers, - IReadOnlyList facts) + private static IEnumerable Flatten(EveSurfaceComponent root) { - 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 actual, - IReadOnlyList 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(); - 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(); - 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, - Path.Combine(directory, "field-volume-material.txt")); - RenderTexture.active = previous; - } - - private static void WriteVolumeMaterialFacts( - Material material, - IReadOnlyDictionary metadata, - string path) - { - if (material == null || metadata == null) return; - var facts = new List(); - 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 ?? ""}:{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); + yield return root; + foreach (var child in root.Children) + foreach (var descendant in Flatten(child)) + yield return descendant; } private static EveUnitySceneProviderSurfaceDocument WorldDocument() diff --git a/ReleaseConsumerProject/Assets/UniversalRenderPipelineGlobalSettings.asset b/ReleaseConsumerProject/Assets/UniversalRenderPipelineGlobalSettings.asset deleted file mode 100644 index 0c4c2e0..0000000 --- a/ReleaseConsumerProject/Assets/UniversalRenderPipelineGlobalSettings.asset +++ /dev/null @@ -1,436 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!114 &11400000 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 2ec995e51a6e251468d2a3fd8a686257, type: 3} - m_Name: UniversalRenderPipelineGlobalSettings - m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalRenderPipelineGlobalSettings - m_ShaderStrippingSetting: - m_Version: 0 - m_ExportShaderVariants: 1 - m_ShaderVariantLogLevel: 0 - m_StripRuntimeDebugShaders: 1 - m_URPShaderStrippingSetting: - m_Version: 0 - m_StripUnusedPostProcessingVariants: 0 - m_StripUnusedVariants: 1 - m_StripScreenCoordOverrideVariants: 1 - m_ShaderVariantLogLevel: 0 - m_ExportShaderVariants: 1 - m_StripDebugVariants: 1 - m_StripUnusedPostProcessingVariants: 0 - m_StripUnusedVariants: 1 - m_StripScreenCoordOverrideVariants: 1 - supportRuntimeDebugDisplay: 0 - m_Settings: - m_SettingsList: - m_List: - - rid: 1680211313492492288 - - rid: 1680211313492492289 - - rid: 1680211313492492290 - - rid: 1680211313492492291 - - rid: 1680211313492492292 - - rid: 1680211313492492293 - - rid: 1680211313492492294 - - rid: 1680211313492492295 - - rid: 1680211313492492296 - - rid: 1680211313492492297 - - rid: 1680211313492492298 - - rid: 1680211313492492299 - - rid: 1680211313492492300 - - rid: 1680211313492492301 - - rid: 1680211313492492302 - - rid: 1680211313492492303 - - rid: 1680211313492492304 - - rid: 1680211313492492305 - - rid: 1680211313492492306 - - rid: 1680211313492492307 - - rid: 1680211313492492308 - - rid: 1680211313492492309 - - rid: 1680211313492492310 - - rid: 1680211313492492311 - - rid: 1680211313492492312 - - rid: 1680211313492492313 - - rid: 1680211313492492314 - - rid: 1680211313492492315 - - rid: 1680211313492492316 - - rid: 1680211313492492317 - - rid: 1680211313492492318 - - rid: 1680211313492492319 - - rid: 1680211313492492320 - - rid: 1680211313492492321 - - rid: 1680211313492492322 - m_RuntimeSettings: - m_List: [] - m_AssetVersion: 10 - m_ObsoleteDefaultVolumeProfile: {fileID: 0} - m_RenderingLayerNames: - - Default - m_ValidRenderingLayers: 0 - lightLayerName0: - lightLayerName1: - lightLayerName2: - lightLayerName3: - lightLayerName4: - lightLayerName5: - lightLayerName6: - lightLayerName7: - apvScenesData: - obsoleteSceneBounds: - m_Keys: [] - m_Values: [] - obsoleteHasProbeVolumes: - m_Keys: [] - m_Values: - references: - version: 2 - RefIds: - - rid: 1680211313492492288 - type: {class: RayTracingRenderPipelineResources, ns: UnityEngine.Rendering.UnifiedRayTracing, asm: Unity.UnifiedRayTracing.Runtime} - data: - m_Version: 1 - m_GeometryPoolKernels: {fileID: 7200000, guid: 98e3d58cae7210c4786f67f504c9e899, type: 3} - m_CopyBuffer: {fileID: 7200000, guid: 1b95b5dcf48d1914c9e1e7405c7660e3, type: 3} - m_CopyPositions: {fileID: 7200000, guid: 1ad53a96b58d3c3488dde4f14db1aaeb, type: 3} - m_BitHistogram: {fileID: 7200000, guid: 8670f7ce4b60cef43bed36148aa1b0a2, type: 3} - m_BlockReducePart: {fileID: 7200000, guid: 4e034cc8ea2635c4e9f063e5ddc7ea7a, type: 3} - m_BlockScan: {fileID: 7200000, guid: 4d6d5de35fa45ef4a92119397a045cc9, type: 3} - m_BuildHlbvh: {fileID: 7200000, guid: 2d70cd6be91bd7843a39a54b51c15b13, type: 3} - m_RestructureBvh: {fileID: 7200000, guid: 56641cb88dcb31a4398a4997ef7a7a8c, type: 3} - m_Scatter: {fileID: 7200000, guid: a2eaeefdac4637a44b734e85b7be9186, type: 3} - - rid: 1680211313492492289 - type: {class: UniversalRenderPipelineRuntimeShaders, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - m_Version: 0 - m_FallbackErrorShader: {fileID: 4800000, guid: e6e9a19c3678ded42a3bc431ebef7dbd, type: 3} - m_BlitHDROverlay: {fileID: 4800000, guid: a89bee29cffa951418fc1e2da94d1959, type: 3} - m_CoreBlitPS: {fileID: 4800000, guid: 93446b5c5339d4f00b85c159e1159b7c, type: 3} - m_CoreBlitColorAndDepthPS: {fileID: 4800000, guid: d104b2fc1ca6445babb8e90b0758136b, type: 3} - m_SamplingPS: {fileID: 4800000, guid: 04c410c9937594faa893a11dceb85f7e, type: 3} - m_TerrainDetailLit: {fileID: 0} - m_TerrainDetailGrassBillboard: {fileID: 0} - m_TerrainDetailGrass: {fileID: 0} - - rid: 1680211313492492290 - type: {class: URPTerrainShaderSetting, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - m_Version: 0 - m_IncludeTerrainShaders: 1 - - rid: 1680211313492492291 - type: {class: UniversalRenderPipelineRuntimeTerrainShaders, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - m_Version: 0 - m_TerrainDetailLit: {fileID: 4800000, guid: f6783ab646d374f94b199774402a5144, type: 3} - m_TerrainDetailGrassBillboard: {fileID: 4800000, guid: 29868e73b638e48ca99a19ea58c48d90, type: 3} - m_TerrainDetailGrass: {fileID: 4800000, guid: e507fdfead5ca47e8b9a768b51c291a1, type: 3} - - rid: 1680211313492492292 - type: {class: UniversalRendererResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - m_Version: 0 - m_CopyDepthPS: {fileID: 4800000, guid: d6dae50ee9e1bfa4db75f19f99355220, type: 3} - m_CameraMotionVector: {fileID: 4800000, guid: c56b7e0d4c7cb484e959caeeedae9bbf, type: 3} - m_StencilDeferredPS: {fileID: 4800000, guid: e9155b26e1bc55942a41e518703fe304, type: 3} - m_ClusterDeferred: {fileID: 4800000, guid: 222cce62363a44a380c36bf03b392608, type: 3} - m_StencilDitherMaskSeedPS: {fileID: 4800000, guid: 8c3ee818f2efa514c889881ccb2e95a2, type: 3} - m_DBufferClear: {fileID: 4800000, guid: f056d8bd2a1c7e44e9729144b4c70395, type: 3} - - rid: 1680211313492492293 - type: {class: UniversalRenderPipelineDebugShaders, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - m_DebugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7, type: 3} - m_HdrDebugViewPS: {fileID: 4800000, guid: 573620ae32aec764abd4d728906d2587, type: 3} - m_ProbeVolumeSamplingDebugComputeShader: {fileID: 7200000, guid: 53626a513ea68ce47b59dc1299fe3959, type: 3} - - rid: 1680211313492492294 - type: {class: PostProcessData/ShaderResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - stopNanPS: {fileID: 4800000, guid: 1121bb4e615ca3c48b214e79e841e823, type: 3} - subpixelMorphologicalAntialiasingPS: {fileID: 4800000, guid: 63eaba0ebfb82cc43bde059b4a8c65f6, type: 3} - gaussianDepthOfFieldPS: {fileID: 4800000, guid: 5e7134d6e63e0bc47a1dd2669cedb379, type: 3} - bokehDepthOfFieldPS: {fileID: 4800000, guid: 2aed67ad60045d54ba3a00c91e2d2631, type: 3} - cameraMotionBlurPS: {fileID: 4800000, guid: 1edcd131364091c46a17cbff0b1de97a, type: 3} - paniniProjectionPS: {fileID: 4800000, guid: a15b78cf8ca26ca4fb2090293153c62c, type: 3} - lutBuilderLdrPS: {fileID: 4800000, guid: 65df88701913c224d95fc554db28381a, type: 3} - lutBuilderHdrPS: {fileID: 4800000, guid: ec9fec698a3456d4fb18cf8bacb7a2bc, type: 3} - bloomPS: {fileID: 4800000, guid: 5f1864addb451f54bae8c86d230f736e, type: 3} - temporalAntialiasingPS: {fileID: 4800000, guid: 9c70c1a35ff15f340b38ea84842358bf, type: 3} - LensFlareDataDrivenPS: {fileID: 4800000, guid: 6cda457ac28612740adb23da5d39ea92, type: 3} - LensFlareScreenSpacePS: {fileID: 4800000, guid: 701880fecb344ea4c9cd0db3407ab287, type: 3} - scalingSetupPS: {fileID: 4800000, guid: e8ee25143a34b8c4388709ea947055d1, type: 3} - easuPS: {fileID: 4800000, guid: 562b7ae4f629f144aa97780546fce7c6, type: 3} - uberPostPS: {fileID: 4800000, guid: e7857e9d0c934dc4f83f270f8447b006, type: 3} - finalPostPassPS: {fileID: 4800000, guid: c49e63ed1bbcb334780a3bd19dfed403, type: 3} - m_ShaderResourcesVersion: 0 - - rid: 1680211313492492295 - type: {class: UniversalRenderPipelineEditorShaders, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - m_AutodeskInteractive: {fileID: 4800000, guid: 0e9d5a909a1f7e84882a534d0d11e49f, type: 3} - m_AutodeskInteractiveTransparent: {fileID: 4800000, guid: 5c81372d981403744adbdda4433c9c11, type: 3} - m_AutodeskInteractiveMasked: {fileID: 4800000, guid: 80aa867ac363ac043847b06ad71604cd, type: 3} - m_DefaultSpeedTree7Shader: {fileID: 4800000, guid: 0f4122b9a743b744abe2fb6a0a88868b, type: 3} - m_DefaultSpeedTree8Shader: {fileID: -6465566751694194690, guid: 9920c1f1781549a46ba081a2a15a16ec, type: 3} - m_DefaultSpeedTree9Shader: {fileID: -6465566751694194690, guid: cbd3e1cc4ae141c42a30e33b4d666a61, type: 3} - - rid: 1680211313492492296 - type: {class: RenderGraphSettings, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - m_Version: 0 - - rid: 1680211313492492297 - type: {class: ScreenSpaceAmbientOcclusionDynamicResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - m_BlueNoise256Textures: - - {fileID: 2800000, guid: 36f118343fc974119bee3d09e2111500, type: 3} - - {fileID: 2800000, guid: 4b7b083e6b6734e8bb2838b0b50a0bc8, type: 3} - - {fileID: 2800000, guid: c06cc21c692f94f5fb5206247191eeee, type: 3} - - {fileID: 2800000, guid: cb76dd40fa7654f9587f6a344f125c9a, type: 3} - - {fileID: 2800000, guid: e32226222ff144b24bf3a5a451de54bc, type: 3} - - {fileID: 2800000, guid: 3302065f671a8450b82c9ddf07426f3a, type: 3} - - {fileID: 2800000, guid: 56a77a3e8d64f47b6afe9e3c95cb57d5, type: 3} - m_Version: 0 - - rid: 1680211313492492298 - type: {class: OnTilePostProcessResource, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - m_Version: 0 - m_UberPostShader: {fileID: 4800000, guid: fe4f13c1004a07d4ea1e30bfd0326d9e, type: 3} - - rid: 1680211313492492299 - type: {class: UniversalRenderPipelineRuntimeXRResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - m_xrOcclusionMeshPS: {fileID: 4800000, guid: 4431b1f1f743fbf4eb310a967890cbea, type: 3} - m_xrMirrorViewPS: {fileID: 4800000, guid: d5a307c014552314b9f560906d708772, type: 3} - m_xrMotionVector: {fileID: 4800000, guid: f89aac1e4f84468418fe30e611dff395, type: 3} - - rid: 1680211313492492300 - type: {class: URPReflectionProbeSettings, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Universal.Runtime} - data: - version: 1 - useReflectionProbeRotation: 1 - - rid: 1680211313492492301 - type: {class: UniversalRenderPipelineRuntimeTextures, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - m_Version: 1 - m_BlueNoise64LTex: {fileID: 2800000, guid: e3d24661c1e055f45a7560c033dbb837, type: 3} - m_BayerMatrixTex: {fileID: 2800000, guid: f9ee4ed84c1d10c49aabb9b210b0fc44, type: 3} - m_DebugFontTex: {fileID: 2800000, guid: 26a413214480ef144b2915d6ff4d0beb, type: 3} - - rid: 1680211313492492302 - type: {class: ScreenSpaceAmbientOcclusionPersistentResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - m_Shader: {fileID: 4800000, guid: 0849e84e3d62649e8882e9d6f056a017, type: 3} - m_Version: 0 - - rid: 1680211313492492303 - type: {class: UniversalRenderPipelineEditorMaterials, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - m_DefaultMaterial: {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2} - m_DefaultParticleMaterial: {fileID: 2100000, guid: e823cd5b5d27c0f4b8256e7c12ee3e6d, type: 2} - m_DefaultLineMaterial: {fileID: 2100000, guid: e823cd5b5d27c0f4b8256e7c12ee3e6d, type: 2} - m_DefaultTerrainMaterial: {fileID: 2100000, guid: 594ea882c5a793440b60ff72d896021e, type: 2} - m_DefaultDecalMaterial: {fileID: 2100000, guid: 31d0dcc6f2dd4e4408d18036a2c93862, type: 2} - m_DefaultSpriteMaterial: {fileID: 2100000, guid: 9dfc825aed78fcd4ba02077103263b40, type: 2} - - rid: 1680211313492492304 - type: {class: PostProcessData/TextureResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - blueNoise16LTex: - - {fileID: 2800000, guid: 81200413a40918d4d8702e94db29911c, type: 3} - - {fileID: 2800000, guid: d50c5e07c9911a74982bddf7f3075e7b, type: 3} - - {fileID: 2800000, guid: 1134690bf9216164dbc75050e35b7900, type: 3} - - {fileID: 2800000, guid: 7ce2118f74614a94aa8a0cdf2e6062c3, type: 3} - - {fileID: 2800000, guid: 2ca97df9d1801e84a8a8f2c53cb744f0, type: 3} - - {fileID: 2800000, guid: e63eef8f54aa9dc4da9a5ac094b503b5, type: 3} - - {fileID: 2800000, guid: 39451254daebd6d40b52899c1f1c0c1b, type: 3} - - {fileID: 2800000, guid: c94ad916058dff743b0f1c969ddbe660, type: 3} - - {fileID: 2800000, guid: ed5ea7ce59ca8ec4f9f14bf470a30f35, type: 3} - - {fileID: 2800000, guid: 071e954febf155243a6c81e48f452644, type: 3} - - {fileID: 2800000, guid: 96aaab9cc247d0b4c98132159688c1af, type: 3} - - {fileID: 2800000, guid: fc3fa8f108657e14486697c9a84ccfc5, type: 3} - - {fileID: 2800000, guid: bfed3e498947fcb4890b7f40f54d85b9, type: 3} - - {fileID: 2800000, guid: d512512f4af60a442ab3458489412954, type: 3} - - {fileID: 2800000, guid: 47a45908f6db0cb44a0d5e961143afec, type: 3} - - {fileID: 2800000, guid: 4dcc0502f8586f941b5c4a66717205e8, type: 3} - - {fileID: 2800000, guid: 9d92991794bb5864c8085468b97aa067, type: 3} - - {fileID: 2800000, guid: 14381521ff11cb74abe3fe65401c23be, type: 3} - - {fileID: 2800000, guid: d36f0fe53425e08499a2333cf423634c, type: 3} - - {fileID: 2800000, guid: d4044ea2490d63b43aa1765f8efbf8a9, type: 3} - - {fileID: 2800000, guid: c9bd74624d8070f429e3f46d161f9204, type: 3} - - {fileID: 2800000, guid: d5c9b274310e5524ebe32a4e4da3df1f, type: 3} - - {fileID: 2800000, guid: f69770e54f2823f43badf77916acad83, type: 3} - - {fileID: 2800000, guid: 10b6c6d22e73dea46a8ab36b6eebd629, type: 3} - - {fileID: 2800000, guid: a2ec5cbf5a9b64345ad3fab0912ddf7b, type: 3} - - {fileID: 2800000, guid: 1c3c6d69a645b804fa232004b96b7ad3, type: 3} - - {fileID: 2800000, guid: d18a24d7b4ed50f4387993566d9d3ae2, type: 3} - - {fileID: 2800000, guid: c989e1ed85cf7154caa922fec53e6af6, type: 3} - - {fileID: 2800000, guid: ff47e5a0f105eb34883b973e51f4db62, type: 3} - - {fileID: 2800000, guid: fa042edbfc40fbd4bad0ab9d505b1223, type: 3} - - {fileID: 2800000, guid: 896d9004736809c4fb5973b7c12eb8b9, type: 3} - - {fileID: 2800000, guid: 179f794063d2a66478e6e726f84a65bc, type: 3} - filmGrainTex: - - {fileID: 2800000, guid: 654c582f7f8a5a14dbd7d119cbde215d, type: 3} - - {fileID: 2800000, guid: dd77ffd079630404e879388999033049, type: 3} - - {fileID: 2800000, guid: 1097e90e1306e26439701489f391a6c0, type: 3} - - {fileID: 2800000, guid: f0b67500f7fad3b4c9f2b13e8f41ba6e, type: 3} - - {fileID: 2800000, guid: 9930fb4528622b34687b00bbe6883de7, type: 3} - - {fileID: 2800000, guid: bd9e8c758250ef449a4b4bfaad7a2133, type: 3} - - {fileID: 2800000, guid: 510a2f57334933e4a8dbabe4c30204e4, type: 3} - - {fileID: 2800000, guid: b4db8180660810945bf8d55ab44352ad, type: 3} - - {fileID: 2800000, guid: fd2fd78b392986e42a12df2177d3b89c, type: 3} - - {fileID: 2800000, guid: 5cdee82a77d13994f83b8fdabed7c301, type: 3} - smaaAreaTex: {fileID: 2800000, guid: d1f1048909d55cd4fa1126ab998f617e, type: 3} - smaaSearchTex: {fileID: 2800000, guid: 51eee22c2a633ef4aada830eed57c3fd, type: 3} - m_TexturesResourcesVersion: 0 - - rid: 1680211313492492305 - type: {class: URPDefaultVolumeProfileSettings, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - m_Version: 0 - m_VolumeProfile: {fileID: 11400000, guid: 5b8b49b0bf6036a4a823021e69f80309, type: 2} - - rid: 1680211313492492306 - type: {class: URPShaderStrippingSetting, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - m_Version: 0 - m_StripUnusedPostProcessingVariants: 0 - m_StripUnusedVariants: 1 - m_StripScreenCoordOverrideVariants: 1 - - rid: 1680211313492492307 - type: {class: Renderer2DResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - m_Version: 0 - m_LightShader: {fileID: 4800000, guid: 3f6c848ca3d7bca4bbe846546ac701a1, type: 3} - m_ProjectedShadowShader: {fileID: 4800000, guid: ce09d4a80b88c5a4eb9768fab4f1ee00, type: 3} - m_SpriteShadowShader: {fileID: 4800000, guid: 44fc62292b65ab04eabcf310e799ccf6, type: 3} - m_SpriteUnshadowShader: {fileID: 4800000, guid: de02b375720b5c445afe83cd483bedf3, type: 3} - m_GeometryShadowShader: {fileID: 4800000, guid: 19349a0f9a7ed4c48a27445bcf92e5e1, type: 3} - m_GeometryUnshadowShader: {fileID: 4800000, guid: 77774d9009bb81447b048c907d4c6273, type: 3} - m_CopyDepthPS: {fileID: 4800000, guid: d6dae50ee9e1bfa4db75f19f99355220, type: 3} - m_DefaultLitMaterial: {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2} - m_DefaultUnlitMaterial: {fileID: 2100000, guid: 9dfc825aed78fcd4ba02077103263b40, type: 2} - m_DefaultMaskMaterial: {fileID: 2100000, guid: 15d0c3709176029428a0da2f8cecf0b5, type: 2} - m_DefaultMesh2DLitMaterial: {fileID: 2100000, guid: 9452ae1262a74094f8a68013fbcd1834, type: 2} - - rid: 1680211313492492308 - type: {class: UniversalRenderPipelineEditorAssets, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime} - data: - m_DefaultSettingsVolumeProfile: {fileID: 11400000, guid: eda47df5b85f4f249abf7abd73db2cb2, type: 2} - - rid: 1680211313492492309 - type: {class: GPUResidentDrawerResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.GPUDriven.Runtime} - data: - m_Version: 0 - m_InstanceDataBufferCopyKernels: {fileID: 7200000, guid: f984aeb540ded8b4fbb8a2047ab5b2e2, type: 3} - m_InstanceDataBufferUploadKernels: {fileID: 7200000, guid: 53864816eb00f2343b60e1a2c5a262ef, type: 3} - m_TransformUpdaterKernels: {fileID: 7200000, guid: 2a567b9b2733f8d47a700c3c85bed75b, type: 3} - m_WindDataUpdaterKernels: {fileID: 7200000, guid: fde76746e4fd0ed418c224f6b4084114, type: 3} - m_OccluderDepthPyramidKernels: {fileID: 7200000, guid: 08b2b5fb307b0d249860612774a987da, type: 3} - m_InstanceOcclusionCullingKernels: {fileID: 7200000, guid: f6d223acabc2f974795a5a7864b50e6c, type: 3} - m_OcclusionCullingDebugKernels: {fileID: 7200000, guid: b23e766bcf50ca4438ef186b174557df, type: 3} - m_DebugOcclusionTestPS: {fileID: 4800000, guid: d3f0849180c2d0944bc71060693df100, type: 3} - m_DebugOccluderPS: {fileID: 4800000, guid: b3c92426a88625841ab15ca6a7917248, type: 3} - - rid: 1680211313492492310 - type: {class: VrsRenderPipelineRuntimeResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} - data: - m_TextureComputeShader: {fileID: 7200000, guid: cacb30de6c40c7444bbc78cb0a81fd2a, type: 3} - m_VisualizationShader: {fileID: 4800000, guid: 620b55b8040a88d468e94abe55bed5ba, type: 3} - m_VisualizationLookupTable: - m_Data: - - {r: 0.785, g: 0.23, b: 0.2, a: 1} - - {r: 1, g: 0.8, b: 0.8, a: 1} - - {r: 0.4, g: 0.2, b: 0.2, a: 1} - - {r: 0.51, g: 0.8, b: 0.6, a: 1} - - {r: 0.6, g: 0.8, b: 1, a: 1} - - {r: 0.2, g: 0.4, b: 0.6, a: 1} - - {r: 0.8, g: 1, b: 0.8, a: 1} - - {r: 0.2, g: 0.4, b: 0.2, a: 1} - - {r: 0.125, g: 0.22, b: 0.36, a: 1} - m_ConversionLookupTable: - m_Data: - - {r: 0.785, g: 0.23, b: 0.2, a: 1} - - {r: 1, g: 0.8, b: 0.8, a: 1} - - {r: 0.4, g: 0.2, b: 0.2, a: 1} - - {r: 0.51, g: 0.8, b: 0.6, a: 1} - - {r: 0.6, g: 0.8, b: 1, a: 1} - - {r: 0.2, g: 0.4, b: 0.6, a: 1} - - {r: 0.8, g: 1, b: 0.8, a: 1} - - {r: 0.2, g: 0.4, b: 0.2, a: 1} - - {r: 0.125, g: 0.22, b: 0.36, a: 1} - - rid: 1680211313492492311 - type: {class: STP/RuntimeResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} - data: - m_setupCS: {fileID: 7200000, guid: 33be2e9a5506b2843bdb2bdff9cad5e1, type: 3} - m_preTaaCS: {fileID: 7200000, guid: a679dba8ec4d9ce45884a270b0e22dda, type: 3} - m_taaCS: {fileID: 7200000, guid: 3923900e2b41b5e47bc25bfdcbcdc9e6, type: 3} - - rid: 1680211313492492312 - type: {class: RenderingDebuggerRuntimeResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} - data: - m_version: 0 - - rid: 1680211313492492313 - type: {class: RenderGraphUtilsResources, ns: UnityEngine.Rendering.RenderGraphModule.Util, asm: Unity.RenderPipelines.Core.Runtime} - data: - m_Version: 0 - m_CoreCopyPS: {fileID: 4800000, guid: 12dc59547ea167a4ab435097dd0f9add, type: 3} - - rid: 1680211313492492314 - type: {class: ProbeVolumeBakingResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} - data: - m_Version: 1 - dilationShader: {fileID: 7200000, guid: 6bb382f7de370af41b775f54182e491d, type: 3} - subdivideSceneCS: {fileID: 7200000, guid: bb86f1f0af829fd45b2ebddda1245c22, type: 3} - voxelizeSceneShader: {fileID: 4800000, guid: c8b6a681c7b4e2e4785ffab093907f9e, type: 3} - traceVirtualOffsetCS: {fileID: -6772857160820960102, guid: ff2cbab5da58bf04d82c5f34037ed123, type: 3} - traceVirtualOffsetRT: {fileID: -5126288278712620388, guid: ff2cbab5da58bf04d82c5f34037ed123, type: 3} - skyOcclusionCS: {fileID: -6772857160820960102, guid: 5a2a534753fbdb44e96c3c78b5a6999d, type: 3} - skyOcclusionRT: {fileID: -5126288278712620388, guid: 5a2a534753fbdb44e96c3c78b5a6999d, type: 3} - renderingLayerCS: {fileID: -6772857160820960102, guid: 94a070d33e408384bafc1dea4a565df9, type: 3} - renderingLayerRT: {fileID: -5126288278712620388, guid: 94a070d33e408384bafc1dea4a565df9, type: 3} - - rid: 1680211313492492315 - type: {class: LightmapSamplingSettings, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} - data: - m_Version: 1 - m_UseBicubicLightmapSampling: 0 - - rid: 1680211313492492316 - type: {class: RenderGraphGlobalSettings, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} - data: - m_version: 0 - m_EnableCompilationCaching: 1 - m_EnableValidityChecks: 1 - - rid: 1680211313492492317 - type: {class: IncludeAdditionalRPAssets, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} - data: - m_version: 0 - m_IncludeReferencedInScenes: 0 - m_IncludeAssetsByLabel: 0 - m_LabelToInclude: - - rid: 1680211313492492318 - type: {class: ProbeVolumeDebugResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} - data: - m_Version: 1 - probeVolumeDebugShader: {fileID: 4800000, guid: 3b21275fd12d65f49babb5286f040f2d, type: 3} - probeVolumeFragmentationDebugShader: {fileID: 4800000, guid: 3a80877c579b9144ebdcc6d923bca303, type: 3} - probeVolumeSamplingDebugShader: {fileID: 4800000, guid: bf54e6528c79a224e96346799064c393, type: 3} - probeVolumeOffsetDebugShader: {fileID: 4800000, guid: db8bd7436dc2c5f4c92655307d198381, type: 3} - probeSamplingDebugMesh: {fileID: -3555484719484374845, guid: 20be25aac4e22ee49a7db76fb3df6de2, type: 3} - numbersDisplayTex: {fileID: 2800000, guid: 73fe53b428c5b3440b7e87ee830b608a, type: 3} - - rid: 1680211313492492319 - type: {class: ShaderStrippingSetting, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} - data: - m_Version: 0 - m_ExportShaderVariants: 1 - m_ShaderVariantLogLevel: 0 - m_StripRuntimeDebugShaders: 1 - - rid: 1680211313492492320 - type: {class: ProbeVolumeGlobalSettings, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} - data: - m_Version: 1 - m_ProbeVolumeDisableStreamingAssets: 0 - - rid: 1680211313492492321 - type: {class: ProbeVolumeRuntimeResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime} - data: - m_Version: 1 - probeVolumeBlendStatesCS: {fileID: 7200000, guid: a3f7b8c99de28a94684cb1daebeccf5d, type: 3} - probeVolumeUploadDataCS: {fileID: 7200000, guid: 0951de5992461754fa73650732c4954c, type: 3} - probeVolumeUploadDataL2CS: {fileID: 7200000, guid: 6196f34ed825db14b81fb3eb0ea8d931, type: 3} - - rid: 1680211313492492322 - type: {class: WorldRenderPipelineResources, ns: UnityEngine.PathTracing.Core, asm: Unity.PathTracing.Runtime} - data: - _version: 3 - _blitCubemap: {fileID: 7200000, guid: 5a992812cb320d146a66cc600200cce7, type: 3} - _blitGrayScaleCookie: {fileID: 7200000, guid: 557fa399e33bf7647bda5697c5c158df, type: 3} - _setAlphaChannelShader: {fileID: 7200000, guid: 5efaea0e81c66334aa9d062d6573e6fd, type: 3} - _environmentImportanceSamplingBuild: {fileID: 7200000, guid: 5bb2534d2411d344cbc54f880232640f, type: 3} - _skyBoxMesh: {fileID: 4300000, guid: 0529e6c5f6dea8c4a8c2835ed7de57cb, type: 2} - _sixFaceSkyBoxMesh: {fileID: 4300000, guid: a80925ceebd011741b42509226cefc74, type: 2} - _buildLightGridShader: {fileID: 7200000, guid: 16e47c1641bd0104e92b624601457bb0, type: 3} diff --git a/ReleaseConsumerProject/Assets/UniversalRenderPipelineGlobalSettings.asset.meta b/ReleaseConsumerProject/Assets/UniversalRenderPipelineGlobalSettings.asset.meta deleted file mode 100644 index b0ee24c..0000000 --- a/ReleaseConsumerProject/Assets/UniversalRenderPipelineGlobalSettings.asset.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 9374f1d73c9ee8940895d44ff5dd560e -NativeFormatImporter: - externalObjects: {} - mainObjectFileID: 11400000 - userData: - assetBundleName: - assetBundleVariant: diff --git a/ReleaseConsumerProject/Packages/manifest.json b/ReleaseConsumerProject/Packages/manifest.json index b82d19d..246c0a8 100644 --- a/ReleaseConsumerProject/Packages/manifest.json +++ b/ReleaseConsumerProject/Packages/manifest.json @@ -5,12 +5,11 @@ "com.unity.modules.imageconversion": "1.0.0", "com.unity.modules.physics": "1.0.0", "com.unity.modules.uielements": "1.0.0", - "com.unity.render-pipelines.universal": "17.4.0", - "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#334e60f1928b4212a29dd8b0d19b2c099fe6365e", - "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", - "org.gamecult.eve.surface": "https://github.com/GameCult/Eve.git?path=/packages/org.gamecult.eve.surface#96839ad34c8d464ef622d8bbdd5d277e1ca9d825", - "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#d1663b36d12b60ff5c7249135eff48edf6227ca9", - "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#d1663b36d12b60ff5c7249135eff48edf6227ca9" + "org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.7", + "org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", + "org.gamecult.eve.surface": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#b6f001b300a88aff0bc13372b5c1dbc2002954f8", + "org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#b6f001b300a88aff0bc13372b5c1dbc2002954f8", + "org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0" }, "testables": [ "org.gamecult.eve.unity-scene", diff --git a/ReleaseConsumerProject/Packages/packages-lock.json b/ReleaseConsumerProject/Packages/packages-lock.json index 8ad82a2..8d56d72 100644 --- a/ReleaseConsumerProject/Packages/packages-lock.json +++ b/ReleaseConsumerProject/Packages/packages-lock.json @@ -1,27 +1,5 @@ { "dependencies": { - "com.unity.burst": { - "version": "1.8.29", - "depth": 2, - "source": "registry", - "dependencies": { - "com.unity.mathematics": "1.2.1", - "com.unity.modules.jsonserialize": "1.0.0" - }, - "url": "https://packages.unity.com" - }, - "com.unity.collections": { - "version": "6.4.0", - "depth": 2, - "source": "builtin", - "dependencies": { - "com.unity.burst": "1.8.23", - "com.unity.mathematics": "1.3.2", - "com.unity.nuget.mono-cecil": "1.11.5", - "com.unity.test-framework": "1.4.6", - "com.unity.test-framework.performance": "3.0.3" - } - }, "com.unity.ext.nunit": { "version": "2.0.5", "depth": 1, @@ -35,60 +13,6 @@ "dependencies": {}, "url": "https://packages.unity.com" }, - "com.unity.nuget.mono-cecil": { - "version": "1.11.6", - "depth": 3, - "source": "registry", - "dependencies": {}, - "url": "https://packages.unity.com" - }, - "com.unity.render-pipelines.core": { - "version": "17.4.0", - "depth": 1, - "source": "builtin", - "dependencies": { - "com.unity.burst": "1.8.14", - "com.unity.mathematics": "1.3.2", - "com.unity.ugui": "2.0.0", - "com.unity.collections": "2.4.3", - "com.unity.modules.terrain": "1.0.0", - "com.unity.modules.jsonserialize": "1.0.0" - } - }, - "com.unity.render-pipelines.universal": { - "version": "17.4.0", - "depth": 0, - "source": "builtin", - "dependencies": { - "com.unity.render-pipelines.core": "17.4.0", - "com.unity.shadergraph": "17.4.0", - "com.unity.render-pipelines.universal-config": "17.4.0" - } - }, - "com.unity.render-pipelines.universal-config": { - "version": "17.4.0", - "depth": 1, - "source": "builtin", - "dependencies": { - "com.unity.render-pipelines.core": "17.4.0" - } - }, - "com.unity.searcher": { - "version": "4.9.4", - "depth": 2, - "source": "registry", - "dependencies": {}, - "url": "https://packages.unity.com" - }, - "com.unity.shadergraph": { - "version": "17.4.0", - "depth": 1, - "source": "builtin", - "dependencies": { - "com.unity.render-pipelines.core": "17.4.0", - "com.unity.searcher": "4.9.3" - } - }, "com.unity.test-framework": { "version": "1.6.0", "depth": 0, @@ -99,73 +23,49 @@ "com.unity.modules.jsonserialize": "1.0.0" } }, - "com.unity.test-framework.performance": { - "version": "3.4.0", - "depth": 3, - "source": "registry", - "dependencies": { - "com.unity.test-framework": "1.1.33", - "com.unity.modules.jsonserialize": "1.0.0" - }, - "url": "https://packages.unity.com" - }, - "com.unity.ugui": { - "version": "2.0.0", - "depth": 2, - "source": "builtin", - "dependencies": { - "com.unity.modules.ui": "1.0.0", - "com.unity.modules.imgui": "1.0.0" - } - }, "org.gamecult.cultlib": { - "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#334e60f1928b4212a29dd8b0d19b2c099fe6365e", + "version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.7", "depth": 0, "source": "git", "dependencies": {}, - "hash": "334e60f1928b4212a29dd8b0d19b2c099fe6365e" + "hash": "1f8f832a788e2bdf59df90fb91da583574d3a42d" }, "org.gamecult.eve.plugin-fields": { - "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3", + "version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1", "depth": 0, "source": "git", - "dependencies": { - "org.gamecult.cultlib": "1.0.13" - }, - "hash": "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0" + "dependencies": {}, + "hash": "0b099274a4e2b4c301eced4322166837792f585a" }, "org.gamecult.eve.surface": { - "version": "https://github.com/GameCult/Eve.git?path=/packages/org.gamecult.eve.surface#96839ad34c8d464ef622d8bbdd5d277e1ca9d825", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#b6f001b300a88aff0bc13372b5c1dbc2002954f8", "depth": 0, "source": "git", "dependencies": { - "org.gamecult.cultlib": "1.0.56" + "org.gamecult.cultlib": "1.0.6" }, - "hash": "96839ad34c8d464ef622d8bbdd5d277e1ca9d825" + "hash": "b6f001b300a88aff0bc13372b5c1dbc2002954f8" }, "org.gamecult.eve.unity-scene": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#d1663b36d12b60ff5c7249135eff48edf6227ca9", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#b6f001b300a88aff0bc13372b5c1dbc2002954f8", "depth": 0, "source": "git", "dependencies": { - "org.gamecult.cultlib": "1.0.56", - "org.gamecult.eve.plugin-fields": "0.2.3", - "org.gamecult.eve.surface": "0.3.6", - "org.gamecult.eve.unity-uitoolkit": "0.1.9", - "com.unity.mathematics": "1.3.2", - "com.unity.render-pipelines.universal": "17.4.0", - "com.unity.modules.particlesystem": "1.0.0" + "org.gamecult.cultlib": "1.0.6", + "org.gamecult.eve.plugin-fields": "0.1.1", + "org.gamecult.eve.surface": "0.1.0", + "com.unity.mathematics": "1.3.2" }, - "hash": "d1663b36d12b60ff5c7249135eff48edf6227ca9" + "hash": "b6f001b300a88aff0bc13372b5c1dbc2002954f8" }, "org.gamecult.eve.unity-uitoolkit": { - "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#d1663b36d12b60ff5c7249135eff48edf6227ca9", + "version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0", "depth": 0, "source": "git", "dependencies": { - "org.gamecult.eve.surface": "0.3.6" + "org.gamecult.eve.surface": "0.1.0" }, - "hash": "d1663b36d12b60ff5c7249135eff48edf6227ca9" + "hash": "e10821f1cc81f619479c6507438f6a097d1f0ce2" }, "com.unity.modules.assetbundle": { "version": "1.0.0", @@ -197,24 +97,12 @@ "source": "builtin", "dependencies": {} }, - "com.unity.modules.particlesystem": { - "version": "1.0.0", - "depth": 1, - "source": "builtin", - "dependencies": {} - }, "com.unity.modules.physics": { "version": "1.0.0", "depth": 0, "source": "builtin", "dependencies": {} }, - "com.unity.modules.terrain": { - "version": "1.0.0", - "depth": 2, - "source": "builtin", - "dependencies": {} - }, "com.unity.modules.ui": { "version": "1.0.0", "depth": 1, diff --git a/ReleaseConsumerProject/ProjectSettings/GraphicsSettings.asset b/ReleaseConsumerProject/ProjectSettings/GraphicsSettings.asset index 8b7efdf..4b152c0 100644 --- a/ReleaseConsumerProject/ProjectSettings/GraphicsSettings.asset +++ b/ReleaseConsumerProject/ProjectSettings/GraphicsSettings.asset @@ -37,7 +37,7 @@ GraphicsSettings: m_PreloadedShaders: [] m_PreloadShadersBatchTimeLimit: -1 m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0} - m_CustomRenderPipeline: {fileID: 11400000, guid: 43f32b840bd83ce47af920dadd59d4b3, type: 2} + m_CustomRenderPipeline: {fileID: 0} m_TransparencySortMode: 0 m_TransparencySortAxis: {x: 0, y: 0, z: 1} m_DefaultRenderingPath: 1 @@ -57,8 +57,7 @@ GraphicsSettings: m_FogKeepExp: 1 m_FogKeepExp2: 1 m_AlbedoSwatchInfos: [] - m_RenderPipelineGlobalSettingsMap: - UnityEngine.Rendering.Universal.UniversalRenderPipeline: {fileID: 11400000, guid: 9374f1d73c9ee8940895d44ff5dd560e, type: 2} + m_RenderPipelineGlobalSettingsMap: {} m_ShaderBuildSettings: keywordDeclarationOverrides: [] m_LightsUseLinearIntensity: 0 diff --git a/ReleaseConsumerProject/ProjectSettings/QualitySettings.asset b/ReleaseConsumerProject/ProjectSettings/QualitySettings.asset index 55e71a7..64f8aba 100644 --- a/ReleaseConsumerProject/ProjectSettings/QualitySettings.asset +++ b/ReleaseConsumerProject/ProjectSettings/QualitySettings.asset @@ -313,7 +313,7 @@ QualitySettings: asyncUploadBufferSize: 16 asyncUploadPersistentBuffer: 1 resolutionScalingFixedDPIFactor: 1 - customRenderPipeline: {fileID: 11400000, guid: 43f32b840bd83ce47af920dadd59d4b3, type: 2} + customRenderPipeline: {fileID: 0} terrainQualityOverrides: 0 terrainPixelError: 1 terrainDetailDensityScale: 1 diff --git a/TestProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef b/TestProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef index e7db487..4d21cb3 100644 --- a/TestProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef +++ b/TestProject/Assets/Tests/PlayMode/GameCult.EveUnity.GenericClient.PlayModeTests.asmdef @@ -4,10 +4,8 @@ "references": [ "GameCult.CultLib", "GameCult.Eve.Surface", - "GameCult.Eve.UnityUIToolkit", "GameCult.Eve.UnityScene", - "GameCult.Eve.UnityScene.CultMesh", - "Unity.RenderPipelines.Universal.Runtime" + "GameCult.Eve.UnityScene.CultMesh" ], "includePlatforms": [], "excludePlatforms": [], @@ -16,7 +14,6 @@ "GameCult.Caching.dll", "GameCult.Caching.MessagePack.dll", "GameCult.Mesh.dll", - "GameCult.Mesh.Quic.Native.dll", "GameCult.Networking.dll" ], "autoReferenced": false, diff --git a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs index 8d8cef4..d5dc99b 100644 --- a/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs +++ b/TestProject/Assets/Tests/PlayMode/GenericWorldCaptureTests.cs @@ -2,24 +2,13 @@ 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 { @@ -182,111 +171,34 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests var providerId = Environment.GetEnvironmentVariable("EVEUNITY_PROVIDER_ID") ?? ""; var surfaceId = Environment.GetEnvironmentVariable("EVEUNITY_SURFACE_ID") ?? ""; - var cacheDirectory = Environment.GetEnvironmentVariable("EVEUNITY_CACHE_DIRECTORY") ?? - Path.Combine(Application.temporaryCachePath, $"eve-unity-{Guid.NewGuid():N}"); + 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"); + var lightObject = new GameObject("Generic Eve World Light"); 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(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(); provider.Configure( rendezvousEndpoint, - cacheDirectory, + 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; + var publicationDeadline = Time.realtimeSinceStartup + 10f; while (true) { var published = false; @@ -298,9 +210,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests catch (InvalidOperationException) when (Time.realtimeSinceStartup < publicationDeadline) { } - catch (TimeoutException) when (Time.realtimeSinceStartup < publicationDeadline) - { - } if (published) break; yield return new WaitForSecondsRealtime(0.1f); } @@ -308,1084 +217,168 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests 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, - provider.Selection.AuthorityRuntimeId); - 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(); - host.Configure( + runtime = EveUnityPlayableWorldRuntime.CreateForGameObjectScene( root.transform, provider, provider, - provider, + null, provider, provider); - host.Connect(); - var loweredSurface = new EveUiToolkitSurfaceLowerer().Lower( - provider.CurrentDocument.SurfaceDocument, - provider.Submit); - var cockpit = loweredSurface.Q("aetheria.daemon.game.cockpit"); - Assert.That(cockpit, Is.Not.Null, "The provider did not publish a generic cockpit overlay."); - cockpitProgressCount = cockpit.Query().ToList().Count; - Assert.That(cockpitProgressCount, Is.GreaterThanOrEqualTo(7)); - Assert.That(loweredSurface.Q("aetheria.daemon.game.frame").style.display.value, - Is.EqualTo(DisplayStyle.None), "Daemon diagnostics leaked into the pilot UI."); - trajectories = root.GetComponent(); - 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); + var initial = runtime.Connect(); + Assert.That(initial.ActiveEntities, Is.GreaterThan(0)); 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 marker = FindMarker(root, playerId); - - if (requireSessionGameplay) - { - EveUnitySceneCommandReceipt observedDockReceipt = null; - string dockCommandId = null; - runtime.ReceiptAvailable += receipt => - { - if (receipt.CommandId == dockCommandId) - observedDockReceipt = receipt; - }; - host.SubmitAdvertisedActionIntent(playerId, "simulation.step"); - var dockVersion = runtime.ActiveVersion; - var dock = host.SubmitAdvertisedActionIntent(playerId, "pilot.dock"); - dockCommandId = dock.CommandId; - var dockDeadline = Time.realtimeSinceStartup + 12f; - while (Time.realtimeSinceStartup < dockDeadline && - (observedDockReceipt == null || - !string.Equals(observedDockReceipt.State, "reconciled", StringComparison.OrdinalIgnoreCase) || - runtime.ActiveVersion < observedDockReceipt.SourceVersion || - !(host.InputCapability?.Actions ?? Array.Empty()).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(observedDockReceipt, dock.CommandId, - provider.Selection.ProviderId, provider.Selection.SurfaceId, dockVersion); - var buyAction = (host.InputCapability?.Actions ?? Array.Empty()) - .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()).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()) - .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()).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()).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()).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 playerPrefab = provider.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( - playerFact.AssetRef, - playerFact.EntityKind, - "provider-asset-ref")); - Assert.That(playerPrefab, Is.Not.Null, - $"Provider asset catalog did not resolve player AssetRef '{playerFact.AssetRef}'."); - Assert.That(provider.CurrentAssetBodyTransportKind, - Is.EqualTo(CultMeshBodyTransportKind.SharedFileMapping), - "The first requested provider bundle was not negotiated as a mapped body."); Assert.That( marker.GetComponentsInChildren(includeInactive: true).Length, Is.GreaterThan(1), "The generic client substituted its primitive fallback instead of the provider-authored AssetBundle prefab."); - + var initialPosition = marker.transform.position; initialVersion = runtime.ActiveVersion; - if (requireSessionGameplay) + var request = runtime.SubmitMoveVectorIntent(playerId, 1f, 0f, 1f); + + var deadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < deadline && + (runtime.LastReceipt == null || runtime.ActiveVersion <= initialVersion || + Vector3.Distance(FindMarker(root, playerId).transform.position, initialPosition) < 0.01f)) { - var initialPosition = marker.transform.position; - 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); + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); } - if (requireSessionGameplay) - { + Assert.That(runtime.LastReceipt, Is.Not.Null); + Assert.That(runtime.LastReceipt.CommandId, Is.EqualTo(request.CommandId)); + Assert.That( + runtime.LastReceipt.State, + Is.EqualTo("accepted").Or.EqualTo("reconciled"), + runtime.LastReceipt.Message); + Assert.That(runtime.LastReceipt.SourceVersion, Is.GreaterThan(initialVersion)); + Assert.That(runtime.ActiveVersion, Is.GreaterThan(initialVersion)); + movementDistance = Vector3.Distance(FindMarker(root, playerId).transform.position, initialPosition); + Assert.That(movementDistance, Is.GreaterThan(0.01f)); + movementReceipt = WitnessReceipt.From("movement", runtime.LastReceipt); + 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(); - 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(); - 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(); - Assert.That(combatRenderer, Is.Not.Null); - var focusVersion = runtime.ActiveVersion; - var inputDriver = root.GetComponent() ?? - root.AddComponent(); - 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.LastReceipt, Is.Not.Null); + Assert.That(runtime.LastReceipt.CommandId, Is.EqualTo(focus.CommandId)); + Assert.That(runtime.LastReceipt.State, Is.EqualTo("accepted").Or.EqualTo("reconciled")); + Assert.That(runtime.LastReceipt.SourceVersion, Is.GreaterThan(movementVersion)); + focusReceipt = WitnessReceipt.From("targeting", runtime.LastReceipt); + + var focusVersion = runtime.ActiveVersion; + var action = runtime.SubmitActionIntent(playerId, "0"); + var actionDeadline = Time.realtimeSinceStartup + 12f; + while (Time.realtimeSinceStartup < actionDeadline && + (runtime.LastReceipt == null || runtime.LastReceipt.CommandId != action.CommandId || + runtime.ActiveVersion < runtime.LastReceipt.SourceVersion)) + { + yield return new WaitForSecondsRealtime(0.1f); + runtime.Refresh(); + } + Assert.That(runtime.LastReceipt, Is.Not.Null); + Assert.That(runtime.LastReceipt.CommandId, Is.EqualTo(action.CommandId)); + Assert.That( + runtime.LastReceipt.State, + Is.EqualTo("accepted").Or.EqualTo("reconciled"), + runtime.LastReceipt.Message); + Assert.That(runtime.LastReceipt.SourceVersion, Is.GreaterThan(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(); - 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(); - finalPickupEntityCount = Math.Max(0, finalPickupEntityCount); - + var light = lightObject.AddComponent(); + light.type = LightType.Directional; + light.intensity = 1.4f; + lightObject.transform.rotation = Quaternion.Euler(45f, -35f, 0f); var camera = cameraObject.AddComponent(); camera.clearFlags = CameraClearFlags.SolidColor; camera.backgroundColor = new Color(0.035f, 0.055f, 0.08f, 1f); - camera.cullingMask = -1; + foreach (var entityMarker in root.GetComponentsInChildren()) + { + foreach (var renderer in entityMarker.GetComponentsInChildren()) + { + renderer.material.color = entityMarker.Controllable + ? new Color(0.18f, 0.75f, 1f) + : entityMarker.EntityKind == "enemy" + ? new Color(1f, 0.25f, 0.2f) + : new Color(1f, 0.78f, 0.15f); + } + } var playerMarker = FindMarker(root, playerId); var playerRenderers = playerMarker.GetComponentsInChildren(); 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(), Is.Empty, - "The provider-authored player presentation retained gameplay colliders."); - Assert.That(playerMarker.GetComponentsInChildren(), Is.Empty, - "The provider-authored player presentation retained gameplay rigid bodies."); + var playerBounds = playerRenderers[0].bounds; 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(); - foreach (var renderer in root.GetComponentsInChildren(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); + playerBounds.Encapsulate(renderer.bounds); + var halfFovRadians = camera.fieldOfView * 0.5f * Mathf.Deg2Rad; + var cameraDistance = Mathf.Max(8f, playerBounds.extents.magnitude / Mathf.Tan(halfFovRadians) * 1.35f); + camera.nearClipPlane = Mathf.Max(0.01f, cameraDistance * 0.001f); + camera.farClipPlane = Mathf.Max(1000f, cameraDistance + playerBounds.size.magnitude * 4f); + camera.transform.position = playerBounds.center + new Vector3(1f, 0.65f, -1.2f).normalized * cameraDistance; + camera.transform.LookAt(playerBounds.center); + target = new RenderTexture(640, 360, 24, RenderTextureFormat.ARGB32); camera.targetTexture = target; - var cameraRig = cameraObject.AddComponent(); - 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); - var expectedTexturedPlayerMaterialCount = requireSessionGameplay ? 8 : 7; - Assert.That(texturedPlayerMaterialCount, Is.GreaterThanOrEqualTo(expectedTexturedPlayerMaterialCount), - "The provider bundle must retain its advertised native material textures."); - var directionalLightIntensity = UnityEngine.Object.FindObjectsOfType() - .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(); - mapCamera.cullingMask = 1 << mapLayer; - Assert.That(mapCamera.cullingMask, Is.EqualTo(1 << mapLayer)); - var captureFieldVolume = root.GetComponent(); - 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; + camera.Render(); RenderTexture.active = target; - pixels = new Texture2D(captureWidth, captureHeight, TextureFormat.RGB24, false); - pixels.ReadPixels(new Rect(0, 0, captureWidth, captureHeight), 0, 0); + pixels = new Texture2D(640, 360, TextureFormat.RGB24, false); + pixels.ReadPixels(new Rect(0, 0, 640, 360), 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(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(); - 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."); - if (requireSessionGameplay) - { - 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(); - 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(includeInactive: true) - .Select(marker => PresentedEntityFact.From(camera, marker)) - .ToArray()); + actionReceipt); } finally { RenderTexture.active = null; var camera = cameraObject.GetComponent(); if (camera != null) camera.targetTexture = null; - var mapCamera = mapCameraObject.GetComponent(); - 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); + runtime?.Dispose(); + UnityEngine.Object.DestroyImmediate(lightObject); UnityEngine.Object.DestroyImmediate(cameraObject); UnityEngine.Object.DestroyImmediate(root); } } - private static async Task ReceiveAdvertisedRealtimeFrameAsync( - string rendezvousEndpoint, - string verseId, - string authorityRuntimeId) - { - 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( - new CultMeshSessionTarget(verseId, authorityRuntimeId), - 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) + WitnessReceipt action) { 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 } + receipts = new[] { movement, targeting, action } }; Directory.CreateDirectory(Path.GetDirectoryName(Path.GetFullPath(path))); File.WriteAllText(path, JsonUtility.ToJson(document, true)); @@ -1394,116 +387,23 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests [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) @@ -1513,137 +413,11 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests 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(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()) @@ -1654,270 +428,6 @@ namespace GameCult.EveUnity.GenericClient.PlayModeTests 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 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 allRenderers, - IReadOnlyList measuredRenderers, - IReadOnlyList 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 actual, - IReadOnlyList 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(); - 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(); - 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, - Path.Combine(directory, "field-volume-material.txt")); - RenderTexture.active = previous; - } - - private static void WriteVolumeMaterialFacts( - Material material, - IReadOnlyDictionary metadata, - string path) - { - if (material == null || metadata == null) return; - var facts = new List(); - 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 ?? ""}:{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[] diff --git a/TestProject/Packages/manifest.json b/TestProject/Packages/manifest.json index 157057e..b08495a 100644 --- a/TestProject/Packages/manifest.json +++ b/TestProject/Packages/manifest.json @@ -5,9 +5,9 @@ "com.unity.modules.imageconversion": "1.0.0", "com.unity.modules.physics": "1.0.0", "com.unity.modules.uielements": "1.0.0", - "org.gamecult.cultlib": "file:../../artifacts/test-dependencies/org.gamecult.cultlib", + "org.gamecult.cultlib": "file:../../../CultLib/artifacts/unity/org.gamecult.cultlib", "org.gamecult.eve.plugin-fields": "file:../../../EvePlugins/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields", - "org.gamecult.eve.surface": "file:../../../Eve/packages/org.gamecult.eve.surface", + "org.gamecult.eve.surface": "file:../../packages/org.gamecult.eve.surface", "org.gamecult.eve.unity-scene": "file:../../packages/org.gamecult.eve.unity-scene", "org.gamecult.eve.unity-uitoolkit": "file:../../packages/org.gamecult.eve.unity-uitoolkit" }, diff --git a/TestProject/Packages/packages-lock.json b/TestProject/Packages/packages-lock.json index d631673..6b032e8 100644 --- a/TestProject/Packages/packages-lock.json +++ b/TestProject/Packages/packages-lock.json @@ -1,94 +1,11 @@ { "dependencies": { - "com.unity.burst": { - "version": "1.8.29", - "depth": 3, - "source": "registry", - "dependencies": { - "com.unity.mathematics": "1.2.1", - "com.unity.modules.jsonserialize": "1.0.0" - }, - "url": "https://packages.unity.com" - }, - "com.unity.collections": { - "version": "6.4.0", - "depth": 3, - "source": "builtin", - "dependencies": { - "com.unity.burst": "1.8.23", - "com.unity.mathematics": "1.3.2", - "com.unity.nuget.mono-cecil": "1.11.5", - "com.unity.test-framework": "1.4.6", - "com.unity.test-framework.performance": "3.0.3" - } - }, "com.unity.ext.nunit": { "version": "2.0.5", "depth": 1, "source": "builtin", "dependencies": {} }, - "com.unity.mathematics": { - "version": "1.3.3", - "depth": 1, - "source": "registry", - "dependencies": {}, - "url": "https://packages.unity.com" - }, - "com.unity.nuget.mono-cecil": { - "version": "1.11.6", - "depth": 4, - "source": "registry", - "dependencies": {}, - "url": "https://packages.unity.com" - }, - "com.unity.render-pipelines.core": { - "version": "17.4.0", - "depth": 2, - "source": "builtin", - "dependencies": { - "com.unity.burst": "1.8.14", - "com.unity.mathematics": "1.3.2", - "com.unity.ugui": "2.0.0", - "com.unity.collections": "2.4.3", - "com.unity.modules.terrain": "1.0.0", - "com.unity.modules.jsonserialize": "1.0.0" - } - }, - "com.unity.render-pipelines.universal": { - "version": "17.4.0", - "depth": 1, - "source": "builtin", - "dependencies": { - "com.unity.render-pipelines.core": "17.4.0", - "com.unity.shadergraph": "17.4.0", - "com.unity.render-pipelines.universal-config": "17.4.0" - } - }, - "com.unity.render-pipelines.universal-config": { - "version": "17.4.0", - "depth": 2, - "source": "builtin", - "dependencies": { - "com.unity.render-pipelines.core": "17.4.0" - } - }, - "com.unity.searcher": { - "version": "4.9.4", - "depth": 3, - "source": "registry", - "dependencies": {}, - "url": "https://packages.unity.com" - }, - "com.unity.shadergraph": { - "version": "17.4.0", - "depth": 2, - "source": "builtin", - "dependencies": { - "com.unity.render-pipelines.core": "17.4.0", - "com.unity.searcher": "4.9.3" - } - }, "com.unity.test-framework": { "version": "1.6.0", "depth": 0, @@ -99,27 +16,8 @@ "com.unity.modules.jsonserialize": "1.0.0" } }, - "com.unity.test-framework.performance": { - "version": "3.4.0", - "depth": 4, - "source": "registry", - "dependencies": { - "com.unity.test-framework": "1.1.33", - "com.unity.modules.jsonserialize": "1.0.0" - }, - "url": "https://packages.unity.com" - }, - "com.unity.ugui": { - "version": "2.0.0", - "depth": 3, - "source": "builtin", - "dependencies": { - "com.unity.modules.ui": "1.0.0", - "com.unity.modules.imgui": "1.0.0" - } - }, "org.gamecult.cultlib": { - "version": "file:../../artifacts/test-dependencies/org.gamecult.cultlib", + "version": "file:../../../CultLib/artifacts/unity/org.gamecult.cultlib", "depth": 0, "source": "local", "dependencies": {} @@ -128,16 +26,14 @@ "version": "file:../../../EvePlugins/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields", "depth": 0, "source": "local", - "dependencies": { - "org.gamecult.cultlib": "1.0.13" - } + "dependencies": {} }, "org.gamecult.eve.surface": { - "version": "file:../../../Eve/packages/org.gamecult.eve.surface", + "version": "file:../../packages/org.gamecult.eve.surface", "depth": 0, "source": "local", "dependencies": { - "org.gamecult.cultlib": "1.0.56" + "org.gamecult.cultlib": "1.0.3" } }, "org.gamecult.eve.unity-scene": { @@ -145,13 +41,9 @@ "depth": 0, "source": "local", "dependencies": { - "org.gamecult.cultlib": "1.0.56", - "org.gamecult.eve.plugin-fields": "0.2.3", - "org.gamecult.eve.surface": "0.3.6", - "org.gamecult.eve.unity-uitoolkit": "0.1.9", - "com.unity.mathematics": "1.3.2", - "com.unity.render-pipelines.universal": "17.4.0", - "com.unity.modules.particlesystem": "1.0.0" + "org.gamecult.cultlib": "1.0.3", + "org.gamecult.eve.plugin-fields": "0.1.1", + "org.gamecult.eve.surface": "0.1.0" } }, "org.gamecult.eve.unity-uitoolkit": { @@ -159,7 +51,7 @@ "depth": 0, "source": "local", "dependencies": { - "org.gamecult.eve.surface": "0.3.6" + "org.gamecult.eve.surface": "0.1.0" } }, "com.unity.modules.assetbundle": { @@ -192,24 +84,12 @@ "source": "builtin", "dependencies": {} }, - "com.unity.modules.particlesystem": { - "version": "1.0.0", - "depth": 1, - "source": "builtin", - "dependencies": {} - }, "com.unity.modules.physics": { "version": "1.0.0", "depth": 0, "source": "builtin", "dependencies": {} }, - "com.unity.modules.terrain": { - "version": "1.0.0", - "depth": 3, - "source": "builtin", - "dependencies": {} - }, "com.unity.modules.ui": { "version": "1.0.0", "depth": 1, diff --git a/docs/aetheria-released-package-witness.md b/docs/aetheria-released-package-witness.md deleted file mode 100644 index 6446cec..0000000 --- a/docs/aetheria-released-package-witness.md +++ /dev/null @@ -1,370 +0,0 @@ -# Aetheria released-package witness - -## Current integration gate - -The generic `ReleaseConsumerProject` is pinned to released Git packages: - -- `org.gamecult.eve.unity-scene` `0.3.134`, commit - `d1663b36d12b60ff5c7249135eff48edf6227ca9`, and - `org.gamecult.eve.surface` `0.3.6`, commit - `96839ad34c8d464ef622d8bbdd5d277e1ca9d825`; -- `org.gamecult.eve.plugin-fields` `0.2.3`, commit - `c5a4a75c1b727499b16c2dae1895f29e2a9f72f0`; -- `org.gamecult.eve.unity-uitoolkit` `0.1.9`, commit - `d1663b36d12b60ff5c7249135eff48edf6227ca9`; -- `org.gamecult.cultlib` `1.0.56`, commit - `334e60f1928b4212a29dd8b0d19b2c099fe6365e`. - -The hand-run integration gates are the cold lowering proof and the warm -full-session proof: - -```powershell -pwsh -File .\scripts\run-aetheria-daemon-world-witness.ps1 ` - -CultLibRoot E:\Projects\CultLib ` - -CacheState cold ` - -AssetCacheDirectory artifacts\admitted-release-cold-cache ` - -OutputDirectory artifacts\admitted-release-cold ` - -SkipAssetBundleBuild -``` - -Both connect directly to the Aetheria daemon. Control, subscriptions, commands, -and receipts use `cultnet+tcp`; provider files use `cultmesh-content+tcp`; -same-machine entity state uses a mapped SoA body; and the released native QUIC -client receives the advertised latest-only realtime body. No Odin hop or -Aetheria-specific gameplay code participates in the Unity runtime. This -admission pass proves the package graph with the isolated EditMode consumer; it -does not claim a new rendered end-to-end witness. - -## Historical witness ledger - -The retained cold proof is -`artifacts/cultlib-1043-scene-03100-cold5/runtime-witness.cold.json`. It began -with no bodies or partials and passed in 57.8 seconds end to end (14.9 seconds -inside PlayMode). The client materialized nine requested bundles totaling -56,872,521 bytes: shaders, core resources, UI, Longinus, tractor beam, the three -visible celestial types, and the visible asteroid. Every body matches the -current provider file by SHA-256 and length; no partial remained. Unused ships, -stations, shield, mine, turret, pickup, lightning, and explosion bundles stayed -unloaded. This cold profile owns content transfer, world lowering, fog and -Stardust execution, and pilot/map camera isolation. It deliberately issues no -gameplay command into the paused Terminus fixture. - -The retained warm full-session proof is -`artifacts/cultlib-1043-scene-03100-warm/runtime-witness.warm.json`. It passed in -70.7 seconds end to end (27.9 seconds inside PlayMode). The generic client docks, -buys and sells one typed item, resumes and undocks, moves, targets, aims, holds -and releases the tractor beam, destroys the proof hostile, and collects the -resulting loot exactly once through daemon-owned XZ proximity. All eight -retained receipts are provider-owned and reconciled. Eleven on-demand bundles -totaling 74,191,724 bytes were present by the end; gameplay loaded only the -additional content it exercised. The pilot camera excludes all eleven map -renderers and the map camera includes them. - -The pilot capture proves that provider fog, Stardust, celestial meshes, and ship -meshes reach URP, but it is not final visual approval: hulls remain crushed to -black and the cloud frame remains too hot and cyan-grey. Those are provider -visual-lever and shader-calibration issues, not concealed transport failures. -The map capture is clean and contains only map-channel glyphs on its dark map -background. - -An earlier released-package cold world witness passed with: - -- `org.gamecult.eve.unity-scene` `0.3.63`, commit - `3135d24bb84f40719b121b51634811c85af0a7af`; -- `org.gamecult.eve.plugin-fields` `0.2.3`, commit - `c5a4a75c1b727499b16c2dae1895f29e2a9f72f0`; -- `org.gamecult.eve.surface` `0.2.2`, commit - `140e1bd963a0033e66777a3b2c5fe6e9c97dfe32`; -- `org.gamecult.eve.unity-uitoolkit` `0.1.1`, commit - `4d0cbe0185bdc4fc65eb63503a7c5cb578539669`; -- `org.gamecult.cultlib` `1.0.15`, commit - `419053ebe2325848051c4f4d8ba352cd4286c424`; -- the generic `ReleaseConsumerProject` client connected directly to the - Aetheria daemon. - -The cold `0.3.59` witness in -`artifacts/aetheria-daemon-adaptive-exposure-cold` resolves the tagged package, -transfers and lowers the 56,170,916-byte provider bundle -`34dd156038c9c55b96136e10805f35f2bc7f2aa4e8dda8a519e57aa5a83329c8`, and -passes in 53.3 seconds. EveUnity lowers Aetheria's advertised historical -histogram exposure as a provider-agnostic camera semantic; the bundled static -volume profile remains neutral and is no longer a competing exposure owner. -Average pilot-frame luminance fell from `0.8716` in the restored-scattering -camera witness to `0.5096`, recovering visible cloud structure. The capture is -still cyan and visibly posterized, so temporal/color parity remains open. -Map-channel isolation also passes: the pilot camera excludes the 11 map -renderers and the map camera includes them. - -The subsequent released `0.3.61` warm witness in -`artifacts/aetheria-daemon-temporal-bootstrap-warm` restores two temporal -details from the fossil renderer as explicit provider-program semantics. The -fog history projects the previous view through the current projection, and the -first history frame uses the provider-advertised `ultra` quality before -settling to the scene's serialized `high` quality. Average luminance remains -effectively unchanged while mean neighboring-pixel variation falls from -`0.0332` to `0.0174` and Laplacian high-frequency energy falls from `0.1050` -to `0.0531`. This proves the temporal history is accumulating and that its -bootstrap seed mattered. The remaining cyan/tonemap mismatch is still open. - -The cold HD witness in `artifacts/aetheria-daemon-fog-alpha-cold-hd` then -transfers the rebuilt 56,168,970-byte provider bundle -`a829e57731fbe08f4564fa131468148e99f6b4204708ad3e64498885532b5091` -through `SharedFileMapping` and captures after 128 temporal composites at -1280x720. It restores the historical fog history ABI: the temporal pass stores -density directly in alpha, so the composite consumes that density directly -instead of decoding it as the raymarch pass's packed distance/density payload. -The flat grey opacity slabs disappear. The released client test passes in 52.7 -seconds with 238 fog composites and Stardust draws; the pilot camera still -excludes map renderers and the map camera includes them. Remaining visual work -is color/composition parity, not broken fog opacity or an unsettled witness. - -The released `0.3.62` warm HD witness in -`artifacts/aetheria-daemon-hdr-grading-warm-hd` lowers the provider's -`hdr-before-tonemap.v1` grading-space contract. This restores the fossil -profile's HDR ordering—grading before ACES—rather than inheriting the released -consumer project's default LDR grading after tonemapping. EveUnity restores the -consumer's prior pipeline mode when the camera is released and fails closed -when the requested grading space cannot be lowered. - -The asserted rerun in `artifacts/aetheria-daemon-hdr-grading-asserted-warm-hd` -also completes the live destruction-loot path on the same released client. One -daemon destruction produces the canonical pickup; tractor power reaches `1` -and returns to `0`; one Ymir `begin` fact produces exactly one -`pickup.collected` event; cargo changes from `0` to `1`; and no client pickup -command participates in the transaction. The event identity begins -`ymir-fact:` and contains the retained Box3D session, step, contact episode, -and begin-fact identity. - -The released `0.3.63` warm HD witness in -`artifacts/aetheria-daemon-temporal-projection-0363-warm-hd` makes the native -volume history projection convention an explicit provider-program semantic. -Aetheria advertises the non-render-target projection convention used by its -historical `Graphics.Blit` temporal pass; the generic URP lowerer applies that -convention without naming Aetheria or interpreting fog. The witness captures -the naturally rendered camera target after 359 volume composites instead of -issuing a second out-of-band `Camera.Render()`. Package tests pass 105/105 and -the released gameplay witness still proves movement, combat, destruction loot, -one Ymir contact collection, held tractor input, and camera-channel isolation. -The remaining visual controls (fog parameters, stellar tint, ambient intensity, -histogram exposure, grading, and temporal settings) remain provider-owned -levers; this proof does not claim a final art grade. - -The released `0.3.63` warm rejection witness in -`artifacts/aetheria-daemon-cargo-rejection-0363-warm` selects the daemon-owned -`cargo-capacity-rejection-proof` scenario. The controlled ship begins with all -generated cargo bays filled from typed item volume and bay capacity. Combat -creates the salvage pickup normally; player Ymir Begin contacts emit -`pickup.rejected` with `cargo-capacity`, preserve cargo at `5` items, and leave -the pickup live. The final run observed two later player recontacts with two -distinct `ymir-fact:` identities and no player collection event. The generic -witness correlates feedback through advertised SoA `SourceIndex` and feedback -`TargetEntityIndex`; events from other ships are not attributed to the pilot. - -Current released-package evidence is in -`artifacts/aetheria-daemon-mapped-body-cold`; despite that retained directory -name, `runtime-witness.warm.json` is the current passing run. The warm PlayMode test -passed and recorded provider-owned reconciled movement, look, targeting, -tractor press, tractor release, contact-gated cargo collection, and action -receipts. The fingerprinted April 2021 catalog recovery now restores the -distinct common `Longinus`, rare `LonginusX`, and `Djinni` rows plus the missing -turn-thruster equipment needed to outfit ordinary hulls. This run generated the -player as a common directional-thruster `Longinus`; the other six presented -ships were `Djinni`, and no `LonginusX` appeared. The player moved 14.34 world -units and completed its authoritative hardware-driven look before combat. -The daemon-owned look direction reached the SoA body rotation, -and the generic aim presentation rendered at the advertised 50-unit minimum -convergence distance. Aetheria derives ambient color from daemon-owned stellar -`LightColor` values and advertises `studio2` only as the reflection cubemap; the -gravity-fog raymarch owns the visible frame. The released lowerer records flat -ambient mode, color `(1.46,1.2556,0.8468)`, ambient-probe energy `4.6354`, and -custom reflection texture `studio2`. No skybox asset or key light is advertised, -and the generic camera rig records zero live directional intensity. The earlier -invented `0.75` light and false skybox authority were deleted rather than retained -as material-conversion compensators. -Aetheria also advertises the original undocked `ARPG.unity` Third Person Rig: -entity-forward perspective follow, `30` unit distance, `60` degree vertical -field of view, canonical bottom-origin target screen position `0.64,0.19`, zero position damping, and -`1`-`4096` clip planes. Its generic `aim.convergence-point.v1` relation makes -the optical axis reach the daemon-published aim point while preserving that -entity framing. The generic camera rig lowers both constraints without -Aetheria-specific camera code. The distinct `70` unit Docked Rig no longer -incorrectly owns the flight view. -Aetheria's pilot surface also publishes a transparent cockpit overlay. The -released generic UI Toolkit lowerer produced seven native `ProgressBar` elements -for hull, shield, capacitor, weapon cooldown, target lock, and target state. Daemon frame, -player, and command diagnostics remain available in the document but lower -with `display: none` in the pilot presentation. The runnable generic-client -launcher mounts the same advertised document and forwards its typed commands; -it contains no Aetheria gameplay or client-authored world light. - -The same surface publishes a `field.volume3d` node over four canonical Eve -Fields splat layers. Its surface contract contains logical ports such as -`surfaceHeight`, `patch`, and `tint`; it contains no Unity `_Nebula*` property -names or pass indices. The selected `unity-scene` provider asset variant owns -that concrete shader ABI through `unity.volume.*` metadata. The released -generic lowerer resolved the provider shader and dither texture, rasterized all - four layers, and composited `195` pilot-camera frames through daemon frame `491`. -The provider variant advertises the fossil shader's raymarch, temporal-history, -and composite passes plus their logical ports. EveUnity allocates and resets the -history targets generically; a partial temporal ABI fails closed. - -The two PNGs below are deliberate camera-channel captures, not a composited -screen capture, so they prove world/map isolation rather than HUD pixels. The -native cockpit proof is the asserted UI Toolkit visual tree recorded as -`cockpitProgressCount: 7` in the witness facts. The same run records completed -daemon lock progress `1.0`; Unity does not manufacture or smooth that value. - -Camera-channel facts: - -- provider-authored player renderers: `7` active, with no embedded shield renderer; -- pilot changed pixels: `217,323`; -- pilot average luminance: `0.1322155`; -- pilot bright pixels: `101,510`; -- map-channel renderers: `10`; -- map changed pixels: `3,179`; -- native cockpit progress bars: `7`; -- daemon tractor power at capture: `1.0` after the held input completed its - authored ramp; -- daemon tractor power after the advertised release: `0`; -- provider tractor particle systems: `1`; -- pickup entities before/after destruction and collection: `0` / `0`; -- provider `pickup.collected` events: exactly `1`; -- collected item and quantity: one canonical `aetheria.item_definition:*` key, - quantity `1`; -- authoritative cargo quantity before/after: `0` / `1`; -- the pilot camera excludes the advertised map layer; -- the player prefab's embedded layer-14 map icon contributes exactly `0` pilot - pixels; -- the map camera renders exactly the advertised map layer. - -The daemon also publishes the current zone's celestial presentation through the -same generic entity SoA generation as ordinary world entities. The current warm -witness records eleven sun/planet/gas-giant rows and eighteen asteroid rows. -Every row resolved a provider asset with enabled native renderers, and seven -celestial instances intersected the pilot frustum. These identities are -presentation-only and cannot receive gameplay commands. EveUnity contains no -Aetheria or celestial branch; release `0.3.55` includes the generic prefab wrapper -so semantic instance scale composes with, rather than overwrites, provider-local -scale. - -Provider prefab construction now removes the fossil shield renderer embedded in -each ship. That renderer's custom shader performs collision-local dither clipping; -flattening it to ordinary opaque URP Lit had produced the large black ellipsoids -previously mistaken for celestial bodies. Shield state remains daemon truth and -shield impacts use the separately advertised receipt-driven effect. Material -conversion also preserves source lighting models: the map icon's unlit source -now becomes `Universal Render Pipeline/Unlit`, so map glyphs remain readable -without resurrecting the false scene light. - -Visual inspection confirms that map glyphs are absent from the pilot frame and -present in the map-only frame. This is transport, authority, field-production, -and lowering proof, not visual parity: the current pilot capture contains the -blue fog field and the provider's magenta-and-gold Longinus hull with no opaque -shield ellipsoid. The celestial composition still lacks the fossil capture's -readable close body. Release -`0.3.57` includes generic lowering for Eve Fields `0.2.3`'s -`AnimatedRadialCosine` source alongside `PowerPulse`, dither-scale, -temporal-history, and finite-look-at semantics. Aetheria publishes the fossil -global animated simplex/cellular/ambient producer stack, body-owned radial wave -frequency, separate negative `gravity.height` and positive -`fog.surface_height`, and the authored `ARPG.unity` zone-brush extent. The - witness records player Y `-101.5035` and camera Y `-84.7369`; Unity does not -sample terrain to invent either value. The four rasterized field layers and HDR -raymarch/history targets are populated, so the remaining failure is in native -presentation/composition or provider art calibration rather than missing state -delivery. Color, lighting, patch structure, readable world geometry, and -gravity-wave visual parity remain open. EveUnity must not compensate with -Aetheria-specific lighting or fog rules. - -The `0.3.57` run also closes two camera-contract omissions. Aetheria advertises -the fossil pilot camera's neutral temporal-reprojection values; the released -generic lowerer applies URP TAA with `0.99` history blend, `0.1` jitter scale, -high quality, and zero sharpening. Fog and Stardust now consume one generic -camera-relative viewport-frame resolver rather than projecting the same splat -document independently. The live witness fails unless their snapped grid -centers are identical; the latest run resolved both to `(-6,-6)` after movement. -The provider HLSL now consumes Eve Fields' positive world-axis viewport directly; -it no longer retains the two-axis compensation once required by the deleted -fossil gravity camera. An asymmetric GPU fixture fails unless positive-world X -samples positive texture X, while the existing one-cell remap proof still keeps -705 overlapping world particles bit-identical. -The capture is still visibly rough and is not a visual-parity claim. - -The fossil Stardust material used a screen-space coverage mask that changes on -every rendered frame and depends on TAA to reconstruct stable subpixel stars. -The earlier URP port had replaced that with a fixed alpha threshold and -additive blending. Aetheria now advertises the provider dither texture, -viewport scale, and logical render-frame port; generic EveUnity binds them -without knowing their product meaning. The live witness resolves those native -ports from provider metadata and checks the actual material instance. The -provider bundle source gate requires the temporal clip, overwrite blend, and -depth write and rejects `_AlphaClip`. - -Substance is not part of this path; later texture baking belongs in Blender. -Aetheria currently bundles ambientCG's 1K `Metal012` color, normal, and -metalness maps under CC0. The same pre-generated maps now replace dead -Substance archive sub-assets on the brushed aluminium, tinted car paint, -cockpit, steel, black-metal, and radiator materials. The released witness - resolves textured native URP materials in `8` distinct player-material slots, -up from `3`, without adding another texture payload. This is temporary provider -art with recorded provenance, not a replacement texture pipeline. - -The tractor proof is structural, not yet a native-look victory lap. The daemon -publishes `beam.presentation` and advertises `pilot.scoop` as a -`button-hold.v1` value action. The generic client routes scalar `1` on press and -scalar `0` on release through the advertised `SetTractorPower` operation. Both -commands receive provider-owned reconciled receipts, and the daemon ramps the -published power from `1.0` at capture to exactly `0` after release. The provider -build removes the fossil's embedded tractor object from player/ship prefabs, so -the standalone `beam.presentation` prefab is the only tractor renderer. Its -cyan/yellow dotted band is visible in the pilot capture and absent from the map -capture. -The current `0.3.72` warm gameplay run is retained in -`artifacts/aetheria-daemon-0372-warm5`. It begins with no pickup. A -daemon-authoritative shot destroys the deterministic salvage target and creates -one provider-owned pickup carrying a canonical generated cargo item. Held -tractor input causes Ymir-owned attraction; collection itself is an -authoritative XZ proximity transaction and does not depend on a contact fact. -The daemon removes the pickup once, changes cargo from `0` to `1`, and emits one -stable `zone:0:pickup:0:collected` feedback event. No client pickup command or -Ymir contact receipt owns cargo mutation. - -The same run records 709 URP field-volume composites, 65,536 stateless field -particles, and 11 map-channel renderers. The pilot camera excludes every map -renderer and the map camera includes them. The serialized consumer renderer now -contains `EveUnityRendererFeature`; the configuration utility marks -`UniversalRendererData` dirty before saving so batch and editor cameras use the -same URP scheduling path. The captures are integration evidence, not a final art -grade: the pilot frame remains overexposed and the hull reads nearly black. -Effect tuning remains provider art work; the runtime must not compensate with -an Aetheria-specific beam. - -Primary artifacts: - -- `results.xml`: one passing PlayMode witness; -- `runtime-witness.json`: package, content, receipt, aim, combat, and - camera facts; -- `aetheria-daemon-world.png`: pilot-camera capture; -- `aetheria-daemon-map.png`: map-only capture. - -## Cold delivery status - -Cold CDN transfer is part of the passing readiness gate. The released `0.3.91` -run in `artifacts/cold-release-0391` began with zero cached bodies and partials, -received only the typed manifest through snapshot state, transferred bounded -content chunks through the managed CultMesh content session, and atomically -promoted one `56,204,750`-byte body named by SHA-256 -`f2c8acf938998703799e909cbf0c6e3ed9ad5d6f83b708a0b7b6e76ee33f7160`. -It left zero partials and selected `SharedFileMapping` over the verified file. - -The cold-only failure exposed by `0.3.90` was a bootstrap ownership race, not a -bulk snapshot transfer: live SoA subscriptions began before the blocking cold -asset acquisition, so their retained capability expired before Unity pumped it. -`0.3.91` resolves the surface first, completes provider-asset acquisition, and -only then starts live subscriptions. A fresh current generation therefore owns -the first lowered frame. No retry timer or stale-body reinterpretation exists. - -This proves cold acquisition and local mapped reuse, not provider-to-client -network zero-copy, shared-memory delivery, or GPU zero-copy. Managed remote -chunks are still copied and fragmented before the transfer owner commits the -mapped file; network-body streaming and copy-volume profiling remain open. diff --git a/docs/authority-map.md b/docs/authority-map.md index 3be5f6c..15c6148 100644 --- a/docs/authority-map.md +++ b/docs/authority-map.md @@ -9,9 +9,6 @@ importing provider product code into the generic runtime. EveUnity owns Unity scene projection, `GameObject` lifecycle, camera and input drivers, asset-provider hooks, package release, runtime tests, and captures. -`CultMeshClient` owns physical route selection, reconnection, shared typed -resource sessions, and their lease lifetime. Unity receives stable Verse and -provider identities and cannot preserve a physical route as gameplay authority. ## Inputs @@ -20,11 +17,7 @@ provider identities and cannot preserve a physical route as gameplay authority. - `gamecult.eve.provider_advertisement.v1` - `gamecult.eve.surface.v1` - provider asset-manifest documents -- runtime-selected asset-variant metadata, including native rendering-program bindings -- generic field-volume lifecycle declarations and logical texture/scalar/matrix ports -- generic viewport-to-texture scale bindings between logical vector and texture ports - provider command receipts -- semantic render-channel inclusion and exclusion policy - Unity input and local presentation assets ## Outputs @@ -36,83 +29,23 @@ provider identities and cannot preserve a physical route as gameplay authority. ## Derived State -`ActiveWorld`, scene objects, transforms, markers, camera pose, native -render-channel layer mappings, resolved native shader ports/passes, asset caches, -per-volume raymarch targets, temporal-history targets, previous camera matrices, +`ActiveWorld`, scene objects, transforms, markers, camera pose, asset caches, presentation counters, and receipt display state are projections. They are not -world truth. Temporal targets reset when the volume program, node, or render size -changes and can never become provider state. - -The optional Unity cache directory contains verified content-transfer state and -mapped body bytes only. It is disposable derived data, not a CultCache replica -of provider gameplay documents. - -Camera-relative field viewports have one generic resolver. The advertised -viewport frame owns its extent, spatial-cell scale, raster texels per cell, and -snap policy; fog volumes and stateless particle programs consume the identical -projected `EveFieldsViewport`. URP TAA state is likewise derived from the -world's neutral `temporal-reprojection.v1` camera contract and is restored on -release. - -Particle materials may consume advertised presentation-only inputs through -logical native metadata: provider-owned textures, viewport-to-texture scale, -and the current render-frame index. The runtime frame index owns temporal -coverage only; it cannot drive particle identity, phase, position, or any -daemon-visible state. +world truth. ## Forbidden Writers Unity transforms, input drivers, camera rigs, scene sinks, asset caches, and pending receipts may not mutate or simulate provider world state. Plugin -projection shells may not implement Sai, Norn, or TeX semantics. Portable -surfaces may not carry Unity layer numbers, camera culling masks, shader property -names, shader keywords, or native pass indices. Those bindings belong to the -selected runtime asset variant. A native volume program that advertises a -temporal pass must also advertise its current-sample, history, -previous-view-projection, and reset-history ports; partial temporal programs fail -closed. - -Unity code may not open provider snapshot sessions, schema clients, write -forwarders, subscriptions, or replica shards against a discovered physical -route. Reads, watches, content/body transfer, realtime frames, and command -submissions share the same stable-identity `CultMeshClient` owner. -Remote bootstrap and asset materialization complete through `PrepareAsync` -before the scene is mounted. `Connect`, `Mount`, input, refresh, and disposal -cannot synchronously wait on CultMesh. A bounded command outbox owns delivery; -continuous movement/look values may be replaced by a newer value, but transport -send completion cannot retire any command. Only the provider's canonical -receipt retires its idempotency key. Unacknowledged work is retried fairly; it -cannot become client-authored success or starve unrelated commands. - -Viewport-sized shader inputs are likewise presentation state. A provider may -relate a logical vector port to a logical native texture port through -`viewportTextureScaleBindings`; EveUnity derives the vector from the active -camera viewport and resolved texture dimensions on every render. A provider -literal is not allowed to impersonate those runtime dimensions, and an -unresolvable relation fails closed. - -The provider selects camera semantics and framing from its authoritative mode. -`planar.top-down-follow.v1` derives a downward presentation from the advertised -target and framing values. `perspective.entity-forward-follow.v1` derives its -view direction from the presented entity rotation and applies the same generic -distance, screen-position, damping, and lens contract. When the provider also -advertises `aim.convergence-point.v1`, the camera and aim marker consume one -derived convergence point from the `aim.presentation` node. The native camera -rotation and position are solved together so the optical axis reaches that -point while the followed entity remains at the advertised screen coordinate. -EveUnity owns only this native projection. It cannot collapse distinct provider -modes into one camera opinion or infer a product camera from entity kind. +projection shells may not implement Sai, Norn, or TeX semantics. ## Shared Paths -Initial preparation asks one rendezvous for Verses, selects an advertised -authority runtime and provider surface by semantic kind, then leases it through -the explicit Verse/authority-runtime target. The Eve provider ID remains a -separate surface-owner identity. -Refresh, route rotation, reconnect, and terminal-receipt reconciliation remain -inside the same identity-owned session. Movement, focus, target, and action -input all become `gamecult.eve.command_invocation.v1` through the -provider-advertised command boundary. +Initial connect discovers a Verse endpoint and selects a provider surface by +semantic kind. Refresh, reconnect, and terminal-receipt reconciliation consume +that provider's surface documents. Movement, focus, target, and action input all +become `gamecult.eve.command_invocation.v1` through the provider-advertised +command boundary. ## Cut Line diff --git a/packages/org.gamecult.eve.surface/Directory.Build.props b/packages/org.gamecult.eve.surface/Directory.Build.props new file mode 100644 index 0000000..994fcb6 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Directory.Build.props @@ -0,0 +1,7 @@ + + + $(MSBuildThisFileDirectory)bin~\ + $(MSBuildThisFileDirectory)obj~\ + $(MSBuildThisFileDirectory)obj~\ + + diff --git a/packages/org.gamecult.eve.surface/GameCult.Eve.Contracts.csproj b/packages/org.gamecult.eve.surface/GameCult.Eve.Contracts.csproj new file mode 100644 index 0000000..6b453f0 --- /dev/null +++ b/packages/org.gamecult.eve.surface/GameCult.Eve.Contracts.csproj @@ -0,0 +1,17 @@ + + + netstandard2.1 + enable + latest + enable + GameCult.Eve.Surface + Portable Eve surface, provider, input, plugin, and entity-view contracts. + $(MSBuildThisFileDirectory)..\..\..\CultLib + + + + + + + + diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity.meta b/packages/org.gamecult.eve.surface/GameCult.Eve.Contracts.csproj.meta similarity index 67% rename from packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity.meta rename to packages/org.gamecult.eve.surface/GameCult.Eve.Contracts.csproj.meta index d1f9c0e..3b5cba0 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity.meta +++ b/packages/org.gamecult.eve.surface/GameCult.Eve.Contracts.csproj.meta @@ -1,6 +1,5 @@ fileFormatVersion: 2 -guid: 6435ff06293834c49aa631e74b51a552 -folderAsset: yes +guid: 8a186b6d206ffa847aa276e984162512 DefaultImporter: externalObjects: {} userData: diff --git a/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj new file mode 100644 index 0000000..f07f7e8 --- /dev/null +++ b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj @@ -0,0 +1,15 @@ + + + netstandard2.1 + enable + latest + enable + Portable Eve surface and provider contracts. + $(MSBuildThisFileDirectory)..\..\..\CultLib + + + + + + + diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposure.compute.meta b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj.meta similarity index 54% rename from packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposure.compute.meta rename to packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj.meta index 3fe7574..751910b 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposure.compute.meta +++ b/packages/org.gamecult.eve.surface/GameCult.Eve.Surface.csproj.meta @@ -1,8 +1,7 @@ fileFormatVersion: 2 -guid: c8c45489b6ba44558342ebc5381c12d1 -ComputeShaderImporter: +guid: ce2d4913d64690c44ae68935014fd79d +DefaultImporter: externalObjects: {} - currentAPIMask: 4 userData: assetBundleName: assetBundleVariant: diff --git a/packages/org.gamecult.eve.surface/README.md b/packages/org.gamecult.eve.surface/README.md new file mode 100644 index 0000000..eb65c17 --- /dev/null +++ b/packages/org.gamecult.eve.surface/README.md @@ -0,0 +1,29 @@ +# Eve Surface Contract + +This package defines the retained CultUI surface document that every Eve +runtime lowers. + +Components own normal child layout through `Children`, live CultMesh value +bindings through `StateBindings`, and nested synced document regions through +`EmbeddedDocuments`. An embedded document slot names the child document id, +schema, presentation kind, and route hint; renderers resolve that child through +CultMesh instead of rebuilding it with runtime-local projection code. + +See `../../docs/surface-contract-v1.md` for the full contract and +`../../web/fixtures/cultui-embedded-surface.json` for the parity fixture. +Runtime discovery and required feature coverage live in +`../../tools/parity/parity-manifest.json`; active GUI runtimes must list +`embeddedDocuments` as a supported feature and require the `embedded-surface` +fixture. + +Verification entrypoints: + +- Browser/TypeScript: `node --test web\eve-dsl.test.mjs` +- Semantic runtime matrix: `powershell -ExecutionPolicy Bypass -File .\scripts\run-parity-harness.ps1` +- Flutter GUI contract, when Flutter is installed: `flutter test --plain-name embedded_surface_fixture_contract` +- Rust/CultMesh document sync: run `cargo test rust_preserves_cultui_embedded_surface_slots_through_typed_document_sync` from `E:/Projects/CultLib/packages/cultnet-rs` + +The parity manifest is the discovery source for web, Flutter desktop/Linux/Android, +iOS/UIKit, Android/Kotlin, Unity UI Toolkit, and Rust contract coverage. Do not +add a new renderer claim without adding its nested-surface fixture requirement +there. diff --git a/packages/org.gamecult.eve.unity-scene/Third Party Notices.md.meta b/packages/org.gamecult.eve.surface/README.md.meta similarity index 75% rename from packages/org.gamecult.eve.unity-scene/Third Party Notices.md.meta rename to packages/org.gamecult.eve.surface/README.md.meta index 4745e42..4bc751a 100644 --- a/packages/org.gamecult.eve.unity-scene/Third Party Notices.md.meta +++ b/packages/org.gamecult.eve.surface/README.md.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 5702faba00994033870c858bb2124d91 +guid: fe5aa27636bead047b2aecb14fdc59f2 TextScriptImporter: externalObjects: {} userData: diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Resources.meta b/packages/org.gamecult.eve.surface/Runtime.meta similarity index 77% rename from packages/org.gamecult.eve.unity-scene/Runtime/Resources.meta rename to packages/org.gamecult.eve.surface/Runtime.meta index fb9f65a..f150917 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Resources.meta +++ b/packages/org.gamecult.eve.surface/Runtime.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 63a640b4e19e5764eb030d665a2b7c00 +guid: 2164f6fea1216214d895e7dc99c46ecf folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/packages/org.gamecult.eve.surface/Runtime/EveAssetCatalogDocument.cs b/packages/org.gamecult.eve.surface/Runtime/EveAssetCatalogDocument.cs new file mode 100644 index 0000000..eba3b44 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveAssetCatalogDocument.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using GameCult.Caching; +using MessagePack; + +#nullable enable + +namespace GameCult.Eve.Surface +{ + [CultDocument("gamecult.eve.asset_catalog", SchemaId)] + [MessagePackObject] + public sealed class EveAssetCatalogDocument + { + public const string SchemaId = "gamecult.eve.asset_catalog.v1"; + + [SerializationConstructor] + public EveAssetCatalogDocument( + string schema, + string providerId, + string catalogId, + long version, + string updatedAtUtc, + EveAssetCatalogEntry[] assets) + { + Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema; + ProviderId = providerId ?? ""; + CatalogId = catalogId ?? ""; + Version = version; + UpdatedAtUtc = updatedAtUtc ?? ""; + Assets = assets ?? Array.Empty(); + } + + public EveAssetCatalogDocument( + string providerId, + string catalogId, + long version, + string updatedAtUtc, + EveAssetCatalogEntry[] assets) + : this(SchemaId, providerId, catalogId, version, updatedAtUtc, assets) + { + } + + [Key(0)] public string Schema { get; } + [Key(1)] public string ProviderId { get; } + [Key(2)] public string CatalogId { get; } + [Key(3)] public long Version { get; } + [Key(4)] public string UpdatedAtUtc { get; } + [Key(5)] public EveAssetCatalogEntry[] Assets { get; } + } + + [MessagePackObject] + public sealed class EveAssetCatalogEntry + { + [SerializationConstructor] + public EveAssetCatalogEntry( + string assetRef, + string assetKind, + EveAssetVariant[] variants, + Dictionary metadata) + { + AssetRef = assetRef ?? ""; + AssetKind = assetKind ?? ""; + Variants = variants ?? Array.Empty(); + Metadata = metadata == null + ? new Dictionary(StringComparer.Ordinal) + : new Dictionary(metadata, StringComparer.Ordinal); + } + + public EveAssetCatalogEntry( + string assetRef, + string assetKind, + EveAssetVariant[] variants, + IReadOnlyDictionary? metadata = null) + : this( + assetRef, + assetKind, + variants, + metadata == null ? new Dictionary() : new Dictionary(metadata)) + { + } + + [Key(0)] public string AssetRef { get; } + [Key(1)] public string AssetKind { get; } + [Key(2)] public EveAssetVariant[] Variants { get; } + [Key(3)] public Dictionary Metadata { get; } + } + + [MessagePackObject] + public sealed class EveAssetVariant + { + [SerializationConstructor] + public EveAssetVariant( + string runtimeId, + string platform, + string format, + string uri, + string contentHash, + long sizeBytes, + string assetKey, + Dictionary metadata) + { + RuntimeId = runtimeId ?? ""; + Platform = platform ?? ""; + Format = format ?? ""; + Uri = uri ?? ""; + ContentHash = contentHash ?? ""; + SizeBytes = sizeBytes; + AssetKey = assetKey ?? ""; + Metadata = metadata == null + ? new Dictionary(StringComparer.Ordinal) + : new Dictionary(metadata, StringComparer.Ordinal); + } + + public EveAssetVariant( + string runtimeId, + string platform, + string format, + string uri, + string contentHash, + long sizeBytes, + string assetKey, + IReadOnlyDictionary? metadata = null) + : this( + runtimeId, + platform, + format, + uri, + contentHash, + sizeBytes, + assetKey, + metadata == null ? new Dictionary() : new Dictionary(metadata)) + { + } + + [Key(0)] public string RuntimeId { get; } + [Key(1)] public string Platform { get; } + [Key(2)] public string Format { get; } + [Key(3)] public string Uri { get; } + [Key(4)] public string ContentHash { get; } + [Key(5)] public long SizeBytes { get; } + [Key(6)] public string AssetKey { get; } + [Key(7)] public Dictionary Metadata { get; } + } +} diff --git a/packages/org.gamecult.eve.surface/Runtime/EveAssetCatalogDocument.cs.meta b/packages/org.gamecult.eve.surface/Runtime/EveAssetCatalogDocument.cs.meta new file mode 100644 index 0000000..0af6487 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveAssetCatalogDocument.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8f53654f7c98d894588127ba001f11b4 \ No newline at end of file diff --git a/packages/org.gamecult.eve.surface/Runtime/EveCultMeshSurfaceSelection.cs b/packages/org.gamecult.eve.surface/Runtime/EveCultMeshSurfaceSelection.cs new file mode 100644 index 0000000..3af0c86 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveCultMeshSurfaceSelection.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using GameCult.Mesh; + +#nullable enable + +namespace GameCult.Eve.Surface +{ + public sealed class EveSurfaceRequest + { + public string ProviderId { get; set; } = ""; + public string SurfaceId { get; set; } = ""; + public string SurfaceKind { get; set; } = ""; + public string LoweringTarget { get; set; } = ""; + public bool RequireActive { get; set; } = true; + } + + public sealed class EveSurfaceSelection + { + internal EveSurfaceSelection( + EveProviderAdvertisementDocument provider, + EveAdvertisedSurface advertisement, + CultMeshDocumentHandle surface, + IReadOnlyList plugins) + { + Provider = provider; + Advertisement = advertisement; + Surface = surface; + Plugins = plugins; + } + + public EveProviderAdvertisementDocument Provider { get; } + public EveAdvertisedSurface Advertisement { get; } + public CultMeshDocumentHandle Surface { get; } + public IReadOnlyList Plugins { get; } + } + + public static class EveCultMeshSurfaceSelection + { + public static async Task SurfaceAsync( + this CultMeshClient mesh, + string endpointId, + EveSurfaceRequest? request = null, + CancellationToken cancellationToken = default, + Action? observeStage = null) + { + if (mesh == null) throw new ArgumentNullException(nameof(mesh)); + var resolved = request ?? new EveSurfaceRequest(); + IReadOnlyList providerDocuments; + if (string.IsNullOrWhiteSpace(resolved.ProviderId)) + { + observeStage?.Invoke("provider-collection-opening"); + var providers = await mesh.CollectionAsync(endpointId, cancellationToken) + .ConfigureAwait(false); + observeStage?.Invoke("provider-collection-ready"); + providerDocuments = await providers.LatestAsync().ConfigureAwait(false); + } + else + { + observeStage?.Invoke("provider-document-opening"); + var provider = await mesh.DocumentAsync( + endpointId, + ProviderAdvertisementRecordRef(resolved.ProviderId), + cancellationToken) + .ConfigureAwait(false); + observeStage?.Invoke("provider-document-ready"); + providerDocuments = new[] { await provider.LatestAsync().ConfigureAwait(false) }; + } + observeStage?.Invoke("provider-snapshot-ready"); + var match = Select(providerDocuments, resolved); + IReadOnlyList plugins = Array.Empty(); + if (match.Surface.RequiresPlugins.Count > 0) + { + var advertisements = await mesh.CollectionAsync(endpointId, cancellationToken) + .ConfigureAwait(false); + plugins = ResolvePlugins(match.Surface, await advertisements.LatestAsync().ConfigureAwait(false)); + observeStage?.Invoke("plugin-snapshot-ready"); + } + observeStage?.Invoke("surface-document-opening"); + var surface = await mesh.DocumentAsync( + endpointId, + match.Surface.RecordRef, + cancellationToken) + .ConfigureAwait(false); + observeStage?.Invoke("surface-document-ready"); + return new EveSurfaceSelection(match.Provider, match.Surface, surface, plugins); + } + + public static IReadOnlyList ResolvePlugins( + EveAdvertisedSurface surface, + IEnumerable advertisements) + { + if (surface == null) throw new ArgumentNullException(nameof(surface)); + if (advertisements == null) throw new ArgumentNullException(nameof(advertisements)); + var available = advertisements + .GroupBy(plugin => plugin.PluginId, StringComparer.Ordinal) + .ToDictionary(group => group.Key, group => group.OrderByDescending(plugin => plugin.Version, StringComparer.Ordinal).First(), StringComparer.Ordinal); + var selected = new List(); + foreach (var requirement in surface.RequiresPlugins) + { + if (!available.TryGetValue(requirement.PluginId, out var plugin)) + { + if (string.Equals(requirement.Availability, "required", StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException( + $"Eve surface '{surface.SurfaceId}' requires unavailable plugin '{requirement.PluginId}'."); + continue; + } + + var claims = plugin.Schemas.Concat(plugin.ComponentKinds).Concat(plugin.Commands) + .ToHashSet(StringComparer.Ordinal); + var missing = requirement.RequiredCapabilities.Where(capability => !claims.Contains(capability)).ToArray(); + if (missing.Length > 0) + { + if (string.Equals(requirement.Availability, "required", StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException( + $"Eve plugin '{plugin.PluginId}' does not advertise required capabilities: {string.Join(", ", missing)}."); + continue; + } + selected.Add(plugin); + } + return selected; + } + + public static (EveProviderAdvertisementDocument Provider, EveAdvertisedSurface Surface) Select( + IEnumerable providers, + EveSurfaceRequest request) + { + if (providers == null) throw new ArgumentNullException(nameof(providers)); + if (request == null) throw new ArgumentNullException(nameof(request)); + var match = providers + .Where(provider => Matches(request.ProviderId, provider.ProviderId)) + .SelectMany(provider => provider.Surfaces.Select(surface => new { Provider = provider, Surface = surface })) + .Where(candidate => Matches(request.SurfaceId, candidate.Surface.SurfaceId)) + .Where(candidate => Matches(request.SurfaceKind, candidate.Surface.SurfaceKind)) + .Where(candidate => !request.RequireActive || string.Equals(candidate.Surface.Status, "active", StringComparison.OrdinalIgnoreCase)) + .Where(candidate => string.IsNullOrWhiteSpace(request.LoweringTarget) || + candidate.Surface.WorldInteraction?.LoweringTargets.Any(target => + string.Equals(target, request.LoweringTarget, StringComparison.Ordinal)) == true) + .OrderBy(candidate => candidate.Provider.ProviderId, StringComparer.Ordinal) + .ThenBy(candidate => candidate.Surface.SurfaceId, StringComparer.Ordinal) + .FirstOrDefault(); + if (match == null) + throw new InvalidOperationException( + $"No advertised Eve surface matched provider='{request.ProviderId}', surface='{request.SurfaceId}', " + + $"kind='{request.SurfaceKind}', loweringTarget='{request.LoweringTarget}'."); + if (!string.Equals(match.Surface.Transport, "cultmesh-record", StringComparison.Ordinal)) + throw new InvalidOperationException( + $"Eve surface '{match.Surface.SurfaceId}' uses unsupported transport '{match.Surface.Transport}'."); + if (string.IsNullOrWhiteSpace(match.Surface.RecordRef)) + throw new InvalidOperationException($"Eve surface '{match.Surface.SurfaceId}' does not advertise a record reference."); + return (match.Provider, match.Surface); + } + + private static bool Matches(string expected, string actual) => + string.IsNullOrWhiteSpace(expected) || string.Equals(expected, actual, StringComparison.Ordinal); + + public static string ProviderAdvertisementRecordRef(string providerId) + { + if (string.IsNullOrWhiteSpace(providerId)) + throw new ArgumentException("Provider id must be non-empty.", nameof(providerId)); + return $"eve:provider:{providerId.Trim()}"; + } + } +} diff --git a/packages/org.gamecult.eve.surface/Runtime/EveCultMeshSurfaceSelection.cs.meta b/packages/org.gamecult.eve.surface/Runtime/EveCultMeshSurfaceSelection.cs.meta new file mode 100644 index 0000000..5385db9 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveCultMeshSurfaceSelection.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: b3c40df8a2a14d7db2f45f7018591fd3 diff --git a/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs b/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs new file mode 100644 index 0000000..22d55aa --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs @@ -0,0 +1,99 @@ +using System; +using GameCult.Caching; +using MessagePack; +using GameCult.Mesh; + +#nullable enable + +namespace GameCult.Eve.Surface +{ + [CultDocument("gamecult.eve.entity_soa_view", SchemaId)] + [MessagePackObject] + public sealed class EveEntitySoaViewDocument + { + public const string SchemaId = "gamecult.eve.entity_soa_view.v1"; + + [Key(0)] public string Schema { get; set; } = SchemaId; + [Key(1)] public string ProviderId { get; set; } = ""; + [Key(2)] public string ViewId { get; set; } = ""; + [Key(3)] public long FrameId { get; set; } + [Key(4)] public long Generation { get; set; } + [Key(5)] public string PublishedAtUtc { get; set; } = ""; + [Key(6)] public string Backend { get; set; } = "cultcache"; + [Key(7)] public string SynchronizationMode { get; set; } = "immutable_frame"; + [Key(8)] public EveEntitySoaBuffer[] Buffers { get; set; } = Array.Empty(); + [Key(9)] public EveEntitySoaColumn[] Columns { get; set; } = Array.Empty(); + [Key(10)] public EveEntitySoaDirtyRange[] DirtyRanges { get; set; } = Array.Empty(); + [Key(11)] public EveEntityRenderGroup[] RenderGroups { get; set; } = Array.Empty(); + [Key(12)] public EveEntitySoaIdentity[] Identities { get; set; } = Array.Empty(); + [Key(13)] public CultMeshBodyDescriptor? Body { get; set; } + } + + [MessagePackObject] + public sealed class EveEntitySoaIdentity + { + [Key(0)] public int EntityIndex { get; set; } = -1; + [Key(1)] public string EntityId { get; set; } = ""; + [Key(2)] public string Kind { get; set; } = ""; + [Key(3)] public string Label { get; set; } = ""; + [Key(4)] public string Faction { get; set; } = ""; + [Key(5)] public bool Selectable { get; set; } = true; + [Key(6)] public bool Controllable { get; set; } + [Key(7)] public string AssetRef { get; set; } = ""; + } + + [MessagePackObject] + public sealed class EveEntitySoaBuffer + { + [Key(0)] public string BufferId { get; set; } = ""; + [Key(1)] public string Backend { get; set; } = "cultcache"; + [Key(2)] public string Location { get; set; } = ""; + [Key(3)] public long ByteOffset { get; set; } + [Key(4)] public long ByteLength { get; set; } + [Key(5)] public long Generation { get; set; } + } + + [MessagePackObject] + public sealed class EveEntitySoaColumn + { + [Key(0)] public string ColumnId { get; set; } = ""; + [Key(1)] public string Semantic { get; set; } = ""; + [Key(2)] public string BufferId { get; set; } = ""; + [Key(3)] public string ScalarType { get; set; } = ""; + [Key(4)] public long ByteOffset { get; set; } + [Key(5)] public int ElementStride { get; set; } + [Key(6)] public int ElementCount { get; set; } + [Key(7)] public string Unit { get; set; } = ""; + [Key(8)] public string CoordinateSpace { get; set; } = ""; + } + + [MessagePackObject] + public sealed class EveEntitySoaDirtyRange + { + [Key(0)] public string ColumnId { get; set; } = ""; + [Key(1)] public int StartIndex { get; set; } + [Key(2)] public int Count { get; set; } + [Key(3)] public long Generation { get; set; } + } + + [MessagePackObject] + public sealed class EveEntityRenderGroup + { + [Key(0)] public int GroupId { get; set; } + [Key(1)] public string MeshAssetRef { get; set; } = ""; + [Key(2)] public string MaterialAssetRef { get; set; } = ""; + [Key(3)] public int SubMeshIndex { get; set; } + [Key(4)] public int Layer { get; set; } + [Key(5)] public int InstanceCount { get; set; } + [Key(6)] public float DefaultScale { get; set; } = 1f; + [Key(7)] public float BoundsCenterX { get; set; } + [Key(8)] public float BoundsCenterY { get; set; } + [Key(9)] public float BoundsCenterZ { get; set; } + [Key(10)] public float BoundsSizeX { get; set; } + [Key(11)] public float BoundsSizeY { get; set; } + [Key(12)] public float BoundsSizeZ { get; set; } + [Key(13)] public string ShadowMode { get; set; } = "on"; + [Key(14)] public bool ReceiveShadows { get; set; } = true; + [Key(15)] public int Lod { get; set; } = -1; + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityRendererFeature.cs.meta b/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs.meta similarity index 83% rename from packages/org.gamecult.eve.unity-scene/Runtime/EveUnityRendererFeature.cs.meta rename to packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs.meta index 82fdbfc..b2882e9 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityRendererFeature.cs.meta +++ b/packages/org.gamecult.eve.surface/Runtime/EveEntitySoaViewDocument.cs.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 340385338d304b5aaf069562d4e7b457 +guid: 8e87e1be783846508efea45787f2d45c MonoImporter: externalObjects: {} serializedVersion: 2 diff --git a/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs b/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs new file mode 100644 index 0000000..356edee --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using GameCult.Caching; +using MessagePack; + +namespace GameCult.Eve.Surface +{ + [CultDocument("gamecult.eve.input_capability", SchemaId)] + [MessagePackObject] + public sealed class EveInputCapabilityDocument + { + public const string SchemaId = "gamecult.eve.input_capability.v1"; + [Key(0)] public string Schema { get; set; } = SchemaId; + [Key(1)] public string ProviderId { get; set; } = ""; + [Key(2)] public string CapabilityId { get; set; } = ""; + [Key(3)] public long Version { get; set; } + [Key(4)] public EveInputActionDocument[] Actions { get; set; } = Array.Empty(); + [Key(5)] public EveInputProfileDocument[] DefaultProfiles { get; set; } = Array.Empty(); + } + + [MessagePackObject] + public sealed class EveInputActionDocument + { + [Key(0)] public string ActionId { get; set; } = ""; + [Key(1)] public string Label { get; set; } = ""; + [Key(2)] public string Operation { get; set; } = ""; + [Key(3)] public string Context { get; set; } = "pilot"; + [Key(4)] public string Category { get; set; } = "ship"; + [Key(5)] public string Availability { get; set; } = "available"; + [Key(6)] public string SourceRef { get; set; } = ""; + [Key(7)] public Dictionary Payload { get; set; } = new Dictionary(StringComparer.Ordinal); + } + + [MessagePackObject] + public sealed class EveInputProfileDocument + { + [Key(0)] public string ProfileId { get; set; } = ""; + [Key(1)] public string DeviceClass { get; set; } = ""; + [Key(2)] public EveInputBindingDocument[] Bindings { get; set; } = Array.Empty(); + } + + [MessagePackObject] + public sealed class EveInputBindingDocument + { + [Key(0)] public string BindingId { get; set; } = ""; + [Key(1)] public string ActionId { get; set; } = ""; + [Key(2)] public EveInputGestureDocument Gesture { get; set; } = new EveInputGestureDocument(); + [Key(3)] public bool ActionBar { get; set; } + } + + [MessagePackObject] + public sealed class EveInputGestureDocument + { + [Key(0)] public string Kind { get; set; } = "direct"; + [Key(1)] public string[] Controls { get; set; } = Array.Empty(); + [Key(2)] public int MaxStepIntervalMs { get; set; } = 650; + [Key(3)] public string CompletionControl { get; set; } = ""; + } +} diff --git a/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs.meta b/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs.meta new file mode 100644 index 0000000..9a01871 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveInputCapabilityDocument.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4c297c7dbb64a0f49a7f805e770c5e55 \ No newline at end of file diff --git a/packages/org.gamecult.eve.surface/Runtime/EveInputDeviceLayouts.cs b/packages/org.gamecult.eve.surface/Runtime/EveInputDeviceLayouts.cs new file mode 100644 index 0000000..35425a3 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveInputDeviceLayouts.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; + +namespace GameCult.Eve.Surface +{ + public sealed class EveInputDeviceLayout + { + public string LayoutId { get; set; } = ""; + public string DeviceClass { get; set; } = ""; + public IReadOnlyList Controls { get; set; } = Array.Empty(); + public bool SupportsDirectionalStrings { get; set; } + + public static EveInputDeviceLayout AnsiKeyboardMouse() => new EveInputDeviceLayout + { + LayoutId = "keyboard.ansi-104+mouse.standard", + DeviceClass = "keyboard-mouse", + Controls = new[] + { + Key("keyboard.escape", "Esc", 0, 0, 1.25f), Key("keyboard.f1", "F1", 2, 0), Key("keyboard.f2", "F2", 3, 0), + Key("keyboard.1", "1", 1, 1), Key("keyboard.2", "2", 2, 1), Key("keyboard.3", "3", 3, 1), Key("keyboard.4", "4", 4, 1), Key("keyboard.5", "5", 5, 1), + Key("keyboard.tab", "Tab", 0, 2, 1.5f), Key("keyboard.q", "Q", 1.5f, 2), Key("keyboard.w", "W", 2.5f, 2), Key("keyboard.e", "E", 3.5f, 2), Key("keyboard.r", "R", 4.5f, 2), + Key("keyboard.a", "A", 1.75f, 3), Key("keyboard.s", "S", 2.75f, 3), Key("keyboard.d", "D", 3.75f, 3), Key("keyboard.f", "F", 4.75f, 3), + Key("keyboard.leftShift", "Shift", 0, 4, 2.25f), Key("keyboard.space", "Space", 3.75f, 5, 6.25f), + Key("mouse.primary", "Mouse 1", 18, 1), Key("mouse.secondary", "Mouse 2", 19, 1), Key("mouse.middle", "Mouse 3", 18.5f, 2) + } + }; + + public static EveInputDeviceLayout StandardGamepad() => new EveInputDeviceLayout + { + LayoutId = "gamepad.standard", + DeviceClass = "gamepad", + SupportsDirectionalStrings = true, + Controls = new[] + { + Key("gamepad.leftStick", "Left Stick", 2, 3, 2), Key("gamepad.rightStick", "Right Stick", 8, 4, 2), + Key("gamepad.dpad.up", "Up", 1, 1), Key("gamepad.dpad.left", "Left", 0, 2), Key("gamepad.dpad.right", "Right", 2, 2), Key("gamepad.dpad.down", "Down", 1, 3), + Key("gamepad.buttonSouth", "A / Cross", 10, 3), Key("gamepad.buttonEast", "B / Circle", 11, 2), Key("gamepad.buttonWest", "X / Square", 9, 2), Key("gamepad.buttonNorth", "Y / Triangle", 10, 1), + Key("gamepad.leftTrigger", "LT / L2", 2, 0, 2), Key("gamepad.rightTrigger", "RT / R2", 9, 0, 2) + } + }; + + private static EveInputLayoutControl Key(string id, string label, float x, float y, float width = 1) => + new EveInputLayoutControl { Control = id, Label = label, X = x, Y = y, Width = width, Height = 1 }; + } + + public sealed class EveInputLayoutControl + { + public string Control { get; set; } = ""; + public string Label { get; set; } = ""; + public float X { get; set; } + public float Y { get; set; } + public float Width { get; set; } = 1; + public float Height { get; set; } = 1; + } +} diff --git a/packages/org.gamecult.eve.surface/Runtime/EveInputDeviceLayouts.cs.meta b/packages/org.gamecult.eve.surface/Runtime/EveInputDeviceLayouts.cs.meta new file mode 100644 index 0000000..0d7a9a2 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveInputDeviceLayouts.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8a2a1258551e42a9831a3e4f9483d928 diff --git a/packages/org.gamecult.eve.surface/Runtime/EveInputGestures.cs b/packages/org.gamecult.eve.surface/Runtime/EveInputGestures.cs new file mode 100644 index 0000000..a318066 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveInputGestures.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace GameCult.Eve.Surface +{ + public sealed class EveInputGesture + { + public string Kind { get; set; } = "direct"; + public IReadOnlyList Controls { get; set; } = Array.Empty(); + public int MaxStepIntervalMs { get; set; } = 650; + public string CompletionControl { get; set; } = ""; + } + + public sealed class EveInputBinding + { + public string BindingId { get; set; } = ""; + public string ActionId { get; set; } = ""; + public EveInputGesture Gesture { get; set; } = new EveInputGesture(); + public bool ActionBar { get; set; } + } + + public sealed class EveInputGestureResolver + { + private readonly IReadOnlyList _bindings; + private readonly HashSet _pressed = new HashSet(StringComparer.Ordinal); + private readonly Dictionary _sequences = new Dictionary(StringComparer.Ordinal); + + public EveInputGestureResolver(IEnumerable bindings) + { + _bindings = (bindings ?? Array.Empty()).Where(binding => binding != null).ToArray(); + } + + public IReadOnlyList Press(string control, long timestampMs) + { + _pressed.Add(control ?? ""); + var actions = new HashSet(StringComparer.Ordinal); + foreach (var binding in _bindings) + { + var gesture = binding.Gesture ?? new EveInputGesture(); + if (gesture.Kind == "direct" && gesture.Controls.FirstOrDefault() == control) + actions.Add(binding.ActionId); + else if (gesture.Kind == "chord" && gesture.Controls.Count > 0 && gesture.Controls.All(_pressed.Contains)) + actions.Add(binding.ActionId); + else if (gesture.Kind == "sequence" && Advance(binding, control ?? "", timestampMs)) + actions.Add(binding.ActionId); + } + return actions.ToArray(); + } + + public void Release(string control) => _pressed.Remove(control ?? ""); + + public void Reset() + { + _pressed.Clear(); + _sequences.Clear(); + } + + private bool Advance(EveInputBinding binding, string control, long timestampMs) + { + var controls = binding.Gesture.Controls; + if (controls.Count == 0) return false; + _sequences.TryGetValue(binding.BindingId, out var previous); + var index = previous.Index > 0 && timestampMs - previous.TimestampMs <= Math.Max(50, binding.Gesture.MaxStepIntervalMs) ? previous.Index : 0; + var next = controls[index] == control ? index + 1 : controls[0] == control ? 1 : 0; + if (next == controls.Count) + { + _sequences.Remove(binding.BindingId); + return string.IsNullOrWhiteSpace(binding.Gesture.CompletionControl); + } + _sequences[binding.BindingId] = new SequenceState(next, timestampMs); + return false; + } + + private readonly struct SequenceState + { + public SequenceState(int index, long timestampMs) { Index = index; TimestampMs = timestampMs; } + public int Index { get; } + public long TimestampMs { get; } + } + } +} diff --git a/packages/org.gamecult.eve.surface/Runtime/EveInputGestures.cs.meta b/packages/org.gamecult.eve.surface/Runtime/EveInputGestures.cs.meta new file mode 100644 index 0000000..3abaaab --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveInputGestures.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e3c2941be243a754092ac2543d1e6bf7 \ No newline at end of file diff --git a/packages/org.gamecult.eve.surface/Runtime/EvePluginAdvertisementDocument.cs b/packages/org.gamecult.eve.surface/Runtime/EvePluginAdvertisementDocument.cs new file mode 100644 index 0000000..d213976 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EvePluginAdvertisementDocument.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using GameCult.Caching; +using MessagePack; + +#nullable enable + +namespace GameCult.Eve.Surface +{ + [CultDocument("gamecult.eve.plugin_advertisement", SchemaId)] + [MessagePackObject] + public sealed class EvePluginAdvertisementDocument + { + public const string SchemaId = "gamecult.eve.plugin_advertisement.v1"; + + public EvePluginAdvertisementDocument( + string pluginId, + string ownerService, + string version, + string manifestAddress, + EvePluginRuntimeAdvertisement runtime, + IReadOnlyList schemas, + IReadOnlyList componentKinds, + IReadOnlyList commands, + IReadOnlyList fixtures) + : this(SchemaId, pluginId, ownerService, version, manifestAddress, runtime, schemas, componentKinds, commands, fixtures) + { + } + + [SerializationConstructor] + public EvePluginAdvertisementDocument( + string schema, + string pluginId, + string ownerService, + string version, + string manifestAddress, + EvePluginRuntimeAdvertisement runtime, + IReadOnlyList schemas, + IReadOnlyList componentKinds, + IReadOnlyList commands, + IReadOnlyList fixtures) + { + Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema; + PluginId = pluginId ?? ""; + OwnerService = ownerService ?? ""; + Version = version ?? ""; + ManifestAddress = manifestAddress ?? ""; + Runtime = runtime ?? new EvePluginRuntimeAdvertisement("", "", Array.Empty(), Array.Empty(), null); + Schemas = schemas ?? Array.Empty(); + ComponentKinds = componentKinds ?? Array.Empty(); + Commands = commands ?? Array.Empty(); + Fixtures = fixtures ?? Array.Empty(); + } + + [Key(0)] public string Schema { get; } + [Key(1), CultName] public string PluginId { get; } + [Key(2)] public string OwnerService { get; } + [Key(3)] public string Version { get; } + [Key(4)] public string ManifestAddress { get; } + [Key(5)] public EvePluginRuntimeAdvertisement Runtime { get; } + [Key(6)] public IReadOnlyList Schemas { get; } + [Key(7)] public IReadOnlyList ComponentKinds { get; } + [Key(8)] public IReadOnlyList Commands { get; } + [Key(9)] public IReadOnlyList Fixtures { get; } + } + + [MessagePackObject] + public sealed class EvePluginRuntimeAdvertisement + { + [SerializationConstructor] + public EvePluginRuntimeAdvertisement( + string invocationModel, + string contract, + IReadOnlyList transports, + IReadOnlyList authority, + EvePluginSidecarAdvertisement? sidecar) + { + InvocationModel = invocationModel ?? ""; + Contract = contract ?? ""; + Transports = transports ?? Array.Empty(); + Authority = authority ?? Array.Empty(); + Sidecar = sidecar; + } + + [Key(0)] public string InvocationModel { get; } + [Key(1)] public string Contract { get; } + [Key(2)] public IReadOnlyList Transports { get; } + [Key(3)] public IReadOnlyList Authority { get; } + [Key(4)] public EvePluginSidecarAdvertisement? Sidecar { get; } + } + + [MessagePackObject] + public sealed class EvePluginSidecarAdvertisement + { + [SerializationConstructor] + public EvePluginSidecarAdvertisement( + string processKind, + string protocol, + string requestSchema, + string responseSchema, + IReadOnlyList operations, + string commandEnvelope, + string receiptSchema, + string stateAuthority) + { + ProcessKind = processKind ?? ""; + Protocol = protocol ?? ""; + RequestSchema = requestSchema ?? ""; + ResponseSchema = responseSchema ?? ""; + Operations = operations ?? Array.Empty(); + CommandEnvelope = commandEnvelope ?? ""; + ReceiptSchema = receiptSchema ?? ""; + StateAuthority = stateAuthority ?? ""; + } + + [Key(0)] public string ProcessKind { get; } + [Key(1)] public string Protocol { get; } + [Key(2)] public string RequestSchema { get; } + [Key(3)] public string ResponseSchema { get; } + [Key(4)] public IReadOnlyList Operations { get; } + [Key(5)] public string CommandEnvelope { get; } + [Key(6)] public string ReceiptSchema { get; } + [Key(7)] public string StateAuthority { get; } + } +} diff --git a/packages/org.gamecult.eve.surface/Runtime/EvePluginAdvertisementDocument.cs.meta b/packages/org.gamecult.eve.surface/Runtime/EvePluginAdvertisementDocument.cs.meta new file mode 100644 index 0000000..69b1c21 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EvePluginAdvertisementDocument.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7a5b91c7defe47d2a38940a460aa59c5 diff --git a/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs b/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs new file mode 100644 index 0000000..ec8e37f --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs @@ -0,0 +1,289 @@ +using System; +using System.Collections.Generic; +using GameCult.Caching; +using MessagePack; + +#nullable enable + +namespace GameCult.Eve.Surface +{ + [CultDocument("gamecult.eve.provider_advertisement", SchemaId)] + [MessagePackObject] + public sealed class EveProviderAdvertisementDocument + { + public const string SchemaId = "gamecult.eve.provider_advertisement.v1"; + + public EveProviderAdvertisementDocument( + string providerId, + string serviceId, + string verseId, + string title, + string kind, + string cultMeshAddress, + string updatedAtUtc, + EveProviderFreshness freshness, + IReadOnlyList schemas, + IReadOnlyList witnesses, + IReadOnlyList surfaces, + IReadOnlyList commands) + : this(SchemaId, providerId, serviceId, verseId, title, kind, cultMeshAddress, updatedAtUtc, freshness, schemas, witnesses, surfaces, commands) + { + } + + [SerializationConstructor] + public EveProviderAdvertisementDocument( + string schema, + string providerId, + string serviceId, + string verseId, + string title, + string kind, + string cultMeshAddress, + string updatedAtUtc, + EveProviderFreshness freshness, + IReadOnlyList schemas, + IReadOnlyList witnesses, + IReadOnlyList surfaces, + IReadOnlyList commands) + { + Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema; + ProviderId = providerId ?? ""; + ServiceId = serviceId ?? ""; + VerseId = verseId ?? ""; + Title = title ?? ""; + Kind = kind ?? ""; + CultMeshAddress = cultMeshAddress ?? ""; + UpdatedAtUtc = updatedAtUtc ?? ""; + Freshness = freshness ?? new EveProviderFreshness("unknown", "", 0); + Schemas = schemas ?? Array.Empty(); + Witnesses = witnesses ?? Array.Empty(); + Surfaces = surfaces ?? Array.Empty(); + Commands = commands ?? Array.Empty(); + } + + [Key(0)] public string Schema { get; } + [Key(1)] public string ProviderId { get; } + [Key(2)] public string ServiceId { get; } + [Key(3)] public string VerseId { get; } + [Key(4)] public string Title { get; } + [Key(5)] public string Kind { get; } + [Key(6)] public string CultMeshAddress { get; } + [Key(7)] public string UpdatedAtUtc { get; } + [Key(8)] public EveProviderFreshness Freshness { get; } + [Key(9)] public IReadOnlyList Schemas { get; } + [Key(10)] public IReadOnlyList Witnesses { get; } + [Key(11)] public IReadOnlyList Surfaces { get; } + [Key(12)] public IReadOnlyList Commands { get; } + } + + [MessagePackObject] + public sealed class EveProviderFreshness + { + [SerializationConstructor] + public EveProviderFreshness(string state, string lastSeenAtUtc, long maxAgeMs) + { + State = state ?? ""; + LastSeenAtUtc = lastSeenAtUtc ?? ""; + MaxAgeMs = maxAgeMs; + } + + [Key(0)] public string State { get; } + [Key(1)] public string LastSeenAtUtc { get; } + [Key(2)] public long MaxAgeMs { get; } + } + + [MessagePackObject] + public sealed class EveProviderWitness + { + [SerializationConstructor] + public EveProviderWitness(string kind, string reference, string summary) + { + Kind = kind ?? ""; + Reference = reference ?? ""; + Summary = summary ?? ""; + } + + [Key(0)] public string Kind { get; } + [Key(1)] public string Reference { get; } + [Key(2)] public string Summary { get; } + } + + [MessagePackObject] + public sealed class EveAdvertisedSurface + { + [SerializationConstructor] + public EveAdvertisedSurface( + string surfaceId, + string schema, + string recordRef, + string transport, + string status, + string surfaceKind, + EveWorldInteractionAdvertisement? worldInteraction = null, + IReadOnlyList? requiresPlugins = null) + { + SurfaceId = surfaceId ?? ""; + Schema = schema ?? ""; + RecordRef = recordRef ?? ""; + Transport = transport ?? ""; + Status = status ?? ""; + SurfaceKind = surfaceKind ?? ""; + WorldInteraction = worldInteraction; + RequiresPlugins = requiresPlugins ?? Array.Empty(); + } + + [Key(0)] public string SurfaceId { get; } + [Key(1)] public string Schema { get; } + [Key(2)] public string RecordRef { get; } + [Key(3)] public string Transport { get; } + [Key(4)] public string Status { get; } + [Key(5)] public string SurfaceKind { get; } + [Key(6)] public EveWorldInteractionAdvertisement? WorldInteraction { get; } + [Key(7)] public IReadOnlyList RequiresPlugins { get; } + } + + [MessagePackObject] + public sealed class EvePluginRequirement + { + [SerializationConstructor] + public EvePluginRequirement( + string pluginId, + string versionRange, + string availability, + IReadOnlyList requiredCapabilities, + IReadOnlyList optionalCapabilities) + { + PluginId = pluginId ?? ""; + VersionRange = versionRange ?? ""; + Availability = string.IsNullOrWhiteSpace(availability) ? "required" : availability; + RequiredCapabilities = requiredCapabilities ?? Array.Empty(); + OptionalCapabilities = optionalCapabilities ?? Array.Empty(); + } + + [Key(0)] public string PluginId { get; } + [Key(1)] public string VersionRange { get; } + [Key(2)] public string Availability { get; } + [Key(3)] public IReadOnlyList RequiredCapabilities { get; } + [Key(4)] public IReadOnlyList OptionalCapabilities { get; } + } + + [MessagePackObject] + public sealed class EveWorldInteractionAdvertisement + { + [SerializationConstructor] + public EveWorldInteractionAdvertisement( + string projectionKind, + IReadOnlyList stateSchemas, + string commandBoundary, + string commandRecordRef, + string receiptSchema, + string receiptRecordRef, + string assetManifestRecordRef, + IReadOnlyList loweringTargets, + string ownership) + { + ProjectionKind = projectionKind ?? ""; + StateSchemas = stateSchemas ?? Array.Empty(); + CommandBoundary = commandBoundary ?? ""; + CommandRecordRef = commandRecordRef ?? ""; + ReceiptSchema = receiptSchema ?? ""; + ReceiptRecordRef = receiptRecordRef ?? ""; + AssetManifestRecordRef = assetManifestRecordRef ?? ""; + LoweringTargets = loweringTargets ?? Array.Empty(); + Ownership = ownership ?? ""; + } + + [Key(0)] public string ProjectionKind { get; } + [Key(1)] public IReadOnlyList StateSchemas { get; } + [Key(2)] public string CommandBoundary { get; } + [Key(3)] public string CommandRecordRef { get; } + [Key(4)] public string ReceiptSchema { get; } + [Key(5)] public string ReceiptRecordRef { get; } + [Key(6)] public string AssetManifestRecordRef { get; } + [Key(7)] public IReadOnlyList LoweringTargets { get; } + [Key(8)] public string Ownership { get; } + } + + [MessagePackObject] + public sealed class EveAdvertisedCommand + { + [SerializationConstructor] + public EveAdvertisedCommand(string command, string surfaceId, string transport, string summary) + { + Command = command ?? ""; + SurfaceId = surfaceId ?? ""; + Transport = transport ?? ""; + Summary = summary ?? ""; + } + + [Key(0)] public string Command { get; } + [Key(1)] public string SurfaceId { get; } + [Key(2)] public string Transport { get; } + [Key(3)] public string Summary { get; } + } + + [CultDocument("gamecult.eve.command_receipt", SchemaId)] + [MessagePackObject] + public sealed class EveCommandReceiptDocument + { + public const string SchemaId = "gamecult.eve.command_receipt.v1"; + + public EveCommandReceiptDocument( + string receiptId, + string commandId, + string command, + string state, + string ownerRepo, + string authority, + string providerId, + string surfaceId, + string message, + string issuedAtUtc, + long sourceVersion) + : this(SchemaId, receiptId, commandId, command, state, ownerRepo, authority, providerId, surfaceId, message, issuedAtUtc, sourceVersion) + { + } + + [SerializationConstructor] + public EveCommandReceiptDocument( + string schema, + string receiptId, + string commandId, + string command, + string state, + string ownerRepo, + string authority, + string providerId, + string surfaceId, + string message, + string issuedAtUtc, + long sourceVersion) + { + Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema; + ReceiptId = receiptId ?? ""; + CommandId = commandId ?? ""; + Command = command ?? ""; + State = state ?? ""; + OwnerRepo = ownerRepo ?? ""; + Authority = authority ?? ""; + ProviderId = providerId ?? ""; + SurfaceId = surfaceId ?? ""; + Message = message ?? ""; + IssuedAtUtc = issuedAtUtc ?? ""; + SourceVersion = sourceVersion; + } + + [Key(0)] public string Schema { get; } + [Key(1)] public string ReceiptId { get; } + [Key(2)] public string CommandId { get; } + [Key(3)] public string Command { get; } + [Key(4)] public string State { get; } + [Key(5)] public string OwnerRepo { get; } + [Key(6)] public string Authority { get; } + [Key(7)] public string ProviderId { get; } + [Key(8)] public string SurfaceId { get; } + [Key(9)] public string Message { get; } + [Key(10)] public string IssuedAtUtc { get; } + [Key(11)] public long SourceVersion { get; } + } +} diff --git a/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs.meta b/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs.meta new file mode 100644 index 0000000..f29c875 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveProviderDocuments.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f7ea0cd07c4d5da4099b5acf8d44ca2e \ No newline at end of file diff --git a/packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs new file mode 100644 index 0000000..2d88eb5 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs @@ -0,0 +1,874 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using GameCult.Mesh; + +#nullable enable + +namespace GameCult.Eve.Surface +{ + /// + /// Fluent CultUI/Eve surface builder. Eve owns the surface document; CultMesh only supplies typed binding descriptors. + /// + public sealed class EveSurfaceBuilder + { + private readonly string _surfaceId; + private readonly List _children = new(); + private readonly List _styles = new(); + private readonly List _commands = new(); + private EveSurfaceComponent? _root; + private long _version = 1; + private string _providerId = "gamecult"; + private string _providerKind = "cultui.surface"; + private string _title = ""; + private string _updatedAtUtc = ""; + + public EveSurfaceBuilder(string surfaceId) + { + _surfaceId = RequireNonEmpty(surfaceId, nameof(surfaceId)); + } + + public EveSurfaceBuilder Provider(string providerId, string providerKind) + { + _providerId = providerId ?? ""; + _providerKind = providerKind ?? ""; + return this; + } + + public EveSurfaceBuilder Title(string title) + { + _title = title ?? ""; + _children.Add(Text($"{_surfaceId}.title", _title, "text.title")); + return this; + } + + public EveSurfaceBuilder TitleSubtitle(string title, string subtitle) + { + _title = string.IsNullOrWhiteSpace(subtitle) ? title ?? "" : $"{title} {subtitle}"; + _children.Add(Text($"{_surfaceId}.title", title ?? "", "text.title")); + _children.Add(Text($"{_surfaceId}.subtitle", subtitle ?? "", "text.subtitle")); + return this; + } + + public EveSurfaceBuilder Text(string text, string? id = null) + { + _children.Add(Text(id ?? $"{_surfaceId}.text.{_children.Count}", text, "text")); + return this; + } + + public EveSurfaceBuilder Button(string label, string command) + { + var operation = CultMesh.OperationBinding(command, label); + _commands.Add(new EveCommandTemplate(operation)); + _children.Add(ButtonComponent($"{_surfaceId}.button.{Slug(label)}", label, operation)); + return this; + } + + public EveSurfaceBuilder Button(string label, CultMeshOperationBindingDescriptor operation) + { + if (operation == null) throw new ArgumentNullException(nameof(operation)); + _commands.Add(new EveCommandTemplate(operation)); + _children.Add(ButtonComponent($"{_surfaceId}.button.{Slug(label)}", label, operation)); + return this; + } + + public EveSurfaceBuilder ButtonColumn(string id, Action build) + { + return Group(id, "column", build); + } + + public EveSurfaceBuilder ButtonRow(string id, Action build) + { + return Group(id, "row", build); + } + + public EveSurfaceBuilder Form(string id, Action build) + { + if (build == null) throw new ArgumentNullException(nameof(build)); + var form = new EveSurfaceFormBuilder(id, AddCommand); + build(form); + _children.Add(form.Build()); + return this; + } + + public EveSurfaceBuilder RootColumn(string id, Action build) + { + _root = BuildContainer(id, "partition", Props(("split", "y")), build); + return this; + } + + public EveSurfaceBuilder RootRow(string id, Action build) + { + _root = BuildContainer(id, "partition", Props(("split", "x")), build); + return this; + } + + public EveSurfaceBuilder RootGrid( + string id, + int columns, + int rows, + Action build) + { + _root = BuildContainer( + id, + "grid", + Props( + ("columns", columns.ToString(System.Globalization.CultureInfo.InvariantCulture)), + ("rows", rows.ToString(System.Globalization.CultureInfo.InvariantCulture))), + build); + return this; + } + + public EveSurfaceBuilder Row(string id, Action build) + { + _children.Add(BuildContainer(id, "partition", Props(("split", "x")), build)); + return this; + } + + public EveSurfaceBuilder Column(string id, Action build) + { + _children.Add(BuildContainer(id, "partition", Props(("split", "y")), build)); + return this; + } + + public EveSurfaceBuilder Grid(string id, int columns, int rows, Action build) + { + _children.Add(BuildContainer( + id, + "grid", + Props( + ("columns", columns.ToString(System.Globalization.CultureInfo.InvariantCulture)), + ("rows", rows.ToString(System.Globalization.CultureInfo.InvariantCulture))), + build)); + return this; + } + + public EveSurfaceBuilder Modal(string id, string title, Action build) + { + _children.Add(BuildContainer(id, "modal", Props(("title", title ?? "")), build)); + return this; + } + + public EveSurfaceBuilder EmbeddedDocument( + string slotId, + string documentId, + string schemaId, + string presentationKind, + CultMeshRouteHint? routeHint = null) + { + _children.Add(new EveSurfaceComponent( + $"{_surfaceId}.slot.{Slug(slotId)}", + "surface.slot", + new Dictionary(StringComparer.Ordinal) + { + ["slotId"] = slotId ?? "", + ["documentId"] = documentId ?? "", + ["schemaId"] = schemaId ?? "", + ["presentationKind"] = presentationKind ?? "" + }, + Array.Empty(), + Array.Empty(), + new[] + { + new EveEmbeddedDocumentSlot( + slotId ?? "", + documentId ?? "", + schemaId ?? "", + presentationKind ?? "", + routeHint) + })); + return this; + } + + public EveSurfaceBuilder Style(string name, string value) + { + _styles.Add(new EveStyleToken(name, value)); + return this; + } + + public EveSurfaceBuilder Version(long version) + { + _version = version; + return this; + } + + public EveSurfaceBuilder UpdatedAtUtc(string updatedAtUtc) + { + _updatedAtUtc = updatedAtUtc ?? ""; + return this; + } + + public EveSurfaceDocument Build() + { + return new EveSurfaceDocument( + _providerId, + _providerKind, + _title, + _version, + string.IsNullOrWhiteSpace(_updatedAtUtc) ? DateTime.UtcNow.ToString("O") : _updatedAtUtc, + new EveSurfaceTree( + _surfaceId, + _root ?? new EveSurfaceComponent( + $"{_surfaceId}.root", + "surface", + EmptyProps(), + _children.ToArray()), + _styles.ToArray()), + _commands.ToArray()); + } + + private EveSurfaceBuilder Group(string id, string kind, Action build) + { + if (build == null) throw new ArgumentNullException(nameof(build)); + var group = new EveSurfaceGroupBuilder(id, kind, AddCommand); + build(group); + _children.Add(group.Build()); + return this; + } + + private EveSurfaceComponent BuildContainer( + string id, + string kind, + IReadOnlyDictionary props, + Action build) + { + if (build == null) throw new ArgumentNullException(nameof(build)); + var container = new EveSurfaceContainerBuilder(id, kind, props, AddCommand); + build(container); + return container.Build(); + } + + private void AddCommand(EveCommandTemplate command) + { + if (command != null) + _commands.Add(command); + } + + internal static EveSurfaceComponent Text(string id, string value, string kind) + { + return new EveSurfaceComponent( + id, + kind, + new Dictionary(StringComparer.Ordinal) + { + ["value"] = value ?? "" + }, + Array.Empty()); + } + + internal static EveSurfaceComponent ButtonComponent( + string id, + string label, + CultMeshOperationBindingDescriptor operation) + { + return new EveSurfaceComponent( + id, + "control.button", + new Dictionary(StringComparer.Ordinal) + { + ["label"] = label ?? operation.Label, + ["command"] = operation.OperationId, + ["operationId"] = operation.OperationId, + ["schemaId"] = operation.SchemaId + }, + Array.Empty()); + } + + internal static Dictionary EmptyProps() + { + return new Dictionary(StringComparer.Ordinal); + } + + internal static Dictionary Props(params (string Key, string Value)[] values) + { + return (values ?? Array.Empty<(string Key, string Value)>()) + .ToDictionary(value => value.Key, value => value.Value ?? "", StringComparer.Ordinal); + } + + internal static string Slug(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return "unnamed"; + + var chars = value + .Select(character => char.IsLetterOrDigit(character) ? char.ToLowerInvariant(character) : '.') + .ToArray(); + var slug = new string(chars).Trim('.'); + while (slug.Contains("..", StringComparison.Ordinal)) + slug = slug.Replace("..", ".", StringComparison.Ordinal); + return string.IsNullOrWhiteSpace(slug) ? "unnamed" : slug; + } + + private static string RequireNonEmpty(string value, string paramName) + { + return string.IsNullOrWhiteSpace(value) + ? throw new ArgumentException("Value must be non-empty.", paramName) + : value; + } + } + + public sealed class EveSurfaceContainerBuilder + { + private readonly string _id; + private readonly string _kind; + private readonly Dictionary _props; + private readonly Action _addCommand; + private readonly List _children = new(); + private readonly List _stateBindings = new(); + private readonly List _embeddedDocuments = new(); + + internal EveSurfaceContainerBuilder( + string id, + string kind, + IReadOnlyDictionary props, + Action addCommand) + { + _id = id ?? ""; + _kind = kind ?? "partition"; + _props = new Dictionary(props ?? EveSurfaceBuilder.EmptyProps(), StringComparer.Ordinal); + _addCommand = addCommand ?? throw new ArgumentNullException(nameof(addCommand)); + } + + public EveSurfaceContainerBuilder Gap(int value) => Prop("gap", value); + + public EveSurfaceContainerBuilder Padding(int value) => Prop("padding", value); + + public EveSurfaceContainerBuilder Size(string value) => Prop("size", value); + + public EveSurfaceContainerBuilder Min(string value) => Prop("min", value); + + public EveSurfaceContainerBuilder Max(string value) => Prop("max", value); + + public EveSurfaceContainerBuilder Align(string value) => Prop("align", value); + + public EveSurfaceContainerBuilder Clip(string value = "hidden") => Prop("clip", value); + + public EveSurfaceContainerBuilder Scroll(string value = "y") => Prop("scroll", value); + + public EveSurfaceContainerBuilder Region(string value) => Prop("region", value); + + public EveSurfaceContainerBuilder Bind(CultMeshStateBindingDescriptor binding) + { + if (binding != null) + _stateBindings.Add(binding); + return this; + } + + public EveSurfaceContainerBuilder Row(string id, Action build) + { + _children.Add(BuildContainer(id, "partition", EveSurfaceBuilder.Props(("split", "x")), build)); + return this; + } + + public EveSurfaceContainerBuilder Column(string id, Action build) + { + _children.Add(BuildContainer(id, "partition", EveSurfaceBuilder.Props(("split", "y")), build)); + return this; + } + + public EveSurfaceContainerBuilder Grid( + string id, + int columns, + int rows, + Action build) + { + _children.Add(BuildContainer( + id, + "grid", + EveSurfaceBuilder.Props( + ("columns", columns.ToString(System.Globalization.CultureInfo.InvariantCulture)), + ("rows", rows.ToString(System.Globalization.CultureInfo.InvariantCulture))), + build)); + return this; + } + + public EveSurfaceContainerBuilder Pane(string id, string title, Action build) + { + _children.Add(BuildContainer(id, "pane", EveSurfaceBuilder.Props(("title", title ?? "")), build)); + return this; + } + + public EveSurfaceContainerBuilder Card(string id, string title, Action build) + { + _children.Add(BuildContainer(id, "card", EveSurfaceBuilder.Props(("title", title ?? "")), build)); + return this; + } + + public EveSurfaceContainerBuilder Modal(string id, string title, Action build) + { + _children.Add(BuildContainer(id, "modal", EveSurfaceBuilder.Props(("title", title ?? "")), build)); + return this; + } + + public EveSurfaceContainerBuilder FieldRow( + string id, + string label, + Action build) + { + return Row(id, row => row + .Gap(8) + .Label($"{id}.label", label, labelPart => labelPart.Size("12rem")) + .Column($"{id}.field", field => + { + field.Size("1fr"); + build(field); + })); + } + + public EveSurfaceContainerBuilder Label( + string id, + string text, + Action? configure = null) + { + return Leaf(id, "label", EveSurfaceBuilder.Props(("value", text ?? "")), null, null, configure); + } + + public EveSurfaceContainerBuilder Text(string id, string text) + { + return Leaf(id, "text", EveSurfaceBuilder.Props(("value", text ?? ""))); + } + + public EveSurfaceContainerBuilder Title(string id, string text) + { + return Leaf(id, "text.title", EveSurfaceBuilder.Props(("value", text ?? ""))); + } + + public EveSurfaceContainerBuilder Metric( + string id, + string label, + string value, + CultMeshStateBindingDescriptor? binding = null) + { + return Leaf( + id, + "metric", + EveSurfaceBuilder.Props(("label", label ?? ""), ("value", value ?? "")), + binding); + } + + public EveSurfaceContainerBuilder Progress( + string id, + string label, + string value, + string min, + string max, + CultMeshStateBindingDescriptor? binding = null) + { + return Leaf( + id, + "progress", + EveSurfaceBuilder.Props( + ("label", label ?? ""), + ("value", value ?? ""), + ("min", min ?? ""), + ("max", max ?? "")), + binding); + } + + public EveSurfaceContainerBuilder Button(string id, string label, string command) + { + return Button(id, label, CultMesh.OperationBinding(command, label)); + } + + public EveSurfaceContainerBuilder Button( + string id, + string label, + CultMeshOperationBindingDescriptor operation) + { + if (operation == null) throw new ArgumentNullException(nameof(operation)); + _addCommand(new EveCommandTemplate(operation)); + _children.Add(EveSurfaceBuilder.ButtonComponent(id, label, operation)); + return this; + } + + public EveSurfaceContainerBuilder PopupButton( + string id, + string label, + string title, + Action build, + string anchor = "viewport", + string placement = "center", + int offsetX = 0, + int offsetY = 0) + { + if (build == null) throw new ArgumentNullException(nameof(build)); + var popup = BuildContainer( + $"{id}.popup", + "modal", + EveSurfaceBuilder.Props( + ("title", title ?? label ?? ""), + ("anchor", anchor ?? "viewport"), + ("placement", placement ?? "center"), + ("offsetX", offsetX.ToString(System.Globalization.CultureInfo.InvariantCulture)), + ("offsetY", offsetY.ToString(System.Globalization.CultureInfo.InvariantCulture)), + ("presentation", "popover")), + build); + _children.Add(new EveSurfaceComponent( + id ?? "", + "control.popup", + EveSurfaceBuilder.Props( + ("label", label ?? ""), + ("title", title ?? label ?? ""), + ("anchor", anchor ?? "viewport"), + ("placement", placement ?? "center"), + ("offsetX", offsetX.ToString(System.Globalization.CultureInfo.InvariantCulture)), + ("offsetY", offsetY.ToString(System.Globalization.CultureInfo.InvariantCulture))), + new[] { popup })); + return this; + } + + public EveSurfaceContainerBuilder EnumField( + string id, + string label, + string value, + CultMeshStateBindingDescriptor optionsBinding, + CultMeshOperationBindingDescriptor setValue, + string anchor = "trigger", + string placement = "right-down") + { + return FieldRow(id, label, field => field.PopupButton( + $"{id}.dropdown", + value, + label, + popup => popup.OptionList($"{id}.options", label, optionsBinding, setValue), + anchor, + placement)); + } + + public EveSurfaceContainerBuilder OptionList( + string id, + string label, + CultMeshStateBindingDescriptor itemsBinding, + CultMeshOperationBindingDescriptor operation) + { + if (itemsBinding == null) throw new ArgumentNullException(nameof(itemsBinding)); + if (operation == null) throw new ArgumentNullException(nameof(operation)); + _addCommand(new EveCommandTemplate(operation)); + _children.Add(new EveSurfaceComponent( + id ?? "", + "options", + EveSurfaceBuilder.Props( + ("label", label ?? ""), + ("command", operation.OperationId), + ("operationId", operation.OperationId), + ("schemaId", operation.SchemaId)), + Array.Empty(), + new[] { itemsBinding })); + return this; + } + + public EveSurfaceContainerBuilder TextInput( + string id, + string label, + string value, + CultMeshOperationBindingDescriptor operation, + CultMeshStateBindingDescriptor? binding = null) + { + return Control("control.input.text", id, label, value, operation, binding); + } + + public EveSurfaceContainerBuilder Toggle( + string id, + string label, + bool value, + CultMeshOperationBindingDescriptor operation, + CultMeshStateBindingDescriptor? binding = null) + { + return Control("control.toggle", id, label, value ? "true" : "false", operation, binding); + } + + public EveSurfaceContainerBuilder Slider( + string id, + string label, + string value, + string min, + string max, + string step, + CultMeshOperationBindingDescriptor operation, + CultMeshStateBindingDescriptor? binding = null) + { + return Control( + "control.slider", + id, + label, + value, + operation, + binding, + ("min", min ?? ""), + ("max", max ?? ""), + ("step", step ?? "")); + } + + public EveSurfaceContainerBuilder Select( + string id, + string label, + string value, + CultMeshOperationBindingDescriptor operation, + CultMeshStateBindingDescriptor? binding = null) + { + return Control("control.select", id, label, value, operation, binding); + } + + public EveSurfaceContainerBuilder EmbeddedDocument( + string id, + string slotId, + string documentId, + string schemaId, + string presentationKind, + CultMeshRouteHint? routeHint = null) + { + _children.Add(new EveSurfaceComponent( + id ?? "", + "surface.slot", + EveSurfaceBuilder.Props( + ("slotId", slotId ?? ""), + ("documentId", documentId ?? ""), + ("schemaId", schemaId ?? ""), + ("presentationKind", presentationKind ?? "")), + Array.Empty(), + Array.Empty(), + new[] + { + new EveEmbeddedDocumentSlot( + slotId ?? "", + documentId ?? "", + schemaId ?? "", + presentationKind ?? "", + routeHint) + })); + return this; + } + + internal EveSurfaceComponent Build() + { + return new EveSurfaceComponent( + _id, + _kind, + _props, + _children.ToArray(), + _stateBindings.ToArray(), + _embeddedDocuments.ToArray()); + } + + private EveSurfaceContainerBuilder Prop(string key, int value) + { + _props[key] = value.ToString(System.Globalization.CultureInfo.InvariantCulture); + return this; + } + + private EveSurfaceContainerBuilder Prop(string key, string value) + { + _props[key] = value ?? ""; + return this; + } + + private EveSurfaceContainerBuilder Leaf( + string id, + string kind, + IReadOnlyDictionary props, + CultMeshStateBindingDescriptor? binding = null, + IReadOnlyList? children = null, + Action? configure = null) + { + var builder = new EveSurfaceContainerBuilder(id, kind, props, _addCommand); + configure?.Invoke(builder); + var component = new EveSurfaceComponent( + id ?? "", + kind ?? "", + new Dictionary(props ?? EveSurfaceBuilder.EmptyProps(), StringComparer.Ordinal), + children ?? Array.Empty(), + binding == null ? Array.Empty() : new[] { binding }); + if (builder._props.Count != (props?.Count ?? 0)) + { + component = new EveSurfaceComponent( + id ?? "", + kind ?? "", + new Dictionary(builder._props, StringComparer.Ordinal), + children ?? Array.Empty(), + binding == null ? Array.Empty() : new[] { binding }); + } + _children.Add(component); + return this; + } + + private EveSurfaceContainerBuilder Control( + string kind, + string id, + string label, + string value, + CultMeshOperationBindingDescriptor operation, + CultMeshStateBindingDescriptor? binding, + params (string Key, string Value)[] extraProps) + { + if (operation == null) throw new ArgumentNullException(nameof(operation)); + _addCommand(new EveCommandTemplate(operation)); + var props = EveSurfaceBuilder.Props( + new[] + { + ("label", label ?? ""), + ("value", value ?? ""), + ("command", operation.OperationId), + ("operationId", operation.OperationId), + ("schemaId", operation.SchemaId) + }.Concat(extraProps ?? Array.Empty<(string Key, string Value)>()).ToArray()); + return Leaf(kind: kind, id: id, props: props, binding: binding); + } + + private EveSurfaceComponent BuildContainer( + string id, + string kind, + IReadOnlyDictionary props, + Action build) + { + if (build == null) throw new ArgumentNullException(nameof(build)); + var container = new EveSurfaceContainerBuilder(id, kind, props, _addCommand); + build(container); + return container.Build(); + } + } + + public sealed class EveSurfaceGroupBuilder + { + private readonly string _id; + private readonly string _kind; + private readonly Action _addCommand; + private readonly List _children = new(); + + internal EveSurfaceGroupBuilder( + string id, + string kind, + Action addCommand) + { + _id = id ?? ""; + _kind = kind ?? "column"; + _addCommand = addCommand ?? throw new ArgumentNullException(nameof(addCommand)); + } + + public EveSurfaceGroupBuilder Button(string label, string command) + { + var operation = CultMesh.OperationBinding(command, label); + _addCommand(new EveCommandTemplate(operation)); + _children.Add(EveSurfaceBuilder.ButtonComponent($"{_id}.button.{EveSurfaceBuilder.Slug(label)}", label, operation)); + return this; + } + + public EveSurfaceGroupBuilder Button(string label, CultMeshOperationBindingDescriptor operation) + { + if (operation == null) throw new ArgumentNullException(nameof(operation)); + _addCommand(new EveCommandTemplate(operation)); + _children.Add(EveSurfaceBuilder.ButtonComponent($"{_id}.button.{EveSurfaceBuilder.Slug(label)}", label, operation)); + return this; + } + + internal EveSurfaceComponent Build() + { + return new EveSurfaceComponent(_id, _kind, EveSurfaceBuilder.EmptyProps(), _children.ToArray()); + } + } + + public sealed class EveSurfaceFormBuilder + { + private readonly string _id; + private readonly Action _addCommand; + private readonly List _children = new(); + + internal EveSurfaceFormBuilder(string id, Action addCommand) + { + _id = id ?? ""; + _addCommand = addCommand ?? throw new ArgumentNullException(nameof(addCommand)); + } + + public EveSurfaceFormBuilder Text( + string label, + string value, + CultMeshOperationBindingDescriptor operation, + CultMeshStateBindingDescriptor? binding = null) + { + if (operation == null) throw new ArgumentNullException(nameof(operation)); + _addCommand(new EveCommandTemplate(operation)); + _children.Add(Control( + "control.text", + label, + value, + operation, + binding)); + return this; + } + + public EveSurfaceFormBuilder Toggle( + string label, + bool value, + CultMeshOperationBindingDescriptor operation, + CultMeshStateBindingDescriptor? binding = null) + { + if (operation == null) throw new ArgumentNullException(nameof(operation)); + _addCommand(new EveCommandTemplate(operation)); + _children.Add(Control( + "control.toggle", + label, + value ? "true" : "false", + operation, + binding)); + return this; + } + + public EveSurfaceFormBuilder Metric( + string label, + string value, + CultMeshStateBindingDescriptor? binding = null) + { + var bindings = binding == null + ? Array.Empty() + : new[] { binding }; + _children.Add(new EveSurfaceComponent( + $"{_id}.metric.{EveSurfaceBuilder.Slug(label)}", + "metric", + new Dictionary(StringComparer.Ordinal) + { + ["label"] = label ?? "", + ["value"] = value ?? "" + }, + Array.Empty(), + bindings)); + return this; + } + + internal EveSurfaceComponent Build() + { + return new EveSurfaceComponent(_id, "form", EveSurfaceBuilder.EmptyProps(), _children.ToArray()); + } + + private EveSurfaceComponent Control( + string kind, + string label, + string value, + CultMeshOperationBindingDescriptor operation, + CultMeshStateBindingDescriptor? binding) + { + var bindings = binding == null + ? Array.Empty() + : new[] { binding }; + return new EveSurfaceComponent( + $"{_id}.{EveSurfaceBuilder.Slug(label)}", + kind, + new Dictionary(StringComparer.Ordinal) + { + ["label"] = label ?? "", + ["value"] = value ?? "", + ["command"] = operation.OperationId, + ["operationId"] = operation.OperationId, + ["schemaId"] = operation.SchemaId + }, + Array.Empty(), + bindings); + } + } + + public static class EveSurface + { + public static EveSurfaceBuilder Create(string surfaceId) + { + return new EveSurfaceBuilder(surfaceId); + } + } +} diff --git a/packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs.meta b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs.meta new file mode 100644 index 0000000..adc41da --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 97ac7f757692aef4db64d75d0df6fe51 \ No newline at end of file diff --git a/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs new file mode 100644 index 0000000..c4eb051 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs @@ -0,0 +1,414 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using GameCult.Caching; +using GameCult.Mesh; +using MessagePack; + +#nullable enable + +namespace GameCult.Eve.Surface +{ + [CultDocument("gamecult.eve.surface", "gamecult.eve.surface.v1")] + [MessagePackObject] + public sealed class EveSurfaceDocument + { + public const string DefaultType = "surface-state"; + public const string SchemaId = "gamecult.eve.surface.v1"; + + public EveSurfaceDocument( + string providerId, + string providerKind, + string title, + long version, + string updatedAtUtc, + EveSurfaceTree surface, + IReadOnlyList commands) + : this(DefaultType, SchemaId, providerId, providerKind, title, version, updatedAtUtc, surface, commands) + { + } + + [SerializationConstructor] + public EveSurfaceDocument( + string type, + string schema, + string providerId, + string providerKind, + string title, + long version, + string updatedAtUtc, + EveSurfaceTree surface, + IReadOnlyList commands) + { + Type = string.IsNullOrWhiteSpace(type) ? DefaultType : type; + Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema; + ProviderId = providerId ?? ""; + ProviderKind = providerKind ?? ""; + Title = title ?? ""; + Version = version; + UpdatedAtUtc = updatedAtUtc ?? ""; + Surface = surface ?? throw new ArgumentNullException(nameof(surface)); + Commands = commands ?? Array.Empty(); + } + + [Key(0)] + public string Type { get; } + + [Key(1)] + public string Schema { get; } + + [Key(2)] + public string ProviderId { get; } + + [Key(3)] + public string ProviderKind { get; } + + [Key(4)] + public string Title { get; } + + [Key(5)] + public long Version { get; } + + [Key(6)] + public string UpdatedAtUtc { get; } + + [Key(7)] + public EveSurfaceTree Surface { get; } + + [Key(8)] + public IReadOnlyList Commands { get; } + } + + [MessagePackObject] + public sealed class EveSurfaceTree + { + [SerializationConstructor] + public EveSurfaceTree(string id, EveSurfaceComponent root, IReadOnlyList styles) + { + Id = id ?? ""; + Root = root ?? throw new ArgumentNullException(nameof(root)); + Styles = styles ?? Array.Empty(); + } + + [Key(0)] + public string Id { get; } + + [Key(1)] + public EveSurfaceComponent Root { get; } + + [Key(2)] + public IReadOnlyList Styles { get; } + } + + [MessagePackObject] + public sealed class EveSurfaceComponent + { + public EveSurfaceComponent( + string id, + string kind, + IReadOnlyDictionary props, + IReadOnlyList children) + : this(id, kind, props, children, StateBindingsFromProps(props)) + { + } + + public EveSurfaceComponent( + string id, + string kind, + IReadOnlyDictionary props, + IReadOnlyList children, + IReadOnlyList stateBindings) + : this(id, kind, props, children, stateBindings, Array.Empty()) + { + } + + public EveSurfaceComponent( + string id, + string kind, + IReadOnlyDictionary props, + IReadOnlyList children, + IReadOnlyList stateBindings, + IReadOnlyList embeddedDocuments, + IReadOnlyDictionary? layout = null, + IReadOnlyDictionary? style = null) + : this( + id, + kind, + props, + children, + (stateBindings ?? Array.Empty()) + .Select(CultMeshStateBindingRecord.FromBinding) + .ToArray(), + embeddedDocuments, + layout, + style) + { + } + + [SerializationConstructor] + public EveSurfaceComponent( + string id, + string kind, + IReadOnlyDictionary props, + IReadOnlyList children, + CultMeshStateBindingRecord[] stateBindingRecords, + IReadOnlyList embeddedDocuments, + IReadOnlyDictionary? layout = null, + IReadOnlyDictionary? style = null) + { + Id = id ?? ""; + Kind = kind ?? ""; + Props = props ?? new Dictionary(StringComparer.Ordinal); + Children = children ?? Array.Empty(); + StateBindingRecords = stateBindingRecords ?? Array.Empty(); + EmbeddedDocuments = embeddedDocuments ?? Array.Empty(); + Layout = layout ?? new Dictionary(StringComparer.Ordinal); + Style = style ?? new Dictionary(StringComparer.Ordinal); + } + + [Key(0)] + public string Id { get; } + + [Key(1)] + public string Kind { get; } + + [Key(2)] + public IReadOnlyDictionary Props { get; } + + [Key(3)] + public IReadOnlyList Children { get; } + + [Key(4)] + public CultMeshStateBindingRecord[] StateBindingRecords { get; } + + [IgnoreMember] + public IReadOnlyList StateBindings => + StateBindingRecords.Select(record => record.ToBinding()).ToArray(); + + [Key(5)] + public IReadOnlyList EmbeddedDocuments { get; } + + [Key(6)] + public IReadOnlyDictionary Layout { get; } + + [Key(7)] + public IReadOnlyDictionary Style { get; } + + public string GetProp(string key, string fallback = "") + { + return Props.TryGetValue(key, out var value) ? value : fallback; + } + + private static IReadOnlyList StateBindingsFromProps( + IReadOnlyDictionary? props) + { + if (props == null || props.Count == 0) + return Array.Empty(); + + var bindings = new List(); + foreach (var prop in props) + { + if (string.IsNullOrWhiteSpace(prop.Value) || + !prop.Key.EndsWith("PointerId", StringComparison.Ordinal)) + { + continue; + } + + var targetProp = prop.Key.Substring(0, prop.Key.Length - "PointerId".Length); + if (targetProp.Length == 0) + targetProp = "value"; + bindings.Add(new CultMeshStateBindingDescriptor(targetProp, prop.Value)); + } + + return bindings; + } + } + + [MessagePackObject] + public sealed class EveEmbeddedDocumentSlot + { + public EveEmbeddedDocumentSlot( + string slotId, + string documentId, + string schemaId, + string presentationKind, + CultMeshRouteHint? routeHint = null) + : this(slotId, documentId, schemaId, presentationKind, CultMeshRouteRecord.FromRoute(routeHint)) + { + } + + [SerializationConstructor] + public EveEmbeddedDocumentSlot( + string slotId, + string documentId, + string schemaId, + string presentationKind, + CultMeshRouteRecord route) + { + SlotId = slotId ?? ""; + DocumentId = documentId ?? ""; + SchemaId = schemaId ?? ""; + PresentationKind = presentationKind ?? ""; + Route = route ?? CultMeshRouteRecord.FromRoute(CultMeshRouteHint.Automatic); + } + + [Key(0)] + public string SlotId { get; } + + [Key(1)] + public string DocumentId { get; } + + [Key(2)] + public string SchemaId { get; } + + [Key(3)] + public string PresentationKind { get; } + + [Key(4)] + public CultMeshRouteRecord Route { get; } + + [IgnoreMember] + public CultMeshRouteHint RouteHint => Route.ToRoute(); + } + + [MessagePackObject] + public sealed class EveStyleToken + { + [SerializationConstructor] + public EveStyleToken(string name, string value) + { + Name = name ?? ""; + Value = value ?? ""; + } + + [Key(0)] + public string Name { get; } + + [Key(1)] + public string Value { get; } + } + + [MessagePackObject] + public sealed class EveCommandTemplate + { + public EveCommandTemplate(CultMeshOperationBindingDescriptor operation) + : this(CultMeshOperationBindingRecord.FromBinding(operation)) + { + } + + [SerializationConstructor] + public EveCommandTemplate(CultMeshOperationBindingRecord operationRecord) + { + OperationRecord = operationRecord ?? throw new ArgumentNullException(nameof(operationRecord)); + } + + [Key(0)] + public CultMeshOperationBindingRecord OperationRecord { get; } + + [IgnoreMember] + public CultMeshOperationBindingDescriptor Operation => OperationRecord.ToBinding(); + + [IgnoreMember] + public string Command => Operation.OperationId; + + [IgnoreMember] + public string Label => Operation.Label; + + [IgnoreMember] + public string Transport => Operation.RouteHint.Description ?? ""; + } + + [CultDocument("gamecult.eve.command", SchemaId)] + [MessagePackObject] + public sealed class EveSurfaceCommandRequest + { + public const string SchemaId = "gamecult.eve.command_invocation.v1"; + + public EveSurfaceCommandRequest( + string providerId, + string surfaceId, + CultMeshOperationInvocationDescriptor operation, + CultMeshOperationPayload payload, + DateTimeOffset issuedAt, + string clientId, + string commandBoundary = "", + string receiptSchema = "") + : this( + SchemaId, + providerId, + surfaceId, + CultMeshOperationInvocationRecord.FromInvocation(operation), + payload?.ToDictionary(entry => entry.Key, entry => entry.Value, StringComparer.Ordinal) + ?? new Dictionary(StringComparer.Ordinal), + issuedAt, + clientId, + commandBoundary, + receiptSchema) + { + } + + [SerializationConstructor] + public EveSurfaceCommandRequest( + string schema, + string providerId, + string surfaceId, + CultMeshOperationInvocationRecord operation, + Dictionary payload, + DateTimeOffset issuedAt, + string clientId, + string commandBoundary, + string receiptSchema) + { + Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema; + ProviderId = providerId; + SurfaceId = surfaceId; + OperationRecord = operation ?? throw new ArgumentNullException(nameof(operation)); + PayloadFields = payload == null + ? new Dictionary(StringComparer.Ordinal) + : new Dictionary(payload, StringComparer.Ordinal); + IssuedAt = issuedAt; + ClientId = clientId; + CommandBoundary = commandBoundary ?? ""; + ReceiptSchema = receiptSchema ?? ""; + } + + [Key(0)] + public string Schema { get; } + + [Key(1)] + public string ProviderId { get; } + + [Key(2)] + public string SurfaceId { get; } + + [Key(3)] + public CultMeshOperationInvocationRecord OperationRecord { get; } + + [IgnoreMember] + public CultMeshOperationInvocationDescriptor Operation => OperationRecord.ToInvocation(); + + [IgnoreMember] + public string Command => Operation.OperationId; + + [Key(4)] + public Dictionary PayloadFields { get; } + + [IgnoreMember] + public CultMeshOperationPayload Payload => new CultMeshOperationPayload(PayloadFields); + + [Key(5)] + public DateTimeOffset IssuedAt { get; } + + [Key(6)] + public string ClientId { get; } + + [Key(7)] + public string CommandBoundary { get; } + + [Key(8)] + public string ReceiptSchema { get; } + + [IgnoreMember] + public string CommandId => Operation.IdempotencyKey ?? ""; + } +} diff --git a/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs.meta b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs.meta new file mode 100644 index 0000000..ebd5f02 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6516c42104b235f42b15efeb4639bb73 \ No newline at end of file diff --git a/packages/org.gamecult.eve.surface/Runtime/GameCult.Eve.Surface.asmdef b/packages/org.gamecult.eve.surface/Runtime/GameCult.Eve.Surface.asmdef new file mode 100644 index 0000000..8041c88 --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/GameCult.Eve.Surface.asmdef @@ -0,0 +1,20 @@ +{ + "name": "GameCult.Eve.Surface", + "rootNamespace": "GameCult.Eve.Surface", + "references": [ + "GameCult.Mesh" + ], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": true, + "precompiledReferences": [ + "GameCult.Caching.dll", + "GameCult.Mesh.dll", + "MessagePack.Annotations.dll" + ], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": true +} diff --git a/packages/org.gamecult.eve.surface/Runtime/GameCult.Eve.Surface.asmdef.meta b/packages/org.gamecult.eve.surface/Runtime/GameCult.Eve.Surface.asmdef.meta new file mode 100644 index 0000000..d8f543e --- /dev/null +++ b/packages/org.gamecult.eve.surface/Runtime/GameCult.Eve.Surface.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: a9004278c14816345941af2f2ce673ca +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.surface/package.json b/packages/org.gamecult.eve.surface/package.json new file mode 100644 index 0000000..7c99f0e --- /dev/null +++ b/packages/org.gamecult.eve.surface/package.json @@ -0,0 +1,18 @@ +{ + "name": "org.gamecult.eve.surface", + "displayName": "Eve Surface Contract", + "version": "0.1.0", + "unity": "2021.3", + "description": "Unity-compatible DTOs for gamecult.eve.surface.v1 documents.", + "dependencies": { + "org.gamecult.cultlib": "1.0.6" + }, + "keywords": [ + "Eve", + "CultUI", + "CultMesh", + "Surface" + ], + "license": "MPL-2.0", + "category": "Scripting" +} diff --git a/packages/org.gamecult.eve.surface/package.json.meta b/packages/org.gamecult.eve.surface/package.json.meta new file mode 100644 index 0000000..8e89223 --- /dev/null +++ b/packages/org.gamecult.eve.surface/package.json.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 49f1738bd94075e489c82190cf660b1d +PackageManifestImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index 52d0a60..d93183b 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -5,127 +5,22 @@ interactive worlds. Providers implement the surface-document, asset-manifest, command-sink, and receipt-source ports. `EveUnityPlayableWorldClientBootstrap` wires those ports -to the generic scene host, input driver, camera rig, `GameObject` sink, and UI -Toolkit surface overlay. The overlay lowers provider-owned panes, metrics, -progress, inventory, and controls from the same Eve document used by the scene. -Stable structures update their native controls in place; only structural -changes rebuild the visual tree. Scene, field, and reactive render subtrees are -owned by their native lowerers and are not duplicated into empty UI elements. +to the generic scene host, input driver, camera rig, and `GameObject` sink. `EveUnityCultMeshPlayableWorldProvider` is the runtime composition root for a networked client. Give it one Verse rendezvous endpoint and it discovers an advertised `interactive-world`, constructs the generic CultMesh transport, and provides every bootstrap port. Provider, surface, and Verse IDs are optional -selection filters; they are not product knowledge required by the client. The -selected Verse ID and its advertised authority-runtime ID form the stable -CultMesh session target. The Eve provider ID separately names the UI owner found -through that target. Physical routes are resolved and re-resolved inside -`CultMeshClient`; they are never published as provider selection state or -cached as Unity authority. The optional local path is only an ephemeral -content/body cache, not a replicated gameplay database. -Await `PrepareAsync()` before mounting the bootstrap or reading provider ports. -It owns discovery, initial document reads, content transfer, asynchronous asset -bundle loading, and live lease installation. Once preparation succeeds, -`Mount()` and `Connect()` perform no remote work. Input submission only enters a -bounded asynchronous outbox: one-shot commands retain their idempotency keys, -while movement and look values coalesce to the latest value per entity. A -successful send is not completion: the command remains pending and retries with -the same idempotency key until its canonical provider receipt arrives. Failed -or unacknowledged commands rotate behind other due work, so one poison command -cannot starve the outbox. -Live entity, field, and receipt polling likewise remains non-blocking. Unity's -main thread applies completed typed documents and assets; it never waits for a -CultMesh task, keeping Play, Pause, input, inspection, and rendering responsive -while a provider is slow or temporarily unreachable. - -An Eve world may qualify its asset catalog with a different provider, Verse, -authority runtime, and Odin rendezvous set than the surface provider. The -transport resolves that exact asset session and commits assets as one generation: -catalog, verified bundles, leases, lookups, render policy, and surface lowering -are prepared off to the side, then swapped together. A failed or superseded -candidate is disposed without unloading or rewriting the mounted generation. -The mounted provider's trust policy authenticates only that provider. A -provider-qualified asset source in another Verse/runtime uses the configured -cross-target Odin trust for its catalog, subscription, manifests, content, and -bundles; local-development trust does not cross that boundary. -The candidate opens its catalog lease before preload, observes updates in a -candidate-owned buffer, and verifies the leased version again before commit. -An update that lands during preload therefore rebuilds the candidate instead -of disappearing between a one-shot read and a later subscription. - -Navigation keeps the outgoing presentation quiesced until the candidate route -is ready, then commits the provider route before activating the candidate. -Presentation connections are provider-owned leases: disposing the outgoing -host releases only its lease and cannot disconnect the newly committed Verse. +selection filters; they are not product knowledge required by the client. The package never imports provider product assemblies. -Add `EveUnityRendererFeature` to the Universal Renderer used by Eve cameras. -The feature is the single URP-owned scheduling point for provider-advertised -volume, particle, and exposure programs. The lowerers use Unity 6 RenderGraph -resource declarations; effects that sample camera color request an intermediate -color texture and never read the back buffer. Do not schedule these effects from -`RenderPipelineManager` callbacks. The provider owns programs, assets, and -parameters; the Universal Renderer owns when and where they execute. -Provider volume programs render through both Game and SceneView cameras. Each -camera owns separate raymarch and temporal-history targets, so editor preview -cannot resize or contaminate the playable camera's history. Presentation-only -exposure remains bound to the leased Game camera. - -Advertised input bindings are performed from the provider's semantic action -catalog. Direct and chord gestures submit once per press edge; held values -submit their advertised press/release scalar. `view-direction.v1` samples the -active world camera's normalized world-space forward direction into the three -advertised payload keys. Providers own what that ray means; EveUnity never -selects targets or imports provider gameplay policy. - -`EveUnityInputActionBar` lowers provider suggestions and profile-visible actions -from that same catalog. It resolves `iconRef` through the provider asset -manifest, disables unavailable actions, preserves press/release behavior for -held controls, samples the active camera for view-direction controls, and -submits `scalar.v1` values from an explicit numeric field. Its named UI Toolkit -elements and classes are presentation levers; the component never imports a -provider assembly or mutates provider-owned current values optimistically. - -`IEveUnityPresentedEntityRegistry` exposes each committed SoA generation as -immutable entity facts plus presentation transforms, addressable by Eve entity -id or source index. Camera, selection, and provider UI adapters consume that -registry instead of reconstructing provider gameplay objects. It owns -presentation lookup only; simulation state and commands remain provider-owned. - -World surfaces may exclude semantic render channels through the -`excludedRenderChannels` world-root property. Runtime asset variants map those -channel names to Unity layers with `renderChannel..unityLayer` -metadata. Surfaces never publish Unity layer numbers or camera masks: providers -own which semantic content belongs in a view, while EveUnity owns the native -layer mapping and subtracts only those layers from the camera's configured -culling mask. - -World surfaces may also advertise a presentation-only directional key light -through `keyLightDirection`, `keyLightColor`, and `keyLightIntensity`. -EveUnity lowers those generic values into a transient Unity light and removes -it when the world camera lease ends. The provider owns the lighting design; -the light never becomes physics, visibility, or gameplay authority. - Retained provider feedback is lowered without becoming Unity authority. `EveUnityFeedbackPresenter` emits each new `feedback.event` identity once, and `EveUnityShotReceiptPresenter` does the same for `shot.receipt` trajectories. The generic `EveUnityShotTrajectoryRenderer` renders provider-authored origin, endpoint, duration, and hit outcome, then expires the visual locally. It never uses Unity collision to infer damage or report a hit back to the provider. -Providers may publish a non-spatial `combat.presentation` component beside the -SoA body view. `EveUnityCombatPresentationRenderer` lowers its selected target, -contact visibility, lock progress, meter ratios, semantic roles, and timing -into reticle, lock, shield/hull, and hit-marker visuals. Hit markers require a -new deduplicated shot receipt from the advertised controlled source to the -advertised selection with authoritative applied or shield-absorbed damage. -SoA remains the only transform authority; the presentation component never -duplicates bodies or computes combat outcomes. -Continuous `beam.presentation` nodes bind provider-authored effect assets to -the current presented-entity generation. The generic beam renderer follows the -advertised source transform, applies provider-owned power to particle emission, -and reconciles asset lifecycles by semantic role. It never raycasts, applies -force, infers contact, collects cargo, or emits gameplay receipts. The reusable `LightningCompute` renderer and its shader/material bundle live in this runtime package with their original Unity GUIDs. Provider asset bundles may therefore reference the effect without importing Aetheria gameplay code. @@ -134,55 +29,3 @@ therefore reference the effect without importing Aetheria gameplay code. `org.gamecult.eve.plugin-fields` contracts. `EveFieldsSplatRasterizer` owns the Unity structured-buffer and RenderTexture lowering without importing provider document classes. - -`field.volume3d` surfaces name logical texture ports, feature names, quality, -and authored scalar/vector parameters. Concrete Unity shader properties, -keywords, and pass indices belong to the selected `unity-scene` asset variant -under `unity.volume.*` metadata. The lowerer fails closed when that native -program descriptor is absent or incomplete. This keeps provider shader ABIs -out of Eve's semantic surface while allowing a provider-owned shader to consume -portable Eve Fields documents. Volume composition runs as a generic URP render -pass before post-processing so the provider-advertised profile grades both the -native scene and the provider volume through the same camera pipeline. - -Camera-relative field consumers share `viewportAnchor`, `span`, -`cellWorldSize`, `viewportSnapLayer`, and `viewportSnapTexels` as one viewport -frame contract. Volume and particle lowerers resolve that frame through the -same generic path, so a snapped stateless particle lattice cannot drift from -the field textures it samples. A `world.scene3d` may also request -`temporal-reprojection.v1`; EveUnity lowers its neutral history blend, jitter, -quality, and sharpening values to URP TAA and restores the previous camera -state when the world releases the camera. - -Native volume programs also advertise the projection convention consumed by -their history matrix. `non-render-target-projection.previous-view.v1` preserves -the projection convention used by legacy `Graphics.Blit` temporal shaders even -when EveUnity executes the program through a render-target-backed URP pass; -providers must not rely on a runtime guessing that Y-axis convention. - -`field.particles3d` may advertise provider material textures, viewport-relative -texture scales, and a logical render-frame index port. These are presentation -inputs: EveUnity binds the selected provider assets and current display frame -without assigning product meaning to the shader. Stateless simulation time and -particle positions remain provider-document inputs. - -Providers may advertise per-layer resolution scales, mipmap use, and filtering -through `layerTargetDescriptors`; the Unity lowerer applies those native target -requirements without assigning gameplay or product meaning to the layer keys. -`documentFloatBindings` maps standard document scalars to logical float ports as -`source=port,scale,offset`. This lets animated native programs consume -provider-authoritative simulation time without making the client clock an -authority. -Native volume programs may advertise a `cameraToWorld` matrix port when their -procedural passes require an explicit camera transform instead of relying on -render-pipeline globals. - -Command finality is presentation-causal. When a terminal receipt names a -`sourceVersion` newer than the mounted surface, EveUnity keeps the command and -its exact receipt lease alive, makes the obsolete surface read-only, and emits -the terminal receipt only after that surface generation and its qualified asset -catalog have committed together. Asset preparation failure therefore cannot -leave an accepted Verse switch paired with actionable controls from the prior -Verse. Embedded surfaces may recompose the visible tree, but their versions are -derived rendering inputs and cannot satisfy or clear the advertised base -surface's finality barrier. diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/AssemblyInfo.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/AssemblyInfo.cs deleted file mode 100644 index 61285ea..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/AssemblyInfo.cs +++ /dev/null @@ -1,3 +0,0 @@ -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("GameCult.Eve.UnityScene.Tests")] diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/AssemblyInfo.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/AssemblyInfo.cs.meta deleted file mode 100644 index db8b4c5..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/AssemblyInfo.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 68e2f1a625b94fc39dcc44d33e0cf318 diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshCommandOutbox.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshCommandOutbox.cs deleted file mode 100644 index f8c89fa..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshCommandOutbox.cs +++ /dev/null @@ -1,296 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using GameCult.Eve.Surface; - -#nullable enable - -namespace GameCult.Eve.UnityScene -{ - /// - /// Owns receipt-backed asynchronous command delivery for the CultMesh Unity - /// transport. A successful transport send only moves a command into - /// AwaitingCanonicalReceipt. The provider receipt is the sole completion - /// signal. Retries retain the original idempotency key, and one failed - /// command cannot monopolize delivery of the rest of the bounded outbox. - /// - internal sealed class EveUnityCultMeshCommandOutbox : IDisposable - { - internal enum DeliveryState - { - Queued, - Sending, - AwaitingCanonicalReceipt - } - - private readonly object _gate = new object(); - private readonly List _items = new List(); - private readonly Dictionary _byCommandId = - new Dictionary(StringComparer.Ordinal); - private readonly Dictionary _continuous = - new Dictionary(StringComparer.Ordinal); - private readonly Func _send; - private readonly Func _continuousKey; - private readonly Action _discard; - private readonly int _commandCapacity; - private readonly int _continuousCapacity; - private readonly TimeSpan _retryDelay; - private readonly TimeSpan _maximumRetryDelay; - private readonly TimeSpan _sendAttemptTimeout; - private readonly SemaphoreSlim _changed = new SemaphoreSlim(0); - private readonly CancellationTokenSource _lifetime = new CancellationTokenSource(); - private readonly Task _worker; - private bool _disposed; - private long _sequence; - - public EveUnityCultMeshCommandOutbox( - Func send, - Func continuousKey, - Action? discard = null, - int commandCapacity = 256, - int continuousCapacity = 32, - TimeSpan? retryDelay = null, - TimeSpan? maximumRetryDelay = null, - TimeSpan? sendAttemptTimeout = null) - { - _send = send ?? throw new ArgumentNullException(nameof(send)); - _continuousKey = continuousKey ?? throw new ArgumentNullException(nameof(continuousKey)); - _discard = discard ?? (_ => { }); - if (commandCapacity <= 0) throw new ArgumentOutOfRangeException(nameof(commandCapacity)); - if (continuousCapacity <= 0) throw new ArgumentOutOfRangeException(nameof(continuousCapacity)); - _commandCapacity = commandCapacity; - _continuousCapacity = continuousCapacity; - _retryDelay = retryDelay ?? TimeSpan.FromMilliseconds(100); - _maximumRetryDelay = maximumRetryDelay ?? TimeSpan.FromSeconds(2); - _sendAttemptTimeout = sendAttemptTimeout ?? TimeSpan.FromSeconds(2); - if (_retryDelay <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(retryDelay)); - if (_maximumRetryDelay < _retryDelay) throw new ArgumentOutOfRangeException(nameof(maximumRetryDelay)); - if (_sendAttemptTimeout <= TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(sendAttemptTimeout)); - _worker = Task.Run(DeliverAsync); - } - - internal int PendingCount - { - get { lock (_gate) return _items.Count; } - } - - internal DeliveryState? StateOf(string commandId) - { - lock (_gate) - return _byCommandId.TryGetValue(commandId, out var item) ? item.State : null; - } - - public void Enqueue(EveSurfaceCommandRequest request) - { - if (request == null) throw new ArgumentNullException(nameof(request)); - if (string.IsNullOrWhiteSpace(request.CommandId)) - throw new InvalidOperationException("Eve command invocations require an idempotency key."); - - string? discarded = null; - lock (_gate) - { - if (_disposed) throw new ObjectDisposedException(nameof(EveUnityCultMeshCommandOutbox)); - if (_byCommandId.ContainsKey(request.CommandId)) - throw new InvalidOperationException($"Eve command '{request.CommandId}' is already pending."); - - var key = _continuousKey(request); - key = string.IsNullOrWhiteSpace(key) ? null : key; - if (key == null) - { - if (_items.Count(item => item.ContinuousKey == null) >= _commandCapacity) - throw new InvalidOperationException( - $"The EveUnity command outbox reached its {_commandCapacity}-command bound."); - } - else if (_continuous.TryGetValue(key, out var previous)) - { - RemoveLocked(previous); - discarded = previous.Request.CommandId; - } - else if (_continuous.Count >= _continuousCapacity) - { - throw new InvalidOperationException( - $"The EveUnity continuous-control outbox reached its {_continuousCapacity}-binding bound."); - } - - var item = new OutboxItem(request, key, ++_sequence); - _items.Add(item); - _byCommandId.Add(request.CommandId, item); - if (key != null) _continuous[key] = item; - } - - if (discarded != null) _discard(discarded); - _changed.Release(); - } - - /// Completes a command only when its canonical provider receipt arrives. - public bool Acknowledge(string commandId) - { - if (string.IsNullOrWhiteSpace(commandId)) return false; - lock (_gate) - { - if (!_byCommandId.TryGetValue(commandId, out var item)) return false; - RemoveLocked(item); - } - _changed.Release(); - return true; - } - - public void Dispose() - { - OutboxItem[] discarded; - lock (_gate) - { - if (_disposed) return; - _disposed = true; - discarded = _items.ToArray(); - _items.Clear(); - _byCommandId.Clear(); - _continuous.Clear(); - } - foreach (var item in discarded) _discard(item.Request.CommandId); - _lifetime.Cancel(); - _changed.Release(); - } - - private async Task DeliverAsync() - { - var cancellationToken = _lifetime.Token; - try - { - while (true) - { - var selection = TakeNext(); - if (selection.Item == null) - { - await WaitForChangeOrDueAsync(selection.Delay, cancellationToken).ConfigureAwait(false); - continue; - } - await DeliverOnceAsync(selection.Item, cancellationToken).ConfigureAwait(false); - } - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - } - } - - private async Task DeliverOnceAsync(OutboxItem item, CancellationToken cancellationToken) - { - var sent = false; - using (var attempt = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) - { - attempt.CancelAfter(_sendAttemptTimeout); - try - { - await _send(item.Request, attempt.Token).ConfigureAwait(false); - sent = true; - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - throw; - } - catch - { - // The same idempotency key is retried after other due items. - } - } - - lock (_gate) - { - if (!_byCommandId.TryGetValue(item.Request.CommandId, out var current) || - !ReferenceEquals(item, current)) - return; - item.Attempts++; - item.State = sent ? DeliveryState.AwaitingCanonicalReceipt : DeliveryState.Queued; - item.NextAttemptAt = DateTimeOffset.UtcNow + RetryDelay(item.Attempts); - item.Sequence = ++_sequence; - } - _changed.Release(); - } - - private Selection TakeNext() - { - lock (_gate) - { - var now = DateTimeOffset.UtcNow; - var item = _items - .Where(candidate => candidate.State != DeliveryState.Sending && candidate.NextAttemptAt <= now) - .OrderBy(candidate => candidate.Sequence) - .FirstOrDefault(); - if (item != null) - { - item.State = DeliveryState.Sending; - return new Selection(item, null); - } - var next = _items - .Where(candidate => candidate.State != DeliveryState.Sending) - .Select(candidate => (DateTimeOffset?)candidate.NextAttemptAt) - .OrderBy(value => value) - .FirstOrDefault(); - return new Selection(null, next.HasValue ? Max(TimeSpan.Zero, next.Value - now) : null); - } - } - - private async Task WaitForChangeOrDueAsync(TimeSpan? delay, CancellationToken cancellationToken) - { - if (!delay.HasValue) - { - await _changed.WaitAsync(cancellationToken).ConfigureAwait(false); - return; - } - var milliseconds = (int)Math.Min(int.MaxValue, Math.Max(1, delay.Value.TotalMilliseconds)); - await _changed.WaitAsync(milliseconds, cancellationToken).ConfigureAwait(false); - } - - private TimeSpan RetryDelay(int attempts) - { - var multiplier = Math.Pow(2, Math.Min(attempts - 1, 10)); - return TimeSpan.FromMilliseconds(Math.Min( - _maximumRetryDelay.TotalMilliseconds, - _retryDelay.TotalMilliseconds * multiplier)); - } - - private void RemoveLocked(OutboxItem item) - { - _items.Remove(item); - _byCommandId.Remove(item.Request.CommandId); - if (item.ContinuousKey != null && - _continuous.TryGetValue(item.ContinuousKey, out var current) && - ReferenceEquals(item, current)) - _continuous.Remove(item.ContinuousKey); - } - - private static TimeSpan Max(TimeSpan left, TimeSpan right) => left >= right ? left : right; - - private sealed class OutboxItem - { - public OutboxItem(EveSurfaceCommandRequest request, string? continuousKey, long sequence) - { - Request = request; - ContinuousKey = continuousKey; - Sequence = sequence; - NextAttemptAt = DateTimeOffset.MinValue; - } - - public EveSurfaceCommandRequest Request { get; } - public string? ContinuousKey { get; } - public DeliveryState State { get; set; } - public DateTimeOffset NextAttemptAt { get; set; } - public int Attempts { get; set; } - public long Sequence { get; set; } - } - - private readonly struct Selection - { - public Selection(OutboxItem? item, TimeSpan? delay) - { - Item = item; - Delay = delay; - } - - public OutboxItem? Item { get; } - public TimeSpan? Delay { get; } - } - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshCommandOutbox.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshCommandOutbox.cs.meta deleted file mode 100644 index a6c7e7a..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshCommandOutbox.cs.meta +++ /dev/null @@ -1,3 +0,0 @@ -fileFormatVersion: 2 -guid: 61d76e98f2054d86bf8e7480d4411c39 -timeCreated: 1787039650 diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshConnectivity.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshConnectivity.cs deleted file mode 100644 index 5e163c8..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshConnectivity.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; -using GameCult.Mesh; -using GameCult.Networking; -using GameCult.Networking.WebSockets; - -#nullable enable - -namespace GameCult.Eve.UnityScene -{ - /// - /// Owns the transport set used by EveUnity for both rendezvous discovery and - /// the selected provider. Trust remains caller-owned; this class only makes - /// every supported secure plane reachable through the same configuration. - /// - internal static class EveUnityCultMeshConnectivity - { - public static CultMeshVerseDiscoveryClientOptions Discovery() => new() - { - CreateClientForEndpoint = CreateSchemaClient - }; - - public static ICultMeshTransportConnector[] SchemaConnectors() => new ICultMeshTransportConnector[] - { - new CultMeshTcpSchemaTransportConnector(), - new CultMeshUriSchemaTransportConnector( - "cultnet-websocket", - new[] { "ws", "wss" }, - _ => new CultNetWebSocketSchemaClient()) - }; - - public static ICultMeshContentTransportConnector[] ContentConnectors() => - new ICultMeshContentTransportConnector[] - { - new CultMeshHttpsContentTransportConnector(), - new CultMeshTcpContentTransportConnector() - }; - - private static ICultNetSchemaClient CreateSchemaClient(string endpoint) - { - if (Uri.TryCreate(endpoint, UriKind.Absolute, out var uri) && - (string.Equals(uri.Scheme, "ws", StringComparison.OrdinalIgnoreCase) || - string.Equals(uri.Scheme, "wss", StringComparison.OrdinalIgnoreCase))) - { - return new CultNetWebSocketSchemaClient(); - } - return CultNetSchemaClients.CreateForEndpoint(endpoint); - } - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshConnectivity.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshConnectivity.cs.meta deleted file mode 100644 index a701492..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshConnectivity.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 7f7cb06716fd4f319ade147a69acb74a diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index 5a403f4..04cbeb1 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -2,18 +2,16 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Security.Cryptography; using System.Collections.Concurrent; -using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using GameCult.Caching; -using GameCult.Eve.PluginFields; +using GameCult.Caching.MessagePack; using GameCult.Eve.Surface; -using GameCult.Eve.UnityScene.Fields; using GameCult.Mesh; -using GameCult.Mesh.Quic.Native; +using GameCult.Networking; using UnityEngine; -using Stopwatch = System.Diagnostics.Stopwatch; #nullable enable @@ -24,110 +22,62 @@ namespace GameCult.Eve.UnityScene IEveUnityGameObjectAssetProvider, IDisposable { - private const int AssetPresentationAttemptLimit = 3; + private const string RemoteShardId = "provider"; - private readonly string _cachePath; - private readonly string _rendezvousEndpoint; - private readonly CultMeshSessionTarget _target; + private static readonly Type[] WireDocumentTypes = + { + typeof(EveProviderAdvertisementDocument), + typeof(EveSurfaceDocument), + typeof(EveSurfaceCommandRequest), + typeof(EveCommandReceiptDocument), + typeof(EveAssetCatalogDocument), + typeof(EveEntitySoaViewDocument), + typeof(EveInputCapabilityDocument), + typeof(CultMeshCdnArtifactManifest), + typeof(CultMeshCdnArtifactChunk) + }; + + private readonly EveUnityCultMeshProviderSelection _selection; + private readonly CultMeshClient _mesh; + private readonly string _endpointId; private readonly string _providerId; private readonly string _surfaceId; private readonly string _runtimeId; - private readonly CultMeshAuthorityTrustPolicy _authorityTrust; - private readonly CultMeshAuthorityTrustPolicy _crossTargetAuthorityTrust; - private readonly Dictionary _pendingCommands = - new Dictionary(StringComparer.Ordinal); - private readonly Dictionary> _receiptSubscriptions = - new Dictionary>(StringComparer.Ordinal); - private readonly object _presentationFinalityGate = new object(); - private readonly Dictionary _deferredTerminalReceipts = - new Dictionary(StringComparer.Ordinal); - private readonly HashSet _continuousCommandIds = new HashSet(StringComparer.Ordinal); + private CultMeshSession? _documentSession; + private readonly HashSet _pendingCommandIds = new HashSet(StringComparer.Ordinal); + private readonly HashSet _publishedReceiptIds = new HashSet(StringComparer.Ordinal); + private readonly Dictionary _prefabs = new Dictionary(StringComparer.Ordinal); + private readonly Dictionary _nativeAssets = new Dictionary(StringComparer.Ordinal); + private readonly List _assetBundles = new List(); + private readonly Dictionary _cameraCullingMasks = new Dictionary(StringComparer.Ordinal); private readonly ConcurrentQueue _liveDocuments = new ConcurrentQueue(); - private readonly object _realtimeFrameGate = new object(); - private readonly Dictionary _latestRealtimeFrames = - new Dictionary(StringComparer.Ordinal); - private readonly HashSet _queuedRealtimeFrameBodies = - new HashSet(StringComparer.Ordinal); - private readonly Dictionary _embeddedSurfaces = - new Dictionary(StringComparer.Ordinal); - private readonly List _documentLeases = new List(); - private readonly List _documentWatches = new List(); - private readonly CancellationTokenSource _lifetime = new CancellationTokenSource(); - private readonly SemaphoreSlim _assetGenerationGate = new SemaphoreSlim(1, 1); - private CultMeshClient? _meshClient; - private bool _liveDocumentsReady; - private CultCache? _contentState; - private AssetGeneration _assetGeneration; + private readonly CancellationTokenSource _backgroundCancellation = new CancellationTokenSource(); + private CultNetDocumentRegistry? _networkRegistry; + private readonly List _liveWatches = new List(); + private bool _liveSubscriptionsOpen; private EveProviderAdvertisementDocument? _advertisement; private EveAdvertisedSurface? _advertisedSurface; - private EveSurfaceDocument? _baseSurface; - private CultMeshBodyPublicationResolver? _bodyResolver; - private EveEntitySoaViewDocument? _latestEntityLayout; - private CultMeshBodyPublicationDocument? _latestEntityPublication; - private CultMeshMappedFrameBodyCursor? _mappedEntityFrameCursor; - private CultMeshBodyDescriptor? _mappedEntityFrameContract; - private CancellationTokenSource? _realtimeLifetime; - private CancellationTokenSource? _surfaceAssetCandidateLifetime; - private Task? _realtimePump; - private CultMeshRealtimeSession? _realtimeSession; - private EveUnityCultMeshCommandOutbox? _commandOutbox; - private PendingAssetCatalogUpdate? _pendingAssetCatalog; - private bool _assetCatalogUpdateRunning; - private long _surfaceAssetGeneration; - private long _highestObservedBaseSurfaceVersion = -1; - private long _activeBaseSurfaceCandidateVersion = -1; - private long _activeBaseSurfaceCandidateGeneration = -1; - private int _lastAssetPresentationAttemptCount; - private long _mountedBaseSurfaceVersion; - private long _presentationBarrierVersion = -1; - private long _lastQueuedEntityViewEpoch = -1; - private long _lastQueuedEntityViewSequence = -1; - private long _lastPresentedEntityViewEpoch = -1; - private long _lastPresentedEntityViewSequence = -1; - private bool _bootstrapped; - private bool _disposed; + private string _assetCatalogPointer = ""; + private Task? _assetCatalogTask; + private EveEntitySoaViewDocument? _deferredEntityView; public EveUnityCultMeshLiveProviderTransport( - string cachePath, - string rendezvousEndpoint, - string verseId, - string authorityRuntimeId, - string providerId, - string surfaceId, - string runtimeId = "eve-unity", - CultMeshBodyPublicationResolver? bodyResolver = null, - CultMeshAuthorityTrustPolicy? authorityTrust = null, - CultMeshAuthorityTrustPolicy? crossTargetAuthorityTrust = null) + string replicaPath, + EveUnityCultMeshProviderSelection selection, + string runtimeId = "eve-unity") { - _cachePath = string.IsNullOrWhiteSpace(cachePath) - ? throw new ArgumentException("Cache path must be non-empty.", nameof(cachePath)) - : Path.GetFullPath(cachePath); - _rendezvousEndpoint = string.IsNullOrWhiteSpace(rendezvousEndpoint) - ? throw new ArgumentException("CultMesh rendezvous endpoint must be non-empty.", nameof(rendezvousEndpoint)) - : rendezvousEndpoint.Trim(); - _target = new CultMeshSessionTarget(verseId, authorityRuntimeId); - _providerId = string.IsNullOrWhiteSpace(providerId) - ? throw new ArgumentException("Provider id must be non-empty.", nameof(providerId)) - : providerId.Trim(); - _assetGeneration = new AssetGeneration( - new AssetSource(_target, _providerId, "", Array.Empty()), - sourceIdentity: "", - catalogVersion: -1, - meshClient: null); - _surfaceId = string.IsNullOrWhiteSpace(surfaceId) - ? throw new ArgumentException("Surface id must be non-empty.", nameof(surfaceId)) - : surfaceId.Trim(); + _ = replicaPath; + _selection = selection ?? throw new ArgumentNullException(nameof(selection)); + _mesh = selection.Mesh; + _endpointId = selection.EndpointId; + _providerId = selection.ProviderId; + _surfaceId = selection.SurfaceId; _runtimeId = string.IsNullOrWhiteSpace(runtimeId) ? "eve-unity" : runtimeId.Trim(); - _bodyResolver = bodyResolver; - _authorityTrust = authorityTrust ?? new CultMeshAuthorityTrustPolicy( - CultMeshAuthorityTrustMode.AuthenticatedRemote); - _crossTargetAuthorityTrust = crossTargetAuthorityTrust ?? _authorityTrust; CurrentSurfaceDocument = EmptySurfaceDocument(); CurrentAssetManifestDocument = EmptyAssetManifest(); - CurrentInputCapability = new EveInputCapabilityDocument(); } - public string TransportKind => "eve-cultmesh-identity-session"; + public string TransportKind => "eve-cultmesh-remote-replica"; public string SurfacePointer => CurrentSurfaceDocument.SourcePointer; @@ -137,9 +87,7 @@ namespace GameCult.Eve.UnityScene public EveUnityPlayableWorldAssetManifestDocument CurrentAssetManifestDocument { get; private set; } - public EveInputCapabilityDocument CurrentInputCapability { get; private set; } - - public CultMeshBodyTransportKind? CurrentAssetBodyTransportKind => _assetGeneration.BodyTransportKind; + public EveInputCapabilityDocument? CurrentInputCapability { get; private set; } public event Action? SurfaceDocumentAvailable; @@ -147,24 +95,12 @@ namespace GameCult.Eve.UnityScene public event Action? CommandReceiptAvailable; - public event Action? EntityViewAvailable; - - public event Action? FieldsSplatsAvailable; - - private static CultMeshBodyPublicationHandle BodyPublicationHandle(EveEntitySoaViewDocument document) - { - if (document == null) throw new ArgumentNullException(nameof(document)); - if (document.Buffers == null || document.Buffers.Length == 0) - throw new InvalidOperationException("Entity layout does not name a primary logical buffer."); - return new CultMeshBodyPublicationHandle( - document.Buffers[0].BufferId, - document.ProducerEpoch, - document.Sequence); - } + public event Action? EntityViewAvailable; public void Connect() { - EnsurePrepared(); + EnsureOpen(); + Refresh(); } public void Disconnect() @@ -173,197 +109,43 @@ namespace GameCult.Eve.UnityScene public void Refresh() { - EnsurePrepared(); - PumpLiveEvents(); - } - - public async Task PrepareAsync(CancellationToken cancellationToken = default) - { - if (_bootstrapped) - return; - - try - { - EnsureOpen(); - var elapsed = Stopwatch.StartNew(); - await ResolveAdvertisementAsync(forceRefresh: true, cancellationToken); - TraceStartup("advertisement", elapsed); - await RefreshSurfaceAsync(cancellationToken); - TraceStartup("surface", elapsed); - await RefreshAssetCatalogAsync(cancellationToken); - TraceStartup("asset-catalog-and-bundles", elapsed); - await EnsureLiveDocumentsAsync(cancellationToken); - TraceStartup("subscriptions", elapsed); - _commandOutbox = new EveUnityCultMeshCommandOutbox( - SendCommandAsync, - ContinuousCommandKey, - ForgetPendingCommand); - _bootstrapped = true; - } - catch (Exception error) when (error is IOException || error is SocketException || error is TimeoutException) - { - throw new InvalidOperationException( - $"CultMesh provider '{_providerId}' bootstrap through target '{_target}' failed.", - error); - } - } - - private void EnsurePrepared() - { - if (!_bootstrapped) - throw new InvalidOperationException( - "The EveUnity CultMesh transport is not prepared. Await PrepareAsync before mounting or connecting it."); - } - - private static void TraceStartup(string phase, Stopwatch elapsed) - { - if (string.Equals(Environment.GetEnvironmentVariable("EVEUNITY_TRACE_STARTUP_PHASES"), "1", StringComparison.Ordinal)) - Debug.Log($"EveUnity startup phase {phase} took {elapsed.Elapsed.TotalMilliseconds:0.###}ms."); - elapsed.Restart(); - } - - private static string FindComponentProp(EveSurfaceComponent component, string key) - { - var value = component.GetProp(key); - if (!string.IsNullOrWhiteSpace(value)) - return value; - foreach (var child in component.Children) - { - value = FindComponentProp(child, key); - if (!string.IsNullOrWhiteSpace(value)) - return value; - } - return ""; + EnsureOpen(); + ResolveAdvertisement(); + RefreshSurface(); + RefreshAssetCatalog(); } public void PumpLiveEvents() { while (_liveDocuments.TryDequeue(out var document)) { - if (document is PendingEntityGeneration entityGeneration) - PublishEntityView(entityGeneration.View, entityGeneration.Publication); - else if (document is PendingRealtimeEntityFrame realtimeFrame) - PublishPendingRealtimeEntityFrame(realtimeFrame.BodyId); - else if (document is EveFieldsSplatsDocument fields) - FieldsSplatsAvailable?.Invoke(fields); - else if (document is PendingSurfaceDocument surface) + if (document is EveEntitySoaViewDocument entityView) { - if (string.Equals(surface.RecordKey, _advertisedSurface?.RecordRef, StringComparison.Ordinal)) - PublishBaseSurface(surface.Document); + if (!string.IsNullOrWhiteSpace(_assetCatalogPointer) && CurrentAssetCatalogVersion < 0) + _deferredEntityView = entityView; else - PublishEmbeddedSurface(surface.RecordKey, surface.Document); + EntityViewAvailable?.Invoke(entityView); } + else if (document is EveSurfaceDocument surface) + PublishSurface(surface); + else if (document is EveCommandReceiptDocument receipt) + PublishReceipt(receipt); else if (document is EveInputCapabilityDocument inputCapability) CurrentInputCapability = inputCapability; else if (document is EveAssetCatalogDocument assetCatalog) - QueueAssetCatalogUpdate(CurrentAssetSourceIdentity, assetCatalog); - else if (document is EveCommandReceiptDocument receipt) - PublishReceipt(receipt); - else if (document is Exception error) - throw error; + ApplyAssetCatalog(assetCatalog); + else if (document is PreparedAssetCatalog preparedAssetCatalog) + ApplyPreparedAssetCatalog(preparedAssetCatalog); + else if (document is BackgroundFailure failure) + throw new InvalidOperationException(failure.Operation + " failed.", failure.Error); } - PumpMappedEntityFrame(); - } - - private void QueueEntityView(EveEntitySoaViewDocument document) - { - TraceHotState($"layout epoch={document.ProducerEpoch} sequence={document.Sequence} identities={document.Identities?.Length ?? 0}"); - _latestEntityLayout = document; - var publication = _latestEntityPublication; - if (publication != null && PublicationMatchesLayout(publication, document)) - QueueEntityGeneration(document, publication); - } - - private void QueueBodyPublication(CultMeshBodyPublicationDocument publication) - { - _latestEntityPublication = publication; - var layout = _latestEntityLayout; - if (layout == null || layout.Buffers == null || layout.Buffers.Length == 0 || - !string.Equals(layout.Buffers[0].BufferId, publication.BodyId, StringComparison.Ordinal)) - { - TraceHotState($"publication skipped body={publication.BodyId} sequence={publication.Sequence} layout={(layout == null ? "missing" : "incompatible")}"); - return; - } - - TraceHotState($"publication body={publication.BodyId} epoch={publication.ProducerEpoch} sequence={publication.Sequence} plane={string.Join(",", publication.Representations.Select(value => value.TransportKind))}"); - - var generation = PublicationMatchesLayout(publication, layout) - ? layout - : MaterializeEntityGeneration(layout, publication.ProducerEpoch, publication.Sequence); - QueueEntityGeneration(generation, publication); - } - - private void QueueEntityGeneration( - EveEntitySoaViewDocument document, - CultMeshBodyPublicationDocument publication) - { - if (document.ProducerEpoch < _lastQueuedEntityViewEpoch || - (document.ProducerEpoch == _lastQueuedEntityViewEpoch && - document.Sequence <= _lastQueuedEntityViewSequence)) - return; - _lastQueuedEntityViewEpoch = document.ProducerEpoch; - _lastQueuedEntityViewSequence = document.Sequence; - _liveDocuments.Enqueue(new PendingEntityGeneration(document, publication)); - } - - private static bool PublicationMatchesLayout( - CultMeshBodyPublicationDocument publication, - EveEntitySoaViewDocument layout) => - publication.ProducerEpoch == layout.ProducerEpoch && - publication.Sequence == layout.Sequence; - - private sealed class PendingEntityGeneration - { - public PendingEntityGeneration( - EveEntitySoaViewDocument view, - CultMeshBodyPublicationDocument publication) - { - View = view; - Publication = publication; - } - - public EveEntitySoaViewDocument View { get; } - public CultMeshBodyPublicationDocument Publication { get; } - } - - private static EveEntitySoaViewDocument MaterializeEntityGeneration( - EveEntitySoaViewDocument layout, - long producerEpoch, - long sequence) - { - return new EveEntitySoaViewDocument - { - Schema = layout.Schema, - ProviderId = layout.ProviderId, - ViewId = layout.ViewId, - PublishedAtUtc = DateTimeOffset.UtcNow.ToString("O"), - BodySchemaId = layout.BodySchemaId, - LayoutVersion = layout.LayoutVersion, - ProducerEpoch = producerEpoch, - Sequence = sequence, - Capacity = layout.Capacity, - Buffers = layout.Buffers, - Columns = layout.Columns, - DirtyRanges = (layout.DirtyRanges ?? Array.Empty()) - .Select(range => new EveEntitySoaDirtyRange - { - ColumnId = range.ColumnId, - StartIndex = range.StartIndex, - Count = range.Count, - Sequence = sequence - }) - .ToArray(), - RenderGroups = layout.RenderGroups, - Identities = layout.Identities, - FrameId = sequence - }; } public void SubmitCommand(EveSurfaceCommandRequest request) { if (request == null) throw new ArgumentNullException(nameof(request)); - EnsurePrepared(); - EnsurePresentationWritable(); + EnsureOpen(); + ResolveAdvertisement(); var interaction = RequireWorldInteraction(); if (string.IsNullOrWhiteSpace(interaction.CommandRecordRef)) throw new InvalidOperationException("The provider advertisement does not publish a command record reference."); @@ -377,629 +159,124 @@ namespace GameCult.Eve.UnityScene if (string.IsNullOrWhiteSpace(commandId)) throw new InvalidOperationException("Eve command invocations require an idempotency key."); - lock (_pendingCommands) + var recordKey = ChildRecordKey(interaction.CommandRecordRef, commandId); + if (string.IsNullOrWhiteSpace(interaction.ReceiptRecordRef)) + throw new InvalidOperationException("The provider advertisement does not publish a receipt record reference."); + var receiptKey = ChildRecordKey(interaction.ReceiptRecordRef, commandId); + var receipt = _mesh.DocumentAsync(_endpointId, receiptKey) + .GetAwaiter().GetResult(); + _liveWatches.Add(receipt.Watch(value => _liveDocuments.Enqueue(value))); + var message = _networkRegistry!.CreateRawDocumentPutMessage( + $"eve-unity-{commandId}", + new CultRecordHandle(new CultRecordKey(recordKey)), + request, + new CultNetDocumentMessageOptions + { + SourceRuntimeId = _runtimeId, + SourceRole = "eve-unity" + }); + message.ShardId = RemoteShardId; + message.ShardEpoch = 1; + _documentSession!.SendCultNet(message); + _pendingCommandIds.Add(commandId); + _ = AwaitReceiptAsync(receiptKey, commandId, _backgroundCancellation.Token); + } + + private async Task AwaitReceiptAsync(string receiptKey, string commandId, CancellationToken cancellationToken) + { + for (var attempt = 0; attempt < 60 && !cancellationToken.IsCancellationRequested; attempt++) { - if (_pendingCommands.ContainsKey(commandId)) - throw new InvalidOperationException($"Eve command '{commandId}' is already pending."); - _pendingCommands.Add(commandId, request); + try + { + var receipt = await _mesh.ReadAsync( + _endpointId, + receiptKey, + TimeSpan.FromSeconds(2), + cancellationToken) + .ConfigureAwait(false); + if (string.Equals(receipt.CommandId, commandId, StringComparison.Ordinal)) + { + _liveDocuments.Enqueue(receipt); + return; + } + } + catch (InvalidOperationException) + { + } + try + { + await Task.Delay(100, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + return; + } } - try { _commandOutbox!.Enqueue(request); } - catch - { - ForgetPendingCommand(commandId); - throw; - } - } - - private Task SendCommandAsync(EveSurfaceCommandRequest request, CancellationToken cancellationToken) - { - return SendCommandWithReceiptLeaseAsync(request, cancellationToken); - } - - private async Task SendCommandWithReceiptLeaseAsync( - EveSurfaceCommandRequest request, - CancellationToken cancellationToken) - { - await EnsureReceiptSubscriptionAsync(request.CommandId, cancellationToken).ConfigureAwait(false); - var interaction = RequireWorldInteraction(); - var recordKey = ChildRecordKey(interaction.CommandRecordRef, request.CommandId); - await _meshClient!.SubmitDocumentAsync( - _target, - recordKey, - request, - _runtimeId, - "eve-unity", - cancellationToken) - .ConfigureAwait(false); - } - - private string? ContinuousCommandKey(EveSurfaceCommandRequest request) - { - if (!request.PayloadFields.TryGetValue("commandId", out var commandId) || - !_continuousCommandIds.Contains(commandId)) - return null; - request.PayloadFields.TryGetValue("entityId", out var entityId); - return commandId + "\u001f" + (entityId ?? ""); - } - - private void ForgetPendingCommand(string commandId) - { - Task? subscription = null; - lock (_pendingCommands) - { - _pendingCommands.Remove(commandId); - if (_receiptSubscriptions.TryGetValue(commandId, out subscription)) - _receiptSubscriptions.Remove(commandId); - } - if (subscription != null) DisposeReceiptSubscription(subscription); } public void Dispose() { - _disposed = true; - _lifetime.Cancel(); - _commandOutbox?.Dispose(); - _commandOutbox = null; - _assetGeneration.Dispose(); - _mappedEntityFrameCursor?.Dispose(); - _mappedEntityFrameCursor = null; - _mappedEntityFrameContract = null; - _realtimeLifetime?.Cancel(); - _surfaceAssetCandidateLifetime?.Cancel(); - _surfaceAssetCandidateLifetime?.Dispose(); - _surfaceAssetCandidateLifetime = null; - _realtimeSession?.Dispose(); - _realtimeSession = null; - _realtimePump = null; - _realtimeLifetime?.Dispose(); - _realtimeLifetime = null; - DisposeLiveDocuments(); - _contentState?.Dispose(); - _contentState = null; - _meshClient?.Dispose(); - _meshClient = null; - _baseSurface = null; - _embeddedSurfaces.Clear(); - lock (_pendingCommands) - { - _pendingCommands.Clear(); - foreach (var subscription in _receiptSubscriptions.Values) - DisposeReceiptSubscription(subscription); - _receiptSubscriptions.Clear(); - } - lock (_presentationFinalityGate) - { - _deferredTerminalReceipts.Clear(); - _presentationBarrierVersion = -1; - } - _lifetime.Dispose(); + _backgroundCancellation.Cancel(); + foreach (var bundle in _assetBundles) bundle.Unload(unloadAllLoadedObjects: false); + _assetBundles.Clear(); + _prefabs.Clear(); + _nativeAssets.Clear(); + foreach (var watch in _liveWatches) watch.Dispose(); + _liveWatches.Clear(); + _mesh.Dispose(); + _documentSession = null; + _networkRegistry = null; + _pendingCommandIds.Clear(); + _publishedReceiptIds.Clear(); + _backgroundCancellation.Dispose(); } public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) { if (asset == null) throw new ArgumentNullException(nameof(asset)); - EnsureAssetPrepared(asset.AssetRef); - return _assetGeneration.Prefabs.TryGetValue(asset.AssetRef, out var prefab) ? prefab : null; + return _prefabs.TryGetValue(asset.AssetRef, out var prefab) ? prefab : null; } public UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType) { if (asset == null) throw new ArgumentNullException(nameof(asset)); if (assetType == null) throw new ArgumentNullException(nameof(assetType)); - EnsureAssetPrepared(asset.AssetRef); - return _assetGeneration.NativeAssets.TryGetValue(asset.AssetRef, out var value) && assetType.IsInstanceOfType(value) + return _nativeAssets.TryGetValue(asset.AssetRef, out var value) && assetType.IsInstanceOfType(value) ? value : null; } - private void EnsureAssetPrepared(string assetRef) + public bool TryGetCameraCullingMask(string viewId, out int cullingMask) { - EnsurePrepared(); - if (_assetGeneration.AssetSelections.ContainsKey(assetRef) && - !_assetGeneration.NativeAssets.ContainsKey(assetRef)) - throw new InvalidOperationException( - $"Provider asset '{assetRef}' was not materialized during asynchronous preparation."); - } - - public bool TryResolveAssetMetadata( - EveUnityPlayableWorldAssetBinding asset, - out IReadOnlyDictionary metadata) - { - if (asset == null) throw new ArgumentNullException(nameof(asset)); - return _assetGeneration.NativeAssetMetadata.TryGetValue(asset.AssetRef, out metadata!); - } - - public bool TryGetRenderChannelLayer(string channel, out int layer) - { - return _assetGeneration.RenderChannelLayers.TryGetValue(channel ?? "", out layer); + return _cameraCullingMasks.TryGetValue(viewId ?? "", out cullingMask); } private void EnsureOpen() { - if (_meshClient != null) - return; - _meshClient = CreateMeshClient(new[] { _rendezvousEndpoint }, _authorityTrust); - } - - private CultMeshClient CreateMeshClient( - IReadOnlyList rendezvousEndpoints, - CultMeshAuthorityTrustPolicy authorityTrust) - { - return new CultMeshClient(new CultMeshClientOptions - { - RendezvousEndpoints = rendezvousEndpoints, - Discovery = EveUnityCultMeshConnectivity.Discovery(), - Sessions = new CultMeshSessionManagerOptions - { - Trust = authorityTrust ?? throw new ArgumentNullException(nameof(authorityTrust)) - }, - Connectors = EveUnityCultMeshConnectivity.SchemaConnectors(), - ContentConnectors = EveUnityCultMeshConnectivity.ContentConnectors(), - RealtimeConnectors = new ICultMeshRealtimeTransportConnector[] - { - new CultMeshNativeQuicRealtimeTransportConnector() - } - }); - } - - private CultMeshClient AssetMeshClient(AssetGeneration? generation = null) => - (generation ?? _assetGeneration).MeshClient ?? - _meshClient ?? throw new InvalidOperationException("The CultMesh client is not open."); - - private async Task ResolveAdvertisementAsync( - bool forceRefresh = false, - CancellationToken cancellationToken = default) - { - if (!forceRefresh && _advertisement != null && _advertisedSurface != null) - return; - using var lease = await _meshClient! - .LeaseCollectionAsync(_target, cancellationToken); - var documents = await lease.Handle.LatestAsync(); - _advertisement = documents.FirstOrDefault(document => - string.Equals(document.ProviderId, _providerId, StringComparison.Ordinal)) - ?? throw new InvalidOperationException($"Provider '{_providerId}' did not publish an Eve advertisement."); - _advertisedSurface = _advertisement.Surfaces.FirstOrDefault(surface => - string.Equals(surface.SurfaceId, _surfaceId, StringComparison.Ordinal)) - ?? throw new InvalidOperationException($"Provider '{_providerId}' did not advertise surface '{_surfaceId}'."); - if (!string.Equals(_advertisedSurface.Transport, "cultmesh-record", StringComparison.Ordinal)) - throw new InvalidOperationException("EveUnity CultMesh transport requires a cultmesh-record surface advertisement."); - if (string.IsNullOrWhiteSpace(_advertisedSurface.RecordRef)) - throw new InvalidOperationException("The provider surface advertisement does not publish a record reference."); - } - - private CultMeshBodyPublicationResolver CreateBodyResolver() - { - var producerIds = RequireAdvertisedBodyProducerIds(_advertisement); - if (string.IsNullOrWhiteSpace(_advertisement!.ServiceId)) - throw new InvalidOperationException( - $"Eve provider '{_advertisement.ProviderId}' has no service-instance identity for body transport."); - var mappedRoot = _cachePath; - var networkBodies = _meshClient!.BodyProvider( - _advertisement.ProviderId, - _target, - new CultMeshSessionBodyProviderOptions { ResponseTimeout = TimeSpan.FromSeconds(2) }); - return new CultMeshBodyPublicationResolver(new CultMeshBodyTransportService( - new ICultMeshBodyTransportAdapter[] - { - new CultMeshSharedMemoryBodyAdapter(), - new CultMeshMappedBodyAdapter(mappedRoot), - new CultMeshNetworkBodyAdapter(networkBodies) - }, - (candidateProducerId, _) => IsAdvertisedBodyProducer(producerIds, candidateProducerId))); - } - - private static bool IsAdvertisedBodyProducer( - IReadOnlyList advertisedProducerIds, - string candidateProducerId) => - advertisedProducerIds.Any(producerId => - string.Equals(candidateProducerId, producerId, StringComparison.Ordinal)); - - private static IReadOnlyList RequireAdvertisedBodyProducerIds(EveProviderAdvertisementDocument? advertisement) - { - if (advertisement == null) - throw new InvalidOperationException("The Eve provider advertisement must be resolved before body transport authorization."); - var producerIds = advertisement.AuthorizedBodyProducerIds - .Where(producerId => !string.IsNullOrWhiteSpace(producerId)) - .Distinct(StringComparer.Ordinal) - .ToArray(); - if (producerIds.Length == 0) - throw new InvalidOperationException( - $"Eve provider '{advertisement.ProviderId}' does not advertise an authorized body producer."); - return producerIds; - } - - private static string NormalizeContentHash(string value) - { - if (string.IsNullOrWhiteSpace(value)) - throw new InvalidDataException("CultMesh content hash is missing."); - var normalized = value.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase) - ? value.Substring("sha256:".Length) - : value; - return normalized.ToLowerInvariant(); - } - - private void PublishEntityView( - EveEntitySoaViewDocument document, - CultMeshBodyPublicationDocument publication) - { - EnsurePrepared(); - if (!string.Equals(document.ProviderId, _advertisement!.ProviderId, StringComparison.Ordinal)) - throw new UnauthorizedAccessException( - $"Entity layout provider '{document.ProviderId}' does not match advertised Eve provider '{_advertisement.ProviderId}'."); - if (document.Buffers == null || document.Buffers.Length == 0) - throw new InvalidOperationException("Entity layout does not name a primary logical buffer."); - var handle = BodyPublicationHandle(document); - handle.Validate(publication); - var now = DateTimeOffset.UtcNow; - if (!IsPublicationLive(publication, now)) - return; - EnsureMappedEntityFrameCursor(publication); - if (_mappedEntityFrameCursor != null) - return; - EnsureRealtimeEntityState(); - } - - private void EnsureMappedEntityFrameCursor(CultMeshBodyPublicationDocument publication) - { - var descriptor = (publication.Representations ?? Array.Empty()) - .FirstOrDefault(CultMeshMappedFrameBodyCursor.CanOpen); - if (descriptor == null) - { - _mappedEntityFrameCursor?.Dispose(); - _mappedEntityFrameCursor = null; - _mappedEntityFrameContract = null; - return; - } - - var current = _mappedEntityFrameContract; - if (current != null && - string.Equals(FrameCapabilityIdentity(current.CapabilityToken), - FrameCapabilityIdentity(descriptor.CapabilityToken), StringComparison.Ordinal) && - string.Equals(current.BodyId, descriptor.BodyId, StringComparison.Ordinal) && - string.Equals(current.SchemaId, descriptor.SchemaId, StringComparison.Ordinal) && - current.LayoutVersion == descriptor.LayoutVersion && - current.Capacity == descriptor.Capacity && - current.ProducerEpoch == descriptor.ProducerEpoch && - current.ByteSize == descriptor.ByteSize) + if (_documentSession != null) return; - _mappedEntityFrameCursor?.Dispose(); - try - { - _mappedEntityFrameCursor = new CultMeshMappedFrameBodyCursor(descriptor); - _mappedEntityFrameContract = descriptor; - TraceHotState( - $"mapped frame cursor body={descriptor.BodyId} epoch={descriptor.ProducerEpoch} sequence={descriptor.Sequence}"); - } - catch (Exception error) when ( - error is IOException || - error is UnauthorizedAccessException || - error is InvalidOperationException) - { - _mappedEntityFrameCursor = null; - _mappedEntityFrameContract = null; - TraceHotState($"mapped frame unavailable; selecting remote realtime plane: {error.Message}"); - } + var cacheRegistry = CultMesh.CreateCultCacheDocumentRegistry(WireDocumentTypes); + _networkRegistry = CultMesh.CreateCultNetDocumentRegistry(WireDocumentTypes, cacheRegistry); + _documentSession = _mesh.ConnectAsync(_endpointId, CultMeshProtocols.Documents) + .GetAwaiter().GetResult(); } - private void EnsureRealtimeEntityState() + private void ResolveAdvertisement() { - if (_realtimePump != null) - return; - if (_advertisement == null || string.IsNullOrWhiteSpace(_advertisement.VerseId)) - throw new InvalidOperationException( - $"Eve provider '{_providerId}' does not advertise the Verse identity required for realtime state."); - _realtimeLifetime = new CancellationTokenSource(); - if (!string.Equals(_advertisement.VerseId, _target.VerseId, StringComparison.Ordinal)) - throw new InvalidOperationException( - $"Eve provider '{_providerId}' advertisement Verse '{_advertisement.VerseId}' does not match selected Verse '{_target.VerseId}'."); - _realtimePump = Task.Run(() => PumpRealtimeEntityStateAsync(_realtimeLifetime.Token)); + _advertisement = _selection.Surface.Provider; + _advertisedSurface = _selection.Surface.Advertisement; } - private async Task PumpRealtimeEntityStateAsync(CancellationToken cancellationToken) + private void RefreshSurface() { - try - { - _realtimeSession = await _meshClient! - .ConnectRealtimeAsync(_target, cancellationToken) - .ConfigureAwait(false); - TraceHotState($"realtime state connected transport={_realtimeSession.TransportId} target={_target}"); - while (!cancellationToken.IsCancellationRequested) - { - var frame = await _realtimeSession.ReceiveAsync(cancellationToken).ConfigureAwait(false); - EnqueueRealtimeEntityFrame(frame); - } - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - } - catch (ObjectDisposedException) when (_disposed) - { - } - catch (Exception error) - { - _liveDocuments.Enqueue(error); - } + var surface = _selection.Surface.Surface.LatestAsync().GetAwaiter().GetResult(); + PublishSurface(surface); } - private void EnqueueRealtimeEntityFrame(CultMeshRealtimeFrame frame) + private void PublishSurface(EveSurfaceDocument surface) { - if (frame == null) - throw new ArgumentNullException(nameof(frame)); - lock (_realtimeFrameGate) - { - if (_latestRealtimeFrames.TryGetValue(frame.BodyId, out var current) && - (frame.ProducerEpoch < current.ProducerEpoch || - (frame.ProducerEpoch == current.ProducerEpoch && frame.Sequence <= current.Sequence))) - return; - _latestRealtimeFrames[frame.BodyId] = frame; - if (_queuedRealtimeFrameBodies.Add(frame.BodyId)) - _liveDocuments.Enqueue(new PendingRealtimeEntityFrame(frame.BodyId)); - } - } - - private void PublishPendingRealtimeEntityFrame(string bodyId) - { - CultMeshRealtimeFrame? frame; - lock (_realtimeFrameGate) - { - _queuedRealtimeFrameBodies.Remove(bodyId); - if (!_latestRealtimeFrames.TryGetValue(bodyId, out frame)) - return; - _latestRealtimeFrames.Remove(bodyId); - } - PublishRealtimeEntityFrame(frame); - } - - private void PublishRealtimeEntityFrame(CultMeshRealtimeFrame frame) - { - var layout = _latestEntityLayout - ?? throw new InvalidOperationException("Realtime entity state arrived before its Eve SoA layout."); - var publication = _latestEntityPublication - ?? throw new InvalidOperationException("Realtime entity state arrived before its body publication."); - if (!string.Equals(frame.BodyId, publication.BodyId, StringComparison.Ordinal) || - !string.Equals(frame.SchemaId, layout.BodySchemaId, StringComparison.Ordinal)) - throw new UnauthorizedAccessException("Realtime entity state does not match the advertised Eve body contract."); - if (frame.Delivery != CultMeshRealtimeDelivery.LatestOnly) - throw new InvalidDataException("Playable entity state requires latest-only QUIC delivery."); - if (frame.ProducerEpoch < _lastPresentedEntityViewEpoch || - (frame.ProducerEpoch == _lastPresentedEntityViewEpoch && - frame.Sequence <= _lastPresentedEntityViewSequence)) - return; - var representation = (publication.Representations ?? Array.Empty()) - .FirstOrDefault(value => - string.Equals(value.BodyId, frame.BodyId, StringComparison.Ordinal) && - string.Equals(value.SchemaId, frame.SchemaId, StringComparison.Ordinal)) - ?? throw new InvalidDataException("Realtime entity state has no advertised body representation."); - if (frame.Payload.Length != representation.ByteSize) - throw new InvalidDataException( - $"Realtime entity body size {frame.Payload.Length} does not match advertised size {representation.ByteSize}."); - - var generation = MaterializeEntityGeneration(layout, frame.ProducerEpoch, frame.Sequence); - var lease = new RealtimeEntityBodyLease( - new CultMeshBodyDescriptor - { - BodyId = frame.BodyId, - SchemaId = frame.SchemaId, - LayoutVersion = layout.LayoutVersion, - ByteSize = frame.Payload.Length, - Capacity = layout.Capacity, - ProducerEpoch = frame.ProducerEpoch, - Sequence = frame.Sequence, - AccessMode = CultMeshBodyAccessMode.ReadOnly, - Synchronization = CultMeshBodySynchronization.EpochSequence, - LeaseExpiresAtUnixMs = DateTimeOffset.UtcNow.AddSeconds(2).ToUnixTimeMilliseconds(), - TransportKind = CultMeshBodyTransportKind.Network, - CapabilityToken = "authenticated-quic", - SemanticHash = "" - }, - frame.Payload.ToArray()); - var handler = EntityViewAvailable; - if (handler == null) - { - lease.Dispose(); - return; - } - try - { - handler(generation, lease); - _lastPresentedEntityViewEpoch = frame.ProducerEpoch; - _lastPresentedEntityViewSequence = frame.Sequence; - } - catch - { - lease.Dispose(); - throw; - } - } - - private sealed class PendingRealtimeEntityFrame - { - public PendingRealtimeEntityFrame(string bodyId) => - BodyId = string.IsNullOrWhiteSpace(bodyId) - ? throw new ArgumentException("Body id must be non-empty.", nameof(bodyId)) - : bodyId; - - public string BodyId { get; } - } - - private sealed class RealtimeEntityBodyLease : ICultMeshBodyReadLease - { - private byte[] _bytes; - - public RealtimeEntityBodyLease(CultMeshBodyDescriptor descriptor, byte[] bytes) - { - Descriptor = descriptor; - _bytes = bytes; - } - - public CultMeshBodyDescriptor Descriptor { get; } - public CultMeshBodyTransportKind TransportKind => CultMeshBodyTransportKind.Network; - public byte ReadByte(long offset) { Validate(offset, 1); return _bytes[(int)offset]; } - public int ReadInt32(long offset) { Validate(offset, 4); return BitConverter.ToInt32(_bytes, (int)offset); } - public long ReadInt64(long offset) { Validate(offset, 8); return BitConverter.ToInt64(_bytes, (int)offset); } - public float ReadSingle(long offset) { Validate(offset, 4); return BitConverter.ToSingle(_bytes, (int)offset); } - public double ReadDouble(long offset) { Validate(offset, 8); return BitConverter.ToDouble(_bytes, (int)offset); } - - public int CopyTo(long offset, byte[] destination, int destinationOffset, int count) - { - Validate(offset, count); - Buffer.BlockCopy(_bytes, (int)offset, destination, destinationOffset, count); - return count; - } - - public void Dispose() => _bytes = Array.Empty(); - - private void Validate(long offset, int count) - { - if (offset < 0 || count < 0 || offset > _bytes.LongLength - count) - throw new ArgumentOutOfRangeException(nameof(offset)); - } - } - - private void PumpMappedEntityFrame() - { - var cursor = _mappedEntityFrameCursor; - var layout = _latestEntityLayout; - if (cursor == null || layout == null || !cursor.TryAcquireLatest(out var lease)) - return; - - var descriptor = lease.Descriptor; - if (descriptor.ProducerEpoch < _lastPresentedEntityViewEpoch || - (descriptor.ProducerEpoch == _lastPresentedEntityViewEpoch && - descriptor.Sequence <= _lastPresentedEntityViewSequence)) - { - lease.Dispose(); - return; - } - - var generation = MaterializeEntityGeneration( - layout, - descriptor.ProducerEpoch, - descriptor.Sequence); - var handler = EntityViewAvailable; - if (handler == null) - { - lease.Dispose(); - return; - } - try - { - handler(generation, lease); - _lastPresentedEntityViewEpoch = descriptor.ProducerEpoch; - _lastPresentedEntityViewSequence = descriptor.Sequence; - } - catch - { - lease.Dispose(); - throw; - } - } - - private static string FrameCapabilityIdentity(string capabilityToken) - { - var separator = capabilityToken.LastIndexOf('.'); - return separator > 0 ? capabilityToken.Substring(0, separator) : capabilityToken; - } - - private static bool IsPublicationLive( - CultMeshBodyPublicationDocument publication, - DateTimeOffset now) => - publication.LivenessExpiresAtUnixMs > now.ToUnixTimeMilliseconds(); - - private async Task RefreshSurfaceAsync(CancellationToken cancellationToken) - { - var surface = await _meshClient! - .ReadAsync(_target, _advertisedSurface!.RecordRef, cancellationToken); - PublishBaseSurface(surface); - } - - private void PublishBaseSurface(EveSurfaceDocument surface) - { - if (surface == null) throw new ArgumentNullException(nameof(surface)); - if (!TryBeginBaseSurfaceCandidate(surface.Version, out var generation)) - return; - CancellationToken candidateToken; - lock (_presentationFinalityGate) - { - _surfaceAssetCandidateLifetime?.Cancel(); - _surfaceAssetCandidateLifetime?.Dispose(); - _surfaceAssetCandidateLifetime = CancellationTokenSource.CreateLinkedTokenSource(_lifetime.Token); - candidateToken = _surfaceAssetCandidateLifetime.Token; - } - if (!_bootstrapped) - { - _baseSurface = surface; - PublishComposedSurface(surface.Version); - return; - } - - var source = ResolveAssetSource(surface); - if (string.Equals(source.Identity, CurrentAssetSourceIdentity, StringComparison.Ordinal) && - (source.RequiredCatalogVersion <= 0 || source.RequiredCatalogVersion == CurrentAssetCatalogVersion)) - { - _baseSurface = surface; - PublishComposedSurface(surface.Version); - return; - } - - // A surface and its provider-qualified asset catalog are one presentation - // candidate. Keep the previous surface mounted until the candidate assets - // have resolved and preloaded; never lower a remote Verse surface against - // the previous Verse's catalog merely because the surface record arrived first. - BeginPresentationTransition(surface.Version); - _ = RefreshAssetsForSurfaceAsync(surface, source, generation, candidateToken); - } - - private bool TryBeginBaseSurfaceCandidate(long version, out long generation) - { - lock (_presentationFinalityGate) - { - generation = -1; - if (version < _highestObservedBaseSurfaceVersion || - version <= _mountedBaseSurfaceVersion || - (version == _highestObservedBaseSurfaceVersion && - _activeBaseSurfaceCandidateVersion == version)) - return false; - _highestObservedBaseSurfaceVersion = Math.Max(_highestObservedBaseSurfaceVersion, version); - generation = Interlocked.Increment(ref _surfaceAssetGeneration); - _activeBaseSurfaceCandidateVersion = version; - _activeBaseSurfaceCandidateGeneration = generation; - return true; - } - } - - private void RetireFailedBaseSurfaceCandidate(long version, long generation) - { - lock (_presentationFinalityGate) - { - if (_activeBaseSurfaceCandidateVersion != version || - _activeBaseSurfaceCandidateGeneration != generation) - return; - _activeBaseSurfaceCandidateVersion = -1; - _activeBaseSurfaceCandidateGeneration = -1; - } - } - - private void PublishEmbeddedSurface(string recordKey, EveSurfaceDocument surface) - { - if (string.IsNullOrWhiteSpace(recordKey)) - throw new InvalidOperationException("An embedded Eve surface update has no record identity."); - _embeddedSurfaces[recordKey] = surface ?? throw new ArgumentNullException(nameof(surface)); - PublishComposedSurface(); - } - - private void PublishComposedSurface(long? committedBaseVersion = null) - { - ApplyPreparedSurface(PrepareComposedSurface( - _baseSurface ?? throw new InvalidOperationException("The base Eve surface is unavailable.")), - committedBaseVersion); - } - - private PreparedSurface PrepareComposedSurface(EveSurfaceDocument baseSurface) - { - var surface = ComposeSurface(baseSurface, _embeddedSurfaces); var interaction = RequireWorldInteraction(); - var document = new EveUnitySceneProviderSurfaceDocument( + CurrentSurfaceDocument = new EveUnitySceneProviderSurfaceDocument( surface, new EveUnitySceneProviderSurfaceAdvertisement( _advertisedSurface.SurfaceId, @@ -1011,989 +288,188 @@ namespace GameCult.Eve.UnityScene interaction.Ownership)), _advertisedSurface.RecordRef, surface.Version); - var lowered = new EveUnitySceneSurfaceLowerer() - .Lower(document.SurfaceDocument, document.AdvertisedSurface); - var continuousCommands = new HashSet(StringComparer.Ordinal); - if (!string.IsNullOrWhiteSpace(lowered.PlayableWorld?.MovementCommand)) - continuousCommands.Add(lowered.PlayableWorld.MovementCommand); - if (!string.IsNullOrWhiteSpace(lowered.PlayableWorld?.LookCommand)) - continuousCommands.Add(lowered.PlayableWorld.LookCommand); - return new PreparedSurface(document, continuousCommands); - } - - private void ApplyPreparedSurface(PreparedSurface prepared, long? committedBaseVersion = null) - { - CurrentSurfaceDocument = prepared.Document; - _continuousCommandIds.Clear(); - foreach (var command in prepared.ContinuousCommands) - _continuousCommandIds.Add(command); SurfaceDocumentAvailable?.Invoke(CurrentSurfaceDocument); - if (committedBaseVersion.HasValue) - CompletePresentationTransition(committedBaseVersion.Value); + EnsureLiveSubscriptions(); } - internal static EveSurfaceDocument ComposeSurface( - EveSurfaceDocument surface, - IReadOnlyDictionary embeddedSurfaces) - { - var root = ComposeComponent(surface.Surface.Root, embeddedSurfaces); - var version = embeddedSurfaces.Values.Select(value => value.Version) - .DefaultIfEmpty(surface.Version) - .Append(surface.Version) - .Max(); - return new EveSurfaceDocument( - surface.Type, - surface.Schema, - surface.ProviderId, - surface.ProviderKind, - surface.Title, - version, - surface.UpdatedAtUtc, - new EveSurfaceTree(surface.Surface.Id, root, surface.Surface.Styles), - surface.Commands); - } - - private static EveSurfaceComponent ComposeComponent( - EveSurfaceComponent component, - IReadOnlyDictionary embeddedSurfaces) - { - var children = component.Children.Select(child => ComposeComponent(child, embeddedSurfaces)).ToList(); - foreach (var slot in component.EmbeddedDocuments ?? Array.Empty()) - { - if (!string.Equals(slot.SchemaId, EveSurfaceDocument.SchemaId, StringComparison.Ordinal) || - !embeddedSurfaces.TryGetValue(slot.DocumentId, out var embedded)) - continue; - children.Add(ComposeComponent(embedded.Surface.Root, embeddedSurfaces)); - } - return new EveSurfaceComponent( - component.Id, - component.Kind, - component.Props, - children, - component.StateBindingRecords, - component.EmbeddedDocuments, - component.Layout, - component.Style); - } - - private async Task EnsureLiveDocumentsAsync(CancellationToken cancellationToken) - { - if (_liveDocumentsReady) return; - DisposeLiveDocuments(); - try - { - await OpenLiveDocumentsAsync(cancellationToken); - _liveDocumentsReady = true; - } - catch - { - DisposeLiveDocuments(); - throw; - } - } - - private async Task OpenLiveDocumentsAsync(CancellationToken cancellationToken) + private void EnsureLiveSubscriptions() { + if (_liveSubscriptionsOpen) return; + _liveSubscriptionsOpen = true; + _liveWatches.Add(_selection.Surface.Surface.Watch(surface => _liveDocuments.Enqueue(surface))); var lowered = new EveUnitySceneSurfaceLowerer() .Lower(CurrentSurfaceDocument.SurfaceDocument, CurrentSurfaceDocument.AdvertisedSurface); string entityViewPointer = lowered.PlayableWorld == null ? "" : lowered.PlayableWorld.EntityViewPointerId; - string entityBodyId = lowered.PlayableWorld == null - ? "" - : lowered.PlayableWorld.EntityBodyId; - var fieldRefs = lowered.PlayableWorld == null - ? Array.Empty() - : lowered.PlayableWorld.FieldVolumes - .Select(field => field.DocumentRef) - .Where(value => !string.IsNullOrWhiteSpace(value)) - .Distinct(StringComparer.Ordinal) - .ToArray(); if (!string.IsNullOrWhiteSpace(entityViewPointer)) { - if (string.IsNullOrWhiteSpace(entityBodyId)) - throw new InvalidOperationException( - "The playable world advertises an entity-view pointer without its logical body id."); - _bodyResolver ??= CreateBodyResolver(); - await LeaseDocumentAsync(entityViewPointer, QueueEntityView, cancellationToken); - await LeaseCollectionAsync(publication => - { - if (string.Equals(publication.BodyId, entityBodyId, StringComparison.Ordinal)) - QueueBodyPublication(publication); - }, cancellationToken); + var entities = _mesh.DocumentAsync(_endpointId, entityViewPointer) + .GetAwaiter().GetResult(); + _liveDocuments.Enqueue(entities.LatestAsync().GetAwaiter().GetResult()); + _liveWatches.Add(entities.Watch(view => _liveDocuments.Enqueue(view))); } - foreach (var fieldRef in fieldRefs) - await LeaseDocumentAsync( - fieldRef, - fields => _liveDocuments.Enqueue(fields), - cancellationToken); + var inputCapabilityPointer = lowered.PlayableWorld?.InputCapabilityPointerId ?? ""; + if (!string.IsNullOrWhiteSpace(inputCapabilityPointer)) + { + var inputs = _mesh.DocumentAsync(_endpointId, inputCapabilityPointer) + .GetAwaiter().GetResult(); + CurrentInputCapability = inputs.LatestAsync().GetAwaiter().GetResult(); + _liveWatches.Add(inputs.Watch(document => _liveDocuments.Enqueue(document))); + } + } - await LeaseDocumentAsync( - _advertisedSurface!.RecordRef, - surface => _liveDocuments.Enqueue(new PendingSurfaceDocument(_advertisedSurface.RecordRef, surface)), - cancellationToken, - publishInitial: false); - var embeddedSlots = EnumerateEmbeddedDocuments(CurrentSurfaceDocument.SurfaceDocument.Surface.Root) - .Where(slot => string.Equals(slot.SchemaId, EveSurfaceDocument.SchemaId, StringComparison.Ordinal)) - .Where(slot => !string.IsNullOrWhiteSpace(slot.DocumentId)) - .GroupBy(slot => slot.DocumentId, StringComparer.Ordinal) - .Select(group => group.First()) - .ToArray(); - foreach (var slot in embeddedSlots) - { - await LeaseDocumentAsync( - slot.DocumentId, - surface => _liveDocuments.Enqueue(new PendingSurfaceDocument(slot.DocumentId, surface)), - cancellationToken); - } - var inputCapabilityRef = FindComponentProp( - CurrentSurfaceDocument.SurfaceDocument.Surface.Root, - "inputCapability"); - if (!string.IsNullOrWhiteSpace(inputCapabilityRef)) - { - await LeaseDocumentAsync( - inputCapabilityRef, - document => _liveDocuments.Enqueue(document), - cancellationToken); - } + private void RefreshAssetCatalog() + { var interaction = RequireWorldInteraction(); - if (!string.Equals(interaction.ReceiptSchema, EveCommandReceiptDocument.SchemaId, StringComparison.Ordinal)) - throw new InvalidOperationException( - $"The provider advertises receipt schema '{interaction.ReceiptSchema}', expected '{EveCommandReceiptDocument.SchemaId}'."); - if (string.IsNullOrWhiteSpace(interaction.ReceiptRecordRef)) - throw new InvalidOperationException("The provider advertisement does not publish a receipt record reference."); - } - - private Task EnsureReceiptSubscriptionAsync( - string commandId, - CancellationToken cancellationToken) - { - Task subscription; - lock (_pendingCommands) - { - if (_receiptSubscriptions.TryGetValue(commandId, out subscription!)) return subscription; - subscription = OpenReceiptSubscriptionAsync(commandId, cancellationToken); - _receiptSubscriptions.Add(commandId, subscription); - } - return RemoveFailedReceiptSubscriptionAsync(commandId, subscription); - } - - private async Task RemoveFailedReceiptSubscriptionAsync( - string commandId, - Task subscription) - { - try { return await subscription.ConfigureAwait(false); } - catch - { - lock (_pendingCommands) - { - if (_receiptSubscriptions.TryGetValue(commandId, out var current) && - ReferenceEquals(current, subscription)) - _receiptSubscriptions.Remove(commandId); - } - throw; - } - } - - private async Task OpenReceiptSubscriptionAsync( - string commandId, - CancellationToken cancellationToken) - { - var recordKey = ChildRecordKey(RequireWorldInteraction().ReceiptRecordRef, commandId); - var lease = await _meshClient!.LeaseDocumentAsync( - _target, - recordKey, - cancellationToken) - .ConfigureAwait(false); - IDisposable? watch = null; - try - { - watch = lease.Handle.Watch(receipt => QueueReceipt(commandId, receipt)); - try { QueueReceipt(commandId, await lease.Handle.LatestAsync().ConfigureAwait(false)); } - catch (KeyNotFoundException) { } - return new ReceiptSubscription(lease, watch); - } - catch - { - watch?.Dispose(); - lease.Dispose(); - throw; - } - } - - private void QueueReceipt(string expectedCommandId, EveCommandReceiptDocument receipt) - { - if (receipt == null) return; - lock (_pendingCommands) - { - if (!_pendingCommands.TryGetValue(expectedCommandId, out var request) || - !ReceiptMatches(request, receipt)) - return; - } - _liveDocuments.Enqueue(receipt); - } - - private static void DisposeReceiptSubscription(Task subscription) - { - if (subscription.Status == TaskStatus.RanToCompletion) - subscription.Result.Dispose(); - else - _ = subscription.ContinueWith( - completed => { if (completed.Status == TaskStatus.RanToCompletion) completed.Result.Dispose(); }, - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - } - - private void DisposeLiveDocuments() - { - foreach (var watch in _documentWatches) watch.Dispose(); - _documentWatches.Clear(); - foreach (var lease in _documentLeases) lease.Dispose(); - _documentLeases.Clear(); - _liveDocumentsReady = false; - } - - private async Task LeaseDocumentAsync( - string recordKey, - Action publish, - CancellationToken cancellationToken, - bool publishInitial = true) - where TDocument : class - { - var lease = await _meshClient!.LeaseDocumentAsync(_target, recordKey, cancellationToken); - _documentLeases.Add(lease); - if (publishInitial) publish(await lease.Handle.LatestAsync()); - _documentWatches.Add(lease.Handle.Watch(publish)); - } - - private async Task LeaseCollectionAsync( - Action publish, - CancellationToken cancellationToken, - bool includeInitialSnapshot = true) - where TDocument : class - { - var lease = await _meshClient!.LeaseCollectionAsync( - _target, - includeInitialSnapshot, - cancellationToken); - _documentLeases.Add(lease); - if (includeInitialSnapshot) - foreach (var document in await lease.Handle.LatestAsync()) - publish(document); - _documentWatches.Add(lease.Handle.WatchChanges(change => - { - if (change.Document != null) publish(change.Document); - })); - } - - private static IEnumerable EnumerateEmbeddedDocuments(EveSurfaceComponent component) - { - foreach (var slot in component.EmbeddedDocuments ?? Array.Empty()) - yield return slot; - foreach (var child in component.Children ?? Array.Empty()) - foreach (var slot in EnumerateEmbeddedDocuments(child)) - yield return slot; - } - - private sealed class PendingSurfaceDocument - { - public PendingSurfaceDocument(string recordKey, EveSurfaceDocument document) - { - RecordKey = recordKey ?? ""; - Document = document ?? throw new ArgumentNullException(nameof(document)); - } - - public string RecordKey { get; } - public EveSurfaceDocument Document { get; } - } - - private sealed class PreparedSurface - { - public PreparedSurface( - EveUnitySceneProviderSurfaceDocument document, - IReadOnlyCollection continuousCommands) - { - Document = document; - ContinuousCommands = continuousCommands; - } - - public EveUnitySceneProviderSurfaceDocument Document { get; } - public IReadOnlyCollection ContinuousCommands { get; } - } - - private static void TraceHotState(string message) - { - if (string.Equals(Environment.GetEnvironmentVariable("AETHERIA_TRACE_CLIENT_RUDP"), "1", StringComparison.Ordinal)) - Debug.Log($"EveUnity CultMesh hot state: {message}"); - } - - private async Task RefreshAssetCatalogAsync(CancellationToken cancellationToken) - { - var source = ResolveAssetSource( - _baseSurface ?? throw new InvalidOperationException("The provider surface must be available before its assets are resolved.")); - if (string.IsNullOrWhiteSpace(source.ManifestRecordRef)) + if (string.IsNullOrWhiteSpace(interaction.AssetManifestRecordRef)) return; - var candidate = await BuildAssetGenerationAsync(source, cancellationToken); - CommitAssetGeneration(candidate); - } - - private async Task RefreshAssetsForSurfaceAsync( - EveSurfaceDocument surface, - AssetSource source, - long generation, - CancellationToken cancellationToken) - { - Exception? finalError = null; - Interlocked.Exchange(ref _lastAssetPresentationAttemptCount, 0); - for (var attempt = 1; attempt <= AssetPresentationAttemptLimit; attempt++) - { - Interlocked.Exchange(ref _lastAssetPresentationAttemptCount, attempt); - try - { - if (generation != Interlocked.Read(ref _surfaceAssetGeneration)) - return; - await _assetGenerationGate.WaitAsync(cancellationToken); - try - { - if (generation != Interlocked.Read(ref _surfaceAssetGeneration)) - return; - var candidate = await BuildAssetGenerationAsync(source, cancellationToken); - if (generation != Interlocked.Read(ref _surfaceAssetGeneration)) - { - candidate.Dispose(); - return; - } - CommitAssetGeneration(candidate, surface); - return; - } - finally - { - _assetGenerationGate.Release(); - } - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - return; - } - catch (Exception error) - { - finalError = error; - if (generation != Interlocked.Read(ref _surfaceAssetGeneration)) - return; - if (attempt < AssetPresentationAttemptLimit) - { - try - { - await Task.Delay(TimeSpan.FromMilliseconds(100 * attempt), cancellationToken); - } - catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) - { - return; - } - } - } - } - RetireFailedBaseSurfaceCandidate(surface.Version, generation); - _liveDocuments.Enqueue(finalError ?? new InvalidOperationException( - $"Eve surface {surface.Version} asset preparation exhausted its retry policy.")); - } - - private AssetSource ResolveAssetSource(EveSurfaceDocument surface) - { - var world = FindWorldScene(surface.Surface.Root); - var manifestRecordRef = world?.GetProp("assetManifest") ?? ""; - if (string.IsNullOrWhiteSpace(manifestRecordRef)) - manifestRecordRef = RequireWorldInteraction().AssetManifestRecordRef; - var providerId = world?.GetProp("assetProviderId") ?? ""; - var verseId = world?.GetProp("assetVerseId") ?? ""; - var authorityRuntimeId = world?.GetProp("assetAuthorityRuntimeId") ?? ""; - var requiredCatalogVersion = long.TryParse( - world?.GetProp("assetCatalogVersion"), - out var parsedCatalogVersion) - ? Math.Max(0, parsedCatalogVersion) - : 0; - var rendezvousEndpoints = ParseEndpointList(world?.GetProp("assetRendezvousEndpoints") ?? ""); - var hasQualifiedSource = !string.IsNullOrWhiteSpace(providerId) || - !string.IsNullOrWhiteSpace(verseId) || - !string.IsNullOrWhiteSpace(authorityRuntimeId); - if (!hasQualifiedSource) - return new AssetSource( - _target, - _providerId, - manifestRecordRef, - Array.Empty(), - requiredCatalogVersion); - if (string.IsNullOrWhiteSpace(providerId) || string.IsNullOrWhiteSpace(verseId) || - string.IsNullOrWhiteSpace(authorityRuntimeId)) - throw new InvalidOperationException( - "A provider-qualified Eve asset source requires assetProviderId, assetVerseId, and assetAuthorityRuntimeId."); - return new AssetSource( - new CultMeshSessionTarget(verseId, authorityRuntimeId), - providerId, - manifestRecordRef, - rendezvousEndpoints, - requiredCatalogVersion); - } - - private async Task BuildAssetGenerationAsync( - AssetSource source, - CancellationToken cancellationToken) - { - while (true) - { - cancellationToken.ThrowIfCancellationRequested(); - var crossTarget = IsCrossTarget(source); - var endpoints = AssetRendezvousEndpoints(source, crossTarget); - var candidate = new AssetGeneration( - source, - source.Identity, - catalogVersion: -1, - crossTarget || source.RendezvousEndpoints.Count > 0 - ? CreateMeshClient( - endpoints, - AssetAuthorityTrust(source)) - : null); - try - { - var catalog = await AttachAssetCatalogSubscriptionAsync( - candidate, - source.RequiredCatalogVersion, - cancellationToken); - ConfigureAssetCatalog(candidate, catalog); - ReuseCompatibleBundles(_assetGeneration, candidate); - await PreloadAssetsAsync(candidate, cancellationToken); - if (source.RequiredCatalogVersion > 0) - return candidate; - var latest = await candidate.CatalogLease!.Handle.LatestAsync(); - candidate.ObserveCatalog(latest); - if (latest.Version != candidate.CatalogVersion) - { - candidate.Dispose(); - continue; - } - return candidate; - } - catch - { - candidate.Dispose(); - throw; - } - } - } - - private bool IsCrossTarget(AssetSource source) => - !string.Equals(source.Target.VerseId, _target.VerseId, StringComparison.Ordinal) || - !string.Equals(source.Target.AuthorityRuntimeId, _target.AuthorityRuntimeId, StringComparison.Ordinal); - - private CultMeshAuthorityTrustPolicy AssetAuthorityTrust(AssetSource source) => - IsCrossTarget(source) ? _crossTargetAuthorityTrust : _authorityTrust; - - private IReadOnlyList AssetRendezvousEndpoints(AssetSource source, bool crossTarget) - { - if (source.RendezvousEndpoints.Count > 0) - return source.RendezvousEndpoints; - if (crossTarget) - throw new InvalidOperationException( - $"Cross-target asset source '{source.Target.VerseId}/{source.Target.AuthorityRuntimeId}' " + - "must advertise at least one Odin rendezvous endpoint."); - return new[] { _rendezvousEndpoint }; - } - - private async Task AttachAssetCatalogSubscriptionAsync( - AssetGeneration candidate, - long requiredCatalogVersion, - CancellationToken cancellationToken) - { - var lease = await AssetMeshClient(candidate) - .LeaseDocumentAsync( - candidate.Source.Target, - candidate.Source.ManifestRecordRef, - cancellationToken); - IDisposable? watch = null; - try - { - var catalog = await lease.Handle.LatestAsync(); - candidate.ObserveCatalog(catalog); - if (requiredCatalogVersion > 0) - { - RequireCatalogVersion(catalog, requiredCatalogVersion, candidate.Source.ManifestRecordRef); - candidate.CatalogLease = lease; - return catalog; - } - - watch = lease.Handle.Watch(observedCatalog => - { - var pending = candidate.ObserveCatalog(observedCatalog); - if (pending != null) - QueueAssetCatalogUpdate(candidate.SourceIdentity, pending); - }); - candidate.CatalogWatch = watch; - candidate.CatalogLease = lease; - return catalog; - } - catch - { - watch?.Dispose(); - lease.Dispose(); - throw; - } - } - - private static void RequireCatalogVersion( - EveAssetCatalogDocument catalog, - long requiredCatalogVersion, - string manifestRecordRef) - { - if (catalog == null) throw new ArgumentNullException(nameof(catalog)); - if (requiredCatalogVersion <= 0 || catalog.Version == requiredCatalogVersion) + if (string.Equals(_assetCatalogPointer, interaction.AssetManifestRecordRef, StringComparison.Ordinal)) return; - throw new InvalidDataException( - $"Asset catalog record '{manifestRecordRef}' contains version {catalog.Version}, " + - $"but the Eve surface requires immutable version {requiredCatalogVersion}."); + + _assetCatalogPointer = interaction.AssetManifestRecordRef; + var platform = CurrentBundlePlatform(); + var cacheRoot = AssetCacheRoot(); + _assetCatalogTask = ObserveBackgroundAsync( + OpenAssetCatalogAsync(_assetCatalogPointer, platform, cacheRoot), + "CultMesh asset catalog preparation"); } - private void CommitAssetGeneration(AssetGeneration candidate, EveSurfaceDocument? surface = null) - { - PreparedSurface prepared; - try - { - prepared = PrepareComposedSurface(surface ?? _baseSurface ?? - throw new InvalidOperationException("The base Eve surface is unavailable.")); - } - catch - { - candidate.Dispose(); - throw; - } - var previous = _assetGeneration; - previous.TransferSharedBundlesTo(candidate); - _assetGeneration = candidate; - var pendingCatalog = candidate.ActivateCatalogObservation(); - if (pendingCatalog != null && candidate.Source.RequiredCatalogVersion <= 0) - QueueAssetCatalogUpdate(candidate.SourceIdentity, pendingCatalog); - if (surface != null) - _baseSurface = surface; - try - { - ApplyPreparedSurface(prepared, surface?.Version); - } - finally - { - previous.Dispose(); - } - } - - private static void ReuseCompatibleBundles(AssetGeneration current, AssetGeneration candidate) - { - foreach (var pair in candidate.BundleVariants) - { - if (!current.LoadedBundlesByUri.TryGetValue(pair.Key, out var bundle) || - !current.BundleVariants.TryGetValue(pair.Key, out var existing) || - !string.Equals(existing.ContentHash, pair.Value.ContentHash, StringComparison.Ordinal) || - existing.SizeBytes != pair.Value.SizeBytes) - continue; - var selections = candidate.BundleSelections[pair.Key]; - if (selections.Any(selection => - !current.NativeAssets.ContainsKey(selection.Asset.AssetRef))) - continue; - - candidate.AssetBundles.Add(bundle); - candidate.BorrowedAssetBundles.Add(bundle); - candidate.LoadedBundlesByUri[pair.Key] = bundle; - candidate.LoadedBundleUris.Add(pair.Key); - foreach (var selection in selections) - { - var value = current.NativeAssets[selection.Asset.AssetRef]; - candidate.NativeAssets[selection.Asset.AssetRef] = value; - if (value is GameObject prefab) - candidate.Prefabs[selection.Asset.AssetRef] = prefab; - if (selection.Asset.Metadata.TryGetValue("presentationRole", out var role) && - !string.IsNullOrWhiteSpace(role)) - { - candidate.NativeAssets[role] = value; - if (value is GameObject rolePrefab) - candidate.Prefabs[role] = rolePrefab; - } - } - } - } - - private static EveSurfaceComponent? FindWorldScene(EveSurfaceComponent component) - { - if (string.Equals(component.Kind, "world.scene3d", StringComparison.Ordinal) || - string.Equals(component.Kind, "world", StringComparison.Ordinal)) - return component; - foreach (var child in component.Children ?? Array.Empty()) - { - var found = FindWorldScene(child); - if (found != null) return found; - } - return null; - } - - private static IReadOnlyList ParseEndpointList(string value) => - (value ?? "") - .Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) - .Select(endpoint => endpoint.Trim()) - .Where(endpoint => endpoint.Length > 0) - .Distinct(StringComparer.Ordinal) - .ToArray(); - - private static void ConfigureAssetCatalog(AssetGeneration candidate, EveAssetCatalogDocument catalog) - { - var selected = catalog.Assets - .Select(asset => new SelectedAsset( - asset, - asset.Variants.FirstOrDefault(variant => - string.Equals(variant.RuntimeId, "unity-scene", StringComparison.Ordinal) && - string.Equals(variant.Platform, CurrentBundlePlatform(), StringComparison.Ordinal)))) - .Where(selection => selection.Variant != null) - .ToArray(); - if (selected.Length == 0) - { - var advertisedVariants = catalog.Assets - .SelectMany(asset => asset.Variants.Select(variant => - $"{asset.AssetRef}:{variant.RuntimeId}/{variant.Platform}")) - .ToArray(); - throw new InvalidOperationException( - $"Provider asset catalog '{catalog.CatalogId}' has no unity-scene/{CurrentBundlePlatform()} variant. " + - $"Advertised variants: {string.Join(", ", advertisedVariants)}"); - } - candidate.SetCatalogVersion(catalog.Version); - ReadCameraPolicies(candidate, selected.Select(selection => selection.Variant!)); - foreach (var selection in selected) - { - var variant = selection.Variant!; - candidate.AssetSelections[selection.Asset.AssetRef] = selection; - candidate.BundleVariants[variant.Uri] = variant; - if (!candidate.BundleSelections.TryGetValue(variant.Uri, out var group)) - { - group = new List(); - candidate.BundleSelections[variant.Uri] = group; - } - group.Add(selection); - var metadata = MergeAssetMetadata(selection.Asset.Metadata, variant.Metadata); - candidate.NativeAssetMetadata[selection.Asset.AssetRef] = metadata; - if (selection.Asset.Metadata.TryGetValue("presentationRole", out var role) && - !string.IsNullOrWhiteSpace(role)) - { - candidate.AssetSelections[role] = selection; - candidate.NativeAssetMetadata[role] = metadata; - } - } - } - - private void QueueAssetCatalogUpdate(string sourceIdentity, EveAssetCatalogDocument catalog) - { - if (catalog == null) throw new ArgumentNullException(nameof(catalog)); - if (_assetGeneration.Source.RequiredCatalogVersion > 0) - { - _pendingAssetCatalog = null; - return; - } - _pendingAssetCatalog = new PendingAssetCatalogUpdate( - sourceIdentity, - catalog); - if (_assetCatalogUpdateRunning) return; - _assetCatalogUpdateRunning = true; - _ = DrainAssetCatalogUpdatesAsync(); - } - - private async Task DrainAssetCatalogUpdatesAsync() + private async Task ObserveBackgroundAsync(Task operation, string name) { try { - while (!_lifetime.IsCancellationRequested) - { - var update = _pendingAssetCatalog; - _pendingAssetCatalog = null; - if (update == null) break; - if (_assetGeneration.Source.RequiredCatalogVersion > 0) - continue; - if (!string.Equals(update.SourceIdentity, CurrentAssetSourceIdentity, StringComparison.Ordinal)) - continue; - if (update.Catalog.Version == CurrentAssetCatalogVersion) - continue; - await _assetGenerationGate.WaitAsync(_lifetime.Token); - try - { - if (!string.Equals(update.SourceIdentity, CurrentAssetSourceIdentity, StringComparison.Ordinal) || - update.Catalog.Version == CurrentAssetCatalogVersion || - _assetGeneration.Source.RequiredCatalogVersion > 0) - continue; - var currentSource = _assetGeneration.Source; - var source = new AssetSource( - currentSource.Target, - currentSource.ProviderId, - currentSource.ManifestRecordRef, - currentSource.RendezvousEndpoints, - 0); - var candidate = await BuildAssetGenerationAsync(source, _lifetime.Token); - if (!string.Equals(update.SourceIdentity, CurrentAssetSourceIdentity, StringComparison.Ordinal) || - _assetGeneration.Source.RequiredCatalogVersion > 0) - { - candidate.Dispose(); - continue; - } - CommitAssetGeneration(candidate); - } - finally - { - _assetGenerationGate.Release(); - } - } - } - catch (OperationCanceledException) when (_lifetime.IsCancellationRequested) - { + await operation.ConfigureAwait(false); } catch (Exception error) { - _liveDocuments.Enqueue(error); - } - finally - { - _assetCatalogUpdateRunning = false; - if (_pendingAssetCatalog != null && !_lifetime.IsCancellationRequested) - QueueAssetCatalogUpdate(_pendingAssetCatalog.SourceIdentity, _pendingAssetCatalog.Catalog); + _liveDocuments.Enqueue(new BackgroundFailure(name, error)); } } - private async Task PreloadAssetsAsync(AssetGeneration candidate, CancellationToken cancellationToken) + private async Task OpenAssetCatalogAsync(string pointer, string platform, string cacheRoot) { - foreach (var uri in candidate.BundleVariants.Keys.OrderBy(value => value, StringComparer.Ordinal).ToArray()) - await EnsureBundleLoadedAsync(candidate, uri, cancellationToken); + var document = await _mesh.DocumentAsync(_endpointId, pointer).ConfigureAwait(false); + var catalog = await document.LatestAsync().ConfigureAwait(false); + _liveDocuments.Enqueue(await PrepareAssetCatalogAsync(catalog, platform, cacheRoot).ConfigureAwait(false)); + _liveWatches.Add(document.Watch(next => + { + _ = Task.Run(async () => + _liveDocuments.Enqueue(await PrepareAssetCatalogAsync(next, platform, cacheRoot).ConfigureAwait(false))); + })); } - private async Task EnsureBundleLoadedAsync( - AssetGeneration candidate, - string uri, - CancellationToken cancellationToken) + private void ApplyAssetCatalog(EveAssetCatalogDocument catalog) { - if (candidate.LoadedBundleUris.Contains(uri)) return; - if (!candidate.BundleVariants.TryGetValue(uri, out var variant)) - throw new InvalidOperationException($"Provider asset bundle '{uri}' is not present in the selected runtime catalog."); - if (!candidate.LoadingBundleUris.Add(uri)) - throw new InvalidOperationException($"Provider asset bundle dependency cycle includes '{uri}'."); - try - { - if (variant.Metadata.TryGetValue("unity.bundleDependencyUris", out var dependencies)) - foreach (var dependency in dependencies.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries)) - await EnsureBundleLoadedAsync(candidate, dependency, cancellationToken); + if (catalog == null || catalog.Version == CurrentAssetCatalogVersion) return; + var platform = CurrentBundlePlatform(); + var cacheRoot = AssetCacheRoot(); + _assetCatalogTask = Task.Run(async () => + _liveDocuments.Enqueue(await PrepareAssetCatalogAsync(catalog, platform, cacheRoot).ConfigureAwait(false))); + } - var groupElapsed = Stopwatch.StartNew(); - var descriptor = await AssetMeshClient(candidate) - .ReadAsync(candidate.Source.Target, uri, cancellationToken); - TraceStartup("asset-manifest", groupElapsed); - var bundle = await LoadVerifiedBundleAsync(candidate, descriptor, variant, cancellationToken); - TraceStartup("asset-transfer-and-bundle-load", groupElapsed); - if (bundle == null) - throw new InvalidOperationException($"Unity could not load provider asset bundle '{uri}'."); - candidate.AssetBundles.Add(bundle); - candidate.LoadedBundlesByUri[uri] = bundle; - var bundleAssetNames = bundle.GetAllAssetNames(); - foreach (var selection in candidate.BundleSelections[uri]) + private void ApplyPreparedAssetCatalog(PreparedAssetCatalog prepared) + { + var catalog = prepared.Catalog; + if (catalog.Version == CurrentAssetCatalogVersion) return; + + foreach (var bundle in _assetBundles) bundle.Unload(unloadAllLoadedObjects: false); + _assetBundles.Clear(); + _prefabs.Clear(); + _nativeAssets.Clear(); + _cameraCullingMasks.Clear(); + var selected = catalog.Assets + .Select(asset => new { - var bundleAssetName = ResolveBundleAssetName(bundleAssetNames, selection.Variant!.AssetKey); - var value = bundleAssetName == null - ? null - : await LoadBundleAssetAsync(bundle, bundleAssetName, selection.Asset.AssetKind, cancellationToken); - if (value == null) - throw new InvalidOperationException( - $"Provider asset '{selection.Asset.AssetRef}' advertises missing Unity bundle asset " + - $"'{selection.Variant.AssetKey}'. Available assets: " + string.Join(", ", bundleAssetNames)); - candidate.NativeAssets[selection.Asset.AssetRef] = value; + Asset = asset, + Variant = asset.Variants.FirstOrDefault(variant => + string.Equals(variant.RuntimeId, "unity-scene", StringComparison.Ordinal) && + string.Equals(variant.Platform, prepared.Platform, StringComparison.Ordinal)) + }) + .Where(selection => selection.Variant != null) + .ToArray(); + ReadCameraPolicies(selected.Select(selection => selection.Variant!)); + foreach (var group in selected.GroupBy(selection => selection.Variant!.Uri, StringComparer.Ordinal)) + { + var variant = group.First().Variant!; + var bytes = prepared.BundleBytes[variant.Uri]; + var bundle = AssetBundle.LoadFromMemory(bytes); + if (bundle == null) + throw new InvalidOperationException($"Unity could not load provider asset bundle '{variant.Uri}'."); + _assetBundles.Add(bundle); + foreach (var selection in group) + { + var value = bundle.LoadAsset(selection.Variant!.AssetKey); + if (value == null) continue; + _nativeAssets[selection.Asset.AssetRef] = value; if (selection.Asset.Metadata.TryGetValue("presentationRole", out var role) && !string.IsNullOrWhiteSpace(role)) - candidate.NativeAssets[role] = value; + _nativeAssets[role] = value; if (value is GameObject prefab) { - candidate.Prefabs[selection.Asset.AssetRef] = prefab; - if (!string.IsNullOrWhiteSpace(role)) candidate.Prefabs[role] = prefab; + _prefabs[selection.Asset.AssetRef] = prefab; + if (!string.IsNullOrWhiteSpace(role)) _prefabs[role] = prefab; } } - candidate.LoadedBundleUris.Add(uri); - TraceStartup("bundle-assets", groupElapsed); } - finally + + CurrentAssetCatalogVersion = catalog.Version; + if (_deferredEntityView != null) { - candidate.LoadingBundleUris.Remove(uri); + var deferred = _deferredEntityView; + _deferredEntityView = null; + EntityViewAvailable?.Invoke(deferred); } } - private static async Task LoadBundleAssetAsync( - AssetBundle bundle, - string assetName, - string assetKind, - CancellationToken cancellationToken) + private async Task PrepareAssetCatalogAsync( + EveAssetCatalogDocument catalog, + string platform, + string cacheRoot) { - var request = string.Equals(assetKind, "sprite", StringComparison.Ordinal) - ? bundle.LoadAssetAsync(assetName) - : bundle.LoadAssetAsync(assetName); - await AwaitUnityAsync(request, cancellationToken); - return request.asset; + var variants = catalog.Assets + .SelectMany(asset => asset.Variants) + .Where(variant => string.Equals(variant.RuntimeId, "unity-scene", StringComparison.Ordinal) && + string.Equals(variant.Platform, platform, StringComparison.Ordinal)) + .GroupBy(variant => variant.Uri, StringComparer.Ordinal) + .Select(group => group.First()) + .ToArray(); + var bundles = new Dictionary(StringComparer.Ordinal); + foreach (var variant in variants) + { + var descriptor = await _mesh.ReadAsync(_endpointId, variant.Uri) + .ConfigureAwait(false); + bundles[variant.Uri] = await ReadCachedBundleAsync(descriptor, variant, cacheRoot).ConfigureAwait(false); + } + return new PreparedAssetCatalog(catalog, platform, bundles); } - private sealed class SelectedAsset - { - public SelectedAsset(EveAssetCatalogEntry asset, EveAssetVariant? variant) - { - Asset = asset; - Variant = variant; - } - - public EveAssetCatalogEntry Asset { get; } - public EveAssetVariant? Variant { get; } - } - - private sealed class AssetGeneration : IDisposable - { - private readonly object _catalogObservationGate = new object(); - private EveAssetCatalogDocument? _latestObservedCatalog; - private bool _catalogObservationActive; - - public AssetGeneration( - AssetSource source, - string sourceIdentity, - long catalogVersion, - CultMeshClient? meshClient) - { - Source = source; - SourceIdentity = sourceIdentity ?? ""; - CatalogVersion = catalogVersion; - MeshClient = meshClient; - } - - public AssetSource Source { get; } - public string SourceIdentity { get; } - public long CatalogVersion { get; private set; } - public CultMeshClient? MeshClient { get; } - public CultMeshDocumentLease? CatalogLease { get; set; } - public IDisposable? CatalogWatch { get; set; } - public CultMeshContentTransferService? ContentTransfer { get; set; } - public CultMeshVerifiedBodyMappingBroker? BodyMappings { get; set; } - public CultMeshBodyTransportKind? BodyTransportKind { get; set; } - public Dictionary Prefabs { get; } = new Dictionary(StringComparer.Ordinal); - public Dictionary NativeAssets { get; } = new Dictionary(StringComparer.Ordinal); - public Dictionary> NativeAssetMetadata { get; } = - new Dictionary>(StringComparer.Ordinal); - public List AssetBundles { get; } = new List(); - public HashSet BorrowedAssetBundles { get; } = new HashSet(); - public Dictionary LoadedBundlesByUri { get; } = - new Dictionary(StringComparer.Ordinal); - public Dictionary AssetSelections { get; } = - new Dictionary(StringComparer.Ordinal); - public Dictionary> BundleSelections { get; } = - new Dictionary>(StringComparer.Ordinal); - public Dictionary BundleVariants { get; } = - new Dictionary(StringComparer.Ordinal); - public HashSet LoadedBundleUris { get; } = new HashSet(StringComparer.Ordinal); - public HashSet LoadingBundleUris { get; } = new HashSet(StringComparer.Ordinal); - public List BodyLeases { get; } = new List(); - public Dictionary RenderChannelLayers { get; } = - new Dictionary(StringComparer.Ordinal); - - public void TransferSharedBundlesTo(AssetGeneration next) - { - foreach (var bundle in next.BorrowedAssetBundles) - AssetBundles.Remove(bundle); - next.BorrowedAssetBundles.Clear(); - } - - public void SetCatalogVersion(long catalogVersion) - { - if (CatalogVersion >= 0 && CatalogVersion != catalogVersion) - throw new InvalidOperationException("An asset generation cannot change its catalog version after configuration."); - CatalogVersion = catalogVersion; - } - - public EveAssetCatalogDocument? ObserveCatalog(EveAssetCatalogDocument catalog) - { - lock (_catalogObservationGate) - { - _latestObservedCatalog = catalog ?? throw new ArgumentNullException(nameof(catalog)); - return _catalogObservationActive && catalog.Version != CatalogVersion ? catalog : null; - } - } - - public EveAssetCatalogDocument? ActivateCatalogObservation() - { - lock (_catalogObservationGate) - { - _catalogObservationActive = true; - return _latestObservedCatalog != null && _latestObservedCatalog.Version != CatalogVersion - ? _latestObservedCatalog - : null; - } - } - - public void Dispose() - { - CatalogWatch?.Dispose(); - CatalogWatch = null; - CatalogLease?.Dispose(); - CatalogLease = null; - MeshClient?.Dispose(); - foreach (var bundle in AssetBundles) - if (!BorrowedAssetBundles.Contains(bundle)) - bundle.Unload(unloadAllLoadedObjects: false); - AssetBundles.Clear(); - BorrowedAssetBundles.Clear(); - foreach (var lease in BodyLeases) - lease.Dispose(); - BodyLeases.Clear(); - Prefabs.Clear(); - NativeAssets.Clear(); - NativeAssetMetadata.Clear(); - AssetSelections.Clear(); - BundleSelections.Clear(); - BundleVariants.Clear(); - LoadedBundleUris.Clear(); - LoadingBundleUris.Clear(); - LoadedBundlesByUri.Clear(); - RenderChannelLayers.Clear(); - } - } - - private static string? ResolveBundleAssetName(IEnumerable bundleAssetNames, string advertisedAssetKey) - { - return bundleAssetNames.FirstOrDefault(assetName => - string.Equals(assetName, advertisedAssetKey, StringComparison.OrdinalIgnoreCase)); - } - - private static IReadOnlyDictionary MergeAssetMetadata( - IReadOnlyDictionary assetMetadata, - IReadOnlyDictionary variantMetadata) - { - var merged = new Dictionary(StringComparer.Ordinal); - foreach (var pair in assetMetadata) merged[pair.Key] = pair.Value; - foreach (var pair in variantMetadata) merged[pair.Key] = pair.Value; - return merged; - } - - private static void ReadCameraPolicies(AssetGeneration candidate, IEnumerable variants) + private void ReadCameraPolicies(IEnumerable variants) { + _cameraCullingMasks.Clear(); foreach (var variant in variants) { foreach (var pair in variant.Metadata) { - const string prefix = "renderChannel."; - const string suffix = ".unityLayer"; + const string prefix = "view."; + const string suffix = ".excludeUnityLayers"; if (!pair.Key.StartsWith(prefix, StringComparison.Ordinal) || !pair.Key.EndsWith(suffix, StringComparison.Ordinal)) continue; - var channel = pair.Key.Substring(prefix.Length, pair.Key.Length - prefix.Length - suffix.Length); - if (!string.IsNullOrWhiteSpace(channel) && - int.TryParse(pair.Value, out var layer) && layer >= 0 && layer < 32) - candidate.RenderChannelLayers[channel] = layer; + var viewId = pair.Key.Substring(prefix.Length, pair.Key.Length - prefix.Length - suffix.Length); + var mask = -1; + foreach (var token in (pair.Value ?? "").Split(',')) + { + if (int.TryParse(token.Trim(), out var layer) && layer >= 0 && layer < 32) + mask &= ~(1 << layer); + } + _cameraCullingMasks[viewId] = mask; } } } - private long CurrentAssetCatalogVersion => _assetGeneration.CatalogVersion; - - private string CurrentAssetSourceIdentity => _assetGeneration.SourceIdentity; + private long CurrentAssetCatalogVersion { get; set; } = -1; private static string CurrentBundlePlatform() { @@ -2003,214 +479,126 @@ namespace GameCult.Eve.UnityScene : Application.platform.ToString(); } - private CultMeshContentTransferService ContentTransfer(AssetGeneration candidate) + private byte[] ReadBundle(CultMeshCdnArtifactManifest manifest) + => ReadBundleAsync(manifest).GetAwaiter().GetResult(); + + private async Task ReadBundleAsync(CultMeshCdnArtifactManifest manifest) { - if (candidate.ContentTransfer != null) - return candidate.ContentTransfer; - if (_advertisement == null) - throw new InvalidOperationException("The Eve provider advertisement must be prepared before content transfer."); - if (string.IsNullOrWhiteSpace(_advertisement!.ServiceId)) - throw new InvalidOperationException($"Eve provider '{_advertisement.ProviderId}' has no service-instance identity for content transport."); - var cacheRoot = Environment.GetEnvironmentVariable("EVEUNITY_ASSET_CACHE_PATH"); - if (string.IsNullOrWhiteSpace(cacheRoot)) - cacheRoot = Path.Combine(Application.persistentDataPath, "EveUnity", "assets"); - candidate.BodyMappings = new CultMeshVerifiedBodyMappingBroker(cacheRoot); - _contentState ??= new CultCache( - CultMesh.CreateCultCacheDocumentRegistry(typeof(CultMeshContentTransferStateDocument))); - candidate.ContentTransfer = new CultMeshContentTransferService( - _contentState, - new[] + var bytes = new byte[checked((int)manifest.SizeBytes)]; + var references = manifest.Chunks.OrderBy(chunk => chunk.Offset).ToArray(); + var batches = references.Select((reference, index) => new { reference, index }) + .GroupBy(value => value.index / 2) + .Select(group => group.Select(value => value.reference).ToArray()) + .ToArray(); + using var concurrency = new SemaphoreSlim(2, 2); + var downloads = batches.Select(async batch => + { + await concurrency.WaitAsync().ConfigureAwait(false); + try { - AssetMeshClient(candidate).ContentProvider( - candidate.Source.ProviderId, - candidate.Source.Target) - }, - new CultMeshContentTransferOptions(cacheRoot), - candidate.BodyMappings); - return candidate.ContentTransfer; + var chunks = await _mesh.ReadManyAsync( + _endpointId, + batch.Select(reference => reference.RecordKey).ToArray()) + .ConfigureAwait(false); + foreach (var reference in batch) + { + var chunk = chunks.FirstOrDefault(candidate => + string.Equals(candidate.ChunkHash, reference.ChunkHash, StringComparison.Ordinal)); + if (chunk == null) + throw new InvalidOperationException($"Provider asset chunk '{reference.RecordKey}' was not available."); + if (chunk.SizeBytes != reference.SizeBytes || chunk.Payload.Length != reference.SizeBytes) + throw new InvalidDataException($"Provider asset chunk '{reference.RecordKey}' size did not match its manifest."); + Buffer.BlockCopy(chunk.Payload, 0, bytes, checked((int)reference.Offset), reference.SizeBytes); + } + } + finally + { + concurrency.Release(); + } + }).ToArray(); + await Task.WhenAll(downloads).ConfigureAwait(false); + return bytes; } - private async Task LoadVerifiedBundleAsync( - AssetGeneration candidate, + private byte[] ReadCachedBundle(CultMeshCdnArtifactManifest manifest, EveAssetVariant variant) + => ReadCachedBundleAsync(manifest, variant, AssetCacheRoot()).GetAwaiter().GetResult(); + + private async Task ReadCachedBundleAsync( CultMeshCdnArtifactManifest manifest, EveAssetVariant variant, - CancellationToken cancellationToken) + string cacheRoot) { - var hash = NormalizeContentHash(variant.ContentHash); - if (manifest.SizeBytes != variant.SizeBytes) - throw new InvalidOperationException($"Provider asset bundle '{variant.Uri}' size did not match its catalog."); - if (!string.Equals(NormalizeContentHash(manifest.ContentHash), - hash, StringComparison.Ordinal)) - throw new InvalidOperationException($"Provider asset bundle '{variant.Uri}' hash did not match its catalog."); - - var now = DateTimeOffset.UtcNow; - var network = NetworkArtifactDescriptor(manifest, now, TimeSpan.FromMinutes(5)); - var elapsed = Stopwatch.StartNew(); - var mapped = await ContentTransfer(candidate) - .FetchMappedContentAsync(manifest, network, now, TimeSpan.FromMinutes(5), cancellationToken); - TraceStartup("content-materialization", elapsed); - var validationRequest = new CultMeshBodyValidationRequest + Directory.CreateDirectory(cacheRoot); + var hash = variant.ContentHash.StartsWith("sha256:", StringComparison.Ordinal) + ? variant.ContentHash.Substring("sha256:".Length) + : variant.ContentHash; + var cachePath = Path.Combine(cacheRoot, hash + ".bundle"); + if (File.Exists(cachePath)) { - BodyId = network.BodyId, - SchemaId = network.SchemaId, - LayoutVersion = network.LayoutVersion, - ProducerEpoch = network.ProducerEpoch, - Sequence = network.Sequence, - Capacity = network.Capacity, - AccessMode = CultMeshBodyAccessMode.ReadOnly, - NowUtc = now - }; - var transport = new CultMeshBodyTransportService( - new ICultMeshBodyTransportAdapter[] - { - new CultMeshMappedBodyAdapter(candidate.BodyMappings!), - new CultMeshNetworkBodyAdapter(_ => File.ReadAllBytes(mapped.VerifiedPath)) - }, - descriptor => string.Equals(descriptor.BodyId, manifest.ArtifactId, StringComparison.Ordinal) && - string.Equals(descriptor.SemanticHash, hash, StringComparison.Ordinal)); - var negotiated = transport.NegotiateReadOnly(mapped.Descriptor, network, validationRequest); - candidate.BodyTransportKind = negotiated.SelectedTransport; - candidate.BodyLeases.Add(negotiated.Lease); - var bundleRequest = AssetBundle.LoadFromFileAsync(mapped.VerifiedPath); - await AwaitUnityAsync(bundleRequest, cancellationToken); - TraceStartup("asset-bundle-load-from-file", elapsed); - return bundleRequest.assetBundle; - } - - private static async Task AwaitUnityAsync(AsyncOperation operation, CancellationToken cancellationToken) - { - while (!operation.isDone) - { - cancellationToken.ThrowIfCancellationRequested(); - await Task.Yield(); + var cached = File.ReadAllBytes(cachePath); + VerifyBundle(cached, variant); + return cached; } - cancellationToken.ThrowIfCancellationRequested(); + + var bytes = await ReadBundleAsync(manifest).ConfigureAwait(false); + VerifyBundle(bytes, variant); + var temporaryPath = cachePath + ".tmp"; + File.WriteAllBytes(temporaryPath, bytes); + if (File.Exists(cachePath)) File.Delete(cachePath); + File.Move(temporaryPath, cachePath); + return bytes; } - private static CultMeshBodyDescriptor NetworkArtifactDescriptor( - CultMeshCdnArtifactManifest manifest, - DateTimeOffset now, - TimeSpan leaseDuration) + private static string AssetCacheRoot() { - if (manifest.SizeBytes > int.MaxValue) - throw new InvalidOperationException("CultMesh artifact capacity exceeds the current body descriptor bound."); - return new CultMeshBodyDescriptor + var cacheRoot = Environment.GetEnvironmentVariable("EVEUNITY_ASSET_CACHE_PATH"); + return string.IsNullOrWhiteSpace(cacheRoot) + ? Path.Combine(Application.persistentDataPath, "EveUnity", "assets") + : cacheRoot; + } + + private sealed class PreparedAssetCatalog + { + public PreparedAssetCatalog(EveAssetCatalogDocument catalog, string platform, Dictionary bundleBytes) { - BodyId = manifest.ArtifactId, - SchemaId = "gamecult.mesh.cdn-artifact.v1", - LayoutVersion = 1, - ByteSize = manifest.SizeBytes, - Capacity = checked((int)manifest.SizeBytes), - ProducerEpoch = 1, - Sequence = 0, - AccessMode = CultMeshBodyAccessMode.ReadOnly, - Synchronization = CultMeshBodySynchronization.ImmutableSequence, - LeaseExpiresAtUnixMs = now.Add(leaseDuration).ToUnixTimeMilliseconds(), - TransportKind = CultMeshBodyTransportKind.Network, - CapabilityToken = CultMeshCdnArtifactManifest.CreateRecordKey(manifest).Value, - SemanticHash = NormalizeContentHash(manifest.ContentHash) - }; + Catalog = catalog; + Platform = platform; + BundleBytes = bundleBytes; + } + + public EveAssetCatalogDocument Catalog { get; } + public string Platform { get; } + public Dictionary BundleBytes { get; } + } + + private sealed class BackgroundFailure + { + public BackgroundFailure(string operation, Exception error) + { + Operation = operation; + Error = error; + } + + public string Operation { get; } + public Exception Error { get; } + } + + private static void VerifyBundle(byte[] bytes, EveAssetVariant variant) + { + if (bytes.LongLength != variant.SizeBytes) + throw new InvalidOperationException($"Provider asset bundle '{variant.Uri}' size did not match its catalog."); + byte[] digest; + using (var sha256 = SHA256.Create()) + digest = sha256.ComputeHash(bytes); + var actual = "sha256:" + string.Concat(digest.Select(value => value.ToString("x2"))); + if (!string.Equals(actual, variant.ContentHash, StringComparison.Ordinal)) + throw new InvalidOperationException($"Provider asset bundle '{variant.Uri}' hash did not match its catalog."); } private void PublishReceipt(EveCommandReceiptDocument receipt) { - var terminal = IsTerminalReceiptState(receipt.State); - lock (_pendingCommands) - { - if (!_pendingCommands.TryGetValue(receipt.CommandId, out var request) || - !ReceiptMatches(request, receipt)) - return; - } - if (terminal) - { - _commandOutbox?.Acknowledge(receipt.CommandId); - if (DeferTerminalReceiptUntilPresentation(receipt)) - return; - } - FinalizeReceipt(receipt, terminal); - } - - private void EnsurePresentationWritable() - { - lock (_presentationFinalityGate) - { - if (_presentationBarrierVersion > _mountedBaseSurfaceVersion) - throw new InvalidOperationException( - $"The Eve presentation is awaiting provider surface version {_presentationBarrierVersion}; " + - $"mounted base version {_mountedBaseSurfaceVersion} is read-only until that generation commits."); - } - } - - private void BeginPresentationTransition(long sourceVersion) - { - lock (_presentationFinalityGate) - { - if (sourceVersion <= _mountedBaseSurfaceVersion) - return; - _presentationBarrierVersion = Math.Max(_presentationBarrierVersion, sourceVersion); - } - } - - private bool DeferTerminalReceiptUntilPresentation(EveCommandReceiptDocument receipt) - { - if (receipt.PresentationSurfaceVersion <= 0) - return false; - lock (_presentationFinalityGate) - { - if (receipt.PresentationSurfaceVersion <= _mountedBaseSurfaceVersion) - return false; - _presentationBarrierVersion = Math.Max( - _presentationBarrierVersion, - receipt.PresentationSurfaceVersion); - _deferredTerminalReceipts[receipt.CommandId] = receipt; - return true; - } - } - - private void CompletePresentationTransition(long committedVersion) - { - EveCommandReceiptDocument[] ready; - lock (_presentationFinalityGate) - { - _mountedBaseSurfaceVersion = Math.Max(_mountedBaseSurfaceVersion, committedVersion); - if (_activeBaseSurfaceCandidateVersion == committedVersion) - { - _activeBaseSurfaceCandidateVersion = -1; - _activeBaseSurfaceCandidateGeneration = -1; - } - ready = _deferredTerminalReceipts.Values - .Where(receipt => receipt.PresentationSurfaceVersion <= _mountedBaseSurfaceVersion) - .ToArray(); - foreach (var receipt in ready) - _deferredTerminalReceipts.Remove(receipt.CommandId); - if (_presentationBarrierVersion <= _mountedBaseSurfaceVersion) - _presentationBarrierVersion = -1; - } - foreach (var receipt in ready) - FinalizeReceipt(receipt, terminal: true); - } - - private long MountedBaseSurfaceVersion - { - get - { - lock (_presentationFinalityGate) - return _mountedBaseSurfaceVersion; - } - } - - private void FinalizeReceipt(EveCommandReceiptDocument receipt, bool terminal) - { - if (terminal) - { - lock (_pendingCommands) - { - if (!_pendingCommands.ContainsKey(receipt.CommandId)) - return; - _pendingCommands.Remove(receipt.CommandId); - } - ForgetPendingCommand(receipt.CommandId); - } + if (!_pendingCommandIds.Contains(receipt.CommandId) || !_publishedReceiptIds.Add(receipt.ReceiptId)) + return; CommandReceiptAvailable?.Invoke(new EveUnitySceneCommandReceipt( receipt.ReceiptId, receipt.Command, @@ -2223,101 +611,11 @@ namespace GameCult.Eve.UnityScene receipt.SurfaceId, receipt.Message, DateTimeOffset.TryParse(receipt.IssuedAtUtc, out var issuedAt) ? issuedAt : null, - receipt.SourceVersion, - receipt.Navigation == null - ? null - : new EveUnitySceneNavigationTarget( - receipt.Navigation.VerseId, - receipt.Navigation.ProviderId, - receipt.Navigation.SurfaceId, - receipt.Navigation.SurfaceKind, - receipt.Navigation.RendezvousEndpoints, - receipt.Navigation.AuthorityRuntimeId))); - } - - private sealed class AssetSource - { - public AssetSource( - CultMeshSessionTarget target, - string providerId, - string manifestRecordRef, - IReadOnlyList rendezvousEndpoints) - : this(target, providerId, manifestRecordRef, rendezvousEndpoints, 0) - { - } - - public AssetSource( - CultMeshSessionTarget target, - string providerId, - string manifestRecordRef, - IReadOnlyList rendezvousEndpoints, - long requiredCatalogVersion) - { - Target = target; - ProviderId = providerId ?? ""; - ManifestRecordRef = manifestRecordRef ?? ""; - RendezvousEndpoints = rendezvousEndpoints ?? Array.Empty(); - RequiredCatalogVersion = Math.Max(0, requiredCatalogVersion); - Identity = Target.VerseId + "\u001f" + Target.AuthorityRuntimeId + "\u001f" + - ProviderId + "\u001f" + ManifestRecordRef + "\u001f" + - string.Join("\u001e", RendezvousEndpoints); - } - - public CultMeshSessionTarget Target { get; } - public string ProviderId { get; } - public string ManifestRecordRef { get; } - public IReadOnlyList RendezvousEndpoints { get; } - public long RequiredCatalogVersion { get; } - public string Identity { get; } - } - - private sealed class PendingAssetCatalogUpdate - { - public PendingAssetCatalogUpdate(string sourceIdentity, EveAssetCatalogDocument catalog) - { - SourceIdentity = sourceIdentity ?? ""; - Catalog = catalog; - } - - public string SourceIdentity { get; } - public EveAssetCatalogDocument Catalog { get; } - } - - private bool ReceiptMatches(EveSurfaceCommandRequest request, EveCommandReceiptDocument receipt) => - string.Equals(receipt.Schema, EveCommandReceiptDocument.SchemaId, StringComparison.Ordinal) && - string.Equals(receipt.CommandId, request.CommandId, StringComparison.Ordinal) && - string.Equals(receipt.Command, request.Command, StringComparison.Ordinal) && - string.Equals(receipt.ProviderId, request.ProviderId, StringComparison.Ordinal) && - string.Equals(receipt.SurfaceId, request.SurfaceId, StringComparison.Ordinal) && - string.Equals(receipt.Authority, _target.AuthorityRuntimeId, StringComparison.Ordinal) && - string.Equals(receipt.InvocationHash, EveCommandInvocationHash.Compute(request), StringComparison.Ordinal) && - IsKnownReceiptState(receipt.State); - - private static bool IsKnownReceiptState(string state) => - string.Equals(state, "pending", StringComparison.OrdinalIgnoreCase) || - IsTerminalReceiptState(state); - - private static bool IsTerminalReceiptState(string state) => - string.Equals(state, "accepted", StringComparison.OrdinalIgnoreCase) || - string.Equals(state, "denied", StringComparison.OrdinalIgnoreCase) || - string.Equals(state, "reconciled", StringComparison.OrdinalIgnoreCase); - - private sealed class ReceiptSubscription : IDisposable - { - private IDisposable? _lease; - private IDisposable? _watch; - - public ReceiptSubscription(IDisposable lease, IDisposable watch) - { - _lease = lease; - _watch = watch; - } - - public void Dispose() - { - Interlocked.Exchange(ref _watch, null)?.Dispose(); - Interlocked.Exchange(ref _lease, null)?.Dispose(); - } + receipt.SourceVersion)); + if (string.Equals(receipt.State, "accepted", StringComparison.OrdinalIgnoreCase) || + string.Equals(receipt.State, "denied", StringComparison.OrdinalIgnoreCase) || + string.Equals(receipt.State, "reconciled", StringComparison.OrdinalIgnoreCase)) + _pendingCommandIds.Remove(receipt.CommandId); } private EveWorldInteractionAdvertisement RequireWorldInteraction() diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs index 3d356f3..71c2425 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs @@ -1,17 +1,9 @@ using System; using System.IO; using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; using System.Threading.Tasks; -using Stopwatch = System.Diagnostics.Stopwatch; using GameCult.Eve.Surface; -using GameCult.Eve.PluginFields; -using GameCult.Eve.UnityScene.Fields; -using GameCult.Mesh; using UnityEngine; -using UnityEngine.Serialization; #nullable enable @@ -25,39 +17,28 @@ namespace GameCult.Eve.UnityScene IEveUnitySceneCommandReceiptSource, IEveUnityProviderRefreshSource, IEveUnityEntitySoaViewDocumentSource, - IEveUnityFieldsSplatsDocumentSource, + IEveUnityInputCapabilityDocumentSource, IEveUnityCameraRenderPolicySource, - IEveUnityNativeAssetProvider, - IEveUnityNativeAssetMetadataProvider, - IEveUnityInputCapabilitySource, - IEveUnityNavigableProvider + IEveUnityNativeAssetProvider { [SerializeField] private string rendezvousEndpoint = ""; [SerializeField] private string providerFilter = ""; [SerializeField] private string surfaceFilter = ""; [SerializeField] private string verseFilter = ""; [SerializeField] private string surfaceKind = "interactive-world"; - [FormerlySerializedAs("replicaPath")] - [SerializeField] private string cacheDirectory = ""; + [SerializeField] private string replicaPath = ""; [SerializeField] private string runtimeId = "eve-unity"; - private CultMeshAuthorityTrustPolicy _authorityTrust = new CultMeshAuthorityTrustPolicy( - CultMeshAuthorityTrustMode.AuthenticatedRemote); - private CultMeshAuthorityTrustPolicy _navigationAuthorityTrust = new CultMeshAuthorityTrustPolicy( - CultMeshAuthorityTrustMode.AuthenticatedRemote); private EveUnityCultMeshLiveProviderTransport? _transport; private EveUnitySceneLiveProviderBridge? _bridge; - private PreviousProvider? _previousProvider; - private bool _navigationRouteCommitted; - private int _connectionLeaseCount; - private Task? _preparation; - private CancellationTokenSource? _preparationLifetime; - private readonly ConcurrentQueue _pendingEntityViews = new ConcurrentQueue(); - private readonly ConcurrentQueue _pendingFields = new ConcurrentQueue(); + private Task? _discovery; + private readonly ConcurrentQueue _pendingEntityViews = new ConcurrentQueue(); - public event Action? EntityViewAvailable; + public EveEntitySoaViewDocument? CurrentEntityView { get; private set; } - public event Action? FieldsSplatsAvailable; + public EveInputCapabilityDocument? CurrentInputCapability => _transport?.CurrentInputCapability; + + public event Action? EntityViewAvailable; public EveUnityCultMeshProviderSelection? Selection { get; private set; } @@ -65,541 +46,159 @@ namespace GameCult.Eve.UnityScene public EveUnitySceneProviderSurfaceDocument CurrentDocument => Bridge.CurrentSurfaceDocument; - public EveInputCapabilityDocument CurrentInputCapability + public event Action? DocumentAvailable { - get - { - RequirePrepared(); - return _transport!.CurrentInputCapability; - } + add { Bridge.DocumentAvailable += value; } + remove { Bridge.DocumentAvailable -= value; } } - public CultMeshBodyTransportKind? CurrentAssetBodyTransportKind + public event Action? ReceiptAvailable { - get - { - RequirePrepared(); - return _transport!.CurrentAssetBodyTransportKind; - } + add { Bridge.ReceiptAvailable += value; } + remove { Bridge.ReceiptAvailable -= value; } } - public event Action? DocumentAvailable; - - public event Action? ReceiptAvailable; - public void Configure( string endpoint, - string localCacheDirectory = "", + string replicaStorePath = "", string providerId = "", string surfaceId = "", string verseId = "", string requiredSurfaceKind = "interactive-world", - string clientRuntimeId = "eve-unity", - CultMeshAuthorityTrustPolicy? authorityTrust = null, - CultMeshAuthorityTrustPolicy? navigationAuthorityTrust = null) + string clientRuntimeId = "eve-unity") { - if (_bridge != null || _preparation != null) + if (_bridge != null) throw new InvalidOperationException("Disconnect the active provider before changing discovery configuration."); rendezvousEndpoint = endpoint ?? ""; - cacheDirectory = localCacheDirectory ?? ""; + replicaPath = replicaStorePath ?? ""; providerFilter = providerId ?? ""; surfaceFilter = surfaceId ?? ""; verseFilter = verseId ?? ""; surfaceKind = string.IsNullOrWhiteSpace(requiredSurfaceKind) ? "interactive-world" : requiredSurfaceKind; runtimeId = string.IsNullOrWhiteSpace(clientRuntimeId) ? "eve-unity" : clientRuntimeId; - _authorityTrust = authorityTrust ?? new CultMeshAuthorityTrustPolicy( - CultMeshAuthorityTrustMode.AuthenticatedRemote); - _navigationAuthorityTrust = navigationAuthorityTrust ?? _authorityTrust; } public void Connect() { - // During navigation the candidate presentation connects before the route - // becomes canonical, so every new lease must connect its selected bridge. Bridge.Connect(); - checked { _connectionLeaseCount++; } - } - - public Task PrepareAsync() - { - if (_bridge != null) - return Task.CompletedTask; - if (_preparation == null || _preparation.IsCanceled || _preparation.IsFaulted) - { - _preparationLifetime?.Dispose(); - _preparationLifetime = new CancellationTokenSource(); - _preparation = PrepareTransportAsync(_preparationLifetime.Token); - } - return _preparation; } public void Disconnect() { - if (_connectionLeaseCount <= 0) - return; - _connectionLeaseCount--; - if (_connectionLeaseCount == 0) - _bridge?.Disconnect(); - } - - public async Task NavigateAsync(EveUnitySceneNavigationTarget target) - { - if (target == null) throw new ArgumentNullException(nameof(target)); - if (string.IsNullOrWhiteSpace(target.VerseId)) - throw new ArgumentException("Eve provider navigation requires a stable Verse identity.", nameof(target)); - if (string.IsNullOrWhiteSpace(target.SurfaceId)) - throw new ArgumentException("Eve provider navigation requires a surface identity.", nameof(target)); - - var endpoints = (target.RendezvousEndpoints ?? Array.Empty()) - .Where(endpoint => !string.IsNullOrWhiteSpace(endpoint)) - .Select(endpoint => endpoint.Trim()) - .Distinct(StringComparer.Ordinal) - .ToArray(); - if (endpoints.Length == 0 && !string.IsNullOrWhiteSpace(rendezvousEndpoint)) - endpoints = new[] { rendezvousEndpoint }; - if (endpoints.Length == 0) - throw new InvalidOperationException("Eve provider navigation requires at least one Odin rendezvous endpoint."); - - var requiredSurfaceKind = string.IsNullOrWhiteSpace(target.SurfaceKind) - ? "interactive-world" - : target.SurfaceKind; - var prepared = await PrepareCandidateAsync( - endpoints, - target.ProviderId, - target.SurfaceId, - requiredSurfaceKind, - target.VerseId, - target.AuthorityRuntimeId, - _navigationAuthorityTrust, - _navigationAuthorityTrust, - CancellationToken.None).ConfigureAwait(false); - - StagePrepared(prepared); - rendezvousEndpoint = prepared.Selection.RendezvousEndpoint; - verseFilter = target.VerseId; - providerFilter = target.ProviderId; - surfaceFilter = target.SurfaceId; - surfaceKind = requiredSurfaceKind; - _authorityTrust = _navigationAuthorityTrust; - _preparation = Task.CompletedTask; - } - - public void CommitNavigation() - { - var previous = _previousProvider; - if (previous == null) return; - if (_navigationRouteCommitted) return; - if (previous.Bridge != null) - previous.Bridge.ReceiptAvailable -= ForwardReceipt; - AttachPrepared(_transport, _bridge); - _navigationRouteCommitted = true; - } - - public void FinalizeNavigation() - { - var previous = _previousProvider; - if (previous == null) return; - _previousProvider = null; - _navigationRouteCommitted = false; - SafeDispose(previous.Bridge); - SafeDispose(previous.Transport); - } - - public void RollbackNavigation() - { - var previous = _previousProvider; - if (previous == null) return; - if (previous.Bridge != null) - previous.Bridge.ReceiptAvailable -= ForwardReceipt; - DetachPrepared(_transport, _bridge); - var rejectedBridge = _bridge; - var rejectedTransport = _transport; - while (_pendingEntityViews.TryDequeue(out var pending)) pending.Lease.Dispose(); - while (_pendingFields.TryDequeue(out _)) { } - - Selection = previous.Selection; - _transport = previous.Transport; - _bridge = previous.Bridge; - rendezvousEndpoint = previous.RendezvousEndpoint; - verseFilter = previous.VerseFilter; - providerFilter = previous.ProviderFilter; - surfaceFilter = previous.SurfaceFilter; - surfaceKind = previous.SurfaceKind; - _authorityTrust = previous.AuthorityTrust; - AttachPrepared(_transport, _bridge); - _previousProvider = null; - _navigationRouteCommitted = false; - _preparation = Task.CompletedTask; - SafeDispose(rejectedBridge); - SafeDispose(rejectedTransport); + _bridge?.Disconnect(); } public void Refresh() { - var activeBridge = _navigationRouteCommitted - ? Bridge - : _previousProvider?.Bridge ?? Bridge; - if (activeBridge.IsConnected) - activeBridge.Refresh(); + if (Bridge.IsConnected) + Bridge.Refresh(); else - activeBridge.Connect(); + Bridge.Connect(); + _transport?.PumpLiveEvents(); + DrainEntityViews(); } public void Submit(EveSurfaceCommandRequest request) { - // Navigation preparation is not a command-routing commit. The mounted - // presentation continues to target its old provider until the visual - // candidate has been lowered and accepted by the bootstrap. - (_navigationRouteCommitted ? Bridge : _previousProvider?.Bridge ?? Bridge).Submit(request); + Bridge.Submit(request); } public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) { - RequirePrepared(); + EnsureTransport(); return _transport!.ResolvePrefab(asset); } public UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType) { - RequirePrepared(); + EnsureTransport(); return _transport!.ResolveAsset(asset, assetType); } - public bool TryResolveAssetMetadata( - EveUnityPlayableWorldAssetBinding asset, - out IReadOnlyDictionary metadata) + public bool TryGetCameraCullingMask(string viewId, out int cullingMask) { - RequirePrepared(); - return _transport!.TryResolveAssetMetadata(asset, out metadata); - } - - public bool TryGetRenderChannelLayer(string channel, out int layer) - { - RequirePrepared(); - return _transport!.TryGetRenderChannelLayer(channel, out layer); + EnsureTransport(); + return _transport!.TryGetCameraCullingMask(viewId, out cullingMask); } private void OnDestroy() { - ReleaseTransport(); - } - - private void ReleaseTransport() - { - _preparationLifetime?.Cancel(); - if (_bridge != null) - { - _bridge.DocumentAvailable -= ForwardDocument; - _bridge.ReceiptAvailable -= ForwardReceipt; - } + var transportOwnedConnection = _transport != null; _bridge?.Dispose(); _transport?.Dispose(); - _previousProvider?.Bridge?.Dispose(); - _previousProvider?.Transport?.Dispose(); - _previousProvider = null; - _navigationRouteCommitted = false; - _connectionLeaseCount = 0; - while (_pendingEntityViews.TryDequeue(out var pending)) pending.Lease.Dispose(); - while (_pendingFields.TryDequeue(out _)) { } _bridge = null; _transport = null; - _preparation = null; - _preparationLifetime?.Dispose(); - _preparationLifetime = null; Selection = null; + if (!transportOwnedConnection && _discovery?.Status == TaskStatus.RanToCompletion) + _discovery.Result.Mesh.Dispose(); + _discovery = null; } private void Update() { - _previousProvider?.Transport?.PumpLiveEvents(); + CompleteDiscoveryOnMainThread(); _transport?.PumpLiveEvents(); - EveFieldsSplatsDocument? latestFields = null; - while (_pendingFields.TryDequeue(out var fields)) - latestFields = fields; - if (latestFields != null) - FieldsSplatsAvailable?.Invoke(latestFields); - EntityViewLease? latest = null; - while (_pendingEntityViews.TryDequeue(out var next)) - { - latest?.Lease.Dispose(); - latest = next; - } + DrainEntityViews(); + } + + private void DrainEntityViews() + { + EveEntitySoaViewDocument? latest = null; + while (_pendingEntityViews.TryDequeue(out var next)) latest = next; if (latest == null) return; - var handler = EntityViewAvailable; - if (handler == null) latest.Lease.Dispose(); - else handler(latest.Document, latest.Lease); + CurrentEntityView = latest; + EntityViewAvailable?.Invoke(latest); } private EveUnitySceneLiveProviderBridge Bridge { get { - RequirePrepared(); + EnsureTransport(); return _bridge!; } } - private async Task PrepareTransportAsync(CancellationToken cancellationToken) + private void EnsureTransport() { - var elapsed = Stopwatch.StartNew(); if (_transport != null) return; if (string.IsNullOrWhiteSpace(rendezvousEndpoint)) throw new InvalidOperationException("EveUnity requires a CultMesh rendezvous endpoint."); - var prepared = await PrepareCandidateAsync( - new[] { rendezvousEndpoint }, - providerFilter, - surfaceFilter, - surfaceKind, - verseFilter, - "", - _authorityTrust, - _navigationAuthorityTrust, - cancellationToken).ConfigureAwait(false); - TraceStartup($"discovery and transport preparation {elapsed.Elapsed.TotalMilliseconds:0.###}ms"); - AdoptPrepared(prepared); + if (_discovery == null) + _discovery = new EveUnityCultMeshProviderDiscovery().DiscoverAsync( + rendezvousEndpoint, + providerFilter, + surfaceFilter, + surfaceKind, + verseFilter); + CompleteDiscoveryOnMainThread(); + if (_transport == null) + throw new InvalidOperationException("EveUnity CultMesh provider discovery is still in progress."); } - private async Task PrepareCandidateAsync( - IReadOnlyList endpoints, - string requiredProviderId, - string requiredSurfaceId, - string requiredSurfaceKind, - string requiredVerseId, - string requiredAuthorityRuntimeId, - CultMeshAuthorityTrustPolicy trust, - CultMeshAuthorityTrustPolicy crossTargetTrust, - CancellationToken cancellationToken) + private void CompleteDiscoveryOnMainThread() { - var failures = new List(); - foreach (var endpoint in endpoints) - { - cancellationToken.ThrowIfCancellationRequested(); - try - { - var selection = await new EveUnityCultMeshProviderDiscovery(trust).DiscoverAsync( - endpoint, - requiredProviderId, - requiredSurfaceId, - requiredSurfaceKind, - requiredVerseId, - cancellationToken, - requiredAuthorityRuntimeId).ConfigureAwait(false); - var resolvedCachePath = string.IsNullOrWhiteSpace(cacheDirectory) - ? Path.Combine(Application.temporaryCachePath, $"eve-unity-{GetInstanceID()}") - : cacheDirectory; - var transport = new EveUnityCultMeshLiveProviderTransport( - resolvedCachePath, - selection.RendezvousEndpoint, - selection.VerseId, - selection.AuthorityRuntimeId, - selection.ProviderId, - selection.SurfaceId, - runtimeId, - authorityTrust: trust, - crossTargetAuthorityTrust: crossTargetTrust); - try - { - await transport.PrepareAsync(cancellationToken).ConfigureAwait(false); - cancellationToken.ThrowIfCancellationRequested(); - return new PreparedProvider(selection, transport, new EveUnitySceneLiveProviderBridge(transport)); - } - catch - { - transport.Dispose(); - throw; - } - } - catch (Exception error) when (error is not OperationCanceledException) - { - failures.Add($"{endpoint}: {error.Message}"); - } - } + if (_transport != null || _discovery == null || !_discovery.IsCompleted) + return; + if (_discovery.IsFaulted) + throw _discovery.Exception?.GetBaseException() + ?? new InvalidOperationException("EveUnity CultMesh provider discovery failed."); + if (_discovery.IsCanceled) + throw new InvalidOperationException("EveUnity CultMesh provider discovery was cancelled."); - throw new InvalidOperationException( - $"No configured Odin endpoint prepared Verse '{requiredVerseId}' surface '{requiredSurfaceId}'. " + - string.Join(" | ", failures)); - } - - private void AdoptPrepared(PreparedProvider prepared) - { - if (_previousProvider != null) - throw new InvalidOperationException("A staged Eve provider navigation must be committed or rolled back first."); - var oldBridge = _bridge; - var oldTransport = _transport; - DetachPrepared(oldTransport, oldBridge); - - Selection = prepared.Selection; - _transport = prepared.Transport; - _bridge = prepared.Bridge; - AttachPrepared(_transport, _bridge); - - while (_pendingEntityViews.TryDequeue(out var pending)) pending.Lease.Dispose(); - while (_pendingFields.TryDequeue(out _)) { } - oldBridge?.Dispose(); - oldTransport?.Dispose(); - } - - private void StagePrepared(PreparedProvider prepared) - { - if (_previousProvider != null) - { - prepared.Bridge.Dispose(); - prepared.Transport.Dispose(); - throw new InvalidOperationException("A staged Eve provider navigation must be committed or rolled back first."); - } - - _previousProvider = new PreviousProvider( + Selection = _discovery.Result; + var resolvedReplicaPath = string.IsNullOrWhiteSpace(replicaPath) + ? Path.Combine(Application.temporaryCachePath, $"eve-unity-{GetInstanceID()}.cc") + : replicaPath; + _transport = new EveUnityCultMeshLiveProviderTransport( + resolvedReplicaPath, Selection, - _transport, - _bridge, - rendezvousEndpoint, - verseFilter, - providerFilter, - surfaceFilter, - surfaceKind, - _authorityTrust); - _navigationRouteCommitted = false; - DetachPrepared(_transport, _bridge); - if (_previousProvider.Bridge != null) - _previousProvider.Bridge.ReceiptAvailable += ForwardReceipt; - Selection = prepared.Selection; - _transport = prepared.Transport; - _bridge = prepared.Bridge; - while (_pendingEntityViews.TryDequeue(out var pending)) pending.Lease.Dispose(); - while (_pendingFields.TryDequeue(out _)) { } - } - - private void AttachPrepared( - EveUnityCultMeshLiveProviderTransport? transport, - EveUnitySceneLiveProviderBridge? bridge) - { - if (transport != null) - { - transport.EntityViewAvailable += QueueEntityView; - transport.FieldsSplatsAvailable += QueueFields; - } - if (bridge != null) - { - bridge.DocumentAvailable += ForwardDocument; - bridge.ReceiptAvailable += ForwardReceipt; - } - } - - private void DetachPrepared( - EveUnityCultMeshLiveProviderTransport? transport, - EveUnitySceneLiveProviderBridge? bridge) - { - if (transport != null) - { - transport.EntityViewAvailable -= QueueEntityView; - transport.FieldsSplatsAvailable -= QueueFields; - } - if (bridge == null) return; - bridge.DocumentAvailable -= ForwardDocument; - bridge.ReceiptAvailable -= ForwardReceipt; - } - - private void QueueEntityView(EveEntitySoaViewDocument view, ICultMeshBodyReadLease lease) => - _pendingEntityViews.Enqueue(new EntityViewLease(view, lease)); - - private void QueueFields(EveFieldsSplatsDocument fields) => _pendingFields.Enqueue(fields); - - private void ForwardDocument(EveUnitySceneProviderSurfaceDocument document) => - DocumentAvailable?.Invoke(document); - - private void ForwardReceipt(EveUnitySceneCommandReceipt receipt) => - ReceiptAvailable?.Invoke(receipt); - - private static void SafeDispose(IDisposable? disposable) - { - try - { - disposable?.Dispose(); - } - catch (Exception error) - { - Debug.LogWarning($"Eve provider cleanup failed after navigation ownership changed: {error}"); - } - } - - private static void TraceStartup(string message) - { - if (string.Equals(Environment.GetEnvironmentVariable("EVEUNITY_TRACE_STARTUP_PHASES"), "1", StringComparison.Ordinal)) - Debug.Log($"EveUnity startup phase {message}."); - } - - private void RequirePrepared() - { - if (_transport == null || _bridge == null) - throw new InvalidOperationException( - "The EveUnity CultMesh provider is not prepared. Await PrepareAsync before mounting the playable world."); - } - - private sealed class EntityViewLease - { - public EntityViewLease(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease) - { - Document = document; - Lease = lease; - } - - public EveEntitySoaViewDocument Document { get; } - public ICultMeshBodyReadLease Lease { get; } - } - - private sealed class PreparedProvider - { - public PreparedProvider( - EveUnityCultMeshProviderSelection selection, - EveUnityCultMeshLiveProviderTransport transport, - EveUnitySceneLiveProviderBridge bridge) - { - Selection = selection; - Transport = transport; - Bridge = bridge; - } - - public EveUnityCultMeshProviderSelection Selection { get; } - public EveUnityCultMeshLiveProviderTransport Transport { get; } - public EveUnitySceneLiveProviderBridge Bridge { get; } - } - - private sealed class PreviousProvider - { - public PreviousProvider( - EveUnityCultMeshProviderSelection? selection, - EveUnityCultMeshLiveProviderTransport? transport, - EveUnitySceneLiveProviderBridge? bridge, - string rendezvousEndpoint, - string verseFilter, - string providerFilter, - string surfaceFilter, - string surfaceKind, - CultMeshAuthorityTrustPolicy authorityTrust) - { - Selection = selection; - Transport = transport; - Bridge = bridge; - RendezvousEndpoint = rendezvousEndpoint; - VerseFilter = verseFilter; - ProviderFilter = providerFilter; - SurfaceFilter = surfaceFilter; - SurfaceKind = surfaceKind; - AuthorityTrust = authorityTrust; - } - - public EveUnityCultMeshProviderSelection? Selection { get; } - public EveUnityCultMeshLiveProviderTransport? Transport { get; } - public EveUnitySceneLiveProviderBridge? Bridge { get; } - public string RendezvousEndpoint { get; } - public string VerseFilter { get; } - public string ProviderFilter { get; } - public string SurfaceFilter { get; } - public string SurfaceKind { get; } - public CultMeshAuthorityTrustPolicy AuthorityTrust { get; } + runtimeId); + _transport.EntityViewAvailable += view => _pendingEntityViews.Enqueue(view); + _bridge = new EveUnitySceneLiveProviderBridge(_transport); } } } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshProviderDiscovery.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshProviderDiscovery.cs index 7772dbc..fe34dba 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshProviderDiscovery.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshProviderDiscovery.cs @@ -1,11 +1,9 @@ using System; -using System.Collections.Generic; -using System.Linq; using System.Threading; using System.Threading.Tasks; using GameCult.Eve.Surface; using GameCult.Mesh; -using GameCult.Networking; +using UnityEngine; #nullable enable @@ -13,155 +11,65 @@ namespace GameCult.Eve.UnityScene { public sealed class EveUnityCultMeshProviderSelection { - public EveUnityCultMeshProviderSelection( - string rendezvousEndpoint, - string verseId, - string authorityRuntimeId, - string providerId, - string surfaceId, - string surfaceKind) + internal EveUnityCultMeshProviderSelection( + string endpointId, + CultMeshClient mesh, + EveSurfaceSelection surface) { - RendezvousEndpoint = rendezvousEndpoint ?? ""; - VerseId = verseId ?? ""; - AuthorityRuntimeId = authorityRuntimeId ?? ""; - ProviderId = providerId ?? ""; - SurfaceId = surfaceId ?? ""; - SurfaceKind = surfaceKind ?? ""; + EndpointId = endpointId; + Mesh = mesh; + Surface = surface; } - public string RendezvousEndpoint { get; } - public string VerseId { get; } - public string AuthorityRuntimeId { get; } - public string ProviderId { get; } - public string SurfaceId { get; } - public string SurfaceKind { get; } + public string EndpointId { get; } + public string VerseId => Surface.Provider.VerseId; + public string ProviderId => Surface.Provider.ProviderId; + public string SurfaceId => Surface.Advertisement.SurfaceId; + public string SurfaceKind => Surface.Advertisement.SurfaceKind; + public CultMeshClient Mesh { get; } + public EveSurfaceSelection Surface { get; } } public sealed class EveUnityCultMeshProviderDiscovery { - private readonly CultMeshAuthorityTrustPolicy _authorityTrust; - - public EveUnityCultMeshProviderDiscovery(CultMeshAuthorityTrustPolicy? authorityTrust = null) - { - _authorityTrust = authorityTrust ?? new CultMeshAuthorityTrustPolicy( - CultMeshAuthorityTrustMode.AuthenticatedRemote); - } - public async Task DiscoverAsync( string rendezvousEndpoint, string providerId = "", string surfaceId = "", string surfaceKind = "interactive-world", string verseId = "", - CancellationToken cancellationToken = default, - string requiredAuthorityRuntimeId = "") + CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(rendezvousEndpoint)) throw new ArgumentException("Rendezvous endpoint must be non-empty.", nameof(rendezvousEndpoint)); + var endpointId = !string.IsNullOrWhiteSpace(verseId) + ? verseId + : !string.IsNullOrWhiteSpace(providerId) ? providerId : "eve.providers"; - CultMeshVerseCatalogResponseMessage response; + var mesh = new CultMeshClient(new CultMeshClientOptions + { + RendezvousEndpoints = new[] { rendezvousEndpoint } + }); try { - response = await CultMesh.CreateVerseDiscoveryClient(EveUnityCultMeshConnectivity.Discovery()).FetchAsync( - rendezvousEndpoint, - new CultMeshVerseCatalogRequestMessage + var surface = await mesh.SurfaceAsync( + endpointId, + new EveSurfaceRequest { - VerseIds = string.IsNullOrWhiteSpace(verseId) ? null : new[] { verseId }, - TransportVersion = "cultmesh.v0" - }); - cancellationToken.ThrowIfCancellationRequested(); + ProviderId = providerId, + SurfaceId = surfaceId, + SurfaceKind = surfaceKind, + LoweringTarget = "unity-scene" + }, + cancellationToken, + stage => Debug.Log($"EveUnity CultMesh discovery: {stage}")).ConfigureAwait(false); + return new EveUnityCultMeshProviderSelection(endpointId, mesh, surface); } - catch (Exception error) + catch { - throw new InvalidOperationException( - $"Could not query CultMesh rendezvous endpoint '{rendezvousEndpoint}'.", - error); + mesh.Dispose(); + throw; } - - var candidates = response.Verses - .Where(verse => string.IsNullOrWhiteSpace(verseId) || - string.Equals(verse.VerseId, verseId, StringComparison.Ordinal)) - .Where(verse => !string.IsNullOrWhiteSpace(verse.VerseId) && - (verse.DiscoveryEndpoints ?? Array.Empty()).Any(endpoint => - !string.IsNullOrWhiteSpace(endpoint))) - .GroupBy(verse => verse.VerseId, StringComparer.Ordinal) - .Select(group => group.First()) - .ToArray(); - if (candidates.Length == 0) - throw new InvalidOperationException("The rendezvous endpoint advertised no compatible Verse endpoints."); - - var failures = new List(); - var observed = new List(); - using var mesh = new CultMeshClient(new CultMeshClientOptions - { - RendezvousEndpoints = new[] { rendezvousEndpoint }, - Discovery = EveUnityCultMeshConnectivity.Discovery(), - Sessions = new CultMeshSessionManagerOptions { Trust = _authorityTrust }, - Connectors = EveUnityCultMeshConnectivity.SchemaConnectors(), - ContentConnectors = EveUnityCultMeshConnectivity.ContentConnectors() - }); - foreach (var candidate in candidates) - { - foreach (var authorityRuntimeId in EligibleAuthorityRuntimeIds( - candidate.AuthorityRuntimeIds, - requiredAuthorityRuntimeId)) - { - try - { - var target = new CultMeshSessionTarget(candidate.VerseId, authorityRuntimeId); - using var advertisementsLease = await mesh - .LeaseCollectionAsync(target, cancellationToken) - .ConfigureAwait(false); - var advertisements = await advertisementsLease.Handle.LatestAsync().ConfigureAwait(false); - observed.AddRange(advertisements.Select(document => - $"{target}: {document.ProviderId}[{string.Join(",", document.Surfaces.Select(surface => $"{surface.SurfaceId}:{surface.SurfaceKind}"))}]")); - var advertisement = advertisements - .Where(document => string.IsNullOrWhiteSpace(providerId) || - string.Equals(document.ProviderId, providerId, StringComparison.Ordinal)) - .Select(document => new - { - Document = document, - Surface = document.Surfaces.FirstOrDefault(surface => - (string.IsNullOrWhiteSpace(surfaceId) || - string.Equals(surface.SurfaceId, surfaceId, StringComparison.Ordinal)) && - (string.IsNullOrWhiteSpace(surfaceKind) || - string.Equals(surface.SurfaceKind, surfaceKind, StringComparison.Ordinal))) - }) - .FirstOrDefault(match => match.Surface != null); - if (advertisement?.Surface == null) - continue; - - return new EveUnityCultMeshProviderSelection( - rendezvousEndpoint, - candidate.VerseId, - authorityRuntimeId, - advertisement.Document.ProviderId, - advertisement.Surface.SurfaceId, - advertisement.Surface.SurfaceKind); - } - catch (Exception error) - { - failures.Add($"{candidate.VerseId}/{authorityRuntimeId}: {error.Message}"); - } - } - } - - var filter = $"provider='{providerId}', surface='{surfaceId}', kind='{surfaceKind}', authority='{requiredAuthorityRuntimeId}'"; - var detail = failures.Count == 0 ? "" : $" Endpoint failures: {string.Join(" | ", failures)}"; - var observedDetail = observed.Count == 0 ? "" : $" Observed: {string.Join(" | ", observed)}"; - throw new InvalidOperationException($"No advertised Eve surface matched {filter}.{detail}{observedDetail}"); } - - internal static IReadOnlyList EligibleAuthorityRuntimeIds( - IEnumerable? authorityRuntimeIds, - string requiredAuthorityRuntimeId) => - (authorityRuntimeIds ?? Array.Empty()) - .Where(id => !string.IsNullOrWhiteSpace(id)) - .Where(id => string.IsNullOrWhiteSpace(requiredAuthorityRuntimeId) || - string.Equals(id, requiredAuthorityRuntimeId, StringComparison.Ordinal)) - .Distinct(StringComparer.Ordinal) - .OrderBy(id => id, StringComparer.Ordinal) - .ToArray(); - } } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/GameCult.Eve.UnityScene.CultMesh.asmdef b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/GameCult.Eve.UnityScene.CultMesh.asmdef index 671a17f..405df3f 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/GameCult.Eve.UnityScene.CultMesh.asmdef +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/GameCult.Eve.UnityScene.CultMesh.asmdef @@ -4,7 +4,6 @@ "references": [ "GameCult.Eve.Surface", "GameCult.Eve.UnityScene", - "GameCult.Eve.PluginFields", "UnityEngine.AssetBundleModule" ], "includePlatforms": [], @@ -15,9 +14,7 @@ "GameCult.Caching.dll", "GameCult.Caching.MessagePack.dll", "GameCult.Mesh.dll", - "GameCult.Mesh.Quic.Native.dll", - "GameCult.Networking.dll", - "GameCult.Networking.WebSockets.dll" + "GameCult.Networking.dll" ], "autoReferenced": true, "defineConstraints": [], diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdaptiveExposureRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdaptiveExposureRenderer.cs deleted file mode 100644 index 281d5e8..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdaptiveExposureRenderer.cs +++ /dev/null @@ -1,226 +0,0 @@ -using System; -using UnityEngine; -using UnityEngine.Rendering; -using UnityEngine.Rendering.RenderGraphModule; -using UnityEngine.Rendering.Universal; - -#nullable enable - -namespace GameCult.Eve.UnityScene -{ - public sealed class EveUnityAdaptiveExposureRenderer : MonoBehaviour, IEveUnityRenderPassSource - { - private const int HistogramBins = 128; - private ComputeShader? _compute; - private Material? _multiplyMaterial; - private ComputeBuffer? _histogram; - private readonly RenderTexture?[] _exposure = new RenderTexture?[2]; - private int _currentExposure = -1; - private Camera? _camera; - private AdaptiveExposurePass? _pass; - - private float _lowPercent; - private float _highPercent; - private float _minimumEv; - private float _maximumEv; - private float _keyValue; - private float _speedUp; - private float _speedDown; - private bool _progressive; - - public long AppliedFrameCount { get; private set; } - public bool IsConfigured { get; private set; } - public static RenderPassEvent ExposureRenderPassEvent => - (RenderPassEvent)((int)RenderPassEvent.BeforeRenderingPostProcessing + 1); - int IEveUnityRenderPassSource.RenderOrder => 300; - - public void Configure( - float lowPercent, - float highPercent, - float minimumEv, - float maximumEv, - float keyValue, - string adaptation, - float speedUp, - float speedDown) - { - _lowPercent = Mathf.Clamp(lowPercent, 1f, 98.99f); - _highPercent = Mathf.Clamp(highPercent, _lowPercent + 0.01f, 99f); - _minimumEv = Mathf.Min(minimumEv, maximumEv); - _maximumEv = Mathf.Max(minimumEv, maximumEv); - _keyValue = Mathf.Max(0f, keyValue); - _speedUp = Mathf.Max(0f, speedUp); - _speedDown = Mathf.Max(0f, speedDown); - _progressive = string.Equals(adaptation, "progressive", StringComparison.Ordinal); - IsConfigured = true; - enabled = true; - } - - private void OnEnable() - { - _camera = GetComponent(); - EveUnityRenderPassRegistry.Register(this); - } - - private void OnDisable() - { - EveUnityRenderPassRegistry.Unregister(this); - ReleaseResources(); - } - - private void OnDestroy() => ReleaseResources(); - - bool IEveUnityRenderPassSource.TryEnqueuePass(Camera camera, ScriptableRenderer renderer) - { - if (!IsConfigured || _camera == null || camera != _camera) return false; - _pass ??= new AdaptiveExposurePass(this); - renderer.EnqueuePass(_pass); - return true; - } - - private bool EnsureResources() - { - _compute ??= Resources.Load("EveUnity/AdaptiveExposure"); - if (_multiplyMaterial == null) - { - var shader = Shader.Find("Hidden/EveUnity/AdaptiveExposureMultiply"); - if (shader != null) _multiplyMaterial = new Material(shader) { hideFlags = HideFlags.HideAndDontSave }; - } - _histogram ??= new ComputeBuffer(HistogramBins, sizeof(uint), ComputeBufferType.Structured); - for (var index = 0; index < _exposure.Length; index++) - { - if (_exposure[index] != null && _exposure[index]!.IsCreated()) continue; - _exposure[index] = new RenderTexture(1, 1, 0, RenderTextureFormat.RFloat, RenderTextureReadWrite.Linear) - { - name = $"Eve Adaptive Exposure {index}", - enableRandomWrite = true, - filterMode = FilterMode.Point, - wrapMode = TextureWrapMode.Clamp, - hideFlags = HideFlags.HideAndDontSave - }; - _exposure[index]!.Create(); - } - return _compute != null && _multiplyMaterial != null && _histogram != null; - } - - private void Apply(UnsafeCommandBuffer commands, TextureHandle colorTarget, int width, int height) - { - if (!EnsureResources() || _compute == null || _histogram == null || _multiplyMaterial == null) return; - - var clear = _compute.FindKernel("ClearHistogram"); - commands.SetComputeBufferParam(_compute, clear, "_Histogram", _histogram); - commands.DispatchCompute(_compute, clear, Mathf.CeilToInt(HistogramBins / 64f), 1, 1); - - var build = _compute.FindKernel("BuildHistogram"); - commands.SetComputeBufferParam(_compute, build, "_Histogram", _histogram); - commands.SetComputeTextureParam(_compute, build, "_Source", colorTarget); - commands.SetComputeVectorParam(_compute, "_HistogramScaleOffsetResolution", new Vector4( - 1f / 18f, - 0.5f, - Mathf.Max(1, width), - Mathf.Max(1, height))); - commands.DispatchCompute( - _compute, - build, - Mathf.CeilToInt(width / 2f / 16f), - Mathf.CeilToInt(height / 2f / 16f), - 1); - - var destination = _currentExposure < 0 ? 0 : (_currentExposure + 1) % 2; - var calculate = _compute.FindKernel( - _currentExposure < 0 || !_progressive ? "CalculateFixedExposure" : "CalculateProgressiveExposure"); - commands.SetComputeBufferParam(_compute, calculate, "_Histogram", _histogram); - commands.SetComputeVectorParam(_compute, "_ExposureFilterAndBrightness", new Vector4( - _lowPercent * 0.01f, - _highPercent * 0.01f, - Mathf.Pow(2f, _minimumEv), - Mathf.Pow(2f, _maximumEv))); - commands.SetComputeVectorParam(_compute, "_ExposureAdaptation", new Vector4( - _speedDown, - _speedUp, - _keyValue, - Mathf.Max(0f, Time.deltaTime))); - if (_currentExposure >= 0) - commands.SetComputeTextureParam(_compute, calculate, "_PreviousExposure", _exposure[_currentExposure]!); - commands.SetComputeTextureParam(_compute, calculate, "_DestinationExposure", _exposure[destination]!); - commands.DispatchCompute(_compute, calculate, 1, 1, 1); - - _currentExposure = destination; - _multiplyMaterial.SetTexture("_ExposureTexture", _exposure[_currentExposure]); - commands.SetRenderTarget(colorTarget); - commands.DrawProcedural(Matrix4x4.identity, _multiplyMaterial, 0, MeshTopology.Triangles, 3, 1); - AppliedFrameCount++; - } - - internal static float ExposureForAverageLuminance(float averageLuminance, float keyValue) => - Mathf.Max(0f, keyValue) / Mathf.Max(1e-6f, averageLuminance); - - internal static float AdaptExposure(float previous, float next, float deltaTime, float speedUp, float speedDown) - { - var speed = next > previous ? Mathf.Max(0f, speedDown) : Mathf.Max(0f, speedUp); - return previous + (next - previous) * (1f - Mathf.Pow(2f, -Mathf.Max(0f, deltaTime) * speed)); - } - - private void ReleaseResources() - { - _histogram?.Release(); - _histogram = null; - foreach (var texture in _exposure) - { - if (texture == null) continue; - texture.Release(); - if (Application.isPlaying) Destroy(texture); else DestroyImmediate(texture); - } - _exposure[0] = null; - _exposure[1] = null; - _currentExposure = -1; - if (_multiplyMaterial != null) - { - if (Application.isPlaying) Destroy(_multiplyMaterial); else DestroyImmediate(_multiplyMaterial); - _multiplyMaterial = null; - } - } - - private sealed class AdaptiveExposurePass : ScriptableRenderPass - { - private readonly EveUnityAdaptiveExposureRenderer _owner; - - private sealed class PassData - { - internal EveUnityAdaptiveExposureRenderer Owner = null!; - internal TextureHandle ColorTarget; - internal int Width; - internal int Height; - } - - internal AdaptiveExposurePass(EveUnityAdaptiveExposureRenderer owner) - { - _owner = owner; - renderPassEvent = ExposureRenderPassEvent; - // Histogram construction samples the active camera color. URP must therefore - // provide an intermediate color texture instead of exposing the back buffer. - requiresIntermediateTexture = true; - } - - public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) - { - var cameraData = frameData.Get(); - if (cameraData.camera == null || cameraData.camera != _owner._camera) return; - var resourceData = frameData.Get(); - if (resourceData.isActiveTargetBackBuffer) return; - var colorTarget = resourceData.activeColorTexture; - if (!colorTarget.IsValid()) return; - - using var builder = renderGraph.AddUnsafePass("Eve Adaptive Exposure", out var passData); - passData.Owner = _owner; - passData.ColorTarget = colorTarget; - passData.Width = cameraData.camera.pixelWidth; - passData.Height = cameraData.camera.pixelHeight; - builder.UseTexture(passData.ColorTarget, AccessFlags.ReadWrite); - builder.AllowPassCulling(false); - builder.SetRenderFunc(static (PassData data, UnsafeGraphContext context) => - data.Owner.Apply(context.cmd, data.ColorTarget, data.Width, data.Height)); - } - } - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdaptiveExposureRenderer.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdaptiveExposureRenderer.cs.meta deleted file mode 100644 index fc25c4a..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdaptiveExposureRenderer.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 3cb42e7da8b34febb348f01a598f2eea diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdvertisedInputAction.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdvertisedInputAction.cs deleted file mode 100644 index 8012fcc..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdvertisedInputAction.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using GameCult.Eve.Surface; - -#nullable enable - -namespace GameCult.Eve.UnityScene -{ - public sealed class EveUnityAdvertisedInputAction - { - public const string ButtonHoldValueModel = "button-hold.v1"; - public const string AxisValueModel = "axis.v1"; - public const string ScalarValueModel = "scalar.v1"; - public const string ViewDirectionValueModel = "view-direction.v1"; - - private readonly EveInputActionDocument _action; - - private EveUnityAdvertisedInputAction(EveInputActionDocument action) - { - _action = action; - } - - public string ActionId => _action.ActionId ?? ""; - public string Operation => _action.Operation ?? ""; - public string Availability => _action.Availability ?? ""; - public string InputValueModel => _action.InputValue?.Model ?? ""; - public string InputValuePayloadKey => _action.InputValue?.PayloadKey ?? ""; - public IReadOnlyList InputValuePayloadKeys => _action.InputValue?.PayloadKeys ?? Array.Empty(); - public double? CurrentValue => _action.InputValue?.CurrentValue; - public double? MinimumValue => _action.InputValue?.MinimumValue; - public double? MaximumValue => _action.InputValue?.MaximumValue; - public double? StepValue => _action.InputValue?.StepValue; - public string Unit => _action.InputValue?.Unit ?? ""; - public bool IsButtonHold => string.Equals(InputValueModel, ButtonHoldValueModel, StringComparison.Ordinal); - public bool IsScalar => string.Equals(InputValueModel, ScalarValueModel, StringComparison.Ordinal); - public bool IsViewDirection => string.Equals(InputValueModel, ViewDirectionValueModel, StringComparison.Ordinal); - - public static EveUnityAdvertisedInputAction Resolve( - EveInputCapabilityDocument? capability, - string actionId, - bool requireAvailable = true) - { - if (capability == null) - throw new InvalidOperationException("The provider did not advertise an input capability."); - var action = (capability.Actions ?? Array.Empty()).FirstOrDefault(candidate => - string.Equals(candidate?.ActionId, actionId, StringComparison.Ordinal)); - if (action == null || string.IsNullOrWhiteSpace(action.Operation)) - throw new InvalidOperationException($"Input action '{actionId}' is not advertised with an operation."); - if (requireAvailable && !string.Equals(action.Availability, "available", StringComparison.OrdinalIgnoreCase)) - throw new InvalidOperationException($"Input action '{actionId}' is not available."); - return new EveUnityAdvertisedInputAction(action); - } - - public IReadOnlyDictionary BuildPayload(string entityId, float? inputValue = null) - { - var payload = _action.Payload == null - ? new Dictionary(StringComparer.Ordinal) - : new Dictionary(_action.Payload, StringComparer.Ordinal); - payload["entityId"] = entityId ?? ""; - payload["actionId"] = ActionId; - - if (inputValue.HasValue) - { - if (float.IsNaN(inputValue.Value) || float.IsInfinity(inputValue.Value)) - throw new ArgumentOutOfRangeException(nameof(inputValue), "Input action value must be finite."); - if (string.IsNullOrWhiteSpace(InputValueModel) || string.IsNullOrWhiteSpace(InputValuePayloadKey)) - throw new InvalidOperationException($"Input action '{ActionId}' does not advertise a value payload."); - payload[InputValuePayloadKey] = inputValue.Value.ToString("R", CultureInfo.InvariantCulture); - } - - return payload; - } - - public IReadOnlyDictionary BuildViewDirectionPayload( - string entityId, - float directionX, - float directionY, - float directionZ) - { - if (!IsViewDirection) - throw new InvalidOperationException($"Input action '{ActionId}' does not advertise a view-direction value."); - if (InputValuePayloadKeys.Count != 3 || InputValuePayloadKeys.Any(string.IsNullOrWhiteSpace) || - InputValuePayloadKeys.Distinct(StringComparer.Ordinal).Count() != 3) - throw new InvalidOperationException($"Input action '{ActionId}' does not advertise three distinct view-direction payload keys."); - if (!IsFinite(directionX) || !IsFinite(directionY) || !IsFinite(directionZ)) - throw new ArgumentOutOfRangeException(nameof(directionX), "View direction must be finite."); - var length = Math.Sqrt( - (directionX * directionX) + - (directionY * directionY) + - (directionZ * directionZ)); - if (length <= 0.000001) - throw new ArgumentOutOfRangeException(nameof(directionX), "View direction must be non-zero."); - - var payload = BuildPayload(entityId).ToDictionary(value => value.Key, value => value.Value, StringComparer.Ordinal); - payload[InputValuePayloadKeys[0]] = ((float)(directionX / length)).ToString("R", CultureInfo.InvariantCulture); - payload[InputValuePayloadKeys[1]] = ((float)(directionY / length)).ToString("R", CultureInfo.InvariantCulture); - payload[InputValuePayloadKeys[2]] = ((float)(directionZ / length)).ToString("R", CultureInfo.InvariantCulture); - return payload; - } - - public IReadOnlyDictionary BuildScalarPayload(string entityId, double value) - { - if (!IsScalar) - throw new InvalidOperationException($"Input action '{ActionId}' does not advertise a scalar value."); - if (double.IsNaN(value) || double.IsInfinity(value)) - throw new ArgumentOutOfRangeException(nameof(value), "Scalar input value must be finite."); - if (!CurrentValue.HasValue || double.IsNaN(CurrentValue.Value) || double.IsInfinity(CurrentValue.Value)) - throw new InvalidOperationException($"Input action '{ActionId}' does not advertise a finite current scalar value."); - if (MinimumValue.HasValue && value < MinimumValue.Value) - throw new ArgumentOutOfRangeException(nameof(value), $"Scalar input value is below the advertised minimum {MinimumValue.Value}."); - if (MaximumValue.HasValue && value > MaximumValue.Value) - throw new ArgumentOutOfRangeException(nameof(value), $"Scalar input value is above the advertised maximum {MaximumValue.Value}."); - if (MinimumValue.HasValue && MaximumValue.HasValue && MinimumValue.Value > MaximumValue.Value) - throw new InvalidOperationException($"Input action '{ActionId}' advertises an inverted scalar range."); - if (StepValue.HasValue && (!IsFinite(StepValue.Value) || StepValue.Value <= 0)) - throw new InvalidOperationException($"Input action '{ActionId}' advertises an invalid scalar step."); - if (string.IsNullOrWhiteSpace(InputValuePayloadKey)) - throw new InvalidOperationException($"Input action '{ActionId}' does not advertise a scalar payload key."); - - var payload = BuildPayload(entityId).ToDictionary(pair => pair.Key, pair => pair.Value, StringComparer.Ordinal); - payload[InputValuePayloadKey] = value.ToString("R", CultureInfo.InvariantCulture); - return payload; - } - - private static bool IsFinite(float value) => !float.IsNaN(value) && !float.IsInfinity(value); - private static bool IsFinite(double value) => !double.IsNaN(value) && !double.IsInfinity(value); - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdvertisedInputAction.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdvertisedInputAction.cs.meta deleted file mode 100644 index a2236f2..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdvertisedInputAction.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: c7555b03e7f9451aa891e92958b845c5 diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentation.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentation.cs deleted file mode 100644 index 4b9bdf4..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentation.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using System.Globalization; -using UnityEngine; - -#nullable enable - -namespace GameCult.Eve.UnityScene -{ - public sealed class EveUnityAimPresentation - { - public EveUnityAimPresentation(EveUnitySceneNode node) - { - if (node == null) throw new ArgumentNullException(nameof(node)); - ControlledEntityId = Get(node, "controlledEntityId"); - ConvergenceTargetEntityId = Get(node, "convergenceTargetEntityId"); - ViewDotRole = Get(node, "viewDotRole"); - MinimumConvergenceDistance = Math.Max(0.01f, Number(node, "minimumConvergenceDistance", 50f)); - ViewDotRadius = Math.Max(0.01f, Number(node, "viewDotRadius", 0.8f)); - } - - public string ControlledEntityId { get; } - public string ConvergenceTargetEntityId { get; } - public string ViewDotRole { get; } - public float MinimumConvergenceDistance { get; } - public float ViewDotRadius { get; } - - public bool TryResolveViewPoint(EveUnityPlayableWorldClientHost? host, out Vector3 viewPoint) - { - viewPoint = default; - if (host == null || string.IsNullOrWhiteSpace(ControlledEntityId)) return false; - var controlled = FindMarker(host, ControlledEntityId); - if (controlled == null) return false; - var distance = MinimumConvergenceDistance; - var target = FindMarker(host, ConvergenceTargetEntityId); - if (target != null) - distance = Mathf.Max(distance, Vector3.Distance(controlled.transform.position, target.transform.position)); - var forward = controlled.transform.forward; - forward.y = 0f; - if (forward.sqrMagnitude <= 0.0001f) forward = Vector3.forward; - forward.Normalize(); - viewPoint = controlled.transform.position + forward * distance; - return true; - } - - public static EveUnityAimPresentation? Find(EveUnitySceneProjection? projection) => - projection == null ? null : Find(projection.Root); - - private static EveUnityAimPresentation? Find(EveUnitySceneNode node) - { - if (string.Equals(node.ComponentKind, "aim.presentation", StringComparison.Ordinal)) - return new EveUnityAimPresentation(node); - foreach (var child in node.Children) - { - var found = Find(child); - if (found != null) return found; - } - return null; - } - - private static EveUnityPlayableWorldEntityMarker? FindMarker( - EveUnityPlayableWorldClientHost host, - string entityId) - { - if (string.IsNullOrWhiteSpace(entityId)) return null; - foreach (var marker in host.SceneRoot.GetComponentsInChildren(true)) - if (string.Equals(marker.EntityId, entityId, StringComparison.Ordinal)) return marker; - return null; - } - - private static string Get(EveUnitySceneNode node, string key) => - node.Props.TryGetValue(key, out var value) ? value ?? "" : ""; - - private static float Number(EveUnitySceneNode node, string key, float fallback) => - float.TryParse(Get(node, key), NumberStyles.Float, CultureInfo.InvariantCulture, out var value) - ? value - : fallback; - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentation.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentation.cs.meta deleted file mode 100644 index 8a50d5a..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentation.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: f859926d39664cefa5aa96ed3c2d8903 diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs deleted file mode 100644 index 321ca96..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs +++ /dev/null @@ -1,123 +0,0 @@ -using System; -using UnityEngine; - -#nullable enable - -namespace GameCult.Eve.UnityScene -{ - public sealed class EveUnityAimPresentationRenderer : MonoBehaviour - { - [SerializeField] private Color viewDotColor = new Color(0.35f, 0.95f, 1f, 0.95f); - [SerializeField] private float width = 0.1f; - - private EveUnityPlayableWorldClientHost? _host; - private LineRenderer? _ring; - private LineRenderer? _horizontal; - private LineRenderer? _vertical; - private Material? _material; - - public EveUnityAimPresentation? Current { get; private set; } - public bool ViewDotVisible => _ring != null && _ring.enabled; - public Vector3 ViewDotPosition { get; private set; } - - public void Bind(EveUnityPlayableWorldClientHost host) - { - _host = host != null ? host : throw new ArgumentNullException(nameof(host)); - } - - public void RefreshNow() - { - Current = EveUnityAimPresentation.Find(_host?.ActiveProjection); - if (Current == null || string.IsNullOrWhiteSpace(Current.ControlledEntityId)) - { - SetVisible(false); - return; - } - - if (!Current.TryResolveViewPoint(_host, out var viewPoint)) - { - SetVisible(false); - return; - } - ViewDotPosition = viewPoint; - - EnsureVisuals(); - var camera = _host?.ActiveCameraTransform; - if (camera == null) - { - SetVisible(false); - return; - } - var right = camera.right; - var up = camera.up; - var center = ViewDotPosition - camera.forward * 0.1f; - Ring(_ring!, center, right, up, Current.ViewDotRadius); - Segment(_horizontal!, center, right, Current.ViewDotRadius * 1.6f); - Segment(_vertical!, center, up, Current.ViewDotRadius * 1.6f); - SetVisible(true); - } - - private void LateUpdate() => RefreshNow(); - - private void EnsureVisuals() - { - if (_ring != null) return; - var root = new GameObject("Eve aim presentation"); - root.transform.SetParent(transform, false); - _ring = Line(root.transform, "View direction ring"); - _horizontal = Line(root.transform, "View direction horizontal"); - _vertical = Line(root.transform, "View direction vertical"); - } - - private LineRenderer Line(Transform parent, string name) - { - var child = new GameObject(name); - child.transform.SetParent(parent, false); - var line = child.AddComponent(); - line.useWorldSpace = true; - line.material = SharedMaterial(); - line.startColor = line.endColor = viewDotColor; - line.startWidth = line.endWidth = Mathf.Max(0.01f, width); - line.numCapVertices = 2; - return line; - } - - private Material SharedMaterial() - { - if (_material != null) return _material; - var shader = Shader.Find("Sprites/Default") ?? Shader.Find("Universal Render Pipeline/Unlit"); - if (shader == null) throw new InvalidOperationException("EveUnity requires an unlit shader for aim presentation."); - return _material = new Material(shader) { name = "EveUnity aim presentation" }; - } - - private static void Ring(LineRenderer line, Vector3 center, Vector3 right, Vector3 up, float radius) - { - const int segments = 33; - line.positionCount = segments; - for (var index = 0; index < segments; index++) - { - var angle = index / (float)(segments - 1) * Mathf.PI * 2f; - line.SetPosition(index, center + right * (Mathf.Cos(angle) * radius) + up * (Mathf.Sin(angle) * radius)); - } - } - - private static void Segment(LineRenderer line, Vector3 center, Vector3 axis, float radius) - { - line.positionCount = 2; - line.SetPosition(0, center - axis * radius); - line.SetPosition(1, center + axis * radius); - } - - private void SetVisible(bool visible) - { - if (_ring != null) _ring.enabled = visible; - if (_horizontal != null) _horizontal.enabled = visible; - if (_vertical != null) _vertical.enabled = visible; - } - - private void OnDestroy() - { - if (_material != null) Destroy(_material); - } - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs.meta deleted file mode 100644 index 66058c7..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAimPresentationRenderer.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 97c7b809756a4350a6e61c67bba69b82 diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentation.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentation.cs deleted file mode 100644 index 6a0e5a5..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentation.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; - -#nullable enable - -namespace GameCult.Eve.UnityScene -{ - public sealed class EveUnityBeamPresentation - { - public const string SourceForwardDirection = "source-forward.v1"; - - public EveUnityBeamPresentation(EveUnitySceneNode node) - { - if (node == null) throw new ArgumentNullException(nameof(node)); - Id = node.Id ?? ""; - SourceEntityId = Get(node, "sourceEntityId"); - AssetRole = Get(node, "assetRole"); - DirectionMode = Get(node, "directionMode"); - if (string.IsNullOrWhiteSpace(DirectionMode)) DirectionMode = SourceForwardDirection; - RenderChannel = Get(node, "renderChannel"); - ActivationActionId = Get(node, "activationActionId"); - Power = NonNegative(node, "power", 0f); - PowerStateSemantic = Get(node, "powerStateSemantic"); - ActivationThreshold = NonNegative(node, "activationThreshold", 0.01f); - Radius = NonNegative(node, "radius", 0f); - MaximumDistance = NonNegative(node, "maximumDistance", 0f); - } - - public string Id { get; } - public string SourceEntityId { get; } - public string AssetRole { get; } - public string DirectionMode { get; } - public string RenderChannel { get; } - public string ActivationActionId { get; } - public float Power { get; } - public string PowerStateSemantic { get; } - public float ActivationThreshold { get; } - public float Radius { get; } - public float MaximumDistance { get; } - public bool UsesSourceForward => - string.Equals(DirectionMode, SourceForwardDirection, StringComparison.Ordinal); - - public static IReadOnlyList FindAll(EveUnitySceneProjection? projection) - { - var found = new List(); - if (projection != null) FindAll(projection.Root, found); - return found; - } - - private static void FindAll(EveUnitySceneNode node, ICollection found) - { - if (string.Equals(node.ComponentKind, "beam.presentation", StringComparison.Ordinal)) - found.Add(new EveUnityBeamPresentation(node)); - foreach (var child in node.Children) FindAll(child, found); - } - - private static string Get(EveUnitySceneNode node, string key) => - node.Props.TryGetValue(key, out var value) ? value ?? "" : ""; - - private static float NonNegative(EveUnitySceneNode node, string key, float fallback) - { - if (!float.TryParse(Get(node, key), NumberStyles.Float, CultureInfo.InvariantCulture, out var value) || - float.IsNaN(value) || float.IsInfinity(value)) - return fallback; - return Math.Max(0f, value); - } - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentation.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentation.cs.meta deleted file mode 100644 index 4acc86b..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentation.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 3086cd42c94f2624197d19704909f3f5 \ No newline at end of file diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs deleted file mode 100644 index 3a28964..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; - -#nullable enable - -namespace GameCult.Eve.UnityScene -{ - public sealed class EveUnityBeamPresentationRenderer : MonoBehaviour - { - private readonly Dictionary _active = new Dictionary(StringComparer.Ordinal); - private EveUnityPlayableWorldClientHost? _host; - private IEveUnityGameObjectAssetProvider? _assetProvider; - - public int ActiveBeamCount => _active.Count; - public int ActiveParticleSystemCount - { - get - { - var count = 0; - foreach (var beam in _active.Values) - if (beam.Root != null) - count += beam.Root.GetComponentsInChildren(true).Length; - return count; - } - } - - public void Bind( - EveUnityPlayableWorldClientHost host, - IEveUnityGameObjectAssetProvider? assetProvider = null) - { - _host = host != null ? host : throw new ArgumentNullException(nameof(host)); - _assetProvider = assetProvider; - } - - public void RefreshNow() - { - Apply( - EveUnityBeamPresentation.FindAll(_host?.ActiveProjection), - _host?.PresentedEntities, - _assetProvider); - } - - public void Apply( - IReadOnlyList presentations, - IEveUnityPresentedEntityRegistry? entities, - IEveUnityGameObjectAssetProvider? assetProvider) - { - var retained = new HashSet(StringComparer.Ordinal); - foreach (var presentation in presentations ?? Array.Empty()) - { - if (string.IsNullOrWhiteSpace(presentation.Id) || - string.IsNullOrWhiteSpace(presentation.SourceEntityId) || - string.IsNullOrWhiteSpace(presentation.AssetRole) || - !presentation.UsesSourceForward || entities == null || - !entities.TryGetByEntityId(presentation.SourceEntityId, out var source)) - continue; - - retained.Add(presentation.Id); - if (!_active.TryGetValue(presentation.Id, out var beam) || - !string.Equals(beam.SourceEntityId, presentation.SourceEntityId, StringComparison.Ordinal) || - !string.Equals(beam.AssetRole, presentation.AssetRole, StringComparison.Ordinal) || - beam.Root == null) - { - Remove(presentation.Id); - var prefab = assetProvider?.ResolvePrefab(new EveUnityPlayableWorldAssetBinding( - presentation.AssetRole, presentation.AssetRole, "provider-asset-ref")); - if (prefab == null) continue; - var root = Instantiate(prefab, source.Transform, false); - root.name = "Eve beam " + presentation.Id; - beam = new ActiveBeam(root, presentation.SourceEntityId, presentation.AssetRole); - _active[presentation.Id] = beam; - } - else if (beam.Root.transform.parent != source.Transform) - { - beam.Root.transform.SetParent(source.Transform, false); - } - - var power = presentation.Power; - if (!string.IsNullOrWhiteSpace(presentation.PowerStateSemantic)) - power = source.Entity.TryGetScalar(presentation.PowerStateSemantic, out var livePower) - ? Math.Max(0f, livePower) - : 0f; - var visiblePower = power > presentation.ActivationThreshold - ? power - : 0f; - foreach (var particles in beam.Root.GetComponentsInChildren(true)) - { - var emission = particles.emission; - emission.rateOverTimeMultiplier = visiblePower; - } - beam.Power = visiblePower; - } - - var stale = new List(); - foreach (var id in _active.Keys) - if (!retained.Contains(id)) stale.Add(id); - foreach (var id in stale) Remove(id); - } - - public bool TryGetPower(string presentationId, out float power) - { - if (_active.TryGetValue(presentationId ?? "", out var beam) && beam.Root != null) - { - power = beam.Power; - return true; - } - power = 0f; - return false; - } - - private void LateUpdate() => RefreshNow(); - - private void Remove(string id) - { - if (!_active.TryGetValue(id, out var beam)) return; - _active.Remove(id); - if (beam.Root != null) - { - if (Application.isPlaying) Destroy(beam.Root); - else DestroyImmediate(beam.Root); - } - } - - private void OnDestroy() - { - foreach (var beam in _active.Values) - if (beam.Root != null) - { - if (Application.isPlaying) Destroy(beam.Root); - else DestroyImmediate(beam.Root); - } - _active.Clear(); - _host = null; - _assetProvider = null; - } - - private sealed class ActiveBeam - { - public ActiveBeam(GameObject root, string sourceEntityId, string assetRole) - { - Root = root; - SourceEntityId = sourceEntityId; - AssetRole = assetRole; - } - - public GameObject Root { get; } - public string SourceEntityId { get; } - public string AssetRole { get; } - public float Power { get; set; } - } - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs.meta deleted file mode 100644 index ecae7e9..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityBeamPresentationRenderer.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: f91c99c24bc0e20488fa03daef2eec06 \ No newline at end of file diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentation.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentation.cs deleted file mode 100644 index d3879af..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentation.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; - -#nullable enable - -namespace GameCult.Eve.UnityScene -{ - public sealed class EveUnityCombatPresentation - { - public EveUnityCombatPresentation(EveUnitySceneNode node) - { - if (node == null) throw new ArgumentNullException(nameof(node)); - ControlledEntityId = Get(node, "controlledEntityId"); - ControlledEntityIndex = Int(node, "controlledEntityIndex", -1); - SelectedTargetEntityId = Get(node, "selectedTargetEntityId"); - SelectedTargetEntityIndex = Int(node, "selectedTargetEntityIndex", -1); - TargetVisible = Bool(node, "targetVisible"); - TargetHostile = Bool(node, "targetHostile"); - ContactInformation = Ratio(node, "contactInformation"); - ShieldRatio = Ratio(node, "shieldRatio"); - HullRatio = Ratio(node, "hullRatio"); - LockProgress = Ratio(node, "lockProgress"); - ReticleRole = Get(node, "reticleRole"); - LockRole = Get(node, "lockRole"); - ShieldMeterRole = Get(node, "shieldMeterRole"); - HullMeterRole = Get(node, "hullMeterRole"); - HitMarkerRole = Get(node, "hitMarkerRole"); - LockDisplayThreshold = Ratio(node, "lockDisplayThreshold", 0.01f); - HitMarkerDurationSeconds = Math.Max(0.01f, Number(node, "hitMarkerDurationSeconds", 0.25f)); - RadialFillMinimum = Ratio(node, "radialFillMinimum", 0.25f); - RadialFillMaximum = Ratio(node, "radialFillMaximum", 0.75f); - } - - public string ControlledEntityId { get; } - public int ControlledEntityIndex { get; } - public string SelectedTargetEntityId { get; } - public int SelectedTargetEntityIndex { get; } - public bool TargetVisible { get; } - public bool TargetHostile { get; } - public float ContactInformation { get; } - public float ShieldRatio { get; } - public float HullRatio { get; } - public float LockProgress { get; } - public string ReticleRole { get; } - public string LockRole { get; } - public string ShieldMeterRole { get; } - public string HullMeterRole { get; } - public string HitMarkerRole { get; } - public float LockDisplayThreshold { get; } - public float HitMarkerDurationSeconds { get; } - public float RadialFillMinimum { get; } - public float RadialFillMaximum { get; } - - public float RadialFill(float ratio) => - RadialFillMinimum + (RadialFillMaximum - RadialFillMinimum) * Math.Max(0, Math.Min(1, ratio)); - - public bool PresentsHit(EveUnityShotReceipt receipt) => - receipt != null && - receipt.SourceEntityIndex == ControlledEntityIndex && - receipt.TargetEntityIndex == SelectedTargetEntityIndex && - receipt.Hit && - (receipt.AppliedDamage > 0 || receipt.ShieldAbsorbedDamage > 0); - - public static EveUnityCombatPresentation? Find(EveUnitySceneProjection? projection) - { - if (projection == null) return null; - return Find(projection.Root); - } - - private static EveUnityCombatPresentation? Find(EveUnitySceneNode node) - { - if (string.Equals(node.ComponentKind, "combat.presentation", StringComparison.Ordinal)) - return new EveUnityCombatPresentation(node); - foreach (var child in node.Children) - { - var found = Find(child); - if (found != null) return found; - } - return null; - } - - private static string Get(EveUnitySceneNode node, string key) => - node.Props.TryGetValue(key, out var value) ? value ?? "" : ""; - - private static int Int(EveUnitySceneNode node, string key, int fallback) => - int.TryParse(Get(node, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : fallback; - - private static bool Bool(EveUnitySceneNode node, string key) => - bool.TryParse(Get(node, key), out var value) && value; - - private static float Number(EveUnitySceneNode node, string key, float fallback = 0) => - float.TryParse(Get(node, key), NumberStyles.Float, CultureInfo.InvariantCulture, out var value) ? value : fallback; - - private static float Ratio(EveUnitySceneNode node, string key, float fallback = 0) => - Math.Max(0, Math.Min(1, Number(node, key, fallback))); - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentation.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentation.cs.meta deleted file mode 100644 index e0219d9..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentation.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: e15acab8e9094b18b6dbd3abea0e055d diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs deleted file mode 100644 index 8c23259..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs +++ /dev/null @@ -1,279 +0,0 @@ -using System; -using UnityEngine; - -#nullable enable - -namespace GameCult.Eve.UnityScene -{ - public sealed class EveUnityCombatPresentationRenderer : MonoBehaviour - { - [SerializeField] private Color reticleColor = new Color(0.2f, 0.9f, 1f, 0.9f); - [SerializeField] private Color lockColor = new Color(1f, 0.7f, 0.15f, 1f); - [SerializeField] private Color shieldColor = new Color(0.15f, 0.65f, 1f, 1f); - [SerializeField] private Color hullColor = new Color(1f, 0.35f, 0.2f, 1f); - [SerializeField] private Color hitColor = Color.white; - [SerializeField] private float width = 0.12f; - - private EveUnityPlayableWorldClientHost? _host; - private Transform? _visualRoot; - private LineRenderer? _reticle; - private LineRenderer? _lock; - private LineRenderer? _shield; - private LineRenderer? _hull; - private LineRenderer? _hitA; - private LineRenderer? _hitB; - private Material? _material; - private float _hitUntil; - private float _pendingHitDuration; - private EveUnityCombatPresentation? _lastPresentation; - private Vector3 _lastCenter; - private Vector3 _lastRight; - private Vector3 _lastUp; - private float _lastRadius; - private bool _hasLastTarget; - - public EveUnityCombatPresentation? Current { get; private set; } - public bool ReticleVisible => _reticle != null && _reticle.enabled; - public bool LockVisible => _lock != null && _lock.enabled; - public bool HitMarkerVisible => _hitA != null && _hitA.enabled; - - public void Bind(EveUnityPlayableWorldClientHost host) - { - if (host == null) throw new ArgumentNullException(nameof(host)); - if (ReferenceEquals(_host, host)) return; - Unbind(); - _host = host; - _host.ShotAvailable += OnShotAvailable; - } - - public void Unbind() - { - if (_host != null) _host.ShotAvailable -= OnShotAvailable; - _host = null; - Current = null; - SetVisible(false); - } - - public void RefreshNow() - { - if (_pendingHitDuration > 0) - { - _hitUntil = Time.unscaledTime + _pendingHitDuration; - _pendingHitDuration = 0; - } - Current = EveUnityCombatPresentation.Find(_host?.ActiveProjection); - if (Current == null || string.IsNullOrWhiteSpace(Current.SelectedTargetEntityId)) - { - PresentRetainedHit(); - return; - } - - _lastPresentation = Current; - var marker = FindMarker(Current.SelectedTargetEntityId); - if (marker == null) - { - PresentRetainedHit(); - return; - } - - EnsureVisuals(); - PresentationAxes(out var right, out var up, out var towardCamera, out var cullingMask); - var visualBounds = VisualBounds(marker.transform, cullingMask); - var center = visualBounds?.center ?? (marker.transform.position + Vector3.up * 0.25f); - var radius = Math.Max(0.8f, visualBounds?.extents.magnitude ?? marker.transform.lossyScale.magnitude * 0.8f); - center += towardCamera * 0.1f; - _lastCenter = center; - _lastRight = right; - _lastUp = up; - _lastRadius = radius; - _hasLastTarget = true; - Arc(_reticle!, center, right, up, radius * 1.35f, 1f, 0f); - _reticle!.enabled = Current.TargetVisible; - - var lockVisible = Current.TargetVisible && Current.TargetHostile && - Current.LockProgress > Current.LockDisplayThreshold; - var noise = (1f - Current.LockProgress) * radius * 0.2f; - var frequency = Mathf.Lerp(0.01f, 10f, Mathf.Pow(Current.LockProgress, 4f)); - var noisyCenter = center + - right * ((Mathf.PerlinNoise(Time.unscaledTime * frequency, 0.37f) - 0.5f) * noise) + - up * ((Mathf.PerlinNoise(0.73f, Time.unscaledTime * frequency) - 0.5f) * noise); - var spin = Time.unscaledTime * Mathf.Lerp(5f, 720f, Current.LockProgress * Current.LockProgress); - Arc(_lock!, noisyCenter, right, up, radius * 1.55f, Current.LockProgress, spin); - _lock!.enabled = lockVisible; - - Arc(_shield!, center, right, up, radius * 1.78f, Current.RadialFill(Current.ShieldRatio), -90f); - Arc(_hull!, center, right, up, radius * 1.98f, Current.RadialFill(Current.HullRatio), -90f); - _shield!.enabled = Current.TargetVisible; - _hull!.enabled = Current.TargetVisible; - - var hitVisible = Time.unscaledTime < _hitUntil; - Cross(_hitA!, _hitB!, center, right, up, radius * 0.55f); - _hitA!.enabled = hitVisible; - _hitB!.enabled = hitVisible; - } - - private void PresentRetainedHit() - { - if (!_hasLastTarget || Time.unscaledTime >= _hitUntil) - { - SetVisible(false); - return; - } - EnsureVisuals(); - _reticle!.enabled = _lock!.enabled = _shield!.enabled = _hull!.enabled = false; - Cross(_hitA!, _hitB!, _lastCenter, _lastRight, _lastUp, _lastRadius * 0.55f); - _hitA!.enabled = _hitB!.enabled = true; - } - - private void LateUpdate() => RefreshNow(); - - private void OnShotAvailable(EveUnityShotReceipt receipt) - { - Current = EveUnityCombatPresentation.Find(_host?.ActiveProjection); - var presentation = Current; - if (presentation?.PresentsHit(receipt) != true) - presentation = _lastPresentation; - if (presentation?.PresentsHit(receipt) == true) - { - PresentationAxes(out var right, out var up, out _, out _); - _lastCenter = new Vector3( - (float)receipt.Endpoint.X, - (float)receipt.Endpoint.Y, - (float)receipt.Endpoint.Z); - _lastRight = right; - _lastUp = up; - _lastRadius = Math.Max(0.8f, _lastRadius); - _hasLastTarget = true; - _pendingHitDuration = Math.Max(_pendingHitDuration, presentation.HitMarkerDurationSeconds); - } - } - - private EveUnityPlayableWorldEntityMarker? FindMarker(string entityId) - { - if (_host == null) return null; - foreach (var marker in _host.SceneRoot.GetComponentsInChildren(true)) - if (string.Equals(marker.EntityId, entityId, StringComparison.Ordinal)) return marker; - return null; - } - - private void EnsureVisuals() - { - if (_visualRoot != null) return; - var root = new GameObject("Eve combat presentation"); - root.transform.SetParent(transform, false); - _visualRoot = root.transform; - _reticle = Line("Selected target reticle", reticleColor, width); - _lock = Line("Target lock", lockColor, width * 1.15f); - _shield = Line("Target shield", shieldColor, width * 0.75f); - _hull = Line("Target hull", hullColor, width * 0.75f); - _hitA = Line("Hit marker A", hitColor, width * 1.3f); - _hitB = Line("Hit marker B", hitColor, width * 1.3f); - } - - private LineRenderer Line(string name, Color color, float lineWidth) - { - var child = new GameObject(name); - child.transform.SetParent(_visualRoot, false); - var line = child.AddComponent(); - line.useWorldSpace = true; - line.loop = false; - line.material = SharedMaterial(); - line.startColor = line.endColor = color; - line.startWidth = line.endWidth = Math.Max(0.01f, lineWidth); - line.numCapVertices = 2; - return line; - } - - private Material SharedMaterial() - { - if (_material != null) return _material; - var shader = Shader.Find("Sprites/Default") ?? Shader.Find("Universal Render Pipeline/Unlit"); - if (shader == null) throw new InvalidOperationException("EveUnity requires an unlit shader for combat presentation."); - _material = new Material(shader) { name = "EveUnity combat presentation" }; - return _material; - } - - private static void PresentationAxes(out Vector3 right, out Vector3 up, out Vector3 towardCamera, out int cullingMask) - { - Camera? selected = Camera.main; - if (selected == null) - { - foreach (var candidate in Camera.allCameras) - { - if (!candidate.isActiveAndEnabled || candidate.orthographic) - continue; - selected = candidate; - break; - } - } - right = selected == null ? Vector3.right : selected.transform.right; - up = selected == null ? Vector3.up : selected.transform.up; - towardCamera = selected == null ? Vector3.back : -selected.transform.forward; - cullingMask = selected == null ? -1 : selected.cullingMask; - } - - private static Bounds? VisualBounds(Transform root, int cullingMask) - { - Bounds? bounds = null; - foreach (var renderer in root.GetComponentsInChildren(includeInactive: false)) - { - if (renderer == null || !renderer.enabled) - continue; - if ((cullingMask & (1 << renderer.gameObject.layer)) == 0) - continue; - if (renderer is LineRenderer || renderer is TrailRenderer || - string.Equals(renderer.GetType().Name, "ParticleSystemRenderer", StringComparison.Ordinal)) - continue; - if (bounds.HasValue) - { - var combined = bounds.Value; - combined.Encapsulate(renderer.bounds); - bounds = combined; - } - else - { - bounds = renderer.bounds; - } - } - return bounds; - } - - private static void Arc(LineRenderer line, Vector3 center, Vector3 right, Vector3 up, float radius, float fill, float rotationDegrees) - { - fill = Mathf.Clamp01(fill); - var segments = Math.Max(2, Mathf.CeilToInt(64 * fill) + 1); - line.positionCount = segments; - for (var index = 0; index < segments; index++) - { - var progress = segments <= 1 ? 0 : index / (float)(segments - 1); - var angle = (rotationDegrees + progress * 360f * fill) * Mathf.Deg2Rad; - line.SetPosition(index, center + right * (Mathf.Cos(angle) * radius) + up * (Mathf.Sin(angle) * radius)); - } - } - - private static void Cross(LineRenderer a, LineRenderer b, Vector3 center, Vector3 right, Vector3 up, float radius) - { - a.positionCount = b.positionCount = 2; - a.SetPosition(0, center - right * radius - up * radius); - a.SetPosition(1, center + right * radius + up * radius); - b.SetPosition(0, center - right * radius + up * radius); - b.SetPosition(1, center + right * radius - up * radius); - } - - private void SetVisible(bool visible) - { - if (_reticle != null) _reticle.enabled = visible; - if (_lock != null) _lock.enabled = visible; - if (_shield != null) _shield.enabled = visible; - if (_hull != null) _hull.enabled = visible; - if (_hitA != null) _hitA.enabled = visible && Time.unscaledTime < _hitUntil; - if (_hitB != null) _hitB.enabled = visible && Time.unscaledTime < _hitUntil; - } - - private void OnDestroy() - { - Unbind(); - if (_material != null) Destroy(_material); - } - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs.meta deleted file mode 100644 index 25e78e2..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 5c953d99a7454322b2935b878295b357 diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs index d16691a..31ba21c 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaPresenter.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using GameCult.Eve.Surface; -using GameCult.Mesh; #nullable enable @@ -10,7 +9,13 @@ namespace GameCult.Eve.UnityScene { public interface IEveUnityEntitySoaViewDocumentSource { - event Action EntityViewAvailable; + EveEntitySoaViewDocument? CurrentEntityView { get; } + event Action EntityViewAvailable; + } + + public interface IEveUnityInputCapabilityDocumentSource + { + EveInputCapabilityDocument? CurrentInputCapability { get; } } /// Projects portable entity SoA generations into the ordinary Unity scene sink. @@ -24,10 +29,15 @@ namespace GameCult.Eve.UnityScene _sink = sink ?? throw new ArgumentNullException(nameof(sink)); } - public void Apply(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease) + public int ActiveEntities => _known.Count; + + public void Apply(EveEntitySoaViewDocument document) { - using var view = EveUnityEntitySoaView.Open(document, lease); - var presented = new List(); + using var view = EveUnityEntitySoaView.Open(document); + var identities = (document.Identities ?? Array.Empty()) + .Where(identity => identity != null && identity.EntityIndex >= 0 && !string.IsNullOrWhiteSpace(identity.EntityId)) + .GroupBy(identity => identity.EntityIndex) + .ToDictionary(group => group.Key, group => group.First()); var next = new HashSet(StringComparer.Ordinal); for (var row = 0; row < view.EntityCount; row++) { @@ -35,60 +45,45 @@ namespace GameCult.Eve.UnityScene if (view.TryReadByte("render.visibility", row, out var visible) && visible == 0) continue; if (!view.TryReadVector3("transform.position", row, out var position)) continue; view.TryReadFloat("transform.rotation.radians", row, out var rotationRadians); - view.TryReadVector3("transform.velocity", row, out var velocity); - view.TryReadFloat("physics.body.radius", row, out var radius); view.TryReadFloat("render.scale", row, out var scale); - view.TryReadInt32("render.lod", row, out var lod); view.TryReadUInt32("render.group.id", row, out var groupId); var renderGroup = view.RenderGroups.FirstOrDefault(group => group.GroupId == groupId); - var identity = document.Identities.FirstOrDefault(item => item.Index == entityIndex); - var entityId = identity?.EntityId ?? $"entity:{entityIndex}"; + if (!identities.TryGetValue(entityIndex, out var identity)) + throw new InvalidOperationException( + $"Eve entity SoA view '{document.ViewId}' does not map entity index {entityIndex} to a logical identity."); + var entityId = identity.EntityId; next.Add(entityId); - var assetRef = string.IsNullOrWhiteSpace(identity?.AssetRef) + var assetRef = string.IsNullOrWhiteSpace(identity.AssetRef) ? renderGroup?.MeshAssetRef ?? "" - : identity!.AssetRef; - var scalarState = new Dictionary(StringComparer.Ordinal); - foreach (var semantic in view.FloatSemantics) - if (view.TryReadFloat(semantic, row, out var scalar)) - scalarState[semantic] = scalar; - var fact = new EveUnityPresentedEntity( - entityIndex, - entityId, - identity?.EntityKind ?? "entity", - string.IsNullOrWhiteSpace(identity?.Label) ? entityId : identity!.Label, - identity?.Faction ?? "", - position, - rotationRadians * 57.2957795f, - velocity, - radius, - scale > 0f ? scale : renderGroup?.DefaultScale ?? 1f, - groupId, - lod, - identity?.Selectable ?? true, - identity?.Controllable ?? false, - assetRef, - scalarState); - presented.Add(fact); - if (_sink is IEveUnityEntityGenerationSink) - continue; + : identity.AssetRef; _sink.UpsertEntity( - fact.ToPlayableWorldEntity(), + new EveUnityPlayableWorldEntity( + entityId, + entityId, + identity.Kind, + string.IsNullOrWhiteSpace(identity.Label) ? entityId : identity.Label, + identity.Faction, + assetRef, + position.x, + position.y, + position.z, + rotationRadians * 57.2957795f, + scale > 0f ? scale : renderGroup?.DefaultScale ?? 1f, + selectable: identity.Selectable, + controllable: identity.Controllable, + focusCommand: "", + moveCommand: "", + targetCommand: "", + actionCommand: "", + props: new Dictionary()), new EveUnityPlayableWorldAssetBinding( assetRef, - "entity", + identity.Kind, string.IsNullOrWhiteSpace(assetRef) ? "unity-generated-placeholder" : "provider-asset-ref")); } - if (_sink is IEveUnityEntityGenerationSink generationSink) - { - generationSink.ApplyGeneration(new EveUnityPresentedEntityGeneration( - document.ViewId, document.ProducerEpoch, document.Sequence, presented)); - } - else - { - foreach (var entityId in _known.Where(entityId => !next.Contains(entityId)).ToArray()) - _sink.RemoveEntity(entityId); - } + foreach (var entityId in _known.Where(entityId => !next.Contains(entityId)).ToArray()) + _sink.RemoveEntity(entityId); _known.Clear(); foreach (var entityId in next) _known.Add(entityId); } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs index 5475ddf..db2fd6d 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityEntitySoaView.cs @@ -11,131 +11,111 @@ namespace GameCult.Eve.UnityScene { public sealed class EveUnityEntitySoaView : IDisposable { - private readonly ICultMeshBodyReadLease _lease; - private readonly Dictionary _buffers; + private readonly ICultMeshBodyReadLease _body; private readonly Dictionary _columns; - private readonly string[] _floatSemantics; - private EveUnityEntitySoaView(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease) + private EveUnityEntitySoaView(EveEntitySoaViewDocument document, ICultMeshBodyReadLease body) { Document = document; - _lease = lease; - _buffers = document.Buffers.ToDictionary(buffer => buffer.BufferId, StringComparer.Ordinal); + _body = body; _columns = document.Columns.ToDictionary(column => column.Semantic, StringComparer.Ordinal); - _floatSemantics = document.Columns - .Where(column => string.Equals(column.ScalarType, "float32", StringComparison.Ordinal)) - .Select(column => column.Semantic) - .Where(semantic => !string.IsNullOrWhiteSpace(semantic)) - .ToArray(); } public EveEntitySoaViewDocument Document { get; } - public long Generation => Document.Sequence; + public long Generation => Document.Generation; public IReadOnlyList RenderGroups => Document.RenderGroups; - public int EntityCount => Document.Columns.Length == 0 ? 0 : Document.Columns.Min(column => column.ElementCount); - public IReadOnlyList FloatSemantics => _floatSemantics; - public static EveUnityEntitySoaView Open(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease) + public static EveUnityEntitySoaView Open(EveEntitySoaViewDocument document) { if (document == null) throw new ArgumentNullException(nameof(document)); - if (lease == null) throw new ArgumentNullException(nameof(lease)); - Validate(document, lease); - return new EveUnityEntitySoaView(document, lease); + if (!string.Equals(document.Schema, EveEntitySoaViewDocument.SchemaId, StringComparison.Ordinal)) + throw new ArgumentException($"Expected {EveEntitySoaViewDocument.SchemaId}, got '{document.Schema}'.", nameof(document)); + if (document.Body == null) + throw new InvalidOperationException("Eve entity SoA view did not advertise a CultMesh body."); + var request = new CultMeshBodyValidationRequest + { + BodyId = document.Body.BodyId, + SchemaId = document.Body.SchemaId, + LayoutVersion = document.Body.LayoutVersion, + ProducerEpoch = document.Body.ProducerEpoch, + AccessMode = CultMeshBodyAccessMode.ReadOnly, + NowUtc = DateTimeOffset.UtcNow + }; + var body = new CultMeshSharedMemoryBodyAdapter().OpenReadOnly(document.Body, request); + return new EveUnityEntitySoaView(document, body); } public bool TryReadVector3(string semantic, int index, out Vector3 value) { value = default; - if (!TryColumn(semantic, index, "float3", 12, out var offset)) return false; - value = new Vector3(_lease.ReadSingle(offset), _lease.ReadSingle(offset + 4), _lease.ReadSingle(offset + 8)); + if (!TryColumn(semantic, index, "float3", 12, out var column)) return false; + var offset = column.ByteOffset + (long)index * column.ElementStride; + value = new Vector3(_body.ReadSingle(offset), _body.ReadSingle(offset + 4), _body.ReadSingle(offset + 8)); return true; } public bool TryReadFloat(string semantic, int index, out float value) { value = default; - if (!TryColumn(semantic, index, "float32", 4, out var offset)) return false; - value = _lease.ReadSingle(offset); + if (!TryColumn(semantic, index, "float32", 4, out var column)) return false; + value = _body.ReadSingle(column.ByteOffset + (long)index * column.ElementStride); return true; } public bool TryReadInt32(string semantic, int index, out int value) { value = default; - if (!TryColumn(semantic, index, "int32", 4, out var offset)) return false; - value = _lease.ReadInt32(offset); + if (!TryColumn(semantic, index, "int32", 4, out var column)) return false; + value = _body.ReadInt32(column.ByteOffset + (long)index * column.ElementStride); return true; } public bool TryReadUInt32(string semantic, int index, out uint value) { value = default; - if (!TryColumn(semantic, index, "uint32", 4, out var offset)) return false; - value = unchecked((uint)_lease.ReadInt32(offset)); + if (!TryColumn(semantic, index, "uint32", 4, out var column)) return false; + value = unchecked((uint)_body.ReadInt32(column.ByteOffset + (long)index * column.ElementStride)); return true; } + public int EntityCount => Document.Columns.Length == 0 + ? 0 + : Document.Columns.Min(column => column.ElementCount); + public bool TryReadByte(string semantic, int index, out byte value) { value = default; - if (!TryColumn(semantic, index, "uint8", 1, out var offset)) return false; - value = _lease.ReadByte(offset); + if (!TryColumn(semantic, index, "uint8", 1, out var column)) return false; + value = _body.ReadByte(column.ByteOffset + (long)index * column.ElementStride); return true; } - public void Dispose() => _lease.Dispose(); - - private static void Validate(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease) + public void Dispose() { - if (!string.Equals(document.Schema, EveEntitySoaViewDocument.SchemaId, StringComparison.Ordinal)) - throw new ArgumentException($"Expected {EveEntitySoaViewDocument.SchemaId}, got '{document.Schema}'.", nameof(document)); - if (document.Buffers == null || document.Buffers.Length == 0) - throw new ArgumentException("Entity SoA view requires a primary logical buffer.", nameof(document)); - var descriptor = lease.Descriptor ?? throw new ArgumentException("Body lease has no descriptor.", nameof(lease)); - var primary = document.Buffers[0]; - if (!string.Equals(descriptor.BodyId, primary.BufferId, StringComparison.Ordinal) || - !string.Equals(descriptor.SchemaId, document.BodySchemaId, StringComparison.Ordinal) || - descriptor.LayoutVersion != document.LayoutVersion || - descriptor.ProducerEpoch != document.ProducerEpoch || - descriptor.Sequence != document.Sequence || - descriptor.Capacity != document.Capacity) - throw new InvalidOperationException("CultMesh body lease generation does not match the Eve entity layout."); - if (document.Capacity < 0 || document.Buffers.Any(buffer => - string.IsNullOrWhiteSpace(buffer.BufferId) || buffer.ByteOffset < 0 || buffer.ByteLength < 0 || - buffer.ByteOffset > descriptor.ByteSize - buffer.ByteLength)) - throw new InvalidOperationException("Eve entity layout contains an invalid logical buffer range."); - var buffers = document.Buffers.ToDictionary(buffer => buffer.BufferId, StringComparer.Ordinal); - foreach (var column in document.Columns ?? Array.Empty()) - { - if (!buffers.TryGetValue(column.BufferId, out var buffer) || column.ByteOffset < 0 || - column.ElementStride <= 0 || column.ElementCount < 0 || column.ElementCount > document.Capacity) - throw new InvalidOperationException($"Eve entity column '{column.ColumnId}' has invalid layout metadata."); - var width = ScalarWidth(column.ScalarType); - var occupied = column.ElementCount == 0 ? 0 : (long)(column.ElementCount - 1) * column.ElementStride + width; - if (width == 0 || column.ByteOffset > buffer.ByteLength - occupied) - throw new InvalidOperationException($"Eve entity column '{column.ColumnId}' exceeds its logical buffer."); - } + _body.Dispose(); } - private bool TryColumn(string semantic, int index, string scalarType, int minimumStride, out long offset) + private bool TryColumn( + string semantic, + int index, + string scalarType, + int minimumStride, + out EveEntitySoaColumn column) { - offset = 0; - if (index < 0 || !_columns.TryGetValue(semantic ?? "", out var column)) return false; + column = null!; + if (index < 0 || !_columns.TryGetValue(semantic ?? "", out column)) return false; if (index >= column.ElementCount || column.ElementStride < minimumStride) return false; if (!string.Equals(column.ScalarType, scalarType, StringComparison.Ordinal)) return false; - if (!_buffers.TryGetValue(column.BufferId, out var buffer)) return false; - offset = buffer.ByteOffset + column.ByteOffset + (long)index * column.ElementStride; - return true; + return DocumentBufferContains(column); } - private static int ScalarWidth(string scalarType) => scalarType switch + private bool DocumentBufferContains(EveEntitySoaColumn column) { - "float3" => 12, - "float32" => 4, - "int32" => 4, - "uint32" => 4, - "uint8" => 1, - _ => 0 - }; + var buffer = Document.Buffers.FirstOrDefault(candidate => + string.Equals(candidate.BufferId, column.BufferId, StringComparison.Ordinal)); + if (buffer == null || buffer.ByteOffset != 0 || Document.Body == null) return false; + var columnEnd = column.ByteOffset + (long)column.ElementStride * column.ElementCount; + return column.ByteOffset >= 0 && columnEnd <= Document.Body.ByteSize; + } } } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityFeedbackPresenter.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityFeedbackPresenter.cs index 723671f..1f189fe 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityFeedbackPresenter.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityFeedbackPresenter.cs @@ -11,13 +11,10 @@ namespace GameCult.Eve.UnityScene public EveUnityFeedbackEvent(EveUnitySceneNode node) { EventId = Get(node, "eventId"); Kind = Get(node, "eventKind"); SubjectKey = Get(node, "subjectKey"); - ItemKey = Get(node, "itemKey"); Position = Get(node, "position"); Reason = Get(node, "reason"); + ItemKey = Get(node, "itemKey"); Position = Get(node, "position"); FrameId = Long(node, "frameId", -1); ZoneIndex = Int(node, "zoneIndex", -1); SourceEntityIndex = Int(node, "sourceEntityIndex", -1); TargetEntityIndex = Int(node, "targetEntityIndex", -1); PickupIndex = Int(node, "pickupIndex", -1); ScalarValue = Double(node, "scalarValue", 0); - AuxiliaryValue = Double(node, "auxiliaryValue", 0); - CargoQuantityBefore = Double(node, "cargoQuantityBefore", 0); - CargoQuantityAfter = Double(node, "cargoQuantityAfter", 0); } public string EventId { get; } public string Kind { get; } @@ -29,11 +26,7 @@ namespace GameCult.Eve.UnityScene public string SubjectKey { get; } public string ItemKey { get; } public string Position { get; } - public string Reason { get; } public double ScalarValue { get; } - public double AuxiliaryValue { get; } - public double CargoQuantityBefore { get; } - public double CargoQuantityAfter { get; } private static string Get(EveUnitySceneNode node, string key) => node.Props.TryGetValue(key, out var value) ? value ?? "" : ""; private static int Int(EveUnitySceneNode node, string key, int fallback) => int.TryParse(Get(node, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : fallback; private static long Long(EveUnitySceneNode node, string key, long fallback) => long.TryParse(Get(node, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : fallback; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs index bf1dad0..e4cb824 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using UnityEngine; #nullable enable @@ -17,26 +16,13 @@ namespace GameCult.Eve.UnityScene UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType); } - public interface IEveUnityNativeAssetMetadataProvider - { - bool TryResolveAssetMetadata( - EveUnityPlayableWorldAssetBinding asset, - out IReadOnlyDictionary metadata); - } - - public sealed class EveUnityGameObjectPlayableWorldSceneSink : - IEveUnityPlayableWorldSceneSink, - IEveUnityEntityGenerationSink, - IEveUnityPresentedEntityRegistry, - IDisposable + public sealed class EveUnityGameObjectPlayableWorldSceneSink : IEveUnityPlayableWorldSceneSink { private readonly Transform _root; private readonly IEveUnityGameObjectAssetProvider _assetProvider; private readonly Dictionary _instances = new Dictionary(StringComparer.Ordinal); - private IReadOnlyDictionary _presentedById = - new Dictionary(StringComparer.Ordinal); - private IReadOnlyDictionary _presentedByIndex = - new Dictionary(); + private string _subjectEntityId = ""; + private bool _subjectVisible = true; public EveUnityGameObjectPlayableWorldSceneSink( Transform root, @@ -47,19 +33,15 @@ namespace GameCult.Eve.UnityScene } public int ActiveEntityCount => _instances.Count; - public EveUnityPresentedEntityGeneration? CurrentGeneration { get; private set; } - - public bool TryGetByEntityId(string entityId, out EveUnityPresentedEntityHandle handle) => - _presentedById.TryGetValue(entityId ?? "", out handle!); - - public bool TryGetBySourceIndex(int sourceIndex, out EveUnityPresentedEntityHandle handle) => - _presentedByIndex.TryGetValue(sourceIndex, out handle!); public void ConfigureWorld(EveUnityPlayableWorldProjection world) { if (world == null) throw new ArgumentNullException(nameof(world)); if (!string.IsNullOrWhiteSpace(world.WorldRootId)) _root.name = world.WorldRootId; + _subjectEntityId = world.PlayerEntityId; + _subjectVisible = world.SubjectVisible; + ApplySubjectVisibility(); } public void UpsertEntity(EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset) @@ -87,90 +69,7 @@ namespace GameCult.Eve.UnityScene } ApplyEntity(instance, entity, asset); - } - - public void ApplyGeneration(EveUnityPresentedEntityGeneration generation) - { - if (generation == null) throw new ArgumentNullException(nameof(generation)); - if (generation.Entities.Any(entity => string.IsNullOrWhiteSpace(entity.EntityId))) - throw new InvalidOperationException("Presented entity generations require stable entity ids."); - var factsById = generation.Entities.ToDictionary(entity => entity.EntityId, StringComparer.Ordinal); - var factsByIndex = generation.Entities.ToDictionary(entity => entity.SourceIndex); - GameObject? staging = null; - var created = new Dictionary(StringComparer.Ordinal); - var nextInstances = new Dictionary(StringComparer.Ordinal); - try - { - foreach (var fact in generation.Entities) - { - if (_instances.TryGetValue(fact.EntityId, out var existing) && existing != null) - { - nextInstances.Add(fact.EntityId, existing); - continue; - } - - staging ??= new GameObject($"{_root.name}.generation.{generation.ProducerEpoch}.{generation.Sequence}"); - if (staging.transform.parent == null) - { - staging.SetActive(false); - staging.transform.SetParent(_root, false); - } - var entity = fact.ToPlayableWorldEntity(); - var asset = new EveUnityPlayableWorldAssetBinding( - fact.AssetRef, - fact.EntityKind, - string.IsNullOrWhiteSpace(fact.AssetRef) ? "unity-generated-placeholder" : "provider-asset-ref"); - var instance = InstantiateEntity(entity, asset, staging.transform); - created.Add(fact.EntityId, instance); - nextInstances.Add(fact.EntityId, instance); - } - - foreach (var fact in generation.Entities) - { - var entity = fact.ToPlayableWorldEntity(); - var asset = new EveUnityPlayableWorldAssetBinding( - fact.AssetRef, - fact.EntityKind, - string.IsNullOrWhiteSpace(fact.AssetRef) ? "unity-generated-placeholder" : "provider-asset-ref"); - ApplyEntity(nextInstances[fact.EntityId], entity, asset); - } - - var nextById = factsById.ToDictionary( - pair => pair.Key, - pair => new EveUnityPresentedEntityHandle(pair.Value, nextInstances[pair.Key].transform), - StringComparer.Ordinal); - var nextByIndex = factsByIndex.ToDictionary( - pair => pair.Key, - pair => nextById[pair.Value.EntityId]); - - foreach (var pair in _instances) - if (!nextInstances.ContainsKey(pair.Key) && pair.Value != null) - DestroyInstance(pair.Value); - _instances.Clear(); - foreach (var pair in nextInstances) - { - if (created.ContainsKey(pair.Key)) - { - pair.Value.transform.SetParent(_root, true); - pair.Value.SetActive(true); - } - _instances.Add(pair.Key, pair.Value); - } - _presentedById = nextById; - _presentedByIndex = nextByIndex; - CurrentGeneration = generation; - } - catch - { - foreach (var instance in created.Values) - if (instance != null) DestroyInstance(instance); - throw; - } - finally - { - if (staging != null) - DestroyInstance(staging); - } + ApplySubjectVisibility(instance, entity.EntityId); } public void RemoveEntity(string entityId) @@ -187,33 +86,14 @@ namespace GameCult.Eve.UnityScene DestroyInstance(instance); } - public void Dispose() + private GameObject InstantiateEntity(EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset) { - foreach (var instance in _instances.Values) - if (instance != null) - { - instance.transform.SetParent(null, false); - instance.SetActive(false); - DestroyInstance(instance); - } - _instances.Clear(); - _presentedById = new Dictionary(StringComparer.Ordinal); - _presentedByIndex = new Dictionary(); - CurrentGeneration = null; - } - - private GameObject InstantiateEntity( - EveUnityPlayableWorldEntity entity, - EveUnityPlayableWorldAssetBinding asset, - Transform? parent = null) - { - parent ??= _root; var prefab = _assetProvider.ResolvePrefab(asset); GameObject instance; if (prefab != null) { instance = new GameObject(entity.EntityId); - instance.transform.SetParent(parent, false); + instance.transform.SetParent(_root, false); var visual = UnityEngine.Object.Instantiate(prefab, instance.transform); visual.transform.localPosition = Vector3.zero; visual.transform.localRotation = Quaternion.identity; @@ -223,8 +103,8 @@ namespace GameCult.Eve.UnityScene instance = GameObject.CreatePrimitive(PrimitiveType.Capsule); } - if (instance.transform.parent != parent) - instance.transform.SetParent(parent, false); + if (instance.transform.parent != _root) + instance.transform.SetParent(_root, false); return instance; } @@ -239,7 +119,9 @@ namespace GameCult.Eve.UnityScene : $"{entity.EntityId} ({entity.Label})"; instance.transform.localPosition = new Vector3(entity.PositionX, entity.PositionY, entity.PositionZ); instance.transform.localRotation = Quaternion.Euler(0f, entity.RotationY, 0f); - instance.transform.localScale = Vector3.one * (entity.Radius > 0f ? entity.Radius : 1f); + if (entity.Radius > 0f && + string.Equals(asset.PresentationKind, "unity-generated-placeholder", StringComparison.Ordinal)) + instance.transform.localScale = Vector3.one * entity.Radius; var marker = instance.GetComponent(); if (marker == null) @@ -264,6 +146,19 @@ namespace GameCult.Eve.UnityScene else UnityEngine.Object.DestroyImmediate(instance); } + + private void ApplySubjectVisibility() + { + foreach (var pair in _instances) + ApplySubjectVisibility(pair.Value, pair.Key); + } + + private void ApplySubjectVisibility(GameObject instance, string entityId) + { + if (instance == null || !string.Equals(entityId, _subjectEntityId, StringComparison.Ordinal)) + return; + instance.SetActive(_subjectVisible); + } } public sealed class EveUnitySemanticEntityPresentation : MonoBehaviour diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityInputActionBar.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityInputActionBar.cs deleted file mode 100644 index d434950..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityInputActionBar.cs +++ /dev/null @@ -1,295 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using GameCult.Eve.Surface; -using UnityEngine; -using UnityEngine.UIElements; - -#nullable enable - -namespace GameCult.Eve.UnityScene -{ - public sealed class EveUnityInputActionBar : MonoBehaviour - { - private readonly Dictionary _rows = - new Dictionary(StringComparer.Ordinal); - private readonly HashSet _pressedHolds = new HashSet(StringComparer.Ordinal); - private EveUnityPlayableWorldClientHost? _host; - private UIDocument? _document; - private VisualElement? _root; - private string _structureKey = ""; - private float _nextRefreshAt; - - public VisualElement? Root => _root; - public IReadOnlyCollection PresentedActionIds => _rows.Keys; - - private void Awake() => EnsureBuilt(); - - public void Bind(EveUnityPlayableWorldClientHost host) - { - _host = host != null ? host : throw new ArgumentNullException(nameof(host)); - EnsureBuilt(); - RefreshNow(); - } - - private void Update() - { - if (Time.unscaledTime < _nextRefreshAt) return; - _nextRefreshAt = Time.unscaledTime + 0.2f; - RefreshNow(); - } - - public void RefreshNow() - { - EnsureBuilt(); - var capability = _host?.InputCapability; - var actions = SelectActions(capability).ToArray(); - var structureKey = string.Join("\n", actions.Select(action => - $"{action.ActionId}\t{action.Label}\t{action.IconRef}\t{action.InputValue?.Model}")); - if (!string.Equals(_structureKey, structureKey, StringComparison.Ordinal)) - { - ReleaseAllHolds(); - Rebuild(actions); - _structureKey = structureKey; - } - foreach (var action in actions) - if (_rows.TryGetValue(action.ActionId ?? "", out var row)) - Apply(row, action); - if (_root != null) - _root.style.display = actions.Length == 0 ? DisplayStyle.None : DisplayStyle.Flex; - } - - public static IReadOnlyList SelectActions(EveInputCapabilityDocument? capability) - { - if (capability == null) return Array.Empty(); - var actions = (capability.Actions ?? Array.Empty()) - .Where(action => action != null && !string.IsNullOrWhiteSpace(action.ActionId)) - .ToDictionary(action => action.ActionId, StringComparer.Ordinal); - var selected = new List(); - var seen = new HashSet(StringComparer.Ordinal); - var profile = (capability.DefaultProfiles ?? Array.Empty()) - .FirstOrDefault(candidate => string.Equals(candidate.DeviceClass, "keyboard-mouse", StringComparison.Ordinal)) - ?? (capability.DefaultProfiles ?? Array.Empty()).FirstOrDefault(); - foreach (var binding in profile?.Bindings ?? Array.Empty()) - if (binding.ActionBar && actions.TryGetValue(binding.ActionId ?? "", out var action) && seen.Add(action.ActionId)) - selected.Add(action); - foreach (var action in actions.Values.OrderBy(candidate => candidate.ActionId, StringComparer.Ordinal)) - if (action.ActionBar && seen.Add(action.ActionId)) - selected.Add(action); - return selected; - } - - public void SubmitPerformed(string actionId) - { - var host = RequireHost(); - var action = EveUnityAdvertisedInputAction.Resolve(host.InputCapability, actionId); - if (action.IsViewDirection) - { - var view = host.ActiveCameraTransform; - if (view == null || view.forward.sqrMagnitude <= 0.000001f) return; - var direction = view.forward.normalized; - host.SubmitAdvertisedActionViewDirectionIntent( - RequireEntityId(host), actionId, direction.x, direction.y, direction.z); - return; - } - host.SubmitAdvertisedActionIntent(RequireEntityId(host), actionId); - } - - public void SubmitHold(string actionId, bool active) - { - var host = RequireHost(); - host.SubmitAdvertisedActionValueIntent(RequireEntityId(host), actionId, active ? 1f : 0f); - } - - public void SubmitScalar(string actionId, string value) - { - if (!double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed) || - double.IsNaN(parsed) || double.IsInfinity(parsed)) - throw new FormatException($"'{value}' is not a finite scalar value."); - var host = RequireHost(); - host.SubmitAdvertisedActionScalarIntent(RequireEntityId(host), actionId, parsed); - } - - private void Rebuild(IReadOnlyList actions) - { - _root!.Clear(); - _rows.Clear(); - _pressedHolds.Clear(); - foreach (var action in actions) - { - var row = BuildRow(action); - _rows[action.ActionId] = row; - _root.Add(row.Root); - } - } - - private void OnDisable() => ReleaseAllHolds(); - - private void ReleaseAllHolds() - { - foreach (var actionId in _pressedHolds.ToArray()) - { - try { SubmitHold(actionId, false); } - catch (InvalidOperationException) { } - } - _pressedHolds.Clear(); - } - - private ActionRow BuildRow(EveInputActionDocument action) - { - var root = new VisualElement { name = "eve-action-slot-" + SafeName(action.ActionId) }; - root.AddToClassList("eve-input-action-slot"); - root.style.width = 112; - root.style.minHeight = 72; - root.style.paddingLeft = root.style.paddingRight = 6; - root.style.paddingTop = root.style.paddingBottom = 5; - root.style.marginLeft = root.style.marginRight = 3; - root.style.backgroundColor = new Color(0.025f, 0.03f, 0.035f, 0.88f); - root.style.borderLeftWidth = root.style.borderRightWidth = 1; - root.style.borderTopWidth = root.style.borderBottomWidth = 1; - root.style.borderLeftColor = root.style.borderRightColor = new Color(0.35f, 0.42f, 0.46f, 0.8f); - root.style.borderTopColor = root.style.borderBottomColor = new Color(0.35f, 0.42f, 0.46f, 0.8f); - - var icon = new Image { name = "icon", scaleMode = ScaleMode.ScaleToFit }; - icon.AddToClassList("eve-input-action-icon"); - icon.style.height = 28; - root.Add(icon); - - var label = new Label(action.Label ?? action.ActionId) { name = "label" }; - label.AddToClassList("eve-input-action-label"); - label.style.fontSize = 9; - label.style.color = Color.white; - label.style.unityTextAlign = TextAnchor.MiddleCenter; - label.style.whiteSpace = WhiteSpace.Normal; - root.Add(label); - - TextField? scalar = null; - Button? trigger = null; - if (string.Equals(action.InputValue?.Model, EveUnityAdvertisedInputAction.ScalarValueModel, StringComparison.Ordinal)) - { - var controls = new VisualElement(); - controls.style.flexDirection = FlexDirection.Row; - scalar = new TextField { name = "scalar" }; - scalar.style.flexGrow = 1; - scalar.RegisterCallback(evt => - { - if (evt.keyCode != KeyCode.Return && evt.keyCode != KeyCode.KeypadEnter) return; - SubmitScalar(action.ActionId, scalar.value); - evt.StopPropagation(); - }); - controls.Add(scalar); - trigger = new Button(() => SubmitScalar(action.ActionId, scalar.value)) { text = "Set", name = "submit" }; - controls.Add(trigger); - root.Add(controls); - } - else - { - trigger = new Button { text = "Use", name = "trigger" }; - if (string.Equals(action.InputValue?.Model, EveUnityAdvertisedInputAction.ButtonHoldValueModel, StringComparison.Ordinal)) - { - trigger.RegisterCallback(evt => - { - if (!_pressedHolds.Add(action.ActionId)) return; - trigger.CapturePointer(evt.pointerId); - SubmitHold(action.ActionId, true); - }); - trigger.RegisterCallback(evt => - { - ReleaseHold(action.ActionId); - if (trigger.HasPointerCapture(evt.pointerId)) trigger.ReleasePointer(evt.pointerId); - }); - trigger.RegisterCallback(_ => ReleaseHold(action.ActionId)); - } - else - { - trigger.clicked += () => SubmitPerformed(action.ActionId); - } - root.Add(trigger); - } - return new ActionRow(root, icon, label, trigger, scalar); - } - - private void Apply(ActionRow row, EveInputActionDocument action) - { - row.Label.text = action.Label ?? action.ActionId; - var available = string.Equals(action.Availability, "available", StringComparison.OrdinalIgnoreCase); - row.Root.SetEnabled(available); - row.Root.style.opacity = available ? 1f : 0.45f; - if (row.Scalar != null && action.InputValue?.CurrentValue is double current && - row.Scalar.focusController?.focusedElement != row.Scalar) - { - row.Scalar.SetValueWithoutNotify(current.ToString("R", CultureInfo.InvariantCulture)); - row.Scalar.tooltip = string.IsNullOrWhiteSpace(action.InputValue.Unit) - ? "" : action.InputValue.Unit; - } - if (!string.IsNullOrWhiteSpace(action.IconRef) && _host?.NativeAssetProvider != null) - { - var texture = _host.NativeAssetProvider.ResolveAsset( - new EveUnityPlayableWorldAssetBinding(action.IconRef, "", "provider-asset-ref"), - typeof(Texture2D)) as Texture2D; - row.Icon.image = texture; - } - else row.Icon.image = null; - } - - private void ReleaseHold(string actionId) - { - if (!_pressedHolds.Remove(actionId)) return; - SubmitHold(actionId, false); - } - - private EveUnityPlayableWorldClientHost RequireHost() => - _host ?? throw new InvalidOperationException("The generic input action bar is not bound to a playable-world host."); - - private static string RequireEntityId(EveUnityPlayableWorldClientHost host) => - host.ActiveWorld?.PlayerEntityId ?? throw new InvalidOperationException("The playable world has no active player entity."); - - private void EnsureBuilt() - { - if (_root != null) - { - AttachToDocument(); - return; - } - _document = EveUnityUiDocumentRuntime.Ensure(gameObject); - _root = new VisualElement { name = "eve-input-action-bar" }; - _root.AddToClassList("eve-input-action-bar"); - _root.style.position = Position.Absolute; - _root.style.left = Length.Percent(50); - _root.style.bottom = 18; - _root.style.translate = new Translate(Length.Percent(-50), 0); - _root.style.flexDirection = FlexDirection.Row; - _root.style.alignItems = Align.FlexEnd; - AttachToDocument(); - } - - private void AttachToDocument() - { - var documentRoot = _document?.rootVisualElement; - if (documentRoot != null && _root != null && _root.parent == null) - documentRoot.Add(_root); - } - - private static string SafeName(string value) => - new string((value ?? "").Select(character => char.IsLetterOrDigit(character) ? character : '-').ToArray()); - - private sealed class ActionRow - { - public ActionRow(VisualElement root, Image icon, Label label, Button? trigger, TextField? scalar) - { - Root = root; - Icon = icon; - Label = label; - Trigger = trigger; - Scalar = scalar; - } - - public VisualElement Root { get; } - public Image Icon { get; } - public Label Label { get; } - public Button? Trigger { get; } - public TextField? Scalar { get; } - } - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityInputActionBar.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityInputActionBar.cs.meta deleted file mode 100644 index 36502a1..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityInputActionBar.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: bb62601554a846f39e46a6969c10eff7 diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index 19d1a63..942e9f9 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -1,6 +1,4 @@ using UnityEngine; -using UnityEngine.Rendering; -using UnityEngine.Rendering.Universal; #nullable enable @@ -8,13 +6,11 @@ namespace GameCult.Eve.UnityScene { public interface IEveUnityCameraRenderPolicySource { - bool TryGetRenderChannelLayer(string channel, out int layer); + bool TryGetCameraCullingMask(string viewId, out int cullingMask); } public sealed class EveUnityPlayableWorldCameraRig : MonoBehaviour { - private static EveUnityPlayableWorldCameraRig? _environmentOwner; - [SerializeField] private EveUnityPlayableWorldClientHost? host; [SerializeField] private Transform? cameraTransform; [SerializeField] private bool driveInLateUpdate = true; @@ -23,49 +19,17 @@ namespace GameCult.Eve.UnityScene [SerializeField] private float yawDegrees = 35f; [SerializeField] private float followDamping = 12f; private IEveUnityCameraRenderPolicySource? _renderPolicySource; - private EveUnityPlayableWorldClientHost? _cameraHost; - private bool _ownsAmbientEnvironment; - private AmbientMode _previousAmbientMode; - private Color _previousAmbientLight; - private float _previousAmbientIntensity; - private Material? _previousSkybox; - private DefaultReflectionMode _previousReflectionMode; - private Texture? _previousCustomReflection; - private float _previousReflectionIntensity; - private Camera? _environmentCamera; - private CameraClearFlags _previousCameraClearFlags; - private GameObject? _keyLightObject; - private GameObject? _postProcessVolumeObject; - private Volume? _postProcessVolume; - private UniversalAdditionalCameraData? _postProcessCameraData; - private EveUnityAdaptiveExposureRenderer? _adaptiveExposure; - private bool _createdPostProcessCameraData; - private bool _previousRenderPostProcessing; - private AntialiasingMode _previousAntialiasing; - private TemporalAA.Settings _previousTaaSettings; - private UniversalRenderPipelineAsset? _colorGradingPipelineAsset; - private ColorGradingMode _previousColorGradingMode; public EveUnityPlayableWorldClientHost? Host { get => host; - set - { - if (ReferenceEquals(host, value)) return; - ReleaseRig(); - host = value; - } + set => host = value; } public Transform? CameraTransform { get => cameraTransform; - set - { - if (ReferenceEquals(cameraTransform, value)) return; - ReleaseRig(); - cameraTransform = value; - } + set => cameraTransform = value; } public IEveUnityCameraRenderPolicySource? RenderPolicySource @@ -74,102 +38,34 @@ namespace GameCult.Eve.UnityScene set => _renderPolicySource = value; } - public void ReleaseRig() - { - ReleaseCamera(); - RestoreAmbientEnvironment(); - } - public bool ApplyRig(float deltaTime) { var resolvedHost = ResolveHost(); var activeWorld = resolvedHost?.ActiveWorld; if (resolvedHost == null || activeWorld == null) - { - ReleaseCamera(); - RestoreAmbientEnvironment(); return false; - } - if ((activeWorld.CameraRig == "planar.top-down-follow.v1" || - activeWorld.CameraRig == "perspective.entity-forward-follow.v1") && - !HasValidFramedFollowContract(activeWorld)) - { - ReleaseCamera(); - RestoreAmbientEnvironment(); - return false; - } if (!string.IsNullOrWhiteSpace(activeWorld.CameraRig) && - activeWorld.CameraRig != "planar.top-down-follow.v1" && - activeWorld.CameraRig != "perspective.entity-forward-follow.v1" && activeWorld.CameraRig != "arpg.orbital-follow.v1" && activeWorld.CameraRig != "third-person-orbit") { - ReleaseCamera(); return false; } var camera = cameraTransform != null ? cameraTransform : transform; - var targetEntityId = string.IsNullOrWhiteSpace(activeWorld.CameraTargetEntityId) - ? activeWorld.PlayerEntityId - : activeWorld.CameraTargetEntityId; - var player = FindEntity(resolvedHost, targetEntityId); - if (player == null) - { - ReleaseCamera(); + var targetMarker = FindEntityMarker(resolvedHost, activeWorld.CameraTargetEntityId); + if (targetMarker == null) return false; - } - if (!TryResolveEnvironment(resolvedHost, activeWorld, out var skybox, out var reflection, out var postProcessProfile)) - { - ReleaseRig(); - return false; - } - if (!resolvedHost.TryClaimWorldCamera(this, camera)) - { - RestoreAmbientEnvironment(); - return false; - } - _cameraHost = resolvedHost; - if (_environmentOwner != null && !ReferenceEquals(_environmentOwner, this)) - { - ReleaseCamera(); - return false; - } - _environmentOwner = this; var cameraComponent = camera.GetComponent(); - if (postProcessProfile != null && cameraComponent == null) + if (cameraComponent != null && + _renderPolicySource != null && + _renderPolicySource.TryGetCameraCullingMask(activeWorld.ViewId, out var cullingMask)) { - ReleaseRig(); - return false; - } - if (!ApplyColorGradingSpace(activeWorld)) - { - ReleaseRig(); - return false; - } - ApplyAmbientEnvironment(activeWorld, cameraComponent, skybox, reflection, postProcessProfile); - ApplyKeyLight(activeWorld); - if (cameraComponent != null && _renderPolicySource != null) - { - var cullingMask = cameraComponent.cullingMask; - foreach (var channel in activeWorld.ExcludedRenderChannels) - { - if (_renderPolicySource.TryGetRenderChannelLayer(channel, out var layer) && - layer >= 0 && layer < 32) - cullingMask &= ~(1 << layer); - } cameraComponent.cullingMask = cullingMask; } - if (activeWorld.CameraRig == "planar.top-down-follow.v1") - return ApplyPlanarTopDown(activeWorld, camera, cameraComponent, player.position, deltaTime); - if (activeWorld.CameraRig == "perspective.entity-forward-follow.v1") - return ApplyEntityForwardFollow(activeWorld, resolvedHost, camera, cameraComponent, player, deltaTime); - - var bounds = CalculateVisualBounds( - player, - cameraComponent != null ? cameraComponent.cullingMask : -1); - var target = bounds?.center ?? player.position; + var bounds = CalculateVisualBounds(targetMarker); + var target = bounds?.center ?? targetMarker.transform.position; var radius = bounds?.extents.magnitude ?? 0f; var verticalFov = cameraComponent != null ? cameraComponent.fieldOfView : 60f; var framingDistance = radius > 0f @@ -188,524 +84,23 @@ namespace GameCult.Eve.UnityScene return true; } - private static bool ApplyPlanarTopDown( - EveUnityPlayableWorldProjection world, - Transform camera, - Camera? cameraComponent, - Vector3 target, - float deltaTime) - { - var resolvedDistance = world.CameraDistance; - var verticalFov = world.CameraVerticalFieldOfViewDegrees; - if (cameraComponent != null) - { - cameraComponent.orthographic = false; - cameraComponent.lensShift = Vector2.zero; - cameraComponent.fieldOfView = verticalFov; - cameraComponent.nearClipPlane = world.CameraNearClipPlane; - cameraComponent.farClipPlane = world.CameraFarClipPlane; - } - var aspect = cameraComponent != null ? cameraComponent.aspect : 16f / 9f; - var verticalHalfExtent = resolvedDistance * Mathf.Tan(verticalFov * 0.5f * Mathf.Deg2Rad); - var horizontalHalfExtent = verticalHalfExtent * Mathf.Max(0.01f, aspect); - var screenOffset = new Vector3( - (Mathf.Clamp01(world.CameraTargetScreenX) - 0.5f) * 2f * horizontalHalfExtent, - 0f, - (Mathf.Clamp01(world.CameraTargetScreenY) - 0.5f) * 2f * verticalHalfExtent); - var desiredPosition = target + (Vector3.up * resolvedDistance) - screenOffset; - var damping = Mathf.Max(0f, world.CameraPositionDamping); - var t = deltaTime <= 0f || damping <= 0f - ? 1f - : 1f - Mathf.Exp(-damping * deltaTime); - camera.position = Vector3.Lerp(camera.position, desiredPosition, t); - camera.rotation = Quaternion.LookRotation(Vector3.down, Vector3.forward); - - return true; - } - - private static bool ApplyEntityForwardFollow( - EveUnityPlayableWorldProjection world, - EveUnityPlayableWorldClientHost host, - Transform camera, - Camera? cameraComponent, - Transform target, - float deltaTime) - { - var resolvedDistance = world.CameraDistance; - var verticalFov = world.CameraVerticalFieldOfViewDegrees; - if (cameraComponent != null) - { - cameraComponent.orthographic = false; - cameraComponent.lensShift = Vector2.zero; - cameraComponent.fieldOfView = verticalFov; - cameraComponent.nearClipPlane = world.CameraNearClipPlane; - cameraComponent.farClipPlane = world.CameraFarClipPlane; - } - - var forward = target.forward; - forward.y = 0f; - if (forward.sqrMagnitude <= 0.0001f) - forward = Vector3.forward; - forward.Normalize(); - var rotation = Quaternion.LookRotation(forward, Vector3.up); - var aspect = cameraComponent != null ? cameraComponent.aspect : 16f / 9f; - var verticalHalfExtent = resolvedDistance * Mathf.Tan(verticalFov * 0.5f * Mathf.Deg2Rad); - var horizontalHalfExtent = verticalHalfExtent * Mathf.Max(0.01f, aspect); - var horizontalOffset = (Mathf.Clamp01(world.CameraTargetScreenX) - 0.5f) * 2f * horizontalHalfExtent; - var verticalOffset = (Mathf.Clamp01(world.CameraTargetScreenY) - 0.5f) * 2f * verticalHalfExtent; - if (string.Equals(world.CameraLookAt, "aim.convergence-point.v1", System.StringComparison.Ordinal)) - { - var aim = EveUnityAimPresentation.Find(host.ActiveProjection); - if (aim == null || !aim.TryResolveViewPoint(host, out var aimPoint)) return false; - var targetToAim = aimPoint - target.position; - var targetToAimDistanceSquared = targetToAim.sqrMagnitude; - var screenOffsetSquared = horizontalOffset * horizontalOffset + verticalOffset * verticalOffset; - if (targetToAimDistanceSquared <= screenOffsetSquared + 0.0001f) return false; - var targetToAimDirection = targetToAim.normalized; - var opticalDepthDelta = Mathf.Sqrt(targetToAimDistanceSquared - screenOffsetSquared); - var cameraLocalTargetToAim = new Vector3( - -horizontalOffset, - -verticalOffset, - opticalDepthDelta); - rotation = Quaternion.LookRotation(targetToAimDirection, Vector3.up) * - Quaternion.FromToRotation(cameraLocalTargetToAim.normalized, Vector3.forward); - } - var right = rotation * Vector3.right; - var up = rotation * Vector3.up; - var opticalForward = rotation * Vector3.forward; - var desiredPosition = target.position - - (opticalForward * resolvedDistance) - - (right * horizontalOffset) - - (up * verticalOffset); - var damping = Mathf.Max(0f, world.CameraPositionDamping); - var t = deltaTime <= 0f || damping <= 0f - ? 1f - : 1f - Mathf.Exp(-damping * deltaTime); - camera.position = Vector3.Lerp(camera.position, desiredPosition, t); - camera.rotation = rotation; - return true; - } - - private static bool HasValidFramedFollowContract(EveUnityPlayableWorldProjection world) => - world.CameraDistance > 0f && - world.CameraVerticalFieldOfViewDegrees > 0f && - world.CameraVerticalFieldOfViewDegrees < 180f && - world.CameraTargetScreenX >= 0f && world.CameraTargetScreenX <= 1f && - world.CameraTargetScreenY >= 0f && world.CameraTargetScreenY <= 1f && - world.CameraPositionDamping >= 0f && - world.CameraNearClipPlane > 0f && - world.CameraFarClipPlane > world.CameraNearClipPlane; - - private static bool TryResolveEnvironment( - EveUnityPlayableWorldClientHost host, - EveUnityPlayableWorldProjection world, - out Material? skybox, - out Cubemap? reflection, - out VolumeProfile? postProcessProfile) - { - skybox = null; - reflection = null; - postProcessProfile = null; - if (!IsFinite(world.AmbientLightR) || - !IsFinite(world.AmbientLightG) || - !IsFinite(world.AmbientLightB) || - !IsFinite(world.AmbientLightIntensity) || - !IsFinite(world.ReflectionIntensity) || - !IsFinite(world.KeyLightDirectionX) || - !IsFinite(world.KeyLightDirectionY) || - !IsFinite(world.KeyLightDirectionZ) || - !IsFinite(world.KeyLightColorR) || - !IsFinite(world.KeyLightColorG) || - !IsFinite(world.KeyLightColorB) || - !IsFinite(world.KeyLightIntensity) || - !IsFinite(world.TemporalHistoryBlend) || - !IsFinite(world.TemporalJitterScale) || - !IsFinite(world.TemporalSharpening) || - (!string.IsNullOrWhiteSpace(world.CameraReconstruction) && - !string.Equals(world.CameraReconstruction, "temporal-reprojection.v1", System.StringComparison.Ordinal)) || - (!string.IsNullOrWhiteSpace(world.ColorGradingSpace) && - !string.Equals(world.ColorGradingSpace, "hdr-before-tonemap.v1", System.StringComparison.Ordinal)) || - (string.Equals(world.CameraReconstruction, "temporal-reprojection.v1", System.StringComparison.Ordinal) && - (world.TemporalHistoryBlend < 0f || world.TemporalHistoryBlend > 1f || - world.TemporalJitterScale < 0f || world.TemporalJitterScale > 1f || - world.TemporalSharpening < 0f || world.TemporalSharpening > 1f || - !IsTemporalQuality(world.TemporalQuality)))) - return false; - var assets = host.NativeAssetProvider; - if (!string.IsNullOrWhiteSpace(world.SkyboxAssetRef)) - { - skybox = assets?.ResolveAsset( - new EveUnityPlayableWorldAssetBinding( - world.SkyboxAssetRef, - "", - "provider-asset-ref"), - typeof(Material)) as Material; - if (skybox == null || skybox.shader == null || !skybox.shader.isSupported) - return false; - } - if (!string.IsNullOrWhiteSpace(world.ReflectionAssetRef)) - { - reflection = assets?.ResolveAsset( - new EveUnityPlayableWorldAssetBinding( - world.ReflectionAssetRef, - "", - "provider-asset-ref"), - typeof(Cubemap)) as Cubemap; - if (reflection == null) - return false; - } - if (!string.IsNullOrWhiteSpace(world.PostProcessProfileAssetRef)) - { - postProcessProfile = assets?.ResolveAsset( - new EveUnityPlayableWorldAssetBinding( - world.PostProcessProfileAssetRef, - "", - "provider-asset-ref"), - typeof(VolumeProfile)) as VolumeProfile; - if (postProcessProfile == null) - return false; - } - return true; - } - - private bool ApplyColorGradingSpace(EveUnityPlayableWorldProjection world) - { - if (string.IsNullOrWhiteSpace(world.ColorGradingSpace)) - { - RestoreColorGradingSpace(); - return true; - } - var asset = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset; - if (asset == null || - !string.Equals(world.ColorGradingSpace, "hdr-before-tonemap.v1", System.StringComparison.Ordinal)) - return false; - if (!ReferenceEquals(_colorGradingPipelineAsset, asset)) - { - RestoreColorGradingSpace(); - _colorGradingPipelineAsset = asset; - _previousColorGradingMode = asset.colorGradingMode; - } - asset.colorGradingMode = ColorGradingMode.HighDynamicRange; - return true; - } - - private void RestoreColorGradingSpace() - { - if (_colorGradingPipelineAsset == null) return; - _colorGradingPipelineAsset.colorGradingMode = _previousColorGradingMode; - _colorGradingPipelineAsset = null; - } - - private void ApplyKeyLight(EveUnityPlayableWorldProjection world) - { - var direction = new Vector3( - world.KeyLightDirectionX, - world.KeyLightDirectionY, - world.KeyLightDirectionZ); - if (world.KeyLightIntensity <= 0f || direction.sqrMagnitude <= 0.000001f) - { - ReleaseKeyLight(); - return; - } - - if (_keyLightObject == null) - { - _keyLightObject = new GameObject("Eve World Key Light"); - _keyLightObject.transform.SetParent(transform, worldPositionStays: false); - var created = _keyLightObject.AddComponent(); - created.type = LightType.Directional; - created.shadows = LightShadows.None; - } - - var light = _keyLightObject.GetComponent(); - light.color = new Color( - Mathf.Max(0f, world.KeyLightColorR), - Mathf.Max(0f, world.KeyLightColorG), - Mathf.Max(0f, world.KeyLightColorB), - 1f); - light.intensity = Mathf.Max(0f, world.KeyLightIntensity); - var normalized = direction.normalized; - var up = Mathf.Abs(Vector3.Dot(normalized, Vector3.up)) > 0.999f - ? Vector3.forward - : Vector3.up; - _keyLightObject.transform.rotation = Quaternion.LookRotation(normalized, up); - _keyLightObject.SetActive(true); - } - - private void ReleaseKeyLight() - { - if (_keyLightObject == null) - return; - _keyLightObject.SetActive(false); - if (Application.isPlaying) - Destroy(_keyLightObject); - else - DestroyImmediate(_keyLightObject); - _keyLightObject = null; - } - - private static bool IsFinite(float value) => !float.IsNaN(value) && !float.IsInfinity(value); - - private void ApplyAmbientEnvironment( - EveUnityPlayableWorldProjection world, - Camera? camera, - Material? skybox, - Cubemap? reflection, - VolumeProfile? postProcessProfile) - { - var environmentChanged = !ReferenceEquals(RenderSettings.skybox, skybox); - if (!_ownsAmbientEnvironment) - { - _previousAmbientMode = RenderSettings.ambientMode; - _previousAmbientLight = RenderSettings.ambientLight; - _previousAmbientIntensity = RenderSettings.ambientIntensity; - _previousSkybox = RenderSettings.skybox; - _previousReflectionMode = RenderSettings.defaultReflectionMode; - _previousCustomReflection = RenderSettings.customReflectionTexture; - _previousReflectionIntensity = RenderSettings.reflectionIntensity; - _environmentCamera = camera; - if (camera != null) - _previousCameraClearFlags = camera.clearFlags; - _ownsAmbientEnvironment = true; - } - if (skybox != null) - { - RenderSettings.skybox = skybox; - RenderSettings.ambientMode = AmbientMode.Skybox; - RenderSettings.ambientIntensity = Mathf.Max(0f, world.AmbientLightIntensity); - if (camera != null) - camera.clearFlags = CameraClearFlags.Skybox; - } - else - { - RenderSettings.skybox = _previousSkybox; - RenderSettings.ambientMode = AmbientMode.Flat; - RenderSettings.ambientIntensity = _previousAmbientIntensity; - RenderSettings.ambientLight = new Color( - Mathf.Max(0f, world.AmbientLightR * world.AmbientLightIntensity), - Mathf.Max(0f, world.AmbientLightG * world.AmbientLightIntensity), - Mathf.Max(0f, world.AmbientLightB * world.AmbientLightIntensity), - 1f); - if (_environmentCamera != null) - _environmentCamera.clearFlags = _previousCameraClearFlags; - } - if (reflection != null) - { - RenderSettings.defaultReflectionMode = DefaultReflectionMode.Custom; - RenderSettings.customReflectionTexture = reflection; - RenderSettings.reflectionIntensity = Mathf.Max(0f, world.ReflectionIntensity); - } - else - { - RenderSettings.defaultReflectionMode = _previousReflectionMode; - RenderSettings.customReflectionTexture = _previousCustomReflection; - RenderSettings.reflectionIntensity = _previousReflectionIntensity; - } - if (environmentChanged) - DynamicGI.UpdateEnvironment(); - ApplyCameraRendering(camera, postProcessProfile, world); - } - - private void ApplyCameraRendering( - Camera? camera, - VolumeProfile? profile, - EveUnityPlayableWorldProjection world) - { - var usesTemporalReprojection = string.Equals( - world.CameraReconstruction, - "temporal-reprojection.v1", - System.StringComparison.Ordinal); - var usesAdaptiveExposure = string.Equals( - world.ExposureMode, - "histogram.v1", - System.StringComparison.Ordinal); - if (profile == null && !usesTemporalReprojection && !usesAdaptiveExposure) - { - RestorePostProcess(); - return; - } - if (camera == null) - return; - if (_postProcessCameraData != null && !ReferenceEquals(_postProcessCameraData.gameObject, camera.gameObject)) - RestorePostProcess(); - if (_postProcessCameraData == null) - { - _postProcessCameraData = camera.GetComponent(); - _createdPostProcessCameraData = _postProcessCameraData == null; - if (_postProcessCameraData == null) - _postProcessCameraData = camera.gameObject.AddComponent(); - _previousRenderPostProcessing = _postProcessCameraData.renderPostProcessing; - _previousAntialiasing = _postProcessCameraData.antialiasing; - _previousTaaSettings = _postProcessCameraData.taaSettings; - } - _postProcessCameraData.renderPostProcessing = profile != null || _previousRenderPostProcessing; - if (usesTemporalReprojection) - { - _postProcessCameraData.antialiasing = AntialiasingMode.TemporalAntiAliasing; - ref var settings = ref _postProcessCameraData.taaSettings; - settings = TemporalAA.Settings.Create(); - settings.quality = ParseTemporalQuality(world.TemporalQuality); - settings.baseBlendFactor = world.TemporalHistoryBlend; - settings.jitterScale = world.TemporalJitterScale; - settings.contrastAdaptiveSharpening = world.TemporalSharpening; - } - else - { - _postProcessCameraData.antialiasing = _previousAntialiasing; - _postProcessCameraData.taaSettings = _previousTaaSettings; - } - if (profile != null) - { - if (_postProcessVolumeObject == null) - { - _postProcessVolumeObject = new GameObject("Eve World Post Process"); - _postProcessVolumeObject.transform.SetParent(transform, worldPositionStays: false); - _postProcessVolume = _postProcessVolumeObject.AddComponent(); - _postProcessVolume.isGlobal = true; - _postProcessVolume.priority = 1000f; - _postProcessVolume.weight = 1f; - } - _postProcessVolume!.sharedProfile = profile; - _postProcessVolumeObject.SetActive(true); - } - else if (_postProcessVolumeObject != null) - { - _postProcessVolumeObject.SetActive(false); - } - - if (usesAdaptiveExposure) - { - if (_adaptiveExposure == null) - _adaptiveExposure = camera.gameObject.AddComponent(); - _adaptiveExposure.Configure( - world.ExposureLowPercent, - world.ExposureHighPercent, - world.ExposureMinimumEv, - world.ExposureMaximumEv, - world.ExposureKeyValue, - world.ExposureAdaptation, - world.ExposureSpeedUp, - world.ExposureSpeedDown); - } - else - { - ReleaseAdaptiveExposure(); - } - } - - private static bool IsTemporalQuality(string quality) => - quality == "very-low" || quality == "low" || quality == "medium" || - quality == "high" || quality == "very-high"; - - private static TemporalAAQuality ParseTemporalQuality(string quality) - { - switch (quality) - { - case "very-low": return TemporalAAQuality.VeryLow; - case "low": return TemporalAAQuality.Low; - case "medium": return TemporalAAQuality.Medium; - case "very-high": return TemporalAAQuality.VeryHigh; - default: return TemporalAAQuality.High; - } - } - - private void RestorePostProcess() - { - RestoreColorGradingSpace(); - ReleaseAdaptiveExposure(); - if (_postProcessCameraData != null) - { - if (_createdPostProcessCameraData) - { - if (Application.isPlaying) - Destroy(_postProcessCameraData); - else - DestroyImmediate(_postProcessCameraData); - } - else - { - _postProcessCameraData.renderPostProcessing = _previousRenderPostProcessing; - _postProcessCameraData.antialiasing = _previousAntialiasing; - _postProcessCameraData.taaSettings = _previousTaaSettings; - } - } - _postProcessCameraData = null; - _createdPostProcessCameraData = false; - if (_postProcessVolumeObject != null) - { - if (Application.isPlaying) - Destroy(_postProcessVolumeObject); - else - DestroyImmediate(_postProcessVolumeObject); - } - _postProcessVolumeObject = null; - _postProcessVolume = null; - } - - private void ReleaseAdaptiveExposure() - { - if (_adaptiveExposure == null) return; - if (Application.isPlaying) - Destroy(_adaptiveExposure); - else - DestroyImmediate(_adaptiveExposure); - _adaptiveExposure = null; - } - - private void RestoreAmbientEnvironment() - { - ReleaseKeyLight(); - RestorePostProcess(); - if (!_ownsAmbientEnvironment) return; - var environmentChanged = !ReferenceEquals(RenderSettings.skybox, _previousSkybox); - RenderSettings.ambientMode = _previousAmbientMode; - RenderSettings.ambientLight = _previousAmbientLight; - RenderSettings.ambientIntensity = _previousAmbientIntensity; - RenderSettings.skybox = _previousSkybox; - RenderSettings.defaultReflectionMode = _previousReflectionMode; - RenderSettings.customReflectionTexture = _previousCustomReflection; - RenderSettings.reflectionIntensity = _previousReflectionIntensity; - if (_environmentCamera != null) - _environmentCamera.clearFlags = _previousCameraClearFlags; - _environmentCamera = null; - _previousSkybox = null; - _previousCustomReflection = null; - _ownsAmbientEnvironment = false; - if (ReferenceEquals(_environmentOwner, this)) - _environmentOwner = null; - if (environmentChanged) - DynamicGI.UpdateEnvironment(); - } - - private void ReleaseCamera() - { - _cameraHost?.ReleaseWorldCamera(this); - _cameraHost = null; - } - - private static Bounds? CalculateVisualBounds(Transform player, int cullingMask) + private static Bounds? CalculateVisualBounds(EveUnityPlayableWorldEntityMarker target) { + var renderers = target.GetComponentsInChildren(includeInactive: false); Bounds? bounds = null; - foreach (var root in new[] { player }) + foreach (var visual in renderers) { - foreach (var visual in root.GetComponentsInChildren(includeInactive: false)) + if (visual == null || !visual.enabled) + continue; + if (bounds.HasValue) { - if (visual == null || !visual.enabled || - (cullingMask & (1 << visual.gameObject.layer)) == 0) - continue; - if (visual is LineRenderer || visual is TrailRenderer || - string.Equals(visual.GetType().Name, "ParticleSystemRenderer", System.StringComparison.Ordinal)) - continue; - if (bounds.HasValue) - { - var combined = bounds.Value; - combined.Encapsulate(visual.bounds); - bounds = combined; - } - else - { - bounds = visual.bounds; - } + var combined = bounds.Value; + combined.Encapsulate(visual.bounds); + bounds = combined; + } + else + { + bounds = visual.bounds; } } return bounds; @@ -717,10 +112,6 @@ namespace GameCult.Eve.UnityScene ApplyRig(Time.deltaTime); } - private void OnDisable() => ReleaseRig(); - - private void OnDestroy() => ReleaseRig(); - private EveUnityPlayableWorldClientHost? ResolveHost() { if (host != null) @@ -730,23 +121,18 @@ namespace GameCult.Eve.UnityScene return host; } - private static Transform? FindEntity( + private static EveUnityPlayableWorldEntityMarker? FindEntityMarker( EveUnityPlayableWorldClientHost host, - string playerEntityId) + string entityId) { - if (string.IsNullOrWhiteSpace(playerEntityId)) + if (string.IsNullOrWhiteSpace(entityId)) return null; - if (host.PresentedEntities?.CurrentGeneration != null) - return host.PresentedEntities.TryGetByEntityId(playerEntityId, out var presented) - ? presented.Transform - : null; - - var markers = host.SceneRoot.GetComponentsInChildren(); + var markers = host.SceneRoot.GetComponentsInChildren(includeInactive: true); foreach (var marker in markers) { - if (marker != null && marker.EntityId == playerEntityId) - return marker.transform; + if (marker != null && marker.EntityId == entityId) + return marker; } return null; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientBootstrap.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientBootstrap.cs index ea39b34..fcd0924 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientBootstrap.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientBootstrap.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using UnityEngine; #nullable enable @@ -30,378 +29,13 @@ namespace GameCult.Eve.UnityScene public EveUnityPlayableWorldPresentation? LastPresentation { get; private set; } - public EveUnitySceneNavigationFailure? LastNavigationFailure { get; private set; } - - public event Action? NavigationFailed; - - private IEveUnitySceneCommandReceiptSource? _navigationReceiptSource; - private bool _navigationInProgress; - private StagedPresentation? _ownedPresentation; - public void ConfigureProvider(MonoBehaviour providerComponent) { - UnbindNavigation(); provider = providerComponent != null ? providerComponent : throw new ArgumentNullException(nameof(providerComponent)); providerSurfaceDocuments = provider; commandSink = provider; assetManifestDocuments = provider; receiptSource = provider; - BindNavigation(); - } - - private void OnDestroy() - { - UnbindNavigation(); - _ownedPresentation?.Dispose(); - _ownedPresentation = null; - } - - private void BindNavigation() - { - if (provider is not IEveUnityNavigableProvider || provider is not IEveUnitySceneCommandReceiptSource source) - return; - _navigationReceiptSource = source; - source.ReceiptAvailable += OnReceiptAvailable; - } - - private void UnbindNavigation() - { - if (_navigationReceiptSource != null) - _navigationReceiptSource.ReceiptAvailable -= OnReceiptAvailable; - _navigationReceiptSource = null; - } - - private void OnReceiptAvailable(EveUnitySceneCommandReceipt receipt) - { - if (_navigationInProgress || receipt.Navigation == null) - return; - if (!string.Equals(receipt.State, "accepted", StringComparison.OrdinalIgnoreCase) && - !string.Equals(receipt.State, "reconciled", StringComparison.OrdinalIgnoreCase)) - return; - StartCoroutine(NavigateAsync(receipt.Navigation)); - } - - private IEnumerator NavigateAsync(EveUnitySceneNavigationTarget target) - { - if (provider is not IEveUnityNavigableProvider navigable) - yield break; - _navigationInProgress = true; - var navigation = navigable.NavigateAsync(target); - StagedPresentation? staged = null; - PresentationSwap? swap = null; - Transform? previousRoot = null; - var previousRootWasActive = false; - while (!navigation.IsCompleted) - yield return null; - try - { - navigation.GetAwaiter().GetResult(); - staged = PrepareStagedPresentation(); - previousRoot = sceneRoot; - previousRootWasActive = previousRoot == null || previousRoot.gameObject.activeSelf; - if (previousRoot != null && !ReferenceEquals(previousRoot, transform) && - !ReferenceEquals(previousRoot, staged.SceneRoot)) - previousRoot.gameObject.SetActive(false); - navigable.CommitNavigation(); - swap = ActivateStagedPresentation(staged, previousRootWasActive); - navigable.FinalizeNavigation(); - swap.Complete(); - swap = null; - staged = null; - LastNavigationFailure = null; - } - catch (Exception error) - { - var providerRestored = false; - try - { - navigable.RollbackNavigation(); - providerRestored = true; - } - catch (Exception rollbackError) - { - Debug.LogError($"Eve provider route rollback failed; the previous presentation will remain inactive. {rollbackError}"); - } - if (providerRestored) - { - try - { - if (swap != null) - swap.Rollback(); - else if (previousRoot != null) - previousRoot.gameObject.SetActive(previousRootWasActive); - } - catch (Exception rollbackError) - { - Debug.LogError($"Eve previous presentation rollback failed after provider restoration. {rollbackError}"); - } - } - try - { - staged?.Dispose(); - } - catch (Exception cleanupError) - { - Debug.LogWarning($"Eve rejected presentation cleanup failed. {cleanupError}"); - } - var failure = new EveUnitySceneNavigationFailure(target, error.Message, DateTimeOffset.UtcNow); - LastNavigationFailure = failure; - NavigationFailed?.Invoke(failure); - Debug.LogError($"Eve provider navigation failed; the current surface remains mounted. {error}"); - } - finally - { - _navigationInProgress = false; - } - } - - private StagedPresentation PrepareStagedPresentation() - { - var resolvedProvider = provider; - var surfaceDocuments = ResolveBehaviour( - providerSurfaceDocuments, - resolvedProvider, - nameof(providerSurfaceDocuments)); - var resolvedCommandSink = ResolveBehaviour( - commandSink, - resolvedProvider, - nameof(commandSink)); - var resolvedAssetDocuments = ResolveOptionalBehaviour( - assetManifestDocuments, - resolvedProvider); - var resolvedReceiptSource = ResolveOptionalBehaviour( - receiptSource, - resolvedProvider); - var resolvedFallbackAssetProvider = ResolveOptionalBehaviour( - fallbackAssetProvider, - resolvedProvider); - - var stagingObject = new GameObject("Eve Unity Candidate Presentation"); - stagingObject.SetActive(false); - stagingObject.transform.SetParent(transform, false); - var stagingRootObject = new GameObject("Eve Unity Candidate Scene Root"); - stagingRootObject.transform.SetParent(stagingObject.transform, false); - var stagingHost = stagingObject.AddComponent(); - stagingHost.ConnectOnEnable = false; - try - { - stagingHost.Configure( - stagingRootObject.transform, - surfaceDocuments, - resolvedCommandSink, - resolvedAssetDocuments, - resolvedReceiptSource, - resolvedFallbackAssetProvider); - var presentation = stagingHost.Connect(); - return new StagedPresentation(stagingObject, stagingRootObject.transform, stagingHost, presentation); - } - catch - { - stagingHost.Disconnect(); - DestroyObject(stagingObject); - throw; - } - } - - private PresentationSwap ActivateStagedPresentation( - StagedPresentation staged, - bool previousRootWasActive) - { - var previousHost = host; - var previousRoot = sceneRoot; - var previousPresentation = LastPresentation; - var input = GetComponent(); - var previousInputHost = input?.Host; - var previousInputCamera = input?.CameraTransform; - var rig = GetComponent(); - var previousRigHost = rig?.Host; - var previousRigCamera = rig?.CameraTransform; - var previousRigPolicy = rig?.RenderPolicySource; - - try - { - staged.GameObject.SetActive(true); - if (attachInputDriver) - ConfigureInputDriver(staged.Host); - if (attachCameraRig) - ConfigureCameraRig(staged.Host); - host = staged.Host; - sceneRoot = staged.SceneRoot; - LastPresentation = staged.Presentation; - return new PresentationSwap( - this, - staged, - _ownedPresentation, - previousHost, - previousRoot, - previousPresentation, - previousRootWasActive, - input, - previousInputHost, - previousInputCamera, - rig, - previousRigHost, - previousRigCamera, - previousRigPolicy); - } - catch - { - if (input != null) - { - input.Host = previousInputHost; - input.CameraTransform = previousInputCamera; - } - if (rig != null) - { - rig.Host = previousRigHost; - rig.CameraTransform = previousRigCamera; - rig.RenderPolicySource = previousRigPolicy; - } - host = previousHost; - sceneRoot = previousRoot; - LastPresentation = previousPresentation; - throw; - } - } - - private static void DestroyObject(UnityEngine.Object value) - { - if (Application.isPlaying) - Destroy(value); - else - DestroyImmediate(value); - } - - private sealed class StagedPresentation : IDisposable - { - public StagedPresentation( - GameObject gameObject, - Transform sceneRoot, - EveUnityPlayableWorldClientHost host, - EveUnityPlayableWorldPresentation presentation) - { - GameObject = gameObject; - SceneRoot = sceneRoot; - Host = host; - Presentation = presentation; - } - - public GameObject GameObject { get; } - public Transform SceneRoot { get; } - public EveUnityPlayableWorldClientHost Host { get; } - public EveUnityPlayableWorldPresentation Presentation { get; } - - public void Dispose() - { - try - { - Host.Disconnect(); - } - catch (Exception error) - { - Debug.LogWarning($"Eve presentation disconnect failed during cleanup: {error}"); - } - finally - { - DestroyObject(GameObject); - } - } - } - - private sealed class PresentationSwap - { - private readonly EveUnityPlayableWorldClientBootstrap _owner; - private readonly StagedPresentation _candidate; - private readonly StagedPresentation? _previousOwnedPresentation; - private readonly EveUnityPlayableWorldClientHost? _previousHost; - private readonly Transform? _previousRoot; - private readonly EveUnityPlayableWorldPresentation? _previousPresentation; - private readonly bool _previousRootWasActive; - private readonly EveUnityPlayableWorldInputDriver? _input; - private readonly EveUnityPlayableWorldClientHost? _previousInputHost; - private readonly Transform? _previousInputCamera; - private readonly EveUnityPlayableWorldCameraRig? _rig; - private readonly EveUnityPlayableWorldClientHost? _previousRigHost; - private readonly Transform? _previousRigCamera; - private readonly IEveUnityCameraRenderPolicySource? _previousRigPolicy; - private bool _settled; - - public PresentationSwap( - EveUnityPlayableWorldClientBootstrap owner, - StagedPresentation candidate, - StagedPresentation? previousOwnedPresentation, - EveUnityPlayableWorldClientHost? previousHost, - Transform? previousRoot, - EveUnityPlayableWorldPresentation? previousPresentation, - bool previousRootWasActive, - EveUnityPlayableWorldInputDriver? input, - EveUnityPlayableWorldClientHost? previousInputHost, - Transform? previousInputCamera, - EveUnityPlayableWorldCameraRig? rig, - EveUnityPlayableWorldClientHost? previousRigHost, - Transform? previousRigCamera, - IEveUnityCameraRenderPolicySource? previousRigPolicy) - { - _owner = owner; - _candidate = candidate; - _previousOwnedPresentation = previousOwnedPresentation; - _previousHost = previousHost; - _previousRoot = previousRoot; - _previousPresentation = previousPresentation; - _previousRootWasActive = previousRootWasActive; - _input = input; - _previousInputHost = previousInputHost; - _previousInputCamera = previousInputCamera; - _rig = rig; - _previousRigHost = previousRigHost; - _previousRigCamera = previousRigCamera; - _previousRigPolicy = previousRigPolicy; - } - - public void Complete() - { - if (_settled) return; - _settled = true; - _owner._ownedPresentation = _candidate; - if (_previousOwnedPresentation != null && - !ReferenceEquals(_previousOwnedPresentation, _candidate)) - { - _previousOwnedPresentation.Dispose(); - return; - } - if (_previousHost == null || ReferenceEquals(_previousHost, _candidate.Host)) return; - try - { - _previousHost.Disconnect(); - } - catch (Exception error) - { - Debug.LogWarning($"Eve previous presentation disconnect failed after navigation commit: {error}"); - } - } - - public void Rollback() - { - if (_settled) return; - _settled = true; - _owner.host = _previousHost; - _owner.sceneRoot = _previousRoot; - _owner.LastPresentation = _previousPresentation; - if (_input != null) - { - _input.Host = _previousInputHost; - _input.CameraTransform = _previousInputCamera; - } - if (_rig != null) - { - _rig.Host = _previousRigHost; - _rig.CameraTransform = _previousRigCamera; - _rig.RenderPolicySource = _previousRigPolicy; - } - if (_previousRoot != null) - _previousRoot.gameObject.SetActive(_previousRootWasActive); - _candidate.GameObject.SetActive(false); - } } public EveUnityPlayableWorldPresentation Mount() @@ -557,21 +191,4 @@ namespace GameCult.Eve.UnityScene return null; } } - - public sealed class EveUnitySceneNavigationFailure - { - public EveUnitySceneNavigationFailure( - EveUnitySceneNavigationTarget target, - string diagnostic, - DateTimeOffset failedAtUtc) - { - Target = target; - Diagnostic = diagnostic; - FailedAtUtc = failedAtUtc; - } - - public EveUnitySceneNavigationTarget Target { get; } - public string Diagnostic { get; } - public DateTimeOffset FailedAtUtc { get; } - } } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs index de42f6d..2dfe724 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using GameCult.Eve.Surface; -using GameCult.Eve.UnityScene.Fields; using UnityEngine; #nullable enable @@ -22,7 +21,6 @@ namespace GameCult.Eve.UnityScene [SerializeField] private bool renderShotTrajectories = true; private float _nextRefreshAt; - private object? _cameraOwner; public EveUnityPlayableWorldRuntime? Runtime { get; private set; } @@ -33,16 +31,8 @@ namespace GameCult.Eve.UnityScene public EveUnityPlayableWorldPresentation? LastPresentation => Runtime?.LastPresentation; public EveUnitySceneCommandReceipt? LastReceipt => Runtime?.LastReceipt; - public IEveUnityPresentedEntityRegistry? PresentedEntities => Runtime?.PresentedEntities; - - public EveInputCapabilityDocument? InputCapability => - (providerSurfaceDocuments as IEveUnityInputCapabilitySource)?.CurrentInputCapability; public long ActiveVersion => Runtime?.ActiveVersion ?? 0; - public int ConnectionEpoch { get; private set; } - public Transform? ActiveCameraTransform { get; private set; } - public IEveUnityNativeAssetProvider? NativeAssetProvider => - Runtime?.GameObjectAssetProvider as IEveUnityNativeAssetProvider; public event Action? FeedbackAvailable; public event Action? ShotAvailable; @@ -98,57 +88,17 @@ namespace GameCult.Eve.UnityScene var feedbackEffects = GetComponent(); if (feedbackEffects == null) feedbackEffects = gameObject.AddComponent(); feedbackEffects.Bind(this, Runtime.GameObjectAssetProvider); - var combatPresentation = GetComponent(); - if (combatPresentation == null) combatPresentation = gameObject.AddComponent(); - combatPresentation.Bind(this); - var aimPresentation = GetComponent(); - if (aimPresentation == null) aimPresentation = gameObject.AddComponent(); - aimPresentation.Bind(this); - var beamPresentation = GetComponent(); - if (beamPresentation == null) beamPresentation = gameObject.AddComponent(); - beamPresentation.Bind(this, Runtime.GameObjectAssetProvider); var thermal = GetComponent(); var thermalHud = GetComponent(); if (thermalHud == null) thermalHud = gameObject.AddComponent(); if (thermal == null) thermal = gameObject.AddComponent(); thermal.Bind(this, Runtime.GameObjectAssetProvider as IEveUnityNativeAssetProvider); - var fieldVolume = GetComponent(); - if (fieldVolume == null) fieldVolume = gameObject.AddComponent(); - fieldVolume.Bind(this, providerSurfaceDocuments as IEveUnityFieldsSplatsDocumentSource); - var fieldParticles = GetComponent(); - if (fieldParticles == null) fieldParticles = gameObject.AddComponent(); - fieldParticles.Bind(this, providerSurfaceDocuments as IEveUnityFieldsSplatsDocumentSource); - var actionBar = GetComponent(); - if (actionBar == null) actionBar = gameObject.AddComponent(); - actionBar.Bind(this); - var uiOverlay = GetComponent(); - if (uiOverlay == null) uiOverlay = gameObject.AddComponent(); - uiOverlay.Bind(providerSurfaceDocuments!, commandSink!); var presentation = Runtime.Connect(); - ConnectionEpoch++; _nextRefreshAt = Time.unscaledTime + Math.Max(0.01f, refreshIntervalSeconds); return presentation; } - internal bool TryClaimWorldCamera(object owner, Transform camera) - { - if (owner == null) throw new ArgumentNullException(nameof(owner)); - if (camera == null) throw new ArgumentNullException(nameof(camera)); - if (_cameraOwner != null && !ReferenceEquals(_cameraOwner, owner)) - return false; - _cameraOwner = owner; - ActiveCameraTransform = camera; - return true; - } - - internal void ReleaseWorldCamera(object owner) - { - if (!ReferenceEquals(_cameraOwner, owner)) return; - _cameraOwner = null; - ActiveCameraTransform = null; - } - public EveUnityPlayableWorldPresentation Refresh() { RefreshProviderSources(); @@ -178,16 +128,6 @@ namespace GameCult.Eve.UnityScene return RequireRuntime().SubmitMoveVectorIntent(entityId, directionX, directionY, scalarValue, issuedAt); } - public EveSurfaceCommandRequest SubmitLookDirectionIntent( - string entityId, - float directionX, - float directionY, - float directionZ, - DateTimeOffset? issuedAt = null) - { - return RequireRuntime().SubmitLookDirectionIntent(entityId, directionX, directionY, directionZ, issuedAt); - } - public EveSurfaceCommandRequest SubmitFocusIntent( string entityId, DateTimeOffset? issuedAt = null) @@ -211,63 +151,8 @@ namespace GameCult.Eve.UnityScene return RequireRuntime().SubmitActionIntent(entityId, actionId, issuedAt); } - public EveSurfaceCommandRequest SubmitAdvertisedActionIntent( - string entityId, - string actionId, - DateTimeOffset? issuedAt = null) - { - var action = EveUnityAdvertisedInputAction.Resolve(InputCapability, actionId); - return RequireRuntime().SubmitCommandIntent(action.Operation, action.BuildPayload(entityId), issuedAt); - } - - public EveSurfaceCommandRequest SubmitAdvertisedActionValueIntent( - string entityId, - string actionId, - float inputValue, - DateTimeOffset? issuedAt = null) - { - var action = EveUnityAdvertisedInputAction.Resolve( - InputCapability, - actionId, - requireAvailable: inputValue != 0f); - return RequireRuntime().SubmitCommandIntent( - action.Operation, - action.BuildPayload(entityId, inputValue), - issuedAt); - } - - public EveSurfaceCommandRequest SubmitAdvertisedActionViewDirectionIntent( - string entityId, - string actionId, - float directionX, - float directionY, - float directionZ, - DateTimeOffset? issuedAt = null) - { - var action = EveUnityAdvertisedInputAction.Resolve(InputCapability, actionId); - return RequireRuntime().SubmitCommandIntent( - action.Operation, - action.BuildViewDirectionPayload(entityId, directionX, directionY, directionZ), - issuedAt); - } - - public EveSurfaceCommandRequest SubmitAdvertisedActionScalarIntent( - string entityId, - string actionId, - double value, - DateTimeOffset? issuedAt = null) - { - var action = EveUnityAdvertisedInputAction.Resolve(InputCapability, actionId); - return RequireRuntime().SubmitCommandIntent( - action.Operation, - action.BuildScalarPayload(entityId, value), - issuedAt); - } - public void Disconnect() { - if (_cameraOwner is EveUnityPlayableWorldCameraRig rig) - rig.ReleaseRig(); if (Runtime != null) { Runtime.FeedbackAvailable -= OnFeedbackAvailable; @@ -275,8 +160,6 @@ namespace GameCult.Eve.UnityScene } Runtime?.Dispose(); Runtime = null; - _cameraOwner = null; - ActiveCameraTransform = null; } private void OnFeedbackAvailable(EveUnityFeedbackEvent value) => FeedbackAvailable?.Invoke(value); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs index 1b2e8ca..2ba9bef 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; using GameCult.Eve.Surface; using UnityEngine; @@ -8,11 +6,6 @@ using UnityEngine; namespace GameCult.Eve.UnityScene { - public interface IEveUnityInputCapabilitySource - { - EveInputCapabilityDocument CurrentInputCapability { get; } - } - public sealed class EveUnityPlayableWorldInputDriver : MonoBehaviour { [SerializeField] private EveUnityPlayableWorldClientHost? host; @@ -21,35 +14,17 @@ namespace GameCult.Eve.UnityScene [SerializeField] private string horizontalAxis = "Horizontal"; [SerializeField] private string verticalAxis = "Vertical"; [SerializeField] private string actionButton = "Fire1"; - [SerializeField] private string lookHorizontalAxis = "Mouse X"; - [SerializeField] private float defaultLookSensitivityRadians = 0.001f; [SerializeField] private float deadZone = 0.01f; [SerializeField] private float commandIntervalSeconds = 0.05f; [SerializeField] private bool submitZeroOnRelease = true; private float _nextCommandAt; private bool _wasMoving; - private float _lookYawRadians; - private bool _hasLookYaw; - private float _pendingLookHorizontal; - private string _lookOwnerKey = ""; - private readonly Dictionary _heldBindings = - new Dictionary(StringComparer.Ordinal); - private readonly Dictionary _performedBindings = - new Dictionary(StringComparer.Ordinal); - - public Func? DigitalControlReader { get; set; } public EveUnityPlayableWorldClientHost? Host { get => host; - set - { - if (ReferenceEquals(host, value)) return; - ReleaseHeldActions(); - host = value; - ResetLookState(); - } + set => host = value; } public Transform? CameraTransform @@ -64,7 +39,7 @@ namespace GameCult.Eve.UnityScene DateTimeOffset? issuedAt = null) { var resolvedHost = ResolveHost(); - if (resolvedHost == null || resolvedHost.ActiveWorld == null) + if (resolvedHost == null || resolvedHost.ActiveWorld == null || !resolvedHost.ActiveWorld.MovementEnabled) return null; var movement = EveUnityPlayableWorldMoveVector.FromCameraRelativeInput( @@ -96,265 +71,27 @@ namespace GameCult.Eve.UnityScene issuedAt); } - public EveSurfaceCommandRequest? SubmitPrimaryAction(string actionId = "") + public EveSurfaceCommandRequest? SubmitPrimaryAction(string actionId = "primary") { var resolvedHost = ResolveHost(); if (resolvedHost?.ActiveWorld == null) return null; - var resolvedActionId = string.IsNullOrWhiteSpace(actionId) - ? ResolvePrimaryActionId(resolvedHost.InputCapability) - : actionId; - if (string.IsNullOrWhiteSpace(resolvedActionId)) - return null; - - return resolvedHost.SubmitAdvertisedActionIntent( + return resolvedHost.SubmitActionIntent( resolvedHost.ActiveWorld.PlayerEntityId, - resolvedActionId); - } - - public EveSurfaceCommandRequest? SubmitActionValue( - string actionId, - float inputValue, - DateTimeOffset? issuedAt = null) - { - var resolvedHost = ResolveHost(); - if (resolvedHost?.ActiveWorld == null || string.IsNullOrWhiteSpace(actionId)) - return null; - return resolvedHost.SubmitAdvertisedActionValueIntent( - resolvedHost.ActiveWorld.PlayerEntityId, - actionId, - inputValue, - issuedAt); - } - - public EveSurfaceCommandRequest? SubmitViewDirectionAction( - string actionId, - DateTimeOffset? issuedAt = null) - { - var resolvedHost = ResolveHost(); - if (resolvedHost?.ActiveWorld == null || string.IsNullOrWhiteSpace(actionId)) - return null; - var view = cameraTransform ?? resolvedHost.ActiveCameraTransform; - if (view == null || view.forward.sqrMagnitude <= 0.000001f) - return null; - var direction = view.forward.normalized; - return resolvedHost.SubmitAdvertisedActionViewDirectionIntent( - resolvedHost.ActiveWorld.PlayerEntityId, - actionId, - direction.x, - direction.y, - direction.z, - issuedAt); - } - - public EveSurfaceCommandRequest? SubmitLookInput( - float horizontalDelta, - DateTimeOffset? issuedAt = null) - { - var resolvedHost = ResolveHost(); - var world = resolvedHost?.ActiveWorld; - if (world == null || !string.Equals(world.LookModel, "planar-yaw.v1", StringComparison.Ordinal) || - string.IsNullOrWhiteSpace(world.LookCommand) || - Mathf.Abs(horizontalDelta) <= 0.0001f) - return null; - - var ownerKey = $"{resolvedHost!.ConnectionEpoch}\n{world.WorldRootId}\n{world.PlayerEntityId}"; - if (!_hasLookYaw || !string.Equals(_lookOwnerKey, ownerKey, StringComparison.Ordinal)) - { - _lookYawRadians = ResolveControlledYawRadians(resolvedHost!, world.PlayerEntityId); - _hasLookYaw = true; - _lookOwnerKey = ownerKey; - } - var sensitivity = Mathf.Abs(world.LookSensitivityRadians) > 0.000001f - ? world.LookSensitivityRadians - : defaultLookSensitivityRadians; - _lookYawRadians += horizontalDelta * sensitivity; - var direction = EveUnityPlayableWorldLookDirection.FromPlanarYaw(_lookYawRadians); - return resolvedHost!.SubmitLookDirectionIntent( - world.PlayerEntityId, - direction.DirectionX, - 0f, - direction.DirectionZ, - issuedAt); - } - - public void QueueLookInput(float horizontalDelta) => _pendingLookHorizontal += horizontalDelta; - - public EveSurfaceCommandRequest? SubmitPendingLookInput(DateTimeOffset? issuedAt = null) - { - var pending = _pendingLookHorizontal; - _pendingLookHorizontal = 0f; - return SubmitLookInput(pending, issuedAt); - } - - public static string ResolvePrimaryActionId(EveInputCapabilityDocument? capability) - { - if (capability == null) - return ""; - var profile = (capability.DefaultProfiles ?? Array.Empty()) - .FirstOrDefault(value => string.Equals(value.DeviceClass, "keyboard-mouse", StringComparison.Ordinal)) - ?? (capability.DefaultProfiles ?? Array.Empty()).FirstOrDefault(); - var binding = (profile?.Bindings ?? Array.Empty()) - .FirstOrDefault(value => (value.Gesture?.Controls ?? Array.Empty()) - .Any(control => string.Equals(control, "mouse.primary", StringComparison.Ordinal))) - ?? (profile?.Bindings ?? Array.Empty()).FirstOrDefault(value => value.ActionBar); - if (binding == null) - return ""; - var action = (capability.Actions ?? Array.Empty()) - .FirstOrDefault(value => string.Equals(value.ActionId, binding.ActionId, StringComparison.Ordinal)); - return action != null && string.Equals(action.Availability, "available", StringComparison.OrdinalIgnoreCase) - ? action.ActionId - : ""; + string.IsNullOrWhiteSpace(actionId) ? "primary" : actionId); } private void Update() { - if (!driveInUpdate) - return; - - if (!string.IsNullOrWhiteSpace(lookHorizontalAxis)) - QueueLookInput(Input.GetAxisRaw(lookHorizontalAxis)); - if (Time.unscaledTime < _nextCommandAt) + if (!driveInUpdate || Time.unscaledTime < _nextCommandAt) return; _nextCommandAt = Time.unscaledTime + Math.Max(0.01f, commandIntervalSeconds); SubmitMoveVectorInput(Input.GetAxisRaw(horizontalAxis), Input.GetAxisRaw(verticalAxis)); - SubmitPendingLookInput(); - SubmitChangedAdvertisedActions(); if (!string.IsNullOrWhiteSpace(actionButton) && Input.GetButtonDown(actionButton)) - SubmitPrimaryAction(); - } - - public void SubmitChangedAdvertisedActions() - { - SubmitChangedHeldActions(); - SubmitChangedPerformedActions(); - } - - private void SubmitChangedHeldActions() - { - var resolvedHost = ResolveHost(); - var capability = resolvedHost?.InputCapability; - if (resolvedHost?.ActiveWorld == null || capability == null) - return; - - var profile = (capability.DefaultProfiles ?? Array.Empty()) - .FirstOrDefault(value => string.Equals(value.DeviceClass, "keyboard-mouse", StringComparison.Ordinal)); - var retained = new HashSet(StringComparer.Ordinal); - foreach (var binding in profile?.Bindings ?? Array.Empty()) - { - var action = (capability.Actions ?? Array.Empty()) - .FirstOrDefault(candidate => string.Equals(candidate.ActionId, binding.ActionId, StringComparison.Ordinal)); - if (action?.InputValue == null || - !string.Equals(action.InputValue.Model, EveUnityAdvertisedInputAction.ButtonHoldValueModel, StringComparison.Ordinal) || - !TryReadDigitalGesture(binding.Gesture, out var pressed)) - continue; - - var stateKey = string.IsNullOrWhiteSpace(binding.BindingId) ? binding.ActionId : binding.BindingId; - retained.Add(stateKey); - if (!_heldBindings.TryGetValue(stateKey, out var previous)) - { - _heldBindings[stateKey] = new HeldBindingState(binding.ActionId, pressed); - if (pressed) SubmitActionValue(binding.ActionId, 1f); - continue; - } - if (previous.Pressed == pressed) continue; - _heldBindings[stateKey] = new HeldBindingState(binding.ActionId, pressed); - SubmitActionValue(binding.ActionId, pressed ? 1f : 0f); - } - - foreach (var stale in _heldBindings.Keys.Where(key => !retained.Contains(key)).ToArray()) - { - var previous = _heldBindings[stale]; - if (previous.Pressed) TrySubmitRelease(previous.ActionId); - _heldBindings.Remove(stale); - } - } - - private void SubmitChangedPerformedActions() - { - var resolvedHost = ResolveHost(); - var capability = resolvedHost?.InputCapability; - if (resolvedHost?.ActiveWorld == null || capability == null) - return; - - var profile = (capability.DefaultProfiles ?? Array.Empty()) - .FirstOrDefault(value => string.Equals(value.DeviceClass, "keyboard-mouse", StringComparison.Ordinal)); - var retained = new HashSet(StringComparer.Ordinal); - foreach (var binding in profile?.Bindings ?? Array.Empty()) - { - var action = (capability.Actions ?? Array.Empty()) - .FirstOrDefault(candidate => string.Equals(candidate.ActionId, binding.ActionId, StringComparison.Ordinal)); - if (action == null || - !string.Equals(action.Availability, "available", StringComparison.OrdinalIgnoreCase) || - string.Equals(action.InputValue?.Model, EveUnityAdvertisedInputAction.ButtonHoldValueModel, StringComparison.Ordinal) || - (!string.IsNullOrWhiteSpace(action.InputValue?.Model) && - !string.Equals(action.InputValue.Model, EveUnityAdvertisedInputAction.ViewDirectionValueModel, StringComparison.Ordinal)) || - !TryReadDigitalGesture(binding.Gesture, out var pressed)) - continue; - - var stateKey = string.IsNullOrWhiteSpace(binding.BindingId) ? binding.ActionId : binding.BindingId; - retained.Add(stateKey); - var wasPressed = _performedBindings.TryGetValue(stateKey, out var previous) && previous.Pressed; - _performedBindings[stateKey] = new PerformedBindingState(binding.ActionId, pressed); - if (!pressed || wasPressed) continue; - if (string.Equals(action.InputValue?.Model, EveUnityAdvertisedInputAction.ViewDirectionValueModel, StringComparison.Ordinal)) - SubmitViewDirectionAction(binding.ActionId); - else - resolvedHost.SubmitAdvertisedActionIntent(resolvedHost.ActiveWorld.PlayerEntityId, binding.ActionId); - } - - foreach (var stale in _performedBindings.Keys.Where(key => !retained.Contains(key)).ToArray()) - _performedBindings.Remove(stale); - } - - private bool TryReadDigitalGesture(EveInputGestureDocument? gesture, out bool pressed) - { - pressed = false; - if (gesture == null || - !(string.Equals(gesture.Kind, "direct", StringComparison.Ordinal) || - string.Equals(gesture.Kind, "chord", StringComparison.Ordinal))) - return false; - var controls = gesture.Controls ?? Array.Empty(); - if (controls.Length == 0) return false; - foreach (var control in controls) - { - var value = DigitalControlReader?.Invoke(control) ?? ReadStandardDigitalControl(control); - if (!value.HasValue) return false; - if (!value.Value) return true; - } - pressed = true; - return true; - } - - private static bool? ReadStandardDigitalControl(string control) - { - if (string.Equals(control, "mouse.primary", StringComparison.Ordinal)) return Input.GetMouseButton(0); - if (string.Equals(control, "mouse.secondary", StringComparison.Ordinal)) return Input.GetMouseButton(1); - if (string.Equals(control, "mouse.middle", StringComparison.Ordinal)) return Input.GetMouseButton(2); - const string keyboardPrefix = "keyboard."; - if (control != null && control.StartsWith(keyboardPrefix, StringComparison.Ordinal) && - Enum.TryParse(control.Substring(keyboardPrefix.Length), true, out KeyCode key)) - return Input.GetKey(key); - return null; - } - - private void OnDisable() => ReleaseHeldActions(); - - private void ReleaseHeldActions() - { - foreach (var state in _heldBindings.Values) - if (state.Pressed) TrySubmitRelease(state.ActionId); - _heldBindings.Clear(); - _performedBindings.Clear(); - } - - private void TrySubmitRelease(string actionId) - { - try { SubmitActionValue(actionId, 0f); } - catch (InvalidOperationException) { } + SubmitPrimaryAction(actionButton); } private EveUnityPlayableWorldClientHost? ResolveHost() @@ -365,61 +102,6 @@ namespace GameCult.Eve.UnityScene host = GetComponent(); return host; } - - private static float ResolveControlledYawRadians(EveUnityPlayableWorldClientHost host, string entityId) - { - foreach (var marker in host.SceneRoot.GetComponentsInChildren(true)) - if (string.Equals(marker.EntityId, entityId, StringComparison.Ordinal)) - return marker.transform.eulerAngles.y * Mathf.Deg2Rad; - return 0f; - } - - private void ResetLookState() - { - _lookYawRadians = 0f; - _hasLookYaw = false; - _pendingLookHorizontal = 0f; - _lookOwnerKey = ""; - } - - private readonly struct HeldBindingState - { - public HeldBindingState(string actionId, bool pressed) - { - ActionId = actionId ?? ""; - Pressed = pressed; - } - - public string ActionId { get; } - public bool Pressed { get; } - } - - private readonly struct PerformedBindingState - { - public PerformedBindingState(string actionId, bool pressed) - { - ActionId = actionId ?? ""; - Pressed = pressed; - } - - public string ActionId { get; } - public bool Pressed { get; } - } - } - - public readonly struct EveUnityPlayableWorldLookDirection - { - public EveUnityPlayableWorldLookDirection(float directionX, float directionZ) - { - DirectionX = directionX; - DirectionZ = directionZ; - } - - public float DirectionX { get; } - public float DirectionZ { get; } - - public static EveUnityPlayableWorldLookDirection FromPlanarYaw(float yawRadians) => - new EveUnityPlayableWorldLookDirection(Mathf.Sin(yawRadians), Mathf.Cos(yawRadians)); } public readonly struct EveUnityPlayableWorldMoveVector diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs index f8d7182..222d9ad 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Threading.Tasks; using GameCult.Eve.Surface; #nullable enable @@ -12,22 +11,24 @@ namespace GameCult.Eve.UnityScene private readonly EveUnitySceneProviderConnection _connection; private readonly EveUnityPlayableWorldPresenter _presenter; private readonly IEveUnitySceneCommandReceiptSource? _receiptSource; + private readonly Func? _visibleStateVersion; private readonly EveUnityFeedbackPresenter _feedback = new EveUnityFeedbackPresenter(); private readonly EveUnityShotReceiptPresenter _shots = new EveUnityShotReceiptPresenter(); private readonly List _pendingReceipts = new List(); - private long _activeStateVersion; private bool _connected; private bool _receiptConnected; public EveUnityPlayableWorldLiveClient( EveUnitySceneProviderConnection connection, EveUnityPlayableWorldPresenter presenter, - IEveUnitySceneCommandReceiptSource? receiptSource = null) + IEveUnitySceneCommandReceiptSource? receiptSource = null, + Func? visibleStateVersion = null) { _connection = connection ?? throw new ArgumentNullException(nameof(connection)); _presenter = presenter ?? throw new ArgumentNullException(nameof(presenter)); _receiptSource = receiptSource; + _visibleStateVersion = visibleStateVersion; } public EveUnitySceneProjection? ActiveProjection => _connection.ActiveProjection; @@ -38,14 +39,7 @@ namespace GameCult.Eve.UnityScene public EveUnitySceneCommandReceipt? LastReceipt { get; private set; } - public long ActiveVersion => Math.Max(_connection.ActiveVersion, _activeStateVersion); - - public void AdvanceStateVersion(long version) - { - if (version <= _activeStateVersion) return; - _activeStateVersion = version; - PublishReceiptsWhoseStateIsVisible(); - } + public long ActiveVersion => Math.Max(_connection.ActiveVersion, _visibleStateVersion?.Invoke() ?? 0); public string SourcePointer => _connection.SourcePointer; @@ -97,16 +91,6 @@ namespace GameCult.Eve.UnityScene return _connection.SubmitMoveVectorIntent(entityId, directionX, directionY, scalarValue, issuedAt); } - public EveSurfaceCommandRequest SubmitLookDirectionIntent( - string entityId, - float directionX, - float directionY, - float directionZ, - DateTimeOffset? issuedAt = null) - { - return _connection.SubmitLookDirectionIntent(entityId, directionX, directionY, directionZ, issuedAt); - } - public EveSurfaceCommandRequest SubmitFocusIntent( string entityId, DateTimeOffset? issuedAt = null) @@ -124,18 +108,10 @@ namespace GameCult.Eve.UnityScene public EveSurfaceCommandRequest SubmitActionIntent( string entityId, - string actionId, + EveInputActionDocument action, DateTimeOffset? issuedAt = null) { - return _connection.SubmitActionIntent(entityId, actionId, issuedAt); - } - - public EveSurfaceCommandRequest SubmitCommandIntent( - string commandId, - IReadOnlyDictionary? payload = null, - DateTimeOffset? issuedAt = null) - { - return _connection.SubmitCommandIntent(commandId, payload, issuedAt); + return _connection.SubmitActionIntent(entityId, action, issuedAt); } public void Disconnect() @@ -235,43 +211,6 @@ namespace GameCult.Eve.UnityScene event Action ReceiptAvailable; } - public interface IEveUnityNavigableProvider - { - Task NavigateAsync(EveUnitySceneNavigationTarget target); - - void CommitNavigation(); - - void FinalizeNavigation(); - - void RollbackNavigation(); - } - - public sealed class EveUnitySceneNavigationTarget - { - public EveUnitySceneNavigationTarget( - string verseId, - string providerId, - string surfaceId, - string surfaceKind, - IReadOnlyList? rendezvousEndpoints = null, - string authorityRuntimeId = "") - { - VerseId = verseId ?? ""; - AuthorityRuntimeId = authorityRuntimeId ?? ""; - ProviderId = providerId ?? ""; - SurfaceId = surfaceId ?? ""; - SurfaceKind = surfaceKind ?? ""; - RendezvousEndpoints = rendezvousEndpoints ?? Array.Empty(); - } - - public string VerseId { get; } - public string AuthorityRuntimeId { get; } - public string ProviderId { get; } - public string SurfaceId { get; } - public string SurfaceKind { get; } - public IReadOnlyList RendezvousEndpoints { get; } - } - public sealed class EveUnitySceneCommandReceipt { public EveUnitySceneCommandReceipt( @@ -286,8 +225,7 @@ namespace GameCult.Eve.UnityScene string surfaceId = "", string message = "", DateTimeOffset? issuedAtUtc = null, - long sourceVersion = 0, - EveUnitySceneNavigationTarget? navigation = null) + long sourceVersion = 0) { ReceiptId = receiptId ?? ""; Command = command ?? ""; @@ -301,7 +239,6 @@ namespace GameCult.Eve.UnityScene Message = message ?? ""; IssuedAtUtc = issuedAtUtc; SourceVersion = sourceVersion; - Navigation = navigation; } public string Schema { get; } @@ -328,8 +265,6 @@ namespace GameCult.Eve.UnityScene public long SourceVersion { get; } - public EveUnitySceneNavigationTarget? Navigation { get; } - public bool IsProviderOwned => !string.IsNullOrWhiteSpace(OwnerRepo) && !string.Equals(OwnerRepo, "EveUnity", StringComparison.Ordinal); public bool ShouldRefreshProviderSurface => diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs index 94ca473..393a7ba 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs @@ -1,5 +1,5 @@ using System; -using System.Collections.Generic; +using System.Linq; using GameCult.Eve.Surface; using UnityEngine; @@ -14,9 +14,8 @@ namespace GameCult.Eve.UnityScene private readonly EveUnityPlayableWorldLiveClient _client; private readonly EveUnityPlayableWorldAssetManifestDocumentSource? _assetManifestSource; private readonly IEveUnityEntitySoaViewDocumentSource? _entityViews; + private readonly IEveUnityInputCapabilityDocumentSource? _inputCapabilities; private readonly EveUnityEntitySoaPresenter _entityPresenter; - private readonly IEveUnityPresentedEntityRegistry? _presentedEntities; - private IDisposable? _ownedSceneSink; private bool _entityViewsConnected; private bool _assetManifestConnected; @@ -36,13 +35,14 @@ namespace GameCult.Eve.UnityScene AssetManifests = assetManifests ?? new EveUnityPlayableWorldAssetManifestCache(); _surfaceSource = new EveUnitySceneProviderSurfaceDocumentSource(surfaceDocuments); _entityViews = surfaceDocuments as IEveUnityEntitySoaViewDocumentSource; - _presentedEntities = sceneSink as IEveUnityPresentedEntityRegistry; + _inputCapabilities = surfaceDocuments as IEveUnityInputCapabilityDocumentSource; _entityPresenter = new EveUnityEntitySoaPresenter(sceneSink); _connection = new EveUnitySceneProviderConnection(_surfaceSource, commandSink); _client = new EveUnityPlayableWorldLiveClient( _connection, new EveUnityPlayableWorldPresenter(sceneSink, assetResolver ?? new EveUnityAssetRefResolver()), - receiptSource); + receiptSource, + () => _entityViews?.CurrentEntityView?.FrameId ?? 0); if (assetManifestDocuments != null) _assetManifestSource = new EveUnityPlayableWorldAssetManifestDocumentSource(assetManifestDocuments); @@ -51,13 +51,12 @@ namespace GameCult.Eve.UnityScene public EveUnityPlayableWorldAssetManifestCache AssetManifests { get; } public IEveUnityGameObjectAssetProvider? GameObjectAssetProvider { get; private set; } - public IEveUnityPresentedEntityRegistry? PresentedEntities => _presentedEntities; public EveUnityPlayableWorldProjection? ActiveWorld => _client.ActiveWorld; public EveUnitySceneProjection? ActiveProjection => _client.ActiveProjection; - public EveUnityPlayableWorldPresentation? LastPresentation => _client.LastPresentation; + public EveUnityPlayableWorldPresentation? LastPresentation => ComposePresentation(_client.LastPresentation); public EveUnitySceneCommandReceipt? LastReceipt => _client.LastReceipt; @@ -108,22 +107,23 @@ namespace GameCult.Eve.UnityScene new EveUnityAssetRefResolver(), assetManifests); runtime.GameObjectAssetProvider = liveAssetProvider; - runtime._ownedSceneSink = sceneSink; return runtime; } public EveUnityPlayableWorldPresentation Connect() { EnsureAssetManifestConnected(); - var presentation = _client.Connect(); + _client.Connect(); EnsureEntityViewsConnected(); - return presentation; + return ComposePresentation(_client.LastPresentation)!; } public EveUnityPlayableWorldPresentation Refresh() { EnsureAssetManifestConnected(); - return _client.Refresh(); + EnsureEntityViewsConnected(); + _client.Refresh(); + return ComposePresentation(_client.LastPresentation)!; } public EveSurfaceCommandRequest SubmitMoveIntent( @@ -146,16 +146,6 @@ namespace GameCult.Eve.UnityScene return _client.SubmitMoveVectorIntent(entityId, directionX, directionY, scalarValue, issuedAt); } - public EveSurfaceCommandRequest SubmitLookDirectionIntent( - string entityId, - float directionX, - float directionY, - float directionZ, - DateTimeOffset? issuedAt = null) - { - return _client.SubmitLookDirectionIntent(entityId, directionX, directionY, directionZ, issuedAt); - } - public EveSurfaceCommandRequest SubmitFocusIntent( string entityId, DateTimeOffset? issuedAt = null) @@ -176,15 +166,14 @@ namespace GameCult.Eve.UnityScene string actionId, DateTimeOffset? issuedAt = null) { - return _client.SubmitActionIntent(entityId, actionId, issuedAt); - } - - public EveSurfaceCommandRequest SubmitCommandIntent( - string commandId, - IReadOnlyDictionary? payload = null, - DateTimeOffset? issuedAt = null) - { - return _client.SubmitCommandIntent(commandId, payload, issuedAt); + var capability = _inputCapabilities?.CurrentInputCapability + ?? throw new InvalidOperationException("The active Eve surface does not publish an input capability document."); + var action = (capability.Actions ?? Array.Empty()) + .FirstOrDefault(candidate => string.Equals(candidate.ActionId, actionId, StringComparison.Ordinal)) + ?? throw new InvalidOperationException($"The active Eve input capability does not advertise action '{actionId}'."); + if (!string.Equals(action.Availability, "available", StringComparison.OrdinalIgnoreCase)) + throw new InvalidOperationException($"Eve action '{actionId}' is not currently available."); + return _client.SubmitActionIntent(entityId, action, issuedAt); } public void Disconnect() @@ -209,8 +198,6 @@ namespace GameCult.Eve.UnityScene Disconnect(); _client.Dispose(); _assetManifestSource?.Dispose(); - _ownedSceneSink?.Dispose(); - _ownedSceneSink = null; } private void EnsureAssetManifestConnected() @@ -228,18 +215,30 @@ namespace GameCult.Eve.UnityScene if (_entityViewsConnected || _entityViews == null) return; _entityViews.EntityViewAvailable += OnEntityViewAvailable; _entityViewsConnected = true; + if (_entityViews.CurrentEntityView != null) + _entityPresenter.Apply(_entityViews.CurrentEntityView); } - private void OnEntityViewAvailable(EveEntitySoaViewDocument document, GameCult.Mesh.ICultMeshBodyReadLease lease) + private void OnEntityViewAvailable(EveEntitySoaViewDocument document) => _entityPresenter.Apply(document); + + private EveUnityPlayableWorldPresentation? ComposePresentation( + EveUnityPlayableWorldPresentation? surfacePresentation) { - _client.AdvanceStateVersion(document.FrameId); - _entityPresenter.Apply(document, lease); + if (surfacePresentation == null || _entityViews?.CurrentEntityView == null) + return surfacePresentation; + + return new EveUnityPlayableWorldPresentation( + surfacePresentation.WorldRootId, + surfacePresentation.PlayerEntityId, + surfacePresentation.InputProfile, + surfacePresentation.CameraRig, + surfacePresentation.UpsertedEntities, + surfacePresentation.RemovedEntities, + _entityPresenter.ActiveEntities); } } - public sealed class EveUnityLivePlayableWorldAssetProvider : - IEveUnityNativeAssetProvider, - IEveUnityNativeAssetMetadataProvider + public sealed class EveUnityLivePlayableWorldAssetProvider : IEveUnityNativeAssetProvider { private readonly EveUnityPlayableWorldAssetManifestCache _assetManifests; private readonly Func _activeWorld; @@ -286,16 +285,5 @@ namespace GameCult.Eve.UnityScene } return (_fallback as IEveUnityNativeAssetProvider)?.ResolveAsset(asset, assetType); } - - public bool TryResolveAssetMetadata( - EveUnityPlayableWorldAssetBinding asset, - out IReadOnlyDictionary metadata) - { - if (asset == null) throw new ArgumentNullException(nameof(asset)); - if (_fallback is IEveUnityNativeAssetMetadataProvider provider) - return provider.TryResolveAssetMetadata(asset, out metadata); - metadata = new Dictionary(StringComparer.Ordinal); - return false; - } } } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs deleted file mode 100644 index cb03b14..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using UnityEngine; - -#nullable enable - -namespace GameCult.Eve.UnityScene -{ - public sealed class EveUnityPresentedEntity - { - public EveUnityPresentedEntity( - int sourceIndex, - string entityId, - string entityKind, - string label, - string faction, - Vector3 position, - float rotationYDegrees, - Vector3 velocity, - float radius, - float scale, - uint renderGroupId, - int lod, - bool selectable, - bool controllable, - string assetRef, - IReadOnlyDictionary? scalarState = null) - { - SourceIndex = sourceIndex; - EntityId = entityId ?? ""; - EntityKind = entityKind ?? ""; - Label = label ?? ""; - Faction = faction ?? ""; - Position = position; - RotationYDegrees = rotationYDegrees; - Velocity = velocity; - Radius = radius; - Scale = scale; - RenderGroupId = renderGroupId; - Lod = lod; - Selectable = selectable; - Controllable = controllable; - AssetRef = assetRef ?? ""; - ScalarState = scalarState ?? new Dictionary(StringComparer.Ordinal); - } - - public int SourceIndex { get; } - public string EntityId { get; } - public string EntityKind { get; } - public string Label { get; } - public string Faction { get; } - public Vector3 Position { get; } - public float RotationYDegrees { get; } - public Vector3 Velocity { get; } - public float Radius { get; } - public float Scale { get; } - public uint RenderGroupId { get; } - public int Lod { get; } - public bool Selectable { get; } - public bool Controllable { get; } - public string AssetRef { get; } - public IReadOnlyDictionary ScalarState { get; } - - public bool TryGetScalar(string semantic, out float value) => - ScalarState.TryGetValue(semantic ?? "", out value); - - public EveUnityPlayableWorldEntity ToPlayableWorldEntity() => new EveUnityPlayableWorldEntity( - EntityId, - EntityId, - EntityKind, - Label, - Faction, - AssetRef, - Position.x, - Position.y, - Position.z, - RotationYDegrees, - Scale > 0f ? Scale : Radius, - Selectable, - Controllable, - "", "", "", ""); - } - - public sealed class EveUnityPresentedEntityGeneration - { - public EveUnityPresentedEntityGeneration( - string viewId, - long producerEpoch, - long sequence, - IEnumerable entities) - { - ViewId = viewId ?? ""; - ProducerEpoch = producerEpoch; - Sequence = sequence; - Entities = (entities ?? throw new ArgumentNullException(nameof(entities))).ToArray(); - } - - public string ViewId { get; } - public long ProducerEpoch { get; } - public long Sequence { get; } - public IReadOnlyList Entities { get; } - } - - public sealed class EveUnityPresentedEntityHandle - { - public EveUnityPresentedEntityHandle(EveUnityPresentedEntity entity, Transform transform) - { - Entity = entity ?? throw new ArgumentNullException(nameof(entity)); - Transform = transform != null ? transform : throw new ArgumentNullException(nameof(transform)); - } - - public EveUnityPresentedEntity Entity { get; } - public Transform Transform { get; } - } - - public interface IEveUnityPresentedEntityRegistry - { - EveUnityPresentedEntityGeneration? CurrentGeneration { get; } - bool TryGetByEntityId(string entityId, out EveUnityPresentedEntityHandle handle); - bool TryGetBySourceIndex(int sourceIndex, out EveUnityPresentedEntityHandle handle); - } - - public interface IEveUnityEntityGenerationSink - { - void ApplyGeneration(EveUnityPresentedEntityGeneration generation); - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs.meta deleted file mode 100644 index c16abad..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPresentedEntities.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 3f7e67e0c24449bab80f2dd15cf2e118 diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityRendererFeature.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityRendererFeature.cs deleted file mode 100644 index 562eaf8..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityRendererFeature.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.Rendering.Universal; - -#nullable enable - -namespace GameCult.Eve.UnityScene -{ - internal interface IEveUnityRenderPassSource - { - int RenderOrder { get; } - bool TryEnqueuePass(Camera camera, ScriptableRenderer renderer); - } - - internal static class EveUnityRenderPassRegistry - { - private static readonly List Sources = - new List(); - - [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] - private static void Reset() => Sources.Clear(); - - internal static void Register(IEveUnityRenderPassSource source) - { - if (source == null) throw new ArgumentNullException(nameof(source)); - if (Sources.Contains(source)) return; - Sources.Add(source); - Sources.Sort(static (left, right) => left.RenderOrder.CompareTo(right.RenderOrder)); - } - - internal static void Unregister(IEveUnityRenderPassSource source) => Sources.Remove(source); - - internal static void Enqueue(Camera camera, ScriptableRenderer renderer) - { - for (var index = 0; index < Sources.Count; index++) - Sources[index].TryEnqueuePass(camera, renderer); - } - } - - /// - /// URP-owned scheduling point for generic Eve world effects. - /// Add this feature to the Universal Renderer used by Eve cameras. - /// Provider surfaces supply effect programs, assets, and parameters; this feature only lowers them. - /// - public sealed class EveUnityRendererFeature : ScriptableRendererFeature - { - public override void Create() - { - } - - public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) - { - var camera = renderingData.cameraData.camera; - if (camera == null || !SupportsCamera(renderingData.cameraData.cameraType)) return; - EveUnityRenderPassRegistry.Enqueue(camera, renderer); - } - - internal static bool SupportsCamera(CameraType cameraType) => - cameraType == CameraType.Game || cameraType == CameraType.SceneView; - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneClientSession.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneClientSession.cs index 473670d..16efb3d 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneClientSession.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneClientSession.cs @@ -83,26 +83,6 @@ namespace GameCult.Eve.UnityScene issuedAt); } - public EveSurfaceCommandRequest CreateLookDirectionIntent( - string entityId, - float directionX, - float directionY, - float directionZ, - DateTimeOffset? issuedAt = null) - { - var playableWorld = RequirePlayableWorld(); - return CreatePlayableWorldIntent( - playableWorld.LookCommand, - new Dictionary(StringComparer.Ordinal) - { - ["entityId"] = entityId ?? "", - ["directionX"] = FormatFloat(directionX), - ["directionY"] = FormatFloat(directionY), - ["directionZ"] = FormatFloat(directionZ) - }, - issuedAt); - } - public EveSurfaceCommandRequest CreateFocusIntent( string entityId, DateTimeOffset? issuedAt = null) @@ -135,17 +115,18 @@ namespace GameCult.Eve.UnityScene public EveSurfaceCommandRequest CreateActionIntent( string entityId, - string actionId, + EveInputActionDocument action, DateTimeOffset? issuedAt = null) { - var playableWorld = RequirePlayableWorld(); + if (action == null) throw new ArgumentNullException(nameof(action)); + var payload = new Dictionary(action.Payload ?? new Dictionary(), StringComparer.Ordinal) + { + ["entityId"] = entityId ?? "", + ["actionId"] = action.ActionId ?? "" + }; return CreatePlayableWorldIntent( - playableWorld.ActionCommand, - new Dictionary(StringComparer.Ordinal) - { - ["entityId"] = entityId ?? "", - ["actionId"] = actionId ?? "" - }, + action.Operation, + payload, issuedAt); } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneLiveProviderBridge.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneLiveProviderBridge.cs index 7ccaea0..59814ca 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneLiveProviderBridge.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneLiveProviderBridge.cs @@ -239,18 +239,8 @@ namespace GameCult.Eve.UnityScene _transport.SurfaceDocumentAvailable += OnSurfaceDocumentAvailable; _transport.AssetManifestDocumentAvailable += OnAssetManifestDocumentAvailable; _transport.CommandReceiptAvailable += OnCommandReceiptAvailable; - try - { - _transport.Connect(); - _connected = true; - } - catch - { - _transport.SurfaceDocumentAvailable -= OnSurfaceDocumentAvailable; - _transport.AssetManifestDocumentAvailable -= OnAssetManifestDocumentAvailable; - _transport.CommandReceiptAvailable -= OnCommandReceiptAvailable; - throw; - } + _transport.Connect(); + _connected = true; } public void Disconnect() diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneProviderConnection.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneProviderConnection.cs index f38c3d5..de6037c 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneProviderConnection.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneProviderConnection.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using GameCult.Eve.Surface; #nullable enable @@ -222,16 +221,6 @@ namespace GameCult.Eve.UnityScene return Submit(_session.CreateMoveVectorIntent(entityId, directionX, directionY, scalarValue, issuedAt)); } - public EveSurfaceCommandRequest SubmitLookDirectionIntent( - string entityId, - float directionX, - float directionY, - float directionZ, - DateTimeOffset? issuedAt = null) - { - return Submit(_session.CreateLookDirectionIntent(entityId, directionX, directionY, directionZ, issuedAt)); - } - public EveSurfaceCommandRequest SubmitFocusIntent( string entityId, DateTimeOffset? issuedAt = null) @@ -249,18 +238,10 @@ namespace GameCult.Eve.UnityScene public EveSurfaceCommandRequest SubmitActionIntent( string entityId, - string actionId, + EveInputActionDocument action, DateTimeOffset? issuedAt = null) { - return Submit(_session.CreateActionIntent(entityId, actionId, issuedAt)); - } - - public EveSurfaceCommandRequest SubmitCommandIntent( - string commandId, - IReadOnlyDictionary? payload = null, - DateTimeOffset? issuedAt = null) - { - return Submit(_session.CreatePlayableWorldIntent(commandId, payload, issuedAt)); + return Submit(_session.CreateActionIntent(entityId, action, issuedAt)); } public void Disconnect() diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index b14f549..b0217c3 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using GameCult.Eve.PluginFields; using GameCult.Eve.Surface; using GameCult.Mesh; @@ -104,16 +102,10 @@ namespace GameCult.Eve.UnityScene return null; var entities = new List(); - var fieldVolumes = new List(); - var fieldParticles = new List(); foreach (var component in Flatten(worldRoot)) { if (string.Equals(component.Kind, "world.entity3d", StringComparison.Ordinal)) entities.Add(BuildPlayableEntity(component)); - else if (string.Equals(component.Kind, "field.volume3d", StringComparison.Ordinal)) - fieldVolumes.Add(BuildFieldVolume(component)); - else if (string.Equals(component.Kind, "field.particles3d", StringComparison.Ordinal)) - fieldParticles.Add(BuildFieldParticles(component)); } return new EveUnityPlayableWorldProjection( @@ -131,75 +123,15 @@ namespace GameCult.Eve.UnityScene entities, worldRoot.GetProp("entityViewPointerId"), worldRoot.GetProp("entityViewSchema"), - worldRoot.GetProp("entityBodyId"), worldRoot.GetProp("zoneRenderPointerId"), worldRoot.GetProp("zoneRenderSchema"), - ParseStringList(worldRoot.GetProp("excludedRenderChannels")), + worldRoot.GetProp("inputCapability"), worldRoot.GetProp("cameraTargetEntityId"), - ParseFloat(worldRoot.GetProp("cameraDistance"), 0f), - ParseFloat(worldRoot.GetProp("cameraVerticalFieldOfViewDegrees"), 0f), - ParseFloat(worldRoot.GetProp("cameraTargetScreenX"), 0.5f), - ParseFloat(worldRoot.GetProp("cameraTargetScreenY"), 0.5f), - ParseFloat(worldRoot.GetProp("cameraPositionDamping"), 0f), - ParseVector3(worldRoot.GetProp("ambientLightColor")), - ParseFloat(worldRoot.GetProp("ambientLightIntensity"), 1f), - ParseFloat(worldRoot.GetProp("cameraNearClipPlane"), 0f), - ParseFloat(worldRoot.GetProp("cameraFarClipPlane"), 0f), - worldRoot.GetProp("lookCommand"), - ParseFloat(worldRoot.GetProp("lookSensitivityRadians"), 0f), - worldRoot.GetProp("lookModel"), - worldRoot.GetProp("skyboxAssetRef"), - worldRoot.GetProp("reflectionAssetRef"), - ParseFloat(worldRoot.GetProp("reflectionIntensity"), 1f), - worldRoot.GetProp("postProcessProfileAssetRef"), - ParseVector3(worldRoot.GetProp("keyLightDirection")), - ParseVector3(worldRoot.GetProp("keyLightColor")), - ParseFloat(worldRoot.GetProp("keyLightIntensity"), 0f), - fieldVolumes, - worldRoot.GetProp("cameraLookAt"), - fieldParticles, - worldRoot.GetProp("cameraReconstruction"), - worldRoot.GetProp("temporalQuality", "high"), - ParseFloat(worldRoot.GetProp("temporalHistoryBlend"), 0f), - ParseFloat(worldRoot.GetProp("temporalJitterScale"), 0f), - ParseFloat(worldRoot.GetProp("temporalSharpening"), 0f), - worldRoot.GetProp("exposureMode"), - ParseFloat(worldRoot.GetProp("exposureLowPercent"), 50f), - ParseFloat(worldRoot.GetProp("exposureHighPercent"), 95f), - ParseFloat(worldRoot.GetProp("exposureMinimumEv"), 0f), - ParseFloat(worldRoot.GetProp("exposureMaximumEv"), 0f), - ParseFloat(worldRoot.GetProp("exposureKeyValue"), 1f), - worldRoot.GetProp("exposureAdaptation", "progressive"), - ParseFloat(worldRoot.GetProp("exposureSpeedUp"), 2f), - ParseFloat(worldRoot.GetProp("exposureSpeedDown"), 1f), - worldRoot.GetProp("colorGradingSpace"), - worldRoot.GetProp("assetProviderId"), - worldRoot.GetProp("assetVerseId"), - worldRoot.GetProp("assetAuthorityRuntimeId"), - ParseStringList(worldRoot.GetProp("assetRendezvousEndpoints"))); + ParseBoolean(worldRoot.GetProp("subjectVisible", "true")), + ParseBoolean(worldRoot.GetProp("movementEnabled", "true")), + worldRoot.GetProp("presentationMode", "world")); } - private static EveUnityFieldVolumeProjection BuildFieldVolume(EveSurfaceComponent component) => - new EveUnityFieldVolumeProjection( - component.Id, - component.GetProp("documentRef"), - component.GetProp("documentSchema", EveFieldsSchemas.Splats), - component.GetProp("materialAssetRef"), - component.GetProp("renderChannel", "world.transparent"), - component.GetProp("compositeMode", "premultiplied-alpha"), - component.GetProp("quality", "normal"), - component.Props); - - private static EveUnityFieldParticlesProjection BuildFieldParticles(EveSurfaceComponent component) => - new EveUnityFieldParticlesProjection( - component.Id, - component.GetProp("documentRef"), - component.GetProp("documentSchema", EveFieldsSchemas.Splats), - component.GetProp("computeProgramAssetRef"), - component.GetProp("materialAssetRef"), - component.GetProp("renderChannel", "world.transparent"), - component.Props); - private static EveUnityPlayableWorldEntity BuildPlayableEntity(EveSurfaceComponent component) { var position = ParseVector3(component.GetProp("position")); @@ -271,16 +203,6 @@ namespace GameCult.Eve.UnityScene string.Equals(value, "1", StringComparison.Ordinal); } - private static IReadOnlyList ParseStringList(string value) - { - return (value ?? "") - .Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) - .Select(part => part.Trim()) - .Where(part => !string.IsNullOrWhiteSpace(part)) - .Distinct(StringComparer.Ordinal) - .ToArray(); - } - private static EveUnityScenePluginProjection? BuildPluginProjection(EveSurfaceComponent component) { if (SaiVisualNovelAdapter.CanProject(component)) @@ -406,52 +328,13 @@ namespace GameCult.Eve.UnityScene IReadOnlyList entities, string entityViewPointerId = "", string entityViewSchema = "", - string entityBodyId = "", string zoneRenderPointerId = "", string zoneRenderSchema = "", - IReadOnlyList? excludedRenderChannels = null, + string inputCapabilityPointerId = "", string cameraTargetEntityId = "", - float cameraDistance = 0f, - float cameraVerticalFieldOfViewDegrees = 0f, - float cameraTargetScreenX = 0.5f, - float cameraTargetScreenY = 0.5f, - float cameraPositionDamping = 0f, - (float r, float g, float b) ambientLightColor = default, - float ambientLightIntensity = 1f, - float cameraNearClipPlane = 0f, - float cameraFarClipPlane = 0f, - string lookCommand = "", - float lookSensitivityRadians = 0f, - string lookModel = "", - string skyboxAssetRef = "", - string reflectionAssetRef = "", - float reflectionIntensity = 1f, - string postProcessProfileAssetRef = "", - (float x, float y, float z) keyLightDirection = default, - (float r, float g, float b) keyLightColor = default, - float keyLightIntensity = 0f, - IReadOnlyList? fieldVolumes = null, - string cameraLookAt = "", - IReadOnlyList? fieldParticles = null, - string cameraReconstruction = "", - string temporalQuality = "high", - float temporalHistoryBlend = 0f, - float temporalJitterScale = 0f, - float temporalSharpening = 0f, - string exposureMode = "", - float exposureLowPercent = 50f, - float exposureHighPercent = 95f, - float exposureMinimumEv = 0f, - float exposureMaximumEv = 0f, - float exposureKeyValue = 1f, - string exposureAdaptation = "progressive", - float exposureSpeedUp = 2f, - float exposureSpeedDown = 1f, - string colorGradingSpace = "", - string assetProviderId = "", - string assetVerseId = "", - string assetAuthorityRuntimeId = "", - IReadOnlyList? assetRendezvousEndpoints = null) + bool subjectVisible = true, + bool movementEnabled = true, + string presentationMode = "world") { WorldRootId = worldRootId ?? ""; StatePointerId = statePointerId ?? ""; @@ -467,58 +350,15 @@ namespace GameCult.Eve.UnityScene Entities = entities ?? Array.Empty(); EntityViewPointerId = entityViewPointerId ?? ""; EntityViewSchema = entityViewSchema ?? ""; - EntityBodyId = entityBodyId ?? ""; ZoneRenderPointerId = zoneRenderPointerId ?? ""; ZoneRenderSchema = zoneRenderSchema ?? ""; - ExcludedRenderChannels = excludedRenderChannels ?? Array.Empty(); - CameraTargetEntityId = cameraTargetEntityId ?? ""; - CameraDistance = cameraDistance; - CameraVerticalFieldOfViewDegrees = cameraVerticalFieldOfViewDegrees; - CameraTargetScreenX = cameraTargetScreenX; - CameraTargetScreenY = cameraTargetScreenY; - CameraPositionDamping = cameraPositionDamping; - AmbientLightR = ambientLightColor.r; - AmbientLightG = ambientLightColor.g; - AmbientLightB = ambientLightColor.b; - AmbientLightIntensity = ambientLightIntensity; - CameraNearClipPlane = cameraNearClipPlane; - CameraFarClipPlane = cameraFarClipPlane; - LookCommand = lookCommand ?? ""; - LookSensitivityRadians = lookSensitivityRadians; - LookModel = lookModel ?? ""; - SkyboxAssetRef = skyboxAssetRef ?? ""; - ReflectionAssetRef = reflectionAssetRef ?? ""; - ReflectionIntensity = reflectionIntensity; - PostProcessProfileAssetRef = postProcessProfileAssetRef ?? ""; - KeyLightDirectionX = keyLightDirection.x; - KeyLightDirectionY = keyLightDirection.y; - KeyLightDirectionZ = keyLightDirection.z; - KeyLightColorR = keyLightColor.r; - KeyLightColorG = keyLightColor.g; - KeyLightColorB = keyLightColor.b; - KeyLightIntensity = keyLightIntensity; - FieldVolumes = fieldVolumes ?? Array.Empty(); - FieldParticles = fieldParticles ?? Array.Empty(); - CameraLookAt = cameraLookAt ?? ""; - CameraReconstruction = cameraReconstruction ?? ""; - TemporalQuality = temporalQuality ?? ""; - TemporalHistoryBlend = temporalHistoryBlend; - TemporalJitterScale = temporalJitterScale; - TemporalSharpening = temporalSharpening; - ExposureMode = exposureMode ?? ""; - ExposureLowPercent = exposureLowPercent; - ExposureHighPercent = exposureHighPercent; - ExposureMinimumEv = exposureMinimumEv; - ExposureMaximumEv = exposureMaximumEv; - ExposureKeyValue = exposureKeyValue; - ExposureAdaptation = exposureAdaptation ?? ""; - ExposureSpeedUp = exposureSpeedUp; - ExposureSpeedDown = exposureSpeedDown; - ColorGradingSpace = colorGradingSpace ?? ""; - AssetProviderId = assetProviderId ?? ""; - AssetVerseId = assetVerseId ?? ""; - AssetAuthorityRuntimeId = assetAuthorityRuntimeId ?? ""; - AssetRendezvousEndpoints = assetRendezvousEndpoints ?? Array.Empty(); + InputCapabilityPointerId = inputCapabilityPointerId ?? ""; + CameraTargetEntityId = string.IsNullOrWhiteSpace(cameraTargetEntityId) + ? PlayerEntityId + : cameraTargetEntityId; + SubjectVisible = subjectVisible; + MovementEnabled = movementEnabled; + PresentationMode = presentationMode ?? ""; } public string WorldRootId { get; } @@ -529,191 +369,41 @@ namespace GameCult.Eve.UnityScene public string EntityViewSchema { get; } - public string EntityBodyId { get; } - public string ZoneRenderPointerId { get; } public string ZoneRenderSchema { get; } + public string InputCapabilityPointerId { get; } + public string AssetManifest { get; } - public string AssetProviderId { get; } - - public string AssetVerseId { get; } - - public string AssetAuthorityRuntimeId { get; } - - public IReadOnlyList AssetRendezvousEndpoints { get; } - public string InputProfile { get; } public string CameraRig { get; } - public string CameraTargetEntityId { get; } - - public string CameraLookAt { get; } - - public string CameraReconstruction { get; } - - public string TemporalQuality { get; } - - public float TemporalHistoryBlend { get; } - - public float TemporalJitterScale { get; } - - public float TemporalSharpening { get; } - - public string ExposureMode { get; } - - public string ColorGradingSpace { get; } - - public float ExposureLowPercent { get; } - - public float ExposureHighPercent { get; } - - public float ExposureMinimumEv { get; } - - public float ExposureMaximumEv { get; } - - public float ExposureKeyValue { get; } - - public string ExposureAdaptation { get; } - - public float ExposureSpeedUp { get; } - - public float ExposureSpeedDown { get; } - - public float CameraDistance { get; } - - public float CameraVerticalFieldOfViewDegrees { get; } - - public float CameraTargetScreenX { get; } - - public float CameraTargetScreenY { get; } - - public float CameraPositionDamping { get; } - - public float CameraNearClipPlane { get; } - - public float CameraFarClipPlane { get; } - - public float AmbientLightR { get; } - - public float AmbientLightG { get; } - - public float AmbientLightB { get; } - - public float AmbientLightIntensity { get; } - public string ViewId { get; } public string PlayerEntityId { get; } + public string CameraTargetEntityId { get; } + + public bool SubjectVisible { get; } + + public bool MovementEnabled { get; } + + public string PresentationMode { get; } + public string MovementCommand { get; } - public string LookCommand { get; } - - public float LookSensitivityRadians { get; } - - public string LookModel { get; } - - public string SkyboxAssetRef { get; } - - public string ReflectionAssetRef { get; } - - public float ReflectionIntensity { get; } - - public string PostProcessProfileAssetRef { get; } - - public float KeyLightDirectionX { get; } - - public float KeyLightDirectionY { get; } - - public float KeyLightDirectionZ { get; } - - public float KeyLightColorR { get; } - - public float KeyLightColorG { get; } - - public float KeyLightColorB { get; } - - public float KeyLightIntensity { get; } - public string FocusCommand { get; } public string TargetCommand { get; } public string ActionCommand { get; } - public IReadOnlyList ExcludedRenderChannels { get; } - public int EntityCount => Entities.Count; public IReadOnlyList Entities { get; } - - public IReadOnlyList FieldVolumes { get; } - public IReadOnlyList FieldParticles { get; } - } - - public sealed class EveUnityFieldVolumeProjection - { - public EveUnityFieldVolumeProjection( - string nodeId, - string documentRef, - string documentSchema, - string materialAssetRef, - string renderChannel, - string compositeMode, - string quality, - IReadOnlyDictionary? props = null) - { - NodeId = nodeId ?? ""; - DocumentRef = documentRef ?? ""; - DocumentSchema = documentSchema ?? ""; - MaterialAssetRef = materialAssetRef ?? ""; - RenderChannel = renderChannel ?? ""; - CompositeMode = compositeMode ?? ""; - Quality = quality ?? ""; - Props = props ?? new Dictionary(StringComparer.Ordinal); - } - - public string NodeId { get; } - public string DocumentRef { get; } - public string DocumentSchema { get; } - public string MaterialAssetRef { get; } - public string RenderChannel { get; } - public string CompositeMode { get; } - public string Quality { get; } - public IReadOnlyDictionary Props { get; } - } - - public sealed class EveUnityFieldParticlesProjection - { - public EveUnityFieldParticlesProjection( - string nodeId, - string documentRef, - string documentSchema, - string computeProgramAssetRef, - string materialAssetRef, - string renderChannel, - IReadOnlyDictionary? props = null) - { - NodeId = nodeId ?? ""; - DocumentRef = documentRef ?? ""; - DocumentSchema = documentSchema ?? ""; - ComputeProgramAssetRef = computeProgramAssetRef ?? ""; - MaterialAssetRef = materialAssetRef ?? ""; - RenderChannel = renderChannel ?? ""; - Props = props ?? new Dictionary(StringComparer.Ordinal); - } - - public string NodeId { get; } - public string DocumentRef { get; } - public string DocumentSchema { get; } - public string ComputeProgramAssetRef { get; } - public string MaterialAssetRef { get; } - public string RenderChannel { get; } - public IReadOnlyDictionary Props { get; } } public sealed class EveUnityPlayableWorldEntity diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotReceiptPresenter.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotReceiptPresenter.cs index 9bd38fe..5f853ce 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotReceiptPresenter.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotReceiptPresenter.cs @@ -38,9 +38,6 @@ namespace GameCult.Eve.UnityScene ItemKey = Get(node, "itemKey"); ImpactKind = Get(node, "impactKind"); PresentationIntensity = Math.Max(0, Double(node, "presentationIntensity", 1)); - AppliedDamage = Math.Max(0, Double(node, "appliedDamage", 0)); - ShieldAbsorbedDamage = Math.Max(0, Double(node, "shieldAbsorbedDamage", 0)); - LockQuality = Math.Max(0, Math.Min(1, Double(node, "lockQuality", 0))); } public string ShotId { get; } @@ -57,9 +54,6 @@ namespace GameCult.Eve.UnityScene public string ItemKey { get; } public string ImpactKind { get; } public double PresentationIntensity { get; } - public double AppliedDamage { get; } - public double ShieldAbsorbedDamage { get; } - public double LockQuality { get; } private static string Get(EveUnitySceneNode node, string key) => node.Props.TryGetValue(key, out var value) ? value ?? "" : ""; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs index 4e2ca7f..ecee9ca 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using UnityEngine; #nullable enable @@ -21,7 +20,6 @@ namespace GameCult.Eve.UnityScene private Material? _material; public int ActiveTrajectoryCount => _active.Count; - public int ActiveFallbackTrajectoryCount => _active.Count(trajectory => trajectory.Line != null); public void Bind(EveUnityPlayableWorldClientHost host, IEveUnityGameObjectAssetProvider? assetProvider = null) { @@ -45,10 +43,19 @@ namespace GameCult.Eve.UnityScene if (receipt == null) throw new ArgumentNullException(nameof(receipt)); var shot = new GameObject("Eve shot " + receipt.ShotId); shot.transform.SetParent(transform, false); + var line = shot.AddComponent(); + line.useWorldSpace = true; + line.positionCount = 2; + var intensityScale = Mathf.Clamp(Mathf.Sqrt((float)receipt.PresentationIntensity), 0.5f, 4f); + line.startWidth = Math.Max(0.01f, width * intensityScale); + line.endWidth = Math.Max(0.01f, width * 0.5f * intensityScale); + line.material = SharedMaterial(); + line.startColor = line.endColor = receipt.Hit ? hitColor : missColor; + var origin = Vector(receipt.Origin); + line.SetPosition(0, origin); var endpoint = Vector(receipt.Endpoint); var effectPrefab = ResolveEffectPrefab(receipt); - LineRenderer? line = null; if (effectPrefab != null) { var effect = Instantiate(effectPrefab, shot.transform); @@ -64,23 +71,10 @@ namespace GameCult.Eve.UnityScene lightning.StartAnimation(); } } - else - { - line = shot.AddComponent(); - line.useWorldSpace = true; - line.positionCount = 2; - var intensityScale = Mathf.Clamp(Mathf.Sqrt((float)receipt.PresentationIntensity), 0.5f, 4f); - line.startWidth = Math.Max(0.01f, width * intensityScale); - line.endWidth = Math.Max(0.01f, width * 0.5f * intensityScale); - line.material = SharedMaterial(); - line.startColor = line.endColor = receipt.Hit ? hitColor : missColor; - line.SetPosition(0, origin); - } var impactPrefab = ResolveRolePrefab("effect.impact." + receipt.ImpactKind); var travels = string.Equals(receipt.PresentationKind, "bolt", StringComparison.Ordinal) || string.Equals(receipt.PresentationKind, "guided", StringComparison.Ordinal); - if (line != null) - line.SetPosition(1, travels ? origin : endpoint); + line.SetPosition(1, travels ? origin : endpoint); _active.Add(new ActiveTrajectory( shot, line, @@ -99,14 +93,14 @@ namespace GameCult.Eve.UnityScene { var trajectory = _active[index]; var progress = Mathf.Clamp01((now - trajectory.StartedAt) / trajectory.Duration); - if (trajectory.Travels && trajectory.Line != null) + if (trajectory.Travels) trajectory.Line.SetPosition(1, Vector3.Lerp(trajectory.Origin, trajectory.Endpoint, progress)); if (progress >= 1 && !trajectory.ImpactPresented) { trajectory.ImpactPresented = true; if (trajectory.ImpactPrefab != null) { - var impact = Instantiate(trajectory.ImpactPrefab, trajectory.Root.transform); + var impact = Instantiate(trajectory.ImpactPrefab, transform); impact.transform.position = trajectory.Endpoint; } } @@ -152,7 +146,7 @@ namespace GameCult.Eve.UnityScene private sealed class ActiveTrajectory { - public ActiveTrajectory(GameObject root, LineRenderer? line, Vector3 origin, Vector3 endpoint, float startedAt, float duration, bool travels, GameObject? impactPrefab) + public ActiveTrajectory(GameObject root, LineRenderer line, Vector3 origin, Vector3 endpoint, float startedAt, float duration, bool travels, GameObject? impactPrefab) { Root = root; Line = line; @@ -165,7 +159,7 @@ namespace GameCult.Eve.UnityScene } public GameObject Root { get; } - public LineRenderer? Line { get; } + public LineRenderer Line { get; } public Vector3 Origin { get; } public Vector3 Endpoint { get; } public float StartedAt { get; } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityThermalHudSink.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityThermalHudSink.cs index 1ea683d..ea9919a 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityThermalHudSink.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityThermalHudSink.cs @@ -30,7 +30,14 @@ namespace GameCult.Eve.UnityScene AttachToDocument(); return; } - _document = EveUnityUiDocumentRuntime.Ensure(gameObject); + var panel = ScriptableObject.CreateInstance(); + panel.name = "Eve thermal HUD panel"; + panel.scaleMode = PanelScaleMode.ScaleWithScreenSize; + panel.referenceResolution = new Vector2Int(1920, 1080); + panel.match = 0.5f; + _document = gameObject.AddComponent(); + _document.panelSettings = panel; + _document.sortingOrder = 100; _root = new VisualElement(); Build(_root); AttachToDocument(); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityUiDocumentRuntime.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityUiDocumentRuntime.cs deleted file mode 100644 index fb4481b..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityUiDocumentRuntime.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using UnityEngine; -using UnityEngine.UIElements; - -#nullable enable - -namespace GameCult.Eve.UnityScene -{ - internal static class EveUnityUiDocumentRuntime - { - private const string ThemeResource = "EveUnity/UnityDefaultRuntimeTheme"; - - public static UIDocument Ensure(GameObject host) - { - var document = host.GetComponent(); - if (document != null) return document; - - var panel = host.transform.parent?.GetComponentInParent()?.panelSettings; - if (panel == null) - { - var theme = Resources.Load(ThemeResource); - if (theme == null) - throw new InvalidOperationException($"EveUnity runtime theme resource '{ThemeResource}' is missing."); - - panel = ScriptableObject.CreateInstance(); - panel.name = "Eve playable-world HUD panel"; - panel.scaleMode = PanelScaleMode.ScaleWithScreenSize; - panel.referenceResolution = new Vector2Int(1920, 1080); - panel.match = 0.5f; - panel.themeStyleSheet = theme; - } - - document = host.AddComponent(); - document.panelSettings = panel; - document.sortingOrder = 100; - return document; - } - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityUiDocumentRuntime.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityUiDocumentRuntime.cs.meta deleted file mode 100644 index 977f4f6..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityUiDocumentRuntime.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 894b54214cb34993a7984f73cb45349f diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityUiToolkitOverlay.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityUiToolkitOverlay.cs deleted file mode 100644 index efd74a1..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityUiToolkitOverlay.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System; -using GameCult.Eve.UnityUIToolkit; -using UnityEngine; -using UnityEngine.UIElements; - -#nullable enable - -namespace GameCult.Eve.UnityScene -{ - public sealed class EveUnityUiToolkitOverlay : MonoBehaviour - { - private IEveUnitySceneProviderSurfaceDocumentSource? _documents; - private IEveUnitySceneCommandSink? _commands; - private UIDocument? _document; - private EveUiToolkitSurfacePresenter? _presenter; - private VisualElement? _mountedRoot; - - public VisualElement? Root => _mountedRoot; - - public void Bind(MonoBehaviour documentSource, MonoBehaviour commandSink) - { - if (documentSource is not IEveUnitySceneProviderSurfaceDocumentSource documents) - throw new ArgumentException( - $"{nameof(documentSource)} must implement {nameof(IEveUnitySceneProviderSurfaceDocumentSource)}.", - nameof(documentSource)); - if (commandSink is not IEveUnitySceneCommandSink commands) - throw new ArgumentException( - $"{nameof(commandSink)} must implement {nameof(IEveUnitySceneCommandSink)}.", - nameof(commandSink)); - - Unsubscribe(); - _documents = documents; - _commands = commands; - _documents.DocumentAvailable += OnDocumentAvailable; - EnsureDocument(); - Present(_documents.CurrentDocument); - } - - private void OnDestroy() => Unsubscribe(); - - private void OnDocumentAvailable(EveUnitySceneProviderSurfaceDocument document) => Present(document); - - private void Present(EveUnitySceneProviderSurfaceDocument document) - { - if (document == null || _commands == null) return; - _presenter ??= new EveUiToolkitSurfacePresenter(); - var replaced = _presenter.Present(document.SurfaceDocument, _commands.Submit); - if (replaced && _presenter.Root != null) - { - _mountedRoot?.RemoveFromHierarchy(); - _mountedRoot = _presenter.Root; - _mountedRoot.AddToClassList("eve-playable-world-ui-overlay"); - } - AttachToDocument(); - } - - private void EnsureDocument() - { - _document = EveUnityUiDocumentRuntime.Ensure(gameObject); - } - - private void AttachToDocument() - { - EnsureDocument(); - var documentRoot = _document?.rootVisualElement; - if (_mountedRoot != null && _mountedRoot.parent == null && documentRoot != null) - documentRoot.Add(_mountedRoot); - } - - private void Unsubscribe() - { - if (_documents != null) - _documents.DocumentAvailable -= OnDocumentAvailable; - _documents = null; - _commands = null; - } - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityUiToolkitOverlay.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityUiToolkitOverlay.cs.meta deleted file mode 100644 index 672f84b..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityUiToolkitOverlay.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 843283a94b8a41bab85db52bd84aa59a diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs index dc94d57..ddd6abb 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatBuffer.cs @@ -11,9 +11,8 @@ namespace GameCult.Eve.UnityScene.Fields public const string SplatCountPropertyName = "_EveFieldsSplatCount"; public const string ViewportToClipPropertyName = "_EveFieldsViewportToClip"; public const string ChannelFilterPropertyName = "_EveFieldsChannelFilter"; - public const string SimulationTimePropertyName = "_EveFieldsSimulationTimeSeconds"; - private const int SplatStrideBytes = 96; + private const int SplatStrideBytes = 80; private EveFieldsGpuSplat[] splats = Array.Empty(); private GraphicsBuffer buffer; private int capacity; @@ -55,7 +54,6 @@ namespace GameCult.Eve.UnityScene.Fields RotationChannelFalloff = new Vector4((float)Read(source.RotationCos, index, 1), (float)Read(source.RotationSin, index, 0), channel, Read(source.Falloff, index, EveFieldsSplatFalloffs.Smooth)), LayerSource = new Vector4(layer, Read(source.SourceKind, index, EveFieldsSplatSourceKinds.Constant), (float)Read(source.AnimationSpeed, index, 0), (float)Read(source.SourceFlags, index, 0)), SourceFrequencyPhase = new Vector4((float)Read(source.FrequencyX, index, 1), (float)Read(source.FrequencyY, index, 1), (float)Read(source.PhaseX, index, 0), (float)Read(source.PhaseY, index, 0)), - FalloffParameters = new Vector4((float)Read(source.FalloffScale, index, 1), (float)Read(source.FalloffExponent, index, 1), 0, 0), Value = new Vector4((float)Read(source.ValueR, index, 0), (float)Read(source.ValueG, index, 0), (float)Read(source.ValueB, index, 0), (float)Read(source.ValueA, index, 1)) }; } @@ -101,7 +99,6 @@ namespace GameCult.Eve.UnityScene.Fields public Vector4 RotationChannelFalloff; public Vector4 LayerSource; public Vector4 SourceFrequencyPhase; - public Vector4 FalloffParameters; public Vector4 Value; } } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs index c87f90b..b8638a5 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatRasterizer.cs @@ -26,7 +26,6 @@ namespace GameCult.Eve.UnityScene.Fields private static readonly int SplatCountId = Shader.PropertyToID(EveFieldsSplatBuffer.SplatCountPropertyName); private static readonly int ViewportToClipId = Shader.PropertyToID(EveFieldsSplatBuffer.ViewportToClipPropertyName); private static readonly int ChannelFilterId = Shader.PropertyToID(EveFieldsSplatBuffer.ChannelFilterPropertyName); - private static readonly int SimulationTimeId = Shader.PropertyToID(EveFieldsSplatBuffer.SimulationTimePropertyName); public RenderTexture TargetTexture => targetTexture != null ? targetTexture : ownedTargetTexture; public int LastDrawnCount { get; private set; } @@ -49,7 +48,6 @@ namespace GameCult.Eve.UnityScene.Fields commandBuffer.SetGlobalBuffer(SplatBufferId, splatBuffer.Buffer); commandBuffer.SetGlobalInt(SplatCountId, splatBuffer.Count); commandBuffer.SetGlobalInt(ChannelFilterId, channelFilter); - commandBuffer.SetGlobalFloat(SimulationTimeId, (float)document.SimulationTimeSeconds); commandBuffer.SetGlobalMatrix(ViewportToClipId, BuildViewportToClip(document.Viewport)); commandBuffer.DrawProcedural(Matrix4x4.identity, material, Mathf.Max(0, materialPass), MeshTopology.Triangles, 6, splatBuffer.Count); Graphics.ExecuteCommandBuffer(commandBuffer); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl index 316316d..a7ec7a9 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl @@ -7,7 +7,6 @@ struct EveFieldsSplat float4 rotationChannelFalloff; float4 layerSource; float4 sourceFrequencyPhase; - float4 falloffParameters; float4 value; }; @@ -15,105 +14,31 @@ StructuredBuffer _EveFieldsSplats; float4x4 _EveFieldsViewportToClip; int _EveFieldsSplatCount; int _EveFieldsChannelFilter; -float _EveFieldsSimulationTimeSeconds; float _ValueScale; -// Ashima Arts webgl-noise, translated by Keijiro Takahashi. -// Copyright (C) 2011 Ashima Arts. Distributed under the MIT License. -float3 EveFieldsMod289(float3 x) +float hash21(float2 p) { - return x - floor(x / 289.0) * 289.0; + p = frac(p * float2(123.34, 456.21)); + p += dot(p, p + 45.32); + return frac(p.x * p.y); } -float4 EveFieldsMod289(float4 x) +float valueNoise(float2 p) { - return x - floor(x / 289.0) * 289.0; -} - -float4 EveFieldsPermute(float4 x) -{ - return EveFieldsMod289((x * 34.0 + 1.0) * x); -} - -float4 EveFieldsTaylorInvSqrt(float4 r) -{ - return 1.79284291400159 - r * 0.85373472095314; -} - -float EveFieldsSimplexNoise3D(float3 v) -{ - const float2 C = float2(1.0 / 6.0, 1.0 / 3.0); - float3 i = floor(v + dot(v, C.yyy)); - float3 x0 = v - i + dot(i, C.xxx); - float3 g = step(x0.yzx, x0.xyz); - float3 l = 1.0 - g; - float3 i1 = min(g.xyz, l.zxy); - float3 i2 = max(g.xyz, l.zxy); - float3 x1 = x0 - i1 + C.xxx; - float3 x2 = x0 - i2 + C.yyy; - float3 x3 = x0 - 0.5; - i = EveFieldsMod289(i); - float4 p = EveFieldsPermute(EveFieldsPermute(EveFieldsPermute( - i.z + float4(0.0, i1.z, i2.z, 1.0)) - + i.y + float4(0.0, i1.y, i2.y, 1.0)) - + i.x + float4(0.0, i1.x, i2.x, 1.0)); - float4 j = p - 49.0 * floor(p / 49.0); - float4 x_ = floor(j / 7.0); - float4 y_ = floor(j - 7.0 * x_); - float4 x = (x_ * 2.0 + 0.5) / 7.0 - 1.0; - float4 y = (y_ * 2.0 + 0.5) / 7.0 - 1.0; - float4 h = 1.0 - abs(x) - abs(y); - float4 b0 = float4(x.xy, y.xy); - float4 b1 = float4(x.zw, y.zw); - float4 s0 = floor(b0) * 2.0 + 1.0; - float4 s1 = floor(b1) * 2.0 + 1.0; - float4 sh = -step(h, 0.0); - float4 a0 = b0.xzyw + s0.xzyw * sh.xxyy; - float4 a1 = b1.xzyw + s1.xzyw * sh.zzww; - float3 g0 = float3(a0.xy, h.x); - float3 g1 = float3(a0.zw, h.y); - float3 g2 = float3(a1.xy, h.z); - float3 g3 = float3(a1.zw, h.w); - float4 norm = EveFieldsTaylorInvSqrt(float4( - dot(g0, g0), dot(g1, g1), dot(g2, g2), dot(g3, g3))); - g0 *= norm.x; - g1 *= norm.y; - g2 *= norm.z; - g3 *= norm.w; - float4 m = max(0.6 - float4( - dot(x0, x0), dot(x1, x1), dot(x2, x2), dot(x3, x3)), 0.0); - m *= m; - m *= m; - return 42.0 * dot(m, float4( - dot(x0, g0), dot(x1, g1), dot(x2, g2), dot(x3, g3))); -} - -float EveFieldsCellDistanceB(float2 p) -{ - p = frac(p) - 0.5; - return (length(p) * 1.5 + 0.25) * - max(abs(p.x) * 0.866 + p.y * 0.5, -p.y); -} - -float EveFieldsAnimatedCellNoiseB(float2 p, float time) -{ - float2 o = sin(float2(1.93, 0.0) + time) * 0.166; - float a = EveFieldsCellDistanceB(p + float2(o.x, 0)); - float b = EveFieldsCellDistanceB(p + float2(0, 0.5 + o.y)); - p = mul(p + 0.5, -float2x2(0.5, -0.866, 0.866, 0.5)); - float c = EveFieldsCellDistanceB(p + float2(o.x, 0)); - float d = EveFieldsCellDistanceB(p + float2(0, 0.5 + o.y)); - p = mul(p + 0.5, -float2x2(0.5, -0.866, 0.866, 0.5)); - float e = EveFieldsCellDistanceB(p + float2(o.x, 0)); - float f = EveFieldsCellDistanceB(p + float2(0, 0.5 + o.y)); - return min(min(min(a, b), min(c, d)), min(e, f)) * 2.0; + float2 i = floor(p); + float2 f = frac(p); + float a = hash21(i); + float b = hash21(i + float2(1, 0)); + float c = hash21(i + float2(0, 1)); + float d = hash21(i + float2(1, 1)); + float2 u = f * f * (3 - 2 * f); + return lerp(lerp(a, b, u.x), lerp(c, d, u.x), u.y) * 2 - 1; } struct Varyings { float4 position : SV_POSITION; float2 localUv : TEXCOORD0; - float2 fieldWorld : TEXCOORD2; nointerpolation uint instanceId : TEXCOORD1; }; @@ -129,22 +54,16 @@ Varyings Vert(uint vertexId : SV_VertexID, uint instanceId : SV_InstanceID) Varyings output; output.position = mul(_EveFieldsViewportToClip, float4(world, 0, 1)); output.localUv = local; - output.fieldWorld = world; output.instanceId = instanceId; return output; } -float ResolveFalloff(float2 localUv, int falloff, float scale, float exponent) +float ResolveFalloff(float2 localUv, int falloff) { float distance01 = saturate(length(localUv)); if (falloff == 0) return 1; if (falloff == 1) return saturate(1 - distance01); if (falloff == 3) return smoothstep(0, 1, distance01); - if (falloff == 4) - { - float pulseDistance = distance01 * max(0, scale); - return pow(saturate(1 - pulseDistance * pulseDistance), max(0.0001, exponent)); - } return 1 - smoothstep(0, 1, distance01); } @@ -153,39 +72,16 @@ float4 Frag(Varyings input) : SV_Target EveFieldsSplat splat = _EveFieldsSplats[input.instanceId]; int channel = (int)round(splat.rotationChannelFalloff.z); if (_EveFieldsChannelFilter >= 0 && channel != _EveFieldsChannelFilter) discard; - float alpha = ResolveFalloff( - input.localUv, - (int)round(splat.rotationChannelFalloff.w), - splat.falloffParameters.x, - splat.falloffParameters.y); + float alpha = ResolveFalloff(input.localUv, (int)round(splat.rotationChannelFalloff.w)); clip(alpha - 0.0001); int sourceKind = (int)round(splat.layerSource.y); float source = 1; if (sourceKind == 1 || sourceKind == 2) { - float timeCoordinate = sourceKind == 2 - ? _EveFieldsSimulationTimeSeconds * splat.layerSource.z - : 0; - source = EveFieldsSimplexNoise3D(float3( - input.fieldWorld * splat.sourceFrequencyPhase.xy + splat.sourceFrequencyPhase.zw, - timeCoordinate)); + float timeOffset = sourceKind == 2 ? _Time.y * splat.layerSource.z : 0; + source = valueNoise(input.localUv * splat.sourceFrequencyPhase.xy + splat.sourceFrequencyPhase.zw + timeOffset); + if (splat.layerSource.w != 0) source = abs(source); } - else if (sourceKind == 3) - { - source = EveFieldsAnimatedCellNoiseB( - input.fieldWorld * splat.sourceFrequencyPhase.xy + splat.sourceFrequencyPhase.zw, - _EveFieldsSimulationTimeSeconds * splat.layerSource.z); - } - else if (sourceKind == 4) - { - float distance01 = length(input.localUv); - float radialExponent = max(0.0001, splat.sourceFrequencyPhase.y); - source = cos( - pow(distance01, radialExponent) * splat.sourceFrequencyPhase.x + - splat.sourceFrequencyPhase.z + - _EveFieldsSimulationTimeSeconds * splat.layerSource.z); - } - if (((int)round(splat.layerSource.w) & 1) != 0) source = abs(source); return splat.value * (alpha * source * _ValueScale); } #endif diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs deleted file mode 100644 index e40a392..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; -using GameCult.Eve.PluginFields; - -namespace GameCult.Eve.UnityScene.Fields -{ - public interface IEveUnityFieldsSplatsDocumentSource - { - event Action FieldsSplatsAvailable; - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs.meta deleted file mode 100644 index 608135d..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 01897652f3f84ae0be3a99c9f9837d9d -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs deleted file mode 100644 index 803bfd1..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs +++ /dev/null @@ -1,678 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using GameCult.Eve.PluginFields; -using UnityEngine; -using UnityEngine.Rendering; -using UnityEngine.Rendering.RenderGraphModule; -using UnityEngine.Rendering.Universal; - -#nullable enable - -namespace GameCult.Eve.UnityScene.Fields -{ - public sealed class EveUnityFieldsParticleRenderer : MonoBehaviour, IEveUnityRenderPassSource - { - private EveUnityPlayableWorldClientHost? _host; - private IEveUnityFieldsSplatsDocumentSource? _source; - private EveFieldsSplatsDocument? _document; - private EveFieldsSplatLayerRenderer? _layers; - private GameObject? _layerRoot; - private readonly List _targets = new List(); - private ComputeShader? _computeSource; - private ComputeShader? _compute; - private Material? _materialSource; - private Material? _material; - private IReadOnlyDictionary? _computeMetadata; - private IReadOnlyDictionary? _materialMetadata; - private readonly Dictionary _assetTextures = new Dictionary(StringComparer.Ordinal); - private readonly Dictionary _materialAssetTextures = new Dictionary(StringComparer.Ordinal); - private ComputeBuffer? _particles; - private ComputeBuffer? _quadPoints; - private EveUnityFieldsParticlePass? _renderPass; - private string _activeNodeId = ""; - private long _rasterizedFrameId = -1; - private Vector2 _rasterizedCenter = new Vector2(float.NaN, float.NaN); - - public long PresentedFrameId { get; private set; } = -1; - public int ParticleCount { get; private set; } - public int DispatchCount { get; private set; } - public int DrawCount { get; private set; } - public Vector2 LastGridCenter { get; private set; } - public bool ProgramReady => _compute != null && _material != null && _particles != null && _quadPoints != null; - public static RenderPassEvent CompositionRenderPassEvent => RenderPassEvent.BeforeRenderingPostProcessing; - int IEveUnityRenderPassSource.RenderOrder => 200; - - public void Bind(EveUnityPlayableWorldClientHost host, IEveUnityFieldsSplatsDocumentSource? source) - { - if (host == null) throw new ArgumentNullException(nameof(host)); - if (_source != null) _source.FieldsSplatsAvailable -= OnFieldsSplatsAvailable; - _host = host; - _source = source; - if (_source != null) _source.FieldsSplatsAvailable += OnFieldsSplatsAvailable; - enabled = _source != null; - } - - private void OnEnable() => EveUnityRenderPassRegistry.Register(this); - - private void OnDisable() - { - EveUnityRenderPassRegistry.Unregister(this); - ReleaseState(); - } - - private void OnDestroy() - { - if (_source != null) _source.FieldsSplatsAvailable -= OnFieldsSplatsAvailable; - _source = null; - _host = null; - } - - private void OnFieldsSplatsAvailable(EveFieldsSplatsDocument document) - { - if (document == null || document.FrameId < PresentedFrameId) return; - _document = document; - PresentedFrameId = document.FrameId; - } - - bool IEveUnityRenderPassSource.TryEnqueuePass(Camera camera, ScriptableRenderer renderer) - { - if (_host == null || _document == null || camera == null || - _host.ActiveCameraTransform != camera.transform) - return false; - var field = ActiveField(); - if (field == null || !PrepareFrame(field, camera, _document)) return false; - _renderPass ??= new EveUnityFieldsParticlePass(this); - renderer.EnqueuePass(_renderPass); - return true; - } - - private EveUnityFieldParticlesProjection? ActiveField() => - _host?.ActiveWorld?.FieldParticles.FirstOrDefault(field => - string.Equals(field.DocumentSchema, EveFieldsSchemas.Splats, StringComparison.Ordinal)); - - private bool PrepareFrame( - EveUnityFieldParticlesProjection field, - Camera camera, - EveFieldsSplatsDocument document) - { - if (!EnsurePrograms(field) || !EnsureBuffers(field)) return false; - EnsureTargets(field); - EnsureLayerRenderer(); - if (_layers == null) return false; - - var baseWidth = PositiveInt(field, "textureWidth", 512); - var baseHeight = PositiveInt(field, "textureHeight", 512); - var snapLayer = Prop(field, "viewportSnapLayer"); - var snapTarget = _targets.FirstOrDefault(target => - string.Equals(target.LayerKey, snapLayer, StringComparison.Ordinal)); - var snapWidth = snapTarget == null - ? 0 - : Mathf.Max(1, Mathf.RoundToInt(baseWidth * Mathf.Max(0.01f, snapTarget.WidthScale))); - var snapHeight = snapTarget == null - ? 0 - : Mathf.Max(1, Mathf.RoundToInt(baseHeight * Mathf.Max(0.01f, snapTarget.HeightScale))); - if (!EveUnityFieldsViewportFrame.TryResolve( - document, - field.Props, - new Vector2(camera.transform.position.x, camera.transform.position.z), - snapWidth, - snapHeight, - out var projected, - out var center)) return false; - var viewportWidth = (float)(projected.Viewport.MaxX - projected.Viewport.MinX); - var viewportHeight = (float)(projected.Viewport.MaxY - projected.Viewport.MinY); - - if (_rasterizedFrameId != document.FrameId || _rasterizedCenter != center) - { - _layers.Render(projected, _targets, baseWidth, baseHeight); - _rasterizedFrameId = document.FrameId; - _rasterizedCenter = center; - } - - ApplyProgramBindings(field, document, center, viewportWidth, viewportHeight); - var kernel = ProgramKernel("update"); - if (kernel < 0) return false; - var threadGroupSize = PositiveInt(field, "threadGroupSize", 1); - var groups = Mathf.CeilToInt((float)ParticleCount / threadGroupSize); - if (groups <= 0) return false; - _compute!.Dispatch(kernel, groups, 1, 1); - DispatchCount++; - LastGridCenter = center; - return true; - } - - private bool EnsurePrograms(EveUnityFieldParticlesProjection field) - { - var provider = _host?.NativeAssetProvider; - if (provider == null || provider is not IEveUnityNativeAssetMetadataProvider metadataProvider || - string.IsNullOrWhiteSpace(field.ComputeProgramAssetRef) || - string.IsNullOrWhiteSpace(field.MaterialAssetRef)) - return false; - - var computeBinding = new EveUnityPlayableWorldAssetBinding( - field.ComputeProgramAssetRef, "", "provider-asset-ref"); - var materialBinding = new EveUnityPlayableWorldAssetBinding( - field.MaterialAssetRef, "", "provider-asset-ref"); - var compute = provider.ResolveAsset(computeBinding, typeof(ComputeShader)) as ComputeShader; - var material = provider.ResolveAsset(materialBinding, typeof(Material)) as Material; - if (compute == null || material == null || material.shader == null || !material.shader.isSupported || - !metadataProvider.TryResolveAssetMetadata(computeBinding, out var computeMetadata) || - !metadataProvider.TryResolveAssetMetadata(materialBinding, out var materialMetadata) || - !TryValidateProgramMetadata(computeMetadata, materialMetadata, out _) || - !TryValidateAdvertisedContract(field, compute, material, computeMetadata, materialMetadata)) - return false; - - var resolvedTextures = new Dictionary(StringComparer.Ordinal); - foreach (var binding in ParseBindings(Prop(field, "assetTextureBindings"))) - { - var texture = provider.ResolveAsset( - new EveUnityPlayableWorldAssetBinding(binding.Key, "", "provider-asset-ref"), - typeof(Texture)) as Texture; - if (texture == null) return false; - resolvedTextures[binding.Value] = texture; - } - var resolvedMaterialTextures = new Dictionary(StringComparer.Ordinal); - foreach (var binding in ParseBindings(Prop(field, "materialAssetTextureBindings"))) - { - var texture = provider.ResolveAsset( - new EveUnityPlayableWorldAssetBinding(binding.Key, "", "provider-asset-ref"), - typeof(Texture)) as Texture; - if (texture == null) return false; - resolvedMaterialTextures[binding.Value] = texture; - } - - if (ReferenceEquals(compute, _computeSource) && ReferenceEquals(material, _materialSource) && - string.Equals(field.NodeId, _activeNodeId, StringComparison.Ordinal) && ProgramReady && - resolvedTextures.Count == _assetTextures.Count && resolvedTextures.All(binding => - _assetTextures.TryGetValue(binding.Key, out var current) && ReferenceEquals(current, binding.Value)) && - resolvedMaterialTextures.Count == _materialAssetTextures.Count && resolvedMaterialTextures.All(binding => - _materialAssetTextures.TryGetValue(binding.Key, out var current) && ReferenceEquals(current, binding.Value))) - return true; - - ReleaseProgramsAndBuffers(); - _computeSource = compute; - _materialSource = material; - _computeMetadata = computeMetadata; - _materialMetadata = materialMetadata; - _activeNodeId = field.NodeId; - _compute = Instantiate(compute); - _material = new Material(material) { hideFlags = HideFlags.DontSave }; - _assetTextures.Clear(); - foreach (var binding in resolvedTextures) _assetTextures[binding.Key] = binding.Value; - _materialAssetTextures.Clear(); - foreach (var binding in resolvedMaterialTextures) - { - _materialAssetTextures[binding.Key] = binding.Value; - SetMaterialTexture(binding.Key, binding.Value); - } - foreach (var feature in Prop(field, "features").Split(';')) - { - var logical = feature.Trim(); - if (!string.IsNullOrWhiteSpace(logical) && - TryComputeProgramValue($"unity.particles.feature.{logical}.keyword", out var keyword) && - !string.IsNullOrWhiteSpace(keyword)) - _compute.EnableKeyword(keyword); - } - return true; - } - - public static bool TryValidateProgramMetadata( - IReadOnlyDictionary? compute, - IReadOnlyDictionary? material, - out string error) - { - if (compute == null || material == null) - return Fail("Field-particle programs require compute and material metadata.", out error); - var computeKeys = new[] - { - "unity.particles.kernel.update", - "unity.particles.bufferPort.particles", - "unity.particles.texturePort.surfaceHeight", - "unity.particles.texturePort.tint", - "unity.particles.texturePort.hue", - "unity.particles.vectorPort.viewportTransform", - "unity.particles.vectorPort.timeVector", - "unity.particles.floatPort.time", - "unity.particles.floatPort.period", - "unity.particles.floatPort.spacing", - "unity.particles.intPort.span" - }; - if (computeKeys.Any(key => !compute.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value))) - return Fail("Field-particle compute metadata is missing a required logical port.", out error); - var materialKeys = new[] - { - "unity.particles.pass.render", - "unity.particles.bufferPort.particles", - "unity.particles.bufferPort.quadPoints" - }; - if (materialKeys.Any(key => !material.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value)) || - !int.TryParse(material["unity.particles.pass.render"], NumberStyles.Integer, - CultureInfo.InvariantCulture, out var pass) || pass < 0) - return Fail("Field-particle material metadata is missing a valid render pass or buffer port.", out error); - error = ""; - return true; - } - - private static bool TryValidateAdvertisedContract( - EveUnityFieldParticlesProjection field, - ComputeShader compute, - Material material, - IReadOnlyDictionary computeMetadata, - IReadOnlyDictionary materialMetadata) - { - var kernelName = computeMetadata["unity.particles.kernel.update"]; - if (!compute.HasKernel(kernelName)) return false; - var kernel = compute.FindKernel(kernelName); - compute.GetKernelThreadGroupSizes(kernel, out var nativeX, out var nativeY, out var nativeZ); - if (!int.TryParse(Prop(field, "threadGroupSize"), NumberStyles.Integer, - CultureInfo.InvariantCulture, out var advertisedGroupSize) || - advertisedGroupSize <= 0 || nativeX != advertisedGroupSize || nativeY != 1 || nativeZ != 1) - return false; - - if (!int.TryParse(materialMetadata["unity.particles.pass.render"], NumberStyles.Integer, - CultureInfo.InvariantCulture, out var pass) || pass < 0 || pass >= material.passCount) - return false; - - foreach (var binding in ParseBindingPairs(Prop(field, "layerBindings"))) - if (!HasPort(computeMetadata, "texturePort", binding.Value)) return false; - foreach (var binding in ParseBindingPairs(Prop(field, "assetTextureBindings"))) - if (!HasPort(computeMetadata, "texturePort", binding.Value)) return false; - if (!TryValidateMaterialPresentationContract(field, materialMetadata, out _)) return false; - foreach (var parameter in ParseBindingPairs(Prop(field, "floatParameters"))) - if (!HasPort(computeMetadata, "floatPort", parameter.Key) || - !TryFloat(parameter.Value, out _)) return false; - var floatParameters = ParseBindings(Prop(field, "floatParameters")); - if (!floatParameters.TryGetValue("spacing", out var spacingValue) || - !TryFloat(spacingValue, out var spacing) || - !TryFloat(Prop(field, "cellWorldSize"), out var cellWorldSize) || - Mathf.Abs(spacing - cellWorldSize) > 0.0001f || - PositiveInt(field, "gravityTexelsPerCell", 0) != - PositiveInt(field, "viewportSnapTexels", 0)) return false; - foreach (var binding in ParseBindingPairs(Prop(field, "documentFloatBindings"))) - { - var pieces = binding.Value.Split(','); - if (pieces.Length != 3 || !HasPort(computeMetadata, "floatPort", pieces[0].Trim()) || - !TryFloat(pieces[1], out _) || !TryFloat(pieces[2], out _)) return false; - } - - var timeVectorPort = Prop(field, "documentTimeVectorPort"); - if (!string.IsNullOrWhiteSpace(timeVectorPort) && - !HasPort(computeMetadata, "vectorPort", timeVectorPort)) return false; - foreach (var feature in Prop(field, "features").Split(';')) - { - var logical = feature.Trim(); - if (!string.IsNullOrWhiteSpace(logical) && - (!computeMetadata.TryGetValue($"unity.particles.feature.{logical}.keyword", out var keyword) || - string.IsNullOrWhiteSpace(keyword))) return false; - } - - return PositiveInt(field, "span", 0) > 0 && - PositiveInt(field, "particleStrideBytes", 0) > 0 && - PositiveInt(field, "textureWidth", 0) > 0 && - PositiveInt(field, "textureHeight", 0) > 0; - } - - public static bool TryValidateMaterialPresentationContract( - EveUnityFieldParticlesProjection field, - IReadOnlyDictionary? materialMetadata, - out string error) - { - if (field == null || materialMetadata == null) - return Fail("Field-particle material presentation contract is missing.", out error); - var textures = ParseBindings(Prop(field, "materialAssetTextureBindings")); - foreach (var binding in textures) - if (!HasPort(materialMetadata, "texturePort", binding.Value)) - return Fail("Field-particle material texture binding has no native logical port.", out error); - foreach (var binding in ParseBindingPairs(Prop(field, "materialViewportTextureScaleBindings"))) - { - if (!HasPort(materialMetadata, "vectorPort", binding.Key) || - !textures.Values.Contains(binding.Value)) - return Fail("Field-particle viewport texture-scale binding is incomplete.", out error); - } - var framePort = Prop(field, "materialRenderFrameIndexPort"); - if (!string.IsNullOrWhiteSpace(framePort) && !HasPort(materialMetadata, "intPort", framePort)) - return Fail("Field-particle render-frame binding has no native logical port.", out error); - error = ""; - return true; - } - - private static bool HasPort( - IReadOnlyDictionary metadata, - string kind, - string logical) => - metadata.TryGetValue($"unity.particles.{kind}.{logical}", out var native) && - !string.IsNullOrWhiteSpace(native); - - private bool EnsureBuffers(EveUnityFieldParticlesProjection field) - { - var span = PositiveInt(field, "span", 0); - var stride = PositiveInt(field, "particleStrideBytes", 0); - if (span <= 0 || span > 4096 || stride <= 0 || stride > 1024) return false; - var count64 = (long)span * span; - if (count64 <= 0 || count64 > 16_777_216) return false; - var count = (int)count64; - if (_particles != null && ParticleCount == count) return true; - - _particles?.Release(); - _quadPoints?.Release(); - _particles = new ComputeBuffer(count, stride, ComputeBufferType.Structured); - _quadPoints = new ComputeBuffer(6, sizeof(float) * 3, ComputeBufferType.Structured); - _quadPoints.SetData(new[] - { - new Vector3(-0.5f, 0.5f), new Vector3(0.5f, 0.5f), new Vector3(0.5f, -0.5f), - new Vector3(0.5f, -0.5f), new Vector3(-0.5f, -0.5f), new Vector3(-0.5f, 0.5f) - }); - ParticleCount = count; - return true; - } - - private void ApplyProgramBindings( - EveUnityFieldParticlesProjection field, - EveFieldsSplatsDocument document, - Vector2 center, - float viewportWidth, - float viewportHeight) - { - var kernel = ProgramKernel("update"); - if (kernel < 0) return; - SetComputeBuffer(kernel, "particles", _particles!); - SetMaterialBuffer("particles", _particles!); - SetMaterialBuffer("quadPoints", _quadPoints!); - - foreach (var binding in ParseBindings(Prop(field, "layerBindings"))) - { - var target = _targets.FirstOrDefault(value => string.Equals(value.LayerKey, binding.Key, StringComparison.Ordinal)); - if (target?.TargetTexture != null) SetComputeTexture(kernel, binding.Value, target.TargetTexture); - } - foreach (var binding in _assetTextures) - SetComputeTexture(kernel, binding.Key, binding.Value); - SetComputeVector("viewportTransform", new Vector4(center.x, center.y, viewportWidth, viewportHeight)); - - var time = (float)document.SimulationTimeSeconds; - var timeVectorPort = Prop(field, "documentTimeVectorPort"); - if (!string.IsNullOrWhiteSpace(timeVectorPort)) - SetComputeVector(timeVectorPort, new Vector4(time / 20f, time, time * 2f, time * 3f)); - if (TryEvaluateDocumentFloatBindings( - document, - Prop(field, "documentFloatBindings"), - out var documentFloats)) - foreach (var binding in documentFloats) - SetComputeFloat(binding.Key, binding.Value); - foreach (var parameter in ParseBindings(Prop(field, "floatParameters"))) - if (TryFloat(parameter.Value, out var value)) SetComputeFloat(parameter.Key, value); - SetComputeInt("span", PositiveInt(field, "span", 0)); - } - - private void RenderParticles( - UnsafeCommandBuffer commands, - Camera camera, - TextureHandle colorTarget, - TextureHandle depthTarget) - { - if (_host == null || _host.ActiveCameraTransform != camera.transform || !ProgramReady) return; - var field = ActiveField(); - if (field == null) return; - foreach (var binding in ParseBindings(Prop(field, "materialViewportTextureScaleBindings"))) - { - if (!_materialAssetTextures.TryGetValue(binding.Value, out var texture) || - texture.width <= 0 || texture.height <= 0) return; - SetMaterialVector(binding.Key, new Vector4( - (float)Math.Max(1, camera.pixelWidth) / texture.width, - (float)Math.Max(1, camera.pixelHeight) / texture.height, - 0f, - 0f)); - } - var framePort = Prop(field, "materialRenderFrameIndexPort"); - if (!string.IsNullOrWhiteSpace(framePort)) SetMaterialInt(framePort, Time.frameCount); - if (depthTarget.IsValid()) commands.SetRenderTarget(colorTarget, depthTarget); - else commands.SetRenderTarget(colorTarget); - commands.SetViewport(camera.pixelRect); - commands.DrawProcedural(Matrix4x4.identity, _material!, MaterialPass("render"), - MeshTopology.Triangles, 6, ParticleCount); - DrawCount++; - } - - private void EnsureTargets(EveUnityFieldParticlesProjection field) - { - var bindings = ParseBindings(Prop(field, "layerBindings")); - var descriptors = ParseBindings(Prop(field, "layerTargetDescriptors")); - if (string.Equals(_activeNodeId, field.NodeId, StringComparison.Ordinal) && - _targets.Count == bindings.Count && _targets.All(target => bindings.ContainsKey(target.LayerKey))) - return; - _targets.Clear(); - foreach (var binding in bindings) - { - var target = new EveFieldsSplatLayerTarget { LayerKey = binding.Key }; - if (descriptors.TryGetValue(binding.Key, out var descriptor)) - EveUnityFieldsVolumeRenderer.TryApplyLayerTargetDescriptor(target, descriptor); - _targets.Add(target); - } - } - - private void EnsureLayerRenderer() - { - if (_layers != null) return; - _layerRoot = new GameObject("Eve Fields Particle Layers") { hideFlags = HideFlags.DontSave }; - _layerRoot.transform.SetParent(transform, false); - _layers = _layerRoot.AddComponent(); - } - - private sealed class EveUnityFieldsParticlePass : ScriptableRenderPass - { - private readonly EveUnityFieldsParticleRenderer _owner; - private sealed class PassData - { - internal Camera Camera = null!; - internal EveUnityFieldsParticleRenderer Owner = null!; - internal TextureHandle ColorTarget; - internal TextureHandle DepthTarget; - } - - internal EveUnityFieldsParticlePass(EveUnityFieldsParticleRenderer owner) - { - _owner = owner; - renderPassEvent = CompositionRenderPassEvent; - ConfigureInput(ScriptableRenderPassInput.Depth); - } - - public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) - { - var cameraData = frameData.Get(); - if (cameraData.camera == null || _owner._host == null || - _owner._host.ActiveCameraTransform != cameraData.camera.transform) - return; - var resources = frameData.Get(); - if (!resources.activeColorTexture.IsValid()) return; - using var builder = renderGraph.AddUnsafePass("Eve Fields Particles", out var data); - data.Camera = cameraData.camera; - data.Owner = _owner; - data.ColorTarget = resources.activeColorTexture; - data.DepthTarget = resources.activeDepthTexture; - builder.UseTexture(data.ColorTarget, AccessFlags.Write); - if (data.DepthTarget.IsValid()) builder.UseTexture(data.DepthTarget, AccessFlags.Read); - builder.AllowPassCulling(false); - builder.SetRenderFunc(static (PassData pass, UnsafeGraphContext context) => - pass.Owner.RenderParticles(context.cmd, pass.Camera, pass.ColorTarget, pass.DepthTarget)); - } - } - - private int ProgramKernel(string logical) - { - if (_compute == null || !TryComputeProgramValue($"unity.particles.kernel.{logical}", out var name) || - string.IsNullOrWhiteSpace(name) || !_compute.HasKernel(name)) - return -1; - return _compute.FindKernel(name); - } - - private int MaterialPass(string logical) => - TryMaterialProgramValue($"unity.particles.pass.{logical}", out var value) && - int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var pass) && pass >= 0 - ? pass - : -1; - - private void SetComputeTexture(int kernel, string logical, Texture texture) - { - if (TryComputeProgramValue($"unity.particles.texturePort.{logical}", out var property)) - _compute!.SetTexture(kernel, property, texture); - } - - private void SetComputeBuffer(int kernel, string logical, ComputeBuffer buffer) - { - if (TryComputeProgramValue($"unity.particles.bufferPort.{logical}", out var property)) - _compute!.SetBuffer(kernel, property, buffer); - } - - private void SetMaterialBuffer(string logical, ComputeBuffer buffer) - { - if (TryMaterialProgramValue($"unity.particles.bufferPort.{logical}", out var property)) - _material!.SetBuffer(property, buffer); - } - - private void SetMaterialTexture(string logical, Texture texture) - { - if (TryMaterialProgramValue($"unity.particles.texturePort.{logical}", out var property)) - _material!.SetTexture(property, texture); - } - - private void SetMaterialVector(string logical, Vector4 value) - { - if (TryMaterialProgramValue($"unity.particles.vectorPort.{logical}", out var property)) - _material!.SetVector(property, value); - } - - private void SetMaterialInt(string logical, int value) - { - if (TryMaterialProgramValue($"unity.particles.intPort.{logical}", out var property)) - _material!.SetInt(property, value); - } - - private void SetComputeFloat(string logical, float value) - { - if (TryComputeProgramValue($"unity.particles.floatPort.{logical}", out var property)) - _compute!.SetFloat(property, value); - } - - private void SetComputeInt(string logical, int value) - { - if (TryComputeProgramValue($"unity.particles.intPort.{logical}", out var property)) - _compute!.SetInt(property, value); - } - - private void SetComputeVector(string logical, Vector4 value) - { - if (TryComputeProgramValue($"unity.particles.vectorPort.{logical}", out var property)) - _compute!.SetVector(property, value); - } - - private bool TryComputeProgramValue(string key, out string value) - { - value = ""; - return _computeMetadata != null && _computeMetadata.TryGetValue(key, out value!); - } - - private bool TryMaterialProgramValue(string key, out string value) - { - value = ""; - return _materialMetadata != null && _materialMetadata.TryGetValue(key, out value!); - } - - private void ReleaseState() - { - ReleaseProgramsAndBuffers(); - if (_layerRoot != null) Destroy(_layerRoot); - _layerRoot = null; - _layers = null; - _targets.Clear(); - _document = null; - PresentedFrameId = -1; - DispatchCount = 0; - DrawCount = 0; - LastGridCenter = default; - _rasterizedFrameId = -1; - _rasterizedCenter = new Vector2(float.NaN, float.NaN); - } - - private void ReleaseProgramsAndBuffers() - { - _particles?.Release(); - _quadPoints?.Release(); - _particles = null; - _quadPoints = null; - ParticleCount = 0; - if (_compute != null) Destroy(_compute); - if (_material != null) Destroy(_material); - _compute = null; - _material = null; - _computeSource = null; - _materialSource = null; - _computeMetadata = null; - _materialMetadata = null; - _assetTextures.Clear(); - _materialAssetTextures.Clear(); - _activeNodeId = ""; - } - - private static bool Fail(string message, out string error) - { - error = message; - return false; - } - - private static Dictionary ParseBindings(string value) - { - var result = new Dictionary(StringComparer.Ordinal); - foreach (var pair in ParseBindingPairs(value)) result[pair.Key] = pair.Value; - return result; - } - - private static IReadOnlyList> ParseBindingPairs(string value) - { - var result = new List>(); - foreach (var pair in (value ?? "").Split(';')) - { - var separator = pair.IndexOf('='); - if (separator <= 0 || separator >= pair.Length - 1) continue; - result.Add(new KeyValuePair( - pair.Substring(0, separator).Trim(), - pair.Substring(separator + 1).Trim())); - } - return result; - } - - public static bool TryEvaluateDocumentFloatBindings( - EveFieldsSplatsDocument document, - string bindings, - out IReadOnlyList> values) - { - var result = new List>(); - foreach (var binding in ParseBindingPairs(bindings)) - { - if (!EveUnityFieldsVolumeRenderer.TryEvaluateDocumentFloatBinding( - document, binding.Key, binding.Value, out var port, out var value)) - { - values = Array.Empty>(); - return false; - } - result.Add(new KeyValuePair(port, value)); - } - values = result; - return true; - } - - private static string Prop(EveUnityFieldParticlesProjection field, string key) => - field.Props.TryGetValue(key, out var value) ? value ?? "" : ""; - - private static int PositiveInt(EveUnityFieldParticlesProjection field, string key, int fallback) => - int.TryParse(Prop(field, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && value > 0 - ? value - : fallback; - - private static float PositiveFloat(EveUnityFieldParticlesProjection field, string key, float fallback) => - TryFloat(Prop(field, key), out var value) && value > 0f ? value : fallback; - - private static bool TryFloat(string value, out float result) => - float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out result) && float.IsFinite(result); - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs.meta deleted file mode 100644 index 0e14dd5..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: e403315156af1eb4e9cebaa4cec1bcb8 \ No newline at end of file diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsViewportFrame.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsViewportFrame.cs deleted file mode 100644 index 608a02c..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsViewportFrame.cs +++ /dev/null @@ -1,128 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using GameCult.Eve.PluginFields; -using UnityEngine; - -#nullable enable - -namespace GameCult.Eve.UnityScene.Fields -{ - public static class EveUnityFieldsViewportFrame - { - public static bool TryResolve( - EveFieldsSplatsDocument source, - IReadOnlyDictionary props, - Vector2 cameraPosition, - int snapTextureWidth, - int snapTextureHeight, - out EveFieldsSplatsDocument projected, - out Vector2 center) - { - projected = source; - center = default; - if (source == null || source.Viewport == null) return false; - - var width = (float)(source.Viewport.MaxX - source.Viewport.MinX); - var height = (float)(source.Viewport.MaxY - source.Viewport.MinY); - if (!float.IsFinite(width) || !float.IsFinite(height) || width <= 0f || height <= 0f) - return false; - - center = new Vector2( - (float)((source.Viewport.MinX + source.Viewport.MaxX) * 0.5), - (float)((source.Viewport.MinY + source.Viewport.MaxY) * 0.5)); - if (!string.Equals(Prop(props, "viewportAnchor"), "active-camera.xz", StringComparison.Ordinal)) - return true; - - var span = PositiveInt(props, "span"); - var cellWorldSize = PositiveFloat(props, "cellWorldSize"); - var snapTexels = PositiveInt(props, "viewportSnapTexels"); - if (!TryValidateSpatialLattice( - width, height, snapTextureWidth, snapTextureHeight, - span, cellWorldSize, snapTexels)) - return false; - - center.x = SnapCameraCoordinate(cameraPosition.x, width, snapTextureWidth, snapTexels); - center.y = SnapCameraCoordinate(cameraPosition.y, height, snapTextureHeight, snapTexels); - projected = Project(source, center, width, height); - return true; - } - - public static float SnapCameraCoordinate( - float coordinate, - float viewportWidth, - int snapTargetWidth, - float snapTexels) - { - if (!float.IsFinite(coordinate) || !float.IsFinite(viewportWidth) || - !float.IsFinite(snapTexels) || viewportWidth <= 0f || - snapTargetWidth <= 0 || snapTexels <= 0f) - throw new ArgumentOutOfRangeException(nameof(viewportWidth)); - var snapLength = viewportWidth * snapTexels / snapTargetWidth; - return (int)(coordinate / snapLength) * snapLength; - } - - public static bool TryValidateSpatialLattice( - float viewportWidth, - float viewportHeight, - int gravityTextureWidth, - int gravityTextureHeight, - int span, - float cellWorldSize, - int gravityTexelsPerCell) - { - if (!float.IsFinite(viewportWidth) || !float.IsFinite(viewportHeight) || - !float.IsFinite(cellWorldSize) || viewportWidth <= 0f || viewportHeight <= 0f || - gravityTextureWidth <= 0 || gravityTextureHeight <= 0 || span <= 0 || - cellWorldSize <= 0f || gravityTexelsPerCell <= 0) - return false; - var expectedExtent = span * cellWorldSize; - var expectedCellFromX = viewportWidth * gravityTexelsPerCell / gravityTextureWidth; - var expectedCellFromY = viewportHeight * gravityTexelsPerCell / gravityTextureHeight; - const float tolerance = 0.0001f; - return Mathf.Abs(viewportWidth - expectedExtent) <= tolerance && - Mathf.Abs(viewportHeight - expectedExtent) <= tolerance && - Mathf.Abs(expectedCellFromX - cellWorldSize) <= tolerance && - Mathf.Abs(expectedCellFromY - cellWorldSize) <= tolerance; - } - - private static EveFieldsSplatsDocument Project( - EveFieldsSplatsDocument source, - Vector2 center, - float width, - float height) => - new EveFieldsSplatsDocument - { - Schema = source.Schema, - FrameId = source.FrameId, - PublishedAtUtc = source.PublishedAtUtc, - SimulationTimeSeconds = source.SimulationTimeSeconds, - RunId = source.RunId, - ZoneIndex = source.ZoneIndex, - ZoneName = source.ZoneName, - Viewport = new EveFieldsViewport - { - MinX = center.x - width * 0.5, - MinY = center.y - height * 0.5, - MaxX = center.x + width * 0.5, - MaxY = center.y + height * 0.5 - }, - Layers = source.Layers, - Splats = source.Splats - }; - - private static string Prop(IReadOnlyDictionary props, string key) => - props != null && props.TryGetValue(key, out var value) ? value ?? "" : ""; - - private static int PositiveInt(IReadOnlyDictionary props, string key) => - int.TryParse(Prop(props, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && value > 0 - ? value - : 0; - - private static float PositiveFloat(IReadOnlyDictionary props, string key) => - float.TryParse(Prop(props, key), NumberStyles.Float, CultureInfo.InvariantCulture, out var value) && - float.IsFinite(value) && value > 0f - ? value - : 0f; - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsViewportFrame.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsViewportFrame.cs.meta deleted file mode 100644 index 9ad030b..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsViewportFrame.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: e9db43b5b96144a3a0df57a791b53547 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs deleted file mode 100644 index 24597aa..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ /dev/null @@ -1,746 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; -using GameCult.Eve.PluginFields; -using UnityEngine; -using UnityEngine.Rendering; -using UnityEngine.Rendering.RenderGraphModule; -using UnityEngine.Rendering.Universal; - -#nullable enable - -namespace GameCult.Eve.UnityScene.Fields -{ - public sealed class EveUnityFieldsVolumeRenderer : MonoBehaviour, IEveUnityRenderPassSource - { - private EveUnityPlayableWorldClientHost? _host; - private IEveUnityFieldsSplatsDocumentSource? _source; - private EveFieldsSplatsDocument? _document; - private EveFieldsSplatLayerRenderer? _layers; - private readonly List _targets = new List(); - private Material? _material; - private Shader? _sourceShader; - private IReadOnlyDictionary? _programMetadata; - private readonly Dictionary _assetTexturesByPort = - new Dictionary(StringComparer.Ordinal); - private readonly Dictionary _cameraRenderStates = - new Dictionary(); - private EveUnityFieldsVolumePass? _renderPass; - private string _activeNodeId = ""; - private long _rasterizedFrameId = -1; - private Vector2 _rasterizedCenter = new Vector2(float.NaN, float.NaN); - - public long PresentedFrameId { get; private set; } = -1; - public int PresentedLayerCount => _targets.Count(target => target.TargetTexture != null); - public long CompositeCount { get; private set; } - public bool UsesTemporalHistory => HasTemporalProgram(); - public Vector2 LastGridCenter { get; private set; } - public static RenderPassEvent CompositionRenderPassEvent => RenderPassEvent.BeforeRenderingPostProcessing; - int IEveUnityRenderPassSource.RenderOrder => 100; - - public void Bind(EveUnityPlayableWorldClientHost host, IEveUnityFieldsSplatsDocumentSource? source) - { - if (host == null) throw new ArgumentNullException(nameof(host)); - if (_source != null) _source.FieldsSplatsAvailable -= OnFieldsSplatsAvailable; - _host = host; - _source = source; - if (_source != null) _source.FieldsSplatsAvailable += OnFieldsSplatsAvailable; - enabled = _source != null; - } - - private void OnEnable() => EveUnityRenderPassRegistry.Register(this); - - private void OnDisable() - { - EveUnityRenderPassRegistry.Unregister(this); - ReleaseRenderState(); - } - - private void OnDestroy() - { - if (_source != null) _source.FieldsSplatsAvailable -= OnFieldsSplatsAvailable; - _source = null; - _host = null; - } - - private void OnFieldsSplatsAvailable(EveFieldsSplatsDocument document) - { - if (document == null || document.FrameId < PresentedFrameId) return; - _document = document; - PresentedFrameId = document.FrameId; - } - - private bool TryRenderLayers( - EveFieldsSplatsDocument document, - Camera camera, - out EveFieldsSplatsDocument projected) - { - projected = document; - var field = ActiveField(); - if (field == null) return false; - EnsureLayerRenderer(); - EnsureTargets(field); - var width = PositiveInt(field, "textureWidth", 512); - var height = PositiveInt(field, "textureHeight", 512); - var snapLayer = Prop(field, "viewportSnapLayer"); - var snapTarget = _targets.FirstOrDefault(target => - string.Equals(target.LayerKey, snapLayer, StringComparison.Ordinal)); - var snapWidth = snapTarget == null - ? 0 - : Mathf.Max(1, Mathf.RoundToInt(width * Mathf.Max(0.01f, snapTarget.WidthScale))); - var snapHeight = snapTarget == null - ? 0 - : Mathf.Max(1, Mathf.RoundToInt(height * Mathf.Max(0.01f, snapTarget.HeightScale))); - if (!EveUnityFieldsViewportFrame.TryResolve( - document, - field.Props, - new Vector2(camera.transform.position.x, camera.transform.position.z), - snapWidth, - snapHeight, - out projected, - out var center)) return false; - if (_rasterizedFrameId != document.FrameId || _rasterizedCenter != center) - { - _layers!.Render(projected, _targets, width, height); - _rasterizedFrameId = document.FrameId; - _rasterizedCenter = center; - } - LastGridCenter = center; - if (!EnsureMaterial(field)) return false; - ApplyFieldBindings(field, projected); - return true; - } - - bool IEveUnityRenderPassSource.TryEnqueuePass(Camera camera, ScriptableRenderer renderer) - { - if (_host == null || _document == null || camera == null || - !CanRenderCamera(_host, camera)) - return false; - _renderPass ??= new EveUnityFieldsVolumePass(this); - renderer.EnqueuePass(_renderPass); - return true; - } - - internal static bool CanRenderCamera(EveUnityPlayableWorldClientHost host, Camera camera) => - camera.cameraType == CameraType.SceneView || host.ActiveCameraTransform == camera.transform; - - private void RenderVolume( - UnsafeCommandBuffer commands, - Camera camera, - TextureHandle colorTarget, - TextureHandle depthTarget) - { - if (_host == null || _document == null || camera == null || - !CanRenderCamera(_host, camera)) - return; - var field = ActiveField(); - if (field == null || !TryRenderLayers(_document, camera, out _)) return; - camera.depthTextureMode |= DepthTextureMode.Depth; - var state = CameraState(camera); - EnsureVolumeTextures(camera, field, state); - if (state.RaymarchTexture == null || !ApplyViewportTextureScaleBindings(field, camera)) return; - ApplyQuality(field, state.HistoryTextureIndex < 0); - - var gpuProjection = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true); - var nonRenderTargetProjection = GL.GetGPUProjectionMatrix(camera.projectionMatrix, false); - var viewProjection = gpuProjection * camera.worldToCameraMatrix; - SetMatrixPort("cameraInverseViewProjection", viewProjection.inverse); - SetMatrixPort("cameraToWorld", CameraTransformToWorld(camera)); - SetVectorPort("cameraProjectionExtents", ProjectionExtents(camera)); - SetFloatPort("raymarchOffset", Halton(Time.frameCount & 1023, 3)); - - var raymarchPass = ProgramPass("raymarch"); - var compositePass = ProgramPass("composite"); - var temporalPass = ProgramPass("temporal"); - var useTemporalHistory = temporalPass >= 0; - var historyWriteIndex = -1; - RenderTexture finalCloudTexture = state.RaymarchTexture; - - if (useTemporalHistory) - { - historyWriteIndex = state.HistoryTextureIndex < 0 ? 0 : (state.HistoryTextureIndex + 1) % state.HistoryTextures.Length; - var historyReadTexture = state.HistoryTextureIndex < 0 - ? state.RaymarchTexture - : state.HistoryTextures[state.HistoryTextureIndex]; - finalCloudTexture = state.HistoryTextures[historyWriteIndex]!; - SetTexturePort("currentSample", state.RaymarchTexture); - SetTexturePort("history", historyReadTexture!); - SetMatrixPort( - "previousViewProjection", - state.HistoryTextureIndex < 0 - ? viewProjection - : ResolvePreviousViewProjection( - _programMetadata, - state.PreviousViewProjection, - gpuProjection, - nonRenderTargetProjection, - state.PreviousView)); - SetFloatPort("resetHistory", state.HistoryTextureIndex < 0 ? 1f : 0f); - } - SetTexturePort("cloud", finalCloudTexture); - - commands.SetRenderTarget(state.RaymarchTexture); - commands.SetViewport(new Rect(0f, 0f, state.RaymarchTexture.width, state.RaymarchTexture.height)); - commands.ClearRenderTarget(false, true, Color.clear); - commands.DrawProcedural(Matrix4x4.identity, _material!, raymarchPass, MeshTopology.Triangles, 3, 1); - if (useTemporalHistory) - { - commands.SetRenderTarget(finalCloudTexture); - commands.SetViewport(new Rect(0f, 0f, finalCloudTexture.width, finalCloudTexture.height)); - commands.ClearRenderTarget(false, true, Color.clear); - commands.DrawProcedural(Matrix4x4.identity, _material!, temporalPass, MeshTopology.Triangles, 3, 1); - } - if (depthTarget.IsValid()) - commands.SetRenderTarget(colorTarget, depthTarget); - else - commands.SetRenderTarget(colorTarget); - commands.SetViewport(camera.pixelRect); - commands.DrawProcedural(Matrix4x4.identity, _material!, compositePass, MeshTopology.Triangles, 3, 1); - if (useTemporalHistory) - { - state.HistoryTextureIndex = historyWriteIndex; - state.PreviousViewProjection = viewProjection; - state.PreviousView = camera.worldToCameraMatrix; - } - CompositeCount++; - } - - private EveUnityFieldVolumeProjection? ActiveField() => - _host?.ActiveWorld?.FieldVolumes.FirstOrDefault(field => - string.Equals(field.DocumentSchema, EveFieldsSchemas.Splats, StringComparison.Ordinal)); - - private bool EnsureMaterial(EveUnityFieldVolumeProjection field) - { - var provider = _host?.NativeAssetProvider; - if (provider == null || string.IsNullOrWhiteSpace(field.MaterialAssetRef)) return false; - var shader = provider.ResolveAsset( - new EveUnityPlayableWorldAssetBinding(field.MaterialAssetRef, "", "provider-asset-ref"), - typeof(Shader)) as Shader; - if (shader == null || !shader.isSupported) return false; - var shaderBinding = new EveUnityPlayableWorldAssetBinding(field.MaterialAssetRef, "", "provider-asset-ref"); - if (provider is not IEveUnityNativeAssetMetadataProvider metadataProvider || - !metadataProvider.TryResolveAssetMetadata(shaderBinding, out var metadata)) - return false; - _programMetadata = metadata; - if (!TryValidateProgramMetadata(metadata, out _)) - return false; - if (_material != null && ReferenceEquals(shader, _sourceShader) && - string.Equals(_activeNodeId, field.NodeId, StringComparison.Ordinal)) return true; - if (_material != null) Destroy(_material); - _sourceShader = shader; - _activeNodeId = field.NodeId; - _material = new Material(shader) { hideFlags = HideFlags.DontSave }; - _assetTexturesByPort.Clear(); - ResetTemporalHistory(); - foreach (var parameter in ParseBindings(Prop(field, "floatParameters"))) - { - if (TryFloat(parameter.Value, out var value)) SetFloatPort(parameter.Key, value); - } - foreach (var parameter in ParseBindings(Prop(field, "vectorParameters"))) - if (TryVector(parameter.Value, out var vector)) SetVectorPort(parameter.Key, vector); - foreach (var binding in ParseBindings(Prop(field, "assetTextureBindings"))) - { - var texture = provider.ResolveAsset( - new EveUnityPlayableWorldAssetBinding(binding.Key, "", "provider-asset-ref"), - typeof(Texture)) as Texture; - if (texture != null) - { - _assetTexturesByPort[binding.Value] = texture; - SetTexturePort(binding.Value, texture); - } - } - foreach (var feature in Prop(field, "features").Split(';')) - { - var logicalFeature = feature.Trim(); - if (!string.IsNullOrWhiteSpace(logicalFeature) && - TryProgramValue($"unity.volume.feature.{logicalFeature}.keyword", out var featureKeyword) && - !string.IsNullOrWhiteSpace(featureKeyword)) - _material.EnableKeyword(featureKeyword); - } - return true; - } - - private void ApplyQuality(EveUnityFieldVolumeProjection field, bool bootstrap) - { - if (_material == null || _programMetadata == null) return; - foreach (var binding in _programMetadata) - { - if (binding.Key.StartsWith("unity.volume.quality.", StringComparison.Ordinal) && - binding.Key.EndsWith(".keyword", StringComparison.Ordinal) && - !string.IsNullOrWhiteSpace(binding.Value)) - _material.DisableKeyword(binding.Value); - } - var quality = ResolveQuality(_programMetadata, Prop(field, "quality"), bootstrap); - if (TryProgramValue($"unity.volume.quality.{quality}.keyword", out var keyword) && - !string.IsNullOrWhiteSpace(keyword)) - _material.EnableKeyword(keyword); - } - - public static string ResolveQuality( - IReadOnlyDictionary? metadata, - string steadyQuality, - bool bootstrap) - { - if (bootstrap && metadata != null && - metadata.TryGetValue("unity.volume.quality.bootstrap", out var bootstrapQuality) && - !string.IsNullOrWhiteSpace(bootstrapQuality)) - return bootstrapQuality.Trim().ToLowerInvariant(); - return (steadyQuality ?? "").Trim().ToLowerInvariant(); - } - - private void ApplyFieldBindings(EveUnityFieldVolumeProjection field, EveFieldsSplatsDocument document) - { - if (_material == null) return; - var bindings = ParseBindings(Prop(field, "layerBindings")); - foreach (var binding in bindings) - { - var target = _targets.FirstOrDefault(value => string.Equals(value.LayerKey, binding.Key, StringComparison.Ordinal)); - if (target?.TargetTexture != null) SetTexturePort(binding.Value, target.TargetTexture); - } - var viewport = document.Viewport; - var width = viewport.MaxX - viewport.MinX; - var height = viewport.MaxY - viewport.MinY; - SetVectorPort("viewportTransform", new Vector4( - (float)((viewport.MinX + viewport.MaxX) * 0.5), - (float)((viewport.MinY + viewport.MaxY) * 0.5), - (float)width, - (float)height)); - foreach (var binding in ParseBindings(Prop(field, "documentFloatBindings"))) - { - if (TryEvaluateDocumentFloatBinding( - document, - binding.Key, - binding.Value, - out var port, - out var value)) - SetFloatPort(port, value); - } - } - - public static bool TryEvaluateDocumentFloatBinding( - EveFieldsSplatsDocument? document, - string source, - string descriptor, - out string port, - out float value) - { - port = ""; - value = 0f; - if (document == null) return false; - var values = (descriptor ?? "").Split(','); - if (values.Length != 3 || - string.IsNullOrWhiteSpace(values[0]) || - !TryFloat(values[1].Trim(), out var scale) || - !TryFloat(values[2].Trim(), out var offset)) - return false; - - double sourceValue; - switch ((source ?? "").Trim()) - { - case "simulationTimeSeconds": - sourceValue = document.SimulationTimeSeconds; - break; - default: - return false; - } - - port = values[0].Trim(); - value = (float)(sourceValue * scale + offset); - return float.IsFinite(value); - } - - private void EnsureTargets(EveUnityFieldVolumeProjection field) - { - var bindings = ParseBindings(Prop(field, "layerBindings")); - var descriptors = ParseBindings(Prop(field, "layerTargetDescriptors")); - if (string.Equals(_activeNodeId, field.NodeId, StringComparison.Ordinal) && - _targets.Count == bindings.Count && - _targets.All(target => bindings.ContainsKey(target.LayerKey))) - { - foreach (var target in _targets) - ApplyLayerTargetDescriptor(target, descriptors); - return; - } - _targets.Clear(); - foreach (var binding in bindings) - { - var target = new EveFieldsSplatLayerTarget { LayerKey = binding.Key }; - ApplyLayerTargetDescriptor(target, descriptors); - _targets.Add(target); - } - } - - private static void ApplyLayerTargetDescriptor( - EveFieldsSplatLayerTarget target, - IReadOnlyDictionary descriptors) - { - target.WidthScale = 1f; - target.HeightScale = 1f; - target.UseMipMaps = false; - target.FilterMode = FilterMode.Bilinear; - if (descriptors.TryGetValue(target.LayerKey, out var descriptor)) - TryApplyLayerTargetDescriptor(target, descriptor); - } - - public static bool TryApplyLayerTargetDescriptor(EveFieldsSplatLayerTarget? target, string descriptor) - { - if (target == null) return false; - var values = (descriptor ?? "").Split(','); - if (values.Length != 4 || - !TryFloat(values[0].Trim(), out var widthScale) || widthScale <= 0 || - !TryFloat(values[1].Trim(), out var heightScale) || heightScale <= 0 || - !bool.TryParse(values[2].Trim(), out var useMipMaps) || - !Enum.TryParse(values[3].Trim(), true, out FilterMode filterMode)) - return false; - target.WidthScale = widthScale; - target.HeightScale = heightScale; - target.UseMipMaps = useMipMaps; - target.FilterMode = filterMode; - return true; - } - - private void EnsureLayerRenderer() - { - if (_layers != null) return; - _layers = GetComponent(); - if (_layers == null) _layers = gameObject.AddComponent(); - } - - private CameraRenderState CameraState(Camera camera) - { - var cameraId = camera.GetInstanceID(); - if (_cameraRenderStates.TryGetValue(cameraId, out var state)) return state; - state = new CameraRenderState(); - _cameraRenderStates.Add(cameraId, state); - return state; - } - - private void EnsureVolumeTextures( - Camera camera, - EveUnityFieldVolumeProjection field, - CameraRenderState state) - { - var downsample = Mathf.Clamp(NonNegativeInt(field, "downsample", 1), 0, 3); - var width = Mathf.Max(1, camera.pixelWidth >> downsample); - var height = Mathf.Max(1, camera.pixelHeight >> downsample); - var resetHistory = EnsureRenderTexture( - ref state.RaymarchTexture, - width, - height, - $"Eve Fields Volume Raymarch {camera.GetInstanceID()}"); - if (HasTemporalProgram()) - { - resetHistory |= EnsureRenderTexture(ref state.HistoryTextures[0], width, height, "Eve Fields Volume History A"); - resetHistory |= EnsureRenderTexture(ref state.HistoryTextures[1], width, height, "Eve Fields Volume History B"); - } - else - { - ReleaseRenderTexture(ref state.HistoryTextures[0]); - ReleaseRenderTexture(ref state.HistoryTextures[1]); - resetHistory = true; - } - if (resetHistory) ResetTemporalHistory(state); - } - - private void ReleaseRenderState() - { - foreach (var state in _cameraRenderStates.Values) - { - ReleaseRenderTexture(ref state.RaymarchTexture); - ReleaseRenderTexture(ref state.HistoryTextures[0]); - ReleaseRenderTexture(ref state.HistoryTextures[1]); - } - _cameraRenderStates.Clear(); - if (_material != null) { Destroy(_material); _material = null; } - _sourceShader = null; - _programMetadata = null; - _assetTexturesByPort.Clear(); - _activeNodeId = ""; - _rasterizedFrameId = -1; - _rasterizedCenter = new Vector2(float.NaN, float.NaN); - LastGridCenter = default; - PresentedFrameId = -1; - CompositeCount = 0; - ResetTemporalHistory(); - } - - private sealed class EveUnityFieldsVolumePass : ScriptableRenderPass - { - private readonly EveUnityFieldsVolumeRenderer _owner; - - private sealed class PassData - { - internal Camera Camera = null!; - internal EveUnityFieldsVolumeRenderer Owner = null!; - internal TextureHandle ColorTarget; - internal TextureHandle DepthTarget; - } - - internal EveUnityFieldsVolumePass(EveUnityFieldsVolumeRenderer owner) - { - _owner = owner; - renderPassEvent = CompositionRenderPassEvent; - ConfigureInput(ScriptableRenderPassInput.Depth); - } - - public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) - { - var cameraData = frameData.Get(); - if (cameraData.camera == null || _owner._host == null || - !CanRenderCamera(_owner._host, cameraData.camera)) - return; - - var resourceData = frameData.Get(); - var colorTarget = resourceData.activeColorTexture; - if (!colorTarget.IsValid()) return; - - using var builder = renderGraph.AddUnsafePass("Eve Fields Volume", out var passData); - passData.Camera = cameraData.camera; - passData.Owner = _owner; - passData.ColorTarget = colorTarget; - passData.DepthTarget = resourceData.activeDepthTexture; - - builder.UseTexture(passData.ColorTarget, AccessFlags.Write); - if (passData.DepthTarget.IsValid()) - builder.UseTexture(passData.DepthTarget, AccessFlags.Read); - builder.AllowPassCulling(false); - builder.SetRenderFunc(static (PassData data, UnsafeGraphContext context) => - data.Owner.RenderVolume(context.cmd, data.Camera, data.ColorTarget, data.DepthTarget)); - } - } - - private void ResetTemporalHistory() - { - foreach (var state in _cameraRenderStates.Values) ResetTemporalHistory(state); - } - - private static void ResetTemporalHistory(CameraRenderState state) - { - state.HistoryTextureIndex = -1; - state.PreviousViewProjection = Matrix4x4.identity; - state.PreviousView = Matrix4x4.identity; - } - - private sealed class CameraRenderState - { - internal RenderTexture? RaymarchTexture; - internal readonly RenderTexture?[] HistoryTextures = new RenderTexture?[2]; - internal int HistoryTextureIndex = -1; - internal Matrix4x4 PreviousViewProjection = Matrix4x4.identity; - internal Matrix4x4 PreviousView = Matrix4x4.identity; - } - - public static Matrix4x4 ResolvePreviousViewProjection( - IReadOnlyDictionary? metadata, - Matrix4x4 previousViewProjection, - Matrix4x4 currentGpuProjection, - Matrix4x4 currentNonRenderTargetProjection, - Matrix4x4 previousView) - { - if (metadata == null || - !metadata.TryGetValue("unity.volume.matrixSemantic.previousViewProjection", out var semantic)) - return previousViewProjection; - if (string.Equals(semantic, "current-projection.previous-view.v1", StringComparison.Ordinal)) - return currentGpuProjection * previousView; - if (string.Equals(semantic, "non-render-target-projection.previous-view.v1", StringComparison.Ordinal)) - return currentNonRenderTargetProjection * previousView; - return previousViewProjection; - } - - private bool HasTemporalProgram() => ProgramPass("temporal") >= 0; - - private bool ApplyViewportTextureScaleBindings(EveUnityFieldVolumeProjection field, Camera camera) - { - foreach (var binding in ParseBindings(Prop(field, "viewportTextureScaleBindings"))) - { - if (string.IsNullOrWhiteSpace(ProgramPort("vector", binding.Key)) || - string.IsNullOrWhiteSpace(ProgramPort("texture", binding.Value)) || - !_assetTexturesByPort.TryGetValue(binding.Value, out var texture) || - !TryComputeViewportTextureScale(camera.pixelWidth, camera.pixelHeight, texture, out var scale)) - return false; - SetVectorPort(binding.Key, scale); - } - return true; - } - - public static bool TryComputeViewportTextureScale( - int viewportWidth, - int viewportHeight, - Texture? texture, - out Vector4 scale) - { - scale = default; - if (texture == null || texture.width <= 0 || texture.height <= 0) return false; - scale = new Vector4( - (float)Math.Max(1, viewportWidth) / texture.width, - (float)Math.Max(1, viewportHeight) / texture.height, - 0f, - 0f); - return true; - } - - private static bool EnsureRenderTexture(ref RenderTexture? texture, int width, int height, string name) - { - if (texture != null && texture.width == width && texture.height == height) return false; - ReleaseRenderTexture(ref texture); - texture = new RenderTexture(width, height, 0, RenderTextureFormat.ARGBFloat) - { - name = name, - filterMode = FilterMode.Bilinear, - wrapMode = TextureWrapMode.Clamp - }; - texture.Create(); - return true; - } - - private static void ReleaseRenderTexture(ref RenderTexture? texture) - { - if (texture == null) return; - texture.Release(); - Destroy(texture); - texture = null; - } - - public static bool TryValidateProgramMetadata( - IReadOnlyDictionary? metadata, - out string error) - { - error = ""; - if (metadata == null) return Fail("Native volume program metadata is missing.", out error); - if (!ValidPass(metadata, "raymarch")) return Fail("Native volume raymarch pass is missing.", out error); - if (!ValidPass(metadata, "composite")) return Fail("Native volume composite pass is missing.", out error); - if (!ValidPort(metadata, "texture", "cloud") || - !ValidPort(metadata, "matrix", "cameraInverseViewProjection") || - !ValidPort(metadata, "vector", "cameraProjectionExtents") || - !ValidPort(metadata, "vector", "viewportTransform") || - !ValidPort(metadata, "float", "raymarchOffset")) - return Fail("Native volume program is missing a required raymarch or composite port.", out error); - - if (!metadata.ContainsKey("unity.volume.pass.temporal")) return true; - if (!ValidPass(metadata, "temporal")) return Fail("Native volume temporal pass is invalid.", out error); - if (!ValidPort(metadata, "texture", "currentSample") || - !ValidPort(metadata, "texture", "history") || - !ValidPort(metadata, "matrix", "previousViewProjection") || - !ValidPort(metadata, "float", "resetHistory")) - return Fail("Native volume temporal pass is missing a required history port.", out error); - if (metadata.TryGetValue("unity.volume.matrixSemantic.previousViewProjection", out var semantic) && - !string.Equals(semantic, "previous-view-projection.v1", StringComparison.Ordinal) && - !string.Equals(semantic, "current-projection.previous-view.v1", StringComparison.Ordinal) && - !string.Equals(semantic, "non-render-target-projection.previous-view.v1", StringComparison.Ordinal)) - return Fail("Native volume temporal pass has an unsupported previous-view-projection semantic.", out error); - if (metadata.TryGetValue("unity.volume.quality.bootstrap", out var bootstrapQuality) && - (!metadata.TryGetValue( - $"unity.volume.quality.{bootstrapQuality.Trim().ToLowerInvariant()}.keyword", - out var bootstrapKeyword) || string.IsNullOrWhiteSpace(bootstrapKeyword))) - return Fail("Native volume temporal pass has no keyword for its bootstrap quality.", out error); - return true; - } - - private static bool ValidPass(IReadOnlyDictionary metadata, string pass) => - metadata.TryGetValue($"unity.volume.pass.{pass}", out var value) && - int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) && parsed >= 0; - - private static bool ValidPort(IReadOnlyDictionary metadata, string kind, string port) => - metadata.TryGetValue($"unity.volume.{kind}Port.{port}", out var value) && - !string.IsNullOrWhiteSpace(value); - - private static bool Fail(string message, out string error) - { - error = message; - return false; - } - - private void SetMatrixPort(string port, Matrix4x4 value) - { - var property = ProgramPort("matrix", port); - if (!string.IsNullOrWhiteSpace(property)) _material!.SetMatrix(property, value); - } - - private void SetVectorPort(string port, Vector4 value) - { - var property = ProgramPort("vector", port); - if (!string.IsNullOrWhiteSpace(property)) _material!.SetVector(property, value); - } - - private void SetFloatPort(string port, float value) - { - var property = ProgramPort("float", port); - if (!string.IsNullOrWhiteSpace(property)) _material!.SetFloat(property, value); - } - - private void SetTexturePort(string port, Texture value) - { - var property = ProgramPort("texture", port); - if (!string.IsNullOrWhiteSpace(property)) _material!.SetTexture(property, value); - } - - private string ProgramPort(string kind, string port) => - TryProgramValue($"unity.volume.{kind}Port.{port}", out var value) ? value : ""; - - private int ProgramPass(string pass) => - TryProgramValue($"unity.volume.pass.{pass}", out var value) && - int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) && parsed >= 0 - ? parsed - : -1; - - private bool TryProgramValue(string key, out string value) - { - value = ""; - return _programMetadata != null && _programMetadata.TryGetValue(key, out value!); - } - - private static Vector4 ProjectionExtents(Camera camera) - { - var y = camera.orthographic ? camera.orthographicSize : Mathf.Tan(0.5f * Mathf.Deg2Rad * camera.fieldOfView); - return new Vector4(y * camera.aspect, y, 0f, 0f); - } - - private static Matrix4x4 CameraTransformToWorld(Camera camera) => - camera.transform.localToWorldMatrix; - - private static float Halton(int index, int radix) - { - var result = 0f; - var fraction = 1f / radix; - while (index > 0) { result += (index % radix) * fraction; index /= radix; fraction /= radix; } - return result; - } - - private static Dictionary ParseBindings(string value) - { - var result = new Dictionary(StringComparer.Ordinal); - foreach (var pair in (value ?? "").Split(';')) - { - var separator = pair.IndexOf('='); - if (separator <= 0 || separator >= pair.Length - 1) continue; - result[pair.Substring(0, separator).Trim()] = pair.Substring(separator + 1).Trim(); - } - return result; - } - - private static string Prop(EveUnityFieldVolumeProjection field, string key) => - field.Props.TryGetValue(key, out var value) ? value ?? "" : ""; - - private static int PositiveInt(EveUnityFieldVolumeProjection field, string key, int fallback) => - int.TryParse(Prop(field, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && value > 0 ? value : fallback; - - private static int NonNegativeInt(EveUnityFieldVolumeProjection field, string key, int fallback) => - int.TryParse(Prop(field, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && value >= 0 ? value : fallback; - - private static bool TryFloat(string value, out float result) => - float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out result) && !float.IsNaN(result) && !float.IsInfinity(result); - - private static bool TryVector(string value, out Vector4 result) - { - result = default; - var values = (value ?? "").Split(','); - if (values.Length < 2 || values.Length > 4) return false; - var parsed = new float[4]; - for (var index = 0; index < values.Length; index++) - if (!TryFloat(values[index], out parsed[index])) return false; - result = new Vector4(parsed[0], parsed[1], parsed[2], parsed[3]); - return true; - } - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs.meta deleted file mode 100644 index bf9a46e..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 9c5d41ca3a3a4335b1b82104bba3e20a -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/GameCult.Eve.UnityScene.asmdef b/packages/org.gamecult.eve.unity-scene/Runtime/GameCult.Eve.UnityScene.asmdef index 6f91767..784339a 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/GameCult.Eve.UnityScene.asmdef +++ b/packages/org.gamecult.eve.unity-scene/Runtime/GameCult.Eve.UnityScene.asmdef @@ -4,10 +4,7 @@ "references": [ "GameCult.Eve.Surface", "GameCult.Eve.PluginFields", - "GameCult.Eve.UnityUIToolkit", - "Unity.Mathematics", - "Unity.RenderPipelines.Core.Runtime", - "Unity.RenderPipelines.Universal.Runtime" + "Unity.Mathematics" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposure.compute b/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposure.compute deleted file mode 100644 index f1e1f27..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposure.compute +++ /dev/null @@ -1,98 +0,0 @@ -#pragma kernel ClearHistogram -#pragma kernel BuildHistogram -#pragma kernel CalculateFixedExposure -#pragma kernel CalculateProgressiveExposure - -RWStructuredBuffer _Histogram; -Texture2D _Source; -SamplerState sampler_LinearClamp; -Texture2D _PreviousExposure; -RWTexture2D _DestinationExposure; - -float4 _HistogramScaleOffsetResolution; -float4 _ExposureFilterAndBrightness; -float4 _ExposureAdaptation; - -groupshared uint GroupHistogram[128]; - -[numthreads(64, 1, 1)] -void ClearHistogram(uint threadId : SV_DispatchThreadID) -{ - if (threadId < 128) _Histogram[threadId] = 0; -} - -[numthreads(16, 16, 1)] -void BuildHistogram(uint2 dispatchId : SV_DispatchThreadID, uint2 groupId : SV_GroupThreadID) -{ - uint localId = groupId.y * 16 + groupId.x; - if (localId < 128) GroupHistogram[localId] = 0; - GroupMemoryBarrierWithGroupSync(); - - float2 pixel = float2(dispatchId) * 2.0; - if (pixel.x < _HistogramScaleOffsetResolution.z && pixel.y < _HistogramScaleOffsetResolution.w) - { - float2 uv = pixel / _HistogramScaleOffsetResolution.zw; - float2 distanceFromCenter = abs(uv - 0.5.xx); - float vignette = saturate(1.0 - dot(distanceFromCenter, distanceFromCenter)); - uint weight = (uint)(64.0 * vignette * vignette); - float3 color = _Source.SampleLevel(sampler_LinearClamp, uv, 0).rgb; - float luminance = max(1e-6, dot(color, float3(0.2126729, 0.7151522, 0.0721750))); - float normalizedLogLuminance = saturate( - log2(luminance) * _HistogramScaleOffsetResolution.x + _HistogramScaleOffsetResolution.y); - uint bin = (uint)(normalizedLogLuminance * 127.0); - InterlockedAdd(GroupHistogram[bin], weight); - } - - GroupMemoryBarrierWithGroupSync(); - if (localId < 128) InterlockedAdd(_Histogram[localId], GroupHistogram[localId]); -} - -float LuminanceForBin(uint bin) -{ - float normalized = (float)bin / 128.0; - return exp2((normalized - _HistogramScaleOffsetResolution.y) / _HistogramScaleOffsetResolution.x); -} - -float CalculateExposure() -{ - float total = 0.0; - for (uint index = 0; index < 128; index++) total += (float)_Histogram[index]; - - float discardDark = total * _ExposureFilterAndBrightness.x; - float includeThrough = total * _ExposureFilterAndBrightness.y; - float weightedLuminance = 0.0; - float includedWeight = 0.0; - for (uint index = 0; index < 128; index++) - { - float count = (float)_Histogram[index]; - float darkOffset = min(discardDark, count); - count -= darkOffset; - discardDark -= darkOffset; - includeThrough -= darkOffset; - count = min(includeThrough, count); - includeThrough -= count; - weightedLuminance += LuminanceForBin(index) * count; - includedWeight += count; - } - - float average = weightedLuminance / max(includedWeight, 1e-6); - average = clamp(average, _ExposureFilterAndBrightness.z, _ExposureFilterAndBrightness.w); - return _ExposureAdaptation.z / max(average, 1e-6); -} - -[numthreads(1, 1, 1)] -void CalculateFixedExposure(uint3 id : SV_DispatchThreadID) -{ - _DestinationExposure[uint2(0, 0)] = CalculateExposure(); -} - -[numthreads(1, 1, 1)] -void CalculateProgressiveExposure(uint3 id : SV_DispatchThreadID) -{ - float nextExposure = CalculateExposure(); - float previousExposure = _PreviousExposure[uint2(0, 0)]; - float delta = nextExposure - previousExposure; - float speed = delta > 0.0 ? _ExposureAdaptation.x : _ExposureAdaptation.y; - _DestinationExposure[uint2(0, 0)] = previousExposure + - delta * (1.0 - exp2(-_ExposureAdaptation.w * speed)); -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposureMultiply.shader b/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposureMultiply.shader deleted file mode 100644 index d54ded1..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposureMultiply.shader +++ /dev/null @@ -1,39 +0,0 @@ -Shader "Hidden/EveUnity/AdaptiveExposureMultiply" -{ - SubShader - { - Pass - { - Cull Off ZWrite Off ZTest Always - Blend DstColor Zero - ColorMask RGB - - HLSLPROGRAM - #pragma target 4.5 - #pragma vertex Vert - #pragma fragment Frag - - Texture2D _ExposureTexture; - - struct Varyings - { - float4 position : SV_POSITION; - }; - - Varyings Vert(uint vertexId : SV_VertexID) - { - float2 uv = float2((vertexId << 1) & 2, vertexId & 2); - Varyings output; - output.position = float4(uv * 2.0 - 1.0, 0.0, 1.0); - return output; - } - - float4 Frag(Varyings input) : SV_Target - { - float exposure = _ExposureTexture.Load(int3(0, 0, 0)); - return exposure.xxxx; - } - ENDHLSL - } - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposureMultiply.shader.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposureMultiply.shader.meta deleted file mode 100644 index daf9128..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/AdaptiveExposureMultiply.shader.meta +++ /dev/null @@ -1,9 +0,0 @@ -fileFormatVersion: 2 -guid: 48315c6017f548e7a4b46698b3d7229e -ShaderImporter: - externalObjects: {} - defaultTextures: [] - nonModifiableTextures: [] - userData: - assetBundleName: - assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/UnityDefaultRuntimeTheme.tss b/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/UnityDefaultRuntimeTheme.tss deleted file mode 100644 index 51924cb..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/UnityDefaultRuntimeTheme.tss +++ /dev/null @@ -1 +0,0 @@ -@import url("unity-theme://default"); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/UnityDefaultRuntimeTheme.tss.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/UnityDefaultRuntimeTheme.tss.meta deleted file mode 100644 index b26f42d..0000000 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Resources/EveUnity/UnityDefaultRuntimeTheme.tss.meta +++ /dev/null @@ -1,12 +0,0 @@ -fileFormatVersion: 2 -guid: 0f30b970a2764f51ba064b032906259e -ScriptedImporter: - internalIDToNameTable: [] - externalObjects: {} - serializedVersion: 2 - userData: - assetBundleName: - assetBundleVariant: - script: {fileID: 12388, guid: 0000000000000000e000000000000000, type: 0} - disableValidation: 0 - unsupportedSelectorAction: 0 diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveCultMeshSurfaceSelectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveCultMeshSurfaceSelectionTests.cs new file mode 100644 index 0000000..d4b2189 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveCultMeshSurfaceSelectionTests.cs @@ -0,0 +1,135 @@ +using System; +using GameCult.Eve.Surface; +using NUnit.Framework; + +namespace GameCult.Eve.UnityScene.Tests +{ + public sealed class EveCultMeshSurfaceSelectionTests + { + [Test] + public void Select_MatchesDeveloperIntentWithoutPhysicalTopology() + { + var provider = Provider( + new EveAdvertisedSurface( + "aetheria.pilot", + EveSurfaceDocument.SchemaId, + "eve:surface:aetheria.pilot", + "cultmesh-record", + "active", + "interactive-world", + new EveWorldInteractionAdvertisement( + "world3d", + Array.Empty(), + "provider-command", + "eve:commands", + EveCommandReceiptDocument.SchemaId, + "eve:receipts", + "eve:assets", + new[] { "unity-scene", "web" }, + "provider"))); + + var selected = EveCultMeshSurfaceSelection.Select( + new[] { provider }, + new EveSurfaceRequest + { + ProviderId = "aetheria.daemon", + SurfaceKind = "interactive-world", + LoweringTarget = "unity-scene" + }); + + Assert.That(selected.Provider, Is.SameAs(provider)); + Assert.That(selected.Surface.SurfaceId, Is.EqualTo("aetheria.pilot")); + Assert.That(selected.Surface.RecordRef, Is.EqualTo("eve:surface:aetheria.pilot")); + } + + [Test] + public void Select_RejectsUnsupportedLoweringTarget() + { + var provider = Provider(new EveAdvertisedSurface( + "aetheria.pilot", + EveSurfaceDocument.SchemaId, + "eve:surface:aetheria.pilot", + "cultmesh-record", + "active", + "interactive-world", + new EveWorldInteractionAdvertisement( + "world3d", Array.Empty(), "", "", "", "", "", + new[] { "unity-scene" }, "provider"))); + + Assert.Throws(() => EveCultMeshSurfaceSelection.Select( + new[] { provider }, + new EveSurfaceRequest { LoweringTarget = "flutter" })); + } + + [Test] + public void ResolvePlugins_KeepsIndependentRequiredAndOptionalNestedCapabilities() + { + var surface = new EveAdvertisedSurface( + "sai.stage", EveSurfaceDocument.SchemaId, "eve:surface:sai", "cultmesh-record", "active", "interface", + requiresPlugins: new[] + { + new EvePluginRequirement("sai.vn", "^0.1.0", "required", new[] { "vn.stage", "story.choose" }, Array.Empty()), + new EvePluginRequirement("norn.graph", "^0.1.0", "optional-nested", Array.Empty(), new[] { "embed.norn" }), + new EvePluginRequirement("tex.math", "^0.1.0", "optional-nested", Array.Empty(), new[] { "embed.tex" }) + }); + var sai = Plugin("sai.vn", new[] { "vn.stage" }, new[] { "story.choose" }); + var norn = Plugin("norn.graph", new[] { "embed.norn" }, Array.Empty()); + + var selected = EveCultMeshSurfaceSelection.ResolvePlugins(surface, new[] { sai, norn }); + + Assert.That(selected, Is.EqualTo(new[] { sai, norn })); + } + + [Test] + public void ResolvePlugins_RejectsMissingRequiredCapability() + { + var surface = new EveAdvertisedSurface( + "sai.stage", EveSurfaceDocument.SchemaId, "eve:surface:sai", "cultmesh-record", "active", "interface", + requiresPlugins: new[] + { + new EvePluginRequirement("sai.vn", "^0.1.0", "required", new[] { "story.choose" }, Array.Empty()) + }); + + Assert.Throws(() => + EveCultMeshSurfaceSelection.ResolvePlugins( + surface, + new[] { Plugin("sai.vn", new[] { "vn.stage" }, Array.Empty()) })); + } + + private static EveProviderAdvertisementDocument Provider(EveAdvertisedSurface surface) => + new EveProviderAdvertisementDocument( + "aetheria.daemon", + "aetheria", + "aetheria", + "Aetheria", + "game", + "aetheria.daemon", + "2026-07-12T00:00:00Z", + new EveProviderFreshness("fresh", "2026-07-12T00:00:00Z", 5000), + Array.Empty(), + Array.Empty(), + new[] { surface }, + Array.Empty()); + + private static EvePluginAdvertisementDocument Plugin( + string pluginId, + string[] components, + string[] commands) => + new EvePluginAdvertisementDocument( + pluginId, + pluginId.Split('.')[0], + "0.1.0", + "cultmesh://plugins/" + pluginId, + new EvePluginRuntimeAdvertisement( + "sidecar", + "gamecult.eve.plugin_abi.v1", + new[] { "cultmesh" }, + new[] { "plugin" }, + new EvePluginSidecarAdvertisement( + "daemon", "cultmesh", "request", "response", Array.Empty(), "command", "receipt", "plugin")), + Array.Empty(), + components, + commands, + Array.Empty()); + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveCultMeshSurfaceSelectionTests.cs.meta b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveCultMeshSurfaceSelectionTests.cs.meta new file mode 100644 index 0000000..c22c711 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveCultMeshSurfaceSelectionTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c578fb24db4b41fc88ea08bde4c42dd4 diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index 33130a0..f3e130f 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -2,129 +2,15 @@ using System; using System.IO; using System.Linq; using System.Collections.Generic; -using System.Reflection; using GameCult.Eve.PluginFields; using GameCult.Eve.UnityScene.Fields; using NUnit.Framework; using UnityEngine; -using UnityEngine.Rendering.Universal; namespace GameCult.Eve.UnityScene.Tests { public sealed class EveFieldsUnitySceneProjectionTests { - [Test] - public void VolumeCameraToWorldPreservesUnityShaderPositiveZForward() - { - var cameraObject = new GameObject("eve-volume-camera-matrix"); - try - { - var camera = cameraObject.AddComponent(); - camera.transform.SetPositionAndRotation( - new Vector3(12f, -3f, 8f), - Quaternion.Euler(17f, 43f, -9f)); - var method = typeof(EveUnityFieldsVolumeRenderer).GetMethod( - "CameraTransformToWorld", - BindingFlags.NonPublic | BindingFlags.Static); - - Assert.That(method, Is.Not.Null); - var matrix = (Matrix4x4)method!.Invoke(null, new object[] { camera }); - Assert.That(Vector3.Dot( - matrix.MultiplyVector(Vector3.forward).normalized, - camera.transform.forward), Is.EqualTo(1f).Within(0.00001f)); - Assert.That(Vector3.Distance( - matrix.MultiplyPoint3x4(Vector3.zero), - camera.transform.position), Is.LessThan(0.00001f)); - } - finally - { - UnityEngine.Object.DestroyImmediate(cameraObject); - } - } - - [Test] - public void AdaptiveExposureMatchesFossilCompensationAndProgressiveDirection() - { - var exposureMethod = typeof(EveUnityAdaptiveExposureRenderer).GetMethod( - "ExposureForAverageLuminance", - BindingFlags.NonPublic | BindingFlags.Static); - var adaptationMethod = typeof(EveUnityAdaptiveExposureRenderer).GetMethod( - "AdaptExposure", - BindingFlags.NonPublic | BindingFlags.Static); - - Assert.That(exposureMethod, Is.Not.Null); - Assert.That(adaptationMethod, Is.Not.Null); - var brightBound = Mathf.Pow(2f, 0.3f); - var exposure = (float)exposureMethod!.Invoke(null, new object[] { brightBound, 0.5f }); - Assert.That(exposure, Is.EqualTo(0.5f / brightBound).Within(0.000001f)); - - var darkening = (float)adaptationMethod!.Invoke( - null, - new object[] { 1f, 0.25f, 0.5f, 2f, 1f }); - var brightening = (float)adaptationMethod.Invoke( - null, - new object[] { 1f, 4f, 0.5f, 2f, 1f }); - Assert.That(darkening, Is.EqualTo(0.625f).Within(0.000001f)); - Assert.That(brightening, Is.EqualTo(1f + 3f * (1f - Mathf.Pow(2f, -0.5f))).Within(0.000001f)); - } - - [Test] - public void AdaptiveExposureGpuHistogramClampsBrightFrameToAdvertisedMaximumEv() - { - Assert.That(SystemInfo.supportsComputeShaders, Is.True); - var compute = Resources.Load("EveUnity/AdaptiveExposure"); - Assert.That(compute, Is.Not.Null); - var source = new Texture2D(32, 32, TextureFormat.RGBAFloat, false, true); - var pixels = Enumerable.Repeat(new Color(16f, 16f, 16f, 1f), 32 * 32).ToArray(); - source.SetPixels(pixels); - source.Apply(false, false); - using var histogram = new ComputeBuffer(128, sizeof(uint), ComputeBufferType.Structured); - var exposure = new RenderTexture(1, 1, 0, RenderTextureFormat.RFloat, RenderTextureReadWrite.Linear) - { - enableRandomWrite = true - }; - exposure.Create(); - try - { - var clear = compute!.FindKernel("ClearHistogram"); - compute.SetBuffer(clear, "_Histogram", histogram); - compute.Dispatch(clear, 2, 1, 1); - - var build = compute.FindKernel("BuildHistogram"); - compute.SetBuffer(build, "_Histogram", histogram); - compute.SetTexture(build, "_Source", source); - compute.SetVector("_HistogramScaleOffsetResolution", new Vector4(1f / 18f, 0.5f, 32f, 32f)); - compute.Dispatch(build, 1, 1, 1); - - var calculate = compute.FindKernel("CalculateFixedExposure"); - compute.SetBuffer(calculate, "_Histogram", histogram); - compute.SetVector("_ExposureFilterAndBrightness", new Vector4( - 0.4737294f, - 0.99f, - Mathf.Pow(2f, -3f), - Mathf.Pow(2f, 0.3f))); - compute.SetVector("_ExposureAdaptation", new Vector4(1f, 2f, 0.5f, 1f / 60f)); - compute.SetTexture(calculate, "_DestinationExposure", exposure); - compute.Dispatch(calculate, 1, 1, 1); - - var previous = RenderTexture.active; - RenderTexture.active = exposure; - var readback = new Texture2D(1, 1, TextureFormat.RFloat, false, true); - readback.ReadPixels(new Rect(0, 0, 1, 1), 0, 0); - readback.Apply(); - RenderTexture.active = previous; - var actual = readback.GetPixel(0, 0).r; - UnityEngine.Object.DestroyImmediate(readback); - Assert.That(actual, Is.EqualTo(0.5f / Mathf.Pow(2f, 0.3f)).Within(0.001f)); - } - finally - { - exposure.Release(); - UnityEngine.Object.DestroyImmediate(exposure); - UnityEngine.Object.DestroyImmediate(source); - } - } - [Test] public void RasterizerConsumesPluginOwnedDocumentContract() { @@ -137,425 +23,6 @@ namespace GameCult.Eve.UnityScene.Tests assembly.Name?.Contains("Aetheria", StringComparison.OrdinalIgnoreCase) == true), Is.False); } - [Test] - public void CanonicalFieldsPackageOwnsTheCultMeshWireDocument() - { - Assert.That(typeof(EveFieldsSplatsDocument).GetCustomAttributes(false) - .Any(attribute => attribute.GetType().Name == "CultDocumentAttribute"), Is.True); - Assert.That(new EveFieldsSplatsDocument().Schema, Is.EqualTo(EveFieldsSchemas.Splats)); - Assert.That(typeof(EveFieldsSplatsDocument).Assembly.GetReferencedAssemblies() - .Any(assembly => assembly.Name?.Contains("Aetheria", StringComparison.OrdinalIgnoreCase) == true), Is.False); - } - - [Test] - public void VolumeRendererConsumesCanonicalFieldsWithoutProviderCode() - { - Assert.That(typeof(EveUnityFieldsVolumeRenderer).Assembly.GetReferencedAssemblies() - .Any(assembly => assembly.Name?.Contains("Aetheria", StringComparison.OrdinalIgnoreCase) == true), Is.False); - Assert.That(typeof(IEveUnityFieldsSplatsDocumentSource).GetEvent("FieldsSplatsAvailable"), Is.Not.Null); - } - - [Test] - public void UrpRendererFeatureOwnsGenericWorldEffectScheduling() - { - Assert.That(typeof(EveUnityRendererFeature).IsSubclassOf(typeof(ScriptableRendererFeature)), Is.True); - var supportsCamera = typeof(EveUnityRendererFeature).GetMethod( - "SupportsCamera", - BindingFlags.NonPublic | BindingFlags.Static); - Assert.That(supportsCamera, Is.Not.Null); - Assert.That(supportsCamera!.Invoke(null, new object[] { CameraType.Game }), Is.True); - Assert.That(supportsCamera.Invoke(null, new object[] { CameraType.SceneView }), Is.True); - Assert.That(supportsCamera.Invoke(null, new object[] { CameraType.Preview }), Is.False); - AssertRenderOrder(typeof(EveUnityFieldsVolumeRenderer), 100); - AssertRenderOrder(typeof(EveUnityFieldsParticleRenderer), 200); - AssertRenderOrder(typeof(EveUnityAdaptiveExposureRenderer), 300); - } - - [Test] - public void VolumeRendererCompositesBeforeProviderPostProcessing() - { - Assert.That( - EveUnityFieldsVolumeRenderer.CompositionRenderPassEvent, - Is.EqualTo(RenderPassEvent.BeforeRenderingPostProcessing)); - } - - private static void AssertRenderOrder(Type sourceType, int expected) - { - var schedulingInterface = sourceType.GetInterfaces().Single(type => - string.Equals(type.Name, "IEveUnityRenderPassSource", StringComparison.Ordinal)); - var property = schedulingInterface.GetProperty("RenderOrder"); - Assert.That(property, Is.Not.Null); - var gameObject = new GameObject("eve-render-pass-order-test"); - try - { - var component = gameObject.AddComponent(sourceType); - Assert.That(property!.GetValue(component), Is.EqualTo(expected)); - } - finally - { - UnityEngine.Object.DestroyImmediate(gameObject); - } - } - - [Test] - public void ParticleRendererExecutesProviderProgramWithoutProviderCode() - { - Assert.That(typeof(EveUnityFieldsParticleRenderer).Assembly.GetReferencedAssemblies() - .Any(assembly => assembly.Name?.Contains("Aetheria", StringComparison.OrdinalIgnoreCase) == true), Is.False); - Assert.That(EveUnityFieldsParticleRenderer.CompositionRenderPassEvent, - Is.EqualTo(RenderPassEvent.BeforeRenderingPostProcessing)); - } - - [TestCase(11f, 6f)] - [TestCase(-11f, -6f)] - [TestCase(5.999f, 0f)] - public void FieldViewportFrameSnapsToAnIntegralGravityPixelMultiple(float coordinate, float expected) - { - Assert.That(EveUnityFieldsViewportFrame.SnapCameraCoordinate( - coordinate, - 1536f, - 2048, - 8f), Is.EqualTo(expected).Within(0.0001f)); - } - - [Test] - public void FieldViewportFrameAcceptsAlignedGravityAndParticleLattices() - { - Assert.That(EveUnityFieldsViewportFrame.TryValidateSpatialLattice( - 1536f, 1536f, 2048, 2048, 256, 6f, 8), Is.True); - } - - [Test] - public void FieldViewportFrameRejectsAliasedGravityAndParticleLattices() - { - Assert.That(EveUnityFieldsViewportFrame.TryValidateSpatialLattice( - 2000f, 2000f, 2048, 2048, 256, 6f, 8), Is.False); - } - - [Test] - public void FogAndParticlesResolveTheSameSnappedWorldFrame() - { - var document = new EveFieldsSplatsDocument - { - FrameId = 7, - Viewport = new EveFieldsViewport - { - MinX = -768, - MinY = -768, - MaxX = 768, - MaxY = 768 - } - }; - var contract = new Dictionary(StringComparer.Ordinal) - { - ["viewportAnchor"] = "active-camera.xz", - ["span"] = "256", - ["cellWorldSize"] = "6", - ["viewportSnapTexels"] = "8" - }; - - Assert.That(EveUnityFieldsViewportFrame.TryResolve( - document, contract, new Vector2(11f, -11f), 2048, 2048, - out var fog, out var fogCenter), Is.True); - Assert.That(EveUnityFieldsViewportFrame.TryResolve( - document, contract, new Vector2(11f, -11f), 2048, 2048, - out var particles, out var particleCenter), Is.True); - - Assert.That(fogCenter, Is.EqualTo(new Vector2(6f, -6f))); - Assert.That(particleCenter, Is.EqualTo(fogCenter)); - Assert.That(particles.Viewport.MinX, Is.EqualTo(fog.Viewport.MinX)); - Assert.That(particles.Viewport.MinY, Is.EqualTo(fog.Viewport.MinY)); - Assert.That(particles.Viewport.MaxX, Is.EqualTo(fog.Viewport.MaxX)); - Assert.That(particles.Viewport.MaxY, Is.EqualTo(fog.Viewport.MaxY)); - } - - [Test] - public void ParticleRendererFailsClosedWithoutCompleteNativeAbi() - { - var compute = RequiredParticleComputeMetadata(); - var material = RequiredParticleMaterialMetadata(); - Assert.That(EveUnityFieldsParticleRenderer.TryValidateProgramMetadata(compute, material, out var error), - Is.True, error); - - compute.Remove("unity.particles.intPort.span"); - - Assert.That(EveUnityFieldsParticleRenderer.TryValidateProgramMetadata(compute, material, out error), - Is.False); - Assert.That(error, Does.Contain("logical port")); - } - - [Test] - public void ParticleMaterialAcceptsAdvertisedTemporalDitherInputs() - { - var field = new EveUnityFieldParticlesProjection( - "particles", "document", EveFieldsSchemas.Splats, "compute", "material", "world.transparent", - new Dictionary(StringComparer.Ordinal) - { - ["materialAssetTextureBindings"] = "texture.noise=dither", - ["materialViewportTextureScaleBindings"] = "ditherCoordinates=dither", - ["materialRenderFrameIndexPort"] = "frameIndex" - }); - var metadata = RequiredParticleMaterialMetadata(); - metadata["unity.particles.texturePort.dither"] = "_DitheringTex"; - metadata["unity.particles.vectorPort.ditherCoordinates"] = "_DitheringCoords"; - metadata["unity.particles.intPort.frameIndex"] = "_FrameNumber"; - - Assert.That(EveUnityFieldsParticleRenderer.TryValidateMaterialPresentationContract( - field, metadata, out var error), Is.True, error); - } - - [Test] - public void ParticleMaterialRejectsPartialTemporalDitherInputs() - { - var field = new EveUnityFieldParticlesProjection( - "particles", "document", EveFieldsSchemas.Splats, "compute", "material", "world.transparent", - new Dictionary(StringComparer.Ordinal) - { - ["materialAssetTextureBindings"] = "texture.noise=dither", - ["materialViewportTextureScaleBindings"] = "ditherCoordinates=dither", - ["materialRenderFrameIndexPort"] = "frameIndex" - }); - var metadata = RequiredParticleMaterialMetadata(); - metadata["unity.particles.texturePort.dither"] = "_DitheringTex"; - metadata["unity.particles.vectorPort.ditherCoordinates"] = "_DitheringCoords"; - - Assert.That(EveUnityFieldsParticleRenderer.TryValidateMaterialPresentationContract( - field, metadata, out var error), Is.False); - Assert.That(error, Does.Contain("render-frame")); - } - - [Test] - public void ParticleRendererPreservesRepeatedDocumentSourcesForDistinctPorts() - { - var document = new EveFieldsSplatsDocument { SimulationTimeSeconds = 12.5 }; - - Assert.That(EveUnityFieldsParticleRenderer.TryEvaluateDocumentFloatBindings( - document, - "simulationTimeSeconds=time,1,0;simulationTimeSeconds=flowScroll,0.025,0", - out var bindings), Is.True); - Assert.That(bindings.Count, Is.EqualTo(2)); - Assert.That(bindings[0].Key, Is.EqualTo("time")); - Assert.That(bindings[0].Value, Is.EqualTo(12.5f)); - Assert.That(bindings[1].Key, Is.EqualTo("flowScroll")); - Assert.That(bindings[1].Value, Is.EqualTo(0.3125f)); - } - - [Test] - public void VolumeProgramAcceptsPortableTwoPassLifecycle() - { - var metadata = RequiredVolumeProgramMetadata(); - - Assert.That(EveUnityFieldsVolumeRenderer.TryValidateProgramMetadata(metadata, out var error), Is.True, error); - } - - [Test] - public void VolumeProgramAcceptsCompleteTemporalLifecycle() - { - var metadata = RequiredVolumeProgramMetadata(); - metadata["unity.volume.pass.temporal"] = "1"; - metadata["unity.volume.texturePort.currentSample"] = "_CurrentVolume"; - metadata["unity.volume.texturePort.history"] = "_HistoryVolume"; - metadata["unity.volume.matrixPort.previousViewProjection"] = "_PreviousViewProjection"; - metadata["unity.volume.floatPort.resetHistory"] = "_ResetHistory"; - - Assert.That(EveUnityFieldsVolumeRenderer.TryValidateProgramMetadata(metadata, out var error), Is.True, error); - } - - [Test] - public void VolumeRendererUsesCurrentProjectionWithPreviousViewWhenAdvertised() - { - var metadata = RequiredVolumeProgramMetadata(); - metadata["unity.volume.matrixSemantic.previousViewProjection"] = - "current-projection.previous-view.v1"; - var previousViewProjection = Matrix4x4.Translate(new Vector3(1f, 2f, 3f)); - var currentProjection = Matrix4x4.Scale(new Vector3(2f, 3f, 4f)); - var previousView = Matrix4x4.Rotate(Quaternion.Euler(10f, 20f, 30f)); - - var resolved = EveUnityFieldsVolumeRenderer.ResolvePreviousViewProjection( - metadata, - previousViewProjection, - currentProjection, - Matrix4x4.identity, - previousView); - - Assert.That(resolved, Is.EqualTo(currentProjection * previousView)); - Assert.That(resolved, Is.Not.EqualTo(previousViewProjection)); - } - - [Test] - public void VolumeRendererDefaultsToTruePreviousViewProjection() - { - var previousViewProjection = Matrix4x4.Translate(new Vector3(1f, 2f, 3f)); - - var resolved = EveUnityFieldsVolumeRenderer.ResolvePreviousViewProjection( - RequiredVolumeProgramMetadata(), - previousViewProjection, - Matrix4x4.Scale(Vector3.one * 2f), - Matrix4x4.identity, - Matrix4x4.Rotate(Quaternion.Euler(10f, 20f, 30f))); - - Assert.That(resolved, Is.EqualTo(previousViewProjection)); - } - - [Test] - public void VolumeRendererUsesNonRenderTargetProjectionWhenAdvertised() - { - var metadata = RequiredVolumeProgramMetadata(); - metadata["unity.volume.matrixSemantic.previousViewProjection"] = - "non-render-target-projection.previous-view.v1"; - var previousViewProjection = Matrix4x4.Translate(new Vector3(1f, 2f, 3f)); - var renderTargetProjection = Matrix4x4.Scale(new Vector3(2f, -3f, 4f)); - var nonRenderTargetProjection = Matrix4x4.Scale(new Vector3(2f, 3f, 4f)); - var previousView = Matrix4x4.Rotate(Quaternion.Euler(10f, 20f, 30f)); - - var resolved = EveUnityFieldsVolumeRenderer.ResolvePreviousViewProjection( - metadata, - previousViewProjection, - renderTargetProjection, - nonRenderTargetProjection, - previousView); - - Assert.That(resolved, Is.EqualTo(nonRenderTargetProjection * previousView)); - Assert.That(resolved, Is.Not.EqualTo(renderTargetProjection * previousView)); - } - - [Test] - public void VolumeProgramRejectsUnknownPreviousViewProjectionSemantic() - { - var metadata = RequiredVolumeProgramMetadata(); - metadata["unity.volume.pass.temporal"] = "1"; - metadata["unity.volume.texturePort.currentSample"] = "_CurrentVolume"; - metadata["unity.volume.texturePort.history"] = "_HistoryVolume"; - metadata["unity.volume.matrixPort.previousViewProjection"] = "_PreviousViewProjection"; - metadata["unity.volume.floatPort.resetHistory"] = "_ResetHistory"; - metadata["unity.volume.matrixSemantic.previousViewProjection"] = "made-up"; - - Assert.That(EveUnityFieldsVolumeRenderer.TryValidateProgramMetadata(metadata, out var error), Is.False); - Assert.That(error, Does.Contain("previous-view-projection semantic")); - } - - [Test] - public void VolumeRendererUsesAdvertisedBootstrapQualityOnlyForEmptyHistory() - { - var metadata = RequiredVolumeProgramMetadata(); - metadata["unity.volume.quality.bootstrap"] = "ultra"; - metadata["unity.volume.quality.ultra.keyword"] = "ULTRA_QUALITY"; - - Assert.That(EveUnityFieldsVolumeRenderer.ResolveQuality(metadata, "high", true), Is.EqualTo("ultra")); - Assert.That(EveUnityFieldsVolumeRenderer.ResolveQuality(metadata, "high", false), Is.EqualTo("high")); - } - - [Test] - public void VolumeProgramRejectsBootstrapQualityWithoutAdvertisedKeyword() - { - var metadata = RequiredVolumeProgramMetadata(); - metadata["unity.volume.pass.temporal"] = "1"; - metadata["unity.volume.texturePort.currentSample"] = "_CurrentVolume"; - metadata["unity.volume.texturePort.history"] = "_HistoryVolume"; - metadata["unity.volume.matrixPort.previousViewProjection"] = "_PreviousViewProjection"; - metadata["unity.volume.floatPort.resetHistory"] = "_ResetHistory"; - metadata["unity.volume.quality.bootstrap"] = "ultra"; - - Assert.That(EveUnityFieldsVolumeRenderer.TryValidateProgramMetadata(metadata, out var error), Is.False); - Assert.That(error, Does.Contain("bootstrap quality")); - } - - [Test] - public void VolumeProgramRejectsPartialTemporalLifecycle() - { - var metadata = RequiredVolumeProgramMetadata(); - metadata["unity.volume.pass.temporal"] = "1"; - metadata["unity.volume.texturePort.currentSample"] = "_CurrentVolume"; - - Assert.That(EveUnityFieldsVolumeRenderer.TryValidateProgramMetadata(metadata, out var error), Is.False); - Assert.That(error, Does.Contain("history port")); - } - - [Test] - public void VolumeRendererDerivesViewportTextureScaleFromNativeDimensions() - { - var texture = new Texture2D(128, 64, TextureFormat.RGBA32, false); - try - { - Assert.That(EveUnityFieldsVolumeRenderer.TryComputeViewportTextureScale( - 640, - 360, - texture, - out var scale), Is.True); - Assert.That(scale, Is.EqualTo(new Vector4(5f, 5.625f, 0f, 0f))); - } - finally - { - UnityEngine.Object.DestroyImmediate(texture); - } - } - - [Test] - public void VolumeRendererAppliesProviderAdvertisedLayerTargetShape() - { - var target = new EveFieldsSplatLayerTarget { LayerKey = "fog.tint" }; - - Assert.That( - EveUnityFieldsVolumeRenderer.TryApplyLayerTargetDescriptor( - target, - "0.5,0.25,true,trilinear"), - Is.True); - Assert.That(target.WidthScale, Is.EqualTo(0.5f)); - Assert.That(target.HeightScale, Is.EqualTo(0.25f)); - Assert.That(target.UseMipMaps, Is.True); - Assert.That(target.FilterMode, Is.EqualTo(FilterMode.Trilinear)); - } - - [Test] - public void VolumeRendererRejectsMalformedLayerTargetShape() - { - var target = new EveFieldsSplatLayerTarget - { - WidthScale = 2f, - HeightScale = 2f, - UseMipMaps = true, - FilterMode = FilterMode.Trilinear - }; - - Assert.That( - EveUnityFieldsVolumeRenderer.TryApplyLayerTargetDescriptor(target, "0,1,true,bilinear"), - Is.False); - Assert.That(target.WidthScale, Is.EqualTo(2f)); - Assert.That(target.HeightScale, Is.EqualTo(2f)); - Assert.That(target.UseMipMaps, Is.True); - Assert.That(target.FilterMode, Is.EqualTo(FilterMode.Trilinear)); - } - - [Test] - public void VolumeRendererBindsAuthoritativeDocumentTimeToLogicalPort() - { - var document = new EveFieldsSplatsDocument { SimulationTimeSeconds = 12.5 }; - - Assert.That( - EveUnityFieldsVolumeRenderer.TryEvaluateDocumentFloatBinding( - document, - "simulationTimeSeconds", - "flowScroll,0.025,-0.1", - out var port, - out var value), - Is.True); - Assert.That(port, Is.EqualTo("flowScroll")); - Assert.That(value, Is.EqualTo(0.2125f).Within(0.00001f)); - } - - [Test] - public void VolumeRendererRejectsUnknownDocumentFloatSource() - { - Assert.That( - EveUnityFieldsVolumeRenderer.TryEvaluateDocumentFloatBinding( - new EveFieldsSplatsDocument(), - "unityTime", - "flowScroll,1,0", - out _, - out _), - Is.False); - } - [Test] public void PackageContainsGenericFieldsShader() { @@ -563,30 +30,6 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(File.Exists("Packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveFieldsSplatsCore.hlsl"), Is.True); } - private static Dictionary RequiredParticleComputeMetadata() => - new Dictionary(StringComparer.Ordinal) - { - ["unity.particles.kernel.update"] = "UpdateParticles", - ["unity.particles.bufferPort.particles"] = "particles", - ["unity.particles.texturePort.surfaceHeight"] = "_SurfaceHeight", - ["unity.particles.texturePort.tint"] = "_Tint", - ["unity.particles.texturePort.hue"] = "_Hue", - ["unity.particles.vectorPort.viewportTransform"] = "_GridTransform", - ["unity.particles.vectorPort.timeVector"] = "_Time", - ["unity.particles.floatPort.time"] = "time", - ["unity.particles.floatPort.period"] = "period", - ["unity.particles.floatPort.spacing"] = "spacing", - ["unity.particles.intPort.span"] = "span" - }; - - private static Dictionary RequiredParticleMaterialMetadata() => - new Dictionary(StringComparer.Ordinal) - { - ["unity.particles.pass.render"] = "0", - ["unity.particles.bufferPort.particles"] = "particles", - ["unity.particles.bufferPort.quadPoints"] = "quadPoints" - }; - [Test] public void LayerRendererConsumesPluginContractsWithoutProviderBindings() { @@ -606,7 +49,7 @@ namespace GameCult.Eve.UnityScene.Tests var previous = RenderTexture.active; RenderTexture.active = output; - var readback = new Texture2D(64, 64, TextureFormat.RGBA32, false, true); + var readback = new Texture2D(64, 64, TextureFormat.RGBAFloat, false, true); readback.ReadPixels(new Rect(0, 0, 64, 64), 0, 0); readback.Apply(); RenderTexture.active = previous; @@ -619,185 +62,6 @@ namespace GameCult.Eve.UnityScene.Tests UnityEngine.Object.DestroyImmediate(host); } - [Test] - public void RasterizerLowersPortablePowerPulseParameters() - { - var document = new EveFieldsSplatsDocument - { - Viewport = new EveFieldsViewport { MinX = -1, MinY = -1, MaxX = 1, MaxY = 1 }, - Splats = new EveFieldsSplatSoa - { - Count = 1, - CenterX = new[] { 0d }, - CenterY = new[] { 0d }, - HalfExtentX = new[] { 1d }, - HalfExtentY = new[] { 1d }, - RotationCos = new[] { 1d }, - RotationSin = new[] { 0d }, - Channel = new[] { 0 }, - Falloff = new[] { EveFieldsSplatFalloffs.PowerPulse }, - FalloffScale = new[] { 2d }, - FalloffExponent = new[] { 1d }, - ValueR = new[] { 1d }, - ValueA = new[] { 1d }, - LayerIndex = new[] { 0 }, - SourceKind = new[] { EveFieldsSplatSourceKinds.Constant } - } - }; - var host = new GameObject("eve-fields-power-pulse"); - var rasterizer = host.AddComponent(); - var output = rasterizer.Render(document, 128, 128); - var previous = RenderTexture.active; - RenderTexture.active = output; - var readback = new Texture2D(128, 128, TextureFormat.RGBAFloat, false, true); - readback.ReadPixels(new Rect(0, 0, 128, 128), 0, 0); - readback.Apply(); - RenderTexture.active = previous; - - Assert.That(readback.GetPixel(64, 64).r, Is.GreaterThan(0.95f)); - Assert.That(readback.GetPixel(80, 64).r, Is.InRange(0.65f, 0.85f)); - Assert.That(readback.GetPixel(96, 64).r, Is.LessThan(0.02f)); - - UnityEngine.Object.DestroyImmediate(readback); - UnityEngine.Object.DestroyImmediate(host); - } - - [Test] - public void RasterizerAnchorsAnimatedSimplexToFieldWorldAndPublishedSimulationTime() - { - var first = ProceduralDocument( - 0, 100, 50, EveFieldsSplatSourceKinds.AnimatedSimplexNoise, 2); - var shifted = ProceduralDocument( - 25, 125, 75, EveFieldsSplatSourceKinds.AnimatedSimplexNoise, 2); - var later = ProceduralDocument( - 0, 100, 50, EveFieldsSplatSourceKinds.AnimatedSimplexNoise, 7); - - var firstValue = RenderFieldPixel(first, 64, 64); - var shiftedValue = RenderFieldPixel(shifted, 32, 64); - var laterValue = RenderFieldPixel(later, 64, 64); - - Assert.That(shiftedValue, Is.EqualTo(firstValue).Within(0.015f), - "the same field-world coordinate must not swim when the viewport moves"); - Assert.That(Mathf.Abs(laterValue - firstValue), Is.GreaterThan(0.02f), - "animation must use the document's simulation time rather than Unity wall time"); - } - - [Test] - public void RasterizerLowersPortableAnimatedCellNoiseB() - { - var first = ProceduralDocument( - 0, 100, 50, EveFieldsSplatSourceKinds.AnimatedCellNoiseB, 2); - var later = ProceduralDocument( - 0, 100, 50, EveFieldsSplatSourceKinds.AnimatedCellNoiseB, 11); - - var firstValue = RenderFieldPixel(first, 57, 71); - var laterValue = RenderFieldPixel(later, 57, 71); - - Assert.That(firstValue, Is.GreaterThanOrEqualTo(0)); - Assert.That(Mathf.Abs(laterValue - firstValue), Is.GreaterThan(0.005f)); - } - - [Test] - public void RasterizerLowersPortableAnimatedRadialCosine() - { - var initial = RadialCosineDocument(0); - var halfTurnLater = RadialCosineDocument(Math.PI); - - var center = RenderFieldPixel(initial, 64, 64); - var radial = RenderFieldPixel(initial, 80, 64); - var laterCenter = RenderFieldPixel(halfTurnLater, 64, 64); - var localX = (80.5f / 128f) * 2f - 1f; - var localY = (64.5f / 128f) * 2f - 1f; - var expected = Mathf.Cos(Mathf.Pow(Mathf.Sqrt(localX * localX + localY * localY), 1.25f) * Mathf.PI * 2f); - - Assert.That(center, Is.GreaterThan(0.98f)); - Assert.That(radial, Is.EqualTo(expected).Within(0.04f)); - Assert.That(laterCenter, Is.LessThan(-0.98f), - "radial phase animation must use provider-published simulation time"); - } - - private static EveFieldsSplatsDocument RadialCosineDocument(double simulationTime) - { - return new EveFieldsSplatsDocument - { - SimulationTimeSeconds = simulationTime, - Viewport = new EveFieldsViewport { MinX = -1, MinY = -1, MaxX = 1, MaxY = 1 }, - Splats = new EveFieldsSplatSoa - { - Count = 1, - CenterX = new[] { 0d }, - CenterY = new[] { 0d }, - HalfExtentX = new[] { 1d }, - HalfExtentY = new[] { 1d }, - RotationCos = new[] { 1d }, - RotationSin = new[] { 0d }, - Channel = new[] { 0 }, - Falloff = new[] { EveFieldsSplatFalloffs.Solid }, - ValueR = new[] { 1d }, - ValueA = new[] { 1d }, - LayerIndex = new[] { 0 }, - SourceKind = new[] { EveFieldsSplatSourceKinds.AnimatedRadialCosine }, - FrequencyX = new[] { Math.PI * 2d }, - FrequencyY = new[] { 1.25d }, - PhaseX = new[] { 0d }, - AnimationSpeed = new[] { 1d } - } - }; - } - - private static EveFieldsSplatsDocument ProceduralDocument( - double min, - double max, - double center, - int sourceKind, - double simulationTime) - { - return new EveFieldsSplatsDocument - { - SimulationTimeSeconds = simulationTime, - Viewport = new EveFieldsViewport { MinX = min, MinY = 0, MaxX = max, MaxY = 100 }, - Splats = new EveFieldsSplatSoa - { - Count = 1, - CenterX = new[] { center }, - CenterY = new[] { 50d }, - HalfExtentX = new[] { 50d }, - HalfExtentY = new[] { 50d }, - RotationCos = new[] { 1d }, - RotationSin = new[] { 0d }, - Channel = new[] { 0 }, - Falloff = new[] { EveFieldsSplatFalloffs.Solid }, - ValueR = new[] { 1d }, - ValueA = new[] { 1d }, - LayerIndex = new[] { 0 }, - SourceKind = new[] { sourceKind }, - FrequencyX = new[] { 0.037d }, - FrequencyY = new[] { 0.041d }, - PhaseX = new[] { 0.17d }, - PhaseY = new[] { -0.23d }, - AnimationSpeed = new[] { 0.31d }, - SourceFlags = new[] { (double)EveFieldsSplatSourceFlags.AbsoluteValue } - } - }; - } - - private static float RenderFieldPixel(EveFieldsSplatsDocument document, int x, int y) - { - var host = new GameObject("eve-fields-procedural-capture"); - var rasterizer = host.AddComponent(); - var output = rasterizer.Render(document, 128, 128); - var previous = RenderTexture.active; - RenderTexture.active = output; - var readback = new Texture2D(128, 128, TextureFormat.RGBAFloat, false, true); - readback.ReadPixels(new Rect(0, 0, 128, 128), 0, 0); - readback.Apply(); - RenderTexture.active = previous; - var value = readback.GetPixel(x, y).r; - UnityEngine.Object.DestroyImmediate(readback); - UnityEngine.Object.DestroyImmediate(host); - return value; - } - private sealed class CaptureDocument : IEveFieldsSplatsDocument { public string Schema => EveFieldsSchemas.Splats; @@ -808,18 +72,6 @@ namespace GameCult.Eve.UnityScene.Tests public IEveFieldsSplatSoa Splats { get; } = new CaptureSplats(); } - private static Dictionary RequiredVolumeProgramMetadata() => - new Dictionary(StringComparer.Ordinal) - { - ["unity.volume.pass.raymarch"] = "0", - ["unity.volume.pass.composite"] = "2", - ["unity.volume.texturePort.cloud"] = "_Cloud", - ["unity.volume.matrixPort.cameraInverseViewProjection"] = "_CameraInverseViewProjection", - ["unity.volume.vectorPort.cameraProjectionExtents"] = "_CameraProjectionExtents", - ["unity.volume.vectorPort.viewportTransform"] = "_ViewportTransform", - ["unity.volume.floatPort.raymarchOffset"] = "_RaymarchOffset" - }; - private sealed class CaptureViewport : IEveFieldsViewport { public double MinX => -1; @@ -855,8 +107,6 @@ namespace GameCult.Eve.UnityScene.Tests public IReadOnlyList PhaseY => Zero; public IReadOnlyList AnimationSpeed => Zero; public IReadOnlyList SourceFlags => Zero; - public IReadOnlyList FalloffScale => One; - public IReadOnlyList FalloffExponent => One; } } } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAdvertisedInputActionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAdvertisedInputActionTests.cs deleted file mode 100644 index f4d4190..0000000 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAdvertisedInputActionTests.cs +++ /dev/null @@ -1,222 +0,0 @@ -using System; -using System.Collections.Generic; -using GameCult.Eve.Surface; -using NUnit.Framework; - -#nullable enable - -namespace GameCult.Eve.UnityScene.Tests -{ - public sealed class EveUnityAdvertisedInputActionTests - { - [Test] - public void HeldValueOverridesAdvertisedPayloadWithoutMutatingCapability() - { - var sourcePayload = new Dictionary(StringComparer.Ordinal) - { - ["scalarValue"] = "0" - }; - var action = EveUnityAdvertisedInputAction.Resolve(new EveInputCapabilityDocument - { - Actions = new[] - { - new EveInputActionDocument - { - ActionId = "pilot.scoop", - Operation = "aetheria.daemon.commands.SetTractorPower", - Availability = "available", - Payload = sourcePayload, - InputValue = new EveInputValueDocument - { - Model = EveUnityAdvertisedInputAction.ButtonHoldValueModel, - PayloadKey = "scalarValue" - } - } - } - }, "pilot.scoop"); - - var pressed = action.BuildPayload("entity.4", 1f); - var released = action.BuildPayload("entity.4", 0f); - - Assert.That(action.IsButtonHold, Is.True); - Assert.That(pressed["scalarValue"], Is.EqualTo("1")); - Assert.That(released["scalarValue"], Is.EqualTo("0")); - Assert.That(pressed["entityId"], Is.EqualTo("entity.4")); - Assert.That(pressed["actionId"], Is.EqualTo("pilot.scoop")); - Assert.That(sourcePayload["scalarValue"], Is.EqualTo("0")); - } - - [Test] - public void ValueFailsClosedWhenActionDoesNotAdvertiseValueContract() - { - var action = EveUnityAdvertisedInputAction.Resolve(new EveInputCapabilityDocument - { - Actions = new[] - { - new EveInputActionDocument - { - ActionId = "pilot.dock", - Operation = "aetheria.daemon.commands.DockNearest", - Availability = "available" - } - } - }, "pilot.dock"); - - Assert.Throws(() => action.BuildPayload("entity.4", 1f)); - } - - [Test] - public void UnavailableActionCanOnlyBeResolvedForReleaseCleanup() - { - var capability = new EveInputCapabilityDocument - { - Actions = new[] - { - new EveInputActionDocument - { - ActionId = "pilot.scoop", - Operation = "provider.commands.SetPower", - Availability = "unavailable", - InputValue = new EveInputValueDocument - { - Model = EveUnityAdvertisedInputAction.ButtonHoldValueModel, - PayloadKey = "active" - } - } - } - }; - - Assert.Throws(() => - EveUnityAdvertisedInputAction.Resolve(capability, "pilot.scoop")); - Assert.DoesNotThrow(() => - EveUnityAdvertisedInputAction.Resolve(capability, "pilot.scoop", requireAvailable: false) - .BuildPayload("entity.4", 0)); - } - - [Test] - public void ViewDirectionUsesAdvertisedKeysAndNormalizesWithoutMutatingCapability() - { - var sourcePayload = new Dictionary(StringComparer.Ordinal) - { - ["mode"] = "reticle" - }; - var action = EveUnityAdvertisedInputAction.Resolve(new EveInputCapabilityDocument - { - Actions = new[] - { - new EveInputActionDocument - { - ActionId = "pilot.target-reticle", - Operation = "provider.commands.TargetReticle", - Availability = "available", - Payload = sourcePayload, - InputValue = new EveInputValueDocument - { - Model = EveUnityAdvertisedInputAction.ViewDirectionValueModel, - PayloadKeys = new[] { "rayX", "rayY", "rayZ" } - } - } - } - }, "pilot.target-reticle"); - - var payload = action.BuildViewDirectionPayload("entity.4", 3f, 4f, 0f); - - Assert.That(action.IsViewDirection, Is.True); - Assert.That(payload["entityId"], Is.EqualTo("entity.4")); - Assert.That(payload["actionId"], Is.EqualTo("pilot.target-reticle")); - Assert.That(payload["mode"], Is.EqualTo("reticle")); - Assert.That(float.Parse(payload["rayX"], System.Globalization.CultureInfo.InvariantCulture), Is.EqualTo(0.6f).Within(0.000001f)); - Assert.That(float.Parse(payload["rayY"], System.Globalization.CultureInfo.InvariantCulture), Is.EqualTo(0.8f).Within(0.000001f)); - Assert.That(float.Parse(payload["rayZ"], System.Globalization.CultureInfo.InvariantCulture), Is.EqualTo(0f).Within(0.000001f)); - Assert.That(sourcePayload.ContainsKey("rayX"), Is.False); - Assert.Throws(() => action.BuildPayload("entity.4", 1f)); - } - - [Test] - public void ViewDirectionFailsClosedForMalformedKeysAndZeroVector() - { - var action = EveUnityAdvertisedInputAction.Resolve(new EveInputCapabilityDocument - { - Actions = new[] - { - new EveInputActionDocument - { - ActionId = "pilot.target-reticle", - Operation = "provider.commands.TargetReticle", - Availability = "available", - InputValue = new EveInputValueDocument - { - Model = EveUnityAdvertisedInputAction.ViewDirectionValueModel, - PayloadKeys = new[] { "direction", "direction", "direction" } - } - } - } - }, "pilot.target-reticle"); - - Assert.Throws(() => action.BuildViewDirectionPayload("entity.4", 0f, 0f, 1f)); - action = EveUnityAdvertisedInputAction.Resolve(new EveInputCapabilityDocument - { - Actions = new[] - { - new EveInputActionDocument - { - ActionId = "pilot.target-reticle", - Operation = "provider.commands.TargetReticle", - Availability = "available", - InputValue = new EveInputValueDocument - { - Model = EveUnityAdvertisedInputAction.ViewDirectionValueModel, - PayloadKeys = new[] { "x", "y", "z" } - } - } - } - }, "pilot.target-reticle"); - Assert.Throws(() => action.BuildViewDirectionPayload("entity.4", 0f, 0f, 0f)); - } - - [Test] - public void ScalarUsesAdvertisedStateAndRangeWithoutMutatingCapability() - { - var sourcePayload = new Dictionary(StringComparer.Ordinal) - { - ["scalarValue"] = "300", - ["equipmentIndex"] = "2" - }; - var action = EveUnityAdvertisedInputAction.Resolve(new EveInputCapabilityDocument - { - Actions = new[] - { - new EveInputActionDocument - { - ActionId = "equipment.2.temperature", - Operation = "provider.commands.SetTemperature", - Availability = "available", - Payload = sourcePayload, - InputValue = new EveInputValueDocument - { - Model = EveUnityAdvertisedInputAction.ScalarValueModel, - PayloadKey = "scalarValue", - CurrentValue = 300, - MinimumValue = 100, - MaximumValue = 600, - StepValue = 5, - Unit = "kelvin" - } - } - } - }, "equipment.2.temperature"); - - var payload = action.BuildScalarPayload("entity.4", 425.5); - - Assert.That(action.IsScalar, Is.True); - Assert.That(action.CurrentValue, Is.EqualTo(300)); - Assert.That(action.Unit, Is.EqualTo("kelvin")); - Assert.That(payload["scalarValue"], Is.EqualTo("425.5")); - Assert.That(payload["equipmentIndex"], Is.EqualTo("2")); - Assert.That(sourcePayload["scalarValue"], Is.EqualTo("300")); - Assert.Throws(() => action.BuildScalarPayload("entity.4", 99)); - Assert.Throws(() => action.BuildScalarPayload("entity.4", 601)); - Assert.Throws(() => action.BuildScalarPayload("entity.4", double.NaN)); - } - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAdvertisedInputActionTests.cs.meta b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAdvertisedInputActionTests.cs.meta deleted file mode 100644 index 5f325ab..0000000 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAdvertisedInputActionTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: a60138c42bf44c2ba38309b21a886c93 diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAimPresentationTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAimPresentationTests.cs deleted file mode 100644 index fbff51e..0000000 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAimPresentationTests.cs +++ /dev/null @@ -1,57 +0,0 @@ -using System; -using System.Collections.Generic; -using NUnit.Framework; -using UnityEngine; - -#nullable enable - -namespace GameCult.Eve.UnityScene.Tests -{ - public sealed class EveUnityAimPresentationTests - { - [Test] - public void ProjectionPreservesProviderAimConvergenceSemantic() - { - var projection = new EveUnitySceneProjection( - "provider", "surface", "world", "commands", "receipt", "provider", null, - Node("root", "surface", Node("aim", "aim.presentation", new Dictionary - { - ["controlledEntityId"] = "run.zone.0.entity.4", - ["convergenceTargetEntityId"] = "run.zone.0.entity.9", - ["viewDotRole"] = "aim.marker.view-direction", - ["minimumConvergenceDistance"] = "50", - ["viewDotRadius"] = "0.8" - }))); - - var aim = EveUnityAimPresentation.Find(projection); - - Assert.That(aim, Is.Not.Null); - Assert.That(aim!.ControlledEntityId, Is.EqualTo("run.zone.0.entity.4")); - Assert.That(aim.ConvergenceTargetEntityId, Is.EqualTo("run.zone.0.entity.9")); - Assert.That(aim.MinimumConvergenceDistance, Is.EqualTo(50f)); - Assert.That(aim.ViewDotRadius, Is.EqualTo(0.8f)); - } - - [Test] - public void PlanarYawProducesPortableUnitLookDirection() - { - var forward = EveUnityPlayableWorldLookDirection.FromPlanarYaw(0f); - var right = EveUnityPlayableWorldLookDirection.FromPlanarYaw(Mathf.PI * 0.5f); - - Assert.That(forward.DirectionX, Is.EqualTo(0f).Within(0.0001f)); - Assert.That(forward.DirectionZ, Is.EqualTo(1f).Within(0.0001f)); - Assert.That(right.DirectionX, Is.EqualTo(1f).Within(0.0001f)); - Assert.That(right.DirectionZ, Is.EqualTo(0f).Within(0.0001f)); - } - - private static EveUnitySceneNode Node(string id, string kind, params EveUnitySceneNode[] children) => - Node(id, kind, Empty(), children); - - private static EveUnitySceneNode Node(string id, string kind, IReadOnlyDictionary props, params EveUnitySceneNode[] children) => - new EveUnitySceneNode(id, kind, "scene-node", props, Empty(), Empty(), 0, 0, - Array.Empty(), null, children); - - private static IReadOnlyDictionary Empty() => - new Dictionary(StringComparer.Ordinal); - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAimPresentationTests.cs.meta b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAimPresentationTests.cs.meta deleted file mode 100644 index c2b439c..0000000 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityAimPresentationTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 2bcc030e48554a49accc4efc105c36b4 diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs deleted file mode 100644 index 08186a3..0000000 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using NUnit.Framework; -using UnityEngine; - -#nullable enable - -namespace GameCult.Eve.UnityScene.Tests -{ - public sealed class EveUnityBeamPresentationTests - { - [Test] - public void ProjectionPreservesProviderBeamSourceAssetAndFossilShape() - { - var beam = EveUnityBeamPresentation.FindAll(Projection(Node( - "tractor", "beam.presentation", new Dictionary(StringComparer.Ordinal) - { - ["sourceEntityId"] = "run.zone.0.entity.4", - ["assetRole"] = "effect.beam.tractor", - ["directionMode"] = "source-forward.v1", - ["renderChannel"] = "world.effects", - ["activationActionId"] = "pilot.scoop", - ["power"] = "0.625", - ["activationThreshold"] = "0.01", - ["radius"] = "25", - ["maximumDistance"] = "75" - }))).Single(); - - Assert.That(beam.SourceEntityId, Is.EqualTo("run.zone.0.entity.4")); - Assert.That(beam.AssetRole, Is.EqualTo("effect.beam.tractor")); - Assert.That(beam.ActivationActionId, Is.EqualTo("pilot.scoop")); - Assert.That(beam.UsesSourceForward, Is.True); - Assert.That(beam.Power, Is.EqualTo(0.625f).Within(0.0001f)); - Assert.That(beam.Radius, Is.EqualTo(25f)); - Assert.That(beam.MaximumDistance, Is.EqualTo(75f)); - } - - [Test] - public void RendererReconcilesProviderPrefabAgainstPresentedSourceWithoutPhysics() - { - var host = new GameObject("beam-test-host"); - var source = new GameObject("beam-test-source"); - var prefab = new GameObject("beam-test-prefab"); - prefab.AddComponent(); - var renderer = host.AddComponent(); - var registry = new Registry("source", source.transform); - var provider = new Provider(prefab); - try - { - renderer.Apply(new[] { Presentation(0.5f) }, registry, provider); - - Assert.That(provider.LastAssetRef, Is.EqualTo("effect.beam.tractor")); - Assert.That(provider.LastRole, Is.EqualTo("effect.beam.tractor")); - Assert.That(renderer.ActiveBeamCount, Is.EqualTo(1)); - Assert.That(renderer.TryGetPower("tractor", out var power), Is.True); - Assert.That(power, Is.EqualTo(0.5f).Within(0.0001f)); - var instance = source.transform.GetChild(0).gameObject; - Assert.That(instance.GetComponent().emission.rateOverTimeMultiplier, - Is.EqualTo(0.5f).Within(0.0001f)); - Assert.That(instance.GetComponentsInChildren(true), Is.Empty); - Assert.That(instance.GetComponentsInChildren(true), Is.Empty); - - renderer.Apply(new[] { Presentation(0.005f) }, registry, provider); - Assert.That(renderer.TryGetPower("tractor", out power), Is.True); - Assert.That(power, Is.Zero); - - renderer.Apply(Array.Empty(), registry, provider); - Assert.That(renderer.ActiveBeamCount, Is.Zero); - } - finally - { - UnityEngine.Object.DestroyImmediate(host); - UnityEngine.Object.DestroyImmediate(source); - UnityEngine.Object.DestroyImmediate(prefab); - } - } - - [Test] - public void RendererReadsDaemonPowerFromThePresentedEntityGeneration() - { - var host = new GameObject("beam-state-test-host"); - var source = new GameObject("beam-state-test-source"); - var prefab = new GameObject("beam-state-test-prefab"); - prefab.AddComponent(); - var renderer = host.AddComponent(); - var registry = new Registry( - "source", - source.transform, - new Dictionary(StringComparer.Ordinal) { ["effect.beam.power"] = 0.75f }); - try - { - var presentation = new EveUnityBeamPresentation(Node( - "tractor", "beam.presentation", new Dictionary(StringComparer.Ordinal) - { - ["sourceEntityId"] = "source", - ["assetRole"] = "effect.beam.tractor", - ["directionMode"] = "source-forward.v1", - ["power"] = "0.125", - ["powerStateSemantic"] = "effect.beam.power", - ["activationThreshold"] = "0.01" - })); - - renderer.Apply(new[] { presentation }, registry, new Provider(prefab)); - - Assert.That(renderer.TryGetPower("tractor", out var power), Is.True); - Assert.That(power, Is.EqualTo(0.75f).Within(0.0001f)); - } - finally - { - UnityEngine.Object.DestroyImmediate(host); - UnityEngine.Object.DestroyImmediate(source); - UnityEngine.Object.DestroyImmediate(prefab); - } - } - - private static EveUnityBeamPresentation Presentation(float power) => new EveUnityBeamPresentation(Node( - "tractor", "beam.presentation", new Dictionary(StringComparer.Ordinal) - { - ["sourceEntityId"] = "source", - ["assetRole"] = "effect.beam.tractor", - ["directionMode"] = "source-forward.v1", - ["power"] = power.ToString(System.Globalization.CultureInfo.InvariantCulture), - ["activationThreshold"] = "0.01" - })); - - private static EveUnitySceneProjection Projection(params EveUnitySceneNode[] children) => - new EveUnitySceneProjection("provider", "surface", "world", "commands", "receipt", "provider", null, - Node("root", "surface", Empty(), children)); - - private static EveUnitySceneNode Node( - string id, - string kind, - IReadOnlyDictionary props, - params EveUnitySceneNode[] children) => - new EveUnitySceneNode(id, kind, "scene-node", props, Empty(), Empty(), 0, 0, - Array.Empty(), null, children); - - private static IReadOnlyDictionary Empty() => - new Dictionary(StringComparer.Ordinal); - - private sealed class Registry : IEveUnityPresentedEntityRegistry - { - private readonly EveUnityPresentedEntityHandle _handle; - public Registry( - string entityId, - Transform transform, - IReadOnlyDictionary? scalarState = null) - { - _handle = new EveUnityPresentedEntityHandle( - new EveUnityPresentedEntity(0, entityId, "ship", "", "", Vector3.zero, 0, Vector3.zero, - 1, 1, 0, 0, true, true, "", scalarState), - transform); - } - public EveUnityPresentedEntityGeneration? CurrentGeneration => null; - public bool TryGetByEntityId(string entityId, out EveUnityPresentedEntityHandle handle) - { - handle = _handle; - return string.Equals(entityId, _handle.Entity.EntityId, StringComparison.Ordinal); - } - public bool TryGetBySourceIndex(int sourceIndex, out EveUnityPresentedEntityHandle handle) - { - handle = _handle; - return sourceIndex == _handle.Entity.SourceIndex; - } - } - - private sealed class Provider : IEveUnityGameObjectAssetProvider - { - private readonly GameObject _prefab; - public Provider(GameObject prefab) { _prefab = prefab; } - public string LastAssetRef { get; private set; } = ""; - public string LastRole { get; private set; } = ""; - public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) - { - LastAssetRef = asset.AssetRef; - LastRole = asset.EntityKind; - return _prefab; - } - } - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs.meta b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs.meta deleted file mode 100644 index 84e1c67..0000000 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityBeamPresentationTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 7cd6b9cdacbecf14593d6ca2e0044849 \ No newline at end of file diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs deleted file mode 100644 index 8e4bc25..0000000 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using NUnit.Framework; -using UnityEngine; - -#nullable enable - -namespace GameCult.Eve.UnityScene.Tests -{ - public sealed class EveUnityCombatPresentationTests - { - [Test] - public void ProjectionPreservesProviderSelectionLockMetersAndFossilTiming() - { - var projection = Projection(Combat()); - var combat = EveUnityCombatPresentation.Find(projection); - - Assert.That(combat, Is.Not.Null); - Assert.That(combat!.ControlledEntityIndex, Is.EqualTo(4)); - Assert.That(combat.SelectedTargetEntityId, Is.EqualTo("run.zone.0.entity.9")); - Assert.That(combat.TargetVisible, Is.True); - Assert.That(combat.TargetHostile, Is.True); - Assert.That(combat.LockProgress, Is.EqualTo(0.6f).Within(0.0001f)); - Assert.That(combat.RadialFill(0), Is.EqualTo(0.25f).Within(0.0001f)); - Assert.That(combat.RadialFill(1), Is.EqualTo(0.75f).Within(0.0001f)); - Assert.That(combat.HitMarkerDurationSeconds, Is.EqualTo(0.25f)); - } - - [Test] - public void HitMarkerRequiresControlledSourceSelectedTargetAndAuthoritativeDamage() - { - var combat = new EveUnityCombatPresentation(Combat()); - - Assert.That(combat.PresentsHit(Shot(4, 9, true, 12, 0)), Is.True); - Assert.That(combat.PresentsHit(Shot(3, 9, true, 12, 0)), Is.False); - Assert.That(combat.PresentsHit(Shot(4, 8, true, 12, 0)), Is.False); - Assert.That(combat.PresentsHit(Shot(4, 9, false, 0, 0)), Is.False); - Assert.That(combat.PresentsHit(Shot(4, 9, true, 0, 0)), Is.False); - Assert.That(combat.PresentsHit(Shot(4, 9, true, 0, 12)), Is.True); - } - - [Test] - public void HitMarkerRetainsAuthoritativeEndpointAfterTargetLeavesTheWorld() - { - var root = new GameObject("combat-hit-retention-test"); - try - { - var renderer = root.AddComponent(); - var presentation = new EveUnityCombatPresentation(Combat()); - typeof(EveUnityCombatPresentationRenderer) - .GetField("_lastPresentation", BindingFlags.Instance | BindingFlags.NonPublic)! - .SetValue(renderer, presentation); - typeof(EveUnityCombatPresentationRenderer) - .GetMethod("OnShotAvailable", BindingFlags.Instance | BindingFlags.NonPublic)! - .Invoke(renderer, new object[] { Shot(4, 9, true, 12, 0, "7.5,-3.25") }); - typeof(EveUnityCombatPresentationRenderer) - .GetField("_hitUntil", BindingFlags.Instance | BindingFlags.NonPublic)! - .SetValue(renderer, Time.unscaledTime - 1f); - - renderer.RefreshNow(); - - Assert.That(renderer.HitMarkerVisible, Is.True, - "A destroyed target must not erase receipt-owned hit feedback."); - var line = root.transform.Find("Eve combat presentation/Hit marker A")! - .GetComponent(); - var center = (line.GetPosition(0) + line.GetPosition(1)) * 0.5f; - Assert.That(center.x, Is.EqualTo(7.5f).Within(0.0001f)); - Assert.That(center.z, Is.EqualTo(-3.25f).Within(0.0001f)); - } - finally - { - UnityEngine.Object.DestroyImmediate(root); - } - } - - private static EveUnitySceneProjection Projection(params EveUnitySceneNode[] children) => - new EveUnitySceneProjection("provider", "surface", "world", "commands", "receipt", "provider", null, - Node("root", "surface", children)); - - private static EveUnitySceneNode Combat() => Node("combat", "combat.presentation", new Dictionary(StringComparer.Ordinal) - { - ["controlledEntityId"] = "run.zone.0.entity.4", - ["controlledEntityIndex"] = "4", - ["selectedTargetEntityId"] = "run.zone.0.entity.9", - ["selectedTargetEntityIndex"] = "9", - ["targetVisible"] = "true", - ["targetHostile"] = "true", - ["contactInformation"] = "0.8", - ["shieldRatio"] = "0.4", - ["hullRatio"] = "0.9", - ["lockProgress"] = "0.6", - ["hitMarkerDurationSeconds"] = "0.25", - ["radialFillMinimum"] = "0.25", - ["radialFillMaximum"] = "0.75" - }); - - private static EveUnityShotReceipt Shot( - int source, - int target, - bool hit, - double damage, - double shield, - string endpoint = "0,0") => - new EveUnityShotReceipt(Node("shot", "shot.receipt", new Dictionary(StringComparer.Ordinal) - { - ["shotId"] = Guid.NewGuid().ToString("N"), - ["sourceEntityIndex"] = source.ToString(), - ["targetEntityIndex"] = target.ToString(), - ["hit"] = hit.ToString(), - ["appliedDamage"] = damage.ToString(System.Globalization.CultureInfo.InvariantCulture), - ["shieldAbsorbedDamage"] = shield.ToString(System.Globalization.CultureInfo.InvariantCulture), - ["endpoint"] = endpoint - })); - - private static EveUnitySceneNode Node(string id, string kind, params EveUnitySceneNode[] children) => - Node(id, kind, Empty(), children); - - private static EveUnitySceneNode Node(string id, string kind, IReadOnlyDictionary props, params EveUnitySceneNode[] children) => - new EveUnitySceneNode(id, kind, "scene-node", props, Empty(), Empty(), 0, 0, - Array.Empty(), null, children); - - private static IReadOnlyDictionary Empty() => - new Dictionary(StringComparer.Ordinal); - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs.meta b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs.meta deleted file mode 100644 index 6a4265c..0000000 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 30c8f313c1ea4b43858ed212909b7ce9 diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs deleted file mode 100644 index 376bc8a..0000000 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs +++ /dev/null @@ -1,898 +0,0 @@ -using System; -using System.Reflection; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using GameCult.Eve.Surface; -using GameCult.Eve.UnityScene; -using GameCult.Mesh; -using NUnit.Framework; -using UnityEngine; - -namespace GameCult.Eve.UnityScene.Tests -{ - public sealed class EveUnityCultMeshAssetLookupTests - { - [Test] - public void ConnectCannotPerformRemoteWorkBeforeAsynchronousPreparation() - { - using var transport = new EveUnityCultMeshLiveProviderTransport( - "test-cache", - "cultnet+tcp://127.0.0.1:1", - "test.verse", - "test.runtime", - "test.provider", - "test.surface"); - - var elapsed = Stopwatch.StartNew(); - var failure = Assert.Throws(() => transport.Connect()); - - StringAssert.Contains("Await PrepareAsync", failure!.Message); - Assert.That(elapsed.Elapsed, Is.LessThan(TimeSpan.FromMilliseconds(100))); - } - - [Test] - public async Task CommandOutboxCompletesOnlyAfterCanonicalReceipt() - { - var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var sends = 0; - using var outbox = new EveUnityCultMeshCommandOutbox( - async (_, cancellationToken) => - { - Interlocked.Increment(ref sends); - entered.TrySetResult(true); - await AwaitOrCancel(release.Task, cancellationToken); - }, - _ => null, - retryDelay: TimeSpan.FromMilliseconds(250)); - - var elapsed = Stopwatch.StartNew(); - outbox.Enqueue(Request("one-shot", "dock")); - Assert.That(elapsed.Elapsed, Is.LessThan(TimeSpan.FromMilliseconds(100))); - await RequireCompletion(entered.Task); - Assert.That(outbox.PendingCount, Is.EqualTo(1)); - - release.TrySetResult(true); - await WaitUntilAsync(() => outbox.StateOf("one-shot") == - EveUnityCultMeshCommandOutbox.DeliveryState.AwaitingCanonicalReceipt); - Assert.That(outbox.PendingCount, Is.EqualTo(1)); - Assert.That(sends, Is.EqualTo(1)); - Assert.That(outbox.Acknowledge("one-shot"), Is.True); - Assert.That(outbox.Acknowledge("one-shot"), Is.False); - await WaitUntilAsync(() => outbox.PendingCount == 0); - } - - [Test] - public async Task CommandOutboxCoalescesBlockedContinuousInputToTheLatestValue() - { - var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var release = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - var sent = new List(); - using var outbox = new EveUnityCultMeshCommandOutbox( - async (request, cancellationToken) => - { - lock (sent) sent.Add(request.CommandId); - entered.TrySetResult(true); - await AwaitOrCancel(release.Task, cancellationToken); - }, - request => request.PayloadFields["commandId"] + "\u001f" + request.PayloadFields["entityId"], - retryDelay: TimeSpan.FromMilliseconds(250)); - - outbox.Enqueue(Request("move-0", "move")); - await RequireCompletion(entered.Task); - for (var index = 1; index <= 1000; index++) - outbox.Enqueue(Request("move-" + index, "move")); - - Assert.That(outbox.PendingCount, Is.LessThanOrEqualTo(2)); - release.TrySetResult(true); - await WaitUntilAsync(() => outbox.StateOf("move-1000") == - EveUnityCultMeshCommandOutbox.DeliveryState.AwaitingCanonicalReceipt); - Assert.That(outbox.Acknowledge("move-1000"), Is.True); - await WaitUntilAsync(() => outbox.PendingCount == 0); - lock (sent) - { - CollectionAssert.AreEqual(new[] { "move-0", "move-1000" }, sent); - } - } - - [Test] - public async Task CommandOutboxRetriesSameIdWithoutLettingPoisonCommandStarveLaterWork() - { - var attempts = new List(); - using var outbox = new EveUnityCultMeshCommandOutbox( - (request, _) => - { - lock (attempts) attempts.Add(request.CommandId); - if (request.CommandId == "poison") throw new IOException("route unavailable"); - return Task.CompletedTask; - }, - _ => null, - retryDelay: TimeSpan.FromMilliseconds(20), - maximumRetryDelay: TimeSpan.FromMilliseconds(40)); - - outbox.Enqueue(Request("poison", "dock")); - outbox.Enqueue(Request("healthy", "undock")); - - await WaitUntilAsync(() => outbox.StateOf("healthy") == - EveUnityCultMeshCommandOutbox.DeliveryState.AwaitingCanonicalReceipt); - Assert.That(outbox.Acknowledge("healthy"), Is.True); - lock (attempts) - { - Assert.That(attempts.IndexOf("healthy"), Is.GreaterThanOrEqualTo(0)); - Assert.That(attempts.FindAll(value => value == "poison"), Is.Not.Empty); - } - Assert.That(outbox.PendingCount, Is.EqualTo(1)); - } - - [Test] - public async Task CommandOutboxRetransmitsUnacknowledgedCommandWithOriginalId() - { - var attempts = 0; - using var outbox = new EveUnityCultMeshCommandOutbox( - (request, _) => - { - Assert.That(request.CommandId, Is.EqualTo("retry-me")); - Interlocked.Increment(ref attempts); - return Task.CompletedTask; - }, - _ => null, - retryDelay: TimeSpan.FromMilliseconds(20), - maximumRetryDelay: TimeSpan.FromMilliseconds(20)); - - outbox.Enqueue(Request("retry-me", "dock")); - await WaitUntilAsync(() => Volatile.Read(ref attempts) >= 2); - Assert.That(outbox.PendingCount, Is.EqualTo(1)); - Assert.That(outbox.Acknowledge("retry-me"), Is.True); - await WaitUntilAsync(() => outbox.PendingCount == 0); - } - - [Test] - public void PendingReceiptIsObservedWithoutRetiringTheCommand() - { - using var transport = new EveUnityCultMeshLiveProviderTransport( - "test-cache", - "cultnet+tcp://127.0.0.1:1", - "test.verse", - "test.runtime", - "test.provider", - "test.surface"); - var request = Request("launch-1", "launch"); - var pendingField = typeof(EveUnityCultMeshLiveProviderTransport).GetField( - "_pendingCommands", - BindingFlags.NonPublic | BindingFlags.Instance); - var publish = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod( - "PublishReceipt", - BindingFlags.NonPublic | BindingFlags.Instance); - Assert.That(pendingField, Is.Not.Null); - Assert.That(publish, Is.Not.Null); - var pending = (IDictionary)pendingField!.GetValue(transport)!; - pending.Add(request.CommandId, request); - var observed = new List(); - transport.CommandReceiptAvailable += observed.Add; - - publish!.Invoke(transport, new object[] { Receipt(request, "pending", "receipt-pending") }); - - Assert.That(pending.ContainsKey(request.CommandId), Is.True); - Assert.That(observed.Select(value => value.State), Is.EqualTo(new[] { "pending" })); - - publish.Invoke(transport, new object[] - { - Receipt(request, "accepted", "receipt-accepted", sourceVersion: 1000) - }); - - Assert.That(pending.ContainsKey(request.CommandId), Is.False); - Assert.That(observed.Select(value => value.State), Is.EqualTo(new[] { "pending", "accepted" })); - } - - [Test] - public void TerminalReceiptWaitsForItsPresentationGenerationAndFreezesTheOldSurface() - { - using var transport = new EveUnityCultMeshLiveProviderTransport( - "test-cache", - "cultnet+tcp://127.0.0.1:1", - "test.verse", - "test.runtime", - "test.provider", - "test.surface"); - var request = Request("select-verse-1", "selectVerse"); - var type = typeof(EveUnityCultMeshLiveProviderTransport); - var pending = (IDictionary)type.GetField( - "_pendingCommands", - BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(transport)!; - var publish = type.GetMethod("PublishReceipt", BindingFlags.NonPublic | BindingFlags.Instance)!; - var complete = type.GetMethod( - "CompletePresentationTransition", - BindingFlags.NonPublic | BindingFlags.Instance)!; - type.GetField("_bootstrapped", BindingFlags.NonPublic | BindingFlags.Instance)! - .SetValue(transport, true); - complete.Invoke(transport, new object[] { 1L }); - pending.Add(request.CommandId, request); - var observed = new List(); - transport.CommandReceiptAvailable += observed.Add; - using var outbox = new EveUnityCultMeshCommandOutbox( - (_, _) => Task.CompletedTask, - _ => null, - retryDelay: TimeSpan.FromSeconds(30), - maximumRetryDelay: TimeSpan.FromSeconds(30)); - type.GetField("_commandOutbox", BindingFlags.NonPublic | BindingFlags.Instance)! - .SetValue(transport, outbox); - outbox.Enqueue(request); - - publish.Invoke(transport, new object[] - { - Receipt( - request, - "accepted", - "receipt-accepted", - sourceVersion: 1000, - presentationSurfaceVersion: 7) - }); - - Assert.That(pending.ContainsKey(request.CommandId), Is.True); - Assert.That(outbox.PendingCount, Is.Zero); - Assert.That(observed, Is.Empty); - type.GetProperty("CurrentSurfaceDocument")!.SetValue(transport, SceneSurface(100)); - var blocked = Assert.Throws(() => - transport.SubmitCommand(Request("stale-launch", "launch"))); - StringAssert.Contains("read-only", blocked!.Message); - - complete.Invoke(transport, new object[] { 7L }); - - Assert.That(pending.ContainsKey(request.CommandId), Is.False); - Assert.That(observed.Select(value => value.State), Is.EqualTo(new[] { "accepted" })); - } - - [Test] - public void ReceiptForAnotherInvocationCannotRetireTheCommand() - { - using var transport = new EveUnityCultMeshLiveProviderTransport( - "test-cache", - "cultnet+tcp://127.0.0.1:1", - "test.verse", - "test.runtime", - "test.provider", - "test.surface"); - var request = Request("launch-1", "launch"); - var pendingField = typeof(EveUnityCultMeshLiveProviderTransport).GetField( - "_pendingCommands", - BindingFlags.NonPublic | BindingFlags.Instance); - var publish = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod( - "PublishReceipt", - BindingFlags.NonPublic | BindingFlags.Instance); - var pending = (IDictionary)pendingField!.GetValue(transport)!; - pending.Add(request.CommandId, request); - var observed = new List(); - transport.CommandReceiptAvailable += observed.Add; - var collision = new EveCommandReceiptDocument( - "receipt-collision", - request.CommandId, - request.Command, - "accepted", - "AetheriaEve", - "test.runtime", - request.ProviderId, - request.SurfaceId, - "", - DateTimeOffset.UtcNow.ToString("O"), - 1, - invocationHash: EveCommandInvocationHash.Compute(Request(request.CommandId, "another-payload"))); - - publish!.Invoke(transport, new object[] { collision }); - - Assert.That(pending.ContainsKey(request.CommandId), Is.True); - Assert.That(observed, Is.Empty); - } - - [TestCase("provider", "other.provider")] - [TestCase("surface", "other.surface")] - [TestCase("authority", "other.runtime")] - [TestCase("state", "mysterious")] - public void UntrustedReceiptCannotRetireOrPublishTheCommand(string field, string value) - { - using var transport = new EveUnityCultMeshLiveProviderTransport( - "test-cache", - "cultnet+tcp://127.0.0.1:1", - "test.verse", - "test.runtime", - "test.provider", - "test.surface"); - var request = Request("launch-untrusted", "launch"); - var type = typeof(EveUnityCultMeshLiveProviderTransport); - var pending = (IDictionary)type.GetField( - "_pendingCommands", - BindingFlags.NonPublic | BindingFlags.Instance)!.GetValue(transport)!; - var publish = type.GetMethod("PublishReceipt", BindingFlags.NonPublic | BindingFlags.Instance)!; - pending.Add(request.CommandId, request); - var observed = new List(); - transport.CommandReceiptAvailable += observed.Add; - - var providerId = field == "provider" ? value : request.ProviderId; - var surfaceId = field == "surface" ? value : request.SurfaceId; - var authority = field == "authority" ? value : "test.runtime"; - var state = field == "state" ? value : "accepted"; - var receipt = new EveCommandReceiptDocument( - "receipt-untrusted-" + field, - request.CommandId, - request.Command, - state, - "AetheriaEve", - authority, - providerId, - surfaceId, - "", - DateTimeOffset.UtcNow.ToString("O"), - 1, - invocationHash: EveCommandInvocationHash.Compute(request)); - - publish.Invoke(transport, new object[] { receipt }); - - Assert.That(pending.ContainsKey(request.CommandId), Is.True); - Assert.That(observed, Is.Empty); - } - - [Test] - public void ProviderSelectionCarriesStableIdentityWithoutExposingAPhysicalRoute() - { - var selection = new EveUnityCultMeshProviderSelection( - "cultnet+tcp://odin.example:3075", - "aetheria.public", - "aetheria.public", - "aetheria.daemon", - "aetheria.pilot", - "interactive-world"); - - Assert.That(selection.RendezvousEndpoint, Is.EqualTo("cultnet+tcp://odin.example:3075")); - Assert.That(selection.VerseId, Is.EqualTo("aetheria.public")); - Assert.That(selection.AuthorityRuntimeId, Is.EqualTo("aetheria.public")); - Assert.That(typeof(EveUnityCultMeshProviderSelection).GetProperty("Endpoint"), Is.Null); - } - - [Test] - public void NavigationAuthorityCannotBeReplacedByAlphabeticallyEarlierPeer() - { - var target = new EveUnitySceneNavigationTarget( - "gamecult.aetheria", - "aetheria.daemon", - "aetheria.pilot", - "interactive-world", - new[] { "cultnet+tcp://odin.example:3075" }, - "z-owner"); - var eligible = EveUnityCultMeshProviderDiscovery.EligibleAuthorityRuntimeIds( - new[] { "a-decoy", "z-owner" }, - target.AuthorityRuntimeId); - - Assert.That(target.AuthorityRuntimeId, Is.EqualTo("z-owner")); - CollectionAssert.AreEqual(new[] { "z-owner" }, eligible); - } - - private static EveSurfaceCommandRequest Request(string id, string command) => new EveSurfaceCommandRequest( - "test.provider", - "test.surface", - CultMesh.OperationInvocation("test.command", idempotencyKey: id), - CultMesh.OperationPayload(new Dictionary - { - ["commandId"] = command, - ["entityId"] = "ship" - }), - DateTimeOffset.UtcNow, - "test-client"); - - private static EveCommandReceiptDocument Receipt( - EveSurfaceCommandRequest request, - string state, - string receiptId, - long sourceVersion = 0, - long presentationSurfaceVersion = 0) => new EveCommandReceiptDocument( - receiptId, - request.CommandId, - request.Command, - state, - "AetheriaEve", - "test.runtime", - request.ProviderId, - request.SurfaceId, - "", - DateTimeOffset.UtcNow.ToString("O"), - sourceVersion, - invocationHash: EveCommandInvocationHash.Compute(request), - presentationSurfaceVersion: presentationSurfaceVersion); - - [Test] - public void FailedNewestCandidateCanRetryWithoutAllowingAnOlderSurface() - { - using var transport = new EveUnityCultMeshLiveProviderTransport( - "test-cache", - "cultnet+tcp://127.0.0.1:1", - "test.verse", - "test.runtime", - "test.provider", - "test.surface"); - var begin = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod( - "TryBeginBaseSurfaceCandidate", - BindingFlags.NonPublic | BindingFlags.Instance)!; - var retire = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod( - "RetireFailedBaseSurfaceCandidate", - BindingFlags.NonPublic | BindingFlags.Instance)!; - var first = new object[] { 10L, -1L }; - var older = new object[] { 7L, -1L }; - var duplicate = new object[] { 10L, -1L }; - - Assert.That(begin.Invoke(transport, first), Is.True); - Assert.That(begin.Invoke(transport, older), Is.False); - Assert.That(begin.Invoke(transport, duplicate), Is.False); - retire.Invoke(transport, new[] { first[0], first[1] }); - var retry = new object[] { 10L, -1L }; - Assert.That(begin.Invoke(transport, retry), Is.True); - Assert.That((long)retry[1], Is.GreaterThan((long)first[1])); - Assert.That(begin.Invoke(transport, new object[] { 9L, -1L }), Is.False); - } - - [Test] - public async Task FailedAssetPreparationRunsBoundedRetryPolicyAndRetiresCandidate() - { - using var transport = new EveUnityCultMeshLiveProviderTransport( - "test-cache", - "cultnet+tcp://127.0.0.1:1", - "test.verse", - "test.runtime", - "test.provider", - "test.surface"); - var type = typeof(EveUnityCultMeshLiveProviderTransport); - var begin = type.GetMethod( - "TryBeginBaseSurfaceCandidate", - BindingFlags.NonPublic | BindingFlags.Instance)!; - var refresh = type.GetMethod( - "RefreshAssetsForSurfaceAsync", - BindingFlags.NonPublic | BindingFlags.Instance)!; - var attempts = type.GetField( - "_lastAssetPresentationAttemptCount", - BindingFlags.NonPublic | BindingFlags.Instance)!; - var first = new object[] { 10L, -1L }; - Assert.That(begin.Invoke(transport, first), Is.True); - var task = (Task)refresh.Invoke(transport, new object[] - { - SceneSurface(10).SurfaceDocument, - CreateAssetSource("test.verse", "test.runtime", "test.provider", "catalog", 41), - (long)first[1], - CancellationToken.None - })!; - - await task; - - Assert.That(attempts.GetValue(transport), Is.EqualTo(3)); - Assert.That(begin.Invoke(transport, new object[] { 10L, -1L }), Is.True); - } - - private static EveUnitySceneProviderSurfaceDocument SceneSurface(long version) - { - var surface = new EveSurfaceDocument( - "test.provider", - "", - "", - version, - "", - new EveSurfaceTree( - "test.surface", - new EveSurfaceComponent( - "root", - "surface", - new Dictionary(), - Array.Empty()), - Array.Empty()), - Array.Empty()); - return new EveUnitySceneProviderSurfaceDocument( - surface, - new EveUnitySceneProviderSurfaceAdvertisement( - "test.surface", - "interactive-world", - new EveUnitySceneWorldInteraction("", "", EveCommandReceiptDocument.SchemaId, "")), - "test:surface", - version); - } - - private static async Task AwaitOrCancel(Task task, CancellationToken cancellationToken) - { - var cancelled = Task.Delay(Timeout.Infinite, cancellationToken); - if (await Task.WhenAny(task, cancelled) != task) - cancellationToken.ThrowIfCancellationRequested(); - await task; - } - - private static async Task RequireCompletion(Task task) - { - if (await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(2))) != task) - Assert.Fail("Timed out waiting for asynchronous command delivery."); - await task; - } - - private static async Task WaitUntilAsync(Func predicate) - { - var deadline = DateTimeOffset.UtcNow + TimeSpan.FromSeconds(2); - while (!predicate() && DateTimeOffset.UtcNow < deadline) - await Task.Delay(10); - Assert.That(predicate(), Is.True, "Timed out waiting for the command outbox to drain."); - } - - [Test] - public void LiveTransportOwnsOneCultMeshClientAndNoDirectSnapshotReplica() - { - var fields = typeof(EveUnityCultMeshLiveProviderTransport) - .GetFields(BindingFlags.Instance | BindingFlags.NonPublic); - var names = Array.ConvertAll(fields, field => field.Name); - - CollectionAssert.Contains(names, "_meshClient"); - CollectionAssert.DoesNotContain(names, "_node"); - CollectionAssert.DoesNotContain(names, "_snapshot"); - CollectionAssert.DoesNotContain(names, "_networkRegistry"); - CollectionAssert.DoesNotContain(names, "_replicaShard"); - CollectionAssert.DoesNotContain(names, "_subscriptions"); - CollectionAssert.DoesNotContain(names, "_entitySubscriptions"); - } - - [Test] - public void UnavailableTcpRendezvousIsReportedAsRetryablePreparationFailure() - { - var listener = new TcpListener(IPAddress.Loopback, 0); - listener.Start(); - var port = ((IPEndPoint)listener.LocalEndpoint).Port; - listener.Stop(); - - var error = Assert.ThrowsAsync(() => - new EveUnityCultMeshProviderDiscovery().DiscoverAsync($"cultnet+tcp://127.0.0.1:{port}")); - - StringAssert.Contains("Could not query CultMesh rendezvous endpoint", error!.Message); - Assert.That(error.InnerException, Is.TypeOf()); - } - - [Test] - public void UnpreparedProviderGetterFailsFastWithoutStartingNetworkDiscovery() - { - var gameObject = new GameObject("unprepared-eve-provider"); - try - { - var provider = gameObject.AddComponent(); - provider.Configure("rudp://127.0.0.1:1"); - var stopwatch = Stopwatch.StartNew(); - - var error = Assert.Throws(() => _ = provider.CurrentInputCapability); - - stopwatch.Stop(); - StringAssert.Contains("Await PrepareAsync", error!.Message); - Assert.That(stopwatch.ElapsedMilliseconds, Is.LessThan(100)); - Assert.That(provider.Selection, Is.Null); - } - finally - { - UnityEngine.Object.DestroyImmediate(gameObject); - } - } - - [Test] - public void ProviderSubscriptionsDoNotDependOnPreparedTransportLifetime() - { - var gameObject = new GameObject("unprepared-eve-provider-subscriptions"); - var provider = gameObject.AddComponent(); - Action documentHandler = _ => { }; - Action receiptHandler = _ => { }; - - Assert.DoesNotThrow(() => provider.DocumentAvailable += documentHandler); - Assert.DoesNotThrow(() => provider.ReceiptAvailable += receiptHandler); - Assert.DoesNotThrow(() => provider.DocumentAvailable -= documentHandler); - Assert.DoesNotThrow(() => provider.ReceiptAvailable -= receiptHandler); - Assert.DoesNotThrow(() => UnityEngine.Object.DestroyImmediate(gameObject)); - } - - [Test] - public void ResolvesUnityNormalizedBundleNamesWithoutChangingLogicalAssetIdentity() - { - var method = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod( - "ResolveBundleAssetName", - BindingFlags.Static | BindingFlags.NonPublic); - - Assert.That(method, Is.Not.Null); - var result = method!.Invoke(null, new object[] - { - new[] { "assets/generated/eve/thermal/death.asset" }, - "Assets/Generated/Eve/Thermal/Death.asset" - }); - - Assert.That(result, Is.EqualTo("assets/generated/eve/thermal/death.asset")); - } - - [Test] - public void RejectsBundleNamesThatDoNotMatchTheAdvertisedAsset() - { - var method = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod( - "ResolveBundleAssetName", - BindingFlags.Static | BindingFlags.NonPublic); - - var result = method!.Invoke(null, new object[] - { - new[] { "assets/generated/eve/thermal/heatstroke.asset" }, - "Assets/Generated/Eve/Thermal/Death.asset" - }); - - Assert.That(result, Is.Null); - } - - [Test] - public void SelectedRuntimeVariantOwnsConcreteNativeProgramBindings() - { - var method = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod( - "MergeAssetMetadata", - BindingFlags.Static | BindingFlags.NonPublic); - - Assert.That(method, Is.Not.Null); - var result = method!.Invoke(null, new object[] - { - new Dictionary { ["presentationRole"] = "environment.volume" }, - new Dictionary - { - ["unity.volume.texturePort.surfaceHeight"] = "_NebulaSurfaceHeight", - ["unity.volume.pass.raymarch"] = "0" - } - }) as IReadOnlyDictionary; - - Assert.That(result, Is.Not.Null); - Assert.That(result!["presentationRole"], Is.EqualTo("environment.volume")); - Assert.That(result["unity.volume.texturePort.surfaceHeight"], Is.EqualTo("_NebulaSurfaceHeight")); - Assert.That(result["unity.volume.pass.raymarch"], Is.EqualTo("0")); - } - - [Test] - public void ConfiguringCandidateCatalogRegistersBundlesWithoutMaterializingThem() - { - var platformMethod = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod( - "CurrentBundlePlatform", - BindingFlags.Static | BindingFlags.NonPublic); - var configureMethod = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod( - "ConfigureAssetCatalog", - BindingFlags.Static | BindingFlags.NonPublic); - Assert.That(platformMethod, Is.Not.Null); - Assert.That(configureMethod, Is.Not.Null); - var variant = new EveAssetVariant( - "unity-scene", - (string)platformMethod!.Invoke(null, Array.Empty())!, - "unity-assetbundle", - "cdn:manifest:lazy", - "sha256:00", - 1, - "assets/prefab.prefab"); - var catalog = new EveAssetCatalogDocument( - "provider", - "catalog", - 1, - DateTimeOffset.UtcNow.ToString("O"), - new[] { new EveAssetCatalogEntry("prefab.entity", "prefab", new[] { variant }) }); - var candidate = CreateAssetGeneration("verse", "authority", "provider", "catalog", "candidate", 1); - - configureMethod!.Invoke(null, new[] { candidate, catalog }); - - Assert.That(GenerationCollection(candidate, "AssetBundles").Count, Is.Zero); - Assert.That(GenerationDictionary(candidate, "AssetSelections").Count, Is.EqualTo(1)); - ((IDisposable)candidate).Dispose(); - } - - [Test] - public void FailedAssetCandidatePreservesCommittedGeneration() - { - var transportType = typeof(EveUnityCultMeshLiveProviderTransport); - var generationField = transportType.GetField( - "_assetGeneration", - BindingFlags.Instance | BindingFlags.NonPublic)!; - var configureMethod = transportType.GetMethod( - "ConfigureAssetCatalog", - BindingFlags.Static | BindingFlags.NonPublic)!; - using var transport = new EveUnityCultMeshLiveProviderTransport( - Path.Combine(Path.GetTempPath(), $"eve-asset-generation-{Guid.NewGuid():N}.cc"), - "cultnet+tcp://127.0.0.1:1", - "verse-a", - "authority-a", - "provider-a", - "surface-a"); - var initial = (IDisposable)generationField.GetValue(transport)!; - var committed = CreateAssetGeneration( - "verse-a", "authority-a", "provider-a", "catalog-a", "source-a", 1); - var prefab = new GameObject("committed-a"); - GenerationDictionary(committed, "Prefabs")["prefab.a"] = prefab; - initial.Dispose(); - generationField.SetValue(transport, committed); - - var invalidVariant = new EveAssetVariant( - "browser", - "web", - "url", - "https://example.invalid/a", - "sha256:00", - 1, - "prefab.a"); - var invalidCatalog = new EveAssetCatalogDocument( - "provider-b", - "catalog-b", - 2, - DateTimeOffset.UtcNow.ToString("O"), - new[] { new EveAssetCatalogEntry("prefab.b", "prefab", new[] { invalidVariant }) }); - var rejected = CreateAssetGeneration( - "verse-b", "authority-b", "provider-b", "catalog-b", "source-b", -1); - var error = Assert.Throws(() => - configureMethod.Invoke(null, new[] { rejected, invalidCatalog })); - Assert.That(error!.InnerException, Is.TypeOf()); - ((IDisposable)rejected).Dispose(); - Assert.That(generationField.GetValue(transport), Is.SameAs(committed)); - Assert.That(GenerationProperty(committed, "SourceIdentity"), Is.EqualTo("source-a")); - Assert.That(GenerationDictionary(committed, "Prefabs")["prefab.a"], Is.SameAs(prefab)); - UnityEngine.Object.DestroyImmediate(prefab); - } - - [Test] - public void CandidateCatalogObservationSurvivesPrecommitWindow() - { - var candidate = CreateAssetGeneration( - "verse-b", "authority-b", "provider-b", "catalog-b", "source-b", 1); - var candidateType = candidate.GetType(); - var observe = candidateType.GetMethod("ObserveCatalog")!; - var activate = candidateType.GetMethod("ActivateCatalogObservation")!; - var variant = new EveAssetVariant( - "unity-scene", "StandaloneWindows64", "url", "https://example.invalid/b", "sha256:01", 1, "prefab.b"); - var catalogB = new EveAssetCatalogDocument( - "provider-b", "catalog-b", 2, DateTimeOffset.UtcNow.ToString("O"), - new[] { new EveAssetCatalogEntry("prefab.b", "prefab", new[] { variant }) }); - - Assert.That(observe.Invoke(candidate, new object[] { catalogB }), Is.Null); - Assert.That(activate.Invoke(candidate, Array.Empty()), Is.SameAs(catalogB)); - ((IDisposable)candidate).Dispose(); - } - - [Test] - public void PinnedSurfaceCatalogDoesNotAutoAdvanceWithoutANewerBaseSurface() - { - var transportType = typeof(EveUnityCultMeshLiveProviderTransport); - var generationField = transportType.GetField( - "_assetGeneration", - BindingFlags.Instance | BindingFlags.NonPublic)!; - var pendingField = transportType.GetField( - "_pendingAssetCatalog", - BindingFlags.Instance | BindingFlags.NonPublic)!; - var queue = transportType.GetMethod( - "QueueAssetCatalogUpdate", - BindingFlags.Instance | BindingFlags.NonPublic)!; - using var transport = new EveUnityCultMeshLiveProviderTransport( - Path.Combine(Path.GetTempPath(), $"eve-pinned-catalog-{Guid.NewGuid():N}.cc"), - "cultnet+tcp://127.0.0.1:1", - "verse-a", - "authority-a", - "provider-a", - "surface-a"); - var initial = (IDisposable)generationField.GetValue(transport)!; - var committed = CreateAssetGeneration( - "verse-a", "authority-a", "provider-a", "catalog-a", "source-a", 41, 41); - initial.Dispose(); - generationField.SetValue(transport, committed); - var catalog42 = new EveAssetCatalogDocument( - "provider-a", - "catalog-a", - 42, - DateTimeOffset.UtcNow.ToString("O"), - Array.Empty()); - - queue.Invoke(transport, new object[] { "source-a", catalog42 }); - - Assert.That(GenerationProperty(generationField.GetValue(transport)!, "CatalogVersion"), Is.EqualTo(41)); - var committedSource = GenerationProperty(generationField.GetValue(transport)!, "Source"); - Assert.That(GenerationProperty(committedSource, "RequiredCatalogVersion"), Is.EqualTo(41)); - Assert.That(pendingField.GetValue(transport), Is.Null); - } - - [Test] - public void PinnedSurfaceRejectsWrongImmutableCatalogRecordImmediately() - { - var require = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod( - "RequireCatalogVersion", - BindingFlags.Static | BindingFlags.NonPublic)!; - var catalog42 = new EveAssetCatalogDocument( - "provider-a", - "eve:assets:aetheria.daemon:version:42", - 42, - DateTimeOffset.UtcNow.ToString("O"), - Array.Empty()); - - var error = Assert.Throws(() => require.Invoke( - null, - new object[] { catalog42, 41L, "eve:assets:aetheria.daemon:version:41" })); - - Assert.That(error!.InnerException, Is.TypeOf()); - Assert.That(error.InnerException!.Message, Does.Contain("requires immutable version 41")); - } - - [Test] - public void CrossTargetAssetsUseConfiguredRemoteTrust() - { - var localTrust = new CultMeshAuthorityTrustPolicy(CultMeshAuthorityTrustMode.LocalDevelopment); - var remoteTrust = new CultMeshAuthorityTrustPolicy(CultMeshAuthorityTrustMode.AuthenticatedRemote); - using var transport = new EveUnityCultMeshLiveProviderTransport( - Path.Combine(Path.GetTempPath(), $"eve-asset-trust-{Guid.NewGuid():N}.cc"), - "cultnet+tcp://127.0.0.1:1", - "verse-a", - "authority-a", - "provider-a", - "surface-a", - authorityTrust: localTrust, - crossTargetAuthorityTrust: remoteTrust); - var trustMethod = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod( - "AssetAuthorityTrust", - BindingFlags.Instance | BindingFlags.NonPublic)!; - - var localSource = CreateAssetSource("verse-a", "authority-a", "provider-a", "catalog-a"); - var remoteSource = CreateAssetSource("verse-b", "authority-b", "provider-b", "catalog-b"); - - Assert.That(trustMethod.Invoke(transport, new[] { localSource }), Is.SameAs(localTrust)); - Assert.That(trustMethod.Invoke(transport, new[] { remoteSource }), Is.SameAs(remoteTrust)); - } - - private static object CreateAssetSource( - string verseId, - string authorityRuntimeId, - string providerId, - string manifestRecordRef, - long requiredCatalogVersion = 0) - { - var type = typeof(EveUnityCultMeshLiveProviderTransport).GetNestedType( - "AssetSource", - BindingFlags.NonPublic)!; - return Activator.CreateInstance( - type, - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, - null, - new object[] - { - new CultMeshSessionTarget(verseId, authorityRuntimeId), - providerId, - manifestRecordRef, - Array.Empty(), - requiredCatalogVersion - }, - null)!; - } - - private static object CreateAssetGeneration( - string verseId, - string authorityRuntimeId, - string providerId, - string manifestRecordRef, - string sourceIdentity, - long catalogVersion, - long requiredCatalogVersion = 0) - { - var type = typeof(EveUnityCultMeshLiveProviderTransport).GetNestedType( - "AssetGeneration", - BindingFlags.NonPublic)!; - return Activator.CreateInstance( - type, - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, - null, - new[] - { - CreateAssetSource(verseId, authorityRuntimeId, providerId, manifestRecordRef, requiredCatalogVersion), - sourceIdentity, - catalogVersion, - null - }, - null)!; - } - - private static System.Collections.IList GenerationCollection(object generation, string property) => - (System.Collections.IList)generation.GetType().GetProperty(property)!.GetValue(generation)!; - - private static System.Collections.IDictionary GenerationDictionary(object generation, string property) => - (System.Collections.IDictionary)generation.GetType().GetProperty(property)!.GetValue(generation)!; - - private static T GenerationProperty(object generation, string property) => - (T)generation.GetType().GetProperty(property)!.GetValue(generation)!; - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs.meta b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs.meta deleted file mode 100644 index 790a2ed..0000000 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: d746a249576c8e74696ea8add7a25731 \ No newline at end of file diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityEmbeddedSurfaceCompositionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityEmbeddedSurfaceCompositionTests.cs deleted file mode 100644 index c3d1924..0000000 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityEmbeddedSurfaceCompositionTests.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using GameCult.Eve.Surface; -using NUnit.Framework; - -namespace GameCult.Eve.UnityScene.Tests -{ - public sealed class EveUnityEmbeddedSurfaceCompositionTests - { - [Test] - public void ComposesAdvertisedReactiveSurfaceWithoutReplacingBaseTopology() - { - const string record = "eve:surface:test.reactive"; - var slot = new EveEmbeddedDocumentSlot( - "reactive", - record, - EveSurfaceDocument.SchemaId, - "reactive-state"); - var world = Component("world", "world.scene3d"); - var mount = new EveSurfaceComponent( - "reactive-mount", - "layer.reactive", - EmptyProps(), - Array.Empty(), - Array.Empty(), - new[] { slot }); - var @base = Surface(4, Component("root", "surface", world, mount)); - var fragment = Surface(12, Component( - "fragment", - "layer.reactive", - Component("shot", "shot.receipt"))); - - var composed = EveUnityCultMeshLiveProviderTransport.ComposeSurface( - @base, - new Dictionary(StringComparer.Ordinal) - { - [record] = fragment - }); - - Assert.That(composed.Version, Is.EqualTo(12)); - Assert.That(composed.Surface.Root.Children[0].Id, Is.EqualTo("world")); - Assert.That(composed.Surface.Root.Children[1].Children.Single().Id, Is.EqualTo("fragment")); - Assert.That(composed.Surface.Root.Children[1].Children.Single().Children.Single().Kind, - Is.EqualTo("shot.receipt")); - } - - private static EveSurfaceDocument Surface(long version, EveSurfaceComponent root) => - new EveSurfaceDocument( - "provider", - "test", - "Test", - version, - "2026-07-19T00:00:00Z", - new EveSurfaceTree("test", root, Array.Empty()), - Array.Empty()); - - private static EveSurfaceComponent Component( - string id, - string kind, - params EveSurfaceComponent[] children) => - new EveSurfaceComponent(id, kind, EmptyProps(), children); - - private static IReadOnlyDictionary EmptyProps() => - new Dictionary(StringComparer.Ordinal); - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityEmbeddedSurfaceCompositionTests.cs.meta b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityEmbeddedSurfaceCompositionTests.cs.meta deleted file mode 100644 index d304bb8..0000000 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityEmbeddedSurfaceCompositionTests.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 8e721e8052744db6ba925b8a3831de70 diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityFeedbackPresenterTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityFeedbackPresenterTests.cs index 8dc0d35..e4bd612 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityFeedbackPresenterTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityFeedbackPresenterTests.cs @@ -29,51 +29,14 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(emitted.Count, Is.EqualTo(1)); } - [Test] - public void FeedbackEventPreservesProviderOwnedCargoDeltaAndReason() - { - var node = Event("pickup", "pickup.collected", 9, new Dictionary - { - ["itemKey"] = "scrap-metal", - ["scalarValue"] = "1", - ["auxiliaryValue"] = "0", - ["cargoQuantityBefore"] = "0", - ["cargoQuantityAfter"] = "1", - ["reason"] = "" - }); - - var value = new EveUnityFeedbackEvent(node); - - Assert.That(value.ItemKey, Is.EqualTo("scrap-metal")); - Assert.That(value.ScalarValue, Is.EqualTo(1)); - Assert.That(value.AuxiliaryValue, Is.Zero); - Assert.That(value.CargoQuantityBefore, Is.Zero); - Assert.That(value.CargoQuantityAfter, Is.EqualTo(1)); - Assert.That(value.Reason, Is.Empty); - } - private static EveUnitySceneProjection Projection(params EveUnitySceneNode[] events) => new EveUnitySceneProjection("provider", "surface", "world", "commands", "receipt", "provider", null, Node("root", "surface", Node("feedback", "feedback.stream", events))); - private static EveUnitySceneNode Event( - string id, - string kind, - long frame, - IReadOnlyDictionary? extra = null) - { - var props = new Dictionary(StringComparer.Ordinal) - { - ["eventId"] = id, - ["eventKind"] = kind, - ["frameId"] = frame.ToString() - }; - foreach (var pair in extra ?? Empty()) props[pair.Key] = pair.Value; - return + private static EveUnitySceneNode Event(string id, string kind, long frame) => new EveUnitySceneNode(id, "feedback.event", "scene-node", - props, + new Dictionary(StringComparer.Ordinal) { ["eventId"] = id, ["eventKind"] = kind, ["frameId"] = frame.ToString() }, Empty(), Empty(), 0, 0, Array.Empty(), null, Array.Empty()); - } private static EveUnitySceneNode Node(string id, string kind, params EveUnitySceneNode[] children) => new EveUnitySceneNode(id, kind, "scene-node", Empty(), Empty(), Empty(), 0, 0, diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 7f83143..5c3557e 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -1,21 +1,10 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text.RegularExpressions; -using System.Collections; -using System.Threading.Tasks; -using GameCult.Caching; -using GameCult.Eve.PluginFields; +using System.IO.MemoryMappedFiles; using GameCult.Eve.Surface; using GameCult.Mesh; using NUnit.Framework; using UnityEngine; -using UnityEngine.Rendering; -using UnityEngine.Rendering.Universal; -using UnityEngine.UIElements; -using UnityEngine.TestTools; #nullable enable @@ -24,498 +13,42 @@ namespace GameCult.Eve.UnityScene.Tests public sealed class EveUnitySceneSurfaceLowererTests { [Test] - public void LiveTransportDefersDefaultBodyAuthorizationUntilAdvertisementResolution() + public void EntitySoaViewReadsGenericSemanticColumnsFromAdvertisedBackend() { - Assert.DoesNotThrow(() => + var location = $"eve-unity-soa-{Guid.NewGuid():N}"; + using var mapped = MemoryMappedFile.CreateNew(location, 32); + using (var writer = mapped.CreateViewAccessor()) { - using var transport = new EveUnityCultMeshLiveProviderTransport( - "test-cache", - "cultnet://127.0.0.1:3075", - "aetheria", - "aetheria", - "aetheria", - "aetheria.pilot"); - }); - } - - [Test] - public async System.Threading.Tasks.Task LiveTransportResolvesViewGenerationWhenNextPublicationAlreadyExists() - { - var view = EntityLeaseDocument(); - var generationN = BodyPublication(view, sequence: view.Sequence); - var generationNPlusOne = BodyPublication(view, sequence: view.Sequence + 1); - var cache = new CultCache(); - await cache.UpsertAsync(generationN, - new CultRecordHandle(generationN.RecordKey)); - await cache.UpsertAsync(generationNPlusOne, - new CultRecordHandle(generationNPlusOne.RecordKey)); - - var handle = (CultMeshBodyPublicationHandle)typeof(EveUnityCultMeshLiveProviderTransport) - .GetMethod("BodyPublicationHandle", BindingFlags.NonPublic | BindingFlags.Static)! - .Invoke(null, new object[] { view })!; - var resolved = cache.Get(handle.RecordKey); - - Assert.That(resolved, Is.Not.Null); - handle.Validate(resolved!); - Assert.That(resolved!.Sequence, Is.EqualTo(view.Sequence)); - Assert.That(handle.RecordKey, Is.Not.EqualTo(generationNPlusOne.RecordKey)); - } - - [Test] - public void LiveTransportReusesStableLayoutForNewBodyPublications() - { - using var transport = new EveUnityCultMeshLiveProviderTransport( - "test-cache", - "cultnet://127.0.0.1:3075", - "aetheria", - "aetheria", - "aetheria", - "aetheria.pilot"); - var view = EntityLeaseDocument(); - var publication = BodyPublication(view, view.Sequence + 1); - var queueView = typeof(EveUnityCultMeshLiveProviderTransport) - .GetMethod("QueueEntityView", BindingFlags.NonPublic | BindingFlags.Instance)!; - var queuePublication = typeof(EveUnityCultMeshLiveProviderTransport) - .GetMethod("QueueBodyPublication", BindingFlags.NonPublic | BindingFlags.Instance)!; - var queue = (System.Collections.Concurrent.ConcurrentQueue) - typeof(EveUnityCultMeshLiveProviderTransport) - .GetField("_liveDocuments", BindingFlags.NonPublic | BindingFlags.Instance)! - .GetValue(transport)!; - - queueView.Invoke(transport, new object[] { view }); - Assert.That(queue, Is.Empty, "a layout without its body publication is not a complete generation"); - queuePublication.Invoke(transport, new object[] { publication }); - - Assert.That(queue.TryDequeue(out var queued), Is.True); - var generation = queued!.GetType().GetProperty("View")!.GetValue(queued) as EveEntitySoaViewDocument; - Assert.That(generation, Is.Not.Null); - Assert.That(generation!.Sequence, Is.EqualTo(publication.Sequence)); - Assert.That(generation.ProducerEpoch, Is.EqualTo(publication.ProducerEpoch)); - Assert.That(generation.Buffers, Is.SameAs(view.Buffers)); - Assert.That(generation.Columns, Is.SameAs(view.Columns)); - Assert.That(generation.Identities, Is.SameAs(view.Identities)); - Assert.That(generation.DirtyRanges.All(range => range.Sequence == publication.Sequence), Is.True); - } - - [Test] - public void LiveTransportPairsBodyPublicationThatArrivesBeforeItsLayout() - { - using var transport = new EveUnityCultMeshLiveProviderTransport( - "test-cache", - "cultnet://127.0.0.1:3075", - "aetheria", - "aetheria", - "aetheria", - "aetheria.pilot"); - var view = EntityLeaseDocument(); - var publication = BodyPublication(view, view.Sequence); - var queueView = typeof(EveUnityCultMeshLiveProviderTransport) - .GetMethod("QueueEntityView", BindingFlags.NonPublic | BindingFlags.Instance)!; - var queuePublication = typeof(EveUnityCultMeshLiveProviderTransport) - .GetMethod("QueueBodyPublication", BindingFlags.NonPublic | BindingFlags.Instance)!; - var queue = (System.Collections.Concurrent.ConcurrentQueue) - typeof(EveUnityCultMeshLiveProviderTransport) - .GetField("_liveDocuments", BindingFlags.NonPublic | BindingFlags.Instance)! - .GetValue(transport)!; - - queuePublication.Invoke(transport, new object[] { publication }); - Assert.That(queue, Is.Empty, "a publication without its layout is not a complete generation"); - queueView.Invoke(transport, new object[] { view }); - - Assert.That(queue.TryDequeue(out var queued), Is.True); - Assert.That(queued!.GetType().GetProperty("View")!.GetValue(queued), Is.SameAs(view)); - Assert.That(queued.GetType().GetProperty("Publication")!.GetValue(queued), Is.SameAs(publication)); - } - - [Test] - public void LiveTransportKeepsOnlyLatestPendingRealtimeGenerationPerBody() - { - using var transport = new EveUnityCultMeshLiveProviderTransport( - "test-cache", - "cultnet://127.0.0.1:3075", - "provider", - "provider", - "provider", - "provider.pilot"); - var enqueue = typeof(EveUnityCultMeshLiveProviderTransport) - .GetMethod("EnqueueRealtimeEntityFrame", BindingFlags.NonPublic | BindingFlags.Instance)!; - var queue = (System.Collections.Concurrent.ConcurrentQueue) - typeof(EveUnityCultMeshLiveProviderTransport) - .GetField("_liveDocuments", BindingFlags.NonPublic | BindingFlags.Instance)! - .GetValue(transport)!; - var latest = (Dictionary) - typeof(EveUnityCultMeshLiveProviderTransport) - .GetField("_latestRealtimeFrames", BindingFlags.NonPublic | BindingFlags.Instance)! - .GetValue(transport)!; - - for (var sequence = 0; sequence < 100; sequence++) - enqueue.Invoke(transport, new object[] { RealtimeFrame("entities", 7, sequence) }); - enqueue.Invoke(transport, new object[] { RealtimeFrame("entities", 7, 50) }); - - Assert.That(queue.Count, Is.EqualTo(1), "superseded frames must not become render-thread debt"); - Assert.That(latest, Has.Count.EqualTo(1)); - Assert.That(latest["entities"].ProducerEpoch, Is.EqualTo(7)); - Assert.That(latest["entities"].Sequence, Is.EqualTo(99)); - } - - [Test] - public void LiveTransportReadsLaterMappedFramesWithoutNewControlDocuments() - { - using var transport = new EveUnityCultMeshLiveProviderTransport( - "test-cache", - "cultnet://127.0.0.1:3075", - "provider", - "provider", - "provider", - "provider.pilot"); - var view = EntityLeaseDocument(); - view.Sequence = 0; - using var publisher = new CultMeshFrameBodyPublisher( - view.Buffers[0].BufferId, - view.BodySchemaId, - view.LayoutVersion, - view.Capacity, - view.ProducerEpoch, - slotByteLength: 4); - publisher.TryPublish(new byte[] { 1, 2, 3, 4 }, DateTimeOffset.UtcNow, out var bootstrap); - var publication = new CultMeshBodyPublicationDocument - { - BodyId = bootstrap.BodyId, - ProducerId = view.ProviderId, - SchemaId = bootstrap.SchemaId, - LayoutVersion = bootstrap.LayoutVersion, - ByteSize = bootstrap.ByteSize, - Capacity = bootstrap.Capacity, - ProducerEpoch = bootstrap.ProducerEpoch, - Sequence = bootstrap.Sequence, - Synchronization = bootstrap.Synchronization, - LivenessExpiresAtUnixMs = bootstrap.LeaseExpiresAtUnixMs, - Representations = new[] { bootstrap } - }; - var queueView = typeof(EveUnityCultMeshLiveProviderTransport) - .GetMethod("QueueEntityView", BindingFlags.NonPublic | BindingFlags.Instance)!; - var ensureCursor = typeof(EveUnityCultMeshLiveProviderTransport) - .GetMethod("EnsureMappedEntityFrameCursor", BindingFlags.NonPublic | BindingFlags.Instance)!; - var pumpCursor = typeof(EveUnityCultMeshLiveProviderTransport) - .GetMethod("PumpMappedEntityFrame", BindingFlags.NonPublic | BindingFlags.Instance)!; - var queue = (System.Collections.Concurrent.ConcurrentQueue) - typeof(EveUnityCultMeshLiveProviderTransport) - .GetField("_liveDocuments", BindingFlags.NonPublic | BindingFlags.Instance)! - .GetValue(transport)!; - queueView.Invoke(transport, new object[] { view }); - queue.TryDequeue(out _); - ensureCursor.Invoke(transport, new object[] { publication }); - - publisher.TryPublish(new byte[] { 9, 8, 7, 6 }, DateTimeOffset.UtcNow, out _); - EveEntitySoaViewDocument? presented = null; - byte presentedByte = 0; - transport.EntityViewAvailable += (generation, lease) => - { - presented = generation; - presentedByte = lease.ReadByte(0); - lease.Dispose(); - }; - - pumpCursor.Invoke(transport, Array.Empty()); - - Assert.That(presented, Is.Not.Null); - Assert.That(presented!.Sequence, Is.EqualTo(1)); - Assert.That(presentedByte, Is.EqualTo(9)); - Assert.That(queue, Is.Empty, "mapped frames must not be represented as queued CultNet documents"); - } - - [Test] - public void LiveTransportDropsExpiredHistoricalGenerationWithoutReinterpretingLatest() - { - var view = EntityLeaseDocument(); - var publication = BodyPublication(view, view.Sequence); - publication.LivenessExpiresAtUnixMs = 999; - var method = typeof(EveUnityCultMeshLiveProviderTransport) - .GetMethod("IsPublicationLive", BindingFlags.NonPublic | BindingFlags.Static)!; - - var live = (bool)method.Invoke(null, new object[] - { - publication, - DateTimeOffset.FromUnixTimeMilliseconds(1000) - })!; - - Assert.That(live, Is.False); - Assert.That(publication.RecordKey, Is.EqualTo( - new CultMeshBodyPublicationHandle(view.Buffers[0].BufferId, view.ProducerEpoch, view.Sequence).RecordKey)); - } - - [Test] - public void LiveTransportHasNoSnapshotWireRegistryForBodyBytes() - { - var transport = typeof(EveUnityCultMeshLiveProviderTransport); - var wireRegistry = transport.GetField( - "WireDocumentTypes", - BindingFlags.NonPublic | BindingFlags.Static); - var fieldTypes = transport - .GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static) - .Select(field => field.FieldType) - .ToArray(); - - Assert.That(wireRegistry, Is.Null); - Assert.That(fieldTypes.Any(type => type == typeof(CultMeshCdnArtifactChunk)), Is.False); - Assert.That(fieldTypes.Any(type => type.Name == "CultMeshNetworkBodyDocument"), Is.False); - } - - [Test] - public void LiveTransportBuildsImmutableNetworkFallbackForMappedAssetContent() - { - var manifest = CultMeshCdn.PackArtifact( - "provider/world/windows", - Enumerable.Range(0, 64).Select(value => (byte)value).ToArray()).Manifest; - var now = DateTimeOffset.UtcNow; - var method = typeof(EveUnityCultMeshLiveProviderTransport) - .GetMethod("NetworkArtifactDescriptor", BindingFlags.NonPublic | BindingFlags.Static)!; - - var descriptor = (CultMeshBodyDescriptor)method.Invoke( - null, - new object[] { manifest, now, TimeSpan.FromMinutes(5) })!; - - Assert.That(descriptor.BodyId, Is.EqualTo(manifest.ArtifactId)); - Assert.That(descriptor.SchemaId, Is.EqualTo("gamecult.mesh.cdn-artifact.v1")); - Assert.That(descriptor.ByteSize, Is.EqualTo(manifest.SizeBytes)); - Assert.That(descriptor.Capacity, Is.EqualTo(manifest.SizeBytes)); - Assert.That(descriptor.AccessMode, Is.EqualTo(CultMeshBodyAccessMode.ReadOnly)); - Assert.That(descriptor.Synchronization, Is.EqualTo(CultMeshBodySynchronization.ImmutableSequence)); - Assert.That(descriptor.TransportKind, Is.EqualTo(CultMeshBodyTransportKind.Network)); - Assert.That(descriptor.SemanticHash, Is.EqualTo(manifest.ContentHash)); - Assert.That(descriptor.CapabilityToken, Is.EqualTo( - CultMeshCdnArtifactManifest.CreateRecordKey(manifest).Value)); - } - - [Test] - public void LiveTransportAuthorizesOnlyExplicitlyAdvertisedBodyProducers() - { - var advertisement = ProviderAdvertisement("aetheria", "aetheria-daemon-17", "aetheria.daemon"); - var method = typeof(EveUnityCultMeshLiveProviderTransport) - .GetMethod("RequireAdvertisedBodyProducerIds", BindingFlags.NonPublic | BindingFlags.Static)!; - - var producerIds = (IReadOnlyList)method.Invoke(null, new object?[] { advertisement })!; - - Assert.That(producerIds, Is.EqualTo(new[] { "aetheria.daemon" })); - Assert.That(producerIds, Does.Not.Contain(advertisement.ProviderId)); - Assert.That(producerIds, Does.Not.Contain(advertisement.ServiceId)); - - var authorizes = typeof(EveUnityCultMeshLiveProviderTransport) - .GetMethod("IsAdvertisedBodyProducer", BindingFlags.NonPublic | BindingFlags.Static)!; - Assert.That(authorizes.Invoke(null, new object[] { producerIds, "aetheria.daemon" }), Is.True); - Assert.That(authorizes.Invoke(null, new object[] { producerIds, "aetheria" }), Is.False); - Assert.That(authorizes.Invoke(null, new object[] { producerIds, "aetheria-daemon-17" }), Is.False); - Assert.That(authorizes.Invoke(null, new object[] { producerIds, "impostor" }), Is.False); - } - - [Test] - public void LiveTransportRejectsAdvertisementWithoutBodyProducerIdentity() - { - var advertisement = ProviderAdvertisement("aetheria", "aetheria-daemon-17"); - var method = typeof(EveUnityCultMeshLiveProviderTransport) - .GetMethod("RequireAdvertisedBodyProducerIds", BindingFlags.NonPublic | BindingFlags.Static)!; - - var error = Assert.Throws(() => - method.Invoke(null, new object?[] { advertisement })); - - Assert.That(error!.InnerException, Is.TypeOf()); - StringAssert.Contains("does not advertise an authorized body producer", error.InnerException!.Message); - } - - [Test] - public void ProviderAdvertisementRoundTripPreservesBodyProducerAuthority() - { - var advertisement = ProviderAdvertisement( - "aetheria", - "aetheria-daemon-17", - "aetheria.daemon", - "aetheria.asset-publisher"); - - var bytes = MessagePack.MessagePackSerializer.Serialize(advertisement); - var decoded = MessagePack.MessagePackSerializer.Deserialize(bytes); - - Assert.That(decoded.ProviderId, Is.EqualTo("aetheria")); - Assert.That(decoded.ServiceId, Is.EqualTo("aetheria-daemon-17")); - Assert.That(decoded.AuthorizedBodyProducerIds, - Is.EqualTo(new[] { "aetheria.daemon", "aetheria.asset-publisher" })); - } - - [Test] - public void EntitySoaViewReadsGenericSemanticColumnsFromInjectedLease() - { - var bytes = new byte[32]; - Buffer.BlockCopy(new[] { 4f, 5f, 6f, 7f, 8f, 9f }, 0, bytes, 0, 24); - Buffer.BlockCopy(new[] { 41, 42 }, 0, bytes, 24, 8); + writer.Write(0, 4f); + writer.Write(4, 5f); + writer.Write(8, 6f); + writer.Write(12, 7f); + writer.Write(16, 8f); + writer.Write(20, 9f); + writer.Write(24, 41); + writer.Write(28, 42); + } var document = new EveEntitySoaViewDocument { ProviderId = "provider", ViewId = "pilot", - BodySchemaId = "test.entity.slab.v1", - LayoutVersion = 3, - ProducerEpoch = 5, - Sequence = 7, - Capacity = 2, - Buffers = new[] { new EveEntitySoaBuffer { BufferId = "hot", ByteLength = 32 } }, + Generation = 7, + Backend = "memory_mapped_file", + Buffers = new[] { new EveEntitySoaBuffer { BufferId = "hot", Backend = "memory_mapped_file", Location = location, ByteLength = 32, Generation = 7 } }, Columns = new[] { new EveEntitySoaColumn { ColumnId = "position", Semantic = "transform.position", BufferId = "hot", ScalarType = "float3", ElementStride = 12, ElementCount = 2 }, new EveEntitySoaColumn { ColumnId = "entity-index", Semantic = "entity.index", BufferId = "hot", ScalarType = "int32", ByteOffset = 24, ElementStride = 4, ElementCount = 2 } } }; - var lease = new FakeBodyReadLease(document, bytes); - using var view = EveUnityEntitySoaView.Open(document, lease); + using var view = EveUnityEntitySoaView.Open(document); Assert.That(view.TryReadVector3("transform.position", 1, out var position), Is.True); Assert.That(position, Is.EqualTo(new Vector3(7f, 8f, 9f))); Assert.That(view.TryReadInt32("entity.index", 0, out var entityIndex), Is.True); Assert.That(entityIndex, Is.EqualTo(41)); Assert.That(view.Generation, Is.EqualTo(7)); - Assert.That(lease.Disposed, Is.False); - } - - [TestCase("body")] - [TestCase("schema")] - [TestCase("layout")] - [TestCase("epoch")] - [TestCase("sequence")] - [TestCase("capacity")] - public void EntitySoaViewRejectsLeaseThatDisagreesWithLogicalLayout(string mismatch) - { - var document = EntityLeaseDocument(); - var descriptor = FakeBodyReadLease.DescriptorFor(document, 4); - if (mismatch == "body") descriptor.BodyId = "wrong"; - if (mismatch == "schema") descriptor.SchemaId = "wrong"; - if (mismatch == "layout") descriptor.LayoutVersion++; - if (mismatch == "epoch") descriptor.ProducerEpoch++; - if (mismatch == "sequence") descriptor.Sequence++; - if (mismatch == "capacity") descriptor.Capacity++; - - Assert.Throws(() => EveUnityEntitySoaView.Open( - document, new FakeBodyReadLease(descriptor, new byte[4]))); - } - - [Test] - public void EntitySoaPresenterPreservesProviderAuthoredIdentityFacts() - { - var bytes = new byte[16]; - Buffer.BlockCopy(new[] { 12 }, 0, bytes, 0, 4); - Buffer.BlockCopy(new[] { 1f, 2f, 3f }, 0, bytes, 4, 12); - var document = EntityLeaseDocument(); - document.Buffers[0].ByteLength = bytes.Length; - document.Columns = new[] - { - new EveEntitySoaColumn { ColumnId = "entity-index", Semantic = "entity.index", BufferId = "hot", ScalarType = "int32", ElementStride = 4, ElementCount = 1 }, - new EveEntitySoaColumn { ColumnId = "position", Semantic = "transform.position", BufferId = "hot", ScalarType = "float3", ByteOffset = 4, ElementStride = 12, ElementCount = 1 } - }; - var sink = new FakePlayableWorldSceneSink(); - - new EveUnityEntitySoaPresenter(sink).Apply(document, new FakeBodyReadLease(document, bytes)); - - var entity = sink.Upserts.Single().entity; - Assert.That(entity.EntityId, Is.EqualTo("entity:pilot")); - Assert.That(entity.Label, Is.EqualTo("Pilot")); - Assert.That(entity.EntityKind, Is.EqualTo("player")); - Assert.That(entity.Faction, Is.EqualTo("alliance")); - Assert.That(entity.Selectable, Is.True); - Assert.That(entity.Controllable, Is.True); - Assert.That(entity.AssetRef, Is.EqualTo("cultmesh://assets/pilot")); - } - - [Test] - public void EntitySoaPresenterPublishesImmutableGenerationFactsAndRegistryLookups() - { - var root = new GameObject("presented-entities"); - try - { - var bytes = new byte[48]; - Buffer.BlockCopy(new[] { 12 }, 0, bytes, 0, 4); - Buffer.BlockCopy(new[] { 1f, 2f, 3f }, 0, bytes, 4, 12); - Buffer.BlockCopy(new[] { 0.5f }, 0, bytes, 16, 4); - Buffer.BlockCopy(new[] { 4f, 5f, 6f }, 0, bytes, 20, 12); - Buffer.BlockCopy(new[] { 7f }, 0, bytes, 32, 4); - Buffer.BlockCopy(new[] { 1.5f }, 0, bytes, 36, 4); - Buffer.BlockCopy(new[] { 9 }, 0, bytes, 40, 4); - Buffer.BlockCopy(new[] { 2 }, 0, bytes, 44, 4); - var document = EntityLeaseDocument(); - document.Buffers[0].ByteLength = bytes.Length; - document.Columns = new[] - { - SoaColumn("entity.index", "int32", 0, 4), - SoaColumn("transform.position", "float3", 4, 12), - SoaColumn("transform.rotation.radians", "float32", 16, 4), - SoaColumn("transform.velocity", "float3", 20, 12), - SoaColumn("physics.body.radius", "float32", 32, 4), - SoaColumn("render.scale", "float32", 36, 4), - SoaColumn("render.group.id", "uint32", 40, 4), - SoaColumn("render.lod", "int32", 44, 4) - }; - var sink = new EveUnityGameObjectPlayableWorldSceneSink( - root.transform, new FixedGameObjectAssetProvider(null)); - - new EveUnityEntitySoaPresenter(sink).Apply(document, new FakeBodyReadLease(document, bytes)); - - Assert.That(sink.CurrentGeneration, Is.Not.Null); - Assert.That(sink.CurrentGeneration!.ProducerEpoch, Is.EqualTo(document.ProducerEpoch)); - Assert.That(sink.CurrentGeneration.Sequence, Is.EqualTo(document.Sequence)); - Assert.That(sink.TryGetByEntityId("entity:pilot", out var byId), Is.True); - Assert.That(sink.TryGetBySourceIndex(12, out var byIndex), Is.True); - Assert.That(byIndex, Is.SameAs(byId)); - Assert.That(byId.Entity.Position, Is.EqualTo(new Vector3(1f, 2f, 3f))); - Assert.That(byId.Entity.Velocity, Is.EqualTo(new Vector3(4f, 5f, 6f))); - Assert.That(byId.Entity.Radius, Is.EqualTo(7f)); - Assert.That(byId.Entity.Scale, Is.EqualTo(1.5f)); - Assert.That(byId.Entity.RenderGroupId, Is.EqualTo(9)); - Assert.That(byId.Entity.Lod, Is.EqualTo(2)); - Assert.That(byId.Entity.Selectable, Is.True); - Assert.That(byId.Entity.Controllable, Is.True); - Assert.That(byId.Entity.AssetRef, Is.EqualTo("cultmesh://assets/pilot")); - } - finally - { - UnityEngine.Object.DestroyImmediate(root); - } - } - - [Test] - public void GameObjectGenerationRegistrySwapsOnlyAfterCompleteApplication() - { - var root = new GameObject("atomic-generation"); - EveUnityGameObjectPlayableWorldSceneSink? sink = null; - long observedSequenceDuringApply = -1; - try - { - var provider = new ObservingGameObjectAssetProvider(() => - observedSequenceDuringApply = sink?.CurrentGeneration?.Sequence ?? -1); - sink = new EveUnityGameObjectPlayableWorldSceneSink(root.transform, provider); - sink.ApplyGeneration(new EveUnityPresentedEntityGeneration( - "entities", 3, 10, - new[] { PresentedEntity(1, "first", "asset:first") })); - Assert.That(sink.TryGetByEntityId("first", out var firstGeneration), Is.True); - var firstTransform = firstGeneration.Transform; - - observedSequenceDuringApply = -1; - sink.ApplyGeneration(new EveUnityPresentedEntityGeneration( - "entities", 3, 11, - new[] - { - PresentedEntity(1, "first", "asset:first"), - PresentedEntity(2, "second", "asset:second") - })); - - Assert.That(observedSequenceDuringApply, Is.EqualTo(10)); - Assert.That(sink.CurrentGeneration!.Sequence, Is.EqualTo(11)); - Assert.That(sink.TryGetByEntityId("first", out var retained), Is.True); - Assert.That(retained.Transform, Is.SameAs(firstTransform)); - Assert.That(sink.TryGetByEntityId("second", out var second), Is.True); - Assert.That(sink.TryGetBySourceIndex(2, out var byIndex), Is.True); - Assert.That(byIndex, Is.SameAs(second)); - Assert.That(sink.ActiveEntityCount, Is.EqualTo(2)); - } - finally - { - UnityEngine.Object.DestroyImmediate(root); - } } [Test] @@ -573,55 +106,25 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(projection.Root.Children[0].SceneObjectKind, Is.EqualTo("playable-world-root")); Assert.That(projection.Root.Children[0].Children[0].SceneObjectKind, Is.EqualTo("playable-world-entity")); - Assert.That(projection.Root.Children[0].Children.Any(child => child.SceneObjectKind == "world-field-3d"), Is.True); + Assert.That(projection.Root.Children[0].Children[3].SceneObjectKind, Is.EqualTo("world-field-3d")); Assert.That(projection.PlayableWorld, Is.Not.Null); Assert.That(projection.PlayableWorld!.WorldRootId, Is.EqualTo("aetheria.daemon.game.playable")); Assert.That(projection.PlayableWorld.StatePointerId, Is.EqualTo("cultmesh://aetheria/run/current")); Assert.That(projection.PlayableWorld.EntityViewPointerId, Is.EqualTo("cultmesh://aetheria/world/entities.soa")); Assert.That(projection.PlayableWorld.EntityViewSchema, Is.EqualTo(EveEntitySoaViewDocument.SchemaId)); - Assert.That(projection.PlayableWorld.EntityBodyId, Is.EqualTo("eve:entity-soa:aetheria.daemon:pilot")); Assert.That(projection.PlayableWorld.ZoneRenderPointerId, Is.EqualTo("cultmesh://aetheria/world/zone-render")); Assert.That(projection.PlayableWorld.AssetManifest, Is.EqualTo("cultmesh://aetheria/assets/manifest")); - Assert.That(projection.PlayableWorld.AssetProviderId, Is.EqualTo("aetheria.assets")); - Assert.That(projection.PlayableWorld.AssetVerseId, Is.EqualTo("aetheria.mods")); - Assert.That(projection.PlayableWorld.AssetAuthorityRuntimeId, Is.EqualTo("mod-authority")); - Assert.That(projection.PlayableWorld.AssetRendezvousEndpoints, Is.EqualTo(new[] { "wss://odin-a/cultmesh", "wss://odin-b/cultmesh" })); Assert.That(projection.PlayableWorld.InputProfile, Is.EqualTo("arpg-third-person")); - Assert.That(projection.PlayableWorld.CameraRig, Is.EqualTo("planar.top-down-follow.v1")); - Assert.That(projection.PlayableWorld.CameraLookAt, Is.Empty); - Assert.That(projection.PlayableWorld.CameraTargetEntityId, Is.EqualTo("player-vanguard")); - Assert.That(projection.PlayableWorld.CameraDistance, Is.EqualTo(150f)); - Assert.That(projection.PlayableWorld.CameraVerticalFieldOfViewDegrees, Is.EqualTo(60f)); - Assert.That(projection.PlayableWorld.CameraTargetScreenX, Is.EqualTo(0.9f)); - Assert.That(projection.PlayableWorld.CameraTargetScreenY, Is.EqualTo(0.55f)); - Assert.That(projection.PlayableWorld.CameraPositionDamping, Is.EqualTo(5f)); - Assert.That(projection.PlayableWorld.CameraNearClipPlane, Is.EqualTo(0.3f)); - Assert.That(projection.PlayableWorld.CameraFarClipPlane, Is.EqualTo(4096f)); - Assert.That(projection.PlayableWorld.AmbientLightR, Is.EqualTo(0.2f)); - Assert.That(projection.PlayableWorld.AmbientLightG, Is.EqualTo(0.2f)); - Assert.That(projection.PlayableWorld.AmbientLightB, Is.EqualTo(0.2f)); - Assert.That(projection.PlayableWorld.AmbientLightIntensity, Is.EqualTo(1.46f)); - Assert.That(projection.PlayableWorld.SkyboxAssetRef, Is.Empty); - Assert.That(projection.PlayableWorld.ReflectionAssetRef, Is.Empty); - Assert.That(projection.PlayableWorld.PostProcessProfileAssetRef, Is.Empty); - Assert.That(projection.PlayableWorld.ReflectionIntensity, Is.EqualTo(1f)); - Assert.That(projection.PlayableWorld.ExcludedRenderChannels, Is.EqualTo(new[] { "map" })); + Assert.That(projection.PlayableWorld.CameraRig, Is.EqualTo("third-person-orbit")); Assert.That(projection.PlayableWorld.PlayerEntityId, Is.EqualTo("player-vanguard")); + Assert.That(projection.PlayableWorld.CameraTargetEntityId, Is.EqualTo("player-vanguard")); + Assert.That(projection.PlayableWorld.SubjectVisible, Is.True); + Assert.That(projection.PlayableWorld.MovementEnabled, Is.True); + Assert.That(projection.PlayableWorld.PresentationMode, Is.EqualTo("world")); Assert.That(projection.PlayableWorld.MovementCommand, Is.EqualTo("aetheria.daemon.move_intent")); Assert.That(projection.PlayableWorld.FocusCommand, Is.EqualTo("aetheria.daemon.focus")); Assert.That(projection.PlayableWorld.TargetCommand, Is.EqualTo("aetheria.daemon.target")); Assert.That(projection.PlayableWorld.EntityCount, Is.EqualTo(3)); - Assert.That(projection.PlayableWorld.FieldVolumes.Count, Is.EqualTo(1)); - Assert.That(projection.PlayableWorld.FieldVolumes[0].DocumentRef, Is.EqualTo("cultmesh://aetheria/world/fog-splats")); - Assert.That(projection.PlayableWorld.FieldVolumes[0].MaterialAssetRef, Is.EqualTo("shader.environment.gravity-fog")); - Assert.That(projection.PlayableWorld.FieldVolumes[0].RenderChannel, Is.EqualTo("world.transparent")); - Assert.That(projection.PlayableWorld.FieldParticles.Count, Is.EqualTo(1)); - Assert.That(projection.PlayableWorld.FieldParticles[0].DocumentRef, - Is.EqualTo("cultmesh://aetheria/world/fog-splats")); - Assert.That(projection.PlayableWorld.FieldParticles[0].ComputeProgramAssetRef, - Is.EqualTo("compute.environment.stardust")); - Assert.That(projection.PlayableWorld.FieldParticles[0].MaterialAssetRef, - Is.EqualTo("material.environment.stardust")); var player = FindEntity(projection.PlayableWorld, "player-vanguard"); Assert.That(player.EntityKind, Is.EqualTo("player")); @@ -717,18 +220,6 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(moveVectorIntent.Payload.GetString("directionX"), Is.EqualTo("0.25")); Assert.That(moveVectorIntent.Payload.GetString("directionY"), Is.EqualTo("0.75")); Assert.That(moveVectorIntent.Payload.GetString("scalarValue"), Is.EqualTo("0.8")); - - var lookIntent = session.CreateLookDirectionIntent( - "player-vanguard", - 0.6f, - 0f, - 0.8f, - DateTimeOffset.Parse("2026-07-09T00:00:02Z")); - Assert.That(lookIntent.Payload.GetString("commandId"), Is.EqualTo("aetheria.daemon.look_intent")); - Assert.That(lookIntent.Payload.GetString("entityId"), Is.EqualTo("player-vanguard")); - Assert.That(lookIntent.Payload.GetString("directionX"), Is.EqualTo("0.6")); - Assert.That(lookIntent.Payload.GetString("directionY"), Is.EqualTo("0")); - Assert.That(lookIntent.Payload.GetString("directionZ"), Is.EqualTo("0.8")); } [Test] @@ -851,11 +342,25 @@ namespace GameCult.Eve.UnityScene.Tests [Test] public void PlayableWorldRuntimeComposesProviderDocumentsAssetsReceiptsAndSceneSink() { - var surfaceDocuments = new FakeProviderSurfaceDocumentSource(new EveUnitySceneProviderSurfaceDocument( - PlayableArpgDocument(), - Advertisement("aetheria.daemon.game"), - "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", - 1)); + var surfaceDocuments = new FakeProviderSurfaceDocumentSource( + new EveUnitySceneProviderSurfaceDocument( + PlayableArpgDocument(), + Advertisement("aetheria.daemon.game"), + "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", + 1), + currentInputCapability: new EveInputCapabilityDocument + { + ProviderId = "aetheria", + Actions = new[] + { + new EveInputActionDocument + { + ActionId = "weapon-group.2.fire", + Operation = "aetheria.daemon.commands.FireWeaponGroup", + Payload = new Dictionary { ["weaponGroup"] = "2" } + } + } + }); var assetDocuments = new FakeAssetManifestDocumentSource(new EveUnityPlayableWorldAssetManifestDocument( "cultmesh://aetheria/assets/manifest", new[] @@ -905,6 +410,14 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(moveIntent.CommandBoundary, Is.EqualTo("aetheria.daemon.commands")); Assert.That(moveIntent.ReceiptSchema, Is.EqualTo("aetheria.eve_command_acceptance_status.v1")); + var actionIntent = runtime.SubmitActionIntent("player-vanguard", "weapon-group.2.fire"); + + Assert.That(commandSink.Submitted.Count, Is.EqualTo(2)); + Assert.That(commandSink.Submitted[1], Is.SameAs(actionIntent)); + Assert.That(actionIntent.Payload.GetString("commandId"), Is.EqualTo("aetheria.daemon.commands.FireWeaponGroup")); + Assert.That(actionIntent.Payload.GetString("weaponGroup"), Is.EqualTo("2")); + Assert.That(actionIntent.Payload.GetString("actionId"), Is.EqualTo("weapon-group.2.fire")); + surfaceDocuments.Publish(new EveUnitySceneProviderSurfaceDocument( PlayableArpgDocument(includeRaider: false, playerPosition: "14,0,9"), Advertisement("aetheria.daemon.game"), @@ -925,19 +438,61 @@ namespace GameCult.Eve.UnityScene.Tests "aetheria.daemon.move_intent", "accepted", "Aetheria", - "provider-owned-daemon", - navigation: new EveUnitySceneNavigationTarget( - "gamecult.aetheria", - "aetheria", - "aetheria.pilot", - "interactive-world"))); + "provider-owned-daemon")); Assert.That(runtime.LastReceipt, Is.Not.Null); Assert.That(runtime.LastReceipt!.IsProviderOwned, Is.True); Assert.That(runtime.LastReceipt.ShouldRefreshProviderSurface, Is.True); - Assert.That(runtime.LastReceipt.Navigation, Is.Not.Null); - Assert.That(runtime.LastReceipt.Navigation!.VerseId, Is.EqualTo("gamecult.aetheria")); - Assert.That(runtime.LastReceipt.Navigation.SurfaceId, Is.EqualTo("aetheria.pilot")); + } + + [Test] + public void PlayableWorldRuntimeReportsAuthoritativeSoaEntityCountOnConnect() + { + var location = $"eve-unity-runtime-soa-{Guid.NewGuid():N}"; + using var mapped = MemoryMappedFile.CreateNew(location, 16); + using (var writer = mapped.CreateViewAccessor()) + { + writer.Write(0, 4f); + writer.Write(4, 5f); + writer.Write(8, 6f); + writer.Write(12, 41); + } + var entityView = new EveEntitySoaViewDocument + { + ProviderId = "aetheria", + ViewId = "pilot", + Generation = 1, + Backend = "memory_mapped_file", + Buffers = new[] { new EveEntitySoaBuffer { BufferId = "hot", Backend = "memory_mapped_file", Location = location, ByteLength = 16, Generation = 1 } }, + Columns = new[] + { + new EveEntitySoaColumn { ColumnId = "position", Semantic = "transform.position", BufferId = "hot", ScalarType = "float3", ElementStride = 12, ElementCount = 1 }, + new EveEntitySoaColumn { ColumnId = "entity-index", Semantic = "entity.index", BufferId = "hot", ScalarType = "int32", ByteOffset = 12, ElementStride = 4, ElementCount = 1 } + }, + Identities = new[] + { + new EveEntitySoaIdentity { EntityIndex = 41, EntityId = "world:entity:41", Kind = "ship", Label = "Test ship", Controllable = true, AssetRef = "prefab.entity.player" } + } + }; + var surfaceDocuments = new FakeProviderSurfaceDocumentSource( + new EveUnitySceneProviderSurfaceDocument( + PlayableArpgDocument(), + Advertisement("aetheria.daemon.game"), + "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", + 1), + entityView); + var sceneSink = new FakePlayableWorldSceneSink(); + + using var runtime = new EveUnityPlayableWorldRuntime( + surfaceDocuments, + new FakeCommandSink("cultmesh-command-sink"), + sceneSink); + + var presentation = runtime.Connect(); + + Assert.That(presentation.ActiveEntities, Is.EqualTo(1)); + Assert.That(runtime.LastPresentation!.ActiveEntities, Is.EqualTo(1)); + Assert.That(sceneSink.Upserts.Exists(entry => entry.entity.EntityId == "world:entity:41"), Is.True); } [Test] @@ -1015,37 +570,6 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(transport.RefreshCount, Is.EqualTo(0)); } - [Test] - public void LiveProviderBridgeRollsBackSubscriptionsWhenTransportConnectFails() - { - var transport = new FakeLiveProviderTransport( - new EveUnitySceneProviderSurfaceDocument( - PlayableArpgDocument(), - Advertisement("aetheria.daemon.game"), - "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", - 1), - new EveUnityPlayableWorldAssetManifestDocument( - "cultmesh://aetheria/assets/manifest", - Array.Empty(), - "aetheria")) - { - FailNextConnect = true - }; - using var bridge = new EveUnitySceneLiveProviderBridge(transport); - - Assert.Throws(() => bridge.Connect()); - Assert.That(transport.SurfaceSubscriberCount, Is.Zero); - Assert.That(transport.AssetManifestSubscriberCount, Is.Zero); - Assert.That(transport.ReceiptSubscriberCount, Is.Zero); - - bridge.Connect(); - - Assert.That(transport.ConnectCount, Is.EqualTo(2)); - Assert.That(transport.SurfaceSubscriberCount, Is.EqualTo(1)); - Assert.That(transport.AssetManifestSubscriberCount, Is.EqualTo(1)); - Assert.That(transport.ReceiptSubscriberCount, Is.EqualTo(1)); - } - [Test] public void PlayableWorldClientHostMountsInterfaceProviderWithoutProviderTypes() { @@ -1131,9 +655,6 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(bootstrap.SceneRoot!.childCount, Is.EqualTo(3)); Assert.That(hostObject.GetComponent(), Is.Not.Null); Assert.That(hostObject.GetComponent(), Is.Not.Null); - Assert.That(hostObject.GetComponent(), Is.Not.Null); - Assert.That(hostObject.GetComponent().Root, Is.Not.Null); - Assert.That(hostObject.GetComponent().panelSettings.themeStyleSheet, Is.Not.Null); Assert.That(bootstrap.CameraTransform, Is.Not.Null); Assert.That(provider.RefreshCount, Is.EqualTo(1)); @@ -1155,204 +676,6 @@ namespace GameCult.Eve.UnityScene.Tests } } - [UnityTest] - public IEnumerator PlayableWorldClientBootstrapFollowsProviderOwnedReceiptNavigation() - { - var hostObject = new GameObject("generic-eve-navigation-client"); - try - { - var provider = hostObject.AddComponent(); - provider.Set( - new EveUnitySceneProviderSurfaceDocument( - PlayableArpgDocument(), - Advertisement("aetheria.daemon.game"), - "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", - 1), - new EveUnityPlayableWorldAssetManifestDocument( - "cultmesh://aetheria/assets/manifest", - Array.Empty(), - "aetheria")); - var bootstrap = hostObject.AddComponent(); - bootstrap.ConfigureProvider(provider); - bootstrap.Mount(); - var originalHost = bootstrap.Host; - var originalRoot = bootstrap.SceneRoot!; - provider.PreviousPresentationActive = () => originalRoot.gameObject.activeSelf; - - provider.PublishReceipt(new EveUnitySceneCommandReceipt( - "receipt:launch", - "aetheria.hangar.launch", - "launch", - "accepted", - "Aetheria", - "commander-daemon", - navigation: new EveUnitySceneNavigationTarget( - "gamecult.aetheria", - "aetheria.daemon", - "aetheria.pilot", - "interactive-world", - new[] { "cultnet+tcp://odin.example:3076" }))); - - yield return null; - yield return null; - - Assert.That(provider.LastNavigation, Is.Not.Null); - Assert.That(provider.LastNavigation!.VerseId, Is.EqualTo("gamecult.aetheria")); - Assert.That(provider.LastNavigation.RendezvousEndpoints, Is.EqualTo(new[] { "cultnet+tcp://odin.example:3076" })); - Assert.That(bootstrap.Host, Is.Not.SameAs(originalHost)); - Assert.That(bootstrap.Host!.ConnectionEpoch, Is.EqualTo(1)); - Assert.That(provider.CommitNavigationCount, Is.EqualTo(1)); - Assert.That(provider.FinalizeNavigationCount, Is.EqualTo(1)); - Assert.That(provider.RollbackNavigationCount, Is.Zero); - Assert.That(provider.PreviousPresentationActiveDuringCommit, Is.False); - - provider.PublishReceipt(new EveUnitySceneCommandReceipt( - "receipt:launch-again", - "aetheria.hangar.launch", - "launch", - "accepted", - "Aetheria", - "commander-daemon", - navigation: new EveUnitySceneNavigationTarget( - "gamecult.aetheria", - "aetheria.daemon", - "aetheria.pilot", - "interactive-world", - new[] { "cultnet+tcp://odin.example:3076" }))); - - yield return null; - yield return null; - - Assert.That(provider.CommitNavigationCount, Is.EqualTo(2)); - Assert.That(provider.FinalizeNavigationCount, Is.EqualTo(2)); - Assert.That(provider.ActiveConnectionLeases, Is.EqualTo(1), - "disposing superseded presentations must leave the committed Verse connected"); - Assert.That(hostObject.transform.Cast() - .Count(child => child.name == "Eve Unity Candidate Presentation"), Is.EqualTo(1)); - } - finally - { - UnityEngine.Object.DestroyImmediate(hostObject); - } - } - - [UnityTest] - public IEnumerator PlayableWorldClientBootstrapKeepsMountedPresentationAfterNavigationMountFailure() - { - var hostObject = new GameObject("generic-eve-navigation-rollback-client"); - try - { - var provider = hostObject.AddComponent(); - provider.Set( - new EveUnitySceneProviderSurfaceDocument( - PlayableArpgDocument(), - Advertisement("aetheria.daemon.game"), - "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", - 1), - new EveUnityPlayableWorldAssetManifestDocument( - "cultmesh://aetheria/assets/manifest", - Array.Empty(), - "aetheria")); - var bootstrap = hostObject.AddComponent(); - bootstrap.ConfigureProvider(provider); - bootstrap.Mount(); - var originalHost = bootstrap.Host; - var originalRuntime = originalHost!.Runtime; - var originalRoot = bootstrap.SceneRoot; - var originalPresentation = bootstrap.LastPresentation; - provider.FailNextNavigationMount = true; - LogAssert.Expect(LogType.Error, new Regex("Eve provider navigation failed; the current surface remains mounted")); - - provider.PublishReceipt(new EveUnitySceneCommandReceipt( - "receipt:launch-failure", - "aetheria.hangar.launch", - "launch", - "accepted", - "Aetheria", - "commander-daemon", - navigation: new EveUnitySceneNavigationTarget( - "gamecult.aetheria", - "broken-provider", - "aetheria.pilot", - "interactive-world", - new[] { "cultnet+tcp://odin.example:3999" }))); - - yield return null; - yield return null; - - Assert.That(provider.CommitNavigationCount, Is.Zero); - Assert.That(provider.RollbackNavigationCount, Is.EqualTo(1)); - Assert.That(bootstrap.LastNavigationFailure, Is.Not.Null); - Assert.That(bootstrap.Host, Is.SameAs(originalHost)); - Assert.That(bootstrap.Host!.Runtime, Is.SameAs(originalRuntime)); - Assert.That(bootstrap.SceneRoot, Is.SameAs(originalRoot)); - Assert.That(bootstrap.LastPresentation, Is.SameAs(originalPresentation)); - Assert.That(bootstrap.Host!.Runtime, Is.Not.Null); - Assert.That(bootstrap.Host.ActiveWorld, Is.Not.Null); - Assert.That(bootstrap.Host.ActiveWorld!.PlayerEntityId, Is.EqualTo("player-vanguard")); - Assert.That(bootstrap.Host.ConnectionEpoch, Is.EqualTo(1)); - } - finally - { - UnityEngine.Object.DestroyImmediate(hostObject); - } - } - - [UnityTest] - public IEnumerator PlayableWorldClientBootstrapRestoresProviderRouteBeforeReactivatingPreviousView() - { - var hostObject = new GameObject("generic-eve-navigation-order-client"); - try - { - var provider = hostObject.AddComponent(); - provider.Set( - new EveUnitySceneProviderSurfaceDocument( - PlayableArpgDocument(), - Advertisement("aetheria.daemon.game"), - "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", - 1), - new EveUnityPlayableWorldAssetManifestDocument( - "cultmesh://aetheria/assets/manifest", - Array.Empty(), - "aetheria")); - var bootstrap = hostObject.AddComponent(); - bootstrap.ConfigureProvider(provider); - bootstrap.Mount(); - var originalRoot = bootstrap.SceneRoot!; - provider.PreviousPresentationActive = () => originalRoot.gameObject.activeSelf; - provider.FailNextNavigationFinalize = true; - LogAssert.Expect(LogType.Error, new Regex("Eve provider navigation failed; the current surface remains mounted")); - - provider.PublishReceipt(new EveUnitySceneCommandReceipt( - "receipt:launch-finalize-failure", - "aetheria.hangar.launch", - "launch", - "accepted", - "Aetheria", - "commander-daemon", - navigation: new EveUnitySceneNavigationTarget( - "gamecult.aetheria", - "candidate-provider", - "aetheria.pilot", - "interactive-world", - new[] { "cultnet+tcp://odin.example:3999" }))); - - yield return null; - yield return null; - - Assert.That(provider.CommitNavigationCount, Is.EqualTo(1)); - Assert.That(provider.RollbackNavigationCount, Is.EqualTo(1)); - Assert.That(provider.ActiveRoute, Is.EqualTo("original")); - Assert.That(provider.PreviousPresentationActiveDuringRollback, Is.False); - Assert.That(originalRoot.gameObject.activeSelf, Is.True); - Assert.That(bootstrap.Host!.Runtime, Is.Not.Null); - } - finally - { - UnityEngine.Object.DestroyImmediate(hostObject); - } - } - [Test] public void PlayableWorldClientHostSubmitsProviderOwnedMoveVector() { @@ -1435,31 +758,6 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(request.Payload.GetDouble("directionY", 0), Is.EqualTo(0f).Within(0.0001f)); Assert.That(request.Payload.GetDouble("scalarValue", 0), Is.EqualTo(1f).Within(0.0001f)); - var controlledYaw = rootObject.GetComponentsInChildren(true) - .Single(marker => marker.EntityId == "player-vanguard") - .transform.eulerAngles.y * Mathf.Deg2Rad; - driver.QueueLookInput(250f); - driver.QueueLookInput(250f); - driver.QueueLookInput(500f); - var look = driver.SubmitPendingLookInput(); - Assert.That(look, Is.Not.Null); - Assert.That(provider.Submitted.Count, Is.EqualTo(2)); - Assert.That(look!.Payload.GetString("commandId"), Is.EqualTo("aetheria.daemon.look_intent")); - Assert.That(look.Payload.GetDouble("directionX", 0), Is.EqualTo(Mathf.Sin(controlledYaw - 1f)).Within(0.0001f)); - Assert.That(look.Payload.GetDouble("directionZ", 0), Is.EqualTo(Mathf.Cos(controlledYaw - 1f)).Within(0.0001f)); - - host.Connect(); - var rebasedYaw = rootObject.GetComponentsInChildren(true) - .Single(marker => marker.EntityId == "player-vanguard") - .transform.eulerAngles.y * Mathf.Deg2Rad; - var rebasedLook = driver.SubmitLookInput(1000f); - Assert.That(rebasedLook, Is.Not.Null); - Assert.That(provider.Submitted.Count, Is.EqualTo(3)); - Assert.That(rebasedLook!.Payload.GetDouble("directionX", 0), - Is.EqualTo(Mathf.Sin(rebasedYaw - 1f)).Within(0.0001f)); - Assert.That(rebasedLook.Payload.GetDouble("directionZ", 0), - Is.EqualTo(Mathf.Cos(rebasedYaw - 1f)).Within(0.0001f)); - var movement = EveUnityPlayableWorldMoveVector.FromCameraRelativeInput(1f, 1f, null); Assert.That(movement.HasInput, Is.True); Assert.That(movement.ScalarValue, Is.EqualTo(1f).Within(0.0001f)); @@ -1472,329 +770,20 @@ namespace GameCult.Eve.UnityScene.Tests } } - [Test] - public void PlayableWorldInputDriverUsesAdvertisedPrimaryBinding() - { - var capability = new EveInputCapabilityDocument - { - Actions = new[] - { - new EveInputActionDocument - { - ActionId = "weapon-group.0.fire", - Availability = "available" - } - }, - DefaultProfiles = new[] - { - new EveInputProfileDocument - { - ProfileId = "keyboard-mouse", - DeviceClass = "keyboard-mouse", - Bindings = new[] - { - new EveInputBindingDocument - { - ActionId = "weapon-group.0.fire", - Gesture = new EveInputGestureDocument - { - Controls = new[] { "mouse.primary" } - }, - ActionBar = true - } - } - } - } - }; - - Assert.That( - EveUnityPlayableWorldInputDriver.ResolvePrimaryActionId(capability), - Is.EqualTo("weapon-group.0.fire")); - capability.Actions[0].Availability = "unavailable"; - Assert.That(EveUnityPlayableWorldInputDriver.ResolvePrimaryActionId(capability), Is.Empty); - } - - [Test] - public void PlayableWorldInputDriverPerformsAdvertisedViewDirectionBindingOnPressEdge() - { - var rootObject = new GameObject("generic-eve-world-root"); - var hostObject = new GameObject("generic-eve-client"); - var cameraObject = new GameObject("generic-eve-camera"); - hostObject.SetActive(false); - - try - { - cameraObject.transform.rotation = Quaternion.Euler(30f, 90f, 0f); - var capability = new EveInputCapabilityDocument - { - ProviderId = "generic-provider", - CapabilityId = "pilot.input", - Actions = new[] - { - new EveInputActionDocument - { - ActionId = "pilot.target-reticle", - Operation = "generic.commands.TargetReticle", - Availability = "available", - InputValue = new EveInputValueDocument - { - Model = EveUnityAdvertisedInputAction.ViewDirectionValueModel, - PayloadKeys = new[] { "directionX", "directionY", "directionZ" } - } - } - }, - DefaultProfiles = new[] - { - new EveInputProfileDocument - { - ProfileId = "keyboard-mouse", - DeviceClass = "keyboard-mouse", - Bindings = new[] - { - new EveInputBindingDocument - { - BindingId = "target.reticle.r", - ActionId = "pilot.target-reticle", - Gesture = new EveInputGestureDocument - { - Kind = "direct", - Controls = new[] { "keyboard.r" } - } - } - } - } - } - }; - var provider = hostObject.AddComponent(); - provider.Set( - new EveUnitySceneProviderSurfaceDocument( - PlayableArpgDocument(), - Advertisement("aetheria.daemon.game"), - "cultmesh://generic/eve/surfaces/game", - 1), - new EveUnityPlayableWorldAssetManifestDocument( - "cultmesh://generic/assets/manifest", - Array.Empty(), - "generic-provider"), - capability); - var host = hostObject.AddComponent(); - host.Configure(rootObject.transform, provider, provider, provider, provider); - host.Connect(); - var pressed = false; - var driver = hostObject.AddComponent(); - driver.Host = host; - driver.CameraTransform = cameraObject.transform; - driver.DigitalControlReader = control => control == "keyboard.r" ? pressed : (bool?)null; - - driver.SubmitChangedAdvertisedActions(); - pressed = true; - driver.SubmitChangedAdvertisedActions(); - driver.SubmitChangedAdvertisedActions(); - - Assert.That(provider.Submitted.Count, Is.EqualTo(1)); - var request = provider.Submitted.Single(); - Assert.That(request.Payload.GetString("commandId"), Is.EqualTo("generic.commands.TargetReticle")); - Assert.That(request.Payload.GetString("entityId"), Is.EqualTo("player-vanguard")); - Assert.That(request.Payload.GetString("actionId"), Is.EqualTo("pilot.target-reticle")); - var expected = cameraObject.transform.forward.normalized; - Assert.That(request.Payload.GetDouble("directionX", 0), Is.EqualTo(expected.x).Within(0.000001)); - Assert.That(request.Payload.GetDouble("directionY", 0), Is.EqualTo(expected.y).Within(0.000001)); - Assert.That(request.Payload.GetDouble("directionZ", 0), Is.EqualTo(expected.z).Within(0.000001)); - - pressed = false; - driver.SubmitChangedAdvertisedActions(); - pressed = true; - driver.SubmitChangedAdvertisedActions(); - Assert.That(provider.Submitted.Count, Is.EqualTo(2), - "a released and re-pressed generic binding must perform exactly once per press edge"); - } - finally - { - UnityEngine.Object.DestroyImmediate(cameraObject); - UnityEngine.Object.DestroyImmediate(hostObject); - UnityEngine.Object.DestroyImmediate(rootObject); - } - } - - [Test] - public void GenericActionBarSelectsSuggestionsAndSubmitsScalarThroughAdvertisedOperation() - { - var rootObject = new GameObject("generic-eve-world-root"); - var hostObject = new GameObject("generic-eve-client"); - hostObject.SetActive(false); - - try - { - var capability = new EveInputCapabilityDocument - { - ProviderId = "generic-provider", - CapabilityId = "pilot.input", - Actions = new[] - { - new EveInputActionDocument - { - ActionId = "equipment.0.temperature", - Label = "Target Temperature", - Operation = "generic.commands.SetTemperature", - Availability = "available", - ActionBar = true, - IconRef = "item.thermostat.icon", - Payload = new Dictionary(StringComparer.Ordinal) - { - ["equipmentIndex"] = "0", - ["behaviorIndex"] = "3" - }, - InputValue = new EveInputValueDocument - { - Model = EveUnityAdvertisedInputAction.ScalarValueModel, - PayloadKey = "scalarValue", - CurrentValue = 300, - Unit = "kelvin" - } - }, - new EveInputActionDocument - { - ActionId = "pilot.interact", - Label = "Interact", - Operation = "generic.commands.Interact", - Availability = "available" - }, - new EveInputActionDocument - { - ActionId = "pilot.hidden", - Label = "Hidden", - Operation = "generic.commands.Hidden", - Availability = "available" - } - }, - DefaultProfiles = new[] - { - new EveInputProfileDocument - { - ProfileId = "keyboard-mouse", - DeviceClass = "keyboard-mouse", - Bindings = new[] - { - new EveInputBindingDocument - { - BindingId = "interact.f", - ActionId = "pilot.interact", - ActionBar = true, - Gesture = new EveInputGestureDocument - { - Kind = "direct", - Controls = new[] { "keyboard.f" } - } - } - } - } - } - }; - Assert.That(EveUnityInputActionBar.SelectActions(capability) - .Select(action => action.ActionId), - Is.EqualTo(new[] { "pilot.interact", "equipment.0.temperature" })); - - var provider = hostObject.AddComponent(); - provider.Set( - new EveUnitySceneProviderSurfaceDocument( - PlayableArpgDocument(), - Advertisement("aetheria.daemon.game"), - "cultmesh://generic/eve/surfaces/game", - 1), - new EveUnityPlayableWorldAssetManifestDocument( - "cultmesh://generic/assets/manifest", - Array.Empty(), - "generic-provider"), - capability); - var host = hostObject.AddComponent(); - host.Configure(rootObject.transform, provider, provider, provider, provider); - host.Connect(); - var bar = hostObject.GetComponent(); - Assert.That(bar, Is.Not.Null); - bar!.RefreshNow(); - Assert.That(bar.PresentedActionIds, - Is.EquivalentTo(new[] { "pilot.interact", "equipment.0.temperature" })); - - bar.SubmitScalar("equipment.0.temperature", "425.5"); - - var request = provider.Submitted.Single(); - Assert.That(request.Payload.GetString("commandId"), Is.EqualTo("generic.commands.SetTemperature")); - Assert.That(request.Payload.GetString("entityId"), Is.EqualTo("player-vanguard")); - Assert.That(request.Payload.GetString("actionId"), Is.EqualTo("equipment.0.temperature")); - Assert.That(request.Payload.GetString("equipmentIndex"), Is.EqualTo("0")); - Assert.That(request.Payload.GetDouble("scalarValue", 0), Is.EqualTo(425.5).Within(0.000001)); - } - finally - { - UnityEngine.Object.DestroyImmediate(hostObject); - UnityEngine.Object.DestroyImmediate(rootObject); - } - } - [Test] public void PlayableWorldCameraRigFollowsAdvertisedPlayerEntityWithoutProviderTypes() { var rootObject = new GameObject("generic-eve-world-root"); var hostObject = new GameObject("generic-eve-client"); var cameraObject = new GameObject("generic-eve-camera"); - var competingCameraObject = new GameObject("competing-eve-camera"); - var secondaryRootObject = new GameObject("secondary-eve-world-root"); - var secondaryHostObject = new GameObject("secondary-eve-client"); - var secondaryCameraObject = new GameObject("secondary-eve-camera"); hostObject.SetActive(false); - secondaryHostObject.SetActive(false); - var ambientMode = RenderSettings.ambientMode; - var ambientLight = RenderSettings.ambientLight; - var ambientIntensity = RenderSettings.ambientIntensity; - var previousSkybox = RenderSettings.skybox; - var previousReflectionMode = RenderSettings.defaultReflectionMode; - var previousCustomReflection = RenderSettings.customReflectionTexture; - var previousReflectionIntensity = RenderSettings.reflectionIntensity; - var previousDefaultPipeline = GraphicsSettings.defaultRenderPipeline; - var previousQualityPipeline = QualitySettings.renderPipeline; - var gradingRendererData = ScriptableObject.CreateInstance(); - var gradingPipelineAsset = UniversalRenderPipelineAsset.Create(gradingRendererData); - gradingPipelineAsset.colorGradingMode = ColorGradingMode.LowDynamicRange; - GraphicsSettings.defaultRenderPipeline = gradingPipelineAsset; - QualitySettings.renderPipeline = gradingPipelineAsset; - var previousGradingMode = gradingPipelineAsset.colorGradingMode; - Material? skyboxMaterial = null; - Cubemap? reflectionCubemap = null; - VolumeProfile? postProcessProfile = null; try { var provider = hostObject.AddComponent(); - var nativeAssets = hostObject.AddComponent(); - var skyboxShader = Shader.Find("Skybox/Procedural"); - Assert.That(skyboxShader, Is.Not.Null); - skyboxMaterial = new Material(skyboxShader!); - reflectionCubemap = new Cubemap(4, TextureFormat.RGBA32, false); - postProcessProfile = ScriptableObject.CreateInstance(); - nativeAssets.Set("material.environment.skybox", skyboxMaterial); - nativeAssets.Set("texture.environment.reflection", reflectionCubemap); - nativeAssets.Set("profile.environment.flight", postProcessProfile); provider.Set( new EveUnitySceneProviderSurfaceDocument( - PlayableArpgDocument( - skyboxAssetRef: "material.environment.skybox", - reflectionAssetRef: "texture.environment.reflection", - postProcessProfileAssetRef: "profile.environment.flight", - cameraReconstruction: "temporal-reprojection.v1", - temporalQuality: "high", - temporalHistoryBlend: "0.99", - temporalJitterScale: "0.1", - temporalSharpening: "0", - exposureMode: "histogram.v1", - exposureLowPercent: "47.37294", - exposureHighPercent: "99", - exposureMinimumEv: "-3", - exposureMaximumEv: "0.3", - exposureKeyValue: "0.5", - exposureAdaptation: "progressive", - exposureSpeedUp: "2", - exposureSpeedDown: "1", - colorGradingSpace: "hdr-before-tonemap.v1"), + PlayableArpgDocument(), Advertisement("aetheria.daemon.game"), "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", 1), @@ -1804,237 +793,22 @@ namespace GameCult.Eve.UnityScene.Tests "aetheria")); var host = hostObject.AddComponent(); - host.Configure(rootObject.transform, provider, provider, provider, provider, nativeAssets); + host.Configure(rootObject.transform, provider, provider, provider, provider); host.Connect(); - hostObject.SetActive(true); - var exposedWorld = host.ActiveProjection?.PlayableWorld; - Assert.That(exposedWorld, Is.Not.Null); - Assert.That(exposedWorld!.ExposureMode, Is.EqualTo("histogram.v1")); - Assert.That(exposedWorld.ColorGradingSpace, Is.EqualTo("hdr-before-tonemap.v1")); - Assert.That(exposedWorld.ExposureLowPercent, Is.EqualTo(47.37294f).Within(0.00001f)); - Assert.That(exposedWorld.ExposureHighPercent, Is.EqualTo(99f)); - Assert.That(exposedWorld.ExposureMinimumEv, Is.EqualTo(-3f)); - Assert.That(exposedWorld.ExposureMaximumEv, Is.EqualTo(0.3f).Within(0.00001f)); - Assert.That(exposedWorld.ExposureKeyValue, Is.EqualTo(0.5f)); - Assert.That(exposedWorld.ExposureAdaptation, Is.EqualTo("progressive")); - Assert.That(exposedWorld.ExposureSpeedUp, Is.EqualTo(2f)); - Assert.That(exposedWorld.ExposureSpeedDown, Is.EqualTo(1f)); var rig = hostObject.AddComponent(); rig.Host = host; rig.CameraTransform = cameraObject.transform; - rig.RenderPolicySource = new FixedRenderChannelPolicy("map", 14); - var camera = cameraObject.AddComponent(); - camera.clearFlags = CameraClearFlags.SolidColor; - camera.aspect = 16f / 9f; - camera.orthographic = true; - camera.lensShift = new Vector2(0.25f, -0.2f); var player = rootObject.GetComponentInChildren(); Assert.That(player, Is.Not.Null); var largeVisual = GameObject.CreatePrimitive(PrimitiveType.Cube); largeVisual.transform.SetParent(player.transform, false); largeVisual.transform.localScale = Vector3.one * 100f; - largeVisual.layer = 14; Assert.That(rig.ApplyRig(0f), Is.True); - Assert.That(host.ActiveCameraTransform, Is.SameAs(cameraObject.transform)); - Assert.That(camera.cullingMask & (1 << 14), Is.Zero); - Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); - Assert.That(camera.orthographic, Is.False); - Assert.That(camera.lensShift, Is.EqualTo(Vector2.zero)); - Assert.That(camera.nearClipPlane, Is.EqualTo(0.3f).Within(0.001f)); - Assert.That(camera.farClipPlane, Is.EqualTo(4096f).Within(0.001f)); - Assert.That(cameraObject.transform.position.y - player.transform.position.y, Is.EqualTo(150f).Within(0.001f)); - var viewport = camera.WorldToViewportPoint(player.transform.position); - Assert.That(viewport.x, Is.EqualTo(0.9f).Within(0.001f)); - Assert.That(viewport.y, Is.EqualTo(0.55f).Within(0.001f)); - Assert.That(RenderSettings.ambientMode, Is.EqualTo(UnityEngine.Rendering.AmbientMode.Skybox)); - Assert.That(RenderSettings.ambientIntensity, Is.EqualTo(1.46f).Within(0.001f)); - Assert.That(RenderSettings.skybox, Is.SameAs(skyboxMaterial)); - Assert.That(RenderSettings.defaultReflectionMode, Is.EqualTo(DefaultReflectionMode.Custom)); - Assert.That(RenderSettings.customReflectionTexture, Is.SameAs(reflectionCubemap)); - Assert.That(RenderSettings.reflectionIntensity, Is.EqualTo(1f).Within(0.001f)); - Assert.That(camera.clearFlags, Is.EqualTo(CameraClearFlags.Skybox)); - Assert.That(camera.GetComponent(), Is.Not.Null); - var cameraData = camera.GetComponent(); - Assert.That(cameraData.renderPostProcessing, Is.True); - Assert.That(cameraData.antialiasing, Is.EqualTo(AntialiasingMode.TemporalAntiAliasing)); - Assert.That(cameraData.taaSettings.quality, Is.EqualTo(TemporalAAQuality.High)); - Assert.That(cameraData.taaSettings.baseBlendFactor, Is.EqualTo(0.99f).Within(0.0001f)); - Assert.That(cameraData.taaSettings.jitterScale, Is.EqualTo(0.1f).Within(0.0001f)); - Assert.That(cameraData.taaSettings.contrastAdaptiveSharpening, Is.Zero); - var adaptiveExposure = camera.GetComponent(); - Assert.That(adaptiveExposure, Is.Not.Null); - Assert.That(adaptiveExposure.IsConfigured, Is.True); - Assert.That(gradingPipelineAsset.colorGradingMode, Is.EqualTo(ColorGradingMode.HighDynamicRange)); - var postProcessTransform = hostObject.transform.Find("Eve World Post Process"); - Assert.That(postProcessTransform, Is.Not.Null); - Assert.That(postProcessTransform.GetComponent().sharedProfile, Is.SameAs(postProcessProfile)); - var keyLightTransform = hostObject.transform.Find("Eve World Key Light"); - Assert.That(keyLightTransform, Is.Not.Null); - var keyLight = keyLightTransform.GetComponent(); - Assert.That(keyLight.type, Is.EqualTo(LightType.Directional)); - Assert.That(keyLight.intensity, Is.EqualTo(0.75f).Within(0.001f)); - Assert.That(keyLight.color, Is.EqualTo(new Color(1f, 0.95f, 0.9f, 1f))); - Assert.That(Vector3.Dot(keyLightTransform.forward, new Vector3(0.4f, -1f, 0.25f).normalized), - Is.GreaterThan(0.999f)); - Assert.That(nativeAssets.LastBinding?.AssetRef, Is.EqualTo("profile.environment.flight")); - Assert.That(nativeAssets.LastBinding?.EntityKind, Is.Empty); - - var secondaryHost = secondaryHostObject.AddComponent(); - secondaryHost.Configure(secondaryRootObject.transform, provider, provider, provider, provider, nativeAssets); - secondaryHost.Connect(); - secondaryHostObject.SetActive(true); - secondaryCameraObject.AddComponent(); - var secondaryRig = secondaryCameraObject.AddComponent(); - secondaryRig.Host = secondaryHost; - secondaryRig.CameraTransform = secondaryCameraObject.transform; - Assert.That(secondaryRig.ApplyRig(0f), Is.False); - Assert.That(secondaryHost.ActiveCameraTransform, Is.Null); - Assert.That(RenderSettings.skybox, Is.SameAs(skyboxMaterial)); - - var competingRig = competingCameraObject.AddComponent(); - competingRig.Host = host; - competingRig.CameraTransform = competingCameraObject.transform; - Assert.That(competingRig.ApplyRig(0f), Is.False); - Assert.That(host.ActiveCameraTransform, Is.SameAs(cameraObject.transform)); - - var aim = hostObject.GetComponent(); - Assert.That(aim, Is.Not.Null); - var playerPosition = player.transform.position; - var playerRotation = player.transform.rotation; - aim.RefreshNow(); - Assert.That(aim.ViewDotVisible, Is.True); - Assert.That(Vector3.Distance(playerPosition, aim.ViewDotPosition), Is.EqualTo(50f).Within(0.001f)); - Assert.That(player.transform.position, Is.EqualTo(playerPosition)); - Assert.That(player.transform.rotation, Is.EqualTo(playerRotation)); - - var selectedTarget = rootObject.GetComponentsInChildren() - .Single(marker => marker.EntityId == "raider-scout"); - Assert.That(EveUnityCombatPresentation.Find(host.ActiveProjection)?.SelectedTargetEntityId, - Is.EqualTo("raider-scout")); - var poseWithoutSelectedTarget = cameraObject.transform.position; - selectedTarget.transform.position = new Vector3(10000f, 0f, -10000f); - Assert.That(rig.ApplyRig(0f), Is.True); - Assert.That(cameraObject.transform.position, Is.EqualTo(poseWithoutSelectedTarget)); - viewport = camera.WorldToViewportPoint(player.transform.position); - Assert.That(viewport.x, Is.EqualTo(0.9f).Within(0.001f)); - Assert.That(viewport.y, Is.EqualTo(0.55f).Within(0.001f)); - - var beforeFollow = cameraObject.transform.position; - player.transform.position += Vector3.right * 10f; - Assert.That(rig.ApplyRig(0.1f), Is.True); - Assert.That( - cameraObject.transform.position.x - beforeFollow.x, - Is.EqualTo(10f * (1f - Mathf.Exp(-0.5f))).Within(0.001f)); - rig.CameraTransform = null; - Assert.That(host.ActiveCameraTransform, Is.Null); - Assert.That(hostObject.transform.Find("Eve World Key Light"), Is.Null); - Assert.That(hostObject.transform.Find("Eve World Post Process"), Is.Null); - Assert.That(camera.GetComponent(), Is.Null); - Assert.That(gradingPipelineAsset.colorGradingMode, Is.EqualTo(previousGradingMode)); - aim.RefreshNow(); - Assert.That(aim.ViewDotVisible, Is.False); - Assert.That(RenderSettings.ambientMode, Is.EqualTo(ambientMode)); - Assert.That(RenderSettings.ambientLight, Is.EqualTo(ambientLight)); - Assert.That(RenderSettings.ambientIntensity, Is.EqualTo(ambientIntensity)); - Assert.That(RenderSettings.skybox, Is.SameAs(previousSkybox)); - Assert.That(RenderSettings.defaultReflectionMode, Is.EqualTo(previousReflectionMode)); - Assert.That(RenderSettings.customReflectionTexture, Is.SameAs(previousCustomReflection)); - Assert.That(RenderSettings.reflectionIntensity, Is.EqualTo(previousReflectionIntensity)); - Assert.That(camera.clearFlags, Is.EqualTo(CameraClearFlags.SolidColor)); - rig.CameraTransform = cameraObject.transform; - nativeAssets.Clear(); - nativeAssets.Set("some.other.asset", skyboxMaterial); - Assert.That(rig.ApplyRig(0f), Is.False); - Assert.That(host.ActiveCameraTransform, Is.Null); - nativeAssets.Set("material.environment.skybox", reflectionCubemap); - nativeAssets.Set("texture.environment.reflection", skyboxMaterial); - nativeAssets.Set("profile.environment.flight", skyboxMaterial); - Assert.That(rig.ApplyRig(0f), Is.False); - Assert.That(host.ActiveCameraTransform, Is.Null); - } - finally - { - RenderSettings.ambientMode = ambientMode; - RenderSettings.ambientLight = ambientLight; - RenderSettings.ambientIntensity = ambientIntensity; - RenderSettings.skybox = previousSkybox; - RenderSettings.defaultReflectionMode = previousReflectionMode; - RenderSettings.customReflectionTexture = previousCustomReflection; - RenderSettings.reflectionIntensity = previousReflectionIntensity; - QualitySettings.renderPipeline = previousQualityPipeline; - GraphicsSettings.defaultRenderPipeline = previousDefaultPipeline; - UnityEngine.Object.DestroyImmediate(gradingPipelineAsset); - UnityEngine.Object.DestroyImmediate(gradingRendererData); - if (skyboxMaterial != null) UnityEngine.Object.DestroyImmediate(skyboxMaterial); - if (reflectionCubemap != null) UnityEngine.Object.DestroyImmediate(reflectionCubemap); - if (postProcessProfile != null) UnityEngine.Object.DestroyImmediate(postProcessProfile); - UnityEngine.Object.DestroyImmediate(secondaryCameraObject); - UnityEngine.Object.DestroyImmediate(secondaryHostObject); - UnityEngine.Object.DestroyImmediate(secondaryRootObject); - UnityEngine.Object.DestroyImmediate(competingCameraObject); - UnityEngine.Object.DestroyImmediate(cameraObject); - UnityEngine.Object.DestroyImmediate(hostObject); - UnityEngine.Object.DestroyImmediate(rootObject); - } - } - - [Test] - public void PlayableWorldCameraRigFramesEntityAndLooksAtAdvertisedConvergencePointWithoutProviderTypes() - { - var rootObject = new GameObject("generic-forward-world-root"); - var hostObject = new GameObject("generic-forward-client"); - var cameraObject = new GameObject("generic-forward-camera"); - hostObject.SetActive(false); - try - { - var provider = hostObject.AddComponent(); - var nativeAssets = hostObject.AddComponent(); - provider.Set( - new EveUnitySceneProviderSurfaceDocument( - PlayableArpgDocument( - cameraRig: "perspective.entity-forward-follow.v1", - cameraDistance: "30", - cameraTargetScreenX: "0.64", - cameraTargetScreenY: "0.81", - cameraPositionDamping: "0", - cameraNearClipPlane: "1", - cameraFarClipPlane: "4096", - cameraLookAt: "aim.convergence-point.v1"), - Advertisement("aetheria.daemon.game"), - "cultmesh://generic/eve/surfaces/forward", - 1), - new EveUnityPlayableWorldAssetManifestDocument( - "cultmesh://generic/assets/manifest", - Array.Empty(), - "generic")); - - var host = hostObject.AddComponent(); - host.Configure(rootObject.transform, provider, provider, provider, provider, nativeAssets); - host.Connect(); - hostObject.SetActive(true); - var camera = cameraObject.AddComponent(); - camera.aspect = 16f / 9f; - var rig = hostObject.AddComponent(); - rig.Host = host; - rig.CameraTransform = cameraObject.transform; - - var player = rootObject.GetComponentInChildren(); - Assert.That(player, Is.Not.Null); - Assert.That(rig.ApplyRig(0f), Is.True); - var aimPoint = player!.transform.position + player.transform.forward * 50f; - var cameraToAim = (aimPoint - cameraObject.transform.position).normalized; - Assert.That(Vector3.Dot(cameraObject.transform.forward, cameraToAim), Is.GreaterThan(0.999f)); - Assert.That(cameraObject.transform.forward.y, Is.GreaterThan(0f)); - Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); - Assert.That(camera.nearClipPlane, Is.EqualTo(1f).Within(0.001f)); - Assert.That(camera.farClipPlane, Is.EqualTo(4096f).Within(0.001f)); - var viewport = camera.WorldToViewportPoint(player.transform.position); - Assert.That(viewport.x, Is.EqualTo(0.64f).Within(0.001f)); - Assert.That(viewport.y, Is.EqualTo(0.81f).Within(0.001f)); - Assert.That(cameraObject.transform.position.y, Is.LessThan(player.transform.position.y)); + Assert.That(cameraObject.transform.position.y, Is.GreaterThan(0f)); + Assert.That(Vector3.Distance(cameraObject.transform.position, largeVisual.transform.position), Is.GreaterThan(100f)); } finally { @@ -2045,7 +819,7 @@ namespace GameCult.Eve.UnityScene.Tests } [Test] - public void SemanticScaleWrapsProviderAuthoredScaleAndFallbackGeometry() + public void ProviderPrefabKeepsAuthoredScaleWhileFallbackUsesSemanticRadius() { var root = new GameObject("world"); var prefab = new GameObject("provider-prefab"); @@ -2060,7 +834,7 @@ namespace GameCult.Eve.UnityScene.Tests "prefab.ship", "ship", "provider-asset-ref")); var instance = root.transform.GetChild(0); - Assert.That(instance.localScale, Is.EqualTo(Vector3.one * 12f)); + Assert.That(instance.localScale, Is.EqualTo(Vector3.one)); Assert.That(instance.GetChild(0).localScale, Is.EqualTo(new Vector3(2f, 3f, 4f))); var fallbackSink = new EveUnityGameObjectPlayableWorldSceneSink( @@ -2091,7 +865,7 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(firstPresentation.WorldRootId, Is.EqualTo("aetheria.daemon.game.playable")); Assert.That(firstPresentation.PlayerEntityId, Is.EqualTo("player-vanguard")); Assert.That(firstPresentation.InputProfile, Is.EqualTo("arpg-third-person")); - Assert.That(firstPresentation.CameraRig, Is.EqualTo("planar.top-down-follow.v1")); + Assert.That(firstPresentation.CameraRig, Is.EqualTo("third-person-orbit")); Assert.That(firstPresentation.UpsertedEntities, Is.EqualTo(3)); Assert.That(firstPresentation.RemovedEntities, Is.EqualTo(0)); Assert.That(firstPresentation.ActiveEntities, Is.EqualTo(3)); @@ -2188,7 +962,7 @@ namespace GameCult.Eve.UnityScene.Tests } [Test] - public void LivePlayableWorldClientReconcilesReceiptAfterReactiveStateVersionAdvances() + public void LivePlayableWorldClientWaitsForProviderSurfaceUpdateAfterReceipt() { var source = new FakeProviderSurfaceSource( "aetheria", @@ -2245,18 +1019,12 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(client.LastReceipt.State, Is.EqualTo("pending")); Assert.That(client.ActiveVersion, Is.EqualTo(1)); - client.AdvanceStateVersion(2); - - Assert.That(client.LastReceipt.State, Is.EqualTo("reconciled")); - Assert.That(client.ActiveVersion, Is.EqualTo(2)); - Assert.That(sceneSink.ConfiguredWorlds.Count, Is.EqualTo(1), - "reactive state visibility must not require republishing heavy surface topology"); - source.Publish(new EveUnitySceneProviderSurfaceSnapshot( PlayableArpgDocument(includeRaider: false, playerPosition: "30,0,30"), Advertisement("aetheria.daemon.game"), "cultmesh://aetheria/eve/surfaces/aetheria.daemon.game", 2)); + Assert.That(client.LastReceipt.State, Is.EqualTo("reconciled")); Assert.That(client.ActiveVersion, Is.EqualTo(2)); Assert.That(client.LastPresentation, Is.Not.Null); Assert.That(client.LastPresentation!.ActiveEntities, Is.EqualTo(2)); @@ -2556,33 +1324,7 @@ namespace GameCult.Eve.UnityScene.Tests private static EveSurfaceDocument PlayableArpgDocument( bool includeRaider = true, - string playerPosition = "0,0,0", - string skyboxAssetRef = "", - string reflectionAssetRef = "", - string postProcessProfileAssetRef = "", - string cameraRig = "planar.top-down-follow.v1", - string cameraDistance = "150", - string cameraTargetScreenX = "0.9", - string cameraTargetScreenY = "0.55", - string cameraPositionDamping = "5", - string cameraNearClipPlane = "0.3", - string cameraFarClipPlane = "4096", - string cameraLookAt = "", - string cameraReconstruction = "", - string temporalQuality = "high", - string temporalHistoryBlend = "0", - string temporalJitterScale = "0", - string temporalSharpening = "0", - string exposureMode = "", - string exposureLowPercent = "50", - string exposureHighPercent = "95", - string exposureMinimumEv = "0", - string exposureMaximumEv = "0", - string exposureKeyValue = "1", - string exposureAdaptation = "progressive", - string exposureSpeedUp = "2", - string exposureSpeedDown = "1", - string colorGradingSpace = "") + string playerPosition = "0,0,0") { var playableChildren = new List { @@ -2641,31 +1383,6 @@ namespace GameCult.Eve.UnityScene.Tests "", "")); - playableChildren.Add(new EveSurfaceComponent( - "aetheria.daemon.game.aim", - "aim.presentation", - new Dictionary(StringComparer.Ordinal) - { - ["controlledEntityId"] = "player-vanguard", - ["convergenceTargetEntityId"] = "", - ["viewDotRole"] = "pilot.view-direction", - ["minimumConvergenceDistance"] = "50", - ["viewDotRadius"] = "0.8" - }, - Array.Empty())); - - playableChildren.Add(new EveSurfaceComponent( - "aetheria.daemon.game.combat", - "combat.presentation", - new Dictionary(StringComparer.Ordinal) - { - ["controlledEntityId"] = "player-vanguard", - ["selectedTargetEntityId"] = "raider-scout", - ["targetVisible"] = "true", - ["targetHostile"] = "true" - }, - Array.Empty())); - playableChildren.Add(new EveSurfaceComponent( "aetheria.daemon.game.flow", "field.vector3d", @@ -2678,36 +1395,6 @@ namespace GameCult.Eve.UnityScene.Tests }, Array.Empty())); - playableChildren.Add(new EveSurfaceComponent( - "aetheria.daemon.game.gravity-fog", - "field.volume3d", - new Dictionary(StringComparer.Ordinal) - { - ["documentRef"] = "cultmesh://aetheria/world/fog-splats", - ["documentSchema"] = EveFieldsSchemas.Splats, - ["materialAssetRef"] = "shader.environment.gravity-fog", - ["renderChannel"] = "world.transparent", - ["compositeMode"] = "premultiplied-alpha", - ["quality"] = "normal", - ["layerBindings"] = "fog.surface_height=surfaceHeight;fog.tint=tint" - }, - Array.Empty())); - - playableChildren.Add(new EveSurfaceComponent( - "aetheria.daemon.game.stardust", - "field.particles3d", - new Dictionary(StringComparer.Ordinal) - { - ["documentRef"] = "cultmesh://aetheria/world/fog-splats", - ["documentSchema"] = EveFieldsSchemas.Splats, - ["computeProgramAssetRef"] = "compute.environment.stardust", - ["materialAssetRef"] = "material.environment.stardust", - ["renderChannel"] = "world.transparent", - ["span"] = "256", - ["spacing"] = "6" - }, - Array.Empty())); - return new EveSurfaceDocument( "aetheria", "game.runtime", @@ -2730,55 +1417,17 @@ namespace GameCult.Eve.UnityScene.Tests ["statePointerId"] = "cultmesh://aetheria/run/current", ["entityViewPointerId"] = "cultmesh://aetheria/world/entities.soa", ["entityViewSchema"] = EveEntitySoaViewDocument.SchemaId, - ["entityBodyId"] = "eve:entity-soa:aetheria.daemon:pilot", ["zoneRenderPointerId"] = "cultmesh://aetheria/world/zone-render", ["zoneRenderSchema"] = "gamecult.aetheria.zone_render.v1", ["assetManifest"] = "cultmesh://aetheria/assets/manifest", - ["assetProviderId"] = "aetheria.assets", - ["assetVerseId"] = "aetheria.mods", - ["assetAuthorityRuntimeId"] = "mod-authority", - ["assetRendezvousEndpoints"] = "wss://odin-a/cultmesh;wss://odin-b/cultmesh", ["inputProfile"] = "arpg-third-person", - ["cameraRig"] = cameraRig, - ["cameraLookAt"] = cameraLookAt, - ["cameraTargetEntityId"] = "player-vanguard", - ["cameraDistance"] = cameraDistance, - ["cameraVerticalFieldOfViewDegrees"] = "60", - ["cameraTargetScreenX"] = cameraTargetScreenX, - ["cameraTargetScreenY"] = cameraTargetScreenY, - ["cameraPositionDamping"] = cameraPositionDamping, - ["cameraNearClipPlane"] = cameraNearClipPlane, - ["cameraFarClipPlane"] = cameraFarClipPlane, - ["ambientLightColor"] = "0.2,0.2,0.2", - ["ambientLightIntensity"] = "1.46", - ["skyboxAssetRef"] = skyboxAssetRef, - ["reflectionAssetRef"] = reflectionAssetRef, - ["reflectionIntensity"] = "1", - ["postProcessProfileAssetRef"] = postProcessProfileAssetRef, - ["cameraReconstruction"] = cameraReconstruction, - ["temporalQuality"] = temporalQuality, - ["temporalHistoryBlend"] = temporalHistoryBlend, - ["temporalJitterScale"] = temporalJitterScale, - ["temporalSharpening"] = temporalSharpening, - ["exposureMode"] = exposureMode, - ["exposureLowPercent"] = exposureLowPercent, - ["exposureHighPercent"] = exposureHighPercent, - ["exposureMinimumEv"] = exposureMinimumEv, - ["exposureMaximumEv"] = exposureMaximumEv, - ["exposureKeyValue"] = exposureKeyValue, - ["exposureAdaptation"] = exposureAdaptation, - ["exposureSpeedUp"] = exposureSpeedUp, - ["exposureSpeedDown"] = exposureSpeedDown, - ["colorGradingSpace"] = colorGradingSpace, - ["keyLightDirection"] = "0.4,-1,0.25", - ["keyLightColor"] = "1,0.95,0.9", - ["keyLightIntensity"] = "0.75", - ["excludedRenderChannels"] = "map", + ["cameraRig"] = "third-person-orbit", ["playerEntityId"] = "player-vanguard", + ["cameraTargetEntityId"] = "player-vanguard", + ["subjectVisible"] = "true", + ["movementEnabled"] = "true", + ["presentationMode"] = "world", ["movementCommand"] = "aetheria.daemon.move_intent", - ["lookCommand"] = "aetheria.daemon.look_intent", - ["lookModel"] = "planar-yaw.v1", - ["lookSensitivityRadians"] = "-0.001", ["focusCommand"] = "aetheria.daemon.focus", ["targetCommand"] = "aetheria.daemon.target", ["actionCommand"] = "aetheria.daemon.use_equipment" @@ -2975,22 +1624,42 @@ namespace GameCult.Eve.UnityScene.Tests } } - private sealed class FakeProviderSurfaceDocumentSource : IEveUnitySceneProviderSurfaceDocumentSource + private sealed class FakeProviderSurfaceDocumentSource : + IEveUnitySceneProviderSurfaceDocumentSource, + IEveUnityEntitySoaViewDocumentSource, + IEveUnityInputCapabilityDocumentSource { - public FakeProviderSurfaceDocumentSource(EveUnitySceneProviderSurfaceDocument currentDocument) + public FakeProviderSurfaceDocumentSource( + EveUnitySceneProviderSurfaceDocument currentDocument, + EveEntitySoaViewDocument? currentEntityView = null, + EveInputCapabilityDocument? currentInputCapability = null) { CurrentDocument = currentDocument; + CurrentEntityView = currentEntityView; + CurrentInputCapability = currentInputCapability; } public EveUnitySceneProviderSurfaceDocument CurrentDocument { get; private set; } public event Action? DocumentAvailable; + public EveEntitySoaViewDocument? CurrentEntityView { get; private set; } + + public event Action? EntityViewAvailable; + + public EveInputCapabilityDocument? CurrentInputCapability { get; } + public void Publish(EveUnitySceneProviderSurfaceDocument document) { CurrentDocument = document; DocumentAvailable?.Invoke(document); } + + public void PublishEntityView(EveEntitySoaViewDocument document) + { + CurrentEntityView = document; + EntityViewAvailable?.Invoke(document); + } } private sealed class FakeCommandReceiptSource : IEveUnitySceneCommandReceiptSource @@ -3087,10 +1756,6 @@ namespace GameCult.Eve.UnityScene.Tests private sealed class FakeLiveProviderTransport : IEveUnitySceneLiveProviderTransport { - private Action? _surfaceDocumentAvailable; - private Action? _assetManifestDocumentAvailable; - private Action? _commandReceiptAvailable; - public FakeLiveProviderTransport( EveUnitySceneProviderSurfaceDocument surfaceDocument, EveUnityPlayableWorldAssetManifestDocument assetManifestDocument) @@ -3117,38 +1782,15 @@ namespace GameCult.Eve.UnityScene.Tests public int RefreshCount { get; private set; } - public bool FailNextConnect { get; set; } + public event Action? SurfaceDocumentAvailable; - public int SurfaceSubscriberCount => _surfaceDocumentAvailable?.GetInvocationList().Length ?? 0; + public event Action? AssetManifestDocumentAvailable; - public int AssetManifestSubscriberCount => _assetManifestDocumentAvailable?.GetInvocationList().Length ?? 0; - - public int ReceiptSubscriberCount => _commandReceiptAvailable?.GetInvocationList().Length ?? 0; - - public event Action? SurfaceDocumentAvailable - { - add => _surfaceDocumentAvailable += value; - remove => _surfaceDocumentAvailable -= value; - } - - public event Action? AssetManifestDocumentAvailable - { - add => _assetManifestDocumentAvailable += value; - remove => _assetManifestDocumentAvailable -= value; - } - - public event Action? CommandReceiptAvailable - { - add => _commandReceiptAvailable += value; - remove => _commandReceiptAvailable -= value; - } + public event Action? CommandReceiptAvailable; public void Connect() { ConnectCount++; - if (!FailNextConnect) return; - FailNextConnect = false; - throw new IOException("transient transport failure"); } public void Disconnect() @@ -3169,18 +1811,18 @@ namespace GameCult.Eve.UnityScene.Tests public void PublishSurface(EveUnitySceneProviderSurfaceDocument document) { CurrentSurfaceDocument = document; - _surfaceDocumentAvailable?.Invoke(document); + SurfaceDocumentAvailable?.Invoke(document); } public void PublishAssetManifest(EveUnityPlayableWorldAssetManifestDocument document) { CurrentAssetManifestDocument = document; - _assetManifestDocumentAvailable?.Invoke(document); + AssetManifestDocumentAvailable?.Invoke(document); } public void PublishReceipt(EveUnitySceneCommandReceipt receipt) { - _commandReceiptAvailable?.Invoke(receipt); + CommandReceiptAvailable?.Invoke(receipt); } } @@ -3204,166 +1846,6 @@ namespace GameCult.Eve.UnityScene.Tests "", "", "", ""); } - private static EveEntitySoaViewDocument EntityLeaseDocument() => new EveEntitySoaViewDocument - { - ProviderId = "provider", - ViewId = "entities", - BodySchemaId = "test.entity.slab.v1", - LayoutVersion = 3, - ProducerEpoch = 5, - Sequence = 7, - Capacity = 1, - Buffers = new[] { new EveEntitySoaBuffer { BufferId = "hot", ByteLength = 4 } }, - Identities = new[] - { - new EveEntityIdentity - { - Index = 12, - EntityId = "entity:pilot", - EntityKind = "player", - Label = "Pilot", - Faction = "alliance", - Selectable = true, - Controllable = true, - AssetRef = "cultmesh://assets/pilot" - } - }, - Columns = new[] - { - new EveEntitySoaColumn - { - ColumnId = "entity-index", - Semantic = "entity.index", - BufferId = "hot", - ScalarType = "int32", - ElementStride = 4, - ElementCount = 1 - } - } - }; - - private static CultMeshRealtimeFrame RealtimeFrame(string bodyId, long epoch, long sequence) => - new CultMeshRealtimeFrame - { - ChannelId = "state", - BodyId = bodyId, - SchemaId = "test.entity.slab.v1", - ProducerEpoch = epoch, - Sequence = sequence, - Delivery = CultMeshRealtimeDelivery.LatestOnly, - Payload = new byte[4] - }; - - private static EveProviderAdvertisementDocument ProviderAdvertisement( - string providerId, - string serviceId, - params string[] authorizedBodyProducerIds) => - new EveProviderAdvertisementDocument( - providerId, - serviceId, - "test-verse", - "Test Provider", - "test", - "cultmesh://test/provider", - DateTimeOffset.UtcNow.ToString("O"), - new EveProviderFreshness("fresh", DateTimeOffset.UtcNow.ToString("O"), 5000), - Array.Empty(), - Array.Empty(), - Array.Empty(), - Array.Empty(), - authorizedBodyProducerIds); - - private static CultMeshBodyPublicationDocument BodyPublication( - EveEntitySoaViewDocument view, - long sequence) - { - var descriptor = FakeBodyReadLease.DescriptorFor(view, view.Buffers[0].ByteLength); - descriptor.Sequence = sequence; - descriptor.Synchronization = CultMeshBodySynchronization.ImmutableSequence; - descriptor.LeaseExpiresAtUnixMs = DateTimeOffset.UtcNow.AddMinutes(1).ToUnixTimeMilliseconds(); - descriptor.TransportKind = CultMeshBodyTransportKind.SharedFileMapping; - var network = FakeBodyReadLease.DescriptorFor(view, view.Buffers[0].ByteLength); - network.Sequence = sequence; - network.Synchronization = descriptor.Synchronization; - network.LeaseExpiresAtUnixMs = descriptor.LeaseExpiresAtUnixMs; - network.TransportKind = CultMeshBodyTransportKind.Network; - return new CultMeshBodyPublicationDocument - { - BodyId = descriptor.BodyId, - ProducerId = view.ProviderId, - SchemaId = descriptor.SchemaId, - LayoutVersion = descriptor.LayoutVersion, - ByteSize = descriptor.ByteSize, - Capacity = descriptor.Capacity, - ProducerEpoch = descriptor.ProducerEpoch, - Sequence = sequence, - Synchronization = descriptor.Synchronization, - LivenessExpiresAtUnixMs = descriptor.LeaseExpiresAtUnixMs, - Representations = new[] { descriptor, network } - }; - } - - private static EveEntitySoaColumn SoaColumn( - string semantic, - string scalarType, - long byteOffset, - int stride) => new EveEntitySoaColumn - { - ColumnId = semantic, - Semantic = semantic, - BufferId = "hot", - ScalarType = scalarType, - ByteOffset = byteOffset, - ElementStride = stride, - ElementCount = 1 - }; - - private static EveUnityPresentedEntity PresentedEntity(int index, string entityId, string assetRef) => - new EveUnityPresentedEntity( - index, entityId, "entity", entityId, "", Vector3.zero, 0f, Vector3.zero, - 1f, 1f, 0, 0, true, false, assetRef); - - private sealed class FakeBodyReadLease : ICultMeshBodyReadLease - { - private readonly byte[] _bytes; - - public FakeBodyReadLease(EveEntitySoaViewDocument document, byte[] bytes) - : this(DescriptorFor(document, bytes.Length), bytes) { } - - public FakeBodyReadLease(CultMeshBodyDescriptor descriptor, byte[] bytes) - { - Descriptor = descriptor; - _bytes = bytes; - } - - public CultMeshBodyDescriptor Descriptor { get; } - public CultMeshBodyTransportKind TransportKind => CultMeshBodyTransportKind.Network; - public bool Disposed { get; private set; } - public byte ReadByte(long offset) => _bytes[checked((int)offset)]; - public int ReadInt32(long offset) => BitConverter.ToInt32(_bytes, checked((int)offset)); - public long ReadInt64(long offset) => BitConverter.ToInt64(_bytes, checked((int)offset)); - public float ReadSingle(long offset) => BitConverter.ToSingle(_bytes, checked((int)offset)); - public double ReadDouble(long offset) => BitConverter.ToDouble(_bytes, checked((int)offset)); - public int CopyTo(long offset, byte[] destination, int destinationOffset, int count) - { - Buffer.BlockCopy(_bytes, checked((int)offset), destination, destinationOffset, count); - return count; - } - public void Dispose() => Disposed = true; - - public static CultMeshBodyDescriptor DescriptorFor(EveEntitySoaViewDocument document, long byteSize) => - new CultMeshBodyDescriptor - { - BodyId = document.Buffers[0].BufferId, - SchemaId = document.BodySchemaId, - LayoutVersion = document.LayoutVersion, - ByteSize = byteSize, - Capacity = document.Capacity, - ProducerEpoch = document.ProducerEpoch, - Sequence = document.Sequence - }; - } - private sealed class FixedGameObjectAssetProvider : IEveUnityGameObjectAssetProvider { private readonly GameObject? _prefab; @@ -3376,104 +1858,18 @@ namespace GameCult.Eve.UnityScene.Tests public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) => _prefab; } - private sealed class FixedNativeAssetProviderComponent : MonoBehaviour, IEveUnityNativeAssetProvider - { - private readonly Dictionary _assets = - new Dictionary(StringComparer.Ordinal); - - public EveUnityPlayableWorldAssetBinding? LastBinding { get; private set; } - - public void Set(string assetRef, UnityEngine.Object asset) - { - _assets[assetRef ?? ""] = asset; - } - - public void Clear() => _assets.Clear(); - - public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) => null; - - public UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType) - { - LastBinding = asset; - return _assets.TryGetValue(asset?.AssetRef ?? "", out var value) && assetType.IsInstanceOfType(value) - ? value - : null; - } - } - - private sealed class ObservingGameObjectAssetProvider : IEveUnityGameObjectAssetProvider - { - private readonly Action _onResolve; - - public ObservingGameObjectAssetProvider(Action onResolve) - { - _onResolve = onResolve; - } - - public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) - { - _onResolve(); - return null; - } - } - - private sealed class FixedRenderChannelPolicy : IEveUnityCameraRenderPolicySource - { - private readonly string _channel; - private readonly int _layer; - - public FixedRenderChannelPolicy(string channel, int layer) - { - _channel = channel; - _layer = layer; - } - - public bool TryGetRenderChannelLayer(string channel, out int layer) - { - layer = _layer; - return string.Equals(channel, _channel, StringComparison.Ordinal); - } - } - private sealed class FakePlayableWorldProviderComponent : MonoBehaviour, IEveUnitySceneProviderSurfaceDocumentSource, IEveUnityPlayableWorldAssetManifestDocumentSource, IEveUnitySceneCommandSink, IEveUnitySceneCommandReceiptSource, - IEveUnityProviderRefreshSource, - IEveUnityInputCapabilitySource, - IEveUnitySceneProviderSurfaceDocumentConnection, - IEveUnityNavigableProvider + IEveUnityProviderRefreshSource { public List Submitted { get; } = new List(); public int RefreshCount { get; private set; } - public EveUnitySceneNavigationTarget? LastNavigation { get; private set; } - - public int CommitNavigationCount { get; private set; } - - public int FinalizeNavigationCount { get; private set; } - - public int RollbackNavigationCount { get; private set; } - - public bool FailNextNavigationMount { get; set; } - - public bool FailNextNavigationFinalize { get; set; } - - public int ActiveConnectionLeases { get; private set; } - - public string ActiveRoute { get; private set; } = "original"; - - public Func? PreviousPresentationActive { get; set; } - - public bool? PreviousPresentationActiveDuringCommit { get; private set; } - - public bool? PreviousPresentationActiveDuringRollback { get; private set; } - - private bool _failSurfaceRead; - public string SinkKind => "fake-provider-command-sink"; public string ManifestRef => CurrentDocument.ManifestRef; @@ -3491,13 +1887,8 @@ namespace GameCult.Eve.UnityScene.Tests Array.Empty(), "aetheria"); - public EveInputCapabilityDocument CurrentInputCapability { get; private set; } = - new EveInputCapabilityDocument(); - EveUnitySceneProviderSurfaceDocument IEveUnitySceneProviderSurfaceDocumentSource.CurrentDocument => - _failSurfaceRead - ? throw new InvalidOperationException("Deliberate candidate surface read failure.") - : CurrentSurfaceDocument; + CurrentSurfaceDocument; public event Action? DocumentAvailable; @@ -3513,12 +1904,10 @@ namespace GameCult.Eve.UnityScene.Tests public void Set( EveUnitySceneProviderSurfaceDocument surfaceDocument, - EveUnityPlayableWorldAssetManifestDocument assetManifest, - EveInputCapabilityDocument? inputCapability = null) + EveUnityPlayableWorldAssetManifestDocument assetManifest) { CurrentSurfaceDocument = surfaceDocument; CurrentDocument = assetManifest; - CurrentInputCapability = inputCapability ?? new EveInputCapabilityDocument(); } public void Refresh() @@ -3537,51 +1926,6 @@ namespace GameCult.Eve.UnityScene.Tests { ReceiptAvailable?.Invoke(receipt); } - - public Task NavigateAsync(EveUnitySceneNavigationTarget target) - { - LastNavigation = target; - _failSurfaceRead = FailNextNavigationMount; - FailNextNavigationMount = false; - return Task.CompletedTask; - } - - public void Connect() - { - ActiveConnectionLeases++; - } - - public void Disconnect() - { - if (ActiveConnectionLeases > 0) - ActiveConnectionLeases--; - } - - public void CommitNavigation() - { - PreviousPresentationActiveDuringCommit = PreviousPresentationActive?.Invoke(); - CommitNavigationCount++; - ActiveRoute = "candidate"; - _failSurfaceRead = false; - } - - public void FinalizeNavigation() - { - FinalizeNavigationCount++; - if (FailNextNavigationFinalize) - { - FailNextNavigationFinalize = false; - throw new InvalidOperationException("Deliberate provider finalization failure."); - } - } - - public void RollbackNavigation() - { - RollbackNavigationCount++; - ActiveRoute = "original"; - _failSurfaceRead = false; - PreviousPresentationActiveDuringRollback = PreviousPresentationActive?.Invoke(); - } } } } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotReceiptPresenterTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotReceiptPresenterTests.cs index 02920dd..98beaa5 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotReceiptPresenterTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotReceiptPresenterTests.cs @@ -34,8 +34,6 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(emitted[0].ItemKey, Is.EqualTo("test-cannon")); Assert.That(emitted[0].ImpactKind, Is.EqualTo("shield")); Assert.That(emitted[0].PresentationIntensity, Is.EqualTo(9)); - Assert.That(emitted[0].AppliedDamage, Is.EqualTo(12)); - Assert.That(emitted[0].ShieldAbsorbedDamage, Is.EqualTo(8)); Assert.That(presenter.Apply(next), Is.Zero); } @@ -71,9 +69,7 @@ namespace GameCult.Eve.UnityScene.Tests ["presentationKind"] = "test-laser", ["itemKey"] = "test-cannon", ["impactKind"] = "shield", - ["presentationIntensity"] = "9", - ["appliedDamage"] = "12", - ["shieldAbsorbedDamage"] = "8" + ["presentationIntensity"] = "9" }, Empty(), Empty(), 0, 0, Array.Empty(), null, Array.Empty()); diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotTrajectoryRendererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotTrajectoryRendererTests.cs deleted file mode 100644 index 8dff40b..0000000 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotTrajectoryRendererTests.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Collections.Generic; -using NUnit.Framework; -using UnityEngine; - -#nullable enable - -namespace GameCult.Eve.UnityScene.Tests -{ - public sealed class EveUnityShotTrajectoryRendererTests - { - [Test] - public void ProviderShotEffectReplacesGenericFallbackLine() - { - var root = new GameObject("shot-renderer-provider-test"); - var prefab = new GameObject("provider-bolt"); - try - { - var host = root.AddComponent(); - var renderer = root.AddComponent(); - renderer.Bind(host, new BoltProvider(prefab)); - - renderer.Present(Shot("provider-shot")); - - Assert.That(renderer.ActiveTrajectoryCount, Is.EqualTo(1)); - Assert.That(renderer.ActiveFallbackTrajectoryCount, Is.Zero); - Assert.That(root.GetComponentsInChildren(), Is.Empty); - Assert.That(root.transform.GetChild(0).Find("provider-bolt(Clone)"), Is.Not.Null); - } - finally - { - UnityEngine.Object.DestroyImmediate(root); - UnityEngine.Object.DestroyImmediate(prefab); - } - } - - [Test] - public void MissingProviderShotEffectUsesGenericFallbackLine() - { - var root = new GameObject("shot-renderer-fallback-test"); - try - { - var host = root.AddComponent(); - var renderer = root.AddComponent(); - renderer.Bind(host, new BoltProvider(null)); - - renderer.Present(Shot("fallback-shot")); - - Assert.That(renderer.ActiveTrajectoryCount, Is.EqualTo(1)); - Assert.That(renderer.ActiveFallbackTrajectoryCount, Is.EqualTo(1)); - Assert.That(root.GetComponentsInChildren(), Has.Length.EqualTo(1)); - } - finally - { - UnityEngine.Object.DestroyImmediate(root); - } - } - - private static EveUnityShotReceipt Shot(string id) => - new EveUnityShotReceipt(new EveUnitySceneNode(id, "shot.receipt", "scene-node", - new Dictionary(StringComparer.Ordinal) - { - ["shotId"] = id, - ["origin"] = "0,0", - ["endpoint"] = "10,0", - ["presentationDuration"] = "0.2", - ["presentationKind"] = "bolt", - ["impactKind"] = "armor", - ["presentationIntensity"] = "85", - ["hit"] = "true" - }, Empty(), Empty(), 0, 0, Array.Empty(), null, - Array.Empty())); - - private static IReadOnlyDictionary Empty() => - new Dictionary(StringComparer.Ordinal); - - private sealed class BoltProvider : IEveUnityGameObjectAssetProvider - { - private readonly GameObject? _prefab; - public BoltProvider(GameObject? prefab) { _prefab = prefab; } - public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) => - asset.EntityKind == "effect.shot.bolt" ? _prefab : null; - } - } -} diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotTrajectoryRendererTests.cs.meta b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotTrajectoryRendererTests.cs.meta deleted file mode 100644 index 9cf0c04..0000000 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityShotTrajectoryRendererTests.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: a1d80f8e29164e23ad367feefb67c49e -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/GameCult.Eve.UnityScene.Tests.asmdef b/packages/org.gamecult.eve.unity-scene/Tests/Editor/GameCult.Eve.UnityScene.Tests.asmdef index 5617adc..2f8a14e 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/GameCult.Eve.UnityScene.Tests.asmdef +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/GameCult.Eve.UnityScene.Tests.asmdef @@ -4,11 +4,7 @@ "references": [ "GameCult.Eve.Surface", "GameCult.Eve.UnityScene", - "GameCult.Eve.UnityScene.CultMesh", - "GameCult.Eve.UnityUIToolkit", - "GameCult.Eve.PluginFields", - "Unity.RenderPipelines.Core.Runtime", - "Unity.RenderPipelines.Universal.Runtime" + "GameCult.Eve.PluginFields" ], "includePlatforms": [ "Editor" diff --git a/packages/org.gamecult.eve.unity-scene/Third Party Notices.md b/packages/org.gamecult.eve.unity-scene/Third Party Notices.md deleted file mode 100644 index e66c613..0000000 --- a/packages/org.gamecult.eve.unity-scene/Third Party Notices.md +++ /dev/null @@ -1,27 +0,0 @@ -# Third-party notices - -## Ashima Arts webgl-noise - -The portable 3D simplex implementation in `Runtime/Fields/EveFieldsSplatsCore.hlsl` -is derived from Ashima Arts' `webgl-noise`, translated to Unity shader syntax by -Keijiro Takahashi. - -Copyright (C) 2011 Ashima Arts. - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json b/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json index 244999a..122968b 100644 --- a/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json +++ b/packages/org.gamecult.eve.unity-scene/eve-runtime-capability.json @@ -46,17 +46,12 @@ "pluginId": "fields.surface", "capabilities": [ "field.surface2d", - "gravity.surface", - "field.volume3d", - "volume.raymarch", - "volume.temporal-history", - "volume.composite" + "gravity.surface" ], "projectionAdapter": "EveFieldsSplatRasterizer", "evidencePaths": [ "Runtime/Fields/EveFieldsSplatRasterizer.cs", "Runtime/Fields/EveFieldsSplatLayerRenderer.cs", - "Runtime/Fields/EveUnityFieldsVolumeRenderer.cs", "Tests/Editor/EveFieldsUnitySceneProjectionTests.cs", "artifacts/fields-surface/latest/unity-fields.png" ] @@ -113,15 +108,12 @@ "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs", - "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityUiToolkitOverlay.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientBootstrap.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityFeedbackPresenter.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotReceiptPresenter.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityShotTrajectoryRenderer.cs", - "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentation.cs", - "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityCombatPresentationRenderer.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityProviderRefreshSource.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldPresenter.cs", "packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs", @@ -129,8 +121,7 @@ "packages/org.gamecult.eve.unity-scene/Runtime/SaiVisualNovelUnitySceneProjectionAdapter.cs", "packages/org.gamecult.eve.unity-scene/Runtime/NornGraphUnitySceneProjectionAdapter.cs", "packages/org.gamecult.eve.unity-scene/Runtime/TeXMathUnitySceneProjectionAdapter.cs", - "packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs", - "packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCombatPresentationTests.cs" + "packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs" ] } ], @@ -160,17 +151,10 @@ "requiredPackageDependencies": [ { "packageName": "org.gamecult.eve.surface", - "version": "0.3.0", + "version": "0.1.0", "packageManager": "upm", - "ownerRepo": "Eve", - "purpose": "Renderer-neutral contracts and builders for gamecult.eve.surface.v1 documents." - }, - { - "packageName": "org.gamecult.eve.unity-uitoolkit", - "version": "0.1.3", - "packageManager": "upm", - "ownerRepo": "EveUnity", - "purpose": "Generic UI Toolkit lowering for provider-owned Eve panes, metrics, progress, text, and controls." + "ownerRepo": "EveUnity", + "purpose": "Shared Unity DTOs for gamecult.eve.surface.v1 documents." } ] }, diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index f5dcbe0..a241183 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,17 +1,14 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.134", - "unity": "6000.0", + "version": "0.1.4", + "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { - "org.gamecult.cultlib": "1.0.56", - "org.gamecult.eve.plugin-fields": "0.2.3", - "org.gamecult.eve.surface": "0.3.6", - "org.gamecult.eve.unity-uitoolkit": "0.1.9", - "com.unity.mathematics": "1.3.2", - "com.unity.render-pipelines.universal": "17.4.0", - "com.unity.modules.particlesystem": "1.0.0" + "org.gamecult.cultlib": "1.0.6", + "org.gamecult.eve.plugin-fields": "0.1.1", + "org.gamecult.eve.surface": "0.1.0", + "com.unity.mathematics": "1.3.2" }, "keywords": [ "Eve", diff --git a/packages/org.gamecult.eve.unity-uitoolkit/README.md b/packages/org.gamecult.eve.unity-uitoolkit/README.md index cf788bd..a0b325c 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/README.md +++ b/packages/org.gamecult.eve.unity-uitoolkit/README.md @@ -27,13 +27,6 @@ Nested CultUI regions use component `EmbeddedDocuments`. Pass an mounts the resolved child surface under the slot while preserving the child document's command surface id. -`progress` components lower to native UI Toolkit `ProgressBar` elements. The -provider supplies a normalized `ratio` (falling back to `value`), label, and -layout; Unity clamps the native visual to `[0,1]` without becoming the value -authority. Generated layout supports relative or absolute positioning plus -`inset`, `top`, `right`, `bottom`, and `left`, so provider-authored overlays -can compose over scene lowerers without consumer-specific HUD code. - Discovery and test coverage for this feature live in the shared Eve parity matrix: `../../tools/parity/parity-manifest.json` names `embeddedDocuments` and `../../web/fixtures/cultui-embedded-surface.json` is the canonical fixture. diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs index f5397a2..f989ccf 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs @@ -13,11 +13,6 @@ namespace GameCult.Eve.UnityUIToolkit public sealed class EveUiToolkitSurfaceLowerer { private readonly EveUiToolkitSurfaceOptions _options; - private EveSurfaceComponent? _inventoryDragSource; - private Vector2 _inventoryDragStart; - private bool _inventoryPointerMoved; - private bool _suppressInventoryClick; - private readonly List _inventoryPreviewCells = new(); public EveUiToolkitSurfaceLowerer(EveUiToolkitSurfaceOptions? options = null) { @@ -46,22 +41,9 @@ namespace GameCult.Eve.UnityUIToolkit element.AddToClassList($"eve-kind-{SafeClass(component.Kind)}"); element.userData = component; ApplyGeneratedLayout(element, component); - element.SetEnabled(IsEnabled(component)); - - if (IsHidden(component) || IsExternalProjectionRoot(component.Kind)) - return element; - - // Select options are semantic children consumed by the control itself. - // They are not separately lowered visual elements. - if (NormalizeKind(component.Kind) == "control.select") - return element; foreach (var child in component.Children) - { - var loweredChild = LowerComponent(child, document, commandSink); - PositionInventoryChild(component, child, loweredChild); - element.Add(loweredChild); - } + element.Add(LowerComponent(child, document, commandSink)); foreach (var slot in component.EmbeddedDocuments) { @@ -115,14 +97,6 @@ namespace GameCult.Eve.UnityUIToolkit element.style.alignItems = Align.Stretch; return element; } - case EveInventoryInteraction.GridKind: - return InventoryGrid(component, document, commandSink); - case EveInventoryInteraction.ItemKind: - return InventoryItem(component, document, commandSink); - case EveInventoryInteraction.DragSessionKind: - return InventoryDragSession(component); - case "control.select": - return Select(component, document, commandSink); case "partition": { var element = new VisualElement(); @@ -132,8 +106,6 @@ namespace GameCult.Eve.UnityUIToolkit element.style.flexWrap = Wrap.Wrap; return element; } - case "scroll": - return new ScrollView(ScrollViewMode.Vertical); case "pane": case "modal": case "card": @@ -143,11 +115,7 @@ namespace GameCult.Eve.UnityUIToolkit card.style.flexDirection = FlexDirection.Column; var title = component.GetProp("title"); if (!string.IsNullOrWhiteSpace(title)) - { - var titleLabel = TitleLabel(title); - titleLabel.name = "title"; - card.Add(titleLabel); - } + card.Add(TitleLabel(title)); return card; } case "metric": @@ -155,24 +123,17 @@ namespace GameCult.Eve.UnityUIToolkit var metric = new VisualElement(); metric.AddToClassList("eve-metric"); metric.style.flexDirection = FlexDirection.Column; - var label = MutedLabel(component.GetProp("label")); - label.name = "label"; - metric.Add(label); - var value = ValueLabel(component.GetProp("value")); - value.name = "value"; - metric.Add(value); + metric.Add(MutedLabel(component.GetProp("label"))); + metric.Add(ValueLabel(component.GetProp("value"))); return metric; } case "progress": { - var progress = new ProgressBar - { - title = component.GetProp("label"), - lowValue = 0f, - highValue = 1f, - value = ParseRatio(component.GetProp("ratio", component.GetProp("value"))) - }; + var progress = new VisualElement(); progress.AddToClassList("eve-progress"); + progress.style.flexDirection = FlexDirection.Column; + progress.Add(MutedLabel(component.GetProp("label"))); + progress.Add(ValueLabel(component.GetProp("value"))); return progress; } case "options": @@ -246,234 +207,6 @@ namespace GameCult.Eve.UnityUIToolkit } } - private VisualElement InventoryGrid( - EveSurfaceComponent component, - EveSurfaceDocument document, - Action? commandSink) - { - var grid = new VisualElement(); - grid.AddToClassList("eve-inventory-grid"); - grid.style.position = Position.Relative; - var columns = Math.Max(1, ParseInt(component.GetProp("columns"), 6)); - var rows = Math.Max(1, ParseInt(component.GetProp("rows"), 3)); - var cellSize = Math.Max(1f, ParseNumber(component.GetProp("cellSize"), 72f)); - var gap = Math.Max(0f, ParseNumber(component.GetProp("cellGap"), 4f)); - grid.style.width = columns * cellSize + Math.Max(0, columns - 1) * gap; - grid.style.height = rows * cellSize + Math.Max(0, rows - 1) * gap; - for (var index = 0; index < Math.Min(columns * rows, 256); index++) - { - var cell = new VisualElement(); - cell.AddToClassList("eve-inventory-cell"); - cell.style.position = Position.Absolute; - cell.style.left = (index % columns) * (cellSize + gap); - cell.style.top = (index / columns) * (cellSize + gap); - cell.style.width = cellSize; - cell.style.height = cellSize; - grid.Add(cell); - } - grid.RegisterCallback(evt => - { - if (_inventoryDragSource == null || _suppressInventoryClick) - return; - if (TryEmitInventoryDrop(document, _inventoryDragSource, component, grid, evt.position, commandSink)) - _inventoryDragSource = null; - ClearInventoryPreview(); - }); - return grid; - } - - private VisualElement InventoryItem( - EveSurfaceComponent component, - EveSurfaceDocument document, - Action? commandSink) - { - var item = new Button { text = component.GetProp("label", component.GetProp("itemKey")) }; - item.AddToClassList("eve-inventory-item"); - item.RegisterCallback(evt => - { - if (!ParseBool(component.GetProp("draggable", "true"))) - return; - _inventoryDragSource = component; - _inventoryDragStart = evt.position; - _inventoryPointerMoved = false; - ClearInventoryPreview(); - item.CapturePointer(evt.pointerId); - evt.StopPropagation(); - }); - item.RegisterCallback(evt => - { - if (_inventoryDragSource != component) - return; - var pointerPosition = new Vector2(evt.position.x, evt.position.y); - if ((pointerPosition - _inventoryDragStart).sqrMagnitude > 16f) - { - _inventoryPointerMoved = true; - var picked = item.panel?.Pick(evt.position); - var targetElement = InventoryGridAncestor(picked); - if (targetElement?.userData is EveSurfaceComponent target) - ShowInventoryPreview(component, target, targetElement, evt.position); - else - ClearInventoryPreview(); - } - }); - item.RegisterCallback(evt => - { - if (_inventoryDragSource != component) - return; - item.ReleasePointer(evt.pointerId); - if (_inventoryPointerMoved) - { - var picked = item.panel?.Pick(evt.position); - var targetElement = InventoryGridAncestor(picked); - if (targetElement?.userData is EveSurfaceComponent target) - TryEmitInventoryDrop(document, component, target, targetElement, evt.position, commandSink); - _inventoryDragSource = null; - _suppressInventoryClick = true; - ClearInventoryPreview(); - } - evt.StopPropagation(); - }); - item.RegisterCallback(evt => - { - if (_suppressInventoryClick) - { - _suppressInventoryClick = false; - evt.StopPropagation(); - return; - } - _inventoryDragSource = component; - evt.StopPropagation(); - }); - return item; - } - - private static VisualElement InventoryDragSession(EveSurfaceComponent component) - { - var panel = new VisualElement(); - panel.AddToClassList("eve-inventory-drag-session"); - var active = ParseBool(component.GetProp("active")); - panel.Add(BodyLabel(active - ? component.GetProp("itemKey", "Dragging") - : "No active drag")); - return panel; - } - - private static void PositionInventoryChild( - EveSurfaceComponent parent, - EveSurfaceComponent child, - VisualElement element) - { - if (!string.Equals(parent.Kind, EveInventoryInteraction.GridKind, StringComparison.Ordinal) || - !string.Equals(child.Kind, EveInventoryInteraction.ItemKind, StringComparison.Ordinal)) - return; - var cellSize = Math.Max(1f, ParseNumber(parent.GetProp("cellSize"), 72f)); - var gap = Math.Max(0f, ParseNumber(parent.GetProp("cellGap"), 4f)); - var widthCells = Math.Max(1, ParseInt(child.GetProp("shapeWidth"), 1)); - var heightCells = Math.Max(1, ParseInt(child.GetProp("shapeHeight"), 1)); - var rotation = child.GetProp("rotation"); - if (string.Equals(rotation, "Clockwise", StringComparison.OrdinalIgnoreCase) || - string.Equals(rotation, "CounterClockwise", StringComparison.OrdinalIgnoreCase) || - string.Equals(rotation, "Right", StringComparison.OrdinalIgnoreCase) || - string.Equals(rotation, "Left", StringComparison.OrdinalIgnoreCase) || - string.Equals(rotation, "Rotate90", StringComparison.OrdinalIgnoreCase) || - string.Equals(rotation, "Rotate270", StringComparison.OrdinalIgnoreCase)) - { - (widthCells, heightCells) = (heightCells, widthCells); - } - element.style.position = Position.Absolute; - element.style.left = Math.Max(0, ParseInt(child.GetProp("x"), 0)) * (cellSize + gap); - element.style.top = Math.Max(0, ParseInt(child.GetProp("y"), 0)) * (cellSize + gap); - element.style.width = widthCells * cellSize + Math.Max(0, widthCells - 1) * gap; - element.style.height = heightCells * cellSize + Math.Max(0, heightCells - 1) * gap; - } - - private static VisualElement? InventoryGridAncestor(VisualElement? element) - { - while (element != null) - { - if (element.userData is EveSurfaceComponent component && - string.Equals(component.Kind, EveInventoryInteraction.GridKind, StringComparison.Ordinal)) - return element; - element = element.parent; - } - return null; - } - - private static bool TryEmitInventoryDrop( - EveSurfaceDocument document, - EveSurfaceComponent source, - EveSurfaceComponent target, - VisualElement targetElement, - Vector2 panelPosition, - Action? commandSink) - { - if (commandSink == null) - return false; - var local = targetElement.WorldToLocal(panelPosition); - var pitch = Math.Max(1f, ParseNumber(target.GetProp("cellSize"), 72f) + - Math.Max(0f, ParseNumber(target.GetProp("cellGap"), 4f))); - var x = Math.Max(0, (int)Math.Floor(local.x / pitch)); - var y = Math.Max(0, (int)Math.Floor(local.y / pitch)); - if (!EveInventoryInteraction.TryCreateDropRequest( - document, source, target, x, y, "unity-uitoolkit", out var request) || request == null) - return false; - commandSink(request); - return true; - } - - private void ShowInventoryPreview( - EveSurfaceComponent source, - EveSurfaceComponent target, - VisualElement targetElement, - Vector2 panelPosition) - { - ClearInventoryPreview(); - var local = targetElement.WorldToLocal(panelPosition); - var cellSize = Math.Max(1f, ParseNumber(target.GetProp("cellSize"), 72f)); - var gap = Math.Max(0f, ParseNumber(target.GetProp("cellGap"), 4f)); - var pitch = cellSize + gap; - var x = Math.Max(0, (int)Math.Floor(local.x / Math.Max(1f, pitch))); - var y = Math.Max(0, (int)Math.Floor(local.y / Math.Max(1f, pitch))); - if (!EveInventoryInteraction.TryCreatePlacementPreview(source, target, x, y, out var preview) || - preview == null) - return; - - foreach (var cell in preview.Cells) - { - var element = new VisualElement { pickingMode = PickingMode.Ignore }; - element.AddToClassList("eve-inventory-placement-preview"); - element.AddToClassList(preview.IsValid - ? "eve-inventory-placement-valid" - : "eve-inventory-placement-invalid"); - element.style.position = Position.Absolute; - element.style.left = cell.X * pitch; - element.style.top = cell.Y * pitch; - element.style.width = cellSize; - element.style.height = cellSize; - element.style.backgroundColor = preview.IsValid - ? new Color(0.2f, 0.9f, 0.65f, 0.34f) - : new Color(1f, 0.25f, 0.2f, 0.38f); - targetElement.Add(element); - _inventoryPreviewCells.Add(element); - } - } - - private void ClearInventoryPreview() - { - foreach (var cell in _inventoryPreviewCells) - cell.RemoveFromHierarchy(); - _inventoryPreviewCells.Clear(); - } - - private static int ParseInt(string value, int fallback) => - int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) ? parsed : fallback; - - private static bool ParseBool(string value) => - bool.TryParse(value, out var parsed) && parsed; - - private static float ParseNumber(string value, float fallback) => - float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var parsed) ? parsed : fallback; - private static void EmitCommand( EveSurfaceDocument document, EveSurfaceComponent component, @@ -514,62 +247,10 @@ namespace GameCult.Eve.UnityUIToolkit foreach (var template in document.Commands) { if (string.Equals(template.Command, command, StringComparison.Ordinal)) - return GameCult.Mesh.CultMesh.OperationInvocation( - template.Operation, - idempotencyKey: $"unity-uitoolkit-{Guid.NewGuid():N}"); + return GameCult.Mesh.CultMesh.OperationInvocation(template.Operation); } - return GameCult.Mesh.CultMesh.OperationInvocation( - command, - idempotencyKey: $"unity-uitoolkit-{Guid.NewGuid():N}"); - } - - private static VisualElement Select( - EveSurfaceComponent component, - EveSurfaceDocument document, - Action? commandSink) - { - var options = component.Children - .Where(child => NormalizeKind(child.Kind) == "control.option") - .Select(child => new - { - Label = child.GetProp("label", child.GetProp("value")), - Value = child.GetProp("value") - }) - .Where(option => !string.IsNullOrWhiteSpace(option.Value)) - .ToArray(); - var duplicateLabels = options - .GroupBy(option => option.Label, StringComparer.Ordinal) - .Where(group => group.Count() > 1) - .Select(group => group.Key) - .ToHashSet(StringComparer.Ordinal); - var labels = options - .Select(option => duplicateLabels.Contains(option.Label) - ? $"{option.Label} [{option.Value}]" - : option.Label) - .ToList(); - var selectedValue = component.GetProp("value"); - var selectedIndex = Array.FindIndex(options, option => - string.Equals(option.Value, selectedValue, StringComparison.Ordinal)); - var field = new DropdownField(component.GetProp("label"), labels, Math.Max(0, selectedIndex)); - field.RegisterValueChangedCallback(change => - { - var index = field.index; - if (index < 0 || index >= options.Length) - return; - var option = options[index]; - var payload = new Dictionary(component.Props, StringComparer.Ordinal) - { - ["value"] = option.Value - }; - EmitCommand( - document, - component, - component.GetProp("command", component.GetProp("operationId")), - GameCult.Mesh.CultMesh.OperationPayload(payload), - commandSink); - }); - return field; + return GameCult.Mesh.CultMesh.OperationInvocation(command); } private static Label TitleLabel(string text) @@ -632,26 +313,6 @@ namespace GameCult.Eve.UnityUIToolkit element.style.display = string.Equals(display, "none", StringComparison.OrdinalIgnoreCase) ? DisplayStyle.None : DisplayStyle.Flex; - if (TryGet(layout, "position", out var position)) - element.style.position = string.Equals(position, "absolute", StringComparison.OrdinalIgnoreCase) - ? Position.Absolute - : Position.Relative; - if (TryGet(layout, "inset", out var inset)) - { - var value = ParseLength(inset); - element.style.top = value; - element.style.right = value; - element.style.bottom = value; - element.style.left = value; - } - if (TryGet(layout, "top", out var top)) - element.style.top = ParseLength(top); - if (TryGet(layout, "right", out var right)) - element.style.right = ParseLength(right); - if (TryGet(layout, "bottom", out var bottom)) - element.style.bottom = ParseLength(bottom); - if (TryGet(layout, "left", out var left)) - element.style.left = ParseLength(left); if (TryGet(layout, "direction", out var direction)) { if (string.Equals(direction, "horizontal", StringComparison.OrdinalIgnoreCase) || @@ -667,12 +328,6 @@ namespace GameCult.Eve.UnityUIToolkit element.style.alignItems = align; if (TryGet(layout, "justifyContent", out var justifyContent) && TryParseJustify(justifyContent, out var justify)) element.style.justifyContent = justify; - if (TryGet(layout, "flexGrow", out var flexGrow)) - element.style.flexGrow = ParseFloat(flexGrow); - if (TryGet(layout, "flexShrink", out var flexShrink)) - element.style.flexShrink = ParseFloat(flexShrink); - if (TryGet(layout, "flexBasis", out var flexBasis)) - element.style.flexBasis = ParseLength(flexBasis); if (TryGet(layout, "width", out var width)) element.style.width = ParseLength(width); if (TryGet(layout, "minWidth", out var minWidth)) @@ -749,14 +404,6 @@ namespace GameCult.Eve.UnityUIToolkit return false; } - private static bool IsEnabled(EveSurfaceComponent component) - { - if (ParseBool(component.GetProp("disabled"))) - return false; - var enabled = component.GetProp("enabled"); - return string.IsNullOrWhiteSpace(enabled) || ParseBool(enabled); - } - private static StyleLength ParseLength(string value) { value = FirstToken(value); @@ -783,13 +430,6 @@ namespace GameCult.Eve.UnityUIToolkit : StyleKeyword.Null; } - private static float ParseRatio(string value) - { - return float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var ratio) - ? Mathf.Clamp01(ratio) - : 0f; - } - private static void ApplyBoxLength(string value, Action apply) { apply(ParseLength(value)); @@ -884,21 +524,6 @@ namespace GameCult.Eve.UnityUIToolkit return kind; } - internal static bool IsHidden(EveSurfaceComponent component) => - component.Layout != null && - component.Layout.TryGetValue("display", out var display) && - string.Equals(display, "none", StringComparison.OrdinalIgnoreCase); - - internal static bool IsExternalProjectionRoot(string kind) - { - kind = NormalizeKind(kind); - return string.Equals(kind, "world.scene3d", StringComparison.Ordinal) || - string.Equals(kind, "world.scene2d", StringComparison.Ordinal) || - string.Equals(kind, "field.volume3d", StringComparison.Ordinal) || - string.Equals(kind, "field.particles3d", StringComparison.Ordinal) || - string.Equals(kind, "layer.reactive", StringComparison.Ordinal); - } - private static string SafeName(string value) { return string.IsNullOrWhiteSpace(value) ? "eve.component" : value; diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfacePresenter.cs b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfacePresenter.cs deleted file mode 100644 index 812ef72..0000000 --- a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfacePresenter.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using GameCult.Eve.Surface; -using UnityEngine.UIElements; - -#nullable enable - -namespace GameCult.Eve.UnityUIToolkit -{ - public sealed class EveUiToolkitSurfacePresenter - { - private readonly EveUiToolkitSurfaceLowerer _lowerer; - private string _structure = ""; - - public EveUiToolkitSurfacePresenter(EveUiToolkitSurfaceOptions? options = null) - { - _lowerer = new EveUiToolkitSurfaceLowerer(options); - } - - public VisualElement? Root { get; private set; } - - public bool Present( - EveSurfaceDocument document, - Action? commandSink = null) - { - if (document == null) throw new ArgumentNullException(nameof(document)); - var structure = StructureOf(document); - if (Root == null || !string.Equals(_structure, structure, StringComparison.Ordinal)) - { - Root = _lowerer.Lower(document, commandSink); - _structure = structure; - return true; - } - - var elements = Root.Query() - .ToList() - .Where(element => element.userData is EveSurfaceComponent) - .ToDictionary(element => ((EveSurfaceComponent)element.userData).Id, StringComparer.Ordinal); - if (Root.userData is EveSurfaceComponent rootComponent) - elements[rootComponent.Id] = Root; - ApplyDynamic(document.Surface.Root, elements); - return false; - } - - private static void ApplyDynamic( - EveSurfaceComponent component, - IReadOnlyDictionary elements) - { - if (!elements.TryGetValue(component.Id, out var element)) - return; - element.userData = component; - switch (NormalizeKind(component.Kind)) - { - case "pane": - case "modal": - case "card": - var title = element.Q