@@ -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 )
0 commit comments