MCP Updates
This commit is contained in:
parent
79fd695989
commit
385a3069e4
|
|
@ -65,6 +65,7 @@ public partial class RokojoriPlugin: EditorPlugin
|
||||||
GizmoDrawerPlugin gizmoDrawerPlugin = new GizmoDrawerPlugin();
|
GizmoDrawerPlugin gizmoDrawerPlugin = new GizmoDrawerPlugin();
|
||||||
GodotEditorInspectorTools inspectorTools = new GodotEditorInspectorTools();
|
GodotEditorInspectorTools inspectorTools = new GodotEditorInspectorTools();
|
||||||
DeduplicatingExportPlugin deduplicatingExportPlugin = new DeduplicatingExportPlugin();
|
DeduplicatingExportPlugin deduplicatingExportPlugin = new DeduplicatingExportPlugin();
|
||||||
|
Rokojori.Tools.McpServer mcpServer = new Rokojori.Tools.McpServer();
|
||||||
|
|
||||||
public static readonly string path = "res://addons/rokojori_action_library";
|
public static readonly string path = "res://addons/rokojori_action_library";
|
||||||
|
|
||||||
|
|
@ -168,12 +169,16 @@ public partial class RokojoriPlugin: EditorPlugin
|
||||||
|
|
||||||
inspectorTools.rokojoriPlugin = this;
|
inspectorTools.rokojoriPlugin = this;
|
||||||
inspectorTools.AddPlugins();
|
inspectorTools.AddPlugins();
|
||||||
|
|
||||||
|
mcpServer.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void _ExitTree()
|
public override void _ExitTree()
|
||||||
{
|
{
|
||||||
this.LogInfo();
|
this.LogInfo();
|
||||||
|
|
||||||
|
mcpServer.Stop();
|
||||||
|
|
||||||
RemoveNode3DGizmoPlugin( gizmoDrawerPlugin );
|
RemoveNode3DGizmoPlugin( gizmoDrawerPlugin );
|
||||||
RemoveExportPlugin( deduplicatingExportPlugin );
|
RemoveExportPlugin( deduplicatingExportPlugin );
|
||||||
|
|
||||||
|
|
@ -236,6 +241,8 @@ public partial class RokojoriPlugin: EditorPlugin
|
||||||
|
|
||||||
public override void _Process( double delta )
|
public override void _Process( double delta )
|
||||||
{
|
{
|
||||||
|
mcpServer.ProcessPendingToolCalls();
|
||||||
|
|
||||||
var editingScene = EditorInterface.Singleton.GetEditedSceneRoot();
|
var editingScene = EditorInterface.Singleton.GetEditedSceneRoot();
|
||||||
|
|
||||||
if ( lastUpdateTime.HasExpired( reloadDuration ) )
|
if ( lastUpdateTime.HasExpired( reloadDuration ) )
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ namespace Rokojori
|
||||||
// var startValue = ReflectionHelper.GetValue<float>( target, targetMember );
|
// var startValue = ReflectionHelper.GetValue<float>( target, targetMember );
|
||||||
var startValue = GetTargetValue( target, targetMemberPath );
|
var startValue = GetTargetValue( target, targetMemberPath );
|
||||||
|
|
||||||
if ( forceStartValue && Engine.IsEditorHint() )
|
if ( forceStartValue )
|
||||||
{
|
{
|
||||||
startValue = forcedStartValue;
|
startValue = forcedStartValue;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
[gd_resource type="Resource" script_class="FloatPropertyName" format=3 uid="uid://c21kji5ygrnue"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://jqgdm3r2u8xq" path="res://addons/rokojori_action_library/Runtime/Shading/Properties/FloatPropertyName.cs" id="1_gn73r"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_gn73r")
|
||||||
|
propertyName = "emission_energy_multiplier"
|
||||||
|
metadata/_custom_type_script = "uid://jqgdm3r2u8xq"
|
||||||
|
|
@ -361,6 +361,11 @@ namespace Rokojori
|
||||||
}
|
}
|
||||||
|
|
||||||
occlusion.ApplyOcclusion( this );
|
occlusion.ApplyOcclusion( this );
|
||||||
|
|
||||||
|
if ( preset.disableFrustumCulling )
|
||||||
|
{
|
||||||
|
meshInstance.CustomAabb = Box3.FromPositionAndScale( Vector3.Zero, Vector3.One * 100000 );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,10 @@ namespace Rokojori
|
||||||
[Export]
|
[Export]
|
||||||
public FlareLayer[] layers = [];
|
public FlareLayer[] layers = [];
|
||||||
|
|
||||||
|
[Export]
|
||||||
|
public bool disableFrustumCulling = false;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[ExportGroup("Settings")]
|
[ExportGroup("Settings")]
|
||||||
[Export]
|
[Export]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
#if TOOLS
|
||||||
|
|
||||||
|
namespace Rokojori.Tools
|
||||||
|
{
|
||||||
|
using Rokojori;
|
||||||
|
|
||||||
|
public static class JsonRpc
|
||||||
|
{
|
||||||
|
public static JSONObject BuildResult( JSONData id, JSONData result )
|
||||||
|
{
|
||||||
|
var obj = new JSONObject();
|
||||||
|
obj.Set( "jsonrpc", "2.0" );
|
||||||
|
obj.Set( "id", id ?? new JSONValue() );
|
||||||
|
obj.Set( "result", result );
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JSONObject BuildError( JSONData id, int code, string message )
|
||||||
|
{
|
||||||
|
var error = new JSONObject();
|
||||||
|
error.Set( "code", (double) code );
|
||||||
|
error.Set( "message", message );
|
||||||
|
|
||||||
|
var obj = new JSONObject();
|
||||||
|
obj.Set( "jsonrpc", "2.0" );
|
||||||
|
obj.Set( "id", id ?? new JSONValue() );
|
||||||
|
obj.Set( "error", error );
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetMethod( JSONObject envelope )
|
||||||
|
{
|
||||||
|
var data = envelope.Get( "method" );
|
||||||
|
return data != null && data.isString ? data.stringValue : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool IsNotification( JSONObject envelope )
|
||||||
|
{
|
||||||
|
return ! envelope.HasKey( "id" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://clk267spq5gga
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
#if TOOLS
|
||||||
|
|
||||||
|
namespace Rokojori.Tools
|
||||||
|
{
|
||||||
|
using Rokojori;
|
||||||
|
|
||||||
|
// JSONObject.GetString/GetNumber/GetBoolean etc. call Get(key) and dereference the result
|
||||||
|
// without a null check - they throw if the key is simply absent, which is the normal case
|
||||||
|
// for optional/missing request fields. This is the null-safe version for that situation.
|
||||||
|
public static class JsonUtil
|
||||||
|
{
|
||||||
|
public static string GetStringOrNull( JSONObject obj, string key )
|
||||||
|
{
|
||||||
|
if ( obj == null )
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = obj.Get( key );
|
||||||
|
return data != null && data.isString ? data.stringValue : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c5sdnp77gvpl5
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
#if TOOLS
|
||||||
|
|
||||||
|
namespace Rokojori.Tools
|
||||||
|
{
|
||||||
|
using Rokojori;
|
||||||
|
|
||||||
|
// Pure protocol logic - no Godot API access except inside ExecuteTool's route calls,
|
||||||
|
// which is why only tools/call needs to cross onto the main thread (see McpServer).
|
||||||
|
public class McpProtocolHandler
|
||||||
|
{
|
||||||
|
public JSONObject Initialize( JSONData id, JSONObject paramsObj )
|
||||||
|
{
|
||||||
|
var requestedVersion = JsonUtil.GetStringOrNull( paramsObj, "protocolVersion" );
|
||||||
|
var protocolVersion = string.IsNullOrEmpty( requestedVersion ) ? "2025-06-18" : requestedVersion;
|
||||||
|
|
||||||
|
var capabilities = new JSONObject();
|
||||||
|
capabilities.Set( "tools", new JSONObject() );
|
||||||
|
|
||||||
|
var serverInfo = new JSONObject();
|
||||||
|
serverInfo.Set( "name", "godot-mcp" );
|
||||||
|
serverInfo.Set( "version", "0.1.0" );
|
||||||
|
|
||||||
|
var result = new JSONObject();
|
||||||
|
result.Set( "protocolVersion", protocolVersion );
|
||||||
|
result.Set( "capabilities", capabilities );
|
||||||
|
result.Set( "serverInfo", serverInfo );
|
||||||
|
|
||||||
|
return JsonRpc.BuildResult( id, result );
|
||||||
|
}
|
||||||
|
|
||||||
|
public JSONObject ToolsList( JSONData id )
|
||||||
|
{
|
||||||
|
var tools = new JSONArray();
|
||||||
|
tools.Push( ReadResourceRoute.ToolDefinition() );
|
||||||
|
tools.Push( WriteResourceRoute.ToolDefinition() );
|
||||||
|
tools.Push( ReadSceneFragmentRoute.ToolDefinition() );
|
||||||
|
tools.Push( WriteSceneFragmentRoute.ToolDefinition() );
|
||||||
|
|
||||||
|
var result = new JSONObject();
|
||||||
|
result.Set( "tools", tools );
|
||||||
|
|
||||||
|
return JsonRpc.BuildResult( id, result );
|
||||||
|
}
|
||||||
|
|
||||||
|
public JSONObject ToolCallResult( JSONData id, ToolCallOutcome outcome )
|
||||||
|
{
|
||||||
|
var item = new JSONObject();
|
||||||
|
item.Set( "type", "text" );
|
||||||
|
item.Set( "text", outcome.text );
|
||||||
|
|
||||||
|
var content = new JSONArray();
|
||||||
|
content.Push( item );
|
||||||
|
|
||||||
|
var result = new JSONObject();
|
||||||
|
result.Set( "content", content );
|
||||||
|
result.Set( "isError", outcome.isError );
|
||||||
|
|
||||||
|
return JsonRpc.BuildResult( id, result );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs on the main thread, dequeued from McpServer's pending-call queue.
|
||||||
|
public ToolCallOutcome ExecuteTool( string toolName, JSONObject arguments )
|
||||||
|
{
|
||||||
|
switch ( toolName )
|
||||||
|
{
|
||||||
|
case "read_resource":
|
||||||
|
return ReadResourceRoute.Execute( arguments );
|
||||||
|
|
||||||
|
case "write_resource":
|
||||||
|
return WriteResourceRoute.Execute( arguments );
|
||||||
|
|
||||||
|
case "read_scene_fragment":
|
||||||
|
return ReadSceneFragmentRoute.Execute( arguments );
|
||||||
|
|
||||||
|
case "write_scene_fragment":
|
||||||
|
return WriteSceneFragmentRoute.Execute( arguments );
|
||||||
|
|
||||||
|
default:
|
||||||
|
return ToolCallOutcome.Error( "Unknown tool: " + toolName );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://byu52qd5hc76w
|
||||||
|
|
@ -0,0 +1,359 @@
|
||||||
|
#if TOOLS
|
||||||
|
|
||||||
|
using Godot;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Net;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Rokojori.Tools
|
||||||
|
{
|
||||||
|
using Rokojori;
|
||||||
|
|
||||||
|
// Owned by RokojoriPlugin: Start()/Stop() from _EnterTree/_ExitTree,
|
||||||
|
// ProcessPendingToolCalls() from the existing _Process tick.
|
||||||
|
//
|
||||||
|
// Threading: HttpListener runs its accept loop on a background thread. initialize,
|
||||||
|
// notifications/initialized and tools/list are pure protocol/metadata and are answered
|
||||||
|
// directly on that thread. Only tools/call touches Godot APIs (ResourceLoader, PackedScene,
|
||||||
|
// the scene tree) - it is enqueued and the background thread blocks until the main thread
|
||||||
|
// drains the queue and signals the result back.
|
||||||
|
public class McpServer
|
||||||
|
{
|
||||||
|
public const string FlagFilePath = "res://.godot-mcp";
|
||||||
|
public const int PortRangeStart = 9400;
|
||||||
|
public const int PortRangeCount = 20;
|
||||||
|
|
||||||
|
HttpListener listener;
|
||||||
|
Thread listenerThread;
|
||||||
|
volatile bool running = false;
|
||||||
|
int boundPort = -1;
|
||||||
|
string projectPath = "";
|
||||||
|
|
||||||
|
readonly McpProtocolHandler protocol = new McpProtocolHandler();
|
||||||
|
readonly ConcurrentQueue<PendingToolCall> pendingCalls = new ConcurrentQueue<PendingToolCall>();
|
||||||
|
|
||||||
|
public bool IsRunning { get { return running; } }
|
||||||
|
public int BoundPort { get { return boundPort; } }
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
if ( running )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var flagGlobalPath = ProjectSettings.GlobalizePath( FlagFilePath );
|
||||||
|
|
||||||
|
if ( ! FilesSync.FileExists( flagGlobalPath ) )
|
||||||
|
{
|
||||||
|
RJLog.Log( "MCP", "Flag file not found (" + FlagFilePath + ") - server not starting." );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
projectPath = ProjectSettings.GlobalizePath( "res://" );
|
||||||
|
|
||||||
|
for ( var port = PortRangeStart; port < PortRangeStart + PortRangeCount; port++ )
|
||||||
|
{
|
||||||
|
if ( TryBind( port ) )
|
||||||
|
{
|
||||||
|
boundPort = port;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( listener == null )
|
||||||
|
{
|
||||||
|
RJLog.Error( "MCP", "Could not bind any port in range", PortRangeStart, "-", ( PortRangeStart + PortRangeCount - 1 ) );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
running = true;
|
||||||
|
listenerThread = new Thread( ListenLoop );
|
||||||
|
listenerThread.IsBackground = true;
|
||||||
|
listenerThread.Start();
|
||||||
|
|
||||||
|
RJLog.Log( "MCP", "Server started on port", boundPort, "for project", projectPath );
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
if ( ! running )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
running = false;
|
||||||
|
|
||||||
|
// A tools/call request blocks the single HTTP thread on pending.waitHandle, waiting for
|
||||||
|
// a future _Process() tick to dequeue and signal it. If that tick never comes (e.g. the
|
||||||
|
// editor is pausing the main loop to hot-reload this very assembly), the thread stays
|
||||||
|
// alive holding a reference into this assembly and the reload fails to unload it. Stop()
|
||||||
|
// always runs on the main thread, never concurrently with _Process(), so it can safely
|
||||||
|
// stand in for the tick that won't arrive and release any still-queued call right now.
|
||||||
|
while ( pendingCalls.TryDequeue( out var pending ) )
|
||||||
|
{
|
||||||
|
pending.outcome = ToolCallOutcome.Error( "MCP server is stopping" );
|
||||||
|
pending.waitHandle.Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The accept loop is parked inside the blocking listener.GetContext() call from its previous
|
||||||
|
// iteration. HttpListener.Stop()/Close() are documented to abort that call, but that has not
|
||||||
|
// proven reliable in practice - if it doesn't, the thread stays parked forever, which blocks
|
||||||
|
// Godot's assembly unload on every future hot reload, not just one that catches a request
|
||||||
|
// in flight. Firing one harmless loopback request guarantees GetContext() returns (with that
|
||||||
|
// request) so the loop can observe running == false on its own and exit, instead of depending
|
||||||
|
// on Stop()/Close() to interrupt it.
|
||||||
|
WakeListenerThread();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
listener?.Stop();
|
||||||
|
listener?.Close();
|
||||||
|
}
|
||||||
|
catch ( Exception )
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
listenerThread?.Join( 2000 );
|
||||||
|
listener = null;
|
||||||
|
boundPort = -1;
|
||||||
|
|
||||||
|
RJLog.Log( "MCP", "Server stopped." );
|
||||||
|
}
|
||||||
|
|
||||||
|
void WakeListenerThread()
|
||||||
|
{
|
||||||
|
if ( boundPort <= 0 )
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using ( var client = new System.Net.Sockets.TcpClient() )
|
||||||
|
{
|
||||||
|
client.Connect( "127.0.0.1", boundPort );
|
||||||
|
|
||||||
|
var bytes = Encoding.ASCII.GetBytes(
|
||||||
|
"GET /__mcp_shutdown HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
client.GetStream().Write( bytes, 0, bytes.Length );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch ( Exception )
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drains tool-call requests queued by the HTTP thread. Must run on the main thread.
|
||||||
|
public void ProcessPendingToolCalls()
|
||||||
|
{
|
||||||
|
while ( pendingCalls.TryDequeue( out var pending ) )
|
||||||
|
{
|
||||||
|
ToolCallOutcome outcome;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
outcome = protocol.ExecuteTool( pending.toolName, pending.arguments );
|
||||||
|
}
|
||||||
|
catch ( Exception e )
|
||||||
|
{
|
||||||
|
RJLog.Error( "MCP", "Tool execution failed", pending.toolName, e );
|
||||||
|
outcome = ToolCallOutcome.Error( e.Message );
|
||||||
|
}
|
||||||
|
|
||||||
|
pending.outcome = outcome;
|
||||||
|
pending.waitHandle.Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TryBind( int port )
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var candidate = new HttpListener();
|
||||||
|
// http.sys matches the Host header against registered prefixes exactly - "localhost"
|
||||||
|
// and "127.0.0.1" don't match each other, so both are registered since the MCP client
|
||||||
|
// config could plausibly use either.
|
||||||
|
candidate.Prefixes.Add( "http://localhost:" + port + "/" );
|
||||||
|
candidate.Prefixes.Add( "http://127.0.0.1:" + port + "/" );
|
||||||
|
candidate.Start();
|
||||||
|
listener = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch ( HttpListenerException )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
catch ( System.Net.Sockets.SocketException )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ListenLoop()
|
||||||
|
{
|
||||||
|
while ( running )
|
||||||
|
{
|
||||||
|
HttpListenerContext context;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
context = listener.GetContext();
|
||||||
|
}
|
||||||
|
catch ( Exception )
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HandleContext( context );
|
||||||
|
}
|
||||||
|
catch ( Exception e )
|
||||||
|
{
|
||||||
|
RJLog.Error( "MCP", "Unhandled error handling request", e );
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 500;
|
||||||
|
context.Response.OutputStream.Close();
|
||||||
|
}
|
||||||
|
catch ( Exception )
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleContext( HttpListenerContext context )
|
||||||
|
{
|
||||||
|
var request = context.Request;
|
||||||
|
var response = context.Response;
|
||||||
|
|
||||||
|
if ( request.HttpMethod == "GET" && request.Url.AbsolutePath == "/project-path" )
|
||||||
|
{
|
||||||
|
var discovery = new JSONObject();
|
||||||
|
discovery.Set( "projectPath", projectPath );
|
||||||
|
discovery.Set( "port", (double) boundPort );
|
||||||
|
WriteJson( response, 200, discovery );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( request.HttpMethod != "POST" || request.Url.AbsolutePath != "/mcp" )
|
||||||
|
{
|
||||||
|
response.StatusCode = 404;
|
||||||
|
response.OutputStream.Close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string body;
|
||||||
|
|
||||||
|
using ( var reader = new System.IO.StreamReader( request.InputStream, request.ContentEncoding ?? Encoding.UTF8 ) )
|
||||||
|
{
|
||||||
|
body = reader.ReadToEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
HandleJsonRpc( body, response );
|
||||||
|
}
|
||||||
|
|
||||||
|
void HandleJsonRpc( string body, HttpListenerResponse response )
|
||||||
|
{
|
||||||
|
JSONObject envelope;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var parsed = JSON.Parse( body );
|
||||||
|
envelope = parsed != null ? parsed.AsObject() : null;
|
||||||
|
|
||||||
|
if ( envelope == null )
|
||||||
|
{
|
||||||
|
throw new Exception( "Request body is not a JSON object" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch ( Exception )
|
||||||
|
{
|
||||||
|
WriteJson( response, 200, JsonRpc.BuildError( new JSONValue(), -32700, "Parse error" ) );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = envelope.Get( "id" );
|
||||||
|
var isNotification = JsonRpc.IsNotification( envelope );
|
||||||
|
var method = JsonRpc.GetMethod( envelope );
|
||||||
|
var paramsObj = envelope.GetObject( "params" );
|
||||||
|
|
||||||
|
if ( isNotification )
|
||||||
|
{
|
||||||
|
WriteEmpty( response, 202 );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ( method )
|
||||||
|
{
|
||||||
|
case "initialize":
|
||||||
|
WriteJson( response, 200, protocol.Initialize( id, paramsObj ) );
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "tools/list":
|
||||||
|
WriteJson( response, 200, protocol.ToolsList( id ) );
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "tools/call":
|
||||||
|
var outcome = DispatchToolCallBlocking( paramsObj );
|
||||||
|
WriteJson( response, 200, protocol.ToolCallResult( id, outcome ) );
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
WriteJson( response, 200, JsonRpc.BuildError( id, -32601, "Method not found: " + method ) );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolCallOutcome DispatchToolCallBlocking( JSONObject paramsObj )
|
||||||
|
{
|
||||||
|
var toolName = JsonUtil.GetStringOrNull( paramsObj, "name" );
|
||||||
|
var arguments = paramsObj != null ? paramsObj.GetObject( "arguments" ) : null;
|
||||||
|
|
||||||
|
if ( arguments == null )
|
||||||
|
{
|
||||||
|
arguments = new JSONObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
var pending = new PendingToolCall( toolName, arguments );
|
||||||
|
pendingCalls.Enqueue( pending );
|
||||||
|
|
||||||
|
var completed = pending.waitHandle.Wait( TimeSpan.FromSeconds( 30 ) );
|
||||||
|
|
||||||
|
if ( ! completed )
|
||||||
|
{
|
||||||
|
return ToolCallOutcome.Error( "Timed out waiting for the main thread to execute tool: " + toolName );
|
||||||
|
}
|
||||||
|
|
||||||
|
return pending.outcome ?? ToolCallOutcome.Error( "Tool produced no result: " + toolName );
|
||||||
|
}
|
||||||
|
|
||||||
|
static void WriteJson( HttpListenerResponse response, int statusCode, JSONData data )
|
||||||
|
{
|
||||||
|
var text = data == null ? "" : JSON.Stringify( data );
|
||||||
|
var bytes = Encoding.UTF8.GetBytes( text );
|
||||||
|
|
||||||
|
response.StatusCode = statusCode;
|
||||||
|
response.ContentType = "application/json";
|
||||||
|
response.ContentLength64 = bytes.Length;
|
||||||
|
response.OutputStream.Write( bytes, 0, bytes.Length );
|
||||||
|
response.OutputStream.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void WriteEmpty( HttpListenerResponse response, int statusCode )
|
||||||
|
{
|
||||||
|
response.StatusCode = statusCode;
|
||||||
|
response.ContentLength64 = 0;
|
||||||
|
response.OutputStream.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://njs03u236he2
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#if TOOLS
|
||||||
|
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace Rokojori.Tools
|
||||||
|
{
|
||||||
|
using Rokojori;
|
||||||
|
|
||||||
|
public class PendingToolCall
|
||||||
|
{
|
||||||
|
public string toolName;
|
||||||
|
public JSONObject arguments;
|
||||||
|
public ManualResetEventSlim waitHandle = new ManualResetEventSlim( false );
|
||||||
|
public ToolCallOutcome outcome;
|
||||||
|
|
||||||
|
public PendingToolCall( string toolName, JSONObject arguments )
|
||||||
|
{
|
||||||
|
this.toolName = toolName;
|
||||||
|
this.arguments = arguments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dy170v7wjkcj5
|
||||||
|
|
@ -0,0 +1,86 @@
|
||||||
|
#if TOOLS
|
||||||
|
|
||||||
|
using Godot;
|
||||||
|
using Godot.Collections;
|
||||||
|
|
||||||
|
namespace Rokojori.Tools
|
||||||
|
{
|
||||||
|
using Rokojori;
|
||||||
|
|
||||||
|
// Walks GetPropertyList() filtered to PropertyUsageFlags.Storage, matching what
|
||||||
|
// Godot's own .tscn/.tres serializer persists - so round-trips don't add/lose properties.
|
||||||
|
public static class PropertyReflection
|
||||||
|
{
|
||||||
|
public static JSONObject ToJson( GodotObject target )
|
||||||
|
{
|
||||||
|
var result = new JSONObject();
|
||||||
|
var properties = target.GetPropertyList();
|
||||||
|
|
||||||
|
foreach ( var propertyDictionary in properties )
|
||||||
|
{
|
||||||
|
var dict = (Dictionary) propertyDictionary;
|
||||||
|
|
||||||
|
if ( ! IsStored( dict ) )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = dict[ "name" ].AsString();
|
||||||
|
|
||||||
|
if ( string.IsNullOrEmpty( name ) )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = target.Get( name );
|
||||||
|
result.Set( name, VariantJson.ToJson( value ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void ApplyFromJson( GodotObject target, JSONObject data )
|
||||||
|
{
|
||||||
|
var properties = target.GetPropertyList();
|
||||||
|
var typesByName = new System.Collections.Generic.Dictionary<string, Variant.Type>();
|
||||||
|
|
||||||
|
foreach ( var propertyDictionary in properties )
|
||||||
|
{
|
||||||
|
var dict = (Dictionary) propertyDictionary;
|
||||||
|
var name = dict[ "name" ].AsString();
|
||||||
|
|
||||||
|
if ( ! string.IsNullOrEmpty( name ) )
|
||||||
|
{
|
||||||
|
typesByName[ name ] = (Variant.Type) dict[ "type" ].AsInt32();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( var i = 0; i < data.keys.Count; i++ )
|
||||||
|
{
|
||||||
|
var name = data.keys[ i ];
|
||||||
|
|
||||||
|
if ( ! typesByName.ContainsKey( name ) )
|
||||||
|
{
|
||||||
|
RJLog.Error( "MCP PropertyReflection: unknown property", name, "on", target.GetClass() );
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var variant = VariantJson.FromJson( data.values[ i ], typesByName[ name ] );
|
||||||
|
target.Set( name, variant );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool IsStored( Dictionary propertyInfo )
|
||||||
|
{
|
||||||
|
if ( ! propertyInfo.ContainsKey( "usage" ) )
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var usage = propertyInfo[ "usage" ].AsInt64();
|
||||||
|
return ( usage & (long) PropertyUsageFlags.Storage ) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bdgxk1qlp7hqq
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
#if TOOLS
|
||||||
|
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Rokojori.Tools
|
||||||
|
{
|
||||||
|
using Rokojori;
|
||||||
|
|
||||||
|
public static class ReadResourceRoute
|
||||||
|
{
|
||||||
|
public static JSONObject ToolDefinition()
|
||||||
|
{
|
||||||
|
var pathSchema = new JSONObject();
|
||||||
|
pathSchema.Set( "type", "string" );
|
||||||
|
pathSchema.Set( "description", "res:// path to the resource (.tres/.res)" );
|
||||||
|
|
||||||
|
var properties = new JSONObject();
|
||||||
|
properties.Set( "path", pathSchema );
|
||||||
|
|
||||||
|
var required = new JSONArray();
|
||||||
|
required.Push( "path" );
|
||||||
|
|
||||||
|
var schema = new JSONObject();
|
||||||
|
schema.Set( "type", "object" );
|
||||||
|
schema.Set( "properties", properties );
|
||||||
|
schema.Set( "required", required );
|
||||||
|
|
||||||
|
var tool = new JSONObject();
|
||||||
|
tool.Set( "name", "read_resource" );
|
||||||
|
tool.Set( "description", "Read a Resource's stored properties as JSON." );
|
||||||
|
tool.Set( "inputSchema", schema );
|
||||||
|
|
||||||
|
return tool;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ToolCallOutcome Execute( JSONObject arguments )
|
||||||
|
{
|
||||||
|
var path = JsonUtil.GetStringOrNull( arguments, "path" );
|
||||||
|
|
||||||
|
if ( string.IsNullOrEmpty( path ) )
|
||||||
|
{
|
||||||
|
return ToolCallOutcome.Error( "Missing required argument: path" );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! ResourceLoader.Exists( path, "" ) )
|
||||||
|
{
|
||||||
|
return ToolCallOutcome.Error( "Resource not found: " + path );
|
||||||
|
}
|
||||||
|
|
||||||
|
var resource = ResourceLoader.Load( path );
|
||||||
|
|
||||||
|
if ( resource == null )
|
||||||
|
{
|
||||||
|
return ToolCallOutcome.Error( "Failed to load resource: " + path );
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = new JSONObject();
|
||||||
|
data.Set( "path", path );
|
||||||
|
data.Set( "type", resource.GetClass() );
|
||||||
|
data.Set( "properties", PropertyReflection.ToJson( resource ) );
|
||||||
|
|
||||||
|
return ToolCallOutcome.Ok( JSON.Stringify( data ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bgmx7o7yhxauh
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
#if TOOLS
|
||||||
|
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Rokojori.Tools
|
||||||
|
{
|
||||||
|
using Rokojori;
|
||||||
|
|
||||||
|
public static class ReadSceneFragmentRoute
|
||||||
|
{
|
||||||
|
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 target node. Empty array = scene root." );
|
||||||
|
|
||||||
|
var levelSchema = new JSONObject();
|
||||||
|
levelSchema.Set( "type", "integer" );
|
||||||
|
levelSchema.Set( "description", "Descendant depth to include: 0 = all, 1 = direct children only, 2 = two levels, etc. Defaults to 0." );
|
||||||
|
|
||||||
|
var properties = new JSONObject();
|
||||||
|
properties.Set( "filePath", filePathSchema );
|
||||||
|
properties.Set( "scenePath", scenePathSchema );
|
||||||
|
properties.Set( "levelOfChildren", levelSchema );
|
||||||
|
|
||||||
|
var required = new JSONArray();
|
||||||
|
required.Push( "filePath" );
|
||||||
|
required.Push( "scenePath" );
|
||||||
|
|
||||||
|
var schema = new JSONObject();
|
||||||
|
schema.Set( "type", "object" );
|
||||||
|
schema.Set( "properties", properties );
|
||||||
|
schema.Set( "required", required );
|
||||||
|
|
||||||
|
var tool = new JSONObject();
|
||||||
|
tool.Set( "name", "read_scene_fragment" );
|
||||||
|
tool.Set( "description", "Read a node subtree out of a scene file without instantiating it." );
|
||||||
|
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 levelData = arguments != null ? arguments.Get( "levelOfChildren" ) : null;
|
||||||
|
var levelOfChildren = levelData != null && levelData.isNumber ? levelData.intValue : 0;
|
||||||
|
|
||||||
|
if ( string.IsNullOrEmpty( filePath ) )
|
||||||
|
{
|
||||||
|
return ToolCallOutcome.Error( "Missing required argument: filePath" );
|
||||||
|
}
|
||||||
|
|
||||||
|
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 );
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = packedScene.GetState();
|
||||||
|
var scenePath = ScenePathResolver.ToStringArray( scenePathArray );
|
||||||
|
var targetRelativePath = ScenePathResolver.ToRelativePath( scenePath );
|
||||||
|
var targetIndex = ScenePathResolver.FindNodeIndex( state, targetRelativePath );
|
||||||
|
|
||||||
|
if ( targetIndex == -1 )
|
||||||
|
{
|
||||||
|
return ToolCallOutcome.Error( "Node not found at scenePath: " + targetRelativePath );
|
||||||
|
}
|
||||||
|
|
||||||
|
var startingLevels = levelOfChildren == 0 ? -1 : levelOfChildren;
|
||||||
|
var fragment = SerializeNode( state, targetIndex, targetRelativePath, startingLevels );
|
||||||
|
|
||||||
|
return ToolCallOutcome.Ok( JSON.Stringify( fragment ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
static JSONObject SerializeNode( SceneState state, int index, string relativePath, int remainingLevels )
|
||||||
|
{
|
||||||
|
var obj = new JSONObject();
|
||||||
|
obj.Set( "name", state.GetNodeName( index ).ToString() );
|
||||||
|
obj.Set( "type", state.GetNodeType( index ).ToString() );
|
||||||
|
|
||||||
|
var properties = new JSONObject();
|
||||||
|
var propertyCount = state.GetNodePropertyCount( index );
|
||||||
|
|
||||||
|
for ( var i = 0; i < propertyCount; i++ )
|
||||||
|
{
|
||||||
|
var propName = state.GetNodePropertyName( index, i ).ToString();
|
||||||
|
var propValue = state.GetNodePropertyValue( index, i );
|
||||||
|
properties.Set( propName, VariantJson.ToJson( propValue ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.Set( "properties", properties );
|
||||||
|
|
||||||
|
if ( remainingLevels == 0 )
|
||||||
|
{
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
var children = new JSONObject();
|
||||||
|
var nodeCount = state.GetNodeCount();
|
||||||
|
var nextLevels = remainingLevels == -1 ? -1 : remainingLevels - 1;
|
||||||
|
|
||||||
|
for ( var i = 0; i < nodeCount; i++ )
|
||||||
|
{
|
||||||
|
var candidatePath = state.GetNodePath( i, false ).ToString();
|
||||||
|
|
||||||
|
if ( ScenePathResolver.DepthOf( relativePath, candidatePath ) != 1 )
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var childName = state.GetNodeName( i ).ToString();
|
||||||
|
children.Set( childName, SerializeNode( state, i, candidatePath, nextLevels ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
obj.Set( "children", children );
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bxa62u3hiuy3y
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
#if TOOLS
|
||||||
|
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Rokojori.Tools
|
||||||
|
{
|
||||||
|
using Rokojori;
|
||||||
|
|
||||||
|
public static class WriteResourceRoute
|
||||||
|
{
|
||||||
|
public static JSONObject ToolDefinition()
|
||||||
|
{
|
||||||
|
var pathSchema = new JSONObject();
|
||||||
|
pathSchema.Set( "type", "string" );
|
||||||
|
pathSchema.Set( "description", "res:// path to write the resource to" );
|
||||||
|
|
||||||
|
var typeSchema = new JSONObject();
|
||||||
|
typeSchema.Set( "type", "string" );
|
||||||
|
typeSchema.Set( "description", "Godot class name, required only when creating a new resource at 'path'" );
|
||||||
|
|
||||||
|
var propertiesSchema = new JSONObject();
|
||||||
|
propertiesSchema.Set( "type", "object" );
|
||||||
|
propertiesSchema.Set( "description", "Property name -> JSON value map, as returned by read_resource" );
|
||||||
|
|
||||||
|
var properties = new JSONObject();
|
||||||
|
properties.Set( "path", pathSchema );
|
||||||
|
properties.Set( "type", typeSchema );
|
||||||
|
properties.Set( "properties", propertiesSchema );
|
||||||
|
|
||||||
|
var required = new JSONArray();
|
||||||
|
required.Push( "path" );
|
||||||
|
required.Push( "properties" );
|
||||||
|
|
||||||
|
var schema = new JSONObject();
|
||||||
|
schema.Set( "type", "object" );
|
||||||
|
schema.Set( "properties", properties );
|
||||||
|
schema.Set( "required", required );
|
||||||
|
|
||||||
|
var tool = new JSONObject();
|
||||||
|
tool.Set( "name", "write_resource" );
|
||||||
|
tool.Set( "description", "Create or update a Resource's stored properties and save it." );
|
||||||
|
tool.Set( "inputSchema", schema );
|
||||||
|
|
||||||
|
return tool;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ToolCallOutcome Execute( JSONObject arguments )
|
||||||
|
{
|
||||||
|
var path = JsonUtil.GetStringOrNull( arguments, "path" );
|
||||||
|
var typeName = JsonUtil.GetStringOrNull( arguments, "type" );
|
||||||
|
var properties = arguments != null ? arguments.GetObject( "properties" ) : null;
|
||||||
|
|
||||||
|
if ( string.IsNullOrEmpty( path ) )
|
||||||
|
{
|
||||||
|
return ToolCallOutcome.Error( "Missing required argument: path" );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( properties == null )
|
||||||
|
{
|
||||||
|
return ToolCallOutcome.Error( "Missing required argument: properties" );
|
||||||
|
}
|
||||||
|
|
||||||
|
Resource resource = null;
|
||||||
|
|
||||||
|
if ( ResourceLoader.Exists( path, "" ) )
|
||||||
|
{
|
||||||
|
resource = ResourceLoader.Load( path );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( resource == null )
|
||||||
|
{
|
||||||
|
if ( string.IsNullOrEmpty( typeName ) )
|
||||||
|
{
|
||||||
|
return ToolCallOutcome.Error( "Resource does not exist yet at '" + path + "' - 'type' is required to create it" );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! ClassDB.ClassExists( typeName ) )
|
||||||
|
{
|
||||||
|
return ToolCallOutcome.Error( "Unknown class: " + typeName );
|
||||||
|
}
|
||||||
|
|
||||||
|
resource = ClassDB.Instantiate( typeName ).As<Resource>();
|
||||||
|
|
||||||
|
if ( resource == null )
|
||||||
|
{
|
||||||
|
return ToolCallOutcome.Error( "Class is not a Resource: " + typeName );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyReflection.ApplyFromJson( resource, properties );
|
||||||
|
|
||||||
|
var error = ResourceSaver.Save( resource, path );
|
||||||
|
|
||||||
|
if ( error != Error.Ok )
|
||||||
|
{
|
||||||
|
return ToolCallOutcome.Error( "Failed to save resource: " + error );
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new JSONObject();
|
||||||
|
result.Set( "path", path );
|
||||||
|
result.Set( "type", resource.GetClass() );
|
||||||
|
|
||||||
|
return ToolCallOutcome.Ok( JSON.Stringify( result ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c5f0mp0tkb8re
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
#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
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://blb8c2s0ukunt
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
#if TOOLS
|
||||||
|
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Rokojori.Tools
|
||||||
|
{
|
||||||
|
// scenePath is a list of child names from the scene root, e.g. ["Player","CharacterBody3D"].
|
||||||
|
// SceneState has no parent/child API - nodes are a flat list, each with its own full relative
|
||||||
|
// path string (root is "."), so matching is by path-string comparison / prefix.
|
||||||
|
public static class ScenePathResolver
|
||||||
|
{
|
||||||
|
// SceneState.GetNodePath(idx, false) stringifies relative to the scene root as "./Name/Child",
|
||||||
|
// i.e. with an explicit "." leading segment - not a bare "Name/Child" as a naive join would produce.
|
||||||
|
public static string ToRelativePath( string[] scenePath )
|
||||||
|
{
|
||||||
|
if ( scenePath == null || scenePath.Length == 0 )
|
||||||
|
{
|
||||||
|
return ".";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "./" + string.Join( "/", scenePath );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int FindNodeIndex( SceneState state, string relativePath )
|
||||||
|
{
|
||||||
|
var count = state.GetNodeCount();
|
||||||
|
|
||||||
|
for ( var i = 0; i < count; i++ )
|
||||||
|
{
|
||||||
|
if ( state.GetNodePath( i, false ).ToString() == relativePath )
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 0 = same node, 1 = direct child, 2 = grandchild, etc. -1 = not a descendant.
|
||||||
|
public static int DepthOf( string rootRelativePath, string candidateRelativePath )
|
||||||
|
{
|
||||||
|
if ( rootRelativePath == candidateRelativePath )
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var prefix = rootRelativePath == "." ? "./" : rootRelativePath + "/";
|
||||||
|
|
||||||
|
if ( ! candidateRelativePath.StartsWith( prefix ) )
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var remainder = candidateRelativePath.Substring( prefix.Length );
|
||||||
|
return remainder.Split( '/' ).Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Node FindNode( Node root, string[] scenePath )
|
||||||
|
{
|
||||||
|
if ( scenePath == null || scenePath.Length == 0 )
|
||||||
|
{
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
return root.GetNodeOrNull( new NodePath( string.Join( "/", scenePath ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string[] ToStringArray( Rokojori.JSONArray array )
|
||||||
|
{
|
||||||
|
if ( array == null )
|
||||||
|
{
|
||||||
|
return new string[ 0 ];
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new string[ array.size ];
|
||||||
|
|
||||||
|
for ( var i = 0; i < array.size; i++ )
|
||||||
|
{
|
||||||
|
result[ i ] = array.Get( i ).stringValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://d30pddlfk6qc2
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
#if TOOLS
|
||||||
|
|
||||||
|
namespace Rokojori.Tools
|
||||||
|
{
|
||||||
|
public class ToolCallOutcome
|
||||||
|
{
|
||||||
|
public string text;
|
||||||
|
public bool isError;
|
||||||
|
|
||||||
|
public static ToolCallOutcome Ok( string text )
|
||||||
|
{
|
||||||
|
return new ToolCallOutcome{ text = text, isError = false };
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ToolCallOutcome Error( string text )
|
||||||
|
{
|
||||||
|
return new ToolCallOutcome{ text = text, isError = true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bx25ytgd1y8ho
|
||||||
|
|
@ -0,0 +1,365 @@
|
||||||
|
#if TOOLS
|
||||||
|
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Rokojori.Tools
|
||||||
|
{
|
||||||
|
using Rokojori;
|
||||||
|
|
||||||
|
// Two-way mapping between Godot Variant and the addon's own JSON types.
|
||||||
|
// Sub-resources are referenced by resource_path; an embedded sub-resource with
|
||||||
|
// no saved path round-trips as a tagged placeholder rather than its real data -
|
||||||
|
// not needed by the first 4 routes, revisit if a fragment ever needs it.
|
||||||
|
public static class VariantJson
|
||||||
|
{
|
||||||
|
public static JSONData ToJson( Variant v )
|
||||||
|
{
|
||||||
|
switch ( v.VariantType )
|
||||||
|
{
|
||||||
|
case Variant.Type.Nil:
|
||||||
|
return new JSONValue();
|
||||||
|
|
||||||
|
case Variant.Type.Bool:
|
||||||
|
return new JSONValue( v.AsBool() );
|
||||||
|
|
||||||
|
case Variant.Type.Int:
|
||||||
|
return new JSONValue( (double) v.AsInt64() );
|
||||||
|
|
||||||
|
case Variant.Type.Float:
|
||||||
|
return new JSONValue( v.AsDouble() );
|
||||||
|
|
||||||
|
case Variant.Type.String:
|
||||||
|
case Variant.Type.StringName:
|
||||||
|
return new JSONValue( v.AsString() );
|
||||||
|
|
||||||
|
case Variant.Type.NodePath:
|
||||||
|
return new JSONValue( v.AsNodePath().ToString() );
|
||||||
|
|
||||||
|
case Variant.Type.Vector2:
|
||||||
|
return Vector2ToJson( v.AsVector2() );
|
||||||
|
|
||||||
|
case Variant.Type.Vector2I:
|
||||||
|
var v2i = v.AsVector2I();
|
||||||
|
return Vector2ToJson( new Vector2( v2i.X, v2i.Y ) );
|
||||||
|
|
||||||
|
case Variant.Type.Vector3:
|
||||||
|
return Vector3ToJson( v.AsVector3() );
|
||||||
|
|
||||||
|
case Variant.Type.Vector3I:
|
||||||
|
var v3i = v.AsVector3I();
|
||||||
|
return Vector3ToJson( new Vector3( v3i.X, v3i.Y, v3i.Z ) );
|
||||||
|
|
||||||
|
case Variant.Type.Vector4:
|
||||||
|
return Vector4ToJson( v.AsVector4() );
|
||||||
|
|
||||||
|
case Variant.Type.Color:
|
||||||
|
return ColorToJson( v.AsColor() );
|
||||||
|
|
||||||
|
case Variant.Type.Array:
|
||||||
|
return ArrayToJson( v.AsGodotArray() );
|
||||||
|
|
||||||
|
case Variant.Type.Dictionary:
|
||||||
|
return DictionaryToJson( v.AsGodotDictionary() );
|
||||||
|
|
||||||
|
case Variant.Type.Object:
|
||||||
|
return ObjectToJson( v.AsGodotObject() );
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Not modeled explicitly (Rid, Callable, Signal, Transform*, Basis, Plane, Quaternion, Projection, Aabb, Rect2...).
|
||||||
|
// Falls back to a readable string so round-tripping these doesn't crash - just loses write support.
|
||||||
|
return new JSONValue( v.ToString() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Variant FromJson( JSONData data, Variant.Type targetType )
|
||||||
|
{
|
||||||
|
if ( data == null || data.isNull )
|
||||||
|
{
|
||||||
|
return new Variant();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ( targetType )
|
||||||
|
{
|
||||||
|
case Variant.Type.Nil:
|
||||||
|
// Untyped [Export] Variant field - no declared type to convert against, shape-sniff instead.
|
||||||
|
return FromJsonInferred( data );
|
||||||
|
|
||||||
|
case Variant.Type.Bool:
|
||||||
|
return data.booleanValue;
|
||||||
|
|
||||||
|
case Variant.Type.Int:
|
||||||
|
return data.longValue;
|
||||||
|
|
||||||
|
case Variant.Type.Float:
|
||||||
|
return data.numberValue;
|
||||||
|
|
||||||
|
case Variant.Type.String:
|
||||||
|
return data.stringValue;
|
||||||
|
|
||||||
|
case Variant.Type.StringName:
|
||||||
|
return new StringName( data.stringValue );
|
||||||
|
|
||||||
|
case Variant.Type.NodePath:
|
||||||
|
return new NodePath( data.stringValue );
|
||||||
|
|
||||||
|
case Variant.Type.Vector2:
|
||||||
|
return JsonToVector2( data.AsObject() );
|
||||||
|
|
||||||
|
case Variant.Type.Vector2I:
|
||||||
|
var v2 = JsonToVector2( data.AsObject() );
|
||||||
|
return new Vector2I( (int) v2.X, (int) v2.Y );
|
||||||
|
|
||||||
|
case Variant.Type.Vector3:
|
||||||
|
return JsonToVector3( data.AsObject() );
|
||||||
|
|
||||||
|
case Variant.Type.Vector3I:
|
||||||
|
var v3 = JsonToVector3( data.AsObject() );
|
||||||
|
return new Vector3I( (int) v3.X, (int) v3.Y, (int) v3.Z );
|
||||||
|
|
||||||
|
case Variant.Type.Vector4:
|
||||||
|
return JsonToVector4( data.AsObject() );
|
||||||
|
|
||||||
|
case Variant.Type.Color:
|
||||||
|
return JsonToColor( data.AsObject() );
|
||||||
|
|
||||||
|
case Variant.Type.Array:
|
||||||
|
return JsonToArray( data.AsArray() );
|
||||||
|
|
||||||
|
case Variant.Type.Dictionary:
|
||||||
|
return JsonToDictionary( data.AsObject() );
|
||||||
|
|
||||||
|
case Variant.Type.Object:
|
||||||
|
return JsonToResource( data.AsObject() );
|
||||||
|
|
||||||
|
default:
|
||||||
|
RJLog.Error( "MCP VariantJson: write not supported for Variant type", targetType );
|
||||||
|
return new Variant();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for Array/Dictionary element values, which have no declared Variant type of their own -
|
||||||
|
// shape-sniffs the JSON (keys x/y/z/w, r/g/b/a, __resourcePath) rather than a real type lookup.
|
||||||
|
public static Variant FromJsonInferred( JSONData data )
|
||||||
|
{
|
||||||
|
if ( data == null || data.isNull )
|
||||||
|
{
|
||||||
|
return new Variant();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( data.isBoolean )
|
||||||
|
{
|
||||||
|
return data.booleanValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( data.isNumber )
|
||||||
|
{
|
||||||
|
return data.numberValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( data.isString )
|
||||||
|
{
|
||||||
|
return data.stringValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( data.isArray )
|
||||||
|
{
|
||||||
|
return JsonToArray( data.AsArray() );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( data.isObject )
|
||||||
|
{
|
||||||
|
var obj = data.AsObject();
|
||||||
|
|
||||||
|
if ( obj.Contains( "__resourcePath" ) )
|
||||||
|
{
|
||||||
|
return JsonToResource( obj );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( obj.Contains( "x" ) && obj.Contains( "y" ) && obj.Contains( "z" ) && obj.Contains( "w" ) )
|
||||||
|
{
|
||||||
|
return JsonToVector4( obj );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( obj.Contains( "r" ) && obj.Contains( "g" ) && obj.Contains( "b" ) )
|
||||||
|
{
|
||||||
|
return JsonToColor( obj );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( obj.Contains( "x" ) && obj.Contains( "y" ) && obj.Contains( "z" ) )
|
||||||
|
{
|
||||||
|
return JsonToVector3( obj );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( obj.Contains( "x" ) && obj.Contains( "y" ) )
|
||||||
|
{
|
||||||
|
return JsonToVector2( obj );
|
||||||
|
}
|
||||||
|
|
||||||
|
return JsonToDictionary( obj );
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Variant();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JSONObject Vector2ToJson( Vector2 v )
|
||||||
|
{
|
||||||
|
var o = new JSONObject();
|
||||||
|
o.Set( "x", (double) v.X );
|
||||||
|
o.Set( "y", (double) v.Y );
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JSONObject Vector3ToJson( Vector3 v )
|
||||||
|
{
|
||||||
|
var o = new JSONObject();
|
||||||
|
o.Set( "x", (double) v.X );
|
||||||
|
o.Set( "y", (double) v.Y );
|
||||||
|
o.Set( "z", (double) v.Z );
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JSONObject Vector4ToJson( Vector4 v )
|
||||||
|
{
|
||||||
|
var o = new JSONObject();
|
||||||
|
o.Set( "x", (double) v.X );
|
||||||
|
o.Set( "y", (double) v.Y );
|
||||||
|
o.Set( "z", (double) v.Z );
|
||||||
|
o.Set( "w", (double) v.W );
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JSONObject ColorToJson( Color c )
|
||||||
|
{
|
||||||
|
var o = new JSONObject();
|
||||||
|
o.Set( "r", (double) c.R );
|
||||||
|
o.Set( "g", (double) c.G );
|
||||||
|
o.Set( "b", (double) c.B );
|
||||||
|
o.Set( "a", (double) c.A );
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Vector2 JsonToVector2( JSONObject o )
|
||||||
|
{
|
||||||
|
if ( o == null ) return Vector2.Zero;
|
||||||
|
return new Vector2( (float) o.GetNumberOr( "x", 0 ), (float) o.GetNumberOr( "y", 0 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Vector3 JsonToVector3( JSONObject o )
|
||||||
|
{
|
||||||
|
if ( o == null ) return Vector3.Zero;
|
||||||
|
return new Vector3( (float) o.GetNumberOr( "x", 0 ), (float) o.GetNumberOr( "y", 0 ), (float) o.GetNumberOr( "z", 0 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Vector4 JsonToVector4( JSONObject o )
|
||||||
|
{
|
||||||
|
if ( o == null ) return Vector4.Zero;
|
||||||
|
return new Vector4( (float) o.GetNumberOr( "x", 0 ), (float) o.GetNumberOr( "y", 0 ), (float) o.GetNumberOr( "z", 0 ), (float) o.GetNumberOr( "w", 0 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Color JsonToColor( JSONObject o )
|
||||||
|
{
|
||||||
|
if ( o == null ) return Colors.White;
|
||||||
|
return new Color(
|
||||||
|
(float) o.GetNumberOr( "r", 1 ), (float) o.GetNumberOr( "g", 1 ),
|
||||||
|
(float) o.GetNumberOr( "b", 1 ), (float) o.GetNumberOr( "a", 1 )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JSONArray ArrayToJson( Godot.Collections.Array array )
|
||||||
|
{
|
||||||
|
var result = new JSONArray();
|
||||||
|
|
||||||
|
foreach ( var item in array )
|
||||||
|
{
|
||||||
|
result.Push( ToJson( item ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Godot.Collections.Array JsonToArray( JSONArray array )
|
||||||
|
{
|
||||||
|
var result = new Godot.Collections.Array();
|
||||||
|
|
||||||
|
if ( array == null )
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( var item in array.values )
|
||||||
|
{
|
||||||
|
result.Add( FromJsonInferred( item ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JSONObject DictionaryToJson( Godot.Collections.Dictionary dict )
|
||||||
|
{
|
||||||
|
var result = new JSONObject();
|
||||||
|
|
||||||
|
foreach ( var key in dict.Keys )
|
||||||
|
{
|
||||||
|
result.Set( key.AsString(), ToJson( dict[ key ] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Godot.Collections.Dictionary JsonToDictionary( JSONObject o )
|
||||||
|
{
|
||||||
|
var result = new Godot.Collections.Dictionary();
|
||||||
|
|
||||||
|
if ( o == null )
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
for ( var i = 0; i < o.keys.Count; i++ )
|
||||||
|
{
|
||||||
|
result[ o.keys[ i ] ] = FromJsonInferred( o.values[ i ] );
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JSONData ObjectToJson( GodotObject obj )
|
||||||
|
{
|
||||||
|
if ( obj == null )
|
||||||
|
{
|
||||||
|
return new JSONValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
var wrapper = new JSONObject();
|
||||||
|
|
||||||
|
if ( obj is Resource resource )
|
||||||
|
{
|
||||||
|
if ( ! string.IsNullOrEmpty( resource.ResourcePath ) )
|
||||||
|
{
|
||||||
|
wrapper.Set( "__resourcePath", resource.ResourcePath );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
wrapper.Set( "__embeddedResourceType", resource.GetClass() );
|
||||||
|
}
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new JSONValue( obj.ToString() );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Resource JsonToResource( JSONObject o )
|
||||||
|
{
|
||||||
|
if ( o == null || ! o.Contains( "__resourcePath" ) )
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = o.GetString( "__resourcePath" );
|
||||||
|
return ResourceLoader.Load( path );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
uid://dvmeiwgj8apa4
|
||||||
Loading…
Reference in New Issue