Prepare a small file and command
Use the Paper 26.3 and Java 25 project. Replace the main class with the code below; command is registered in onEnable, there is no need to add commands to manifest. After the first run, create plugins/TrailNotes/note.txt in UTF-8, for example with the line “Today we are building a bridge.”
Check the scheme of synchronous and background tasks in Paper Scheduling. The background part of this experience only reads Path and bytes. It does not access world or the saved Player object.
Capture the UUID, read the bytes and check the request number
The full class replaces TrailNotesPlugin.java. The requests map is used only in the main thread: before starting work, upon exit, and when processing the result. Reading 4097 bytes allows you to reject a file larger than 4 KiB without loading the entire content into memory.
package ru.example.trailnotes;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerQuitEvent;
import org.bukkit.plugin.java.JavaPlugin;
public final class TrailNotesPlugin extends JavaPlugin implements Listener {
private final Map<UUID, Long> requests = new HashMap<>();
private long sequence;
@Override public void onEnable() {
getDataFolder().mkdirs();
registerCommand("note", (source, args) -> readNote(source.getSender(), args));
getServer().getPluginManager().registerEvents(this, this);
}
@EventHandler public void onQuit(PlayerQuitEvent e) {
requests.remove(e.getPlayer().getUniqueId());
}
private void readNote(CommandSender sender, String[] args) {
if (!(sender instanceof Player player)) {
sender.sendMessage("Нужен игрок.");
return;
}
UUID uuid = player.getUniqueId();
long request = ++sequence;
requests.put(uuid, request);
Path path = getDataFolder().toPath().resolve("note.txt");
getServer().getScheduler().runTaskAsynchronously(this, () -> {
String message;
try {
byte[] bytes;
try (InputStream input = Files.newInputStream(path)) {
bytes = input.readNBytes(4097);
}
if (bytes.length > 4096) throw new java.io.IOException("Слишком большой файл");
message = new String(bytes, StandardCharsets.UTF_8);
} catch (Exception error) {
message = "Не удалось прочитать note.txt";
}
String result = message;
if (!isEnabled()) return;
try {
getServer().getScheduler().runTask(this, () -> {
if (!Long.valueOf(request).equals(requests.get(uuid))) return;
Player current = getServer().getPlayer(uuid);
requests.remove(uuid);
if (current != null) current.sendMessage(result);
});
} catch (org.bukkit.plugin.IllegalPluginAccessException ignored) {
// Плагин отключился до постановки результата.
}
});
return;
}
@Override public void onDisable() {
requests.clear();
getServer().getScheduler().cancelTasks(this);
}
}Check file boundaries and stale responses
Build the JAR and install it on a test server. Execute /note: expected line from file. The deleted file should give an understandable failure, and not disable the plugin. To generate boundary files on the test server machine, use Python:
- Two quick /note: the response of the last request number will be applied.
- Exit and new entry: The old response should not come to the new session.
- Disabling the test server during reading: the game callback is not executed after disable.
from pathlib import Path
p = Path('plugins/TrailNotes/note.txt')
p.write_bytes(b'a' * 4096) # /note: читается
# Затем замените 4096 на 4097: /note сообщает об отказе.Limit the number of tasks before publishing
Discarding a stale response does not cancel a read already in progress. For a public command, allow one active request per UUID and impose a total queue limit. HTTP needs a separate timeout and response-size limit; SQL needs a bounded connection pool.
Don't wait for the result via join or get on the game thread: this will stop the tick again. The time in tick is also not equal to wall seconds under load. The example applies to regular Paper; porting to Folia requires different planning.
The request number is also useful for screens that the player has already changed. Link a response to an open menu.
Background data parsing should not change operating settings piecemeal. Replace the entire tested model.
