rokojori_action_library/Tools/mcp/Routes/WriteSceneFragmentRoute.cs

219 lines
6.7 KiB
C#
Raw Normal View History

2026-10-03 12:15:58 +00:00
#if TOOLS
using Godot;
namespace Rokojori.Tools
{
using Rokojori;
public static class WriteSceneFragmentRoute
{
public static JSONObject ToolDefinition()
{
var filePathSchema = new JSONObject();
filePathSchema.Set( "type", "string" );
filePathSchema.Set( "description", "res:// path to the scene (.tscn)" );
var scenePathSchema = new JSONObject();
scenePathSchema.Set( "type", "array" );
scenePathSchema.Set( "description", "Child names from the scene root to the node the fragment patches. Empty array = scene root. The node itself must already exist - only its descendants can be newly created." );
var fragmentSchema = new JSONObject();
fragmentSchema.Set( "type", "object" );
fragmentSchema.Set( "description", "Patch data: { properties: {...}, children: { childName: {...} | { \"__remove\": true } } }. Children are keyed by name, omitted children are left untouched." );
var properties = new JSONObject();
properties.Set( "filePath", filePathSchema );
properties.Set( "scenePath", scenePathSchema );
properties.Set( "sceneFragmentData", fragmentSchema );
var required = new JSONArray();
required.Push( "filePath" );
required.Push( "scenePath" );
required.Push( "sceneFragmentData" );
var schema = new JSONObject();
schema.Set( "type", "object" );
schema.Set( "properties", properties );
schema.Set( "required", required );
var tool = new JSONObject();
tool.Set( "name", "write_scene_fragment" );
tool.Set( "description", "Patch a node subtree in a scene file: set properties, add/patch/remove children by name." );
tool.Set( "inputSchema", schema );
return tool;
}
public static ToolCallOutcome Execute( JSONObject arguments )
{
var filePath = JsonUtil.GetStringOrNull( arguments, "filePath" );
var scenePathArray = arguments != null ? arguments.GetArray( "scenePath" ) : null;
var fragmentData = arguments != null ? arguments.GetObject( "sceneFragmentData" ) : null;
if ( string.IsNullOrEmpty( filePath ) )
{
return ToolCallOutcome.Error( "Missing required argument: filePath" );
}
if ( fragmentData == null )
{
return ToolCallOutcome.Error( "Missing required argument: sceneFragmentData" );
}
if ( ! ResourceLoader.Exists( filePath, "PackedScene" ) )
{
return ToolCallOutcome.Error( "Scene not found: " + filePath );
}
var packedScene = ResourceLoader.Load<PackedScene>( filePath );
if ( packedScene == null )
{
return ToolCallOutcome.Error( "Failed to load scene: " + filePath );
}
// Plain Instantiate() resolves every property on every node to a concrete value, including
// ones a node only inherits from an instanced sub-scene. Pack() then has no way to tell
// "inherited" apart from "actually overridden" and bakes the current value of every property
// on every instanced node in the whole tree into an explicit override - corrupting unrelated
// sub-scenes (e.g. a vehicle model) far outside the patched fragment. GenEditState.Main keeps
// the instance-vs-override bookkeeping alive so Pack() only stores genuine overrides, matching
// how the editor itself instantiates a scene it's about to re-save as the main edited scene.
var root = packedScene.Instantiate( PackedScene.GenEditState.Main );
if ( root == null )
{
return ToolCallOutcome.Error( "Failed to instantiate scene: " + filePath );
}
var scenePath = ScenePathResolver.ToStringArray( scenePathArray );
var target = ScenePathResolver.FindNode( root, scenePath );
if ( target == null )
{
root.Free();
return ToolCallOutcome.Error( "Node not found at scenePath: " + ScenePathResolver.ToRelativePath( scenePath ) );
}
ApplyFragment( root, target, fragmentData );
var packError = packedScene.Pack( root );
if ( packError != Error.Ok )
{
root.Free();
return ToolCallOutcome.Error( "Failed to pack scene: " + packError );
}
var saveError = ResourceSaver.Save( packedScene, filePath );
root.Free();
if ( saveError != Error.Ok )
{
return ToolCallOutcome.Error( "Failed to save scene: " + saveError );
}
var result = new JSONObject();
result.Set( "filePath", filePath );
result.Set( "scenePath", ScenePathResolver.ToRelativePath( scenePath ) );
return ToolCallOutcome.Ok( JSON.Stringify( result ) );
}
static void ApplyFragment( Node root, Node target, JSONObject fragmentData )
{
var properties = fragmentData.GetObject( "properties" );
if ( properties != null )
{
PropertyReflection.ApplyFromJson( target, properties );
}
var children = fragmentData.GetObject( "children" );
if ( children == null )
{
return;
}
for ( var i = 0; i < children.keys.Count; i++ )
{
var childName = children.keys[ i ];
var childData = children.values[ i ].AsObject();
if ( childData == null )
{
continue;
}
var existingChild = FindDirectChild( target, childName );
if ( childData.Contains( "__remove" ) )
{
if ( existingChild != null )
{
target.RemoveChild( existingChild );
existingChild.Free();
}
continue;
}
if ( existingChild != null )
{
ApplyFragment( root, existingChild, childData );
continue;
}
var typeName = JsonUtil.GetStringOrNull( childData, "type" );
if ( string.IsNullOrEmpty( typeName ) )
{
RJLog.Error( "MCP WriteSceneFragment: new child needs a 'type'", childName );
continue;
}
if ( ! ClassDB.ClassExists( typeName ) )
{
RJLog.Error( "MCP WriteSceneFragment: unknown class", typeName );
continue;
}
var newNode = ClassDB.Instantiate( typeName ).As<Node>();
if ( newNode == null )
{
RJLog.Error( "MCP WriteSceneFragment: class is not a Node", typeName );
continue;
}
newNode.Name = childName;
target.AddChild( newNode );
newNode.Owner = root;
ApplyFragment( root, newNode, childData );
}
}
static Node FindDirectChild( Node parent, string name )
{
var count = parent.GetChildCount( false );
for ( var i = 0; i < count; i++ )
{
var child = parent.GetChild( i, false );
if ( child.Name.ToString() == name )
{
return child;
}
}
return null;
}
}
}
#endif