Admit EveUnity transactional presentation authority

This commit is contained in:
Meta Vens 2026-08-26 20:36:38 +02:00
commit f0afa7681b
157 changed files with 19653 additions and 3003 deletions

3
.gitignore vendored
View file

@ -9,3 +9,6 @@ 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

View file

@ -15,7 +15,10 @@ 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.
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.
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\run-package-tests.ps1

View file

@ -0,0 +1,15 @@
%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: []

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5b8b49b0bf6036a4a823021e69f80309
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 42bb4a36b1c94282ba7a4cad2b073bbc
folderAsset: yes

View file

@ -0,0 +1,20 @@
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<GenericEveUnityLauncher>();
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);
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8fcd5fd63f21489eb5a70306d22ed4a4

View file

@ -0,0 +1,50 @@
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<UniversalRendererData>(RendererPath);
if (renderer == null)
{
renderer = ScriptableObject.CreateInstance<UniversalRendererData>();
AssetDatabase.CreateAsset(renderer, RendererPath);
}
renderer.postProcessData = AssetDatabase.LoadAssetAtPath<PostProcessData>(
AssetDatabase.GUIDToAssetPath(PostProcessDataGuid));
if (!renderer.rendererFeatures.OfType<EveUnityRendererFeature>().Any())
{
var feature = ScriptableObject.CreateInstance<EveUnityRendererFeature>();
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<UniversalRenderPipelineAsset>(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();
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c07b26ad876001846ba5ea9d3743352e

View file

@ -0,0 +1,100 @@
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<GenericEveUnityLauncher>() != null) return;
new GameObject("Generic EveUnity Client").AddComponent<GenericEveUnityLauncher>();
}
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>();
camera.clearFlags = CameraClearFlags.SolidColor;
camera.backgroundColor = new Color(0.025f, 0.035f, 0.055f);
camera.nearClipPlane = 0.05f;
camera.farClipPlane = 10000f;
_provider = gameObject.AddComponent<EveUnityCultMeshPlayableWorldProvider>();
_provider.Configure(endpoint, replica, surfaceId: surface, requiredSurfaceKind: "interactive-world", clientRuntimeId: "eve-unity-interactive");
_provider.DocumentAvailable += OnSurfaceAvailable;
_surfaceLowerer = new EveUiToolkitSurfaceLowerer();
_panelSettings = ScriptableObject.CreateInstance<PanelSettings>();
_panelSettings.scaleMode = PanelScaleMode.ScaleWithScreenSize;
_panelSettings.referenceResolution = new Vector2Int(1920, 1080);
_panelSettings.sortingOrder = 20;
_surfaceDocument = gameObject.AddComponent<UIDocument>();
_surfaceDocument.panelSettings = _panelSettings;
_bootstrap = gameObject.AddComponent<EveUnityPlayableWorldClientBootstrap>();
_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);
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5c2c8060cff846cda741810156b0558a

View file

@ -0,0 +1,143 @@
%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}

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 43f32b840bd83ce47af920dadd59d4b3
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,69 @@
%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

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cbe5d2831ab649e42b9fb94644e029a9
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View file

@ -4,8 +4,10 @@
"references": [
"GameCult.CultLib",
"GameCult.Eve.Surface",
"GameCult.Eve.UnityUIToolkit",
"GameCult.Eve.UnityScene",
"GameCult.Eve.UnityScene.CultMesh"
"GameCult.Eve.UnityScene.CultMesh",
"Unity.RenderPipelines.Universal.Runtime"
],
"includePlatforms": [],
"excludePlatforms": [],
@ -14,6 +16,7 @@
"GameCult.Caching.dll",
"GameCult.Caching.MessagePack.dll",
"GameCult.Mesh.dll",
"GameCult.Mesh.Quic.Native.dll",
"GameCult.Networking.dll"
],
"autoReferenced": false,

View file

@ -0,0 +1,436 @@
%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}

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9374f1d73c9ee8940895d44ff5dd560e
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View file

@ -5,11 +5,12 @@
"com.unity.modules.imageconversion": "1.0.0",
"com.unity.modules.physics": "1.0.0",
"com.unity.modules.uielements": "1.0.0",
"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"
"com.unity.render-pipelines.universal": "17.4.0",
"org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#21163d83fa2670dbcc9cdd57c6c1776b33a91d62",
"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#e7b09f755de2fa54e2c5936331e3a03839a13cd1",
"org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#82b48cd089b55e47f5112b2801f2eb48c1c002dc",
"org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#82b48cd089b55e47f5112b2801f2eb48c1c002dc"
},
"testables": [
"org.gamecult.eve.unity-scene",

View file

@ -1,5 +1,27 @@
{
"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,
@ -13,6 +35,60 @@
"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,
@ -23,49 +99,73 @@
"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#cultlib-unity-v1.0.7",
"version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#21163d83fa2670dbcc9cdd57c6c1776b33a91d62",
"depth": 0,
"source": "git",
"dependencies": {},
"hash": "1f8f832a788e2bdf59df90fb91da583574d3a42d"
"hash": "21163d83fa2670dbcc9cdd57c6c1776b33a91d62"
},
"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.1.1",
"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",
"depth": 0,
"source": "git",
"dependencies": {},
"hash": "0b099274a4e2b4c301eced4322166837792f585a"
"dependencies": {
"org.gamecult.cultlib": "1.0.13"
},
"hash": "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0"
},
"org.gamecult.eve.surface": {
"version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#b6f001b300a88aff0bc13372b5c1dbc2002954f8",
"version": "https://github.com/GameCult/Eve.git?path=/packages/org.gamecult.eve.surface#e7b09f755de2fa54e2c5936331e3a03839a13cd1",
"depth": 0,
"source": "git",
"dependencies": {
"org.gamecult.cultlib": "1.0.6"
"org.gamecult.cultlib": "1.0.45"
},
"hash": "b6f001b300a88aff0bc13372b5c1dbc2002954f8"
"hash": "e7b09f755de2fa54e2c5936331e3a03839a13cd1"
},
"org.gamecult.eve.unity-scene": {
"version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#b6f001b300a88aff0bc13372b5c1dbc2002954f8",
"version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#82b48cd089b55e47f5112b2801f2eb48c1c002dc",
"depth": 0,
"source": "git",
"dependencies": {
"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"
"org.gamecult.cultlib": "1.0.47",
"org.gamecult.eve.plugin-fields": "0.2.3",
"org.gamecult.eve.surface": "0.3.2",
"org.gamecult.eve.unity-uitoolkit": "0.1.4",
"com.unity.mathematics": "1.3.2",
"com.unity.render-pipelines.universal": "17.4.0",
"com.unity.modules.particlesystem": "1.0.0"
},
"hash": "b6f001b300a88aff0bc13372b5c1dbc2002954f8"
"hash": "82b48cd089b55e47f5112b2801f2eb48c1c002dc"
},
"org.gamecult.eve.unity-uitoolkit": {
"version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0",
"version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#82b48cd089b55e47f5112b2801f2eb48c1c002dc",
"depth": 0,
"source": "git",
"dependencies": {
"org.gamecult.eve.surface": "0.1.0"
"org.gamecult.eve.surface": "0.3.2"
},
"hash": "e10821f1cc81f619479c6507438f6a097d1f0ce2"
"hash": "82b48cd089b55e47f5112b2801f2eb48c1c002dc"
},
"com.unity.modules.assetbundle": {
"version": "1.0.0",
@ -97,12 +197,24 @@
"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,

View file

@ -37,7 +37,7 @@ GraphicsSettings:
m_PreloadedShaders: []
m_PreloadShadersBatchTimeLimit: -1
m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0}
m_CustomRenderPipeline: {fileID: 0}
m_CustomRenderPipeline: {fileID: 11400000, guid: 43f32b840bd83ce47af920dadd59d4b3, type: 2}
m_TransparencySortMode: 0
m_TransparencySortAxis: {x: 0, y: 0, z: 1}
m_DefaultRenderingPath: 1
@ -57,7 +57,8 @@ GraphicsSettings:
m_FogKeepExp: 1
m_FogKeepExp2: 1
m_AlbedoSwatchInfos: []
m_RenderPipelineGlobalSettingsMap: {}
m_RenderPipelineGlobalSettingsMap:
UnityEngine.Rendering.Universal.UniversalRenderPipeline: {fileID: 11400000, guid: 9374f1d73c9ee8940895d44ff5dd560e, type: 2}
m_ShaderBuildSettings:
keywordDeclarationOverrides: []
m_LightsUseLinearIntensity: 0

View file

@ -313,7 +313,7 @@ QualitySettings:
asyncUploadBufferSize: 16
asyncUploadPersistentBuffer: 1
resolutionScalingFixedDPIFactor: 1
customRenderPipeline: {fileID: 0}
customRenderPipeline: {fileID: 11400000, guid: 43f32b840bd83ce47af920dadd59d4b3, type: 2}
terrainQualityOverrides: 0
terrainPixelError: 1
terrainDetailDensityScale: 1

View file

@ -4,8 +4,10 @@
"references": [
"GameCult.CultLib",
"GameCult.Eve.Surface",
"GameCult.Eve.UnityUIToolkit",
"GameCult.Eve.UnityScene",
"GameCult.Eve.UnityScene.CultMesh"
"GameCult.Eve.UnityScene.CultMesh",
"Unity.RenderPipelines.Universal.Runtime"
],
"includePlatforms": [],
"excludePlatforms": [],
@ -14,6 +16,7 @@
"GameCult.Caching.dll",
"GameCult.Caching.MessagePack.dll",
"GameCult.Mesh.dll",
"GameCult.Mesh.Quic.Native.dll",
"GameCult.Networking.dll"
],
"autoReferenced": false,

File diff suppressed because it is too large Load diff

View file

@ -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:../../../CultLib/artifacts/unity/org.gamecult.cultlib",
"org.gamecult.cultlib": "file:../../artifacts/test-dependencies/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:../../packages/org.gamecult.eve.surface",
"org.gamecult.eve.surface": "file:../../../Eve/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"
},

View file

@ -1,11 +1,94 @@
{
"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,
@ -16,8 +99,27 @@
"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:../../../CultLib/artifacts/unity/org.gamecult.cultlib",
"version": "file:../../artifacts/test-dependencies/org.gamecult.cultlib",
"depth": 0,
"source": "local",
"dependencies": {}
@ -26,14 +128,16 @@
"version": "file:../../../EvePlugins/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields",
"depth": 0,
"source": "local",
"dependencies": {}
"dependencies": {
"org.gamecult.cultlib": "1.0.13"
}
},
"org.gamecult.eve.surface": {
"version": "file:../../packages/org.gamecult.eve.surface",
"version": "file:../../../Eve/packages/org.gamecult.eve.surface",
"depth": 0,
"source": "local",
"dependencies": {
"org.gamecult.cultlib": "1.0.3"
"org.gamecult.cultlib": "1.0.45"
}
},
"org.gamecult.eve.unity-scene": {
@ -41,9 +145,13 @@
"depth": 0,
"source": "local",
"dependencies": {
"org.gamecult.cultlib": "1.0.3",
"org.gamecult.eve.plugin-fields": "0.1.1",
"org.gamecult.eve.surface": "0.1.0"
"org.gamecult.cultlib": "1.0.47",
"org.gamecult.eve.plugin-fields": "0.2.3",
"org.gamecult.eve.surface": "0.3.2",
"org.gamecult.eve.unity-uitoolkit": "0.1.4",
"com.unity.mathematics": "1.3.2",
"com.unity.render-pipelines.universal": "17.4.0",
"com.unity.modules.particlesystem": "1.0.0"
}
},
"org.gamecult.eve.unity-uitoolkit": {
@ -51,7 +159,7 @@
"depth": 0,
"source": "local",
"dependencies": {
"org.gamecult.eve.surface": "0.1.0"
"org.gamecult.eve.surface": "0.3.2"
}
},
"com.unity.modules.assetbundle": {
@ -84,12 +192,24 @@
"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,

View file

@ -0,0 +1,368 @@
# Aetheria released-package witness
## Current integration gate
The generic `ReleaseConsumerProject` is pinned to released Git packages:
- `org.gamecult.eve.unity-scene` `0.3.100`, commit
`3632bfb3354065b78a90b921d7025c4c3fa92be4`, and
`org.gamecult.eve.surface` `0.2.4`, commit
`e08fa08335f99e9edddeb706912eecfad07cb281`;
- `org.gamecult.eve.plugin-fields` `0.2.3`, commit
`c5a4a75c1b727499b16c2dae1895f29e2a9f72f0`;
- `org.gamecult.eve.unity-uitoolkit` `0.1.1`, commit
`4d0cbe0185bdc4fc65eb63503a7c5cb578539669`;
- `org.gamecult.cultlib` `1.0.43`, commit
`f67f5122ed1bd11da016e7b820ed60145ccd0299`.
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-codex-cultmesh-reliability `
-CacheState cold `
-AssetCacheDirectory artifacts\cultlib-1043-scene-03100-cold-cache `
-OutputDirectory artifacts\cultlib-1043-scene-03100-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.
The current 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 current 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.
## Historical witness ledger
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.

View file

@ -9,6 +9,9 @@ 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
@ -17,7 +20,11 @@ drivers, asset-provider hooks, package release, runtime tests, and captures.
- `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
@ -29,23 +36,83 @@ drivers, asset-provider hooks, package release, runtime tests, and captures.
## Derived State
`ActiveWorld`, scene objects, transforms, markers, camera pose, asset caches,
`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,
presentation counters, and receipt display state are projections. They are not
world truth.
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.
## 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.
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.
## Shared Paths
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.
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.
## Cut Line

View file

@ -1,7 +0,0 @@
<Project>
<PropertyGroup>
<BaseOutputPath>$(MSBuildThisFileDirectory)bin~\</BaseOutputPath>
<BaseIntermediateOutputPath>$(MSBuildThisFileDirectory)obj~\</BaseIntermediateOutputPath>
<MSBuildProjectExtensionsPath>$(MSBuildThisFileDirectory)obj~\</MSBuildProjectExtensionsPath>
</PropertyGroup>
</Project>

View file

@ -1,14 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Description>Portable Eve surface and provider contracts.</Description>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\CultLib\src\GameCult.Caching\GameCult.Caching.csproj" />
<ProjectReference Include="..\..\..\CultLib\src\GameCult.Mesh\GameCult.Mesh.csproj" />
</ItemGroup>
</Project>

View file

@ -1,29 +0,0 @@
# 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.

View file

@ -1,144 +0,0 @@
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<EveAssetCatalogEntry>();
}
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<string, string> metadata)
{
AssetRef = assetRef ?? "";
AssetKind = assetKind ?? "";
Variants = variants ?? Array.Empty<EveAssetVariant>();
Metadata = metadata == null
? new Dictionary<string, string>(StringComparer.Ordinal)
: new Dictionary<string, string>(metadata, StringComparer.Ordinal);
}
public EveAssetCatalogEntry(
string assetRef,
string assetKind,
EveAssetVariant[] variants,
IReadOnlyDictionary<string, string>? metadata = null)
: this(
assetRef,
assetKind,
variants,
metadata == null ? new Dictionary<string, string>() : new Dictionary<string, string>(metadata))
{
}
[Key(0)] public string AssetRef { get; }
[Key(1)] public string AssetKind { get; }
[Key(2)] public EveAssetVariant[] Variants { get; }
[Key(3)] public Dictionary<string, string> 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<string, string> metadata)
{
RuntimeId = runtimeId ?? "";
Platform = platform ?? "";
Format = format ?? "";
Uri = uri ?? "";
ContentHash = contentHash ?? "";
SizeBytes = sizeBytes;
AssetKey = assetKey ?? "";
Metadata = metadata == null
? new Dictionary<string, string>(StringComparer.Ordinal)
: new Dictionary<string, string>(metadata, StringComparer.Ordinal);
}
public EveAssetVariant(
string runtimeId,
string platform,
string format,
string uri,
string contentHash,
long sizeBytes,
string assetKey,
IReadOnlyDictionary<string, string>? metadata = null)
: this(
runtimeId,
platform,
format,
uri,
contentHash,
sizeBytes,
assetKey,
metadata == null ? new Dictionary<string, string>() : new Dictionary<string, string>(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<string, string> Metadata { get; }
}
}

View file

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 8f53654f7c98d894588127ba001f11b4

View file

@ -1,83 +0,0 @@
using System;
using GameCult.Caching;
using MessagePack;
#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<EveEntitySoaBuffer>();
[Key(9)] public EveEntitySoaColumn[] Columns { get; set; } = Array.Empty<EveEntitySoaColumn>();
[Key(10)] public EveEntitySoaDirtyRange[] DirtyRanges { get; set; } = Array.Empty<EveEntitySoaDirtyRange>();
[Key(11)] public EveEntityRenderGroup[] RenderGroups { get; set; } = Array.Empty<EveEntityRenderGroup>();
}
[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;
}
}

View file

@ -1,55 +0,0 @@
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<EveInputLayoutControl> Controls { get; set; } = Array.Empty<EveInputLayoutControl>();
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;
}
}

View file

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 8a2a1258551e42a9831a3e4f9483d928

View file

@ -1,82 +0,0 @@
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<string> Controls { get; set; } = Array.Empty<string>();
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<EveInputBinding> _bindings;
private readonly HashSet<string> _pressed = new HashSet<string>(StringComparer.Ordinal);
private readonly Dictionary<string, SequenceState> _sequences = new Dictionary<string, SequenceState>(StringComparer.Ordinal);
public EveInputGestureResolver(IEnumerable<EveInputBinding> bindings)
{
_bindings = (bindings ?? Array.Empty<EveInputBinding>()).Where(binding => binding != null).ToArray();
}
public IReadOnlyList<string> Press(string control, long timestampMs)
{
_pressed.Add(control ?? "");
var actions = new HashSet<string>(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; }
}
}
}

View file

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: e3c2941be243a754092ac2543d1e6bf7

View file

@ -1,261 +0,0 @@
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<string> schemas,
IReadOnlyList<EveProviderWitness> witnesses,
IReadOnlyList<EveAdvertisedSurface> surfaces,
IReadOnlyList<EveAdvertisedCommand> 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<string> schemas,
IReadOnlyList<EveProviderWitness> witnesses,
IReadOnlyList<EveAdvertisedSurface> surfaces,
IReadOnlyList<EveAdvertisedCommand> 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<string>();
Witnesses = witnesses ?? Array.Empty<EveProviderWitness>();
Surfaces = surfaces ?? Array.Empty<EveAdvertisedSurface>();
Commands = commands ?? Array.Empty<EveAdvertisedCommand>();
}
[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<string> Schemas { get; }
[Key(10)] public IReadOnlyList<EveProviderWitness> Witnesses { get; }
[Key(11)] public IReadOnlyList<EveAdvertisedSurface> Surfaces { get; }
[Key(12)] public IReadOnlyList<EveAdvertisedCommand> 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)
{
SurfaceId = surfaceId ?? "";
Schema = schema ?? "";
RecordRef = recordRef ?? "";
Transport = transport ?? "";
Status = status ?? "";
SurfaceKind = surfaceKind ?? "";
WorldInteraction = worldInteraction;
}
[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; }
}
[MessagePackObject]
public sealed class EveWorldInteractionAdvertisement
{
[SerializationConstructor]
public EveWorldInteractionAdvertisement(
string projectionKind,
IReadOnlyList<string> stateSchemas,
string commandBoundary,
string commandRecordRef,
string receiptSchema,
string receiptRecordRef,
string assetManifestRecordRef,
IReadOnlyList<string> loweringTargets,
string ownership)
{
ProjectionKind = projectionKind ?? "";
StateSchemas = stateSchemas ?? Array.Empty<string>();
CommandBoundary = commandBoundary ?? "";
CommandRecordRef = commandRecordRef ?? "";
ReceiptSchema = receiptSchema ?? "";
ReceiptRecordRef = receiptRecordRef ?? "";
AssetManifestRecordRef = assetManifestRecordRef ?? "";
LoweringTargets = loweringTargets ?? Array.Empty<string>();
Ownership = ownership ?? "";
}
[Key(0)] public string ProjectionKind { get; }
[Key(1)] public IReadOnlyList<string> 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<string> 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; }
}
}

View file

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: f7ea0cd07c4d5da4099b5acf8d44ca2e

View file

@ -1,874 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GameCult.Mesh;
#nullable enable
namespace GameCult.Eve.Surface
{
/// <summary>
/// Fluent CultUI/Eve surface builder. Eve owns the surface document; CultMesh only supplies typed binding descriptors.
/// </summary>
public sealed class EveSurfaceBuilder
{
private readonly string _surfaceId;
private readonly List<EveSurfaceComponent> _children = new();
private readonly List<EveStyleToken> _styles = new();
private readonly List<EveCommandTemplate> _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<EveSurfaceGroupBuilder> build)
{
return Group(id, "column", build);
}
public EveSurfaceBuilder ButtonRow(string id, Action<EveSurfaceGroupBuilder> build)
{
return Group(id, "row", build);
}
public EveSurfaceBuilder Form(string id, Action<EveSurfaceFormBuilder> 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<EveSurfaceContainerBuilder> build)
{
_root = BuildContainer(id, "partition", Props(("split", "y")), build);
return this;
}
public EveSurfaceBuilder RootRow(string id, Action<EveSurfaceContainerBuilder> build)
{
_root = BuildContainer(id, "partition", Props(("split", "x")), build);
return this;
}
public EveSurfaceBuilder RootGrid(
string id,
int columns,
int rows,
Action<EveSurfaceContainerBuilder> 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<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "partition", Props(("split", "x")), build));
return this;
}
public EveSurfaceBuilder Column(string id, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "partition", Props(("split", "y")), build));
return this;
}
public EveSurfaceBuilder Grid(string id, int columns, int rows, Action<EveSurfaceContainerBuilder> 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<EveSurfaceContainerBuilder> 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<string, string>(StringComparer.Ordinal)
{
["slotId"] = slotId ?? "",
["documentId"] = documentId ?? "",
["schemaId"] = schemaId ?? "",
["presentationKind"] = presentationKind ?? ""
},
Array.Empty<EveSurfaceComponent>(),
Array.Empty<CultMeshStateBindingDescriptor>(),
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<EveSurfaceGroupBuilder> 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<string, string> props,
Action<EveSurfaceContainerBuilder> 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<string, string>(StringComparer.Ordinal)
{
["value"] = value ?? ""
},
Array.Empty<EveSurfaceComponent>());
}
internal static EveSurfaceComponent ButtonComponent(
string id,
string label,
CultMeshOperationBindingDescriptor operation)
{
return new EveSurfaceComponent(
id,
"control.button",
new Dictionary<string, string>(StringComparer.Ordinal)
{
["label"] = label ?? operation.Label,
["command"] = operation.OperationId,
["operationId"] = operation.OperationId,
["schemaId"] = operation.SchemaId
},
Array.Empty<EveSurfaceComponent>());
}
internal static Dictionary<string, string> EmptyProps()
{
return new Dictionary<string, string>(StringComparer.Ordinal);
}
internal static Dictionary<string, string> 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<string, string> _props;
private readonly Action<EveCommandTemplate> _addCommand;
private readonly List<EveSurfaceComponent> _children = new();
private readonly List<CultMeshStateBindingDescriptor> _stateBindings = new();
private readonly List<EveEmbeddedDocumentSlot> _embeddedDocuments = new();
internal EveSurfaceContainerBuilder(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
Action<EveCommandTemplate> addCommand)
{
_id = id ?? "";
_kind = kind ?? "partition";
_props = new Dictionary<string, string>(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<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "partition", EveSurfaceBuilder.Props(("split", "x")), build));
return this;
}
public EveSurfaceContainerBuilder Column(string id, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "partition", EveSurfaceBuilder.Props(("split", "y")), build));
return this;
}
public EveSurfaceContainerBuilder Grid(
string id,
int columns,
int rows,
Action<EveSurfaceContainerBuilder> 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<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "pane", EveSurfaceBuilder.Props(("title", title ?? "")), build));
return this;
}
public EveSurfaceContainerBuilder Card(string id, string title, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "card", EveSurfaceBuilder.Props(("title", title ?? "")), build));
return this;
}
public EveSurfaceContainerBuilder Modal(string id, string title, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "modal", EveSurfaceBuilder.Props(("title", title ?? "")), build));
return this;
}
public EveSurfaceContainerBuilder FieldRow(
string id,
string label,
Action<EveSurfaceContainerBuilder> 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<EveSurfaceContainerBuilder>? 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<EveSurfaceContainerBuilder> 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<EveSurfaceComponent>(),
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<EveSurfaceComponent>(),
Array.Empty<CultMeshStateBindingDescriptor>(),
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<string, string> props,
CultMeshStateBindingDescriptor? binding = null,
IReadOnlyList<EveSurfaceComponent>? children = null,
Action<EveSurfaceContainerBuilder>? configure = null)
{
var builder = new EveSurfaceContainerBuilder(id, kind, props, _addCommand);
configure?.Invoke(builder);
var component = new EveSurfaceComponent(
id ?? "",
kind ?? "",
new Dictionary<string, string>(props ?? EveSurfaceBuilder.EmptyProps(), StringComparer.Ordinal),
children ?? Array.Empty<EveSurfaceComponent>(),
binding == null ? Array.Empty<CultMeshStateBindingDescriptor>() : new[] { binding });
if (builder._props.Count != (props?.Count ?? 0))
{
component = new EveSurfaceComponent(
id ?? "",
kind ?? "",
new Dictionary<string, string>(builder._props, StringComparer.Ordinal),
children ?? Array.Empty<EveSurfaceComponent>(),
binding == null ? Array.Empty<CultMeshStateBindingDescriptor>() : 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<string, string> props,
Action<EveSurfaceContainerBuilder> 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<EveCommandTemplate> _addCommand;
private readonly List<EveSurfaceComponent> _children = new();
internal EveSurfaceGroupBuilder(
string id,
string kind,
Action<EveCommandTemplate> 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<EveCommandTemplate> _addCommand;
private readonly List<EveSurfaceComponent> _children = new();
internal EveSurfaceFormBuilder(string id, Action<EveCommandTemplate> 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<CultMeshStateBindingDescriptor>()
: new[] { binding };
_children.Add(new EveSurfaceComponent(
$"{_id}.metric.{EveSurfaceBuilder.Slug(label)}",
"metric",
new Dictionary<string, string>(StringComparer.Ordinal)
{
["label"] = label ?? "",
["value"] = value ?? ""
},
Array.Empty<EveSurfaceComponent>(),
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<CultMeshStateBindingDescriptor>()
: new[] { binding };
return new EveSurfaceComponent(
$"{_id}.{EveSurfaceBuilder.Slug(label)}",
kind,
new Dictionary<string, string>(StringComparer.Ordinal)
{
["label"] = label ?? "",
["value"] = value ?? "",
["command"] = operation.OperationId,
["operationId"] = operation.OperationId,
["schemaId"] = operation.SchemaId
},
Array.Empty<EveSurfaceComponent>(),
bindings);
}
}
public static class EveSurface
{
public static EveSurfaceBuilder Create(string surfaceId)
{
return new EveSurfaceBuilder(surfaceId);
}
}
}

View file

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 97ac7f757692aef4db64d75d0df6fe51

View file

@ -1,414 +0,0 @@
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<EveCommandTemplate> 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<EveCommandTemplate> 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<EveCommandTemplate>();
}
[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<EveCommandTemplate> Commands { get; }
}
[MessagePackObject]
public sealed class EveSurfaceTree
{
[SerializationConstructor]
public EveSurfaceTree(string id, EveSurfaceComponent root, IReadOnlyList<EveStyleToken> styles)
{
Id = id ?? "";
Root = root ?? throw new ArgumentNullException(nameof(root));
Styles = styles ?? Array.Empty<EveStyleToken>();
}
[Key(0)]
public string Id { get; }
[Key(1)]
public EveSurfaceComponent Root { get; }
[Key(2)]
public IReadOnlyList<EveStyleToken> Styles { get; }
}
[MessagePackObject]
public sealed class EveSurfaceComponent
{
public EveSurfaceComponent(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
IReadOnlyList<EveSurfaceComponent> children)
: this(id, kind, props, children, StateBindingsFromProps(props))
{
}
public EveSurfaceComponent(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
IReadOnlyList<EveSurfaceComponent> children,
IReadOnlyList<CultMeshStateBindingDescriptor> stateBindings)
: this(id, kind, props, children, stateBindings, Array.Empty<EveEmbeddedDocumentSlot>())
{
}
public EveSurfaceComponent(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
IReadOnlyList<EveSurfaceComponent> children,
IReadOnlyList<CultMeshStateBindingDescriptor> stateBindings,
IReadOnlyList<EveEmbeddedDocumentSlot> embeddedDocuments,
IReadOnlyDictionary<string, string>? layout = null,
IReadOnlyDictionary<string, string>? style = null)
: this(
id,
kind,
props,
children,
(stateBindings ?? Array.Empty<CultMeshStateBindingDescriptor>())
.Select(CultMeshStateBindingRecord.FromBinding)
.ToArray(),
embeddedDocuments,
layout,
style)
{
}
[SerializationConstructor]
public EveSurfaceComponent(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
IReadOnlyList<EveSurfaceComponent> children,
CultMeshStateBindingRecord[] stateBindingRecords,
IReadOnlyList<EveEmbeddedDocumentSlot> embeddedDocuments,
IReadOnlyDictionary<string, string>? layout = null,
IReadOnlyDictionary<string, string>? style = null)
{
Id = id ?? "";
Kind = kind ?? "";
Props = props ?? new Dictionary<string, string>(StringComparer.Ordinal);
Children = children ?? Array.Empty<EveSurfaceComponent>();
StateBindingRecords = stateBindingRecords ?? Array.Empty<CultMeshStateBindingRecord>();
EmbeddedDocuments = embeddedDocuments ?? Array.Empty<EveEmbeddedDocumentSlot>();
Layout = layout ?? new Dictionary<string, string>(StringComparer.Ordinal);
Style = style ?? new Dictionary<string, string>(StringComparer.Ordinal);
}
[Key(0)]
public string Id { get; }
[Key(1)]
public string Kind { get; }
[Key(2)]
public IReadOnlyDictionary<string, string> Props { get; }
[Key(3)]
public IReadOnlyList<EveSurfaceComponent> Children { get; }
[Key(4)]
public CultMeshStateBindingRecord[] StateBindingRecords { get; }
[IgnoreMember]
public IReadOnlyList<CultMeshStateBindingDescriptor> StateBindings =>
StateBindingRecords.Select(record => record.ToBinding()).ToArray();
[Key(5)]
public IReadOnlyList<EveEmbeddedDocumentSlot> EmbeddedDocuments { get; }
[Key(6)]
public IReadOnlyDictionary<string, string> Layout { get; }
[Key(7)]
public IReadOnlyDictionary<string, string> Style { get; }
public string GetProp(string key, string fallback = "")
{
return Props.TryGetValue(key, out var value) ? value : fallback;
}
private static IReadOnlyList<CultMeshStateBindingDescriptor> StateBindingsFromProps(
IReadOnlyDictionary<string, string>? props)
{
if (props == null || props.Count == 0)
return Array.Empty<CultMeshStateBindingDescriptor>();
var bindings = new List<CultMeshStateBindingDescriptor>();
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<string, string>(StringComparer.Ordinal),
issuedAt,
clientId,
commandBoundary,
receiptSchema)
{
}
[SerializationConstructor]
public EveSurfaceCommandRequest(
string schema,
string providerId,
string surfaceId,
CultMeshOperationInvocationRecord operation,
Dictionary<string, string> 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<string, string>(StringComparer.Ordinal)
: new Dictionary<string, string>(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<string, string> 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 ?? "";
}
}

View file

@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 6516c42104b235f42b15efeb4639bb73

View file

@ -1,20 +0,0 @@
{
"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
}

View file

@ -1,18 +0,0 @@
{
"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"
}

View file

@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 49f1738bd94075e489c82190cf660b1d
PackageManifestImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -5,22 +5,127 @@ 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, and `GameObject` sink.
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.
`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.
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.
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.<channel>.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.
@ -29,3 +134,55 @@ 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.

View file

@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("GameCult.Eve.UnityScene.Tests")]

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 68e2f1a625b94fc39dcc44d33e0cf318

View file

@ -0,0 +1,296 @@
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
{
/// <summary>
/// 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.
/// </summary>
internal sealed class EveUnityCultMeshCommandOutbox : IDisposable
{
internal enum DeliveryState
{
Queued,
Sending,
AwaitingCanonicalReceipt
}
private readonly object _gate = new object();
private readonly List<OutboxItem> _items = new List<OutboxItem>();
private readonly Dictionary<string, OutboxItem> _byCommandId =
new Dictionary<string, OutboxItem>(StringComparer.Ordinal);
private readonly Dictionary<string, OutboxItem> _continuous =
new Dictionary<string, OutboxItem>(StringComparer.Ordinal);
private readonly Func<EveSurfaceCommandRequest, CancellationToken, Task> _send;
private readonly Func<EveSurfaceCommandRequest, string?> _continuousKey;
private readonly Action<string> _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<EveSurfaceCommandRequest, CancellationToken, Task> send,
Func<EveSurfaceCommandRequest, string?> continuousKey,
Action<string>? 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();
}
/// <summary>Completes a command only when its canonical provider receipt arrives.</summary>
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; }
}
}
}

View file

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 61d76e98f2054d86bf8e7480d4411c39
timeCreated: 1787039650

View file

@ -0,0 +1,49 @@
using System;
using GameCult.Mesh;
using GameCult.Networking;
using GameCult.Networking.WebSockets;
#nullable enable
namespace GameCult.Eve.UnityScene
{
/// <summary>
/// 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.
/// </summary>
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);
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7f7cb06716fd4f319ade147a69acb74a

View file

@ -1,8 +1,17 @@
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
@ -16,24 +25,39 @@ namespace GameCult.Eve.UnityScene
IEveUnitySceneCommandReceiptSource,
IEveUnityProviderRefreshSource,
IEveUnityEntitySoaViewDocumentSource,
IEveUnityFieldsSplatsDocumentSource,
IEveUnityCameraRenderPolicySource,
IEveUnityNativeAssetProvider
IEveUnityNativeAssetProvider,
IEveUnityNativeAssetMetadataProvider,
IEveUnityInputCapabilitySource,
IEveUnityNavigableProvider
{
[SerializeField] private string rendezvousEndpoint = "";
[SerializeField] private string providerFilter = "";
[SerializeField] private string surfaceFilter = "";
[SerializeField] private string verseFilter = "";
[SerializeField] private string surfaceKind = "interactive-world";
[SerializeField] private string replicaPath = "";
[FormerlySerializedAs("replicaPath")]
[SerializeField] private string cacheDirectory = "";
[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 readonly ConcurrentQueue<EveEntitySoaViewDocument> _pendingEntityViews = new ConcurrentQueue<EveEntitySoaViewDocument>();
private PreviousProvider? _previousProvider;
private bool _navigationRouteCommitted;
private int _connectionLeaseCount;
private Task? _preparation;
private CancellationTokenSource? _preparationLifetime;
private readonly ConcurrentQueue<EntityViewLease> _pendingEntityViews = new ConcurrentQueue<EntityViewLease>();
private readonly ConcurrentQueue<EveFieldsSplatsDocument> _pendingFields = new ConcurrentQueue<EveFieldsSplatsDocument>();
public EveEntitySoaViewDocument? CurrentEntityView { get; private set; }
public event Action<EveEntitySoaViewDocument, ICultMeshBodyReadLease>? EntityViewAvailable;
public event Action<EveEntitySoaViewDocument>? EntityViewAvailable;
public event Action<EveFieldsSplatsDocument>? FieldsSplatsAvailable;
public EveUnityCultMeshProviderSelection? Selection { get; private set; }
@ -41,132 +65,541 @@ namespace GameCult.Eve.UnityScene
public EveUnitySceneProviderSurfaceDocument CurrentDocument => Bridge.CurrentSurfaceDocument;
public event Action<EveUnitySceneProviderSurfaceDocument>? DocumentAvailable
public EveInputCapabilityDocument CurrentInputCapability
{
add { Bridge.DocumentAvailable += value; }
remove { Bridge.DocumentAvailable -= value; }
get
{
RequirePrepared();
return _transport!.CurrentInputCapability;
}
}
public event Action<EveUnitySceneCommandReceipt>? ReceiptAvailable
public CultMeshBodyTransportKind? CurrentAssetBodyTransportKind
{
add { Bridge.ReceiptAvailable += value; }
remove { Bridge.ReceiptAvailable -= value; }
get
{
RequirePrepared();
return _transport!.CurrentAssetBodyTransportKind;
}
}
public event Action<EveUnitySceneProviderSurfaceDocument>? DocumentAvailable;
public event Action<EveUnitySceneCommandReceipt>? ReceiptAvailable;
public void Configure(
string endpoint,
string replicaStorePath = "",
string localCacheDirectory = "",
string providerId = "",
string surfaceId = "",
string verseId = "",
string requiredSurfaceKind = "interactive-world",
string clientRuntimeId = "eve-unity")
string clientRuntimeId = "eve-unity",
CultMeshAuthorityTrustPolicy? authorityTrust = null,
CultMeshAuthorityTrustPolicy? navigationAuthorityTrust = null)
{
if (_bridge != null)
if (_bridge != null || _preparation != null)
throw new InvalidOperationException("Disconnect the active provider before changing discovery configuration.");
rendezvousEndpoint = endpoint ?? "";
replicaPath = replicaStorePath ?? "";
cacheDirectory = localCacheDirectory ?? "";
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()
{
_bridge?.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<string>())
.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);
}
public void Refresh()
{
if (Bridge.IsConnected)
Bridge.Refresh();
var activeBridge = _navigationRouteCommitted
? Bridge
: _previousProvider?.Bridge ?? Bridge;
if (activeBridge.IsConnected)
activeBridge.Refresh();
else
Bridge.Connect();
activeBridge.Connect();
}
public void Submit(EveSurfaceCommandRequest request)
{
Bridge.Submit(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);
}
public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset)
{
EnsureTransport();
RequirePrepared();
return _transport!.ResolvePrefab(asset);
}
public UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType)
{
EnsureTransport();
RequirePrepared();
return _transport!.ResolveAsset(asset, assetType);
}
public bool TryGetCameraCullingMask(string viewId, out int cullingMask)
public bool TryResolveAssetMetadata(
EveUnityPlayableWorldAssetBinding asset,
out IReadOnlyDictionary<string, string> metadata)
{
EnsureTransport();
return _transport!.TryGetCameraCullingMask(viewId, out cullingMask);
RequirePrepared();
return _transport!.TryResolveAssetMetadata(asset, out metadata);
}
public bool TryGetRenderChannelLayer(string channel, out int layer)
{
RequirePrepared();
return _transport!.TryGetRenderChannelLayer(channel, out layer);
}
private void OnDestroy()
{
ReleaseTransport();
}
private void ReleaseTransport()
{
_preparationLifetime?.Cancel();
if (_bridge != null)
{
_bridge.DocumentAvailable -= ForwardDocument;
_bridge.ReceiptAvailable -= ForwardReceipt;
}
_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;
}
private void Update()
{
_previousProvider?.Transport?.PumpLiveEvents();
_transport?.PumpLiveEvents();
EveEntitySoaViewDocument? latest = null;
while (_pendingEntityViews.TryDequeue(out var next)) latest = next;
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;
}
if (latest == null) return;
CurrentEntityView = latest;
EntityViewAvailable?.Invoke(latest);
var handler = EntityViewAvailable;
if (handler == null) latest.Lease.Dispose();
else handler(latest.Document, latest.Lease);
}
private EveUnitySceneLiveProviderBridge Bridge
{
get
{
EnsureTransport();
RequirePrepared();
return _bridge!;
}
}
private void EnsureTransport()
private async Task PrepareTransportAsync(CancellationToken cancellationToken)
{
var elapsed = Stopwatch.StartNew();
if (_transport != null)
return;
if (string.IsNullOrWhiteSpace(rendezvousEndpoint))
throw new InvalidOperationException("EveUnity requires a CultMesh rendezvous endpoint.");
Selection = new EveUnityCultMeshProviderDiscovery().Discover(
rendezvousEndpoint,
var prepared = await PrepareCandidateAsync(
new[] { rendezvousEndpoint },
providerFilter,
surfaceFilter,
surfaceKind,
verseFilter);
var resolvedReplicaPath = string.IsNullOrWhiteSpace(replicaPath)
? Path.Combine(Application.temporaryCachePath, $"eve-unity-{GetInstanceID()}.cc")
: replicaPath;
_transport = new EveUnityCultMeshLiveProviderTransport(
resolvedReplicaPath,
Selection.Endpoint,
Selection.ProviderId,
Selection.SurfaceId,
runtimeId);
_transport.EntityViewAvailable += view => _pendingEntityViews.Enqueue(view);
_bridge = new EveUnitySceneLiveProviderBridge(_transport);
verseFilter,
"",
_authorityTrust,
_navigationAuthorityTrust,
cancellationToken).ConfigureAwait(false);
TraceStartup($"discovery and transport preparation {elapsed.Elapsed.TotalMilliseconds:0.###}ms");
AdoptPrepared(prepared);
}
private async Task<PreparedProvider> PrepareCandidateAsync(
IReadOnlyList<string> endpoints,
string requiredProviderId,
string requiredSurfaceId,
string requiredSurfaceKind,
string requiredVerseId,
string requiredAuthorityRuntimeId,
CultMeshAuthorityTrustPolicy trust,
CultMeshAuthorityTrustPolicy crossTargetTrust,
CancellationToken cancellationToken)
{
var failures = new List<string>();
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}");
}
}
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,
_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; }
}
}
}

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using GameCult.Eve.Surface;
using GameCult.Mesh;
@ -13,21 +14,24 @@ namespace GameCult.Eve.UnityScene
public sealed class EveUnityCultMeshProviderSelection
{
public EveUnityCultMeshProviderSelection(
string endpoint,
string rendezvousEndpoint,
string verseId,
string authorityRuntimeId,
string providerId,
string surfaceId,
string surfaceKind)
{
Endpoint = endpoint ?? "";
RendezvousEndpoint = rendezvousEndpoint ?? "";
VerseId = verseId ?? "";
AuthorityRuntimeId = authorityRuntimeId ?? "";
ProviderId = providerId ?? "";
SurfaceId = surfaceId ?? "";
SurfaceKind = surfaceKind ?? "";
}
public string Endpoint { get; }
public string RendezvousEndpoint { get; }
public string VerseId { get; }
public string AuthorityRuntimeId { get; }
public string ProviderId { get; }
public string SurfaceId { get; }
public string SurfaceKind { get; }
@ -35,104 +39,129 @@ namespace GameCult.Eve.UnityScene
public sealed class EveUnityCultMeshProviderDiscovery
{
private static readonly Type[] AdvertisementDocumentTypes =
{
typeof(EveProviderAdvertisementDocument)
};
private readonly CultMeshAuthorityTrustPolicy _authorityTrust;
public EveUnityCultMeshProviderSelection Discover(
public EveUnityCultMeshProviderDiscovery(CultMeshAuthorityTrustPolicy? authorityTrust = null)
{
_authorityTrust = authorityTrust ?? new CultMeshAuthorityTrustPolicy(
CultMeshAuthorityTrustMode.AuthenticatedRemote);
}
public async Task<EveUnityCultMeshProviderSelection> DiscoverAsync(
string rendezvousEndpoint,
string providerId = "",
string surfaceId = "",
string surfaceKind = "interactive-world",
string verseId = "")
string verseId = "",
CancellationToken cancellationToken = default,
string requiredAuthorityRuntimeId = "")
{
if (string.IsNullOrWhiteSpace(rendezvousEndpoint))
throw new ArgumentException("Rendezvous endpoint must be non-empty.", nameof(rendezvousEndpoint));
var response = RunNetwork(() => CultMesh.CreateVerseDiscoveryClient().FetchAsync(
CultMeshVerseCatalogResponseMessage response;
try
{
response = await CultMesh.CreateVerseDiscoveryClient(EveUnityCultMeshConnectivity.Discovery()).FetchAsync(
rendezvousEndpoint,
new CultMeshVerseCatalogRequestMessage
{
VerseIds = string.IsNullOrWhiteSpace(verseId) ? null : new[] { verseId },
TransportVersion = "cultmesh.v0"
}));
});
cancellationToken.ThrowIfCancellationRequested();
}
catch (Exception error)
{
throw new InvalidOperationException(
$"Could not query CultMesh rendezvous endpoint '{rendezvousEndpoint}'.",
error);
}
var candidates = response.Verses
.Where(verse => string.IsNullOrWhiteSpace(verseId) ||
string.Equals(verse.VerseId, verseId, StringComparison.Ordinal))
.SelectMany(verse => (verse.DiscoveryEndpoints ?? Array.Empty<string>())
.Select(endpoint => new { Verse = verse, Endpoint = endpoint }))
.Where(candidate => !string.IsNullOrWhiteSpace(candidate.Endpoint))
.Where(verse => !string.IsNullOrWhiteSpace(verse.VerseId) &&
(verse.DiscoveryEndpoints ?? Array.Empty<string>()).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<string>();
var observed = new List<string>();
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)
{
try
foreach (var authorityRuntimeId in EligibleAuthorityRuntimeIds(
candidate.AuthorityRuntimeIds,
requiredAuthorityRuntimeId))
{
var advertisement = FetchAdvertisements(candidate.Endpoint)
.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;
try
{
var target = new CultMeshSessionTarget(candidate.VerseId, authorityRuntimeId);
using var advertisementsLease = await mesh
.LeaseCollectionAsync<EveProviderAdvertisementDocument>(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(
candidate.Endpoint,
candidate.Verse.VerseId,
advertisement.Document.ProviderId,
advertisement.Surface.SurfaceId,
advertisement.Surface.SurfaceKind);
}
catch (Exception error)
{
failures.Add($"{candidate.Endpoint}: {error.Message}");
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}'";
var filter = $"provider='{providerId}', surface='{surfaceId}', kind='{surfaceKind}', authority='{requiredAuthorityRuntimeId}'";
var detail = failures.Count == 0 ? "" : $" Endpoint failures: {string.Join(" | ", failures)}";
throw new InvalidOperationException($"No advertised Eve surface matched {filter}.{detail}");
var observedDetail = observed.Count == 0 ? "" : $" Observed: {string.Join(" | ", observed)}";
throw new InvalidOperationException($"No advertised Eve surface matched {filter}.{detail}{observedDetail}");
}
private static EveProviderAdvertisementDocument[] FetchAdvertisements(string endpoint)
{
var cacheRegistry = CultMesh.CreateCultCacheDocumentRegistry(AdvertisementDocumentTypes);
var networkRegistry = CultMesh.CreateCultNetDocumentRegistry(AdvertisementDocumentTypes, cacheRegistry);
var snapshot = CultMesh.SnapshotEndpoint(
endpoint,
new CultMeshSnapshotEndpointOptions
{
Context = CultMesh.Verse("eve.discovery", "eve-unity").Context,
DocumentRegistry = networkRegistry,
Request = new CultMeshSnapshotRequestOptions
{
ShardId = "provider",
ShardEpoch = 1,
ConnectTimeout = TimeSpan.FromSeconds(5),
ResponseTimeout = TimeSpan.FromSeconds(10),
MessageIdPrefix = "eve-unity-discovery",
RudpRuntimeId = "eve-unity.discovery",
RudpMaxFragmentBytes = 1024
}
});
return RunNetwork(() => snapshot.FetchDocumentsAsync<EveProviderAdvertisementDocument>())
internal static IReadOnlyList<string> EligibleAuthorityRuntimeIds(
IEnumerable<string>? authorityRuntimeIds,
string requiredAuthorityRuntimeId) =>
(authorityRuntimeIds ?? Array.Empty<string>())
.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();
}
private static T RunNetwork<T>(Func<Task<T>> operation) =>
Task.Run(operation).GetAwaiter().GetResult();
}
}

View file

@ -4,6 +4,7 @@
"references": [
"GameCult.Eve.Surface",
"GameCult.Eve.UnityScene",
"GameCult.Eve.PluginFields",
"UnityEngine.AssetBundleModule"
],
"includePlatforms": [],
@ -14,7 +15,9 @@
"GameCult.Caching.dll",
"GameCult.Caching.MessagePack.dll",
"GameCult.Mesh.dll",
"GameCult.Networking.dll"
"GameCult.Mesh.Quic.Native.dll",
"GameCult.Networking.dll",
"GameCult.Networking.WebSockets.dll"
],
"autoReferenced": true,
"defineConstraints": [],

View file

@ -0,0 +1,226 @@
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<Camera>();
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<ComputeShader>("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<UniversalCameraData>();
if (cameraData.camera == null || cameraData.camera != _owner._camera) return;
var resourceData = frameData.Get<UniversalResourceData>();
if (resourceData.isActiveTargetBackBuffer) return;
var colorTarget = resourceData.activeColorTexture;
if (!colorTarget.IsValid()) return;
using var builder = renderGraph.AddUnsafePass<PassData>("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));
}
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3cb42e7da8b34febb348f01a598f2eea

View file

@ -0,0 +1,130 @@
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<string> InputValuePayloadKeys => _action.InputValue?.PayloadKeys ?? Array.Empty<string>();
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<EveInputActionDocument>()).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<string, string> BuildPayload(string entityId, float? inputValue = null)
{
var payload = _action.Payload == null
? new Dictionary<string, string>(StringComparer.Ordinal)
: new Dictionary<string, string>(_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<string, string> 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<string, string> 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);
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: c7555b03e7f9451aa891e92958b845c5

View file

@ -0,0 +1,78 @@
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<EveUnityPlayableWorldEntityMarker>(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;
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f859926d39664cefa5aa96ed3c2d8903

View file

@ -0,0 +1,123 @@
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<LineRenderer>();
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);
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 97c7b809756a4350a6e61c67bba69b82

View file

@ -0,0 +1,69 @@
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<EveUnityBeamPresentation> FindAll(EveUnitySceneProjection? projection)
{
var found = new List<EveUnityBeamPresentation>();
if (projection != null) FindAll(projection.Root, found);
return found;
}
private static void FindAll(EveUnitySceneNode node, ICollection<EveUnityBeamPresentation> 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);
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3086cd42c94f2624197d19704909f3f5

View file

@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using UnityEngine;
#nullable enable
namespace GameCult.Eve.UnityScene
{
public sealed class EveUnityBeamPresentationRenderer : MonoBehaviour
{
private readonly Dictionary<string, ActiveBeam> _active = new Dictionary<string, ActiveBeam>(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<ParticleSystem>(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<EveUnityBeamPresentation> presentations,
IEveUnityPresentedEntityRegistry? entities,
IEveUnityGameObjectAssetProvider? assetProvider)
{
var retained = new HashSet<string>(StringComparer.Ordinal);
foreach (var presentation in presentations ?? Array.Empty<EveUnityBeamPresentation>())
{
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<ParticleSystem>(true))
{
var emission = particles.emission;
emission.rateOverTimeMultiplier = visiblePower;
}
beam.Power = visiblePower;
}
var stale = new List<string>();
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; }
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f91c99c24bc0e20488fa03daef2eec06

View file

@ -0,0 +1,98 @@
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)));
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e15acab8e9094b18b6dbd3abea0e055d

View file

@ -0,0 +1,279 @@
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<EveUnityPlayableWorldEntityMarker>(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<LineRenderer>();
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<Renderer>(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);
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5c953d99a7454322b2935b878295b357

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using GameCult.Eve.Surface;
using GameCult.Mesh;
#nullable enable
@ -9,8 +10,7 @@ namespace GameCult.Eve.UnityScene
{
public interface IEveUnityEntitySoaViewDocumentSource
{
EveEntitySoaViewDocument? CurrentEntityView { get; }
event Action<EveEntitySoaViewDocument> EntityViewAvailable;
event Action<EveEntitySoaViewDocument, ICultMeshBodyReadLease> EntityViewAvailable;
}
/// <summary>Projects portable entity SoA generations into the ordinary Unity scene sink.</summary>
@ -24,9 +24,10 @@ namespace GameCult.Eve.UnityScene
_sink = sink ?? throw new ArgumentNullException(nameof(sink));
}
public void Apply(EveEntitySoaViewDocument document)
public void Apply(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease)
{
using var view = EveUnityEntitySoaView.Open(document);
using var view = EveUnityEntitySoaView.Open(document, lease);
var presented = new List<EveUnityPresentedEntity>();
var next = new HashSet<string>(StringComparer.Ordinal);
for (var row = 0; row < view.EntityCount; row++)
{
@ -34,40 +35,60 @@ 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 entityId = $"entity:{entityIndex}";
var identity = document.Identities.FirstOrDefault(item => item.Index == entityIndex);
var entityId = identity?.EntityId ?? $"entity:{entityIndex}";
next.Add(entityId);
var assetRef = renderGroup?.MeshAssetRef ?? "";
var assetRef = string.IsNullOrWhiteSpace(identity?.AssetRef)
? renderGroup?.MeshAssetRef ?? ""
: identity!.AssetRef;
var scalarState = new Dictionary<string, float>(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;
_sink.UpsertEntity(
new EveUnityPlayableWorldEntity(
entityId,
entityId,
"entity",
entityId,
"",
assetRef,
position.x,
position.y,
position.z,
rotationRadians * 57.2957795f,
scale > 0f ? scale : renderGroup?.DefaultScale ?? 1f,
selectable: true,
controllable: false,
focusCommand: "",
moveCommand: "",
targetCommand: "",
actionCommand: "",
props: new Dictionary<string, string>()),
fact.ToPlayableWorldEntity(),
new EveUnityPlayableWorldAssetBinding(
assetRef,
"entity",
string.IsNullOrWhiteSpace(assetRef) ? "unity-generated-placeholder" : "provider-asset-ref"));
}
foreach (var entityId in _known.Where(entityId => !next.Contains(entityId)).ToArray())
_sink.RemoveEntity(entityId);
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);
}
_known.Clear();
foreach (var entityId in next) _known.Add(entityId);
}

View file

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO.MemoryMappedFiles;
using System.Linq;
using GameCult.Eve.Surface;
using GameCult.Mesh;
using UnityEngine;
#nullable enable
@ -11,104 +11,131 @@ namespace GameCult.Eve.UnityScene
{
public sealed class EveUnityEntitySoaView : IDisposable
{
private readonly Dictionary<string, MemoryMappedViewAccessor> _buffers =
new Dictionary<string, MemoryMappedViewAccessor>(StringComparer.Ordinal);
private readonly ICultMeshBodyReadLease _lease;
private readonly Dictionary<string, EveEntitySoaBuffer> _buffers;
private readonly Dictionary<string, EveEntitySoaColumn> _columns;
private readonly string[] _floatSemantics;
private EveUnityEntitySoaView(EveEntitySoaViewDocument document)
private EveUnityEntitySoaView(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease)
{
Document = document;
_lease = lease;
_buffers = document.Buffers.ToDictionary(buffer => buffer.BufferId, StringComparer.Ordinal);
_columns = document.Columns.ToDictionary(column => column.Semantic, StringComparer.Ordinal);
foreach (var buffer in document.Buffers)
{
if (!string.Equals(buffer.Backend, "memory_mapped_file", StringComparison.Ordinal))
throw new NotSupportedException($"EveUnity does not support entity SoA backend '{buffer.Backend}'.");
var mapped = MemoryMappedFile.OpenExisting(buffer.Location, MemoryMappedFileRights.Read);
_buffers.Add(buffer.BufferId, mapped.CreateViewAccessor(
buffer.ByteOffset,
buffer.ByteLength,
MemoryMappedFileAccess.Read));
mapped.Dispose();
}
_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.Generation;
public long Generation => Document.Sequence;
public IReadOnlyList<EveEntityRenderGroup> RenderGroups => Document.RenderGroups;
public int EntityCount => Document.Columns.Length == 0 ? 0 : Document.Columns.Min(column => column.ElementCount);
public IReadOnlyList<string> FloatSemantics => _floatSemantics;
public static EveUnityEntitySoaView Open(EveEntitySoaViewDocument document)
public static EveUnityEntitySoaView Open(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease)
{
if (document == null) throw new ArgumentNullException(nameof(document));
if (!string.Equals(document.Schema, EveEntitySoaViewDocument.SchemaId, StringComparison.Ordinal))
throw new ArgumentException($"Expected {EveEntitySoaViewDocument.SchemaId}, got '{document.Schema}'.", nameof(document));
return new EveUnityEntitySoaView(document);
if (lease == null) throw new ArgumentNullException(nameof(lease));
Validate(document, lease);
return new EveUnityEntitySoaView(document, lease);
}
public bool TryReadVector3(string semantic, int index, out Vector3 value)
{
value = default;
if (!TryColumn(semantic, index, "float3", 12, out var column, out var accessor)) return false;
var offset = column.ByteOffset + (long)index * column.ElementStride;
value = new Vector3(accessor.ReadSingle(offset), accessor.ReadSingle(offset + 4), accessor.ReadSingle(offset + 8));
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));
return true;
}
public bool TryReadFloat(string semantic, int index, out float value)
{
value = default;
if (!TryColumn(semantic, index, "float32", 4, out var column, out var accessor)) return false;
value = accessor.ReadSingle(column.ByteOffset + (long)index * column.ElementStride);
if (!TryColumn(semantic, index, "float32", 4, out var offset)) return false;
value = _lease.ReadSingle(offset);
return true;
}
public bool TryReadInt32(string semantic, int index, out int value)
{
value = default;
if (!TryColumn(semantic, index, "int32", 4, out var column, out var accessor)) return false;
value = accessor.ReadInt32(column.ByteOffset + (long)index * column.ElementStride);
if (!TryColumn(semantic, index, "int32", 4, out var offset)) return false;
value = _lease.ReadInt32(offset);
return true;
}
public bool TryReadUInt32(string semantic, int index, out uint value)
{
value = default;
if (!TryColumn(semantic, index, "uint32", 4, out var column, out var accessor)) return false;
value = accessor.ReadUInt32(column.ByteOffset + (long)index * column.ElementStride);
if (!TryColumn(semantic, index, "uint32", 4, out var offset)) return false;
value = unchecked((uint)_lease.ReadInt32(offset));
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 column, out var accessor)) return false;
value = accessor.ReadByte(column.ByteOffset + (long)index * column.ElementStride);
if (!TryColumn(semantic, index, "uint8", 1, out var offset)) return false;
value = _lease.ReadByte(offset);
return true;
}
public void Dispose()
public void Dispose() => _lease.Dispose();
private static void Validate(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease)
{
foreach (var accessor in _buffers.Values) accessor.Dispose();
_buffers.Clear();
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<EveEntitySoaColumn>())
{
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.");
}
}
private bool TryColumn(
string semantic,
int index,
string scalarType,
int minimumStride,
out EveEntitySoaColumn column,
out MemoryMappedViewAccessor accessor)
private bool TryColumn(string semantic, int index, string scalarType, int minimumStride, out long offset)
{
column = null!;
accessor = null!;
if (index < 0 || !_columns.TryGetValue(semantic ?? "", out column)) return false;
offset = 0;
if (index < 0 || !_columns.TryGetValue(semantic ?? "", out var column)) return false;
if (index >= column.ElementCount || column.ElementStride < minimumStride) return false;
if (!string.Equals(column.ScalarType, scalarType, StringComparison.Ordinal)) return false;
return _buffers.TryGetValue(column.BufferId, out accessor);
if (!_buffers.TryGetValue(column.BufferId, out var buffer)) return false;
offset = buffer.ByteOffset + column.ByteOffset + (long)index * column.ElementStride;
return true;
}
private static int ScalarWidth(string scalarType) => scalarType switch
{
"float3" => 12,
"float32" => 4,
"int32" => 4,
"uint32" => 4,
"uint8" => 1,
_ => 0
};
}
}

View file

@ -11,10 +11,13 @@ 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");
ItemKey = Get(node, "itemKey"); Position = Get(node, "position"); Reason = Get(node, "reason");
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; }
@ -26,7 +29,11 @@ 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;

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
#nullable enable
@ -16,11 +17,26 @@ namespace GameCult.Eve.UnityScene
UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType);
}
public sealed class EveUnityGameObjectPlayableWorldSceneSink : IEveUnityPlayableWorldSceneSink
public interface IEveUnityNativeAssetMetadataProvider
{
bool TryResolveAssetMetadata(
EveUnityPlayableWorldAssetBinding asset,
out IReadOnlyDictionary<string, string> metadata);
}
public sealed class EveUnityGameObjectPlayableWorldSceneSink :
IEveUnityPlayableWorldSceneSink,
IEveUnityEntityGenerationSink,
IEveUnityPresentedEntityRegistry,
IDisposable
{
private readonly Transform _root;
private readonly IEveUnityGameObjectAssetProvider _assetProvider;
private readonly Dictionary<string, GameObject> _instances = new Dictionary<string, GameObject>(StringComparer.Ordinal);
private IReadOnlyDictionary<string, EveUnityPresentedEntityHandle> _presentedById =
new Dictionary<string, EveUnityPresentedEntityHandle>(StringComparer.Ordinal);
private IReadOnlyDictionary<int, EveUnityPresentedEntityHandle> _presentedByIndex =
new Dictionary<int, EveUnityPresentedEntityHandle>();
public EveUnityGameObjectPlayableWorldSceneSink(
Transform root,
@ -31,6 +47,13 @@ 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)
{
@ -66,6 +89,90 @@ 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<string, GameObject>(StringComparer.Ordinal);
var nextInstances = new Dictionary<string, GameObject>(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);
}
}
public void RemoveEntity(string entityId)
{
if (string.IsNullOrWhiteSpace(entityId))
@ -80,14 +187,33 @@ namespace GameCult.Eve.UnityScene
DestroyInstance(instance);
}
private GameObject InstantiateEntity(EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset)
public void Dispose()
{
foreach (var instance in _instances.Values)
if (instance != null)
{
instance.transform.SetParent(null, false);
instance.SetActive(false);
DestroyInstance(instance);
}
_instances.Clear();
_presentedById = new Dictionary<string, EveUnityPresentedEntityHandle>(StringComparer.Ordinal);
_presentedByIndex = new Dictionary<int, EveUnityPresentedEntityHandle>();
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(_root, false);
instance.transform.SetParent(parent, false);
var visual = UnityEngine.Object.Instantiate(prefab, instance.transform);
visual.transform.localPosition = Vector3.zero;
visual.transform.localRotation = Quaternion.identity;
@ -97,8 +223,8 @@ namespace GameCult.Eve.UnityScene
instance = GameObject.CreatePrimitive(PrimitiveType.Capsule);
}
if (instance.transform.parent != _root)
instance.transform.SetParent(_root, false);
if (instance.transform.parent != parent)
instance.transform.SetParent(parent, false);
return instance;
}
@ -113,9 +239,7 @@ 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);
if (entity.Radius > 0f &&
string.Equals(asset.PresentationKind, "unity-generated-placeholder", StringComparison.Ordinal))
instance.transform.localScale = Vector3.one * entity.Radius;
instance.transform.localScale = Vector3.one * (entity.Radius > 0f ? entity.Radius : 1f);
var marker = instance.GetComponent<EveUnityPlayableWorldEntityMarker>();
if (marker == null)

View file

@ -0,0 +1,295 @@
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<string, ActionRow> _rows =
new Dictionary<string, ActionRow>(StringComparer.Ordinal);
private readonly HashSet<string> _pressedHolds = new HashSet<string>(StringComparer.Ordinal);
private EveUnityPlayableWorldClientHost? _host;
private UIDocument? _document;
private VisualElement? _root;
private string _structureKey = "";
private float _nextRefreshAt;
public VisualElement? Root => _root;
public IReadOnlyCollection<string> 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<EveInputActionDocument> SelectActions(EveInputCapabilityDocument? capability)
{
if (capability == null) return Array.Empty<EveInputActionDocument>();
var actions = (capability.Actions ?? Array.Empty<EveInputActionDocument>())
.Where(action => action != null && !string.IsNullOrWhiteSpace(action.ActionId))
.ToDictionary(action => action.ActionId, StringComparer.Ordinal);
var selected = new List<EveInputActionDocument>();
var seen = new HashSet<string>(StringComparer.Ordinal);
var profile = (capability.DefaultProfiles ?? Array.Empty<EveInputProfileDocument>())
.FirstOrDefault(candidate => string.Equals(candidate.DeviceClass, "keyboard-mouse", StringComparison.Ordinal))
?? (capability.DefaultProfiles ?? Array.Empty<EveInputProfileDocument>()).FirstOrDefault();
foreach (var binding in profile?.Bindings ?? Array.Empty<EveInputBindingDocument>())
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<EveInputActionDocument> 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<KeyDownEvent>(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<PointerDownEvent>(evt =>
{
if (!_pressedHolds.Add(action.ActionId)) return;
trigger.CapturePointer(evt.pointerId);
SubmitHold(action.ActionId, true);
});
trigger.RegisterCallback<PointerUpEvent>(evt =>
{
ReleaseHold(action.ActionId);
if (trigger.HasPointerCapture(evt.pointerId)) trigger.ReleasePointer(evt.pointerId);
});
trigger.RegisterCallback<PointerCaptureOutEvent>(_ => 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; }
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: bb62601554a846f39e46a6969c10eff7

View file

@ -1,4 +1,6 @@
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
#nullable enable
@ -6,11 +8,13 @@ namespace GameCult.Eve.UnityScene
{
public interface IEveUnityCameraRenderPolicySource
{
bool TryGetCameraCullingMask(string viewId, out int cullingMask);
bool TryGetRenderChannelLayer(string channel, out int layer);
}
public sealed class EveUnityPlayableWorldCameraRig : MonoBehaviour
{
private static EveUnityPlayableWorldCameraRig? _environmentOwner;
[SerializeField] private EveUnityPlayableWorldClientHost? host;
[SerializeField] private Transform? cameraTransform;
[SerializeField] private bool driveInLateUpdate = true;
@ -19,17 +23,49 @@ 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 => host = value;
set
{
if (ReferenceEquals(host, value)) return;
ReleaseRig();
host = value;
}
}
public Transform? CameraTransform
{
get => cameraTransform;
set => cameraTransform = value;
set
{
if (ReferenceEquals(cameraTransform, value)) return;
ReleaseRig();
cameraTransform = value;
}
}
public IEveUnityCameraRenderPolicySource? RenderPolicySource
@ -38,34 +74,102 @@ 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 player = FindPlayerMarker(resolvedHost, activeWorld.PlayerEntityId);
var targetEntityId = string.IsNullOrWhiteSpace(activeWorld.CameraTargetEntityId)
? activeWorld.PlayerEntityId
: activeWorld.CameraTargetEntityId;
var player = FindEntity(resolvedHost, targetEntityId);
if (player == null)
{
ReleaseCamera();
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<Camera>();
if (cameraComponent != null &&
_renderPolicySource != null &&
_renderPolicySource.TryGetCameraCullingMask(activeWorld.ViewId, out var cullingMask))
if (postProcessProfile != null && cameraComponent == null)
{
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;
}
var bounds = CalculateVisualBounds(player);
var target = bounds?.center ?? player.transform.position;
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 radius = bounds?.extents.magnitude ?? 0f;
var verticalFov = cameraComponent != null ? cameraComponent.fieldOfView : 60f;
var framingDistance = radius > 0f
@ -84,23 +188,524 @@ namespace GameCult.Eve.UnityScene
return true;
}
private static Bounds? CalculateVisualBounds(EveUnityPlayableWorldEntityMarker player)
private static bool ApplyPlanarTopDown(
EveUnityPlayableWorldProjection world,
Transform camera,
Camera? cameraComponent,
Vector3 target,
float deltaTime)
{
var renderers = player.GetComponentsInChildren<Renderer>(includeInactive: false);
Bounds? bounds = null;
foreach (var visual in renderers)
var resolvedDistance = world.CameraDistance;
var verticalFov = world.CameraVerticalFieldOfViewDegrees;
if (cameraComponent != null)
{
if (visual == null || !visual.enabled)
continue;
if (bounds.HasValue)
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<Light>();
created.type = LightType.Directional;
created.shadows = LightShadows.None;
}
var light = _keyLightObject.GetComponent<Light>();
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<UniversalAdditionalCameraData>();
_createdPostProcessCameraData = _postProcessCameraData == null;
if (_postProcessCameraData == null)
_postProcessCameraData = camera.gameObject.AddComponent<UniversalAdditionalCameraData>();
_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)
{
var combined = bounds.Value;
combined.Encapsulate(visual.bounds);
bounds = combined;
_postProcessVolumeObject = new GameObject("Eve World Post Process");
_postProcessVolumeObject.transform.SetParent(transform, worldPositionStays: false);
_postProcessVolume = _postProcessVolumeObject.AddComponent<Volume>();
_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<EveUnityAdaptiveExposureRenderer>();
_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
{
bounds = visual.bounds;
_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)
{
Bounds? bounds = null;
foreach (var root in new[] { player })
{
foreach (var visual in root.GetComponentsInChildren<Renderer>(includeInactive: false))
{
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;
}
}
}
return bounds;
@ -112,6 +717,10 @@ namespace GameCult.Eve.UnityScene
ApplyRig(Time.deltaTime);
}
private void OnDisable() => ReleaseRig();
private void OnDestroy() => ReleaseRig();
private EveUnityPlayableWorldClientHost? ResolveHost()
{
if (host != null)
@ -121,18 +730,23 @@ namespace GameCult.Eve.UnityScene
return host;
}
private static EveUnityPlayableWorldEntityMarker? FindPlayerMarker(
private static Transform? FindEntity(
EveUnityPlayableWorldClientHost host,
string playerEntityId)
{
if (string.IsNullOrWhiteSpace(playerEntityId))
return null;
if (host.PresentedEntities?.CurrentGeneration != null)
return host.PresentedEntities.TryGetByEntityId(playerEntityId, out var presented)
? presented.Transform
: null;
var markers = host.SceneRoot.GetComponentsInChildren<EveUnityPlayableWorldEntityMarker>();
foreach (var marker in markers)
{
if (marker != null && marker.EntityId == playerEntityId)
return marker;
return marker.transform;
}
return null;

View file

@ -1,4 +1,5 @@
using System;
using System.Collections;
using UnityEngine;
#nullable enable
@ -29,13 +30,378 @@ namespace GameCult.Eve.UnityScene
public EveUnityPlayableWorldPresentation? LastPresentation { get; private set; }
public EveUnitySceneNavigationFailure? LastNavigationFailure { get; private set; }
public event Action<EveUnitySceneNavigationFailure>? 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<IEveUnitySceneProviderSurfaceDocumentSource>(
providerSurfaceDocuments,
resolvedProvider,
nameof(providerSurfaceDocuments));
var resolvedCommandSink = ResolveBehaviour<IEveUnitySceneCommandSink>(
commandSink,
resolvedProvider,
nameof(commandSink));
var resolvedAssetDocuments = ResolveOptionalBehaviour<IEveUnityPlayableWorldAssetManifestDocumentSource>(
assetManifestDocuments,
resolvedProvider);
var resolvedReceiptSource = ResolveOptionalBehaviour<IEveUnitySceneCommandReceiptSource>(
receiptSource,
resolvedProvider);
var resolvedFallbackAssetProvider = ResolveOptionalBehaviour<IEveUnityGameObjectAssetProvider>(
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<EveUnityPlayableWorldClientHost>();
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<EveUnityPlayableWorldInputDriver>();
var previousInputHost = input?.Host;
var previousInputCamera = input?.CameraTransform;
var rig = GetComponent<EveUnityPlayableWorldCameraRig>();
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()
@ -191,4 +557,21 @@ 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; }
}
}

View file

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using GameCult.Eve.Surface;
using GameCult.Eve.UnityScene.Fields;
using UnityEngine;
#nullable enable
@ -21,6 +22,7 @@ namespace GameCult.Eve.UnityScene
[SerializeField] private bool renderShotTrajectories = true;
private float _nextRefreshAt;
private object? _cameraOwner;
public EveUnityPlayableWorldRuntime? Runtime { get; private set; }
@ -31,8 +33,16 @@ 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<EveUnityFeedbackEvent>? FeedbackAvailable;
public event Action<EveUnityShotReceipt>? ShotAvailable;
@ -88,17 +98,57 @@ namespace GameCult.Eve.UnityScene
var feedbackEffects = GetComponent<EveUnityFeedbackEffectRenderer>();
if (feedbackEffects == null) feedbackEffects = gameObject.AddComponent<EveUnityFeedbackEffectRenderer>();
feedbackEffects.Bind(this, Runtime.GameObjectAssetProvider);
var combatPresentation = GetComponent<EveUnityCombatPresentationRenderer>();
if (combatPresentation == null) combatPresentation = gameObject.AddComponent<EveUnityCombatPresentationRenderer>();
combatPresentation.Bind(this);
var aimPresentation = GetComponent<EveUnityAimPresentationRenderer>();
if (aimPresentation == null) aimPresentation = gameObject.AddComponent<EveUnityAimPresentationRenderer>();
aimPresentation.Bind(this);
var beamPresentation = GetComponent<EveUnityBeamPresentationRenderer>();
if (beamPresentation == null) beamPresentation = gameObject.AddComponent<EveUnityBeamPresentationRenderer>();
beamPresentation.Bind(this, Runtime.GameObjectAssetProvider);
var thermal = GetComponent<EveUnityThermalPresenter>();
var thermalHud = GetComponent<EveUnityThermalHudSink>();
if (thermalHud == null) thermalHud = gameObject.AddComponent<EveUnityThermalHudSink>();
if (thermal == null) thermal = gameObject.AddComponent<EveUnityThermalPresenter>();
thermal.Bind(this, Runtime.GameObjectAssetProvider as IEveUnityNativeAssetProvider);
var fieldVolume = GetComponent<EveUnityFieldsVolumeRenderer>();
if (fieldVolume == null) fieldVolume = gameObject.AddComponent<EveUnityFieldsVolumeRenderer>();
fieldVolume.Bind(this, providerSurfaceDocuments as IEveUnityFieldsSplatsDocumentSource);
var fieldParticles = GetComponent<EveUnityFieldsParticleRenderer>();
if (fieldParticles == null) fieldParticles = gameObject.AddComponent<EveUnityFieldsParticleRenderer>();
fieldParticles.Bind(this, providerSurfaceDocuments as IEveUnityFieldsSplatsDocumentSource);
var actionBar = GetComponent<EveUnityInputActionBar>();
if (actionBar == null) actionBar = gameObject.AddComponent<EveUnityInputActionBar>();
actionBar.Bind(this);
var uiOverlay = GetComponent<EveUnityUiToolkitOverlay>();
if (uiOverlay == null) uiOverlay = gameObject.AddComponent<EveUnityUiToolkitOverlay>();
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();
@ -128,6 +178,16 @@ 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)
@ -151,8 +211,63 @@ 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;
@ -160,6 +275,8 @@ namespace GameCult.Eve.UnityScene
}
Runtime?.Dispose();
Runtime = null;
_cameraOwner = null;
ActiveCameraTransform = null;
}
private void OnFeedbackAvailable(EveUnityFeedbackEvent value) => FeedbackAvailable?.Invoke(value);

View file

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GameCult.Eve.Surface;
using UnityEngine;
@ -6,6 +8,11 @@ using UnityEngine;
namespace GameCult.Eve.UnityScene
{
public interface IEveUnityInputCapabilitySource
{
EveInputCapabilityDocument CurrentInputCapability { get; }
}
public sealed class EveUnityPlayableWorldInputDriver : MonoBehaviour
{
[SerializeField] private EveUnityPlayableWorldClientHost? host;
@ -14,17 +21,35 @@ 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<string, HeldBindingState> _heldBindings =
new Dictionary<string, HeldBindingState>(StringComparer.Ordinal);
private readonly Dictionary<string, PerformedBindingState> _performedBindings =
new Dictionary<string, PerformedBindingState>(StringComparer.Ordinal);
public Func<string, bool?>? DigitalControlReader { get; set; }
public EveUnityPlayableWorldClientHost? Host
{
get => host;
set => host = value;
set
{
if (ReferenceEquals(host, value)) return;
ReleaseHeldActions();
host = value;
ResetLookState();
}
}
public Transform? CameraTransform
@ -71,27 +96,265 @@ namespace GameCult.Eve.UnityScene
issuedAt);
}
public EveSurfaceCommandRequest? SubmitPrimaryAction(string actionId = "primary")
public EveSurfaceCommandRequest? SubmitPrimaryAction(string actionId = "")
{
var resolvedHost = ResolveHost();
if (resolvedHost?.ActiveWorld == null)
return null;
return resolvedHost.SubmitActionIntent(
var resolvedActionId = string.IsNullOrWhiteSpace(actionId)
? ResolvePrimaryActionId(resolvedHost.InputCapability)
: actionId;
if (string.IsNullOrWhiteSpace(resolvedActionId))
return null;
return resolvedHost.SubmitAdvertisedActionIntent(
resolvedHost.ActiveWorld.PlayerEntityId,
string.IsNullOrWhiteSpace(actionId) ? "primary" : actionId);
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<EveInputProfileDocument>())
.FirstOrDefault(value => string.Equals(value.DeviceClass, "keyboard-mouse", StringComparison.Ordinal))
?? (capability.DefaultProfiles ?? Array.Empty<EveInputProfileDocument>()).FirstOrDefault();
var binding = (profile?.Bindings ?? Array.Empty<EveInputBindingDocument>())
.FirstOrDefault(value => (value.Gesture?.Controls ?? Array.Empty<string>())
.Any(control => string.Equals(control, "mouse.primary", StringComparison.Ordinal)))
?? (profile?.Bindings ?? Array.Empty<EveInputBindingDocument>()).FirstOrDefault(value => value.ActionBar);
if (binding == null)
return "";
var action = (capability.Actions ?? Array.Empty<EveInputActionDocument>())
.FirstOrDefault(value => string.Equals(value.ActionId, binding.ActionId, StringComparison.Ordinal));
return action != null && string.Equals(action.Availability, "available", StringComparison.OrdinalIgnoreCase)
? action.ActionId
: "";
}
private void Update()
{
if (!driveInUpdate || Time.unscaledTime < _nextCommandAt)
if (!driveInUpdate)
return;
if (!string.IsNullOrWhiteSpace(lookHorizontalAxis))
QueueLookInput(Input.GetAxisRaw(lookHorizontalAxis));
if (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(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<EveInputProfileDocument>())
.FirstOrDefault(value => string.Equals(value.DeviceClass, "keyboard-mouse", StringComparison.Ordinal));
var retained = new HashSet<string>(StringComparer.Ordinal);
foreach (var binding in profile?.Bindings ?? Array.Empty<EveInputBindingDocument>())
{
var action = (capability.Actions ?? Array.Empty<EveInputActionDocument>())
.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<EveInputProfileDocument>())
.FirstOrDefault(value => string.Equals(value.DeviceClass, "keyboard-mouse", StringComparison.Ordinal));
var retained = new HashSet<string>(StringComparer.Ordinal);
foreach (var binding in profile?.Bindings ?? Array.Empty<EveInputBindingDocument>())
{
var action = (capability.Actions ?? Array.Empty<EveInputActionDocument>())
.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<string>();
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) { }
}
private EveUnityPlayableWorldClientHost? ResolveHost()
@ -102,6 +365,61 @@ namespace GameCult.Eve.UnityScene
host = GetComponent<EveUnityPlayableWorldClientHost>();
return host;
}
private static float ResolveControlledYawRadians(EveUnityPlayableWorldClientHost host, string entityId)
{
foreach (var marker in host.SceneRoot.GetComponentsInChildren<EveUnityPlayableWorldEntityMarker>(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

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using GameCult.Eve.Surface;
#nullable enable
@ -15,6 +16,7 @@ namespace GameCult.Eve.UnityScene
private readonly EveUnityShotReceiptPresenter _shots = new EveUnityShotReceiptPresenter();
private readonly List<EveUnitySceneCommandReceipt> _pendingReceipts =
new List<EveUnitySceneCommandReceipt>();
private long _activeStateVersion;
private bool _connected;
private bool _receiptConnected;
@ -36,7 +38,14 @@ namespace GameCult.Eve.UnityScene
public EveUnitySceneCommandReceipt? LastReceipt { get; private set; }
public long ActiveVersion => _connection.ActiveVersion;
public long ActiveVersion => Math.Max(_connection.ActiveVersion, _activeStateVersion);
public void AdvanceStateVersion(long version)
{
if (version <= _activeStateVersion) return;
_activeStateVersion = version;
PublishReceiptsWhoseStateIsVisible();
}
public string SourcePointer => _connection.SourcePointer;
@ -88,6 +97,16 @@ 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)
@ -111,6 +130,14 @@ namespace GameCult.Eve.UnityScene
return _connection.SubmitActionIntent(entityId, actionId, issuedAt);
}
public EveSurfaceCommandRequest SubmitCommandIntent(
string commandId,
IReadOnlyDictionary<string, string>? payload = null,
DateTimeOffset? issuedAt = null)
{
return _connection.SubmitCommandIntent(commandId, payload, issuedAt);
}
public void Disconnect()
{
if (_connected)
@ -208,6 +235,43 @@ namespace GameCult.Eve.UnityScene
event Action<EveUnitySceneCommandReceipt> 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<string>? rendezvousEndpoints = null,
string authorityRuntimeId = "")
{
VerseId = verseId ?? "";
AuthorityRuntimeId = authorityRuntimeId ?? "";
ProviderId = providerId ?? "";
SurfaceId = surfaceId ?? "";
SurfaceKind = surfaceKind ?? "";
RendezvousEndpoints = rendezvousEndpoints ?? Array.Empty<string>();
}
public string VerseId { get; }
public string AuthorityRuntimeId { get; }
public string ProviderId { get; }
public string SurfaceId { get; }
public string SurfaceKind { get; }
public IReadOnlyList<string> RendezvousEndpoints { get; }
}
public sealed class EveUnitySceneCommandReceipt
{
public EveUnitySceneCommandReceipt(
@ -222,7 +286,8 @@ namespace GameCult.Eve.UnityScene
string surfaceId = "",
string message = "",
DateTimeOffset? issuedAtUtc = null,
long sourceVersion = 0)
long sourceVersion = 0,
EveUnitySceneNavigationTarget? navigation = null)
{
ReceiptId = receiptId ?? "";
Command = command ?? "";
@ -236,6 +301,7 @@ namespace GameCult.Eve.UnityScene
Message = message ?? "";
IssuedAtUtc = issuedAtUtc;
SourceVersion = sourceVersion;
Navigation = navigation;
}
public string Schema { get; }
@ -262,6 +328,8 @@ 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 =>

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using GameCult.Eve.Surface;
using UnityEngine;
@ -14,6 +15,8 @@ namespace GameCult.Eve.UnityScene
private readonly EveUnityPlayableWorldAssetManifestDocumentSource? _assetManifestSource;
private readonly IEveUnityEntitySoaViewDocumentSource? _entityViews;
private readonly EveUnityEntitySoaPresenter _entityPresenter;
private readonly IEveUnityPresentedEntityRegistry? _presentedEntities;
private IDisposable? _ownedSceneSink;
private bool _entityViewsConnected;
private bool _assetManifestConnected;
@ -33,6 +36,7 @@ namespace GameCult.Eve.UnityScene
AssetManifests = assetManifests ?? new EveUnityPlayableWorldAssetManifestCache();
_surfaceSource = new EveUnitySceneProviderSurfaceDocumentSource(surfaceDocuments);
_entityViews = surfaceDocuments as IEveUnityEntitySoaViewDocumentSource;
_presentedEntities = sceneSink as IEveUnityPresentedEntityRegistry;
_entityPresenter = new EveUnityEntitySoaPresenter(sceneSink);
_connection = new EveUnitySceneProviderConnection(_surfaceSource, commandSink);
_client = new EveUnityPlayableWorldLiveClient(
@ -47,6 +51,7 @@ namespace GameCult.Eve.UnityScene
public EveUnityPlayableWorldAssetManifestCache AssetManifests { get; }
public IEveUnityGameObjectAssetProvider? GameObjectAssetProvider { get; private set; }
public IEveUnityPresentedEntityRegistry? PresentedEntities => _presentedEntities;
public EveUnityPlayableWorldProjection? ActiveWorld => _client.ActiveWorld;
@ -103,6 +108,7 @@ namespace GameCult.Eve.UnityScene
new EveUnityAssetRefResolver(),
assetManifests);
runtime.GameObjectAssetProvider = liveAssetProvider;
runtime._ownedSceneSink = sceneSink;
return runtime;
}
@ -140,6 +146,16 @@ 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)
@ -163,6 +179,14 @@ namespace GameCult.Eve.UnityScene
return _client.SubmitActionIntent(entityId, actionId, issuedAt);
}
public EveSurfaceCommandRequest SubmitCommandIntent(
string commandId,
IReadOnlyDictionary<string, string>? payload = null,
DateTimeOffset? issuedAt = null)
{
return _client.SubmitCommandIntent(commandId, payload, issuedAt);
}
public void Disconnect()
{
if (_entityViewsConnected && _entityViews != null)
@ -185,6 +209,8 @@ namespace GameCult.Eve.UnityScene
Disconnect();
_client.Dispose();
_assetManifestSource?.Dispose();
_ownedSceneSink?.Dispose();
_ownedSceneSink = null;
}
private void EnsureAssetManifestConnected()
@ -202,14 +228,18 @@ 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) => _entityPresenter.Apply(document);
private void OnEntityViewAvailable(EveEntitySoaViewDocument document, GameCult.Mesh.ICultMeshBodyReadLease lease)
{
_client.AdvanceStateVersion(document.FrameId);
_entityPresenter.Apply(document, lease);
}
}
public sealed class EveUnityLivePlayableWorldAssetProvider : IEveUnityNativeAssetProvider
public sealed class EveUnityLivePlayableWorldAssetProvider :
IEveUnityNativeAssetProvider,
IEveUnityNativeAssetMetadataProvider
{
private readonly EveUnityPlayableWorldAssetManifestCache _assetManifests;
private readonly Func<EveUnityPlayableWorldProjection?> _activeWorld;
@ -256,5 +286,16 @@ namespace GameCult.Eve.UnityScene
}
return (_fallback as IEveUnityNativeAssetProvider)?.ResolveAsset(asset, assetType);
}
public bool TryResolveAssetMetadata(
EveUnityPlayableWorldAssetBinding asset,
out IReadOnlyDictionary<string, string> metadata)
{
if (asset == null) throw new ArgumentNullException(nameof(asset));
if (_fallback is IEveUnityNativeAssetMetadataProvider provider)
return provider.TryResolveAssetMetadata(asset, out metadata);
metadata = new Dictionary<string, string>(StringComparer.Ordinal);
return false;
}
}
}

View file

@ -0,0 +1,128 @@
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<string, float>? 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<string, float>(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<string, float> 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<EveUnityPresentedEntity> 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<EveUnityPresentedEntity> 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);
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3f7e67e0c24449bab80f2dd15cf2e118

View file

@ -0,0 +1,62 @@
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<IEveUnityRenderPassSource> Sources =
new List<IEveUnityRenderPassSource>();
[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);
}
}
/// <summary>
/// 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.
/// </summary>
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;
}
}

View file

@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 8e87e1be783846508efea45787f2d45c
guid: 340385338d304b5aaf069562d4e7b457
MonoImporter:
externalObjects: {}
serializedVersion: 2

View file

@ -83,6 +83,26 @@ 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<string, string>(StringComparer.Ordinal)
{
["entityId"] = entityId ?? "",
["directionX"] = FormatFloat(directionX),
["directionY"] = FormatFloat(directionY),
["directionZ"] = FormatFloat(directionZ)
},
issuedAt);
}
public EveSurfaceCommandRequest CreateFocusIntent(
string entityId,
DateTimeOffset? issuedAt = null)

View file

@ -239,8 +239,18 @@ namespace GameCult.Eve.UnityScene
_transport.SurfaceDocumentAvailable += OnSurfaceDocumentAvailable;
_transport.AssetManifestDocumentAvailable += OnAssetManifestDocumentAvailable;
_transport.CommandReceiptAvailable += OnCommandReceiptAvailable;
_transport.Connect();
_connected = true;
try
{
_transport.Connect();
_connected = true;
}
catch
{
_transport.SurfaceDocumentAvailable -= OnSurfaceDocumentAvailable;
_transport.AssetManifestDocumentAvailable -= OnAssetManifestDocumentAvailable;
_transport.CommandReceiptAvailable -= OnCommandReceiptAvailable;
throw;
}
}
public void Disconnect()

View file

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using GameCult.Eve.Surface;
#nullable enable
@ -221,6 +222,16 @@ 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)
@ -244,6 +255,14 @@ namespace GameCult.Eve.UnityScene
return Submit(_session.CreateActionIntent(entityId, actionId, issuedAt));
}
public EveSurfaceCommandRequest SubmitCommandIntent(
string commandId,
IReadOnlyDictionary<string, string>? payload = null,
DateTimeOffset? issuedAt = null)
{
return Submit(_session.CreatePlayableWorldIntent(commandId, payload, issuedAt));
}
public void Disconnect()
{
if (!_connected)

View file

@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GameCult.Eve.PluginFields;
using GameCult.Eve.Surface;
using GameCult.Mesh;
@ -102,10 +104,16 @@ namespace GameCult.Eve.UnityScene
return null;
var entities = new List<EveUnityPlayableWorldEntity>();
var fieldVolumes = new List<EveUnityFieldVolumeProjection>();
var fieldParticles = new List<EveUnityFieldParticlesProjection>();
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(
@ -123,10 +131,75 @@ namespace GameCult.Eve.UnityScene
entities,
worldRoot.GetProp("entityViewPointerId"),
worldRoot.GetProp("entityViewSchema"),
worldRoot.GetProp("entityBodyId"),
worldRoot.GetProp("zoneRenderPointerId"),
worldRoot.GetProp("zoneRenderSchema"));
worldRoot.GetProp("zoneRenderSchema"),
ParseStringList(worldRoot.GetProp("excludedRenderChannels")),
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")));
}
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"));
@ -198,6 +271,16 @@ namespace GameCult.Eve.UnityScene
string.Equals(value, "1", StringComparison.Ordinal);
}
private static IReadOnlyList<string> 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))
@ -323,8 +406,52 @@ namespace GameCult.Eve.UnityScene
IReadOnlyList<EveUnityPlayableWorldEntity> entities,
string entityViewPointerId = "",
string entityViewSchema = "",
string entityBodyId = "",
string zoneRenderPointerId = "",
string zoneRenderSchema = "")
string zoneRenderSchema = "",
IReadOnlyList<string>? excludedRenderChannels = null,
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<EveUnityFieldVolumeProjection>? fieldVolumes = null,
string cameraLookAt = "",
IReadOnlyList<EveUnityFieldParticlesProjection>? 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<string>? assetRendezvousEndpoints = null)
{
WorldRootId = worldRootId ?? "";
StatePointerId = statePointerId ?? "";
@ -340,8 +467,58 @@ namespace GameCult.Eve.UnityScene
Entities = entities ?? Array.Empty<EveUnityPlayableWorldEntity>();
EntityViewPointerId = entityViewPointerId ?? "";
EntityViewSchema = entityViewSchema ?? "";
EntityBodyId = entityBodyId ?? "";
ZoneRenderPointerId = zoneRenderPointerId ?? "";
ZoneRenderSchema = zoneRenderSchema ?? "";
ExcludedRenderChannels = excludedRenderChannels ?? Array.Empty<string>();
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<EveUnityFieldVolumeProjection>();
FieldParticles = fieldParticles ?? Array.Empty<EveUnityFieldParticlesProjection>();
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<string>();
}
public string WorldRootId { get; }
@ -352,31 +529,191 @@ namespace GameCult.Eve.UnityScene
public string EntityViewSchema { get; }
public string EntityBodyId { get; }
public string ZoneRenderPointerId { get; }
public string ZoneRenderSchema { get; }
public string AssetManifest { get; }
public string AssetProviderId { get; }
public string AssetVerseId { get; }
public string AssetAuthorityRuntimeId { get; }
public IReadOnlyList<string> 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 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<string> ExcludedRenderChannels { get; }
public int EntityCount => Entities.Count;
public IReadOnlyList<EveUnityPlayableWorldEntity> Entities { get; }
public IReadOnlyList<EveUnityFieldVolumeProjection> FieldVolumes { get; }
public IReadOnlyList<EveUnityFieldParticlesProjection> FieldParticles { get; }
}
public sealed class EveUnityFieldVolumeProjection
{
public EveUnityFieldVolumeProjection(
string nodeId,
string documentRef,
string documentSchema,
string materialAssetRef,
string renderChannel,
string compositeMode,
string quality,
IReadOnlyDictionary<string, string>? props = null)
{
NodeId = nodeId ?? "";
DocumentRef = documentRef ?? "";
DocumentSchema = documentSchema ?? "";
MaterialAssetRef = materialAssetRef ?? "";
RenderChannel = renderChannel ?? "";
CompositeMode = compositeMode ?? "";
Quality = quality ?? "";
Props = props ?? new Dictionary<string, string>(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<string, string> Props { get; }
}
public sealed class EveUnityFieldParticlesProjection
{
public EveUnityFieldParticlesProjection(
string nodeId,
string documentRef,
string documentSchema,
string computeProgramAssetRef,
string materialAssetRef,
string renderChannel,
IReadOnlyDictionary<string, string>? props = null)
{
NodeId = nodeId ?? "";
DocumentRef = documentRef ?? "";
DocumentSchema = documentSchema ?? "";
ComputeProgramAssetRef = computeProgramAssetRef ?? "";
MaterialAssetRef = materialAssetRef ?? "";
RenderChannel = renderChannel ?? "";
Props = props ?? new Dictionary<string, string>(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<string, string> Props { get; }
}
public sealed class EveUnityPlayableWorldEntity

View file

@ -38,6 +38,9 @@ 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; }
@ -54,6 +57,9 @@ 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 ?? "" : "";

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
#nullable enable
@ -20,6 +21,7 @@ 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)
{
@ -43,19 +45,10 @@ 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<LineRenderer>();
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);
@ -71,10 +64,23 @@ namespace GameCult.Eve.UnityScene
lightning.StartAnimation();
}
}
else
{
line = shot.AddComponent<LineRenderer>();
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);
line.SetPosition(1, travels ? origin : endpoint);
if (line != null)
line.SetPosition(1, travels ? origin : endpoint);
_active.Add(new ActiveTrajectory(
shot,
line,
@ -93,14 +99,14 @@ namespace GameCult.Eve.UnityScene
{
var trajectory = _active[index];
var progress = Mathf.Clamp01((now - trajectory.StartedAt) / trajectory.Duration);
if (trajectory.Travels)
if (trajectory.Travels && trajectory.Line != null)
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, transform);
var impact = Instantiate(trajectory.ImpactPrefab, trajectory.Root.transform);
impact.transform.position = trajectory.Endpoint;
}
}
@ -146,7 +152,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;
@ -159,7 +165,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; }

View file

@ -30,14 +30,7 @@ namespace GameCult.Eve.UnityScene
AttachToDocument();
return;
}
var panel = ScriptableObject.CreateInstance<PanelSettings>();
panel.name = "Eve thermal HUD panel";
panel.scaleMode = PanelScaleMode.ScaleWithScreenSize;
panel.referenceResolution = new Vector2Int(1920, 1080);
panel.match = 0.5f;
_document = gameObject.AddComponent<UIDocument>();
_document.panelSettings = panel;
_document.sortingOrder = 100;
_document = EveUnityUiDocumentRuntime.Ensure(gameObject);
_root = new VisualElement();
Build(_root);
AttachToDocument();

View file

@ -0,0 +1,39 @@
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<UIDocument>();
if (document != null) return document;
var panel = host.transform.parent?.GetComponentInParent<UIDocument>()?.panelSettings;
if (panel == null)
{
var theme = Resources.Load<ThemeStyleSheet>(ThemeResource);
if (theme == null)
throw new InvalidOperationException($"EveUnity runtime theme resource '{ThemeResource}' is missing.");
panel = ScriptableObject.CreateInstance<PanelSettings>();
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<UIDocument>();
document.panelSettings = panel;
document.sortingOrder = 100;
return document;
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 894b54214cb34993a7984f73cb45349f

Some files were not shown because too many files have changed in this diff Show more