Admit EveUnity transactional presentation authority
This commit is contained in:
commit
f0afa7681b
157 changed files with 19653 additions and 3003 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
15
ReleaseConsumerProject/Assets/DefaultVolumeProfile.asset
Normal file
15
ReleaseConsumerProject/Assets/DefaultVolumeProfile.asset
Normal 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: []
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5b8b49b0bf6036a4a823021e69f80309
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
3
ReleaseConsumerProject/Assets/Editor.meta
Normal file
3
ReleaseConsumerProject/Assets/Editor.meta
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 42bb4a36b1c94282ba7a4cad2b073bbc
|
||||
folderAsset: yes
|
||||
20
ReleaseConsumerProject/Assets/Editor/GenericEveUnityBuild.cs
Normal file
20
ReleaseConsumerProject/Assets/Editor/GenericEveUnityBuild.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8fcd5fd63f21489eb5a70306d22ed4a4
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c07b26ad876001846ba5ea9d3743352e
|
||||
100
ReleaseConsumerProject/Assets/GenericEveUnityLauncher.cs
Normal file
100
ReleaseConsumerProject/Assets/GenericEveUnityLauncher.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5c2c8060cff846cda741810156b0558a
|
||||
|
|
@ -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}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 43f32b840bd83ce47af920dadd59d4b3
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
69
ReleaseConsumerProject/Assets/GenericEveUnityRenderer.asset
Normal file
69
ReleaseConsumerProject/Assets/GenericEveUnityRenderer.asset
Normal 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
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: cbe5d2831ab649e42b9fb94644e029a9
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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
|
|
@ -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}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9374f1d73c9ee8940895d44ff5dd560e
|
||||
NativeFormatImporter:
|
||||
externalObjects: {}
|
||||
mainObjectFileID: 11400000
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
368
docs/aetheria-released-package-witness.md
Normal file
368
docs/aetheria-released-package-witness.md
Normal 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.
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<BaseOutputPath>$(MSBuildThisFileDirectory)bin~\</BaseOutputPath>
|
||||
<BaseIntermediateOutputPath>$(MSBuildThisFileDirectory)obj~\</BaseIntermediateOutputPath>
|
||||
<MSBuildProjectExtensionsPath>$(MSBuildThisFileDirectory)obj~\</MSBuildProjectExtensionsPath>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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.
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8f53654f7c98d894588127ba001f11b4
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8a2a1258551e42a9831a3e4f9483d928
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e3c2941be243a754092ac2543d1e6bf7
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f7ea0cd07c4d5da4099b5acf8d44ca2e
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 97ac7f757692aef4db64d75d0df6fe51
|
||||
|
|
@ -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 ?? "";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6516c42104b235f42b15efeb4639bb73
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 49f1738bd94075e489c82190cf660b1d
|
||||
PackageManifestImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("GameCult.Eve.UnityScene.Tests")]
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 68e2f1a625b94fc39dcc44d33e0cf318
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 61d76e98f2054d86bf8e7480d4411c39
|
||||
timeCreated: 1787039650
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7f7cb06716fd4f319ade147a69acb74a
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": [],
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3cb42e7da8b34febb348f01a598f2eea
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c7555b03e7f9451aa891e92958b845c5
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f859926d39664cefa5aa96ed3c2d8903
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 97c7b809756a4350a6e61c67bba69b82
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3086cd42c94f2624197d19704909f3f5
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f91c99c24bc0e20488fa03daef2eec06
|
||||
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e15acab8e9094b18b6dbd3abea0e055d
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 5c953d99a7454322b2935b878295b357
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bb62601554a846f39e46a6969c10eff7
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3f7e67e0c24449bab80f2dd15cf2e118
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8e87e1be783846508efea45787f2d45c
|
||||
guid: 340385338d304b5aaf069562d4e7b457
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ?? "" : "";
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 894b54214cb34993a7984f73cb45349f
|
||||
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue