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 )
|
public override void _Process( double delta )
|
||||||
{
|
{
|
||||||
mcpServer.ProcessPendingToolCalls();
|
mcpServer.ProcessPendingRequests();
|
||||||
|
|
||||||
var editingScene = EditorInterface.Singleton.GetEditedSceneRoot();
|
var editingScene = EditorInterface.Singleton.GetEditedSceneRoot();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ namespace Rokojori
|
||||||
|
|
||||||
protected override void _OnTrigger()
|
protected override void _OnTrigger()
|
||||||
{
|
{
|
||||||
this.LogInfo( "Started Float Tween" );
|
// this.LogInfo( "Started Float Tween" );
|
||||||
|
|
||||||
|
|
||||||
// if ( Engine.IsEditorHint() )
|
// if ( Engine.IsEditorHint() )
|
||||||
|
|
@ -119,7 +119,7 @@ namespace Rokojori
|
||||||
|
|
||||||
AnimationManager.StartAnimation( this, target, targetMemberPath );
|
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,
|
_timeID = TimelineManager.ScheduleSpanWith( duration,
|
||||||
( span, type )=>
|
( span, type )=>
|
||||||
|
|
|
||||||
|
|
@ -855,21 +855,26 @@ namespace Rokojori
|
||||||
RemoveAndDeleteChildren( parent, includeInternal );
|
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( child );
|
||||||
parent.RemoveChild( node );
|
|
||||||
|
|
||||||
if ( ! queue )
|
if ( ! queue )
|
||||||
{
|
{
|
||||||
node.Free();
|
child.Free();
|
||||||
}
|
}
|
||||||
else
|
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 )
|
public static void DestroyChildrenRange( this Node parent, int startIndex, int range, bool includeInternal = false, bool queue = true )
|
||||||
{
|
{
|
||||||
parent.LogInfo( startIndex, range, parent.GetChildCount() );
|
parent.LogInfo( startIndex, range, parent.GetChildCount() );
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,8 @@ namespace Rokojori
|
||||||
|
|
||||||
public void _OnSensor( SensorEvent se )
|
public void _OnSensor( SensorEvent se )
|
||||||
{
|
{
|
||||||
this.LogInfo( "SensorEvent:", se );
|
// this.LogInfo( "SensorEvent:", se );
|
||||||
|
|
||||||
if ( pointer == null || pointer.pointable == null )
|
if ( pointer == null || pointer.pointable == null )
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,8 @@ namespace Rokojori
|
||||||
[Export]
|
[Export]
|
||||||
public int maxHits = 3;
|
public int maxHits = 3;
|
||||||
|
|
||||||
List<CollisionData> collisions = new List<CollisionData>();
|
// List<CollisionData> collisions = new List<CollisionData>();
|
||||||
int numCollisions = 0;
|
// int numCollisions = 0;
|
||||||
|
|
||||||
[Export]
|
[Export]
|
||||||
public Vector3 to;
|
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 );
|
return IntegerExclusiveInRange( 0, max );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public HexagonCoord HexagonDirection()
|
||||||
|
{
|
||||||
|
return HexagonCoord.Direction( IntegerInclusiveInRange( 0, 5 ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public char FromText( string source )
|
public char FromText( string source )
|
||||||
{
|
{
|
||||||
|
|
@ -243,6 +248,7 @@ namespace Rokojori
|
||||||
|
|
||||||
return list[ index ];
|
return list[ index ];
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public T GrabFromRange<T>( List<T> list, int start, int end )
|
public T GrabFromRange<T>( List<T> list, int start, int end )
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||||
using System;
|
using System;
|
||||||
using Godot;
|
using Godot;
|
||||||
using Rokojori.Extensions;
|
using Rokojori.Extensions;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Rokojori;
|
namespace Rokojori;
|
||||||
|
|
||||||
|
|
@ -93,6 +94,11 @@ public partial class TimelineManager:NetworkNode
|
||||||
Engine.TimeScale = GetRunnerByTimeline( timeScaleTimeline ).modulatedSpeed;
|
Engine.TimeScale = GetRunnerByTimeline( timeScaleTimeline ).modulatedSpeed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( Engine.IsEditorHint() && _runners == null )
|
||||||
|
{
|
||||||
|
Initialize();
|
||||||
|
}
|
||||||
|
|
||||||
_runners.ForEach(
|
_runners.ForEach(
|
||||||
r =>
|
r =>
|
||||||
{
|
{
|
||||||
|
|
@ -195,7 +201,23 @@ public partial class TimelineManager:NetworkNode
|
||||||
|
|
||||||
public TimelineRunner GetRunnerByTimeline( Timeline timeline )
|
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 )
|
public TimelineRunner GetRunner( int index )
|
||||||
|
|
|
||||||
|
|
@ -5,20 +5,31 @@ using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace Rokojori.Tools
|
namespace Rokojori.Tools
|
||||||
{
|
{
|
||||||
using Rokojori;
|
using Rokojori;
|
||||||
|
|
||||||
// Owned by RokojoriPlugin: Start()/Stop() from _EnterTree/_ExitTree,
|
// 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,
|
// Threading: HttpListener accepts via BeginGetContext/EndGetContext (thread-pool callbacks),
|
||||||
// notifications/initialized and tools/list are pure protocol/metadata and are answered
|
// not a dedicated foreground/background Thread blocked in GetContext() - no thread is ever
|
||||||
// directly on that thread. Only tools/call touches Godot APIs (ResourceLoader, PackedScene,
|
// blocked waiting on anything in this class, which matters because a blocked thread with a
|
||||||
// the scene tree) - it is enqueued and the background thread blocks until the main thread
|
// stack frame into this assembly keeps Godot's C# hot reload from unloading it until that
|
||||||
// drains the queue and signals the result back.
|
// 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 class McpServer
|
||||||
{
|
{
|
||||||
public const string FlagFilePath = "res://.godot-mcp";
|
public const string FlagFilePath = "res://.godot-mcp";
|
||||||
|
|
@ -26,13 +37,12 @@ namespace Rokojori.Tools
|
||||||
public const int PortRangeCount = 20;
|
public const int PortRangeCount = 20;
|
||||||
|
|
||||||
HttpListener listener;
|
HttpListener listener;
|
||||||
Thread listenerThread;
|
|
||||||
volatile bool running = false;
|
volatile bool running = false;
|
||||||
int boundPort = -1;
|
int boundPort = -1;
|
||||||
string projectPath = "";
|
string projectPath = "";
|
||||||
|
|
||||||
readonly McpProtocolHandler protocol = new McpProtocolHandler();
|
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 bool IsRunning { get { return running; } }
|
||||||
public int BoundPort { get { return boundPort; } }
|
public int BoundPort { get { return boundPort; } }
|
||||||
|
|
@ -70,9 +80,7 @@ namespace Rokojori.Tools
|
||||||
}
|
}
|
||||||
|
|
||||||
running = true;
|
running = true;
|
||||||
listenerThread = new Thread( ListenLoop );
|
BeginAccept();
|
||||||
listenerThread.IsBackground = true;
|
|
||||||
listenerThread.Start();
|
|
||||||
|
|
||||||
RJLog.Log( "MCP", "Server started on port", boundPort, "for project", projectPath );
|
RJLog.Log( "MCP", "Server started on port", boundPort, "for project", projectPath );
|
||||||
}
|
}
|
||||||
|
|
@ -86,27 +94,19 @@ namespace Rokojori.Tools
|
||||||
|
|
||||||
running = false;
|
running = false;
|
||||||
|
|
||||||
// A tools/call request blocks the single HTTP thread on pending.waitHandle, waiting for
|
// Any request still waiting for a future _Process() tick to dequeue it would otherwise sit
|
||||||
// a future _Process() tick to dequeue and signal it. If that tick never comes (e.g. the
|
// unanswered forever if that tick never comes (e.g. the editor pausing the main loop to
|
||||||
// editor is pausing the main loop to hot-reload this very assembly), the thread stays
|
// hot-reload this very assembly). Stop() always runs on the main thread, never concurrently
|
||||||
// alive holding a reference into this assembly and the reload fails to unload it. Stop()
|
// with _Process(), so it can safely stand in for the tick that won't arrive and answer any
|
||||||
// always runs on the main thread, never concurrently with _Process(), so it can safely
|
// still-queued request right now instead of leaving its HTTP response hanging.
|
||||||
// stand in for the tick that won't arrive and release any still-queued call right now.
|
while ( pendingRequests.TryDequeue( out var pending ) )
|
||||||
while ( pendingCalls.TryDequeue( out var pending ) )
|
|
||||||
{
|
{
|
||||||
pending.outcome = ToolCallOutcome.Error( "MCP server is stopping" );
|
AbortPendingRequest( pending );
|
||||||
pending.waitHandle.Set();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The accept loop is parked inside the blocking listener.GetContext() call from its previous
|
// Closing the listener faults any pending BeginGetContext registration, which invokes
|
||||||
// iteration. HttpListener.Stop()/Close() are documented to abort that call, but that has not
|
// OnGetContext on a thread-pool callback thread with an exception from EndGetContext() -
|
||||||
// proven reliable in practice - if it doesn't, the thread stays parked forever, which blocks
|
// handled there by simply not chaining another BeginAccept(). No dedicated thread to join.
|
||||||
// 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
|
try
|
||||||
{
|
{
|
||||||
listener?.Stop();
|
listener?.Stop();
|
||||||
|
|
@ -116,57 +116,101 @@ namespace Rokojori.Tools
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
listenerThread?.Join( 2000 );
|
|
||||||
listener = null;
|
listener = null;
|
||||||
boundPort = -1;
|
boundPort = -1;
|
||||||
|
|
||||||
RJLog.Log( "MCP", "Server stopped." );
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using ( var client = new System.Net.Sockets.TcpClient() )
|
listener.BeginGetContext( OnGetContext, null );
|
||||||
{
|
|
||||||
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 )
|
||||||
|
{
|
||||||
|
// Listener already stopped/closed - nothing to accept.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnGetContext( IAsyncResult ar )
|
||||||
|
{
|
||||||
|
HttpListenerContext context;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 500;
|
||||||
|
context.Response.OutputStream.Close();
|
||||||
}
|
}
|
||||||
catch ( Exception )
|
catch ( Exception )
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
// Drains tool-call requests queued by the HTTP thread. Must run on the main thread.
|
|
||||||
public void ProcessPendingToolCalls()
|
|
||||||
{
|
{
|
||||||
while ( pendingCalls.TryDequeue( out var pending ) )
|
BeginAccept();
|
||||||
{
|
|
||||||
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;
|
// Drains requests queued by the HTTP callback thread and handles them fully - JSON parsing,
|
||||||
pending.waitHandle.Set();
|
// 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()
|
// 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
|
||||||
while ( running )
|
// /mcp POST body is handed off untouched - see the class comment for why.
|
||||||
{
|
|
||||||
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 )
|
void HandleContext( HttpListenerContext context )
|
||||||
{
|
{
|
||||||
var request = context.Request;
|
var request = context.Request;
|
||||||
|
|
@ -257,9 +269,10 @@ namespace Rokojori.Tools
|
||||||
body = reader.ReadToEnd();
|
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 )
|
void HandleJsonRpc( string body, HttpListenerResponse response )
|
||||||
{
|
{
|
||||||
JSONObject envelope;
|
JSONObject envelope;
|
||||||
|
|
@ -276,7 +289,7 @@ namespace Rokojori.Tools
|
||||||
}
|
}
|
||||||
catch ( Exception )
|
catch ( Exception )
|
||||||
{
|
{
|
||||||
WriteJson( response, 200, JsonRpc.BuildError( new JSONValue(), -32700, "Parse error" ) );
|
TryWriteJson( response, JsonRpc.BuildError( new JSONValue(), -32700, "Parse error" ) );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -287,32 +300,31 @@ namespace Rokojori.Tools
|
||||||
|
|
||||||
if ( isNotification )
|
if ( isNotification )
|
||||||
{
|
{
|
||||||
WriteEmpty( response, 202 );
|
TryWriteEmpty( response, 202 );
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch ( method )
|
switch ( method )
|
||||||
{
|
{
|
||||||
case "initialize":
|
case "initialize":
|
||||||
WriteJson( response, 200, protocol.Initialize( id, paramsObj ) );
|
TryWriteJson( response, protocol.Initialize( id, paramsObj ) );
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "tools/list":
|
case "tools/list":
|
||||||
WriteJson( response, 200, protocol.ToolsList( id ) );
|
TryWriteJson( response, protocol.ToolsList( id ) );
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "tools/call":
|
case "tools/call":
|
||||||
var outcome = DispatchToolCallBlocking( paramsObj );
|
HandleToolCall( id, paramsObj, response );
|
||||||
WriteJson( response, 200, protocol.ToolCallResult( id, outcome ) );
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
WriteJson( response, 200, JsonRpc.BuildError( id, -32601, "Method not found: " + method ) );
|
TryWriteJson( response, JsonRpc.BuildError( id, -32601, "Method not found: " + method ) );
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolCallOutcome DispatchToolCallBlocking( JSONObject paramsObj )
|
void HandleToolCall( JSONData id, JSONObject paramsObj, HttpListenerResponse response )
|
||||||
{
|
{
|
||||||
var toolName = JsonUtil.GetStringOrNull( paramsObj, "name" );
|
var toolName = JsonUtil.GetStringOrNull( paramsObj, "name" );
|
||||||
var arguments = paramsObj != null ? paramsObj.GetObject( "arguments" ) : null;
|
var arguments = paramsObj != null ? paramsObj.GetObject( "arguments" ) : null;
|
||||||
|
|
@ -322,17 +334,44 @@ namespace Rokojori.Tools
|
||||||
arguments = new JSONObject();
|
arguments = new JSONObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
var pending = new PendingToolCall( toolName, arguments );
|
ToolCallOutcome outcome;
|
||||||
pendingCalls.Enqueue( pending );
|
|
||||||
|
|
||||||
var completed = pending.waitHandle.Wait( TimeSpan.FromSeconds( 30 ) );
|
try
|
||||||
|
|
||||||
if ( ! completed )
|
|
||||||
{
|
{
|
||||||
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 )
|
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