Own Unity surface and UI Toolkit packages

This commit is contained in:
Meta Vens 2026-07-10 08:09:39 +01:00
commit 13526a9086
47 changed files with 3484 additions and 3 deletions

View file

@ -5,7 +5,7 @@
"com.unity.modules.imageconversion": "1.0.0",
"com.unity.modules.physics": "1.0.0",
"org.gamecult.cultlib": "file:../../../CultLib/artifacts/unity/org.gamecult.cultlib",
"org.gamecult.eve.surface": "file:../../../Eve/packages/org.gamecult.eve.surface",
"org.gamecult.eve.surface": "file:../../packages/org.gamecult.eve.surface",
"org.gamecult.eve.unity-scene": "file:../../packages/org.gamecult.eve.unity-scene"
},
"testables": [

View file

@ -23,7 +23,7 @@
"dependencies": {}
},
"org.gamecult.eve.surface": {
"version": "file:../../../Eve/packages/org.gamecult.eve.surface",
"version": "file:../../packages/org.gamecult.eve.surface",
"depth": 0,
"source": "local",
"dependencies": {

View file

@ -0,0 +1,29 @@
# Eve Surface Contract
This package defines the retained CultUI surface document that every Eve
runtime lowers.
Components own normal child layout through `Children`, live CultMesh value
bindings through `StateBindings`, and nested synced document regions through
`EmbeddedDocuments`. An embedded document slot names the child document id,
schema, presentation kind, and route hint; renderers resolve that child through
CultMesh instead of rebuilding it with runtime-local projection code.
See `../../docs/surface-contract-v1.md` for the full contract and
`../../web/fixtures/cultui-embedded-surface.json` for the parity fixture.
Runtime discovery and required feature coverage live in
`../../tools/parity/parity-manifest.json`; active GUI runtimes must list
`embeddedDocuments` as a supported feature and require the `embedded-surface`
fixture.
Verification entrypoints:
- Browser/TypeScript: `node --test web\eve-dsl.test.mjs`
- Semantic runtime matrix: `powershell -ExecutionPolicy Bypass -File .\scripts\run-parity-harness.ps1`
- Flutter GUI contract, when Flutter is installed: `flutter test --plain-name embedded_surface_fixture_contract`
- Rust/CultMesh document sync: run `cargo test rust_preserves_cultui_embedded_surface_slots_through_typed_document_sync` from `E:/Projects/CultLib/packages/cultnet-rs`
The parity manifest is the discovery source for web, Flutter desktop/Linux/Android,
iOS/UIKit, Android/Kotlin, Unity UI Toolkit, and Rust contract coverage. Do not
add a new renderer claim without adding its nested-surface fixture requirement
there.

View file

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

View file

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

View file

@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using GameCult.Caching;
using MessagePack;
#nullable enable
namespace GameCult.Eve.Surface
{
[CultDocument("gamecult.eve.asset_catalog", SchemaId)]
[MessagePackObject]
public sealed class EveAssetCatalogDocument
{
public const string SchemaId = "gamecult.eve.asset_catalog.v1";
[SerializationConstructor]
public EveAssetCatalogDocument(
string schema,
string providerId,
string catalogId,
long version,
string updatedAtUtc,
EveAssetCatalogEntry[] assets)
{
Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema;
ProviderId = providerId ?? "";
CatalogId = catalogId ?? "";
Version = version;
UpdatedAtUtc = updatedAtUtc ?? "";
Assets = assets ?? Array.Empty<EveAssetCatalogEntry>();
}
public EveAssetCatalogDocument(
string providerId,
string catalogId,
long version,
string updatedAtUtc,
EveAssetCatalogEntry[] assets)
: this(SchemaId, providerId, catalogId, version, updatedAtUtc, assets)
{
}
[Key(0)] public string Schema { get; }
[Key(1)] public string ProviderId { get; }
[Key(2)] public string CatalogId { get; }
[Key(3)] public long Version { get; }
[Key(4)] public string UpdatedAtUtc { get; }
[Key(5)] public EveAssetCatalogEntry[] Assets { get; }
}
[MessagePackObject]
public sealed class EveAssetCatalogEntry
{
[SerializationConstructor]
public EveAssetCatalogEntry(
string assetRef,
string assetKind,
EveAssetVariant[] variants,
Dictionary<string, string> metadata)
{
AssetRef = assetRef ?? "";
AssetKind = assetKind ?? "";
Variants = variants ?? Array.Empty<EveAssetVariant>();
Metadata = metadata == null
? new Dictionary<string, string>(StringComparer.Ordinal)
: new Dictionary<string, string>(metadata, StringComparer.Ordinal);
}
public EveAssetCatalogEntry(
string assetRef,
string assetKind,
EveAssetVariant[] variants,
IReadOnlyDictionary<string, string>? metadata = null)
: this(
assetRef,
assetKind,
variants,
metadata == null ? new Dictionary<string, string>() : new Dictionary<string, string>(metadata))
{
}
[Key(0)] public string AssetRef { get; }
[Key(1)] public string AssetKind { get; }
[Key(2)] public EveAssetVariant[] Variants { get; }
[Key(3)] public Dictionary<string, string> Metadata { get; }
}
[MessagePackObject]
public sealed class EveAssetVariant
{
[SerializationConstructor]
public EveAssetVariant(
string runtimeId,
string platform,
string format,
string uri,
string contentHash,
long sizeBytes,
string assetKey,
Dictionary<string, string> metadata)
{
RuntimeId = runtimeId ?? "";
Platform = platform ?? "";
Format = format ?? "";
Uri = uri ?? "";
ContentHash = contentHash ?? "";
SizeBytes = sizeBytes;
AssetKey = assetKey ?? "";
Metadata = metadata == null
? new Dictionary<string, string>(StringComparer.Ordinal)
: new Dictionary<string, string>(metadata, StringComparer.Ordinal);
}
public EveAssetVariant(
string runtimeId,
string platform,
string format,
string uri,
string contentHash,
long sizeBytes,
string assetKey,
IReadOnlyDictionary<string, string>? metadata = null)
: this(
runtimeId,
platform,
format,
uri,
contentHash,
sizeBytes,
assetKey,
metadata == null ? new Dictionary<string, string>() : new Dictionary<string, string>(metadata))
{
}
[Key(0)] public string RuntimeId { get; }
[Key(1)] public string Platform { get; }
[Key(2)] public string Format { get; }
[Key(3)] public string Uri { get; }
[Key(4)] public string ContentHash { get; }
[Key(5)] public long SizeBytes { get; }
[Key(6)] public string AssetKey { get; }
[Key(7)] public Dictionary<string, string> Metadata { get; }
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8f53654f7c98d894588127ba001f11b4

View file

@ -0,0 +1,261 @@
using System;
using System.Collections.Generic;
using GameCult.Caching;
using MessagePack;
#nullable enable
namespace GameCult.Eve.Surface
{
[CultDocument("gamecult.eve.provider_advertisement", SchemaId)]
[MessagePackObject]
public sealed class EveProviderAdvertisementDocument
{
public const string SchemaId = "gamecult.eve.provider_advertisement.v1";
public EveProviderAdvertisementDocument(
string providerId,
string serviceId,
string verseId,
string title,
string kind,
string cultMeshAddress,
string updatedAtUtc,
EveProviderFreshness freshness,
IReadOnlyList<string> schemas,
IReadOnlyList<EveProviderWitness> witnesses,
IReadOnlyList<EveAdvertisedSurface> surfaces,
IReadOnlyList<EveAdvertisedCommand> commands)
: this(SchemaId, providerId, serviceId, verseId, title, kind, cultMeshAddress, updatedAtUtc, freshness, schemas, witnesses, surfaces, commands)
{
}
[SerializationConstructor]
public EveProviderAdvertisementDocument(
string schema,
string providerId,
string serviceId,
string verseId,
string title,
string kind,
string cultMeshAddress,
string updatedAtUtc,
EveProviderFreshness freshness,
IReadOnlyList<string> schemas,
IReadOnlyList<EveProviderWitness> witnesses,
IReadOnlyList<EveAdvertisedSurface> surfaces,
IReadOnlyList<EveAdvertisedCommand> commands)
{
Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema;
ProviderId = providerId ?? "";
ServiceId = serviceId ?? "";
VerseId = verseId ?? "";
Title = title ?? "";
Kind = kind ?? "";
CultMeshAddress = cultMeshAddress ?? "";
UpdatedAtUtc = updatedAtUtc ?? "";
Freshness = freshness ?? new EveProviderFreshness("unknown", "", 0);
Schemas = schemas ?? Array.Empty<string>();
Witnesses = witnesses ?? Array.Empty<EveProviderWitness>();
Surfaces = surfaces ?? Array.Empty<EveAdvertisedSurface>();
Commands = commands ?? Array.Empty<EveAdvertisedCommand>();
}
[Key(0)] public string Schema { get; }
[Key(1)] public string ProviderId { get; }
[Key(2)] public string ServiceId { get; }
[Key(3)] public string VerseId { get; }
[Key(4)] public string Title { get; }
[Key(5)] public string Kind { get; }
[Key(6)] public string CultMeshAddress { get; }
[Key(7)] public string UpdatedAtUtc { get; }
[Key(8)] public EveProviderFreshness Freshness { get; }
[Key(9)] public IReadOnlyList<string> Schemas { get; }
[Key(10)] public IReadOnlyList<EveProviderWitness> Witnesses { get; }
[Key(11)] public IReadOnlyList<EveAdvertisedSurface> Surfaces { get; }
[Key(12)] public IReadOnlyList<EveAdvertisedCommand> Commands { get; }
}
[MessagePackObject]
public sealed class EveProviderFreshness
{
[SerializationConstructor]
public EveProviderFreshness(string state, string lastSeenAtUtc, long maxAgeMs)
{
State = state ?? "";
LastSeenAtUtc = lastSeenAtUtc ?? "";
MaxAgeMs = maxAgeMs;
}
[Key(0)] public string State { get; }
[Key(1)] public string LastSeenAtUtc { get; }
[Key(2)] public long MaxAgeMs { get; }
}
[MessagePackObject]
public sealed class EveProviderWitness
{
[SerializationConstructor]
public EveProviderWitness(string kind, string reference, string summary)
{
Kind = kind ?? "";
Reference = reference ?? "";
Summary = summary ?? "";
}
[Key(0)] public string Kind { get; }
[Key(1)] public string Reference { get; }
[Key(2)] public string Summary { get; }
}
[MessagePackObject]
public sealed class EveAdvertisedSurface
{
[SerializationConstructor]
public EveAdvertisedSurface(
string surfaceId,
string schema,
string recordRef,
string transport,
string status,
string surfaceKind,
EveWorldInteractionAdvertisement? worldInteraction = null)
{
SurfaceId = surfaceId ?? "";
Schema = schema ?? "";
RecordRef = recordRef ?? "";
Transport = transport ?? "";
Status = status ?? "";
SurfaceKind = surfaceKind ?? "";
WorldInteraction = worldInteraction;
}
[Key(0)] public string SurfaceId { get; }
[Key(1)] public string Schema { get; }
[Key(2)] public string RecordRef { get; }
[Key(3)] public string Transport { get; }
[Key(4)] public string Status { get; }
[Key(5)] public string SurfaceKind { get; }
[Key(6)] public EveWorldInteractionAdvertisement? WorldInteraction { get; }
}
[MessagePackObject]
public sealed class EveWorldInteractionAdvertisement
{
[SerializationConstructor]
public EveWorldInteractionAdvertisement(
string projectionKind,
IReadOnlyList<string> stateSchemas,
string commandBoundary,
string commandRecordRef,
string receiptSchema,
string receiptRecordRef,
string assetManifestRecordRef,
IReadOnlyList<string> loweringTargets,
string ownership)
{
ProjectionKind = projectionKind ?? "";
StateSchemas = stateSchemas ?? Array.Empty<string>();
CommandBoundary = commandBoundary ?? "";
CommandRecordRef = commandRecordRef ?? "";
ReceiptSchema = receiptSchema ?? "";
ReceiptRecordRef = receiptRecordRef ?? "";
AssetManifestRecordRef = assetManifestRecordRef ?? "";
LoweringTargets = loweringTargets ?? Array.Empty<string>();
Ownership = ownership ?? "";
}
[Key(0)] public string ProjectionKind { get; }
[Key(1)] public IReadOnlyList<string> StateSchemas { get; }
[Key(2)] public string CommandBoundary { get; }
[Key(3)] public string CommandRecordRef { get; }
[Key(4)] public string ReceiptSchema { get; }
[Key(5)] public string ReceiptRecordRef { get; }
[Key(6)] public string AssetManifestRecordRef { get; }
[Key(7)] public IReadOnlyList<string> LoweringTargets { get; }
[Key(8)] public string Ownership { get; }
}
[MessagePackObject]
public sealed class EveAdvertisedCommand
{
[SerializationConstructor]
public EveAdvertisedCommand(string command, string surfaceId, string transport, string summary)
{
Command = command ?? "";
SurfaceId = surfaceId ?? "";
Transport = transport ?? "";
Summary = summary ?? "";
}
[Key(0)] public string Command { get; }
[Key(1)] public string SurfaceId { get; }
[Key(2)] public string Transport { get; }
[Key(3)] public string Summary { get; }
}
[CultDocument("gamecult.eve.command_receipt", SchemaId)]
[MessagePackObject]
public sealed class EveCommandReceiptDocument
{
public const string SchemaId = "gamecult.eve.command_receipt.v1";
public EveCommandReceiptDocument(
string receiptId,
string commandId,
string command,
string state,
string ownerRepo,
string authority,
string providerId,
string surfaceId,
string message,
string issuedAtUtc,
long sourceVersion)
: this(SchemaId, receiptId, commandId, command, state, ownerRepo, authority, providerId, surfaceId, message, issuedAtUtc, sourceVersion)
{
}
[SerializationConstructor]
public EveCommandReceiptDocument(
string schema,
string receiptId,
string commandId,
string command,
string state,
string ownerRepo,
string authority,
string providerId,
string surfaceId,
string message,
string issuedAtUtc,
long sourceVersion)
{
Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema;
ReceiptId = receiptId ?? "";
CommandId = commandId ?? "";
Command = command ?? "";
State = state ?? "";
OwnerRepo = ownerRepo ?? "";
Authority = authority ?? "";
ProviderId = providerId ?? "";
SurfaceId = surfaceId ?? "";
Message = message ?? "";
IssuedAtUtc = issuedAtUtc ?? "";
SourceVersion = sourceVersion;
}
[Key(0)] public string Schema { get; }
[Key(1)] public string ReceiptId { get; }
[Key(2)] public string CommandId { get; }
[Key(3)] public string Command { get; }
[Key(4)] public string State { get; }
[Key(5)] public string OwnerRepo { get; }
[Key(6)] public string Authority { get; }
[Key(7)] public string ProviderId { get; }
[Key(8)] public string SurfaceId { get; }
[Key(9)] public string Message { get; }
[Key(10)] public string IssuedAtUtc { get; }
[Key(11)] public long SourceVersion { get; }
}
}

View file

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

View file

@ -0,0 +1,874 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GameCult.Mesh;
#nullable enable
namespace GameCult.Eve.Surface
{
/// <summary>
/// Fluent CultUI/Eve surface builder. Eve owns the surface document; CultMesh only supplies typed binding descriptors.
/// </summary>
public sealed class EveSurfaceBuilder
{
private readonly string _surfaceId;
private readonly List<EveSurfaceComponent> _children = new();
private readonly List<EveStyleToken> _styles = new();
private readonly List<EveCommandTemplate> _commands = new();
private EveSurfaceComponent? _root;
private long _version = 1;
private string _providerId = "gamecult";
private string _providerKind = "cultui.surface";
private string _title = "";
private string _updatedAtUtc = "";
public EveSurfaceBuilder(string surfaceId)
{
_surfaceId = RequireNonEmpty(surfaceId, nameof(surfaceId));
}
public EveSurfaceBuilder Provider(string providerId, string providerKind)
{
_providerId = providerId ?? "";
_providerKind = providerKind ?? "";
return this;
}
public EveSurfaceBuilder Title(string title)
{
_title = title ?? "";
_children.Add(Text($"{_surfaceId}.title", _title, "text.title"));
return this;
}
public EveSurfaceBuilder TitleSubtitle(string title, string subtitle)
{
_title = string.IsNullOrWhiteSpace(subtitle) ? title ?? "" : $"{title} {subtitle}";
_children.Add(Text($"{_surfaceId}.title", title ?? "", "text.title"));
_children.Add(Text($"{_surfaceId}.subtitle", subtitle ?? "", "text.subtitle"));
return this;
}
public EveSurfaceBuilder Text(string text, string? id = null)
{
_children.Add(Text(id ?? $"{_surfaceId}.text.{_children.Count}", text, "text"));
return this;
}
public EveSurfaceBuilder Button(string label, string command)
{
var operation = CultMesh.OperationBinding(command, label);
_commands.Add(new EveCommandTemplate(operation));
_children.Add(ButtonComponent($"{_surfaceId}.button.{Slug(label)}", label, operation));
return this;
}
public EveSurfaceBuilder Button(string label, CultMeshOperationBindingDescriptor operation)
{
if (operation == null) throw new ArgumentNullException(nameof(operation));
_commands.Add(new EveCommandTemplate(operation));
_children.Add(ButtonComponent($"{_surfaceId}.button.{Slug(label)}", label, operation));
return this;
}
public EveSurfaceBuilder ButtonColumn(string id, Action<EveSurfaceGroupBuilder> build)
{
return Group(id, "column", build);
}
public EveSurfaceBuilder ButtonRow(string id, Action<EveSurfaceGroupBuilder> build)
{
return Group(id, "row", build);
}
public EveSurfaceBuilder Form(string id, Action<EveSurfaceFormBuilder> build)
{
if (build == null) throw new ArgumentNullException(nameof(build));
var form = new EveSurfaceFormBuilder(id, AddCommand);
build(form);
_children.Add(form.Build());
return this;
}
public EveSurfaceBuilder RootColumn(string id, Action<EveSurfaceContainerBuilder> build)
{
_root = BuildContainer(id, "partition", Props(("split", "y")), build);
return this;
}
public EveSurfaceBuilder RootRow(string id, Action<EveSurfaceContainerBuilder> build)
{
_root = BuildContainer(id, "partition", Props(("split", "x")), build);
return this;
}
public EveSurfaceBuilder RootGrid(
string id,
int columns,
int rows,
Action<EveSurfaceContainerBuilder> build)
{
_root = BuildContainer(
id,
"grid",
Props(
("columns", columns.ToString(System.Globalization.CultureInfo.InvariantCulture)),
("rows", rows.ToString(System.Globalization.CultureInfo.InvariantCulture))),
build);
return this;
}
public EveSurfaceBuilder Row(string id, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "partition", Props(("split", "x")), build));
return this;
}
public EveSurfaceBuilder Column(string id, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "partition", Props(("split", "y")), build));
return this;
}
public EveSurfaceBuilder Grid(string id, int columns, int rows, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(
id,
"grid",
Props(
("columns", columns.ToString(System.Globalization.CultureInfo.InvariantCulture)),
("rows", rows.ToString(System.Globalization.CultureInfo.InvariantCulture))),
build));
return this;
}
public EveSurfaceBuilder Modal(string id, string title, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "modal", Props(("title", title ?? "")), build));
return this;
}
public EveSurfaceBuilder EmbeddedDocument(
string slotId,
string documentId,
string schemaId,
string presentationKind,
CultMeshRouteHint? routeHint = null)
{
_children.Add(new EveSurfaceComponent(
$"{_surfaceId}.slot.{Slug(slotId)}",
"surface.slot",
new Dictionary<string, string>(StringComparer.Ordinal)
{
["slotId"] = slotId ?? "",
["documentId"] = documentId ?? "",
["schemaId"] = schemaId ?? "",
["presentationKind"] = presentationKind ?? ""
},
Array.Empty<EveSurfaceComponent>(),
Array.Empty<CultMeshStateBindingDescriptor>(),
new[]
{
new EveEmbeddedDocumentSlot(
slotId ?? "",
documentId ?? "",
schemaId ?? "",
presentationKind ?? "",
routeHint)
}));
return this;
}
public EveSurfaceBuilder Style(string name, string value)
{
_styles.Add(new EveStyleToken(name, value));
return this;
}
public EveSurfaceBuilder Version(long version)
{
_version = version;
return this;
}
public EveSurfaceBuilder UpdatedAtUtc(string updatedAtUtc)
{
_updatedAtUtc = updatedAtUtc ?? "";
return this;
}
public EveSurfaceDocument Build()
{
return new EveSurfaceDocument(
_providerId,
_providerKind,
_title,
_version,
string.IsNullOrWhiteSpace(_updatedAtUtc) ? DateTime.UtcNow.ToString("O") : _updatedAtUtc,
new EveSurfaceTree(
_surfaceId,
_root ?? new EveSurfaceComponent(
$"{_surfaceId}.root",
"surface",
EmptyProps(),
_children.ToArray()),
_styles.ToArray()),
_commands.ToArray());
}
private EveSurfaceBuilder Group(string id, string kind, Action<EveSurfaceGroupBuilder> build)
{
if (build == null) throw new ArgumentNullException(nameof(build));
var group = new EveSurfaceGroupBuilder(id, kind, AddCommand);
build(group);
_children.Add(group.Build());
return this;
}
private EveSurfaceComponent BuildContainer(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
Action<EveSurfaceContainerBuilder> build)
{
if (build == null) throw new ArgumentNullException(nameof(build));
var container = new EveSurfaceContainerBuilder(id, kind, props, AddCommand);
build(container);
return container.Build();
}
private void AddCommand(EveCommandTemplate command)
{
if (command != null)
_commands.Add(command);
}
internal static EveSurfaceComponent Text(string id, string value, string kind)
{
return new EveSurfaceComponent(
id,
kind,
new Dictionary<string, string>(StringComparer.Ordinal)
{
["value"] = value ?? ""
},
Array.Empty<EveSurfaceComponent>());
}
internal static EveSurfaceComponent ButtonComponent(
string id,
string label,
CultMeshOperationBindingDescriptor operation)
{
return new EveSurfaceComponent(
id,
"control.button",
new Dictionary<string, string>(StringComparer.Ordinal)
{
["label"] = label ?? operation.Label,
["command"] = operation.OperationId,
["operationId"] = operation.OperationId,
["schemaId"] = operation.SchemaId
},
Array.Empty<EveSurfaceComponent>());
}
internal static Dictionary<string, string> EmptyProps()
{
return new Dictionary<string, string>(StringComparer.Ordinal);
}
internal static Dictionary<string, string> Props(params (string Key, string Value)[] values)
{
return (values ?? Array.Empty<(string Key, string Value)>())
.ToDictionary(value => value.Key, value => value.Value ?? "", StringComparer.Ordinal);
}
internal static string Slug(string? value)
{
if (string.IsNullOrWhiteSpace(value))
return "unnamed";
var chars = value
.Select(character => char.IsLetterOrDigit(character) ? char.ToLowerInvariant(character) : '.')
.ToArray();
var slug = new string(chars).Trim('.');
while (slug.Contains("..", StringComparison.Ordinal))
slug = slug.Replace("..", ".", StringComparison.Ordinal);
return string.IsNullOrWhiteSpace(slug) ? "unnamed" : slug;
}
private static string RequireNonEmpty(string value, string paramName)
{
return string.IsNullOrWhiteSpace(value)
? throw new ArgumentException("Value must be non-empty.", paramName)
: value;
}
}
public sealed class EveSurfaceContainerBuilder
{
private readonly string _id;
private readonly string _kind;
private readonly Dictionary<string, string> _props;
private readonly Action<EveCommandTemplate> _addCommand;
private readonly List<EveSurfaceComponent> _children = new();
private readonly List<CultMeshStateBindingDescriptor> _stateBindings = new();
private readonly List<EveEmbeddedDocumentSlot> _embeddedDocuments = new();
internal EveSurfaceContainerBuilder(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
Action<EveCommandTemplate> addCommand)
{
_id = id ?? "";
_kind = kind ?? "partition";
_props = new Dictionary<string, string>(props ?? EveSurfaceBuilder.EmptyProps(), StringComparer.Ordinal);
_addCommand = addCommand ?? throw new ArgumentNullException(nameof(addCommand));
}
public EveSurfaceContainerBuilder Gap(int value) => Prop("gap", value);
public EveSurfaceContainerBuilder Padding(int value) => Prop("padding", value);
public EveSurfaceContainerBuilder Size(string value) => Prop("size", value);
public EveSurfaceContainerBuilder Min(string value) => Prop("min", value);
public EveSurfaceContainerBuilder Max(string value) => Prop("max", value);
public EveSurfaceContainerBuilder Align(string value) => Prop("align", value);
public EveSurfaceContainerBuilder Clip(string value = "hidden") => Prop("clip", value);
public EveSurfaceContainerBuilder Scroll(string value = "y") => Prop("scroll", value);
public EveSurfaceContainerBuilder Region(string value) => Prop("region", value);
public EveSurfaceContainerBuilder Bind(CultMeshStateBindingDescriptor binding)
{
if (binding != null)
_stateBindings.Add(binding);
return this;
}
public EveSurfaceContainerBuilder Row(string id, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "partition", EveSurfaceBuilder.Props(("split", "x")), build));
return this;
}
public EveSurfaceContainerBuilder Column(string id, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "partition", EveSurfaceBuilder.Props(("split", "y")), build));
return this;
}
public EveSurfaceContainerBuilder Grid(
string id,
int columns,
int rows,
Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(
id,
"grid",
EveSurfaceBuilder.Props(
("columns", columns.ToString(System.Globalization.CultureInfo.InvariantCulture)),
("rows", rows.ToString(System.Globalization.CultureInfo.InvariantCulture))),
build));
return this;
}
public EveSurfaceContainerBuilder Pane(string id, string title, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "pane", EveSurfaceBuilder.Props(("title", title ?? "")), build));
return this;
}
public EveSurfaceContainerBuilder Card(string id, string title, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "card", EveSurfaceBuilder.Props(("title", title ?? "")), build));
return this;
}
public EveSurfaceContainerBuilder Modal(string id, string title, Action<EveSurfaceContainerBuilder> build)
{
_children.Add(BuildContainer(id, "modal", EveSurfaceBuilder.Props(("title", title ?? "")), build));
return this;
}
public EveSurfaceContainerBuilder FieldRow(
string id,
string label,
Action<EveSurfaceContainerBuilder> build)
{
return Row(id, row => row
.Gap(8)
.Label($"{id}.label", label, labelPart => labelPart.Size("12rem"))
.Column($"{id}.field", field =>
{
field.Size("1fr");
build(field);
}));
}
public EveSurfaceContainerBuilder Label(
string id,
string text,
Action<EveSurfaceContainerBuilder>? configure = null)
{
return Leaf(id, "label", EveSurfaceBuilder.Props(("value", text ?? "")), null, null, configure);
}
public EveSurfaceContainerBuilder Text(string id, string text)
{
return Leaf(id, "text", EveSurfaceBuilder.Props(("value", text ?? "")));
}
public EveSurfaceContainerBuilder Title(string id, string text)
{
return Leaf(id, "text.title", EveSurfaceBuilder.Props(("value", text ?? "")));
}
public EveSurfaceContainerBuilder Metric(
string id,
string label,
string value,
CultMeshStateBindingDescriptor? binding = null)
{
return Leaf(
id,
"metric",
EveSurfaceBuilder.Props(("label", label ?? ""), ("value", value ?? "")),
binding);
}
public EveSurfaceContainerBuilder Progress(
string id,
string label,
string value,
string min,
string max,
CultMeshStateBindingDescriptor? binding = null)
{
return Leaf(
id,
"progress",
EveSurfaceBuilder.Props(
("label", label ?? ""),
("value", value ?? ""),
("min", min ?? ""),
("max", max ?? "")),
binding);
}
public EveSurfaceContainerBuilder Button(string id, string label, string command)
{
return Button(id, label, CultMesh.OperationBinding(command, label));
}
public EveSurfaceContainerBuilder Button(
string id,
string label,
CultMeshOperationBindingDescriptor operation)
{
if (operation == null) throw new ArgumentNullException(nameof(operation));
_addCommand(new EveCommandTemplate(operation));
_children.Add(EveSurfaceBuilder.ButtonComponent(id, label, operation));
return this;
}
public EveSurfaceContainerBuilder PopupButton(
string id,
string label,
string title,
Action<EveSurfaceContainerBuilder> build,
string anchor = "viewport",
string placement = "center",
int offsetX = 0,
int offsetY = 0)
{
if (build == null) throw new ArgumentNullException(nameof(build));
var popup = BuildContainer(
$"{id}.popup",
"modal",
EveSurfaceBuilder.Props(
("title", title ?? label ?? ""),
("anchor", anchor ?? "viewport"),
("placement", placement ?? "center"),
("offsetX", offsetX.ToString(System.Globalization.CultureInfo.InvariantCulture)),
("offsetY", offsetY.ToString(System.Globalization.CultureInfo.InvariantCulture)),
("presentation", "popover")),
build);
_children.Add(new EveSurfaceComponent(
id ?? "",
"control.popup",
EveSurfaceBuilder.Props(
("label", label ?? ""),
("title", title ?? label ?? ""),
("anchor", anchor ?? "viewport"),
("placement", placement ?? "center"),
("offsetX", offsetX.ToString(System.Globalization.CultureInfo.InvariantCulture)),
("offsetY", offsetY.ToString(System.Globalization.CultureInfo.InvariantCulture))),
new[] { popup }));
return this;
}
public EveSurfaceContainerBuilder EnumField(
string id,
string label,
string value,
CultMeshStateBindingDescriptor optionsBinding,
CultMeshOperationBindingDescriptor setValue,
string anchor = "trigger",
string placement = "right-down")
{
return FieldRow(id, label, field => field.PopupButton(
$"{id}.dropdown",
value,
label,
popup => popup.OptionList($"{id}.options", label, optionsBinding, setValue),
anchor,
placement));
}
public EveSurfaceContainerBuilder OptionList(
string id,
string label,
CultMeshStateBindingDescriptor itemsBinding,
CultMeshOperationBindingDescriptor operation)
{
if (itemsBinding == null) throw new ArgumentNullException(nameof(itemsBinding));
if (operation == null) throw new ArgumentNullException(nameof(operation));
_addCommand(new EveCommandTemplate(operation));
_children.Add(new EveSurfaceComponent(
id ?? "",
"options",
EveSurfaceBuilder.Props(
("label", label ?? ""),
("command", operation.OperationId),
("operationId", operation.OperationId),
("schemaId", operation.SchemaId)),
Array.Empty<EveSurfaceComponent>(),
new[] { itemsBinding }));
return this;
}
public EveSurfaceContainerBuilder TextInput(
string id,
string label,
string value,
CultMeshOperationBindingDescriptor operation,
CultMeshStateBindingDescriptor? binding = null)
{
return Control("control.input.text", id, label, value, operation, binding);
}
public EveSurfaceContainerBuilder Toggle(
string id,
string label,
bool value,
CultMeshOperationBindingDescriptor operation,
CultMeshStateBindingDescriptor? binding = null)
{
return Control("control.toggle", id, label, value ? "true" : "false", operation, binding);
}
public EveSurfaceContainerBuilder Slider(
string id,
string label,
string value,
string min,
string max,
string step,
CultMeshOperationBindingDescriptor operation,
CultMeshStateBindingDescriptor? binding = null)
{
return Control(
"control.slider",
id,
label,
value,
operation,
binding,
("min", min ?? ""),
("max", max ?? ""),
("step", step ?? ""));
}
public EveSurfaceContainerBuilder Select(
string id,
string label,
string value,
CultMeshOperationBindingDescriptor operation,
CultMeshStateBindingDescriptor? binding = null)
{
return Control("control.select", id, label, value, operation, binding);
}
public EveSurfaceContainerBuilder EmbeddedDocument(
string id,
string slotId,
string documentId,
string schemaId,
string presentationKind,
CultMeshRouteHint? routeHint = null)
{
_children.Add(new EveSurfaceComponent(
id ?? "",
"surface.slot",
EveSurfaceBuilder.Props(
("slotId", slotId ?? ""),
("documentId", documentId ?? ""),
("schemaId", schemaId ?? ""),
("presentationKind", presentationKind ?? "")),
Array.Empty<EveSurfaceComponent>(),
Array.Empty<CultMeshStateBindingDescriptor>(),
new[]
{
new EveEmbeddedDocumentSlot(
slotId ?? "",
documentId ?? "",
schemaId ?? "",
presentationKind ?? "",
routeHint)
}));
return this;
}
internal EveSurfaceComponent Build()
{
return new EveSurfaceComponent(
_id,
_kind,
_props,
_children.ToArray(),
_stateBindings.ToArray(),
_embeddedDocuments.ToArray());
}
private EveSurfaceContainerBuilder Prop(string key, int value)
{
_props[key] = value.ToString(System.Globalization.CultureInfo.InvariantCulture);
return this;
}
private EveSurfaceContainerBuilder Prop(string key, string value)
{
_props[key] = value ?? "";
return this;
}
private EveSurfaceContainerBuilder Leaf(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
CultMeshStateBindingDescriptor? binding = null,
IReadOnlyList<EveSurfaceComponent>? children = null,
Action<EveSurfaceContainerBuilder>? configure = null)
{
var builder = new EveSurfaceContainerBuilder(id, kind, props, _addCommand);
configure?.Invoke(builder);
var component = new EveSurfaceComponent(
id ?? "",
kind ?? "",
new Dictionary<string, string>(props ?? EveSurfaceBuilder.EmptyProps(), StringComparer.Ordinal),
children ?? Array.Empty<EveSurfaceComponent>(),
binding == null ? Array.Empty<CultMeshStateBindingDescriptor>() : new[] { binding });
if (builder._props.Count != (props?.Count ?? 0))
{
component = new EveSurfaceComponent(
id ?? "",
kind ?? "",
new Dictionary<string, string>(builder._props, StringComparer.Ordinal),
children ?? Array.Empty<EveSurfaceComponent>(),
binding == null ? Array.Empty<CultMeshStateBindingDescriptor>() : new[] { binding });
}
_children.Add(component);
return this;
}
private EveSurfaceContainerBuilder Control(
string kind,
string id,
string label,
string value,
CultMeshOperationBindingDescriptor operation,
CultMeshStateBindingDescriptor? binding,
params (string Key, string Value)[] extraProps)
{
if (operation == null) throw new ArgumentNullException(nameof(operation));
_addCommand(new EveCommandTemplate(operation));
var props = EveSurfaceBuilder.Props(
new[]
{
("label", label ?? ""),
("value", value ?? ""),
("command", operation.OperationId),
("operationId", operation.OperationId),
("schemaId", operation.SchemaId)
}.Concat(extraProps ?? Array.Empty<(string Key, string Value)>()).ToArray());
return Leaf(kind: kind, id: id, props: props, binding: binding);
}
private EveSurfaceComponent BuildContainer(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
Action<EveSurfaceContainerBuilder> build)
{
if (build == null) throw new ArgumentNullException(nameof(build));
var container = new EveSurfaceContainerBuilder(id, kind, props, _addCommand);
build(container);
return container.Build();
}
}
public sealed class EveSurfaceGroupBuilder
{
private readonly string _id;
private readonly string _kind;
private readonly Action<EveCommandTemplate> _addCommand;
private readonly List<EveSurfaceComponent> _children = new();
internal EveSurfaceGroupBuilder(
string id,
string kind,
Action<EveCommandTemplate> addCommand)
{
_id = id ?? "";
_kind = kind ?? "column";
_addCommand = addCommand ?? throw new ArgumentNullException(nameof(addCommand));
}
public EveSurfaceGroupBuilder Button(string label, string command)
{
var operation = CultMesh.OperationBinding(command, label);
_addCommand(new EveCommandTemplate(operation));
_children.Add(EveSurfaceBuilder.ButtonComponent($"{_id}.button.{EveSurfaceBuilder.Slug(label)}", label, operation));
return this;
}
public EveSurfaceGroupBuilder Button(string label, CultMeshOperationBindingDescriptor operation)
{
if (operation == null) throw new ArgumentNullException(nameof(operation));
_addCommand(new EveCommandTemplate(operation));
_children.Add(EveSurfaceBuilder.ButtonComponent($"{_id}.button.{EveSurfaceBuilder.Slug(label)}", label, operation));
return this;
}
internal EveSurfaceComponent Build()
{
return new EveSurfaceComponent(_id, _kind, EveSurfaceBuilder.EmptyProps(), _children.ToArray());
}
}
public sealed class EveSurfaceFormBuilder
{
private readonly string _id;
private readonly Action<EveCommandTemplate> _addCommand;
private readonly List<EveSurfaceComponent> _children = new();
internal EveSurfaceFormBuilder(string id, Action<EveCommandTemplate> addCommand)
{
_id = id ?? "";
_addCommand = addCommand ?? throw new ArgumentNullException(nameof(addCommand));
}
public EveSurfaceFormBuilder Text(
string label,
string value,
CultMeshOperationBindingDescriptor operation,
CultMeshStateBindingDescriptor? binding = null)
{
if (operation == null) throw new ArgumentNullException(nameof(operation));
_addCommand(new EveCommandTemplate(operation));
_children.Add(Control(
"control.text",
label,
value,
operation,
binding));
return this;
}
public EveSurfaceFormBuilder Toggle(
string label,
bool value,
CultMeshOperationBindingDescriptor operation,
CultMeshStateBindingDescriptor? binding = null)
{
if (operation == null) throw new ArgumentNullException(nameof(operation));
_addCommand(new EveCommandTemplate(operation));
_children.Add(Control(
"control.toggle",
label,
value ? "true" : "false",
operation,
binding));
return this;
}
public EveSurfaceFormBuilder Metric(
string label,
string value,
CultMeshStateBindingDescriptor? binding = null)
{
var bindings = binding == null
? Array.Empty<CultMeshStateBindingDescriptor>()
: new[] { binding };
_children.Add(new EveSurfaceComponent(
$"{_id}.metric.{EveSurfaceBuilder.Slug(label)}",
"metric",
new Dictionary<string, string>(StringComparer.Ordinal)
{
["label"] = label ?? "",
["value"] = value ?? ""
},
Array.Empty<EveSurfaceComponent>(),
bindings));
return this;
}
internal EveSurfaceComponent Build()
{
return new EveSurfaceComponent(_id, "form", EveSurfaceBuilder.EmptyProps(), _children.ToArray());
}
private EveSurfaceComponent Control(
string kind,
string label,
string value,
CultMeshOperationBindingDescriptor operation,
CultMeshStateBindingDescriptor? binding)
{
var bindings = binding == null
? Array.Empty<CultMeshStateBindingDescriptor>()
: new[] { binding };
return new EveSurfaceComponent(
$"{_id}.{EveSurfaceBuilder.Slug(label)}",
kind,
new Dictionary<string, string>(StringComparer.Ordinal)
{
["label"] = label ?? "",
["value"] = value ?? "",
["command"] = operation.OperationId,
["operationId"] = operation.OperationId,
["schemaId"] = operation.SchemaId
},
Array.Empty<EveSurfaceComponent>(),
bindings);
}
}
public static class EveSurface
{
public static EveSurfaceBuilder Create(string surfaceId)
{
return new EveSurfaceBuilder(surfaceId);
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 97ac7f757692aef4db64d75d0df6fe51

View file

@ -0,0 +1,414 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GameCult.Caching;
using GameCult.Mesh;
using MessagePack;
#nullable enable
namespace GameCult.Eve.Surface
{
[CultDocument("gamecult.eve.surface", "gamecult.eve.surface.v1")]
[MessagePackObject]
public sealed class EveSurfaceDocument
{
public const string DefaultType = "surface-state";
public const string SchemaId = "gamecult.eve.surface.v1";
public EveSurfaceDocument(
string providerId,
string providerKind,
string title,
long version,
string updatedAtUtc,
EveSurfaceTree surface,
IReadOnlyList<EveCommandTemplate> commands)
: this(DefaultType, SchemaId, providerId, providerKind, title, version, updatedAtUtc, surface, commands)
{
}
[SerializationConstructor]
public EveSurfaceDocument(
string type,
string schema,
string providerId,
string providerKind,
string title,
long version,
string updatedAtUtc,
EveSurfaceTree surface,
IReadOnlyList<EveCommandTemplate> commands)
{
Type = string.IsNullOrWhiteSpace(type) ? DefaultType : type;
Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema;
ProviderId = providerId ?? "";
ProviderKind = providerKind ?? "";
Title = title ?? "";
Version = version;
UpdatedAtUtc = updatedAtUtc ?? "";
Surface = surface ?? throw new ArgumentNullException(nameof(surface));
Commands = commands ?? Array.Empty<EveCommandTemplate>();
}
[Key(0)]
public string Type { get; }
[Key(1)]
public string Schema { get; }
[Key(2)]
public string ProviderId { get; }
[Key(3)]
public string ProviderKind { get; }
[Key(4)]
public string Title { get; }
[Key(5)]
public long Version { get; }
[Key(6)]
public string UpdatedAtUtc { get; }
[Key(7)]
public EveSurfaceTree Surface { get; }
[Key(8)]
public IReadOnlyList<EveCommandTemplate> Commands { get; }
}
[MessagePackObject]
public sealed class EveSurfaceTree
{
[SerializationConstructor]
public EveSurfaceTree(string id, EveSurfaceComponent root, IReadOnlyList<EveStyleToken> styles)
{
Id = id ?? "";
Root = root ?? throw new ArgumentNullException(nameof(root));
Styles = styles ?? Array.Empty<EveStyleToken>();
}
[Key(0)]
public string Id { get; }
[Key(1)]
public EveSurfaceComponent Root { get; }
[Key(2)]
public IReadOnlyList<EveStyleToken> Styles { get; }
}
[MessagePackObject]
public sealed class EveSurfaceComponent
{
public EveSurfaceComponent(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
IReadOnlyList<EveSurfaceComponent> children)
: this(id, kind, props, children, StateBindingsFromProps(props))
{
}
public EveSurfaceComponent(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
IReadOnlyList<EveSurfaceComponent> children,
IReadOnlyList<CultMeshStateBindingDescriptor> stateBindings)
: this(id, kind, props, children, stateBindings, Array.Empty<EveEmbeddedDocumentSlot>())
{
}
public EveSurfaceComponent(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
IReadOnlyList<EveSurfaceComponent> children,
IReadOnlyList<CultMeshStateBindingDescriptor> stateBindings,
IReadOnlyList<EveEmbeddedDocumentSlot> embeddedDocuments,
IReadOnlyDictionary<string, string>? layout = null,
IReadOnlyDictionary<string, string>? style = null)
: this(
id,
kind,
props,
children,
(stateBindings ?? Array.Empty<CultMeshStateBindingDescriptor>())
.Select(CultMeshStateBindingRecord.FromBinding)
.ToArray(),
embeddedDocuments,
layout,
style)
{
}
[SerializationConstructor]
public EveSurfaceComponent(
string id,
string kind,
IReadOnlyDictionary<string, string> props,
IReadOnlyList<EveSurfaceComponent> children,
CultMeshStateBindingRecord[] stateBindingRecords,
IReadOnlyList<EveEmbeddedDocumentSlot> embeddedDocuments,
IReadOnlyDictionary<string, string>? layout = null,
IReadOnlyDictionary<string, string>? style = null)
{
Id = id ?? "";
Kind = kind ?? "";
Props = props ?? new Dictionary<string, string>(StringComparer.Ordinal);
Children = children ?? Array.Empty<EveSurfaceComponent>();
StateBindingRecords = stateBindingRecords ?? Array.Empty<CultMeshStateBindingRecord>();
EmbeddedDocuments = embeddedDocuments ?? Array.Empty<EveEmbeddedDocumentSlot>();
Layout = layout ?? new Dictionary<string, string>(StringComparer.Ordinal);
Style = style ?? new Dictionary<string, string>(StringComparer.Ordinal);
}
[Key(0)]
public string Id { get; }
[Key(1)]
public string Kind { get; }
[Key(2)]
public IReadOnlyDictionary<string, string> Props { get; }
[Key(3)]
public IReadOnlyList<EveSurfaceComponent> Children { get; }
[Key(4)]
public CultMeshStateBindingRecord[] StateBindingRecords { get; }
[IgnoreMember]
public IReadOnlyList<CultMeshStateBindingDescriptor> StateBindings =>
StateBindingRecords.Select(record => record.ToBinding()).ToArray();
[Key(5)]
public IReadOnlyList<EveEmbeddedDocumentSlot> EmbeddedDocuments { get; }
[Key(6)]
public IReadOnlyDictionary<string, string> Layout { get; }
[Key(7)]
public IReadOnlyDictionary<string, string> Style { get; }
public string GetProp(string key, string fallback = "")
{
return Props.TryGetValue(key, out var value) ? value : fallback;
}
private static IReadOnlyList<CultMeshStateBindingDescriptor> StateBindingsFromProps(
IReadOnlyDictionary<string, string>? props)
{
if (props == null || props.Count == 0)
return Array.Empty<CultMeshStateBindingDescriptor>();
var bindings = new List<CultMeshStateBindingDescriptor>();
foreach (var prop in props)
{
if (string.IsNullOrWhiteSpace(prop.Value) ||
!prop.Key.EndsWith("PointerId", StringComparison.Ordinal))
{
continue;
}
var targetProp = prop.Key.Substring(0, prop.Key.Length - "PointerId".Length);
if (targetProp.Length == 0)
targetProp = "value";
bindings.Add(new CultMeshStateBindingDescriptor(targetProp, prop.Value));
}
return bindings;
}
}
[MessagePackObject]
public sealed class EveEmbeddedDocumentSlot
{
public EveEmbeddedDocumentSlot(
string slotId,
string documentId,
string schemaId,
string presentationKind,
CultMeshRouteHint? routeHint = null)
: this(slotId, documentId, schemaId, presentationKind, CultMeshRouteRecord.FromRoute(routeHint))
{
}
[SerializationConstructor]
public EveEmbeddedDocumentSlot(
string slotId,
string documentId,
string schemaId,
string presentationKind,
CultMeshRouteRecord route)
{
SlotId = slotId ?? "";
DocumentId = documentId ?? "";
SchemaId = schemaId ?? "";
PresentationKind = presentationKind ?? "";
Route = route ?? CultMeshRouteRecord.FromRoute(CultMeshRouteHint.Automatic);
}
[Key(0)]
public string SlotId { get; }
[Key(1)]
public string DocumentId { get; }
[Key(2)]
public string SchemaId { get; }
[Key(3)]
public string PresentationKind { get; }
[Key(4)]
public CultMeshRouteRecord Route { get; }
[IgnoreMember]
public CultMeshRouteHint RouteHint => Route.ToRoute();
}
[MessagePackObject]
public sealed class EveStyleToken
{
[SerializationConstructor]
public EveStyleToken(string name, string value)
{
Name = name ?? "";
Value = value ?? "";
}
[Key(0)]
public string Name { get; }
[Key(1)]
public string Value { get; }
}
[MessagePackObject]
public sealed class EveCommandTemplate
{
public EveCommandTemplate(CultMeshOperationBindingDescriptor operation)
: this(CultMeshOperationBindingRecord.FromBinding(operation))
{
}
[SerializationConstructor]
public EveCommandTemplate(CultMeshOperationBindingRecord operationRecord)
{
OperationRecord = operationRecord ?? throw new ArgumentNullException(nameof(operationRecord));
}
[Key(0)]
public CultMeshOperationBindingRecord OperationRecord { get; }
[IgnoreMember]
public CultMeshOperationBindingDescriptor Operation => OperationRecord.ToBinding();
[IgnoreMember]
public string Command => Operation.OperationId;
[IgnoreMember]
public string Label => Operation.Label;
[IgnoreMember]
public string Transport => Operation.RouteHint.Description ?? "";
}
[CultDocument("gamecult.eve.command", SchemaId)]
[MessagePackObject]
public sealed class EveSurfaceCommandRequest
{
public const string SchemaId = "gamecult.eve.command_invocation.v1";
public EveSurfaceCommandRequest(
string providerId,
string surfaceId,
CultMeshOperationInvocationDescriptor operation,
CultMeshOperationPayload payload,
DateTimeOffset issuedAt,
string clientId,
string commandBoundary = "",
string receiptSchema = "")
: this(
SchemaId,
providerId,
surfaceId,
CultMeshOperationInvocationRecord.FromInvocation(operation),
payload?.ToDictionary(entry => entry.Key, entry => entry.Value, StringComparer.Ordinal)
?? new Dictionary<string, string>(StringComparer.Ordinal),
issuedAt,
clientId,
commandBoundary,
receiptSchema)
{
}
[SerializationConstructor]
public EveSurfaceCommandRequest(
string schema,
string providerId,
string surfaceId,
CultMeshOperationInvocationRecord operation,
Dictionary<string, string> payload,
DateTimeOffset issuedAt,
string clientId,
string commandBoundary,
string receiptSchema)
{
Schema = string.IsNullOrWhiteSpace(schema) ? SchemaId : schema;
ProviderId = providerId;
SurfaceId = surfaceId;
OperationRecord = operation ?? throw new ArgumentNullException(nameof(operation));
PayloadFields = payload == null
? new Dictionary<string, string>(StringComparer.Ordinal)
: new Dictionary<string, string>(payload, StringComparer.Ordinal);
IssuedAt = issuedAt;
ClientId = clientId;
CommandBoundary = commandBoundary ?? "";
ReceiptSchema = receiptSchema ?? "";
}
[Key(0)]
public string Schema { get; }
[Key(1)]
public string ProviderId { get; }
[Key(2)]
public string SurfaceId { get; }
[Key(3)]
public CultMeshOperationInvocationRecord OperationRecord { get; }
[IgnoreMember]
public CultMeshOperationInvocationDescriptor Operation => OperationRecord.ToInvocation();
[IgnoreMember]
public string Command => Operation.OperationId;
[Key(4)]
public Dictionary<string, string> PayloadFields { get; }
[IgnoreMember]
public CultMeshOperationPayload Payload => new CultMeshOperationPayload(PayloadFields);
[Key(5)]
public DateTimeOffset IssuedAt { get; }
[Key(6)]
public string ClientId { get; }
[Key(7)]
public string CommandBoundary { get; }
[Key(8)]
public string ReceiptSchema { get; }
[IgnoreMember]
public string CommandId => Operation.IdempotencyKey ?? "";
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6516c42104b235f42b15efeb4639bb73

View file

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

View file

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

View file

@ -0,0 +1,18 @@
{
"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.",
"dependencies": {
"org.gamecult.cultlib": "1.0.3"
},
"keywords": [
"Eve",
"CultUI",
"CultMesh",
"Surface"
],
"license": "MPL-2.0",
"category": "Scripting"
}

View file

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

View file

@ -129,7 +129,7 @@
"packageName": "org.gamecult.eve.surface",
"version": "0.1.0",
"packageManager": "upm",
"ownerRepo": "Eve",
"ownerRepo": "EveUnity",
"purpose": "Shared Unity DTOs for gamecult.eve.surface.v1 documents."
}
]

View file

@ -0,0 +1,150 @@
# 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.
Plugin semantics enter through plugin ABI sidecars and advertisements, not
through Unity. This package includes first-party projection adapters for the
`sai.vn` projection surface (`vn.stage`, dialogue panels, action rails, and
story command requests), the `norn.graph` projection surface (`embed.norn`),
and the `tex.math` source-fallback projection surface (`embed.tex`,
`tex.inline`, and `tex.block`). Sai still owns story state and command
semantics. Norn still owns graph layout and graph semantics. TeX still owns
parsing, typesetting, baseline metrics, and cached render semantics. Unity only
owns native projection of already-declared plugin capabilities and emits
`gamecult.eve.command.v1` requests.
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.
Nested CultUI regions use component `EmbeddedDocuments`. Pass an
`EmbeddedDocumentResolver` through `EveUiToolkitSurfaceOptions`; the lowerer
mounts the resolved child surface under the slot while preserving the child
document's command surface id.
Discovery and test coverage for this feature live in the shared Eve parity
matrix: `../../tools/parity/parity-manifest.json` names `embeddedDocuments` and
`../../web/fixtures/cultui-embedded-surface.json` is the canonical fixture.
Unity consumers should keep verifier coverage that builds a parent surface with
a resolver-backed child, then checks for the embedded visual element rather than
copying child state into a local adapter.
The runtime capability manifest is
`eve-runtime-capability.json`. Its lifecycle section records the current split
evidence:
- release: typed UPM release request contract, UPM `.tgz` artifact smoke, and
incubating package identity;
- test: package-owned EditMode tests run through Aetheria in Unity batchmode,
plus Aetheria consumer-build smoke through Unity's generated project;
- capture: typed capture request contract and pending Unity editor or
batchmode PNG artifact.
The release stage also declares the UPM release contract:
`org.gamecult.eve.unity-uitoolkit` is released from this package root, reads
its version from `package.json`, and uses tag pattern
`eveunity-uitoolkit-v{version}` once the tag is cut from EveUnity.
`tools/eveunity/eveunity-release-contract.mjs` builds a
`gamecult.eve.runtime_release_request.v1` request from the runtime capability
manifest and UPM package manifest. The request names the package version, tag,
artifact path, dependency set, required package dependency owners, and target
`GameCult/EveUnity` repository without pretending Eve has published the tagged
release.
`tools/eveunity/eveunity-release-artifact.mjs` consumes that request and runs
`npm pack` against the declared package root, producing the declared
`org.gamecult.eve.unity-uitoolkit-{version}.tgz` artifact as local smoke
evidence. The tagged release still belongs in the EveUnity repo.
The test stage declares the Unity EditMode runner contract: the current runner
is `scripts/run-aetheria-unity-editmode-tests.ps1`, runs
`GameCult.Eve.UnityUIToolkit.Tests` on `EditMode`, writes XML and log artifacts
under `artifacts/aetheria-unity-editmode/{stamp}`, and temporarily adds
`org.gamecult.eve.unity-uitoolkit` to Aetheria's Unity `testables`. This is a
Unity package consumption proof. The test contract names the CultLib-owned
NuGet/precompiled assembly inputs Unity needs during incubation; CultLib still
owns the .NET/NuGet dependency story for its assemblies.
Each managed dependency record carries a nested `dependencyContract` naming
CultLib as `sourceAuthority`, `nuget` as `packageSource`, the package id,
assembly name, version policy, Unity incubation resolution mode, and the
handoff requirement for EveUnity. This is not Eve inventing a second dependency
system. It is EveUnity declaring the assemblies it consumes while CultLib keeps
the package authority.
The capture stage declares `gamecult.eve.runtime_capture_request.v1` request
construction through `tools/eveunity/eveunity-capture-contract.mjs`. That
request is derived from the runtime capability manifest and Aetheria's provider
advertisement, then points at the PNG artifact EveUnity must produce later. It
does not fake a screenshot; it makes the capture input contract executable.
`tools/eveunity/eveunity-uitoolkit-capture-artifact.mjs` also emits a typed
`gamecult.eve.unity_uitoolkit_projection.v1` JSON projection artifact for the
Aetheria world surface. That artifact is runtime projection evidence, not
provider state and not a Unity frame capture.
The manifest also declares `worldSurfaceLowering` target `unity-uitoolkit`.
That means this package can lower provider-advertised `interactive-world` and
`interactive-world-editor` surfaces as semantic UI Toolkit command surfaces.
It does not claim Unity scene/world simulation ownership; Aetheria still owns
world state, assets, command acceptance, and receipts.
Those lifecycle claims are validated by the parity harness. A missing evidence
path is a runtime capability error, not a README footnote.
The split handoff manifest is `eveunity-split-handoff.json`. It is the
machine-readable map of what EveUnity must carry out of Eve incubation: the UPM
package body, the Unity test lifecycle, the Unity capture lifecycle, the Eve
contracts it consumes, and the forbidden imports it must not use as shortcuts.
It does not make the package ready to split by itself; it makes the remaining
release, test, and capture blockers inspectable.
From the repository root, run the split lifecycle smoke with:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\run-eveunity-lifecycle-smoke.ps1
```
That smoke validates `eve-runtime-capability.json`, checks lifecycle evidence
paths, builds the UPM artifact smoke, runs the Aetheria Unity package
consumer-build smoke, and verifies the split handoff manifest. Pass
`-RunUnityEditMode` when the local Unity editor should also execute the
incubating batchmode EditMode test runner. The final tagged UPM release,
batchmode runner ownership, and capture artifact still graduate to `EveUnity`.
The capture contract smoke is:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\run-eveunity-capture-contract-smoke.ps1
```
The semantic projection capture smoke is:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\run-eveunity-uitoolkit-capture-smoke.ps1
```
The release contract smoke is:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\run-eveunity-release-contract-smoke.ps1
```
The release artifact smoke is:
```powershell
powershell -ExecutionPolicy Bypass -File .\scripts\run-eveunity-release-artifact-smoke.ps1
```
For Aetheria, the Unity evidence path is:
- `powershell -ExecutionPolicy Bypass -File ..\..\scripts\run-aetheria-unity-package-smoke.ps1`
- `powershell -ExecutionPolicy Bypass -File ..\..\scripts\run-aetheria-unity-editmode-tests.ps1`
The EditMode test asmdef declares Unity precompiled references for the
Brokkr/CultMesh DLLs Unity needs to resolve `GameCult.Mesh`. That is Unity
assembly plumbing, not a claim that CultLib lacks NuGet/.NET packaging.
Those checks sit alongside Eve's shared browser, Flutter, iOS, Android/Kotlin,
and Rust contract tests, all discoverable from the parity manifest.

View file

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

View file

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

View file

@ -0,0 +1,547 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
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;
ApplyGeneratedLayout(element, component);
foreach (var child in component.Children)
element.Add(LowerComponent(child, document, commandSink));
foreach (var slot in component.EmbeddedDocuments)
{
var nested = LowerEmbeddedDocument(slot, commandSink);
if (nested != null)
element.Add(nested);
}
return element;
}
private VisualElement? LowerEmbeddedDocument(
EveEmbeddedDocumentSlot slot,
Action<EveSurfaceCommandRequest>? commandSink)
{
var document = _options.EmbeddedDocumentResolver?.Invoke(slot);
if (document == null)
return null;
var element = LowerComponent(document.Surface.Root, document, commandSink);
element.name = SafeName(string.IsNullOrWhiteSpace(slot.SlotId) ? document.Surface.Id : slot.SlotId);
element.AddToClassList("eve-embedded-document");
element.AddToClassList($"eve-embedded-kind-{SafeClass(slot.PresentationKind)}");
ApplyStyleTokens(element, document.Surface.Styles);
return element;
}
private VisualElement CreateElement(
EveSurfaceComponent component,
EveSurfaceDocument document,
Action<EveSurfaceCommandRequest>? commandSink)
{
var projectionAdapter = _options.FindPluginProjectionAdapter(component);
if (projectionAdapter != null)
return projectionAdapter.Lower(component, document, 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 "partition":
{
var element = new VisualElement();
element.style.flexDirection = component.GetProp("split") == "x"
? FlexDirection.Row
: FlexDirection.Column;
element.style.flexWrap = Wrap.Wrap;
return element;
}
case "pane":
case "modal":
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 "progress":
{
var progress = new VisualElement();
progress.AddToClassList("eve-progress");
progress.style.flexDirection = FlexDirection.Column;
progress.Add(MutedLabel(component.GetProp("label")));
progress.Add(ValueLabel(component.GetProp("value")));
return progress;
}
case "options":
{
var options = new VisualElement();
options.AddToClassList("eve-options");
options.style.flexDirection = FlexDirection.Column;
var label = component.GetProp("label");
if (!string.IsNullOrWhiteSpace(label))
options.Add(TitleLabel(label));
return options;
}
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":
case "control.popup":
{
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":
case "control.input.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, GameCult.Mesh.CultMesh.OperationPayload(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,
GameCult.Mesh.CultMesh.OperationPayload(component.Props),
commandSink);
}
private static void EmitCommand(
EveSurfaceDocument document,
EveSurfaceComponent component,
string command,
GameCult.Mesh.CultMeshOperationPayload payload,
Action<EveSurfaceCommandRequest>? commandSink)
{
if (commandSink == null || string.IsNullOrWhiteSpace(command))
return;
commandSink(new EveSurfaceCommandRequest(
document.ProviderId,
document.Surface.Id,
ResolveOperation(document, command),
payload,
DateTimeOffset.UtcNow,
"unity-uitoolkit"));
}
private static GameCult.Mesh.CultMeshOperationInvocationDescriptor ResolveOperation(
EveSurfaceDocument document,
string command)
{
foreach (var template in document.Commands)
{
if (string.Equals(template.Command, command, StringComparison.Ordinal))
return GameCult.Mesh.CultMesh.OperationInvocation(template.Operation);
}
return GameCult.Mesh.CultMesh.OperationInvocation(command);
}
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 void ApplyGeneratedLayout(VisualElement element, EveSurfaceComponent component)
{
var layout = component.Layout ?? new Dictionary<string, string>(StringComparer.Ordinal);
var style = component.Style ?? new Dictionary<string, string>(StringComparer.Ordinal);
if (TryGet(layout, "display", out var display))
element.style.display = string.Equals(display, "none", StringComparison.OrdinalIgnoreCase)
? DisplayStyle.None
: DisplayStyle.Flex;
if (TryGet(layout, "direction", out var direction))
{
if (string.Equals(direction, "horizontal", StringComparison.OrdinalIgnoreCase) ||
string.Equals(direction, "row", StringComparison.OrdinalIgnoreCase))
element.style.flexDirection = FlexDirection.Row;
else if (string.Equals(direction, "vertical", StringComparison.OrdinalIgnoreCase) ||
string.Equals(direction, "column", StringComparison.OrdinalIgnoreCase))
element.style.flexDirection = FlexDirection.Column;
}
if (TryGet(layout, "flexWrap", out var flexWrap))
element.style.flexWrap = string.Equals(flexWrap, "wrap", StringComparison.OrdinalIgnoreCase) ? Wrap.Wrap : Wrap.NoWrap;
if (TryGet(layout, "alignItems", out var alignItems) && TryParseAlign(alignItems, out var align))
element.style.alignItems = align;
if (TryGet(layout, "justifyContent", out var justifyContent) && TryParseJustify(justifyContent, out var justify))
element.style.justifyContent = justify;
if (TryGet(layout, "width", out var width))
element.style.width = ParseLength(width);
if (TryGet(layout, "minWidth", out var minWidth))
element.style.minWidth = ParseLength(minWidth);
if (TryGet(layout, "maxWidth", out var maxWidth))
element.style.maxWidth = ParseLength(maxWidth);
if (TryGet(layout, "height", out var height))
element.style.height = ParseLength(height);
if (TryGet(layout, "minHeight", out var minHeight))
element.style.minHeight = ParseLength(minHeight);
if (TryGet(layout, "maxHeight", out var maxHeight))
element.style.maxHeight = ParseLength(maxHeight);
if (TryGet(layout, "padding", out var padding))
ApplyBoxLength(padding, value =>
{
element.style.paddingTop = value;
element.style.paddingRight = value;
element.style.paddingBottom = value;
element.style.paddingLeft = value;
});
if (TryGet(layout, "margin", out var margin))
ApplyBoxLength(margin, value =>
{
element.style.marginTop = value;
element.style.marginRight = value;
element.style.marginBottom = value;
element.style.marginLeft = value;
});
if (TryGet(layout, "overflow", out var overflow))
element.style.overflow = string.Equals(overflow, "hidden", StringComparison.OrdinalIgnoreCase)
? Overflow.Hidden
: Overflow.Visible;
if (TryGet(style, "background", out var background) && TryParseColor(background, out var backgroundColor))
element.style.backgroundColor = backgroundColor;
if (TryGet(style, "color", out var color) && TryParseColor(color, out var textColor))
element.style.color = textColor;
if (TryGet(style, "borderWidth", out var borderWidth))
ApplyBoxFloat(borderWidth, value =>
{
element.style.borderTopWidth = value;
element.style.borderRightWidth = value;
element.style.borderBottomWidth = value;
element.style.borderLeftWidth = value;
});
if (TryGet(style, "borderColor", out var borderColor) && TryParseColor(borderColor, out var border))
{
element.style.borderTopColor = border;
element.style.borderRightColor = border;
element.style.borderBottomColor = border;
element.style.borderLeftColor = border;
}
if (TryGet(style, "borderRadius", out var borderRadius))
ApplyBoxLength(borderRadius, value =>
{
element.style.borderTopLeftRadius = value;
element.style.borderTopRightRadius = value;
element.style.borderBottomRightRadius = value;
element.style.borderBottomLeftRadius = value;
});
if (TryGet(style, "fontSize", out var fontSize))
element.style.fontSize = ParseLength(fontSize);
if (TryGet(style, "fontWeight", out var fontWeight) &&
(string.Equals(fontWeight, "bold", StringComparison.OrdinalIgnoreCase) || fontWeight == "700"))
element.style.unityFontStyleAndWeight = FontStyle.Bold;
}
private static bool TryGet(IReadOnlyDictionary<string, string> values, string key, out string value)
{
if (values.TryGetValue(key, out value) && !string.IsNullOrWhiteSpace(value))
return true;
value = "";
return false;
}
private static StyleLength ParseLength(string value)
{
value = FirstToken(value);
if (string.Equals(value, "auto", StringComparison.OrdinalIgnoreCase))
return StyleKeyword.Auto;
if (value.EndsWith("%", StringComparison.Ordinal) &&
float.TryParse(value.Substring(0, value.Length - 1), NumberStyles.Float, CultureInfo.InvariantCulture, out var percent))
return new Length(percent, LengthUnit.Percent);
if (value.EndsWith("px", StringComparison.OrdinalIgnoreCase))
value = value.Substring(0, value.Length - 2);
if (float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var pixels))
return new Length(pixels, LengthUnit.Pixel);
return StyleKeyword.Null;
}
private static StyleFloat ParseFloat(string value)
{
value = FirstToken(value);
if (value.EndsWith("px", StringComparison.OrdinalIgnoreCase))
value = value.Substring(0, value.Length - 2);
return float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out var pixels)
? pixels
: StyleKeyword.Null;
}
private static void ApplyBoxLength(string value, Action<StyleLength> apply)
{
apply(ParseLength(value));
}
private static void ApplyBoxFloat(string value, Action<StyleFloat> apply)
{
apply(ParseFloat(value));
}
private static bool TryParseColor(string value, out Color color)
{
value = value.Trim();
if (value.StartsWith("var(", StringComparison.Ordinal))
{
color = default;
return false;
}
return ColorUtility.TryParseHtmlString(value, out color);
}
private static bool TryParseAlign(string value, out Align align)
{
switch (value)
{
case "center":
align = Align.Center;
return true;
case "end":
case "flex-end":
align = Align.FlexEnd;
return true;
case "stretch":
align = Align.Stretch;
return true;
case "start":
case "flex-start":
align = Align.FlexStart;
return true;
default:
align = Align.Auto;
return false;
}
}
private static bool TryParseJustify(string value, out Justify justify)
{
switch (value)
{
case "center":
justify = Justify.Center;
return true;
case "end":
case "flex-end":
justify = Justify.FlexEnd;
return true;
case "space-between":
justify = Justify.SpaceBetween;
return true;
case "space-around":
justify = Justify.SpaceAround;
return true;
case "start":
case "flex-start":
justify = Justify.FlexStart;
return true;
default:
justify = Justify.FlexStart;
return false;
}
}
private static string FirstToken(string value)
{
return (value ?? "").Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? "";
}
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,2 @@
fileFormatVersion: 2
guid: bef5882c69652c54fb26b07d63e5bcdd

View file

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GameCult.Eve.Surface;
#nullable enable
namespace GameCult.Eve.UnityUIToolkit
{
public sealed class EveUiToolkitSurfaceOptions
{
public static EveUiToolkitSurfaceOptions Default { get; } = new EveUiToolkitSurfaceOptions();
public EveUiToolkitSurfaceOptions(
Func<EveEmbeddedDocumentSlot, EveSurfaceDocument?>? embeddedDocumentResolver = null,
IReadOnlyList<IEveUiToolkitPluginProjectionAdapter>? pluginProjectionAdapters = null)
{
EmbeddedDocumentResolver = embeddedDocumentResolver;
PluginProjectionAdapters = pluginProjectionAdapters ?? new IEveUiToolkitPluginProjectionAdapter[]
{
new SaiVisualNovelUiToolkitProjectionAdapter(),
new NornGraphUiToolkitProjectionAdapter(),
new TeXMathUiToolkitProjectionAdapter()
};
}
public Func<EveEmbeddedDocumentSlot, EveSurfaceDocument?>? EmbeddedDocumentResolver { get; }
public IReadOnlyList<IEveUiToolkitPluginProjectionAdapter> PluginProjectionAdapters { get; }
public IEveUiToolkitPluginProjectionAdapter? FindPluginProjectionAdapter(EveSurfaceComponent component)
{
return PluginProjectionAdapters.FirstOrDefault(adapter => adapter.CanLower(component));
}
}
}

View file

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

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,7 @@
fileFormatVersion: 2
guid: 093779c1124fb5241b9c16e18173520a
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using GameCult.Eve.Surface;
using UnityEngine.UIElements;
#nullable enable
namespace GameCult.Eve.UnityUIToolkit
{
public interface IEveUiToolkitPluginProjectionAdapter
{
string PluginId { get; }
IReadOnlyList<string> Capabilities { get; }
bool CanLower(EveSurfaceComponent component);
VisualElement Lower(
EveSurfaceComponent component,
EveSurfaceDocument document,
Action<EveSurfaceCommandRequest>? commandSink);
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 8e93b6d6e2364ef4a91766cbf5a44b62

View file

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using GameCult.Eve.Surface;
using GameCult.Mesh;
using UnityEngine;
using UnityEngine.UIElements;
#nullable enable
namespace GameCult.Eve.UnityUIToolkit
{
public sealed class NornGraphUiToolkitProjectionAdapter : IEveUiToolkitPluginProjectionAdapter
{
public string PluginId => "norn.graph";
public IReadOnlyList<string> Capabilities { get; } = new[]
{
"embed.norn"
};
public bool CanLower(EveSurfaceComponent component)
{
return string.Equals(component.Kind, "embed.norn", StringComparison.Ordinal);
}
public VisualElement Lower(
EveSurfaceComponent component,
EveSurfaceDocument document,
Action<EveSurfaceCommandRequest>? commandSink)
{
var graph = new VisualElement();
graph.AddToClassList("eve-plugin-projection");
graph.AddToClassList("eve-plugin-norn-graph");
graph.AddToClassList("norn-graph");
graph.style.flexDirection = FlexDirection.Column;
var title = component.GetProp("label", component.GetProp("title", "Norn graph"));
var titleLabel = new Label(title);
titleLabel.AddToClassList("norn-graph-title");
titleLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
graph.Add(titleLabel);
var mode = component.GetProp("mode", component.GetProp("layoutMode", "embedded"));
var modeLabel = new Label(mode);
modeLabel.AddToClassList("norn-graph-mode");
graph.Add(modeLabel);
var focusCommand = component.GetProp("command", component.GetProp("focusCommand", "graph.focus"));
graph.RegisterCallback<ClickEvent>(_ => EmitGraphCommand(document, component, focusCommand, commandSink));
return graph;
}
private static void EmitGraphCommand(
EveSurfaceDocument document,
EveSurfaceComponent component,
string command,
Action<EveSurfaceCommandRequest>? commandSink)
{
if (commandSink == null || string.IsNullOrWhiteSpace(command))
return;
commandSink(new EveSurfaceCommandRequest(
document.ProviderId,
document.Surface.Id,
ResolveOperation(document, command),
CultMesh.OperationPayload(component.Props),
DateTimeOffset.UtcNow,
"unity-uitoolkit"));
}
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);
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 729936c97f3c45f7bba0fcd6de8b40f5

View file

@ -0,0 +1,205 @@
using System;
using System.Collections.Generic;
using GameCult.Eve.Surface;
using GameCult.Mesh;
using UnityEngine;
using UnityEngine.UIElements;
#nullable enable
namespace GameCult.Eve.UnityUIToolkit
{
public sealed class SaiVisualNovelUiToolkitProjectionAdapter : IEveUiToolkitPluginProjectionAdapter
{
public string PluginId => "sai.vn";
public IReadOnlyList<string> Capabilities { get; } = new[]
{
"vn.stage",
"story.choose",
"story.continue",
"story.jump"
};
public bool CanLower(EveSurfaceComponent component)
{
switch (component.Kind)
{
case "vn.stage":
case "panel.dialogue":
case "text.dialogue":
case "rail.actions":
return true;
case "control.button":
return IsStoryCommand(component);
default:
return false;
}
}
public VisualElement Lower(
EveSurfaceComponent component,
EveSurfaceDocument document,
Action<EveSurfaceCommandRequest>? commandSink)
{
switch (component.Kind)
{
case "vn.stage":
return Stage(component);
case "panel.dialogue":
return DialoguePanel(component);
case "text.dialogue":
return DialogueText(component);
case "rail.actions":
return ActionRail();
case "control.button":
return StoryButton(component, document, commandSink);
default:
return new VisualElement();
}
}
private static VisualElement Stage(EveSurfaceComponent component)
{
var stage = new VisualElement();
stage.AddToClassList("eve-plugin-projection");
stage.AddToClassList("eve-plugin-sai-vn");
stage.AddToClassList("sai-vn-stage");
stage.style.flexGrow = 1;
stage.style.flexDirection = FlexDirection.Column;
var storyId = component.GetProp("storyId");
var currentPath = component.GetProp("currentPath");
if (!string.IsNullOrWhiteSpace(storyId) || !string.IsNullOrWhiteSpace(currentPath))
{
var state = new Label(string.IsNullOrWhiteSpace(currentPath)
? storyId
: $"{storyId}:{currentPath}");
state.AddToClassList("sai-vn-state");
state.style.unityFontStyleAndWeight = FontStyle.Bold;
stage.Add(state);
}
return stage;
}
private static VisualElement DialoguePanel(EveSurfaceComponent component)
{
var panel = new VisualElement();
panel.AddToClassList("eve-plugin-projection");
panel.AddToClassList("eve-plugin-sai-vn");
panel.AddToClassList("sai-vn-dialogue");
panel.style.flexDirection = FlexDirection.Column;
var speaker = component.GetProp("speaker");
if (!string.IsNullOrWhiteSpace(speaker))
{
var speakerLabel = new Label(speaker);
speakerLabel.AddToClassList("sai-vn-speaker");
speakerLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
panel.Add(speakerLabel);
}
var scene = component.GetProp("scene");
if (!string.IsNullOrWhiteSpace(scene))
{
var sceneLabel = new Label(scene);
sceneLabel.AddToClassList("sai-vn-scene");
panel.Add(sceneLabel);
}
return panel;
}
private static VisualElement DialogueText(EveSurfaceComponent component)
{
var text = new Label(component.GetProp("text"));
text.AddToClassList("eve-plugin-projection");
text.AddToClassList("eve-plugin-sai-vn");
text.AddToClassList("sai-vn-dialogue-text");
return text;
}
private static VisualElement ActionRail()
{
var rail = new VisualElement();
rail.AddToClassList("eve-plugin-projection");
rail.AddToClassList("eve-plugin-sai-vn");
rail.AddToClassList("sai-vn-actions");
rail.style.flexDirection = FlexDirection.Row;
rail.style.flexWrap = Wrap.Wrap;
return rail;
}
private static Button StoryButton(
EveSurfaceComponent component,
EveSurfaceDocument document,
Action<EveSurfaceCommandRequest>? commandSink)
{
var label = component.GetProp("label", component.GetProp("title", "Continue"));
var command = StoryCommand(component);
var button = new Button(() => EmitStoryCommand(document, component, command, commandSink)) { text = label };
button.AddToClassList("eve-plugin-projection");
button.AddToClassList("eve-plugin-sai-vn");
button.AddToClassList("sai-vn-story-command");
button.AddToClassList($"sai-vn-command-{SafeClass(command)}");
return button;
}
private static bool IsStoryCommand(EveSurfaceComponent component)
{
return StoryCommand(component).StartsWith("story.", StringComparison.Ordinal);
}
private static string StoryCommand(EveSurfaceComponent component)
{
return component.GetProp("command", component.GetProp("action", component.GetProp("type")));
}
private static void EmitStoryCommand(
EveSurfaceDocument document,
EveSurfaceComponent component,
string command,
Action<EveSurfaceCommandRequest>? commandSink)
{
if (commandSink == null || string.IsNullOrWhiteSpace(command))
return;
commandSink(new EveSurfaceCommandRequest(
document.ProviderId,
document.Surface.Id,
ResolveOperation(document, command),
CultMesh.OperationPayload(component.Props),
DateTimeOffset.UtcNow,
"unity-uitoolkit"));
}
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 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,2 @@
fileFormatVersion: 2
guid: 4e8f6fc7d7ef4dd7b22ec33d4f81d321

View file

@ -0,0 +1,69 @@
using System;
using System.Collections.Generic;
using GameCult.Eve.Surface;
using UnityEngine.UIElements;
#nullable enable
namespace GameCult.Eve.UnityUIToolkit
{
public sealed class TeXMathUiToolkitProjectionAdapter : IEveUiToolkitPluginProjectionAdapter
{
public string PluginId => "tex.math";
public IReadOnlyList<string> Capabilities { get; } = new[]
{
"embed.tex",
"tex.inline",
"tex.block"
};
public bool CanLower(EveSurfaceComponent component)
{
return string.Equals(component.Kind, "embed.tex", StringComparison.Ordinal);
}
public VisualElement Lower(
EveSurfaceComponent component,
EveSurfaceDocument document,
Action<EveSurfaceCommandRequest>? commandSink)
{
var tex = new VisualElement();
tex.AddToClassList("eve-plugin-projection");
tex.AddToClassList("eve-plugin-tex-math");
tex.AddToClassList("tex-math");
tex.AddToClassList($"tex-display-{SafeClass(component.GetProp("display", "inline"))}");
tex.style.flexDirection = FlexDirection.Column;
var label = component.GetProp("label", "TeX");
if (!string.IsNullOrWhiteSpace(label))
{
var labelElement = new Label(label);
labelElement.AddToClassList("tex-math-label");
tex.Add(labelElement);
}
var source = component.GetProp("source", component.GetProp("sourceUri", ""));
var sourceElement = new Label(source);
sourceElement.AddToClassList("tex-math-source-fallback");
tex.Add(sourceElement);
return tex;
}
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,2 @@
fileFormatVersion: 2
guid: d6c655fae0c77064490eba9c3ed1fb4c

View file

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

View file

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

View file

@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using GameCult.Eve.Surface;
using GameCult.Mesh;
using NUnit.Framework;
using UnityEngine.UIElements;
#nullable enable
namespace GameCult.Eve.UnityUIToolkit.Tests
{
public sealed class EveUiToolkitSurfaceLowererTests
{
[Test]
public void DefaultOptionsExposeSaiNornAndTeXProjectionAdapters()
{
var options = EveUiToolkitSurfaceOptions.Default;
Assert.That(options.FindPluginProjectionAdapter(Component("stage", "vn.stage"))?.PluginId, Is.EqualTo("sai.vn"));
Assert.That(options.FindPluginProjectionAdapter(Component("graph", "embed.norn"))?.PluginId, Is.EqualTo("norn.graph"));
Assert.That(options.FindPluginProjectionAdapter(Component("math", "embed.tex"))?.PluginId, Is.EqualTo("tex.math"));
}
[Test]
public void SaiStageLowersThroughRuntimeProjectionAdapter()
{
var document = Document(Component(
"stage",
"vn.stage",
new Dictionary<string, string>
{
["storyId"] = "sai.fixture",
["currentPath"] = "intro"
}));
var root = new EveUiToolkitSurfaceLowerer().Lower(document);
Assert.That(root.ClassListContains("eve-plugin-projection"), Is.True);
Assert.That(root.ClassListContains("eve-plugin-sai-vn"), Is.True);
Assert.That(root.ClassListContains("sai-vn-stage"), Is.True);
}
[Test]
public void NornGraphLowersThroughRuntimeProjectionAdapter()
{
var document = Document(Component(
"graph",
"embed.norn",
new Dictionary<string, string>
{
["label"] = "World graph",
["mode"] = "embedded"
}));
var root = new EveUiToolkitSurfaceLowerer().Lower(document);
Assert.That(root.ClassListContains("eve-plugin-projection"), Is.True);
Assert.That(root.ClassListContains("eve-plugin-norn-graph"), Is.True);
Assert.That(root.ClassListContains("norn-graph"), Is.True);
}
[Test]
public void TeXMathLowersThroughRuntimeProjectionAdapterAsSourceFallback()
{
var document = Document(Component(
"math",
"embed.tex",
new Dictionary<string, string>
{
["label"] = "Bifrost voting weight",
["source"] = "\\\\mathrm{votes}(p)=1+\\\\lfloor\\\\log_b(1+p)\\\\rfloor",
["display"] = "block"
}));
var root = new EveUiToolkitSurfaceLowerer().Lower(document);
Assert.That(root.ClassListContains("eve-plugin-projection"), Is.True);
Assert.That(root.ClassListContains("eve-plugin-tex-math"), Is.True);
Assert.That(root.ClassListContains("tex-math"), Is.True);
Assert.That(root.ClassListContains("tex-display-block"), Is.True);
}
[Test]
public void EmbeddedDocumentSlotMountsResolverBackedChildSurface()
{
var child = Document(
Component("child-root", "text", new Dictionary<string, string> { ["text"] = "Nested surface" }),
surfaceId: "child-surface");
var parentRoot = new EveSurfaceComponent(
"parent-root",
"surface",
EmptyProps(),
Array.Empty<EveSurfaceComponent>(),
Array.Empty<CultMeshStateBindingDescriptor>(),
new[]
{
new EveEmbeddedDocumentSlot(
"child-slot",
"child-surface",
EveSurfaceDocument.SchemaId,
"inline")
});
var lowerer = new EveUiToolkitSurfaceLowerer(new EveUiToolkitSurfaceOptions(
embeddedDocumentResolver: _ => child,
pluginProjectionAdapters: Array.Empty<IEveUiToolkitPluginProjectionAdapter>()));
var root = lowerer.Lower(Document(parentRoot));
var embedded = root.Query<VisualElement>(className: "eve-embedded-document").First();
Assert.That(embedded, Is.Not.Null);
Assert.That(embedded.name, Is.EqualTo("child-slot"));
Assert.That(embedded.ClassListContains("eve-embedded-kind-inline"), Is.True);
}
private static EveSurfaceDocument Document(EveSurfaceComponent root, string surfaceId = "test-surface")
{
return new EveSurfaceDocument(
"gamecult.eve.unity.tests",
"runtime.test",
"Unity UI Toolkit tests",
1,
"2026-07-08T00:00:00Z",
new EveSurfaceTree(surfaceId, root, Array.Empty<EveStyleToken>()),
Array.Empty<EveCommandTemplate>());
}
private static EveSurfaceComponent Component(
string id,
string kind,
IReadOnlyDictionary<string, string>? props = null)
{
return new EveSurfaceComponent(
id,
kind,
props ?? EmptyProps(),
Array.Empty<EveSurfaceComponent>());
}
private static IReadOnlyDictionary<string, string> EmptyProps()
{
return new Dictionary<string, string>(StringComparer.Ordinal);
}
}
}

View file

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2921e30d39fe49098bc3763c538ed67c

View file

@ -0,0 +1,27 @@
{
"name": "GameCult.Eve.UnityUIToolkit.Tests",
"rootNamespace": "GameCult.Eve.UnityUIToolkit.Tests",
"references": [
"GameCult.Eve.UnityUIToolkit",
"GameCult.Eve.Surface",
"GameCult.Mesh"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"GameCult.Caching.dll",
"GameCult.Mesh.dll",
"MessagePack.Annotations.dll"
],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"optionalUnityReferences": [
"TestAssemblies"
],
"noEngineReferences": false
}

View file

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

View file

@ -0,0 +1,239 @@
{
"schema": "gamecult.eve.runtime_capability.v1",
"runtimeId": "unity-uitoolkit",
"title": "Unity / UI Toolkit",
"owner": "EveUnity",
"incubation": {
"ownerRepo": "EveUnity",
"currentHostRepo": "EveUnity",
"splitTarget": "EveUnity",
"graduationTrigger": "Graduated: EveUnity owns the UI Toolkit package, plugin projection adapters, package build, and Unity tests."
},
"supportedFeatures": [
"embeddedDocuments"
],
"supportedPlugins": [
{
"pluginId": "sai.vn",
"capabilities": [
"vn.stage",
"story.choose",
"story.continue",
"story.jump"
],
"projectionAdapter": "SaiVisualNovelUiToolkitProjectionAdapter"
},
{
"pluginId": "norn.graph",
"capabilities": [
"embed.norn"
],
"projectionAdapter": "NornGraphUiToolkitProjectionAdapter"
},
{
"pluginId": "tex.math",
"capabilities": [
"embed.tex",
"tex.inline",
"tex.block"
],
"projectionAdapter": "TeXMathUiToolkitProjectionAdapter"
}
],
"unsupportedPlugins": [],
"commandTransport": {
"schema": "gamecult.eve.command_invocation.v1",
"emitter": "EveUiToolkitSurfaceLowerer.EmitCommand",
"clientId": "unity-uitoolkit"
},
"worldSurfaceLowering": [
{
"targetId": "unity-uitoolkit",
"surfaceKinds": ["interactive-world", "interactive-world-editor"],
"projectionKinds": ["provider-authored-world-surface", "provider-authored-world-editor-surface"],
"supportLevel": "ui-toolkit-semantic-command-surface",
"ownership": "runtime-lowers-provider-world-surface-without-owning-world-state",
"evidencePaths": [
"packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs",
"packages/org.gamecult.eve.unity-uitoolkit/Runtime/TeXMathUiToolkitProjectionAdapter.cs",
"packages/org.gamecult.eve.unity-uitoolkit/Tests/Editor/EveUiToolkitSurfaceLowererTests.cs",
"web/fixtures/aetheria.provider-advertisement.json",
"web/fixtures/eve-world-smoke.provider-advertisement.json"
]
}
],
"lifecycle": {
"release": {
"ownerRepo": "EveUnity",
"status": "upm-package-artifact-smoke",
"packageName": "org.gamecult.eve.unity-uitoolkit",
"packageManifest": "packages/org.gamecult.eve.unity-uitoolkit/package.json",
"releaseContract": {
"ownerRepo": "EveUnity",
"packageName": "org.gamecult.eve.unity-uitoolkit",
"packageRoot": "packages/org.gamecult.eve.unity-uitoolkit",
"versionSource": "packages/org.gamecult.eve.unity-uitoolkit/package.json",
"tagPattern": "eveunity-uitoolkit-v{version}",
"artifactKind": "upm-package",
"requestSchema": "gamecult.eve.runtime_release_request.v1",
"requestBuilder": "scripts/pack-uitoolkit.ps1",
"artifactBuilder": "scripts/pack-uitoolkit.ps1",
"artifactPattern": "artifacts/eveunity-uitoolkit-release/{version}/org.gamecult.eve.unity-uitoolkit-{version}.tgz",
"publishProof": "Tagged UPM release is created from the EveUnity repo and references this package root.",
"requiredPackageDependencies": [
{
"packageName": "org.gamecult.eve.surface",
"version": "0.1.0",
"packageManager": "upm",
"ownerRepo": "EveUnity",
"purpose": "Shared Unity DTOs for gamecult.eve.surface.v1 documents."
}
]
},
"evidencePaths": [
"packages/org.gamecult.eve.unity-uitoolkit/package.json",
"packages/org.gamecult.eve.unity-uitoolkit/README.md",
"scripts/pack-uitoolkit.ps1"
],
"pendingProofs": [
"Tagged UPM release cut from EveUnity rather than Eve incubation."
]
},
"test": {
"ownerRepo": "EveUnity",
"status": "batchmode-editmode-tests-and-consumer-build-smoke",
"command": "powershell -ExecutionPolicy Bypass -File .\\scripts\\run-uitoolkit-tests.ps1",
"testContract": {
"ownerRepo": "EveUnity",
"runnerKind": "unity-editmode-batchmode",
"runnerScript": "scripts/run-uitoolkit-tests.ps1",
"consumerProject": "E:\\Projects\\Aetheria",
"unityExeDefault": "C:\\Program Files\\Unity\\Hub\\Editor\\6000.4.2f1\\Editor\\Unity.exe",
"packageName": "org.gamecult.eve.unity-uitoolkit",
"testAssembly": "GameCult.Eve.UnityUIToolkit.Tests",
"testPlatform": "EditMode",
"resultsArtifact": "artifacts/aetheria-unity-editmode/{stamp}/unity-editmode-results.xml",
"logArtifact": "artifacts/aetheria-unity-editmode/{stamp}/unity-editmode.log",
"manifestMutation": "Temporarily adds org.gamecult.eve.unity-uitoolkit to Aetheria Packages/manifest.json testables and restores the manifest after the run.",
"managedAssemblyDependencies": [
{
"assemblyName": "GameCult.Caching.dll",
"packageId": "GameCult.Caching",
"packageManager": "nuget",
"ownerRepo": "CultLib",
"resolutionMode": "Unity precompiled reference during EveUnity incubation.",
"dependencyContract": {
"sourceAuthority": "CultLib",
"packageSource": "nuget",
"packageId": "GameCult.Caching",
"assemblyName": "GameCult.Caching.dll",
"versionPolicy": "CultLib-owned package version stream.",
"unityResolution": "EveUnity consumes this as a Unity precompiled reference during incubation.",
"handoffRequirement": "EveUnity split must resolve this through CultLib's NuGet/package feed or a documented Unity import without vendoring CultLib ownership into Eve."
}
},
{
"assemblyName": "GameCult.Mesh.dll",
"packageId": "GameCult.Mesh",
"packageManager": "nuget",
"ownerRepo": "CultLib",
"resolutionMode": "Unity precompiled reference during EveUnity incubation.",
"dependencyContract": {
"sourceAuthority": "CultLib",
"packageSource": "nuget",
"packageId": "GameCult.Mesh",
"assemblyName": "GameCult.Mesh.dll",
"versionPolicy": "CultLib-owned package version stream.",
"unityResolution": "EveUnity consumes this as a Unity precompiled reference during incubation.",
"handoffRequirement": "EveUnity split must resolve this through CultLib's NuGet/package feed or a documented Unity import without vendoring CultLib ownership into Eve."
}
},
{
"assemblyName": "MessagePack.Annotations.dll",
"packageId": "MessagePack.Annotations",
"packageManager": "nuget",
"ownerRepo": "CultLib",
"resolutionMode": "Unity precompiled reference during EveUnity incubation.",
"dependencyContract": {
"sourceAuthority": "CultLib",
"packageSource": "nuget",
"packageId": "MessagePack.Annotations",
"assemblyName": "MessagePack.Annotations.dll",
"versionPolicy": "CultLib-owned transitive package version stream.",
"unityResolution": "EveUnity consumes this as a Unity precompiled reference during incubation.",
"handoffRequirement": "EveUnity split must resolve this through CultLib's NuGet/package feed or a documented Unity import without vendoring CultLib ownership into Eve."
}
}
]
},
"evidencePaths": [
"packages/org.gamecult.eve.unity-uitoolkit/Tests/Editor/GameCult.Eve.UnityUIToolkit.Tests.asmdef",
"packages/org.gamecult.eve.unity-uitoolkit/Tests/Editor/EveUiToolkitSurfaceLowererTests.cs",
"scripts/run-uitoolkit-tests.ps1"
],
"pendingProofs": [
"Cut and publish the first tagged EveUnity UI Toolkit package release."
]
},
"capture": {
"ownerRepo": "EveUnity",
"status": "pending-editor-capture",
"captureContract": {
"ownerRepo": "EveUnity",
"runtimeId": "unity-uitoolkit",
"targetId": "unity-uitoolkit",
"requestSchema": "gamecult.eve.runtime_capture_request.v1",
"requestBuilder": "tools/eveunity/eveunity-capture-contract.mjs",
"advertisementPath": "web/fixtures/aetheria.provider-advertisement.json",
"captureKind": "unity-editor-or-batchmode-png",
"artifactKind": "png",
"artifactPattern": "artifacts/eveunity-uitoolkit-capture/{stamp}/unity-uitoolkit.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."
},
{
"providerId": "aetheria",
"surfaceId": "aetheria.daemon.editor",
"purpose": "Aetheria interactive-world-editor capture control."
}
],
"authority": "Capture observes runtime projection only; provider state, command acceptance, and receipts stay provider-owned.",
"publishProof": "Unity editor or batchmode capture artifact is attached to the runtime conformance report from EveUnity."
},
"evidencePaths": [
"tools/eveunity/eveunity-capture-contract.mjs",
"tools/eveunity/eveunity-capture-contract.test.mjs",
"scripts/run-eveunity-capture-contract-smoke.ps1",
"tools/eveunity/eveunity-uitoolkit-capture-artifact.mjs",
"tools/eveunity/eveunity-uitoolkit-capture-artifact.test.mjs",
"scripts/run-eveunity-uitoolkit-capture-smoke.ps1",
"web/fixtures/aetheria.provider-advertisement.json"
],
"pendingProofs": [
"Unity batchmode or editor capture artifact attached to the runtime conformance report."
]
}
},
"capture": {
"status": "json-projection",
"note": "Unity UI Toolkit attaches a provider-advertisement-backed gamecult.eve.unity_uitoolkit_projection.v1 JSON projection artifact; Unity editor or batchmode PNG capture remains an EveUnity split blocker.",
"artifacts": [
{
"kind": "json-projection",
"schema": "gamecult.eve.unity_uitoolkit_projection.v1",
"path": "artifacts/eveunity-uitoolkit-capture/latest/unity-uitoolkit-projection.json",
"runtimeId": "unity-uitoolkit",
"providerId": "aetheria",
"surfaceId": "aetheria.daemon.game",
"sourceSurfacePath": "web/fixtures/aetheria-world-surface.json",
"requestPath": "artifacts/eveunity-uitoolkit-capture/latest/capture-request.json"
}
]
}
}

View file

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

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

View file

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

View file

@ -0,0 +1,12 @@
param([string] $OutputRoot = "artifacts\packages")
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$packageRoot = Join-Path $repoRoot "packages\org.gamecult.eve.unity-uitoolkit"
$output = if ([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 UI Toolkit package failed to pack" }
$artifact = Join-Path $output ($packed | Select-Object -Last 1)
if (-not (Test-Path -LiteralPath $artifact)) { throw "EveUnity UI Toolkit artifact was not produced: $artifact" }
Write-Host "EveUnity UI Toolkit package: $artifact"

View file

@ -0,0 +1,45 @@
param(
[string] $AetheriaRoot = "E:\Projects\Aetheria",
[string] $UnityExe = "C:\Program Files\Unity\Hub\Editor\6000.4.2f1\Editor\Unity.exe",
[string] $OutputRoot = "artifacts\uitoolkit-consumer"
)
$ErrorActionPreference = "Stop"
$repoRoot = Split-Path -Parent $PSScriptRoot
$manifestPath = Join-Path $AetheriaRoot "Packages\manifest.json"
$output = if ([IO.Path]::IsPathRooted($OutputRoot)) { $OutputRoot } else { Join-Path $repoRoot $OutputRoot }
foreach ($required in @($manifestPath, $UnityExe, (Join-Path $repoRoot "packages\org.gamecult.eve.unity-uitoolkit"))) {
if (-not (Test-Path -LiteralPath $required)) { throw "Required EveUnity UI Toolkit test path not found: $required" }
}
$runRoot = Join-Path $output (Get-Date -Format "yyyyMMddTHHmmss")
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 -LiteralPath $manifestPath -Raw
try {
$manifest = $originalManifest | ConvertFrom-Json
$testables = @($manifest.testables) | Where-Object { $_ }
if ($testables -notcontains "org.gamecult.eve.unity-uitoolkit") { $testables += "org.gamecult.eve.unity-uitoolkit" }
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.UnityUIToolkit.Tests",
"-testResults", $resultsPath, "-logFile", $logPath
)
$process = Start-Process -FilePath $UnityExe -ArgumentList $arguments -Wait -PassThru -WindowStyle Hidden
if ($process.ExitCode -ne 0) {
Get-Content -LiteralPath $logPath -Tail 120
throw "EveUnity UI Toolkit tests failed with exit code $($process.ExitCode)"
}
} finally {
Set-Content -NoNewline -LiteralPath $manifestPath -Value $originalManifest
}
[xml]$results = Get-Content -LiteralPath $resultsPath -Raw
$run = $results.SelectSingleNode("//test-run")
if ($null -eq $run -or [int]$run.total -eq 0 -or [int]$run.failed -gt 0) { throw "EveUnity UI Toolkit tests did not pass: $resultsPath" }
Write-Host "EveUnity UI Toolkit tests: $($run.passed) passed, $($run.total) total"