Best Practices - litjisz/Astra GitHub Wiki
This guide outlines recommended practices when developing plugins with the Astra framework, based on the actual implementation and design patterns found in the codebase.
Organize your code into modules based on functionality. Each module should have a single responsibility:
@AutoRegisterModule
public class EconomyModule extends AbstractModule {
@Override
public void enable() {
// Initialize economy functionality
getLogger().info("Economy module enabled");
}
@Override
public void disable() {
// Clean up economy resources
getLogger().info("Economy module disabled");
}
@Override
public void reload() {
// Reload economy configuration
getLogger().info("Economy module reloaded");
}
}
Always implement all lifecycle methods in your modules:
@Override
public void enable() {
// Initialize resources
// Register listeners
// Load configuration
}
@Override
public void disable() {
// Save data
// Unregister listeners
// Release resources
}
@Override
public void reload() {
// Reload configuration
// Update any runtime variables
}
Access modules and services through the Implements
system instead of static references:
// Good practice
TaskManager taskManager = Implements.fetch(TaskManager.class);
// Avoid static references
// Bad practice: MyStaticClass.getTaskManager();
In modules that extend AbstractModule
, use the provided helper methods:
// In an AbstractModule subclass
ConfigManager configManager = getConfig();
Logger logger = getLogger();
Astra plugin = getPlugin();
Extend CommandBase
for all commands and use annotations for automatic registration:
@AutoRegisterCommand(
name = "economy",
permission = "myplugin.command.economy",
aliases = {"eco", "money"}
)
public class EconomyCommand extends CommandBase {
@Override
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
// Command implementation
return true;
}
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
// Tab completion implementation
return new ArrayList<>();
}
}
Always implement tab completion for better user experience:
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
List<String> completions = new ArrayList<>();
if (args.length == 1) {
String[] subcommands = {"balance", "pay", "top"};
for (String subcommand : subcommands) {
if (subcommand.toLowerCase().startsWith(args[0].toLowerCase())) {
completions.add(subcommand);
}
}
}
return completions;
}
Use the ConfigManager
for all configuration needs:
// Load a configuration file
FileConfiguration config = configManager.loadConfig("settings");
// Get values with defaults
String serverName = configManager.getString("settings", "server.name", "Default Server");
int maxPlayers = configManager.getInt("settings", "server.max-players", 20);
// Save changes
configManager.saveConfig("settings");
Provide default configurations in your resources folder:
// The ConfigManager will automatically load defaults from your resources
// if they exist when loading a configuration
FileConfiguration config = configManager.loadConfig("settings");
Use the task system for all scheduled operations:
// Create and run a task
AsyncAstraTask task = taskManager.createAsyncTask("data-save", () -> {
// Save data to database
});
// Set priority
task.setPriority(TaskPriority.HIGH);
// Add completion callback
task.onComplete(() -> {
getLogger().info("Data saved successfully");
});
// Add error handling
task.onError(throwable -> {
getLogger().error("Failed to save data", throwable);
});
// Schedule the task
taskManager.scheduleTask(task);
For operations that depend on other operations:
// Create first task
AsyncAstraTask loadTask = taskManager.createAsyncTask("load-data", () -> {
// Load data
});
// Create dependent task
AsyncAstraTask processTask = taskManager.createAsyncTask("process-data", () -> {
// Process data
});
// Add dependency
processTask.addDependency("load-data");
// Schedule both tasks
taskManager.scheduleTask(loadTask);
taskManager.scheduleTask(processTask);
Use the provided Logger
class for all logging:
// In a module
getLogger().info("Operation completed successfully");
getLogger().warning("Something might be wrong");
getLogger().error("Operation failed", exception);
getLogger().debug("Processing item: " + item);
Choose the appropriate log level for each message:
-
info()
- General information about normal operation -
warning()
- Potential issues that don't prevent operation -
error()
- Errors that prevent specific operations -
debug()
- Detailed information for troubleshooting
Always use try-catch blocks for operations that might fail:
try {
// Operation that might fail
} catch (Exception e) {
getLogger().error("Operation failed", e);
}
Include context in error messages:
try {
// Load player data
} catch (Exception e) {
getLogger().error("Failed to load data for player: " + playerName, e);
}
Take advantage of the automatic registration system:
@AutoRegisterModule
public class MyModule extends AbstractModule {
// Module implementation
}
@AutoRegisterCommand(name = "mycommand")
public class MyCommand extends CommandBase {
// Command implementation
}
@AutoRegisterListener
public class MyListener implements Listener {
// Event handlers
}
@AutoRegisterTask(
id = "my-task",
async = true,
period = 1200
)
public class MyTask extends AsyncAstraTask {
// Task implementation
}
In your main plugin class, scan your plugin's package:
@Override
protected void onInitialize() {
// Scan the plugin's package for annotated classes
scanPackage(getClass().getPackage().getName());
}
By following these best practices, you'll create plugins that are more maintainable, robust, and aligned with the design principles of the Astra framework.