Skip to content

Restore paintings and item frames on Folia without teleporting - #1011

Merged
Intelli merged 4 commits into
PlayPro:masterfrom
tricrotism:for-upstream/folia-hanging-restore
Oct 2, 2026
Merged

Intelli merged 4 commits into
PlayPro:masterfrom
tricrotism:for-upstream/folia-hanging-restore

Conversation

@tricrotism

Copy link
Copy Markdown
Contributor

Summary

On Folia, /co rollback of a broken painting does not bring it back. The restore spawns the entity and then calls Entity#teleport, which Folia refuses with UnsupportedOperationException: Must use teleportAsync while in region threading. This change spawns paintings and item frames directly at their recorded spot on Folia, with facing and art set before they join the world.

The problem

HangingUtil.spawnHanging restores a hanging entity in two steps:

  1. Spawn it at spawnBlock with World#spawn.
  2. teleport() it to the recorded block, then call setFacingDirection and setArt (paintings, utility/entity/HangingUtil.java:136-143) or setFacingDirection and setItem (frames, :153-163).

On Folia, CraftEntity.teleport0 always throws UnsupportedOperationException("Must use teleportAsync while in region threading") (checked in the Folia 1.21.11 server jar).

  • Paintings. The teleport at HangingUtil.java:141 is outside any try, so the exception escapes spawnHanging, setFacingDirection and setArt never run, and the error is reported from the rollback's region task. In the test below, both paintings were missing after the rollback, and the console logged the exception once per painting.
  • Item frames. The teleport at :156 is inside a try whose catch is empty, so the exception is swallowed and setFacingDirection and setItem never run. In the test below the frames still came back correctly, because their spawn spot was already their recorded block, the only wall picked the right facing, and the frame's item is restored separately by the container rollback. A frame whose spawn spot is not its recorded block, or where the spawn picks a different face (floors, ceilings, corners), is left as the spawn placed it. I did not build a test for those cases.

The fix

On Folia only, spawnHanging spawns the painting or frame directly at its recorded block with World#spawn(location, class, consumer). The consumer calls setFacingDirection(face, true) and setArt(art, true) for paintings, or setFacingDirection(face, true) and setItem for frames, before the entity is added to the world. No teleport is needed.

Paper and Spigot keep the existing spawn-then-teleport path unchanged.

Behaviour change

  • Paper and Spigot: none.
  • Folia: paintings are restored at their recorded block with their art. Frames get their recorded facing and item set at spawn instead of being left as the spawn placed them.

Risk

Folia only. The consumer uses the same setFacingDirection(face, true) force flag the existing path uses.

Testing

Build: mvn package passes.

Live, HangingCheck test plugin on a fresh server with SQLite: it builds a wall, hangs a 1x1 painting (kebab), a 2x1 painting (pool), a frame holding a diamond and an empty frame, fires HangingBreakEvent with cause EXPLOSION for each and removes them, runs /co rollback u:#explosion t:10m r:#global, then compares every hanging entity's block, facing, art and item against the snapshot taken before.

Server Upstream 3af1079 This branch
Folia 1.21.11 FAIL: both paintings missing, UnsupportedOperationException at HangingUtil.java:141 twice PASS, no errors
Paper 26.2 PASS PASS

Rollback spawns a painting or item frame and then teleports it into place, but Folia always throws from Entity#teleport, so paintings are lost and frames never get their recorded facing or item. On Folia the entity is now spawned directly at its recorded block with its facing, art or item set in the pre-spawn consumer. Paper and Spigot are unchanged.
@netlify

netlify Bot commented Sep 23, 2026

Copy link
Copy Markdown

❌ Deploy Preview for coreprotect failed. Why did it fail? →

Name Link
🔨 Latest commit d5d1a4b
🔍 Latest deploy log https://app.netlify.com/projects/coreprotect/deploys/6ab3ed546dc33e0009c5b9d1

@Intelli

Intelli commented Sep 25, 2026

Copy link
Copy Markdown
Contributor

Thanks -- automated review is requesting the following change:

  • Compiling against the current API selects World#spawn(Location, Class, java.util.function.Consumer), whereas Folia 1.19.4 and 1.20.1 expose the overload with org.bukkit.util.Consumer. This causes NoSuchMethodError on those servers, which the existing catch (Exception) blocks do not catch.

Compiling against the current API binds World#spawn(Location, Class,
Consumer) to the java.util.function.Consumer overload, which only exists
from Bukkit 1.20.2. Folia 1.19.4 and 1.20.1 only have the
org.bukkit.util.Consumer overload, so restoring a painting or item frame
there threw NoSuchMethodError. The spawn now goes through
BukkitAdapter.spawn, which calls the java.util.function.Consumer overload
when World has it and otherwise invokes the org.bukkit.util.Consumer
overload by reflection.
@Intelli
Intelli merged commit 80caab5 into PlayPro:master Oct 2, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants