Skip to content

Commit d24c7ec

Browse files
committed
2A - First non-beta release
read changelog.md for actual changes
1 parent 0c9343e commit d24c7ec

16 files changed

Lines changed: 228 additions & 102 deletions

changelog.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,23 @@ Feature update
1111

1212
- Optimized for single request latency
1313
- Use batching to speed up multiple requests
14+
- Now uses orjson for faster parsing
15+
- Automatic handle management
1416

1517
#### New APIs
1618

1719
- Tab list API on Player
18-
- Region utils on World
20+
- Region utils on World: .set_block, .fill, .replace, .fill_sphere, .fill_cylinder, .fill_line
1921
- Particle shapes on World
20-
- Entity spawn helpers on World
22+
- Entity spawn helpers on World: spawn_at_player, spawn_projectile, spawn_with_nbt.
2123
- Support for command execution on Server
2224
- world.entities property
2325
- RaycastResult.distance and .hit_face
2426

2527
#### New helpers
2628

2729
- Sidebar: Scoreboard helper
28-
- Config: YAML config helper
30+
- Config: TOML config helper
2931
- Cooldown: Automatically manage cooldowns
3032
- Hologram: Show floating text
3133
- Menu / MenuItem: Create easy chest GUIs
@@ -35,6 +37,7 @@ Feature update
3537
- ItemDisplay: Show floating items
3638
- ImageDisplay: Show images in the world
3739
- ItemBuilder: Easily create items
40+
- Shutdown event
3841

3942
#### API improvements
4043

@@ -45,11 +48,14 @@ Feature update
4548
- Added command description parameter
4649
- Location: .add, .clone, .distance, .distance_squared are now sync
4750
- Scoreboard, Team, Objective, and BossBar creation methods are now sync
51+
- Config: Added support for multiple formats, toml (default), json, and properties.
52+
- Commands can be now executes as console
4853

4954
#### Cleanup
5055

5156
- Entity class moved before Player
5257
- Moved most of the code from a single file to multiple
58+
- Improved typing across python bridge
5359

5460
#### Misc
5561

docs/site/exceptions.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ <h2 id="entitygoneexception">EntityGoneException</h2>
168168

169169
<span class="dc">@event</span>
170170
<span class="kw">async</span> <span class="kw">def</span> entity_damage(e):
171-
<span class="kw">await</span> server.wait(<span class="nb">20</span>) <span class="cm"># Wait 1 second</span>
171+
<span class="kw">await</span> server.after(<span class="nb">20</span>) <span class="cm"># Wait 1 second</span>
172172
<span class="kw">try</span>:
173173
<span class="kw">await</span> e.entity.set_fire_ticks(<span class="nb">100</span>)
174174
<span class="kw">except</span> EntityGoneException:
@@ -179,7 +179,7 @@ <h3 id="when-to-expect-it">When to expect it</h3>
179179
<p>Any awaitable method on <code>Entity</code> or <code>Player</code> can raise <code>EntityGoneException</code> if the underlying Java entity has been garbage collected. This is especially common when:</p>
180180
<ul>
181181
<li>You store entity references across ticks</li>
182-
<li>You use <code>server.wait()</code> between getting a reference and using it</li>
182+
<li>You use <code>server.after()</code> between getting a reference and using it</li>
183183
<li>You handle events where the entity might die (e.g. <code>entity_damage</code>)</li>
184184
</ul>
185185
<h3 id="best-practice">Best practice</h3>

docs/site/server.html

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
<li><a href="#world" style="padding-left:40px;font-size:.8rem">world</a></li>
6666
<li><a href="#create_boss_bar" style="padding-left:40px;font-size:.8rem">create_boss_bar</a></li>
6767
<li><a href="#get_advancement" style="padding-left:40px;font-size:.8rem">get_advancement</a></li>
68-
<li><a href="#wait" style="padding-left:40px;font-size:.8rem">wait</a></li>
68+
<li><a href="#after" style="padding-left:40px;font-size:.8rem">after</a></li>
6969
<li><a href="#frame" style="padding-left:40px;font-size:.8rem">frame</a></li>
7070
<li><a href="#atomic" style="padding-left:40px;font-size:.8rem">atomic</a></li>
7171
<li><a href="#flush" style="padding-left:40px;font-size:.8rem">flush</a></li>
@@ -238,7 +238,7 @@ <h3 id="scheduler">scheduler</h3>
238238
<ul>
239239
<li><strong>Type:</strong> <code>any</code></li>
240240
</ul>
241-
<p>The Bukkit scheduler. Rarely needed — use <code>server.wait()</code> and <code>@task</code> instead.</p>
241+
<p>The Bukkit scheduler. Rarely needed — use <code>server.after()</code> and <code>@task</code> instead.</p>
242242
<h3 id="scoreboard_manager">scoreboard_manager</h3>
243243
<ul>
244244
<li><strong>Type:</strong> <code>any</code></li>
@@ -302,8 +302,8 @@ <h3 id="get_advancement">get_advancement</h3>
302302
<li><code>key</code> (<code>str</code>) — The advancement key (e.g. <code>"minecraft:story/mine_diamond"</code>).</li>
303303
<li><strong>Returns:</strong> <code>Awaitable[</code><a href="advancement.html"><code>Advancement</code></a><code>]</code></li>
304304
</ul>
305-
<h3 id="wait">wait</h3>
306-
<pre><code class="language-python"><span class="kw">await</span> server.wait(ticks: int = <span class="nb">1</span>, after: callable | <span class="kw">None</span> = <span class="kw">None</span>)
305+
<h3 id="after">after</h3>
306+
<pre><code class="language-python"><span class="kw">await</span> server.after(ticks: int = <span class="nb">1</span>, after: callable | <span class="kw">None</span> = <span class="kw">None</span>)
307307
</code></pre>
308308
<p>Pause execution for a number of ticks. Optionally run a callback after the wait.</p>
309309
<ul>
@@ -312,11 +312,11 @@ <h3 id="wait">wait</h3>
312312
<li><code>after</code> (<code>callable | None</code>) — Optional callback to run after the wait completes.</li>
313313
<li><strong>Returns:</strong> <code>Awaitable[None]</code></li>
314314
</ul>
315-
<pre><code class="language-python"><span class="kw">await</span> server.wait(<span class="nb">20</span>) <span class="cm"># Wait 1 second</span>
316-
<span class="kw">await</span> server.wait(<span class="nb">60</span>) <span class="cm"># Wait 3 seconds</span>
315+
<pre><code class="language-python"><span class="kw">await</span> server.after(<span class="nb">20</span>) <span class="cm"># Wait 1 second</span>
316+
<span class="kw">await</span> server.after(<span class="nb">60</span>) <span class="cm"># Wait 3 seconds</span>
317317

318318
<span class="cm"># With callback</span>
319-
<span class="kw">await</span> server.wait(<span class="nb">20</span>, after=<span class="kw">lambda</span>: print(<span class="st">&quot;Done waiting!&quot;</span>))
319+
<span class="kw">await</span> server.after(<span class="nb">20</span>, after=<span class="kw">lambda</span>: print(<span class="st">&quot;Done waiting!&quot;</span>))
320320
</code></pre>
321321
<h3 id="frame">frame</h3>
322322
<pre><code class="language-python"><span class="kw">async</span> <span class="kw">with</span> server.frame():

docs/src/exceptions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ from bridge import *
4343

4444
@event
4545
async def entity_damage(e):
46-
await server.wait(20) # Wait 1 second
46+
await server.after(20) # Wait 1 second
4747
try:
4848
await e.entity.set_fire_ticks(100)
4949
except EntityGoneException:
@@ -56,7 +56,7 @@ async def entity_damage(e):
5656
Any awaitable method on `Entity` or `Player` can raise `EntityGoneException` if the underlying Java entity has been garbage collected. This is especially common when:
5757

5858
- You store entity references across ticks
59-
- You use `server.wait()` between getting a reference and using it
59+
- You use `server.after()` between getting a reference and using it
6060
- You handle events where the entity might die (e.g. `entity_damage`)
6161

6262
### Best practice

docs/src/server.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ The Bukkit plugin manager. Primarily useful for advanced reflection use cases.
9898

9999
- **Type:** `any`
100100

101-
The Bukkit scheduler. Rarely needed — use `server.wait()` and `@task` instead.
101+
The Bukkit scheduler. Rarely needed — use `server.after()` and `@task` instead.
102102

103103
### scoreboard_manager
104104

@@ -189,10 +189,10 @@ Get an advancement by its namespaced key.
189189
- `key` (`str`) — The advancement key (e.g. `"minecraft:story/mine_diamond"`).
190190
- **Returns:** `Awaitable[`[`Advancement`](advancement.md)`]`
191191

192-
### wait
192+
### after
193193

194194
```python
195-
await server.wait(ticks: int = 1, after: callable | None = None)
195+
await server.after(ticks: int = 1, after: callable | None = None)
196196
```
197197

198198
Pause execution for a number of ticks. Optionally run a callback after the wait.
@@ -203,11 +203,11 @@ Pause execution for a number of ticks. Optionally run a callback after the wait.
203203
- **Returns:** `Awaitable[None]`
204204

205205
```python
206-
await server.wait(20) # Wait 1 second
207-
await server.wait(60) # Wait 3 seconds
206+
await server.after(20) # Wait 1 second
207+
await server.after(60) # Wait 3 seconds
208208

209209
# With callback
210-
await server.wait(20, after=lambda: print("Done waiting!"))
210+
await server.after(20, after=lambda: print("Done waiting!"))
211211
```
212212

213213
### frame

releases/pyjavabridge-1C.jar

-1.94 KB
Binary file not shown.

releases/pyjavabridge-dev.jar

78.6 KB
Binary file not shown.

src/main/java/com/pyjavabridge/BridgeInstance.java

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@
6262
import java.util.concurrent.ConcurrentHashMap;
6363
import java.util.concurrent.CountDownLatch;
6464
import java.util.concurrent.TimeUnit;
65-
import java.util.concurrent.atomic.AtomicInteger;
65+
import org.bukkit.event.Listener;
66+
import org.bukkit.event.EventHandler;
67+
import org.bukkit.event.player.PlayerQuitEvent;
6668

6769
public class BridgeInstance {
6870
private final PyJavaBridgePlugin plugin;
@@ -89,10 +91,10 @@ public class BridgeInstance {
8991
private final ReflectFacade reflectFacade = new ReflectFacade();
9092
private final RegionFacade regionFacade = new RegionFacade();
9193
private final ParticleFacade particleFacade = new ParticleFacade();
92-
private PermissionsFacade permissionsFacade;
93-
private MetricsFacade metricsFacade;
94-
private RefFacade refFacade;
95-
private CommandsFacade commandsFacade;
94+
private final PermissionsFacade permissionsFacade;
95+
private final MetricsFacade metricsFacade;
96+
private final RefFacade refFacade;
97+
private final CommandsFacade commandsFacade;
9698

9799
private ServerSocket serverSocket;
98100
private Socket socket;
@@ -117,6 +119,19 @@ public class BridgeInstance {
117119
this.metricsFacade = new MetricsFacade(plugin);
118120
this.refFacade = new RefFacade(this);
119121
this.commandsFacade = new CommandsFacade(plugin, this);
122+
123+
Bukkit.getPluginManager().registerEvents(new Listener() {
124+
@EventHandler
125+
public void onPlayerQuit(PlayerQuitEvent event) {
126+
PermissionAttachment attachment = permissionAttachments.remove(event.getPlayer().getUniqueId());
127+
if (attachment != null) {
128+
try {
129+
event.getPlayer().removeAttachment(attachment);
130+
} catch (Exception ignored) {
131+
}
132+
}
133+
}
134+
}, plugin);
120135
}
121136

122137
public boolean isRunning() {
@@ -203,6 +218,8 @@ private void bridgeLoop() {
203218
try {
204219
serverSocket.setSoTimeout(30_000);
205220
socket = serverSocket.accept();
221+
closeQuietly(serverSocket);
222+
serverSocket = null;
206223
plugin.getLogger().info("[" + name + "] Python connected");
207224

208225
reader = new DataInputStream(socket.getInputStream());
@@ -239,8 +256,14 @@ private void bridgeLoop() {
239256
}
240257
byte[] payload = new byte[length];
241258
reader.readFully(payload);
242-
JsonObject message = JsonParser.parseString(new String(payload, StandardCharsets.UTF_8))
243-
.getAsJsonObject();
259+
JsonObject message;
260+
try {
261+
message = JsonParser.parseString(new String(payload, StandardCharsets.UTF_8))
262+
.getAsJsonObject();
263+
} catch (Exception e) {
264+
plugin.getLogger().severe("[" + name + "] Failed to parse message: " + e.getMessage());
265+
continue;
266+
}
244267
handleMessage(message);
245268
}
246269

src/main/java/com/pyjavabridge/EventDispatcher.java

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -122,32 +122,35 @@ private boolean dispatchCancellableEvent(Event event, String eventName, JsonObje
122122
}
123123
PendingEvent pending = pendingEvents.get(eventId);
124124
if (pending != null) {
125-
long deadline = System.currentTimeMillis() + timeoutMs;
126125
try {
127-
while (System.currentTimeMillis() < deadline && pending.latch.getCount() > 0) {
128-
plugin.drainMainThreadQueue();
129-
pending.latch.await(5, java.util.concurrent.TimeUnit.MILLISECONDS);
126+
long deadline = System.currentTimeMillis() + timeoutMs;
127+
try {
128+
while (System.currentTimeMillis() < deadline && pending.latch.getCount() > 0) {
129+
plugin.drainMainThreadQueue();
130+
pending.latch.await(5, java.util.concurrent.TimeUnit.MILLISECONDS);
131+
}
132+
} catch (InterruptedException ignored) {
133+
Thread.currentThread().interrupt();
130134
}
131-
} catch (InterruptedException ignored) {
132-
Thread.currentThread().interrupt();
133-
}
134-
boolean cancelRequested = pending.cancelRequested.get();
135-
if (pending.chatOverride != null && isChatEvent(pending.event)) {
136-
pending.cancellable.setCancelled(true);
137-
String message = pending.chatOverride;
138-
Bukkit.getScheduler().runTask(plugin,
139-
() -> Bukkit.getServer().broadcast(net.kyori.adventure.text.Component.text(message)));
140-
}
141-
if (pending.damageOverride != null && pending.event instanceof EntityDamageEvent damageEvent) {
142-
damageEvent.setDamage(pending.damageOverride);
143-
}
144-
if (cancelRequested && cancelMode == CancelMode.EVENT) {
145-
pending.cancellable.setCancelled(true);
146-
}
147-
if (pending.latch.getCount() > 0 && timeoutMs >= 100) {
148-
plugin.getLogger().warning("[" + name + "] Event handler timed out for " + eventName);
135+
boolean cancelRequested = pending.cancelRequested.get();
136+
if (pending.chatOverride != null && isChatEvent(pending.event)) {
137+
pending.cancellable.setCancelled(true);
138+
String message = pending.chatOverride;
139+
Bukkit.getScheduler().runTask(plugin,
140+
() -> Bukkit.getServer().broadcast(net.kyori.adventure.text.Component.text(message)));
141+
}
142+
if (pending.damageOverride != null && pending.event instanceof EntityDamageEvent damageEvent) {
143+
damageEvent.setDamage(pending.damageOverride);
144+
}
145+
if (cancelRequested && cancelMode == CancelMode.EVENT) {
146+
pending.cancellable.setCancelled(true);
147+
}
148+
if (pending.latch.getCount() > 0 && timeoutMs >= 100) {
149+
plugin.getLogger().warning("[" + name + "] Event handler timed out for " + eventName);
150+
}
151+
} finally {
152+
pendingEvents.remove(eventId);
149153
}
150-
pendingEvents.remove(eventId);
151154
}
152155
return cancellable && ((org.bukkit.event.Cancellable) event).isCancelled();
153156
}

src/main/java/com/pyjavabridge/PyJavaBridgePlugin.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -234,8 +234,9 @@ private void startFileWatcher(Path scriptsDir) {
234234
}
235235
watchEnabled = true;
236236
fileWatcherThread = new Thread(() -> {
237+
java.nio.file.WatchService watcher = null;
237238
try {
238-
java.nio.file.WatchService watcher = java.nio.file.FileSystems.getDefault().newWatchService();
239+
watcher = java.nio.file.FileSystems.getDefault().newWatchService();
239240
scriptsDir.register(watcher,
240241
java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY,
241242
java.nio.file.StandardWatchEventKinds.ENTRY_CREATE);
@@ -265,11 +266,17 @@ private void startFileWatcher(Path scriptsDir) {
265266
}
266267
}
267268
}
268-
watcher.close();
269269
} catch (Exception e) {
270270
if (watchEnabled) {
271271
getLogger().warning("File watcher error: " + e.getMessage());
272272
}
273+
} finally {
274+
if (watcher != null) {
275+
try {
276+
watcher.close();
277+
} catch (Exception ignored) {
278+
}
279+
}
273280
}
274281
}, "PyJavaBridge-FileWatcher");
275282
fileWatcherThread.setDaemon(true);

0 commit comments

Comments
 (0)