Establish EveUnity runtime owner repo
This commit is contained in:
commit
9341d44d8c
54 changed files with 5565 additions and 0 deletions
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
artifacts/
|
||||
Library/
|
||||
Logs/
|
||||
Temp/
|
||||
obj/
|
||||
*.csproj
|
||||
*.sln
|
||||
|
||||
21
README.md
Normal file
21
README.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# EveUnity
|
||||
|
||||
EveUnity is the Unity lowering client for Eve interactive surfaces.
|
||||
|
||||
It owns Unity projection, scene lifecycle, input capture, camera behavior,
|
||||
asset resolution hooks, UPM packaging, runtime tests, and frame evidence. It
|
||||
does not own provider state, simulation, commands, receipts, assets, or plugin
|
||||
semantics.
|
||||
|
||||
The first package is `org.gamecult.eve.unity-scene`. A generic Unity host can
|
||||
connect any provider component that implements the EveUnity provider ports,
|
||||
including Aetheria's provider-owned adapter for its daemon-published 3D ARPG
|
||||
surface.
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\run-package-tests.ps1
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\pack.ps1
|
||||
```
|
||||
|
||||
See [docs/authority-map.md](docs/authority-map.md) for the live boundary.
|
||||
|
||||
52
docs/authority-map.md
Normal file
52
docs/authority-map.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# EveUnity Authority Map
|
||||
|
||||
## Objective
|
||||
|
||||
Lower provider-authored Eve surfaces into a playable Unity scene without
|
||||
importing provider product code into the generic runtime.
|
||||
|
||||
## Owner
|
||||
|
||||
EveUnity owns Unity scene projection, `GameObject` lifecycle, camera and input
|
||||
drivers, asset-provider hooks, package release, runtime tests, and captures.
|
||||
|
||||
## Inputs
|
||||
|
||||
- `gamecult.eve.provider_advertisement.v1`
|
||||
- `gamecult.eve.surface.v1`
|
||||
- provider asset-manifest documents
|
||||
- provider command receipts
|
||||
- Unity input and local presentation assets
|
||||
|
||||
## Outputs
|
||||
|
||||
- disposable Unity scene projection
|
||||
- `gamecult.eve.command.v1` intents sent through the advertised boundary
|
||||
- runtime capability, test, release, and capture evidence
|
||||
|
||||
## Derived State
|
||||
|
||||
`ActiveWorld`, scene objects, transforms, markers, camera pose, asset caches,
|
||||
presentation counters, and receipt display state are projections. They are not
|
||||
world truth.
|
||||
|
||||
## Forbidden Writers
|
||||
|
||||
Unity transforms, input drivers, camera rigs, scene sinks, asset caches, and
|
||||
pending receipts may not mutate or simulate provider world state. Plugin
|
||||
projection shells may not implement Sai, Norn, or TeX semantics.
|
||||
|
||||
## Shared Paths
|
||||
|
||||
Initial connect, refresh, reconnect, and terminal-receipt reconciliation all
|
||||
consume provider surface documents. Movement, focus, target, and action input
|
||||
all become `gamecult.eve.command.v1` through the provider-advertised command
|
||||
boundary.
|
||||
|
||||
## Cut Line
|
||||
|
||||
Aetheria retains its daemon, authored surfaces, assets, command acceptance,
|
||||
receipts, adapter, and provider conformance scenarios. Eve retains contracts
|
||||
and conformance policy. EveUnity retains only generic Unity projection and its
|
||||
runtime lifecycle.
|
||||
|
||||
11
packages/org.gamecult.eve.unity-scene/README.md
Normal file
11
packages/org.gamecult.eve.unity-scene/README.md
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Eve Unity Scene Lowering
|
||||
|
||||
Provider-agnostic Unity scene lowering for `gamecult.eve.surface.v1`
|
||||
interactive worlds.
|
||||
|
||||
Providers implement the surface-document, asset-manifest, command-sink, and
|
||||
receipt-source ports. `EveUnityPlayableWorldClientBootstrap` wires those ports
|
||||
to the generic scene host, input driver, camera rig, and `GameObject` sink.
|
||||
|
||||
The package never imports provider product assemblies.
|
||||
|
||||
7
packages/org.gamecult.eve.unity-scene/README.md.meta
Normal file
7
packages/org.gamecult.eve.unity-scene/README.md.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 74b6947fafbfd0d42a642b7527a32258
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
packages/org.gamecult.eve.unity-scene/Runtime.meta
Normal file
8
packages/org.gamecult.eve.unity-scene/Runtime.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 1ed3bb976c01b68498f8da912b8ddf2d
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public interface IEveUnityGameObjectAssetProvider
|
||||
{
|
||||
GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset);
|
||||
}
|
||||
|
||||
public sealed class EveUnityGameObjectPlayableWorldSceneSink : IEveUnityPlayableWorldSceneSink
|
||||
{
|
||||
private readonly Transform _root;
|
||||
private readonly IEveUnityGameObjectAssetProvider _assetProvider;
|
||||
private readonly Dictionary<string, GameObject> _instances = new Dictionary<string, GameObject>(StringComparer.Ordinal);
|
||||
|
||||
public EveUnityGameObjectPlayableWorldSceneSink(
|
||||
Transform root,
|
||||
IEveUnityGameObjectAssetProvider assetProvider)
|
||||
{
|
||||
_root = root != null ? root : throw new ArgumentNullException(nameof(root));
|
||||
_assetProvider = assetProvider ?? throw new ArgumentNullException(nameof(assetProvider));
|
||||
}
|
||||
|
||||
public int ActiveEntityCount => _instances.Count;
|
||||
|
||||
public void ConfigureWorld(EveUnityPlayableWorldProjection world)
|
||||
{
|
||||
if (world == null) throw new ArgumentNullException(nameof(world));
|
||||
if (!string.IsNullOrWhiteSpace(world.WorldRootId))
|
||||
_root.name = world.WorldRootId;
|
||||
}
|
||||
|
||||
public void UpsertEntity(EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset)
|
||||
{
|
||||
if (entity == null) throw new ArgumentNullException(nameof(entity));
|
||||
if (asset == null) throw new ArgumentNullException(nameof(asset));
|
||||
if (string.IsNullOrWhiteSpace(entity.EntityId))
|
||||
return;
|
||||
|
||||
GameObject instance;
|
||||
if (!_instances.TryGetValue(entity.EntityId, out instance) || instance == null)
|
||||
{
|
||||
instance = InstantiateEntity(entity, asset);
|
||||
_instances[entity.EntityId] = instance;
|
||||
}
|
||||
else
|
||||
{
|
||||
var marker = instance.GetComponent<EveUnityPlayableWorldEntityMarker>();
|
||||
if (marker != null && !string.Equals(marker.AssetRef, asset.AssetRef, StringComparison.Ordinal))
|
||||
{
|
||||
DestroyInstance(instance);
|
||||
instance = InstantiateEntity(entity, asset);
|
||||
_instances[entity.EntityId] = instance;
|
||||
}
|
||||
}
|
||||
|
||||
ApplyEntity(instance, entity, asset);
|
||||
}
|
||||
|
||||
public void RemoveEntity(string entityId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(entityId))
|
||||
return;
|
||||
|
||||
GameObject instance;
|
||||
if (!_instances.TryGetValue(entityId, out instance))
|
||||
return;
|
||||
|
||||
_instances.Remove(entityId);
|
||||
if (instance != null)
|
||||
DestroyInstance(instance);
|
||||
}
|
||||
|
||||
private GameObject InstantiateEntity(EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset)
|
||||
{
|
||||
var prefab = _assetProvider.ResolvePrefab(asset);
|
||||
var instance = prefab != null
|
||||
? UnityEngine.Object.Instantiate(prefab, _root)
|
||||
: GameObject.CreatePrimitive(PrimitiveType.Capsule);
|
||||
|
||||
if (instance.transform.parent != _root)
|
||||
instance.transform.SetParent(_root, false);
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static void ApplyEntity(
|
||||
GameObject instance,
|
||||
EveUnityPlayableWorldEntity entity,
|
||||
EveUnityPlayableWorldAssetBinding asset)
|
||||
{
|
||||
instance.name = string.IsNullOrWhiteSpace(entity.Label)
|
||||
? entity.EntityId
|
||||
: $"{entity.EntityId} ({entity.Label})";
|
||||
instance.transform.localPosition = new Vector3(entity.PositionX, entity.PositionY, entity.PositionZ);
|
||||
instance.transform.localRotation = Quaternion.Euler(0f, entity.RotationY, 0f);
|
||||
if (entity.Radius > 0f)
|
||||
instance.transform.localScale = Vector3.one * entity.Radius;
|
||||
|
||||
var marker = instance.GetComponent<EveUnityPlayableWorldEntityMarker>();
|
||||
if (marker == null)
|
||||
marker = instance.AddComponent<EveUnityPlayableWorldEntityMarker>();
|
||||
|
||||
marker.Apply(entity, asset);
|
||||
}
|
||||
|
||||
private static void DestroyInstance(GameObject instance)
|
||||
{
|
||||
if (Application.isPlaying)
|
||||
UnityEngine.Object.Destroy(instance);
|
||||
else
|
||||
UnityEngine.Object.DestroyImmediate(instance);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EveUnityResourcesAssetProvider : IEveUnityGameObjectAssetProvider
|
||||
{
|
||||
public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset)
|
||||
{
|
||||
if (asset == null) throw new ArgumentNullException(nameof(asset));
|
||||
var resourcesPath = EveUnityPlayableWorldAssetManifestEntry.NormalizeResourcesPath(asset.AssetRef);
|
||||
return string.IsNullOrWhiteSpace(resourcesPath)
|
||||
? null
|
||||
: Resources.Load<GameObject>(resourcesPath);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EveUnityPlayableWorldEntityMarker : MonoBehaviour
|
||||
{
|
||||
public string EntityId = "";
|
||||
public string NodeId = "";
|
||||
public string EntityKind = "";
|
||||
public string Label = "";
|
||||
public string Faction = "";
|
||||
public string AssetRef = "";
|
||||
public string PresentationKind = "";
|
||||
public bool Selectable;
|
||||
public bool Controllable;
|
||||
|
||||
public void Apply(EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset)
|
||||
{
|
||||
if (entity == null) throw new ArgumentNullException(nameof(entity));
|
||||
if (asset == null) throw new ArgumentNullException(nameof(asset));
|
||||
|
||||
EntityId = entity.EntityId;
|
||||
NodeId = entity.NodeId;
|
||||
EntityKind = entity.EntityKind;
|
||||
Label = entity.Label;
|
||||
Faction = entity.Faction;
|
||||
AssetRef = asset.AssetRef;
|
||||
PresentationKind = asset.PresentationKind;
|
||||
Selectable = entity.Selectable;
|
||||
Controllable = entity.Controllable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e924fe3501e6e584392cb405f916c9c9
|
||||
|
|
@ -0,0 +1,341 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public sealed class EveUnityPlayableWorldAssetManifest
|
||||
{
|
||||
private readonly Dictionary<string, EveUnityPlayableWorldAssetManifestEntry> _byAssetRef =
|
||||
new Dictionary<string, EveUnityPlayableWorldAssetManifestEntry>(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, EveUnityPlayableWorldAssetManifestEntry> _byEntityKind =
|
||||
new Dictionary<string, EveUnityPlayableWorldAssetManifestEntry>(StringComparer.Ordinal);
|
||||
|
||||
public EveUnityPlayableWorldAssetManifest(
|
||||
string manifestRef,
|
||||
IReadOnlyList<EveUnityPlayableWorldAssetManifestEntry> entries)
|
||||
{
|
||||
ManifestRef = manifestRef ?? "";
|
||||
Entries = entries ?? Array.Empty<EveUnityPlayableWorldAssetManifestEntry>();
|
||||
|
||||
foreach (var entry in Entries)
|
||||
{
|
||||
if (entry == null)
|
||||
continue;
|
||||
if (!string.IsNullOrWhiteSpace(entry.AssetRef))
|
||||
_byAssetRef[entry.AssetRef] = entry;
|
||||
if (!string.IsNullOrWhiteSpace(entry.EntityKind))
|
||||
_byEntityKind[entry.EntityKind] = entry;
|
||||
}
|
||||
}
|
||||
|
||||
public string ManifestRef { get; }
|
||||
|
||||
public IReadOnlyList<EveUnityPlayableWorldAssetManifestEntry> Entries { get; }
|
||||
|
||||
public static EveUnityPlayableWorldAssetManifest FromDocument(EveUnityPlayableWorldAssetManifestDocument document)
|
||||
{
|
||||
if (document == null) throw new ArgumentNullException(nameof(document));
|
||||
|
||||
var entries = new List<EveUnityPlayableWorldAssetManifestEntry>();
|
||||
foreach (var entry in document.Entries)
|
||||
{
|
||||
if (entry == null)
|
||||
continue;
|
||||
|
||||
entries.Add(new EveUnityPlayableWorldAssetManifestEntry(
|
||||
entry.AssetRef,
|
||||
entry.EntityKind,
|
||||
entry.ResourcesPath,
|
||||
entry.PrefabKey,
|
||||
entry.PresentationKind));
|
||||
}
|
||||
|
||||
return new EveUnityPlayableWorldAssetManifest(document.ManifestRef, entries);
|
||||
}
|
||||
|
||||
public EveUnityPlayableWorldAssetManifestEntry? Find(EveUnityPlayableWorldAssetBinding asset)
|
||||
{
|
||||
if (asset == null) throw new ArgumentNullException(nameof(asset));
|
||||
|
||||
EveUnityPlayableWorldAssetManifestEntry entry;
|
||||
if (!string.IsNullOrWhiteSpace(asset.AssetRef) && _byAssetRef.TryGetValue(asset.AssetRef, out entry))
|
||||
return entry;
|
||||
if (!string.IsNullOrWhiteSpace(asset.EntityKind) && _byEntityKind.TryGetValue(asset.EntityKind, out entry))
|
||||
return entry;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IEveUnityPlayableWorldAssetManifestSource
|
||||
{
|
||||
string ManifestRef { get; }
|
||||
|
||||
EveUnityPlayableWorldAssetManifest CurrentManifest { get; }
|
||||
|
||||
event Action<EveUnityPlayableWorldAssetManifest> ManifestAvailable;
|
||||
}
|
||||
|
||||
public sealed class EveUnityPlayableWorldAssetManifestCache
|
||||
{
|
||||
private readonly Dictionary<string, EveUnityPlayableWorldAssetManifest> _manifests =
|
||||
new Dictionary<string, EveUnityPlayableWorldAssetManifest>(StringComparer.Ordinal);
|
||||
private readonly Dictionary<IEveUnityPlayableWorldAssetManifestSource, Action<EveUnityPlayableWorldAssetManifest>> _subscriptions =
|
||||
new Dictionary<IEveUnityPlayableWorldAssetManifestSource, Action<EveUnityPlayableWorldAssetManifest>>();
|
||||
|
||||
public int Count => _manifests.Count;
|
||||
|
||||
public void Add(EveUnityPlayableWorldAssetManifest manifest)
|
||||
{
|
||||
if (manifest == null) throw new ArgumentNullException(nameof(manifest));
|
||||
if (string.IsNullOrWhiteSpace(manifest.ManifestRef))
|
||||
throw new ArgumentException("Asset manifest ref is required.", nameof(manifest));
|
||||
|
||||
_manifests[manifest.ManifestRef] = manifest;
|
||||
}
|
||||
|
||||
public void Connect(IEveUnityPlayableWorldAssetManifestSource source)
|
||||
{
|
||||
if (source == null) throw new ArgumentNullException(nameof(source));
|
||||
if (_subscriptions.ContainsKey(source))
|
||||
return;
|
||||
|
||||
Add(source.CurrentManifest);
|
||||
Action<EveUnityPlayableWorldAssetManifest> handler = Add;
|
||||
source.ManifestAvailable += handler;
|
||||
_subscriptions[source] = handler;
|
||||
}
|
||||
|
||||
public void Disconnect(IEveUnityPlayableWorldAssetManifestSource source)
|
||||
{
|
||||
if (source == null) throw new ArgumentNullException(nameof(source));
|
||||
Action<EveUnityPlayableWorldAssetManifest> handler;
|
||||
if (!_subscriptions.TryGetValue(source, out handler))
|
||||
return;
|
||||
|
||||
source.ManifestAvailable -= handler;
|
||||
_subscriptions.Remove(source);
|
||||
}
|
||||
|
||||
public bool TryGet(string manifestRef, out EveUnityPlayableWorldAssetManifest? manifest)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(manifestRef))
|
||||
{
|
||||
manifest = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return _manifests.TryGetValue(manifestRef, out manifest);
|
||||
}
|
||||
|
||||
public EveUnityPlayableWorldAssetManifest? GetForWorld(EveUnityPlayableWorldProjection world)
|
||||
{
|
||||
if (world == null) throw new ArgumentNullException(nameof(world));
|
||||
EveUnityPlayableWorldAssetManifest? manifest;
|
||||
return TryGet(world.AssetManifest, out manifest) ? manifest : null;
|
||||
}
|
||||
|
||||
public IEveUnityGameObjectAssetProvider CreateGameObjectAssetProvider(
|
||||
EveUnityPlayableWorldProjection world,
|
||||
IEveUnityGameObjectAssetProvider fallback)
|
||||
{
|
||||
if (world == null) throw new ArgumentNullException(nameof(world));
|
||||
if (fallback == null) throw new ArgumentNullException(nameof(fallback));
|
||||
|
||||
var manifest = GetForWorld(world);
|
||||
return manifest == null
|
||||
? fallback
|
||||
: new EveUnityManifestGameObjectAssetProvider(manifest, fallback);
|
||||
}
|
||||
}
|
||||
|
||||
public interface IEveUnityPlayableWorldAssetManifestDocumentSource
|
||||
{
|
||||
string ManifestRef { get; }
|
||||
|
||||
EveUnityPlayableWorldAssetManifestDocument CurrentDocument { get; }
|
||||
|
||||
event Action<EveUnityPlayableWorldAssetManifestDocument> DocumentAvailable;
|
||||
}
|
||||
|
||||
public sealed class EveUnityPlayableWorldAssetManifestDocumentSource : IEveUnityPlayableWorldAssetManifestSource, IDisposable
|
||||
{
|
||||
private readonly IEveUnityPlayableWorldAssetManifestDocumentSource _documentSource;
|
||||
private bool _connected;
|
||||
|
||||
public EveUnityPlayableWorldAssetManifestDocumentSource(
|
||||
IEveUnityPlayableWorldAssetManifestDocumentSource documentSource)
|
||||
{
|
||||
_documentSource = documentSource ?? throw new ArgumentNullException(nameof(documentSource));
|
||||
CurrentManifest = EveUnityPlayableWorldAssetManifest.FromDocument(_documentSource.CurrentDocument);
|
||||
}
|
||||
|
||||
public string ManifestRef => CurrentManifest.ManifestRef;
|
||||
|
||||
public EveUnityPlayableWorldAssetManifest CurrentManifest { get; private set; }
|
||||
|
||||
public event Action<EveUnityPlayableWorldAssetManifest>? ManifestAvailable;
|
||||
|
||||
public void Connect()
|
||||
{
|
||||
if (_connected)
|
||||
return;
|
||||
|
||||
_documentSource.DocumentAvailable += OnDocumentAvailable;
|
||||
_connected = true;
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
if (!_connected)
|
||||
return;
|
||||
|
||||
_documentSource.DocumentAvailable -= OnDocumentAvailable;
|
||||
_connected = false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
private void OnDocumentAvailable(EveUnityPlayableWorldAssetManifestDocument document)
|
||||
{
|
||||
CurrentManifest = EveUnityPlayableWorldAssetManifest.FromDocument(document);
|
||||
ManifestAvailable?.Invoke(CurrentManifest);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EveUnityPlayableWorldAssetManifestDocument
|
||||
{
|
||||
public const string SchemaId = "gamecult.eve.unity_playable_world_asset_manifest.v1";
|
||||
|
||||
public EveUnityPlayableWorldAssetManifestDocument(
|
||||
string manifestRef,
|
||||
IReadOnlyList<EveUnityPlayableWorldAssetManifestDocumentEntry> entries,
|
||||
string providerId = "",
|
||||
string schema = SchemaId)
|
||||
{
|
||||
ManifestRef = manifestRef ?? "";
|
||||
Entries = entries ?? Array.Empty<EveUnityPlayableWorldAssetManifestDocumentEntry>();
|
||||
ProviderId = providerId ?? "";
|
||||
Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema;
|
||||
}
|
||||
|
||||
public string Schema { get; }
|
||||
|
||||
public string ManifestRef { get; }
|
||||
|
||||
public string ProviderId { get; }
|
||||
|
||||
public IReadOnlyList<EveUnityPlayableWorldAssetManifestDocumentEntry> Entries { get; }
|
||||
}
|
||||
|
||||
public sealed class EveUnityPlayableWorldAssetManifestDocumentEntry
|
||||
{
|
||||
public EveUnityPlayableWorldAssetManifestDocumentEntry(
|
||||
string assetRef,
|
||||
string entityKind,
|
||||
string resourcesPath,
|
||||
string prefabKey,
|
||||
string presentationKind = "provider-asset-ref")
|
||||
{
|
||||
AssetRef = assetRef ?? "";
|
||||
EntityKind = entityKind ?? "";
|
||||
ResourcesPath = resourcesPath ?? "";
|
||||
PrefabKey = prefabKey ?? "";
|
||||
PresentationKind = presentationKind ?? "";
|
||||
}
|
||||
|
||||
public string AssetRef { get; }
|
||||
|
||||
public string EntityKind { get; }
|
||||
|
||||
public string ResourcesPath { get; }
|
||||
|
||||
public string PrefabKey { get; }
|
||||
|
||||
public string PresentationKind { get; }
|
||||
}
|
||||
|
||||
public sealed class EveUnityPlayableWorldAssetManifestEntry
|
||||
{
|
||||
public EveUnityPlayableWorldAssetManifestEntry(
|
||||
string assetRef,
|
||||
string entityKind,
|
||||
string resourcesPath,
|
||||
string prefabKey,
|
||||
string presentationKind = "provider-asset-ref")
|
||||
{
|
||||
AssetRef = assetRef ?? "";
|
||||
EntityKind = entityKind ?? "";
|
||||
ResourcesPath = NormalizeResourcesPath(resourcesPath);
|
||||
PrefabKey = prefabKey ?? "";
|
||||
PresentationKind = presentationKind ?? "";
|
||||
}
|
||||
|
||||
public string AssetRef { get; }
|
||||
|
||||
public string EntityKind { get; }
|
||||
|
||||
public string ResourcesPath { get; }
|
||||
|
||||
public string PrefabKey { get; }
|
||||
|
||||
public string PresentationKind { get; }
|
||||
|
||||
public static string NormalizeResourcesPath(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
return "";
|
||||
|
||||
var normalized = path.Trim();
|
||||
foreach (var prefix in new[] { "resources://", "resource://", "Resources/" })
|
||||
{
|
||||
if (normalized.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
normalized = normalized.Substring(prefix.Length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (normalized.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase))
|
||||
normalized = normalized.Substring(0, normalized.Length - ".prefab".Length);
|
||||
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EveUnityManifestGameObjectAssetProvider : IEveUnityGameObjectAssetProvider
|
||||
{
|
||||
private readonly EveUnityPlayableWorldAssetManifest _manifest;
|
||||
private readonly IEveUnityGameObjectAssetProvider? _fallback;
|
||||
|
||||
public EveUnityManifestGameObjectAssetProvider(
|
||||
EveUnityPlayableWorldAssetManifest manifest,
|
||||
IEveUnityGameObjectAssetProvider? fallback = null)
|
||||
{
|
||||
_manifest = manifest ?? throw new ArgumentNullException(nameof(manifest));
|
||||
_fallback = fallback;
|
||||
}
|
||||
|
||||
public string ManifestRef => _manifest.ManifestRef;
|
||||
|
||||
public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset)
|
||||
{
|
||||
if (asset == null) throw new ArgumentNullException(nameof(asset));
|
||||
|
||||
var entry = _manifest.Find(asset);
|
||||
if (entry != null && !string.IsNullOrWhiteSpace(entry.ResourcesPath))
|
||||
{
|
||||
var prefab = Resources.Load<GameObject>(entry.ResourcesPath);
|
||||
if (prefab != null)
|
||||
return prefab;
|
||||
}
|
||||
|
||||
return _fallback?.ResolvePrefab(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0a1f1e9cabc1b044dbf58bc878a6d09a
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
using UnityEngine;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public sealed class EveUnityPlayableWorldCameraRig : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private EveUnityPlayableWorldClientHost? host;
|
||||
[SerializeField] private Transform? cameraTransform;
|
||||
[SerializeField] private bool driveInLateUpdate = true;
|
||||
[SerializeField] private float distance = 10f;
|
||||
[SerializeField] private float height = 6f;
|
||||
[SerializeField] private float yawDegrees = 35f;
|
||||
[SerializeField] private float followDamping = 12f;
|
||||
|
||||
public EveUnityPlayableWorldClientHost? Host
|
||||
{
|
||||
get => host;
|
||||
set => host = value;
|
||||
}
|
||||
|
||||
public Transform? CameraTransform
|
||||
{
|
||||
get => cameraTransform;
|
||||
set => cameraTransform = value;
|
||||
}
|
||||
|
||||
public bool ApplyRig(float deltaTime)
|
||||
{
|
||||
var resolvedHost = ResolveHost();
|
||||
var activeWorld = resolvedHost?.ActiveWorld;
|
||||
if (resolvedHost == null || activeWorld == null)
|
||||
return false;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(activeWorld.CameraRig) &&
|
||||
activeWorld.CameraRig != "arpg.orbital-follow.v1" &&
|
||||
activeWorld.CameraRig != "third-person-orbit")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var camera = cameraTransform != null ? cameraTransform : transform;
|
||||
var player = FindPlayerMarker(resolvedHost, activeWorld.PlayerEntityId);
|
||||
if (player == null)
|
||||
return false;
|
||||
|
||||
var target = player.transform.position;
|
||||
var orbit = Quaternion.Euler(0f, yawDegrees, 0f) * (Vector3.back * Mathf.Max(0.1f, distance));
|
||||
var desiredPosition = target + orbit + (Vector3.up * Mathf.Max(0f, height));
|
||||
var t = deltaTime <= 0f
|
||||
? 1f
|
||||
: 1f - Mathf.Exp(-Mathf.Max(0.01f, followDamping) * deltaTime);
|
||||
|
||||
camera.position = Vector3.Lerp(camera.position, desiredPosition, t);
|
||||
camera.LookAt(target);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (driveInLateUpdate)
|
||||
ApplyRig(Time.deltaTime);
|
||||
}
|
||||
|
||||
private EveUnityPlayableWorldClientHost? ResolveHost()
|
||||
{
|
||||
if (host != null)
|
||||
return host;
|
||||
|
||||
host = GetComponent<EveUnityPlayableWorldClientHost>();
|
||||
return host;
|
||||
}
|
||||
|
||||
private static EveUnityPlayableWorldEntityMarker? FindPlayerMarker(
|
||||
EveUnityPlayableWorldClientHost host,
|
||||
string playerEntityId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(playerEntityId))
|
||||
return null;
|
||||
|
||||
var markers = host.SceneRoot.GetComponentsInChildren<EveUnityPlayableWorldEntityMarker>();
|
||||
foreach (var marker in markers)
|
||||
{
|
||||
if (marker != null && marker.EntityId == playerEntityId)
|
||||
return marker;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3e68cf66c50b4951a9d7f7de68f0ad0e
|
||||
|
|
@ -0,0 +1,193 @@
|
|||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public sealed class EveUnityPlayableWorldClientBootstrap : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private EveUnityPlayableWorldClientHost? host;
|
||||
[SerializeField] private MonoBehaviour? provider;
|
||||
[SerializeField] private MonoBehaviour? providerSurfaceDocuments;
|
||||
[SerializeField] private MonoBehaviour? commandSink;
|
||||
[SerializeField] private MonoBehaviour? assetManifestDocuments;
|
||||
[SerializeField] private MonoBehaviour? receiptSource;
|
||||
[SerializeField] private MonoBehaviour? fallbackAssetProvider;
|
||||
[SerializeField] private Transform? sceneRoot;
|
||||
[SerializeField] private Transform? cameraTransform;
|
||||
[SerializeField] private bool createSceneRootIfMissing = true;
|
||||
[SerializeField] private bool attachInputDriver = true;
|
||||
[SerializeField] private bool attachCameraRig = true;
|
||||
[SerializeField] private bool connectOnEnable = true;
|
||||
|
||||
public EveUnityPlayableWorldClientHost? Host => host;
|
||||
|
||||
public Transform? SceneRoot => sceneRoot;
|
||||
|
||||
public Transform? CameraTransform => cameraTransform;
|
||||
|
||||
public EveUnityPlayableWorldPresentation? LastPresentation { get; private set; }
|
||||
|
||||
public void ConfigureProvider(MonoBehaviour providerComponent)
|
||||
{
|
||||
provider = providerComponent != null ? providerComponent : throw new ArgumentNullException(nameof(providerComponent));
|
||||
providerSurfaceDocuments = provider;
|
||||
commandSink = provider;
|
||||
assetManifestDocuments = provider;
|
||||
receiptSource = provider;
|
||||
}
|
||||
|
||||
public EveUnityPlayableWorldPresentation Mount()
|
||||
{
|
||||
var resolvedHost = ResolveHost();
|
||||
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,
|
||||
null);
|
||||
|
||||
sceneRoot = ResolveSceneRoot();
|
||||
resolvedHost.ConnectOnEnable = false;
|
||||
resolvedHost.Configure(
|
||||
sceneRoot,
|
||||
surfaceDocuments,
|
||||
resolvedCommandSink,
|
||||
resolvedAssetDocuments,
|
||||
resolvedReceiptSource,
|
||||
resolvedFallbackAssetProvider);
|
||||
|
||||
if (attachInputDriver)
|
||||
ConfigureInputDriver(resolvedHost);
|
||||
|
||||
if (attachCameraRig)
|
||||
ConfigureCameraRig(resolvedHost);
|
||||
|
||||
LastPresentation = resolvedHost.Connect();
|
||||
return LastPresentation;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (host != null && provider != null)
|
||||
host.ConnectOnEnable = false;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (connectOnEnable)
|
||||
Mount();
|
||||
}
|
||||
|
||||
private EveUnityPlayableWorldClientHost ResolveHost()
|
||||
{
|
||||
if (host != null)
|
||||
return host;
|
||||
|
||||
host = GetComponent<EveUnityPlayableWorldClientHost>();
|
||||
if (host == null)
|
||||
host = gameObject.AddComponent<EveUnityPlayableWorldClientHost>();
|
||||
|
||||
return host;
|
||||
}
|
||||
|
||||
private Transform ResolveSceneRoot()
|
||||
{
|
||||
if (sceneRoot != null)
|
||||
return sceneRoot;
|
||||
|
||||
if (!createSceneRootIfMissing)
|
||||
return transform;
|
||||
|
||||
var root = new GameObject("Eve Unity Playable World Root");
|
||||
root.transform.SetParent(transform, false);
|
||||
sceneRoot = root.transform;
|
||||
return sceneRoot;
|
||||
}
|
||||
|
||||
private Transform ResolveCameraTransform()
|
||||
{
|
||||
if (cameraTransform != null)
|
||||
return cameraTransform;
|
||||
|
||||
var mainCamera = Camera.main;
|
||||
if (mainCamera != null)
|
||||
{
|
||||
cameraTransform = mainCamera.transform;
|
||||
return cameraTransform;
|
||||
}
|
||||
|
||||
var cameraObject = new GameObject("Eve Unity Playable World Camera");
|
||||
cameraObject.transform.SetParent(transform, false);
|
||||
cameraObject.AddComponent<Camera>();
|
||||
cameraTransform = cameraObject.transform;
|
||||
return cameraTransform;
|
||||
}
|
||||
|
||||
private void ConfigureInputDriver(EveUnityPlayableWorldClientHost resolvedHost)
|
||||
{
|
||||
var driver = GetComponent<EveUnityPlayableWorldInputDriver>();
|
||||
if (driver == null)
|
||||
driver = gameObject.AddComponent<EveUnityPlayableWorldInputDriver>();
|
||||
|
||||
driver.Host = resolvedHost;
|
||||
driver.CameraTransform = ResolveCameraTransform();
|
||||
}
|
||||
|
||||
private void ConfigureCameraRig(EveUnityPlayableWorldClientHost resolvedHost)
|
||||
{
|
||||
var rig = GetComponent<EveUnityPlayableWorldCameraRig>();
|
||||
if (rig == null)
|
||||
rig = gameObject.AddComponent<EveUnityPlayableWorldCameraRig>();
|
||||
|
||||
rig.Host = resolvedHost;
|
||||
rig.CameraTransform = ResolveCameraTransform();
|
||||
}
|
||||
|
||||
private MonoBehaviour ResolveBehaviour<T>(
|
||||
MonoBehaviour? explicitComponent,
|
||||
MonoBehaviour? providerComponent,
|
||||
string fieldName) where T : class
|
||||
{
|
||||
var resolved = ResolveOptionalBehaviour<T>(explicitComponent, providerComponent);
|
||||
if (resolved != null)
|
||||
return resolved;
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"Eve Unity playable world bootstrap requires '{fieldName}' or provider to implement {typeof(T).Name}.");
|
||||
}
|
||||
|
||||
private MonoBehaviour? ResolveOptionalBehaviour<T>(
|
||||
MonoBehaviour? explicitComponent,
|
||||
MonoBehaviour? providerComponent) where T : class
|
||||
{
|
||||
if (explicitComponent is T)
|
||||
return explicitComponent;
|
||||
|
||||
if (providerComponent is T)
|
||||
return providerComponent;
|
||||
|
||||
var components = GetComponents<MonoBehaviour>();
|
||||
foreach (var component in components)
|
||||
{
|
||||
if (component is T)
|
||||
return component;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 1d5e4b8e1e104bb99522dc2b75d7f4c9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameCult.Eve.Surface;
|
||||
using UnityEngine;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public sealed class EveUnityPlayableWorldClientHost : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private Transform? sceneRoot;
|
||||
[SerializeField] private MonoBehaviour? providerSurfaceDocuments;
|
||||
[SerializeField] private MonoBehaviour? commandSink;
|
||||
[SerializeField] private MonoBehaviour? assetManifestDocuments;
|
||||
[SerializeField] private MonoBehaviour? receiptSource;
|
||||
[SerializeField] private MonoBehaviour? fallbackAssetProvider;
|
||||
[SerializeField] private bool connectOnEnable;
|
||||
[SerializeField] private bool refreshInUpdate = true;
|
||||
[SerializeField] private float refreshIntervalSeconds = 0.1f;
|
||||
|
||||
private float _nextRefreshAt;
|
||||
|
||||
public EveUnityPlayableWorldRuntime? Runtime { get; private set; }
|
||||
|
||||
public EveUnityPlayableWorldProjection? ActiveWorld => Runtime?.ActiveWorld;
|
||||
|
||||
public EveUnitySceneProjection? ActiveProjection => Runtime?.ActiveProjection;
|
||||
|
||||
public EveUnityPlayableWorldPresentation? LastPresentation => Runtime?.LastPresentation;
|
||||
|
||||
public EveUnitySceneCommandReceipt? LastReceipt => Runtime?.LastReceipt;
|
||||
|
||||
public long ActiveVersion => Runtime?.ActiveVersion ?? 0;
|
||||
|
||||
public Transform SceneRoot => sceneRoot == null ? transform : sceneRoot;
|
||||
|
||||
public bool ConnectOnEnable
|
||||
{
|
||||
get => connectOnEnable;
|
||||
set => connectOnEnable = value;
|
||||
}
|
||||
|
||||
public void Configure(
|
||||
Transform? sceneRoot,
|
||||
MonoBehaviour providerSurfaceDocuments,
|
||||
MonoBehaviour commandSink,
|
||||
MonoBehaviour? assetManifestDocuments = null,
|
||||
MonoBehaviour? receiptSource = null,
|
||||
MonoBehaviour? fallbackAssetProvider = null)
|
||||
{
|
||||
this.sceneRoot = sceneRoot;
|
||||
this.providerSurfaceDocuments = providerSurfaceDocuments;
|
||||
this.commandSink = commandSink;
|
||||
this.assetManifestDocuments = assetManifestDocuments;
|
||||
this.receiptSource = receiptSource;
|
||||
this.fallbackAssetProvider = fallbackAssetProvider;
|
||||
}
|
||||
|
||||
public EveUnityPlayableWorldPresentation Connect()
|
||||
{
|
||||
Disconnect();
|
||||
RefreshProviderSources();
|
||||
|
||||
Runtime = EveUnityPlayableWorldRuntime.CreateForGameObjectScene(
|
||||
SceneRoot,
|
||||
Required<IEveUnitySceneProviderSurfaceDocumentSource>(
|
||||
providerSurfaceDocuments,
|
||||
nameof(providerSurfaceDocuments)),
|
||||
Required<IEveUnitySceneCommandSink>(
|
||||
commandSink,
|
||||
nameof(commandSink)),
|
||||
Optional<IEveUnityPlayableWorldAssetManifestDocumentSource>(assetManifestDocuments),
|
||||
Optional<IEveUnitySceneCommandReceiptSource>(receiptSource),
|
||||
Optional<IEveUnityGameObjectAssetProvider>(fallbackAssetProvider));
|
||||
|
||||
return Runtime.Connect();
|
||||
}
|
||||
|
||||
public EveUnityPlayableWorldPresentation Refresh()
|
||||
{
|
||||
RefreshProviderSources();
|
||||
if (Runtime == null)
|
||||
return Connect();
|
||||
|
||||
return Runtime.Refresh();
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitMoveIntent(
|
||||
string entityId,
|
||||
float targetX,
|
||||
float targetY,
|
||||
float targetZ,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return RequireRuntime().SubmitMoveIntent(entityId, targetX, targetY, targetZ, issuedAt);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitMoveVectorIntent(
|
||||
string entityId,
|
||||
float directionX,
|
||||
float directionY,
|
||||
float scalarValue = 1f,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return RequireRuntime().SubmitMoveVectorIntent(entityId, directionX, directionY, scalarValue, issuedAt);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitFocusIntent(
|
||||
string entityId,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return RequireRuntime().SubmitFocusIntent(entityId, issuedAt);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitTargetIntent(
|
||||
string sourceEntityId,
|
||||
string targetEntityId,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return RequireRuntime().SubmitTargetIntent(sourceEntityId, targetEntityId, issuedAt);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitActionIntent(
|
||||
string entityId,
|
||||
string actionId,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return RequireRuntime().SubmitActionIntent(entityId, actionId, issuedAt);
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
Runtime?.Dispose();
|
||||
Runtime = null;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (connectOnEnable)
|
||||
Connect();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!refreshInUpdate || Runtime == null)
|
||||
return;
|
||||
|
||||
if (Time.unscaledTime < _nextRefreshAt)
|
||||
return;
|
||||
|
||||
_nextRefreshAt = Time.unscaledTime + Math.Max(0.01f, refreshIntervalSeconds);
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
private EveUnityPlayableWorldRuntime RequireRuntime()
|
||||
{
|
||||
if (Runtime == null)
|
||||
throw new InvalidOperationException("Eve Unity playable world client host is not connected.");
|
||||
|
||||
return Runtime;
|
||||
}
|
||||
|
||||
private void RefreshProviderSources()
|
||||
{
|
||||
var refreshed = new HashSet<MonoBehaviour>();
|
||||
RefreshProvider(providerSurfaceDocuments, refreshed);
|
||||
RefreshProvider(assetManifestDocuments, refreshed);
|
||||
RefreshProvider(receiptSource, refreshed);
|
||||
RefreshProvider(commandSink, refreshed);
|
||||
}
|
||||
|
||||
private static void RefreshProvider(MonoBehaviour? behaviour, ISet<MonoBehaviour> refreshed)
|
||||
{
|
||||
if (behaviour is IEveUnityProviderRefreshSource refreshSource && refreshed.Add(behaviour))
|
||||
refreshSource.Refresh();
|
||||
}
|
||||
|
||||
private static T Required<T>(MonoBehaviour? behaviour, string fieldName) where T : class
|
||||
{
|
||||
var resolved = behaviour as T;
|
||||
if (resolved == null)
|
||||
throw new InvalidOperationException(
|
||||
$"Eve Unity playable world client host requires '{fieldName}' to implement {typeof(T).Name}.");
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private static T? Optional<T>(MonoBehaviour? behaviour) where T : class
|
||||
{
|
||||
return behaviour as T;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: df3f3a1abd3f14541bf6d7ec59343661
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
using System;
|
||||
using GameCult.Eve.Surface;
|
||||
using UnityEngine;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public sealed class EveUnityPlayableWorldInputDriver : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private EveUnityPlayableWorldClientHost? host;
|
||||
[SerializeField] private Transform? cameraTransform;
|
||||
[SerializeField] private bool driveInUpdate = true;
|
||||
[SerializeField] private string horizontalAxis = "Horizontal";
|
||||
[SerializeField] private string verticalAxis = "Vertical";
|
||||
[SerializeField] private string actionButton = "Fire1";
|
||||
[SerializeField] private float deadZone = 0.01f;
|
||||
[SerializeField] private float commandIntervalSeconds = 0.05f;
|
||||
[SerializeField] private bool submitZeroOnRelease = true;
|
||||
|
||||
private float _nextCommandAt;
|
||||
private bool _wasMoving;
|
||||
|
||||
public EveUnityPlayableWorldClientHost? Host
|
||||
{
|
||||
get => host;
|
||||
set => host = value;
|
||||
}
|
||||
|
||||
public Transform? CameraTransform
|
||||
{
|
||||
get => cameraTransform;
|
||||
set => cameraTransform = value;
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest? SubmitMoveVectorInput(
|
||||
float horizontal,
|
||||
float vertical,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
var resolvedHost = ResolveHost();
|
||||
if (resolvedHost == null || resolvedHost.ActiveWorld == null)
|
||||
return null;
|
||||
|
||||
var movement = EveUnityPlayableWorldMoveVector.FromCameraRelativeInput(
|
||||
horizontal,
|
||||
vertical,
|
||||
cameraTransform,
|
||||
deadZone);
|
||||
|
||||
if (!movement.HasInput)
|
||||
{
|
||||
if (!submitZeroOnRelease || !_wasMoving)
|
||||
return null;
|
||||
|
||||
_wasMoving = false;
|
||||
return resolvedHost.SubmitMoveVectorIntent(
|
||||
resolvedHost.ActiveWorld.PlayerEntityId,
|
||||
0f,
|
||||
0f,
|
||||
0f,
|
||||
issuedAt);
|
||||
}
|
||||
|
||||
_wasMoving = true;
|
||||
return resolvedHost.SubmitMoveVectorIntent(
|
||||
resolvedHost.ActiveWorld.PlayerEntityId,
|
||||
movement.DirectionX,
|
||||
movement.DirectionY,
|
||||
movement.ScalarValue,
|
||||
issuedAt);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest? SubmitPrimaryAction(string actionId = "primary")
|
||||
{
|
||||
var resolvedHost = ResolveHost();
|
||||
if (resolvedHost?.ActiveWorld == null)
|
||||
return null;
|
||||
|
||||
return resolvedHost.SubmitActionIntent(
|
||||
resolvedHost.ActiveWorld.PlayerEntityId,
|
||||
string.IsNullOrWhiteSpace(actionId) ? "primary" : actionId);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!driveInUpdate || Time.unscaledTime < _nextCommandAt)
|
||||
return;
|
||||
|
||||
_nextCommandAt = Time.unscaledTime + Math.Max(0.01f, commandIntervalSeconds);
|
||||
SubmitMoveVectorInput(Input.GetAxisRaw(horizontalAxis), Input.GetAxisRaw(verticalAxis));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(actionButton) && Input.GetButtonDown(actionButton))
|
||||
SubmitPrimaryAction(actionButton);
|
||||
}
|
||||
|
||||
private EveUnityPlayableWorldClientHost? ResolveHost()
|
||||
{
|
||||
if (host != null)
|
||||
return host;
|
||||
|
||||
host = GetComponent<EveUnityPlayableWorldClientHost>();
|
||||
return host;
|
||||
}
|
||||
}
|
||||
|
||||
public readonly struct EveUnityPlayableWorldMoveVector
|
||||
{
|
||||
public EveUnityPlayableWorldMoveVector(
|
||||
float directionX,
|
||||
float directionY,
|
||||
float scalarValue,
|
||||
bool hasInput)
|
||||
{
|
||||
DirectionX = directionX;
|
||||
DirectionY = directionY;
|
||||
ScalarValue = scalarValue;
|
||||
HasInput = hasInput;
|
||||
}
|
||||
|
||||
public float DirectionX { get; }
|
||||
|
||||
public float DirectionY { get; }
|
||||
|
||||
public float ScalarValue { get; }
|
||||
|
||||
public bool HasInput { get; }
|
||||
|
||||
public static EveUnityPlayableWorldMoveVector FromCameraRelativeInput(
|
||||
float horizontal,
|
||||
float vertical,
|
||||
Transform? cameraTransform,
|
||||
float deadZone = 0.01f)
|
||||
{
|
||||
var input = new Vector2(horizontal, vertical);
|
||||
var magnitude = Mathf.Clamp01(input.magnitude);
|
||||
if (magnitude <= Mathf.Max(0f, deadZone))
|
||||
return new EveUnityPlayableWorldMoveVector(0f, 0f, 0f, false);
|
||||
|
||||
Vector3 direction;
|
||||
if (cameraTransform == null)
|
||||
{
|
||||
direction = new Vector3(horizontal, 0f, vertical);
|
||||
}
|
||||
else
|
||||
{
|
||||
var forward = cameraTransform.forward;
|
||||
forward.y = 0f;
|
||||
if (forward.sqrMagnitude <= 0.0001f)
|
||||
forward = Vector3.forward;
|
||||
forward.Normalize();
|
||||
|
||||
var right = cameraTransform.right;
|
||||
right.y = 0f;
|
||||
if (right.sqrMagnitude <= 0.0001f)
|
||||
right = Vector3.right;
|
||||
right.Normalize();
|
||||
|
||||
direction = (right * horizontal) + (forward * vertical);
|
||||
}
|
||||
|
||||
if (direction.sqrMagnitude <= 0.0001f)
|
||||
return new EveUnityPlayableWorldMoveVector(0f, 0f, 0f, false);
|
||||
|
||||
direction.Normalize();
|
||||
return new EveUnityPlayableWorldMoveVector(
|
||||
direction.x,
|
||||
direction.z,
|
||||
magnitude,
|
||||
true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7ad1be678e6e4d6d9c72769358e0a233
|
||||
|
|
@ -0,0 +1,225 @@
|
|||
using System;
|
||||
using GameCult.Eve.Surface;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public sealed class EveUnityPlayableWorldLiveClient : IDisposable
|
||||
{
|
||||
private readonly EveUnitySceneProviderConnection _connection;
|
||||
private readonly EveUnityPlayableWorldPresenter _presenter;
|
||||
private readonly IEveUnitySceneCommandReceiptSource? _receiptSource;
|
||||
private readonly bool _refreshOnTerminalReceipt;
|
||||
private bool _connected;
|
||||
private bool _receiptConnected;
|
||||
|
||||
public EveUnityPlayableWorldLiveClient(
|
||||
EveUnitySceneProviderConnection connection,
|
||||
EveUnityPlayableWorldPresenter presenter,
|
||||
IEveUnitySceneCommandReceiptSource? receiptSource = null,
|
||||
bool refreshOnTerminalReceipt = true)
|
||||
{
|
||||
_connection = connection ?? throw new ArgumentNullException(nameof(connection));
|
||||
_presenter = presenter ?? throw new ArgumentNullException(nameof(presenter));
|
||||
_receiptSource = receiptSource;
|
||||
_refreshOnTerminalReceipt = refreshOnTerminalReceipt;
|
||||
}
|
||||
|
||||
public EveUnitySceneProjection? ActiveProjection => _connection.ActiveProjection;
|
||||
|
||||
public EveUnityPlayableWorldProjection? ActiveWorld => ActiveProjection?.PlayableWorld;
|
||||
|
||||
public EveUnityPlayableWorldPresentation? LastPresentation { get; private set; }
|
||||
|
||||
public EveUnitySceneCommandReceipt? LastReceipt { get; private set; }
|
||||
|
||||
public long ActiveVersion => _connection.ActiveVersion;
|
||||
|
||||
public string SourcePointer => _connection.SourcePointer;
|
||||
|
||||
public event Action<EveUnitySceneCommandReceipt>? ReceiptAvailable;
|
||||
|
||||
public EveUnityPlayableWorldPresentation Connect()
|
||||
{
|
||||
EnsureSubscribed();
|
||||
EnsureReceiptSubscribed();
|
||||
_connection.Connect();
|
||||
return RequirePresentation();
|
||||
}
|
||||
|
||||
public EveUnityPlayableWorldPresentation Refresh()
|
||||
{
|
||||
EnsureSubscribed();
|
||||
_connection.Refresh();
|
||||
return RequirePresentation();
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitMoveIntent(
|
||||
string entityId,
|
||||
float targetX,
|
||||
float targetY,
|
||||
float targetZ,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return _connection.SubmitMoveIntent(entityId, targetX, targetY, targetZ, issuedAt);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitMoveVectorIntent(
|
||||
string entityId,
|
||||
float directionX,
|
||||
float directionY,
|
||||
float scalarValue = 1f,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return _connection.SubmitMoveVectorIntent(entityId, directionX, directionY, scalarValue, issuedAt);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitFocusIntent(
|
||||
string entityId,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return _connection.SubmitFocusIntent(entityId, issuedAt);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitTargetIntent(
|
||||
string sourceEntityId,
|
||||
string targetEntityId,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return _connection.SubmitTargetIntent(sourceEntityId, targetEntityId, issuedAt);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitActionIntent(
|
||||
string entityId,
|
||||
string actionId,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return _connection.SubmitActionIntent(entityId, actionId, issuedAt);
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
if (_connected)
|
||||
{
|
||||
_connection.ProjectionUpdated -= OnProjectionUpdated;
|
||||
_connected = false;
|
||||
}
|
||||
|
||||
if (_receiptConnected && _receiptSource != null)
|
||||
{
|
||||
_receiptSource.ReceiptAvailable -= OnReceiptAvailable;
|
||||
_receiptConnected = false;
|
||||
}
|
||||
|
||||
_connection.Disconnect();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disconnect();
|
||||
_connection.Dispose();
|
||||
}
|
||||
|
||||
private void EnsureSubscribed()
|
||||
{
|
||||
if (_connected)
|
||||
return;
|
||||
|
||||
_connection.ProjectionUpdated += OnProjectionUpdated;
|
||||
_connected = true;
|
||||
}
|
||||
|
||||
private void EnsureReceiptSubscribed()
|
||||
{
|
||||
if (_receiptSource == null || _receiptConnected)
|
||||
return;
|
||||
|
||||
_receiptSource.ReceiptAvailable += OnReceiptAvailable;
|
||||
_receiptConnected = true;
|
||||
}
|
||||
|
||||
private void OnProjectionUpdated(EveUnitySceneProjection projection)
|
||||
{
|
||||
LastPresentation = _presenter.Apply(projection);
|
||||
}
|
||||
|
||||
private void OnReceiptAvailable(EveUnitySceneCommandReceipt receipt)
|
||||
{
|
||||
LastReceipt = receipt ?? throw new ArgumentNullException(nameof(receipt));
|
||||
ReceiptAvailable?.Invoke(receipt);
|
||||
|
||||
if (_refreshOnTerminalReceipt && receipt.ShouldRefreshProviderSurface)
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private EveUnityPlayableWorldPresentation RequirePresentation()
|
||||
{
|
||||
if (LastPresentation == null)
|
||||
throw new InvalidOperationException("The active Unity scene surface has not produced a playable world presentation.");
|
||||
return LastPresentation;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IEveUnitySceneCommandReceiptSource
|
||||
{
|
||||
event Action<EveUnitySceneCommandReceipt> ReceiptAvailable;
|
||||
}
|
||||
|
||||
public sealed class EveUnitySceneCommandReceipt
|
||||
{
|
||||
public EveUnitySceneCommandReceipt(
|
||||
string receiptId,
|
||||
string command,
|
||||
string commandId,
|
||||
string state,
|
||||
string ownerRepo,
|
||||
string authority,
|
||||
string schema = "gamecult.eve.command_receipt.v1",
|
||||
string providerId = "",
|
||||
string surfaceId = "",
|
||||
string message = "",
|
||||
DateTimeOffset? issuedAtUtc = null)
|
||||
{
|
||||
ReceiptId = receiptId ?? "";
|
||||
Command = command ?? "";
|
||||
CommandId = commandId ?? "";
|
||||
State = state ?? "";
|
||||
OwnerRepo = ownerRepo ?? "";
|
||||
Authority = authority ?? "";
|
||||
Schema = string.IsNullOrWhiteSpace(schema) ? "gamecult.eve.command_receipt.v1" : schema;
|
||||
ProviderId = providerId ?? "";
|
||||
SurfaceId = surfaceId ?? "";
|
||||
Message = message ?? "";
|
||||
IssuedAtUtc = issuedAtUtc;
|
||||
}
|
||||
|
||||
public string Schema { get; }
|
||||
|
||||
public string ReceiptId { get; }
|
||||
|
||||
public string Command { get; }
|
||||
|
||||
public string CommandId { get; }
|
||||
|
||||
public string State { get; }
|
||||
|
||||
public string OwnerRepo { get; }
|
||||
|
||||
public string Authority { get; }
|
||||
|
||||
public string ProviderId { get; }
|
||||
|
||||
public string SurfaceId { get; }
|
||||
|
||||
public string Message { get; }
|
||||
|
||||
public DateTimeOffset? IssuedAtUtc { get; }
|
||||
|
||||
public bool IsProviderOwned => !string.IsNullOrWhiteSpace(OwnerRepo) && !string.Equals(OwnerRepo, "EveUnity", StringComparison.Ordinal);
|
||||
|
||||
public bool ShouldRefreshProviderSurface =>
|
||||
string.Equals(State, "accepted", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(State, "reconciled", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 3b46f9972e945ca4daa29473de0bfb9b
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public interface IEveUnityPlayableWorldSceneSink
|
||||
{
|
||||
void ConfigureWorld(EveUnityPlayableWorldProjection world);
|
||||
|
||||
void UpsertEntity(EveUnityPlayableWorldEntity entity, EveUnityPlayableWorldAssetBinding asset);
|
||||
|
||||
void RemoveEntity(string entityId);
|
||||
}
|
||||
|
||||
public interface IEveUnityPlayableWorldAssetResolver
|
||||
{
|
||||
EveUnityPlayableWorldAssetBinding Resolve(EveUnityPlayableWorldEntity entity);
|
||||
}
|
||||
|
||||
public sealed class EveUnityPlayableWorldPresenter
|
||||
{
|
||||
private readonly IEveUnityPlayableWorldSceneSink _sceneSink;
|
||||
private readonly IEveUnityPlayableWorldAssetResolver _assetResolver;
|
||||
private readonly HashSet<string> _knownEntityIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
public EveUnityPlayableWorldPresenter(
|
||||
IEveUnityPlayableWorldSceneSink sceneSink,
|
||||
IEveUnityPlayableWorldAssetResolver assetResolver)
|
||||
{
|
||||
_sceneSink = sceneSink ?? throw new ArgumentNullException(nameof(sceneSink));
|
||||
_assetResolver = assetResolver ?? throw new ArgumentNullException(nameof(assetResolver));
|
||||
}
|
||||
|
||||
public EveUnityPlayableWorldPresentation Apply(EveUnitySceneProjection projection)
|
||||
{
|
||||
if (projection == null) throw new ArgumentNullException(nameof(projection));
|
||||
if (projection.PlayableWorld == null)
|
||||
throw new InvalidOperationException("Unity playable world presentation requires a playableWorld projection.");
|
||||
|
||||
return Apply(projection.PlayableWorld);
|
||||
}
|
||||
|
||||
public EveUnityPlayableWorldPresentation Apply(EveUnityPlayableWorldProjection world)
|
||||
{
|
||||
if (world == null) throw new ArgumentNullException(nameof(world));
|
||||
|
||||
var nextEntityIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
_sceneSink.ConfigureWorld(world);
|
||||
|
||||
var upserted = 0;
|
||||
foreach (var entity in world.Entities)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(entity.EntityId))
|
||||
continue;
|
||||
|
||||
nextEntityIds.Add(entity.EntityId);
|
||||
_sceneSink.UpsertEntity(entity, _assetResolver.Resolve(entity));
|
||||
upserted++;
|
||||
}
|
||||
|
||||
var removed = 0;
|
||||
var previousEntityIds = new List<string>(_knownEntityIds);
|
||||
foreach (var entityId in previousEntityIds)
|
||||
{
|
||||
if (nextEntityIds.Contains(entityId))
|
||||
continue;
|
||||
|
||||
_sceneSink.RemoveEntity(entityId);
|
||||
_knownEntityIds.Remove(entityId);
|
||||
removed++;
|
||||
}
|
||||
|
||||
foreach (var entityId in nextEntityIds)
|
||||
_knownEntityIds.Add(entityId);
|
||||
|
||||
return new EveUnityPlayableWorldPresentation(
|
||||
world.WorldRootId,
|
||||
world.PlayerEntityId,
|
||||
world.InputProfile,
|
||||
world.CameraRig,
|
||||
upserted,
|
||||
removed,
|
||||
_knownEntityIds.Count);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EveUnityPlayableWorldAssetBinding
|
||||
{
|
||||
public EveUnityPlayableWorldAssetBinding(
|
||||
string assetRef,
|
||||
string entityKind,
|
||||
string presentationKind)
|
||||
{
|
||||
AssetRef = assetRef ?? "";
|
||||
EntityKind = entityKind ?? "";
|
||||
PresentationKind = presentationKind ?? "";
|
||||
}
|
||||
|
||||
public string AssetRef { get; }
|
||||
|
||||
public string EntityKind { get; }
|
||||
|
||||
public string PresentationKind { get; }
|
||||
}
|
||||
|
||||
public sealed class EveUnityPlayableWorldPresentation
|
||||
{
|
||||
public EveUnityPlayableWorldPresentation(
|
||||
string worldRootId,
|
||||
string playerEntityId,
|
||||
string inputProfile,
|
||||
string cameraRig,
|
||||
int upsertedEntities,
|
||||
int removedEntities,
|
||||
int activeEntities)
|
||||
{
|
||||
WorldRootId = worldRootId ?? "";
|
||||
PlayerEntityId = playerEntityId ?? "";
|
||||
InputProfile = inputProfile ?? "";
|
||||
CameraRig = cameraRig ?? "";
|
||||
UpsertedEntities = upsertedEntities;
|
||||
RemovedEntities = removedEntities;
|
||||
ActiveEntities = activeEntities;
|
||||
}
|
||||
|
||||
public string WorldRootId { get; }
|
||||
|
||||
public string PlayerEntityId { get; }
|
||||
|
||||
public string InputProfile { get; }
|
||||
|
||||
public string CameraRig { get; }
|
||||
|
||||
public int UpsertedEntities { get; }
|
||||
|
||||
public int RemovedEntities { get; }
|
||||
|
||||
public int ActiveEntities { get; }
|
||||
}
|
||||
|
||||
public sealed class EveUnityAssetRefResolver : IEveUnityPlayableWorldAssetResolver
|
||||
{
|
||||
public EveUnityPlayableWorldAssetBinding Resolve(EveUnityPlayableWorldEntity entity)
|
||||
{
|
||||
if (entity == null) throw new ArgumentNullException(nameof(entity));
|
||||
return new EveUnityPlayableWorldAssetBinding(
|
||||
entity.AssetRef,
|
||||
entity.EntityKind,
|
||||
string.IsNullOrWhiteSpace(entity.AssetRef) ? "unity-generated-placeholder" : "provider-asset-ref");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 1df1c82e51d1ee04c96247cdff61101a
|
||||
|
|
@ -0,0 +1,207 @@
|
|||
using System;
|
||||
using GameCult.Eve.Surface;
|
||||
using UnityEngine;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public sealed class EveUnityPlayableWorldRuntime : IDisposable
|
||||
{
|
||||
private readonly EveUnitySceneProviderSurfaceDocumentSource _surfaceSource;
|
||||
private readonly EveUnitySceneProviderConnection _connection;
|
||||
private readonly EveUnityPlayableWorldLiveClient _client;
|
||||
private readonly EveUnityPlayableWorldAssetManifestDocumentSource? _assetManifestSource;
|
||||
private bool _assetManifestConnected;
|
||||
|
||||
public EveUnityPlayableWorldRuntime(
|
||||
IEveUnitySceneProviderSurfaceDocumentSource surfaceDocuments,
|
||||
IEveUnitySceneCommandSink commandSink,
|
||||
IEveUnityPlayableWorldSceneSink sceneSink,
|
||||
IEveUnityPlayableWorldAssetManifestDocumentSource? assetManifestDocuments = null,
|
||||
IEveUnitySceneCommandReceiptSource? receiptSource = null,
|
||||
IEveUnityPlayableWorldAssetResolver? assetResolver = null,
|
||||
EveUnityPlayableWorldAssetManifestCache? assetManifests = null)
|
||||
{
|
||||
if (surfaceDocuments == null) throw new ArgumentNullException(nameof(surfaceDocuments));
|
||||
if (commandSink == null) throw new ArgumentNullException(nameof(commandSink));
|
||||
if (sceneSink == null) throw new ArgumentNullException(nameof(sceneSink));
|
||||
|
||||
AssetManifests = assetManifests ?? new EveUnityPlayableWorldAssetManifestCache();
|
||||
_surfaceSource = new EveUnitySceneProviderSurfaceDocumentSource(surfaceDocuments);
|
||||
_connection = new EveUnitySceneProviderConnection(_surfaceSource, commandSink);
|
||||
_client = new EveUnityPlayableWorldLiveClient(
|
||||
_connection,
|
||||
new EveUnityPlayableWorldPresenter(sceneSink, assetResolver ?? new EveUnityAssetRefResolver()),
|
||||
receiptSource);
|
||||
|
||||
if (assetManifestDocuments != null)
|
||||
_assetManifestSource = new EveUnityPlayableWorldAssetManifestDocumentSource(assetManifestDocuments);
|
||||
}
|
||||
|
||||
public EveUnityPlayableWorldAssetManifestCache AssetManifests { get; }
|
||||
|
||||
public EveUnityPlayableWorldProjection? ActiveWorld => _client.ActiveWorld;
|
||||
|
||||
public EveUnitySceneProjection? ActiveProjection => _client.ActiveProjection;
|
||||
|
||||
public EveUnityPlayableWorldPresentation? LastPresentation => _client.LastPresentation;
|
||||
|
||||
public EveUnitySceneCommandReceipt? LastReceipt => _client.LastReceipt;
|
||||
|
||||
public long ActiveVersion => _client.ActiveVersion;
|
||||
|
||||
public string SourcePointer => _client.SourcePointer;
|
||||
|
||||
public event Action<EveUnitySceneCommandReceipt>? ReceiptAvailable
|
||||
{
|
||||
add { _client.ReceiptAvailable += value; }
|
||||
remove { _client.ReceiptAvailable -= value; }
|
||||
}
|
||||
|
||||
public static EveUnityPlayableWorldRuntime CreateForGameObjectScene(
|
||||
Transform root,
|
||||
IEveUnitySceneProviderSurfaceDocumentSource surfaceDocuments,
|
||||
IEveUnitySceneCommandSink commandSink,
|
||||
IEveUnityPlayableWorldAssetManifestDocumentSource? assetManifestDocuments = null,
|
||||
IEveUnitySceneCommandReceiptSource? receiptSource = null,
|
||||
IEveUnityGameObjectAssetProvider? fallbackAssetProvider = null)
|
||||
{
|
||||
EveUnityPlayableWorldRuntime? runtime = null;
|
||||
var assetManifests = new EveUnityPlayableWorldAssetManifestCache();
|
||||
var liveAssetProvider = new EveUnityLivePlayableWorldAssetProvider(
|
||||
assetManifests,
|
||||
() => runtime?.ActiveWorld,
|
||||
fallbackAssetProvider ?? new EveUnityResourcesAssetProvider());
|
||||
var sceneSink = new EveUnityGameObjectPlayableWorldSceneSink(root, liveAssetProvider);
|
||||
|
||||
runtime = new EveUnityPlayableWorldRuntime(
|
||||
surfaceDocuments,
|
||||
commandSink,
|
||||
sceneSink,
|
||||
assetManifestDocuments,
|
||||
receiptSource,
|
||||
new EveUnityAssetRefResolver(),
|
||||
assetManifests);
|
||||
return runtime;
|
||||
}
|
||||
|
||||
public EveUnityPlayableWorldPresentation Connect()
|
||||
{
|
||||
EnsureAssetManifestConnected();
|
||||
return _client.Connect();
|
||||
}
|
||||
|
||||
public EveUnityPlayableWorldPresentation Refresh()
|
||||
{
|
||||
EnsureAssetManifestConnected();
|
||||
return _client.Refresh();
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitMoveIntent(
|
||||
string entityId,
|
||||
float targetX,
|
||||
float targetY,
|
||||
float targetZ,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return _client.SubmitMoveIntent(entityId, targetX, targetY, targetZ, issuedAt);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitMoveVectorIntent(
|
||||
string entityId,
|
||||
float directionX,
|
||||
float directionY,
|
||||
float scalarValue = 1f,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return _client.SubmitMoveVectorIntent(entityId, directionX, directionY, scalarValue, issuedAt);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitFocusIntent(
|
||||
string entityId,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return _client.SubmitFocusIntent(entityId, issuedAt);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitTargetIntent(
|
||||
string sourceEntityId,
|
||||
string targetEntityId,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return _client.SubmitTargetIntent(sourceEntityId, targetEntityId, issuedAt);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitActionIntent(
|
||||
string entityId,
|
||||
string actionId,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return _client.SubmitActionIntent(entityId, actionId, issuedAt);
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
_client.Disconnect();
|
||||
|
||||
if (_assetManifestConnected && _assetManifestSource != null)
|
||||
{
|
||||
AssetManifests.Disconnect(_assetManifestSource);
|
||||
_assetManifestSource.Disconnect();
|
||||
_assetManifestConnected = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disconnect();
|
||||
_client.Dispose();
|
||||
_assetManifestSource?.Dispose();
|
||||
}
|
||||
|
||||
private void EnsureAssetManifestConnected()
|
||||
{
|
||||
if (_assetManifestConnected || _assetManifestSource == null)
|
||||
return;
|
||||
|
||||
_assetManifestSource.Connect();
|
||||
AssetManifests.Connect(_assetManifestSource);
|
||||
_assetManifestConnected = true;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EveUnityLivePlayableWorldAssetProvider : IEveUnityGameObjectAssetProvider
|
||||
{
|
||||
private readonly EveUnityPlayableWorldAssetManifestCache _assetManifests;
|
||||
private readonly Func<EveUnityPlayableWorldProjection?> _activeWorld;
|
||||
private readonly IEveUnityGameObjectAssetProvider _fallback;
|
||||
|
||||
public EveUnityLivePlayableWorldAssetProvider(
|
||||
EveUnityPlayableWorldAssetManifestCache assetManifests,
|
||||
Func<EveUnityPlayableWorldProjection?> activeWorld,
|
||||
IEveUnityGameObjectAssetProvider fallback)
|
||||
{
|
||||
_assetManifests = assetManifests ?? throw new ArgumentNullException(nameof(assetManifests));
|
||||
_activeWorld = activeWorld ?? throw new ArgumentNullException(nameof(activeWorld));
|
||||
_fallback = fallback ?? throw new ArgumentNullException(nameof(fallback));
|
||||
}
|
||||
|
||||
public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset)
|
||||
{
|
||||
if (asset == null) throw new ArgumentNullException(nameof(asset));
|
||||
|
||||
var world = _activeWorld();
|
||||
var manifest = world == null ? null : _assetManifests.GetForWorld(world);
|
||||
if (manifest != null)
|
||||
{
|
||||
var provider = new EveUnityManifestGameObjectAssetProvider(manifest, _fallback);
|
||||
var prefab = provider.ResolvePrefab(asset);
|
||||
if (prefab != null)
|
||||
return prefab;
|
||||
}
|
||||
|
||||
return _fallback.ResolvePrefab(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9323e6fa0b61837499fa84b30e597e92
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public interface IEveUnityProviderRefreshSource
|
||||
{
|
||||
void Refresh();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: babddb726f9747e43a7789eb6b584fad
|
||||
|
|
@ -0,0 +1,220 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using GameCult.Eve.Surface;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public sealed class EveUnitySceneClientSession
|
||||
{
|
||||
public const string RuntimeClientId = "unity-scene";
|
||||
|
||||
private readonly EveUnitySceneSurfaceLowerer _lowerer;
|
||||
private EveUnitySceneProviderSurfaceSnapshot? _activeSnapshot;
|
||||
|
||||
public EveUnitySceneClientSession()
|
||||
: this(new EveUnitySceneSurfaceLowerer())
|
||||
{
|
||||
}
|
||||
|
||||
public EveUnitySceneClientSession(EveUnitySceneSurfaceLowerer lowerer)
|
||||
{
|
||||
_lowerer = lowerer ?? throw new ArgumentNullException(nameof(lowerer));
|
||||
}
|
||||
|
||||
public EveUnitySceneProjection? ActiveProjection { get; private set; }
|
||||
|
||||
public string ActiveSourcePointer => _activeSnapshot?.SourcePointer ?? "";
|
||||
|
||||
public long ActiveVersion => _activeSnapshot?.Version ?? 0;
|
||||
|
||||
public EveUnitySceneProjection Connect(EveUnitySceneProviderSurfaceSnapshot snapshot)
|
||||
{
|
||||
if (snapshot == null) throw new ArgumentNullException(nameof(snapshot));
|
||||
|
||||
var projection = _lowerer.Lower(snapshot.Document, snapshot.AdvertisedSurface);
|
||||
_activeSnapshot = snapshot;
|
||||
ActiveProjection = projection;
|
||||
return projection;
|
||||
}
|
||||
|
||||
public EveUnitySceneProjection ApplySnapshot(EveUnitySceneProviderSurfaceSnapshot snapshot)
|
||||
{
|
||||
return Connect(snapshot);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest CreateMoveIntent(
|
||||
string entityId,
|
||||
float targetX,
|
||||
float targetY,
|
||||
float targetZ,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
var playableWorld = RequirePlayableWorld();
|
||||
return CreatePlayableWorldIntent(
|
||||
playableWorld.MovementCommand,
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["entityId"] = entityId ?? "",
|
||||
["targetPosition"] = FormatVector3(targetX, targetY, targetZ)
|
||||
},
|
||||
issuedAt);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest CreateMoveVectorIntent(
|
||||
string entityId,
|
||||
float directionX,
|
||||
float directionY,
|
||||
float scalarValue = 1f,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
var playableWorld = RequirePlayableWorld();
|
||||
return CreatePlayableWorldIntent(
|
||||
playableWorld.MovementCommand,
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["entityId"] = entityId ?? "",
|
||||
["directionX"] = FormatFloat(directionX),
|
||||
["directionY"] = FormatFloat(directionY),
|
||||
["scalarValue"] = FormatFloat(scalarValue)
|
||||
},
|
||||
issuedAt);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest CreateFocusIntent(
|
||||
string entityId,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
var playableWorld = RequirePlayableWorld();
|
||||
return CreatePlayableWorldIntent(
|
||||
playableWorld.FocusCommand,
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["entityId"] = entityId ?? ""
|
||||
},
|
||||
issuedAt);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest CreateTargetIntent(
|
||||
string sourceEntityId,
|
||||
string targetEntityId,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
var playableWorld = RequirePlayableWorld();
|
||||
return CreatePlayableWorldIntent(
|
||||
playableWorld.TargetCommand,
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["sourceEntityId"] = sourceEntityId ?? "",
|
||||
["targetEntityId"] = targetEntityId ?? ""
|
||||
},
|
||||
issuedAt);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest CreateActionIntent(
|
||||
string entityId,
|
||||
string actionId,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
var playableWorld = RequirePlayableWorld();
|
||||
return CreatePlayableWorldIntent(
|
||||
playableWorld.ActionCommand,
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["entityId"] = entityId ?? "",
|
||||
["actionId"] = actionId ?? ""
|
||||
},
|
||||
issuedAt);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest CreatePlayableWorldIntent(
|
||||
string commandId,
|
||||
IReadOnlyDictionary<string, string>? payload = null,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(commandId))
|
||||
throw new InvalidOperationException("Playable world command id is missing from the active surface.");
|
||||
|
||||
var snapshot = RequireSnapshot();
|
||||
var projection = RequireProjection();
|
||||
var envelopePayload = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["commandId"] = commandId
|
||||
};
|
||||
|
||||
if (payload != null)
|
||||
{
|
||||
foreach (var entry in payload)
|
||||
envelopePayload[entry.Key] = entry.Value;
|
||||
}
|
||||
|
||||
return _lowerer.CreateCommandIntent(
|
||||
snapshot.Document,
|
||||
snapshot.AdvertisedSurface,
|
||||
projection.CommandBoundary,
|
||||
envelopePayload,
|
||||
issuedAt);
|
||||
}
|
||||
|
||||
private EveUnityPlayableWorldProjection RequirePlayableWorld()
|
||||
{
|
||||
var projection = RequireProjection();
|
||||
if (projection.PlayableWorld == null)
|
||||
throw new InvalidOperationException("The active Unity scene surface does not expose a playable world projection.");
|
||||
return projection.PlayableWorld;
|
||||
}
|
||||
|
||||
private EveUnitySceneProjection RequireProjection()
|
||||
{
|
||||
if (ActiveProjection == null)
|
||||
throw new InvalidOperationException("No provider surface snapshot has been connected to the Unity scene client session.");
|
||||
return ActiveProjection;
|
||||
}
|
||||
|
||||
private EveUnitySceneProviderSurfaceSnapshot RequireSnapshot()
|
||||
{
|
||||
if (_activeSnapshot == null)
|
||||
throw new InvalidOperationException("No provider surface snapshot has been connected to the Unity scene client session.");
|
||||
return _activeSnapshot;
|
||||
}
|
||||
|
||||
private static string FormatVector3(float x, float y, float z)
|
||||
{
|
||||
return string.Join(
|
||||
",",
|
||||
x.ToString("R", CultureInfo.InvariantCulture),
|
||||
y.ToString("R", CultureInfo.InvariantCulture),
|
||||
z.ToString("R", CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
private static string FormatFloat(float value)
|
||||
{
|
||||
return value.ToString("R", CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EveUnitySceneProviderSurfaceSnapshot
|
||||
{
|
||||
public EveUnitySceneProviderSurfaceSnapshot(
|
||||
EveSurfaceDocument document,
|
||||
EveUnitySceneProviderSurfaceAdvertisement advertisedSurface,
|
||||
string sourcePointer,
|
||||
long version = 0)
|
||||
{
|
||||
Document = document ?? throw new ArgumentNullException(nameof(document));
|
||||
AdvertisedSurface = advertisedSurface ?? throw new ArgumentNullException(nameof(advertisedSurface));
|
||||
SourcePointer = sourcePointer ?? "";
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public EveSurfaceDocument Document { get; }
|
||||
|
||||
public EveUnitySceneProviderSurfaceAdvertisement AdvertisedSurface { get; }
|
||||
|
||||
public string SourcePointer { get; }
|
||||
|
||||
public long Version { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d2b8fac8eb072f8419afa594928840ef
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
using System;
|
||||
using GameCult.Eve.Surface;
|
||||
using UnityEngine;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public interface IEveUnitySceneLiveProviderTransport
|
||||
{
|
||||
string TransportKind { get; }
|
||||
|
||||
string SurfacePointer { get; }
|
||||
|
||||
string AssetManifestPointer { get; }
|
||||
|
||||
EveUnitySceneProviderSurfaceDocument CurrentSurfaceDocument { get; }
|
||||
|
||||
EveUnityPlayableWorldAssetManifestDocument CurrentAssetManifestDocument { get; }
|
||||
|
||||
event Action<EveUnitySceneProviderSurfaceDocument> SurfaceDocumentAvailable;
|
||||
|
||||
event Action<EveUnityPlayableWorldAssetManifestDocument> AssetManifestDocumentAvailable;
|
||||
|
||||
event Action<EveUnitySceneCommandReceipt> CommandReceiptAvailable;
|
||||
|
||||
void Connect();
|
||||
|
||||
void Disconnect();
|
||||
|
||||
void Refresh();
|
||||
|
||||
void SubmitCommand(EveSurfaceCommandRequest request);
|
||||
}
|
||||
|
||||
public abstract class EveUnitySceneLiveProviderTransportBehaviour :
|
||||
MonoBehaviour,
|
||||
IEveUnitySceneLiveProviderTransport
|
||||
{
|
||||
public abstract string TransportKind { get; }
|
||||
|
||||
public abstract string SurfacePointer { get; }
|
||||
|
||||
public abstract string AssetManifestPointer { get; }
|
||||
|
||||
public abstract EveUnitySceneProviderSurfaceDocument CurrentSurfaceDocument { get; }
|
||||
|
||||
public abstract EveUnityPlayableWorldAssetManifestDocument CurrentAssetManifestDocument { get; }
|
||||
|
||||
public abstract event Action<EveUnitySceneProviderSurfaceDocument> SurfaceDocumentAvailable;
|
||||
|
||||
public abstract event Action<EveUnityPlayableWorldAssetManifestDocument> AssetManifestDocumentAvailable;
|
||||
|
||||
public abstract event Action<EveUnitySceneCommandReceipt> CommandReceiptAvailable;
|
||||
|
||||
public abstract void Connect();
|
||||
|
||||
public abstract void Disconnect();
|
||||
|
||||
public abstract void Refresh();
|
||||
|
||||
public abstract void SubmitCommand(EveSurfaceCommandRequest request);
|
||||
}
|
||||
|
||||
public sealed class EveUnitySceneLiveProviderBridgeComponent :
|
||||
MonoBehaviour,
|
||||
IEveUnitySceneProviderSurfaceDocumentSource,
|
||||
IEveUnitySceneProviderSurfaceDocumentConnection,
|
||||
IEveUnityPlayableWorldAssetManifestDocumentSource,
|
||||
IEveUnitySceneCommandSink,
|
||||
IEveUnitySceneCommandReceiptSource,
|
||||
IEveUnityProviderRefreshSource
|
||||
{
|
||||
[SerializeField] private MonoBehaviour? transportBehaviour;
|
||||
|
||||
private EveUnitySceneLiveProviderBridge? _bridge;
|
||||
private IEveUnitySceneLiveProviderTransport? _configuredTransport;
|
||||
|
||||
public string TransportKind => Bridge.TransportKind;
|
||||
|
||||
public string SurfacePointer => Bridge.SurfacePointer;
|
||||
|
||||
public string AssetManifestPointer => Bridge.AssetManifestPointer;
|
||||
|
||||
public string SinkKind => Bridge.SinkKind;
|
||||
|
||||
public string ManifestRef => Bridge.ManifestRef;
|
||||
|
||||
public EveUnitySceneProviderSurfaceDocument CurrentSurfaceDocument => Bridge.CurrentSurfaceDocument;
|
||||
|
||||
public EveUnityPlayableWorldAssetManifestDocument CurrentAssetManifestDocument => Bridge.CurrentAssetManifestDocument;
|
||||
|
||||
EveUnitySceneProviderSurfaceDocument IEveUnitySceneProviderSurfaceDocumentSource.CurrentDocument =>
|
||||
Bridge.CurrentSurfaceDocument;
|
||||
|
||||
EveUnityPlayableWorldAssetManifestDocument IEveUnityPlayableWorldAssetManifestDocumentSource.CurrentDocument =>
|
||||
Bridge.CurrentAssetManifestDocument;
|
||||
|
||||
public event Action<EveUnitySceneProviderSurfaceDocument>? DocumentAvailable
|
||||
{
|
||||
add { Bridge.DocumentAvailable += value; }
|
||||
remove { Bridge.DocumentAvailable -= value; }
|
||||
}
|
||||
|
||||
public event Action<EveUnityPlayableWorldAssetManifestDocument>? AssetManifestDocumentAvailable
|
||||
{
|
||||
add { Bridge.AssetManifestDocumentAvailable += value; }
|
||||
remove { Bridge.AssetManifestDocumentAvailable -= value; }
|
||||
}
|
||||
|
||||
event Action<EveUnityPlayableWorldAssetManifestDocument> IEveUnityPlayableWorldAssetManifestDocumentSource.DocumentAvailable
|
||||
{
|
||||
add { Bridge.AssetManifestDocumentAvailable += value; }
|
||||
remove { Bridge.AssetManifestDocumentAvailable -= value; }
|
||||
}
|
||||
|
||||
public event Action<EveUnitySceneCommandReceipt>? ReceiptAvailable
|
||||
{
|
||||
add { Bridge.ReceiptAvailable += value; }
|
||||
remove { Bridge.ReceiptAvailable -= value; }
|
||||
}
|
||||
|
||||
public void Configure(IEveUnitySceneLiveProviderTransport transport)
|
||||
{
|
||||
if (transport == null) throw new ArgumentNullException(nameof(transport));
|
||||
_bridge?.Dispose();
|
||||
_configuredTransport = transport;
|
||||
_bridge = new EveUnitySceneLiveProviderBridge(transport);
|
||||
}
|
||||
|
||||
public void Connect()
|
||||
{
|
||||
Bridge.Connect();
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
_bridge?.Disconnect();
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
Bridge.Connect();
|
||||
Bridge.Refresh();
|
||||
}
|
||||
|
||||
public void Submit(EveSurfaceCommandRequest request)
|
||||
{
|
||||
Bridge.Submit(request);
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
_bridge?.Dispose();
|
||||
_bridge = null;
|
||||
}
|
||||
|
||||
private EveUnitySceneLiveProviderBridge Bridge
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_bridge != null)
|
||||
return _bridge;
|
||||
|
||||
var transport = _configuredTransport ?? transportBehaviour as IEveUnitySceneLiveProviderTransport;
|
||||
if (transport == null)
|
||||
throw new InvalidOperationException(
|
||||
"Eve Unity scene live provider bridge requires a transport implementing IEveUnitySceneLiveProviderTransport.");
|
||||
|
||||
_bridge = new EveUnitySceneLiveProviderBridge(transport);
|
||||
return _bridge;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EveUnitySceneLiveProviderBridge :
|
||||
IEveUnitySceneProviderSurfaceDocumentSource,
|
||||
IEveUnitySceneProviderSurfaceDocumentConnection,
|
||||
IEveUnityPlayableWorldAssetManifestDocumentSource,
|
||||
IEveUnitySceneCommandSink,
|
||||
IEveUnitySceneCommandReceiptSource,
|
||||
IEveUnityProviderRefreshSource,
|
||||
IDisposable
|
||||
{
|
||||
private readonly IEveUnitySceneLiveProviderTransport _transport;
|
||||
private bool _connected;
|
||||
|
||||
public EveUnitySceneLiveProviderBridge(IEveUnitySceneLiveProviderTransport transport)
|
||||
{
|
||||
_transport = transport ?? throw new ArgumentNullException(nameof(transport));
|
||||
CurrentSurfaceDocument = _transport.CurrentSurfaceDocument;
|
||||
CurrentAssetManifestDocument = _transport.CurrentAssetManifestDocument;
|
||||
}
|
||||
|
||||
public string TransportKind => _transport.TransportKind;
|
||||
|
||||
public string SurfacePointer => _transport.SurfacePointer;
|
||||
|
||||
public string AssetManifestPointer => _transport.AssetManifestPointer;
|
||||
|
||||
public string SinkKind => _transport.TransportKind;
|
||||
|
||||
public string ManifestRef => CurrentAssetManifestDocument.ManifestRef;
|
||||
|
||||
public EveUnitySceneProviderSurfaceDocument CurrentSurfaceDocument { get; private set; }
|
||||
|
||||
public EveUnityPlayableWorldAssetManifestDocument CurrentAssetManifestDocument { get; private set; }
|
||||
|
||||
EveUnitySceneProviderSurfaceDocument IEveUnitySceneProviderSurfaceDocumentSource.CurrentDocument =>
|
||||
CurrentSurfaceDocument;
|
||||
|
||||
EveUnityPlayableWorldAssetManifestDocument IEveUnityPlayableWorldAssetManifestDocumentSource.CurrentDocument =>
|
||||
CurrentAssetManifestDocument;
|
||||
|
||||
public event Action<EveUnitySceneProviderSurfaceDocument>? DocumentAvailable;
|
||||
|
||||
public event Action<EveUnityPlayableWorldAssetManifestDocument>? AssetManifestDocumentAvailable;
|
||||
|
||||
event Action<EveUnityPlayableWorldAssetManifestDocument> IEveUnityPlayableWorldAssetManifestDocumentSource.DocumentAvailable
|
||||
{
|
||||
add { AssetManifestDocumentAvailable += value; }
|
||||
remove { AssetManifestDocumentAvailable -= value; }
|
||||
}
|
||||
|
||||
public event Action<EveUnitySceneCommandReceipt>? ReceiptAvailable;
|
||||
|
||||
public void Connect()
|
||||
{
|
||||
if (_connected)
|
||||
return;
|
||||
|
||||
_transport.SurfaceDocumentAvailable += OnSurfaceDocumentAvailable;
|
||||
_transport.AssetManifestDocumentAvailable += OnAssetManifestDocumentAvailable;
|
||||
_transport.CommandReceiptAvailable += OnCommandReceiptAvailable;
|
||||
_transport.Connect();
|
||||
_connected = true;
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
if (!_connected)
|
||||
return;
|
||||
|
||||
_transport.SurfaceDocumentAvailable -= OnSurfaceDocumentAvailable;
|
||||
_transport.AssetManifestDocumentAvailable -= OnAssetManifestDocumentAvailable;
|
||||
_transport.CommandReceiptAvailable -= OnCommandReceiptAvailable;
|
||||
_transport.Disconnect();
|
||||
_connected = false;
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
_transport.Refresh();
|
||||
}
|
||||
|
||||
public void Submit(EveSurfaceCommandRequest request)
|
||||
{
|
||||
if (request == null) throw new ArgumentNullException(nameof(request));
|
||||
_transport.SubmitCommand(request);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
private void OnSurfaceDocumentAvailable(EveUnitySceneProviderSurfaceDocument document)
|
||||
{
|
||||
CurrentSurfaceDocument = document ?? throw new ArgumentNullException(nameof(document));
|
||||
DocumentAvailable?.Invoke(CurrentSurfaceDocument);
|
||||
}
|
||||
|
||||
private void OnAssetManifestDocumentAvailable(EveUnityPlayableWorldAssetManifestDocument document)
|
||||
{
|
||||
CurrentAssetManifestDocument = document ?? throw new ArgumentNullException(nameof(document));
|
||||
AssetManifestDocumentAvailable?.Invoke(CurrentAssetManifestDocument);
|
||||
}
|
||||
|
||||
private void OnCommandReceiptAvailable(EveUnitySceneCommandReceipt receipt)
|
||||
{
|
||||
if (receipt == null) throw new ArgumentNullException(nameof(receipt));
|
||||
ReceiptAvailable?.Invoke(receipt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 7e11af135f8f48c48a2e0d88f8f9d04c
|
||||
|
|
@ -0,0 +1,291 @@
|
|||
using System;
|
||||
using GameCult.Eve.Surface;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public interface IEveUnitySceneProviderSurfaceSource
|
||||
{
|
||||
string ProviderId { get; }
|
||||
|
||||
string SurfaceId { get; }
|
||||
|
||||
string SourcePointer { get; }
|
||||
|
||||
EveUnitySceneProviderSurfaceSnapshot CurrentSnapshot { get; }
|
||||
|
||||
event Action<EveUnitySceneProviderSurfaceSnapshot> SnapshotAvailable;
|
||||
|
||||
void Connect();
|
||||
|
||||
void Refresh();
|
||||
|
||||
void Disconnect();
|
||||
}
|
||||
|
||||
public interface IEveUnitySceneCommandSink
|
||||
{
|
||||
string SinkKind { get; }
|
||||
|
||||
void Submit(EveSurfaceCommandRequest request);
|
||||
}
|
||||
|
||||
public interface IEveUnitySceneProviderSurfaceDocumentSource
|
||||
{
|
||||
EveUnitySceneProviderSurfaceDocument CurrentDocument { get; }
|
||||
|
||||
event Action<EveUnitySceneProviderSurfaceDocument> DocumentAvailable;
|
||||
}
|
||||
|
||||
public interface IEveUnitySceneProviderSurfaceDocumentConnection
|
||||
{
|
||||
void Connect();
|
||||
|
||||
void Disconnect();
|
||||
}
|
||||
|
||||
public sealed class EveUnitySceneProviderSurfaceDocument
|
||||
{
|
||||
public EveUnitySceneProviderSurfaceDocument(
|
||||
EveSurfaceDocument surfaceDocument,
|
||||
EveUnitySceneProviderSurfaceAdvertisement advertisedSurface,
|
||||
string sourcePointer,
|
||||
long version = 0)
|
||||
{
|
||||
SurfaceDocument = surfaceDocument ?? throw new ArgumentNullException(nameof(surfaceDocument));
|
||||
AdvertisedSurface = advertisedSurface ?? throw new ArgumentNullException(nameof(advertisedSurface));
|
||||
SourcePointer = sourcePointer ?? "";
|
||||
Version = version == 0 ? surfaceDocument.Version : version;
|
||||
}
|
||||
|
||||
public EveSurfaceDocument SurfaceDocument { get; }
|
||||
|
||||
public EveUnitySceneProviderSurfaceAdvertisement AdvertisedSurface { get; }
|
||||
|
||||
public string SourcePointer { get; }
|
||||
|
||||
public long Version { get; }
|
||||
|
||||
public EveUnitySceneProviderSurfaceSnapshot ToSnapshot()
|
||||
{
|
||||
return new EveUnitySceneProviderSurfaceSnapshot(
|
||||
SurfaceDocument,
|
||||
AdvertisedSurface,
|
||||
SourcePointer,
|
||||
Version);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EveUnitySceneProviderSurfaceDocumentSource : IEveUnitySceneProviderSurfaceSource, IDisposable
|
||||
{
|
||||
private readonly IEveUnitySceneProviderSurfaceDocumentSource _documentSource;
|
||||
private readonly IEveUnitySceneProviderSurfaceDocumentConnection? _documentConnection;
|
||||
private readonly IEveUnityProviderRefreshSource? _refreshSource;
|
||||
private bool _connected;
|
||||
|
||||
public EveUnitySceneProviderSurfaceDocumentSource(
|
||||
IEveUnitySceneProviderSurfaceDocumentSource documentSource)
|
||||
{
|
||||
_documentSource = documentSource ?? throw new ArgumentNullException(nameof(documentSource));
|
||||
_documentConnection = documentSource as IEveUnitySceneProviderSurfaceDocumentConnection;
|
||||
_refreshSource = documentSource as IEveUnityProviderRefreshSource;
|
||||
CurrentSnapshot = _documentSource.CurrentDocument.ToSnapshot();
|
||||
}
|
||||
|
||||
public string ProviderId => CurrentSnapshot.Document.ProviderId;
|
||||
|
||||
public string SurfaceId => CurrentSnapshot.AdvertisedSurface.SurfaceId;
|
||||
|
||||
public string SourcePointer => CurrentSnapshot.SourcePointer;
|
||||
|
||||
public EveUnitySceneProviderSurfaceSnapshot CurrentSnapshot { get; private set; }
|
||||
|
||||
public event Action<EveUnitySceneProviderSurfaceSnapshot>? SnapshotAvailable;
|
||||
|
||||
public void Connect()
|
||||
{
|
||||
if (_connected)
|
||||
return;
|
||||
|
||||
_documentSource.DocumentAvailable += OnDocumentAvailable;
|
||||
_documentConnection?.Connect();
|
||||
_connected = true;
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
_refreshSource?.Refresh();
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
if (!_connected)
|
||||
return;
|
||||
|
||||
_documentConnection?.Disconnect();
|
||||
_documentSource.DocumentAvailable -= OnDocumentAvailable;
|
||||
_connected = false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
private void OnDocumentAvailable(EveUnitySceneProviderSurfaceDocument document)
|
||||
{
|
||||
if (document == null) throw new ArgumentNullException(nameof(document));
|
||||
CurrentSnapshot = document.ToSnapshot();
|
||||
SnapshotAvailable?.Invoke(CurrentSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EveUnitySceneProviderConnection : IDisposable
|
||||
{
|
||||
private readonly IEveUnitySceneProviderSurfaceSource _surfaceSource;
|
||||
private readonly IEveUnitySceneCommandSink _commandSink;
|
||||
private readonly EveUnitySceneClientSession _session;
|
||||
private bool _connected;
|
||||
|
||||
public EveUnitySceneProviderConnection(
|
||||
IEveUnitySceneProviderSurfaceSource surfaceSource,
|
||||
IEveUnitySceneCommandSink commandSink)
|
||||
: this(surfaceSource, commandSink, new EveUnitySceneClientSession())
|
||||
{
|
||||
}
|
||||
|
||||
public EveUnitySceneProviderConnection(
|
||||
IEveUnitySceneProviderSurfaceSource surfaceSource,
|
||||
IEveUnitySceneCommandSink commandSink,
|
||||
EveUnitySceneClientSession session)
|
||||
{
|
||||
_surfaceSource = surfaceSource ?? throw new ArgumentNullException(nameof(surfaceSource));
|
||||
_commandSink = commandSink ?? throw new ArgumentNullException(nameof(commandSink));
|
||||
_session = session ?? throw new ArgumentNullException(nameof(session));
|
||||
}
|
||||
|
||||
public string ProviderId => _surfaceSource.ProviderId;
|
||||
|
||||
public string SurfaceId => _surfaceSource.SurfaceId;
|
||||
|
||||
public string SourcePointer => _surfaceSource.SourcePointer;
|
||||
|
||||
public EveUnitySceneProjection? ActiveProjection => _session.ActiveProjection;
|
||||
|
||||
public long ActiveVersion => _session.ActiveVersion;
|
||||
|
||||
public event Action<EveUnitySceneProjection>? ProjectionUpdated;
|
||||
|
||||
public EveUnitySceneProjection Connect()
|
||||
{
|
||||
if (!_connected)
|
||||
{
|
||||
_surfaceSource.SnapshotAvailable += OnSnapshotAvailable;
|
||||
_surfaceSource.Connect();
|
||||
_connected = true;
|
||||
}
|
||||
|
||||
if (HasAppliedCurrentSnapshot())
|
||||
return _session.ActiveProjection!;
|
||||
|
||||
return ApplySnapshot(_surfaceSource.CurrentSnapshot);
|
||||
}
|
||||
|
||||
public EveUnitySceneProjection Refresh()
|
||||
{
|
||||
_surfaceSource.Refresh();
|
||||
if (HasAppliedCurrentSnapshot())
|
||||
return _session.ActiveProjection!;
|
||||
|
||||
return ApplySnapshot(_surfaceSource.CurrentSnapshot);
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitMoveIntent(
|
||||
string entityId,
|
||||
float targetX,
|
||||
float targetY,
|
||||
float targetZ,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return Submit(_session.CreateMoveIntent(entityId, targetX, targetY, targetZ, issuedAt));
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitMoveVectorIntent(
|
||||
string entityId,
|
||||
float directionX,
|
||||
float directionY,
|
||||
float scalarValue = 1f,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return Submit(_session.CreateMoveVectorIntent(entityId, directionX, directionY, scalarValue, issuedAt));
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitFocusIntent(
|
||||
string entityId,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return Submit(_session.CreateFocusIntent(entityId, issuedAt));
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitTargetIntent(
|
||||
string sourceEntityId,
|
||||
string targetEntityId,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return Submit(_session.CreateTargetIntent(sourceEntityId, targetEntityId, issuedAt));
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest SubmitActionIntent(
|
||||
string entityId,
|
||||
string actionId,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
return Submit(_session.CreateActionIntent(entityId, actionId, issuedAt));
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
if (!_connected)
|
||||
return;
|
||||
|
||||
_surfaceSource.SnapshotAvailable -= OnSnapshotAvailable;
|
||||
_surfaceSource.Disconnect();
|
||||
_connected = false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
|
||||
private EveSurfaceCommandRequest Submit(EveSurfaceCommandRequest request)
|
||||
{
|
||||
_commandSink.Submit(request);
|
||||
return request;
|
||||
}
|
||||
|
||||
private EveUnitySceneProjection ApplySnapshot(EveUnitySceneProviderSurfaceSnapshot snapshot)
|
||||
{
|
||||
var projection = _session.ApplySnapshot(snapshot);
|
||||
ProjectionUpdated?.Invoke(projection);
|
||||
return projection;
|
||||
}
|
||||
|
||||
private bool HasAppliedCurrentSnapshot()
|
||||
{
|
||||
var projection = _session.ActiveProjection;
|
||||
if (projection == null)
|
||||
return false;
|
||||
|
||||
var snapshot = _surfaceSource.CurrentSnapshot;
|
||||
return _session.ActiveVersion == snapshot.Version &&
|
||||
string.Equals(_session.ActiveSourcePointer, snapshot.SourcePointer, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private void OnSnapshotAvailable(EveUnitySceneProviderSurfaceSnapshot snapshot)
|
||||
{
|
||||
ApplySnapshot(snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9e93e937d561d4941b5080a454091f7a
|
||||
|
|
@ -0,0 +1,578 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameCult.Eve.Surface;
|
||||
using GameCult.Mesh;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public sealed class EveUnitySceneSurfaceLowerer
|
||||
{
|
||||
private static readonly SaiVisualNovelUnitySceneProjectionAdapter SaiVisualNovelAdapter = new SaiVisualNovelUnitySceneProjectionAdapter();
|
||||
private static readonly NornGraphUnitySceneProjectionAdapter NornGraphAdapter = new NornGraphUnitySceneProjectionAdapter();
|
||||
private static readonly TeXMathUnitySceneProjectionAdapter TeXMathAdapter = new TeXMathUnitySceneProjectionAdapter();
|
||||
|
||||
public EveUnitySceneProjection Lower(
|
||||
EveSurfaceDocument document,
|
||||
EveUnitySceneProviderSurfaceAdvertisement advertisedSurface)
|
||||
{
|
||||
if (document == null) throw new ArgumentNullException(nameof(document));
|
||||
if (advertisedSurface == null) throw new ArgumentNullException(nameof(advertisedSurface));
|
||||
if (!string.Equals(document.Surface.Id, advertisedSurface.SurfaceId, StringComparison.Ordinal))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Active surface '{document.Surface.Id}' does not match advertised Unity scene surface '{advertisedSurface.SurfaceId}'.");
|
||||
}
|
||||
|
||||
return new EveUnitySceneProjection(
|
||||
document.ProviderId,
|
||||
document.Surface.Id,
|
||||
advertisedSurface.WorldInteraction.ProjectionKind,
|
||||
advertisedSurface.WorldInteraction.CommandBoundary,
|
||||
advertisedSurface.WorldInteraction.ReceiptSchema,
|
||||
advertisedSurface.WorldInteraction.Ownership,
|
||||
BuildPlayableWorld(document.Surface.Root),
|
||||
BuildSceneGraph(document.Surface.Root));
|
||||
}
|
||||
|
||||
public EveSurfaceCommandRequest CreateCommandIntent(
|
||||
EveSurfaceDocument document,
|
||||
EveUnitySceneProviderSurfaceAdvertisement advertisedSurface,
|
||||
string command,
|
||||
IReadOnlyDictionary<string, string>? payload = null,
|
||||
DateTimeOffset? issuedAt = null)
|
||||
{
|
||||
if (document == null) throw new ArgumentNullException(nameof(document));
|
||||
if (advertisedSurface == null) throw new ArgumentNullException(nameof(advertisedSurface));
|
||||
if (string.IsNullOrWhiteSpace(command)) throw new ArgumentException("Command is required.", nameof(command));
|
||||
|
||||
var projection = Lower(document, advertisedSurface);
|
||||
return new EveSurfaceCommandRequest(
|
||||
projection.ProviderId,
|
||||
projection.SurfaceId,
|
||||
ResolveOperation(document, command),
|
||||
CultMesh.OperationPayload(payload ?? new Dictionary<string, string>(StringComparer.Ordinal)),
|
||||
issuedAt ?? DateTimeOffset.UtcNow,
|
||||
"unity-scene",
|
||||
projection.CommandBoundary,
|
||||
projection.ReceiptSchema);
|
||||
}
|
||||
|
||||
private static CultMeshOperationInvocationDescriptor ResolveOperation(
|
||||
EveSurfaceDocument document,
|
||||
string command)
|
||||
{
|
||||
foreach (var template in document.Commands)
|
||||
{
|
||||
if (string.Equals(template.Command, command, StringComparison.Ordinal))
|
||||
return CultMesh.OperationInvocation(template.Operation);
|
||||
}
|
||||
|
||||
return CultMesh.OperationInvocation(command);
|
||||
}
|
||||
|
||||
private static EveUnitySceneNode BuildSceneGraph(EveSurfaceComponent component)
|
||||
{
|
||||
var children = new List<EveUnitySceneNode>(component.Children.Count);
|
||||
foreach (var child in component.Children)
|
||||
{
|
||||
children.Add(BuildSceneGraph(child));
|
||||
}
|
||||
|
||||
return new EveUnitySceneNode(
|
||||
component.Id,
|
||||
component.Kind,
|
||||
SceneObjectKind(component.Kind),
|
||||
component.Props,
|
||||
component.Layout,
|
||||
component.Style,
|
||||
component.StateBindings.Count,
|
||||
component.EmbeddedDocuments.Count,
|
||||
BuildEmbeddedDocumentSlots(component.EmbeddedDocuments),
|
||||
BuildPluginProjection(component),
|
||||
children);
|
||||
}
|
||||
|
||||
private static EveUnityPlayableWorldProjection? BuildPlayableWorld(EveSurfaceComponent root)
|
||||
{
|
||||
var worldRoot = FindFirst(root, component => string.Equals(component.Kind, "world.scene3d", StringComparison.Ordinal));
|
||||
if (worldRoot == null)
|
||||
return null;
|
||||
|
||||
var entities = new List<EveUnityPlayableWorldEntity>();
|
||||
foreach (var component in Flatten(worldRoot))
|
||||
{
|
||||
if (string.Equals(component.Kind, "world.entity3d", StringComparison.Ordinal))
|
||||
entities.Add(BuildPlayableEntity(component));
|
||||
}
|
||||
|
||||
return new EveUnityPlayableWorldProjection(
|
||||
worldRoot.Id,
|
||||
worldRoot.GetProp("statePointerId", worldRoot.GetProp("worldStatePointerId")),
|
||||
worldRoot.GetProp("assetManifest", worldRoot.GetProp("assetManifestUri")),
|
||||
worldRoot.GetProp("inputProfile"),
|
||||
worldRoot.GetProp("cameraRig"),
|
||||
worldRoot.GetProp("playerEntityId"),
|
||||
worldRoot.GetProp("movementCommand"),
|
||||
worldRoot.GetProp("focusCommand"),
|
||||
worldRoot.GetProp("targetCommand"),
|
||||
worldRoot.GetProp("actionCommand"),
|
||||
entities);
|
||||
}
|
||||
|
||||
private static EveUnityPlayableWorldEntity BuildPlayableEntity(EveSurfaceComponent component)
|
||||
{
|
||||
var position = ParseVector3(component.GetProp("position"));
|
||||
return new EveUnityPlayableWorldEntity(
|
||||
component.Id,
|
||||
component.GetProp("entityId", component.Id),
|
||||
component.GetProp("entityKind", component.GetProp("kind")),
|
||||
component.GetProp("label", component.GetProp("name")),
|
||||
component.GetProp("faction"),
|
||||
component.GetProp("assetRef", component.GetProp("meshRef", component.GetProp("prefabRef"))),
|
||||
position.x,
|
||||
position.y,
|
||||
position.z,
|
||||
ParseFloat(component.GetProp("rotationY", component.GetProp("yaw")), 0f),
|
||||
ParseFloat(component.GetProp("radius"), 0f),
|
||||
ParseBoolean(component.GetProp("selectable")),
|
||||
ParseBoolean(component.GetProp("controllable")),
|
||||
component.GetProp("focusCommand"),
|
||||
component.GetProp("moveCommand"),
|
||||
component.GetProp("targetCommand"),
|
||||
component.GetProp("actionCommand"));
|
||||
}
|
||||
|
||||
private static EveSurfaceComponent? FindFirst(
|
||||
EveSurfaceComponent component,
|
||||
Predicate<EveSurfaceComponent> predicate)
|
||||
{
|
||||
if (predicate(component))
|
||||
return component;
|
||||
foreach (var child in component.Children)
|
||||
{
|
||||
var found = FindFirst(child, predicate);
|
||||
if (found != null)
|
||||
return found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<EveSurfaceComponent> Flatten(EveSurfaceComponent component)
|
||||
{
|
||||
yield return component;
|
||||
foreach (var child in component.Children)
|
||||
{
|
||||
foreach (var nested in Flatten(child))
|
||||
yield return nested;
|
||||
}
|
||||
}
|
||||
|
||||
private static (float x, float y, float z) ParseVector3(string value)
|
||||
{
|
||||
var parts = (value ?? "").Split(',');
|
||||
return (
|
||||
parts.Length > 0 ? ParseFloat(parts[0], 0f) : 0f,
|
||||
parts.Length > 1 ? ParseFloat(parts[1], 0f) : 0f,
|
||||
parts.Length > 2 ? ParseFloat(parts[2], 0f) : 0f);
|
||||
}
|
||||
|
||||
private static float ParseFloat(string value, float fallback)
|
||||
{
|
||||
return float.TryParse(value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out var parsed)
|
||||
? parsed
|
||||
: fallback;
|
||||
}
|
||||
|
||||
private static bool ParseBoolean(string value)
|
||||
{
|
||||
return string.Equals(value, "true", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(value, "1", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static EveUnityScenePluginProjection? BuildPluginProjection(EveSurfaceComponent component)
|
||||
{
|
||||
if (SaiVisualNovelAdapter.CanProject(component))
|
||||
return SaiVisualNovelAdapter.Project(component);
|
||||
if (NornGraphAdapter.CanProject(component))
|
||||
return NornGraphAdapter.Project(component);
|
||||
if (TeXMathAdapter.CanProject(component))
|
||||
return TeXMathAdapter.Project(component);
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<EveUnitySceneEmbeddedDocumentSlot> BuildEmbeddedDocumentSlots(
|
||||
IReadOnlyList<EveEmbeddedDocumentSlot> embeddedDocuments)
|
||||
{
|
||||
var slots = new List<EveUnitySceneEmbeddedDocumentSlot>(embeddedDocuments.Count);
|
||||
foreach (var slot in embeddedDocuments)
|
||||
{
|
||||
slots.Add(new EveUnitySceneEmbeddedDocumentSlot(
|
||||
slot.SlotId,
|
||||
slot.DocumentId,
|
||||
slot.SchemaId,
|
||||
slot.PresentationKind));
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
|
||||
private static string SceneObjectKind(string componentKind)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(componentKind))
|
||||
return "empty";
|
||||
if (string.Equals(componentKind, "world.scene3d", StringComparison.Ordinal))
|
||||
return "playable-world-root";
|
||||
if (string.Equals(componentKind, "world.entity3d", StringComparison.Ordinal))
|
||||
return "playable-world-entity";
|
||||
if (string.Equals(componentKind, "field.vector3d", StringComparison.Ordinal) ||
|
||||
string.Equals(componentKind, "field.scalar3d", StringComparison.Ordinal))
|
||||
return "world-field-3d";
|
||||
if (string.Equals(componentKind, "vn.stage", StringComparison.Ordinal))
|
||||
return "sai-vn-scene-stage";
|
||||
if (string.Equals(componentKind, "panel.dialogue", StringComparison.Ordinal) ||
|
||||
string.Equals(componentKind, "text.dialogue", StringComparison.Ordinal))
|
||||
return "sai-vn-scene-dialogue";
|
||||
if (string.Equals(componentKind, "rail.actions", StringComparison.Ordinal))
|
||||
return "sai-vn-scene-action-rail";
|
||||
if (componentKind.StartsWith("control.", StringComparison.Ordinal))
|
||||
return "command-control";
|
||||
if (string.Equals(componentKind, "embed.norn", StringComparison.Ordinal))
|
||||
return "norn-graph-scene-projection";
|
||||
if (string.Equals(componentKind, "embed.tex", StringComparison.Ordinal))
|
||||
return "tex-math-scene-projection";
|
||||
if (componentKind.StartsWith("embed.", StringComparison.Ordinal))
|
||||
return "plugin-placeholder";
|
||||
if (string.Equals(componentKind, "surface.slot", StringComparison.Ordinal))
|
||||
return "embedded-surface-slot";
|
||||
if (componentKind.StartsWith("world.", StringComparison.Ordinal) ||
|
||||
componentKind.StartsWith("field.", StringComparison.Ordinal))
|
||||
return "world-projection-node";
|
||||
if (componentKind.StartsWith("text.", StringComparison.Ordinal) || string.Equals(componentKind, "label", StringComparison.Ordinal))
|
||||
return "scene-label";
|
||||
return "scene-node";
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EveUnitySceneProjection
|
||||
{
|
||||
public EveUnitySceneProjection(
|
||||
string providerId,
|
||||
string surfaceId,
|
||||
string projectionKind,
|
||||
string commandBoundary,
|
||||
string receiptSchema,
|
||||
string ownership,
|
||||
EveUnityPlayableWorldProjection? playableWorld,
|
||||
EveUnitySceneNode root)
|
||||
{
|
||||
ProviderId = providerId ?? "";
|
||||
SurfaceId = surfaceId ?? "";
|
||||
ProjectionKind = projectionKind ?? "";
|
||||
CommandBoundary = commandBoundary ?? "";
|
||||
ReceiptSchema = receiptSchema ?? "";
|
||||
Ownership = ownership ?? "";
|
||||
PlayableWorld = playableWorld;
|
||||
Root = root ?? throw new ArgumentNullException(nameof(root));
|
||||
}
|
||||
|
||||
public string ProviderId { get; }
|
||||
|
||||
public string SurfaceId { get; }
|
||||
|
||||
public string ProjectionKind { get; }
|
||||
|
||||
public string CommandBoundary { get; }
|
||||
|
||||
public string ReceiptSchema { get; }
|
||||
|
||||
public string Ownership { get; }
|
||||
|
||||
public EveUnityPlayableWorldProjection? PlayableWorld { get; }
|
||||
|
||||
public EveUnitySceneNode Root { get; }
|
||||
}
|
||||
|
||||
public sealed class EveUnityPlayableWorldProjection
|
||||
{
|
||||
public EveUnityPlayableWorldProjection(
|
||||
string worldRootId,
|
||||
string statePointerId,
|
||||
string assetManifest,
|
||||
string inputProfile,
|
||||
string cameraRig,
|
||||
string playerEntityId,
|
||||
string movementCommand,
|
||||
string focusCommand,
|
||||
string targetCommand,
|
||||
string actionCommand,
|
||||
IReadOnlyList<EveUnityPlayableWorldEntity> entities)
|
||||
{
|
||||
WorldRootId = worldRootId ?? "";
|
||||
StatePointerId = statePointerId ?? "";
|
||||
AssetManifest = assetManifest ?? "";
|
||||
InputProfile = inputProfile ?? "";
|
||||
CameraRig = cameraRig ?? "";
|
||||
PlayerEntityId = playerEntityId ?? "";
|
||||
MovementCommand = movementCommand ?? "";
|
||||
FocusCommand = focusCommand ?? "";
|
||||
TargetCommand = targetCommand ?? "";
|
||||
ActionCommand = actionCommand ?? "";
|
||||
Entities = entities ?? Array.Empty<EveUnityPlayableWorldEntity>();
|
||||
}
|
||||
|
||||
public string WorldRootId { get; }
|
||||
|
||||
public string StatePointerId { get; }
|
||||
|
||||
public string AssetManifest { get; }
|
||||
|
||||
public string InputProfile { get; }
|
||||
|
||||
public string CameraRig { get; }
|
||||
|
||||
public string PlayerEntityId { get; }
|
||||
|
||||
public string MovementCommand { get; }
|
||||
|
||||
public string FocusCommand { get; }
|
||||
|
||||
public string TargetCommand { get; }
|
||||
|
||||
public string ActionCommand { get; }
|
||||
|
||||
public int EntityCount => Entities.Count;
|
||||
|
||||
public IReadOnlyList<EveUnityPlayableWorldEntity> Entities { get; }
|
||||
}
|
||||
|
||||
public sealed class EveUnityPlayableWorldEntity
|
||||
{
|
||||
public EveUnityPlayableWorldEntity(
|
||||
string nodeId,
|
||||
string entityId,
|
||||
string entityKind,
|
||||
string label,
|
||||
string faction,
|
||||
string assetRef,
|
||||
float positionX,
|
||||
float positionY,
|
||||
float positionZ,
|
||||
float rotationY,
|
||||
float radius,
|
||||
bool selectable,
|
||||
bool controllable,
|
||||
string focusCommand,
|
||||
string moveCommand,
|
||||
string targetCommand,
|
||||
string actionCommand)
|
||||
{
|
||||
NodeId = nodeId ?? "";
|
||||
EntityId = entityId ?? "";
|
||||
EntityKind = entityKind ?? "";
|
||||
Label = label ?? "";
|
||||
Faction = faction ?? "";
|
||||
AssetRef = assetRef ?? "";
|
||||
PositionX = positionX;
|
||||
PositionY = positionY;
|
||||
PositionZ = positionZ;
|
||||
RotationY = rotationY;
|
||||
Radius = radius;
|
||||
Selectable = selectable;
|
||||
Controllable = controllable;
|
||||
FocusCommand = focusCommand ?? "";
|
||||
MoveCommand = moveCommand ?? "";
|
||||
TargetCommand = targetCommand ?? "";
|
||||
ActionCommand = actionCommand ?? "";
|
||||
}
|
||||
|
||||
public string NodeId { get; }
|
||||
|
||||
public string EntityId { get; }
|
||||
|
||||
public string EntityKind { get; }
|
||||
|
||||
public string Label { get; }
|
||||
|
||||
public string Faction { get; }
|
||||
|
||||
public string AssetRef { get; }
|
||||
|
||||
public float PositionX { get; }
|
||||
|
||||
public float PositionY { get; }
|
||||
|
||||
public float PositionZ { get; }
|
||||
|
||||
public float RotationY { get; }
|
||||
|
||||
public float Radius { get; }
|
||||
|
||||
public bool Selectable { get; }
|
||||
|
||||
public bool Controllable { get; }
|
||||
|
||||
public string FocusCommand { get; }
|
||||
|
||||
public string MoveCommand { get; }
|
||||
|
||||
public string TargetCommand { get; }
|
||||
|
||||
public string ActionCommand { get; }
|
||||
}
|
||||
|
||||
public sealed class EveUnitySceneNode
|
||||
{
|
||||
public EveUnitySceneNode(
|
||||
string id,
|
||||
string componentKind,
|
||||
string sceneObjectKind,
|
||||
IReadOnlyDictionary<string, string> props,
|
||||
IReadOnlyDictionary<string, string> layout,
|
||||
IReadOnlyDictionary<string, string> style,
|
||||
int stateBindingCount,
|
||||
int embeddedDocumentCount,
|
||||
IReadOnlyList<EveUnitySceneEmbeddedDocumentSlot> embeddedDocuments,
|
||||
EveUnityScenePluginProjection? pluginProjection,
|
||||
IReadOnlyList<EveUnitySceneNode> children)
|
||||
{
|
||||
Id = id ?? "";
|
||||
ComponentKind = componentKind ?? "";
|
||||
SceneObjectKind = sceneObjectKind ?? "";
|
||||
Props = props ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
Layout = layout ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
Style = style ?? new Dictionary<string, string>(StringComparer.Ordinal);
|
||||
StateBindingCount = stateBindingCount;
|
||||
EmbeddedDocumentCount = embeddedDocumentCount;
|
||||
EmbeddedDocuments = embeddedDocuments ?? Array.Empty<EveUnitySceneEmbeddedDocumentSlot>();
|
||||
PluginProjection = pluginProjection;
|
||||
Children = children ?? Array.Empty<EveUnitySceneNode>();
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
|
||||
public string ComponentKind { get; }
|
||||
|
||||
public string SceneObjectKind { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, string> Props { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, string> Layout { get; }
|
||||
|
||||
public IReadOnlyDictionary<string, string> Style { get; }
|
||||
|
||||
public int StateBindingCount { get; }
|
||||
|
||||
public int EmbeddedDocumentCount { get; }
|
||||
|
||||
public IReadOnlyList<EveUnitySceneEmbeddedDocumentSlot> EmbeddedDocuments { get; }
|
||||
|
||||
public EveUnityScenePluginProjection? PluginProjection { get; }
|
||||
|
||||
public IReadOnlyList<EveUnitySceneNode> Children { get; }
|
||||
}
|
||||
|
||||
public sealed class EveUnityScenePluginProjection
|
||||
{
|
||||
public EveUnityScenePluginProjection(
|
||||
string pluginId,
|
||||
string projectionKind,
|
||||
string abiSchema,
|
||||
string commandBoundary,
|
||||
IReadOnlyList<string> capabilities,
|
||||
string command,
|
||||
string documentId,
|
||||
string semanticOwner)
|
||||
{
|
||||
PluginId = pluginId ?? "";
|
||||
ProjectionKind = projectionKind ?? "";
|
||||
AbiSchema = abiSchema ?? "";
|
||||
CommandBoundary = commandBoundary ?? "";
|
||||
Capabilities = capabilities ?? Array.Empty<string>();
|
||||
Command = command ?? "";
|
||||
DocumentId = documentId ?? "";
|
||||
SemanticOwner = semanticOwner ?? "";
|
||||
}
|
||||
|
||||
public string PluginId { get; }
|
||||
|
||||
public string ProjectionKind { get; }
|
||||
|
||||
public string AbiSchema { get; }
|
||||
|
||||
public string CommandBoundary { get; }
|
||||
|
||||
public IReadOnlyList<string> Capabilities { get; }
|
||||
|
||||
public string Command { get; }
|
||||
|
||||
public string DocumentId { get; }
|
||||
|
||||
public string SemanticOwner { get; }
|
||||
}
|
||||
|
||||
public sealed class EveUnitySceneEmbeddedDocumentSlot
|
||||
{
|
||||
public EveUnitySceneEmbeddedDocumentSlot(
|
||||
string slotId,
|
||||
string documentId,
|
||||
string schemaId,
|
||||
string presentationKind)
|
||||
{
|
||||
SlotId = slotId ?? "";
|
||||
DocumentId = documentId ?? "";
|
||||
SchemaId = schemaId ?? "";
|
||||
PresentationKind = presentationKind ?? "";
|
||||
}
|
||||
|
||||
public string SlotId { get; }
|
||||
|
||||
public string DocumentId { get; }
|
||||
|
||||
public string SchemaId { get; }
|
||||
|
||||
public string PresentationKind { get; }
|
||||
}
|
||||
|
||||
public sealed class EveUnitySceneProviderSurfaceAdvertisement
|
||||
{
|
||||
public EveUnitySceneProviderSurfaceAdvertisement(
|
||||
string surfaceId,
|
||||
string surfaceKind,
|
||||
EveUnitySceneWorldInteraction worldInteraction)
|
||||
{
|
||||
SurfaceId = surfaceId ?? "";
|
||||
SurfaceKind = surfaceKind ?? "";
|
||||
WorldInteraction = worldInteraction ?? throw new ArgumentNullException(nameof(worldInteraction));
|
||||
}
|
||||
|
||||
public string SurfaceId { get; }
|
||||
|
||||
public string SurfaceKind { get; }
|
||||
|
||||
public EveUnitySceneWorldInteraction WorldInteraction { get; }
|
||||
}
|
||||
|
||||
public sealed class EveUnitySceneWorldInteraction
|
||||
{
|
||||
public EveUnitySceneWorldInteraction(
|
||||
string projectionKind,
|
||||
string commandBoundary,
|
||||
string receiptSchema,
|
||||
string ownership)
|
||||
{
|
||||
ProjectionKind = projectionKind ?? "";
|
||||
CommandBoundary = commandBoundary ?? "";
|
||||
ReceiptSchema = receiptSchema ?? "";
|
||||
Ownership = ownership ?? "";
|
||||
}
|
||||
|
||||
public string ProjectionKind { get; }
|
||||
|
||||
public string CommandBoundary { get; }
|
||||
|
||||
public string ReceiptSchema { get; }
|
||||
|
||||
public string Ownership { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 32422132e752b9a4f8a45b2a5cb01217
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "GameCult.Eve.UnityScene",
|
||||
"rootNamespace": "GameCult.Eve.UnityScene",
|
||||
"references": [
|
||||
"GameCult.Eve.Surface"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a5b5eeaa1ac2b24409cf4718616ebbe3
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameCult.Eve.Surface;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public sealed class NornGraphUnitySceneProjectionAdapter
|
||||
{
|
||||
public string PluginId => "norn.graph";
|
||||
|
||||
public IReadOnlyList<string> Capabilities { get; } = new[]
|
||||
{
|
||||
"embed.norn"
|
||||
};
|
||||
|
||||
public bool CanProject(EveSurfaceComponent component)
|
||||
{
|
||||
return component != null && string.Equals(component.Kind, "embed.norn", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public EveUnityScenePluginProjection Project(EveSurfaceComponent component)
|
||||
{
|
||||
if (component == null) throw new ArgumentNullException(nameof(component));
|
||||
if (!CanProject(component))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Norn scene projection adapter cannot project component kind '{component.Kind}'.");
|
||||
}
|
||||
|
||||
return new EveUnityScenePluginProjection(
|
||||
PluginId,
|
||||
"norn-scene-embedded-graph-shell",
|
||||
"gamecult.eve.plugin_abi.v1",
|
||||
"sidecar-advertised-plugin-abi",
|
||||
Capabilities,
|
||||
component.GetProp(
|
||||
"interaction.nodeAction",
|
||||
component.GetProp("focusCommand", component.GetProp("command", "graph.focus"))),
|
||||
component.GetProp("graph.document", component.GetProp("documentId", "")),
|
||||
"Norn");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bbe1659cc5c50ff4e9ab6f2ef0ed82c3
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameCult.Eve.Surface;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public sealed class SaiVisualNovelUnitySceneProjectionAdapter
|
||||
{
|
||||
public string PluginId => "sai.vn";
|
||||
|
||||
public IReadOnlyList<string> Capabilities { get; } = new[]
|
||||
{
|
||||
"vn.stage",
|
||||
"story.choose",
|
||||
"story.continue",
|
||||
"story.jump"
|
||||
};
|
||||
|
||||
public bool CanProject(EveSurfaceComponent component)
|
||||
{
|
||||
if (component == null)
|
||||
return false;
|
||||
|
||||
switch (component.Kind)
|
||||
{
|
||||
case "vn.stage":
|
||||
case "panel.dialogue":
|
||||
case "text.dialogue":
|
||||
case "rail.actions":
|
||||
return true;
|
||||
case "control.button":
|
||||
return StoryCommand(component).StartsWith("story.", StringComparison.Ordinal);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public EveUnityScenePluginProjection Project(EveSurfaceComponent component)
|
||||
{
|
||||
if (component == null) throw new ArgumentNullException(nameof(component));
|
||||
if (!CanProject(component))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Sai scene projection adapter cannot project component kind '{component.Kind}'.");
|
||||
}
|
||||
|
||||
return new EveUnityScenePluginProjection(
|
||||
PluginId,
|
||||
ProjectionKind(component),
|
||||
"gamecult.eve.plugin_abi.v1",
|
||||
"sidecar-advertised-plugin-abi",
|
||||
Capabilities,
|
||||
StoryCommand(component),
|
||||
component.GetProp("storyId", component.GetProp("currentPath", "")),
|
||||
"Sai");
|
||||
}
|
||||
|
||||
private static string ProjectionKind(EveSurfaceComponent component)
|
||||
{
|
||||
switch (component.Kind)
|
||||
{
|
||||
case "vn.stage":
|
||||
return "sai-vn-scene-stage-shell";
|
||||
case "panel.dialogue":
|
||||
case "text.dialogue":
|
||||
return "sai-vn-scene-dialogue-shell";
|
||||
case "rail.actions":
|
||||
return "sai-vn-scene-action-rail-shell";
|
||||
case "control.button":
|
||||
return "sai-vn-scene-story-command-shell";
|
||||
default:
|
||||
return "sai-vn-scene-projection-shell";
|
||||
}
|
||||
}
|
||||
|
||||
private static string StoryCommand(EveSurfaceComponent component)
|
||||
{
|
||||
return component.GetProp(
|
||||
"command",
|
||||
component.GetProp(
|
||||
"action.command",
|
||||
component.GetProp("action", component.GetProp("type", ""))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fc79e8d0b375f3d41ad7809e2129ca6c
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using GameCult.Eve.Surface;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace GameCult.Eve.UnityScene
|
||||
{
|
||||
public sealed class TeXMathUnitySceneProjectionAdapter
|
||||
{
|
||||
public string PluginId => "tex.math";
|
||||
|
||||
public IReadOnlyList<string> Capabilities { get; } = new[]
|
||||
{
|
||||
"embed.tex",
|
||||
"tex.inline",
|
||||
"tex.block",
|
||||
"tex.scene-placement"
|
||||
};
|
||||
|
||||
public bool CanProject(EveSurfaceComponent component)
|
||||
{
|
||||
return component != null && string.Equals(component.Kind, "embed.tex", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public EveUnityScenePluginProjection Project(EveSurfaceComponent component)
|
||||
{
|
||||
if (component == null) throw new ArgumentNullException(nameof(component));
|
||||
if (!CanProject(component))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"TeX scene projection adapter cannot project component kind '{component.Kind}'.");
|
||||
}
|
||||
|
||||
return new EveUnityScenePluginProjection(
|
||||
PluginId,
|
||||
ProjectionKind(component),
|
||||
"gamecult.eve.plugin_abi.v1",
|
||||
"sidecar-advertised-plugin-abi",
|
||||
Capabilities,
|
||||
"",
|
||||
component.GetProp("sourceUri", component.GetProp("source", "")),
|
||||
"EvePlugins");
|
||||
}
|
||||
|
||||
private static string ProjectionKind(EveSurfaceComponent component)
|
||||
{
|
||||
var display = component.GetProp("display", "inline");
|
||||
if (string.Equals(display, "block", StringComparison.Ordinal))
|
||||
return "tex-math-scene-block-fallback-shell";
|
||||
if (string.Equals(display, "page", StringComparison.Ordinal))
|
||||
return "tex-math-scene-page-fallback-shell";
|
||||
return "tex-math-scene-inline-fallback-shell";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 0184a5155baa9774f9ed133ad871b735
|
||||
8
packages/org.gamecult.eve.unity-scene/Tests.meta
Normal file
8
packages/org.gamecult.eve.unity-scene/Tests.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: b37c64ab7abc96a468018e86c041d431
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
packages/org.gamecult.eve.unity-scene/Tests/Editor.meta
Normal file
8
packages/org.gamecult.eve.unity-scene/Tests/Editor.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: ac7339c6ff81f6f48864df2308ebeed2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d2b3006d5699924498750dbf5f087435
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "GameCult.Eve.UnityScene.Tests",
|
||||
"rootNamespace": "GameCult.Eve.UnityScene.Tests",
|
||||
"references": [
|
||||
"GameCult.Eve.Surface",
|
||||
"GameCult.Eve.UnityScene"
|
||||
],
|
||||
"includePlatforms": [
|
||||
"Editor"
|
||||
],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": false,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 10e7994adb919944aadc88f960300bc4
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
{
|
||||
"schema": "gamecult.eve.runtime_capability.v1",
|
||||
"runtimeId": "unity-scene",
|
||||
"title": "Unity / Scene",
|
||||
"owner": "EveUnity",
|
||||
"incubation": {
|
||||
"ownerRepo": "EveUnity",
|
||||
"currentHostRepo": "EveUnity",
|
||||
"splitTarget": "EveUnity",
|
||||
"graduationTrigger": "Graduated: EveUnity owns the provider-agnostic Unity scene package, release lifecycle, and runtime tests. Real Unity frame capture and live CultMesh transport remain active lifecycle work.",
|
||||
"splitHandoff": {
|
||||
"manifestPath": "",
|
||||
"smokeCommand": ""
|
||||
}
|
||||
},
|
||||
"supportedFeatures": [
|
||||
"providerAdvertisements",
|
||||
"commandTransport",
|
||||
"providerSurfaceSession",
|
||||
"providerSurfaceSource",
|
||||
"providerSurfaceDocumentSource",
|
||||
"liveProviderTransportBridge",
|
||||
"livePlayableWorldClient",
|
||||
"providerCommandReceipts",
|
||||
"providerAssetManifestDocumentSource",
|
||||
"playableWorldClientHost",
|
||||
"playableWorldClientBootstrap",
|
||||
"playableWorldInputDriver",
|
||||
"playableWorldCameraRig",
|
||||
"sceneGraphProjection",
|
||||
"playableWorldProjection",
|
||||
"playableWorldRuntimeHost",
|
||||
"playableWorldScenePresentation",
|
||||
"unityGameObjectSceneSink",
|
||||
"providerAssetManifestResolution",
|
||||
"providerAssetManifestSource",
|
||||
"embeddedDocuments"
|
||||
],
|
||||
"supportedPlugins": [
|
||||
{
|
||||
"pluginId": "sai.vn",
|
||||
"capabilities": [
|
||||
"vn.stage",
|
||||
"story.choose",
|
||||
"story.continue",
|
||||
"story.jump"
|
||||
],
|
||||
"projectionAdapter": "SaiVisualNovelUnitySceneProjectionAdapter"
|
||||
},
|
||||
{
|
||||
"pluginId": "norn.graph",
|
||||
"capabilities": [
|
||||
"embed.norn"
|
||||
],
|
||||
"projectionAdapter": "NornGraphUnitySceneProjectionAdapter"
|
||||
},
|
||||
{
|
||||
"pluginId": "tex.math",
|
||||
"capabilities": [
|
||||
"embed.tex",
|
||||
"tex.inline",
|
||||
"tex.block",
|
||||
"tex.scene-placement"
|
||||
],
|
||||
"projectionAdapter": "TeXMathUnitySceneProjectionAdapter"
|
||||
}
|
||||
],
|
||||
"unsupportedPlugins": [
|
||||
|
||||
],
|
||||
"worldSurfaceLowering": [
|
||||
{
|
||||
"targetId": "unity-scene",
|
||||
"surfaceKinds": [
|
||||
"interactive-world"
|
||||
],
|
||||
"projectionKinds": [
|
||||
"provider-authored-world-surface"
|
||||
],
|
||||
"supportLevel": "unity-playable-world-scene-command-surface",
|
||||
"ownership": "runtime-lowers-provider-world-surface-without-owning-world-state",
|
||||
"evidencePaths": [
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneClientSession.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneProviderConnection.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneLiveProviderBridge.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldLiveClient.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientBootstrap.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldInputDriver.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityProviderRefreshSource.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldPresenter.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldAssetManifest.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/SaiVisualNovelUnitySceneProjectionAdapter.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/NornGraphUnitySceneProjectionAdapter.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/TeXMathUnitySceneProjectionAdapter.cs",
|
||||
"packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs"
|
||||
]
|
||||
}
|
||||
],
|
||||
"commandTransport": {
|
||||
"schema": "gamecult.eve.command.v1",
|
||||
"status": "provider-shell-contract-skeleton",
|
||||
"emitter": "EveUnitySceneSurfaceLowerer.CreateCommandIntent",
|
||||
"clientId": "unity-scene",
|
||||
"authority": "Unity scene input emits command intents through provider-advertised command boundaries; providers accept, deny, and publish receipts."
|
||||
},
|
||||
"lifecycle": {
|
||||
"release": {
|
||||
"ownerRepo": "EveUnity",
|
||||
"status": "owner-repo-upm-artifact",
|
||||
"releaseContract": {
|
||||
"ownerRepo": "EveUnity",
|
||||
"packageName": "org.gamecult.eve.unity-scene",
|
||||
"packageRoot": "packages/org.gamecult.eve.unity-scene",
|
||||
"versionSource": "packages/org.gamecult.eve.unity-scene/package.json",
|
||||
"tagPattern": "eveunity-scene-v{version}",
|
||||
"artifactKind": "upm-package",
|
||||
"requestSchema": "gamecult.eve.runtime_release_request.v1",
|
||||
"requestBuilder": "tools/eveunity/eveunity-release-contract.mjs",
|
||||
"artifactBuilder": "tools/eveunity/eveunity-release-artifact.mjs",
|
||||
"artifactPattern": "artifacts/eveunity-scene-release/{version}/org.gamecult.eve.unity-scene-{version}.tgz",
|
||||
"publishProof": "Tagged Unity scene package release is created from the EveUnity repository.",
|
||||
"requiredPackageDependencies": [
|
||||
{
|
||||
"packageName": "org.gamecult.eve.surface",
|
||||
"version": "0.1.0",
|
||||
"packageManager": "upm",
|
||||
"ownerRepo": "Eve",
|
||||
"purpose": "Shared Unity DTOs for gamecult.eve.surface.v1 documents."
|
||||
}
|
||||
]
|
||||
},
|
||||
"evidencePaths": [
|
||||
"packages/org.gamecult.eve.unity-scene/package.json",
|
||||
"packages/org.gamecult.eve.unity-scene/Runtime/GameCult.Eve.UnityScene.asmdef",
|
||||
"scripts/pack.ps1"
|
||||
],
|
||||
"pendingProofs": [
|
||||
"Cut and publish the first tagged EveUnity package release."
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"ownerRepo": "EveUnity",
|
||||
"status": "unity-scene-editmode-batchmode",
|
||||
"testContract": {
|
||||
"ownerRepo": "EveUnity",
|
||||
"runnerKind": "unity-editmode-batchmode",
|
||||
"runnerScript": "scripts/run-package-tests.ps1",
|
||||
"consumerProject": "E:\\Projects\\Aetheria",
|
||||
"packageName": "org.gamecult.eve.unity-scene",
|
||||
"testAssembly": "GameCult.Eve.UnityScene.Tests",
|
||||
"testPlatform": "EditMode",
|
||||
"resultsArtifact": "artifacts/aetheria-unity-scene-editmode/{stamp}/unity-editmode-results.xml",
|
||||
"logArtifact": "artifacts/aetheria-unity-scene-editmode/{stamp}/unity-editmode.log",
|
||||
"manifestMutation": "Temporarily adds org.gamecult.eve.unity-scene to Aetheria Unity testables, runs GameCult.Eve.UnityScene.Tests, and restores Packages/manifest.json."
|
||||
},
|
||||
"evidencePaths": [
|
||||
"packages/org.gamecult.eve.unity-scene/Tests/Editor/GameCult.Eve.UnityScene.Tests.asmdef",
|
||||
"packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs",
|
||||
"scripts/run-package-tests.ps1"
|
||||
],
|
||||
"pendingProofs": [
|
||||
"Move the Aetheria consumer scenario into a provider-owned conformance pack that installs EveUnity from its owner repository."
|
||||
]
|
||||
},
|
||||
"capture": {
|
||||
"ownerRepo": "EveUnity",
|
||||
"status": "pending-unity-frame-capture",
|
||||
"captureContract": {
|
||||
"ownerRepo": "EveUnity",
|
||||
"runtimeId": "unity-scene",
|
||||
"targetId": "unity-scene",
|
||||
"requestSchema": "gamecult.eve.runtime_capture_request.v1",
|
||||
"requestBuilder": "tools/eveunity/eveunity-capture-contract.mjs",
|
||||
"advertisementPath": "GameCult/Aetheria:web/fixtures/aetheria.provider-advertisement.json",
|
||||
"captureKind": "unity-scene-frame-png",
|
||||
"artifactKind": "png",
|
||||
"artifactPattern": "artifacts/eveunity-scene-capture/{stamp}/unity-scene.png",
|
||||
"conformanceAttachment": "runtime.captureArtifacts[]",
|
||||
"requiredProvider": "aetheria",
|
||||
"requiredSurface": "aetheria.daemon.game",
|
||||
"additionalProviderSurfaces": [
|
||||
{
|
||||
"providerId": "eve.world-smoke",
|
||||
"surfaceId": "eve.world-smoke.surface",
|
||||
"purpose": "Generic non-product interactive-world capture control."
|
||||
}
|
||||
],
|
||||
"authority": "Capture observes Unity scene projection only; provider state, command acceptance, and receipts stay provider-owned.",
|
||||
"publishProof": "Unity scene screenshot or frame-capture PNG artifact is attached to the runtime conformance report from EveUnity."
|
||||
},
|
||||
"evidencePaths": [
|
||||
|
||||
],
|
||||
"pendingProofs": [
|
||||
"Unity scene screenshot or frame-capture PNG artifact is attached to the runtime conformance report."
|
||||
]
|
||||
}
|
||||
},
|
||||
"capture": {
|
||||
"status": "pending-unity-frame-capture",
|
||||
"note": "A real Unity PNG/frame capture driven by a provider surface remains active lifecycle work.",
|
||||
"artifacts": [
|
||||
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 16a73a75d0939424bad58c752f0f8a3e
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
18
packages/org.gamecult.eve.unity-scene/package.json
Normal file
18
packages/org.gamecult.eve.unity-scene/package.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"name": "org.gamecult.eve.unity-scene",
|
||||
"displayName": "Eve Unity Scene Lowering",
|
||||
"version": "0.1.0",
|
||||
"unity": "2021.3",
|
||||
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
|
||||
"dependencies": {
|
||||
"org.gamecult.eve.surface": "0.1.0"
|
||||
},
|
||||
"keywords": [
|
||||
"Eve",
|
||||
"Unity",
|
||||
"Scene",
|
||||
"Interactive World"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"category": "Scripting"
|
||||
}
|
||||
7
packages/org.gamecult.eve.unity-scene/package.json.meta
Normal file
7
packages/org.gamecult.eve.unity-scene/package.json.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8eaedcd0b70a6574c8ddb12c84b444a5
|
||||
PackageManifestImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
27
scripts/pack.ps1
Normal file
27
scripts/pack.ps1
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
param(
|
||||
[string] $OutputRoot = "artifacts\packages"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$packageRoot = Join-Path $repoRoot "packages\org.gamecult.eve.unity-scene"
|
||||
$output = if ([System.IO.Path]::IsPathRooted($OutputRoot)) {
|
||||
$OutputRoot
|
||||
} else {
|
||||
Join-Path $repoRoot $OutputRoot
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $output | Out-Null
|
||||
$packed = npm pack $packageRoot --pack-destination $output
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "EveUnity UPM package failed to pack"
|
||||
}
|
||||
|
||||
$artifact = Join-Path $output ($packed | Select-Object -Last 1)
|
||||
if (-not (Test-Path -LiteralPath $artifact)) {
|
||||
throw "EveUnity package artifact was not produced: $artifact"
|
||||
}
|
||||
|
||||
Write-Host "EveUnity package: $artifact"
|
||||
|
||||
74
scripts/run-package-tests.ps1
Normal file
74
scripts/run-package-tests.ps1
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
param(
|
||||
[string] $AetheriaRoot = "E:\Projects\Aetheria",
|
||||
[string] $UnityExe = "C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Unity.exe",
|
||||
[string] $OutputRoot = "artifacts\aetheria-consumer"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$packageRoot = Join-Path $repoRoot "packages\org.gamecult.eve.unity-scene"
|
||||
$manifestPath = Join-Path $AetheriaRoot "Packages\manifest.json"
|
||||
$output = if ([System.IO.Path]::IsPathRooted($OutputRoot)) {
|
||||
$OutputRoot
|
||||
} else {
|
||||
Join-Path $repoRoot $OutputRoot
|
||||
}
|
||||
|
||||
foreach ($required in @($packageRoot, $manifestPath, $UnityExe)) {
|
||||
if (-not (Test-Path -LiteralPath $required)) {
|
||||
throw "Required EveUnity consumer-test path not found: $required"
|
||||
}
|
||||
}
|
||||
|
||||
$stamp = Get-Date -Format "yyyyMMddTHHmmss"
|
||||
$runRoot = Join-Path $output $stamp
|
||||
New-Item -ItemType Directory -Force -Path $runRoot | Out-Null
|
||||
$resultsPath = Join-Path $runRoot "unity-editmode-results.xml"
|
||||
$logPath = Join-Path $runRoot "unity-editmode.log"
|
||||
$originalManifest = Get-Content -Raw -LiteralPath $manifestPath
|
||||
|
||||
try {
|
||||
$manifest = $originalManifest | ConvertFrom-Json
|
||||
$dependency = "file:" + ($packageRoot -replace '\\', '/')
|
||||
$manifest.dependencies.'org.gamecult.eve.unity-scene' = $dependency
|
||||
$testables = @($manifest.testables) | Where-Object { $_ }
|
||||
if ($testables -notcontains 'org.gamecult.eve.unity-scene') {
|
||||
$testables += 'org.gamecult.eve.unity-scene'
|
||||
}
|
||||
if ($manifest.PSObject.Properties.Name -contains 'testables') {
|
||||
$manifest.testables = $testables
|
||||
} else {
|
||||
$manifest | Add-Member -NotePropertyName testables -NotePropertyValue $testables
|
||||
}
|
||||
$manifest | ConvertTo-Json -Depth 32 | Set-Content -LiteralPath $manifestPath
|
||||
|
||||
$arguments = @(
|
||||
'-batchmode',
|
||||
'-projectPath', $AetheriaRoot,
|
||||
'-runTests',
|
||||
'-testPlatform', 'EditMode',
|
||||
'-assemblyNames', 'GameCult.Eve.UnityScene.Tests',
|
||||
'-testResults', $resultsPath,
|
||||
'-logFile', $logPath
|
||||
)
|
||||
$process = Start-Process -FilePath $UnityExe -ArgumentList $arguments -Wait -PassThru -WindowStyle Hidden
|
||||
if ($process.ExitCode -ne 0) {
|
||||
if (Test-Path -LiteralPath $logPath) {
|
||||
Get-Content -LiteralPath $logPath -Tail 120
|
||||
}
|
||||
throw "EveUnity consumer tests failed with exit code $($process.ExitCode)"
|
||||
}
|
||||
} finally {
|
||||
Set-Content -NoNewline -LiteralPath $manifestPath -Value $originalManifest
|
||||
}
|
||||
|
||||
[xml] $results = Get-Content -Raw -LiteralPath $resultsPath
|
||||
$run = $results.SelectSingleNode('//test-run')
|
||||
if ($null -eq $run -or [int] $run.failed -gt 0) {
|
||||
throw "EveUnity consumer tests did not pass: $resultsPath"
|
||||
}
|
||||
|
||||
Write-Host "EveUnity consumer tests: $($run.passed) passed, $($run.total) total"
|
||||
Write-Host "Results: $resultsPath"
|
||||
|
||||
Reference in a new issue