diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index e432cd7..1339ca3 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -274,11 +274,19 @@ Exit criteria: ## Active Work Queue -1. Add a real Unity plugin-host proof for either Sai or Norn, backed by a - runtime-owned capability manifest and Unity test/capture lifecycle evidence. +1. Add a Unity Norn graph plugin-host proof for `embed.norn`. +2. Move EveUnity toward split readiness with runtime-owned UPM release, + Unity test, and capture lifecycle evidence. Recently cut: +- Unity UI Toolkit now has a first-party `sai.vn` plugin-host proof. The + runtime owns `IEveUiToolkitPluginHost`, registers + `SaiVisualNovelUiToolkitPluginHost` through `EveUiToolkitSurfaceOptions`, and + declares support for `vn.stage`, `story.choose`, `story.continue`, and + `story.jump` in its runtime capability manifest. Sai still owns story state + and command semantics; Unity only lowers the visual stage/dialogue/action + surface and emits Eve command requests. - Unity UI Toolkit now publishes a runtime-owned capability manifest at `packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json`. The parity harness validates that manifest against diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index 7f539c6..ba07355 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -26,6 +26,10 @@ transport schema, and incubation metadata. The central parity manifest may coordinate split-readiness, but the runtime body must publish the capability claim it expects other repos to consume. +Unity UI Toolkit currently uses this path for its first `sai.vn` plugin-host +proof while leaving `norn.graph` and `tex.math` as explicit unsupported plugin +semantics. + ## Run Semantic checks: diff --git a/packages/org.gamecult.eve.unity-uitoolkit/README.md b/packages/org.gamecult.eve.unity-uitoolkit/README.md index 667fc93..76631f3 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/README.md +++ b/packages/org.gamecult.eve.unity-uitoolkit/README.md @@ -7,11 +7,12 @@ 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 are not hosted here yet. `sai.vn`, `norn.graph`, and -`tex.math` surfaces may pass through as generic Eve component structure, but -Unity UI Toolkit does not currently own the Sai VN stage model, Norn graph -layout, or TeX render pipeline. Those remain plugin-owned semantics until an -explicit Unity plugin host exists. +Plugin semantics enter through runtime plugin hosts. The package includes a +first-party `sai.vn` host proof for `vn.stage`, dialogue panels, action rails, +and story command projection. Sai still owns story state and command semantics; +Unity only owns native projection and emits `gamecult.eve.command.v1` requests. +`norn.graph` and `tex.math` still pass through as generic Eve component +structure until they gain explicit Unity plugin hosts. 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 diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs index 662c227..e1a267b 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs @@ -76,6 +76,10 @@ namespace GameCult.Eve.UnityUIToolkit EveSurfaceDocument document, Action? commandSink) { + var pluginHost = _options.FindPluginHost(component); + if (pluginHost != null) + return pluginHost.Lower(component, document, commandSink); + switch (NormalizeKind(component.Kind)) { case "surface": diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceOptions.cs b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceOptions.cs index 4b04a38..4df4a70 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceOptions.cs +++ b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceOptions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using GameCult.Eve.Surface; #nullable enable @@ -10,11 +12,23 @@ namespace GameCult.Eve.UnityUIToolkit public static EveUiToolkitSurfaceOptions Default { get; } = new EveUiToolkitSurfaceOptions(); public EveUiToolkitSurfaceOptions( - Func? embeddedDocumentResolver = null) + Func? embeddedDocumentResolver = null, + IReadOnlyList? pluginHosts = null) { EmbeddedDocumentResolver = embeddedDocumentResolver; + PluginHosts = pluginHosts ?? new IEveUiToolkitPluginHost[] + { + new SaiVisualNovelUiToolkitPluginHost() + }; } public Func? EmbeddedDocumentResolver { get; } + + public IReadOnlyList PluginHosts { get; } + + public IEveUiToolkitPluginHost? FindPluginHost(EveSurfaceComponent component) + { + return PluginHosts.FirstOrDefault(host => host.CanLower(component)); + } } } diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/IEveUiToolkitPluginHost.cs b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/IEveUiToolkitPluginHost.cs new file mode 100644 index 0000000..77f04ee --- /dev/null +++ b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/IEveUiToolkitPluginHost.cs @@ -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 IEveUiToolkitPluginHost + { + string PluginId { get; } + + IReadOnlyList Capabilities { get; } + + bool CanLower(EveSurfaceComponent component); + + VisualElement Lower( + EveSurfaceComponent component, + EveSurfaceDocument document, + Action? commandSink); + } +} diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/IEveUiToolkitPluginHost.cs.meta b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/IEveUiToolkitPluginHost.cs.meta new file mode 100644 index 0000000..f8b26b7 --- /dev/null +++ b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/IEveUiToolkitPluginHost.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8e93b6d6e2364ef4a91766cbf5a44b62 diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/SaiVisualNovelUiToolkitPluginHost.cs b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/SaiVisualNovelUiToolkitPluginHost.cs new file mode 100644 index 0000000..4e9273b --- /dev/null +++ b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/SaiVisualNovelUiToolkitPluginHost.cs @@ -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 SaiVisualNovelUiToolkitPluginHost : IEveUiToolkitPluginHost + { + public string PluginId => "sai.vn"; + + public IReadOnlyList 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? 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-host"); + 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-host"); + 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-host"); + 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-host"); + 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? 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-host"); + 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? 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(); + } + } +} diff --git a/packages/org.gamecult.eve.unity-uitoolkit/Runtime/SaiVisualNovelUiToolkitPluginHost.cs.meta b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/SaiVisualNovelUiToolkitPluginHost.cs.meta new file mode 100644 index 0000000..e87f0bd --- /dev/null +++ b/packages/org.gamecult.eve.unity-uitoolkit/Runtime/SaiVisualNovelUiToolkitPluginHost.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4e8f6fc7d7ef4dd7b22ec33d4f81d321 diff --git a/packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json b/packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json index 82482bd..ac3299c 100644 --- a/packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json +++ b/packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json @@ -12,12 +12,19 @@ "supportedFeatures": [ "embeddedDocuments" ], - "supportedPlugins": [], - "unsupportedPlugins": [ + "supportedPlugins": [ { "pluginId": "sai.vn", - "reason": "Unity UI Toolkit currently lowers Sai VN component kinds as generic Eve containers; no Sai plugin host exists in this runtime." - }, + "capabilities": [ + "vn.stage", + "story.choose", + "story.continue", + "story.jump" + ], + "host": "SaiVisualNovelUiToolkitPluginHost" + } + ], + "unsupportedPlugins": [ { "pluginId": "norn.graph", "reason": "Unity UI Toolkit currently preserves embed.norn as generic Eve structure; no Norn graph plugin host or layout adapter exists in this runtime." diff --git a/packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json.meta b/packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json.meta new file mode 100644 index 0000000..0dd0209 --- /dev/null +++ b/packages/org.gamecult.eve.unity-uitoolkit/eve-runtime-capability.json.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c3f7a6e51d3f4bbd8d892922299df744 diff --git a/tools/parity/parity-manifest.json b/tools/parity/parity-manifest.json index fe17f24..0b985f3 100644 --- a/tools/parity/parity-manifest.json +++ b/tools/parity/parity-manifest.json @@ -429,9 +429,7 @@ "pendingProofs": [ "UPM package and release lifecycle are runtime-owned.", "Unity test and capture lifecycle are runtime-owned.", - "Plugin host behavior declares reusable Sai and Norn capabilities.", - "Aetheria consumes provider advertisements and surfaces without Eve internals.", - "Conformance pack can be consumed without depending on Eve's local harness layout." + "Norn graph plugin host behavior declares reusable embed.norn capabilities." ] } ], @@ -687,11 +685,13 @@ "requiredFixtures": ["embedded-surface"], "pluginFixtures": ["sai-vn"], "supportedFeatures": ["embeddedDocuments"], - "unsupportedPlugins": [ + "supportedPlugins": [ { "pluginId": "sai.vn", - "reason": "Unity UI Toolkit currently lowers Sai VN component kinds as generic Eve containers; no Sai plugin host exists in this runtime." - }, + "capabilities": ["vn.stage", "story.choose", "story.continue", "story.jump"] + } + ], + "unsupportedPlugins": [ { "pluginId": "norn.graph", "reason": "Unity UI Toolkit currently preserves embed.norn as generic Eve structure; no Norn graph plugin host or layout adapter exists in this runtime." @@ -701,11 +701,23 @@ "reason": "Unity UI Toolkit currently preserves embed.tex as generic Eve structure; no TeX render plugin host exists in this runtime." } ], - "expectedPaths": ["packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs"], + "expectedPaths": [ + "packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs", + "packages/org.gamecult.eve.unity-uitoolkit/Runtime/IEveUiToolkitPluginHost.cs", + "packages/org.gamecult.eve.unity-uitoolkit/Runtime/SaiVisualNovelUiToolkitPluginHost.cs" + ], "expectedSourceSymbols": [ { "path": "packages/org.gamecult.eve.unity-uitoolkit/Runtime/EveUiToolkitSurfaceLowerer.cs", - "contains": ["LowerEmbeddedDocument(", "EmbeddedDocuments", "eve-embedded-document"] + "contains": ["LowerEmbeddedDocument(", "EmbeddedDocuments", "eve-embedded-document", "FindPluginHost", "pluginHost.Lower"] + }, + { + "path": "packages/org.gamecult.eve.unity-uitoolkit/Runtime/IEveUiToolkitPluginHost.cs", + "contains": ["PluginId", "Capabilities", "CanLower", "Lower("] + }, + { + "path": "packages/org.gamecult.eve.unity-uitoolkit/Runtime/SaiVisualNovelUiToolkitPluginHost.cs", + "contains": ["sai.vn", "vn.stage", "story.choose", "story.continue", "story.jump", "StoryButton", "EmitStoryCommand", "EveSurfaceCommandRequest", "sai-vn-stage", "sai-vn-dialogue", "sai-vn-actions"] } ], "commandTransportSmoke": {