asecli 0.6.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- asecli/__init__.py +3 -0
- asecli/bridge/__init__.py +40 -0
- asecli/bridge/_editor_port_contract.py +131 -0
- asecli/bridge/_editor_primitives.py +205 -0
- asecli/bridge/_editor_spec_grammar.py +245 -0
- asecli/bridge/_editor_spec_io.py +26 -0
- asecli/bridge/_editor_spec_validation.py +218 -0
- asecli/bridge/_gui_handoff_store.py +78 -0
- asecli/bridge/_gui_project.py +35 -0
- asecli/bridge/_gui_resource_store.py +159 -0
- asecli/bridge/_gui_resource_upgrade.py +122 -0
- asecli/bridge/editor_create.py +173 -0
- asecli/bridge/editor_spec.py +250 -0
- asecli/bridge/graph_geometry.py +249 -0
- asecli/bridge/graph_geometry_parser.py +57 -0
- asecli/bridge/graph_inspect.py +136 -0
- asecli/bridge/gui_handoff.py +100 -0
- asecli/bridge/gui_presentation.py +70 -0
- asecli/bridge/gui_provider_detection.py +197 -0
- asecli/bridge/gui_runtime_probe.py +25 -0
- asecli/bridge/gui_support.py +240 -0
- asecli/bridge/gui_support_resource.py +58 -0
- asecli/bridge/mcp_client.py +231 -0
- asecli/bridge/recompile.py +107 -0
- asecli/bridge/resource_text.py +12 -0
- asecli/bridge/resources/asecli_material_gui.authoring.part00.cs.txt +238 -0
- asecli/bridge/resources/asecli_material_gui.authoring.part01.cs.txt +235 -0
- asecli/bridge/resources/asecli_material_gui.authoring.store.cs.txt +125 -0
- asecli/bridge/resources/asecli_material_gui.condition.cs.txt +99 -0
- asecli/bridge/resources/asecli_material_gui.hydration.cs.txt +105 -0
- asecli/bridge/resources/asecli_material_gui.part00.cs.txt +300 -0
- asecli/bridge/resources/asecli_material_gui.part01.cs.txt +290 -0
- asecli/bridge/resources/asecli_material_gui.reconciliation.cs.txt +174 -0
- asecli/bridge/resources/asecli_material_gui.transaction.cs.txt +108 -0
- asecli/bridge/resources/editor_create.part00.cs.txt +176 -0
- asecli/bridge/resources/editor_create.part01.cs.txt +145 -0
- asecli/bridge/resources/editor_create.part02.cs.txt +139 -0
- asecli/bridge/resources/editor_create.part03.cs.txt +161 -0
- asecli/bridge/resources/editor_create.part04.cs.txt +114 -0
- asecli/bridge/resources/wire_route.transaction.cs.txt +140 -0
- asecli/bridge/wire_route.py +127 -0
- asecli/checks/__init__.py +10 -0
- asecli/checks/checksum.py +53 -0
- asecli/checks/local_vars.py +135 -0
- asecli/checks/usage.py +141 -0
- asecli/checks/validate.py +129 -0
- asecli/cli/__init__.py +1 -0
- asecli/cli/commands.py +237 -0
- asecli/cli/commentary_command.py +158 -0
- asecli/cli/create_command.py +250 -0
- asecli/cli/custom_gui_command.py +170 -0
- asecli/cli/gui_provider.py +30 -0
- asecli/cli/gui_support_command.py +35 -0
- asecli/cli/io.py +214 -0
- asecli/cli/layout_command.py +213 -0
- asecli/cli/main.py +245 -0
- asecli/cli/recompile_metadata.py +53 -0
- asecli/cli/skill_command.py +121 -0
- asecli/cli/usage_command.py +42 -0
- asecli/core/__init__.py +84 -0
- asecli/core/comment_bounds.py +166 -0
- asecli/core/comment_layout.py +167 -0
- asecli/core/comment_metrics.py +132 -0
- asecli/core/comment_purpose.py +140 -0
- asecli/core/commentary.py +225 -0
- asecli/core/compiled_metadata.py +135 -0
- asecli/core/compiled_properties.py +190 -0
- asecli/core/custom_gui.py +250 -0
- asecli/core/custom_gui_versions.py +35 -0
- asecli/core/fishbone_placement.py +159 -0
- asecli/core/fishbone_topology.py +186 -0
- asecli/core/graph_ops.py +122 -0
- asecli/core/layout.py +174 -0
- asecli/core/layout_audit.py +237 -0
- asecli/core/layout_audit_geometry.py +247 -0
- asecli/core/layout_audit_repeated.py +44 -0
- asecli/core/layout_graph.py +113 -0
- asecli/core/local_vars.py +54 -0
- asecli/core/material_gui_condition.py +68 -0
- asecli/core/material_gui_protocol.py +35 -0
- asecli/core/material_gui_spec.py +191 -0
- asecli/core/meticulous_layout.py +114 -0
- asecli/core/model.py +237 -0
- asecli/core/property_presentation.py +233 -0
- asecli/core/wire_geometry.py +83 -0
- asecli/core/wire_router.py +248 -0
- asecli/schema/__init__.py +49 -0
- asecli/schema/data/observed.json +16 -0
- asecli/schema/data/schemas.json +14106 -0
- asecli/skills/asecli/SKILL.md +275 -0
- asecli/skills/asecli/references/layout-standard.md +99 -0
- asecli/skills/asecli/references/master-output-settings-standard.md +51 -0
- asecli/skills/asecli/references/material-property-standard.md +108 -0
- asecli-0.6.2.dist-info/METADATA +594 -0
- asecli-0.6.2.dist-info/RECORD +98 -0
- asecli-0.6.2.dist-info/WHEEL +4 -0
- asecli-0.6.2.dist-info/entry_points.txt +2 -0
- asecli-0.6.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""Parser for backward-compatible ASECLI graph geometry payloads."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
|
|
7
|
+
from .mcp_client import McpError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_geometry_payload(payload: str, geometry_type):
|
|
11
|
+
version = payload.splitlines()[0]
|
|
12
|
+
nodes: dict[str, dict] = {}
|
|
13
|
+
ports: list[tuple[str, str, str, float, float, str]] = []
|
|
14
|
+
for row in payload.splitlines()[1:]:
|
|
15
|
+
parts = row.split("|")
|
|
16
|
+
if parts[0] == "N" and len(parts) in {7, 8}:
|
|
17
|
+
try:
|
|
18
|
+
values = [float(value) for value in parts[2:7]]
|
|
19
|
+
except ValueError as exc:
|
|
20
|
+
raise McpError("MCP graph-geometry result contains non-numeric node geometry") from exc
|
|
21
|
+
if parts[1] in nodes or min(values[2:]) <= 0:
|
|
22
|
+
raise McpError(f"MCP graph-geometry result contains invalid node geometry for {parts[1]}")
|
|
23
|
+
node_title = ""
|
|
24
|
+
if len(parts) == 8 and version == "ASECLI_GEOMETRY_V3":
|
|
25
|
+
try:
|
|
26
|
+
node_title = base64.b64decode(parts[7]).decode("utf-8")
|
|
27
|
+
except Exception as exc:
|
|
28
|
+
raise McpError("MCP graph-geometry result contains malformed node title") from exc
|
|
29
|
+
nodes[parts[1]] = dict(
|
|
30
|
+
x=values[0], y=values[1], width=values[2], height=values[3],
|
|
31
|
+
title_height=values[4], input_ports={}, output_ports={},
|
|
32
|
+
node_title=node_title, input_port_labels={}, output_port_labels={},
|
|
33
|
+
)
|
|
34
|
+
elif parts[0] in {"I", "O"} and len(parts) in {5, 6}:
|
|
35
|
+
try:
|
|
36
|
+
label = ""
|
|
37
|
+
if len(parts) == 6 and version == "ASECLI_GEOMETRY_V3":
|
|
38
|
+
label = base64.b64decode(parts[5]).decode("utf-8")
|
|
39
|
+
ports.append((parts[0], parts[1], parts[2], float(parts[3]), float(parts[4]), label))
|
|
40
|
+
except ValueError as exc:
|
|
41
|
+
raise McpError("MCP graph-geometry result contains non-numeric port geometry") from exc
|
|
42
|
+
except Exception as exc:
|
|
43
|
+
raise McpError("MCP graph-geometry result contains malformed port label") from exc
|
|
44
|
+
elif row:
|
|
45
|
+
raise McpError("MCP graph-geometry result contains a malformed row")
|
|
46
|
+
for direction, node_id, port_id, x, y, label in ports:
|
|
47
|
+
if node_id not in nodes:
|
|
48
|
+
raise McpError(f"MCP graph-geometry port references missing node {node_id}")
|
|
49
|
+
key = "input_ports" if direction == "I" else "output_ports"
|
|
50
|
+
if port_id in nodes[node_id][key]:
|
|
51
|
+
raise McpError(f"MCP graph-geometry result contains duplicate port {node_id}:{port_id}")
|
|
52
|
+
nodes[node_id][key][port_id] = (x - nodes[node_id]["x"], y - nodes[node_id]["y"])
|
|
53
|
+
label_key = "input_port_labels" if direction == "I" else "output_port_labels"
|
|
54
|
+
nodes[node_id][label_key][port_id] = label
|
|
55
|
+
if not nodes:
|
|
56
|
+
raise McpError("MCP graph-geometry result is empty")
|
|
57
|
+
return {node_id: geometry_type(**values) for node_id, values in nodes.items()}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Read editor-accurate ASE node bounds through MCP for visual layout checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from .mcp_client import McpClient, McpError, tool_text
|
|
9
|
+
from .recompile import _detect_project_root
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
BOUNDS_SNIPPET = r'''
|
|
13
|
+
string assetPath = {asset_path};
|
|
14
|
+
var shader = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Shader>(assetPath);
|
|
15
|
+
if (shader == null) throw new System.Exception("shader not found: " + assetPath);
|
|
16
|
+
var previousWindow = AmplifyShaderEditor.UIUtils.CurrentWindow;
|
|
17
|
+
var previousSelection = UnityEditor.Selection.activeObject;
|
|
18
|
+
AmplifyShaderEditor.AmplifyShaderEditorWindow win = null;
|
|
19
|
+
try
|
|
20
|
+
{{
|
|
21
|
+
win = UnityEditor.EditorWindow.CreateInstance<AmplifyShaderEditor.AmplifyShaderEditorWindow>();
|
|
22
|
+
AmplifyShaderEditor.UIUtils.CurrentWindow = win;
|
|
23
|
+
var uiTextInfo = typeof(AmplifyShaderEditor.UIUtils).GetField(
|
|
24
|
+
"m_textInfo",
|
|
25
|
+
System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
|
|
26
|
+
if (uiTextInfo != null && uiTextInfo.GetValue(null) == null)
|
|
27
|
+
uiTextInfo.SetValue(null, new System.Globalization.CultureInfo("en-US", false).TextInfo);
|
|
28
|
+
|
|
29
|
+
var delayedLoad = typeof(AmplifyShaderEditor.AmplifyShaderEditorWindow).GetField(
|
|
30
|
+
"m_delayedLoadObject",
|
|
31
|
+
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
|
32
|
+
if (delayedLoad == null)
|
|
33
|
+
throw new System.Exception("ASE delayed-load field not found");
|
|
34
|
+
delayedLoad.SetValue(win, shader);
|
|
35
|
+
win.Show();
|
|
36
|
+
|
|
37
|
+
var layoutEvent = new UnityEngine.Event();
|
|
38
|
+
layoutEvent.type = UnityEngine.EventType.Layout;
|
|
39
|
+
win.SendEvent(layoutEvent);
|
|
40
|
+
var repaintEvent = new UnityEngine.Event();
|
|
41
|
+
repaintEvent.type = UnityEngine.EventType.Repaint;
|
|
42
|
+
win.SendEvent(repaintEvent);
|
|
43
|
+
|
|
44
|
+
if (win.CurrentGraph == null || win.CurrentGraph.CurrentMasterNode == null)
|
|
45
|
+
throw new System.Exception("ASE graph did not load in GUI event");
|
|
46
|
+
var sb = new System.Text.StringBuilder("ASECLI_BOUNDS_V1\n");
|
|
47
|
+
foreach (var node in win.CurrentGraph.AllNodes)
|
|
48
|
+
{{
|
|
49
|
+
var rect = node.TruePosition;
|
|
50
|
+
sb.Append(node.UniqueId).Append('|')
|
|
51
|
+
.Append(rect.x.ToString("R", System.Globalization.CultureInfo.InvariantCulture)).Append('|')
|
|
52
|
+
.Append(rect.y.ToString("R", System.Globalization.CultureInfo.InvariantCulture)).Append('|')
|
|
53
|
+
.Append(rect.width.ToString("R", System.Globalization.CultureInfo.InvariantCulture)).Append('|')
|
|
54
|
+
.Append(rect.height.ToString("R", System.Globalization.CultureInfo.InvariantCulture)).Append('\n');
|
|
55
|
+
}}
|
|
56
|
+
return sb.ToString();
|
|
57
|
+
}}
|
|
58
|
+
finally
|
|
59
|
+
{{
|
|
60
|
+
UnityEditor.Selection.activeObject = previousSelection;
|
|
61
|
+
AmplifyShaderEditor.UIUtils.CurrentWindow = previousWindow;
|
|
62
|
+
if (win != null)
|
|
63
|
+
{{
|
|
64
|
+
win.Close();
|
|
65
|
+
UnityEngine.Object.DestroyImmediate(win);
|
|
66
|
+
}}
|
|
67
|
+
}}
|
|
68
|
+
'''
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def measure_node_bounds_via_mcp(
|
|
72
|
+
shader_path: str,
|
|
73
|
+
*,
|
|
74
|
+
mcp_url: str = "http://127.0.0.1:8080/mcp",
|
|
75
|
+
instance_token: str | None = None,
|
|
76
|
+
unity_instance: str | None = None,
|
|
77
|
+
allow_remote_mcp: bool = False,
|
|
78
|
+
) -> dict[str, tuple[float, float, float, float]]:
|
|
79
|
+
"""Load a fresh hidden ASE window and return each node's runtime TruePosition."""
|
|
80
|
+
path = Path(shader_path).resolve()
|
|
81
|
+
if not path.exists():
|
|
82
|
+
raise FileNotFoundError(shader_path)
|
|
83
|
+
project_root = _detect_project_root(path)
|
|
84
|
+
asset_path = path.as_posix().removeprefix(project_root.as_posix() + "/")
|
|
85
|
+
client = McpClient(
|
|
86
|
+
mcp_url,
|
|
87
|
+
instance_token=instance_token,
|
|
88
|
+
allow_remote=allow_remote_mcp,
|
|
89
|
+
)
|
|
90
|
+
client.connect()
|
|
91
|
+
arguments = {
|
|
92
|
+
"action": "execute",
|
|
93
|
+
"code": BOUNDS_SNIPPET.format(asset_path=json.dumps(asset_path)),
|
|
94
|
+
}
|
|
95
|
+
if unity_instance is not None:
|
|
96
|
+
arguments["unity_instance"] = unity_instance
|
|
97
|
+
result = client.call_tool("execute_code", arguments)
|
|
98
|
+
envelope_text = tool_text(result, instance_token)
|
|
99
|
+
try:
|
|
100
|
+
envelope = json.loads(envelope_text)
|
|
101
|
+
except json.JSONDecodeError as exc:
|
|
102
|
+
raise McpError("MCP node-bounds result has an unexpected envelope") from exc
|
|
103
|
+
if not isinstance(envelope, dict):
|
|
104
|
+
raise McpError("MCP node-bounds result has an unexpected envelope")
|
|
105
|
+
if envelope.get("success") is False:
|
|
106
|
+
data = envelope.get("data") if isinstance(envelope.get("data"), dict) else {}
|
|
107
|
+
detail = envelope.get("message") or data.get("reason") or "Unity execution failed"
|
|
108
|
+
available = data.get("available_instances")
|
|
109
|
+
if isinstance(available, list) and available:
|
|
110
|
+
detail = f"{detail}; available instances: {', '.join(str(item) for item in available)}"
|
|
111
|
+
raise McpError(str(detail))
|
|
112
|
+
try:
|
|
113
|
+
payload = envelope["data"]["result"]
|
|
114
|
+
except (KeyError, TypeError) as exc:
|
|
115
|
+
raise McpError("MCP node-bounds result has an unexpected envelope") from exc
|
|
116
|
+
if not isinstance(payload, str) or not payload.startswith("ASECLI_BOUNDS_V1\n"):
|
|
117
|
+
raise McpError("MCP node-bounds result has an unexpected payload")
|
|
118
|
+
|
|
119
|
+
bounds: dict[str, tuple[float, float, float, float]] = {}
|
|
120
|
+
for line in payload.splitlines()[1:]:
|
|
121
|
+
if not line:
|
|
122
|
+
continue
|
|
123
|
+
parts = line.split("|")
|
|
124
|
+
if len(parts) != 5:
|
|
125
|
+
raise McpError("MCP node-bounds result contains a malformed row")
|
|
126
|
+
node_id = parts[0]
|
|
127
|
+
try:
|
|
128
|
+
rect = tuple(float(value) for value in parts[1:])
|
|
129
|
+
except ValueError as exc:
|
|
130
|
+
raise McpError("MCP node-bounds result contains a non-numeric rectangle") from exc
|
|
131
|
+
if node_id in bounds:
|
|
132
|
+
raise McpError(f"MCP node-bounds result contains duplicate node id {node_id}")
|
|
133
|
+
bounds[node_id] = rect # type: ignore[assignment]
|
|
134
|
+
if not bounds:
|
|
135
|
+
raise McpError("MCP node-bounds result is empty")
|
|
136
|
+
return bounds
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Recoverable fallback-to-native MZGUI provider handoff."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ._gui_handoff_store import (
|
|
9
|
+
disable_known_fallback,
|
|
10
|
+
handoff_backup_path,
|
|
11
|
+
remove_known_resource,
|
|
12
|
+
restore_known_fallback,
|
|
13
|
+
)
|
|
14
|
+
from ._gui_project import unity_project
|
|
15
|
+
from ._gui_resource_store import install_gui_resource
|
|
16
|
+
from .gui_support import (
|
|
17
|
+
GUI_AUTHORING_SHA256,
|
|
18
|
+
GUI_AUTHORING_SOURCE,
|
|
19
|
+
GUI_SUPPORT_ASSET_PATH,
|
|
20
|
+
GUI_SUPPORT_KNOWN_PREVIOUS,
|
|
21
|
+
GUI_SUPPORT_SHA256,
|
|
22
|
+
inspect_gui_support,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def handoff_gui_support(
|
|
27
|
+
project_root: str | Path,
|
|
28
|
+
*,
|
|
29
|
+
write: bool = False,
|
|
30
|
+
runtime_probe: dict | None = None,
|
|
31
|
+
) -> dict:
|
|
32
|
+
"""Install the authoring-only bridge, then verify native takeover or roll back."""
|
|
33
|
+
project = unity_project(project_root, GUI_SUPPORT_ASSET_PATH)
|
|
34
|
+
state = inspect_gui_support(project_root, runtime_probe=runtime_probe)
|
|
35
|
+
target_digest = state["asecli_material_gui"]["actual_sha256"]
|
|
36
|
+
known = {GUI_SUPPORT_SHA256, *GUI_SUPPORT_KNOWN_PREVIOUS.keys()}
|
|
37
|
+
|
|
38
|
+
pending = sorted(project.target.parent.glob(project.target.name + ".asecli-handoff-*.disabled"))
|
|
39
|
+
if pending:
|
|
40
|
+
return _finish_pending(
|
|
41
|
+
project, state, pending, known, runtime_probe=runtime_probe, write=write
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
runtime = state["native_mzgui"]["runtime_providers"]
|
|
45
|
+
native = [item for item in runtime if item["shader_gui"] and not item["fallback"]]
|
|
46
|
+
fallback = [item for item in runtime if item["fallback"]]
|
|
47
|
+
if (
|
|
48
|
+
state["provider"] != "multiple"
|
|
49
|
+
or target_digest not in known
|
|
50
|
+
or runtime_probe is None
|
|
51
|
+
or not runtime_probe.get("native_authoring_capable")
|
|
52
|
+
or len(runtime) != 2
|
|
53
|
+
or len(native) != 1
|
|
54
|
+
or len(fallback) != 1
|
|
55
|
+
):
|
|
56
|
+
raise RuntimeError("native handoff requires exactly a native provider plus one known fallback")
|
|
57
|
+
backup = handoff_backup_path(project.target, target_digest)
|
|
58
|
+
result = {**state, "action": "prepare_native_handoff", "handoff_backup": str(backup)}
|
|
59
|
+
if not write:
|
|
60
|
+
return result
|
|
61
|
+
disable_known_fallback(project, target_digest, known)
|
|
62
|
+
try:
|
|
63
|
+
install_gui_resource(project, GUI_AUTHORING_SOURCE.encode("utf-8"))
|
|
64
|
+
except BaseException:
|
|
65
|
+
restore_known_fallback(project, backup, target_digest, known)
|
|
66
|
+
raise
|
|
67
|
+
return {
|
|
68
|
+
**result,
|
|
69
|
+
"action": "native_handoff_pending_verification",
|
|
70
|
+
"written": True,
|
|
71
|
+
"requires_editor_recompile": True,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _finish_pending(project, state, pending, known, *, runtime_probe, write):
|
|
76
|
+
if len(pending) != 1 or project.target.is_symlink() or not project.target.is_file():
|
|
77
|
+
raise RuntimeError("GUI handoff state is ambiguous; refusing automatic recovery")
|
|
78
|
+
if hashlib.sha256(project.target.read_bytes()).hexdigest() != GUI_AUTHORING_SHA256:
|
|
79
|
+
raise RuntimeError("GUI handoff authoring bridge changed; refusing automatic recovery")
|
|
80
|
+
backup = pending[0]
|
|
81
|
+
if backup.is_symlink() or not backup.is_file():
|
|
82
|
+
raise RuntimeError("GUI handoff backup is not a regular file; refusing automatic recovery")
|
|
83
|
+
digest = hashlib.sha256(backup.read_bytes()).hexdigest()
|
|
84
|
+
if digest not in known or backup != handoff_backup_path(project.target, digest):
|
|
85
|
+
raise RuntimeError("GUI handoff backup is unknown; refusing automatic recovery")
|
|
86
|
+
runtime = state["native_mzgui"]["runtime_providers"]
|
|
87
|
+
if (
|
|
88
|
+
runtime_probe is not None
|
|
89
|
+
and state["provider"] == "native_mzgui"
|
|
90
|
+
and len(runtime) == 1
|
|
91
|
+
and runtime[0]["shader_gui"]
|
|
92
|
+
and not runtime[0]["fallback"]
|
|
93
|
+
):
|
|
94
|
+
return {**state, "action": "native_handoff_verified", "handoff_backup": str(backup)}
|
|
95
|
+
result = {**state, "action": "rollback_native_handoff", "handoff_backup": str(backup)}
|
|
96
|
+
if write:
|
|
97
|
+
remove_known_resource(project, project.target, GUI_AUTHORING_SHA256)
|
|
98
|
+
restore_known_fallback(project, backup, digest, known)
|
|
99
|
+
result.update(action="native_handoff_rolled_back", written=True)
|
|
100
|
+
return result
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Executable presentation contracts for the built-in material GUI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
INLINE_HELP_PRESENTATION_CONTRACT = {
|
|
7
|
+
"contract": "asecli.inline-help.v1",
|
|
8
|
+
"icon": "none",
|
|
9
|
+
"border": "none",
|
|
10
|
+
"background_rgba": [0.0, 0.0, 0.0, 0.1],
|
|
11
|
+
"accent": {"edge": "left", "width": 3.0, "rgba": [1.0, 1.0, 1.0, 0.3]},
|
|
12
|
+
"typography": {
|
|
13
|
+
"base": "EditorStyles.miniLabel",
|
|
14
|
+
"font_style": "italic",
|
|
15
|
+
"dark_skin_alpha": 0.4,
|
|
16
|
+
"light_skin_alpha": 0.55,
|
|
17
|
+
},
|
|
18
|
+
"layout": {"text_inset": [16.0, 4.0], "word_wrap": True},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
_REQUIRED_SOURCE_MARKERS = {
|
|
22
|
+
"inline_renderer": "DrawInlineHelp(metadata.Help)",
|
|
23
|
+
"mini_typography": "new GUIStyle(EditorStyles.miniLabel)",
|
|
24
|
+
"italic_typography": "style.fontStyle = FontStyle.Italic",
|
|
25
|
+
"word_wrap": "style.wordWrap = true",
|
|
26
|
+
"subtle_background": (
|
|
27
|
+
"EditorGUI.DrawRect(backgroundRect, new Color(0f, 0f, 0f, 0.1f))"
|
|
28
|
+
),
|
|
29
|
+
"left_accent_color": (
|
|
30
|
+
"EditorGUI.DrawRect(accentRect, new Color(1f, 1f, 1f, 0.3f))"
|
|
31
|
+
),
|
|
32
|
+
"left_accent_geometry": "backgroundRect.x + 6f,\n backgroundRect.y + 6f,\n 3f,",
|
|
33
|
+
"text_inset": "row.x + 16f,\n row.y + 4f,",
|
|
34
|
+
"dark_skin_text": "new Color(1f, 1f, 1f, 0.4f)",
|
|
35
|
+
"light_skin_text": "new Color(0f, 0f, 0f, 0.55f)",
|
|
36
|
+
}
|
|
37
|
+
_FORBIDDEN_SOURCE_MARKERS = {
|
|
38
|
+
"native_layout_help_box": "EditorGUILayout.HelpBox(",
|
|
39
|
+
"native_gui_help_box": "EditorGUI.HelpBox(",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def inspect_inline_help_presentation(source: str) -> dict:
|
|
44
|
+
"""Report whether the packaged C# source satisfies the public style contract."""
|
|
45
|
+
violations = [
|
|
46
|
+
f"missing:{name}"
|
|
47
|
+
for name, marker in _REQUIRED_SOURCE_MARKERS.items()
|
|
48
|
+
if marker not in source
|
|
49
|
+
]
|
|
50
|
+
violations.extend(
|
|
51
|
+
f"forbidden:{name}"
|
|
52
|
+
for name, marker in _FORBIDDEN_SOURCE_MARKERS.items()
|
|
53
|
+
if marker in source
|
|
54
|
+
)
|
|
55
|
+
return {
|
|
56
|
+
**INLINE_HELP_PRESENTATION_CONTRACT,
|
|
57
|
+
"valid": not violations,
|
|
58
|
+
"violations": violations,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def require_inline_help_presentation(source: str) -> None:
|
|
63
|
+
"""Fail closed before installation when the packaged style has regressed."""
|
|
64
|
+
result = inspect_inline_help_presentation(source)
|
|
65
|
+
if result["valid"]:
|
|
66
|
+
return
|
|
67
|
+
raise RuntimeError(
|
|
68
|
+
"packaged ASECLI GUI violates inline-help presentation contract: "
|
|
69
|
+
+ ", ".join(result["violations"])
|
|
70
|
+
)
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Static and Editor-runtime detection of the native MZGUI material provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
from .mcp_client import McpClient, McpError, tool_text
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_NATIVE_NAMESPACE_RE = re.compile(r"\bnamespace\s+MZGUI\b")
|
|
13
|
+
_NATIVE_GUI_BASE_RE = re.compile(r"\bclass\s+MZGUI\s*:\s*([A-Za-z_][A-Za-z0-9_:.]*)")
|
|
14
|
+
_SHADER_GUI_ALIAS_RE = re.compile(
|
|
15
|
+
r"\busing\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:global::)?UnityEditor\.ShaderGUI\s*;"
|
|
16
|
+
)
|
|
17
|
+
_GUI_PROBE_MARKER = "ASECLI_GUI_SUPPORT_PROBE_V2:"
|
|
18
|
+
GUI_SUPPORT_PROBE_SNIPPET = r'''
|
|
19
|
+
var probe = new Newtonsoft.Json.Linq.JObject();
|
|
20
|
+
probe["protocol"] = "ASECLI_GUI_SUPPORT_PROBE_V2";
|
|
21
|
+
probe["assets_path"] = System.IO.Path.GetFullPath(UnityEngine.Application.dataPath);
|
|
22
|
+
var providers = new Newtonsoft.Json.Linq.JArray();
|
|
23
|
+
foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies())
|
|
24
|
+
{
|
|
25
|
+
var candidate = assembly.GetType("MZGUI.MZGUI", false);
|
|
26
|
+
if (candidate == null) continue;
|
|
27
|
+
var item = new Newtonsoft.Json.Linq.JObject();
|
|
28
|
+
item["full_name"] = candidate.FullName;
|
|
29
|
+
item["assembly"] = candidate.Assembly.FullName;
|
|
30
|
+
item["shader_gui"] = typeof(UnityEditor.ShaderGUI).IsAssignableFrom(candidate);
|
|
31
|
+
item["fallback"] = candidate.GetInterface(
|
|
32
|
+
"ASECLI.MaterialGUI.IASECLIFallbackProvider") != null;
|
|
33
|
+
providers.Add(item);
|
|
34
|
+
}
|
|
35
|
+
probe["providers"] = providers;
|
|
36
|
+
probe["type_found"] = providers.Count > 0;
|
|
37
|
+
probe["detected"] = providers.Count == 1 && !(bool)providers[0]["fallback"] &&
|
|
38
|
+
(bool)providers[0]["shader_gui"];
|
|
39
|
+
probe["fallback"] = providers.Count == 1 && (bool)providers[0]["fallback"];
|
|
40
|
+
probe["assembly"] = providers.Count == 1 ? providers[0]["assembly"] : null;
|
|
41
|
+
System.Type propertyNode = null;
|
|
42
|
+
foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies())
|
|
43
|
+
{
|
|
44
|
+
propertyNode = assembly.GetType("AmplifyShaderEditor.PropertyNode", false);
|
|
45
|
+
if (propertyNode != null) break;
|
|
46
|
+
}
|
|
47
|
+
var allFields = System.Reflection.BindingFlags.Public |
|
|
48
|
+
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance;
|
|
49
|
+
probe["native_authoring_capable"] = propertyNode != null &&
|
|
50
|
+
propertyNode.GetField("m_mzguiAttribs", allFields) != null &&
|
|
51
|
+
propertyNode.GetField("m_selectedMzguiAttribs", allFields) != null;
|
|
52
|
+
return "ASECLI_GUI_SUPPORT_PROBE_V2:" + probe.ToString(Newtonsoft.Json.Formatting.None);
|
|
53
|
+
'''.strip()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def probe_native_mzgui_for_assets(
|
|
57
|
+
project_assets: Path,
|
|
58
|
+
*,
|
|
59
|
+
mcp_url: str,
|
|
60
|
+
instance_token: str | None,
|
|
61
|
+
allow_remote_mcp: bool,
|
|
62
|
+
) -> dict:
|
|
63
|
+
client = McpClient(mcp_url, instance_token=instance_token, allow_remote=allow_remote_mcp)
|
|
64
|
+
client.connect()
|
|
65
|
+
result = client.call_tool("execute_code", {"action": "execute", "code": GUI_SUPPORT_PROBE_SNIPPET})
|
|
66
|
+
text = tool_text(result, instance_token)
|
|
67
|
+
marker = text.find(_GUI_PROBE_MARKER)
|
|
68
|
+
if marker < 0:
|
|
69
|
+
raise McpError("MCP tool result did not contain the GUI support probe marker")
|
|
70
|
+
raw = text[marker + len(_GUI_PROBE_MARKER) :].lstrip()
|
|
71
|
+
try:
|
|
72
|
+
value, _ = json.JSONDecoder().raw_decode(raw)
|
|
73
|
+
except json.JSONDecodeError as exc:
|
|
74
|
+
raise McpError("MCP GUI support probe returned malformed JSON") from exc
|
|
75
|
+
if not isinstance(value, dict):
|
|
76
|
+
raise McpError("MCP GUI support probe result must be an object")
|
|
77
|
+
validate_runtime_probe(value, project_assets)
|
|
78
|
+
return value
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def validate_runtime_probe(probe: dict, project_assets: Path) -> None:
|
|
82
|
+
if probe.get("protocol") not in {
|
|
83
|
+
"ASECLI_GUI_SUPPORT_PROBE_V1",
|
|
84
|
+
"ASECLI_GUI_SUPPORT_PROBE_V2",
|
|
85
|
+
}:
|
|
86
|
+
raise ValueError("GUI support runtime probe protocol is missing or unsupported")
|
|
87
|
+
if not isinstance(probe.get("detected"), bool) or not isinstance(probe.get("type_found"), bool):
|
|
88
|
+
raise ValueError("GUI support runtime probe booleans are invalid")
|
|
89
|
+
if "fallback" in probe and not isinstance(probe.get("fallback"), bool):
|
|
90
|
+
raise ValueError("GUI support runtime fallback marker is invalid")
|
|
91
|
+
if probe.get("protocol") == "ASECLI_GUI_SUPPORT_PROBE_V2":
|
|
92
|
+
providers = probe.get("providers")
|
|
93
|
+
if not isinstance(providers, list):
|
|
94
|
+
raise ValueError("GUI support runtime providers are invalid")
|
|
95
|
+
for provider in providers:
|
|
96
|
+
if not isinstance(provider, dict):
|
|
97
|
+
raise ValueError("GUI support runtime provider is invalid")
|
|
98
|
+
if not isinstance(provider.get("assembly"), str):
|
|
99
|
+
raise ValueError("GUI support runtime provider assembly is invalid")
|
|
100
|
+
if not isinstance(provider.get("shader_gui"), bool) or not isinstance(
|
|
101
|
+
provider.get("fallback"), bool
|
|
102
|
+
):
|
|
103
|
+
raise ValueError("GUI support runtime provider flags are invalid")
|
|
104
|
+
if not isinstance(probe.get("native_authoring_capable"), bool):
|
|
105
|
+
raise ValueError("GUI support native authoring capability is invalid")
|
|
106
|
+
assets_path = probe.get("assets_path")
|
|
107
|
+
if not isinstance(assets_path, str) or Path(assets_path).resolve() != project_assets.resolve():
|
|
108
|
+
raise ValueError("connected Editor project does not match the requested project root")
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def runtime_providers(probe: dict | None) -> list[dict]:
|
|
112
|
+
"""Normalize V1/V2 probe results without turning an ambiguous hit into native."""
|
|
113
|
+
if probe is None:
|
|
114
|
+
return []
|
|
115
|
+
if probe.get("protocol") == "ASECLI_GUI_SUPPORT_PROBE_V2":
|
|
116
|
+
return [dict(item) for item in probe.get("providers", [])]
|
|
117
|
+
if not probe.get("type_found"):
|
|
118
|
+
return []
|
|
119
|
+
return [
|
|
120
|
+
{
|
|
121
|
+
"full_name": "MZGUI.MZGUI",
|
|
122
|
+
"assembly": probe.get("assembly") or "unknown",
|
|
123
|
+
"shader_gui": bool(probe.get("detected") or probe.get("fallback")),
|
|
124
|
+
"fallback": bool(probe.get("fallback")),
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def scan_native_mzgui(project_root: Path, *, skip_source_name: str) -> tuple[list[str], list[str]]:
|
|
130
|
+
"""Return proven providers and candidates that need an Editor reflection probe."""
|
|
131
|
+
search_roots = [project_root / "Assets", project_root / "Packages"]
|
|
132
|
+
package_cache = project_root / "Library" / "PackageCache"
|
|
133
|
+
if package_cache.is_dir():
|
|
134
|
+
try:
|
|
135
|
+
search_roots.extend(entry for entry in package_cache.iterdir() if "mzgui" in entry.name.lower())
|
|
136
|
+
except OSError:
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
evidence: list[str] = []
|
|
140
|
+
candidates: list[str] = []
|
|
141
|
+
for root in search_roots:
|
|
142
|
+
if not root.is_dir():
|
|
143
|
+
continue
|
|
144
|
+
for source in root.rglob("*.cs"):
|
|
145
|
+
if source.name == skip_source_name:
|
|
146
|
+
continue
|
|
147
|
+
try:
|
|
148
|
+
if source.stat().st_size > 2_000_000:
|
|
149
|
+
continue
|
|
150
|
+
text = source.read_text(encoding="utf-8-sig", errors="ignore")
|
|
151
|
+
except OSError:
|
|
152
|
+
continue
|
|
153
|
+
relative = _relative_evidence(source, project_root)
|
|
154
|
+
if _source_defines_native_mzgui(text):
|
|
155
|
+
evidence.append(relative)
|
|
156
|
+
elif _NATIVE_NAMESPACE_RE.search(text) and re.search(r"\bclass\s+MZGUI\b", text):
|
|
157
|
+
candidates.append(relative)
|
|
158
|
+
for assembly in root.rglob("*.dll"):
|
|
159
|
+
try:
|
|
160
|
+
if assembly.stat().st_size > 100_000_000:
|
|
161
|
+
continue
|
|
162
|
+
data = assembly.read_bytes()
|
|
163
|
+
except OSError:
|
|
164
|
+
continue
|
|
165
|
+
if "mzgui" in assembly.name.lower() or (b"MZGUI" in data and b"ShaderGUI" in data):
|
|
166
|
+
candidates.append(_relative_evidence(assembly, project_root))
|
|
167
|
+
|
|
168
|
+
for manifest_name in ("manifest.json", "packages-lock.json"):
|
|
169
|
+
manifest = project_root / "Packages" / manifest_name
|
|
170
|
+
if not manifest.is_file():
|
|
171
|
+
continue
|
|
172
|
+
try:
|
|
173
|
+
if "mzgui" in manifest.read_text(encoding="utf-8", errors="ignore").lower():
|
|
174
|
+
candidates.append(_relative_evidence(manifest, project_root))
|
|
175
|
+
except OSError:
|
|
176
|
+
continue
|
|
177
|
+
return sorted(set(evidence)), sorted(set(candidates) - set(evidence))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _source_defines_native_mzgui(text: str) -> bool:
|
|
181
|
+
if not _NATIVE_NAMESPACE_RE.search(text):
|
|
182
|
+
return False
|
|
183
|
+
match = _NATIVE_GUI_BASE_RE.search(text)
|
|
184
|
+
if match is None:
|
|
185
|
+
return False
|
|
186
|
+
base = match.group(1).replace("global::", "")
|
|
187
|
+
if base in {"ShaderGUI", "UnityEditor.ShaderGUI"}:
|
|
188
|
+
return True
|
|
189
|
+
aliases = {alias.group(1) for alias in _SHADER_GUI_ALIAS_RE.finditer(text)}
|
|
190
|
+
return base in aliases
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _relative_evidence(path: Path, project_root: Path) -> str:
|
|
194
|
+
try:
|
|
195
|
+
return path.resolve().relative_to(project_root).as_posix()
|
|
196
|
+
except ValueError:
|
|
197
|
+
return str(path.resolve())
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Connected-Editor runtime probe for MZGUI providers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ._gui_project import unity_project
|
|
8
|
+
from .gui_provider_detection import probe_native_mzgui_for_assets
|
|
9
|
+
from .gui_support import GUI_SUPPORT_ASSET_PATH
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def probe_native_mzgui_via_mcp(
|
|
13
|
+
project_root: str | Path,
|
|
14
|
+
*,
|
|
15
|
+
mcp_url: str = "http://127.0.0.1:8080/mcp",
|
|
16
|
+
instance_token: str | None = None,
|
|
17
|
+
allow_remote_mcp: bool = False,
|
|
18
|
+
) -> dict:
|
|
19
|
+
project = unity_project(project_root, GUI_SUPPORT_ASSET_PATH)
|
|
20
|
+
return probe_native_mzgui_for_assets(
|
|
21
|
+
project.assets,
|
|
22
|
+
mcp_url=mcp_url,
|
|
23
|
+
instance_token=instance_token,
|
|
24
|
+
allow_remote_mcp=allow_remote_mcp,
|
|
25
|
+
)
|