Own Unity surface and UI Toolkit packages
This commit is contained in:
parent
46ed99246b
commit
13526a9086
47 changed files with 3484 additions and 3 deletions
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
29
packages/org.gamecult.eve.surface/README.md
Normal file
29
packages/org.gamecult.eve.surface/README.md
Normal 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.
|
||||
7
packages/org.gamecult.eve.surface/README.md.meta
Normal file
7
packages/org.gamecult.eve.surface/README.md.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: fe5aa27636bead047b2aecb14fdc59f2
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
packages/org.gamecult.eve.surface/Runtime.meta
Normal file
8
packages/org.gamecult.eve.surface/Runtime.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2164f6fea1216214d895e7dc99c46ecf
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8f53654f7c98d894588127ba001f11b4
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: f7ea0cd07c4d5da4099b5acf8d44ca2e
|
||||
874
packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs
Normal file
874
packages/org.gamecult.eve.surface/Runtime/EveSurfaceBuilder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 97ac7f757692aef4db64d75d0df6fe51
|
||||
414
packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs
Normal file
414
packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs
Normal 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 ?? "";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6516c42104b235f42b15efeb4639bb73
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a9004278c14816345941af2f2ce673ca
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
18
packages/org.gamecult.eve.surface/package.json
Normal file
18
packages/org.gamecult.eve.surface/package.json
Normal 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"
|
||||
}
|
||||
7
packages/org.gamecult.eve.surface/package.json.meta
Normal file
7
packages/org.gamecult.eve.surface/package.json.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 49f1738bd94075e489c82190cf660b1d
|
||||
PackageManifestImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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."
|
||||
}
|
||||
]
|
||||
|
|
|
|||
150
packages/org.gamecult.eve.unity-uitoolkit/README.md
Normal file
150
packages/org.gamecult.eve.unity-uitoolkit/README.md
Normal 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.
|
||||
7
packages/org.gamecult.eve.unity-uitoolkit/README.md.meta
Normal file
7
packages/org.gamecult.eve.unity-uitoolkit/README.md.meta
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9dabe4b47bd743f41a4c0903fd309eae
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
8
packages/org.gamecult.eve.unity-uitoolkit/Runtime.meta
Normal file
8
packages/org.gamecult.eve.unity-uitoolkit/Runtime.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 40e1d9868e9fdbf408ac5bc46239c817
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: bef5882c69652c54fb26b07d63e5bcdd
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: e2fd027205cc9214eaa7dd84fe75d7bd
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 093779c1124fb5241b9c16e18173520a
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 8e93b6d6e2364ef4a91766cbf5a44b62
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 729936c97f3c45f7bba0fcd6de8b40f5
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 4e8f6fc7d7ef4dd7b22ec33d4f81d321
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: d6c655fae0c77064490eba9c3ed1fb4c
|
||||
8
packages/org.gamecult.eve.unity-uitoolkit/Tests.meta
Normal file
8
packages/org.gamecult.eve.unity-uitoolkit/Tests.meta
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 694b4921723a4d56a9f0f72ad2240feb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
fileFormatVersion: 2
|
||||
guid: acfcd889183e4bfbac82f8883deef300
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 2921e30d39fe49098bc3763c538ed67c
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 353e1aea044b46ac98835959a19fe0ac
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c3f7a6e51d3f4bbd8d892922299df744
|
||||
18
packages/org.gamecult.eve.unity-uitoolkit/package.json
Normal file
18
packages/org.gamecult.eve.unity-uitoolkit/package.json
Normal 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"
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 6c6c93dde27b8dc42bf3866cd5c6c3cb
|
||||
PackageManifestImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
12
scripts/pack-uitoolkit.ps1
Normal file
12
scripts/pack-uitoolkit.ps1
Normal 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"
|
||||
45
scripts/run-uitoolkit-tests.ps1
Normal file
45
scripts/run-uitoolkit-tests.ps1
Normal 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"
|
||||
Reference in a new issue