11package com .pyritone .bridge ;
22
33import com .google .gson .JsonElement ;
4+ import com .google .gson .JsonArray ;
45import com .google .gson .JsonObject ;
56import com .pyritone .bridge .command .PyritoneCommand ;
67import com .pyritone .bridge .config .BridgeConfig ;
910import com .pyritone .bridge .net .ProtocolCodec ;
1011import com .pyritone .bridge .net .WebSocketBridgeServer ;
1112import com .pyritone .bridge .runtime .BaritoneGateway ;
13+ import com .pyritone .bridge .runtime .EntityTypeSelector ;
1214import com .pyritone .bridge .runtime .StatusSubscriptionRegistry ;
1315import com .pyritone .bridge .runtime .TaskRegistry ;
1416import com .pyritone .bridge .runtime .TaskLifecycleResolver ;
2224import net .fabricmc .fabric .api .client .message .v1 .ClientSendMessageEvents ;
2325import net .fabricmc .loader .api .FabricLoader ;
2426import net .minecraft .client .MinecraftClient ;
27+ import net .minecraft .entity .Entity ;
28+ import net .minecraft .registry .Registries ;
2529import net .minecraft .text .MutableText ;
2630import net .minecraft .text .Text ;
2731import net .minecraft .util .Formatting ;
3539import java .nio .file .StandardOpenOption ;
3640import java .security .MessageDigest ;
3741import java .time .Instant ;
42+ import java .util .ArrayList ;
43+ import java .util .Comparator ;
3844import java .util .List ;
45+ import java .util .Locale ;
3946import java .util .Optional ;
4047import java .util .Set ;
4148import java .util .concurrent .CompletionException ;
@@ -76,7 +83,7 @@ public void onInitializeClient() {
7683
7784 ClientTickEvents .END_CLIENT_TICK .register (client -> {
7885 baritoneGateway .tickRegisterPathListener ();
79- baritoneGateway .tickRegisterPyritoneHashCommand (this :: forceCancelActiveTaskFromPyritoneCommand );
86+ baritoneGateway .tickRegisterPyritoneHashCommand (() -> endPythonSessionsFromPyritoneCommand () );
8087 baritoneGateway .tickApplyPyritoneChatBranding ();
8188 tickTaskLifecycle ();
8289 tickStatusStreams ();
@@ -132,8 +139,8 @@ private void ensureBaritoneSettingsFile() {
132139 }
133140 }
134141 private boolean handleOutgoingChat (String message ) {
135- if (message != null && message .trim ().equalsIgnoreCase ("#pyritone cancel " )) {
136- forceCancelActiveTaskFromPyritoneCommand ();
142+ if (message != null && message .trim ().equalsIgnoreCase ("#pyritone end " )) {
143+ endPythonSessionsFromPyritoneCommand ();
137144 return false ;
138145 }
139146 emitWatchMatches ("chat" , message );
@@ -216,6 +223,7 @@ private JsonObject handleRequest(WebSocketBridgeServer.ClientSession session, Js
216223 case "api.metadata.get" -> handleApiMetadataGet (id , params , session );
217224 case "api.construct" -> handleApiConstruct (id , params , session );
218225 case "api.invoke" -> handleApiInvoke (id , params , session );
226+ case "entities.list" -> handleEntitiesList (id , params );
219227 case "baritone.execute" -> handleBaritoneExecute (id , params );
220228 case "task.cancel" -> handleTaskCancel (id );
221229 default -> ProtocolCodec .errorResponse (id , "METHOD_NOT_FOUND" , "Unknown method: " + method );
@@ -304,6 +312,62 @@ private JsonObject handleApiInvoke(String id, JsonObject params, WebSocketBridge
304312 return handleTypedApiRequest (id , () -> typedApiService .invoke (session .sessionId (), params ));
305313 }
306314
315+ private JsonObject handleEntitiesList (String id , JsonObject params ) {
316+ try {
317+ JsonObject result = runOnClientThread (() -> {
318+ MinecraftClient client = MinecraftClient .getInstance ();
319+ if (client == null || client .world == null || client .player == null ) {
320+ throw new TypedApiException ("NOT_IN_WORLD" , "Join a world before listing entities" );
321+ }
322+
323+ EntityTypeSelector selector = EntityTypeSelector .fromParams (params );
324+
325+ List <JsonObject > entities = new ArrayList <>();
326+ for (Entity entity : client .world .getEntities ()) {
327+ if (entity == client .player ) {
328+ continue ;
329+ }
330+
331+ String typeId = Registries .ENTITY_TYPE .getId (entity .getType ()).toString ();
332+ if (!selector .matches (entity , typeId )) {
333+ continue ;
334+ }
335+
336+ JsonObject payload = new JsonObject ();
337+ payload .addProperty ("id" , entity .getUuidAsString ());
338+ payload .addProperty ("type_id" , typeId );
339+ payload .addProperty ("category" , entity .getType ().getSpawnGroup ().name ().toLowerCase (Locale .ROOT ));
340+ payload .addProperty ("x" , entity .getX ());
341+ payload .addProperty ("y" , entity .getY ());
342+ payload .addProperty ("z" , entity .getZ ());
343+ payload .addProperty ("distance_sq" , client .player .squaredDistanceTo (entity ));
344+ entities .add (payload );
345+ }
346+
347+ entities .sort (Comparator .comparingDouble (entity -> entity .get ("distance_sq" ).getAsDouble ()));
348+
349+ JsonArray entries = new JsonArray ();
350+ for (JsonObject entity : entities ) {
351+ entries .add (entity );
352+ }
353+
354+ JsonObject response = new JsonObject ();
355+ response .add ("entities" , entries );
356+ return response ;
357+ });
358+ return ProtocolCodec .successResponse (id , result );
359+ } catch (IllegalArgumentException exception ) {
360+ return ProtocolCodec .errorResponse (id , "BAD_REQUEST" , exception .getMessage ());
361+ } catch (TypedApiException exception ) {
362+ return ProtocolCodec .errorResponse (id , exception .code (), exception .getMessage (), exception .details ());
363+ } catch (TimeoutException exception ) {
364+ return ProtocolCodec .errorResponse (id , "INTERNAL_ERROR" , "Timed out waiting for client thread" );
365+ } catch (Exception exception ) {
366+ LOGGER .debug ("entities.list request failed" , exception );
367+ return ProtocolCodec .errorResponse (id , "INTERNAL_ERROR" , "Unable to list entities" );
368+ }
369+ }
370+
307371 private JsonObject buildStatusPayload (WebSocketBridgeServer .ClientSession session ) {
308372 JsonObject result = new JsonObject ();
309373 result .addProperty ("protocol_version" , BridgeConfig .PROTOCOL_VERSION );
@@ -328,7 +392,9 @@ private JsonObject handleBaritoneExecute(String id, JsonObject params) {
328392 return ProtocolCodec .errorResponse (id , "BAD_REQUEST" , "Missing command" );
329393 }
330394
331- emitPyritoneNotice ("Python execute: " + compactCommand (command ));
395+ String label = asString (params , "label" );
396+ String notice = label != null && !label .isBlank () ? label : command ;
397+ emitPyritoneNotice ("Python execute: " + compactCommand (notice ));
332398
333399 if (!baritoneGateway .isAvailable ()) {
334400 emitPyritoneNotice ("Python execute blocked: Baritone unavailable" );
@@ -447,6 +513,31 @@ private Object resolvePrimaryBaritone() throws ReflectiveOperationException {
447513 return baritoneGateway .resolvePrimaryBaritoneForTypedApi ();
448514 }
449515
516+ public int endPythonSessionsFromPyritoneCommand () {
517+ WebSocketBridgeServer currentServer = this .server ;
518+ if (currentServer == null || !currentServer .isRunning ()) {
519+ emitPyritoneNotice ("Bridge is not running" );
520+ return 0 ;
521+ }
522+
523+ int disconnected = 0 ;
524+ for (WebSocketBridgeServer .ClientSession session : currentServer .sessionSnapshot ()) {
525+ if (!session .isAuthenticated ()) {
526+ continue ;
527+ }
528+ session .close ();
529+ disconnected += 1 ;
530+ }
531+
532+ if (disconnected > 0 ) {
533+ emitPyritoneNotice ("Ended " + disconnected + " Python websocket session(s)" );
534+ } else {
535+ emitPyritoneNotice ("No authenticated Python websocket sessions" );
536+ }
537+
538+ return disconnected ;
539+ }
540+
450541 public boolean forceCancelActiveTaskFromPyritoneCommand () {
451542 Optional <TaskSnapshot > active = taskRegistry .active ();
452543 if (active .isEmpty ()) {
0 commit comments