2026-07-10 02:27:47 +01:00
using System ;
using System.Collections.Generic ;
2026-07-13 21:18:10 +01:00
using System.Linq ;
2026-07-10 02:27:47 +01:00
using UnityEngine ;
#nullable enable
namespace GameCult.Eve.UnityScene
{
public interface IEveUnityGameObjectAssetProvider
{
GameObject ? ResolvePrefab ( EveUnityPlayableWorldAssetBinding asset ) ;
}
2026-07-12 06:25:10 +01:00
public interface IEveUnityNativeAssetProvider : IEveUnityGameObjectAssetProvider
{
UnityEngine . Object ? ResolveAsset ( EveUnityPlayableWorldAssetBinding asset , Type assetType ) ;
}
2026-07-15 19:59:37 +01:00
public interface IEveUnityNativeAssetMetadataProvider
{
bool TryResolveAssetMetadata (
EveUnityPlayableWorldAssetBinding asset ,
out IReadOnlyDictionary < string , string > metadata ) ;
}
2026-07-13 21:18:10 +01:00
public sealed class EveUnityGameObjectPlayableWorldSceneSink :
IEveUnityPlayableWorldSceneSink ,
IEveUnityEntityGenerationSink ,
2026-07-15 06:08:45 +01:00
IEveUnityPresentedEntityRegistry ,
IDisposable
2026-07-10 02:27:47 +01:00
{
private readonly Transform _root ;
private readonly IEveUnityGameObjectAssetProvider _assetProvider ;
private readonly Dictionary < string , GameObject > _instances = new Dictionary < string , GameObject > ( StringComparer . Ordinal ) ;
2026-07-13 21:18:10 +01:00
private IReadOnlyDictionary < string , EveUnityPresentedEntityHandle > _presentedById =
new Dictionary < string , EveUnityPresentedEntityHandle > ( StringComparer . Ordinal ) ;
private IReadOnlyDictionary < int , EveUnityPresentedEntityHandle > _presentedByIndex =
new Dictionary < int , EveUnityPresentedEntityHandle > ( ) ;
2026-07-10 02:27:47 +01:00
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 ;
2026-07-13 21:18:10 +01:00
public EveUnityPresentedEntityGeneration ? CurrentGeneration { get ; private set ; }
public bool TryGetByEntityId ( string entityId , out EveUnityPresentedEntityHandle handle ) = >
_presentedById . TryGetValue ( entityId ? ? "" , out handle ! ) ;
public bool TryGetBySourceIndex ( int sourceIndex , out EveUnityPresentedEntityHandle handle ) = >
_presentedByIndex . TryGetValue ( sourceIndex , out handle ! ) ;
2026-07-10 02:27:47 +01:00
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 ) ;
}
2026-07-13 21:18:10 +01:00
public void ApplyGeneration ( EveUnityPresentedEntityGeneration generation )
{
if ( generation = = null ) throw new ArgumentNullException ( nameof ( generation ) ) ;
if ( generation . Entities . Any ( entity = > string . IsNullOrWhiteSpace ( entity . EntityId ) ) )
throw new InvalidOperationException ( "Presented entity generations require stable entity ids." ) ;
var factsById = generation . Entities . ToDictionary ( entity = > entity . EntityId , StringComparer . Ordinal ) ;
var factsByIndex = generation . Entities . ToDictionary ( entity = > entity . SourceIndex ) ;
GameObject ? staging = null ;
var created = new Dictionary < string , GameObject > ( StringComparer . Ordinal ) ;
var nextInstances = new Dictionary < string , GameObject > ( StringComparer . Ordinal ) ;
try
{
foreach ( var fact in generation . Entities )
{
if ( _instances . TryGetValue ( fact . EntityId , out var existing ) & & existing ! = null )
{
nextInstances . Add ( fact . EntityId , existing ) ;
continue ;
}
staging ? ? = new GameObject ( $"{_root.name}.generation.{generation.ProducerEpoch}.{generation.Sequence}" ) ;
if ( staging . transform . parent = = null )
{
staging . SetActive ( false ) ;
staging . transform . SetParent ( _root , false ) ;
}
var entity = fact . ToPlayableWorldEntity ( ) ;
var asset = new EveUnityPlayableWorldAssetBinding (
fact . AssetRef ,
fact . EntityKind ,
string . IsNullOrWhiteSpace ( fact . AssetRef ) ? "unity-generated-placeholder" : "provider-asset-ref" ) ;
var instance = InstantiateEntity ( entity , asset , staging . transform ) ;
created . Add ( fact . EntityId , instance ) ;
nextInstances . Add ( fact . EntityId , instance ) ;
}
foreach ( var fact in generation . Entities )
{
var entity = fact . ToPlayableWorldEntity ( ) ;
var asset = new EveUnityPlayableWorldAssetBinding (
fact . AssetRef ,
fact . EntityKind ,
string . IsNullOrWhiteSpace ( fact . AssetRef ) ? "unity-generated-placeholder" : "provider-asset-ref" ) ;
ApplyEntity ( nextInstances [ fact . EntityId ] , entity , asset ) ;
}
var nextById = factsById . ToDictionary (
pair = > pair . Key ,
pair = > new EveUnityPresentedEntityHandle ( pair . Value , nextInstances [ pair . Key ] . transform ) ,
StringComparer . Ordinal ) ;
var nextByIndex = factsByIndex . ToDictionary (
pair = > pair . Key ,
pair = > nextById [ pair . Value . EntityId ] ) ;
foreach ( var pair in _instances )
if ( ! nextInstances . ContainsKey ( pair . Key ) & & pair . Value ! = null )
DestroyInstance ( pair . Value ) ;
_instances . Clear ( ) ;
foreach ( var pair in nextInstances )
{
if ( created . ContainsKey ( pair . Key ) )
{
pair . Value . transform . SetParent ( _root , true ) ;
pair . Value . SetActive ( true ) ;
}
_instances . Add ( pair . Key , pair . Value ) ;
}
_presentedById = nextById ;
_presentedByIndex = nextByIndex ;
CurrentGeneration = generation ;
}
catch
{
foreach ( var instance in created . Values )
if ( instance ! = null ) DestroyInstance ( instance ) ;
throw ;
}
finally
{
if ( staging ! = null )
DestroyInstance ( staging ) ;
}
}
2026-07-10 02:27:47 +01:00
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 ) ;
}
2026-07-15 06:08:45 +01:00
public void Dispose ( )
{
foreach ( var instance in _instances . Values )
if ( instance ! = null )
2026-07-15 06:34:39 +01:00
{
instance . transform . SetParent ( null , false ) ;
instance . SetActive ( false ) ;
2026-07-15 06:08:45 +01:00
DestroyInstance ( instance ) ;
2026-07-15 06:34:39 +01:00
}
2026-07-15 06:08:45 +01:00
_instances . Clear ( ) ;
_presentedById = new Dictionary < string , EveUnityPresentedEntityHandle > ( StringComparer . Ordinal ) ;
_presentedByIndex = new Dictionary < int , EveUnityPresentedEntityHandle > ( ) ;
CurrentGeneration = null ;
}
2026-07-13 21:18:10 +01:00
private GameObject InstantiateEntity (
EveUnityPlayableWorldEntity entity ,
EveUnityPlayableWorldAssetBinding asset ,
Transform ? parent = null )
2026-07-10 02:27:47 +01:00
{
2026-07-13 21:18:10 +01:00
parent ? ? = _root ;
2026-07-10 02:27:47 +01:00
var prefab = _assetProvider . ResolvePrefab ( asset ) ;
2026-07-10 06:00:36 +01:00
GameObject instance ;
if ( prefab ! = null )
{
instance = new GameObject ( entity . EntityId ) ;
2026-07-13 21:18:10 +01:00
instance . transform . SetParent ( parent , false ) ;
2026-07-10 06:00:36 +01:00
var visual = UnityEngine . Object . Instantiate ( prefab , instance . transform ) ;
visual . transform . localPosition = Vector3 . zero ;
visual . transform . localRotation = Quaternion . identity ;
}
else
{
instance = GameObject . CreatePrimitive ( PrimitiveType . Capsule ) ;
}
2026-07-10 02:27:47 +01:00
2026-07-13 21:18:10 +01:00
if ( instance . transform . parent ! = parent )
instance . transform . SetParent ( parent , false ) ;
2026-07-10 02:27:47 +01:00
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 ) ;
2026-07-16 04:13:28 +01:00
instance . transform . localScale = Vector3 . one * ( entity . Radius > 0f ? entity . Radius : 1f ) ;
2026-07-10 02:27:47 +01:00
var marker = instance . GetComponent < EveUnityPlayableWorldEntityMarker > ( ) ;
if ( marker = = null )
marker = instance . AddComponent < EveUnityPlayableWorldEntityMarker > ( ) ;
marker . Apply ( entity , asset ) ;
2026-07-12 02:21:59 +01:00
if ( entity . Props . TryGetValue ( "presentationState" , out var presentationState ) & &
! string . IsNullOrWhiteSpace ( presentationState ) )
{
var semanticPresentation = instance . GetComponent < EveUnitySemanticEntityPresentation > ( ) ;
if ( semanticPresentation = = null )
semanticPresentation = instance . AddComponent < EveUnitySemanticEntityPresentation > ( ) ;
semanticPresentation . Apply ( entity . Props ) ;
}
2026-07-10 02:27:47 +01:00
}
private static void DestroyInstance ( GameObject instance )
{
if ( Application . isPlaying )
UnityEngine . Object . Destroy ( instance ) ;
else
UnityEngine . Object . DestroyImmediate ( instance ) ;
}
}
2026-07-12 02:21:59 +01:00
public sealed class EveUnitySemanticEntityPresentation : MonoBehaviour
{
private static readonly int Emission = Shader . PropertyToID ( "_Emission" ) ;
private static readonly int EmissionColor = Shader . PropertyToID ( "_EmissionColor" ) ;
private MaterialPropertyBlock ? _properties ;
private Renderer [ ] _renderers = Array . Empty < Renderer > ( ) ;
private string _state = "" ;
private float _activePulseSeconds = 1f ;
private float _triggeredPulseSeconds = 0.25f ;
private float _activeEmission = 1f ;
private float _triggeredEmission = 4f ;
public string PresentationState = > _state ;
public void Apply ( IReadOnlyDictionary < string , string > props )
{
props ? ? = new Dictionary < string , string > ( StringComparer . Ordinal ) ;
_state = Read ( props , "presentationState" ) ;
_activePulseSeconds = Positive ( ReadFloat ( props , "activePulseSeconds" , _activePulseSeconds ) , 1f ) ;
_triggeredPulseSeconds = Positive ( ReadFloat ( props , "triggeredPulseSeconds" , _triggeredPulseSeconds ) , 0.25f ) ;
_activeEmission = Math . Max ( 0f , ReadFloat ( props , "activeEmission" , _activeEmission ) ) ;
_triggeredEmission = Math . Max ( 0f , ReadFloat ( props , "triggeredEmission" , _triggeredEmission ) ) ;
_renderers = GetComponentsInChildren < Renderer > ( true ) ;
ApplyAt ( Time . time ) ;
}
public void ApplyAt ( float timeSeconds )
{
var triggered = string . Equals ( _state , "triggered" , StringComparison . Ordinal ) ;
var active = triggered | | string . Equals ( _state , "active" , StringComparison . Ordinal ) ;
var period = triggered ? _triggeredPulseSeconds : _activePulseSeconds ;
var peak = triggered ? _triggeredEmission : _activeEmission ;
var envelope = active
? Mathf . Lerp ( 0.1f , 1f , Mathf . Abs ( Mathf . Cos ( Mathf . PI * timeSeconds / period ) ) )
: 0f ;
var value = peak * envelope ;
_properties ? ? = new MaterialPropertyBlock ( ) ;
foreach ( var renderer in _renderers )
{
if ( renderer = = null ) continue ;
renderer . GetPropertyBlock ( _properties ) ;
_properties . SetFloat ( Emission , value ) ;
_properties . SetColor ( EmissionColor , Color . white * value ) ;
renderer . SetPropertyBlock ( _properties ) ;
}
}
private void Update ( ) = > ApplyAt ( Time . time ) ;
private static string Read ( IReadOnlyDictionary < string , string > props , string key ) = >
props . TryGetValue ( key , out var value ) ? value ? ? "" : "" ;
private static float ReadFloat ( IReadOnlyDictionary < string , string > props , string key , float fallback ) = >
props . TryGetValue ( key , out var value ) & & float . TryParse ( value ,
System . Globalization . NumberStyles . Float ,
System . Globalization . CultureInfo . InvariantCulture ,
out var parsed )
? parsed
: fallback ;
private static float Positive ( float value , float fallback ) = > value > 0f ? value : fallback ;
}
2026-07-12 06:25:10 +01:00
public sealed class EveUnityResourcesAssetProvider : IEveUnityNativeAssetProvider
2026-07-10 02:27:47 +01:00
{
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 ) ;
}
2026-07-12 06:25:10 +01:00
public UnityEngine . Object ? ResolveAsset ( EveUnityPlayableWorldAssetBinding asset , Type assetType )
{
if ( asset = = null ) throw new ArgumentNullException ( nameof ( asset ) ) ;
if ( assetType = = null ) throw new ArgumentNullException ( nameof ( assetType ) ) ;
var resourcesPath = EveUnityPlayableWorldAssetManifestEntry . NormalizeResourcesPath ( asset . AssetRef ) ;
return string . IsNullOrWhiteSpace ( resourcesPath ) ? null : Resources . Load ( resourcesPath , assetType ) ;
}
2026-07-10 02:27:47 +01:00
}
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 ;
}
}
}