Skip to content

Fix Block#getType returning STONE, ordered synchronous setBlockData, block-state properties, and bridge hot-path costs - #25

Merged
Snowiiii merged 4 commits into
Pumpkin-MC:masterfrom
TR1LON:fix/hot-path-correctness
Aug 23, 2026
Merged

Fix Block#getType returning STONE, ordered synchronous setBlockData, block-state properties, and bridge hot-path costs#25
Snowiiii merged 4 commits into
Pumpkin-MC:masterfrom
TR1LON:fix/hot-path-correctness

Conversation

@TR1LON

@TR1LON TR1LON commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Correctness and hot-path fixes on the block read/write path and event registration — complementing the j4rs→JNI and dedup work that just landed in 43e65ca (this branch is rebased on top of it; three further issues I'd found — the per-byte event marshalling, the response double-unwrap, and the block_in_place around the atomic health read — were already fixed there, so they're not in this PR).

1. Block#getType() could never return the actual block

PatchBukkitRegionAccessor#getBlockData called the bridge, then discarded the response and returned STONE for every non-air block:

var response = NativeBridgeFfi.getBlockData(request);
if (response != null && !response.getBlockState().isEmpty()) {
    return Bukkit.createBlockData(Material.STONE);   // response never read
}

Now feeds response.getBlockState() through Bukkit.createBlockData(String), which already handles namespaced keys, property suffixes and legacy names.

2. Block-state properties survive the bridge

get_block_data returned only block.name — stairs lost facing, doors lost half. Now includes properties via Block::properties(state_id).to_props()minecraft:oak_stairs[facing=north,half=top]. Symmetrically, set_block_data parsed the string by splitting on [ and placing default_state; it now applies the [k=v,...] suffix through Block::from_properties(...).to_state_id(...).

3. Block#setType returned before the block was set

set_block_data was fire-and-forget (runtime.spawn): read-after-write raced, and two writes to one position could land in either order. Bukkit's contract is synchronous, ordered mutation. Now uses the same block_in_place + block_on pattern other callbacks already use, and honors applyPhysics (NOTIFY_ALL vs NOTIFY_LISTENERS). A comment documents the re-entrancy constraint for anyone wiring block-physics events to the JVM later.

4. ~10 µs of guaranteed ClassNotFoundExceptions per block read

PatchBukkitBlockData.newData ran Class.forName("net.minecraft.SharedConstants") and Class.forName("org.bukkit.craftbukkit.block.data.CraftBlockData") unconditionally on every call. Neither class can exist in this process (no NMS/OBC on the classpath), so both were guaranteed exceptions. Measured on JDK 21/25: ~10 µs per getType() call — ~95% of its total cost (the entire FFI round trip is ~450 ns). Removed.

5. Rust-side registration dedup (defense in depth)

Complements 43e65ca's Java-side registeredBridgeEvents: a (plugin, event type) guard in the Rust registration callback itself, so a duplicate Pumpkin handler can't be created even by callers that bypass PatchBukkitEventManager (direct HandlerList registration, plugin reloads). Each duplicate handler costs a full serialize + cross-thread round trip per event fire and re-invokes listeners that already ran.

6. Misc

PatchBukkitBlock allocated a metadata HashMap per instance — getBlockAt() creates a fresh wrapper per call, so every block read produced ~80 B of garbage. The map is now lazy.

Verification

Developed in a sandbox without Maven Central or a nightly toolchain, so verification is compile-level and unit-level rather than a live server run — CI should be the final arbiter:

  • Java: the touched files + protoc 35.1-generated proto sources compile cleanly under JDK 25 (Temurin 25.0.2) against the classpath extracted from the nightly release binary; unmodified master compiles identically under the same harness (A/B baseline). Re-verified after rebasing onto a6ca90a.
  • Rust: parse-level rustc check and rustfmt --check pass on the touched files. Every new API verified against current Pumpkin sources: Block::properties/to_props/from_properties/to_state_id (pumpkin-data generated block.rs), BlockFlags::NOTIFY_LISTENERS (pumpkin-world/src/world.rs). A full offline cargo check wasn't possible.
  • Behavioural: the block-state string build/parse logic passes 6 standalone cases (property round trip, empty suffix [], whitespace, missing prefix).

Judgment calls worth a maintainer's eye: the sync set_block_state is a semantics change (fire-and-forget → blocking, matching Bukkit's contract); and getBlockData on an unloaded chunk now reports what Pumpkin returns for it rather than always STONE.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Nk1NUKr7NNGxaU3jW5Nmyz

TR1LON added 4 commits August 20, 2026 01:02
…ta map

getBlockData called the native bridge, then discarded the response and returned Material.STONE for every non-air block, so Block#getType() could never report the actual block. Feed the returned block state string through Bukkit.createBlockData, which already parses namespaced keys, property suffixes and legacy names.

Also make PatchBukkitBlock's metadata map lazy: World#getBlockAt creates a fresh wrapper per call, so the eager HashMap was ~80 bytes of garbage per block read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Signed-off-by: TRiLON <d.m.alexandris@gmail.com>
newData ran Class.forName("net.minecraft.SharedConstants") and Class.forName("org.bukkit.craftbukkit.block.data.CraftBlockData") unconditionally on every call. Neither class can exist in this process (PatchBukkit ships no NMS/OBC), so both were guaranteed ClassNotFoundExceptions - measured at roughly 10 microseconds per call, ~95% of the cost of Block#getType() - on the hottest path in the API (getType -> createBlockData). Removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Signed-off-by: TRiLON <d.m.alexandris@gmail.com>
…gistrations

world.rs:
- get_block_data now returns block state properties (facing, half, waterlogged, ...) via Block::properties(state_id).to_props() instead of the bare block name.
- set_block_data applies the [k=v,...] suffix through Block::from_properties instead of silently placing default_state.
- set_block_data was fire-and-forget (runtime.spawn): Block#setType returned before the write landed and two writes to one position could land in either order. Bukkit's contract is synchronous ordered mutation; now uses the same block_in_place + block_on pattern other callbacks use, with a comment documenting the re-entrancy constraint for anyone bridging block-physics events later.
- applyPhysics honored: NOTIFY_ALL vs NOTIFY_LISTENERS.

events.rs: (plugin, event type) dedup guard in the registration callback itself, complementing the Java-side registeredBridgeEvents set from 43e65ca - a duplicate Pumpkin handler costs a full serialize + cross-thread round trip per event fire and re-invokes listeners that already ran.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Signed-off-by: TRiLON <d.m.alexandris@gmail.com>
Signed-off-by: TRiLON <d.m.alexandris@gmail.com>
@Snowiiii

Copy link
Copy Markdown
Member

Ig LGTM, Thanks

@Snowiiii
Snowiiii merged commit 6adee16 into Pumpkin-MC:master Aug 23, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants