Paper Plugin Guide
File mode0

Going further

Folia and regionized scheduling

What Folia changes and how to write code that runs on both Paper and Folia.

Advanced28 min read

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.

Paper has one main thread, Folia has many regions On Paper one main thread ticks the whole world. On Folia the world is split into regions of nearby chunks and each region is ticked by its own thread, so far apart areas never wait for each other. Paper: one main thread Folia: regions in parallel Main thread Region 1 Region 2 Region 3 Everything waits in one line Each region has its own thread Each small square is a chunk: a 16 by 16 column of blocks. What changes for a plugin on Folia No single main thread: use the region schedulers instead of BukkitScheduler. getGlobalRegionScheduler() getRegionScheduler() entity.getScheduler() Folia only loads plugins that say folia-supported: true in plugin.yml.
On Paper everything waits in one line. On Folia, areas far from each other tick on their own threads.

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:

plugin.ymlCompiles on Paper 26.3Compile Lab
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
    • 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

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
  1. 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:

Which scheduler for which job Pick the scheduler by what your task touches. A player or other entity uses the entity scheduler. A block or location uses the region scheduler. Server-wide work uses the global region scheduler. Slow work that touches no game state uses the async scheduler. What does your task touch? A player or another entity health, inventory, messages entity.getScheduler() follows the entity wherever it goes A block or a spot in the world set a block, read a chest getRegionScheduler() runs in the region that owns the spot The whole server announcements, weather, time getGlobalRegionScheduler() one place for server-wide jobs Nothing in the game files, databases, web getAsyncScheduler() a worker thread, then hop back All four work on Paper and on Folia.
Pick the scheduler by what the task touches. All four exist on Paper too.
SchedulerHow you get itRuns the task...
Global region schedulergetServer().getGlobalRegionScheduler()on the global region. For server-wide jobs such as announcements.
Region schedulergetServer().getRegionScheduler()on the region that owns a location (or a chunk). For blocks and places.
Entity schedulerentity.getScheduler(), also for playerson the region where the entity currently is, and it follows the entity when it moves. For players and mobs.
Async schedulergetServer().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) and runAtFixedRate (again and again). The async one calls the first runNow, and takes a real time and a TimeUnit instead 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 ScheduledTask too, so you can keep it and cancel it later.
  • Delays and periods must be at least 1 tick. A 0 is an error. We checked on a real Paper 26.3 server, and runDelayed(..., 0L) throws IllegalArgumentException: 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
        • resources/
          • plugin.ymlContains folia-supported: true

Global region: a server-wide message

FoliaDemoPlugin.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. A repeating task on the global region. The first argument is always your plugin, so Paper knows who owns the task.
  2. The lambda to run. It ignores its task parameter because this message repeats forever.
  3. Initial delay and then period, in ticks: 20 ticks times 60 seconds times 10 minutes. Both must be 1 or more.
  4. 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:

TrackerCommand.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. The map remembers each player's running task. If one exists, this second use of the command turns the tracker off.
  2. Stops the repeating task.
  3. The entity scheduler. Compared with the global one there is one more argument, the retired callback.
  4. The work itself, in a separate method so this one stays short.
  5. The retired callback. If the player disappears, forget their task.
  6. Start after 1 tick (not 0), then repeat every 10 ticks.
  7. The scheduler returns null if 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.

LampCommand.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Runs once, later, in the region that owns location. Compare with the entity scheduler: a location, not an entity, tells it where.
  2. Remembers what the block looked like, to put it back afterwards. Reading it happens inside the task, so the region is right.
  3. Safe here, because this code is running on the thread that owns the block.
  4. 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.

SpawnJumpCommand.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Starts the teleport and returns immediately with a CompletableFuture<Boolean>.
  2. Runs the code in the parentheses when the teleport is finished. success is true if it worked. The code may run on a different thread than the one that started the teleport.
  3. 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.
  4. 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:

NoteCommand.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. The plugin's own data folder, as a Path. The file would be plugins/FoliaDemo/note.txt.
  2. Starts the worker task right away.
  3. The slow part. It touches no game objects, so a worker thread is fine.
  4. 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:

FoliaCheck.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. Information about the server software that is running.
  2. true on Folia (papermc:folia) and false on Paper. On our Paper 26.3 test server the plugin logged Running 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:

JavaOld way
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
runTaskrun on the scheduler that fits the task
runTaskLaterrunDelayed (delay of at least 1)
runTaskTimerrunAtFixedRate (delay and period of at least 1)
runTaskAsynchronouslygetAsyncScheduler().runNow
BukkitRunnableA 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: true without 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 Player from the global task or an async task. Hop to player.getScheduler() first.
  • Forgetting the null check on the task returned by the entity scheduler.
  • Plain HashMaps shared between regions. Use ConcurrentHashMap.
Try it

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:

JavaOld way
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
Which scheduler should run a loop over all players? Which scheduler should change one player?
Hint 2
The change to one player goes through player.getScheduler().run(plugin, task -> ..., null).
Hint 3
The delay in the old code was 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.

FeedPlugin.javaCompiles on Paper 26.3Compile Lab
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
    • 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

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}
  1. The loop runs on the global region, because it is not about one place.
  2. The actual change happens in the player's region, wherever that player is.
  3. No retired callback is needed. If the player is gone, there is no hunger bar to fill.
  4. 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

  1. You want a message to appear above one player's hotbar five seconds from now. Which scheduler fits?

  2. What happens with getServer().getGlobalRegionScheduler().runDelayed(plugin, task, 0L)?

  3. Why should you not use the region scheduler to run a task for a player?

  4. What does folia-supported: true do on a normal Paper server?

  5. A task running on the global region needs to change every player's hunger. How?

Next steps