Hexagon Coord
This commit is contained in:
parent
385a3069e4
commit
1d37c3a6cf
|
|
@ -241,7 +241,7 @@ public partial class RokojoriPlugin: EditorPlugin
|
|||
|
||||
public override void _Process( double delta )
|
||||
{
|
||||
mcpServer.ProcessPendingToolCalls();
|
||||
mcpServer.ProcessPendingRequests();
|
||||
|
||||
var editingScene = EditorInterface.Singleton.GetEditedSceneRoot();
|
||||
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ namespace Rokojori
|
|||
|
||||
protected override void _OnTrigger()
|
||||
{
|
||||
this.LogInfo( "Started Float Tween" );
|
||||
// this.LogInfo( "Started Float Tween" );
|
||||
|
||||
|
||||
// if ( Engine.IsEditorHint() )
|
||||
|
|
@ -119,7 +119,7 @@ namespace Rokojori
|
|||
|
||||
AnimationManager.StartAnimation( this, target, targetMemberPath );
|
||||
|
||||
this.LogInfo( "Start Value Float Tween", HierarchyName.OfAny( target ), target.GetType().Name, targetMemberPath, ">>", startValue );
|
||||
// this.LogInfo( "Start Value Float Tween", HierarchyName.OfAny( target ), target.GetType().Name, targetMemberPath, ">>", startValue );
|
||||
|
||||
_timeID = TimelineManager.ScheduleSpanWith( duration,
|
||||
( span, type )=>
|
||||
|
|
|
|||
|
|
@ -855,21 +855,26 @@ namespace Rokojori
|
|||
RemoveAndDeleteChildren( parent, includeInternal );
|
||||
}
|
||||
|
||||
public static void DestroyChildAt( this Node parent, int index, bool includeInternal = false, bool queue = true )
|
||||
public static void DestroyChild( this Node parent, Node child, bool includeInternal = false, bool queue = true )
|
||||
{
|
||||
var node = parent.GetChild( index, includeInternal );
|
||||
parent.RemoveChild( node );
|
||||
parent.RemoveChild( child );
|
||||
|
||||
if ( ! queue )
|
||||
{
|
||||
node.Free();
|
||||
child.Free();
|
||||
}
|
||||
else
|
||||
{
|
||||
node.QueueFree();
|
||||
child.QueueFree();
|
||||
}
|
||||
}
|
||||
|
||||
public static void DestroyChildAt( this Node parent, int index, bool includeInternal = false, bool queue = true )
|
||||
{
|
||||
var node = parent.GetChild( index, includeInternal );
|
||||
DestroyChild( parent, node, includeInternal, queue );
|
||||
}
|
||||
|
||||
public static void DestroyChildrenRange( this Node parent, int startIndex, int range, bool includeInternal = false, bool queue = true )
|
||||
{
|
||||
parent.LogInfo( startIndex, range, parent.GetChildCount() );
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ namespace Rokojori
|
|||
|
||||
public void _OnSensor( SensorEvent se )
|
||||
{
|
||||
this.LogInfo( "SensorEvent:", se );
|
||||
// this.LogInfo( "SensorEvent:", se );
|
||||
|
||||
if ( pointer == null || pointer.pointable == null )
|
||||
{
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ namespace Rokojori
|
|||
[Export]
|
||||
public int maxHits = 3;
|
||||
|
||||
List<CollisionData> collisions = new List<CollisionData>();
|
||||
int numCollisions = 0;
|
||||
// List<CollisionData> collisions = new List<CollisionData>();
|
||||
// int numCollisions = 0;
|
||||
|
||||
[Export]
|
||||
public Vector3 to;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,169 @@
|
|||
using Godot;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Rokojori;
|
||||
|
||||
|
||||
public struct HexagonCoord
|
||||
{
|
||||
public readonly int q;
|
||||
public readonly int r;
|
||||
|
||||
public HexagonCoord( int q, int r )
|
||||
{
|
||||
this.q = q;
|
||||
this.r = r;
|
||||
}
|
||||
|
||||
public readonly static HexagonCoord Center = new HexagonCoord( 0, 0 );
|
||||
public HexagonCoord Middle => Center;
|
||||
|
||||
public readonly static HexagonCoord East = new HexagonCoord( 1, 0 );
|
||||
public HexagonCoord Left => East;
|
||||
|
||||
public readonly static HexagonCoord NorthEast = new HexagonCoord( 1, -1 );
|
||||
public HexagonCoord LeftUp => NorthEast;
|
||||
|
||||
public readonly static HexagonCoord NorthWest = new HexagonCoord( 0, -1 );
|
||||
public HexagonCoord RightUp => NorthWest;
|
||||
|
||||
public readonly static HexagonCoord West = new HexagonCoord( -1, 0 );
|
||||
public HexagonCoord Right => West;
|
||||
|
||||
public readonly static HexagonCoord SouthWest = new HexagonCoord( -1, 1 );
|
||||
public HexagonCoord RightDown => SouthWest;
|
||||
|
||||
public readonly static HexagonCoord SouthEast = new HexagonCoord( 0, 1 );
|
||||
public HexagonCoord LeftDown => SouthEast;
|
||||
|
||||
|
||||
static readonly HexagonCoord[] directions = new HexagonCoord[]
|
||||
{
|
||||
new HexagonCoord( 1, 0 ),
|
||||
|
||||
new HexagonCoord( 1, -1 ),
|
||||
new HexagonCoord( 0, -1 ),
|
||||
|
||||
new HexagonCoord( -1, 0 ),
|
||||
new HexagonCoord( -1, 1 ),
|
||||
new HexagonCoord( 0, 1 )
|
||||
};
|
||||
|
||||
public static HexagonCoord Direction( int direction )
|
||||
{
|
||||
return directions[ direction ];
|
||||
}
|
||||
|
||||
public HexagonCoord Neighbor( int direction )
|
||||
{
|
||||
var d = directions[ direction ];
|
||||
return this + directions[ direction ];
|
||||
}
|
||||
|
||||
public Vector3 ToWorldPosition( float size )
|
||||
{
|
||||
var x = size * 1.5f * q;
|
||||
var z = size * ( Mathf.Sqrt( 3f ) * 0.5f * q + Mathf.Sqrt( 3f ) * r );
|
||||
|
||||
return new Vector3( x, 0, z );
|
||||
}
|
||||
|
||||
public static HexagonCoord operator +(HexagonCoord a, HexagonCoord b)
|
||||
{
|
||||
return new HexagonCoord(
|
||||
a.q + b.q,
|
||||
a.r + b.r
|
||||
);
|
||||
}
|
||||
|
||||
public static HexagonCoord operator -(HexagonCoord a, HexagonCoord b)
|
||||
{
|
||||
return new HexagonCoord(
|
||||
a.q - b.q,
|
||||
a.r - b.r
|
||||
);
|
||||
}
|
||||
|
||||
public static HexagonCoord operator -(HexagonCoord a)
|
||||
{
|
||||
return new HexagonCoord(
|
||||
-a.q,
|
||||
-a.r
|
||||
);
|
||||
}
|
||||
|
||||
public static HexagonCoord operator *(HexagonCoord a, int scalar)
|
||||
{
|
||||
return new HexagonCoord(
|
||||
a.q * scalar,
|
||||
a.r * scalar
|
||||
);
|
||||
}
|
||||
|
||||
public static HexagonCoord operator *(int scalar, HexagonCoord a)
|
||||
{
|
||||
return a * scalar;
|
||||
}
|
||||
|
||||
public static HexagonCoord operator /(HexagonCoord a, int scalar)
|
||||
{
|
||||
return new HexagonCoord(
|
||||
a.q / scalar,
|
||||
a.r / scalar
|
||||
);
|
||||
}
|
||||
|
||||
public static bool operator ==(HexagonCoord a, HexagonCoord b)
|
||||
{
|
||||
return a.q == b.q && a.r == b.r;
|
||||
}
|
||||
|
||||
public static bool operator !=(HexagonCoord a, HexagonCoord b)
|
||||
{
|
||||
return !(a == b);
|
||||
}
|
||||
|
||||
public override bool Equals( object obj )
|
||||
{
|
||||
return obj is HexagonCoord other && other.q == q && other.r == r;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return q * 397 ^ r;
|
||||
}
|
||||
|
||||
public static List<HexagonCoord> Ring( int radius )
|
||||
{
|
||||
return Ring( HexagonCoord.Center, radius );
|
||||
}
|
||||
|
||||
public static List<HexagonCoord> Ring( HexagonCoord center, int radius )
|
||||
{
|
||||
var results = new List<HexagonCoord>();
|
||||
|
||||
if ( radius == 0 )
|
||||
{
|
||||
results.Add( center );
|
||||
return results;
|
||||
}
|
||||
|
||||
var hex = center;
|
||||
|
||||
for ( int i = 0; i < radius; i++ )
|
||||
{
|
||||
hex = hex.Neighbor( 4 );
|
||||
}
|
||||
|
||||
for ( int side = 0; side < 6; side++ )
|
||||
{
|
||||
for ( int step = 0; step < radius; step++ )
|
||||
{
|
||||
results.Add( hex );
|
||||
hex = hex.Neighbor( side );
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://d2y27n85ddcum
|
||||
|
|
@ -214,6 +214,11 @@ namespace Rokojori
|
|||
return IntegerExclusiveInRange( 0, max );
|
||||
}
|
||||
|
||||
public HexagonCoord HexagonDirection()
|
||||
{
|
||||
return HexagonCoord.Direction( IntegerInclusiveInRange( 0, 5 ) );
|
||||
}
|
||||
|
||||
|
||||
public char FromText( string source )
|
||||
{
|
||||
|
|
@ -243,6 +248,7 @@ namespace Rokojori
|
|||
|
||||
return list[ index ];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
public T GrabFromRange<T>( List<T> list, int start, int end )
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using System.Collections.Generic;
|
|||
using System;
|
||||
using Godot;
|
||||
using Rokojori.Extensions;
|
||||
using System.Linq;
|
||||
|
||||
namespace Rokojori;
|
||||
|
||||
|
|
@ -92,6 +93,11 @@ public partial class TimelineManager:NetworkNode
|
|||
{
|
||||
Engine.TimeScale = GetRunnerByTimeline( timeScaleTimeline ).modulatedSpeed;
|
||||
}
|
||||
|
||||
if ( Engine.IsEditorHint() && _runners == null )
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
_runners.ForEach(
|
||||
r =>
|
||||
|
|
@ -195,7 +201,23 @@ public partial class TimelineManager:NetworkNode
|
|||
|
||||
public TimelineRunner GetRunnerByTimeline( Timeline timeline )
|
||||
{
|
||||
return GetRunner( _runners.FindIndex( r => r.timeLine == timeline ) );
|
||||
if ( _runners == null )
|
||||
{
|
||||
RJLog.Error( "Runners is null" );
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var runner = GetRunner( _runners.FindIndex( r => r?.timeLine == timeline ) );
|
||||
|
||||
if ( runner == null )
|
||||
{
|
||||
var inManager = timelines.Contains( timeline );
|
||||
|
||||
RJLog.Log( "No runner found for:", timeline, "Is in manager:", inManager );
|
||||
}
|
||||
|
||||
return runner;
|
||||
}
|
||||
|
||||
public TimelineRunner GetRunner( int index )
|
||||
|
|
|
|||
|
|
@ -5,20 +5,31 @@ 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.
|
||||
// ProcessPendingRequests() 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.
|
||||
// Threading: HttpListener accepts via BeginGetContext/EndGetContext (thread-pool callbacks),
|
||||
// not a dedicated foreground/background Thread blocked in GetContext() - no thread is ever
|
||||
// blocked waiting on anything in this class, which matters because a blocked thread with a
|
||||
// stack frame into this assembly keeps Godot's C# hot reload from unloading it until that
|
||||
// thread returns, and relying on Stop()/Close() to reliably abort such a thread has not held
|
||||
// up in practice.
|
||||
//
|
||||
// More fundamentally: the HTTP callback thread does raw byte I/O only - read the request body
|
||||
// in, hand it off untouched, write the response bytes out when told to. It never parses JSON,
|
||||
// dispatches protocol methods, or executes a tool. That's not just about tools/call touching
|
||||
// Godot APIs directly (ResourceLoader, PackedScene, the scene tree) - the shared JSON library
|
||||
// this server depends on (JSONParser) calls RJLog on malformed input, and RJLog ultimately
|
||||
// calls GD.Print/GD.PrintErr, which are themselves Godot engine API calls. Every one of those
|
||||
// is only safe from the main thread; calling into the engine from a background thread can
|
||||
// corrupt native-side state in ways that surface as hot-reload failures. So ALL of it - JSON.
|
||||
// Parse included - is deferred to ProcessPendingRequests() on the main thread via
|
||||
// pendingRequests, and the response is written from there too.
|
||||
public class McpServer
|
||||
{
|
||||
public const string FlagFilePath = "res://.godot-mcp";
|
||||
|
|
@ -26,13 +37,12 @@ namespace Rokojori.Tools
|
|||
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>();
|
||||
readonly ConcurrentQueue<PendingRequest> pendingRequests = new ConcurrentQueue<PendingRequest>();
|
||||
|
||||
public bool IsRunning { get { return running; } }
|
||||
public int BoundPort { get { return boundPort; } }
|
||||
|
|
@ -70,9 +80,7 @@ namespace Rokojori.Tools
|
|||
}
|
||||
|
||||
running = true;
|
||||
listenerThread = new Thread( ListenLoop );
|
||||
listenerThread.IsBackground = true;
|
||||
listenerThread.Start();
|
||||
BeginAccept();
|
||||
|
||||
RJLog.Log( "MCP", "Server started on port", boundPort, "for project", projectPath );
|
||||
}
|
||||
|
|
@ -86,27 +94,19 @@ namespace Rokojori.Tools
|
|||
|
||||
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 ) )
|
||||
// Any request still waiting for a future _Process() tick to dequeue it would otherwise sit
|
||||
// unanswered forever if that tick never comes (e.g. the editor pausing the main loop to
|
||||
// hot-reload this very assembly). 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 answer any
|
||||
// still-queued request right now instead of leaving its HTTP response hanging.
|
||||
while ( pendingRequests.TryDequeue( out var pending ) )
|
||||
{
|
||||
pending.outcome = ToolCallOutcome.Error( "MCP server is stopping" );
|
||||
pending.waitHandle.Set();
|
||||
AbortPendingRequest( pending );
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
// Closing the listener faults any pending BeginGetContext registration, which invokes
|
||||
// OnGetContext on a thread-pool callback thread with an exception from EndGetContext() -
|
||||
// handled there by simply not chaining another BeginAccept(). No dedicated thread to join.
|
||||
try
|
||||
{
|
||||
listener?.Stop();
|
||||
|
|
@ -116,57 +116,101 @@ namespace Rokojori.Tools
|
|||
{
|
||||
}
|
||||
|
||||
listenerThread?.Join( 2000 );
|
||||
listener = null;
|
||||
boundPort = -1;
|
||||
|
||||
RJLog.Log( "MCP", "Server stopped." );
|
||||
}
|
||||
|
||||
void WakeListenerThread()
|
||||
// Stop() runs on the main thread, so parsing here (to recover the request's id for a clean
|
||||
// JSON-RPC error) is safe even though JSON.Parse can call RJLog on malformed input.
|
||||
void AbortPendingRequest( PendingRequest pending )
|
||||
{
|
||||
if ( boundPort <= 0 )
|
||||
JSONData id = new JSONValue();
|
||||
|
||||
try
|
||||
{
|
||||
var parsed = JSON.Parse( pending.body );
|
||||
var envelope = parsed != null ? parsed.AsObject() : null;
|
||||
var parsedId = envelope != null ? envelope.Get( "id" ) : null;
|
||||
|
||||
if ( parsedId != null )
|
||||
{
|
||||
id = parsedId;
|
||||
}
|
||||
}
|
||||
catch ( Exception )
|
||||
{
|
||||
}
|
||||
|
||||
TryWriteJson( pending.response, protocol.ToolCallResult( id, ToolCallOutcome.Error( "MCP server is stopping" ) ) );
|
||||
}
|
||||
|
||||
void BeginAccept()
|
||||
{
|
||||
if ( ! running || listener == null )
|
||||
{
|
||||
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 );
|
||||
}
|
||||
listener.BeginGetContext( OnGetContext, null );
|
||||
}
|
||||
catch ( Exception )
|
||||
{
|
||||
// Listener already stopped/closed - nothing to accept.
|
||||
}
|
||||
}
|
||||
|
||||
// Drains tool-call requests queued by the HTTP thread. Must run on the main thread.
|
||||
public void ProcessPendingToolCalls()
|
||||
void OnGetContext( IAsyncResult ar )
|
||||
{
|
||||
while ( pendingCalls.TryDequeue( out var pending ) )
|
||||
HttpListenerContext context;
|
||||
|
||||
try
|
||||
{
|
||||
ToolCallOutcome outcome;
|
||||
context = listener.EndGetContext( ar );
|
||||
}
|
||||
catch ( Exception )
|
||||
{
|
||||
// Stop() closed the listener while this accept was pending - end the chain here.
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
HandleContext( context );
|
||||
}
|
||||
catch ( Exception e )
|
||||
{
|
||||
// This runs on a thread-pool callback thread, not the main thread. Console.Error is
|
||||
// plain .NET, not a Godot API - unlike RJLog, it carries no risk of touching the engine
|
||||
// from the wrong thread (see the class comment above).
|
||||
Console.Error.WriteLine( "[MCP] Unhandled error handling request: " + e );
|
||||
|
||||
try
|
||||
{
|
||||
outcome = protocol.ExecuteTool( pending.toolName, pending.arguments );
|
||||
context.Response.StatusCode = 500;
|
||||
context.Response.OutputStream.Close();
|
||||
}
|
||||
catch ( Exception e )
|
||||
catch ( Exception )
|
||||
{
|
||||
RJLog.Error( "MCP", "Tool execution failed", pending.toolName, e );
|
||||
outcome = ToolCallOutcome.Error( e.Message );
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
BeginAccept();
|
||||
}
|
||||
}
|
||||
|
||||
pending.outcome = outcome;
|
||||
pending.waitHandle.Set();
|
||||
// Drains requests queued by the HTTP callback thread and handles them fully - JSON parsing,
|
||||
// protocol dispatch, tool execution, and writing the HTTP response - on the main thread.
|
||||
// Nothing on the HTTP side ever blocks waiting for this to happen.
|
||||
public void ProcessPendingRequests()
|
||||
{
|
||||
while ( pendingRequests.TryDequeue( out var pending ) )
|
||||
{
|
||||
HandleJsonRpc( pending.body, pending.response );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -194,41 +238,9 @@ namespace Rokojori.Tools
|
|||
}
|
||||
}
|
||||
|
||||
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 )
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Runs on the HTTP callback thread. The /project-path and 404 branches only touch plain .NET
|
||||
// request/response objects and cached strings/ints, so they're answered directly here. A
|
||||
// /mcp POST body is handed off untouched - see the class comment for why.
|
||||
void HandleContext( HttpListenerContext context )
|
||||
{
|
||||
var request = context.Request;
|
||||
|
|
@ -257,9 +269,10 @@ namespace Rokojori.Tools
|
|||
body = reader.ReadToEnd();
|
||||
}
|
||||
|
||||
HandleJsonRpc( body, response );
|
||||
pendingRequests.Enqueue( new PendingRequest( body, response ) );
|
||||
}
|
||||
|
||||
// Runs on the main thread only, dequeued from pendingRequests by ProcessPendingRequests().
|
||||
void HandleJsonRpc( string body, HttpListenerResponse response )
|
||||
{
|
||||
JSONObject envelope;
|
||||
|
|
@ -276,7 +289,7 @@ namespace Rokojori.Tools
|
|||
}
|
||||
catch ( Exception )
|
||||
{
|
||||
WriteJson( response, 200, JsonRpc.BuildError( new JSONValue(), -32700, "Parse error" ) );
|
||||
TryWriteJson( response, JsonRpc.BuildError( new JSONValue(), -32700, "Parse error" ) );
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -287,32 +300,31 @@ namespace Rokojori.Tools
|
|||
|
||||
if ( isNotification )
|
||||
{
|
||||
WriteEmpty( response, 202 );
|
||||
TryWriteEmpty( response, 202 );
|
||||
return;
|
||||
}
|
||||
|
||||
switch ( method )
|
||||
{
|
||||
case "initialize":
|
||||
WriteJson( response, 200, protocol.Initialize( id, paramsObj ) );
|
||||
TryWriteJson( response, protocol.Initialize( id, paramsObj ) );
|
||||
break;
|
||||
|
||||
case "tools/list":
|
||||
WriteJson( response, 200, protocol.ToolsList( id ) );
|
||||
TryWriteJson( response, protocol.ToolsList( id ) );
|
||||
break;
|
||||
|
||||
case "tools/call":
|
||||
var outcome = DispatchToolCallBlocking( paramsObj );
|
||||
WriteJson( response, 200, protocol.ToolCallResult( id, outcome ) );
|
||||
HandleToolCall( id, paramsObj, response );
|
||||
break;
|
||||
|
||||
default:
|
||||
WriteJson( response, 200, JsonRpc.BuildError( id, -32601, "Method not found: " + method ) );
|
||||
TryWriteJson( response, JsonRpc.BuildError( id, -32601, "Method not found: " + method ) );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ToolCallOutcome DispatchToolCallBlocking( JSONObject paramsObj )
|
||||
void HandleToolCall( JSONData id, JSONObject paramsObj, HttpListenerResponse response )
|
||||
{
|
||||
var toolName = JsonUtil.GetStringOrNull( paramsObj, "name" );
|
||||
var arguments = paramsObj != null ? paramsObj.GetObject( "arguments" ) : null;
|
||||
|
|
@ -322,17 +334,44 @@ namespace Rokojori.Tools
|
|||
arguments = new JSONObject();
|
||||
}
|
||||
|
||||
var pending = new PendingToolCall( toolName, arguments );
|
||||
pendingCalls.Enqueue( pending );
|
||||
ToolCallOutcome outcome;
|
||||
|
||||
var completed = pending.waitHandle.Wait( TimeSpan.FromSeconds( 30 ) );
|
||||
|
||||
if ( ! completed )
|
||||
try
|
||||
{
|
||||
return ToolCallOutcome.Error( "Timed out waiting for the main thread to execute tool: " + toolName );
|
||||
outcome = protocol.ExecuteTool( toolName, arguments );
|
||||
}
|
||||
catch ( Exception e )
|
||||
{
|
||||
RJLog.Error( "MCP", "Tool execution failed", toolName, e );
|
||||
outcome = ToolCallOutcome.Error( e.Message );
|
||||
}
|
||||
|
||||
return pending.outcome ?? ToolCallOutcome.Error( "Tool produced no result: " + toolName );
|
||||
TryWriteJson( response, protocol.ToolCallResult( id, outcome ) );
|
||||
}
|
||||
|
||||
void TryWriteJson( HttpListenerResponse response, JSONData data )
|
||||
{
|
||||
try
|
||||
{
|
||||
WriteJson( response, 200, data );
|
||||
}
|
||||
catch ( Exception e )
|
||||
{
|
||||
// Client likely disconnected before the response could be written - nothing to recover.
|
||||
RJLog.Error( "MCP", "Failed to write response", e );
|
||||
}
|
||||
}
|
||||
|
||||
void TryWriteEmpty( HttpListenerResponse response, int statusCode )
|
||||
{
|
||||
try
|
||||
{
|
||||
WriteEmpty( response, statusCode );
|
||||
}
|
||||
catch ( Exception e )
|
||||
{
|
||||
RJLog.Error( "MCP", "Failed to write response", e );
|
||||
}
|
||||
}
|
||||
|
||||
static void WriteJson( HttpListenerResponse response, int statusCode, JSONData data )
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
#if TOOLS
|
||||
|
||||
using System.Net;
|
||||
|
||||
namespace Rokojori.Tools
|
||||
{
|
||||
// A raw HTTP request body + its response, captured by the HTTP callback thread and handed
|
||||
// off untouched. Parsing the body (JSON.Parse, via the shared JSON library) and all protocol/
|
||||
// tool dispatch happens later on the main thread - not here, since that parsing can itself log
|
||||
// through RJLog (a Godot engine API) on malformed input, which is only safe from the main
|
||||
// thread. The callback thread's job ends at reading bytes in and, later, writing bytes out.
|
||||
public class PendingRequest
|
||||
{
|
||||
public string body;
|
||||
public HttpListenerResponse response;
|
||||
|
||||
public PendingRequest( string body, HttpListenerResponse response )
|
||||
{
|
||||
this.body = body;
|
||||
this.response = response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://cijeep5twxvdm
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
#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
|
||||
|
|
@ -1 +0,0 @@
|
|||
uid://dy170v7wjkcj5
|
||||
Loading…
Reference in New Issue