From f685c0406520373cde517cc7a44f0b896c090dc2 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 13 Jun 2026 16:15:03 +0100 Subject: [PATCH] Stage Eve UI Toolkit lowering boundary --- .voidbot/state/aetheria.cc | Bin 37229 -> 37532 bytes Aetheria.State.Unity.Smoke/Program.cs | 13 + .../Aetheria.State.Unity.csproj | 1 + Packages/manifest.json | 2 + .../Runtime/AetheriaRuntimeCatalogStore.cs | 141 +++++++++++ .../GameCult.Aetheria.State.Unity.asmdef | 1 + .../org.gamecult.aetheria.state/package.json | 3 + .../Runtime/EveSurfaceDocument.cs | 154 ++++++++++++ .../Runtime/GameCult.Eve.Surface.asmdef | 14 ++ .../org.gamecult.eve.surface/package.json | 15 ++ .../README.md | 12 + .../Runtime/EveUiToolkitSurfaceLowerer.cs | 232 ++++++++++++++++++ .../Runtime/EveUiToolkitSurfaceOptions.cs | 9 + .../GameCult.Eve.UnityUIToolkit.asmdef | 16 ++ .../package.json | 18 ++ Packages/packages-lock.json | 14 ++ docs/aetheria-perfect-machine-map.md | 42 +++- 17 files changed, 678 insertions(+), 9 deletions(-) create mode 100644 Packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs create mode 100644 Packages/org.gamecult.eve.surface/Runtime/GameCult.Eve.Surface.asmdef create mode 100644 Packages/org.gamecult.eve.surface/package.json create mode 100644 Packages/org.gamecult.eve.unity-uitoolkit/README.md create mode 100644 Packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs create mode 100644 Packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceOptions.cs create mode 100644 Packages/org.gamecult.eve.unity-uitoolkit/Runtime/GameCult.Eve.UnityUIToolkit.asmdef create mode 100644 Packages/org.gamecult.eve.unity-uitoolkit/package.json diff --git a/.voidbot/state/aetheria.cc b/.voidbot/state/aetheria.cc index d48e63357492729044ad49fc0c28b07cac79dbd6..472ec14a4baa96036bc5260e16794d6f81cb46dc 100644 GIT binary patch delta 1372 zcmZuxO=}ZD7}m7ZNFM&z?R%paPllrJnMXZ*=0t*vf#p8zRDul;9>_c-@sYus2d}(4PqjxSBkRfp z0g)@4k5dGcCN=5;0be-8hs#S8l#x~9P`Ff_hmbgRJOG5d(Rg!=QV9)@`4oIgmKd)A z1_qZ&y#k!FnpY9hqBe}Ap#a0RFpfvv{9DH-EZF>3;tUTfPZ-|?M{q2OLMX)&bE(XU z2B&Vcdv#ya6%Oujt)MDJ02hs~FA|J&+^()u<=L9B>IRmCMFjYf!qhw{%|THJzs_`_ zZ*5cDp^j#l5dxz$aG{|>9f9cyC|qE;E-j_z91x>DY)7x;*EQt|Z*G1rV!MpvuZyJl zg4c4I$^bkYg{g#%wD$?97>x+g&=9h&C8f|Hs>7Dx$c>~v(HO|?Pk<81{dNsSMsjUQ zQVBxBq|JfFqGp!miZI+oIYAPuug%4i5LL!eOUtEH9t#s~ZRgSUXW4_luNZA4a!!<@ zDpev7D4CG8e6(jZ+f2B%e>DGVOJ!ujEF?@hDSL#=nXz$ z3*o?mP)N%H3RFQawU4GW7r+&X4(Ju%T26b|V1vzR53b#szX4UYY-?2$aJ5)m0GB9q z7?CalLZZV^2AR4Bv_!%38-zP3+aj&1U)##@+TpLeZnmmu*{=BurO-`i?+Oe7389J+ zvCecNtc;1f`kBr@dLwCd^IUZQK{oy{6_4*|1LM)%^P|!J)se+kopJa7e5V&I-HGpW zXt$7PR>29f3DQwR^Jz&O-9K}kTt{u_sV(T=BGa+OE+gEJ{vJ9I-!AA zb=>m4%XSXgjxXFB**Vg8G5Rqu)_cTGPj()>PkmVX5flSMk$xKV;b)SwnYCsQJ9E-v N@$0oOJNv-t@;@_X@xlNA delta 1112 zcmai!%WD%+6vk~5inSFVv}PgJBly_VX_FXi;NVxm7Km43nKhSd%`B<09dXF|0Yll;35|6X3OQ~CD!}qE5>H0L9 z3M$S($Qt-T(dD{=8W@=H=Mr2mNd@x`7vnQk1cz%|R#7!TmVg@#HZ#nXWnjVq>7!zX zOQAsrs>E#6*0$|3?tSk&O4;GW@yU@4M&<3UhO8p5ui$Mjg7XW2H?xM>R1r2J>>v3MU_dS0|w zQ3k?ciqkqUV@n~uhP8-_2JFGb4X~0S$x)08(6nZtA@ChqS6e#2q<(h_%PFw-Upcoif7Th#Q;I0cUvscL+0g)ZcbFd_(yGqSnStAognU-Y+6bhoK z+r&Bzsb+AZ?F&RPfiT=7SDp`_e|r<_)MpNCYNsnQaGl5_smypW;Pivghc_OLHo>R< ztW3ZOOuI{$y#oig)HwhsDe3iYL)1gfEhnvs5ZfLe0n%QV}wOl6p0JOes><;vZ G`IW!FQ;%){ diff --git a/Aetheria.State.Unity.Smoke/Program.cs b/Aetheria.State.Unity.Smoke/Program.cs index fee7070e6..e8cc37d9a 100644 --- a/Aetheria.State.Unity.Smoke/Program.cs +++ b/Aetheria.State.Unity.Smoke/Program.cs @@ -10,6 +10,7 @@ var statePath = args.Length > 1 await using var client = await AetheriaRuntimeCatalogClient.OpenAsync(statePath); var catalog = client.ReadCatalog(); var packageCatalog = AetheriaRuntimeCatalogStore.OpenReadOnly(statePath); +var packageSurfaces = AetheriaRuntimeCatalogStore.ReadEveSurfaces(statePath); var surface = await client.ReadCatalogSurfaceAsync(); var commitSmokeDirectory = Path.Combine(Path.GetTempPath(), "aetheria-state-unity-smoke", Guid.NewGuid().ToString("N")); var commitSmokeStatePath = Path.Combine(commitSmokeDirectory, "aetheria-world.cc"); @@ -134,6 +135,17 @@ if (surface?.Schema != "gamecult.eve.surface.v1" || throw new InvalidOperationException("Runtime catalog client did not read the typed Eve surface."); } +var packageSurface = packageSurfaces.FirstOrDefault(candidate => candidate.Surface.Id == AetheriaCatalogSurfaceProjector.SurfaceId); +if (packageSurface == null || + packageSurface.Schema != "gamecult.eve.surface.v1" || + packageSurface.ProviderId != "aetheria" || + packageSurface.Surface.Root.Kind != "surface" || + packageSurface.Surface.Root.Children.Count == 0 || + packageSurface.Commands.All(command => command.Command != "aetheria.catalog.refresh")) +{ + throw new InvalidOperationException("Package runtime store did not read the typed Eve surface contract from CultCache."); +} + try { var commit = AetheriaRuntimeStateCommitLog.QueuePlayerSettings( @@ -235,4 +247,5 @@ Console.WriteLine($"Interior/hardpoint sample: {interior.Name} {interior.Interio Console.WriteLine($"Behavior payload sample: {behaviorHost.Name} {behaviorPayload.Kind}/{behaviorPayload.Fields.Count}"); Console.WriteLine($"Behavior sample: {behaviorKind}"); Console.WriteLine($"Eve surface: {surface.Surface.Id}"); +Console.WriteLine($"Package Eve surfaces: {packageSurfaces.Count}"); Console.WriteLine("Runtime state commit log smoke: settings and run zone/entity snapshots queued, applied, and cleared"); diff --git a/Aetheria.State.Unity/Aetheria.State.Unity.csproj b/Aetheria.State.Unity/Aetheria.State.Unity.csproj index df7dd6848..8f8348024 100644 --- a/Aetheria.State.Unity/Aetheria.State.Unity.csproj +++ b/Aetheria.State.Unity/Aetheria.State.Unity.csproj @@ -8,6 +8,7 @@ + diff --git a/Packages/manifest.json b/Packages/manifest.json index 1800e899e..258d9f2f0 100644 --- a/Packages/manifest.json +++ b/Packages/manifest.json @@ -1,5 +1,7 @@ { "dependencies": { + "org.gamecult.eve.surface": "file:org.gamecult.eve.surface", + "org.gamecult.eve.unity-uitoolkit": "file:org.gamecult.eve.unity-uitoolkit", "com.unity.2d.sprite": "1.0.0", "com.unity.2d.tilemap": "1.0.0", "com.unity.ai.navigation": "2.0.8", diff --git a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs index 5faf17d20..dad1e6bea 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs +++ b/Packages/org.gamecult.aetheria.state/Runtime/AetheriaRuntimeCatalogStore.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Collections.Generic; using System.IO; using System.Linq; +using GameCult.Eve.Surface; using MessagePack; #nullable enable @@ -14,6 +15,7 @@ namespace GameCult.Aetheria.State.Unity private const string ItemDefinitionSchema = "aetheria.item_definition"; private const string CorporationSchema = "aetheria.corporation"; private const string NameFileSchema = "aetheria.name_file"; + private const string EveSurfaceSchema = "gamecult.eve.surface"; public static AetheriaRuntimeCatalogSnapshot OpenReadOnly(string stateFilePath) { @@ -44,6 +46,24 @@ namespace GameCult.Aetheria.State.Unity nameFiles.ToArray()); } + public static IReadOnlyList ReadEveSurfaces(string stateFilePath) + { + if (string.IsNullOrWhiteSpace(stateFilePath)) + throw new ArgumentException("State file path must be non-empty.", nameof(stateFilePath)); + + var catalog = ReadSchemaCatalog(stateFilePath); + var surfaces = new List(); + foreach (var record in ReadRecords(stateFilePath)) + { + if (!catalog.TryGetValue(record.SchemaId, out var schemaName) || schemaName != EveSurfaceSchema) + continue; + + surfaces.Add(ReadEveSurface(record.Payload)); + } + + return surfaces; + } + private static Dictionary ReadSchemaCatalog(string stateFilePath) { if (!File.Exists(stateFilePath)) @@ -226,6 +246,23 @@ namespace GameCult.Aetheria.State.Unity return new AetheriaRuntimeNameFile(legacyId, name, nameCount, sampleNames, names); } + private static EveSurfaceDocument ReadEveSurface(byte[] payload) + { + var reader = new MessagePackReader(payload); + var fields = reader.ReadArrayHeader(); + var type = ReadFieldString(ref reader, fields, 0); + var schema = ReadFieldString(ref reader, fields, 1); + var providerId = ReadFieldString(ref reader, fields, 2); + var providerKind = ReadFieldString(ref reader, fields, 3); + var title = ReadFieldString(ref reader, fields, 4); + var version = ReadFieldInt64(ref reader, fields, 5); + var updatedAtUtc = ReadFieldString(ref reader, fields, 6); + var surface = ReadFieldEveSurfaceTree(ref reader, fields, 7); + var commands = ReadFieldEveCommandTemplates(ref reader, fields, 8); + SkipRemaining(ref reader, fields, 9); + return new EveSurfaceDocument(type, schema, providerId, providerKind, title, version, updatedAtUtc, surface, commands); + } + private static string ReadFieldString(ref MessagePackReader reader, int fields, int index) { return index >= fields ? "" : ReadString(ref reader); @@ -236,6 +273,11 @@ namespace GameCult.Aetheria.State.Unity return index >= fields ? 0 : reader.ReadInt32(); } + private static long ReadFieldInt64(ref MessagePackReader reader, int fields, int index) + { + return index >= fields ? 0 : reader.ReadInt64(); + } + private static double ReadFieldDouble(ref MessagePackReader reader, int fields, int index) { return index >= fields ? 0 : reader.ReadDouble(); @@ -273,6 +315,105 @@ namespace GameCult.Aetheria.State.Unity return allegiances; } + private static EveSurfaceTree ReadFieldEveSurfaceTree(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) + return new EveSurfaceTree("", EmptyEveComponent(), Array.Empty()); + + var surfaceFields = reader.ReadArrayHeader(); + var id = ReadFieldString(ref reader, surfaceFields, 0); + var root = ReadFieldEveComponent(ref reader, surfaceFields, 1); + var styles = ReadFieldEveStyleTokens(ref reader, surfaceFields, 2); + SkipRemaining(ref reader, surfaceFields, 3); + return new EveSurfaceTree(id, root, styles); + } + + private static EveSurfaceComponent ReadFieldEveComponent(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) + return EmptyEveComponent(); + + var componentFields = reader.ReadArrayHeader(); + var id = ReadFieldString(ref reader, componentFields, 0); + var kind = ReadFieldString(ref reader, componentFields, 1); + var props = ReadFieldStringMap(ref reader, componentFields, 2); + var children = ReadFieldEveComponents(ref reader, componentFields, 3); + SkipRemaining(ref reader, componentFields, 4); + return new EveSurfaceComponent(id, kind, props, children); + } + + private static IReadOnlyList ReadFieldEveComponents(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var components = new EveSurfaceComponent[count]; + for (var component = 0; component < count; component++) + components[component] = ReadFieldEveComponent(ref reader, 1, 0); + return components; + } + + private static IReadOnlyDictionary ReadFieldStringMap(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return EmptyStringMap(); + var count = reader.ReadMapHeader(); + if (count == 0) return EmptyStringMap(); + var map = new Dictionary(count, StringComparer.Ordinal); + for (var entry = 0; entry < count; entry++) + { + var key = ReadString(ref reader); + var value = ReadString(ref reader); + if (!string.IsNullOrWhiteSpace(key)) + map[key] = value; + } + + return map; + } + + private static IReadOnlyList ReadFieldEveStyleTokens(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var tokens = new EveStyleToken[count]; + for (var token = 0; token < count; token++) + { + var tokenFields = reader.ReadArrayHeader(); + var name = ReadFieldString(ref reader, tokenFields, 0); + var value = ReadFieldString(ref reader, tokenFields, 1); + SkipRemaining(ref reader, tokenFields, 2); + tokens[token] = new EveStyleToken(name, value); + } + + return tokens; + } + + private static IReadOnlyList ReadFieldEveCommandTemplates(ref MessagePackReader reader, int fields, int index) + { + if (index >= fields) return Array.Empty(); + var count = reader.ReadArrayHeader(); + var commands = new EveCommandTemplate[count]; + for (var command = 0; command < count; command++) + { + var commandFields = reader.ReadArrayHeader(); + var name = ReadFieldString(ref reader, commandFields, 0); + var label = ReadFieldString(ref reader, commandFields, 1); + var transport = ReadFieldString(ref reader, commandFields, 2); + SkipRemaining(ref reader, commandFields, 3); + commands[command] = new EveCommandTemplate(name, label, transport); + } + + return commands; + } + + private static EveSurfaceComponent EmptyEveComponent() + { + return new EveSurfaceComponent("", "", EmptyStringMap(), Array.Empty()); + } + + private static IReadOnlyDictionary EmptyStringMap() + { + return new Dictionary(0, StringComparer.Ordinal); + } + private static IReadOnlyList ReadShapeCells(ref MessagePackReader reader) { var count = reader.ReadArrayHeader(); diff --git a/Packages/org.gamecult.aetheria.state/Runtime/GameCult.Aetheria.State.Unity.asmdef b/Packages/org.gamecult.aetheria.state/Runtime/GameCult.Aetheria.State.Unity.asmdef index efe1f5133..607c4733f 100644 --- a/Packages/org.gamecult.aetheria.state/Runtime/GameCult.Aetheria.State.Unity.asmdef +++ b/Packages/org.gamecult.aetheria.state/Runtime/GameCult.Aetheria.State.Unity.asmdef @@ -2,6 +2,7 @@ "name": "GameCult.Aetheria.State.Unity", "rootNamespace": "GameCult.Aetheria.State.Unity", "references": [ + "GameCult.Eve.Surface", "MessagePack" ], "includePlatforms": [], diff --git a/Packages/org.gamecult.aetheria.state/package.json b/Packages/org.gamecult.aetheria.state/package.json index 8e56cee29..1b7ea5f6c 100644 --- a/Packages/org.gamecult.aetheria.state/package.json +++ b/Packages/org.gamecult.aetheria.state/package.json @@ -4,6 +4,9 @@ "version": "0.1.0", "unity": "2021.3", "description": "Unity runtime boundary for Aetheria typed CultCache state and future CultMesh catalog boot.", + "dependencies": { + "org.gamecult.eve.surface": "0.1.0" + }, "keywords": [ "Aetheria", "CultCache", diff --git a/Packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs b/Packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs new file mode 100644 index 000000000..bdfb19ff5 --- /dev/null +++ b/Packages/org.gamecult.eve.surface/Runtime/EveSurfaceDocument.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; + +#nullable enable + +namespace GameCult.Eve.Surface +{ + public sealed class EveSurfaceDocument + { + public EveSurfaceDocument( + string type, + string schema, + string providerId, + string providerKind, + string title, + long version, + string updatedAtUtc, + EveSurfaceTree surface, + IReadOnlyList commands) + { + Type = type; + Schema = schema; + ProviderId = providerId; + ProviderKind = providerKind; + Title = title; + Version = version; + UpdatedAtUtc = updatedAtUtc; + Surface = surface; + Commands = commands; + } + + public string Type { get; } + + public string Schema { get; } + + public string ProviderId { get; } + + public string ProviderKind { get; } + + public string Title { get; } + + public long Version { get; } + + public string UpdatedAtUtc { get; } + + public EveSurfaceTree Surface { get; } + + public IReadOnlyList Commands { get; } + } + + public sealed class EveSurfaceTree + { + public EveSurfaceTree(string id, EveSurfaceComponent root, IReadOnlyList styles) + { + Id = id; + Root = root; + Styles = styles; + } + + public string Id { get; } + + public EveSurfaceComponent Root { get; } + + public IReadOnlyList Styles { get; } + } + + public sealed class EveSurfaceComponent + { + public EveSurfaceComponent( + string id, + string kind, + IReadOnlyDictionary props, + IReadOnlyList children) + { + Id = id; + Kind = kind; + Props = props; + Children = children; + } + + public string Id { get; } + + public string Kind { get; } + + public IReadOnlyDictionary Props { get; } + + public IReadOnlyList Children { get; } + + public string GetProp(string key, string fallback = "") + { + return Props.TryGetValue(key, out var value) ? value : fallback; + } + } + + public sealed class EveStyleToken + { + public EveStyleToken(string name, string value) + { + Name = name; + Value = value; + } + + public string Name { get; } + + public string Value { get; } + } + + public sealed class EveCommandTemplate + { + public EveCommandTemplate(string command, string label, string transport) + { + Command = command; + Label = label; + Transport = transport; + } + + public string Command { get; } + + public string Label { get; } + + public string Transport { get; } + } + + public sealed class EveSurfaceCommandRequest + { + public EveSurfaceCommandRequest( + string providerId, + string surfaceId, + string command, + IReadOnlyDictionary payload, + DateTimeOffset issuedAt, + string clientId) + { + ProviderId = providerId; + SurfaceId = surfaceId; + Command = command; + Payload = payload; + IssuedAt = issuedAt; + ClientId = clientId; + } + + public string ProviderId { get; } + + public string SurfaceId { get; } + + public string Command { get; } + + public IReadOnlyDictionary Payload { get; } + + public DateTimeOffset IssuedAt { get; } + + public string ClientId { get; } + } +} diff --git a/Packages/org.gamecult.eve.surface/Runtime/GameCult.Eve.Surface.asmdef b/Packages/org.gamecult.eve.surface/Runtime/GameCult.Eve.Surface.asmdef new file mode 100644 index 000000000..d8f56beaa --- /dev/null +++ b/Packages/org.gamecult.eve.surface/Runtime/GameCult.Eve.Surface.asmdef @@ -0,0 +1,14 @@ +{ + "name": "GameCult.Eve.Surface", + "rootNamespace": "GameCult.Eve.Surface", + "references": [], + "includePlatforms": [], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": true +} diff --git a/Packages/org.gamecult.eve.surface/package.json b/Packages/org.gamecult.eve.surface/package.json new file mode 100644 index 000000000..bea317d41 --- /dev/null +++ b/Packages/org.gamecult.eve.surface/package.json @@ -0,0 +1,15 @@ +{ + "name": "org.gamecult.eve.surface", + "displayName": "Eve Surface Contract", + "version": "0.1.0", + "unity": "2021.3", + "description": "Unity-compatible DTOs for gamecult.eve.surface.v1 documents.", + "keywords": [ + "Eve", + "CultUI", + "CultMesh", + "Surface" + ], + "license": "MPL-2.0", + "category": "Scripting" +} diff --git a/Packages/org.gamecult.eve.unity-uitoolkit/README.md b/Packages/org.gamecult.eve.unity-uitoolkit/README.md new file mode 100644 index 000000000..063df5f9c --- /dev/null +++ b/Packages/org.gamecult.eve.unity-uitoolkit/README.md @@ -0,0 +1,12 @@ +# Eve UI Toolkit Lowering + +This package lowers `gamecult.eve.surface.v1` retained surface documents into +Unity UI Toolkit `VisualElement` trees. + +It owns native projection only. Providers still own truth, accepted state, +style token values, and command effects through CultMesh/CultNet. Unknown +component kinds degrade to inert containers instead of gaining local semantics. + +This copy is staged in Aetheria because the neighboring Eve repository is dirty +on unrelated work. The intended upstream home is the Eve repository as an +importable Unity package. diff --git a/Packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs b/Packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs new file mode 100644 index 000000000..5be4aa809 --- /dev/null +++ b/Packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using GameCult.Eve.Surface; +using UnityEngine; +using UnityEngine.UIElements; + +#nullable enable + +namespace GameCult.Eve.UnityUIToolkit +{ + public sealed class EveUiToolkitSurfaceLowerer + { + private readonly EveUiToolkitSurfaceOptions _options; + + public EveUiToolkitSurfaceLowerer(EveUiToolkitSurfaceOptions? options = null) + { + _options = options ?? EveUiToolkitSurfaceOptions.Default; + } + + public VisualElement Lower(EveSurfaceDocument document, Action? 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? commandSink) + { + var element = CreateElement(component, document, commandSink); + element.name = SafeName(component.Id); + element.AddToClassList("eve-component"); + element.AddToClassList($"eve-kind-{SafeClass(component.Kind)}"); + element.userData = component; + + foreach (var child in component.Children) + element.Add(LowerComponent(child, document, commandSink)); + + return element; + } + + private VisualElement CreateElement( + EveSurfaceComponent component, + EveSurfaceDocument document, + Action? commandSink) + { + switch (NormalizeKind(component.Kind)) + { + case "surface": + { + var element = new VisualElement(); + element.style.flexGrow = 1; + element.style.flexDirection = FlexDirection.Column; + return element; + } + case "grid": + { + var element = new VisualElement(); + element.style.flexDirection = FlexDirection.Row; + element.style.flexWrap = Wrap.Wrap; + element.style.alignItems = Align.Stretch; + return element; + } + case "card": + { + var card = new VisualElement(); + card.AddToClassList("eve-card"); + card.style.flexDirection = FlexDirection.Column; + var title = component.GetProp("title"); + if (!string.IsNullOrWhiteSpace(title)) + card.Add(TitleLabel(title)); + return card; + } + case "metric": + { + var metric = new VisualElement(); + metric.AddToClassList("eve-metric"); + metric.style.flexDirection = FlexDirection.Column; + metric.Add(MutedLabel(component.GetProp("label"))); + metric.Add(ValueLabel(component.GetProp("value"))); + return metric; + } + case "inspector.kv": + { + var inspector = new VisualElement(); + inspector.AddToClassList("eve-inspector"); + inspector.style.flexDirection = FlexDirection.Column; + return inspector; + } + case "row": + { + var row = new VisualElement(); + row.AddToClassList("eve-row"); + row.style.flexDirection = FlexDirection.Row; + row.style.flexWrap = Wrap.Wrap; + foreach (var prop in component.Props) + row.Add(FieldLabel(prop.Key, prop.Value)); + return row; + } + case "text": + { + return BodyLabel(component.GetProp("text", component.GetProp("value", component.GetProp("title")))); + } + case "control.button": + { + var label = component.GetProp("label", component.GetProp("title", "Invoke")); + var command = component.GetProp("command", component.GetProp("action", "invoke")); + return new Button(() => EmitCommand(document, component, command, commandSink)) { text = label }; + } + 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? commandSink) + { + if (commandSink == null || string.IsNullOrWhiteSpace(command)) + return; + + commandSink(new EveSurfaceCommandRequest( + document.ProviderId, + document.Surface.Id, + command, + new Dictionary(component.Props, StringComparer.Ordinal), + DateTimeOffset.UtcNow, + "unity-uitoolkit")); + } + + private static Label TitleLabel(string text) + { + var label = new Label(text); + label.AddToClassList("eve-title"); + label.style.unityFontStyleAndWeight = FontStyle.Bold; + return label; + } + + private static Label BodyLabel(string text) + { + var label = new Label(text); + label.AddToClassList("eve-text"); + return label; + } + + private static Label MutedLabel(string text) + { + var label = new Label(text); + label.AddToClassList("eve-muted"); + return label; + } + + private static Label ValueLabel(string text) + { + var label = new Label(text); + label.AddToClassList("eve-value"); + label.style.unityFontStyleAndWeight = FontStyle.Bold; + return label; + } + + private static VisualElement FieldLabel(string key, string value) + { + var field = new VisualElement(); + field.AddToClassList("eve-field"); + field.style.flexDirection = FlexDirection.Row; + field.Add(MutedLabel(key)); + field.Add(BodyLabel(value)); + return field; + } + + private static void ApplyStyleTokens(VisualElement root, IReadOnlyList tokens) + { + foreach (var token in tokens) + { + if (string.IsNullOrWhiteSpace(token.Name) || string.IsNullOrWhiteSpace(token.Value)) + continue; + + root.AddToClassList($"eve-style-token-{SafeClass(token.Name)}"); + } + } + + private static string NormalizeKind(string kind) + { + if (string.IsNullOrWhiteSpace(kind)) + return ""; + + if (kind == "panel.dialogue") + return "panel"; + if (kind.StartsWith("text.", StringComparison.Ordinal)) + return "text"; + if (kind.StartsWith("control.button.", StringComparison.Ordinal)) + return "control.button"; + + 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(); + } + } +} diff --git a/Packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceOptions.cs b/Packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceOptions.cs new file mode 100644 index 000000000..53cac247f --- /dev/null +++ b/Packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceOptions.cs @@ -0,0 +1,9 @@ +#nullable enable + +namespace GameCult.Eve.UnityUIToolkit +{ + public sealed class EveUiToolkitSurfaceOptions + { + public static EveUiToolkitSurfaceOptions Default { get; } = new EveUiToolkitSurfaceOptions(); + } +} diff --git a/Packages/org.gamecult.eve.unity-uitoolkit/Runtime/GameCult.Eve.UnityUIToolkit.asmdef b/Packages/org.gamecult.eve.unity-uitoolkit/Runtime/GameCult.Eve.UnityUIToolkit.asmdef new file mode 100644 index 000000000..9fc5a5363 --- /dev/null +++ b/Packages/org.gamecult.eve.unity-uitoolkit/Runtime/GameCult.Eve.UnityUIToolkit.asmdef @@ -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 +} diff --git a/Packages/org.gamecult.eve.unity-uitoolkit/package.json b/Packages/org.gamecult.eve.unity-uitoolkit/package.json new file mode 100644 index 000000000..cf9108717 --- /dev/null +++ b/Packages/org.gamecult.eve.unity-uitoolkit/package.json @@ -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" +} diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index da06d3262..aaa45b08e 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -1,5 +1,19 @@ { "dependencies": { + "org.gamecult.eve.surface": { + "version": "file:org.gamecult.eve.surface", + "depth": 0, + "source": "embedded", + "dependencies": {} + }, + "org.gamecult.eve.unity-uitoolkit": { + "version": "file:org.gamecult.eve.unity-uitoolkit", + "depth": 0, + "source": "embedded", + "dependencies": { + "org.gamecult.eve.surface": "0.1.0" + } + }, "com.unity.2d.sprite": { "version": "1.0.0", "depth": 0, diff --git a/docs/aetheria-perfect-machine-map.md b/docs/aetheria-perfect-machine-map.md index 518497ba1..87a38c837 100644 --- a/docs/aetheria-perfect-machine-map.md +++ b/docs/aetheria-perfect-machine-map.md @@ -189,6 +189,11 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. `eve:surface:aetheria.catalog.operator` with summary, trade-catalog, and corporation views. This is a typed CultCache surface document, not a renderer dashboard or JSON status card. +- The embedded Unity state package can now read `gamecult.eve.surface` records + directly from the CultCache `.cc` store into the shared Eve surface contract + DTOs. This gives Unity the same provider-owned retained tree that the + SDK-style state client reads, without JSON fixtures, HTTP dashboard + summaries, or renderer-owned state. - `GameData/aetheria-world.cc` is now materialized from the importer as the project-local typed state file for the checked-in catalog. The importer stores relative provenance in the state document, not machine-local absolute paths. @@ -271,7 +276,11 @@ legacy catalog cache, and legacy UI paths should be migration-only or deleted. - Runtime UI is old Unity UI/uGUI prefabs plus `MonoBehaviour` scripts under `Assets/Scripts/UI/` and `Assets/Prefabs/UI/`. - Aetheria already has Unity UIElements support available through - `com.unity.modules.uielements`, but no imported Eve Unity lowering package. + `com.unity.modules.uielements`. It now imports local staged + `org.gamecult.eve.surface` and `org.gamecult.eve.unity-uitoolkit` packages: + the first carries no-engine `gamecult.eve.surface.v1` contract DTOs, and the + second lowers those retained trees into UI Toolkit `VisualElement` trees. + These packages are renderer boundaries, not UI truth owners. ## Invariants @@ -396,10 +405,16 @@ a bespoke Unity lowering. The existing CultLib Unity uGUI package remains useful as prior art for resolver-backed controls and inspector generation, but it is not the new portable UI authority. -Until that shared Eve package exists, Aetheria publishes provider surfaces as -typed CultCache documents using a local mirror of `gamecult.eve.surface.v1`. -The mirror is not a new UI authority; it is the state provider contract that a -future Eve package should replace or align with. +Until the shared Eve package lands upstream, Aetheria carries a local staged +Unity package pair: + +- `org.gamecult.eve.surface`: no-engine DTOs for `gamecult.eve.surface.v1`. +- `org.gamecult.eve.unity-uitoolkit`: UI Toolkit lowering from those DTOs to + native `VisualElement` trees. + +These are import boundaries, not Aetheria UI authority. The provider-owned +surface document in CultCache/CultMesh remains the truth, and the lowerer only +projects it. First Aetheria surfaces to publish: @@ -519,7 +534,14 @@ First Aetheria surfaces to publish: 6. Eve UI - Done: publish the typed catalog operator surface from `Aetheria.State`. - - Build or import the Eve UI Toolkit lowering package from the Eve repo. + - Done: stage the Eve surface contract DTO package and UI Toolkit lowering + package as importable Unity packages in Aetheria while the neighboring Eve + repo is dirty on unrelated work. + - Done: teach the embedded Unity state package to read + `gamecult.eve.surface` records from the `.cc` store into that shared + contract. + - Move the staged packages into the Eve repo once its worktree is clean, then + import them back into Aetheria from Eve instead of carrying a local copy. - Replace the old IMGUI DB inspector first, because it is closest to state authority. - Then replace runtime HUD/menu/inventory/map screens. @@ -587,9 +609,11 @@ First Aetheria surfaces to publish: package-owned runtime catalog read models without a MessagePack catalog cache, including typed item masks, interior masks, hardpoint definitions, corporation/name-file links, and typed behavior payloads. It also proves the - Unity runtime state commit log can queue player-settings and run snapshot - commands, the `Aetheria.State` node can apply them into canonical typed - settings/run/zone/entity state, and commands are cleared after application. + embedded package can read provider-owned Eve surface documents from the same + CultCache store. The smoke proves the Unity runtime state commit log can + queue player-settings and run snapshot commands, the `Aetheria.State` node + can apply them into canonical typed settings/run/zone/entity state, and + commands are cleared after application. - Unity play smoke proves runtime UI reads from Eve surfaces and sends commands through the shared state service. - UI Toolkit lowering parity compares Aetheria surfaces against the Eve browser