Skip to content

Commit 3787ea3

Browse files
committed
convert to general get/set_state_properties
add remove_transition for editing
1 parent 85a06b2 commit 3787ea3

4 files changed

Lines changed: 192 additions & 62 deletions

File tree

MCPForUnity/Editor/Tools/Animation/ControllerCreate.cs

Lines changed: 156 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,73 @@ public static object AddTransition(JObject @params)
228228
};
229229
}
230230

231+
// Removes transitions from 'fromState' (or AnyState) to 'toState' in a layer.
232+
// If 'toState' is omitted, removes ALL outgoing transitions from 'fromState'.
233+
// Use with add_transition to "edit" a transition: remove then re-add with new timing.
234+
public static object RemoveTransition(JObject @params)
235+
{
236+
var controller = LoadController(@params);
237+
if (controller == null)
238+
return ControllerNotFoundError(@params);
239+
240+
string fromStateName = @params["fromState"]?.ToString();
241+
if (string.IsNullOrEmpty(fromStateName))
242+
return new { success = false, message = "'fromState' is required" };
243+
string toStateName = @params["toState"]?.ToString(); // optional
244+
245+
int layerIndex = @params["layerIndex"]?.ToObject<int>() ?? 0;
246+
if (layerIndex < 0 || layerIndex >= controller.layers.Length)
247+
return new { success = false, message = $"Layer index {layerIndex} out of range" };
248+
249+
var rootStateMachine = controller.layers[layerIndex].stateMachine;
250+
251+
bool isAnyState = string.Equals(fromStateName, "AnyState", StringComparison.OrdinalIgnoreCase)
252+
|| string.Equals(fromStateName, "Any", StringComparison.OrdinalIgnoreCase)
253+
|| string.Equals(fromStateName, "Any State", StringComparison.OrdinalIgnoreCase);
254+
255+
int removed = 0;
256+
257+
if (isAnyState)
258+
{
259+
foreach (var t in rootStateMachine.anyStateTransitions.ToArray())
260+
{
261+
if (string.IsNullOrEmpty(toStateName) || (t.destinationState != null && t.destinationState.name == toStateName))
262+
{
263+
rootStateMachine.RemoveAnyStateTransition(t);
264+
removed++;
265+
}
266+
}
267+
fromStateName = "AnyState";
268+
}
269+
else
270+
{
271+
AnimatorState fromState = null;
272+
foreach (var cs in rootStateMachine.states)
273+
if (cs.state.name == fromStateName) fromState = cs.state;
274+
if (fromState == null)
275+
return new { success = false, message = $"State '{fromStateName}' not found in layer {layerIndex}" };
276+
277+
foreach (var t in fromState.transitions.ToArray())
278+
{
279+
if (string.IsNullOrEmpty(toStateName) || (t.destinationState != null && t.destinationState.name == toStateName))
280+
{
281+
fromState.RemoveTransition(t);
282+
removed++;
283+
}
284+
}
285+
}
286+
287+
EditorUtility.SetDirty(controller);
288+
AssetDatabase.SaveAssets();
289+
290+
return new
291+
{
292+
success = true,
293+
message = $"Removed {removed} transition(s) from '{fromStateName}'" + (string.IsNullOrEmpty(toStateName) ? "" : $" to '{toStateName}'") + ".",
294+
data = new { fromState = fromStateName, toState = toStateName, removed }
295+
};
296+
}
297+
231298
public static object AddParameter(JObject @params)
232299
{
233300
var controller = LoadController(@params);
@@ -423,13 +490,13 @@ public static object AssignToGameObject(JObject @params)
423490
};
424491
}
425492

426-
// Reads node graph positions for every state (recurses into sub-state-machines).
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.
432-
public static object GetStatePositions(JObject @params)
493+
// Reads per-state properties for every state (recurses into sub-state-machines).
494+
// Returns [{ name, instanceId, layer, x, y, speed, motionInstanceId, motionName,
495+
// motionType }]. 'instanceId' round-trips into set_state_properties for an exact
496+
// match (duplicate names are fine); 'motionInstanceId' lets a caller transfer a
497+
// Motion (incl. FBX-embedded clips) to another state BY REFERENCE - no asset path.
498+
// Pass 'layerIndex' to scope to one layer; results are paged (page_size/cursor).
499+
public static object GetStateProperties(JObject @params)
433500
{
434501
var controller = LoadController(@params);
435502
if (controller == null)
@@ -444,7 +511,7 @@ public static object GetStatePositions(JObject @params)
444511
{
445512
if (layerFilter.HasValue && li != layerFilter.Value)
446513
continue;
447-
CollectPositions(controller.layers[li].stateMachine, li, nodes);
514+
CollectProperties(controller.layers[li].stateMachine, li, nodes);
448515
}
449516

450517
var pagination = PaginationRequest.FromParams(@params, defaultPageSize: 50);
@@ -453,7 +520,7 @@ public static object GetStatePositions(JObject @params)
453520
return new
454521
{
455522
success = true,
456-
message = $"Read {paged.Items.Count} of {paged.TotalCount} state position(s).",
523+
message = $"Read {paged.Items.Count} of {paged.TotalCount} state(s).",
457524
data = new
458525
{
459526
count = paged.TotalCount,
@@ -466,55 +533,63 @@ public static object GetStatePositions(JObject @params)
466533
};
467534
}
468535

469-
private static void CollectPositions(AnimatorStateMachine sm, int layer, List<object> outList)
536+
private static void CollectProperties(AnimatorStateMachine sm, int layer, List<object> outList)
470537
{
471538
var children = sm.states;
472539
for (int i = 0; i < children.Length; i++)
540+
{
541+
var st = children[i].state;
542+
var motion = st.motion;
473543
outList.Add(new
474544
{
475-
name = children[i].state.name,
476-
instanceId = children[i].state.GetInstanceIDLongCompat(),
545+
name = st.name,
546+
instanceId = st.GetInstanceIDLongCompat(),
547+
layer,
477548
x = children[i].position.x,
478549
y = children[i].position.y,
479-
layer
550+
speed = st.speed,
551+
motionInstanceId = motion != null ? motion.GetInstanceIDLongCompat() : (ulong?)null,
552+
motionName = motion != null ? motion.name : null,
553+
motionType = motion != null ? motion.GetType().Name : null
480554
});
555+
}
481556
foreach (var sub in sm.stateMachines)
482-
CollectPositions(sub.stateMachine, layer, outList);
557+
CollectProperties(sub.stateMachine, layer, outList);
483558
}
484559

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.
490-
public static object SetStatePositions(JObject @params)
560+
// Sets per-state properties from a 'states' array of { instanceId, [x], [y],
561+
// [speed], [motionInstanceId] }. States are matched by 'instanceId' for an exact,
562+
// unambiguous hit. Each field is OPTIONAL - only provided fields are written, so the
563+
// same call can move nodes, retime speed, and/or assign motion. 'motionInstanceId'
564+
// is resolved to a Motion via UnityObjectIdCompat.InstanceIDToObjectLongCompat and
565+
// assigned BY REFERENCE (works for FBX sub-asset clips - no asset-path lookup). Recurses into
566+
// sub-state-machines and reassigns stateMachine.states so edits persist.
567+
public static object SetStateProperties(JObject @params)
491568
{
492569
var controller = LoadController(@params);
493570
if (controller == null)
494571
return ControllerNotFoundError(@params);
495572

496-
if (!(@params["positions"] is JArray positions) || positions.Count == 0)
497-
return new { success = false, message = "'positions' array is required: [{ instanceId, x, y }, ...]" };
573+
if (!(@params["states"] is JArray arr) || arr.Count == 0)
574+
return new { success = false, message = "'states' array is required: [{ instanceId, x?, y?, speed?, motionInstanceId? }, ...]" };
498575

499-
var want = new Dictionary<ulong, Vector2>();
500-
foreach (var token in positions)
576+
var want = new Dictionary<ulong, JObject>();
577+
foreach (var token in arr)
501578
{
502579
if (!(token is JObject entry))
503580
continue;
504581
ulong? instanceId = entry["instanceId"]?.ToObject<ulong>();
505-
if (!instanceId.HasValue)
506-
continue;
507-
float x = entry["x"]?.ToObject<float>() ?? 0f;
508-
float y = entry["y"]?.ToObject<float>() ?? 0f;
509-
want[instanceId.Value] = new Vector2(x, y);
582+
if (instanceId.HasValue)
583+
want[instanceId.Value] = entry;
510584
}
511585
if (want.Count == 0)
512-
return new { success = false, message = "No valid entries in 'positions' (each needs an 'instanceId')." };
586+
return new { success = false, message = "No valid entries (each needs an 'instanceId')." };
513587

514588
var matched = new HashSet<ulong>();
515-
Undo.RecordObject(controller, "Set State Positions");
589+
var motionFailures = new List<object>();
590+
Undo.RecordObject(controller, "Set State Properties");
516591
for (int li = 0; li < controller.layers.Length; li++)
517-
ApplyPositions(controller.layers[li].stateMachine, want, matched);
592+
ApplyProperties(controller.layers[li].stateMachine, want, matched, motionFailures);
518593

519594
EditorUtility.SetDirty(controller);
520595
AssetDatabase.SaveAssets();
@@ -523,32 +598,73 @@ public static object SetStatePositions(JObject @params)
523598
return new
524599
{
525600
success = true,
526-
message = $"Positioned {matched.Count} state(s); {unmatched.Count} id(s) unmatched.",
601+
message = $"Updated {matched.Count} state(s); {unmatched.Count} id(s) unmatched; {motionFailures.Count} motion ref(s) failed.",
527602
data = new
528603
{
529604
matched = matched.Count,
530605
requested = want.Count,
531-
unmatched
606+
unmatched,
607+
motionFailures
532608
}
533609
};
534610
}
535611

536-
private static void ApplyPositions(AnimatorStateMachine sm, Dictionary<ulong, Vector2> want, HashSet<ulong> matched)
612+
private static void ApplyProperties(AnimatorStateMachine sm, Dictionary<ulong, JObject> want, HashSet<ulong> matched, List<object> motionFailures)
537613
{
538614
var children = sm.states;
539615
for (int i = 0; i < children.Length; i++)
540616
{
541617
ulong? id = children[i].state.GetInstanceIDLongCompat();
542-
if (id.HasValue && want.TryGetValue(id.Value, out var p))
618+
if (!id.HasValue || !want.TryGetValue(id.Value, out var entry))
619+
continue;
620+
621+
var st = children[i].state;
622+
623+
// Position (x and/or y) - keep the unspecified axis unchanged.
624+
if (entry["x"] != null || entry["y"] != null)
543625
{
544-
children[i].position = new Vector3(p.x, p.y, 0f);
545-
matched.Add(id.Value);
626+
var pos = children[i].position;
627+
float x = entry["x"]?.ToObject<float>() ?? pos.x;
628+
float y = entry["y"]?.ToObject<float>() ?? pos.y;
629+
children[i].position = new Vector3(x, y, 0f);
546630
}
631+
632+
// Speed
633+
if (entry["speed"] != null)
634+
st.speed = entry["speed"].ToObject<float>();
635+
636+
// Motion by reference (resolve instanceId -> Motion object). 0/null clears it.
637+
if (entry["motionInstanceId"] != null)
638+
{
639+
var token = entry["motionInstanceId"];
640+
if (token.Type == JTokenType.Null)
641+
{
642+
st.motion = null;
643+
}
644+
else
645+
{
646+
ulong refId = token.ToObject<ulong>();
647+
if (refId == 0UL)
648+
{
649+
st.motion = null;
650+
}
651+
else
652+
{
653+
var obj = UnityObjectIdCompat.InstanceIDToObjectLongCompat(refId) as Motion;
654+
if (obj != null)
655+
st.motion = obj;
656+
else
657+
motionFailures.Add(new { instanceId = id.Value, motionInstanceId = refId });
658+
}
659+
}
660+
}
661+
662+
matched.Add(id.Value);
547663
}
548-
sm.states = children; // reassign so position edits persist
664+
sm.states = children; // reassign so edits persist
549665

550666
foreach (var sub in sm.stateMachines)
551-
ApplyPositions(sub.stateMachine, want, matched);
667+
ApplyProperties(sub.stateMachine, want, matched, motionFailures);
552668
}
553669

554670
private static AnimatorController LoadController(JObject @params)

MCPForUnity/Editor/Tools/Animation/ManageAnimation.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,10 @@ private static object HandleControllerAction(JObject @params, string action)
217217
{
218218
case "create": return ControllerCreate.Create(@params);
219219
case "add_state": return ControllerCreate.AddState(@params);
220-
case "set_state_positions": return ControllerCreate.SetStatePositions(@params);
221-
case "get_state_positions": return ControllerCreate.GetStatePositions(@params);
220+
case "set_state_properties": return ControllerCreate.SetStateProperties(@params);
221+
case "get_state_properties": return ControllerCreate.GetStateProperties(@params);
222222
case "add_transition": return ControllerCreate.AddTransition(@params);
223+
case "remove_transition": return ControllerCreate.RemoveTransition(@params);
223224
case "add_parameter": return ControllerCreate.AddParameter(@params);
224225
case "get_info": return ControllerCreate.GetInfo(@params);
225226
case "assign": return ControllerCreate.AssignToGameObject(@params);
@@ -230,7 +231,7 @@ private static object HandleControllerAction(JObject @params, string action)
230231
case "create_blend_tree_2d": return ControllerBlendTrees.CreateBlendTree2D(@params);
231232
case "add_blend_tree_child": return ControllerBlendTrees.AddBlendTreeChild(@params);
232233
default:
233-
return new { success = false, message = $"Unknown controller action: {action}. Valid: create, add_state, set_state_positions, get_state_positions, add_transition, add_parameter, get_info, assign, add_layer, remove_layer, set_layer_weight, create_blend_tree_1d, create_blend_tree_2d, add_blend_tree_child" };
234+
return new { success = false, message = $"Unknown controller action: {action}. Valid: create, add_state, set_state_properties, get_state_properties, add_transition, remove_transition, add_parameter, get_info, assign, add_layer, remove_layer, set_layer_weight, create_blend_tree_1d, create_blend_tree_2d, add_blend_tree_child" };
234235
}
235236
}
236237

Server/src/services/tools/manage_animation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616

1717
CONTROLLER_ACTIONS = [
1818
"controller_create", "controller_add_state",
19-
"controller_set_state_positions", "controller_get_state_positions",
20-
"controller_add_transition",
19+
"controller_set_state_properties", "controller_get_state_properties",
20+
"controller_add_transition", "controller_remove_transition",
2121
"controller_add_parameter", "controller_get_info", "controller_assign",
2222
"controller_add_layer", "controller_remove_layer", "controller_set_layer_weight",
2323
"controller_create_blend_tree_1d", "controller_create_blend_tree_2d", "controller_add_blend_tree_child",

0 commit comments

Comments
 (0)