diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e37c5e..008a504 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to this package will be documented in this file. +## [2.39.5] - 2026-07-27 + +Community-reported fixes. Each claim was verified against the shipped Unity assemblies before being acted on — two held up, one did not (documented below). + +### Fixed (ShaderGraph — community PR #23 by @mrooney) +- **`get_edges` reported every edge with blank ids.** The `m_OutputSlot`/`m_InputSlot` patterns used `.*?` **without** `RegexOptions.Singleline`, so `.` could not cross the newline between the key and the nested `m_Id` — and a `.shadergraph` writes one field per line. Every id came back `""` and every slot `0`, while the on-disk edges were perfectly correct (read-only path, nothing was ever corrupted). Verified live: `connect` → `get_edges` now returns the real objectId and slot. +- **Property nodes were still added unbound (issue #18 bug 3).** The `GraphData` rewrite fixed the other three bugs but added Property nodes type-only, so they serialized with an empty `m_Property` and no slots and the next import threw in `PropertyNode.AddOutputSlot`, failing the whole asset. `add_node` now accepts `propertyId` (alias `property`) and binds through `PropertyNode.property`, which rebuilds the output slot from the property's concrete type. An unbound or unknown Property node is now **refused before anything is written** — verified live, target graph intact. +- **The version-mismatch guard now names names.** It reported "One or more ShaderGraph API methods not found", which made any report against this path unfalsifiable in both directions. It now lists exactly which members are missing and asks for the Unity + ShaderGraph versions. +- **`AddNode` accepts either arity.** PR #23's premise was that ShaderGraph 17.3.0 only exposes `AddNode(node, bool)`, disabling the whole suite on Unity 6.3. Reflecting the shipped `Unity.ShaderGraph.Editor` on 17.3.0 shows the opposite — only the 1-arg `AddNode(AbstractMaterialNode)` exists — so we could not reproduce it. Accepting both shapes costs nothing and removes the question. Property-binding members are resolved **optionally**, so a build lacking `PropertyNode` can never disable the entire ShaderGraph surface (the all-or-nothing brittleness the PR would otherwise have introduced). + +### Fixed (community PR #19 by @JetNik) +- **`execute_code` still failed on macOS.** Unity 6000.3+ re-roots the scripting assemblies under `Contents/Resources/Scripting`, and probing only that bare folder missed the nested `MonoBleedingEdge/…` and `DotNetSdkRoslyn` layout inside it. Each Roslyn subpath is now mirrored there. Inert off macOS — the existing `Directory.Exists` guard skips absent paths — and `execute_code` verified still resolving Roslyn on Windows. + +### Added (issue #30 by @VM233 — 2D sprite animation) +- **`animation/set-object-reference-curve`** — object-reference (PPtr) curves, the type Unity uses for `SpriteRenderer.m_Sprite`. `clip.SetCurve`/`AnimationCurve` can only express **float** curves, so sprite-frame animation had no route through the MCP at all and had to be hand-written as `.anim` YAML (version-fragile, and it produced `curve type is invalid` import errors). Routes through `AnimationUtility.SetObjectReferenceCurve` + `EditorCurveBinding.PPtrCurve` so Unity writes the binding itself. Keyframes take an asset path plus an optional sub-asset `name` to select one sprite from a sliced sheet, and the call **fails closed** if any keyframe is unresolvable rather than leaving a clip animating to the wrong frames. +- **`animation/clip-info` now reports object-reference curves.** `GetCurveBindings` returns only float bindings, so a correct sprite clip reported `curveCount: 0` and looked empty — the reporter had no way to verify their own work. Adds `objectReferenceCurves` with binding path, property, type, and each keyframe's time + resolved asset path. +- Verified end to end: a 3-frame sprite clip built through the real MCP tool, read back correctly, and re-imported by Unity with **0 console errors** and all references resolved. Route registry 337 → 338. + ## [2.39.4] - 2026-07-24 Findings from a 33-dimension + 10-blind-spot audit (127 + 40 agents, every CRITICAL/HIGH adversarially verified). The recurring pattern: a correct pattern existed but was never propagated, and guards failed OPEN. diff --git a/Editor/MCPAnimationCommands.cs b/Editor/MCPAnimationCommands.cs index cce002b..8540d7b 100644 --- a/Editor/MCPAnimationCommands.cs +++ b/Editor/MCPAnimationCommands.cs @@ -445,6 +445,31 @@ public static object GetClipInfo(Dictionary args) }); } + // Object-reference (PPtr) curves are a SEPARATE binding set — GetCurveBindings only + // returns float curves, so a sprite-frame animation previously reported curveCount:0 + // and looked empty through the MCP even when the clip was correct (issue #30). + var objectCurves = new List>(); + foreach (var binding in AnimationUtility.GetObjectReferenceCurveBindings(clip)) + { + var frames = AnimationUtility.GetObjectReferenceCurve(clip, binding); + var keys = new List>(); + foreach (var f in frames) + keys.Add(new Dictionary + { + { "time", f.time }, + { "value", f.value != null ? f.value.name : null }, + { "assetPath", f.value != null ? AssetDatabase.GetAssetPath(f.value) : null }, + }); + objectCurves.Add(new Dictionary + { + { "path", binding.path }, + { "propertyName", binding.propertyName }, + { "type", binding.type.Name }, + { "keyframeCount", frames.Length }, + { "keyframes", keys }, + }); + } + var settings = AnimationUtility.GetAnimationClipSettings(clip); return new Dictionary @@ -457,11 +482,109 @@ public static object GetClipInfo(Dictionary args) { "wrapMode", clip.wrapMode.ToString() }, { "curveCount", curves.Count }, { "curves", curves }, + { "objectReferenceCurveCount", objectCurves.Count }, + { "objectReferenceCurves", objectCurves }, { "events", clip.events.Length }, { "isHumanMotion", clip.humanMotion }, }; } + /// + /// Set an object-reference (PPtr) curve — the curve type Unity uses for sprite-frame + /// animation (SpriteRenderer.m_Sprite) and any other UnityEngine.Object-valued property. + /// clip.SetCurve/AnimationCurve can only express FLOAT curves, so this whole class of + /// clip was previously impossible through the MCP and had to be hand-written as .anim + /// YAML — which is version-fragile and produced "curve type is invalid" import errors + /// (issue #30). Routes through AnimationUtility.SetObjectReferenceCurve so Unity itself + /// writes the binding. + /// + public static object SetObjectReferenceCurve(Dictionary args) + { + string path = args.ContainsKey("clipPath") ? args["clipPath"].ToString() : ""; + var clip = AssetDatabase.LoadAssetAtPath(path); + if (clip == null) + return new { error = $"Animation clip not found at '{path}'" }; + + string relativePath = args.ContainsKey("relativePath") ? args["relativePath"].ToString() : ""; + string propertyName = args.ContainsKey("propertyName") ? args["propertyName"].ToString() : "m_Sprite"; + string typeName = args.ContainsKey("type") ? args["type"].ToString() : "SpriteRenderer"; + + var type = ResolveComponentType(typeName); + if (type == null) + return new { error = $"Component type '{typeName}' not found. Use a UnityEngine type name such as 'SpriteRenderer' or 'MeshRenderer'." }; + + if (!args.ContainsKey("keyframes") || !(args["keyframes"] is List kfList) || kfList.Count == 0) + return new { error = "keyframes is required: an array of { time, assetPath } (assetPath may target a sub-asset, e.g. a sliced sprite)." }; + + var frames = new List(); + var unresolved = new List(); + foreach (var kfObj in kfList) + { + if (!(kfObj is Dictionary kf)) continue; + float time = kf.ContainsKey("time") ? Convert.ToSingle(kf["time"]) : 0f; + string assetPath = kf.ContainsKey("assetPath") && kf["assetPath"] != null ? kf["assetPath"].ToString() : null; + string subName = kf.ContainsKey("name") && kf["name"] != null ? kf["name"].ToString() : null; + if (string.IsNullOrEmpty(assetPath)) { unresolved.Add($"t={time}: missing assetPath"); continue; } + + // A sliced sprite sheet holds many Sprite sub-assets under ONE path, so load them + // all and pick by name when the caller names one; otherwise take the first. + UnityEngine.Object value = null; + foreach (var sub in AssetDatabase.LoadAllAssetsAtPath(assetPath)) + { + if (sub == null || !typeof(Sprite).IsAssignableFrom(sub.GetType())) continue; + if (subName == null) { value = sub; break; } + if (sub.name == subName) { value = sub; break; } + } + if (value == null) value = AssetDatabase.LoadAssetAtPath(assetPath); + if (value == null) { unresolved.Add($"t={time}: '{assetPath}'" + (subName != null ? $" (sprite '{subName}')" : "")); continue; } + + frames.Add(new ObjectReferenceKeyframe { time = time, value = value }); + } + + // Fail closed: a partially-resolved curve would silently animate to the wrong frames. + if (unresolved.Count > 0) + return new + { + error = $"{unresolved.Count} keyframe(s) could not be resolved: {string.Join("; ", unresolved)}", + unresolvedKeyframes = unresolved, + }; + + var binding = EditorCurveBinding.PPtrCurve(relativePath, type, propertyName); + Undo.RecordObject(clip, "Set Object Reference Curve"); + AnimationUtility.SetObjectReferenceCurve(clip, binding, frames.ToArray()); + EditorUtility.SetDirty(clip); + AssetDatabase.SaveAssets(); + + return new Dictionary + { + { "success", true }, + { "clipPath", path }, + { "relativePath", relativePath }, + { "propertyName", propertyName }, + { "type", type.Name }, + { "keyframeCount", frames.Count }, + { "clipLength", clip.length }, + }; + } + + /// Resolve a UnityEngine component type by short name across the split modules. + private static Type ResolveComponentType(string typeName) + { + var direct = Type.GetType($"UnityEngine.{typeName}, UnityEngine") + ?? Type.GetType($"UnityEngine.{typeName}, UnityEngine.CoreModule") + ?? Type.GetType(typeName); + if (direct != null) return direct; + // UnityEngine is split into modules (SpriteRenderer lives in UnityEngine.SpriteShapeModule + // / CoreModule depending on version) — scan rather than hardcode the module list. + foreach (var asm in AppDomain.CurrentDomain.GetAssemblies()) + { + if (!asm.GetName().Name.StartsWith("UnityEngine")) continue; + var t = asm.GetType($"UnityEngine.{typeName}"); + if (t != null) return t; + } + return null; + } + public static object SetClipCurve(Dictionary args) { string path = args.ContainsKey("clipPath") ? args["clipPath"].ToString() : ""; diff --git a/Editor/MCPBridgeServer.Routes.g.cs b/Editor/MCPBridgeServer.Routes.g.cs index 1be805f..6b5496a 100644 --- a/Editor/MCPBridgeServer.Routes.g.cs +++ b/Editor/MCPBridgeServer.Routes.g.cs @@ -9,7 +9,7 @@ namespace UnityMCP.Editor { public static partial class MCPBridgeServer { - /// Every route the bridge can dispatch (337 routes). + /// Every route the bridge can dispatch (338 routes). internal static readonly string[] GeneratedRoutes = new string[] { "_meta/routes", @@ -62,6 +62,7 @@ public static partial class MCPBridgeServer "animation/remove-transition", "animation/set-clip-curve", "animation/set-clip-settings", + "animation/set-object-reference-curve", "asmdef/add-references", "asmdef/create", "asmdef/create-ref", diff --git a/Editor/MCPBridgeServer.cs b/Editor/MCPBridgeServer.cs index 68c498d..62ef17c 100644 --- a/Editor/MCPBridgeServer.cs +++ b/Editor/MCPBridgeServer.cs @@ -832,6 +832,8 @@ private static object RouteRequest(string path, string method, string body) return MCPAnimationCommands.GetClipInfo(ParseJson(body)); case "animation/set-clip-curve": return MCPAnimationCommands.SetClipCurve(ParseJson(body)); + case "animation/set-object-reference-curve": + return MCPAnimationCommands.SetObjectReferenceCurve(ParseJson(body)); case "animation/add-layer": return MCPAnimationCommands.AddLayer(ParseJson(body)); case "animation/assign-controller": diff --git a/Editor/MCPEditorCommands.cs b/Editor/MCPEditorCommands.cs index 527be33..463c1ca 100755 --- a/Editor/MCPEditorCommands.cs +++ b/Editor/MCPEditorCommands.cs @@ -122,10 +122,17 @@ private static bool TryLoadRoslyn() // ScriptUpdater also ships Roslyn (Mono-compatible) searchDirs.Add(Path.Combine(data, "Tools", "ScriptUpdater")); // macOS: recent editors ship Roslyn under Contents/Resources/Scripting - // (community PR #19 by JetNik — ExecuteCode was broken on macOS without it) + // (community PR #19 by JetNik — ExecuteCode was broken on macOS without it). + // The bare folder alone isn't enough: on Unity 6000.3+ the assemblies live in + // the SAME nested layout as on Windows, just re-rooted under Resources/Scripting, + // so mirror each Mono/Roslyn subpath there too. Non-existent directories are + // skipped by the Directory.Exists guard below, so this is inert off macOS. searchDirs.Add(Path.Combine(data, "Resources", "Scripting")); + searchDirs.Add(Path.Combine(data, "Resources", "Scripting", "MonoBleedingEdge", "lib", "mono", "4.5")); + searchDirs.Add(Path.Combine(data, "Resources", "Scripting", "MonoBleedingEdge", "lib", "mono", "msbuild", "Current", "bin", "Roslyn")); // DotNetSdkRoslyn contains .NET Core assemblies — may fail on Mono, tried last searchDirs.Add(Path.Combine(data, "DotNetSdkRoslyn")); + searchDirs.Add(Path.Combine(data, "Resources", "Scripting", "DotNetSdkRoslyn")); } if (!string.IsNullOrEmpty(editorDir)) searchDirs.Add(editorDir); diff --git a/Editor/MCPShaderGraphApi.cs b/Editor/MCPShaderGraphApi.cs index 45cea58..b3354ea 100644 --- a/Editor/MCPShaderGraphApi.cs +++ b/Editor/MCPShaderGraphApi.cs @@ -30,6 +30,8 @@ internal static class MCPShaderGraphApi private static Type _graphDataT, _targetT, _blockFieldT, _absNodeT, _slotRefT, _materialSlotT, _messageManagerT, _multiJsonT, _fileUtilT, _propertyNodeT; private static Type _urpTargetT, _litSubT, _unlitSubT, _spriteLitSubT, _spriteUnlitSubT; private static PropertyInfo _messageManagerProp, _objectIdProp, _drawStateProp, _edgesProp, _drawPositionProp; + // Optional — only required to bind a Property node (see Initialize). + private static PropertyInfo _graphPropertiesProp, _propertyNodePropertyProp, _jsonObjectIdProp; private static MethodInfo _addContexts, _initOutputs, _getActiveBlocks, _addRemoveBlocks, _onEnable, _addNode, _getNodeFromId, _connect, _removeEdge, _removeNode, _getNodesGeneric, _getSlotsGeneric, _trySetSubTarget; private static MethodInfo _serialize, _deserializeGeneric, _writeToDisk, _writeGraphToDisk; private static ConstructorInfo _slotRefCtor; @@ -79,12 +81,29 @@ private static void Initialize() _edgesProp = _graphDataT.GetProperty("edges", PIF) ?? throw new InvalidOperationException("edges"); _drawPositionProp = _drawStateProp.PropertyType.GetProperty("position", PIF) ?? throw new InvalidOperationException("drawState.position"); + // Property-node binding members are resolved OPTIONALLY: they are needed only when + // someone adds a Property node. Hard-requiring them (as PR #23 proposed) would mark + // the ENTIRE ShaderGraph surface unavailable on any build where PropertyNode or its + // 'property' setter is renamed — reintroducing exactly the all-or-nothing brittleness + // this guard exists to avoid. AddNode raises a precise error if they're absent. + _graphPropertiesProp = _graphDataT.GetProperty("properties", PIF); + _propertyNodePropertyProp = _propertyNodeT?.GetProperty("property", PIF); + var jsonObjectT = FindType(sg, "JsonObject"); + _jsonObjectIdProp = jsonObjectT?.GetProperty("objectId", PIF); + _addContexts = _graphDataT.GetMethod("AddContexts", PIF); _initOutputs = _graphDataT.GetMethod("InitializeOutputs", PIF); _getActiveBlocks = _graphDataT.GetMethod("GetActiveBlocksForAllActiveTargets", PIF); _addRemoveBlocks = _graphDataT.GetMethod("AddRemoveBlocksFromActiveList", PIF); _onEnable = _graphDataT.GetMethod("OnEnable", PIF); - _addNode = _graphDataT.GetMethod("AddNode", PIF, null, new[] { _absNodeT }, null); + // Defensive arity tolerance. On com.unity.shadergraph 17.3.0 (verified by reflecting + // the shipped Unity.ShaderGraph.Editor assembly) the ONLY instance overload is the + // 1-arg AddNode(AbstractMaterialNode) — the 2-arg form does not exist there. A + // community report (PR #23) described the opposite on their build, which we could not + // reproduce; accepting either shape costs nothing and removes the whole question. + // If neither exists the named guard below reports exactly that. + _addNode = _graphDataT.GetMethod("AddNode", PIF, null, new[] { _absNodeT }, null) + ?? _graphDataT.GetMethod("AddNode", PIF, null, new[] { _absNodeT, typeof(bool) }, null); // GetNodeFromId has both a generic and a non-generic (string) overload, so a // typed GetMethod is ambiguous — pick the non-generic one explicitly. _getNodeFromId = _graphDataT.GetMethods(PIF).FirstOrDefault(m => @@ -107,11 +126,30 @@ private static void Initialize() _writeToDisk = _fileUtilT.GetMethod("WriteToDisk", SF); _writeGraphToDisk = _fileUtilT.GetMethod("WriteShaderGraphToDisk", SF); - if (_addContexts == null || _initOutputs == null || _getActiveBlocks == null || _addRemoveBlocks == null || - _onEnable == null || _addNode == null || _getNodeFromId == null || _connect == null || _removeEdge == null || - _removeNode == null || _getNodesGeneric == null || _serialize == null || _deserializeGeneric == null || - _writeToDisk == null || _writeGraphToDisk == null) - throw new InvalidOperationException("One or more ShaderGraph API methods not found (version mismatch)"); + // Name the members that are actually missing. The old message ("One or more + // ShaderGraph API methods not found") gave a user on an unexpected ShaderGraph + // build no way to say WHICH member broke, and no way for us to reproduce it — + // a bug report against this path was previously unfalsifiable in both directions. + var missing = new List(); + if (_addContexts == null) missing.Add("GraphData.AddContexts"); + if (_initOutputs == null) missing.Add("GraphData.InitializeOutputs"); + if (_getActiveBlocks == null) missing.Add("GraphData.GetActiveBlocksForAllActiveTargets"); + if (_addRemoveBlocks == null) missing.Add("GraphData.AddRemoveBlocksFromActiveList"); + if (_onEnable == null) missing.Add("GraphData.OnEnable"); + if (_addNode == null) missing.Add("GraphData.AddNode(AbstractMaterialNode[, bool])"); + if (_getNodeFromId == null) missing.Add("GraphData.GetNodeFromId(string)"); + if (_connect == null) missing.Add("GraphData.Connect"); + if (_removeEdge == null) missing.Add("GraphData.RemoveEdge"); + if (_removeNode == null) missing.Add("GraphData.RemoveNode"); + if (_getNodesGeneric == null) missing.Add("GraphData.GetNodes"); + if (_serialize == null) missing.Add("MultiJson.Serialize"); + if (_deserializeGeneric == null) missing.Add("MultiJson.Deserialize(4-arg)"); + if (_writeToDisk == null) missing.Add("FileUtilities.WriteToDisk"); + if (_writeGraphToDisk == null) missing.Add("FileUtilities.WriteShaderGraphToDisk"); + if (missing.Count > 0) + throw new InvalidOperationException( + $"ShaderGraph API version mismatch — {missing.Count} member(s) not found on this ShaderGraph build: {string.Join(", ", missing)}. " + + "Please report this list with your Unity and com.unity.shadergraph versions."); } private static MethodInfo _urpTargetTryResolve(Assembly urp) @@ -202,17 +240,58 @@ private static object NewGraph() return graph; } - /// Instantiate a node type, set its position, and add it. Returns its objectId. - internal static string AddNode(object graph, Type nodeType, float x, float y) + /// + /// Instantiate a node type, set its position, and add it. Returns its objectId. + /// + /// A PropertyNode MUST be bound to one of the graph's blackboard properties + /// ( = that property's objectId). An unbound one + /// serializes with an empty m_Property and no slots, and the next import throws inside + /// PropertyNode.AddOutputSlot — which fails the whole asset. That is issue #18 bug 3; + /// the GraphData rewrite fixed the other three but left this one, because it added the + /// node type-only. We now bind it, or refuse before anything is written to disk. + /// + internal static string AddNode(object graph, Type nodeType, float x, float y, string propertyId = null) { var node = Activator.CreateInstance(nodeType); var draw = _drawStateProp.GetValue(node); _drawPositionProp.SetValue(draw, new Rect(x, y, 208, 300)); _drawStateProp.SetValue(node, draw); // DrawState is a struct — write back - _addNode.Invoke(graph, new object[] { node }); + // Both the 1-arg and 2-arg AddNode shapes are accepted (see Initialize); pass the + // preview-preference default when the resolved overload takes it. + _addNode.Invoke(graph, _addNode.GetParameters().Length == 2 + ? new object[] { node, true } + : new object[] { node }); + + if (_propertyNodeT != null && _propertyNodeT.IsInstanceOfType(node)) + { + if (_propertyNodePropertyProp == null || _graphPropertiesProp == null || _jsonObjectIdProp == null) + throw new InvalidOperationException( + "This ShaderGraph build does not expose the members needed to bind a Property node " + + "(PropertyNode.property / GraphData.properties / JsonObject.objectId). Other node types still work."); + if (string.IsNullOrEmpty(propertyId)) + throw new InvalidOperationException( + "A Property node must be bound to a blackboard property: pass 'propertyId' (the property's objectId). " + + "An unbound Property node makes the .shadergraph fail to import."); + var property = FindProperty(graph, propertyId) + ?? throw new InvalidOperationException($"No blackboard property with objectId '{propertyId}' exists on this graph."); + // The setter rebuilds the node's output slot from the property's concrete type. + _propertyNodePropertyProp.SetValue(node, property); + } + return (string)_objectIdProp.GetValue(node); } + /// Find a blackboard property on the graph by its objectId, or null. + private static object FindProperty(object graph, string propertyId) + { + var properties = _graphPropertiesProp.GetValue(graph) as IEnumerable; + if (properties == null) return null; + foreach (var property in properties) + if ((string)_jsonObjectIdProp.GetValue(property) == propertyId) + return property; + return null; + } + internal static object GetNodeFromId(object graph, string nodeId) => _getNodeFromId.Invoke(graph, new object[] { nodeId }); /// Remove a node and its connected edges (byte-faithful to survivors). diff --git a/Editor/MCPShaderGraphCommands.cs b/Editor/MCPShaderGraphCommands.cs index 9bdcd39..032bc2a 100644 --- a/Editor/MCPShaderGraphCommands.cs +++ b/Editor/MCPShaderGraphCommands.cs @@ -749,7 +749,14 @@ public static object AddGraphNode(Dictionary args) // slots and the position is honored; the graph is guaranteed valid on save // (old string-surgery produced empty-slot nodes and dropped the position — #18 bugs 3,4). var graph = MCPShaderGraphApi.LoadGraph(fullPath); - string nodeId = MCPShaderGraphApi.AddNode(graph, resolvedType, posX, posY); + // propertyId (alias: property) binds a Property node to a blackboard property — + // required for that node type, ignored for every other (see AddNode). + string propertyId = args.ContainsKey("propertyId") && args["propertyId"] != null + ? args["propertyId"].ToString() + : args.ContainsKey("property") && args["property"] != null + ? args["property"].ToString() + : null; + string nodeId = MCPShaderGraphApi.AddNode(graph, resolvedType, posX, posY, propertyId); MCPShaderGraphApi.SaveGraph(MCPAssetSafety.ToAssetDatabasePath(path), graph); return new Dictionary @@ -1166,24 +1173,34 @@ private static List> ParseEdgesFromJson(string conten { string edgeJson = edgesSection.Substring(objStart, i - objStart + 1); + // Singleline is REQUIRED: a .shadergraph writes one field per line, so + // without it `.` cannot cross the newline between "m_OutputSlot" and the + // nested "m_Id" — every match failed and get_edges reported each edge as + // {outputNodeId:"", outputSlotId:0, inputNodeId:"", inputSlotId:0} even + // though the on-disk edges were perfectly correct. Read-only path, so + // nothing was ever corrupted, but the reported ids were useless. + // (Reported in PR #23; reproduced against real multi-line edge JSON.) + const System.Text.RegularExpressions.RegexOptions CrossLine = + System.Text.RegularExpressions.RegexOptions.Singleline; + // Extract output node ID string outNodePattern = "\"m_OutputSlot\".*?\"m_Id\"\\s*:\\s*\"([^\"]*)\""; - var outMatch = System.Text.RegularExpressions.Regex.Match(edgeJson, outNodePattern); + var outMatch = System.Text.RegularExpressions.Regex.Match(edgeJson, outNodePattern, CrossLine); string outNodeId = outMatch.Success ? outMatch.Groups[1].Value : ""; // Extract output slot ID string outSlotPattern = "\"m_OutputSlot\".*?\"m_SlotId\"\\s*:\\s*(\\d+)"; - var outSlotMatch = System.Text.RegularExpressions.Regex.Match(edgeJson, outSlotPattern); + var outSlotMatch = System.Text.RegularExpressions.Regex.Match(edgeJson, outSlotPattern, CrossLine); int outSlotId = outSlotMatch.Success ? int.Parse(outSlotMatch.Groups[1].Value) : 0; // Extract input node ID string inNodePattern = "\"m_InputSlot\".*?\"m_Id\"\\s*:\\s*\"([^\"]*)\""; - var inMatch = System.Text.RegularExpressions.Regex.Match(edgeJson, inNodePattern); + var inMatch = System.Text.RegularExpressions.Regex.Match(edgeJson, inNodePattern, CrossLine); string inNodeId = inMatch.Success ? inMatch.Groups[1].Value : ""; // Extract input slot ID string inSlotPattern = "\"m_InputSlot\".*?\"m_SlotId\"\\s*:\\s*(\\d+)"; - var inSlotMatch = System.Text.RegularExpressions.Regex.Match(edgeJson, inSlotPattern); + var inSlotMatch = System.Text.RegularExpressions.Regex.Match(edgeJson, inSlotPattern, CrossLine); int inSlotId = inSlotMatch.Success ? int.Parse(inSlotMatch.Groups[1].Value) : 0; edges.Add(new Dictionary diff --git a/package.json b/package.json index 4cc6e9e..7677e7d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "com.anklebreaker.unity-mcp", - "version": "2.39.4", + "version": "2.39.5", "displayName": "AnkleBreaker Unity MCP", "description": "MCP (Model Context Protocol) bridge plugin for Unity Editor. Enables AI assistants like Claude to control Unity Editor via a local HTTP API — manage scenes, GameObjects, components, assets, builds, and more.", "unity": "2021.3",