Aetheria/Packages/org.gamecult.aetheria.eve-runtime/Runtime/AetheriaEveUnitySurfaceHost.cs
2026-06-30 23:15:57 +01:00

411 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using GameCult.Aetheria.State.Verse;
using GameCult.Eve.Surface;
using GameCult.Eve.UnityUIToolkit;
using GameCult.Mesh;
using UnityEngine;
using UnityEngine.TextCore.LowLevel;
using UnityEngine.UIElements;
using TextCoreFontAsset = UnityEngine.TextCore.Text.FontAsset;
namespace GameCult.Aetheria.EveRuntime
{
public sealed class AetheriaEveUnitySurfaceChrome
{
public float RootPaddingTop = 16f;
public Align RootAlignItems = Align.Center;
public Justify RootJustifyContent = Justify.FlexStart;
public PickingMode RootPickingMode = PickingMode.Ignore;
public float RootPaddingLeft = 0f;
public float RootPaddingRight = 0f;
public float RootPaddingBottom = 0f;
public Color RootBackgroundColor = new Color(0f, 0f, 0f, 0f);
public float Width = 0f;
public float MinWidth = 420f;
public float MaxWidth = 760f;
public float MaxHeight = 0f;
public float FlexGrow = 0f;
public FlexDirection FlexDirection = FlexDirection.Column;
public float PaddingLeft = 16f;
public float PaddingRight = 16f;
public float PaddingTop = 12f;
public float PaddingBottom = 12f;
public float BorderRadius = 8f;
public float BorderWidth = 1f;
public Color BackgroundColor = new Color(0.08f, 0.1f, 0.14f, 0.94f);
public Color BorderColor = new Color(0.3f, 0.47f, 0.71f, 0.8f);
public PickingMode ShellPickingMode = PickingMode.Position;
public bool UseShell = true;
}
public static class AetheriaEveUnitySurfaceHost
{
private static PanelSettings _runtimePanelSettings;
private static readonly Dictionary<string, TextCoreFontAsset> FontCache = new Dictionary<string, TextCoreFontAsset>(StringComparer.Ordinal);
public static UIDocument Render(
Transform owner,
UIDocument document,
string hostName,
EveSurfaceDocument surface,
Action<EveSurfaceCommandRequest> commandHandler,
AetheriaEveUnitySurfaceChrome chrome,
CultMeshStateRefResolver stateRefResolver = null,
Func<EveEmbeddedDocumentSlot, EveSurfaceDocument> embeddedDocumentResolver = null,
int sortingOrder = 1000)
{
if (owner == null)
throw new ArgumentNullException(nameof(owner));
if (surface == null)
throw new ArgumentNullException(nameof(surface));
if (commandHandler == null)
throw new ArgumentNullException(nameof(commandHandler));
document = ResolveDocument(owner, document, hostName, sortingOrder);
document.gameObject.SetActive(true);
var root = document.rootVisualElement;
ConfigureRoot(root, chrome);
var effectiveStateRefResolver = stateRefResolver ?? (
ContainsStateRefs(surface)
? CreateDefaultStateRefResolver()
: null);
surface = AetheriaRuntimeSurfaceDocuments.ResolveStateRefs(surface, effectiveStateRefResolver);
var lowerer = new EveUiToolkitSurfaceLowerer(new EveUiToolkitSurfaceOptions(embeddedDocumentResolver));
VisualElement lowered;
if (chrome.UseShell)
{
var shell = CreateShell(chrome);
root.Add(shell);
lowered = lowerer.Lower(surface, commandHandler);
ApplyTheme(lowered, surface);
shell.Add(lowered);
}
else
{
lowered = lowerer.Lower(surface, commandHandler);
ApplyTheme(lowered, surface);
root.Add(lowered);
}
Debug.Log(
$"Rendered Eve surface '{surface.Surface.Id}' into UIDocument '{document.name}' with panel '{document.panelSettings?.name ?? "none"}'.");
return document;
}
public static UIDocument RenderRuntime(
Transform owner,
UIDocument document,
string hostName,
AetheriaRuntimeSurfaceDocument surface,
Action<EveSurfaceCommandRequest> commandHandler,
AetheriaEveUnitySurfaceChrome chrome,
CultMeshStateRefResolver stateRefResolver = null,
Func<EveEmbeddedDocumentSlot, AetheriaRuntimeSurfaceDocument> embeddedDocumentResolver = null,
int sortingOrder = 1000)
{
if (surface == null)
throw new ArgumentNullException(nameof(surface));
return Render(
owner,
document,
hostName,
AetheriaRuntimeSurfaceDocuments.ToEveSurfaceDocument(surface),
commandHandler,
chrome,
stateRefResolver,
embeddedDocumentResolver == null
? null
: slot =>
{
var nested = embeddedDocumentResolver(slot);
return nested == null ? null : AetheriaRuntimeSurfaceDocuments.ToEveSurfaceDocument(nested);
},
sortingOrder);
}
public static void Hide(UIDocument document)
{
if (document == null)
return;
document.rootVisualElement.Clear();
document.gameObject.SetActive(false);
}
public static void DestroyDocument(UIDocument document)
{
if (document == null)
return;
UnityEngine.Object.Destroy(document.gameObject);
}
private static UIDocument ResolveDocument(
Transform owner,
UIDocument document,
string hostName,
int sortingOrder)
{
if (document != null)
{
if (document.panelSettings == null)
document.panelSettings = ResolveRuntimePanelSettings();
return document;
}
var host = new GameObject(string.IsNullOrWhiteSpace(hostName) ? "Aetheria Eve Surface" : hostName);
host.SetActive(false);
host.transform.SetParent(owner, false);
host.layer = owner.gameObject.layer;
document = host.AddComponent<UIDocument>();
document.sortingOrder = sortingOrder;
document.panelSettings = ResolveRuntimePanelSettings();
return document;
}
private static PanelSettings ResolveRuntimePanelSettings()
{
if (_runtimePanelSettings != null)
return _runtimePanelSettings;
_runtimePanelSettings = ScriptableObject.CreateInstance<PanelSettings>();
_runtimePanelSettings.name = "Aetheria Runtime Eve Panel Settings";
_runtimePanelSettings.scaleMode = PanelScaleMode.ScaleWithScreenSize;
_runtimePanelSettings.referenceResolution = new Vector2Int(1920, 1080);
_runtimePanelSettings.screenMatchMode = PanelScreenMatchMode.MatchWidthOrHeight;
_runtimePanelSettings.match = 0.5f;
_runtimePanelSettings.themeStyleSheet = Resources.Load<ThemeStyleSheet>("UnityDefaultRuntimeTheme");
if (_runtimePanelSettings.themeStyleSheet == null)
Debug.LogWarning("Aetheria runtime Eve panel could not load UnityDefaultRuntimeTheme.tss.");
return _runtimePanelSettings;
}
private static void ConfigureRoot(VisualElement root, AetheriaEveUnitySurfaceChrome chrome)
{
root.Clear();
root.style.flexGrow = 1;
root.style.position = Position.Absolute;
root.style.left = 0;
root.style.top = 0;
root.style.right = 0;
root.style.bottom = 0;
root.style.alignItems = chrome.RootAlignItems;
root.style.justifyContent = chrome.RootJustifyContent;
root.style.paddingLeft = chrome.RootPaddingLeft;
root.style.paddingTop = chrome.RootPaddingTop;
root.style.paddingRight = chrome.RootPaddingRight;
root.style.paddingBottom = chrome.RootPaddingBottom;
if (chrome.RootBackgroundColor.a > 0f)
root.style.backgroundColor = chrome.RootBackgroundColor;
root.pickingMode = chrome.RootPickingMode;
}
private static VisualElement CreateShell(AetheriaEveUnitySurfaceChrome chrome)
{
var shell = new VisualElement();
shell.style.flexDirection = chrome.FlexDirection;
shell.style.flexGrow = chrome.FlexGrow;
if (chrome.Width > 0f)
shell.style.width = chrome.Width;
shell.style.minWidth = chrome.MinWidth;
shell.style.maxWidth = chrome.MaxWidth;
if (chrome.MaxHeight > 0f)
shell.style.maxHeight = chrome.MaxHeight;
shell.style.backgroundColor = chrome.BackgroundColor;
shell.style.borderTopLeftRadius = chrome.BorderRadius;
shell.style.borderTopRightRadius = chrome.BorderRadius;
shell.style.borderBottomLeftRadius = chrome.BorderRadius;
shell.style.borderBottomRightRadius = chrome.BorderRadius;
shell.style.paddingLeft = chrome.PaddingLeft;
shell.style.paddingRight = chrome.PaddingRight;
shell.style.paddingTop = chrome.PaddingTop;
shell.style.paddingBottom = chrome.PaddingBottom;
shell.style.borderLeftWidth = chrome.BorderWidth;
shell.style.borderRightWidth = chrome.BorderWidth;
shell.style.borderTopWidth = chrome.BorderWidth;
shell.style.borderBottomWidth = chrome.BorderWidth;
shell.style.borderLeftColor = chrome.BorderColor;
shell.style.borderRightColor = chrome.BorderColor;
shell.style.borderTopColor = chrome.BorderColor;
shell.style.borderBottomColor = chrome.BorderColor;
shell.pickingMode = chrome.ShellPickingMode;
return shell;
}
private static void ApplyTheme(VisualElement root, EveSurfaceDocument surface)
{
if (root == null || surface?.Surface?.Styles == null)
return;
var tokens = surface.Surface.Styles
.Where(token => !string.IsNullOrWhiteSpace(token?.Name))
.GroupBy(token => token.Name, StringComparer.Ordinal)
.ToDictionary(group => group.Key, group => group.Last().Value ?? "", StringComparer.Ordinal);
var bodyFont = ResolveFont(tokens, "body");
var titleFont = ResolveFont(tokens, "title") ?? bodyFont;
if (bodyFont != null)
{
root.Query<TextElement>().ForEach(element =>
element.style.unityFontDefinition = new StyleFontDefinition(bodyFont));
}
if (titleFont != null)
{
root.Query<TextElement>(className: "eve-kind-text-title").ForEach(element =>
element.style.unityFontDefinition = new StyleFontDefinition(titleFont));
}
}
private static TextCoreFontAsset ResolveFont(IReadOnlyDictionary<string, string> tokens, string role)
{
if (tokens == null || string.IsNullOrWhiteSpace(role))
return null;
if (!tokens.TryGetValue($"font.{role}.family", out var family) || string.IsNullOrWhiteSpace(family))
return null;
tokens.TryGetValue($"font.{role}.style", out var style);
style = string.IsNullOrWhiteSpace(style) ? "Regular" : style;
var key = $"{family}\n{style}";
if (FontCache.TryGetValue(key, out var cached))
return cached;
var font = TryCreateFontAsset(family, style)
?? TryCreateFontAsset(family, "Regular")
?? TryCreateFontAsset($"{family} {style}", "Regular");
if (font == null)
{
Debug.LogWarning($"Aetheria Eve UI could not resolve runtime font '{family}' style '{style}'.");
return null;
}
FontCache[key] = font;
return font;
}
private static TextCoreFontAsset TryCreateFontAsset(string family, string style)
{
try
{
return TextCoreFontAsset.CreateFontAsset(
family,
string.IsNullOrWhiteSpace(style) ? "Regular" : style,
90,
9,
GlyphRenderMode.SDFAA);
}
catch
{
return null;
}
}
private static bool ContainsStateRefs(EveSurfaceDocument surface)
{
return surface?.Surface?.Root != null && ContainsStateRefs(surface.Surface.Root);
}
private static bool ContainsStateRefs(EveSurfaceComponent component)
{
if (component.Props != null &&
component.Props.Any(prop =>
!string.IsNullOrWhiteSpace(prop.Value) &&
(string.Equals(prop.Key, AetheriaRuntimeSurfaceStateRefs.Source, StringComparison.Ordinal) ||
prop.Key.EndsWith("Ref", StringComparison.Ordinal))))
{
return true;
}
foreach (var child in component.Children)
{
if (ContainsStateRefs(child))
return true;
}
return false;
}
private static CultMeshStateRefResolver CreateDefaultStateRefResolver()
{
return AetheriaEveRuntimeUnityHooks.TryCreateDefaultStateRefResolver();
}
}
public static class AetheriaEveRuntimeUnityHooks
{
public static Func<string, AetheriaRuntimeStateBootReport> ResolveStateBoot { get; set; }
public static Func<AetheriaRuntimeStateBootReport, string, AetheriaClientState> RuntimeState { get; set; }
public static Func<AetheriaRuntimeStateBootReport, string, AetheriaControl> Control { get; set; }
public static Func<AetheriaRuntimeStateBootReport, string, AetheriaUi> Ui { get; set; }
public static Func<AetheriaRuntimeStateBootReport, string, CultMeshStateRefResolver> StateRefResolver { get; set; }
public static AetheriaRuntimeStateBootReport RequireStateBoot(string stateFilePathOverride)
{
if (ResolveStateBoot == null)
throw new InvalidOperationException("Aetheria Eve runtime hooks are not installed.");
return ResolveStateBoot(stateFilePathOverride ?? "");
}
public static AetheriaClientState RequireRuntimeState(
AetheriaRuntimeStateBootReport stateBoot,
string runtimeId)
{
if (RuntimeState == null)
throw new InvalidOperationException("Aetheria Eve runtime state hook is not installed.");
return RuntimeState(stateBoot, runtimeId ?? "");
}
public static AetheriaControl RequireControl(
AetheriaRuntimeStateBootReport stateBoot,
string runtimeId)
{
if (Control == null)
throw new InvalidOperationException("Aetheria Eve runtime control hook is not installed.");
return Control(stateBoot, runtimeId ?? "");
}
public static AetheriaUi RequireUi(
AetheriaRuntimeStateBootReport stateBoot,
string runtimeId)
{
if (Ui == null)
throw new InvalidOperationException("Aetheria Eve runtime UI hook is not installed.");
return Ui(stateBoot, runtimeId ?? "");
}
public static CultMeshStateRefResolver TryCreateStateRefResolver(
AetheriaRuntimeStateBootReport stateBoot,
string runtimeId)
{
return StateRefResolver?.Invoke(stateBoot, runtimeId ?? "");
}
public static CultMeshStateRefResolver TryCreateDefaultStateRefResolver()
{
if (ResolveStateBoot == null || StateRefResolver == null)
return null;
var stateBoot = ResolveStateBoot("");
if (!stateBoot.SupportsLocalStateFileRead || !stateBoot.StateFileExists)
return null;
return StateRefResolver(stateBoot, "unity-eve-surface-host");
}
}
}