Add shared Unity Eve packages

This commit is contained in:
Meta Vens 2026-06-16 20:16:30 +01:00
commit 39a994a879
10 changed files with 514 additions and 1 deletions

7
.gitignore vendored
View file

@ -1,5 +1,10 @@
.theos/
packages/
packages/*
!packages/.gitkeep
!packages/org.gamecult.eve.surface/
!packages/org.gamecult.eve.surface/**
!packages/org.gamecult.eve.unity-uitoolkit/
!packages/org.gamecult.eve.unity-uitoolkit/**
obj/
artifacts/
tools/deps/

1
packages/.gitkeep Normal file
View file

@ -0,0 +1 @@

View file

@ -0,0 +1,154 @@
using System;
using System.Collections.Generic;
#nullable enable
namespace GameCult.Eve.Surface
{
public sealed class EveSurfaceDocument
{
public EveSurfaceDocument(
string type,
string schema,
string providerId,
string providerKind,
string title,
long version,
string updatedAtUtc,
EveSurfaceTree surface,
IReadOnlyList<EveCommandTemplate> commands)
{
Type = type;
Schema = schema;
ProviderId = providerId;
ProviderKind = providerKind;
Title = title;
Version = version;
UpdatedAtUtc = updatedAtUtc;
Surface = surface;
Commands = commands;
}
public string Type { get; }
public string Schema { get; }
public string ProviderId { get; }
public string ProviderKind { get; }
public string Title { get; }
public long Version { get; }
public string UpdatedAtUtc { get; }
public EveSurfaceTree Surface { get; }
public IReadOnlyList<EveCommandTemplate> Commands { get; }
}
public sealed class EveSurfaceTree
{
public EveSurfaceTree(string id, EveSurfaceComponent root, IReadOnlyList<EveStyleToken> styles)
{
Id = id;
Root = root;
Styles = styles;
}
public string Id { get; }
public EveSurfaceComponent Root { get; }
public IReadOnlyList<EveStyleToken> Styles { get; }
}
public sealed class EveSurfaceComponent
{
public EveSurfaceComponent(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
IReadOnlyList<EveSurfaceComponent> children)
{
Id = id;
Kind = kind;
Props = props;
Children = children;
}
public string Id { get; }
public string Kind { get; }
public IReadOnlyDictionary<string, string> Props { get; }
public IReadOnlyList<EveSurfaceComponent> Children { get; }
public string GetProp(string key, string fallback = "")
{
return Props.TryGetValue(key, out var value) ? value : fallback;
}
}
public sealed class EveStyleToken
{
public EveStyleToken(string name, string value)
{
Name = name;
Value = value;
}
public string Name { get; }
public string Value { get; }
}
public sealed class EveCommandTemplate
{
public EveCommandTemplate(string command, string label, string transport)
{
Command = command;
Label = label;
Transport = transport;
}
public string Command { get; }
public string Label { get; }
public string Transport { get; }
}
public sealed class EveSurfaceCommandRequest
{
public EveSurfaceCommandRequest(
string providerId,
string surfaceId,
string command,
IReadOnlyDictionary<string, string> payload,
DateTimeOffset issuedAt,
string clientId)
{
ProviderId = providerId;
SurfaceId = surfaceId;
Command = command;
Payload = payload;
IssuedAt = issuedAt;
ClientId = clientId;
}
public string ProviderId { get; }
public string SurfaceId { get; }
public string Command { get; }
public IReadOnlyDictionary<string, string> Payload { get; }
public DateTimeOffset IssuedAt { get; }
public string ClientId { get; }
}
}

View file

@ -0,0 +1,14 @@
{
"name": "GameCult.Eve.Surface",
"rootNamespace": "GameCult.Eve.Surface",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": true
}

View file

@ -0,0 +1,15 @@
{
"name": "org.gamecult.eve.surface",
"displayName": "Eve Surface Contract",
"version": "0.1.0",
"unity": "2021.3",
"description": "Unity-compatible DTOs for gamecult.eve.surface.v1 documents.",
"keywords": [
"Eve",
"CultUI",
"CultMesh",
"Surface"
],
"license": "MPL-2.0",
"category": "Scripting"
}

View file

@ -0,0 +1,12 @@
# Eve UI Toolkit Lowering
This package lowers `gamecult.eve.surface.v1` retained surface documents into
Unity UI Toolkit `VisualElement` trees.
It owns native projection only. Providers still own truth, accepted state,
style token values, and command effects through CultMesh/CultNet. Unknown
component kinds degrade to inert containers instead of gaining local semantics.
This package lives in the Eve repository as the shared Unity lowering target.
Aetheria and other Unity consumers should import it from Eve instead of
carrying local copies.

View file

@ -0,0 +1,269 @@
using System;
using System.Collections.Generic;
using GameCult.Eve.Surface;
using UnityEngine;
using UnityEngine.UIElements;
#nullable enable
namespace GameCult.Eve.UnityUIToolkit
{
public sealed class EveUiToolkitSurfaceLowerer
{
private readonly EveUiToolkitSurfaceOptions _options;
public EveUiToolkitSurfaceLowerer(EveUiToolkitSurfaceOptions? options = null)
{
_options = options ?? EveUiToolkitSurfaceOptions.Default;
}
public VisualElement Lower(EveSurfaceDocument document, Action<EveSurfaceCommandRequest>? commandSink = null)
{
if (document == null) throw new ArgumentNullException(nameof(document));
var root = LowerComponent(document.Surface.Root, document, commandSink);
root.name = string.IsNullOrWhiteSpace(root.name) ? document.Surface.Id : root.name;
root.AddToClassList("eve-surface-root");
ApplyStyleTokens(root, document.Surface.Styles);
return root;
}
private VisualElement LowerComponent(
EveSurfaceComponent component,
EveSurfaceDocument document,
Action<EveSurfaceCommandRequest>? commandSink)
{
var element = CreateElement(component, document, commandSink);
element.name = SafeName(component.Id);
element.AddToClassList("eve-component");
element.AddToClassList($"eve-kind-{SafeClass(component.Kind)}");
element.userData = component;
foreach (var child in component.Children)
element.Add(LowerComponent(child, document, commandSink));
return element;
}
private VisualElement CreateElement(
EveSurfaceComponent component,
EveSurfaceDocument document,
Action<EveSurfaceCommandRequest>? commandSink)
{
switch (NormalizeKind(component.Kind))
{
case "surface":
{
var element = new VisualElement();
element.style.flexGrow = 1;
element.style.flexDirection = FlexDirection.Column;
return element;
}
case "grid":
{
var element = new VisualElement();
element.style.flexDirection = FlexDirection.Row;
element.style.flexWrap = Wrap.Wrap;
element.style.alignItems = Align.Stretch;
return element;
}
case "card":
{
var card = new VisualElement();
card.AddToClassList("eve-card");
card.style.flexDirection = FlexDirection.Column;
var title = component.GetProp("title");
if (!string.IsNullOrWhiteSpace(title))
card.Add(TitleLabel(title));
return card;
}
case "metric":
{
var metric = new VisualElement();
metric.AddToClassList("eve-metric");
metric.style.flexDirection = FlexDirection.Column;
metric.Add(MutedLabel(component.GetProp("label")));
metric.Add(ValueLabel(component.GetProp("value")));
return metric;
}
case "inspector.kv":
{
var inspector = new VisualElement();
inspector.AddToClassList("eve-inspector");
inspector.style.flexDirection = FlexDirection.Column;
return inspector;
}
case "row":
{
var row = new VisualElement();
row.AddToClassList("eve-row");
row.style.flexDirection = FlexDirection.Row;
row.style.flexWrap = Wrap.Wrap;
foreach (var prop in component.Props)
row.Add(FieldLabel(prop.Key, prop.Value));
return row;
}
case "text":
{
return BodyLabel(component.GetProp("text", component.GetProp("value", component.GetProp("title"))));
}
case "control.button":
{
var label = component.GetProp("label", component.GetProp("title", "Invoke"));
var command = component.GetProp("command", component.GetProp("action", "invoke"));
return new Button(() => EmitCommand(document, component, command, commandSink)) { text = label };
}
case "control.text":
{
var label = component.GetProp("label", component.GetProp("title", "Value"));
var command = component.GetProp("command", component.GetProp("action", "set"));
var value = component.GetProp("value");
var field = new TextField(label);
field.SetValueWithoutNotify(value);
field.RegisterValueChangedCallback(evt =>
{
if (string.Equals(evt.newValue, value, StringComparison.Ordinal))
return;
var payload = new Dictionary<string, string>(component.Props, StringComparer.Ordinal)
{
["value"] = evt.newValue ?? ""
};
EmitCommand(document, component, command, payload, commandSink);
});
return field;
}
default:
{
var element = new VisualElement();
element.AddToClassList("eve-unknown");
var title = component.GetProp("title", component.GetProp("label"));
if (!string.IsNullOrWhiteSpace(title))
element.Add(BodyLabel(title));
return element;
}
}
}
private static void EmitCommand(
EveSurfaceDocument document,
EveSurfaceComponent component,
string command,
Action<EveSurfaceCommandRequest>? commandSink)
{
EmitCommand(
document,
component,
command,
new Dictionary<string, string>(component.Props, StringComparer.Ordinal),
commandSink);
}
private static void EmitCommand(
EveSurfaceDocument document,
EveSurfaceComponent component,
string command,
IReadOnlyDictionary<string, string> payload,
Action<EveSurfaceCommandRequest>? commandSink)
{
if (commandSink == null || string.IsNullOrWhiteSpace(command))
return;
commandSink(new EveSurfaceCommandRequest(
document.ProviderId,
document.Surface.Id,
command,
payload,
DateTimeOffset.UtcNow,
"unity-uitoolkit"));
}
private static Label TitleLabel(string text)
{
var label = new Label(text);
label.AddToClassList("eve-title");
label.style.unityFontStyleAndWeight = FontStyle.Bold;
return label;
}
private static Label BodyLabel(string text)
{
var label = new Label(text);
label.AddToClassList("eve-text");
return label;
}
private static Label MutedLabel(string text)
{
var label = new Label(text);
label.AddToClassList("eve-muted");
return label;
}
private static Label ValueLabel(string text)
{
var label = new Label(text);
label.AddToClassList("eve-value");
label.style.unityFontStyleAndWeight = FontStyle.Bold;
return label;
}
private static VisualElement FieldLabel(string key, string value)
{
var field = new VisualElement();
field.AddToClassList("eve-field");
field.style.flexDirection = FlexDirection.Row;
field.Add(MutedLabel(key));
field.Add(BodyLabel(value));
return field;
}
private static void ApplyStyleTokens(VisualElement root, IReadOnlyList<EveStyleToken> tokens)
{
foreach (var token in tokens)
{
if (string.IsNullOrWhiteSpace(token.Name) || string.IsNullOrWhiteSpace(token.Value))
continue;
root.AddToClassList($"eve-style-token-{SafeClass(token.Name)}");
}
}
private static string NormalizeKind(string kind)
{
if (string.IsNullOrWhiteSpace(kind))
return "";
if (kind == "panel.dialogue")
return "panel";
if (kind.StartsWith("text.", StringComparison.Ordinal))
return "text";
if (kind.StartsWith("control.button.", StringComparison.Ordinal))
return "control.button";
if (kind.StartsWith("control.text.", StringComparison.Ordinal))
return "control.text";
return kind;
}
private static string SafeName(string value)
{
return string.IsNullOrWhiteSpace(value) ? "eve.component" : value;
}
private static string SafeClass(string value)
{
if (string.IsNullOrWhiteSpace(value))
return "unknown";
var characters = value.ToCharArray();
for (var index = 0; index < characters.Length; index++)
{
if (!char.IsLetterOrDigit(characters[index]))
characters[index] = '-';
}
return new string(characters).Trim('-').ToLowerInvariant();
}
}
}

View file

@ -0,0 +1,9 @@
#nullable enable
namespace GameCult.Eve.UnityUIToolkit
{
public sealed class EveUiToolkitSurfaceOptions
{
public static EveUiToolkitSurfaceOptions Default { get; } = new EveUiToolkitSurfaceOptions();
}
}

View file

@ -0,0 +1,16 @@
{
"name": "GameCult.Eve.UnityUIToolkit",
"rootNamespace": "GameCult.Eve.UnityUIToolkit",
"references": [
"GameCult.Eve.Surface"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View file

@ -0,0 +1,18 @@
{
"name": "org.gamecult.eve.unity-uitoolkit",
"displayName": "Eve UI Toolkit Lowering",
"version": "0.1.0",
"unity": "2021.3",
"description": "Unity UI Toolkit lowering for gamecult.eve.surface.v1 documents.",
"dependencies": {
"org.gamecult.eve.surface": "0.1.0"
},
"keywords": [
"Eve",
"CultUI",
"UI Toolkit",
"Unity"
],
"license": "MPL-2.0",
"category": "Scripting"
}