Folia and regionized scheduling
What Folia changes and how to write code that runs on both Paper and Folia.
Some very large servers run on a Paper fork called Folia that uses many threads at once. On this page you will learn what Folia changes, which four schedulers replace the old one, what breaks if you ignore them, and how to write plugin code that works on both Paper and Folia.
What is Folia?
On Paper, the whole world is ticked by a single main thread, like a restaurant with one very fast chef who cooks every order. That works until the restaurant gets huge: with hundreds of players spread over a giant world, the chef cannot finish all the orders in 50 ms anymore, and the server lags.
Folia solves this by giving each part of the world its own chef. It splits the world into regions: groups of loaded chunks that are near each other. Each region is ticked on its own thread, in parallel with all the others. Two players who are far apart no longer wait for each other. The world is regionized.
Folia is made by the PaperMC team and is meant for large servers where players spread out over a big world. It has a price: because many threads change the game at the same time, a lot of plugin code that works on Paper is not safe on Folia. That is why Folia refuses to load a plugin unless the plugin says it was written for it.
The global region
Some things belong to no place in the world. Folia keeps them in one special area called the global region. Paper's own documentation lists what it is responsible for: the time of day, the weather cycle, skipping the night when players sleep, running commands from the console, and other jobs that do not belong to any specific region.
Saying "this plugin is Folia-ready"
You tell Folia that your plugin follows its rules with one line in plugin.yml:
Where this file livesfolia-demosrcmainresourcesplugin.yml
Files in src/main/resources are copied into the jar exactly as they are. Paper looks for plugin.yml at the top of the jar.
- folia-demo/
- src/main/
- java/com/example/foliademo/Package com.example.foliademo
- FoliaCheck.javaHelper class
- FoliaDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- LampCommand.javaCommand (BasicCommand)
- NoteCommand.javaCommand (BasicCommand)
- SpawnJumpCommand.javaCommand (BasicCommand)
- TrackerCommand.javaCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlyou are hereTells Paper the plugin's name, version and main class
- java/com/example/foliademo/Package com.example.foliademo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
1name: FoliaDemo2version: '1.0.0'3main: com.example.foliademo.FoliaDemoPlugin4api-version: '26.3'5folia-supported: true6description: Uses only the schedulers that work on both Paper and Folia.7permissions:8 foliademo.use:9 description: Lets a player use the demo commands.10 default: true- Says "I wrote this plugin for Folia". Folia loads the plugin only if this is
true. Paper ignores the line, and the plugin loads normally (we tested that on Paper 26.3 with no warning).
Only add this line when your plugin really follows the rules on this page. The line is a promise, not a magic switch. If you promise and break the rule, the plugin loads on Folia and then fails in confusing ways while the server runs. The plugin.yml page lists every field.
The four schedulers
On Paper you used getServer().getScheduler() for everything, because there was only one thread to schedule on. Folia has many, so the question changes. It is no longer only "when?" but also "where should this code run?" Folia answers with four schedulers. Choose by what your task touches:
| Scheduler | How you get it | Runs the task... |
|---|---|---|
| Global region scheduler | getServer().getGlobalRegionScheduler() | on the global region. For server-wide jobs such as announcements. |
| Region scheduler | getServer().getRegionScheduler() | on the region that owns a location (or a chunk). For blocks and places. |
| Entity scheduler | entity.getScheduler(), also for players | on the region where the entity currently is, and it follows the entity when it moves. For players and mobs. |
| Async scheduler | getServer().getAsyncScheduler() | on a worker thread, in no region at all. For files, databases and the web. |
They all work the same way, so once you know one, you know them all:
- Each has
run(as soon as possible),runDelayed(once, later) andrunAtFixedRate(again and again). The async one calls the firstrunNow, and takes a real time and aTimeUnitinstead of ticks. - Your code is a lambda that receives a ScheduledTask. Call
task.cancel()on it to stop a repeating task from the inside. - The methods return the
ScheduledTasktoo, so you can keep it and cancel it later. - Delays and periods must be at least 1 tick. A
0is an error. We checked on a real Paper 26.3 server, andrunDelayed(..., 0L)throwsIllegalArgumentException: Delay ticks may not be <= 0. The classic scheduler accepted 0, which is a very common thing to trip over.
The entity scheduler has one extra feature, the retired callback. An entity can disappear at any moment: a player logs out, a mob dies. If it vanishes before your delayed task is due, Paper cannot run the task, so it runs the retired callback instead (or nothing, if you pass null). The entity scheduler also returns null instead of a task when the entity is already gone, so check for that.
Example plugin: five small features, four schedulers
The demo plugin has one example for each scheduler. It uses nothing but Folia-style schedulers, so it is portable: the same jar works on Paper and on Folia.
- folia-demo/
- src/
- main/
- java/
- com/example/foliademo/
- FoliaDemoPlugin.javaRegisters the commands and starts the global announcement
- FoliaCheck.javaAsks the server whether it is Folia
- TrackerCommand.java/tracker: a repeating action bar, on the entity scheduler
- LampCommand.java/lamp: changes a block later, on the region scheduler
- SpawnJumpCommand.java/spawnjump: teleports safely with teleportAsync
- NoteCommand.java/note: reads a file on the async scheduler, then comes back
- com/example/foliademo/
- resources/
- plugin.ymlContains folia-supported: true
- java/
- main/
- src/
Global region: a server-wide message
Where this file livesfolia-demosrcmainjavacomexamplefoliademoFoliaDemoPlugin.java
The package com.example.foliademo is the folder path com/example/foliademo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- folia-demo/
- src/main/
- java/com/example/foliademo/Package com.example.foliademo
- FoliaCheck.javaHelper class
- FoliaDemoPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- LampCommand.javaCommand (BasicCommand)
- NoteCommand.javaCommand (BasicCommand)
- SpawnJumpCommand.javaCommand (BasicCommand)
- TrackerCommand.javaCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/foliademo/Package com.example.foliademo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
10@Override11public void onEnable() {12 getComponentLogger().info("Running on Folia: {}", FoliaCheck.isFolia());13 14 getServer().getGlobalRegionScheduler().runAtFixedRate(this,15 task -> getServer().sendRichMessage("<gray>Tip: try <white>/tracker</white>, <white>/lamp</white> and <white>/spawnjump</white>."),16 TEN_MINUTES_IN_TICKS, TEN_MINUTES_IN_TICKS);17 18 getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, event -> {19 event.registrar().register("tracker", "Toggles a live coordinates display",20 new TrackerCommand(this));21 event.registrar().register("lamp", "Lights the block you look at for a few seconds",22 new LampCommand(this));23 event.registrar().register("spawnjump", "Teleports you to the world spawn",24 new SpawnJumpCommand(this));25 event.registrar().register("note", "Reads your note file without freezing the server",26 new NoteCommand(this));27 });28}- A repeating task on the global region. The first argument is always your plugin, so Paper knows who owns the task.
- The lambda to run. It ignores its
taskparameter because this message repeats forever. - Initial delay and then period, in ticks: 20 ticks times 60 seconds times 10 minutes. Both must be 1 or more.
- Logs which server you run on. Handy when you test.
Entity scheduler: something that follows a player
/tracker shows your coordinates in the action bar, ten times a second, until you run it again. The repeating task belongs to the player, so it always runs in the region where that player stands, even after a long teleport:
Where this file livesfolia-demosrcmainjavacomexamplefoliademoTrackerCommand.java
The package com.example.foliademo is the folder path com/example/foliademo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- folia-demo/
- src/main/
- java/com/example/foliademo/Package com.example.foliademo
- FoliaCheck.javaHelper class
- FoliaDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- LampCommand.javaCommand (BasicCommand)
- NoteCommand.javaCommand (BasicCommand)
- SpawnJumpCommand.javaCommand (BasicCommand)
- TrackerCommand.javayou are hereCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/foliademo/Package com.example.foliademo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
26@Override27public void execute(CommandSourceStack source, String[] args) {28 if (!(source.getExecutor() instanceof Player player)) {29 source.getSender().sendRichMessage("<red>Only players can use /tracker.");30 return;31 }32 UUID playerId = player.getUniqueId();33 34 ScheduledTask existing = runningTrackers.remove(playerId);35 if (existing != null) {36 existing.cancel();37 player.sendRichMessage("<yellow>Tracker off.");38 return;39 }40 41 ScheduledTask tracker = player.getScheduler().runAtFixedRate(plugin,42 task -> showCoordinates(player, task),43 () -> runningTrackers.remove(playerId),44 1L, UPDATE_EVERY_TICKS);45 if (tracker != null) {46 runningTrackers.put(playerId, tracker);47 player.sendRichMessage("<green>Tracker on. Run /tracker again to turn it off.");48 }49}- The map remembers each player's running task. If one exists, this second use of the command turns the tracker off.
- Stops the repeating task.
- The entity scheduler. Compared with the global one there is one more argument, the retired callback.
- The work itself, in a separate method so this one stays short.
- The retired callback. If the player disappears, forget their task.
- Start after 1 tick (not 0), then repeat every 10 ticks.
- The scheduler returns
nullif the player is already gone. Do not store a missing task.
The map is a ConcurrentHashMap because on Folia, different players' tasks run on different threads and all touch it. That is the thread-safety rule from Threads, performance and lag, and it matters on Folia all the time.
Region scheduler: something that happens at a place
/lamp makes the block you look at glow for three seconds. Blocks do not move, so a place is the right way to name them: the region scheduler runs the task in whichever region owns that location.
Where this file livesfolia-demosrcmainjavacomexamplefoliademoLampCommand.java
The package com.example.foliademo is the folder path com/example/foliademo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- folia-demo/
- src/main/
- java/com/example/foliademo/Package com.example.foliademo
- FoliaCheck.javaHelper class
- FoliaDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- LampCommand.javayou are hereCommand (BasicCommand)
- NoteCommand.javaCommand (BasicCommand)
- SpawnJumpCommand.javaCommand (BasicCommand)
- TrackerCommand.javaCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/foliademo/Package com.example.foliademo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
22@Override23public void execute(CommandSourceStack source, String[] args) {24 if (!(source.getExecutor() instanceof Player player)) {25 source.getSender().sendRichMessage("<red>Only players can use /lamp.");26 return;27 }28 Block target = player.getTargetBlockExact(20);29 if (target == null) {30 player.sendRichMessage("<red>Look at a block within 20 blocks.");31 return;32 }33 34 Location location = target.getLocation();35 plugin.getServer().getRegionScheduler().runDelayed(plugin, location, lightTask -> {36 Block block = location.getBlock();37 BlockData original = block.getBlockData();38 block.setType(Material.GLOWSTONE);39 40 plugin.getServer().getRegionScheduler().runDelayed(plugin, location, restoreTask ->41 location.getBlock().setBlockData(original), THREE_SECONDS_IN_TICKS);42 }, THREE_SECONDS_IN_TICKS);43 44 player.sendRichMessage("<yellow>That block will glow in 3 seconds, for 3 seconds.");45}- Runs once, later, in the region that owns
location. Compare with the entity scheduler: a location, not an entity, tells it where. - Remembers what the block looked like, to put it back afterwards. Reading it happens inside the task, so the region is right.
- Safe here, because this code is running on the thread that owns the block.
- A second delayed task, scheduled from inside the first one. This puts the original block back after three more seconds.
Teleporting: ask, then react
Moving an entity across the world can mean moving it to another region and another thread, and the target chunk may not even be loaded. So Folia does not let you teleport in one synchronous step. teleportAsync loads the chunk in the background, moves the entity when everything is ready, and gives you a future: a promise of the result.
Where this file livesfolia-demosrcmainjavacomexamplefoliademoSpawnJumpCommand.java
The package com.example.foliademo is the folder path com/example/foliademo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- folia-demo/
- src/main/
- java/com/example/foliademo/Package com.example.foliademo
- FoliaCheck.javaHelper class
- FoliaDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- LampCommand.javaCommand (BasicCommand)
- NoteCommand.javaCommand (BasicCommand)
- SpawnJumpCommand.javayou are hereCommand (BasicCommand)
- TrackerCommand.javaCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/foliademo/Package com.example.foliademo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
17@Override18public void execute(CommandSourceStack source, String[] args) {19 if (!(source.getExecutor() instanceof Player player)) {20 source.getSender().sendRichMessage("<red>Only players can use /spawnjump.");21 return;22 }23 Location spawn = player.getWorld().getSpawnLocation();24 25 player.teleportAsync(spawn).thenAccept(success ->26 player.getScheduler().run(plugin, task -> {27 if (success) {28 player.sendRichMessage("<green>Welcome back to spawn!");29 } else {30 player.sendRichMessage("<red>The teleport did not work.");31 }32 }, null));33}- Starts the teleport and returns immediately with a
CompletableFuture <Boolean>. - Runs the code in the parentheses when the teleport is finished.
successistrueif it worked. The code may run on a different thread than the one that started the teleport. - So it does not touch the player directly. It hands the message to the player's own scheduler, which runs it in the right place.
- The retired callback is
null: if the player vanished meanwhile, nothing needs to happen.
Async scheduler: slow work, then back
This is the round trip from the previous chapter, in Folia clothes. The slow part (reading a file) runs on the async scheduler. To use the result, it hops back through the player's scheduler instead of the Bukkit one:
Where this file livesfolia-demosrcmainjavacomexamplefoliademoNoteCommand.java
The package com.example.foliademo is the folder path com/example/foliademo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- folia-demo/
- src/main/
- java/com/example/foliademo/Package com.example.foliademo
- FoliaCheck.javaHelper class
- FoliaDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- LampCommand.javaCommand (BasicCommand)
- NoteCommand.javayou are hereCommand (BasicCommand)
- SpawnJumpCommand.javaCommand (BasicCommand)
- TrackerCommand.javaCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/foliademo/Package com.example.foliademo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
20@Override21public void execute(CommandSourceStack source, String[] args) {22 if (!(source.getExecutor() instanceof Player player)) {23 source.getSender().sendRichMessage("<red>Only players can use /note.");24 return;25 }26 Path noteFile = plugin.getDataPath().resolve("note.txt");27 28 plugin.getServer().getAsyncScheduler().runNow(plugin, asyncTask -> {29 String note = readNote(noteFile);30 31 player.getScheduler().run(plugin, backTask ->32 player.sendRichMessage("<gray>Note: <white><text>", Placeholder.unparsed("text", note)), null);33 });34}- The plugin's own data folder, as a
Path. The file would beplugins/FoliaDemo/note.txt. - Starts the worker task right away.
- The slow part. It touches no game objects, so a worker thread is fine.
- Comes back to the player's region. Only now does the code use the
Player.
Knowing whether you are on Folia
With the Folia-style schedulers you rarely need to ask, because they work on both. If you do need to know, ask the server for its brand:
Where this file livesfolia-demosrcmainjavacomexamplefoliademoFoliaCheck.java
The package com.example.foliademo is the folder path com/example/foliademo inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- folia-demo/
- src/main/
- java/com/example/foliademo/Package com.example.foliademo
- FoliaCheck.javayou are hereHelper class
- FoliaDemoPlugin.javaMain class: Paper starts here (named as main in plugin.yml)
- LampCommand.javaCommand (BasicCommand)
- NoteCommand.javaCommand (BasicCommand)
- SpawnJumpCommand.javaCommand (BasicCommand)
- TrackerCommand.javaCommand (BasicCommand)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/foliademo/Package com.example.foliademo
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
13static boolean isFolia() {14 return ServerBuildInfo.buildInfo().isBrandCompatible(FOLIA_BRAND);15}- Information about the server software that is running.
trueon Folia (papermc:folia) andfalseon Paper. On our Paper 26.3 test server the plugin loggedRunning on Folia: false.
What breaks on Folia
Code that works on Paper can fail on Folia, usually because it assumes one thread owns everything. These are the usual culprits.
1. The old BukkitScheduler
Folia does not support the classic scheduler, because it has no single main thread to run its tasks on. Code like this, which you will see in almost every older tutorial, fails on Folia:
1Bukkit.getScheduler().runTaskTimer(plugin, () -> {2 plugin.getLogger().info("Still running");3}, 0L, 20L);The replacement depends on what the task touches. A message with no place goes to the global scheduler, a player to player.getScheduler(), a block to the region scheduler, and slow work to the async scheduler. Note the start delay: 0L must become 1L.
| Old way (Paper only) | Folia-style replacement |
|---|---|
runTask | run on the scheduler that fits the task |
runTaskLater | runDelayed (delay of at least 1) |
runTaskTimer | runAtFixedRate (delay and period of at least 1) |
runTaskAsynchronously | getAsyncScheduler().runNow |
BukkitRunnable | A lambda that receives a ScheduledTask |
entity.teleport(location) | entity.teleportAsync(location) |
2. Touching things in another region
On Paper, any code on the main thread may read or change any block or entity anywhere. On Folia, a thread may only touch what its own region owns. A handler for a player's event runs in that player's region, so it may touch that player and the nearby world, but not a block 3,000 blocks away and not another player who is somewhere else.
To act on something elsewhere, schedule a task for it: the entity scheduler for an entity, the region scheduler for a location. To ask whether you are allowed to touch something right now, getServer().isOwnedByCurrentRegion(location) or isOwnedByCurrentRegion(entity) returns true if your current thread owns it. When you break the rule, Folia refuses with an error much like the "Asynchronous ..." error from the previous chapter.
3. Loops over every player
A very common Paper pattern is "every few seconds, loop through getOnlinePlayers() and change each one". On Folia those players live in different regions, and one thread cannot change them all directly. The pattern becomes two levels: a global task that loops, and a hop to each player's scheduler for the actual change. The exercise below does exactly this.
4. Shared data
Because many regions run at once, any list or map your plugin shares between them is touched by several threads at the same moment. Use thread-safe containers such as ConcurrentHashMap, as TrackerCommand does. A plain HashMap that was fine on Paper can corrupt itself on Folia.
5. Other plugins
Your plugin can be perfect and still fail if a plugin it depends on has no folia-supported: true. Folia refuses to load such a plugin at all. Check every dependency, and read the Folia documentation for the current list of rules and for what is unsupported.
Mistakes to avoid
- Adding
folia-supported: truewithout checking. The line is a promise. Test on a real Folia server first. - A delay or period of 0. The new schedulers throw an error. Use 1 or more.
- Using the region scheduler for entities. It does not follow the entity. Use
entity.getScheduler(). - Touching a
Playerfrom the global task or an async task. Hop toplayer.getScheduler()first. - Forgetting the
nullcheck on the task returned by the entity scheduler. - Plain
HashMaps shared between regions. UseConcurrentHashMap.
Make a feeding task Folia-safe
This plugin refills every player's hunger bar every five seconds. It works on Paper, but it would fail on Folia:
1getServer().getScheduler().runTaskTimer(this, () -> {2 for (Player player : getServer().getOnlinePlayers()) {3 player.setFoodLevel(20);4 }5}, 0L, 100L);Rewrite it with the Folia-style schedulers. Remember two things from this page: where the loop belongs, and where the change belongs.
Hint 1
Hint 2
player.getScheduler().run(plugin, task -> ..., null).Hint 3
0L. The new schedulers do not allow it.Show the solution
A global task loops over the players, and each player's own scheduler makes the change in the right region. The first delay becomes 100 ticks, so there is no zero.
Where this file livesfolia-exercisesrcmainjavacomexamplefoliaexerciseFeedPlugin.java
The package com.example.foliaexercise is the folder path com/example/foliaexercise inside src/main/java: every dot in the package name is one folder. IntelliJ creates these folders for you when you make a new package.
- folia-exercise/
- src/main/
- java/com/example/foliaexercise/Package com.example.foliaexercise
- FeedPlugin.javayou are hereMain class: Paper starts here (named as main in plugin.yml)
- resources/Files copied into the jar as they are
- plugin.ymlTells Paper the plugin's name, version and main class
- java/com/example/foliaexercise/Package com.example.foliaexercise
- build.gradle.ktsThe build recipe: Paper 26.3 API, Java 25, how the jar is made
- gradle.propertiesVersion numbers used by the build
- gradlew.batRuns Gradle on Windows without installing it
- settings.gradle.ktsThe project's name
- src/main/
Gray files come with the project template; you rarely edit them. Open the whole project in the Compile Lab, or download it from the project page.
11@Override12public void onEnable() {13 getServer().getGlobalRegionScheduler().runAtFixedRate(this, globalTask -> {14 for (Player player : getServer().getOnlinePlayers()) {15 player.getScheduler().run(this, playerTask -> player.setFoodLevel(FULL_FOOD_LEVEL), null);16 }17 }, FIVE_SECONDS_IN_TICKS, FIVE_SECONDS_IN_TICKS);18}- The loop runs on the global region, because it is not about one place.
- The actual change happens in the player's region, wherever that player is.
- No retired callback is needed. If the player is gone, there is no hunger bar to fill.
- Initial delay and period are both 100 ticks, never 0.
Recap
- Folia splits the world into regions that tick in parallel on different threads. Server-wide work lives in the global region.
- Folia only loads plugins with
folia-supported: true. Paper ignores the line. Only add it after you tested on Folia. - Four schedulers answer "where does this code run": global for the server, region for a location, entity for a player or mob, async for slow work. All four work on Paper too.
- Delays and periods are at least 1 tick. The entity scheduler has a retired callback and may return
null. - The old
BukkitScheduler, synchronous teleports, loops over all players and shared plain maps are what breaks. - Use
teleportAsync, hop to the right scheduler before touching things, and use thread-safe containers.
Quick quiz
You want a message to appear above one player's hotbar five seconds from now. Which scheduler fits?
The task touches one player, so it belongs on the entity scheduler, which runs it where that player is. The global one is for server-wide jobs, and the async one must never touch game objects.What happens with
getServer().getGlobalRegionScheduler().runDelayed(plugin, task, 0L)?The Folia-style schedulers do not accept a delay of 0 ticks. Userunfor "as soon as possible", or a delay of 1.Why should you not use the region scheduler to run a task for a player?
Places stay where they are, entities move. For anything that is about an entity, use the scheduler that goes with it.What does
folia-supported: truedo on a normal Paper server?The line only matters to Folia, which loads plugins only if it is present. On Paper the plugin loads as usual.A task running on the global region needs to change every player's hunger. How?
On Folia, players are in different regions, so one thread cannot change them all directly. The classic scheduler does not exist there.
Next steps
- Review the basics in Scheduling tasks and Threads, performance and lag.
- Read the official Folia documentation before you target a real Folia server.
- Find bugs in your own tasks with Debugging.