Skip to main content

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

  1. Always null-check: Verify objects exist before use
  2. Use async for heavy operations: Keep main thread responsive
  3. Validate user input: Never trust player input
  4. Handle exceptions: Catch and log errors appropriately
  5. Clean up resources: Cancel tasks, clear maps,close connections
  6. Use immutable collections: Prevent external modification with Collections.unmodifiableMap()

Next: See Integration Examples for real-world implementations