Skip to content

Commit 85a06b2

Browse files
committed
use instanceID as main key
Add long entityID for lossless convertion
1 parent e4fbd3a commit 85a06b2

3 files changed

Lines changed: 77 additions & 44 deletions

File tree

MCPForUnity/Editor/Tools/Animation/ControllerCreate.cs

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Linq;
55
using Newtonsoft.Json.Linq;
66
using MCPForUnity.Editor.Helpers;
7+
using MCPForUnity.Runtime.Helpers;
78
using UnityEditor;
89
using UnityEditor.Animations;
910
using UnityEngine;
@@ -423,9 +424,11 @@ public static object AssignToGameObject(JObject @params)
423424
}
424425

425426
// Reads node graph positions for every state (recurses into sub-state-machines).
426-
// Returns [{ name, x, y, layer }] so a caller can analyze the current layout
427-
// before sending back a revised one. Pass 'layerIndex' to scope to one layer;
428-
// results are paged (page_size/cursor) since controllers can have many states.
427+
// Returns [{ name, instanceId, x, y, layer }] so a caller can analyze the current
428+
// layout before sending back a revised one. 'instanceId' round-trips into
429+
// set_state_positions for an unambiguous match (duplicate names are fine).
430+
// Pass 'layerIndex' to scope to one layer; results are paged (page_size/cursor)
431+
// since controllers can have many states.
429432
public static object GetStatePositions(JObject @params)
430433
{
431434
var controller = LoadController(@params);
@@ -470,6 +473,7 @@ private static void CollectPositions(AnimatorStateMachine sm, int layer, List<ob
470473
outList.Add(new
471474
{
472475
name = children[i].state.name,
476+
instanceId = children[i].state.GetInstanceIDLongCompat(),
473477
x = children[i].position.x,
474478
y = children[i].position.y,
475479
layer
@@ -478,41 +482,39 @@ private static void CollectPositions(AnimatorStateMachine sm, int layer, List<ob
478482
CollectPositions(sub.stateMachine, layer, outList);
479483
}
480484

481-
// Sets node graph positions from a 'positions' array of { name, x, y, layer? }.
482-
// Each entry's optional 'layer' (falling back to a top-level 'layerIndex') scopes
483-
// the match to one layer, so a name reused across layers is no longer ambiguous;
484-
// entries with no layer match that name on any layer. Recurses into sub-state-
485-
// machines and reassigns stateMachine.states so the edits persist on the asset.
485+
// Sets node graph positions from a 'positions' array of { instanceId, x, y }.
486+
// States are matched by 'instanceId' (from get_state_positions) for an exact,
487+
// unambiguous hit even when names repeat across layers or sub-state-machines.
488+
// Recurses into sub-state-machines and reassigns stateMachine.states so the
489+
// edits persist on the asset.
486490
public static object SetStatePositions(JObject @params)
487491
{
488492
var controller = LoadController(@params);
489493
if (controller == null)
490494
return ControllerNotFoundError(@params);
491495

492496
if (!(@params["positions"] is JArray positions) || positions.Count == 0)
493-
return new { success = false, message = "'positions' array is required: [{ name, x, y, layer? }, ...]" };
497+
return new { success = false, message = "'positions' array is required: [{ instanceId, x, y }, ...]" };
494498

495-
int? defaultLayer = @params["layerIndex"]?.ToObject<int>();
496-
497-
// Key is "layer:name" when scoped to a layer, else "*:name" to match any layer.
498-
var want = new Dictionary<string, Vector2>();
499+
var want = new Dictionary<ulong, Vector2>();
499500
foreach (var token in positions)
500501
{
501-
string name = token["name"]?.ToString();
502-
if (string.IsNullOrEmpty(name))
502+
if (!(token is JObject entry))
503+
continue;
504+
ulong? instanceId = entry["instanceId"]?.ToObject<ulong>();
505+
if (!instanceId.HasValue)
503506
continue;
504-
float x = token["x"]?.ToObject<float>() ?? 0f;
505-
float y = token["y"]?.ToObject<float>() ?? 0f;
506-
int? layer = token["layer"]?.ToObject<int>() ?? defaultLayer;
507-
want[$"{(layer.HasValue ? layer.Value.ToString() : "*")}:{name}"] = new Vector2(x, y);
507+
float x = entry["x"]?.ToObject<float>() ?? 0f;
508+
float y = entry["y"]?.ToObject<float>() ?? 0f;
509+
want[instanceId.Value] = new Vector2(x, y);
508510
}
509511
if (want.Count == 0)
510-
return new { success = false, message = "No valid entries in 'positions' (each needs a 'name')." };
512+
return new { success = false, message = "No valid entries in 'positions' (each needs an 'instanceId')." };
511513

512-
var matched = new HashSet<string>();
514+
var matched = new HashSet<ulong>();
513515
Undo.RecordObject(controller, "Set State Positions");
514516
for (int li = 0; li < controller.layers.Length; li++)
515-
ApplyPositions(controller.layers[li].stateMachine, li, want, matched);
517+
ApplyPositions(controller.layers[li].stateMachine, want, matched);
516518

517519
EditorUtility.SetDirty(controller);
518520
AssetDatabase.SaveAssets();
@@ -521,7 +523,7 @@ public static object SetStatePositions(JObject @params)
521523
return new
522524
{
523525
success = true,
524-
message = $"Positioned {matched.Count} state(s); {unmatched.Count} key(s) unmatched.",
526+
message = $"Positioned {matched.Count} state(s); {unmatched.Count} id(s) unmatched.",
525527
data = new
526528
{
527529
matched = matched.Count,
@@ -531,27 +533,22 @@ public static object SetStatePositions(JObject @params)
531533
};
532534
}
533535

534-
private static void ApplyPositions(AnimatorStateMachine sm, int layer, Dictionary<string, Vector2> want, HashSet<string> matched)
536+
private static void ApplyPositions(AnimatorStateMachine sm, Dictionary<ulong, Vector2> want, HashSet<ulong> matched)
535537
{
536538
var children = sm.states;
537539
for (int i = 0; i < children.Length; i++)
538540
{
539-
string name = children[i].state.name;
540-
// Prefer a layer-scoped entry; fall back to the any-layer entry.
541-
string scopedKey = $"{layer}:{name}";
542-
string anyKey = $"*:{name}";
543-
string key = want.ContainsKey(scopedKey) ? scopedKey
544-
: want.ContainsKey(anyKey) ? anyKey : null;
545-
if (key != null)
541+
ulong? id = children[i].state.GetInstanceIDLongCompat();
542+
if (id.HasValue && want.TryGetValue(id.Value, out var p))
546543
{
547-
children[i].position = new Vector3(want[key].x, want[key].y, 0f);
548-
matched.Add(key);
544+
children[i].position = new Vector3(p.x, p.y, 0f);
545+
matched.Add(id.Value);
549546
}
550547
}
551548
sm.states = children; // reassign so position edits persist
552549

553550
foreach (var sub in sm.stateMachines)
554-
ApplyPositions(sub.stateMachine, layer, want, matched);
551+
ApplyPositions(sub.stateMachine, want, matched);
555552
}
556553

557554
private static AnimatorController LoadController(JObject @params)

MCPForUnity/Runtime/Helpers/UnityObjectIdCompat.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ namespace MCPForUnity.Runtime.Helpers
1212
/// Version-gated wrappers for the InstanceID ↔ EntityId migration introduced in Unity 6.5
1313
/// and tightened in 6.6.
1414
/// Forward (Object → int): <see cref="GetInstanceIDCompat"/>
15+
/// Forward (Object → ulong, lossless): <see cref="GetInstanceIDLongCompat"/>
1516
/// Reverse (int → Object, Editor-only): <see cref="InstanceIDToObjectCompat"/>
17+
/// Reverse (ulong → Object, Editor-only): <see cref="InstanceIDToObjectLongCompat"/>
1618
/// </summary>
1719
public static class UnityObjectIdCompat
1820
{
@@ -36,6 +38,27 @@ public static int GetInstanceIDCompat(this Object obj)
3638
#endif
3739
}
3840

41+
/// <summary>
42+
/// Like <see cref="GetInstanceIDCompat"/> but returns the full handle without the
43+
/// lossy int truncation: on 6.5+ the EntityId's underlying ulong, on older versions
44+
/// the int instance ID widened to ulong. Returns null for a null object. Use when
45+
/// the handle must round-trip exactly (e.g. matching the same object back across a
46+
/// JSON request).
47+
/// </summary>
48+
public static ulong? GetInstanceIDLongCompat(this Object obj)
49+
{
50+
if (obj == null)
51+
{
52+
return null;
53+
}
54+
55+
#if UNITY_6000_5_OR_NEWER
56+
return EntityId.ToULong(obj.GetEntityId());
57+
#else
58+
return unchecked((ulong)obj.GetInstanceID());
59+
#endif
60+
}
61+
3962
#if UNITY_EDITOR
4063
#if UNITY_6000_6_OR_NEWER
4164
private static MethodInfo _instanceIdToObject;
@@ -68,6 +91,24 @@ public static Object InstanceIDToObjectCompat(int instanceId)
6891
return EditorUtility.EntityIdToObject(instanceId);
6992
#else
7093
return EditorUtility.InstanceIDToObject(instanceId);
94+
#endif
95+
}
96+
97+
/// <summary>
98+
/// Resolves a ulong handle (from <see cref="GetInstanceIDLongCompat"/>) back to a
99+
/// UnityEngine.Object. Disambiguates by Unity version, not by inspecting the numeric
100+
/// range — a wrapped-negative int and a genuine 64-bit EntityId can occupy the same
101+
/// high band, so range checks cannot tell them apart.
102+
/// 6.5+ : the handle is the EntityId's ulong — resolve via EntityId.FromULong.
103+
/// Pre-6.5 : the handle is an int instance ID round-tripped through an unchecked
104+
/// ulong cast (negatives are valid) — cast back and use the int resolver.
105+
/// </summary>
106+
public static Object InstanceIDToObjectLongCompat(ulong instanceId)
107+
{
108+
#if UNITY_6000_5_OR_NEWER
109+
return EditorUtility.EntityIdToObject(EntityId.FromULong(instanceId));
110+
#else
111+
return InstanceIDToObjectCompat(unchecked((int)instanceId));
71112
#endif
72113
}
73114
#endif

Server/tests/test_manage_animation.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -184,21 +184,16 @@ def test_get_state_positions_forwards_paging_and_layer(self):
184184
assert params["properties"]["cursor"] == 20
185185

186186
def test_set_state_positions_forwards_positions(self):
187-
positions = [{"name": "Idle", "x": 100, "y": 0}, {"name": "Walk", "x": 300, "y": 0}]
187+
# instanceId round-trips from get_state_positions; can exceed 32-bit on Unity 6.5+
188+
# (EntityId), and JSON / Python int carry the full 64-bit value losslessly.
189+
positions = [{"instanceId": 8412, "x": 100, "y": 0},
190+
{"instanceId": 18446744073709551000, "x": 300, "y": 0}]
188191
_, params = self._dispatch(
189192
"controller_set_state_positions", properties={"positions": positions}
190193
)
191194
assert params["action"] == "controller_set_state_positions"
192195
assert params["properties"]["positions"] == positions
193-
194-
def test_set_state_positions_forwards_layer_scoping(self):
195-
positions = [{"name": "Idle", "x": 0, "y": 0, "layer": 1}]
196-
_, params = self._dispatch(
197-
"controller_set_state_positions",
198-
properties={"positions": positions, "layerIndex": 0},
199-
)
200-
assert params["properties"]["positions"][0]["layer"] == 1
201-
assert params["properties"]["layerIndex"] == 0
196+
assert params["properties"]["positions"][1]["instanceId"] == 18446744073709551000
202197

203198

204199
# =============================================================================

0 commit comments

Comments
 (0)