Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d32ccf2347 | ||
|
|
59edc30088 | ||
|
|
e44fcb0efa | ||
|
|
65bab4d2cd | ||
|
|
7029a19976 | ||
|
|
3fee35512c | ||
|
|
65f1534b9d | ||
|
|
1a27b5e041 | ||
|
|
cf9a8652a3 | ||
|
|
838a5ac923 | ||
|
|
93551237fa | ||
|
|
66401bedaa | ||
|
|
e836884f16 | ||
|
|
5118387a44 | ||
|
|
10ba7e5b07 | ||
|
|
0dee524662 |
167 changed files with 4302 additions and 19732 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -9,6 +9,3 @@ TestProject/UserSettings/
|
||||||
obj/
|
obj/
|
||||||
*.csproj
|
*.csproj
|
||||||
*.sln
|
*.sln
|
||||||
packages/org.gamecult.eve.surface/bin~/
|
|
||||||
packages/org.gamecult.eve.surface/obj~/
|
|
||||||
packages/org.gamecult.eve.surface/Directory.Build.props.meta
|
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,7 @@ assembly is part of the client.
|
||||||
|
|
||||||
`EveUnityCultMeshPlayableWorldProvider` is the shipped composition root. Its
|
`EveUnityCultMeshPlayableWorldProvider` is the shipped composition root. Its
|
||||||
provider, surface, and Verse filters default to empty, so a client can select by
|
provider, surface, and Verse filters default to empty, so a client can select by
|
||||||
the generic `interactive-world` contract alone. After selection, Unity retains
|
the generic `interactive-world` contract alone.
|
||||||
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
|
||||||
powershell -ExecutionPolicy Bypass -File .\scripts\run-package-tests.ps1
|
powershell -ExecutionPolicy Bypass -File .\scripts\run-package-tests.ps1
|
||||||
|
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
%YAML 1.1
|
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
|
||||||
--- !u!114 &11400000
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: d7fd9488000d3734a9e00ee676215985, type: 3}
|
|
||||||
m_Name: DefaultVolumeProfile
|
|
||||||
m_EditorClassIdentifier: Unity.RenderPipelines.Core.Runtime::UnityEngine.Rendering.VolumeProfile
|
|
||||||
components: []
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 5b8b49b0bf6036a4a823021e69f80309
|
|
||||||
NativeFormatImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
mainObjectFileID: 11400000
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 42bb4a36b1c94282ba7a4cad2b073bbc
|
|
||||||
folderAsset: yes
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
using System.IO;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEditor.SceneManagement;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.SceneManagement;
|
|
||||||
|
|
||||||
public static class GenericEveUnityBuild
|
|
||||||
{
|
|
||||||
public static void BuildWindows()
|
|
||||||
{
|
|
||||||
const string scenePath = "Assets/GenericEveUnity.unity";
|
|
||||||
var scene = EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
|
||||||
new GameObject("Generic EveUnity Client").AddComponent<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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 8fcd5fd63f21489eb5a70306d22ed4a4
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
using System.Linq;
|
|
||||||
using GameCult.Eve.UnityScene;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.Rendering;
|
|
||||||
using UnityEngine.Rendering.Universal;
|
|
||||||
|
|
||||||
public static class GenericEveUnityRenderPipeline
|
|
||||||
{
|
|
||||||
private const string PostProcessDataGuid = "41439944d30ece34e96484bdb6645b55";
|
|
||||||
private const string RendererPath = "Assets/GenericEveUnityRenderer.asset";
|
|
||||||
private const string PipelinePath = "Assets/GenericEveUnityRenderPipeline.asset";
|
|
||||||
|
|
||||||
public static void Configure()
|
|
||||||
{
|
|
||||||
var renderer = AssetDatabase.LoadAssetAtPath<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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: c07b26ad876001846ba5ea9d3743352e
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections;
|
|
||||||
using System.IO;
|
|
||||||
using GameCult.Eve.UnityUIToolkit;
|
|
||||||
using GameCult.Eve.UnityScene;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.UIElements;
|
|
||||||
|
|
||||||
public sealed class GenericEveUnityLauncher : MonoBehaviour
|
|
||||||
{
|
|
||||||
private EveUnityCultMeshPlayableWorldProvider _provider;
|
|
||||||
private EveUnityPlayableWorldClientBootstrap _bootstrap;
|
|
||||||
private EveUiToolkitSurfaceLowerer _surfaceLowerer;
|
|
||||||
private UIDocument _surfaceDocument;
|
|
||||||
private PanelSettings _panelSettings;
|
|
||||||
|
|
||||||
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
|
||||||
private static void StartClient()
|
|
||||||
{
|
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("EVEUNITY_DISABLE_AUTO_LAUNCHER"), "1", StringComparison.Ordinal))
|
|
||||||
return;
|
|
||||||
if (FindObjectOfType<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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 5c2c8060cff846cda741810156b0558a
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
%YAML 1.1
|
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
|
||||||
--- !u!114 &11400000
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: bf2edee5c58d82540a51f03df9d42094, type: 3}
|
|
||||||
m_Name: GenericEveUnityRenderPipeline
|
|
||||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalRenderPipelineAsset
|
|
||||||
k_AssetVersion: 13
|
|
||||||
k_AssetPreviousVersion: 13
|
|
||||||
m_RendererType: 1
|
|
||||||
m_RendererData: {fileID: 0}
|
|
||||||
m_RendererDataList:
|
|
||||||
- {fileID: 11400000, guid: cbe5d2831ab649e42b9fb94644e029a9, type: 2}
|
|
||||||
m_DefaultRendererIndex: 0
|
|
||||||
m_RequireDepthTexture: 0
|
|
||||||
m_RequireOpaqueTexture: 0
|
|
||||||
m_OpaqueDownsampling: 1
|
|
||||||
m_SupportsTerrainHoles: 1
|
|
||||||
m_SupportsHDR: 1
|
|
||||||
m_HDRColorBufferPrecision: 0
|
|
||||||
m_MSAA: 1
|
|
||||||
m_RenderScale: 1
|
|
||||||
m_UpscalingFilter: 0
|
|
||||||
m_FsrOverrideSharpness: 0
|
|
||||||
m_FsrSharpness: 0.92
|
|
||||||
m_EnableLODCrossFade: 1
|
|
||||||
m_LODCrossFadeDitheringType: 1
|
|
||||||
m_ShEvalMode: 0
|
|
||||||
m_LightProbeSystem: 0
|
|
||||||
m_ProbeVolumeMemoryBudget: 1024
|
|
||||||
m_ProbeVolumeBlendingMemoryBudget: 256
|
|
||||||
m_SupportProbeVolumeGPUStreaming: 0
|
|
||||||
m_SupportProbeVolumeDiskStreaming: 0
|
|
||||||
m_SupportProbeVolumeScenarios: 0
|
|
||||||
m_SupportProbeVolumeScenarioBlending: 0
|
|
||||||
m_ProbeVolumeSHBands: 1
|
|
||||||
m_MainLightRenderingMode: 1
|
|
||||||
m_MainLightShadowsSupported: 1
|
|
||||||
m_MainLightShadowmapResolution: 2048
|
|
||||||
m_AdditionalLightsRenderingMode: 1
|
|
||||||
m_AdditionalLightsPerObjectLimit: 4
|
|
||||||
m_AdditionalLightShadowsSupported: 0
|
|
||||||
m_AdditionalLightsShadowmapResolution: 2048
|
|
||||||
m_AdditionalLightsShadowResolutionTierLow: 256
|
|
||||||
m_AdditionalLightsShadowResolutionTierMedium: 512
|
|
||||||
m_AdditionalLightsShadowResolutionTierHigh: 1024
|
|
||||||
m_ReflectionProbeBlending: 0
|
|
||||||
m_ReflectionProbeBoxProjection: 0
|
|
||||||
m_ReflectionProbeAtlas: 1
|
|
||||||
m_ShadowDistance: 50
|
|
||||||
m_ShadowCascadeCount: 1
|
|
||||||
m_Cascade2Split: 0.25
|
|
||||||
m_Cascade3Split: {x: 0.1, y: 0.3}
|
|
||||||
m_Cascade4Split: {x: 0.067, y: 0.2, z: 0.467}
|
|
||||||
m_CascadeBorder: 0.2
|
|
||||||
m_ShadowDepthBias: 1
|
|
||||||
m_ShadowNormalBias: 1
|
|
||||||
m_AnyShadowsSupported: 1
|
|
||||||
m_SoftShadowsSupported: 0
|
|
||||||
m_ConservativeEnclosingSphere: 1
|
|
||||||
m_NumIterationsEnclosingSphere: 64
|
|
||||||
m_SoftShadowQuality: 2
|
|
||||||
m_AdditionalLightsCookieResolution: 2048
|
|
||||||
m_AdditionalLightsCookieFormat: 3
|
|
||||||
m_UseSRPBatcher: 1
|
|
||||||
m_SupportsDynamicBatching: 0
|
|
||||||
m_MixedLightingSupported: 1
|
|
||||||
m_SupportsLightCookies: 1
|
|
||||||
m_SupportsLightLayers: 0
|
|
||||||
m_DebugLevel: 0
|
|
||||||
m_StoreActionsOptimization: 0
|
|
||||||
m_UseAdaptivePerformance: 1
|
|
||||||
m_ColorGradingMode: 0
|
|
||||||
m_ColorGradingLutSize: 32
|
|
||||||
m_AllowPostProcessAlphaOutput: 0
|
|
||||||
m_UseFastSRGBLinearConversion: 0
|
|
||||||
m_SupportDataDrivenLensFlare: 1
|
|
||||||
m_SupportScreenSpaceLensFlare: 1
|
|
||||||
m_GPUResidentDrawerMode: 0
|
|
||||||
m_SmallMeshScreenPercentage: 0
|
|
||||||
m_GPUResidentDrawerEnableOcclusionCullingInCameras: 0
|
|
||||||
m_ShadowType: 1
|
|
||||||
m_LocalShadowsSupported: 0
|
|
||||||
m_LocalShadowsAtlasResolution: 256
|
|
||||||
m_MaxPixelLights: 0
|
|
||||||
m_ShadowAtlasResolution: 256
|
|
||||||
m_VolumeFrameworkUpdateMode: 0
|
|
||||||
m_VolumeProfile: {fileID: 0}
|
|
||||||
apvScenesData:
|
|
||||||
obsoleteSceneBounds:
|
|
||||||
m_Keys: []
|
|
||||||
m_Values: []
|
|
||||||
obsoleteHasProbeVolumes:
|
|
||||||
m_Keys: []
|
|
||||||
m_Values:
|
|
||||||
m_PrefilteringModeMainLightShadows: 1
|
|
||||||
m_PrefilteringModeAdditionalLight: 4
|
|
||||||
m_PrefilteringModeAdditionalLightShadows: 1
|
|
||||||
m_PrefilterXRKeywords: 0
|
|
||||||
m_PrefilteringModeForwardPlus: 1
|
|
||||||
m_PrefilteringModeDeferredRendering: 1
|
|
||||||
m_PrefilteringModeScreenSpaceOcclusion: 1
|
|
||||||
m_PrefilterDebugKeywords: 0
|
|
||||||
m_PrefilterWriteRenderingLayers: 0
|
|
||||||
m_PrefilterHDROutput: 0
|
|
||||||
m_PrefilterAlphaOutput: 0
|
|
||||||
m_PrefilterSSAODepthNormals: 0
|
|
||||||
m_PrefilterSSAOSourceDepthLow: 0
|
|
||||||
m_PrefilterSSAOSourceDepthMedium: 0
|
|
||||||
m_PrefilterSSAOSourceDepthHigh: 0
|
|
||||||
m_PrefilterSSAOInterleaved: 0
|
|
||||||
m_PrefilterSSAOBlueNoise: 0
|
|
||||||
m_PrefilterSSAOSampleCountLow: 0
|
|
||||||
m_PrefilterSSAOSampleCountMedium: 0
|
|
||||||
m_PrefilterSSAOSampleCountHigh: 0
|
|
||||||
m_PrefilterDBufferMRT1: 0
|
|
||||||
m_PrefilterDBufferMRT2: 0
|
|
||||||
m_PrefilterDBufferMRT3: 0
|
|
||||||
m_PrefilterSoftShadowsQualityLow: 0
|
|
||||||
m_PrefilterSoftShadowsQualityMedium: 0
|
|
||||||
m_PrefilterSoftShadowsQualityHigh: 0
|
|
||||||
m_PrefilterSoftShadows: 0
|
|
||||||
m_PrefilterScreenCoord: 0
|
|
||||||
m_PrefilterScreenSpaceIrradiance: 0
|
|
||||||
m_PrefilterNativeRenderPass: 0
|
|
||||||
m_PrefilterUseLegacyLightmaps: 0
|
|
||||||
m_PrefilterBicubicLightmapSampling: 0
|
|
||||||
m_PrefilterReflectionProbeRotation: 0
|
|
||||||
m_PrefilterReflectionProbeBlending: 0
|
|
||||||
m_PrefilterReflectionProbeBoxProjection: 0
|
|
||||||
m_PrefilterReflectionProbeAtlas: 0
|
|
||||||
m_ShaderVariantLogLevel: 0
|
|
||||||
m_ShadowCascades: 0
|
|
||||||
m_Textures:
|
|
||||||
blueNoise64LTex: {fileID: 2800000, guid: e3d24661c1e055f45a7560c033dbb837, type: 3}
|
|
||||||
bayerMatrixTex: {fileID: 2800000, guid: f9ee4ed84c1d10c49aabb9b210b0fc44, type: 3}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 43f32b840bd83ce47af920dadd59d4b3
|
|
||||||
NativeFormatImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
mainObjectFileID: 11400000
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
%YAML 1.1
|
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
|
||||||
--- !u!114 &11400000
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: de640fe3d0db1804a85f9fc8f5cadab6, type: 3}
|
|
||||||
m_Name: GenericEveUnityRenderer
|
|
||||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalRendererData
|
|
||||||
debugShaders:
|
|
||||||
debugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7, type: 3}
|
|
||||||
hdrDebugViewPS: {fileID: 4800000, guid: 573620ae32aec764abd4d728906d2587, type: 3}
|
|
||||||
probeVolumeSamplingDebugComputeShader: {fileID: 7200000, guid: 53626a513ea68ce47b59dc1299fe3959, type: 3}
|
|
||||||
probeVolumeResources:
|
|
||||||
probeVolumeDebugShader: {fileID: 0}
|
|
||||||
probeVolumeFragmentationDebugShader: {fileID: 0}
|
|
||||||
probeVolumeOffsetDebugShader: {fileID: 0}
|
|
||||||
probeVolumeSamplingDebugShader: {fileID: 0}
|
|
||||||
probeSamplingDebugMesh: {fileID: 0}
|
|
||||||
probeSamplingDebugTexture: {fileID: 0}
|
|
||||||
probeVolumeBlendStatesCS: {fileID: 0}
|
|
||||||
m_RendererFeatures:
|
|
||||||
- {fileID: 7891961340342901043}
|
|
||||||
m_RendererFeatureMap:
|
|
||||||
m_UseNativeRenderPass: 0
|
|
||||||
postProcessData: {fileID: 11400000, guid: 41439944d30ece34e96484bdb6645b55, type: 2}
|
|
||||||
m_AssetVersion: 3
|
|
||||||
m_PrepassLayerMask:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_Bits: 4294967295
|
|
||||||
m_OpaqueLayerMask:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_Bits: 4294967295
|
|
||||||
m_TransparentLayerMask:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_Bits: 4294967295
|
|
||||||
m_DefaultStencilState:
|
|
||||||
overrideStencilState: 0
|
|
||||||
stencilReference: 0
|
|
||||||
stencilCompareFunction: 8
|
|
||||||
passOperation: 2
|
|
||||||
failOperation: 0
|
|
||||||
zFailOperation: 0
|
|
||||||
m_ShadowTransparentReceive: 1
|
|
||||||
m_RenderingMode: 0
|
|
||||||
m_DepthPrimingMode: 0
|
|
||||||
m_CopyDepthMode: 1
|
|
||||||
m_DepthAttachmentFormat: 0
|
|
||||||
m_DepthTextureFormat: 0
|
|
||||||
m_AccurateGbufferNormals: 0
|
|
||||||
m_IntermediateTextureMode: 1
|
|
||||||
--- !u!114 &7891961340342901043
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 340385338d304b5aaf069562d4e7b457, type: 3}
|
|
||||||
m_Name: Eve Unity World Effects
|
|
||||||
m_EditorClassIdentifier: GameCult.Eve.UnityScene::GameCult.Eve.UnityScene.EveUnityRendererFeature
|
|
||||||
m_Active: 1
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: cbe5d2831ab649e42b9fb94644e029a9
|
|
||||||
NativeFormatImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
mainObjectFileID: 11400000
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -4,10 +4,8 @@
|
||||||
"references": [
|
"references": [
|
||||||
"GameCult.CultLib",
|
"GameCult.CultLib",
|
||||||
"GameCult.Eve.Surface",
|
"GameCult.Eve.Surface",
|
||||||
"GameCult.Eve.UnityUIToolkit",
|
|
||||||
"GameCult.Eve.UnityScene",
|
"GameCult.Eve.UnityScene",
|
||||||
"GameCult.Eve.UnityScene.CultMesh",
|
"GameCult.Eve.UnityScene.CultMesh"
|
||||||
"Unity.RenderPipelines.Universal.Runtime"
|
|
||||||
],
|
],
|
||||||
"includePlatforms": [],
|
"includePlatforms": [],
|
||||||
"excludePlatforms": [],
|
"excludePlatforms": [],
|
||||||
|
|
@ -16,7 +14,6 @@
|
||||||
"GameCult.Caching.dll",
|
"GameCult.Caching.dll",
|
||||||
"GameCult.Caching.MessagePack.dll",
|
"GameCult.Caching.MessagePack.dll",
|
||||||
"GameCult.Mesh.dll",
|
"GameCult.Mesh.dll",
|
||||||
"GameCult.Mesh.Quic.Native.dll",
|
|
||||||
"GameCult.Networking.dll"
|
"GameCult.Networking.dll"
|
||||||
],
|
],
|
||||||
"autoReferenced": false,
|
"autoReferenced": false,
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,436 +0,0 @@
|
||||||
%YAML 1.1
|
|
||||||
%TAG !u! tag:unity3d.com,2011:
|
|
||||||
--- !u!114 &11400000
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 0}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 2ec995e51a6e251468d2a3fd8a686257, type: 3}
|
|
||||||
m_Name: UniversalRenderPipelineGlobalSettings
|
|
||||||
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Runtime::UnityEngine.Rendering.Universal.UniversalRenderPipelineGlobalSettings
|
|
||||||
m_ShaderStrippingSetting:
|
|
||||||
m_Version: 0
|
|
||||||
m_ExportShaderVariants: 1
|
|
||||||
m_ShaderVariantLogLevel: 0
|
|
||||||
m_StripRuntimeDebugShaders: 1
|
|
||||||
m_URPShaderStrippingSetting:
|
|
||||||
m_Version: 0
|
|
||||||
m_StripUnusedPostProcessingVariants: 0
|
|
||||||
m_StripUnusedVariants: 1
|
|
||||||
m_StripScreenCoordOverrideVariants: 1
|
|
||||||
m_ShaderVariantLogLevel: 0
|
|
||||||
m_ExportShaderVariants: 1
|
|
||||||
m_StripDebugVariants: 1
|
|
||||||
m_StripUnusedPostProcessingVariants: 0
|
|
||||||
m_StripUnusedVariants: 1
|
|
||||||
m_StripScreenCoordOverrideVariants: 1
|
|
||||||
supportRuntimeDebugDisplay: 0
|
|
||||||
m_Settings:
|
|
||||||
m_SettingsList:
|
|
||||||
m_List:
|
|
||||||
- rid: 1680211313492492288
|
|
||||||
- rid: 1680211313492492289
|
|
||||||
- rid: 1680211313492492290
|
|
||||||
- rid: 1680211313492492291
|
|
||||||
- rid: 1680211313492492292
|
|
||||||
- rid: 1680211313492492293
|
|
||||||
- rid: 1680211313492492294
|
|
||||||
- rid: 1680211313492492295
|
|
||||||
- rid: 1680211313492492296
|
|
||||||
- rid: 1680211313492492297
|
|
||||||
- rid: 1680211313492492298
|
|
||||||
- rid: 1680211313492492299
|
|
||||||
- rid: 1680211313492492300
|
|
||||||
- rid: 1680211313492492301
|
|
||||||
- rid: 1680211313492492302
|
|
||||||
- rid: 1680211313492492303
|
|
||||||
- rid: 1680211313492492304
|
|
||||||
- rid: 1680211313492492305
|
|
||||||
- rid: 1680211313492492306
|
|
||||||
- rid: 1680211313492492307
|
|
||||||
- rid: 1680211313492492308
|
|
||||||
- rid: 1680211313492492309
|
|
||||||
- rid: 1680211313492492310
|
|
||||||
- rid: 1680211313492492311
|
|
||||||
- rid: 1680211313492492312
|
|
||||||
- rid: 1680211313492492313
|
|
||||||
- rid: 1680211313492492314
|
|
||||||
- rid: 1680211313492492315
|
|
||||||
- rid: 1680211313492492316
|
|
||||||
- rid: 1680211313492492317
|
|
||||||
- rid: 1680211313492492318
|
|
||||||
- rid: 1680211313492492319
|
|
||||||
- rid: 1680211313492492320
|
|
||||||
- rid: 1680211313492492321
|
|
||||||
- rid: 1680211313492492322
|
|
||||||
m_RuntimeSettings:
|
|
||||||
m_List: []
|
|
||||||
m_AssetVersion: 10
|
|
||||||
m_ObsoleteDefaultVolumeProfile: {fileID: 0}
|
|
||||||
m_RenderingLayerNames:
|
|
||||||
- Default
|
|
||||||
m_ValidRenderingLayers: 0
|
|
||||||
lightLayerName0:
|
|
||||||
lightLayerName1:
|
|
||||||
lightLayerName2:
|
|
||||||
lightLayerName3:
|
|
||||||
lightLayerName4:
|
|
||||||
lightLayerName5:
|
|
||||||
lightLayerName6:
|
|
||||||
lightLayerName7:
|
|
||||||
apvScenesData:
|
|
||||||
obsoleteSceneBounds:
|
|
||||||
m_Keys: []
|
|
||||||
m_Values: []
|
|
||||||
obsoleteHasProbeVolumes:
|
|
||||||
m_Keys: []
|
|
||||||
m_Values:
|
|
||||||
references:
|
|
||||||
version: 2
|
|
||||||
RefIds:
|
|
||||||
- rid: 1680211313492492288
|
|
||||||
type: {class: RayTracingRenderPipelineResources, ns: UnityEngine.Rendering.UnifiedRayTracing, asm: Unity.UnifiedRayTracing.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 1
|
|
||||||
m_GeometryPoolKernels: {fileID: 7200000, guid: 98e3d58cae7210c4786f67f504c9e899, type: 3}
|
|
||||||
m_CopyBuffer: {fileID: 7200000, guid: 1b95b5dcf48d1914c9e1e7405c7660e3, type: 3}
|
|
||||||
m_CopyPositions: {fileID: 7200000, guid: 1ad53a96b58d3c3488dde4f14db1aaeb, type: 3}
|
|
||||||
m_BitHistogram: {fileID: 7200000, guid: 8670f7ce4b60cef43bed36148aa1b0a2, type: 3}
|
|
||||||
m_BlockReducePart: {fileID: 7200000, guid: 4e034cc8ea2635c4e9f063e5ddc7ea7a, type: 3}
|
|
||||||
m_BlockScan: {fileID: 7200000, guid: 4d6d5de35fa45ef4a92119397a045cc9, type: 3}
|
|
||||||
m_BuildHlbvh: {fileID: 7200000, guid: 2d70cd6be91bd7843a39a54b51c15b13, type: 3}
|
|
||||||
m_RestructureBvh: {fileID: 7200000, guid: 56641cb88dcb31a4398a4997ef7a7a8c, type: 3}
|
|
||||||
m_Scatter: {fileID: 7200000, guid: a2eaeefdac4637a44b734e85b7be9186, type: 3}
|
|
||||||
- rid: 1680211313492492289
|
|
||||||
type: {class: UniversalRenderPipelineRuntimeShaders, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 0
|
|
||||||
m_FallbackErrorShader: {fileID: 4800000, guid: e6e9a19c3678ded42a3bc431ebef7dbd, type: 3}
|
|
||||||
m_BlitHDROverlay: {fileID: 4800000, guid: a89bee29cffa951418fc1e2da94d1959, type: 3}
|
|
||||||
m_CoreBlitPS: {fileID: 4800000, guid: 93446b5c5339d4f00b85c159e1159b7c, type: 3}
|
|
||||||
m_CoreBlitColorAndDepthPS: {fileID: 4800000, guid: d104b2fc1ca6445babb8e90b0758136b, type: 3}
|
|
||||||
m_SamplingPS: {fileID: 4800000, guid: 04c410c9937594faa893a11dceb85f7e, type: 3}
|
|
||||||
m_TerrainDetailLit: {fileID: 0}
|
|
||||||
m_TerrainDetailGrassBillboard: {fileID: 0}
|
|
||||||
m_TerrainDetailGrass: {fileID: 0}
|
|
||||||
- rid: 1680211313492492290
|
|
||||||
type: {class: URPTerrainShaderSetting, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 0
|
|
||||||
m_IncludeTerrainShaders: 1
|
|
||||||
- rid: 1680211313492492291
|
|
||||||
type: {class: UniversalRenderPipelineRuntimeTerrainShaders, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 0
|
|
||||||
m_TerrainDetailLit: {fileID: 4800000, guid: f6783ab646d374f94b199774402a5144, type: 3}
|
|
||||||
m_TerrainDetailGrassBillboard: {fileID: 4800000, guid: 29868e73b638e48ca99a19ea58c48d90, type: 3}
|
|
||||||
m_TerrainDetailGrass: {fileID: 4800000, guid: e507fdfead5ca47e8b9a768b51c291a1, type: 3}
|
|
||||||
- rid: 1680211313492492292
|
|
||||||
type: {class: UniversalRendererResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 0
|
|
||||||
m_CopyDepthPS: {fileID: 4800000, guid: d6dae50ee9e1bfa4db75f19f99355220, type: 3}
|
|
||||||
m_CameraMotionVector: {fileID: 4800000, guid: c56b7e0d4c7cb484e959caeeedae9bbf, type: 3}
|
|
||||||
m_StencilDeferredPS: {fileID: 4800000, guid: e9155b26e1bc55942a41e518703fe304, type: 3}
|
|
||||||
m_ClusterDeferred: {fileID: 4800000, guid: 222cce62363a44a380c36bf03b392608, type: 3}
|
|
||||||
m_StencilDitherMaskSeedPS: {fileID: 4800000, guid: 8c3ee818f2efa514c889881ccb2e95a2, type: 3}
|
|
||||||
m_DBufferClear: {fileID: 4800000, guid: f056d8bd2a1c7e44e9729144b4c70395, type: 3}
|
|
||||||
- rid: 1680211313492492293
|
|
||||||
type: {class: UniversalRenderPipelineDebugShaders, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
m_DebugReplacementPS: {fileID: 4800000, guid: cf852408f2e174538bcd9b7fda1c5ae7, type: 3}
|
|
||||||
m_HdrDebugViewPS: {fileID: 4800000, guid: 573620ae32aec764abd4d728906d2587, type: 3}
|
|
||||||
m_ProbeVolumeSamplingDebugComputeShader: {fileID: 7200000, guid: 53626a513ea68ce47b59dc1299fe3959, type: 3}
|
|
||||||
- rid: 1680211313492492294
|
|
||||||
type: {class: PostProcessData/ShaderResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
stopNanPS: {fileID: 4800000, guid: 1121bb4e615ca3c48b214e79e841e823, type: 3}
|
|
||||||
subpixelMorphologicalAntialiasingPS: {fileID: 4800000, guid: 63eaba0ebfb82cc43bde059b4a8c65f6, type: 3}
|
|
||||||
gaussianDepthOfFieldPS: {fileID: 4800000, guid: 5e7134d6e63e0bc47a1dd2669cedb379, type: 3}
|
|
||||||
bokehDepthOfFieldPS: {fileID: 4800000, guid: 2aed67ad60045d54ba3a00c91e2d2631, type: 3}
|
|
||||||
cameraMotionBlurPS: {fileID: 4800000, guid: 1edcd131364091c46a17cbff0b1de97a, type: 3}
|
|
||||||
paniniProjectionPS: {fileID: 4800000, guid: a15b78cf8ca26ca4fb2090293153c62c, type: 3}
|
|
||||||
lutBuilderLdrPS: {fileID: 4800000, guid: 65df88701913c224d95fc554db28381a, type: 3}
|
|
||||||
lutBuilderHdrPS: {fileID: 4800000, guid: ec9fec698a3456d4fb18cf8bacb7a2bc, type: 3}
|
|
||||||
bloomPS: {fileID: 4800000, guid: 5f1864addb451f54bae8c86d230f736e, type: 3}
|
|
||||||
temporalAntialiasingPS: {fileID: 4800000, guid: 9c70c1a35ff15f340b38ea84842358bf, type: 3}
|
|
||||||
LensFlareDataDrivenPS: {fileID: 4800000, guid: 6cda457ac28612740adb23da5d39ea92, type: 3}
|
|
||||||
LensFlareScreenSpacePS: {fileID: 4800000, guid: 701880fecb344ea4c9cd0db3407ab287, type: 3}
|
|
||||||
scalingSetupPS: {fileID: 4800000, guid: e8ee25143a34b8c4388709ea947055d1, type: 3}
|
|
||||||
easuPS: {fileID: 4800000, guid: 562b7ae4f629f144aa97780546fce7c6, type: 3}
|
|
||||||
uberPostPS: {fileID: 4800000, guid: e7857e9d0c934dc4f83f270f8447b006, type: 3}
|
|
||||||
finalPostPassPS: {fileID: 4800000, guid: c49e63ed1bbcb334780a3bd19dfed403, type: 3}
|
|
||||||
m_ShaderResourcesVersion: 0
|
|
||||||
- rid: 1680211313492492295
|
|
||||||
type: {class: UniversalRenderPipelineEditorShaders, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
m_AutodeskInteractive: {fileID: 4800000, guid: 0e9d5a909a1f7e84882a534d0d11e49f, type: 3}
|
|
||||||
m_AutodeskInteractiveTransparent: {fileID: 4800000, guid: 5c81372d981403744adbdda4433c9c11, type: 3}
|
|
||||||
m_AutodeskInteractiveMasked: {fileID: 4800000, guid: 80aa867ac363ac043847b06ad71604cd, type: 3}
|
|
||||||
m_DefaultSpeedTree7Shader: {fileID: 4800000, guid: 0f4122b9a743b744abe2fb6a0a88868b, type: 3}
|
|
||||||
m_DefaultSpeedTree8Shader: {fileID: -6465566751694194690, guid: 9920c1f1781549a46ba081a2a15a16ec, type: 3}
|
|
||||||
m_DefaultSpeedTree9Shader: {fileID: -6465566751694194690, guid: cbd3e1cc4ae141c42a30e33b4d666a61, type: 3}
|
|
||||||
- rid: 1680211313492492296
|
|
||||||
type: {class: RenderGraphSettings, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 0
|
|
||||||
- rid: 1680211313492492297
|
|
||||||
type: {class: ScreenSpaceAmbientOcclusionDynamicResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
m_BlueNoise256Textures:
|
|
||||||
- {fileID: 2800000, guid: 36f118343fc974119bee3d09e2111500, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 4b7b083e6b6734e8bb2838b0b50a0bc8, type: 3}
|
|
||||||
- {fileID: 2800000, guid: c06cc21c692f94f5fb5206247191eeee, type: 3}
|
|
||||||
- {fileID: 2800000, guid: cb76dd40fa7654f9587f6a344f125c9a, type: 3}
|
|
||||||
- {fileID: 2800000, guid: e32226222ff144b24bf3a5a451de54bc, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 3302065f671a8450b82c9ddf07426f3a, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 56a77a3e8d64f47b6afe9e3c95cb57d5, type: 3}
|
|
||||||
m_Version: 0
|
|
||||||
- rid: 1680211313492492298
|
|
||||||
type: {class: OnTilePostProcessResource, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 0
|
|
||||||
m_UberPostShader: {fileID: 4800000, guid: fe4f13c1004a07d4ea1e30bfd0326d9e, type: 3}
|
|
||||||
- rid: 1680211313492492299
|
|
||||||
type: {class: UniversalRenderPipelineRuntimeXRResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
m_xrOcclusionMeshPS: {fileID: 4800000, guid: 4431b1f1f743fbf4eb310a967890cbea, type: 3}
|
|
||||||
m_xrMirrorViewPS: {fileID: 4800000, guid: d5a307c014552314b9f560906d708772, type: 3}
|
|
||||||
m_xrMotionVector: {fileID: 4800000, guid: f89aac1e4f84468418fe30e611dff395, type: 3}
|
|
||||||
- rid: 1680211313492492300
|
|
||||||
type: {class: URPReflectionProbeSettings, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
version: 1
|
|
||||||
useReflectionProbeRotation: 1
|
|
||||||
- rid: 1680211313492492301
|
|
||||||
type: {class: UniversalRenderPipelineRuntimeTextures, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 1
|
|
||||||
m_BlueNoise64LTex: {fileID: 2800000, guid: e3d24661c1e055f45a7560c033dbb837, type: 3}
|
|
||||||
m_BayerMatrixTex: {fileID: 2800000, guid: f9ee4ed84c1d10c49aabb9b210b0fc44, type: 3}
|
|
||||||
m_DebugFontTex: {fileID: 2800000, guid: 26a413214480ef144b2915d6ff4d0beb, type: 3}
|
|
||||||
- rid: 1680211313492492302
|
|
||||||
type: {class: ScreenSpaceAmbientOcclusionPersistentResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
m_Shader: {fileID: 4800000, guid: 0849e84e3d62649e8882e9d6f056a017, type: 3}
|
|
||||||
m_Version: 0
|
|
||||||
- rid: 1680211313492492303
|
|
||||||
type: {class: UniversalRenderPipelineEditorMaterials, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
m_DefaultMaterial: {fileID: 2100000, guid: 31321ba15b8f8eb4c954353edc038b1d, type: 2}
|
|
||||||
m_DefaultParticleMaterial: {fileID: 2100000, guid: e823cd5b5d27c0f4b8256e7c12ee3e6d, type: 2}
|
|
||||||
m_DefaultLineMaterial: {fileID: 2100000, guid: e823cd5b5d27c0f4b8256e7c12ee3e6d, type: 2}
|
|
||||||
m_DefaultTerrainMaterial: {fileID: 2100000, guid: 594ea882c5a793440b60ff72d896021e, type: 2}
|
|
||||||
m_DefaultDecalMaterial: {fileID: 2100000, guid: 31d0dcc6f2dd4e4408d18036a2c93862, type: 2}
|
|
||||||
m_DefaultSpriteMaterial: {fileID: 2100000, guid: 9dfc825aed78fcd4ba02077103263b40, type: 2}
|
|
||||||
- rid: 1680211313492492304
|
|
||||||
type: {class: PostProcessData/TextureResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
blueNoise16LTex:
|
|
||||||
- {fileID: 2800000, guid: 81200413a40918d4d8702e94db29911c, type: 3}
|
|
||||||
- {fileID: 2800000, guid: d50c5e07c9911a74982bddf7f3075e7b, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 1134690bf9216164dbc75050e35b7900, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 7ce2118f74614a94aa8a0cdf2e6062c3, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 2ca97df9d1801e84a8a8f2c53cb744f0, type: 3}
|
|
||||||
- {fileID: 2800000, guid: e63eef8f54aa9dc4da9a5ac094b503b5, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 39451254daebd6d40b52899c1f1c0c1b, type: 3}
|
|
||||||
- {fileID: 2800000, guid: c94ad916058dff743b0f1c969ddbe660, type: 3}
|
|
||||||
- {fileID: 2800000, guid: ed5ea7ce59ca8ec4f9f14bf470a30f35, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 071e954febf155243a6c81e48f452644, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 96aaab9cc247d0b4c98132159688c1af, type: 3}
|
|
||||||
- {fileID: 2800000, guid: fc3fa8f108657e14486697c9a84ccfc5, type: 3}
|
|
||||||
- {fileID: 2800000, guid: bfed3e498947fcb4890b7f40f54d85b9, type: 3}
|
|
||||||
- {fileID: 2800000, guid: d512512f4af60a442ab3458489412954, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 47a45908f6db0cb44a0d5e961143afec, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 4dcc0502f8586f941b5c4a66717205e8, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 9d92991794bb5864c8085468b97aa067, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 14381521ff11cb74abe3fe65401c23be, type: 3}
|
|
||||||
- {fileID: 2800000, guid: d36f0fe53425e08499a2333cf423634c, type: 3}
|
|
||||||
- {fileID: 2800000, guid: d4044ea2490d63b43aa1765f8efbf8a9, type: 3}
|
|
||||||
- {fileID: 2800000, guid: c9bd74624d8070f429e3f46d161f9204, type: 3}
|
|
||||||
- {fileID: 2800000, guid: d5c9b274310e5524ebe32a4e4da3df1f, type: 3}
|
|
||||||
- {fileID: 2800000, guid: f69770e54f2823f43badf77916acad83, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 10b6c6d22e73dea46a8ab36b6eebd629, type: 3}
|
|
||||||
- {fileID: 2800000, guid: a2ec5cbf5a9b64345ad3fab0912ddf7b, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 1c3c6d69a645b804fa232004b96b7ad3, type: 3}
|
|
||||||
- {fileID: 2800000, guid: d18a24d7b4ed50f4387993566d9d3ae2, type: 3}
|
|
||||||
- {fileID: 2800000, guid: c989e1ed85cf7154caa922fec53e6af6, type: 3}
|
|
||||||
- {fileID: 2800000, guid: ff47e5a0f105eb34883b973e51f4db62, type: 3}
|
|
||||||
- {fileID: 2800000, guid: fa042edbfc40fbd4bad0ab9d505b1223, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 896d9004736809c4fb5973b7c12eb8b9, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 179f794063d2a66478e6e726f84a65bc, type: 3}
|
|
||||||
filmGrainTex:
|
|
||||||
- {fileID: 2800000, guid: 654c582f7f8a5a14dbd7d119cbde215d, type: 3}
|
|
||||||
- {fileID: 2800000, guid: dd77ffd079630404e879388999033049, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 1097e90e1306e26439701489f391a6c0, type: 3}
|
|
||||||
- {fileID: 2800000, guid: f0b67500f7fad3b4c9f2b13e8f41ba6e, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 9930fb4528622b34687b00bbe6883de7, type: 3}
|
|
||||||
- {fileID: 2800000, guid: bd9e8c758250ef449a4b4bfaad7a2133, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 510a2f57334933e4a8dbabe4c30204e4, type: 3}
|
|
||||||
- {fileID: 2800000, guid: b4db8180660810945bf8d55ab44352ad, type: 3}
|
|
||||||
- {fileID: 2800000, guid: fd2fd78b392986e42a12df2177d3b89c, type: 3}
|
|
||||||
- {fileID: 2800000, guid: 5cdee82a77d13994f83b8fdabed7c301, type: 3}
|
|
||||||
smaaAreaTex: {fileID: 2800000, guid: d1f1048909d55cd4fa1126ab998f617e, type: 3}
|
|
||||||
smaaSearchTex: {fileID: 2800000, guid: 51eee22c2a633ef4aada830eed57c3fd, type: 3}
|
|
||||||
m_TexturesResourcesVersion: 0
|
|
||||||
- rid: 1680211313492492305
|
|
||||||
type: {class: URPDefaultVolumeProfileSettings, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 0
|
|
||||||
m_VolumeProfile: {fileID: 11400000, guid: 5b8b49b0bf6036a4a823021e69f80309, type: 2}
|
|
||||||
- rid: 1680211313492492306
|
|
||||||
type: {class: URPShaderStrippingSetting, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 0
|
|
||||||
m_StripUnusedPostProcessingVariants: 0
|
|
||||||
m_StripUnusedVariants: 1
|
|
||||||
m_StripScreenCoordOverrideVariants: 1
|
|
||||||
- rid: 1680211313492492307
|
|
||||||
type: {class: Renderer2DResources, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 0
|
|
||||||
m_LightShader: {fileID: 4800000, guid: 3f6c848ca3d7bca4bbe846546ac701a1, type: 3}
|
|
||||||
m_ProjectedShadowShader: {fileID: 4800000, guid: ce09d4a80b88c5a4eb9768fab4f1ee00, type: 3}
|
|
||||||
m_SpriteShadowShader: {fileID: 4800000, guid: 44fc62292b65ab04eabcf310e799ccf6, type: 3}
|
|
||||||
m_SpriteUnshadowShader: {fileID: 4800000, guid: de02b375720b5c445afe83cd483bedf3, type: 3}
|
|
||||||
m_GeometryShadowShader: {fileID: 4800000, guid: 19349a0f9a7ed4c48a27445bcf92e5e1, type: 3}
|
|
||||||
m_GeometryUnshadowShader: {fileID: 4800000, guid: 77774d9009bb81447b048c907d4c6273, type: 3}
|
|
||||||
m_CopyDepthPS: {fileID: 4800000, guid: d6dae50ee9e1bfa4db75f19f99355220, type: 3}
|
|
||||||
m_DefaultLitMaterial: {fileID: 2100000, guid: a97c105638bdf8b4a8650670310a4cd3, type: 2}
|
|
||||||
m_DefaultUnlitMaterial: {fileID: 2100000, guid: 9dfc825aed78fcd4ba02077103263b40, type: 2}
|
|
||||||
m_DefaultMaskMaterial: {fileID: 2100000, guid: 15d0c3709176029428a0da2f8cecf0b5, type: 2}
|
|
||||||
m_DefaultMesh2DLitMaterial: {fileID: 2100000, guid: 9452ae1262a74094f8a68013fbcd1834, type: 2}
|
|
||||||
- rid: 1680211313492492308
|
|
||||||
type: {class: UniversalRenderPipelineEditorAssets, ns: UnityEngine.Rendering.Universal, asm: Unity.RenderPipelines.Universal.Runtime}
|
|
||||||
data:
|
|
||||||
m_DefaultSettingsVolumeProfile: {fileID: 11400000, guid: eda47df5b85f4f249abf7abd73db2cb2, type: 2}
|
|
||||||
- rid: 1680211313492492309
|
|
||||||
type: {class: GPUResidentDrawerResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.GPUDriven.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 0
|
|
||||||
m_InstanceDataBufferCopyKernels: {fileID: 7200000, guid: f984aeb540ded8b4fbb8a2047ab5b2e2, type: 3}
|
|
||||||
m_InstanceDataBufferUploadKernels: {fileID: 7200000, guid: 53864816eb00f2343b60e1a2c5a262ef, type: 3}
|
|
||||||
m_TransformUpdaterKernels: {fileID: 7200000, guid: 2a567b9b2733f8d47a700c3c85bed75b, type: 3}
|
|
||||||
m_WindDataUpdaterKernels: {fileID: 7200000, guid: fde76746e4fd0ed418c224f6b4084114, type: 3}
|
|
||||||
m_OccluderDepthPyramidKernels: {fileID: 7200000, guid: 08b2b5fb307b0d249860612774a987da, type: 3}
|
|
||||||
m_InstanceOcclusionCullingKernels: {fileID: 7200000, guid: f6d223acabc2f974795a5a7864b50e6c, type: 3}
|
|
||||||
m_OcclusionCullingDebugKernels: {fileID: 7200000, guid: b23e766bcf50ca4438ef186b174557df, type: 3}
|
|
||||||
m_DebugOcclusionTestPS: {fileID: 4800000, guid: d3f0849180c2d0944bc71060693df100, type: 3}
|
|
||||||
m_DebugOccluderPS: {fileID: 4800000, guid: b3c92426a88625841ab15ca6a7917248, type: 3}
|
|
||||||
- rid: 1680211313492492310
|
|
||||||
type: {class: VrsRenderPipelineRuntimeResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
|
|
||||||
data:
|
|
||||||
m_TextureComputeShader: {fileID: 7200000, guid: cacb30de6c40c7444bbc78cb0a81fd2a, type: 3}
|
|
||||||
m_VisualizationShader: {fileID: 4800000, guid: 620b55b8040a88d468e94abe55bed5ba, type: 3}
|
|
||||||
m_VisualizationLookupTable:
|
|
||||||
m_Data:
|
|
||||||
- {r: 0.785, g: 0.23, b: 0.2, a: 1}
|
|
||||||
- {r: 1, g: 0.8, b: 0.8, a: 1}
|
|
||||||
- {r: 0.4, g: 0.2, b: 0.2, a: 1}
|
|
||||||
- {r: 0.51, g: 0.8, b: 0.6, a: 1}
|
|
||||||
- {r: 0.6, g: 0.8, b: 1, a: 1}
|
|
||||||
- {r: 0.2, g: 0.4, b: 0.6, a: 1}
|
|
||||||
- {r: 0.8, g: 1, b: 0.8, a: 1}
|
|
||||||
- {r: 0.2, g: 0.4, b: 0.2, a: 1}
|
|
||||||
- {r: 0.125, g: 0.22, b: 0.36, a: 1}
|
|
||||||
m_ConversionLookupTable:
|
|
||||||
m_Data:
|
|
||||||
- {r: 0.785, g: 0.23, b: 0.2, a: 1}
|
|
||||||
- {r: 1, g: 0.8, b: 0.8, a: 1}
|
|
||||||
- {r: 0.4, g: 0.2, b: 0.2, a: 1}
|
|
||||||
- {r: 0.51, g: 0.8, b: 0.6, a: 1}
|
|
||||||
- {r: 0.6, g: 0.8, b: 1, a: 1}
|
|
||||||
- {r: 0.2, g: 0.4, b: 0.6, a: 1}
|
|
||||||
- {r: 0.8, g: 1, b: 0.8, a: 1}
|
|
||||||
- {r: 0.2, g: 0.4, b: 0.2, a: 1}
|
|
||||||
- {r: 0.125, g: 0.22, b: 0.36, a: 1}
|
|
||||||
- rid: 1680211313492492311
|
|
||||||
type: {class: STP/RuntimeResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
|
|
||||||
data:
|
|
||||||
m_setupCS: {fileID: 7200000, guid: 33be2e9a5506b2843bdb2bdff9cad5e1, type: 3}
|
|
||||||
m_preTaaCS: {fileID: 7200000, guid: a679dba8ec4d9ce45884a270b0e22dda, type: 3}
|
|
||||||
m_taaCS: {fileID: 7200000, guid: 3923900e2b41b5e47bc25bfdcbcdc9e6, type: 3}
|
|
||||||
- rid: 1680211313492492312
|
|
||||||
type: {class: RenderingDebuggerRuntimeResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
|
|
||||||
data:
|
|
||||||
m_version: 0
|
|
||||||
- rid: 1680211313492492313
|
|
||||||
type: {class: RenderGraphUtilsResources, ns: UnityEngine.Rendering.RenderGraphModule.Util, asm: Unity.RenderPipelines.Core.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 0
|
|
||||||
m_CoreCopyPS: {fileID: 4800000, guid: 12dc59547ea167a4ab435097dd0f9add, type: 3}
|
|
||||||
- rid: 1680211313492492314
|
|
||||||
type: {class: ProbeVolumeBakingResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 1
|
|
||||||
dilationShader: {fileID: 7200000, guid: 6bb382f7de370af41b775f54182e491d, type: 3}
|
|
||||||
subdivideSceneCS: {fileID: 7200000, guid: bb86f1f0af829fd45b2ebddda1245c22, type: 3}
|
|
||||||
voxelizeSceneShader: {fileID: 4800000, guid: c8b6a681c7b4e2e4785ffab093907f9e, type: 3}
|
|
||||||
traceVirtualOffsetCS: {fileID: -6772857160820960102, guid: ff2cbab5da58bf04d82c5f34037ed123, type: 3}
|
|
||||||
traceVirtualOffsetRT: {fileID: -5126288278712620388, guid: ff2cbab5da58bf04d82c5f34037ed123, type: 3}
|
|
||||||
skyOcclusionCS: {fileID: -6772857160820960102, guid: 5a2a534753fbdb44e96c3c78b5a6999d, type: 3}
|
|
||||||
skyOcclusionRT: {fileID: -5126288278712620388, guid: 5a2a534753fbdb44e96c3c78b5a6999d, type: 3}
|
|
||||||
renderingLayerCS: {fileID: -6772857160820960102, guid: 94a070d33e408384bafc1dea4a565df9, type: 3}
|
|
||||||
renderingLayerRT: {fileID: -5126288278712620388, guid: 94a070d33e408384bafc1dea4a565df9, type: 3}
|
|
||||||
- rid: 1680211313492492315
|
|
||||||
type: {class: LightmapSamplingSettings, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 1
|
|
||||||
m_UseBicubicLightmapSampling: 0
|
|
||||||
- rid: 1680211313492492316
|
|
||||||
type: {class: RenderGraphGlobalSettings, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
|
|
||||||
data:
|
|
||||||
m_version: 0
|
|
||||||
m_EnableCompilationCaching: 1
|
|
||||||
m_EnableValidityChecks: 1
|
|
||||||
- rid: 1680211313492492317
|
|
||||||
type: {class: IncludeAdditionalRPAssets, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
|
|
||||||
data:
|
|
||||||
m_version: 0
|
|
||||||
m_IncludeReferencedInScenes: 0
|
|
||||||
m_IncludeAssetsByLabel: 0
|
|
||||||
m_LabelToInclude:
|
|
||||||
- rid: 1680211313492492318
|
|
||||||
type: {class: ProbeVolumeDebugResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 1
|
|
||||||
probeVolumeDebugShader: {fileID: 4800000, guid: 3b21275fd12d65f49babb5286f040f2d, type: 3}
|
|
||||||
probeVolumeFragmentationDebugShader: {fileID: 4800000, guid: 3a80877c579b9144ebdcc6d923bca303, type: 3}
|
|
||||||
probeVolumeSamplingDebugShader: {fileID: 4800000, guid: bf54e6528c79a224e96346799064c393, type: 3}
|
|
||||||
probeVolumeOffsetDebugShader: {fileID: 4800000, guid: db8bd7436dc2c5f4c92655307d198381, type: 3}
|
|
||||||
probeSamplingDebugMesh: {fileID: -3555484719484374845, guid: 20be25aac4e22ee49a7db76fb3df6de2, type: 3}
|
|
||||||
numbersDisplayTex: {fileID: 2800000, guid: 73fe53b428c5b3440b7e87ee830b608a, type: 3}
|
|
||||||
- rid: 1680211313492492319
|
|
||||||
type: {class: ShaderStrippingSetting, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 0
|
|
||||||
m_ExportShaderVariants: 1
|
|
||||||
m_ShaderVariantLogLevel: 0
|
|
||||||
m_StripRuntimeDebugShaders: 1
|
|
||||||
- rid: 1680211313492492320
|
|
||||||
type: {class: ProbeVolumeGlobalSettings, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 1
|
|
||||||
m_ProbeVolumeDisableStreamingAssets: 0
|
|
||||||
- rid: 1680211313492492321
|
|
||||||
type: {class: ProbeVolumeRuntimeResources, ns: UnityEngine.Rendering, asm: Unity.RenderPipelines.Core.Runtime}
|
|
||||||
data:
|
|
||||||
m_Version: 1
|
|
||||||
probeVolumeBlendStatesCS: {fileID: 7200000, guid: a3f7b8c99de28a94684cb1daebeccf5d, type: 3}
|
|
||||||
probeVolumeUploadDataCS: {fileID: 7200000, guid: 0951de5992461754fa73650732c4954c, type: 3}
|
|
||||||
probeVolumeUploadDataL2CS: {fileID: 7200000, guid: 6196f34ed825db14b81fb3eb0ea8d931, type: 3}
|
|
||||||
- rid: 1680211313492492322
|
|
||||||
type: {class: WorldRenderPipelineResources, ns: UnityEngine.PathTracing.Core, asm: Unity.PathTracing.Runtime}
|
|
||||||
data:
|
|
||||||
_version: 3
|
|
||||||
_blitCubemap: {fileID: 7200000, guid: 5a992812cb320d146a66cc600200cce7, type: 3}
|
|
||||||
_blitGrayScaleCookie: {fileID: 7200000, guid: 557fa399e33bf7647bda5697c5c158df, type: 3}
|
|
||||||
_setAlphaChannelShader: {fileID: 7200000, guid: 5efaea0e81c66334aa9d062d6573e6fd, type: 3}
|
|
||||||
_environmentImportanceSamplingBuild: {fileID: 7200000, guid: 5bb2534d2411d344cbc54f880232640f, type: 3}
|
|
||||||
_skyBoxMesh: {fileID: 4300000, guid: 0529e6c5f6dea8c4a8c2835ed7de57cb, type: 2}
|
|
||||||
_sixFaceSkyBoxMesh: {fileID: 4300000, guid: a80925ceebd011741b42509226cefc74, type: 2}
|
|
||||||
_buildLightGridShader: {fileID: 7200000, guid: 16e47c1641bd0104e92b624601457bb0, type: 3}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 9374f1d73c9ee8940895d44ff5dd560e
|
|
||||||
NativeFormatImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
mainObjectFileID: 11400000
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
|
|
@ -5,12 +5,11 @@
|
||||||
"com.unity.modules.imageconversion": "1.0.0",
|
"com.unity.modules.imageconversion": "1.0.0",
|
||||||
"com.unity.modules.physics": "1.0.0",
|
"com.unity.modules.physics": "1.0.0",
|
||||||
"com.unity.modules.uielements": "1.0.0",
|
"com.unity.modules.uielements": "1.0.0",
|
||||||
"com.unity.render-pipelines.universal": "17.4.0",
|
"org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.7",
|
||||||
"org.gamecult.cultlib": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#334e60f1928b4212a29dd8b0d19b2c099fe6365e",
|
"org.gamecult.eve.plugin-fields": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1",
|
||||||
"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/EveUnity.git?path=/packages/org.gamecult.eve.surface#b6f001b300a88aff0bc13372b5c1dbc2002954f8",
|
||||||
"org.gamecult.eve.surface": "https://github.com/GameCult/Eve.git?path=/packages/org.gamecult.eve.surface#96839ad34c8d464ef622d8bbdd5d277e1ca9d825",
|
"org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#b6f001b300a88aff0bc13372b5c1dbc2002954f8",
|
||||||
"org.gamecult.eve.unity-scene": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#d1663b36d12b60ff5c7249135eff48edf6227ca9",
|
"org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0"
|
||||||
"org.gamecult.eve.unity-uitoolkit": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#d1663b36d12b60ff5c7249135eff48edf6227ca9"
|
|
||||||
},
|
},
|
||||||
"testables": [
|
"testables": [
|
||||||
"org.gamecult.eve.unity-scene",
|
"org.gamecult.eve.unity-scene",
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,5 @@
|
||||||
{
|
{
|
||||||
"dependencies": {
|
"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": {
|
"com.unity.ext.nunit": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
|
|
@ -35,60 +13,6 @@
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"url": "https://packages.unity.com"
|
"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": {
|
"com.unity.test-framework": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
|
|
@ -99,73 +23,49 @@
|
||||||
"com.unity.modules.jsonserialize": "1.0.0"
|
"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": {
|
"org.gamecult.cultlib": {
|
||||||
"version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#334e60f1928b4212a29dd8b0d19b2c099fe6365e",
|
"version": "https://github.com/GameCult/CultLib.git?path=/unity/org.gamecult.cultlib#cultlib-unity-v1.0.7",
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
"source": "git",
|
"source": "git",
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"hash": "334e60f1928b4212a29dd8b0d19b2c099fe6365e"
|
"hash": "1f8f832a788e2bdf59df90fb91da583574d3a42d"
|
||||||
},
|
},
|
||||||
"org.gamecult.eve.plugin-fields": {
|
"org.gamecult.eve.plugin-fields": {
|
||||||
"version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.2.3",
|
"version": "https://github.com/GameCult/EvePlugins.git?path=/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields#eve-plugin-fields-unity-v0.1.1",
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
"source": "git",
|
"source": "git",
|
||||||
"dependencies": {
|
"dependencies": {},
|
||||||
"org.gamecult.cultlib": "1.0.13"
|
"hash": "0b099274a4e2b4c301eced4322166837792f585a"
|
||||||
},
|
|
||||||
"hash": "c5a4a75c1b727499b16c2dae1895f29e2a9f72f0"
|
|
||||||
},
|
},
|
||||||
"org.gamecult.eve.surface": {
|
"org.gamecult.eve.surface": {
|
||||||
"version": "https://github.com/GameCult/Eve.git?path=/packages/org.gamecult.eve.surface#96839ad34c8d464ef622d8bbdd5d277e1ca9d825",
|
"version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.surface#b6f001b300a88aff0bc13372b5c1dbc2002954f8",
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
"source": "git",
|
"source": "git",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"org.gamecult.cultlib": "1.0.56"
|
"org.gamecult.cultlib": "1.0.6"
|
||||||
},
|
},
|
||||||
"hash": "96839ad34c8d464ef622d8bbdd5d277e1ca9d825"
|
"hash": "b6f001b300a88aff0bc13372b5c1dbc2002954f8"
|
||||||
},
|
},
|
||||||
"org.gamecult.eve.unity-scene": {
|
"org.gamecult.eve.unity-scene": {
|
||||||
"version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#d1663b36d12b60ff5c7249135eff48edf6227ca9",
|
"version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-scene#b6f001b300a88aff0bc13372b5c1dbc2002954f8",
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
"source": "git",
|
"source": "git",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"org.gamecult.cultlib": "1.0.56",
|
"org.gamecult.cultlib": "1.0.6",
|
||||||
"org.gamecult.eve.plugin-fields": "0.2.3",
|
"org.gamecult.eve.plugin-fields": "0.1.1",
|
||||||
"org.gamecult.eve.surface": "0.3.6",
|
"org.gamecult.eve.surface": "0.1.0",
|
||||||
"org.gamecult.eve.unity-uitoolkit": "0.1.9",
|
"com.unity.mathematics": "1.3.2"
|
||||||
"com.unity.mathematics": "1.3.2",
|
|
||||||
"com.unity.render-pipelines.universal": "17.4.0",
|
|
||||||
"com.unity.modules.particlesystem": "1.0.0"
|
|
||||||
},
|
},
|
||||||
"hash": "d1663b36d12b60ff5c7249135eff48edf6227ca9"
|
"hash": "b6f001b300a88aff0bc13372b5c1dbc2002954f8"
|
||||||
},
|
},
|
||||||
"org.gamecult.eve.unity-uitoolkit": {
|
"org.gamecult.eve.unity-uitoolkit": {
|
||||||
"version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#d1663b36d12b60ff5c7249135eff48edf6227ca9",
|
"version": "https://github.com/GameCult/EveUnity.git?path=/packages/org.gamecult.eve.unity-uitoolkit#eveunity-uitoolkit-v0.1.0",
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
"source": "git",
|
"source": "git",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"org.gamecult.eve.surface": "0.3.6"
|
"org.gamecult.eve.surface": "0.1.0"
|
||||||
},
|
},
|
||||||
"hash": "d1663b36d12b60ff5c7249135eff48edf6227ca9"
|
"hash": "e10821f1cc81f619479c6507438f6a097d1f0ce2"
|
||||||
},
|
},
|
||||||
"com.unity.modules.assetbundle": {
|
"com.unity.modules.assetbundle": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|
@ -197,24 +97,12 @@
|
||||||
"source": "builtin",
|
"source": "builtin",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
},
|
},
|
||||||
"com.unity.modules.particlesystem": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"depth": 1,
|
|
||||||
"source": "builtin",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"com.unity.modules.physics": {
|
"com.unity.modules.physics": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
"source": "builtin",
|
"source": "builtin",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
},
|
},
|
||||||
"com.unity.modules.terrain": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"depth": 2,
|
|
||||||
"source": "builtin",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"com.unity.modules.ui": {
|
"com.unity.modules.ui": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ GraphicsSettings:
|
||||||
m_PreloadedShaders: []
|
m_PreloadedShaders: []
|
||||||
m_PreloadShadersBatchTimeLimit: -1
|
m_PreloadShadersBatchTimeLimit: -1
|
||||||
m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0}
|
m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0}
|
||||||
m_CustomRenderPipeline: {fileID: 11400000, guid: 43f32b840bd83ce47af920dadd59d4b3, type: 2}
|
m_CustomRenderPipeline: {fileID: 0}
|
||||||
m_TransparencySortMode: 0
|
m_TransparencySortMode: 0
|
||||||
m_TransparencySortAxis: {x: 0, y: 0, z: 1}
|
m_TransparencySortAxis: {x: 0, y: 0, z: 1}
|
||||||
m_DefaultRenderingPath: 1
|
m_DefaultRenderingPath: 1
|
||||||
|
|
@ -57,8 +57,7 @@ GraphicsSettings:
|
||||||
m_FogKeepExp: 1
|
m_FogKeepExp: 1
|
||||||
m_FogKeepExp2: 1
|
m_FogKeepExp2: 1
|
||||||
m_AlbedoSwatchInfos: []
|
m_AlbedoSwatchInfos: []
|
||||||
m_RenderPipelineGlobalSettingsMap:
|
m_RenderPipelineGlobalSettingsMap: {}
|
||||||
UnityEngine.Rendering.Universal.UniversalRenderPipeline: {fileID: 11400000, guid: 9374f1d73c9ee8940895d44ff5dd560e, type: 2}
|
|
||||||
m_ShaderBuildSettings:
|
m_ShaderBuildSettings:
|
||||||
keywordDeclarationOverrides: []
|
keywordDeclarationOverrides: []
|
||||||
m_LightsUseLinearIntensity: 0
|
m_LightsUseLinearIntensity: 0
|
||||||
|
|
|
||||||
|
|
@ -313,7 +313,7 @@ QualitySettings:
|
||||||
asyncUploadBufferSize: 16
|
asyncUploadBufferSize: 16
|
||||||
asyncUploadPersistentBuffer: 1
|
asyncUploadPersistentBuffer: 1
|
||||||
resolutionScalingFixedDPIFactor: 1
|
resolutionScalingFixedDPIFactor: 1
|
||||||
customRenderPipeline: {fileID: 11400000, guid: 43f32b840bd83ce47af920dadd59d4b3, type: 2}
|
customRenderPipeline: {fileID: 0}
|
||||||
terrainQualityOverrides: 0
|
terrainQualityOverrides: 0
|
||||||
terrainPixelError: 1
|
terrainPixelError: 1
|
||||||
terrainDetailDensityScale: 1
|
terrainDetailDensityScale: 1
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,8 @@
|
||||||
"references": [
|
"references": [
|
||||||
"GameCult.CultLib",
|
"GameCult.CultLib",
|
||||||
"GameCult.Eve.Surface",
|
"GameCult.Eve.Surface",
|
||||||
"GameCult.Eve.UnityUIToolkit",
|
|
||||||
"GameCult.Eve.UnityScene",
|
"GameCult.Eve.UnityScene",
|
||||||
"GameCult.Eve.UnityScene.CultMesh",
|
"GameCult.Eve.UnityScene.CultMesh"
|
||||||
"Unity.RenderPipelines.Universal.Runtime"
|
|
||||||
],
|
],
|
||||||
"includePlatforms": [],
|
"includePlatforms": [],
|
||||||
"excludePlatforms": [],
|
"excludePlatforms": [],
|
||||||
|
|
@ -16,7 +14,6 @@
|
||||||
"GameCult.Caching.dll",
|
"GameCult.Caching.dll",
|
||||||
"GameCult.Caching.MessagePack.dll",
|
"GameCult.Caching.MessagePack.dll",
|
||||||
"GameCult.Mesh.dll",
|
"GameCult.Mesh.dll",
|
||||||
"GameCult.Mesh.Quic.Native.dll",
|
|
||||||
"GameCult.Networking.dll"
|
"GameCult.Networking.dll"
|
||||||
],
|
],
|
||||||
"autoReferenced": false,
|
"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.imageconversion": "1.0.0",
|
||||||
"com.unity.modules.physics": "1.0.0",
|
"com.unity.modules.physics": "1.0.0",
|
||||||
"com.unity.modules.uielements": "1.0.0",
|
"com.unity.modules.uielements": "1.0.0",
|
||||||
"org.gamecult.cultlib": "file:../../artifacts/test-dependencies/org.gamecult.cultlib",
|
"org.gamecult.cultlib": "file:../../../CultLib/artifacts/unity/org.gamecult.cultlib",
|
||||||
"org.gamecult.eve.plugin-fields": "file:../../../EvePlugins/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields",
|
"org.gamecult.eve.plugin-fields": "file:../../../EvePlugins/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields",
|
||||||
"org.gamecult.eve.surface": "file:../../../Eve/packages/org.gamecult.eve.surface",
|
"org.gamecult.eve.surface": "file:../../packages/org.gamecult.eve.surface",
|
||||||
"org.gamecult.eve.unity-scene": "file:../../packages/org.gamecult.eve.unity-scene",
|
"org.gamecult.eve.unity-scene": "file:../../packages/org.gamecult.eve.unity-scene",
|
||||||
"org.gamecult.eve.unity-uitoolkit": "file:../../packages/org.gamecult.eve.unity-uitoolkit"
|
"org.gamecult.eve.unity-uitoolkit": "file:../../packages/org.gamecult.eve.unity-uitoolkit"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,94 +1,11 @@
|
||||||
{
|
{
|
||||||
"dependencies": {
|
"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": {
|
"com.unity.ext.nunit": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
"source": "builtin",
|
"source": "builtin",
|
||||||
"dependencies": {}
|
"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": {
|
"com.unity.test-framework": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
|
|
@ -99,27 +16,8 @@
|
||||||
"com.unity.modules.jsonserialize": "1.0.0"
|
"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": {
|
"org.gamecult.cultlib": {
|
||||||
"version": "file:../../artifacts/test-dependencies/org.gamecult.cultlib",
|
"version": "file:../../../CultLib/artifacts/unity/org.gamecult.cultlib",
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
"source": "local",
|
"source": "local",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
|
|
@ -128,16 +26,14 @@
|
||||||
"version": "file:../../../EvePlugins/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields",
|
"version": "file:../../../EvePlugins/plugins/eve-plugin-fields/unity/org.gamecult.eve.plugin-fields",
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
"source": "local",
|
"source": "local",
|
||||||
"dependencies": {
|
"dependencies": {}
|
||||||
"org.gamecult.cultlib": "1.0.13"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"org.gamecult.eve.surface": {
|
"org.gamecult.eve.surface": {
|
||||||
"version": "file:../../../Eve/packages/org.gamecult.eve.surface",
|
"version": "file:../../packages/org.gamecult.eve.surface",
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
"source": "local",
|
"source": "local",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"org.gamecult.cultlib": "1.0.56"
|
"org.gamecult.cultlib": "1.0.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"org.gamecult.eve.unity-scene": {
|
"org.gamecult.eve.unity-scene": {
|
||||||
|
|
@ -145,13 +41,9 @@
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
"source": "local",
|
"source": "local",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"org.gamecult.cultlib": "1.0.56",
|
"org.gamecult.cultlib": "1.0.3",
|
||||||
"org.gamecult.eve.plugin-fields": "0.2.3",
|
"org.gamecult.eve.plugin-fields": "0.1.1",
|
||||||
"org.gamecult.eve.surface": "0.3.6",
|
"org.gamecult.eve.surface": "0.1.0"
|
||||||
"org.gamecult.eve.unity-uitoolkit": "0.1.9",
|
|
||||||
"com.unity.mathematics": "1.3.2",
|
|
||||||
"com.unity.render-pipelines.universal": "17.4.0",
|
|
||||||
"com.unity.modules.particlesystem": "1.0.0"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"org.gamecult.eve.unity-uitoolkit": {
|
"org.gamecult.eve.unity-uitoolkit": {
|
||||||
|
|
@ -159,7 +51,7 @@
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
"source": "local",
|
"source": "local",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"org.gamecult.eve.surface": "0.3.6"
|
"org.gamecult.eve.surface": "0.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"com.unity.modules.assetbundle": {
|
"com.unity.modules.assetbundle": {
|
||||||
|
|
@ -192,24 +84,12 @@
|
||||||
"source": "builtin",
|
"source": "builtin",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
},
|
},
|
||||||
"com.unity.modules.particlesystem": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"depth": 1,
|
|
||||||
"source": "builtin",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"com.unity.modules.physics": {
|
"com.unity.modules.physics": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"depth": 0,
|
"depth": 0,
|
||||||
"source": "builtin",
|
"source": "builtin",
|
||||||
"dependencies": {}
|
"dependencies": {}
|
||||||
},
|
},
|
||||||
"com.unity.modules.terrain": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"depth": 3,
|
|
||||||
"source": "builtin",
|
|
||||||
"dependencies": {}
|
|
||||||
},
|
|
||||||
"com.unity.modules.ui": {
|
"com.unity.modules.ui": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"depth": 1,
|
"depth": 1,
|
||||||
|
|
|
||||||
|
|
@ -1,370 +0,0 @@
|
||||||
# Aetheria released-package witness
|
|
||||||
|
|
||||||
## Current integration gate
|
|
||||||
|
|
||||||
The generic `ReleaseConsumerProject` is pinned to released Git packages:
|
|
||||||
|
|
||||||
- `org.gamecult.eve.unity-scene` `0.3.134`, commit
|
|
||||||
`d1663b36d12b60ff5c7249135eff48edf6227ca9`, and
|
|
||||||
`org.gamecult.eve.surface` `0.3.6`, commit
|
|
||||||
`96839ad34c8d464ef622d8bbdd5d277e1ca9d825`;
|
|
||||||
- `org.gamecult.eve.plugin-fields` `0.2.3`, commit
|
|
||||||
`c5a4a75c1b727499b16c2dae1895f29e2a9f72f0`;
|
|
||||||
- `org.gamecult.eve.unity-uitoolkit` `0.1.9`, commit
|
|
||||||
`d1663b36d12b60ff5c7249135eff48edf6227ca9`;
|
|
||||||
- `org.gamecult.cultlib` `1.0.56`, commit
|
|
||||||
`334e60f1928b4212a29dd8b0d19b2c099fe6365e`.
|
|
||||||
|
|
||||||
The hand-run integration gates are the cold lowering proof and the warm
|
|
||||||
full-session proof:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
pwsh -File .\scripts\run-aetheria-daemon-world-witness.ps1 `
|
|
||||||
-CultLibRoot E:\Projects\CultLib `
|
|
||||||
-CacheState cold `
|
|
||||||
-AssetCacheDirectory artifacts\admitted-release-cold-cache `
|
|
||||||
-OutputDirectory artifacts\admitted-release-cold `
|
|
||||||
-SkipAssetBundleBuild
|
|
||||||
```
|
|
||||||
|
|
||||||
Both connect directly to the Aetheria daemon. Control, subscriptions, commands,
|
|
||||||
and receipts use `cultnet+tcp`; provider files use `cultmesh-content+tcp`;
|
|
||||||
same-machine entity state uses a mapped SoA body; and the released native QUIC
|
|
||||||
client receives the advertised latest-only realtime body. No Odin hop or
|
|
||||||
Aetheria-specific gameplay code participates in the Unity runtime. This
|
|
||||||
admission pass proves the package graph with the isolated EditMode consumer; it
|
|
||||||
does not claim a new rendered end-to-end witness.
|
|
||||||
|
|
||||||
## Historical witness ledger
|
|
||||||
|
|
||||||
The retained cold proof is
|
|
||||||
`artifacts/cultlib-1043-scene-03100-cold5/runtime-witness.cold.json`. It began
|
|
||||||
with no bodies or partials and passed in 57.8 seconds end to end (14.9 seconds
|
|
||||||
inside PlayMode). The client materialized nine requested bundles totaling
|
|
||||||
56,872,521 bytes: shaders, core resources, UI, Longinus, tractor beam, the three
|
|
||||||
visible celestial types, and the visible asteroid. Every body matches the
|
|
||||||
current provider file by SHA-256 and length; no partial remained. Unused ships,
|
|
||||||
stations, shield, mine, turret, pickup, lightning, and explosion bundles stayed
|
|
||||||
unloaded. This cold profile owns content transfer, world lowering, fog and
|
|
||||||
Stardust execution, and pilot/map camera isolation. It deliberately issues no
|
|
||||||
gameplay command into the paused Terminus fixture.
|
|
||||||
|
|
||||||
The retained warm full-session proof is
|
|
||||||
`artifacts/cultlib-1043-scene-03100-warm/runtime-witness.warm.json`. It passed in
|
|
||||||
70.7 seconds end to end (27.9 seconds inside PlayMode). The generic client docks,
|
|
||||||
buys and sells one typed item, resumes and undocks, moves, targets, aims, holds
|
|
||||||
and releases the tractor beam, destroys the proof hostile, and collects the
|
|
||||||
resulting loot exactly once through daemon-owned XZ proximity. All eight
|
|
||||||
retained receipts are provider-owned and reconciled. Eleven on-demand bundles
|
|
||||||
totaling 74,191,724 bytes were present by the end; gameplay loaded only the
|
|
||||||
additional content it exercised. The pilot camera excludes all eleven map
|
|
||||||
renderers and the map camera includes them.
|
|
||||||
|
|
||||||
The pilot capture proves that provider fog, Stardust, celestial meshes, and ship
|
|
||||||
meshes reach URP, but it is not final visual approval: hulls remain crushed to
|
|
||||||
black and the cloud frame remains too hot and cyan-grey. Those are provider
|
|
||||||
visual-lever and shader-calibration issues, not concealed transport failures.
|
|
||||||
The map capture is clean and contains only map-channel glyphs on its dark map
|
|
||||||
background.
|
|
||||||
|
|
||||||
An earlier released-package cold world witness passed with:
|
|
||||||
|
|
||||||
- `org.gamecult.eve.unity-scene` `0.3.63`, commit
|
|
||||||
`3135d24bb84f40719b121b51634811c85af0a7af`;
|
|
||||||
- `org.gamecult.eve.plugin-fields` `0.2.3`, commit
|
|
||||||
`c5a4a75c1b727499b16c2dae1895f29e2a9f72f0`;
|
|
||||||
- `org.gamecult.eve.surface` `0.2.2`, commit
|
|
||||||
`140e1bd963a0033e66777a3b2c5fe6e9c97dfe32`;
|
|
||||||
- `org.gamecult.eve.unity-uitoolkit` `0.1.1`, commit
|
|
||||||
`4d0cbe0185bdc4fc65eb63503a7c5cb578539669`;
|
|
||||||
- `org.gamecult.cultlib` `1.0.15`, commit
|
|
||||||
`419053ebe2325848051c4f4d8ba352cd4286c424`;
|
|
||||||
- the generic `ReleaseConsumerProject` client connected directly to the
|
|
||||||
Aetheria daemon.
|
|
||||||
|
|
||||||
The cold `0.3.59` witness in
|
|
||||||
`artifacts/aetheria-daemon-adaptive-exposure-cold` resolves the tagged package,
|
|
||||||
transfers and lowers the 56,170,916-byte provider bundle
|
|
||||||
`34dd156038c9c55b96136e10805f35f2bc7f2aa4e8dda8a519e57aa5a83329c8`, and
|
|
||||||
passes in 53.3 seconds. EveUnity lowers Aetheria's advertised historical
|
|
||||||
histogram exposure as a provider-agnostic camera semantic; the bundled static
|
|
||||||
volume profile remains neutral and is no longer a competing exposure owner.
|
|
||||||
Average pilot-frame luminance fell from `0.8716` in the restored-scattering
|
|
||||||
camera witness to `0.5096`, recovering visible cloud structure. The capture is
|
|
||||||
still cyan and visibly posterized, so temporal/color parity remains open.
|
|
||||||
Map-channel isolation also passes: the pilot camera excludes the 11 map
|
|
||||||
renderers and the map camera includes them.
|
|
||||||
|
|
||||||
The subsequent released `0.3.61` warm witness in
|
|
||||||
`artifacts/aetheria-daemon-temporal-bootstrap-warm` restores two temporal
|
|
||||||
details from the fossil renderer as explicit provider-program semantics. The
|
|
||||||
fog history projects the previous view through the current projection, and the
|
|
||||||
first history frame uses the provider-advertised `ultra` quality before
|
|
||||||
settling to the scene's serialized `high` quality. Average luminance remains
|
|
||||||
effectively unchanged while mean neighboring-pixel variation falls from
|
|
||||||
`0.0332` to `0.0174` and Laplacian high-frequency energy falls from `0.1050`
|
|
||||||
to `0.0531`. This proves the temporal history is accumulating and that its
|
|
||||||
bootstrap seed mattered. The remaining cyan/tonemap mismatch is still open.
|
|
||||||
|
|
||||||
The cold HD witness in `artifacts/aetheria-daemon-fog-alpha-cold-hd` then
|
|
||||||
transfers the rebuilt 56,168,970-byte provider bundle
|
|
||||||
`a829e57731fbe08f4564fa131468148e99f6b4204708ad3e64498885532b5091`
|
|
||||||
through `SharedFileMapping` and captures after 128 temporal composites at
|
|
||||||
1280x720. It restores the historical fog history ABI: the temporal pass stores
|
|
||||||
density directly in alpha, so the composite consumes that density directly
|
|
||||||
instead of decoding it as the raymarch pass's packed distance/density payload.
|
|
||||||
The flat grey opacity slabs disappear. The released client test passes in 52.7
|
|
||||||
seconds with 238 fog composites and Stardust draws; the pilot camera still
|
|
||||||
excludes map renderers and the map camera includes them. Remaining visual work
|
|
||||||
is color/composition parity, not broken fog opacity or an unsettled witness.
|
|
||||||
|
|
||||||
The released `0.3.62` warm HD witness in
|
|
||||||
`artifacts/aetheria-daemon-hdr-grading-warm-hd` lowers the provider's
|
|
||||||
`hdr-before-tonemap.v1` grading-space contract. This restores the fossil
|
|
||||||
profile's HDR ordering—grading before ACES—rather than inheriting the released
|
|
||||||
consumer project's default LDR grading after tonemapping. EveUnity restores the
|
|
||||||
consumer's prior pipeline mode when the camera is released and fails closed
|
|
||||||
when the requested grading space cannot be lowered.
|
|
||||||
|
|
||||||
The asserted rerun in `artifacts/aetheria-daemon-hdr-grading-asserted-warm-hd`
|
|
||||||
also completes the live destruction-loot path on the same released client. One
|
|
||||||
daemon destruction produces the canonical pickup; tractor power reaches `1`
|
|
||||||
and returns to `0`; one Ymir `begin` fact produces exactly one
|
|
||||||
`pickup.collected` event; cargo changes from `0` to `1`; and no client pickup
|
|
||||||
command participates in the transaction. The event identity begins
|
|
||||||
`ymir-fact:` and contains the retained Box3D session, step, contact episode,
|
|
||||||
and begin-fact identity.
|
|
||||||
|
|
||||||
The released `0.3.63` warm HD witness in
|
|
||||||
`artifacts/aetheria-daemon-temporal-projection-0363-warm-hd` makes the native
|
|
||||||
volume history projection convention an explicit provider-program semantic.
|
|
||||||
Aetheria advertises the non-render-target projection convention used by its
|
|
||||||
historical `Graphics.Blit` temporal pass; the generic URP lowerer applies that
|
|
||||||
convention without naming Aetheria or interpreting fog. The witness captures
|
|
||||||
the naturally rendered camera target after 359 volume composites instead of
|
|
||||||
issuing a second out-of-band `Camera.Render()`. Package tests pass 105/105 and
|
|
||||||
the released gameplay witness still proves movement, combat, destruction loot,
|
|
||||||
one Ymir contact collection, held tractor input, and camera-channel isolation.
|
|
||||||
The remaining visual controls (fog parameters, stellar tint, ambient intensity,
|
|
||||||
histogram exposure, grading, and temporal settings) remain provider-owned
|
|
||||||
levers; this proof does not claim a final art grade.
|
|
||||||
|
|
||||||
The released `0.3.63` warm rejection witness in
|
|
||||||
`artifacts/aetheria-daemon-cargo-rejection-0363-warm` selects the daemon-owned
|
|
||||||
`cargo-capacity-rejection-proof` scenario. The controlled ship begins with all
|
|
||||||
generated cargo bays filled from typed item volume and bay capacity. Combat
|
|
||||||
creates the salvage pickup normally; player Ymir Begin contacts emit
|
|
||||||
`pickup.rejected` with `cargo-capacity`, preserve cargo at `5` items, and leave
|
|
||||||
the pickup live. The final run observed two later player recontacts with two
|
|
||||||
distinct `ymir-fact:` identities and no player collection event. The generic
|
|
||||||
witness correlates feedback through advertised SoA `SourceIndex` and feedback
|
|
||||||
`TargetEntityIndex`; events from other ships are not attributed to the pilot.
|
|
||||||
|
|
||||||
Current released-package evidence is in
|
|
||||||
`artifacts/aetheria-daemon-mapped-body-cold`; despite that retained directory
|
|
||||||
name, `runtime-witness.warm.json` is the current passing run. The warm PlayMode test
|
|
||||||
passed and recorded provider-owned reconciled movement, look, targeting,
|
|
||||||
tractor press, tractor release, contact-gated cargo collection, and action
|
|
||||||
receipts. The fingerprinted April 2021 catalog recovery now restores the
|
|
||||||
distinct common `Longinus`, rare `LonginusX`, and `Djinni` rows plus the missing
|
|
||||||
turn-thruster equipment needed to outfit ordinary hulls. This run generated the
|
|
||||||
player as a common directional-thruster `Longinus`; the other six presented
|
|
||||||
ships were `Djinni`, and no `LonginusX` appeared. The player moved 14.34 world
|
|
||||||
units and completed its authoritative hardware-driven look before combat.
|
|
||||||
The daemon-owned look direction reached the SoA body rotation,
|
|
||||||
and the generic aim presentation rendered at the advertised 50-unit minimum
|
|
||||||
convergence distance. Aetheria derives ambient color from daemon-owned stellar
|
|
||||||
`LightColor` values and advertises `studio2` only as the reflection cubemap; the
|
|
||||||
gravity-fog raymarch owns the visible frame. The released lowerer records flat
|
|
||||||
ambient mode, color `(1.46,1.2556,0.8468)`, ambient-probe energy `4.6354`, and
|
|
||||||
custom reflection texture `studio2`. No skybox asset or key light is advertised,
|
|
||||||
and the generic camera rig records zero live directional intensity. The earlier
|
|
||||||
invented `0.75` light and false skybox authority were deleted rather than retained
|
|
||||||
as material-conversion compensators.
|
|
||||||
Aetheria also advertises the original undocked `ARPG.unity` Third Person Rig:
|
|
||||||
entity-forward perspective follow, `30` unit distance, `60` degree vertical
|
|
||||||
field of view, canonical bottom-origin target screen position `0.64,0.19`, zero position damping, and
|
|
||||||
`1`-`4096` clip planes. Its generic `aim.convergence-point.v1` relation makes
|
|
||||||
the optical axis reach the daemon-published aim point while preserving that
|
|
||||||
entity framing. The generic camera rig lowers both constraints without
|
|
||||||
Aetheria-specific camera code. The distinct `70` unit Docked Rig no longer
|
|
||||||
incorrectly owns the flight view.
|
|
||||||
Aetheria's pilot surface also publishes a transparent cockpit overlay. The
|
|
||||||
released generic UI Toolkit lowerer produced seven native `ProgressBar` elements
|
|
||||||
for hull, shield, capacitor, weapon cooldown, target lock, and target state. Daemon frame,
|
|
||||||
player, and command diagnostics remain available in the document but lower
|
|
||||||
with `display: none` in the pilot presentation. The runnable generic-client
|
|
||||||
launcher mounts the same advertised document and forwards its typed commands;
|
|
||||||
it contains no Aetheria gameplay or client-authored world light.
|
|
||||||
|
|
||||||
The same surface publishes a `field.volume3d` node over four canonical Eve
|
|
||||||
Fields splat layers. Its surface contract contains logical ports such as
|
|
||||||
`surfaceHeight`, `patch`, and `tint`; it contains no Unity `_Nebula*` property
|
|
||||||
names or pass indices. The selected `unity-scene` provider asset variant owns
|
|
||||||
that concrete shader ABI through `unity.volume.*` metadata. The released
|
|
||||||
generic lowerer resolved the provider shader and dither texture, rasterized all
|
|
||||||
four layers, and composited `195` pilot-camera frames through daemon frame `491`.
|
|
||||||
The provider variant advertises the fossil shader's raymarch, temporal-history,
|
|
||||||
and composite passes plus their logical ports. EveUnity allocates and resets the
|
|
||||||
history targets generically; a partial temporal ABI fails closed.
|
|
||||||
|
|
||||||
The two PNGs below are deliberate camera-channel captures, not a composited
|
|
||||||
screen capture, so they prove world/map isolation rather than HUD pixels. The
|
|
||||||
native cockpit proof is the asserted UI Toolkit visual tree recorded as
|
|
||||||
`cockpitProgressCount: 7` in the witness facts. The same run records completed
|
|
||||||
daemon lock progress `1.0`; Unity does not manufacture or smooth that value.
|
|
||||||
|
|
||||||
Camera-channel facts:
|
|
||||||
|
|
||||||
- provider-authored player renderers: `7` active, with no embedded shield renderer;
|
|
||||||
- pilot changed pixels: `217,323`;
|
|
||||||
- pilot average luminance: `0.1322155`;
|
|
||||||
- pilot bright pixels: `101,510`;
|
|
||||||
- map-channel renderers: `10`;
|
|
||||||
- map changed pixels: `3,179`;
|
|
||||||
- native cockpit progress bars: `7`;
|
|
||||||
- daemon tractor power at capture: `1.0` after the held input completed its
|
|
||||||
authored ramp;
|
|
||||||
- daemon tractor power after the advertised release: `0`;
|
|
||||||
- provider tractor particle systems: `1`;
|
|
||||||
- pickup entities before/after destruction and collection: `0` / `0`;
|
|
||||||
- provider `pickup.collected` events: exactly `1`;
|
|
||||||
- collected item and quantity: one canonical `aetheria.item_definition:*` key,
|
|
||||||
quantity `1`;
|
|
||||||
- authoritative cargo quantity before/after: `0` / `1`;
|
|
||||||
- the pilot camera excludes the advertised map layer;
|
|
||||||
- the player prefab's embedded layer-14 map icon contributes exactly `0` pilot
|
|
||||||
pixels;
|
|
||||||
- the map camera renders exactly the advertised map layer.
|
|
||||||
|
|
||||||
The daemon also publishes the current zone's celestial presentation through the
|
|
||||||
same generic entity SoA generation as ordinary world entities. The current warm
|
|
||||||
witness records eleven sun/planet/gas-giant rows and eighteen asteroid rows.
|
|
||||||
Every row resolved a provider asset with enabled native renderers, and seven
|
|
||||||
celestial instances intersected the pilot frustum. These identities are
|
|
||||||
presentation-only and cannot receive gameplay commands. EveUnity contains no
|
|
||||||
Aetheria or celestial branch; release `0.3.55` includes the generic prefab wrapper
|
|
||||||
so semantic instance scale composes with, rather than overwrites, provider-local
|
|
||||||
scale.
|
|
||||||
|
|
||||||
Provider prefab construction now removes the fossil shield renderer embedded in
|
|
||||||
each ship. That renderer's custom shader performs collision-local dither clipping;
|
|
||||||
flattening it to ordinary opaque URP Lit had produced the large black ellipsoids
|
|
||||||
previously mistaken for celestial bodies. Shield state remains daemon truth and
|
|
||||||
shield impacts use the separately advertised receipt-driven effect. Material
|
|
||||||
conversion also preserves source lighting models: the map icon's unlit source
|
|
||||||
now becomes `Universal Render Pipeline/Unlit`, so map glyphs remain readable
|
|
||||||
without resurrecting the false scene light.
|
|
||||||
|
|
||||||
Visual inspection confirms that map glyphs are absent from the pilot frame and
|
|
||||||
present in the map-only frame. This is transport, authority, field-production,
|
|
||||||
and lowering proof, not visual parity: the current pilot capture contains the
|
|
||||||
blue fog field and the provider's magenta-and-gold Longinus hull with no opaque
|
|
||||||
shield ellipsoid. The celestial composition still lacks the fossil capture's
|
|
||||||
readable close body. Release
|
|
||||||
`0.3.57` includes generic lowering for Eve Fields `0.2.3`'s
|
|
||||||
`AnimatedRadialCosine` source alongside `PowerPulse`, dither-scale,
|
|
||||||
temporal-history, and finite-look-at semantics. Aetheria publishes the fossil
|
|
||||||
global animated simplex/cellular/ambient producer stack, body-owned radial wave
|
|
||||||
frequency, separate negative `gravity.height` and positive
|
|
||||||
`fog.surface_height`, and the authored `ARPG.unity` zone-brush extent. The
|
|
||||||
witness records player Y `-101.5035` and camera Y `-84.7369`; Unity does not
|
|
||||||
sample terrain to invent either value. The four rasterized field layers and HDR
|
|
||||||
raymarch/history targets are populated, so the remaining failure is in native
|
|
||||||
presentation/composition or provider art calibration rather than missing state
|
|
||||||
delivery. Color, lighting, patch structure, readable world geometry, and
|
|
||||||
gravity-wave visual parity remain open. EveUnity must not compensate with
|
|
||||||
Aetheria-specific lighting or fog rules.
|
|
||||||
|
|
||||||
The `0.3.57` run also closes two camera-contract omissions. Aetheria advertises
|
|
||||||
the fossil pilot camera's neutral temporal-reprojection values; the released
|
|
||||||
generic lowerer applies URP TAA with `0.99` history blend, `0.1` jitter scale,
|
|
||||||
high quality, and zero sharpening. Fog and Stardust now consume one generic
|
|
||||||
camera-relative viewport-frame resolver rather than projecting the same splat
|
|
||||||
document independently. The live witness fails unless their snapped grid
|
|
||||||
centers are identical; the latest run resolved both to `(-6,-6)` after movement.
|
|
||||||
The provider HLSL now consumes Eve Fields' positive world-axis viewport directly;
|
|
||||||
it no longer retains the two-axis compensation once required by the deleted
|
|
||||||
fossil gravity camera. An asymmetric GPU fixture fails unless positive-world X
|
|
||||||
samples positive texture X, while the existing one-cell remap proof still keeps
|
|
||||||
705 overlapping world particles bit-identical.
|
|
||||||
The capture is still visibly rough and is not a visual-parity claim.
|
|
||||||
|
|
||||||
The fossil Stardust material used a screen-space coverage mask that changes on
|
|
||||||
every rendered frame and depends on TAA to reconstruct stable subpixel stars.
|
|
||||||
The earlier URP port had replaced that with a fixed alpha threshold and
|
|
||||||
additive blending. Aetheria now advertises the provider dither texture,
|
|
||||||
viewport scale, and logical render-frame port; generic EveUnity binds them
|
|
||||||
without knowing their product meaning. The live witness resolves those native
|
|
||||||
ports from provider metadata and checks the actual material instance. The
|
|
||||||
provider bundle source gate requires the temporal clip, overwrite blend, and
|
|
||||||
depth write and rejects `_AlphaClip`.
|
|
||||||
|
|
||||||
Substance is not part of this path; later texture baking belongs in Blender.
|
|
||||||
Aetheria currently bundles ambientCG's 1K `Metal012` color, normal, and
|
|
||||||
metalness maps under CC0. The same pre-generated maps now replace dead
|
|
||||||
Substance archive sub-assets on the brushed aluminium, tinted car paint,
|
|
||||||
cockpit, steel, black-metal, and radiator materials. The released witness
|
|
||||||
resolves textured native URP materials in `8` distinct player-material slots,
|
|
||||||
up from `3`, without adding another texture payload. This is temporary provider
|
|
||||||
art with recorded provenance, not a replacement texture pipeline.
|
|
||||||
|
|
||||||
The tractor proof is structural, not yet a native-look victory lap. The daemon
|
|
||||||
publishes `beam.presentation` and advertises `pilot.scoop` as a
|
|
||||||
`button-hold.v1` value action. The generic client routes scalar `1` on press and
|
|
||||||
scalar `0` on release through the advertised `SetTractorPower` operation. Both
|
|
||||||
commands receive provider-owned reconciled receipts, and the daemon ramps the
|
|
||||||
published power from `1.0` at capture to exactly `0` after release. The provider
|
|
||||||
build removes the fossil's embedded tractor object from player/ship prefabs, so
|
|
||||||
the standalone `beam.presentation` prefab is the only tractor renderer. Its
|
|
||||||
cyan/yellow dotted band is visible in the pilot capture and absent from the map
|
|
||||||
capture.
|
|
||||||
The current `0.3.72` warm gameplay run is retained in
|
|
||||||
`artifacts/aetheria-daemon-0372-warm5`. It begins with no pickup. A
|
|
||||||
daemon-authoritative shot destroys the deterministic salvage target and creates
|
|
||||||
one provider-owned pickup carrying a canonical generated cargo item. Held
|
|
||||||
tractor input causes Ymir-owned attraction; collection itself is an
|
|
||||||
authoritative XZ proximity transaction and does not depend on a contact fact.
|
|
||||||
The daemon removes the pickup once, changes cargo from `0` to `1`, and emits one
|
|
||||||
stable `zone:0:pickup:0:collected` feedback event. No client pickup command or
|
|
||||||
Ymir contact receipt owns cargo mutation.
|
|
||||||
|
|
||||||
The same run records 709 URP field-volume composites, 65,536 stateless field
|
|
||||||
particles, and 11 map-channel renderers. The pilot camera excludes every map
|
|
||||||
renderer and the map camera includes them. The serialized consumer renderer now
|
|
||||||
contains `EveUnityRendererFeature`; the configuration utility marks
|
|
||||||
`UniversalRendererData` dirty before saving so batch and editor cameras use the
|
|
||||||
same URP scheduling path. The captures are integration evidence, not a final art
|
|
||||||
grade: the pilot frame remains overexposed and the hull reads nearly black.
|
|
||||||
Effect tuning remains provider art work; the runtime must not compensate with
|
|
||||||
an Aetheria-specific beam.
|
|
||||||
|
|
||||||
Primary artifacts:
|
|
||||||
|
|
||||||
- `results.xml`: one passing PlayMode witness;
|
|
||||||
- `runtime-witness.json`: package, content, receipt, aim, combat, and
|
|
||||||
camera facts;
|
|
||||||
- `aetheria-daemon-world.png`: pilot-camera capture;
|
|
||||||
- `aetheria-daemon-map.png`: map-only capture.
|
|
||||||
|
|
||||||
## Cold delivery status
|
|
||||||
|
|
||||||
Cold CDN transfer is part of the passing readiness gate. The released `0.3.91`
|
|
||||||
run in `artifacts/cold-release-0391` began with zero cached bodies and partials,
|
|
||||||
received only the typed manifest through snapshot state, transferred bounded
|
|
||||||
content chunks through the managed CultMesh content session, and atomically
|
|
||||||
promoted one `56,204,750`-byte body named by SHA-256
|
|
||||||
`f2c8acf938998703799e909cbf0c6e3ed9ad5d6f83b708a0b7b6e76ee33f7160`.
|
|
||||||
It left zero partials and selected `SharedFileMapping` over the verified file.
|
|
||||||
|
|
||||||
The cold-only failure exposed by `0.3.90` was a bootstrap ownership race, not a
|
|
||||||
bulk snapshot transfer: live SoA subscriptions began before the blocking cold
|
|
||||||
asset acquisition, so their retained capability expired before Unity pumped it.
|
|
||||||
`0.3.91` resolves the surface first, completes provider-asset acquisition, and
|
|
||||||
only then starts live subscriptions. A fresh current generation therefore owns
|
|
||||||
the first lowered frame. No retry timer or stale-body reinterpretation exists.
|
|
||||||
|
|
||||||
This proves cold acquisition and local mapped reuse, not provider-to-client
|
|
||||||
network zero-copy, shared-memory delivery, or GPU zero-copy. Managed remote
|
|
||||||
chunks are still copied and fragmented before the transfer owner commits the
|
|
||||||
mapped file; network-body streaming and copy-volume profiling remain open.
|
|
||||||
|
|
@ -9,9 +9,6 @@ importing provider product code into the generic runtime.
|
||||||
|
|
||||||
EveUnity owns Unity scene projection, `GameObject` lifecycle, camera and input
|
EveUnity owns Unity scene projection, `GameObject` lifecycle, camera and input
|
||||||
drivers, asset-provider hooks, package release, runtime tests, and captures.
|
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
|
## Inputs
|
||||||
|
|
||||||
|
|
@ -20,11 +17,7 @@ provider identities and cannot preserve a physical route as gameplay authority.
|
||||||
- `gamecult.eve.provider_advertisement.v1`
|
- `gamecult.eve.provider_advertisement.v1`
|
||||||
- `gamecult.eve.surface.v1`
|
- `gamecult.eve.surface.v1`
|
||||||
- provider asset-manifest documents
|
- 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
|
- provider command receipts
|
||||||
- semantic render-channel inclusion and exclusion policy
|
|
||||||
- Unity input and local presentation assets
|
- Unity input and local presentation assets
|
||||||
|
|
||||||
## Outputs
|
## Outputs
|
||||||
|
|
@ -36,83 +29,23 @@ provider identities and cannot preserve a physical route as gameplay authority.
|
||||||
|
|
||||||
## Derived State
|
## Derived State
|
||||||
|
|
||||||
`ActiveWorld`, scene objects, transforms, markers, camera pose, native
|
`ActiveWorld`, scene objects, transforms, markers, camera pose, asset caches,
|
||||||
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
|
presentation counters, and receipt display state are projections. They are not
|
||||||
world truth. Temporal targets reset when the volume program, node, or render size
|
world truth.
|
||||||
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
|
## Forbidden Writers
|
||||||
|
|
||||||
Unity transforms, input drivers, camera rigs, scene sinks, asset caches, and
|
Unity transforms, input drivers, camera rigs, scene sinks, asset caches, and
|
||||||
pending receipts may not mutate or simulate provider world state. Plugin
|
pending receipts may not mutate or simulate provider world state. Plugin
|
||||||
projection shells may not implement Sai, Norn, or TeX semantics. Portable
|
projection shells may not implement Sai, Norn, or TeX semantics.
|
||||||
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
|
## Shared Paths
|
||||||
|
|
||||||
Initial preparation asks one rendezvous for Verses, selects an advertised
|
Initial connect discovers a Verse endpoint and selects a provider surface by
|
||||||
authority runtime and provider surface by semantic kind, then leases it through
|
semantic kind. Refresh, reconnect, and terminal-receipt reconciliation consume
|
||||||
the explicit Verse/authority-runtime target. The Eve provider ID remains a
|
that provider's surface documents. Movement, focus, target, and action input all
|
||||||
separate surface-owner identity.
|
become `gamecult.eve.command_invocation.v1` through the provider-advertised
|
||||||
Refresh, route rotation, reconnect, and terminal-receipt reconciliation remain
|
command boundary.
|
||||||
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
|
## Cut Line
|
||||||
|
|
||||||
|
|
|
||||||
7
packages/org.gamecult.eve.surface/Directory.Build.props
Normal file
7
packages/org.gamecult.eve.surface/Directory.Build.props
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<BaseOutputPath>$(MSBuildThisFileDirectory)bin~\</BaseOutputPath>
|
||||||
|
<BaseIntermediateOutputPath>$(MSBuildThisFileDirectory)obj~\</BaseIntermediateOutputPath>
|
||||||
|
<MSBuildProjectExtensionsPath>$(MSBuildThisFileDirectory)obj~\</MSBuildProjectExtensionsPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.1</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<AssemblyName>GameCult.Eve.Surface</AssemblyName>
|
||||||
|
<Description>Portable Eve surface, provider, input, plugin, and entity-view contracts.</Description>
|
||||||
|
<CultLibRoot Condition="'$(CultLibRoot)' == ''">$(MSBuildThisFileDirectory)..\..\..\CultLib</CultLibRoot>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="Runtime\EveCultMeshSurfaceSelection.cs" />
|
||||||
|
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Caching\GameCult.Caching.csproj" />
|
||||||
|
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Mesh\GameCult.Mesh.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 6435ff06293834c49aa631e74b51a552
|
guid: 8a186b6d206ffa847aa276e984162512
|
||||||
folderAsset: yes
|
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
userData:
|
userData:
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<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>
|
||||||
|
<CultLibRoot Condition="'$(CultLibRoot)' == ''">$(MSBuildThisFileDirectory)..\..\..\CultLib</CultLibRoot>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Caching\GameCult.Caching.csproj" />
|
||||||
|
<ProjectReference Include="$(CultLibRoot)\src\GameCult.Mesh\GameCult.Mesh.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: c8c45489b6ba44558342ebc5381c12d1
|
guid: ce2d4913d64690c44ae68935014fd79d
|
||||||
ComputeShaderImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
currentAPIMask: 4
|
|
||||||
userData:
|
userData:
|
||||||
assetBundleName:
|
assetBundleName:
|
||||||
assetBundleVariant:
|
assetBundleVariant:
|
||||||
29
packages/org.gamecult.eve.surface/README.md
Normal file
29
packages/org.gamecult.eve.surface/README.md
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
# Eve Surface Contract
|
||||||
|
|
||||||
|
This package defines the retained CultUI surface document that every Eve
|
||||||
|
runtime lowers.
|
||||||
|
|
||||||
|
Components own normal child layout through `Children`, live CultMesh value
|
||||||
|
bindings through `StateBindings`, and nested synced document regions through
|
||||||
|
`EmbeddedDocuments`. An embedded document slot names the child document id,
|
||||||
|
schema, presentation kind, and route hint; renderers resolve that child through
|
||||||
|
CultMesh instead of rebuilding it with runtime-local projection code.
|
||||||
|
|
||||||
|
See `../../docs/surface-contract-v1.md` for the full contract and
|
||||||
|
`../../web/fixtures/cultui-embedded-surface.json` for the parity fixture.
|
||||||
|
Runtime discovery and required feature coverage live in
|
||||||
|
`../../tools/parity/parity-manifest.json`; active GUI runtimes must list
|
||||||
|
`embeddedDocuments` as a supported feature and require the `embedded-surface`
|
||||||
|
fixture.
|
||||||
|
|
||||||
|
Verification entrypoints:
|
||||||
|
|
||||||
|
- Browser/TypeScript: `node --test web\eve-dsl.test.mjs`
|
||||||
|
- Semantic runtime matrix: `powershell -ExecutionPolicy Bypass -File .\scripts\run-parity-harness.ps1`
|
||||||
|
- Flutter GUI contract, when Flutter is installed: `flutter test --plain-name embedded_surface_fixture_contract`
|
||||||
|
- Rust/CultMesh document sync: run `cargo test rust_preserves_cultui_embedded_surface_slots_through_typed_document_sync` from `E:/Projects/CultLib/packages/cultnet-rs`
|
||||||
|
|
||||||
|
The parity manifest is the discovery source for web, Flutter desktop/Linux/Android,
|
||||||
|
iOS/UIKit, Android/Kotlin, Unity UI Toolkit, and Rust contract coverage. Do not
|
||||||
|
add a new renderer claim without adding its nested-surface fixture requirement
|
||||||
|
there.
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 5702faba00994033870c858bb2124d91
|
guid: fe5aa27636bead047b2aecb14fdc59f2
|
||||||
TextScriptImporter:
|
TextScriptImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
userData:
|
userData:
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 63a640b4e19e5764eb030d665a2b7c00
|
guid: 2164f6fea1216214d895e7dc99c46ecf
|
||||||
folderAsset: yes
|
folderAsset: yes
|
||||||
DefaultImporter:
|
DefaultImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
|
|
@ -0,0 +1,144 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GameCult.Caching;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface
|
||||||
|
{
|
||||||
|
[CultDocument("gamecult.eve.asset_catalog", SchemaId)]
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveAssetCatalogDocument
|
||||||
|
{
|
||||||
|
public const string SchemaId = "gamecult.eve.asset_catalog.v1";
|
||||||
|
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EveAssetCatalogDocument(
|
||||||
|
string schema,
|
||||||
|
string providerId,
|
||||||
|
string catalogId,
|
||||||
|
long version,
|
||||||
|
string updatedAtUtc,
|
||||||
|
EveAssetCatalogEntry[] assets)
|
||||||
|
{
|
||||||
|
Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema;
|
||||||
|
ProviderId = providerId ?? "";
|
||||||
|
CatalogId = catalogId ?? "";
|
||||||
|
Version = version;
|
||||||
|
UpdatedAtUtc = updatedAtUtc ?? "";
|
||||||
|
Assets = assets ?? Array.Empty<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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8f53654f7c98d894588127ba001f11b4
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using GameCult.Mesh;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface
|
||||||
|
{
|
||||||
|
public sealed class EveSurfaceRequest
|
||||||
|
{
|
||||||
|
public string ProviderId { get; set; } = "";
|
||||||
|
public string SurfaceId { get; set; } = "";
|
||||||
|
public string SurfaceKind { get; set; } = "";
|
||||||
|
public string LoweringTarget { get; set; } = "";
|
||||||
|
public bool RequireActive { get; set; } = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class EveSurfaceSelection
|
||||||
|
{
|
||||||
|
internal EveSurfaceSelection(
|
||||||
|
EveProviderAdvertisementDocument provider,
|
||||||
|
EveAdvertisedSurface advertisement,
|
||||||
|
CultMeshDocumentHandle<EveSurfaceDocument> surface,
|
||||||
|
IReadOnlyList<EvePluginAdvertisementDocument> plugins)
|
||||||
|
{
|
||||||
|
Provider = provider;
|
||||||
|
Advertisement = advertisement;
|
||||||
|
Surface = surface;
|
||||||
|
Plugins = plugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EveProviderAdvertisementDocument Provider { get; }
|
||||||
|
public EveAdvertisedSurface Advertisement { get; }
|
||||||
|
public CultMeshDocumentHandle<EveSurfaceDocument> Surface { get; }
|
||||||
|
public IReadOnlyList<EvePluginAdvertisementDocument> Plugins { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class EveCultMeshSurfaceSelection
|
||||||
|
{
|
||||||
|
public static async Task<EveSurfaceSelection> SurfaceAsync(
|
||||||
|
this CultMeshClient mesh,
|
||||||
|
string endpointId,
|
||||||
|
EveSurfaceRequest? request = null,
|
||||||
|
CancellationToken cancellationToken = default,
|
||||||
|
Action<string>? observeStage = null)
|
||||||
|
{
|
||||||
|
if (mesh == null) throw new ArgumentNullException(nameof(mesh));
|
||||||
|
var resolved = request ?? new EveSurfaceRequest();
|
||||||
|
IReadOnlyList<EveProviderAdvertisementDocument> providerDocuments;
|
||||||
|
if (string.IsNullOrWhiteSpace(resolved.ProviderId))
|
||||||
|
{
|
||||||
|
observeStage?.Invoke("provider-collection-opening");
|
||||||
|
var providers = await mesh.CollectionAsync<EveProviderAdvertisementDocument>(endpointId, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
observeStage?.Invoke("provider-collection-ready");
|
||||||
|
providerDocuments = await providers.LatestAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
observeStage?.Invoke("provider-document-opening");
|
||||||
|
var provider = await mesh.DocumentAsync<EveProviderAdvertisementDocument>(
|
||||||
|
endpointId,
|
||||||
|
ProviderAdvertisementRecordRef(resolved.ProviderId),
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
observeStage?.Invoke("provider-document-ready");
|
||||||
|
providerDocuments = new[] { await provider.LatestAsync().ConfigureAwait(false) };
|
||||||
|
}
|
||||||
|
observeStage?.Invoke("provider-snapshot-ready");
|
||||||
|
var match = Select(providerDocuments, resolved);
|
||||||
|
IReadOnlyList<EvePluginAdvertisementDocument> plugins = Array.Empty<EvePluginAdvertisementDocument>();
|
||||||
|
if (match.Surface.RequiresPlugins.Count > 0)
|
||||||
|
{
|
||||||
|
var advertisements = await mesh.CollectionAsync<EvePluginAdvertisementDocument>(endpointId, cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
plugins = ResolvePlugins(match.Surface, await advertisements.LatestAsync().ConfigureAwait(false));
|
||||||
|
observeStage?.Invoke("plugin-snapshot-ready");
|
||||||
|
}
|
||||||
|
observeStage?.Invoke("surface-document-opening");
|
||||||
|
var surface = await mesh.DocumentAsync<EveSurfaceDocument>(
|
||||||
|
endpointId,
|
||||||
|
match.Surface.RecordRef,
|
||||||
|
cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
observeStage?.Invoke("surface-document-ready");
|
||||||
|
return new EveSurfaceSelection(match.Provider, match.Surface, surface, plugins);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<EvePluginAdvertisementDocument> ResolvePlugins(
|
||||||
|
EveAdvertisedSurface surface,
|
||||||
|
IEnumerable<EvePluginAdvertisementDocument> advertisements)
|
||||||
|
{
|
||||||
|
if (surface == null) throw new ArgumentNullException(nameof(surface));
|
||||||
|
if (advertisements == null) throw new ArgumentNullException(nameof(advertisements));
|
||||||
|
var available = advertisements
|
||||||
|
.GroupBy(plugin => plugin.PluginId, StringComparer.Ordinal)
|
||||||
|
.ToDictionary(group => group.Key, group => group.OrderByDescending(plugin => plugin.Version, StringComparer.Ordinal).First(), StringComparer.Ordinal);
|
||||||
|
var selected = new List<EvePluginAdvertisementDocument>();
|
||||||
|
foreach (var requirement in surface.RequiresPlugins)
|
||||||
|
{
|
||||||
|
if (!available.TryGetValue(requirement.PluginId, out var plugin))
|
||||||
|
{
|
||||||
|
if (string.Equals(requirement.Availability, "required", StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Eve surface '{surface.SurfaceId}' requires unavailable plugin '{requirement.PluginId}'.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims = plugin.Schemas.Concat(plugin.ComponentKinds).Concat(plugin.Commands)
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
var missing = requirement.RequiredCapabilities.Where(capability => !claims.Contains(capability)).ToArray();
|
||||||
|
if (missing.Length > 0)
|
||||||
|
{
|
||||||
|
if (string.Equals(requirement.Availability, "required", StringComparison.OrdinalIgnoreCase))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Eve plugin '{plugin.PluginId}' does not advertise required capabilities: {string.Join(", ", missing)}.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
selected.Add(plugin);
|
||||||
|
}
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (EveProviderAdvertisementDocument Provider, EveAdvertisedSurface Surface) Select(
|
||||||
|
IEnumerable<EveProviderAdvertisementDocument> providers,
|
||||||
|
EveSurfaceRequest request)
|
||||||
|
{
|
||||||
|
if (providers == null) throw new ArgumentNullException(nameof(providers));
|
||||||
|
if (request == null) throw new ArgumentNullException(nameof(request));
|
||||||
|
var match = providers
|
||||||
|
.Where(provider => Matches(request.ProviderId, provider.ProviderId))
|
||||||
|
.SelectMany(provider => provider.Surfaces.Select(surface => new { Provider = provider, Surface = surface }))
|
||||||
|
.Where(candidate => Matches(request.SurfaceId, candidate.Surface.SurfaceId))
|
||||||
|
.Where(candidate => Matches(request.SurfaceKind, candidate.Surface.SurfaceKind))
|
||||||
|
.Where(candidate => !request.RequireActive || string.Equals(candidate.Surface.Status, "active", StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Where(candidate => string.IsNullOrWhiteSpace(request.LoweringTarget) ||
|
||||||
|
candidate.Surface.WorldInteraction?.LoweringTargets.Any(target =>
|
||||||
|
string.Equals(target, request.LoweringTarget, StringComparison.Ordinal)) == true)
|
||||||
|
.OrderBy(candidate => candidate.Provider.ProviderId, StringComparer.Ordinal)
|
||||||
|
.ThenBy(candidate => candidate.Surface.SurfaceId, StringComparer.Ordinal)
|
||||||
|
.FirstOrDefault();
|
||||||
|
if (match == null)
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"No advertised Eve surface matched provider='{request.ProviderId}', surface='{request.SurfaceId}', " +
|
||||||
|
$"kind='{request.SurfaceKind}', loweringTarget='{request.LoweringTarget}'.");
|
||||||
|
if (!string.Equals(match.Surface.Transport, "cultmesh-record", StringComparison.Ordinal))
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Eve surface '{match.Surface.SurfaceId}' uses unsupported transport '{match.Surface.Transport}'.");
|
||||||
|
if (string.IsNullOrWhiteSpace(match.Surface.RecordRef))
|
||||||
|
throw new InvalidOperationException($"Eve surface '{match.Surface.SurfaceId}' does not advertise a record reference.");
|
||||||
|
return (match.Provider, match.Surface);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool Matches(string expected, string actual) =>
|
||||||
|
string.IsNullOrWhiteSpace(expected) || string.Equals(expected, actual, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
public static string ProviderAdvertisementRecordRef(string providerId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(providerId))
|
||||||
|
throw new ArgumentException("Provider id must be non-empty.", nameof(providerId));
|
||||||
|
return $"eve:provider:{providerId.Trim()}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: b3c40df8a2a14d7db2f45f7018591fd3
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
using System;
|
||||||
|
using GameCult.Caching;
|
||||||
|
using MessagePack;
|
||||||
|
using GameCult.Mesh;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface
|
||||||
|
{
|
||||||
|
[CultDocument("gamecult.eve.entity_soa_view", SchemaId)]
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveEntitySoaViewDocument
|
||||||
|
{
|
||||||
|
public const string SchemaId = "gamecult.eve.entity_soa_view.v1";
|
||||||
|
|
||||||
|
[Key(0)] public string Schema { get; set; } = SchemaId;
|
||||||
|
[Key(1)] public string ProviderId { get; set; } = "";
|
||||||
|
[Key(2)] public string ViewId { get; set; } = "";
|
||||||
|
[Key(3)] public long FrameId { get; set; }
|
||||||
|
[Key(4)] public long Generation { get; set; }
|
||||||
|
[Key(5)] public string PublishedAtUtc { get; set; } = "";
|
||||||
|
[Key(6)] public string Backend { get; set; } = "cultcache";
|
||||||
|
[Key(7)] public string SynchronizationMode { get; set; } = "immutable_frame";
|
||||||
|
[Key(8)] public EveEntitySoaBuffer[] Buffers { get; set; } = Array.Empty<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>();
|
||||||
|
[Key(12)] public EveEntitySoaIdentity[] Identities { get; set; } = Array.Empty<EveEntitySoaIdentity>();
|
||||||
|
[Key(13)] public CultMeshBodyDescriptor? Body { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveEntitySoaIdentity
|
||||||
|
{
|
||||||
|
[Key(0)] public int EntityIndex { get; set; } = -1;
|
||||||
|
[Key(1)] public string EntityId { get; set; } = "";
|
||||||
|
[Key(2)] public string Kind { get; set; } = "";
|
||||||
|
[Key(3)] public string Label { get; set; } = "";
|
||||||
|
[Key(4)] public string Faction { get; set; } = "";
|
||||||
|
[Key(5)] public bool Selectable { get; set; } = true;
|
||||||
|
[Key(6)] public bool Controllable { get; set; }
|
||||||
|
[Key(7)] public string AssetRef { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveEntitySoaBuffer
|
||||||
|
{
|
||||||
|
[Key(0)] public string BufferId { get; set; } = "";
|
||||||
|
[Key(1)] public string Backend { get; set; } = "cultcache";
|
||||||
|
[Key(2)] public string Location { get; set; } = "";
|
||||||
|
[Key(3)] public long ByteOffset { get; set; }
|
||||||
|
[Key(4)] public long ByteLength { get; set; }
|
||||||
|
[Key(5)] public long Generation { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveEntitySoaColumn
|
||||||
|
{
|
||||||
|
[Key(0)] public string ColumnId { get; set; } = "";
|
||||||
|
[Key(1)] public string Semantic { get; set; } = "";
|
||||||
|
[Key(2)] public string BufferId { get; set; } = "";
|
||||||
|
[Key(3)] public string ScalarType { get; set; } = "";
|
||||||
|
[Key(4)] public long ByteOffset { get; set; }
|
||||||
|
[Key(5)] public int ElementStride { get; set; }
|
||||||
|
[Key(6)] public int ElementCount { get; set; }
|
||||||
|
[Key(7)] public string Unit { get; set; } = "";
|
||||||
|
[Key(8)] public string CoordinateSpace { get; set; } = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveEntitySoaDirtyRange
|
||||||
|
{
|
||||||
|
[Key(0)] public string ColumnId { get; set; } = "";
|
||||||
|
[Key(1)] public int StartIndex { get; set; }
|
||||||
|
[Key(2)] public int Count { get; set; }
|
||||||
|
[Key(3)] public long Generation { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveEntityRenderGroup
|
||||||
|
{
|
||||||
|
[Key(0)] public int GroupId { get; set; }
|
||||||
|
[Key(1)] public string MeshAssetRef { get; set; } = "";
|
||||||
|
[Key(2)] public string MaterialAssetRef { get; set; } = "";
|
||||||
|
[Key(3)] public int SubMeshIndex { get; set; }
|
||||||
|
[Key(4)] public int Layer { get; set; }
|
||||||
|
[Key(5)] public int InstanceCount { get; set; }
|
||||||
|
[Key(6)] public float DefaultScale { get; set; } = 1f;
|
||||||
|
[Key(7)] public float BoundsCenterX { get; set; }
|
||||||
|
[Key(8)] public float BoundsCenterY { get; set; }
|
||||||
|
[Key(9)] public float BoundsCenterZ { get; set; }
|
||||||
|
[Key(10)] public float BoundsSizeX { get; set; }
|
||||||
|
[Key(11)] public float BoundsSizeY { get; set; }
|
||||||
|
[Key(12)] public float BoundsSizeZ { get; set; }
|
||||||
|
[Key(13)] public string ShadowMode { get; set; } = "on";
|
||||||
|
[Key(14)] public bool ReceiveShadows { get; set; } = true;
|
||||||
|
[Key(15)] public int Lod { get; set; } = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
fileFormatVersion: 2
|
fileFormatVersion: 2
|
||||||
guid: 340385338d304b5aaf069562d4e7b457
|
guid: 8e87e1be783846508efea45787f2d45c
|
||||||
MonoImporter:
|
MonoImporter:
|
||||||
externalObjects: {}
|
externalObjects: {}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GameCult.Caching;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface
|
||||||
|
{
|
||||||
|
[CultDocument("gamecult.eve.input_capability", SchemaId)]
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveInputCapabilityDocument
|
||||||
|
{
|
||||||
|
public const string SchemaId = "gamecult.eve.input_capability.v1";
|
||||||
|
[Key(0)] public string Schema { get; set; } = SchemaId;
|
||||||
|
[Key(1)] public string ProviderId { get; set; } = "";
|
||||||
|
[Key(2)] public string CapabilityId { get; set; } = "";
|
||||||
|
[Key(3)] public long Version { get; set; }
|
||||||
|
[Key(4)] public EveInputActionDocument[] Actions { get; set; } = Array.Empty<EveInputActionDocument>();
|
||||||
|
[Key(5)] public EveInputProfileDocument[] DefaultProfiles { get; set; } = Array.Empty<EveInputProfileDocument>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveInputActionDocument
|
||||||
|
{
|
||||||
|
[Key(0)] public string ActionId { get; set; } = "";
|
||||||
|
[Key(1)] public string Label { get; set; } = "";
|
||||||
|
[Key(2)] public string Operation { get; set; } = "";
|
||||||
|
[Key(3)] public string Context { get; set; } = "pilot";
|
||||||
|
[Key(4)] public string Category { get; set; } = "ship";
|
||||||
|
[Key(5)] public string Availability { get; set; } = "available";
|
||||||
|
[Key(6)] public string SourceRef { get; set; } = "";
|
||||||
|
[Key(7)] public Dictionary<string, string> Payload { get; set; } = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveInputProfileDocument
|
||||||
|
{
|
||||||
|
[Key(0)] public string ProfileId { get; set; } = "";
|
||||||
|
[Key(1)] public string DeviceClass { get; set; } = "";
|
||||||
|
[Key(2)] public EveInputBindingDocument[] Bindings { get; set; } = Array.Empty<EveInputBindingDocument>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveInputBindingDocument
|
||||||
|
{
|
||||||
|
[Key(0)] public string BindingId { get; set; } = "";
|
||||||
|
[Key(1)] public string ActionId { get; set; } = "";
|
||||||
|
[Key(2)] public EveInputGestureDocument Gesture { get; set; } = new EveInputGestureDocument();
|
||||||
|
[Key(3)] public bool ActionBar { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveInputGestureDocument
|
||||||
|
{
|
||||||
|
[Key(0)] public string Kind { get; set; } = "direct";
|
||||||
|
[Key(1)] public string[] Controls { get; set; } = Array.Empty<string>();
|
||||||
|
[Key(2)] public int MaxStepIntervalMs { get; set; } = 650;
|
||||||
|
[Key(3)] public string CompletionControl { get; set; } = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4c297c7dbb64a0f49a7f805e770c5e55
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface
|
||||||
|
{
|
||||||
|
public sealed class EveInputDeviceLayout
|
||||||
|
{
|
||||||
|
public string LayoutId { get; set; } = "";
|
||||||
|
public string DeviceClass { get; set; } = "";
|
||||||
|
public IReadOnlyList<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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8a2a1258551e42a9831a3e4f9483d928
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface
|
||||||
|
{
|
||||||
|
public sealed class EveInputGesture
|
||||||
|
{
|
||||||
|
public string Kind { get; set; } = "direct";
|
||||||
|
public IReadOnlyList<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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: e3c2941be243a754092ac2543d1e6bf7
|
||||||
|
|
@ -0,0 +1,125 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GameCult.Caching;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface
|
||||||
|
{
|
||||||
|
[CultDocument("gamecult.eve.plugin_advertisement", SchemaId)]
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EvePluginAdvertisementDocument
|
||||||
|
{
|
||||||
|
public const string SchemaId = "gamecult.eve.plugin_advertisement.v1";
|
||||||
|
|
||||||
|
public EvePluginAdvertisementDocument(
|
||||||
|
string pluginId,
|
||||||
|
string ownerService,
|
||||||
|
string version,
|
||||||
|
string manifestAddress,
|
||||||
|
EvePluginRuntimeAdvertisement runtime,
|
||||||
|
IReadOnlyList<string> schemas,
|
||||||
|
IReadOnlyList<string> componentKinds,
|
||||||
|
IReadOnlyList<string> commands,
|
||||||
|
IReadOnlyList<string> fixtures)
|
||||||
|
: this(SchemaId, pluginId, ownerService, version, manifestAddress, runtime, schemas, componentKinds, commands, fixtures)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EvePluginAdvertisementDocument(
|
||||||
|
string schema,
|
||||||
|
string pluginId,
|
||||||
|
string ownerService,
|
||||||
|
string version,
|
||||||
|
string manifestAddress,
|
||||||
|
EvePluginRuntimeAdvertisement runtime,
|
||||||
|
IReadOnlyList<string> schemas,
|
||||||
|
IReadOnlyList<string> componentKinds,
|
||||||
|
IReadOnlyList<string> commands,
|
||||||
|
IReadOnlyList<string> fixtures)
|
||||||
|
{
|
||||||
|
Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema;
|
||||||
|
PluginId = pluginId ?? "";
|
||||||
|
OwnerService = ownerService ?? "";
|
||||||
|
Version = version ?? "";
|
||||||
|
ManifestAddress = manifestAddress ?? "";
|
||||||
|
Runtime = runtime ?? new EvePluginRuntimeAdvertisement("", "", Array.Empty<string>(), Array.Empty<string>(), null);
|
||||||
|
Schemas = schemas ?? Array.Empty<string>();
|
||||||
|
ComponentKinds = componentKinds ?? Array.Empty<string>();
|
||||||
|
Commands = commands ?? Array.Empty<string>();
|
||||||
|
Fixtures = fixtures ?? Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)] public string Schema { get; }
|
||||||
|
[Key(1), CultName] public string PluginId { get; }
|
||||||
|
[Key(2)] public string OwnerService { get; }
|
||||||
|
[Key(3)] public string Version { get; }
|
||||||
|
[Key(4)] public string ManifestAddress { get; }
|
||||||
|
[Key(5)] public EvePluginRuntimeAdvertisement Runtime { get; }
|
||||||
|
[Key(6)] public IReadOnlyList<string> Schemas { get; }
|
||||||
|
[Key(7)] public IReadOnlyList<string> ComponentKinds { get; }
|
||||||
|
[Key(8)] public IReadOnlyList<string> Commands { get; }
|
||||||
|
[Key(9)] public IReadOnlyList<string> Fixtures { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EvePluginRuntimeAdvertisement
|
||||||
|
{
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EvePluginRuntimeAdvertisement(
|
||||||
|
string invocationModel,
|
||||||
|
string contract,
|
||||||
|
IReadOnlyList<string> transports,
|
||||||
|
IReadOnlyList<string> authority,
|
||||||
|
EvePluginSidecarAdvertisement? sidecar)
|
||||||
|
{
|
||||||
|
InvocationModel = invocationModel ?? "";
|
||||||
|
Contract = contract ?? "";
|
||||||
|
Transports = transports ?? Array.Empty<string>();
|
||||||
|
Authority = authority ?? Array.Empty<string>();
|
||||||
|
Sidecar = sidecar;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)] public string InvocationModel { get; }
|
||||||
|
[Key(1)] public string Contract { get; }
|
||||||
|
[Key(2)] public IReadOnlyList<string> Transports { get; }
|
||||||
|
[Key(3)] public IReadOnlyList<string> Authority { get; }
|
||||||
|
[Key(4)] public EvePluginSidecarAdvertisement? Sidecar { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EvePluginSidecarAdvertisement
|
||||||
|
{
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EvePluginSidecarAdvertisement(
|
||||||
|
string processKind,
|
||||||
|
string protocol,
|
||||||
|
string requestSchema,
|
||||||
|
string responseSchema,
|
||||||
|
IReadOnlyList<string> operations,
|
||||||
|
string commandEnvelope,
|
||||||
|
string receiptSchema,
|
||||||
|
string stateAuthority)
|
||||||
|
{
|
||||||
|
ProcessKind = processKind ?? "";
|
||||||
|
Protocol = protocol ?? "";
|
||||||
|
RequestSchema = requestSchema ?? "";
|
||||||
|
ResponseSchema = responseSchema ?? "";
|
||||||
|
Operations = operations ?? Array.Empty<string>();
|
||||||
|
CommandEnvelope = commandEnvelope ?? "";
|
||||||
|
ReceiptSchema = receiptSchema ?? "";
|
||||||
|
StateAuthority = stateAuthority ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)] public string ProcessKind { get; }
|
||||||
|
[Key(1)] public string Protocol { get; }
|
||||||
|
[Key(2)] public string RequestSchema { get; }
|
||||||
|
[Key(3)] public string ResponseSchema { get; }
|
||||||
|
[Key(4)] public IReadOnlyList<string> Operations { get; }
|
||||||
|
[Key(5)] public string CommandEnvelope { get; }
|
||||||
|
[Key(6)] public string ReceiptSchema { get; }
|
||||||
|
[Key(7)] public string StateAuthority { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 7a5b91c7defe47d2a38940a460aa59c5
|
||||||
|
|
@ -0,0 +1,289 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GameCult.Caching;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface
|
||||||
|
{
|
||||||
|
[CultDocument("gamecult.eve.provider_advertisement", SchemaId)]
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveProviderAdvertisementDocument
|
||||||
|
{
|
||||||
|
public const string SchemaId = "gamecult.eve.provider_advertisement.v1";
|
||||||
|
|
||||||
|
public EveProviderAdvertisementDocument(
|
||||||
|
string providerId,
|
||||||
|
string serviceId,
|
||||||
|
string verseId,
|
||||||
|
string title,
|
||||||
|
string kind,
|
||||||
|
string cultMeshAddress,
|
||||||
|
string updatedAtUtc,
|
||||||
|
EveProviderFreshness freshness,
|
||||||
|
IReadOnlyList<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,
|
||||||
|
IReadOnlyList<EvePluginRequirement>? requiresPlugins = null)
|
||||||
|
{
|
||||||
|
SurfaceId = surfaceId ?? "";
|
||||||
|
Schema = schema ?? "";
|
||||||
|
RecordRef = recordRef ?? "";
|
||||||
|
Transport = transport ?? "";
|
||||||
|
Status = status ?? "";
|
||||||
|
SurfaceKind = surfaceKind ?? "";
|
||||||
|
WorldInteraction = worldInteraction;
|
||||||
|
RequiresPlugins = requiresPlugins ?? Array.Empty<EvePluginRequirement>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)] public string SurfaceId { get; }
|
||||||
|
[Key(1)] public string Schema { get; }
|
||||||
|
[Key(2)] public string RecordRef { get; }
|
||||||
|
[Key(3)] public string Transport { get; }
|
||||||
|
[Key(4)] public string Status { get; }
|
||||||
|
[Key(5)] public string SurfaceKind { get; }
|
||||||
|
[Key(6)] public EveWorldInteractionAdvertisement? WorldInteraction { get; }
|
||||||
|
[Key(7)] public IReadOnlyList<EvePluginRequirement> RequiresPlugins { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EvePluginRequirement
|
||||||
|
{
|
||||||
|
[SerializationConstructor]
|
||||||
|
public EvePluginRequirement(
|
||||||
|
string pluginId,
|
||||||
|
string versionRange,
|
||||||
|
string availability,
|
||||||
|
IReadOnlyList<string> requiredCapabilities,
|
||||||
|
IReadOnlyList<string> optionalCapabilities)
|
||||||
|
{
|
||||||
|
PluginId = pluginId ?? "";
|
||||||
|
VersionRange = versionRange ?? "";
|
||||||
|
Availability = string.IsNullOrWhiteSpace(availability) ? "required" : availability;
|
||||||
|
RequiredCapabilities = requiredCapabilities ?? Array.Empty<string>();
|
||||||
|
OptionalCapabilities = optionalCapabilities ?? Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Key(0)] public string PluginId { get; }
|
||||||
|
[Key(1)] public string VersionRange { get; }
|
||||||
|
[Key(2)] public string Availability { get; }
|
||||||
|
[Key(3)] public IReadOnlyList<string> RequiredCapabilities { get; }
|
||||||
|
[Key(4)] public IReadOnlyList<string> OptionalCapabilities { 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f7ea0cd07c4d5da4099b5acf8d44ca2e
|
||||||
874
packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs
Normal file
874
packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs
Normal file
|
|
@ -0,0 +1,874 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 97ac7f757692aef4db64d75d0df6fe51
|
||||||
414
packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs
Normal file
414
packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs
Normal file
|
|
@ -0,0 +1,414 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using GameCult.Caching;
|
||||||
|
using GameCult.Mesh;
|
||||||
|
using MessagePack;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace GameCult.Eve.Surface
|
||||||
|
{
|
||||||
|
[CultDocument("gamecult.eve.surface", "gamecult.eve.surface.v1")]
|
||||||
|
[MessagePackObject]
|
||||||
|
public sealed class EveSurfaceDocument
|
||||||
|
{
|
||||||
|
public const string DefaultType = "surface-state";
|
||||||
|
public const string SchemaId = "gamecult.eve.surface.v1";
|
||||||
|
|
||||||
|
public EveSurfaceDocument(
|
||||||
|
string providerId,
|
||||||
|
string providerKind,
|
||||||
|
string title,
|
||||||
|
long version,
|
||||||
|
string updatedAtUtc,
|
||||||
|
EveSurfaceTree surface,
|
||||||
|
IReadOnlyList<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 ?? "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 6516c42104b235f42b15efeb4639bb73
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "GameCult.Eve.Surface",
|
||||||
|
"rootNamespace": "GameCult.Eve.Surface",
|
||||||
|
"references": [
|
||||||
|
"GameCult.Mesh"
|
||||||
|
],
|
||||||
|
"includePlatforms": [],
|
||||||
|
"excludePlatforms": [],
|
||||||
|
"allowUnsafeCode": false,
|
||||||
|
"overrideReferences": true,
|
||||||
|
"precompiledReferences": [
|
||||||
|
"GameCult.Caching.dll",
|
||||||
|
"GameCult.Mesh.dll",
|
||||||
|
"MessagePack.Annotations.dll"
|
||||||
|
],
|
||||||
|
"autoReferenced": true,
|
||||||
|
"defineConstraints": [],
|
||||||
|
"versionDefines": [],
|
||||||
|
"noEngineReferences": true
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a9004278c14816345941af2f2ce673ca
|
||||||
|
AssemblyDefinitionImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
18
packages/org.gamecult.eve.surface/package.json
Normal file
18
packages/org.gamecult.eve.surface/package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"name": "org.gamecult.eve.surface",
|
||||||
|
"displayName": "Eve Surface Contract",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"unity": "2021.3",
|
||||||
|
"description": "Unity-compatible DTOs for gamecult.eve.surface.v1 documents.",
|
||||||
|
"dependencies": {
|
||||||
|
"org.gamecult.cultlib": "1.0.6"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"Eve",
|
||||||
|
"CultUI",
|
||||||
|
"CultMesh",
|
||||||
|
"Surface"
|
||||||
|
],
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"category": "Scripting"
|
||||||
|
}
|
||||||
7
packages/org.gamecult.eve.surface/package.json.meta
Normal file
7
packages/org.gamecult.eve.surface/package.json.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 49f1738bd94075e489c82190cf660b1d
|
||||||
|
PackageManifestImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
|
@ -5,127 +5,22 @@ interactive worlds.
|
||||||
|
|
||||||
Providers implement the surface-document, asset-manifest, command-sink, and
|
Providers implement the surface-document, asset-manifest, command-sink, and
|
||||||
receipt-source ports. `EveUnityPlayableWorldClientBootstrap` wires those ports
|
receipt-source ports. `EveUnityPlayableWorldClientBootstrap` wires those ports
|
||||||
to the generic scene host, input driver, camera rig, `GameObject` sink, and UI
|
to the generic scene host, input driver, camera rig, and `GameObject` sink.
|
||||||
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
|
`EveUnityCultMeshPlayableWorldProvider` is the runtime composition root for a
|
||||||
networked client. Give it one Verse rendezvous endpoint and it discovers an
|
networked client. Give it one Verse rendezvous endpoint and it discovers an
|
||||||
advertised `interactive-world`, constructs the generic CultMesh transport, and
|
advertised `interactive-world`, constructs the generic CultMesh transport, and
|
||||||
provides every bootstrap port. Provider, surface, and Verse IDs are optional
|
provides every bootstrap port. Provider, surface, and Verse IDs are optional
|
||||||
selection filters; they are not product knowledge required by the client. The
|
selection filters; they are not product knowledge required by the client.
|
||||||
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.
|
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.
|
Retained provider feedback is lowered without becoming Unity authority.
|
||||||
`EveUnityFeedbackPresenter` emits each new `feedback.event` identity once, and
|
`EveUnityFeedbackPresenter` emits each new `feedback.event` identity once, and
|
||||||
`EveUnityShotReceiptPresenter` does the same for `shot.receipt` trajectories.
|
`EveUnityShotReceiptPresenter` does the same for `shot.receipt` trajectories.
|
||||||
The generic `EveUnityShotTrajectoryRenderer` renders provider-authored origin,
|
The generic `EveUnityShotTrajectoryRenderer` renders provider-authored origin,
|
||||||
endpoint, duration, and hit outcome, then expires the visual locally. It never
|
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.
|
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
|
The reusable `LightningCompute` renderer and its shader/material bundle live in
|
||||||
this runtime package with their original Unity GUIDs. Provider asset bundles may
|
this runtime package with their original Unity GUIDs. Provider asset bundles may
|
||||||
therefore reference the effect without importing Aetheria gameplay code.
|
therefore reference the effect without importing Aetheria gameplay code.
|
||||||
|
|
@ -134,55 +29,3 @@ therefore reference the effect without importing Aetheria gameplay code.
|
||||||
`org.gamecult.eve.plugin-fields` contracts. `EveFieldsSplatRasterizer` owns the
|
`org.gamecult.eve.plugin-fields` contracts. `EveFieldsSplatRasterizer` owns the
|
||||||
Unity structured-buffer and RenderTexture lowering without importing provider
|
Unity structured-buffer and RenderTexture lowering without importing provider
|
||||||
document classes.
|
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.
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
|
|
||||||
[assembly: InternalsVisibleTo("GameCult.Eve.UnityScene.Tests")]
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 68e2f1a625b94fc39dcc44d33e0cf318
|
|
||||||
|
|
@ -1,296 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using GameCult.Eve.Surface;
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace GameCult.Eve.UnityScene
|
|
||||||
{
|
|
||||||
/// <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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 61d76e98f2054d86bf8e7480d4411c39
|
|
||||||
timeCreated: 1787039650
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 7f7cb06716fd4f319ade147a69acb74a
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,17 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Stopwatch = System.Diagnostics.Stopwatch;
|
|
||||||
using GameCult.Eve.Surface;
|
using GameCult.Eve.Surface;
|
||||||
using GameCult.Eve.PluginFields;
|
|
||||||
using GameCult.Eve.UnityScene.Fields;
|
|
||||||
using GameCult.Mesh;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.Serialization;
|
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
|
|
@ -25,39 +17,28 @@ namespace GameCult.Eve.UnityScene
|
||||||
IEveUnitySceneCommandReceiptSource,
|
IEveUnitySceneCommandReceiptSource,
|
||||||
IEveUnityProviderRefreshSource,
|
IEveUnityProviderRefreshSource,
|
||||||
IEveUnityEntitySoaViewDocumentSource,
|
IEveUnityEntitySoaViewDocumentSource,
|
||||||
IEveUnityFieldsSplatsDocumentSource,
|
IEveUnityInputCapabilityDocumentSource,
|
||||||
IEveUnityCameraRenderPolicySource,
|
IEveUnityCameraRenderPolicySource,
|
||||||
IEveUnityNativeAssetProvider,
|
IEveUnityNativeAssetProvider
|
||||||
IEveUnityNativeAssetMetadataProvider,
|
|
||||||
IEveUnityInputCapabilitySource,
|
|
||||||
IEveUnityNavigableProvider
|
|
||||||
{
|
{
|
||||||
[SerializeField] private string rendezvousEndpoint = "";
|
[SerializeField] private string rendezvousEndpoint = "";
|
||||||
[SerializeField] private string providerFilter = "";
|
[SerializeField] private string providerFilter = "";
|
||||||
[SerializeField] private string surfaceFilter = "";
|
[SerializeField] private string surfaceFilter = "";
|
||||||
[SerializeField] private string verseFilter = "";
|
[SerializeField] private string verseFilter = "";
|
||||||
[SerializeField] private string surfaceKind = "interactive-world";
|
[SerializeField] private string surfaceKind = "interactive-world";
|
||||||
[FormerlySerializedAs("replicaPath")]
|
[SerializeField] private string replicaPath = "";
|
||||||
[SerializeField] private string cacheDirectory = "";
|
|
||||||
[SerializeField] private string runtimeId = "eve-unity";
|
[SerializeField] private string runtimeId = "eve-unity";
|
||||||
private CultMeshAuthorityTrustPolicy _authorityTrust = new CultMeshAuthorityTrustPolicy(
|
|
||||||
CultMeshAuthorityTrustMode.AuthenticatedRemote);
|
|
||||||
private CultMeshAuthorityTrustPolicy _navigationAuthorityTrust = new CultMeshAuthorityTrustPolicy(
|
|
||||||
CultMeshAuthorityTrustMode.AuthenticatedRemote);
|
|
||||||
|
|
||||||
private EveUnityCultMeshLiveProviderTransport? _transport;
|
private EveUnityCultMeshLiveProviderTransport? _transport;
|
||||||
private EveUnitySceneLiveProviderBridge? _bridge;
|
private EveUnitySceneLiveProviderBridge? _bridge;
|
||||||
private PreviousProvider? _previousProvider;
|
private Task<EveUnityCultMeshProviderSelection>? _discovery;
|
||||||
private bool _navigationRouteCommitted;
|
private readonly ConcurrentQueue<EveEntitySoaViewDocument> _pendingEntityViews = new ConcurrentQueue<EveEntitySoaViewDocument>();
|
||||||
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 event Action<EveEntitySoaViewDocument, ICultMeshBodyReadLease>? EntityViewAvailable;
|
public EveEntitySoaViewDocument? CurrentEntityView { get; private set; }
|
||||||
|
|
||||||
public event Action<EveFieldsSplatsDocument>? FieldsSplatsAvailable;
|
public EveInputCapabilityDocument? CurrentInputCapability => _transport?.CurrentInputCapability;
|
||||||
|
|
||||||
|
public event Action<EveEntitySoaViewDocument>? EntityViewAvailable;
|
||||||
|
|
||||||
public EveUnityCultMeshProviderSelection? Selection { get; private set; }
|
public EveUnityCultMeshProviderSelection? Selection { get; private set; }
|
||||||
|
|
||||||
|
|
@ -65,541 +46,159 @@ namespace GameCult.Eve.UnityScene
|
||||||
|
|
||||||
public EveUnitySceneProviderSurfaceDocument CurrentDocument => Bridge.CurrentSurfaceDocument;
|
public EveUnitySceneProviderSurfaceDocument CurrentDocument => Bridge.CurrentSurfaceDocument;
|
||||||
|
|
||||||
public EveInputCapabilityDocument CurrentInputCapability
|
public event Action<EveUnitySceneProviderSurfaceDocument>? DocumentAvailable
|
||||||
{
|
{
|
||||||
get
|
add { Bridge.DocumentAvailable += value; }
|
||||||
{
|
remove { Bridge.DocumentAvailable -= value; }
|
||||||
RequirePrepared();
|
|
||||||
return _transport!.CurrentInputCapability;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public CultMeshBodyTransportKind? CurrentAssetBodyTransportKind
|
public event Action<EveUnitySceneCommandReceipt>? ReceiptAvailable
|
||||||
{
|
{
|
||||||
get
|
add { Bridge.ReceiptAvailable += value; }
|
||||||
{
|
remove { Bridge.ReceiptAvailable -= value; }
|
||||||
RequirePrepared();
|
|
||||||
return _transport!.CurrentAssetBodyTransportKind;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public event Action<EveUnitySceneProviderSurfaceDocument>? DocumentAvailable;
|
|
||||||
|
|
||||||
public event Action<EveUnitySceneCommandReceipt>? ReceiptAvailable;
|
|
||||||
|
|
||||||
public void Configure(
|
public void Configure(
|
||||||
string endpoint,
|
string endpoint,
|
||||||
string localCacheDirectory = "",
|
string replicaStorePath = "",
|
||||||
string providerId = "",
|
string providerId = "",
|
||||||
string surfaceId = "",
|
string surfaceId = "",
|
||||||
string verseId = "",
|
string verseId = "",
|
||||||
string requiredSurfaceKind = "interactive-world",
|
string requiredSurfaceKind = "interactive-world",
|
||||||
string clientRuntimeId = "eve-unity",
|
string clientRuntimeId = "eve-unity")
|
||||||
CultMeshAuthorityTrustPolicy? authorityTrust = null,
|
|
||||||
CultMeshAuthorityTrustPolicy? navigationAuthorityTrust = null)
|
|
||||||
{
|
{
|
||||||
if (_bridge != null || _preparation != null)
|
if (_bridge != null)
|
||||||
throw new InvalidOperationException("Disconnect the active provider before changing discovery configuration.");
|
throw new InvalidOperationException("Disconnect the active provider before changing discovery configuration.");
|
||||||
|
|
||||||
rendezvousEndpoint = endpoint ?? "";
|
rendezvousEndpoint = endpoint ?? "";
|
||||||
cacheDirectory = localCacheDirectory ?? "";
|
replicaPath = replicaStorePath ?? "";
|
||||||
providerFilter = providerId ?? "";
|
providerFilter = providerId ?? "";
|
||||||
surfaceFilter = surfaceId ?? "";
|
surfaceFilter = surfaceId ?? "";
|
||||||
verseFilter = verseId ?? "";
|
verseFilter = verseId ?? "";
|
||||||
surfaceKind = string.IsNullOrWhiteSpace(requiredSurfaceKind) ? "interactive-world" : requiredSurfaceKind;
|
surfaceKind = string.IsNullOrWhiteSpace(requiredSurfaceKind) ? "interactive-world" : requiredSurfaceKind;
|
||||||
runtimeId = string.IsNullOrWhiteSpace(clientRuntimeId) ? "eve-unity" : clientRuntimeId;
|
runtimeId = string.IsNullOrWhiteSpace(clientRuntimeId) ? "eve-unity" : clientRuntimeId;
|
||||||
_authorityTrust = authorityTrust ?? new CultMeshAuthorityTrustPolicy(
|
|
||||||
CultMeshAuthorityTrustMode.AuthenticatedRemote);
|
|
||||||
_navigationAuthorityTrust = navigationAuthorityTrust ?? _authorityTrust;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Connect()
|
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();
|
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()
|
public void Disconnect()
|
||||||
{
|
{
|
||||||
if (_connectionLeaseCount <= 0)
|
_bridge?.Disconnect();
|
||||||
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()
|
public void Refresh()
|
||||||
{
|
{
|
||||||
var activeBridge = _navigationRouteCommitted
|
if (Bridge.IsConnected)
|
||||||
? Bridge
|
Bridge.Refresh();
|
||||||
: _previousProvider?.Bridge ?? Bridge;
|
|
||||||
if (activeBridge.IsConnected)
|
|
||||||
activeBridge.Refresh();
|
|
||||||
else
|
else
|
||||||
activeBridge.Connect();
|
Bridge.Connect();
|
||||||
|
_transport?.PumpLiveEvents();
|
||||||
|
DrainEntityViews();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Submit(EveSurfaceCommandRequest request)
|
public void Submit(EveSurfaceCommandRequest request)
|
||||||
{
|
{
|
||||||
// Navigation preparation is not a command-routing commit. The mounted
|
Bridge.Submit(request);
|
||||||
// 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)
|
public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset)
|
||||||
{
|
{
|
||||||
RequirePrepared();
|
EnsureTransport();
|
||||||
return _transport!.ResolvePrefab(asset);
|
return _transport!.ResolvePrefab(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
public UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType)
|
public UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType)
|
||||||
{
|
{
|
||||||
RequirePrepared();
|
EnsureTransport();
|
||||||
return _transport!.ResolveAsset(asset, assetType);
|
return _transport!.ResolveAsset(asset, assetType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryResolveAssetMetadata(
|
public bool TryGetCameraCullingMask(string viewId, out int cullingMask)
|
||||||
EveUnityPlayableWorldAssetBinding asset,
|
|
||||||
out IReadOnlyDictionary<string, string> metadata)
|
|
||||||
{
|
{
|
||||||
RequirePrepared();
|
EnsureTransport();
|
||||||
return _transport!.TryResolveAssetMetadata(asset, out metadata);
|
return _transport!.TryGetCameraCullingMask(viewId, out cullingMask);
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetRenderChannelLayer(string channel, out int layer)
|
|
||||||
{
|
|
||||||
RequirePrepared();
|
|
||||||
return _transport!.TryGetRenderChannelLayer(channel, out layer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDestroy()
|
private void OnDestroy()
|
||||||
{
|
{
|
||||||
ReleaseTransport();
|
var transportOwnedConnection = _transport != null;
|
||||||
}
|
|
||||||
|
|
||||||
private void ReleaseTransport()
|
|
||||||
{
|
|
||||||
_preparationLifetime?.Cancel();
|
|
||||||
if (_bridge != null)
|
|
||||||
{
|
|
||||||
_bridge.DocumentAvailable -= ForwardDocument;
|
|
||||||
_bridge.ReceiptAvailable -= ForwardReceipt;
|
|
||||||
}
|
|
||||||
_bridge?.Dispose();
|
_bridge?.Dispose();
|
||||||
_transport?.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;
|
_bridge = null;
|
||||||
_transport = null;
|
_transport = null;
|
||||||
_preparation = null;
|
|
||||||
_preparationLifetime?.Dispose();
|
|
||||||
_preparationLifetime = null;
|
|
||||||
Selection = null;
|
Selection = null;
|
||||||
|
if (!transportOwnedConnection && _discovery?.Status == TaskStatus.RanToCompletion)
|
||||||
|
_discovery.Result.Mesh.Dispose();
|
||||||
|
_discovery = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
private void Update()
|
||||||
{
|
{
|
||||||
_previousProvider?.Transport?.PumpLiveEvents();
|
CompleteDiscoveryOnMainThread();
|
||||||
_transport?.PumpLiveEvents();
|
_transport?.PumpLiveEvents();
|
||||||
EveFieldsSplatsDocument? latestFields = null;
|
DrainEntityViews();
|
||||||
while (_pendingFields.TryDequeue(out var fields))
|
}
|
||||||
latestFields = fields;
|
|
||||||
if (latestFields != null)
|
private void DrainEntityViews()
|
||||||
FieldsSplatsAvailable?.Invoke(latestFields);
|
{
|
||||||
EntityViewLease? latest = null;
|
EveEntitySoaViewDocument? latest = null;
|
||||||
while (_pendingEntityViews.TryDequeue(out var next))
|
while (_pendingEntityViews.TryDequeue(out var next)) latest = next;
|
||||||
{
|
|
||||||
latest?.Lease.Dispose();
|
|
||||||
latest = next;
|
|
||||||
}
|
|
||||||
if (latest == null) return;
|
if (latest == null) return;
|
||||||
var handler = EntityViewAvailable;
|
CurrentEntityView = latest;
|
||||||
if (handler == null) latest.Lease.Dispose();
|
EntityViewAvailable?.Invoke(latest);
|
||||||
else handler(latest.Document, latest.Lease);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private EveUnitySceneLiveProviderBridge Bridge
|
private EveUnitySceneLiveProviderBridge Bridge
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
RequirePrepared();
|
EnsureTransport();
|
||||||
return _bridge!;
|
return _bridge!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PrepareTransportAsync(CancellationToken cancellationToken)
|
private void EnsureTransport()
|
||||||
{
|
{
|
||||||
var elapsed = Stopwatch.StartNew();
|
|
||||||
if (_transport != null)
|
if (_transport != null)
|
||||||
return;
|
return;
|
||||||
if (string.IsNullOrWhiteSpace(rendezvousEndpoint))
|
if (string.IsNullOrWhiteSpace(rendezvousEndpoint))
|
||||||
throw new InvalidOperationException("EveUnity requires a CultMesh rendezvous endpoint.");
|
throw new InvalidOperationException("EveUnity requires a CultMesh rendezvous endpoint.");
|
||||||
|
|
||||||
var prepared = await PrepareCandidateAsync(
|
if (_discovery == null)
|
||||||
new[] { rendezvousEndpoint },
|
_discovery = new EveUnityCultMeshProviderDiscovery().DiscoverAsync(
|
||||||
providerFilter,
|
rendezvousEndpoint,
|
||||||
surfaceFilter,
|
providerFilter,
|
||||||
surfaceKind,
|
surfaceFilter,
|
||||||
verseFilter,
|
surfaceKind,
|
||||||
"",
|
verseFilter);
|
||||||
_authorityTrust,
|
CompleteDiscoveryOnMainThread();
|
||||||
_navigationAuthorityTrust,
|
if (_transport == null)
|
||||||
cancellationToken).ConfigureAwait(false);
|
throw new InvalidOperationException("EveUnity CultMesh provider discovery is still in progress.");
|
||||||
TraceStartup($"discovery and transport preparation {elapsed.Elapsed.TotalMilliseconds:0.###}ms");
|
|
||||||
AdoptPrepared(prepared);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<PreparedProvider> PrepareCandidateAsync(
|
private void CompleteDiscoveryOnMainThread()
|
||||||
IReadOnlyList<string> endpoints,
|
|
||||||
string requiredProviderId,
|
|
||||||
string requiredSurfaceId,
|
|
||||||
string requiredSurfaceKind,
|
|
||||||
string requiredVerseId,
|
|
||||||
string requiredAuthorityRuntimeId,
|
|
||||||
CultMeshAuthorityTrustPolicy trust,
|
|
||||||
CultMeshAuthorityTrustPolicy crossTargetTrust,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
var failures = new List<string>();
|
if (_transport != null || _discovery == null || !_discovery.IsCompleted)
|
||||||
foreach (var endpoint in endpoints)
|
return;
|
||||||
{
|
if (_discovery.IsFaulted)
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
throw _discovery.Exception?.GetBaseException()
|
||||||
try
|
?? new InvalidOperationException("EveUnity CultMesh provider discovery failed.");
|
||||||
{
|
if (_discovery.IsCanceled)
|
||||||
var selection = await new EveUnityCultMeshProviderDiscovery(trust).DiscoverAsync(
|
throw new InvalidOperationException("EveUnity CultMesh provider discovery was cancelled.");
|
||||||
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(
|
Selection = _discovery.Result;
|
||||||
$"No configured Odin endpoint prepared Verse '{requiredVerseId}' surface '{requiredSurfaceId}'. " +
|
var resolvedReplicaPath = string.IsNullOrWhiteSpace(replicaPath)
|
||||||
string.Join(" | ", failures));
|
? Path.Combine(Application.temporaryCachePath, $"eve-unity-{GetInstanceID()}.cc")
|
||||||
}
|
: replicaPath;
|
||||||
|
_transport = new EveUnityCultMeshLiveProviderTransport(
|
||||||
private void AdoptPrepared(PreparedProvider prepared)
|
resolvedReplicaPath,
|
||||||
{
|
|
||||||
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,
|
Selection,
|
||||||
_transport,
|
runtimeId);
|
||||||
_bridge,
|
_transport.EntityViewAvailable += view => _pendingEntityViews.Enqueue(view);
|
||||||
rendezvousEndpoint,
|
_bridge = new EveUnitySceneLiveProviderBridge(_transport);
|
||||||
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,11 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GameCult.Eve.Surface;
|
using GameCult.Eve.Surface;
|
||||||
using GameCult.Mesh;
|
using GameCult.Mesh;
|
||||||
using GameCult.Networking;
|
using UnityEngine;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
|
|
@ -13,155 +11,65 @@ namespace GameCult.Eve.UnityScene
|
||||||
{
|
{
|
||||||
public sealed class EveUnityCultMeshProviderSelection
|
public sealed class EveUnityCultMeshProviderSelection
|
||||||
{
|
{
|
||||||
public EveUnityCultMeshProviderSelection(
|
internal EveUnityCultMeshProviderSelection(
|
||||||
string rendezvousEndpoint,
|
string endpointId,
|
||||||
string verseId,
|
CultMeshClient mesh,
|
||||||
string authorityRuntimeId,
|
EveSurfaceSelection surface)
|
||||||
string providerId,
|
|
||||||
string surfaceId,
|
|
||||||
string surfaceKind)
|
|
||||||
{
|
{
|
||||||
RendezvousEndpoint = rendezvousEndpoint ?? "";
|
EndpointId = endpointId;
|
||||||
VerseId = verseId ?? "";
|
Mesh = mesh;
|
||||||
AuthorityRuntimeId = authorityRuntimeId ?? "";
|
Surface = surface;
|
||||||
ProviderId = providerId ?? "";
|
|
||||||
SurfaceId = surfaceId ?? "";
|
|
||||||
SurfaceKind = surfaceKind ?? "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string RendezvousEndpoint { get; }
|
public string EndpointId { get; }
|
||||||
public string VerseId { get; }
|
public string VerseId => Surface.Provider.VerseId;
|
||||||
public string AuthorityRuntimeId { get; }
|
public string ProviderId => Surface.Provider.ProviderId;
|
||||||
public string ProviderId { get; }
|
public string SurfaceId => Surface.Advertisement.SurfaceId;
|
||||||
public string SurfaceId { get; }
|
public string SurfaceKind => Surface.Advertisement.SurfaceKind;
|
||||||
public string SurfaceKind { get; }
|
public CultMeshClient Mesh { get; }
|
||||||
|
public EveSurfaceSelection Surface { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class EveUnityCultMeshProviderDiscovery
|
public sealed class EveUnityCultMeshProviderDiscovery
|
||||||
{
|
{
|
||||||
private readonly CultMeshAuthorityTrustPolicy _authorityTrust;
|
|
||||||
|
|
||||||
public EveUnityCultMeshProviderDiscovery(CultMeshAuthorityTrustPolicy? authorityTrust = null)
|
|
||||||
{
|
|
||||||
_authorityTrust = authorityTrust ?? new CultMeshAuthorityTrustPolicy(
|
|
||||||
CultMeshAuthorityTrustMode.AuthenticatedRemote);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<EveUnityCultMeshProviderSelection> DiscoverAsync(
|
public async Task<EveUnityCultMeshProviderSelection> DiscoverAsync(
|
||||||
string rendezvousEndpoint,
|
string rendezvousEndpoint,
|
||||||
string providerId = "",
|
string providerId = "",
|
||||||
string surfaceId = "",
|
string surfaceId = "",
|
||||||
string surfaceKind = "interactive-world",
|
string surfaceKind = "interactive-world",
|
||||||
string verseId = "",
|
string verseId = "",
|
||||||
CancellationToken cancellationToken = default,
|
CancellationToken cancellationToken = default)
|
||||||
string requiredAuthorityRuntimeId = "")
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(rendezvousEndpoint))
|
if (string.IsNullOrWhiteSpace(rendezvousEndpoint))
|
||||||
throw new ArgumentException("Rendezvous endpoint must be non-empty.", nameof(rendezvousEndpoint));
|
throw new ArgumentException("Rendezvous endpoint must be non-empty.", nameof(rendezvousEndpoint));
|
||||||
|
var endpointId = !string.IsNullOrWhiteSpace(verseId)
|
||||||
|
? verseId
|
||||||
|
: !string.IsNullOrWhiteSpace(providerId) ? providerId : "eve.providers";
|
||||||
|
|
||||||
CultMeshVerseCatalogResponseMessage response;
|
var mesh = new CultMeshClient(new CultMeshClientOptions
|
||||||
|
{
|
||||||
|
RendezvousEndpoints = new[] { rendezvousEndpoint }
|
||||||
|
});
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
response = await CultMesh.CreateVerseDiscoveryClient(EveUnityCultMeshConnectivity.Discovery()).FetchAsync(
|
var surface = await mesh.SurfaceAsync(
|
||||||
rendezvousEndpoint,
|
endpointId,
|
||||||
new CultMeshVerseCatalogRequestMessage
|
new EveSurfaceRequest
|
||||||
{
|
{
|
||||||
VerseIds = string.IsNullOrWhiteSpace(verseId) ? null : new[] { verseId },
|
ProviderId = providerId,
|
||||||
TransportVersion = "cultmesh.v0"
|
SurfaceId = surfaceId,
|
||||||
});
|
SurfaceKind = surfaceKind,
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
LoweringTarget = "unity-scene"
|
||||||
|
},
|
||||||
|
cancellationToken,
|
||||||
|
stage => Debug.Log($"EveUnity CultMesh discovery: {stage}")).ConfigureAwait(false);
|
||||||
|
return new EveUnityCultMeshProviderSelection(endpointId, mesh, surface);
|
||||||
}
|
}
|
||||||
catch (Exception error)
|
catch
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException(
|
mesh.Dispose();
|
||||||
$"Could not query CultMesh rendezvous endpoint '{rendezvousEndpoint}'.",
|
throw;
|
||||||
error);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var candidates = response.Verses
|
|
||||||
.Where(verse => string.IsNullOrWhiteSpace(verseId) ||
|
|
||||||
string.Equals(verse.VerseId, verseId, StringComparison.Ordinal))
|
|
||||||
.Where(verse => !string.IsNullOrWhiteSpace(verse.VerseId) &&
|
|
||||||
(verse.DiscoveryEndpoints ?? Array.Empty<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)
|
|
||||||
{
|
|
||||||
foreach (var authorityRuntimeId in EligibleAuthorityRuntimeIds(
|
|
||||||
candidate.AuthorityRuntimeIds,
|
|
||||||
requiredAuthorityRuntimeId))
|
|
||||||
{
|
|
||||||
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(
|
|
||||||
rendezvousEndpoint,
|
|
||||||
candidate.VerseId,
|
|
||||||
authorityRuntimeId,
|
|
||||||
advertisement.Document.ProviderId,
|
|
||||||
advertisement.Surface.SurfaceId,
|
|
||||||
advertisement.Surface.SurfaceKind);
|
|
||||||
}
|
|
||||||
catch (Exception error)
|
|
||||||
{
|
|
||||||
failures.Add($"{candidate.VerseId}/{authorityRuntimeId}: {error.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var filter = $"provider='{providerId}', surface='{surfaceId}', kind='{surfaceKind}', authority='{requiredAuthorityRuntimeId}'";
|
|
||||||
var detail = failures.Count == 0 ? "" : $" Endpoint failures: {string.Join(" | ", failures)}";
|
|
||||||
var observedDetail = observed.Count == 0 ? "" : $" Observed: {string.Join(" | ", observed)}";
|
|
||||||
throw new InvalidOperationException($"No advertised Eve surface matched {filter}.{detail}{observedDetail}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static IReadOnlyList<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();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
"references": [
|
"references": [
|
||||||
"GameCult.Eve.Surface",
|
"GameCult.Eve.Surface",
|
||||||
"GameCult.Eve.UnityScene",
|
"GameCult.Eve.UnityScene",
|
||||||
"GameCult.Eve.PluginFields",
|
|
||||||
"UnityEngine.AssetBundleModule"
|
"UnityEngine.AssetBundleModule"
|
||||||
],
|
],
|
||||||
"includePlatforms": [],
|
"includePlatforms": [],
|
||||||
|
|
@ -15,9 +14,7 @@
|
||||||
"GameCult.Caching.dll",
|
"GameCult.Caching.dll",
|
||||||
"GameCult.Caching.MessagePack.dll",
|
"GameCult.Caching.MessagePack.dll",
|
||||||
"GameCult.Mesh.dll",
|
"GameCult.Mesh.dll",
|
||||||
"GameCult.Mesh.Quic.Native.dll",
|
"GameCult.Networking.dll"
|
||||||
"GameCult.Networking.dll",
|
|
||||||
"GameCult.Networking.WebSockets.dll"
|
|
||||||
],
|
],
|
||||||
"autoReferenced": true,
|
"autoReferenced": true,
|
||||||
"defineConstraints": [],
|
"defineConstraints": [],
|
||||||
|
|
|
||||||
|
|
@ -1,226 +0,0 @@
|
||||||
using System;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.Rendering;
|
|
||||||
using UnityEngine.Rendering.RenderGraphModule;
|
|
||||||
using UnityEngine.Rendering.Universal;
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace GameCult.Eve.UnityScene
|
|
||||||
{
|
|
||||||
public sealed class EveUnityAdaptiveExposureRenderer : MonoBehaviour, IEveUnityRenderPassSource
|
|
||||||
{
|
|
||||||
private const int HistogramBins = 128;
|
|
||||||
private ComputeShader? _compute;
|
|
||||||
private Material? _multiplyMaterial;
|
|
||||||
private ComputeBuffer? _histogram;
|
|
||||||
private readonly RenderTexture?[] _exposure = new RenderTexture?[2];
|
|
||||||
private int _currentExposure = -1;
|
|
||||||
private Camera? _camera;
|
|
||||||
private AdaptiveExposurePass? _pass;
|
|
||||||
|
|
||||||
private float _lowPercent;
|
|
||||||
private float _highPercent;
|
|
||||||
private float _minimumEv;
|
|
||||||
private float _maximumEv;
|
|
||||||
private float _keyValue;
|
|
||||||
private float _speedUp;
|
|
||||||
private float _speedDown;
|
|
||||||
private bool _progressive;
|
|
||||||
|
|
||||||
public long AppliedFrameCount { get; private set; }
|
|
||||||
public bool IsConfigured { get; private set; }
|
|
||||||
public static RenderPassEvent ExposureRenderPassEvent =>
|
|
||||||
(RenderPassEvent)((int)RenderPassEvent.BeforeRenderingPostProcessing + 1);
|
|
||||||
int IEveUnityRenderPassSource.RenderOrder => 300;
|
|
||||||
|
|
||||||
public void Configure(
|
|
||||||
float lowPercent,
|
|
||||||
float highPercent,
|
|
||||||
float minimumEv,
|
|
||||||
float maximumEv,
|
|
||||||
float keyValue,
|
|
||||||
string adaptation,
|
|
||||||
float speedUp,
|
|
||||||
float speedDown)
|
|
||||||
{
|
|
||||||
_lowPercent = Mathf.Clamp(lowPercent, 1f, 98.99f);
|
|
||||||
_highPercent = Mathf.Clamp(highPercent, _lowPercent + 0.01f, 99f);
|
|
||||||
_minimumEv = Mathf.Min(minimumEv, maximumEv);
|
|
||||||
_maximumEv = Mathf.Max(minimumEv, maximumEv);
|
|
||||||
_keyValue = Mathf.Max(0f, keyValue);
|
|
||||||
_speedUp = Mathf.Max(0f, speedUp);
|
|
||||||
_speedDown = Mathf.Max(0f, speedDown);
|
|
||||||
_progressive = string.Equals(adaptation, "progressive", StringComparison.Ordinal);
|
|
||||||
IsConfigured = true;
|
|
||||||
enabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnEnable()
|
|
||||||
{
|
|
||||||
_camera = GetComponent<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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 3cb42e7da8b34febb348f01a598f2eea
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using GameCult.Eve.Surface;
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace GameCult.Eve.UnityScene
|
|
||||||
{
|
|
||||||
public sealed class EveUnityAdvertisedInputAction
|
|
||||||
{
|
|
||||||
public const string ButtonHoldValueModel = "button-hold.v1";
|
|
||||||
public const string AxisValueModel = "axis.v1";
|
|
||||||
public const string ScalarValueModel = "scalar.v1";
|
|
||||||
public const string ViewDirectionValueModel = "view-direction.v1";
|
|
||||||
|
|
||||||
private readonly EveInputActionDocument _action;
|
|
||||||
|
|
||||||
private EveUnityAdvertisedInputAction(EveInputActionDocument action)
|
|
||||||
{
|
|
||||||
_action = action;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ActionId => _action.ActionId ?? "";
|
|
||||||
public string Operation => _action.Operation ?? "";
|
|
||||||
public string Availability => _action.Availability ?? "";
|
|
||||||
public string InputValueModel => _action.InputValue?.Model ?? "";
|
|
||||||
public string InputValuePayloadKey => _action.InputValue?.PayloadKey ?? "";
|
|
||||||
public IReadOnlyList<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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: c7555b03e7f9451aa891e92958b845c5
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Globalization;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace GameCult.Eve.UnityScene
|
|
||||||
{
|
|
||||||
public sealed class EveUnityAimPresentation
|
|
||||||
{
|
|
||||||
public EveUnityAimPresentation(EveUnitySceneNode node)
|
|
||||||
{
|
|
||||||
if (node == null) throw new ArgumentNullException(nameof(node));
|
|
||||||
ControlledEntityId = Get(node, "controlledEntityId");
|
|
||||||
ConvergenceTargetEntityId = Get(node, "convergenceTargetEntityId");
|
|
||||||
ViewDotRole = Get(node, "viewDotRole");
|
|
||||||
MinimumConvergenceDistance = Math.Max(0.01f, Number(node, "minimumConvergenceDistance", 50f));
|
|
||||||
ViewDotRadius = Math.Max(0.01f, Number(node, "viewDotRadius", 0.8f));
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ControlledEntityId { get; }
|
|
||||||
public string ConvergenceTargetEntityId { get; }
|
|
||||||
public string ViewDotRole { get; }
|
|
||||||
public float MinimumConvergenceDistance { get; }
|
|
||||||
public float ViewDotRadius { get; }
|
|
||||||
|
|
||||||
public bool TryResolveViewPoint(EveUnityPlayableWorldClientHost? host, out Vector3 viewPoint)
|
|
||||||
{
|
|
||||||
viewPoint = default;
|
|
||||||
if (host == null || string.IsNullOrWhiteSpace(ControlledEntityId)) return false;
|
|
||||||
var controlled = FindMarker(host, ControlledEntityId);
|
|
||||||
if (controlled == null) return false;
|
|
||||||
var distance = MinimumConvergenceDistance;
|
|
||||||
var target = FindMarker(host, ConvergenceTargetEntityId);
|
|
||||||
if (target != null)
|
|
||||||
distance = Mathf.Max(distance, Vector3.Distance(controlled.transform.position, target.transform.position));
|
|
||||||
var forward = controlled.transform.forward;
|
|
||||||
forward.y = 0f;
|
|
||||||
if (forward.sqrMagnitude <= 0.0001f) forward = Vector3.forward;
|
|
||||||
forward.Normalize();
|
|
||||||
viewPoint = controlled.transform.position + forward * distance;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static EveUnityAimPresentation? Find(EveUnitySceneProjection? projection) =>
|
|
||||||
projection == null ? null : Find(projection.Root);
|
|
||||||
|
|
||||||
private static EveUnityAimPresentation? Find(EveUnitySceneNode node)
|
|
||||||
{
|
|
||||||
if (string.Equals(node.ComponentKind, "aim.presentation", StringComparison.Ordinal))
|
|
||||||
return new EveUnityAimPresentation(node);
|
|
||||||
foreach (var child in node.Children)
|
|
||||||
{
|
|
||||||
var found = Find(child);
|
|
||||||
if (found != null) return found;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static EveUnityPlayableWorldEntityMarker? FindMarker(
|
|
||||||
EveUnityPlayableWorldClientHost host,
|
|
||||||
string entityId)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(entityId)) return null;
|
|
||||||
foreach (var marker in host.SceneRoot.GetComponentsInChildren<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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: f859926d39664cefa5aa96ed3c2d8903
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
using System;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace GameCult.Eve.UnityScene
|
|
||||||
{
|
|
||||||
public sealed class EveUnityAimPresentationRenderer : MonoBehaviour
|
|
||||||
{
|
|
||||||
[SerializeField] private Color viewDotColor = new Color(0.35f, 0.95f, 1f, 0.95f);
|
|
||||||
[SerializeField] private float width = 0.1f;
|
|
||||||
|
|
||||||
private EveUnityPlayableWorldClientHost? _host;
|
|
||||||
private LineRenderer? _ring;
|
|
||||||
private LineRenderer? _horizontal;
|
|
||||||
private LineRenderer? _vertical;
|
|
||||||
private Material? _material;
|
|
||||||
|
|
||||||
public EveUnityAimPresentation? Current { get; private set; }
|
|
||||||
public bool ViewDotVisible => _ring != null && _ring.enabled;
|
|
||||||
public Vector3 ViewDotPosition { get; private set; }
|
|
||||||
|
|
||||||
public void Bind(EveUnityPlayableWorldClientHost host)
|
|
||||||
{
|
|
||||||
_host = host != null ? host : throw new ArgumentNullException(nameof(host));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RefreshNow()
|
|
||||||
{
|
|
||||||
Current = EveUnityAimPresentation.Find(_host?.ActiveProjection);
|
|
||||||
if (Current == null || string.IsNullOrWhiteSpace(Current.ControlledEntityId))
|
|
||||||
{
|
|
||||||
SetVisible(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Current.TryResolveViewPoint(_host, out var viewPoint))
|
|
||||||
{
|
|
||||||
SetVisible(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ViewDotPosition = viewPoint;
|
|
||||||
|
|
||||||
EnsureVisuals();
|
|
||||||
var camera = _host?.ActiveCameraTransform;
|
|
||||||
if (camera == null)
|
|
||||||
{
|
|
||||||
SetVisible(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var right = camera.right;
|
|
||||||
var up = camera.up;
|
|
||||||
var center = ViewDotPosition - camera.forward * 0.1f;
|
|
||||||
Ring(_ring!, center, right, up, Current.ViewDotRadius);
|
|
||||||
Segment(_horizontal!, center, right, Current.ViewDotRadius * 1.6f);
|
|
||||||
Segment(_vertical!, center, up, Current.ViewDotRadius * 1.6f);
|
|
||||||
SetVisible(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LateUpdate() => RefreshNow();
|
|
||||||
|
|
||||||
private void EnsureVisuals()
|
|
||||||
{
|
|
||||||
if (_ring != null) return;
|
|
||||||
var root = new GameObject("Eve aim presentation");
|
|
||||||
root.transform.SetParent(transform, false);
|
|
||||||
_ring = Line(root.transform, "View direction ring");
|
|
||||||
_horizontal = Line(root.transform, "View direction horizontal");
|
|
||||||
_vertical = Line(root.transform, "View direction vertical");
|
|
||||||
}
|
|
||||||
|
|
||||||
private LineRenderer Line(Transform parent, string name)
|
|
||||||
{
|
|
||||||
var child = new GameObject(name);
|
|
||||||
child.transform.SetParent(parent, false);
|
|
||||||
var line = child.AddComponent<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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 97c7b809756a4350a6e61c67bba69b82
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace GameCult.Eve.UnityScene
|
|
||||||
{
|
|
||||||
public sealed class EveUnityBeamPresentation
|
|
||||||
{
|
|
||||||
public const string SourceForwardDirection = "source-forward.v1";
|
|
||||||
|
|
||||||
public EveUnityBeamPresentation(EveUnitySceneNode node)
|
|
||||||
{
|
|
||||||
if (node == null) throw new ArgumentNullException(nameof(node));
|
|
||||||
Id = node.Id ?? "";
|
|
||||||
SourceEntityId = Get(node, "sourceEntityId");
|
|
||||||
AssetRole = Get(node, "assetRole");
|
|
||||||
DirectionMode = Get(node, "directionMode");
|
|
||||||
if (string.IsNullOrWhiteSpace(DirectionMode)) DirectionMode = SourceForwardDirection;
|
|
||||||
RenderChannel = Get(node, "renderChannel");
|
|
||||||
ActivationActionId = Get(node, "activationActionId");
|
|
||||||
Power = NonNegative(node, "power", 0f);
|
|
||||||
PowerStateSemantic = Get(node, "powerStateSemantic");
|
|
||||||
ActivationThreshold = NonNegative(node, "activationThreshold", 0.01f);
|
|
||||||
Radius = NonNegative(node, "radius", 0f);
|
|
||||||
MaximumDistance = NonNegative(node, "maximumDistance", 0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Id { get; }
|
|
||||||
public string SourceEntityId { get; }
|
|
||||||
public string AssetRole { get; }
|
|
||||||
public string DirectionMode { get; }
|
|
||||||
public string RenderChannel { get; }
|
|
||||||
public string ActivationActionId { get; }
|
|
||||||
public float Power { get; }
|
|
||||||
public string PowerStateSemantic { get; }
|
|
||||||
public float ActivationThreshold { get; }
|
|
||||||
public float Radius { get; }
|
|
||||||
public float MaximumDistance { get; }
|
|
||||||
public bool UsesSourceForward =>
|
|
||||||
string.Equals(DirectionMode, SourceForwardDirection, StringComparison.Ordinal);
|
|
||||||
|
|
||||||
public static IReadOnlyList<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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 3086cd42c94f2624197d19704909f3f5
|
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace GameCult.Eve.UnityScene
|
|
||||||
{
|
|
||||||
public sealed class EveUnityBeamPresentationRenderer : MonoBehaviour
|
|
||||||
{
|
|
||||||
private readonly Dictionary<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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: f91c99c24bc0e20488fa03daef2eec06
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace GameCult.Eve.UnityScene
|
|
||||||
{
|
|
||||||
public sealed class EveUnityCombatPresentation
|
|
||||||
{
|
|
||||||
public EveUnityCombatPresentation(EveUnitySceneNode node)
|
|
||||||
{
|
|
||||||
if (node == null) throw new ArgumentNullException(nameof(node));
|
|
||||||
ControlledEntityId = Get(node, "controlledEntityId");
|
|
||||||
ControlledEntityIndex = Int(node, "controlledEntityIndex", -1);
|
|
||||||
SelectedTargetEntityId = Get(node, "selectedTargetEntityId");
|
|
||||||
SelectedTargetEntityIndex = Int(node, "selectedTargetEntityIndex", -1);
|
|
||||||
TargetVisible = Bool(node, "targetVisible");
|
|
||||||
TargetHostile = Bool(node, "targetHostile");
|
|
||||||
ContactInformation = Ratio(node, "contactInformation");
|
|
||||||
ShieldRatio = Ratio(node, "shieldRatio");
|
|
||||||
HullRatio = Ratio(node, "hullRatio");
|
|
||||||
LockProgress = Ratio(node, "lockProgress");
|
|
||||||
ReticleRole = Get(node, "reticleRole");
|
|
||||||
LockRole = Get(node, "lockRole");
|
|
||||||
ShieldMeterRole = Get(node, "shieldMeterRole");
|
|
||||||
HullMeterRole = Get(node, "hullMeterRole");
|
|
||||||
HitMarkerRole = Get(node, "hitMarkerRole");
|
|
||||||
LockDisplayThreshold = Ratio(node, "lockDisplayThreshold", 0.01f);
|
|
||||||
HitMarkerDurationSeconds = Math.Max(0.01f, Number(node, "hitMarkerDurationSeconds", 0.25f));
|
|
||||||
RadialFillMinimum = Ratio(node, "radialFillMinimum", 0.25f);
|
|
||||||
RadialFillMaximum = Ratio(node, "radialFillMaximum", 0.75f);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string ControlledEntityId { get; }
|
|
||||||
public int ControlledEntityIndex { get; }
|
|
||||||
public string SelectedTargetEntityId { get; }
|
|
||||||
public int SelectedTargetEntityIndex { get; }
|
|
||||||
public bool TargetVisible { get; }
|
|
||||||
public bool TargetHostile { get; }
|
|
||||||
public float ContactInformation { get; }
|
|
||||||
public float ShieldRatio { get; }
|
|
||||||
public float HullRatio { get; }
|
|
||||||
public float LockProgress { get; }
|
|
||||||
public string ReticleRole { get; }
|
|
||||||
public string LockRole { get; }
|
|
||||||
public string ShieldMeterRole { get; }
|
|
||||||
public string HullMeterRole { get; }
|
|
||||||
public string HitMarkerRole { get; }
|
|
||||||
public float LockDisplayThreshold { get; }
|
|
||||||
public float HitMarkerDurationSeconds { get; }
|
|
||||||
public float RadialFillMinimum { get; }
|
|
||||||
public float RadialFillMaximum { get; }
|
|
||||||
|
|
||||||
public float RadialFill(float ratio) =>
|
|
||||||
RadialFillMinimum + (RadialFillMaximum - RadialFillMinimum) * Math.Max(0, Math.Min(1, ratio));
|
|
||||||
|
|
||||||
public bool PresentsHit(EveUnityShotReceipt receipt) =>
|
|
||||||
receipt != null &&
|
|
||||||
receipt.SourceEntityIndex == ControlledEntityIndex &&
|
|
||||||
receipt.TargetEntityIndex == SelectedTargetEntityIndex &&
|
|
||||||
receipt.Hit &&
|
|
||||||
(receipt.AppliedDamage > 0 || receipt.ShieldAbsorbedDamage > 0);
|
|
||||||
|
|
||||||
public static EveUnityCombatPresentation? Find(EveUnitySceneProjection? projection)
|
|
||||||
{
|
|
||||||
if (projection == null) return null;
|
|
||||||
return Find(projection.Root);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static EveUnityCombatPresentation? Find(EveUnitySceneNode node)
|
|
||||||
{
|
|
||||||
if (string.Equals(node.ComponentKind, "combat.presentation", StringComparison.Ordinal))
|
|
||||||
return new EveUnityCombatPresentation(node);
|
|
||||||
foreach (var child in node.Children)
|
|
||||||
{
|
|
||||||
var found = Find(child);
|
|
||||||
if (found != null) return found;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string Get(EveUnitySceneNode node, string key) =>
|
|
||||||
node.Props.TryGetValue(key, out var value) ? value ?? "" : "";
|
|
||||||
|
|
||||||
private static int Int(EveUnitySceneNode node, string key, int fallback) =>
|
|
||||||
int.TryParse(Get(node, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) ? value : fallback;
|
|
||||||
|
|
||||||
private static bool Bool(EveUnitySceneNode node, string key) =>
|
|
||||||
bool.TryParse(Get(node, key), out var value) && value;
|
|
||||||
|
|
||||||
private static float Number(EveUnitySceneNode node, string key, float fallback = 0) =>
|
|
||||||
float.TryParse(Get(node, key), NumberStyles.Float, CultureInfo.InvariantCulture, out var value) ? value : fallback;
|
|
||||||
|
|
||||||
private static float Ratio(EveUnitySceneNode node, string key, float fallback = 0) =>
|
|
||||||
Math.Max(0, Math.Min(1, Number(node, key, fallback)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: e15acab8e9094b18b6dbd3abea0e055d
|
|
||||||
|
|
@ -1,279 +0,0 @@
|
||||||
using System;
|
|
||||||
using UnityEngine;
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace GameCult.Eve.UnityScene
|
|
||||||
{
|
|
||||||
public sealed class EveUnityCombatPresentationRenderer : MonoBehaviour
|
|
||||||
{
|
|
||||||
[SerializeField] private Color reticleColor = new Color(0.2f, 0.9f, 1f, 0.9f);
|
|
||||||
[SerializeField] private Color lockColor = new Color(1f, 0.7f, 0.15f, 1f);
|
|
||||||
[SerializeField] private Color shieldColor = new Color(0.15f, 0.65f, 1f, 1f);
|
|
||||||
[SerializeField] private Color hullColor = new Color(1f, 0.35f, 0.2f, 1f);
|
|
||||||
[SerializeField] private Color hitColor = Color.white;
|
|
||||||
[SerializeField] private float width = 0.12f;
|
|
||||||
|
|
||||||
private EveUnityPlayableWorldClientHost? _host;
|
|
||||||
private Transform? _visualRoot;
|
|
||||||
private LineRenderer? _reticle;
|
|
||||||
private LineRenderer? _lock;
|
|
||||||
private LineRenderer? _shield;
|
|
||||||
private LineRenderer? _hull;
|
|
||||||
private LineRenderer? _hitA;
|
|
||||||
private LineRenderer? _hitB;
|
|
||||||
private Material? _material;
|
|
||||||
private float _hitUntil;
|
|
||||||
private float _pendingHitDuration;
|
|
||||||
private EveUnityCombatPresentation? _lastPresentation;
|
|
||||||
private Vector3 _lastCenter;
|
|
||||||
private Vector3 _lastRight;
|
|
||||||
private Vector3 _lastUp;
|
|
||||||
private float _lastRadius;
|
|
||||||
private bool _hasLastTarget;
|
|
||||||
|
|
||||||
public EveUnityCombatPresentation? Current { get; private set; }
|
|
||||||
public bool ReticleVisible => _reticle != null && _reticle.enabled;
|
|
||||||
public bool LockVisible => _lock != null && _lock.enabled;
|
|
||||||
public bool HitMarkerVisible => _hitA != null && _hitA.enabled;
|
|
||||||
|
|
||||||
public void Bind(EveUnityPlayableWorldClientHost host)
|
|
||||||
{
|
|
||||||
if (host == null) throw new ArgumentNullException(nameof(host));
|
|
||||||
if (ReferenceEquals(_host, host)) return;
|
|
||||||
Unbind();
|
|
||||||
_host = host;
|
|
||||||
_host.ShotAvailable += OnShotAvailable;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Unbind()
|
|
||||||
{
|
|
||||||
if (_host != null) _host.ShotAvailable -= OnShotAvailable;
|
|
||||||
_host = null;
|
|
||||||
Current = null;
|
|
||||||
SetVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RefreshNow()
|
|
||||||
{
|
|
||||||
if (_pendingHitDuration > 0)
|
|
||||||
{
|
|
||||||
_hitUntil = Time.unscaledTime + _pendingHitDuration;
|
|
||||||
_pendingHitDuration = 0;
|
|
||||||
}
|
|
||||||
Current = EveUnityCombatPresentation.Find(_host?.ActiveProjection);
|
|
||||||
if (Current == null || string.IsNullOrWhiteSpace(Current.SelectedTargetEntityId))
|
|
||||||
{
|
|
||||||
PresentRetainedHit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_lastPresentation = Current;
|
|
||||||
var marker = FindMarker(Current.SelectedTargetEntityId);
|
|
||||||
if (marker == null)
|
|
||||||
{
|
|
||||||
PresentRetainedHit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
EnsureVisuals();
|
|
||||||
PresentationAxes(out var right, out var up, out var towardCamera, out var cullingMask);
|
|
||||||
var visualBounds = VisualBounds(marker.transform, cullingMask);
|
|
||||||
var center = visualBounds?.center ?? (marker.transform.position + Vector3.up * 0.25f);
|
|
||||||
var radius = Math.Max(0.8f, visualBounds?.extents.magnitude ?? marker.transform.lossyScale.magnitude * 0.8f);
|
|
||||||
center += towardCamera * 0.1f;
|
|
||||||
_lastCenter = center;
|
|
||||||
_lastRight = right;
|
|
||||||
_lastUp = up;
|
|
||||||
_lastRadius = radius;
|
|
||||||
_hasLastTarget = true;
|
|
||||||
Arc(_reticle!, center, right, up, radius * 1.35f, 1f, 0f);
|
|
||||||
_reticle!.enabled = Current.TargetVisible;
|
|
||||||
|
|
||||||
var lockVisible = Current.TargetVisible && Current.TargetHostile &&
|
|
||||||
Current.LockProgress > Current.LockDisplayThreshold;
|
|
||||||
var noise = (1f - Current.LockProgress) * radius * 0.2f;
|
|
||||||
var frequency = Mathf.Lerp(0.01f, 10f, Mathf.Pow(Current.LockProgress, 4f));
|
|
||||||
var noisyCenter = center +
|
|
||||||
right * ((Mathf.PerlinNoise(Time.unscaledTime * frequency, 0.37f) - 0.5f) * noise) +
|
|
||||||
up * ((Mathf.PerlinNoise(0.73f, Time.unscaledTime * frequency) - 0.5f) * noise);
|
|
||||||
var spin = Time.unscaledTime * Mathf.Lerp(5f, 720f, Current.LockProgress * Current.LockProgress);
|
|
||||||
Arc(_lock!, noisyCenter, right, up, radius * 1.55f, Current.LockProgress, spin);
|
|
||||||
_lock!.enabled = lockVisible;
|
|
||||||
|
|
||||||
Arc(_shield!, center, right, up, radius * 1.78f, Current.RadialFill(Current.ShieldRatio), -90f);
|
|
||||||
Arc(_hull!, center, right, up, radius * 1.98f, Current.RadialFill(Current.HullRatio), -90f);
|
|
||||||
_shield!.enabled = Current.TargetVisible;
|
|
||||||
_hull!.enabled = Current.TargetVisible;
|
|
||||||
|
|
||||||
var hitVisible = Time.unscaledTime < _hitUntil;
|
|
||||||
Cross(_hitA!, _hitB!, center, right, up, radius * 0.55f);
|
|
||||||
_hitA!.enabled = hitVisible;
|
|
||||||
_hitB!.enabled = hitVisible;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PresentRetainedHit()
|
|
||||||
{
|
|
||||||
if (!_hasLastTarget || Time.unscaledTime >= _hitUntil)
|
|
||||||
{
|
|
||||||
SetVisible(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
EnsureVisuals();
|
|
||||||
_reticle!.enabled = _lock!.enabled = _shield!.enabled = _hull!.enabled = false;
|
|
||||||
Cross(_hitA!, _hitB!, _lastCenter, _lastRight, _lastUp, _lastRadius * 0.55f);
|
|
||||||
_hitA!.enabled = _hitB!.enabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LateUpdate() => RefreshNow();
|
|
||||||
|
|
||||||
private void OnShotAvailable(EveUnityShotReceipt receipt)
|
|
||||||
{
|
|
||||||
Current = EveUnityCombatPresentation.Find(_host?.ActiveProjection);
|
|
||||||
var presentation = Current;
|
|
||||||
if (presentation?.PresentsHit(receipt) != true)
|
|
||||||
presentation = _lastPresentation;
|
|
||||||
if (presentation?.PresentsHit(receipt) == true)
|
|
||||||
{
|
|
||||||
PresentationAxes(out var right, out var up, out _, out _);
|
|
||||||
_lastCenter = new Vector3(
|
|
||||||
(float)receipt.Endpoint.X,
|
|
||||||
(float)receipt.Endpoint.Y,
|
|
||||||
(float)receipt.Endpoint.Z);
|
|
||||||
_lastRight = right;
|
|
||||||
_lastUp = up;
|
|
||||||
_lastRadius = Math.Max(0.8f, _lastRadius);
|
|
||||||
_hasLastTarget = true;
|
|
||||||
_pendingHitDuration = Math.Max(_pendingHitDuration, presentation.HitMarkerDurationSeconds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private EveUnityPlayableWorldEntityMarker? FindMarker(string entityId)
|
|
||||||
{
|
|
||||||
if (_host == null) return null;
|
|
||||||
foreach (var marker in _host.SceneRoot.GetComponentsInChildren<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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: 5c953d99a7454322b2935b878295b357
|
|
||||||
|
|
@ -2,7 +2,6 @@ using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using GameCult.Eve.Surface;
|
using GameCult.Eve.Surface;
|
||||||
using GameCult.Mesh;
|
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
|
|
@ -10,7 +9,13 @@ namespace GameCult.Eve.UnityScene
|
||||||
{
|
{
|
||||||
public interface IEveUnityEntitySoaViewDocumentSource
|
public interface IEveUnityEntitySoaViewDocumentSource
|
||||||
{
|
{
|
||||||
event Action<EveEntitySoaViewDocument, ICultMeshBodyReadLease> EntityViewAvailable;
|
EveEntitySoaViewDocument? CurrentEntityView { get; }
|
||||||
|
event Action<EveEntitySoaViewDocument> EntityViewAvailable;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IEveUnityInputCapabilityDocumentSource
|
||||||
|
{
|
||||||
|
EveInputCapabilityDocument? CurrentInputCapability { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Projects portable entity SoA generations into the ordinary Unity scene sink.</summary>
|
/// <summary>Projects portable entity SoA generations into the ordinary Unity scene sink.</summary>
|
||||||
|
|
@ -24,10 +29,15 @@ namespace GameCult.Eve.UnityScene
|
||||||
_sink = sink ?? throw new ArgumentNullException(nameof(sink));
|
_sink = sink ?? throw new ArgumentNullException(nameof(sink));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Apply(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease)
|
public int ActiveEntities => _known.Count;
|
||||||
|
|
||||||
|
public void Apply(EveEntitySoaViewDocument document)
|
||||||
{
|
{
|
||||||
using var view = EveUnityEntitySoaView.Open(document, lease);
|
using var view = EveUnityEntitySoaView.Open(document);
|
||||||
var presented = new List<EveUnityPresentedEntity>();
|
var identities = (document.Identities ?? Array.Empty<EveEntitySoaIdentity>())
|
||||||
|
.Where(identity => identity != null && identity.EntityIndex >= 0 && !string.IsNullOrWhiteSpace(identity.EntityId))
|
||||||
|
.GroupBy(identity => identity.EntityIndex)
|
||||||
|
.ToDictionary(group => group.Key, group => group.First());
|
||||||
var next = new HashSet<string>(StringComparer.Ordinal);
|
var next = new HashSet<string>(StringComparer.Ordinal);
|
||||||
for (var row = 0; row < view.EntityCount; row++)
|
for (var row = 0; row < view.EntityCount; row++)
|
||||||
{
|
{
|
||||||
|
|
@ -35,60 +45,45 @@ namespace GameCult.Eve.UnityScene
|
||||||
if (view.TryReadByte("render.visibility", row, out var visible) && visible == 0) continue;
|
if (view.TryReadByte("render.visibility", row, out var visible) && visible == 0) continue;
|
||||||
if (!view.TryReadVector3("transform.position", row, out var position)) continue;
|
if (!view.TryReadVector3("transform.position", row, out var position)) continue;
|
||||||
view.TryReadFloat("transform.rotation.radians", row, out var rotationRadians);
|
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.TryReadFloat("render.scale", row, out var scale);
|
||||||
view.TryReadInt32("render.lod", row, out var lod);
|
|
||||||
view.TryReadUInt32("render.group.id", row, out var groupId);
|
view.TryReadUInt32("render.group.id", row, out var groupId);
|
||||||
var renderGroup = view.RenderGroups.FirstOrDefault(group => group.GroupId == groupId);
|
var renderGroup = view.RenderGroups.FirstOrDefault(group => group.GroupId == groupId);
|
||||||
var identity = document.Identities.FirstOrDefault(item => item.Index == entityIndex);
|
if (!identities.TryGetValue(entityIndex, out var identity))
|
||||||
var entityId = identity?.EntityId ?? $"entity:{entityIndex}";
|
throw new InvalidOperationException(
|
||||||
|
$"Eve entity SoA view '{document.ViewId}' does not map entity index {entityIndex} to a logical identity.");
|
||||||
|
var entityId = identity.EntityId;
|
||||||
next.Add(entityId);
|
next.Add(entityId);
|
||||||
var assetRef = string.IsNullOrWhiteSpace(identity?.AssetRef)
|
var assetRef = string.IsNullOrWhiteSpace(identity.AssetRef)
|
||||||
? renderGroup?.MeshAssetRef ?? ""
|
? renderGroup?.MeshAssetRef ?? ""
|
||||||
: identity!.AssetRef;
|
: 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(
|
_sink.UpsertEntity(
|
||||||
fact.ToPlayableWorldEntity(),
|
new EveUnityPlayableWorldEntity(
|
||||||
|
entityId,
|
||||||
|
entityId,
|
||||||
|
identity.Kind,
|
||||||
|
string.IsNullOrWhiteSpace(identity.Label) ? entityId : identity.Label,
|
||||||
|
identity.Faction,
|
||||||
|
assetRef,
|
||||||
|
position.x,
|
||||||
|
position.y,
|
||||||
|
position.z,
|
||||||
|
rotationRadians * 57.2957795f,
|
||||||
|
scale > 0f ? scale : renderGroup?.DefaultScale ?? 1f,
|
||||||
|
selectable: identity.Selectable,
|
||||||
|
controllable: identity.Controllable,
|
||||||
|
focusCommand: "",
|
||||||
|
moveCommand: "",
|
||||||
|
targetCommand: "",
|
||||||
|
actionCommand: "",
|
||||||
|
props: new Dictionary<string, string>()),
|
||||||
new EveUnityPlayableWorldAssetBinding(
|
new EveUnityPlayableWorldAssetBinding(
|
||||||
assetRef,
|
assetRef,
|
||||||
"entity",
|
identity.Kind,
|
||||||
string.IsNullOrWhiteSpace(assetRef) ? "unity-generated-placeholder" : "provider-asset-ref"));
|
string.IsNullOrWhiteSpace(assetRef) ? "unity-generated-placeholder" : "provider-asset-ref"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_sink is IEveUnityEntityGenerationSink generationSink)
|
foreach (var entityId in _known.Where(entityId => !next.Contains(entityId)).ToArray())
|
||||||
{
|
_sink.RemoveEntity(entityId);
|
||||||
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();
|
_known.Clear();
|
||||||
foreach (var entityId in next) _known.Add(entityId);
|
foreach (var entityId in next) _known.Add(entityId);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,131 +11,111 @@ namespace GameCult.Eve.UnityScene
|
||||||
{
|
{
|
||||||
public sealed class EveUnityEntitySoaView : IDisposable
|
public sealed class EveUnityEntitySoaView : IDisposable
|
||||||
{
|
{
|
||||||
private readonly ICultMeshBodyReadLease _lease;
|
private readonly ICultMeshBodyReadLease _body;
|
||||||
private readonly Dictionary<string, EveEntitySoaBuffer> _buffers;
|
|
||||||
private readonly Dictionary<string, EveEntitySoaColumn> _columns;
|
private readonly Dictionary<string, EveEntitySoaColumn> _columns;
|
||||||
private readonly string[] _floatSemantics;
|
|
||||||
|
|
||||||
private EveUnityEntitySoaView(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease)
|
private EveUnityEntitySoaView(EveEntitySoaViewDocument document, ICultMeshBodyReadLease body)
|
||||||
{
|
{
|
||||||
Document = document;
|
Document = document;
|
||||||
_lease = lease;
|
_body = body;
|
||||||
_buffers = document.Buffers.ToDictionary(buffer => buffer.BufferId, StringComparer.Ordinal);
|
|
||||||
_columns = document.Columns.ToDictionary(column => column.Semantic, StringComparer.Ordinal);
|
_columns = document.Columns.ToDictionary(column => column.Semantic, StringComparer.Ordinal);
|
||||||
_floatSemantics = document.Columns
|
|
||||||
.Where(column => string.Equals(column.ScalarType, "float32", StringComparison.Ordinal))
|
|
||||||
.Select(column => column.Semantic)
|
|
||||||
.Where(semantic => !string.IsNullOrWhiteSpace(semantic))
|
|
||||||
.ToArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public EveEntitySoaViewDocument Document { get; }
|
public EveEntitySoaViewDocument Document { get; }
|
||||||
public long Generation => Document.Sequence;
|
public long Generation => Document.Generation;
|
||||||
public IReadOnlyList<EveEntityRenderGroup> RenderGroups => Document.RenderGroups;
|
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, ICultMeshBodyReadLease lease)
|
public static EveUnityEntitySoaView Open(EveEntitySoaViewDocument document)
|
||||||
{
|
{
|
||||||
if (document == null) throw new ArgumentNullException(nameof(document));
|
if (document == null) throw new ArgumentNullException(nameof(document));
|
||||||
if (lease == null) throw new ArgumentNullException(nameof(lease));
|
if (!string.Equals(document.Schema, EveEntitySoaViewDocument.SchemaId, StringComparison.Ordinal))
|
||||||
Validate(document, lease);
|
throw new ArgumentException($"Expected {EveEntitySoaViewDocument.SchemaId}, got '{document.Schema}'.", nameof(document));
|
||||||
return new EveUnityEntitySoaView(document, lease);
|
if (document.Body == null)
|
||||||
|
throw new InvalidOperationException("Eve entity SoA view did not advertise a CultMesh body.");
|
||||||
|
var request = new CultMeshBodyValidationRequest
|
||||||
|
{
|
||||||
|
BodyId = document.Body.BodyId,
|
||||||
|
SchemaId = document.Body.SchemaId,
|
||||||
|
LayoutVersion = document.Body.LayoutVersion,
|
||||||
|
ProducerEpoch = document.Body.ProducerEpoch,
|
||||||
|
AccessMode = CultMeshBodyAccessMode.ReadOnly,
|
||||||
|
NowUtc = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
var body = new CultMeshSharedMemoryBodyAdapter().OpenReadOnly(document.Body, request);
|
||||||
|
return new EveUnityEntitySoaView(document, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryReadVector3(string semantic, int index, out Vector3 value)
|
public bool TryReadVector3(string semantic, int index, out Vector3 value)
|
||||||
{
|
{
|
||||||
value = default;
|
value = default;
|
||||||
if (!TryColumn(semantic, index, "float3", 12, out var offset)) return false;
|
if (!TryColumn(semantic, index, "float3", 12, out var column)) return false;
|
||||||
value = new Vector3(_lease.ReadSingle(offset), _lease.ReadSingle(offset + 4), _lease.ReadSingle(offset + 8));
|
var offset = column.ByteOffset + (long)index * column.ElementStride;
|
||||||
|
value = new Vector3(_body.ReadSingle(offset), _body.ReadSingle(offset + 4), _body.ReadSingle(offset + 8));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryReadFloat(string semantic, int index, out float value)
|
public bool TryReadFloat(string semantic, int index, out float value)
|
||||||
{
|
{
|
||||||
value = default;
|
value = default;
|
||||||
if (!TryColumn(semantic, index, "float32", 4, out var offset)) return false;
|
if (!TryColumn(semantic, index, "float32", 4, out var column)) return false;
|
||||||
value = _lease.ReadSingle(offset);
|
value = _body.ReadSingle(column.ByteOffset + (long)index * column.ElementStride);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryReadInt32(string semantic, int index, out int value)
|
public bool TryReadInt32(string semantic, int index, out int value)
|
||||||
{
|
{
|
||||||
value = default;
|
value = default;
|
||||||
if (!TryColumn(semantic, index, "int32", 4, out var offset)) return false;
|
if (!TryColumn(semantic, index, "int32", 4, out var column)) return false;
|
||||||
value = _lease.ReadInt32(offset);
|
value = _body.ReadInt32(column.ByteOffset + (long)index * column.ElementStride);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryReadUInt32(string semantic, int index, out uint value)
|
public bool TryReadUInt32(string semantic, int index, out uint value)
|
||||||
{
|
{
|
||||||
value = default;
|
value = default;
|
||||||
if (!TryColumn(semantic, index, "uint32", 4, out var offset)) return false;
|
if (!TryColumn(semantic, index, "uint32", 4, out var column)) return false;
|
||||||
value = unchecked((uint)_lease.ReadInt32(offset));
|
value = unchecked((uint)_body.ReadInt32(column.ByteOffset + (long)index * column.ElementStride));
|
||||||
return true;
|
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)
|
public bool TryReadByte(string semantic, int index, out byte value)
|
||||||
{
|
{
|
||||||
value = default;
|
value = default;
|
||||||
if (!TryColumn(semantic, index, "uint8", 1, out var offset)) return false;
|
if (!TryColumn(semantic, index, "uint8", 1, out var column)) return false;
|
||||||
value = _lease.ReadByte(offset);
|
value = _body.ReadByte(column.ByteOffset + (long)index * column.ElementStride);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() => _lease.Dispose();
|
public void Dispose()
|
||||||
|
|
||||||
private static void Validate(EveEntitySoaViewDocument document, ICultMeshBodyReadLease lease)
|
|
||||||
{
|
{
|
||||||
if (!string.Equals(document.Schema, EveEntitySoaViewDocument.SchemaId, StringComparison.Ordinal))
|
_body.Dispose();
|
||||||
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 long offset)
|
private bool TryColumn(
|
||||||
|
string semantic,
|
||||||
|
int index,
|
||||||
|
string scalarType,
|
||||||
|
int minimumStride,
|
||||||
|
out EveEntitySoaColumn column)
|
||||||
{
|
{
|
||||||
offset = 0;
|
column = null!;
|
||||||
if (index < 0 || !_columns.TryGetValue(semantic ?? "", out var column)) return false;
|
if (index < 0 || !_columns.TryGetValue(semantic ?? "", out column)) return false;
|
||||||
if (index >= column.ElementCount || column.ElementStride < minimumStride) return false;
|
if (index >= column.ElementCount || column.ElementStride < minimumStride) return false;
|
||||||
if (!string.Equals(column.ScalarType, scalarType, StringComparison.Ordinal)) return false;
|
if (!string.Equals(column.ScalarType, scalarType, StringComparison.Ordinal)) return false;
|
||||||
if (!_buffers.TryGetValue(column.BufferId, out var buffer)) return false;
|
return DocumentBufferContains(column);
|
||||||
offset = buffer.ByteOffset + column.ByteOffset + (long)index * column.ElementStride;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int ScalarWidth(string scalarType) => scalarType switch
|
private bool DocumentBufferContains(EveEntitySoaColumn column)
|
||||||
{
|
{
|
||||||
"float3" => 12,
|
var buffer = Document.Buffers.FirstOrDefault(candidate =>
|
||||||
"float32" => 4,
|
string.Equals(candidate.BufferId, column.BufferId, StringComparison.Ordinal));
|
||||||
"int32" => 4,
|
if (buffer == null || buffer.ByteOffset != 0 || Document.Body == null) return false;
|
||||||
"uint32" => 4,
|
var columnEnd = column.ByteOffset + (long)column.ElementStride * column.ElementCount;
|
||||||
"uint8" => 1,
|
return column.ByteOffset >= 0 && columnEnd <= Document.Body.ByteSize;
|
||||||
_ => 0
|
}
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,13 +11,10 @@ namespace GameCult.Eve.UnityScene
|
||||||
public EveUnityFeedbackEvent(EveUnitySceneNode node)
|
public EveUnityFeedbackEvent(EveUnitySceneNode node)
|
||||||
{
|
{
|
||||||
EventId = Get(node, "eventId"); Kind = Get(node, "eventKind"); SubjectKey = Get(node, "subjectKey");
|
EventId = Get(node, "eventId"); Kind = Get(node, "eventKind"); SubjectKey = Get(node, "subjectKey");
|
||||||
ItemKey = Get(node, "itemKey"); Position = Get(node, "position"); Reason = Get(node, "reason");
|
ItemKey = Get(node, "itemKey"); Position = Get(node, "position");
|
||||||
FrameId = Long(node, "frameId", -1); ZoneIndex = Int(node, "zoneIndex", -1);
|
FrameId = Long(node, "frameId", -1); ZoneIndex = Int(node, "zoneIndex", -1);
|
||||||
SourceEntityIndex = Int(node, "sourceEntityIndex", -1); TargetEntityIndex = Int(node, "targetEntityIndex", -1);
|
SourceEntityIndex = Int(node, "sourceEntityIndex", -1); TargetEntityIndex = Int(node, "targetEntityIndex", -1);
|
||||||
PickupIndex = Int(node, "pickupIndex", -1); ScalarValue = Double(node, "scalarValue", 0);
|
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 EventId { get; }
|
||||||
public string Kind { get; }
|
public string Kind { get; }
|
||||||
|
|
@ -29,11 +26,7 @@ namespace GameCult.Eve.UnityScene
|
||||||
public string SubjectKey { get; }
|
public string SubjectKey { get; }
|
||||||
public string ItemKey { get; }
|
public string ItemKey { get; }
|
||||||
public string Position { get; }
|
public string Position { get; }
|
||||||
public string Reason { get; }
|
|
||||||
public double ScalarValue { 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 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 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;
|
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,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
@ -17,26 +16,13 @@ namespace GameCult.Eve.UnityScene
|
||||||
UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType);
|
UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IEveUnityNativeAssetMetadataProvider
|
public sealed class EveUnityGameObjectPlayableWorldSceneSink : IEveUnityPlayableWorldSceneSink
|
||||||
{
|
|
||||||
bool TryResolveAssetMetadata(
|
|
||||||
EveUnityPlayableWorldAssetBinding asset,
|
|
||||||
out IReadOnlyDictionary<string, string> metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class EveUnityGameObjectPlayableWorldSceneSink :
|
|
||||||
IEveUnityPlayableWorldSceneSink,
|
|
||||||
IEveUnityEntityGenerationSink,
|
|
||||||
IEveUnityPresentedEntityRegistry,
|
|
||||||
IDisposable
|
|
||||||
{
|
{
|
||||||
private readonly Transform _root;
|
private readonly Transform _root;
|
||||||
private readonly IEveUnityGameObjectAssetProvider _assetProvider;
|
private readonly IEveUnityGameObjectAssetProvider _assetProvider;
|
||||||
private readonly Dictionary<string, GameObject> _instances = new Dictionary<string, GameObject>(StringComparer.Ordinal);
|
private readonly Dictionary<string, GameObject> _instances = new Dictionary<string, GameObject>(StringComparer.Ordinal);
|
||||||
private IReadOnlyDictionary<string, EveUnityPresentedEntityHandle> _presentedById =
|
private string _subjectEntityId = "";
|
||||||
new Dictionary<string, EveUnityPresentedEntityHandle>(StringComparer.Ordinal);
|
private bool _subjectVisible = true;
|
||||||
private IReadOnlyDictionary<int, EveUnityPresentedEntityHandle> _presentedByIndex =
|
|
||||||
new Dictionary<int, EveUnityPresentedEntityHandle>();
|
|
||||||
|
|
||||||
public EveUnityGameObjectPlayableWorldSceneSink(
|
public EveUnityGameObjectPlayableWorldSceneSink(
|
||||||
Transform root,
|
Transform root,
|
||||||
|
|
@ -47,19 +33,15 @@ namespace GameCult.Eve.UnityScene
|
||||||
}
|
}
|
||||||
|
|
||||||
public int ActiveEntityCount => _instances.Count;
|
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)
|
public void ConfigureWorld(EveUnityPlayableWorldProjection world)
|
||||||
{
|
{
|
||||||
if (world == null) throw new ArgumentNullException(nameof(world));
|
if (world == null) throw new ArgumentNullException(nameof(world));
|
||||||
if (!string.IsNullOrWhiteSpace(world.WorldRootId))
|
if (!string.IsNullOrWhiteSpace(world.WorldRootId))
|
||||||
_root.name = world.WorldRootId;
|
_root.name = world.WorldRootId;
|
||||||
|
_subjectEntityId = world.PlayerEntityId;
|
||||||
|
_subjectVisible = world.SubjectVisible;
|
||||||
|
ApplySubjectVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpsertEntity(EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset)
|
public void UpsertEntity(EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset)
|
||||||
|
|
@ -87,90 +69,7 @@ namespace GameCult.Eve.UnityScene
|
||||||
}
|
}
|
||||||
|
|
||||||
ApplyEntity(instance, entity, asset);
|
ApplyEntity(instance, entity, asset);
|
||||||
}
|
ApplySubjectVisibility(instance, entity.EntityId);
|
||||||
|
|
||||||
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)
|
public void RemoveEntity(string entityId)
|
||||||
|
|
@ -187,33 +86,14 @@ namespace GameCult.Eve.UnityScene
|
||||||
DestroyInstance(instance);
|
DestroyInstance(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
private GameObject InstantiateEntity(EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset)
|
||||||
{
|
{
|
||||||
foreach (var instance in _instances.Values)
|
|
||||||
if (instance != null)
|
|
||||||
{
|
|
||||||
instance.transform.SetParent(null, false);
|
|
||||||
instance.SetActive(false);
|
|
||||||
DestroyInstance(instance);
|
|
||||||
}
|
|
||||||
_instances.Clear();
|
|
||||||
_presentedById = new Dictionary<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);
|
var prefab = _assetProvider.ResolvePrefab(asset);
|
||||||
GameObject instance;
|
GameObject instance;
|
||||||
if (prefab != null)
|
if (prefab != null)
|
||||||
{
|
{
|
||||||
instance = new GameObject(entity.EntityId);
|
instance = new GameObject(entity.EntityId);
|
||||||
instance.transform.SetParent(parent, false);
|
instance.transform.SetParent(_root, false);
|
||||||
var visual = UnityEngine.Object.Instantiate(prefab, instance.transform);
|
var visual = UnityEngine.Object.Instantiate(prefab, instance.transform);
|
||||||
visual.transform.localPosition = Vector3.zero;
|
visual.transform.localPosition = Vector3.zero;
|
||||||
visual.transform.localRotation = Quaternion.identity;
|
visual.transform.localRotation = Quaternion.identity;
|
||||||
|
|
@ -223,8 +103,8 @@ namespace GameCult.Eve.UnityScene
|
||||||
instance = GameObject.CreatePrimitive(PrimitiveType.Capsule);
|
instance = GameObject.CreatePrimitive(PrimitiveType.Capsule);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (instance.transform.parent != parent)
|
if (instance.transform.parent != _root)
|
||||||
instance.transform.SetParent(parent, false);
|
instance.transform.SetParent(_root, false);
|
||||||
|
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
@ -239,7 +119,9 @@ namespace GameCult.Eve.UnityScene
|
||||||
: $"{entity.EntityId} ({entity.Label})";
|
: $"{entity.EntityId} ({entity.Label})";
|
||||||
instance.transform.localPosition = new Vector3(entity.PositionX, entity.PositionY, entity.PositionZ);
|
instance.transform.localPosition = new Vector3(entity.PositionX, entity.PositionY, entity.PositionZ);
|
||||||
instance.transform.localRotation = Quaternion.Euler(0f, entity.RotationY, 0f);
|
instance.transform.localRotation = Quaternion.Euler(0f, entity.RotationY, 0f);
|
||||||
instance.transform.localScale = Vector3.one * (entity.Radius > 0f ? entity.Radius : 1f);
|
if (entity.Radius > 0f &&
|
||||||
|
string.Equals(asset.PresentationKind, "unity-generated-placeholder", StringComparison.Ordinal))
|
||||||
|
instance.transform.localScale = Vector3.one * entity.Radius;
|
||||||
|
|
||||||
var marker = instance.GetComponent<EveUnityPlayableWorldEntityMarker>();
|
var marker = instance.GetComponent<EveUnityPlayableWorldEntityMarker>();
|
||||||
if (marker == null)
|
if (marker == null)
|
||||||
|
|
@ -264,6 +146,19 @@ namespace GameCult.Eve.UnityScene
|
||||||
else
|
else
|
||||||
UnityEngine.Object.DestroyImmediate(instance);
|
UnityEngine.Object.DestroyImmediate(instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ApplySubjectVisibility()
|
||||||
|
{
|
||||||
|
foreach (var pair in _instances)
|
||||||
|
ApplySubjectVisibility(pair.Value, pair.Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplySubjectVisibility(GameObject instance, string entityId)
|
||||||
|
{
|
||||||
|
if (instance == null || !string.Equals(entityId, _subjectEntityId, StringComparison.Ordinal))
|
||||||
|
return;
|
||||||
|
instance.SetActive(_subjectVisible);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class EveUnitySemanticEntityPresentation : MonoBehaviour
|
public sealed class EveUnitySemanticEntityPresentation : MonoBehaviour
|
||||||
|
|
|
||||||
|
|
@ -1,295 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Linq;
|
|
||||||
using GameCult.Eve.Surface;
|
|
||||||
using UnityEngine;
|
|
||||||
using UnityEngine.UIElements;
|
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace GameCult.Eve.UnityScene
|
|
||||||
{
|
|
||||||
public sealed class EveUnityInputActionBar : MonoBehaviour
|
|
||||||
{
|
|
||||||
private readonly Dictionary<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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
fileFormatVersion: 2
|
|
||||||
guid: bb62601554a846f39e46a6969c10eff7
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.Rendering;
|
|
||||||
using UnityEngine.Rendering.Universal;
|
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
|
|
@ -8,13 +6,11 @@ namespace GameCult.Eve.UnityScene
|
||||||
{
|
{
|
||||||
public interface IEveUnityCameraRenderPolicySource
|
public interface IEveUnityCameraRenderPolicySource
|
||||||
{
|
{
|
||||||
bool TryGetRenderChannelLayer(string channel, out int layer);
|
bool TryGetCameraCullingMask(string viewId, out int cullingMask);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class EveUnityPlayableWorldCameraRig : MonoBehaviour
|
public sealed class EveUnityPlayableWorldCameraRig : MonoBehaviour
|
||||||
{
|
{
|
||||||
private static EveUnityPlayableWorldCameraRig? _environmentOwner;
|
|
||||||
|
|
||||||
[SerializeField] private EveUnityPlayableWorldClientHost? host;
|
[SerializeField] private EveUnityPlayableWorldClientHost? host;
|
||||||
[SerializeField] private Transform? cameraTransform;
|
[SerializeField] private Transform? cameraTransform;
|
||||||
[SerializeField] private bool driveInLateUpdate = true;
|
[SerializeField] private bool driveInLateUpdate = true;
|
||||||
|
|
@ -23,49 +19,17 @@ namespace GameCult.Eve.UnityScene
|
||||||
[SerializeField] private float yawDegrees = 35f;
|
[SerializeField] private float yawDegrees = 35f;
|
||||||
[SerializeField] private float followDamping = 12f;
|
[SerializeField] private float followDamping = 12f;
|
||||||
private IEveUnityCameraRenderPolicySource? _renderPolicySource;
|
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
|
public EveUnityPlayableWorldClientHost? Host
|
||||||
{
|
{
|
||||||
get => host;
|
get => host;
|
||||||
set
|
set => host = value;
|
||||||
{
|
|
||||||
if (ReferenceEquals(host, value)) return;
|
|
||||||
ReleaseRig();
|
|
||||||
host = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Transform? CameraTransform
|
public Transform? CameraTransform
|
||||||
{
|
{
|
||||||
get => cameraTransform;
|
get => cameraTransform;
|
||||||
set
|
set => cameraTransform = value;
|
||||||
{
|
|
||||||
if (ReferenceEquals(cameraTransform, value)) return;
|
|
||||||
ReleaseRig();
|
|
||||||
cameraTransform = value;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEveUnityCameraRenderPolicySource? RenderPolicySource
|
public IEveUnityCameraRenderPolicySource? RenderPolicySource
|
||||||
|
|
@ -74,102 +38,34 @@ namespace GameCult.Eve.UnityScene
|
||||||
set => _renderPolicySource = value;
|
set => _renderPolicySource = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ReleaseRig()
|
|
||||||
{
|
|
||||||
ReleaseCamera();
|
|
||||||
RestoreAmbientEnvironment();
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ApplyRig(float deltaTime)
|
public bool ApplyRig(float deltaTime)
|
||||||
{
|
{
|
||||||
var resolvedHost = ResolveHost();
|
var resolvedHost = ResolveHost();
|
||||||
var activeWorld = resolvedHost?.ActiveWorld;
|
var activeWorld = resolvedHost?.ActiveWorld;
|
||||||
if (resolvedHost == null || activeWorld == null)
|
if (resolvedHost == null || activeWorld == null)
|
||||||
{
|
|
||||||
ReleaseCamera();
|
|
||||||
RestoreAmbientEnvironment();
|
|
||||||
return false;
|
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) &&
|
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 != "arpg.orbital-follow.v1" &&
|
||||||
activeWorld.CameraRig != "third-person-orbit")
|
activeWorld.CameraRig != "third-person-orbit")
|
||||||
{
|
{
|
||||||
ReleaseCamera();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var camera = cameraTransform != null ? cameraTransform : transform;
|
var camera = cameraTransform != null ? cameraTransform : transform;
|
||||||
var targetEntityId = string.IsNullOrWhiteSpace(activeWorld.CameraTargetEntityId)
|
var targetMarker = FindEntityMarker(resolvedHost, activeWorld.CameraTargetEntityId);
|
||||||
? activeWorld.PlayerEntityId
|
if (targetMarker == null)
|
||||||
: activeWorld.CameraTargetEntityId;
|
|
||||||
var player = FindEntity(resolvedHost, targetEntityId);
|
|
||||||
if (player == null)
|
|
||||||
{
|
|
||||||
ReleaseCamera();
|
|
||||||
return false;
|
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>();
|
var cameraComponent = camera.GetComponent<Camera>();
|
||||||
if (postProcessProfile != null && cameraComponent == null)
|
if (cameraComponent != null &&
|
||||||
|
_renderPolicySource != null &&
|
||||||
|
_renderPolicySource.TryGetCameraCullingMask(activeWorld.ViewId, out var cullingMask))
|
||||||
{
|
{
|
||||||
ReleaseRig();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!ApplyColorGradingSpace(activeWorld))
|
|
||||||
{
|
|
||||||
ReleaseRig();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ApplyAmbientEnvironment(activeWorld, cameraComponent, skybox, reflection, postProcessProfile);
|
|
||||||
ApplyKeyLight(activeWorld);
|
|
||||||
if (cameraComponent != null && _renderPolicySource != null)
|
|
||||||
{
|
|
||||||
var cullingMask = cameraComponent.cullingMask;
|
|
||||||
foreach (var channel in activeWorld.ExcludedRenderChannels)
|
|
||||||
{
|
|
||||||
if (_renderPolicySource.TryGetRenderChannelLayer(channel, out var layer) &&
|
|
||||||
layer >= 0 && layer < 32)
|
|
||||||
cullingMask &= ~(1 << layer);
|
|
||||||
}
|
|
||||||
cameraComponent.cullingMask = cullingMask;
|
cameraComponent.cullingMask = cullingMask;
|
||||||
}
|
}
|
||||||
if (activeWorld.CameraRig == "planar.top-down-follow.v1")
|
var bounds = CalculateVisualBounds(targetMarker);
|
||||||
return ApplyPlanarTopDown(activeWorld, camera, cameraComponent, player.position, deltaTime);
|
var target = bounds?.center ?? targetMarker.transform.position;
|
||||||
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 radius = bounds?.extents.magnitude ?? 0f;
|
||||||
var verticalFov = cameraComponent != null ? cameraComponent.fieldOfView : 60f;
|
var verticalFov = cameraComponent != null ? cameraComponent.fieldOfView : 60f;
|
||||||
var framingDistance = radius > 0f
|
var framingDistance = radius > 0f
|
||||||
|
|
@ -188,524 +84,23 @@ namespace GameCult.Eve.UnityScene
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ApplyPlanarTopDown(
|
private static Bounds? CalculateVisualBounds(EveUnityPlayableWorldEntityMarker target)
|
||||||
EveUnityPlayableWorldProjection world,
|
|
||||||
Transform camera,
|
|
||||||
Camera? cameraComponent,
|
|
||||||
Vector3 target,
|
|
||||||
float deltaTime)
|
|
||||||
{
|
|
||||||
var resolvedDistance = world.CameraDistance;
|
|
||||||
var verticalFov = world.CameraVerticalFieldOfViewDegrees;
|
|
||||||
if (cameraComponent != null)
|
|
||||||
{
|
|
||||||
cameraComponent.orthographic = false;
|
|
||||||
cameraComponent.lensShift = Vector2.zero;
|
|
||||||
cameraComponent.fieldOfView = verticalFov;
|
|
||||||
cameraComponent.nearClipPlane = world.CameraNearClipPlane;
|
|
||||||
cameraComponent.farClipPlane = world.CameraFarClipPlane;
|
|
||||||
}
|
|
||||||
var aspect = cameraComponent != null ? cameraComponent.aspect : 16f / 9f;
|
|
||||||
var verticalHalfExtent = resolvedDistance * Mathf.Tan(verticalFov * 0.5f * Mathf.Deg2Rad);
|
|
||||||
var horizontalHalfExtent = verticalHalfExtent * Mathf.Max(0.01f, aspect);
|
|
||||||
var screenOffset = new Vector3(
|
|
||||||
(Mathf.Clamp01(world.CameraTargetScreenX) - 0.5f) * 2f * horizontalHalfExtent,
|
|
||||||
0f,
|
|
||||||
(Mathf.Clamp01(world.CameraTargetScreenY) - 0.5f) * 2f * verticalHalfExtent);
|
|
||||||
var desiredPosition = target + (Vector3.up * resolvedDistance) - screenOffset;
|
|
||||||
var damping = Mathf.Max(0f, world.CameraPositionDamping);
|
|
||||||
var t = deltaTime <= 0f || damping <= 0f
|
|
||||||
? 1f
|
|
||||||
: 1f - Mathf.Exp(-damping * deltaTime);
|
|
||||||
camera.position = Vector3.Lerp(camera.position, desiredPosition, t);
|
|
||||||
camera.rotation = Quaternion.LookRotation(Vector3.down, Vector3.forward);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool ApplyEntityForwardFollow(
|
|
||||||
EveUnityPlayableWorldProjection world,
|
|
||||||
EveUnityPlayableWorldClientHost host,
|
|
||||||
Transform camera,
|
|
||||||
Camera? cameraComponent,
|
|
||||||
Transform target,
|
|
||||||
float deltaTime)
|
|
||||||
{
|
|
||||||
var resolvedDistance = world.CameraDistance;
|
|
||||||
var verticalFov = world.CameraVerticalFieldOfViewDegrees;
|
|
||||||
if (cameraComponent != null)
|
|
||||||
{
|
|
||||||
cameraComponent.orthographic = false;
|
|
||||||
cameraComponent.lensShift = Vector2.zero;
|
|
||||||
cameraComponent.fieldOfView = verticalFov;
|
|
||||||
cameraComponent.nearClipPlane = world.CameraNearClipPlane;
|
|
||||||
cameraComponent.farClipPlane = world.CameraFarClipPlane;
|
|
||||||
}
|
|
||||||
|
|
||||||
var forward = target.forward;
|
|
||||||
forward.y = 0f;
|
|
||||||
if (forward.sqrMagnitude <= 0.0001f)
|
|
||||||
forward = Vector3.forward;
|
|
||||||
forward.Normalize();
|
|
||||||
var rotation = Quaternion.LookRotation(forward, Vector3.up);
|
|
||||||
var aspect = cameraComponent != null ? cameraComponent.aspect : 16f / 9f;
|
|
||||||
var verticalHalfExtent = resolvedDistance * Mathf.Tan(verticalFov * 0.5f * Mathf.Deg2Rad);
|
|
||||||
var horizontalHalfExtent = verticalHalfExtent * Mathf.Max(0.01f, aspect);
|
|
||||||
var horizontalOffset = (Mathf.Clamp01(world.CameraTargetScreenX) - 0.5f) * 2f * horizontalHalfExtent;
|
|
||||||
var verticalOffset = (Mathf.Clamp01(world.CameraTargetScreenY) - 0.5f) * 2f * verticalHalfExtent;
|
|
||||||
if (string.Equals(world.CameraLookAt, "aim.convergence-point.v1", System.StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
var aim = EveUnityAimPresentation.Find(host.ActiveProjection);
|
|
||||||
if (aim == null || !aim.TryResolveViewPoint(host, out var aimPoint)) return false;
|
|
||||||
var targetToAim = aimPoint - target.position;
|
|
||||||
var targetToAimDistanceSquared = targetToAim.sqrMagnitude;
|
|
||||||
var screenOffsetSquared = horizontalOffset * horizontalOffset + verticalOffset * verticalOffset;
|
|
||||||
if (targetToAimDistanceSquared <= screenOffsetSquared + 0.0001f) return false;
|
|
||||||
var targetToAimDirection = targetToAim.normalized;
|
|
||||||
var opticalDepthDelta = Mathf.Sqrt(targetToAimDistanceSquared - screenOffsetSquared);
|
|
||||||
var cameraLocalTargetToAim = new Vector3(
|
|
||||||
-horizontalOffset,
|
|
||||||
-verticalOffset,
|
|
||||||
opticalDepthDelta);
|
|
||||||
rotation = Quaternion.LookRotation(targetToAimDirection, Vector3.up) *
|
|
||||||
Quaternion.FromToRotation(cameraLocalTargetToAim.normalized, Vector3.forward);
|
|
||||||
}
|
|
||||||
var right = rotation * Vector3.right;
|
|
||||||
var up = rotation * Vector3.up;
|
|
||||||
var opticalForward = rotation * Vector3.forward;
|
|
||||||
var desiredPosition = target.position -
|
|
||||||
(opticalForward * resolvedDistance) -
|
|
||||||
(right * horizontalOffset) -
|
|
||||||
(up * verticalOffset);
|
|
||||||
var damping = Mathf.Max(0f, world.CameraPositionDamping);
|
|
||||||
var t = deltaTime <= 0f || damping <= 0f
|
|
||||||
? 1f
|
|
||||||
: 1f - Mathf.Exp(-damping * deltaTime);
|
|
||||||
camera.position = Vector3.Lerp(camera.position, desiredPosition, t);
|
|
||||||
camera.rotation = rotation;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool HasValidFramedFollowContract(EveUnityPlayableWorldProjection world) =>
|
|
||||||
world.CameraDistance > 0f &&
|
|
||||||
world.CameraVerticalFieldOfViewDegrees > 0f &&
|
|
||||||
world.CameraVerticalFieldOfViewDegrees < 180f &&
|
|
||||||
world.CameraTargetScreenX >= 0f && world.CameraTargetScreenX <= 1f &&
|
|
||||||
world.CameraTargetScreenY >= 0f && world.CameraTargetScreenY <= 1f &&
|
|
||||||
world.CameraPositionDamping >= 0f &&
|
|
||||||
world.CameraNearClipPlane > 0f &&
|
|
||||||
world.CameraFarClipPlane > world.CameraNearClipPlane;
|
|
||||||
|
|
||||||
private static bool TryResolveEnvironment(
|
|
||||||
EveUnityPlayableWorldClientHost host,
|
|
||||||
EveUnityPlayableWorldProjection world,
|
|
||||||
out Material? skybox,
|
|
||||||
out Cubemap? reflection,
|
|
||||||
out VolumeProfile? postProcessProfile)
|
|
||||||
{
|
|
||||||
skybox = null;
|
|
||||||
reflection = null;
|
|
||||||
postProcessProfile = null;
|
|
||||||
if (!IsFinite(world.AmbientLightR) ||
|
|
||||||
!IsFinite(world.AmbientLightG) ||
|
|
||||||
!IsFinite(world.AmbientLightB) ||
|
|
||||||
!IsFinite(world.AmbientLightIntensity) ||
|
|
||||||
!IsFinite(world.ReflectionIntensity) ||
|
|
||||||
!IsFinite(world.KeyLightDirectionX) ||
|
|
||||||
!IsFinite(world.KeyLightDirectionY) ||
|
|
||||||
!IsFinite(world.KeyLightDirectionZ) ||
|
|
||||||
!IsFinite(world.KeyLightColorR) ||
|
|
||||||
!IsFinite(world.KeyLightColorG) ||
|
|
||||||
!IsFinite(world.KeyLightColorB) ||
|
|
||||||
!IsFinite(world.KeyLightIntensity) ||
|
|
||||||
!IsFinite(world.TemporalHistoryBlend) ||
|
|
||||||
!IsFinite(world.TemporalJitterScale) ||
|
|
||||||
!IsFinite(world.TemporalSharpening) ||
|
|
||||||
(!string.IsNullOrWhiteSpace(world.CameraReconstruction) &&
|
|
||||||
!string.Equals(world.CameraReconstruction, "temporal-reprojection.v1", System.StringComparison.Ordinal)) ||
|
|
||||||
(!string.IsNullOrWhiteSpace(world.ColorGradingSpace) &&
|
|
||||||
!string.Equals(world.ColorGradingSpace, "hdr-before-tonemap.v1", System.StringComparison.Ordinal)) ||
|
|
||||||
(string.Equals(world.CameraReconstruction, "temporal-reprojection.v1", System.StringComparison.Ordinal) &&
|
|
||||||
(world.TemporalHistoryBlend < 0f || world.TemporalHistoryBlend > 1f ||
|
|
||||||
world.TemporalJitterScale < 0f || world.TemporalJitterScale > 1f ||
|
|
||||||
world.TemporalSharpening < 0f || world.TemporalSharpening > 1f ||
|
|
||||||
!IsTemporalQuality(world.TemporalQuality))))
|
|
||||||
return false;
|
|
||||||
var assets = host.NativeAssetProvider;
|
|
||||||
if (!string.IsNullOrWhiteSpace(world.SkyboxAssetRef))
|
|
||||||
{
|
|
||||||
skybox = assets?.ResolveAsset(
|
|
||||||
new EveUnityPlayableWorldAssetBinding(
|
|
||||||
world.SkyboxAssetRef,
|
|
||||||
"",
|
|
||||||
"provider-asset-ref"),
|
|
||||||
typeof(Material)) as Material;
|
|
||||||
if (skybox == null || skybox.shader == null || !skybox.shader.isSupported)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!string.IsNullOrWhiteSpace(world.ReflectionAssetRef))
|
|
||||||
{
|
|
||||||
reflection = assets?.ResolveAsset(
|
|
||||||
new EveUnityPlayableWorldAssetBinding(
|
|
||||||
world.ReflectionAssetRef,
|
|
||||||
"",
|
|
||||||
"provider-asset-ref"),
|
|
||||||
typeof(Cubemap)) as Cubemap;
|
|
||||||
if (reflection == null)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (!string.IsNullOrWhiteSpace(world.PostProcessProfileAssetRef))
|
|
||||||
{
|
|
||||||
postProcessProfile = assets?.ResolveAsset(
|
|
||||||
new EveUnityPlayableWorldAssetBinding(
|
|
||||||
world.PostProcessProfileAssetRef,
|
|
||||||
"",
|
|
||||||
"provider-asset-ref"),
|
|
||||||
typeof(VolumeProfile)) as VolumeProfile;
|
|
||||||
if (postProcessProfile == null)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool ApplyColorGradingSpace(EveUnityPlayableWorldProjection world)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(world.ColorGradingSpace))
|
|
||||||
{
|
|
||||||
RestoreColorGradingSpace();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
var asset = GraphicsSettings.currentRenderPipeline as UniversalRenderPipelineAsset;
|
|
||||||
if (asset == null ||
|
|
||||||
!string.Equals(world.ColorGradingSpace, "hdr-before-tonemap.v1", System.StringComparison.Ordinal))
|
|
||||||
return false;
|
|
||||||
if (!ReferenceEquals(_colorGradingPipelineAsset, asset))
|
|
||||||
{
|
|
||||||
RestoreColorGradingSpace();
|
|
||||||
_colorGradingPipelineAsset = asset;
|
|
||||||
_previousColorGradingMode = asset.colorGradingMode;
|
|
||||||
}
|
|
||||||
asset.colorGradingMode = ColorGradingMode.HighDynamicRange;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RestoreColorGradingSpace()
|
|
||||||
{
|
|
||||||
if (_colorGradingPipelineAsset == null) return;
|
|
||||||
_colorGradingPipelineAsset.colorGradingMode = _previousColorGradingMode;
|
|
||||||
_colorGradingPipelineAsset = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyKeyLight(EveUnityPlayableWorldProjection world)
|
|
||||||
{
|
|
||||||
var direction = new Vector3(
|
|
||||||
world.KeyLightDirectionX,
|
|
||||||
world.KeyLightDirectionY,
|
|
||||||
world.KeyLightDirectionZ);
|
|
||||||
if (world.KeyLightIntensity <= 0f || direction.sqrMagnitude <= 0.000001f)
|
|
||||||
{
|
|
||||||
ReleaseKeyLight();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_keyLightObject == null)
|
|
||||||
{
|
|
||||||
_keyLightObject = new GameObject("Eve World Key Light");
|
|
||||||
_keyLightObject.transform.SetParent(transform, worldPositionStays: false);
|
|
||||||
var created = _keyLightObject.AddComponent<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)
|
|
||||||
{
|
|
||||||
_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
|
|
||||||
{
|
|
||||||
_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)
|
|
||||||
{
|
{
|
||||||
|
var renderers = target.GetComponentsInChildren<Renderer>(includeInactive: false);
|
||||||
Bounds? bounds = null;
|
Bounds? bounds = null;
|
||||||
foreach (var root in new[] { player })
|
foreach (var visual in renderers)
|
||||||
{
|
{
|
||||||
foreach (var visual in root.GetComponentsInChildren<Renderer>(includeInactive: false))
|
if (visual == null || !visual.enabled)
|
||||||
|
continue;
|
||||||
|
if (bounds.HasValue)
|
||||||
{
|
{
|
||||||
if (visual == null || !visual.enabled ||
|
var combined = bounds.Value;
|
||||||
(cullingMask & (1 << visual.gameObject.layer)) == 0)
|
combined.Encapsulate(visual.bounds);
|
||||||
continue;
|
bounds = combined;
|
||||||
if (visual is LineRenderer || visual is TrailRenderer ||
|
}
|
||||||
string.Equals(visual.GetType().Name, "ParticleSystemRenderer", System.StringComparison.Ordinal))
|
else
|
||||||
continue;
|
{
|
||||||
if (bounds.HasValue)
|
bounds = visual.bounds;
|
||||||
{
|
|
||||||
var combined = bounds.Value;
|
|
||||||
combined.Encapsulate(visual.bounds);
|
|
||||||
bounds = combined;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
bounds = visual.bounds;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bounds;
|
return bounds;
|
||||||
|
|
@ -717,10 +112,6 @@ namespace GameCult.Eve.UnityScene
|
||||||
ApplyRig(Time.deltaTime);
|
ApplyRig(Time.deltaTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDisable() => ReleaseRig();
|
|
||||||
|
|
||||||
private void OnDestroy() => ReleaseRig();
|
|
||||||
|
|
||||||
private EveUnityPlayableWorldClientHost? ResolveHost()
|
private EveUnityPlayableWorldClientHost? ResolveHost()
|
||||||
{
|
{
|
||||||
if (host != null)
|
if (host != null)
|
||||||
|
|
@ -730,23 +121,18 @@ namespace GameCult.Eve.UnityScene
|
||||||
return host;
|
return host;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Transform? FindEntity(
|
private static EveUnityPlayableWorldEntityMarker? FindEntityMarker(
|
||||||
EveUnityPlayableWorldClientHost host,
|
EveUnityPlayableWorldClientHost host,
|
||||||
string playerEntityId)
|
string entityId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(playerEntityId))
|
if (string.IsNullOrWhiteSpace(entityId))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if (host.PresentedEntities?.CurrentGeneration != null)
|
var markers = host.SceneRoot.GetComponentsInChildren<EveUnityPlayableWorldEntityMarker>(includeInactive: true);
|
||||||
return host.PresentedEntities.TryGetByEntityId(playerEntityId, out var presented)
|
|
||||||
? presented.Transform
|
|
||||||
: null;
|
|
||||||
|
|
||||||
var markers = host.SceneRoot.GetComponentsInChildren<EveUnityPlayableWorldEntityMarker>();
|
|
||||||
foreach (var marker in markers)
|
foreach (var marker in markers)
|
||||||
{
|
{
|
||||||
if (marker != null && marker.EntityId == playerEntityId)
|
if (marker != null && marker.EntityId == entityId)
|
||||||
return marker.transform;
|
return marker;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
@ -30,378 +29,13 @@ namespace GameCult.Eve.UnityScene
|
||||||
|
|
||||||
public EveUnityPlayableWorldPresentation? LastPresentation { get; private set; }
|
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)
|
public void ConfigureProvider(MonoBehaviour providerComponent)
|
||||||
{
|
{
|
||||||
UnbindNavigation();
|
|
||||||
provider = providerComponent != null ? providerComponent : throw new ArgumentNullException(nameof(providerComponent));
|
provider = providerComponent != null ? providerComponent : throw new ArgumentNullException(nameof(providerComponent));
|
||||||
providerSurfaceDocuments = provider;
|
providerSurfaceDocuments = provider;
|
||||||
commandSink = provider;
|
commandSink = provider;
|
||||||
assetManifestDocuments = provider;
|
assetManifestDocuments = provider;
|
||||||
receiptSource = 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()
|
public EveUnityPlayableWorldPresentation Mount()
|
||||||
|
|
@ -557,21 +191,4 @@ namespace GameCult.Eve.UnityScene
|
||||||
return null;
|
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,7 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using GameCult.Eve.Surface;
|
using GameCult.Eve.Surface;
|
||||||
using GameCult.Eve.UnityScene.Fields;
|
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
@ -22,7 +21,6 @@ namespace GameCult.Eve.UnityScene
|
||||||
[SerializeField] private bool renderShotTrajectories = true;
|
[SerializeField] private bool renderShotTrajectories = true;
|
||||||
|
|
||||||
private float _nextRefreshAt;
|
private float _nextRefreshAt;
|
||||||
private object? _cameraOwner;
|
|
||||||
|
|
||||||
public EveUnityPlayableWorldRuntime? Runtime { get; private set; }
|
public EveUnityPlayableWorldRuntime? Runtime { get; private set; }
|
||||||
|
|
||||||
|
|
@ -33,16 +31,8 @@ namespace GameCult.Eve.UnityScene
|
||||||
public EveUnityPlayableWorldPresentation? LastPresentation => Runtime?.LastPresentation;
|
public EveUnityPlayableWorldPresentation? LastPresentation => Runtime?.LastPresentation;
|
||||||
|
|
||||||
public EveUnitySceneCommandReceipt? LastReceipt => Runtime?.LastReceipt;
|
public EveUnitySceneCommandReceipt? LastReceipt => Runtime?.LastReceipt;
|
||||||
public IEveUnityPresentedEntityRegistry? PresentedEntities => Runtime?.PresentedEntities;
|
|
||||||
|
|
||||||
public EveInputCapabilityDocument? InputCapability =>
|
|
||||||
(providerSurfaceDocuments as IEveUnityInputCapabilitySource)?.CurrentInputCapability;
|
|
||||||
|
|
||||||
public long ActiveVersion => Runtime?.ActiveVersion ?? 0;
|
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<EveUnityFeedbackEvent>? FeedbackAvailable;
|
||||||
public event Action<EveUnityShotReceipt>? ShotAvailable;
|
public event Action<EveUnityShotReceipt>? ShotAvailable;
|
||||||
|
|
@ -98,57 +88,17 @@ namespace GameCult.Eve.UnityScene
|
||||||
var feedbackEffects = GetComponent<EveUnityFeedbackEffectRenderer>();
|
var feedbackEffects = GetComponent<EveUnityFeedbackEffectRenderer>();
|
||||||
if (feedbackEffects == null) feedbackEffects = gameObject.AddComponent<EveUnityFeedbackEffectRenderer>();
|
if (feedbackEffects == null) feedbackEffects = gameObject.AddComponent<EveUnityFeedbackEffectRenderer>();
|
||||||
feedbackEffects.Bind(this, Runtime.GameObjectAssetProvider);
|
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 thermal = GetComponent<EveUnityThermalPresenter>();
|
||||||
var thermalHud = GetComponent<EveUnityThermalHudSink>();
|
var thermalHud = GetComponent<EveUnityThermalHudSink>();
|
||||||
if (thermalHud == null) thermalHud = gameObject.AddComponent<EveUnityThermalHudSink>();
|
if (thermalHud == null) thermalHud = gameObject.AddComponent<EveUnityThermalHudSink>();
|
||||||
if (thermal == null) thermal = gameObject.AddComponent<EveUnityThermalPresenter>();
|
if (thermal == null) thermal = gameObject.AddComponent<EveUnityThermalPresenter>();
|
||||||
thermal.Bind(this, Runtime.GameObjectAssetProvider as IEveUnityNativeAssetProvider);
|
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();
|
var presentation = Runtime.Connect();
|
||||||
ConnectionEpoch++;
|
|
||||||
_nextRefreshAt = Time.unscaledTime + Math.Max(0.01f, refreshIntervalSeconds);
|
_nextRefreshAt = Time.unscaledTime + Math.Max(0.01f, refreshIntervalSeconds);
|
||||||
return presentation;
|
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()
|
public EveUnityPlayableWorldPresentation Refresh()
|
||||||
{
|
{
|
||||||
RefreshProviderSources();
|
RefreshProviderSources();
|
||||||
|
|
@ -178,16 +128,6 @@ namespace GameCult.Eve.UnityScene
|
||||||
return RequireRuntime().SubmitMoveVectorIntent(entityId, directionX, directionY, scalarValue, issuedAt);
|
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(
|
public EveSurfaceCommandRequest SubmitFocusIntent(
|
||||||
string entityId,
|
string entityId,
|
||||||
DateTimeOffset? issuedAt = null)
|
DateTimeOffset? issuedAt = null)
|
||||||
|
|
@ -211,63 +151,8 @@ namespace GameCult.Eve.UnityScene
|
||||||
return RequireRuntime().SubmitActionIntent(entityId, actionId, issuedAt);
|
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()
|
public void Disconnect()
|
||||||
{
|
{
|
||||||
if (_cameraOwner is EveUnityPlayableWorldCameraRig rig)
|
|
||||||
rig.ReleaseRig();
|
|
||||||
if (Runtime != null)
|
if (Runtime != null)
|
||||||
{
|
{
|
||||||
Runtime.FeedbackAvailable -= OnFeedbackAvailable;
|
Runtime.FeedbackAvailable -= OnFeedbackAvailable;
|
||||||
|
|
@ -275,8 +160,6 @@ namespace GameCult.Eve.UnityScene
|
||||||
}
|
}
|
||||||
Runtime?.Dispose();
|
Runtime?.Dispose();
|
||||||
Runtime = null;
|
Runtime = null;
|
||||||
_cameraOwner = null;
|
|
||||||
ActiveCameraTransform = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnFeedbackAvailable(EveUnityFeedbackEvent value) => FeedbackAvailable?.Invoke(value);
|
private void OnFeedbackAvailable(EveUnityFeedbackEvent value) => FeedbackAvailable?.Invoke(value);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using GameCult.Eve.Surface;
|
using GameCult.Eve.Surface;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
|
|
@ -8,11 +6,6 @@ using UnityEngine;
|
||||||
|
|
||||||
namespace GameCult.Eve.UnityScene
|
namespace GameCult.Eve.UnityScene
|
||||||
{
|
{
|
||||||
public interface IEveUnityInputCapabilitySource
|
|
||||||
{
|
|
||||||
EveInputCapabilityDocument CurrentInputCapability { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class EveUnityPlayableWorldInputDriver : MonoBehaviour
|
public sealed class EveUnityPlayableWorldInputDriver : MonoBehaviour
|
||||||
{
|
{
|
||||||
[SerializeField] private EveUnityPlayableWorldClientHost? host;
|
[SerializeField] private EveUnityPlayableWorldClientHost? host;
|
||||||
|
|
@ -21,35 +14,17 @@ namespace GameCult.Eve.UnityScene
|
||||||
[SerializeField] private string horizontalAxis = "Horizontal";
|
[SerializeField] private string horizontalAxis = "Horizontal";
|
||||||
[SerializeField] private string verticalAxis = "Vertical";
|
[SerializeField] private string verticalAxis = "Vertical";
|
||||||
[SerializeField] private string actionButton = "Fire1";
|
[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 deadZone = 0.01f;
|
||||||
[SerializeField] private float commandIntervalSeconds = 0.05f;
|
[SerializeField] private float commandIntervalSeconds = 0.05f;
|
||||||
[SerializeField] private bool submitZeroOnRelease = true;
|
[SerializeField] private bool submitZeroOnRelease = true;
|
||||||
|
|
||||||
private float _nextCommandAt;
|
private float _nextCommandAt;
|
||||||
private bool _wasMoving;
|
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
|
public EveUnityPlayableWorldClientHost? Host
|
||||||
{
|
{
|
||||||
get => host;
|
get => host;
|
||||||
set
|
set => host = value;
|
||||||
{
|
|
||||||
if (ReferenceEquals(host, value)) return;
|
|
||||||
ReleaseHeldActions();
|
|
||||||
host = value;
|
|
||||||
ResetLookState();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Transform? CameraTransform
|
public Transform? CameraTransform
|
||||||
|
|
@ -64,7 +39,7 @@ namespace GameCult.Eve.UnityScene
|
||||||
DateTimeOffset? issuedAt = null)
|
DateTimeOffset? issuedAt = null)
|
||||||
{
|
{
|
||||||
var resolvedHost = ResolveHost();
|
var resolvedHost = ResolveHost();
|
||||||
if (resolvedHost == null || resolvedHost.ActiveWorld == null)
|
if (resolvedHost == null || resolvedHost.ActiveWorld == null || !resolvedHost.ActiveWorld.MovementEnabled)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var movement = EveUnityPlayableWorldMoveVector.FromCameraRelativeInput(
|
var movement = EveUnityPlayableWorldMoveVector.FromCameraRelativeInput(
|
||||||
|
|
@ -96,265 +71,27 @@ namespace GameCult.Eve.UnityScene
|
||||||
issuedAt);
|
issuedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
public EveSurfaceCommandRequest? SubmitPrimaryAction(string actionId = "")
|
public EveSurfaceCommandRequest? SubmitPrimaryAction(string actionId = "primary")
|
||||||
{
|
{
|
||||||
var resolvedHost = ResolveHost();
|
var resolvedHost = ResolveHost();
|
||||||
if (resolvedHost?.ActiveWorld == null)
|
if (resolvedHost?.ActiveWorld == null)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var resolvedActionId = string.IsNullOrWhiteSpace(actionId)
|
return resolvedHost.SubmitActionIntent(
|
||||||
? ResolvePrimaryActionId(resolvedHost.InputCapability)
|
|
||||||
: actionId;
|
|
||||||
if (string.IsNullOrWhiteSpace(resolvedActionId))
|
|
||||||
return null;
|
|
||||||
|
|
||||||
return resolvedHost.SubmitAdvertisedActionIntent(
|
|
||||||
resolvedHost.ActiveWorld.PlayerEntityId,
|
resolvedHost.ActiveWorld.PlayerEntityId,
|
||||||
resolvedActionId);
|
string.IsNullOrWhiteSpace(actionId) ? "primary" : actionId);
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
private void Update()
|
||||||
{
|
{
|
||||||
if (!driveInUpdate)
|
if (!driveInUpdate || Time.unscaledTime < _nextCommandAt)
|
||||||
return;
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(lookHorizontalAxis))
|
|
||||||
QueueLookInput(Input.GetAxisRaw(lookHorizontalAxis));
|
|
||||||
if (Time.unscaledTime < _nextCommandAt)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_nextCommandAt = Time.unscaledTime + Math.Max(0.01f, commandIntervalSeconds);
|
_nextCommandAt = Time.unscaledTime + Math.Max(0.01f, commandIntervalSeconds);
|
||||||
SubmitMoveVectorInput(Input.GetAxisRaw(horizontalAxis), Input.GetAxisRaw(verticalAxis));
|
SubmitMoveVectorInput(Input.GetAxisRaw(horizontalAxis), Input.GetAxisRaw(verticalAxis));
|
||||||
SubmitPendingLookInput();
|
|
||||||
SubmitChangedAdvertisedActions();
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(actionButton) && Input.GetButtonDown(actionButton))
|
if (!string.IsNullOrWhiteSpace(actionButton) && Input.GetButtonDown(actionButton))
|
||||||
SubmitPrimaryAction();
|
SubmitPrimaryAction(actionButton);
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
private EveUnityPlayableWorldClientHost? ResolveHost()
|
||||||
|
|
@ -365,61 +102,6 @@ namespace GameCult.Eve.UnityScene
|
||||||
host = GetComponent<EveUnityPlayableWorldClientHost>();
|
host = GetComponent<EveUnityPlayableWorldClientHost>();
|
||||||
return host;
|
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
|
public readonly struct EveUnityPlayableWorldMoveVector
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using GameCult.Eve.Surface;
|
using GameCult.Eve.Surface;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
@ -12,22 +11,24 @@ namespace GameCult.Eve.UnityScene
|
||||||
private readonly EveUnitySceneProviderConnection _connection;
|
private readonly EveUnitySceneProviderConnection _connection;
|
||||||
private readonly EveUnityPlayableWorldPresenter _presenter;
|
private readonly EveUnityPlayableWorldPresenter _presenter;
|
||||||
private readonly IEveUnitySceneCommandReceiptSource? _receiptSource;
|
private readonly IEveUnitySceneCommandReceiptSource? _receiptSource;
|
||||||
|
private readonly Func<long>? _visibleStateVersion;
|
||||||
private readonly EveUnityFeedbackPresenter _feedback = new EveUnityFeedbackPresenter();
|
private readonly EveUnityFeedbackPresenter _feedback = new EveUnityFeedbackPresenter();
|
||||||
private readonly EveUnityShotReceiptPresenter _shots = new EveUnityShotReceiptPresenter();
|
private readonly EveUnityShotReceiptPresenter _shots = new EveUnityShotReceiptPresenter();
|
||||||
private readonly List<EveUnitySceneCommandReceipt> _pendingReceipts =
|
private readonly List<EveUnitySceneCommandReceipt> _pendingReceipts =
|
||||||
new List<EveUnitySceneCommandReceipt>();
|
new List<EveUnitySceneCommandReceipt>();
|
||||||
private long _activeStateVersion;
|
|
||||||
private bool _connected;
|
private bool _connected;
|
||||||
private bool _receiptConnected;
|
private bool _receiptConnected;
|
||||||
|
|
||||||
public EveUnityPlayableWorldLiveClient(
|
public EveUnityPlayableWorldLiveClient(
|
||||||
EveUnitySceneProviderConnection connection,
|
EveUnitySceneProviderConnection connection,
|
||||||
EveUnityPlayableWorldPresenter presenter,
|
EveUnityPlayableWorldPresenter presenter,
|
||||||
IEveUnitySceneCommandReceiptSource? receiptSource = null)
|
IEveUnitySceneCommandReceiptSource? receiptSource = null,
|
||||||
|
Func<long>? visibleStateVersion = null)
|
||||||
{
|
{
|
||||||
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||||
_presenter = presenter ?? throw new ArgumentNullException(nameof(presenter));
|
_presenter = presenter ?? throw new ArgumentNullException(nameof(presenter));
|
||||||
_receiptSource = receiptSource;
|
_receiptSource = receiptSource;
|
||||||
|
_visibleStateVersion = visibleStateVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EveUnitySceneProjection? ActiveProjection => _connection.ActiveProjection;
|
public EveUnitySceneProjection? ActiveProjection => _connection.ActiveProjection;
|
||||||
|
|
@ -38,14 +39,7 @@ namespace GameCult.Eve.UnityScene
|
||||||
|
|
||||||
public EveUnitySceneCommandReceipt? LastReceipt { get; private set; }
|
public EveUnitySceneCommandReceipt? LastReceipt { get; private set; }
|
||||||
|
|
||||||
public long ActiveVersion => Math.Max(_connection.ActiveVersion, _activeStateVersion);
|
public long ActiveVersion => Math.Max(_connection.ActiveVersion, _visibleStateVersion?.Invoke() ?? 0);
|
||||||
|
|
||||||
public void AdvanceStateVersion(long version)
|
|
||||||
{
|
|
||||||
if (version <= _activeStateVersion) return;
|
|
||||||
_activeStateVersion = version;
|
|
||||||
PublishReceiptsWhoseStateIsVisible();
|
|
||||||
}
|
|
||||||
|
|
||||||
public string SourcePointer => _connection.SourcePointer;
|
public string SourcePointer => _connection.SourcePointer;
|
||||||
|
|
||||||
|
|
@ -97,16 +91,6 @@ namespace GameCult.Eve.UnityScene
|
||||||
return _connection.SubmitMoveVectorIntent(entityId, directionX, directionY, scalarValue, issuedAt);
|
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(
|
public EveSurfaceCommandRequest SubmitFocusIntent(
|
||||||
string entityId,
|
string entityId,
|
||||||
DateTimeOffset? issuedAt = null)
|
DateTimeOffset? issuedAt = null)
|
||||||
|
|
@ -124,18 +108,10 @@ namespace GameCult.Eve.UnityScene
|
||||||
|
|
||||||
public EveSurfaceCommandRequest SubmitActionIntent(
|
public EveSurfaceCommandRequest SubmitActionIntent(
|
||||||
string entityId,
|
string entityId,
|
||||||
string actionId,
|
EveInputActionDocument action,
|
||||||
DateTimeOffset? issuedAt = null)
|
DateTimeOffset? issuedAt = null)
|
||||||
{
|
{
|
||||||
return _connection.SubmitActionIntent(entityId, actionId, issuedAt);
|
return _connection.SubmitActionIntent(entityId, action, issuedAt);
|
||||||
}
|
|
||||||
|
|
||||||
public EveSurfaceCommandRequest SubmitCommandIntent(
|
|
||||||
string commandId,
|
|
||||||
IReadOnlyDictionary<string, string>? payload = null,
|
|
||||||
DateTimeOffset? issuedAt = null)
|
|
||||||
{
|
|
||||||
return _connection.SubmitCommandIntent(commandId, payload, issuedAt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Disconnect()
|
public void Disconnect()
|
||||||
|
|
@ -235,43 +211,6 @@ namespace GameCult.Eve.UnityScene
|
||||||
event Action<EveUnitySceneCommandReceipt> ReceiptAvailable;
|
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 sealed class EveUnitySceneCommandReceipt
|
||||||
{
|
{
|
||||||
public EveUnitySceneCommandReceipt(
|
public EveUnitySceneCommandReceipt(
|
||||||
|
|
@ -286,8 +225,7 @@ namespace GameCult.Eve.UnityScene
|
||||||
string surfaceId = "",
|
string surfaceId = "",
|
||||||
string message = "",
|
string message = "",
|
||||||
DateTimeOffset? issuedAtUtc = null,
|
DateTimeOffset? issuedAtUtc = null,
|
||||||
long sourceVersion = 0,
|
long sourceVersion = 0)
|
||||||
EveUnitySceneNavigationTarget? navigation = null)
|
|
||||||
{
|
{
|
||||||
ReceiptId = receiptId ?? "";
|
ReceiptId = receiptId ?? "";
|
||||||
Command = command ?? "";
|
Command = command ?? "";
|
||||||
|
|
@ -301,7 +239,6 @@ namespace GameCult.Eve.UnityScene
|
||||||
Message = message ?? "";
|
Message = message ?? "";
|
||||||
IssuedAtUtc = issuedAtUtc;
|
IssuedAtUtc = issuedAtUtc;
|
||||||
SourceVersion = sourceVersion;
|
SourceVersion = sourceVersion;
|
||||||
Navigation = navigation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Schema { get; }
|
public string Schema { get; }
|
||||||
|
|
@ -328,8 +265,6 @@ namespace GameCult.Eve.UnityScene
|
||||||
|
|
||||||
public long SourceVersion { get; }
|
public long SourceVersion { get; }
|
||||||
|
|
||||||
public EveUnitySceneNavigationTarget? Navigation { get; }
|
|
||||||
|
|
||||||
public bool IsProviderOwned => !string.IsNullOrWhiteSpace(OwnerRepo) && !string.Equals(OwnerRepo, "EveUnity", StringComparison.Ordinal);
|
public bool IsProviderOwned => !string.IsNullOrWhiteSpace(OwnerRepo) && !string.Equals(OwnerRepo, "EveUnity", StringComparison.Ordinal);
|
||||||
|
|
||||||
public bool ShouldRefreshProviderSurface =>
|
public bool ShouldRefreshProviderSurface =>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Linq;
|
||||||
using GameCult.Eve.Surface;
|
using GameCult.Eve.Surface;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
|
|
||||||
|
|
@ -14,9 +14,8 @@ namespace GameCult.Eve.UnityScene
|
||||||
private readonly EveUnityPlayableWorldLiveClient _client;
|
private readonly EveUnityPlayableWorldLiveClient _client;
|
||||||
private readonly EveUnityPlayableWorldAssetManifestDocumentSource? _assetManifestSource;
|
private readonly EveUnityPlayableWorldAssetManifestDocumentSource? _assetManifestSource;
|
||||||
private readonly IEveUnityEntitySoaViewDocumentSource? _entityViews;
|
private readonly IEveUnityEntitySoaViewDocumentSource? _entityViews;
|
||||||
|
private readonly IEveUnityInputCapabilityDocumentSource? _inputCapabilities;
|
||||||
private readonly EveUnityEntitySoaPresenter _entityPresenter;
|
private readonly EveUnityEntitySoaPresenter _entityPresenter;
|
||||||
private readonly IEveUnityPresentedEntityRegistry? _presentedEntities;
|
|
||||||
private IDisposable? _ownedSceneSink;
|
|
||||||
private bool _entityViewsConnected;
|
private bool _entityViewsConnected;
|
||||||
private bool _assetManifestConnected;
|
private bool _assetManifestConnected;
|
||||||
|
|
||||||
|
|
@ -36,13 +35,14 @@ namespace GameCult.Eve.UnityScene
|
||||||
AssetManifests = assetManifests ?? new EveUnityPlayableWorldAssetManifestCache();
|
AssetManifests = assetManifests ?? new EveUnityPlayableWorldAssetManifestCache();
|
||||||
_surfaceSource = new EveUnitySceneProviderSurfaceDocumentSource(surfaceDocuments);
|
_surfaceSource = new EveUnitySceneProviderSurfaceDocumentSource(surfaceDocuments);
|
||||||
_entityViews = surfaceDocuments as IEveUnityEntitySoaViewDocumentSource;
|
_entityViews = surfaceDocuments as IEveUnityEntitySoaViewDocumentSource;
|
||||||
_presentedEntities = sceneSink as IEveUnityPresentedEntityRegistry;
|
_inputCapabilities = surfaceDocuments as IEveUnityInputCapabilityDocumentSource;
|
||||||
_entityPresenter = new EveUnityEntitySoaPresenter(sceneSink);
|
_entityPresenter = new EveUnityEntitySoaPresenter(sceneSink);
|
||||||
_connection = new EveUnitySceneProviderConnection(_surfaceSource, commandSink);
|
_connection = new EveUnitySceneProviderConnection(_surfaceSource, commandSink);
|
||||||
_client = new EveUnityPlayableWorldLiveClient(
|
_client = new EveUnityPlayableWorldLiveClient(
|
||||||
_connection,
|
_connection,
|
||||||
new EveUnityPlayableWorldPresenter(sceneSink, assetResolver ?? new EveUnityAssetRefResolver()),
|
new EveUnityPlayableWorldPresenter(sceneSink, assetResolver ?? new EveUnityAssetRefResolver()),
|
||||||
receiptSource);
|
receiptSource,
|
||||||
|
() => _entityViews?.CurrentEntityView?.FrameId ?? 0);
|
||||||
|
|
||||||
if (assetManifestDocuments != null)
|
if (assetManifestDocuments != null)
|
||||||
_assetManifestSource = new EveUnityPlayableWorldAssetManifestDocumentSource(assetManifestDocuments);
|
_assetManifestSource = new EveUnityPlayableWorldAssetManifestDocumentSource(assetManifestDocuments);
|
||||||
|
|
@ -51,13 +51,12 @@ namespace GameCult.Eve.UnityScene
|
||||||
public EveUnityPlayableWorldAssetManifestCache AssetManifests { get; }
|
public EveUnityPlayableWorldAssetManifestCache AssetManifests { get; }
|
||||||
|
|
||||||
public IEveUnityGameObjectAssetProvider? GameObjectAssetProvider { get; private set; }
|
public IEveUnityGameObjectAssetProvider? GameObjectAssetProvider { get; private set; }
|
||||||
public IEveUnityPresentedEntityRegistry? PresentedEntities => _presentedEntities;
|
|
||||||
|
|
||||||
public EveUnityPlayableWorldProjection? ActiveWorld => _client.ActiveWorld;
|
public EveUnityPlayableWorldProjection? ActiveWorld => _client.ActiveWorld;
|
||||||
|
|
||||||
public EveUnitySceneProjection? ActiveProjection => _client.ActiveProjection;
|
public EveUnitySceneProjection? ActiveProjection => _client.ActiveProjection;
|
||||||
|
|
||||||
public EveUnityPlayableWorldPresentation? LastPresentation => _client.LastPresentation;
|
public EveUnityPlayableWorldPresentation? LastPresentation => ComposePresentation(_client.LastPresentation);
|
||||||
|
|
||||||
public EveUnitySceneCommandReceipt? LastReceipt => _client.LastReceipt;
|
public EveUnitySceneCommandReceipt? LastReceipt => _client.LastReceipt;
|
||||||
|
|
||||||
|
|
@ -108,22 +107,23 @@ namespace GameCult.Eve.UnityScene
|
||||||
new EveUnityAssetRefResolver(),
|
new EveUnityAssetRefResolver(),
|
||||||
assetManifests);
|
assetManifests);
|
||||||
runtime.GameObjectAssetProvider = liveAssetProvider;
|
runtime.GameObjectAssetProvider = liveAssetProvider;
|
||||||
runtime._ownedSceneSink = sceneSink;
|
|
||||||
return runtime;
|
return runtime;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EveUnityPlayableWorldPresentation Connect()
|
public EveUnityPlayableWorldPresentation Connect()
|
||||||
{
|
{
|
||||||
EnsureAssetManifestConnected();
|
EnsureAssetManifestConnected();
|
||||||
var presentation = _client.Connect();
|
_client.Connect();
|
||||||
EnsureEntityViewsConnected();
|
EnsureEntityViewsConnected();
|
||||||
return presentation;
|
return ComposePresentation(_client.LastPresentation)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EveUnityPlayableWorldPresentation Refresh()
|
public EveUnityPlayableWorldPresentation Refresh()
|
||||||
{
|
{
|
||||||
EnsureAssetManifestConnected();
|
EnsureAssetManifestConnected();
|
||||||
return _client.Refresh();
|
EnsureEntityViewsConnected();
|
||||||
|
_client.Refresh();
|
||||||
|
return ComposePresentation(_client.LastPresentation)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EveSurfaceCommandRequest SubmitMoveIntent(
|
public EveSurfaceCommandRequest SubmitMoveIntent(
|
||||||
|
|
@ -146,16 +146,6 @@ namespace GameCult.Eve.UnityScene
|
||||||
return _client.SubmitMoveVectorIntent(entityId, directionX, directionY, scalarValue, issuedAt);
|
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(
|
public EveSurfaceCommandRequest SubmitFocusIntent(
|
||||||
string entityId,
|
string entityId,
|
||||||
DateTimeOffset? issuedAt = null)
|
DateTimeOffset? issuedAt = null)
|
||||||
|
|
@ -176,15 +166,14 @@ namespace GameCult.Eve.UnityScene
|
||||||
string actionId,
|
string actionId,
|
||||||
DateTimeOffset? issuedAt = null)
|
DateTimeOffset? issuedAt = null)
|
||||||
{
|
{
|
||||||
return _client.SubmitActionIntent(entityId, actionId, issuedAt);
|
var capability = _inputCapabilities?.CurrentInputCapability
|
||||||
}
|
?? throw new InvalidOperationException("The active Eve surface does not publish an input capability document.");
|
||||||
|
var action = (capability.Actions ?? Array.Empty<EveInputActionDocument>())
|
||||||
public EveSurfaceCommandRequest SubmitCommandIntent(
|
.FirstOrDefault(candidate => string.Equals(candidate.ActionId, actionId, StringComparison.Ordinal))
|
||||||
string commandId,
|
?? throw new InvalidOperationException($"The active Eve input capability does not advertise action '{actionId}'.");
|
||||||
IReadOnlyDictionary<string, string>? payload = null,
|
if (!string.Equals(action.Availability, "available", StringComparison.OrdinalIgnoreCase))
|
||||||
DateTimeOffset? issuedAt = null)
|
throw new InvalidOperationException($"Eve action '{actionId}' is not currently available.");
|
||||||
{
|
return _client.SubmitActionIntent(entityId, action, issuedAt);
|
||||||
return _client.SubmitCommandIntent(commandId, payload, issuedAt);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Disconnect()
|
public void Disconnect()
|
||||||
|
|
@ -209,8 +198,6 @@ namespace GameCult.Eve.UnityScene
|
||||||
Disconnect();
|
Disconnect();
|
||||||
_client.Dispose();
|
_client.Dispose();
|
||||||
_assetManifestSource?.Dispose();
|
_assetManifestSource?.Dispose();
|
||||||
_ownedSceneSink?.Dispose();
|
|
||||||
_ownedSceneSink = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void EnsureAssetManifestConnected()
|
private void EnsureAssetManifestConnected()
|
||||||
|
|
@ -228,18 +215,30 @@ namespace GameCult.Eve.UnityScene
|
||||||
if (_entityViewsConnected || _entityViews == null) return;
|
if (_entityViewsConnected || _entityViews == null) return;
|
||||||
_entityViews.EntityViewAvailable += OnEntityViewAvailable;
|
_entityViews.EntityViewAvailable += OnEntityViewAvailable;
|
||||||
_entityViewsConnected = true;
|
_entityViewsConnected = true;
|
||||||
|
if (_entityViews.CurrentEntityView != null)
|
||||||
|
_entityPresenter.Apply(_entityViews.CurrentEntityView);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEntityViewAvailable(EveEntitySoaViewDocument document, GameCult.Mesh.ICultMeshBodyReadLease lease)
|
private void OnEntityViewAvailable(EveEntitySoaViewDocument document) => _entityPresenter.Apply(document);
|
||||||
|
|
||||||
|
private EveUnityPlayableWorldPresentation? ComposePresentation(
|
||||||
|
EveUnityPlayableWorldPresentation? surfacePresentation)
|
||||||
{
|
{
|
||||||
_client.AdvanceStateVersion(document.FrameId);
|
if (surfacePresentation == null || _entityViews?.CurrentEntityView == null)
|
||||||
_entityPresenter.Apply(document, lease);
|
return surfacePresentation;
|
||||||
|
|
||||||
|
return new EveUnityPlayableWorldPresentation(
|
||||||
|
surfacePresentation.WorldRootId,
|
||||||
|
surfacePresentation.PlayerEntityId,
|
||||||
|
surfacePresentation.InputProfile,
|
||||||
|
surfacePresentation.CameraRig,
|
||||||
|
surfacePresentation.UpsertedEntities,
|
||||||
|
surfacePresentation.RemovedEntities,
|
||||||
|
_entityPresenter.ActiveEntities);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class EveUnityLivePlayableWorldAssetProvider :
|
public sealed class EveUnityLivePlayableWorldAssetProvider : IEveUnityNativeAssetProvider
|
||||||
IEveUnityNativeAssetProvider,
|
|
||||||
IEveUnityNativeAssetMetadataProvider
|
|
||||||
{
|
{
|
||||||
private readonly EveUnityPlayableWorldAssetManifestCache _assetManifests;
|
private readonly EveUnityPlayableWorldAssetManifestCache _assetManifests;
|
||||||
private readonly Func<EveUnityPlayableWorldProjection?> _activeWorld;
|
private readonly Func<EveUnityPlayableWorldProjection?> _activeWorld;
|
||||||
|
|
@ -286,16 +285,5 @@ namespace GameCult.Eve.UnityScene
|
||||||
}
|
}
|
||||||
return (_fallback as IEveUnityNativeAssetProvider)?.ResolveAsset(asset, assetType);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue