8888import org .bukkit .event .EventHandler ;
8989import org .bukkit .event .player .PlayerQuitEvent ;
9090
91+ /**
92+ * Represents a single Python script bridge connection.
93+ * Manages the IPC protocol (stdin/stdout), message routing,
94+ * event subscriptions, and object handle registry for one script.
95+ */
9196public class BridgeInstance {
9297 private final PyJavaBridgePlugin plugin ;
9398 private final String name ;
@@ -152,7 +157,9 @@ public void onPlayerQuit(PlayerQuitEvent event) {
152157 if (attachment != null ) {
153158 try {
154159 event .getPlayer ().removeAttachment (attachment );
155- } catch (Exception ignored ) {
160+ } catch (Exception e ) {
161+ // Attachment may already be invalid if plugin is disabling
162+ plugin .getLogger ().fine ("[" + name + "] Could not remove permission attachment: " + e .getMessage ());
156163 }
157164 }
158165 }
@@ -179,6 +186,7 @@ public boolean hasSubscription(String eventName) {
179186 return subscriptions .containsKey (eventName );
180187 }
181188
189+ /** Starts the Python process and bridge communication thread. */
182190 void start () {
183191 running = true ;
184192
@@ -214,7 +222,9 @@ private void shutdownInternal() {
214222 if (obj instanceof org .bukkit .boss .BossBar bar ) {
215223 try {
216224 bar .removeAll ();
217- } catch (Exception ignored ) {}
225+ } catch (Exception e ) {
226+ plugin .getLogger ().fine ("[" + name + "] Could not remove boss bar: " + e .getMessage ());
227+ }
218228 }
219229 }
220230
@@ -254,7 +264,7 @@ private void bridgeLoop() {
254264 } catch (IOException eof ) {
255265 break ;
256266 }
257- if (length <= 0 || length > 16_777_216 ) {
267+ if (length <= 0 || length > plugin . getMaxMessageSize () ) {
258268 plugin .getLogger ().severe ("[" + name + "] Invalid message length: " + length );
259269 break ;
260270 }
@@ -267,6 +277,10 @@ private void bridgeLoop() {
267277 plugin .getLogger ().severe ("[" + name + "] Failed to parse message: " + e .getMessage ());
268278 continue ;
269279 }
280+ if (!message .has ("type" ) || !message .get ("type" ).isJsonPrimitive ()) {
281+ plugin .getLogger ().warning ("[" + name + "] Received message without valid 'type' field, skipping" );
282+ continue ;
283+ }
270284 handleMessage (message );
271285 }
272286
@@ -406,6 +420,21 @@ private void handleHandshake(JsonObject message) {
406420
407421 private void handleRegisterCommand (JsonObject message ) {
408422 String commandName = message .get ("name" ).getAsString ();
423+
424+ // Validate command name
425+ if (commandName == null || commandName .isEmpty ()) {
426+ plugin .getLogger ().warning ("[" + name + "] Attempted to register command with empty name" );
427+ return ;
428+ }
429+ if (commandName .length () > 64 ) {
430+ plugin .getLogger ().warning ("[" + name + "] Command name too long (max 64): " + commandName );
431+ return ;
432+ }
433+ if (!commandName .matches ("[a-zA-Z0-9_-]+" )) {
434+ plugin .getLogger ().warning ("[" + name + "] Invalid command name (alphanumeric, _ and - only): " + commandName );
435+ return ;
436+ }
437+
409438 String permission = message .has ("permission" ) ? message .get ("permission" ).getAsString () : null ;
410439 boolean hasDynamicTabComplete = message .has ("has_tab_complete" ) && message .get ("has_tab_complete" ).getAsBoolean ();
411440
@@ -508,7 +537,8 @@ private void handleRemoveEntities(JsonObject message) {
508537 if (obj instanceof Entity entity ) {
509538 try {
510539 entity .remove ();
511- } catch (Exception ignored ) {
540+ } catch (Exception e ) {
541+ plugin .getLogger ().fine ("[" + name + "] Could not remove entity: " + e .getMessage ());
512542 }
513543 }
514544 registry .release (handle );
@@ -644,22 +674,32 @@ void sendShutdownEvent() {
644674
645675 public Object handleKick (Player player , List <Object > args ) {
646676 String reason = args .isEmpty () ? "" : String .valueOf (args .get (0 ));
677+ // Try API variants in order: Component, String, legacy kickPlayer
647678 try {
648679 Method kick = player .getClass ().getMethod ("kick" , Component .class );
649680 kick .invoke (player , Component .text (reason ));
650681 return null ;
651- } catch (Exception ignored ) {
682+ } catch (NoSuchMethodException e ) {
683+ // API variant not available, try next
684+ } catch (Exception e ) {
685+ plugin .getLogger ().fine ("[" + name + "] kick(Component) failed: " + e .getMessage ());
652686 }
653687 try {
654688 Method kick = player .getClass ().getMethod ("kick" , String .class );
655689 kick .invoke (player , reason );
656690 return null ;
657- } catch (Exception ignored ) {
691+ } catch (NoSuchMethodException e ) {
692+ // API variant not available, try next
693+ } catch (Exception e ) {
694+ plugin .getLogger ().fine ("[" + name + "] kick(String) failed: " + e .getMessage ());
658695 }
659696 try {
660697 Method kickPlayer = player .getClass ().getMethod ("kickPlayer" , String .class );
661698 kickPlayer .invoke (player , reason );
662- } catch (Exception ignored ) {
699+ } catch (NoSuchMethodException e ) {
700+ plugin .getLogger ().warning ("[" + name + "] No kick method found on player class" );
701+ } catch (Exception e ) {
702+ plugin .getLogger ().fine ("[" + name + "] kickPlayer(String) failed: " + e .getMessage ());
663703 }
664704 return null ;
665705 }
@@ -1613,7 +1653,8 @@ private Object invokeInventoryMethod(org.bukkit.inventory.Inventory inventory, S
16131653 viewer .closeInventory ();
16141654 }
16151655 }
1616- } catch (Exception ignored ) {
1656+ } catch (Exception e ) {
1657+ plugin .getLogger ().fine ("[" + name + "] Error closing inventory: " + e .getMessage ());
16171658 }
16181659 return null ;
16191660 }
@@ -1622,7 +1663,8 @@ private Object invokeInventoryMethod(org.bukkit.inventory.Inventory inventory, S
16221663 Method getTitle = inventory .getClass ().getMethod ("getTitle" );
16231664 Object titleObj = getTitle .invoke (inventory );
16241665 return titleObj != null ? titleObj .toString () : "" ;
1625- } catch (Exception ignored ) {
1666+ } catch (Exception e ) {
1667+ // getTitle() may not exist on all inventory types
16261668 return "" ;
16271669 }
16281670 }
@@ -2114,7 +2156,9 @@ private Object invokeItemStackMethod(ItemStack itemStack, String method, List<Ob
21142156 try {
21152157 Method setType = ItemStack .class .getMethod ("setType" , Material .class );
21162158 setType .invoke (itemStack , deserialized .getType ());
2117- } catch (Exception ignored ) {
2159+ } catch (Exception e ) {
2160+ // setType may be deprecated/removed in newer API versions
2161+ plugin .getLogger ().fine ("[" + name + "] ItemStack.setType fallback failed: " + e .getMessage ());
21182162 }
21192163 itemStack .setAmount (deserialized .getAmount ());
21202164 if (deserialized .hasItemMeta ()) {
@@ -2501,7 +2545,7 @@ private Object invokeDisplayMethod(Object target, String method, List<Object> ar
25012545 return UNHANDLED ;
25022546 }
25032547
2504- // #2: Cache getMethods() per class to avoid repeated reflection
2548+ // Cache getMethods() per class to avoid repeated reflection
25052549 private static final ConcurrentHashMap <Class <?>, Method []> reflectiveMethodsCache = new ConcurrentHashMap <>();
25062550
25072551 private Object invokeReflective (Object target , String method , List <Object > args ) throws Exception {
@@ -2631,7 +2675,9 @@ private Sound resolveSound(Object arg) {
26312675 }
26322676 }
26332677 }
2634- } catch (Exception ignored ) {
2678+ } catch (Exception e ) {
2679+ // Sound registry lookup may fail depending on server version
2680+ plugin .getLogger ().fine ("Sound registry lookup failed for '" + name + "': " + e .getMessage ());
26352681 }
26362682 }
26372683
@@ -2644,7 +2690,9 @@ private Sound resolveSound(Object arg) {
26442690 if (soundObj instanceof Sound sound ) {
26452691 return sound ;
26462692 }
2647- } catch (Exception ignored ) {
2693+ } catch (Exception e ) {
2694+ // Sound.valueOf not available or name doesn't match
2695+ plugin .getLogger ().fine ("Sound.valueOf failed for '" + enumName + "': " + e .getMessage ());
26482696 }
26492697 }
26502698 return null ;
@@ -2669,7 +2717,8 @@ private String getTabListValue(Player player, boolean header) {
26692717 Method method = player .getClass ().getMethod (name );
26702718 value = method .invoke (player );
26712719 break ;
2672- } catch (Exception ignored ) {
2720+ } catch (Exception e ) {
2721+ // API variant not available; try next method name
26732722 }
26742723 }
26752724
@@ -2744,12 +2793,14 @@ private boolean tryTabListSetter(Player player, Object header, Object footer, Bo
27442793 method .invoke (player , header );
27452794 return true ;
27462795
2747- } catch (Exception ignored ) {
2796+ } catch (Exception e ) {
2797+ // Try next API method variant
27482798 }
27492799 }
27502800 return false ;
27512801 }
27522802
2803+ /** Serializes a value using the bridge serializer. */
27532804 public JsonElement serialize (Object value ) {
27542805 return serializer .serialize (value );
27552806 }
@@ -2915,6 +2966,7 @@ private JsonObject deserializePayload(byte[] payload) {
29152966 return JsonParser .parseString (new String (payload , StandardCharsets .UTF_8 )).getAsJsonObject ();
29162967 }
29172968
2969+ /** Sends a JSON message to the Python process over the bridge. */
29182970 public void send (JsonObject response ) {
29192971 if (writer == null || !running ) {
29202972 return ;
@@ -2936,8 +2988,8 @@ public void send(JsonObject response) {
29362988 }
29372989
29382990 /**
2939- * Write multiple responses under a single lock acquisition + single flush.
2940- * Much more efficient than calling send() in a loop for batch responses.
2991+ * Write multiple responses under a single lock acquisition and single flush.
2992+ * More efficient than calling send() in a loop for batch responses.
29412993 */
29422994 private void sendAll (List <JsonObject > responses , long startNano ) {
29432995 if (writer == null || !running || responses .isEmpty ()) {
@@ -3091,6 +3143,15 @@ private void startPythonProcess() {
30913143 }
30923144
30933145 private String resolvePythonExecutable () {
3146+ String override = plugin .getPythonExecutableOverride ();
3147+ if (override != null && !"auto" .equalsIgnoreCase (override )) {
3148+ Path overridePath = Path .of (override );
3149+ if (Files .exists (overridePath )) {
3150+ return overridePath .toAbsolutePath ().toString ();
3151+ }
3152+ plugin .getLogger ().warning ("[" + name + "] Configured python-executable not found: " + override + ", falling back to auto-detect" );
3153+ }
3154+
30943155 boolean isWindows = System .getProperty ("os.name" , "" ).toLowerCase ().contains ("win" );
30953156
30963157 Path venvDir = scriptsDir .resolve (".venv" );
@@ -3113,7 +3174,8 @@ private void closeQuietly(AutoCloseable closable) {
31133174 }
31143175 try {
31153176 closable .close ();
3116- } catch (Exception ignored ) {
3177+ } catch (Exception e ) {
3178+ plugin .getLogger ().fine ("[" + name + "] Error during close: " + e .getMessage ());
31173179 }
31183180 }
31193181}
0 commit comments