Best Practices - litjisz/Astra GitHub Wiki

Best Practices

This guide outlines recommended practices when developing plugins with the Astra framework, based on the actual implementation and design patterns found in the codebase.

Module Organization

Use the Module System

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");
    }
}

Implement Proper Lifecycle Methods

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
}

Dependency Management

Use the Implements System

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();

Access Services in AbstractModule

In modules that extend AbstractModule, use the provided helper methods:

// In an AbstractModule subclass
ConfigManager configManager = getConfig();
Logger logger = getLogger();
Astra plugin = getPlugin();

Command Implementation

Use the Command Framework

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<>();
    }
}

Implement Tab Completion

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;
}

Configuration Management

Use the ConfigManager

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");

Create Default Configurations

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");

Task Management

Use the Task System

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);

Use Task Dependencies

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);

Logging

Use the Logger

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);

Use Appropriate Log Levels

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

Error Handling

Use Try-Catch Blocks

Always use try-catch blocks for operations that might fail:

try {
    // Operation that might fail
} catch (Exception e) {
    getLogger().error("Operation failed", e);
}

Provide Meaningful Error Messages

Include context in error messages:

try {
    // Load player data
} catch (Exception e) {
    getLogger().error("Failed to load data for player: " + playerName, e);
}

Class Registration

Use Auto-Registration

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
}

Scan Your Plugin's Package

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.

⚠️ **GitHub.com Fallback** ⚠️