Foundation provides comprehensive debugging tools to help you identify and solve issues during development.
Debug mode
Enabling debug mode
Create a debug.lock file in your plugin’s data folder to enable debug features:
touch plugins/YourPlugin/debug.lock
Foundation automatically detects this file on startup:
// Called automatically by SimplePlugin
Debugger.detectDebugMode();
if (Debugger.isDebugModeEnabled()) {
// Debug features are active
}
Debug mode is detected at plugin startup. You’ll need to restart the server after creating or removing the debug.lock file.
Debug sections
Configure which parts of your plugin to debug in settings.yml:
Debug:
- "database"
- "events"
- "commands"
# Use "*" to debug everything
# - "*"
Then use Debugger.debug() in your code:
import org.mineacademy.fo.debug.Debugger;
public void processCommand(Player player, String command) {
Debugger.debug("commands", "Player " + player.getName() +
" executed: " + command);
// Your command logic
}
public void saveToDatabase(UUID playerId, PlayerData data) {
Debugger.debug("database", "Saving data for player: " + playerId);
Debugger.debug("database", "Data: " + data.toString());
// Database save logic
}
Output appears in console with the section tag:
[commands] Player Notch executed: /spawn
[database] Saving data for player: 069a79f4-44e9-4726-a5be-fca90e38aaf5
[database] Data: PlayerData{level=50, coins=1000}
Queued debug messages
Building multi-line debug output
Use Debugger.put() and Debugger.push() to build complex debug messages:
public void processTransaction(Player player, Transaction transaction) {
if (!Debugger.isDebugged("transactions")) {
return;
}
Debugger.put("transactions", "=== Transaction Start ===");
Debugger.put("transactions", "Player: " + player.getName());
Debugger.put("transactions", "Type: " + transaction.getType());
Debugger.put("transactions", "Amount: " + transaction.getAmount());
Debugger.put("transactions", "Balance Before: " + getBalance(player));
processTransactionInternal(transaction);
Debugger.push("transactions", "Balance After: " + getBalance(player));
// push() prints all queued messages and clears the queue
}
Output:
[transactions] === Transaction Start ===
[transactions] Player: Notch
[transactions] Type: PURCHASE
[transactions] Amount: 500
[transactions] Balance Before: 1000
[transactions] Balance After: 500
Check Debugger.isDebugged() before building expensive debug messages to avoid performance impact in production.
Error logging
Saving errors to file
Debugger.saveError() logs errors to errors.log with full context:
import org.mineacademy.fo.debug.Debugger;
try {
riskyOperation();
} catch (Exception ex) {
Debugger.saveError(ex,
"Failed to process player data",
"Player: " + player.getName(),
"UUID: " + player.getUniqueId(),
"Online: " + player.isOnline()
);
}
The error file includes:
- Timestamp
- Server information (Minecraft version, Java version)
- Plugin list
- Your custom messages
- Full stack trace
- Caused by chain
Example errors.log entry:
------------------------------------[ 2026-03-03 14:30:45 ]-----------------------------------
MyPlugin 1.0.0 encountered a NullPointerException
Running Paper 1.20.4 and Java 17.0.1
Plugins: Vault, WorldEdit, MyPlugin, CoreProtect
----------------------------------------------------------------------------------------------
More Information:
Failed to process player data
Player: Notch
UUID: 069a79f4-44e9-4726-a5be-fca90e38aaf5
Online: true
NullPointerException: Cannot invoke method getName() on null object
at com.example.MyPlugin.getData(MyPlugin.java:123)
at com.example.MyPlugin.onJoin(MyPlugin.java:89)
----------------------------------------------------------------------------------------------
Stack trace utilities
Printing clean stack traces
Debugger.printStackTrace() prints stack traces without Minecraft/Bukkit internals:
Debugger.printStackTrace("Checking call stack");
Output:
!----------------------------------------------------------------------------------------------------------!
Checking call stack
!----------------------------------------------------------------------------------------------------------!
at com.example.MyPlugin.handleCommand(MyPlugin.java:156)
at com.example.CommandHandler.execute(CommandHandler.java:45)
at com.example.MyPlugin.onCommand(MyPlugin.java:89)
--------------------------------------------------------------------------------------------------------end-
Tracing execution route
Debugger.traceRoute() shows the call chain:
List<String> route = Debugger.traceRoute(true); // true = include line numbers
System.out.println("Call route: " + String.join(" > ", route));
Output:
Call route: MyPlugin#handleReward(167) > RewardManager#giveReward(89) > Database#saveReward(234)
Stack traces automatically filter out Minecraft server internals, showing only relevant plugin code.
Printing values
Debug array contents
String[] permissions = {"myplugin.admin", "myplugin.mod", "myplugin.user"};
Debugger.printValues(permissions);
Output:
--------------------------------------------------------------------------------
Enumeration of 3 strings
[0] myplugin.admin
[1] myplugin.mod
[2] myplugin.user
Custom debug output
public void debugPlayerState(Player player) {
if (!Debugger.isDebugged("player")) {
return;
}
Debugger.put("player", "=== Player State ===");
Debugger.put("player", "Name: " + player.getName());
Debugger.put("player", "Health: " + player.getHealth() + "/" + player.getMaxHealth());
Debugger.put("player", "Location: " + formatLocation(player.getLocation()));
Debugger.put("player", "GameMode: " + player.getGameMode());
ItemStack[] items = player.getInventory().getContents();
Debugger.put("player", "Inventory: " + items.length + " slots");
for (int i = 0; i < items.length; i++) {
if (items[i] != null && items[i].getType() != Material.AIR) {
Debugger.put("player", " [" + i + "] " + items[i].getType() + " x" + items[i].getAmount());
}
}
Debugger.push("player", "====================");
}
Real-world debugging patterns
Database debugging
public class DatabaseManager {
private static final String DEBUG_SECTION = "database";
public PlayerData loadData(UUID playerId) {
Debugger.debug(DEBUG_SECTION, "Loading data for " + playerId);
try {
PlayerData data = executeQuery(playerId);
if (data == null) {
Debugger.debug(DEBUG_SECTION, "No data found, creating new");
data = createNewData(playerId);
} else {
Debugger.debug(DEBUG_SECTION, "Loaded: " + data.toString());
}
return data;
} catch (SQLException ex) {
Debugger.saveError(ex,
"Failed to load player data",
"Player ID: " + playerId
);
return null;
}
}
public void saveData(UUID playerId, PlayerData data) {
if (Debugger.isDebugged(DEBUG_SECTION)) {
Debugger.put(DEBUG_SECTION, "Saving player data");
Debugger.put(DEBUG_SECTION, "Player: " + playerId);
Debugger.put(DEBUG_SECTION, "Data: " + data.toJson());
}
try {
executeSave(playerId, data);
Debugger.push(DEBUG_SECTION, "Save successful");
} catch (SQLException ex) {
Debugger.saveError(ex,
"Failed to save player data",
"Player ID: " + playerId,
"Data: " + data.toString()
);
}
}
}
Event debugging
@EventHandler
public void onPlayerJoin(PlayerJoinEvent event) {
Player player = event.getPlayer();
Debugger.debug("events", "Player join: " + player.getName());
try {
loadPlayerData(player);
applyPermissions(player);
sendWelcomeMessage(player);
Debugger.debug("events", "Join processing complete for " + player.getName());
} catch (Exception ex) {
Debugger.saveError(ex,
"Error processing player join",
"Player: " + player.getName(),
"First join: " + !player.hasPlayedBefore()
);
player.kickPlayer("An error occurred. Please try again.");
}
}
Command debugging
public class RewardCommand extends SimpleCommand {
public RewardCommand() {
super("reward|r");
}
@Override
protected void onCommand() {
if (Debugger.isDebugged("commands")) {
List<String> route = Debugger.traceRoute(true);
Debugger.debug("commands", "Command route: " + String.join(" > ", route));
Debugger.debug("commands", "Sender: " + sender.getName());
Debugger.debug("commands", "Args: " + Arrays.toString(args));
}
checkConsole();
Player player = getPlayer();
String rewardType = args.length > 0 ? args[0] : "daily";
Debugger.debug("commands", "Processing " + rewardType + " reward for " + player.getName());
try {
giveReward(player, rewardType);
} catch (Exception ex) {
Debugger.saveError(ex,
"Failed to give reward",
"Player: " + player.getName(),
"Reward type: " + rewardType
);
tellError("Failed to give reward. Please contact an administrator.");
}
}
}
Combine debugging with performance monitoring:
import org.mineacademy.fo.debug.LagCatcher;
import org.mineacademy.fo.debug.Debugger;
public void processLargeOperation(List<Player> players) {
LagCatcher.start("process-players");
Debugger.debug("performance", "Processing " + players.size() + " players");
for (Player player : players) {
processPlayer(player);
}
LagCatcher.end("process-players");
Debugger.debug("performance", "Processing complete");
}