Skip to content

Run simulated item use on the main thread - #3712

Open
Almost10 wants to merge 1 commit into
IntellectualSites:mainfrom
Almost10:fix/item-use-off-main-thread
Open

Almost10 wants to merge 1 commit into
IntellectualSites:mainfrom
Almost10:fix/item-use-off-main-thread

Conversation

@Almost10

Copy link
Copy Markdown

Fixes #3613

Item brushes no longer throw when placing blocks.

@Almost10
Almost10 requested a review from a team as a code owner October 30, 2026 21:28
@MattBDev
MattBDev requested a balanced review from Copilot October 30, 2026 22:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Threading changes routing simulateItemUse to the main thread require live Paper/Folia verification and per-block sync cost assessment that cannot be performed here, warranting deferral to quorum.

No open issues 🎉

What changed in this PR

This PR fixes issue #3613, where item brushes (e.g. //brush apply sphere item red_wool) threw IllegalStateException: BlockCanBuildEvent may only be triggered synchronously. FAWE edits run on async worker threads, but simulateItemUse ultimately fires Bukkit events (via the NMS BlockItem.canPlace/BlockCanBuildEvent path) that require the main thread. The delegate adapter now wraps the parent call in TaskManager.taskManager().sync(...), hopping to the main thread (and running inline when already on it) before performing the simulated item use.

Changes:

  • Wrap getParent().simulateItemUse(...) in TaskManager.taskManager().sync(...) so the simulated item use (and its synchronous events) runs on the main thread.
  • Add the TaskManager import.
File Description
worldedit-bukkit/​src/​main/​java/​com/​fastasyncworldedit/​bukkit/​adapter/​IDelegateBukkitImplAdapter.java Routes simulateItemUse through TaskManager.sync to execute on the main thread, and imports TaskManager.

Assessment: The change is minimal, correct, and idiomatic. TaskManager.taskManager().sync(...) is the established FAWE pattern for Bukkit API calls that must be synchronous when invoked from async edit threads (used across ~52 files, e.g. IBukkitAdapter.java:391, BukkitWorld.java:152, WorldWrapper.java:272). The delegate adapter is confirmed to be in the call path (matching the issue's stack trace), and the parent PaperweightAdapter.simulateItemUse performs the event-firing placement directly, so the delegate is the appropriate place for this fix. sync returns directly when already on the main thread (no self-deadlock), and the boxed Boolean result is never null, so auto-unboxing is safe. I found no objective defects to comment on.

Review effort: Balanced

@Almost10

Copy link
Copy Markdown
Author

Tested this on a live Paper server, comparing the CI build of this PR against the CI build of its base commit.

Setup: Paper 26.3-231-main@8a170a3, FAWE as the only plugin, -Xmx1G.

  • Before: FastAsyncWorldEdit/1419 (2.16.2-SNAPSHOT+b70bf05, the base of this PR)
  • After: PR-3712/1 (2.16.2-SNAPSHOT+b90fe98)

Steps: //mask mossy_stone_bricks, then //brush apply sphere 12 item red_wool, then one right-click on a solid 25×22×31 mossy stone brick cube.

Before (b70bf05) After (this PR)
before-radius12 after-radius12
IllegalStateException: BlockCanBuildEvent may only be triggered synchronously., 0 blocks placed No errors, 3,189 blocks placed

The exact repro from #3613 (//mask mossy_stone_bricks, //brush apply sphere item red_wool, //gmask #air) also works on this PR with no errors.

Per-block sync cost: I timed the radius-12 stroke by polling the red wool count over RCON every ~60 ms. From the first block to the last took 1.43 s for 3,189 blocks (≈2,200 blocks/s, ≈110 per tick).

During the strokes:

  • TPS stayed at 20.0 (1m/5m/15m).
  • The worst tick in the 1-minute window was 29.7 ms.
  • There were no "Can't keep up" warnings.

The main thread works through the queued item uses inside the normal QueueHandler time budget. A large stroke fills in over about a second instead of appearing all at once, but it doesn't lag the server.

Stack trace on the base commit
java.lang.IllegalStateException: BlockCanBuildEvent may only be triggered synchronously.
	at io.papermc.paper.plugin.manager.PaperEventManager.callEvent(PaperEventManager.java:40) ~[paper-26.3.jar:26.3-231-8a170a3]
	at io.papermc.paper.plugin.manager.PaperPluginManagerImpl.callEvent(PaperPluginManagerImpl.java:131) ~[paper-26.3.jar:26.3-231-8a170a3]
	at org.bukkit.plugin.SimplePluginManager.callEvent(SimplePluginManager.java:627) ~[paper-api-26.3.build.231-stable.jar:?]
	at net.minecraft.world.item.BlockItem.canPlace(BlockItem.java:175) ~[paper-26.3.jar:26.3-231-8a170a3]
	at net.minecraft.world.item.BlockItem.getPlacementState(BlockItem.java:146) ~[paper-26.3.jar:26.3-231-8a170a3]
	at net.minecraft.world.item.BlockItem.place(BlockItem.java:71) ~[paper-26.3.jar:26.3-231-8a170a3]
	at net.minecraft.world.item.BlockItem.useOn(BlockItem.java:42) ~[paper-26.3.jar:26.3-231-8a170a3]
	at net.minecraft.world.item.ItemStack.useOn(ItemStack.java:396) ~[paper-26.3.jar:26.3-231-8a170a3]
	at FastAsyncWorldEdit-Paper-2.16.2-SNAPSHOT-main1419.jar//com.sk89q.worldedit.bukkit.adapter.impl.v26_3.PaperweightAdapter.simulateItemUse(PaperweightAdapter.java:722) ~[?:?]
	at FastAsyncWorldEdit-Paper-2.16.2-SNAPSHOT-main1419.jar//com.fastasyncworldedit.bukkit.adapter.IDelegateBukkitImplAdapter.simulateItemUse(IDelegateBukkitImplAdapter.java:102) ~[?:?]
	at FastAsyncWorldEdit-Paper-2.16.2-SNAPSHOT-main1419.jar//com.sk89q.worldedit.bukkit.BukkitWorld.useItem(BukkitWorld.java:679) ~[?:?]
	at FastAsyncWorldEdit-Paper-2.16.2-SNAPSHOT-main1419.jar//com.fastasyncworldedit.core.wrappers.WorldWrapper.useItem(WorldWrapper.java:97) ~[?:?]
	at FastAsyncWorldEdit-Paper-2.16.2-SNAPSHOT-main1419.jar//com.sk89q.worldedit.function.ItemUseFunction.apply(ItemUseFunction.java:46) ~[?:?]
	at FastAsyncWorldEdit-Paper-2.16.2-SNAPSHOT-main1419.jar//com.sk89q.worldedit.function.visitor.RegionVisitor.apply(RegionVisitor.java:172) ~[?:?]
	at FastAsyncWorldEdit-Paper-2.16.2-SNAPSHOT-main1419.jar//com.sk89q.worldedit.function.visitor.RegionVisitor.resume(RegionVisitor.java:158) ~[?:?]
	at FastAsyncWorldEdit-Paper-2.16.2-SNAPSHOT-main1419.jar//com.sk89q.worldedit.function.operation.Operations.completeLegacy(Operations.java:55) ~[?:?]
	at FastAsyncWorldEdit-Paper-2.16.2-SNAPSHOT-main1419.jar//com.sk89q.worldedit.command.tool.brush.OperationFactoryBrush.build(OperationFactoryBrush.java:62) ~[?:?]
	at FastAsyncWorldEdit-Paper-2.16.2-SNAPSHOT-main1419.jar//com.sk89q.worldedit.command.tool.BrushTool.act(BrushTool.java:481) ~[?:?]
	at FastAsyncWorldEdit-Paper-2.16.2-SNAPSHOT-main1419.jar//com.sk89q.worldedit.command.tool.BrushTool.actPrimary(BrushTool.java:367) ~[?:?]
	at FastAsyncWorldEdit-Paper-2.16.2-SNAPSHOT-main1419.jar//com.sk89q.worldedit.extension.platform.PlatformManager.lambda$handlePlayerInput$1(PlatformManager.java:529) ~[?:?]
	at FastAsyncWorldEdit-Paper-2.16.2-SNAPSHOT-main1419.jar//com.sk89q.worldedit.extension.platform.AbstractPlayerActor.lambda$runAction$0(AbstractPlayerActor.java:717) ~[?:?]
Stroke timer output (after)
+  0.00s  53 wool
+  0.06s  161 wool
+  0.12s  313 wool
+  0.18s  466 wool
+  0.24s  661 wool
+  0.30s  780 wool
+  0.38s  880 wool
+  0.45s  950 wool
+  0.56s  1080 wool
+  0.62s  1156 wool
+  0.67s  1242 wool
+  0.73s  1318 wool
+  0.78s  1512 wool
+  0.85s  1590 wool
+  0.90s  1767 wool
+  0.96s  1944 wool
+  1.02s  2058 wool
+  1.08s  2224 wool
+  1.13s  2432 wool
+  1.19s  2618 wool
+  1.25s  2739 wool
+  1.31s  2911 wool
+  1.37s  3060 wool
+  1.43s  3189 wool
DONE: 3189 wool in 1.43s (sample ~9 ms)

Not tested: Folia. This was also a single machine and a single plugin, so it isn't a full benchmark.

@Almost10

Almost10 commented Nov 2, 2026

Copy link
Copy Markdown
Author

@MattBDev look over this whenever you're able to.

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.

IllegalStateException: BlockCanBuildEvent may only be triggered synchronously.

3 participants