Establish EveUnity runtime owner repo

This commit is contained in:
Meta Vens 2026-07-10 02:27:47 +01:00
commit 9341d44d8c
54 changed files with 5565 additions and 0 deletions

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
artifacts/
Library/
Logs/
Temp/
obj/
*.csproj
*.sln

21
README.md Normal file
View 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
View 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.

View 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.

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 74b6947fafbfd0d42a642b7527a32258
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 1ed3bb976c01b68498f8da912b8ddf2d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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;
}
}
}

View file

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

View file

@ -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);
}
}
}

View file

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

View file

@ -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;
}
}
}

View file

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

View file

@ -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;
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1d5e4b8e1e104bb99522dc2b75d7f4c9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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;
}
}
}

View file

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

View file

@ -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);
}
}
}

View file

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

View file

@ -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);
}
}

View file

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

View file

@ -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");
}
}
}

View file

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

View file

@ -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);
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9323e6fa0b61837499fa84b30e597e92

View file

@ -0,0 +1,7 @@
namespace GameCult.Eve.UnityScene
{
public interface IEveUnityProviderRefreshSource
{
void Refresh();
}
}

View file

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

View file

@ -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; }
}
}

View file

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

View file

@ -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);
}
}
}

View file

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

View file

@ -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);
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 9e93e937d561d4941b5080a454091f7a

View file

@ -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; }
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 32422132e752b9a4f8a45b2a5cb01217

View file

@ -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
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: a5b5eeaa1ac2b24409cf4718616ebbe3
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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");
}
}
}

View file

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

View file

@ -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", ""))));
}
}
}

View file

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

View file

@ -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";
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0184a5155baa9774f9ed133ad871b735

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b37c64ab7abc96a468018e86c041d431
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ac7339c6ff81f6f48864df2308ebeed2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

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

View file

@ -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
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 10e7994adb919944aadc88f960300bc4
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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": [
]
}
}

View file

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 16a73a75d0939424bad58c752f0f8a3e
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View 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"
}

View file

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

27
scripts/pack.ps1 Normal file
View 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"

View 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"