Common API Patterns
Reusable patterns and techniques used across Vexor Core plugins.
Singleton Pattern
Most plugins use the Singleton pattern for instance access:
public class YourPlugin extends JavaPlugin {
private static YourPlugin instance;
@Override
public void onEnable() {
instance = this;
}
public static YourPlugin getInstance() {
return instance;
}
}
// Usage
YourPlugin plugin = YourPlugin.getInstance();
Manager Classes
Plugins typically organize functionality into Manager classes:
// DoorManager, TeamManager, CrateManager pattern
public class FeatureManager {
private final YourPlugin plugin;
private final Map<String, Feature> features;
public FeatureManager(YourPlugin plugin) {
this.plugin = plugin;
this.features = new HashMap<>();
}
public void load() {
// Load data from storage
}
public void save() {
// Save data to storage
}
public Feature get(String name) {
return features.get(name);
}
public Map<String, Feature> getAll() {
return Collections.unmodifiableMap(features);
}
}
Configuration Pattern
YAML-based configuration with validation:
public class ConfigManager {
private final YourPlugin plugin;
private FileConfiguration config;
public ConfigManager(YourPlugin plugin) {
this.plugin = plugin;
loadConfig();
}
public void loadConfig() {
plugin.saveDefaultConfig();
plugin.reloadConfig();
config = plugin.getConfig();
validate();
}
private void validate() {
if (!config.contains("required.setting")) {
plugin.getLogger().warning("Missing required setting!");
config.set("required.setting", "default");
plugin.saveConfig();
}
}
public <T> T get(String path, Class<T> type, T defaultValue) {
if type == String.class) {
return type.cast(config.getString(path, (String) defaultValue));
}
// Other types...
return defaultValue;
}
}
Event Handling Pattern
Custom events for plugin integration:
// Define custom event
public class DoorOpenEvent extends Event implements Cancellable {
private static final HandlerList HANDLERS = new HandlerList();
private final AnimatedDoor door;
private final Player player;
private boolean cancelled = false;
public DoorOpenEvent(AnimatedDoor door, Player player) {
this.door = door;
this.player = player;
}
public AnimatedDoor getDoor() { return door; }
public Player getPlayer() { return player; }
@Override
public boolean isCancelled() { return cancelled; }
@Override
public void setCancelled(boolean cancel) { this.cancelled = cancel; }
@Override
public HandlerList getHandlers() { return HANDLERS; }
public static HandlerList getHandlerList() { return HANDLERS; }
}
// Call event
DoorOpenEvent event = new DoorOpenEvent(door, player);
Bukkit.getPluginManager().callEvent(event);
if (!event.isCancelled()) {
// Proceed with action
}
// Listen to event
@EventHandler
public void onDoorOpen(DoorOpenEvent event) {
Player player = event.getPlayer();
AnimatedDoor door = event.getDoor();
// Custom logic
if (shouldPreventOpening(player, door)) {
event.setCancelled(true);
}
}
Data Persistence Pattern
YAML storage with auto-save:
public class DataManager {
private final YourPlugin plugin;
private final File dataFile;
private YamlConfiguration data;
public DataManager(YourPlugin plugin, String filename) {
this.plugin = plugin;
this.dataFile = new File(plugin.getDataFolder(), filename);
load();
}
public void load() {
if (!dataFile.exists()) {
try {
dataFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
data = YamlConfiguration.loadConfiguration(dataFile);
}
public void save() {
try {
data.save(dataFile);
} catch (IOException e) {
plugin.getLogger().severe("Failed to save data: " + e.getMessage());
}
}
public void saveAsync() {
Bukkit.getScheduler().runTaskAsynchronously(plugin, this::save);
}
public YamlConfiguration getData() {
return data;
}
}
async/Sync Task Pattern
Proper task scheduling:
// Async operation (database, file I/O, web requests)
Bukkit.getScheduler().runTaskAsynchronously(plugin, () -> {
// Heavy operation here
DatabaseData result = performDatabaseQuery();
// Switch back to main thread for Bukkit API calls
Bukkit.getScheduler().runTask(plugin, () -> {
// Use result with Bukkit API
player.sendMessage("Query result: " + result);
});
});
// Delayed task
Bukkit.getScheduler().runTaskLater(plugin, () -> {
// Execute after delay
}, 20L); // 20 ticks = 1 second
// Repeating task
BukkitTask task = Bukkit.getScheduler().runTaskTimer(plugin, () -> {
// Execute repeatedly
}, 0L, 100L); // Start immediately, repeat every 5 seconds
// Cancel when needed
task.cancel();
Permission Checking Pattern
Consistent permission handling:
public class PermissionManager {
public static boolean hasPermission(Player player, String permission) {
return player.hasPermission(permission);
}
public static boolean hasAnyPermission(Player player, String... permissions) {
for (String perm : permissions) {
if (player.hasPermission(perm)) {
return true;
}
}
return false;
}
public static boolean hasAllPermissions(Player player, String... permissions) {
for (String perm : permissions) {
if (!player.hasPermission(perm)) {
return false;
}
}
return true;
}
}
// Usage
if (!PermissionManager.hasPermission(player, "plugin.admin")) {
player.sendMessage(ChatColor.RED + "No permission!");
return;
}
GUI Pattern
Inventory-based GUI:
public class ExampleGUI {
private final Inventory inventory;
private final Player player;
public ExampleGUI(Player player) {
this.player = player;
this.inventory = Bukkit.createInventory(null, 27, "Example GUI");
setupItems();
}
private void setupItems() {
// Add items to inventory
ItemStack item = new ItemStack(Material.DIAMOND);
ItemMeta meta = item.getItemMeta();
meta.setDisplayName(ChatColor.AQUA + "Click Me");
item.setItemMeta(meta);
inventory.setItem(13, item);
}
public void open() {
player.openInventory(inventory);
}
}
// Click handler
@EventHandler
public void onInventoryClick(InventoryClickEvent event) {
if (!event.getView().getTitle().equals("Example GUI")) return;
event.setCancelled(true); // Prevent item removal
Player player = (Player) event.getWhoClicked();
ItemStack clicked = event.getCurrentItem();
if (clicked == null || clicked.getType() == Material.AIR) return;
// Handle click
player.sendMessage("You clicked: " + clicked.getType());
}
Validation Pattern
Input validation and error handling:
public class Validator {
public static boolean isValidName(String name) {
return name != null &&
!name.isEmpty() &&
name.length() <= 32 &&
name.matches("[a-zA-Z0-9_]+");
}
public static boolean isValidNumber(String input, int min, int max) {
try {
int value = Integer.parseInt(input);
return value >= min && value <= max;
} catch (NumberFormatException e) {
return false;
}
}
public static Optional<Integer> parseInteger(String input) {
try {
return Optional.of(Integer.parseInt(input));
} catch (NumberFormatException e) {
return Optional.empty();
}
}
}
// Usage
if (!Validator.isValidName(doorName)) {
player.sendMessage(ChatColor.RED + "Invalid name!");
return;
}
Validator.parseInteger(args[0]).ifPresentOrElse(
value -> createDoor(player, value),
() -> player.sendMessage("Invalid number!")
);
Best Practices
- Always null-check: Verify objects exist before use
- Use async for heavy operations: Keep main thread responsive
- Validate user input: Never trust player input
- Handle exceptions: Catch and log errors appropriately
- Clean up resources: Cancel tasks, clear maps,close connections
- Use immutable collections: Prevent external modification with
Collections.unmodifiableMap()
Next: See Integration Examples for real-world implementations