Skip to content

Add Folia support - #6635

Open
lewisgibson wants to merge 13 commits into
EssentialsX:2.xfrom
lewisgibson:folia-support
Open

lewisgibson wants to merge 13 commits into
EssentialsX:2.xfrom
lewisgibson:folia-support

Conversation

@lewisgibson

Copy link
Copy Markdown

Information

This PR closes #6633.

Depends on #6634, whose commit is the first one here. The diff shows it until that one is merged.

Details

Proposed feature:
Make EssentialsX run on Folia, and change nothing on Bukkit, Spigot and Paper.

Folia refuses to load a plugin that does not set folia-supported, and it does not support the Bukkit scheduler. It also splits the world into regions that tick on their own threads, so an entity, block or chunk can only be touched from the thread that owns it, and the world clock, the weather and the command map belong to a global thread.

How it works:

  • TaskSchedulerProvider is a new provider that schedules global, entity, location and asynchronous tasks and says whether the current thread owns an entity or location. BukkitTaskSchedulerProvider (BaseProviders) queues everything onto the main thread, exactly as the Bukkit scheduler calls it replaces did, and is used on every server except Folia. FoliaTaskSchedulerProvider (PaperProvider) uses Folia's region, entity, global and async schedulers and is only selected where RegionizedServer exists. Folia's classes are never loaded elsewhere and the API floor does not change.
  • executeGlobal, executeEntity and executeLocation run the task inline when the current thread already owns what it touches and otherwise hand it to the thread that does. On Bukkit that is always inline, so code written with them behaves as before.
  • Every Bukkit scheduler call in EssentialsX, EssentialsXSpawn, EssentialsXDiscord, EssentialsXDiscordLink and EssentialsXXMPP now goes through the provider. The old scheduler methods on IEssentials stay, are deprecated, and are unsupported on Folia.
  • Teleports eject passengers, check the destination and teleport from the threads that own them, without blocking one thread on another. The AFK, mute and jail checks, /time, /weather, /thunder, /remove, /nuke, /gc, the loop commands that change other players (/heal, /kill, /gamemode, /lightning and so on), kit commands, and the console commands EssentialsX dispatches run on the thread that owns what they change.
  • The teleport request queue, the reply recipient fields and the TPS history are read and written from different regions on Folia, so they are now guarded or volatile.

Each commit covers one area, and the details are in the commit messages.

Not covered: EssentialsXDiscord, DiscordLink and XMPP need accounts, so only their scheduling was changed and they were only boot tested. An error raised inside a task handed to another thread is logged by the server instead of being shown to the command sender.

Environments tested:
OS: Ubuntu 24.04 (WSL2)
Java version: Temurin 25.0.4.1

  • Most recent Paper version (26.3, git-Paper-140)
  • Folia 26.3 (a local build)
  • Spigot 1.21.8 (boot and console commands only)
  • CraftBukkit/Spigot/Paper 1.12.2
  • CraftBukkit 1.8.8

Demonstration:
./gradlew build passes, including checkstyle and 52 unit tests (35 on 2.x). The new ones cover both scheduler providers (inline on Bukkit, the right Folia scheduler and retired handling on Folia), the region guard in the safe location search, the teleport request queue under concurrent access (it fails without the change), and the loop and toggle commands changing a player on their own thread.

I also ran EssentialsX from a bot client against Folia 26.3 and Paper 26.3 (build 140), with two bots about 12000 blocks apart so that they are in different regions, and compared Paper against an unmodified 2.x build:

  • The scripted teleport run (22 checks: /tppos across regions, /msg and /r, /home, /spawn, /tpa, /tpahere, /back after death, a chicken riding the teleportee) passes on Folia, and on Paper with this change and without it.
  • Most of the 153 EssentialsX commands, as about 230 invocations from an op bot (many of them also against the bot in the other region, plus kits that run commands, jails, /essentials reload, chat and rejoining): 249 checks on Folia and on Paper with and without this change, where a check fails on any exception or "Thread failed main thread check" in the log. All pass except one check of my own that is wrong the same way on all three. The replies are the same on Paper with and without this change apart from the version string and a /skull that depends on how long an HTTP lookup takes.
  • Teleport warm-ups (teleport-delay), cancelling one by taking damage, a player disconnecting during a warm-up, unsafe destinations, force-disable-teleport-safety, jail and release (including sending the player to a jail in another region), death and respawn with respawn-listener-priority on, /back, /tpr: run on Folia and Paper. The warm-up cancel by moving check fails identically on Paper with and without this change.
  • AFK, the AFK timeout command, mute and jail timeouts, and the backup command (the timers): pass on Folia and Paper.
  • Nineteen of the 24 sign types (not Trade, Cartography, Grindstone, Loom and Smithing) and the powertools were fired as events from a test plugin, from the thread of the player: no thread errors on Folia, and the same results on Paper with and without this change.
  • Boot with all nine modules on Folia, Paper and Spigot 1.21.8, with console commands on Spigot: no new errors compared with 2.x (the Discord, GeoIP and XMPP errors about missing configuration are the same).

On Folia the same commands without this change either do not load (not marked as supporting Folia) or fail on the first Bukkit scheduler call.

lewisgibson and others added 13 commits October 1, 2026 21:45
PaperLib 1.0.6 reads the server version with a regex that only accepts a single digit major version, so on 26.x it sees version 0 and falls back to its synchronous handlers. Every PaperLib.teleportAsync, getChunkAtAsync and getBedSpawnLocationAsync call in EssentialsX then ran a blocking teleport or chunk load instead of the asynchronous Paper API.

When PaperLib cannot read the version of a server that is newer than 1.13, install an environment that selects the handlers PaperLib would have chosen for a modern Paper.

Co-authored-by: Raw2d <255907405+Raw2d@users.noreply.github.com>
Folia does not support the Bukkit scheduler and splits the world between regions, each of which ticks on its own thread. A task must run on the thread that owns the entity or location it touches, and state that no region owns runs on a global thread.

TaskSchedulerProvider schedules global, entity, location and asynchronous tasks and answers whether the current thread owns an entity or location. BukkitTaskSchedulerProvider queues everything onto the main thread exactly as the Bukkit scheduler calls it replaces did, and is used on every server but Folia. FoliaTaskSchedulerProvider uses the region, entity, global and async schedulers, and is selected only where Folia's RegionizedServer class exists. It lives in PaperProvider so that Folia's API is not loaded anywhere else.

The Bukkit scheduler methods on IEssentials are deprecated, since they cannot work on Folia. Call sites are moved over in the following commits.

Co-authored-by: Raw2d <255907405+Raw2d@users.noreply.github.com>
Replace the Bukkit scheduler calls in the plugin core with TaskSchedulerProvider, choosing for each task the thread that owns what it touches:
- Tasks that act on a player (join flow, MOTD, inventory refreshes, power tools, flight, AFK activity from chat) run on that player's entity thread.
- Timers and one-off tasks that touch no player (backups, command map updates, the economy layer hook) run on the global thread.
- Blocking work (IO, user disposal, update checks) runs asynchronously.

The once a second EssentialsTimer runs on the global thread and hands each player's AFK, mute and jail checks to that player's own thread, and commands that the console has to dispatch (kit commands, the AFK timeout commands) are dispatched from the global thread. On Bukkit, Spigot and Paper all of this runs inline on the main thread as before.

The history behind the TPS average is read by commands on other threads on Folia, so it is now accessed under a lock.

Co-authored-by: Raw2d <255907405+Raw2d@users.noreply.github.com>
On Folia the player being teleported, the chunk they are going to and the player who ran the command can all belong to different regions, and none of them may be touched from the wrong thread.

- A passenger is ejected on the teleportee's own thread, and the teleport carries on from there instead of blocking the calling thread until the eject has run. If the player leaves before it runs the teleport completes as false.
- Safety checks and the teleport itself run on the thread that owns the destination chunk once it has loaded. Without safety checks the teleport is requested through teleportAsync, as Folia has no synchronous teleport.
- The search for a safe location treats blocks owned by another region as unsafe, so it carries on in the region it is allowed to read.
- The warm-up timer ticks asynchronously and hands the teleport to the teleportee's thread. If the teleportee has left by then the timer stops itself.
- Finding a random location reads the surface on the thread that owns the chunk it picked, and the default centre of a random teleport location is worked out by the thread that owns it.

None of this changes where tasks run on Bukkit, Spigot and Paper, where every thread check passes on the main thread.

Co-authored-by: Raw2d <255907405+Raw2d@users.noreply.github.com>
/tpa and /tpahere add to the target's request queue from the requester's thread, while the target reads, expires and removes requests from their own thread and tab completion copies the keys. The queue is a plain LinkedHashMap, so on Folia those accesses can corrupt it or throw a ConcurrentModificationException.

Every method that touches the queue now holds the user's monitor, and the pending keys are returned as a copy.

Co-authored-by: Raw2d <255907405+Raw2d@users.noreply.github.com>
The sender of a message writes the reply recipient and message time of the user they are messaging, which can be on another thread on Folia. Make both fields volatile so /r on the other side sees them.

Co-authored-by: Raw2d <255907405+Raw2d@users.noreply.github.com>
…target them

Commands such as /heal, /feed, /kill, /gamemode, /lightning, /vanish, /fly and /god apply a change to every player matched by their argument, from the thread of whoever ran the command. On Folia that is not the thread that owns the player being changed, and for example killing a player drops their items from the wrong thread.

When the current thread does not own a matched player, EssentialsLoopCommand and EssentialsToggleCommand now make the change on that player's own thread, and an error it raises is shown to the sender from there. On Bukkit, Spigot and Paper every player is owned by the current thread, so the change is made inline and errors propagate as before.
The world clock and the weather belong to the global thread on Folia, and entities and blocks to the thread of the region they are in. Commands that run from a player's region cannot change either directly.

- /time, /weather, /thunder and the time and weather signs set the clock and the weather from the global thread. /time re-applies relative player times from each player's own thread.
- /remove visits the loaded chunks from the thread that owns each of them and reports the total once the last chunk is done.
- /nuke and /antioch spawn their TNT, and /spawnmob spawns its mobs, from the thread that owns where they land.
- /gc reads the tile entity count of a world from the world, since a region cannot read the chunks of other regions on Folia.

On Bukkit, Spigot and Paper all of this still runs inline on the main thread.

Co-authored-by: Raw2d <255907405+Raw2d@users.noreply.github.com>
- /beezooka and /kittycannon remove their projectile from the thread of the entity.
- /skull and /sudo hand their result to the thread of the player it applies to.
- /balancetop from a command block runs on the global thread.
- /seen runs its lookup asynchronously.
- /nyan, which used a BukkitRunnable, plays its tune from a global timer and each note from the thread of the player hearing it.

Co-authored-by: Raw2d <255907405+Raw2d@users.noreply.github.com>
Teleporting a new player to the newbie spawn, sending a returning player to the spawn of their group, and announcing and kitting a new player all run on the thread of that player.

Co-authored-by: Raw2d <255907405+Raw2d@users.noreply.github.com>
…heduler

- Discord messages, commands run from Discord and slash commands run on the global thread, and chat relayed to Discord on the thread of the player who sent it.
- The console relay and the command response buffer tick asynchronously.
- Account link status events are called from the global thread, and a player kicked for not being linked is kicked from their own thread.
- Role sync ticks asynchronously.

Co-authored-by: Raw2d <255907405+Raw2d@users.noreply.github.com>
Presence updates, relayed messages and commands received over XMPP run on the global thread.

Co-authored-by: Raw2d <255907405+Raw2d@users.noreply.github.com>
Folia only loads plugins that set folia-supported in their plugin.yml. Set it on every EssentialsX module, now that all of their scheduled work and thread-bound access goes through the task scheduler, and mention Folia as a supported server in the README.

Co-authored-by: Raw2d <255907405+Raw2d@users.noreply.github.com>
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.

EssentialsX does not load on Folia

1 participant