Add Unity Sai plugin host proof
This commit is contained in:
parent
5e650ec337
commit
84fd4933aa
12 changed files with 304 additions and 20 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ namespace GameCult.Eve.UnityUIToolkit
|
|||
EveSurfaceDocument document,
|
||||
Action<EveSurfaceCommandRequest>? commandSink)
|
||||
{
|
||||
var pluginHost = _options.FindPluginHost(component);
|
||||
if (pluginHost != null)
|
||||
return pluginHost.Lower(component, document, commandSink);
|
||||
|
||||
switch (NormalizeKind(component.Kind))
|
||||
{
|
||||
case "surface":
|
||||
|
|
|
|||
|
|
@ -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<EveEmbeddedDocumentSlot, EveSurfaceDocument?>? embeddedDocumentResolver = null)
|
||||
Func<EveEmbeddedDocumentSlot, EveSurfaceDocument?>? embeddedDocumentResolver = null,
|
||||
IReadOnlyList<IEveUiToolkitPluginHost>? pluginHosts = null)
|
||||
{
|
||||
EmbeddedDocumentResolver = embeddedDocumentResolver;
|
||||
PluginHosts = pluginHosts ?? new IEveUiToolkitPluginHost[]
|
||||
{
|
||||
new SaiVisualNovelUiToolkitPluginHost()
|
||||
};
|
||||
}
|
||||
|
||||
public Func<EveEmbeddedDocumentSlot, EveSurfaceDocument?>? EmbeddedDocumentResolver { get; }
|
||||
|
||||
public IReadOnlyList<IEveUiToolkitPluginHost> PluginHosts { get; }
|
||||
|
||||
public IEveUiToolkitPluginHost? FindPluginHost(EveSurfaceComponent component)
|
||||
{
|
||||
return PluginHosts.FirstOrDefault(host => host.CanLower(component));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<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,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<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-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<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-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<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
|
||||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: c3f7a6e51d3f4bbd8d892922299df744
|
||||
|
|
@ -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": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue