Markdown Converter
Agent skill for markdown-converter
These guidelines outline the coding standards, patterns, and best practices for developing RVNKCore and downstream plugins that utilize the RVNKCore API. Following these standards ensures consistency throughout the codebase and a seamless integration experience.
Sign in to like and favorite skills
These guidelines outline the coding standards, patterns, and best practices for developing RVNKCore and downstream plugins that utilize the RVNKCore API. Following these standards ensures consistency throughout the codebase and a seamless integration experience.
org.fourz.rvnkcore/ ├── api/ # Public API interfaces │ ├── service/ # Service interfaces │ ├── model/ # Data transfer objects │ ├── event/ # Event interfaces │ └── exception/ # API exceptions ├── database/ # Database implementation │ ├── connection/ # Connection management │ ├── query/ # Query building │ ├── repository/ # Data repositories │ └── schema/ # Schema management ├── service/ # Service implementations │ ├── registry/ # Service registry │ ├── player/ # Player services │ ├── config/ # Configuration services │ └── ... # Other services ├── util/ # Utility classes │ ├── log/ # Logging framework │ ├── concurrent/ # Concurrency utilities │ └── ... # Other utilities └── RVNKCore.java # Main plugin class
IPlayerService)PlayerService)PlayerDTO)PlayerRepository)AnnouncementService)ConfigurationManager)ConnectionFactory)DatabaseException)All public APIs must have comprehensive JavaDoc, including:
/** * Manages player data and metadata across plugins. * Provides a unified way to access player information regardless of the data source. * <p> * This service handles caching, persistence, and synchronization of player data. */ public interface IPlayerService { /** * Retrieves a player by UUID. * <p> * This method will first check the cache before querying the database. * If the player is online, the most current data is always returned. * * @param id The UUID of the player to retrieve * @return A CompletableFuture containing the player data, or empty if not found * @throws IllegalArgumentException If id is null */ CompletableFuture<Optional<PlayerDTO>> getPlayer(UUID id); // More methods... }
All packages must include
package-info.java with a description of the package purpose:
/** * Provides service interfaces for the RVNKCore API. * <p> * These interfaces define the contract between RVNKCore and client plugins. * All service implementations adhere to these interfaces. */ package org.fourz.rvnkcore.api.service;
- RVNKException (base exception for all RVNKCore exceptions) |- ServiceException (base for service-related exceptions) |- ServiceNotFoundException |- ServiceInitializationException |- DatabaseException (base for database-related exceptions) |- ConnectionException |- QueryException |- MigrationException |- ConfigException (base for configuration-related exceptions) |- ValidationException |- MigrationException |- APIException (base for API-related exceptions) |- VersionMismatchException |- PermissionDeniedException
public CompletableFuture<PlayerDTO> getPlayerById(UUID id) { Objects.requireNonNull(id, "Player ID cannot be null"); return CompletableFuture.supplyAsync(() -> { try (Connection conn = connectionProvider.getConnection()) { // Query logic return playerDTO; } catch (SQLException e) { logger.error("Failed to retrieve player with ID: " + id, e); throw new DatabaseException("Could not retrieve player data", e); } catch (Exception e) { logger.error("Unexpected error retrieving player: " + id, e); throw new ServiceException("Unexpected error in player service", e); } }); }
All potentially blocking operations (database, I/O, network) must use asynchronous programming:
public CompletableFuture<List<AnnouncementDTO>> getActiveAnnouncements() { return CompletableFuture.supplyAsync(() -> { // Database query logic return announcements; }).exceptionally(ex -> { logger.error("Failed to retrieve active announcements", ex); return Collections.emptyList(); }); }
RVNKCore uses a lightweight service locator pattern through the ServiceRegistry:
public class AnnouncementService implements IAnnouncementService { private final ConnectionProvider connectionProvider; private final AnnouncementRepository repository; private final LogManager logger; public AnnouncementService(ServiceRegistry registry) { this.connectionProvider = registry.getService(ConnectionProvider.class); this.repository = registry.getService(AnnouncementRepository.class); this.logger = LogManager.getInstance(RVNKCore.getInstance(), getClass()); } // Implementation }
The LogManager class is the required centralized logging system for all components:
private final LogManager logger; in classes.this.logger = LogManager.getInstance(plugin, getClass()); in constructors.logger.info(), logger.warning(), logger.error(), logger.debug().logger.error("Message", exception).System.out.println(), Bukkit logger, or Java logger directly.public class ExampleService implements IExampleService { private final LogManager logger; public ExampleService(Plugin plugin) { this.logger = LogManager.getInstance(plugin, getClass()); } public void performAction() { try { // Operation code logger.info("Action completed successfully"); } catch (Exception e) { logger.error("Failed to perform action", e); throw new ServiceException("Action failed", e); } } }
For performance-critical code sections, use the DebugLogger implementation:
timeSection() to profile code sections automatically.public class PerformanceCriticalService { private final DebugLogger logger; public PerformanceCriticalService(Plugin plugin) { this.logger = new DebugLogger(plugin, getClass()); } public Result processData(Data data) { try (AutoCloseable timer = logger.timeSection("processData")) { // Processing code return result; } catch (Exception e) { logger.error("Data processing failed", e); throw new ServiceException("Processing failed", e); } } public Map<String, Long> getPerformanceMetrics() { return logger.getPerformanceMetrics(); } }
ChatFormat.stripColors() when logging player-visible text// Good logging examples logger.info("Loaded 42 announcements from database"); logger.warning("Failed to load player preferences, using defaults"); logger.error("Database connection failed", exception); logger.debug("Processing player login: " + player.getName() + " with permissions: " + permissions);
RVNKCore commands should be simple and focused. Follow these guidelines for command implementation:
CommandExecutor and TabCompleter interfaces directlyExample command implementation:
public class CoreCommand implements CommandExecutor, TabCompleter { private final RVNKCore plugin; private final LogManager logger; public CoreCommand(RVNKCore plugin) { this.plugin = plugin; this.logger = LogManager.getInstance(plugin, getClass()); } @Override public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { if (!sender.hasPermission("rvnkcore.command.use")) { sender.sendMessage(ChatColor.RED + "You don't have permission to use this command."); return true; } if (args.length == 0) { showHelp(sender); return true; } switch (args[0].toLowerCase()) { case "reload": if (sender.hasPermission("rvnkcore.command.reload")) { handleReload(sender); } break; case "info": if (sender.hasPermission("rvnkcore.command.info")) { showPluginInfo(sender); } break; default: showHelp(sender); } return true; } @Override public List<String> onTabComplete(CommandSender sender, Command command, String label, String[] args) { if (args.length == 1) { List<String> completions = new ArrayList<>(); if (sender.hasPermission("rvnkcore.command.reload")) completions.add("reload"); if (sender.hasPermission("rvnkcore.command.info")) completions.add("info"); return completions; } return Collections.emptyList(); } private void showHelp(CommandSender sender) { sender.sendMessage(ChatColor.YELLOW + "RVNKCore Commands:"); if (sender.hasPermission("rvnkcore.command.reload")) { sender.sendMessage(ChatColor.GOLD + "/rvnkcore reload " + ChatColor.WHITE + "- Reload configuration"); } if (sender.hasPermission("rvnkcore.command.info")) { sender.sendMessage(ChatColor.GOLD + "/rvnkcore info " + ChatColor.WHITE + "- Show plugin information"); } } private void handleReload(CommandSender sender) { try { // Reload logic sender.sendMessage(ChatColor.GREEN + "Configuration reloaded successfully."); logger.info("Configuration reloaded by " + sender.getName()); } catch (Exception e) { sender.sendMessage(ChatColor.RED + "Failed to reload configuration."); logger.error("Failed to reload configuration", e); } } private void showPluginInfo(CommandSender sender) { sender.sendMessage(ChatColor.YELLOW + "RVNKCore v" + plugin.getDescription().getVersion()); // Additional info... } } ## Database Access Patterns ### Repository Pattern All database access should use the repository pattern: ```java public class PlayerRepository extends BaseRepository<PlayerDTO, UUID> { public PlayerRepository(ConnectionProvider provider, QueryBuilder queryBuilder) { super(provider, queryBuilder, PlayerDTO.class); } @Override protected String getTableName() { return "players"; } public CompletableFuture<List<PlayerDTO>> findByLastSeen(Timestamp since) { return executeQueryList( queryBuilder.select("*") .from(getTableName()) .where("last_seen > ?", since) .orderBy("last_seen", false), PlayerDTO.class ); } }
Use DTOs for all data transfer between layers:
public class PlayerDTO { private UUID id; private String username; private Timestamp firstJoin; private Timestamp lastSeen; private boolean banned; private Map<String, Object> metadata; // Getters and setters // Builder pattern for construction public static class Builder { private PlayerDTO dto = new PlayerDTO(); public Builder id(UUID id) { dto.id = id; return this; } // Other builder methods public PlayerDTO build() { return dto; } } }
Use transactions for multi-operation sequences:
public CompletableFuture<Boolean> updatePlayerAndPreferences(PlayerDTO player, Map<String, String> preferences) { return transactionManager.inTransaction(() -> { return playerRepository.save(player) .thenCompose(savedPlayer -> { List<CompletableFuture<Boolean>> futures = new ArrayList<>(); for (Map.Entry<String, String> entry : preferences.entrySet()) { futures.add(preferenceRepository.savePreference( player.getId(), entry.getKey(), entry.getValue())); } return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])) .thenApply(v -> true); }); }); }
All services should implement a consistent lifecycle:
public class MyService implements IMyService, AutoCloseable { private final LogManager logger; private boolean initialized = false; public MyService() { this.logger = LogManager.getInstance(); } public void initialize() { if (initialized) return; // Initialization logic initialized = true; logger.info("MyService initialized successfully"); } @Override public void close() { if (!initialized) return; // Cleanup logic initialized = false; logger.info("MyService shut down successfully"); } // Service methods }
Services should be registered with the ServiceRegistry:
// In RVNKCore main class private void initializeServices() { // Create instances PlayerService playerService = new PlayerService(); AnnouncementService announcementService = new AnnouncementService(); // Initialize playerService.initialize(); announcementService.initialize(); // Register serviceRegistry.registerService(IPlayerService.class, playerService); serviceRegistry.registerService(IAnnouncementService.class, announcementService); logger.info("Core services initialized and registered"); }
Use the LogManager for all logging:
private final LogManager logger = LogManager.getInstance(); public void doSomething() { logger.debug("Starting operation with debug info"); logger.info("Operation in progress"); try { // Operation logic } catch (Exception e) { logger.error("Failed to perform operation", e); throw e; } }
Use appropriate log levels:
All core components must have unit tests:
@Test public void testPlayerServiceGetPlayer() { // Arrange UUID playerId = UUID.randomUUID(); PlayerDTO expectedPlayer = new PlayerDTO.Builder() .id(playerId) .username("testplayer") .build(); // Mock repository when(playerRepository.findById(playerId)) .thenReturn(CompletableFuture.completedFuture(expectedPlayer)); // Act PlayerDTO actualPlayer = playerService.getPlayer(playerId).join(); // Assert assertEquals(expectedPlayer.getId(), actualPlayer.getId()); assertEquals(expectedPlayer.getUsername(), actualPlayer.getUsername()); }
Integration tests should verify component interactions:
@Test public void testDatabaseServiceIntegration() { // Create test database connection ConnectionProvider provider = new SQLiteConnectionProvider(":memory:"); // Create schema SchemaManager schemaManager = new SchemaManager(provider); schemaManager.createSchema(); // Create repository PlayerRepository repository = new PlayerRepository(provider, new SQLiteQueryBuilder()); // Create player PlayerDTO player = new PlayerDTO.Builder() .id(UUID.randomUUID()) .username("testplayer") .build(); // Save and retrieve repository.save(player).join(); PlayerDTO retrieved = repository.findById(player.getId()).join(); // Verify assertEquals(player.getId(), retrieved.getId()); assertEquals(player.getUsername(), retrieved.getUsername()); }
public interface IAnnouncementService { // Core methods CompletableFuture<AnnouncementDTO> createAnnouncement(AnnouncementDTO announcement); CompletableFuture<Optional<AnnouncementDTO>> getAnnouncement(String id); CompletableFuture<Boolean> deleteAnnouncement(String id); CompletableFuture<List<AnnouncementDTO>> getAnnouncements(Optional<String> type); // Default convenience methods default CompletableFuture<List<AnnouncementDTO>> getAllAnnouncements() { return getAnnouncements(Optional.empty()); } default CompletableFuture<List<AnnouncementDTO>> getAnnouncementsByType(String type) { return getAnnouncements(Optional.of(type)); } }
/** * @deprecated As of version 2.0.0, replaced by {@link #newMethod()} * Will be removed in version 3.0.0 */ @Deprecated public CompletableFuture<String> oldMethod() { // Forward to new method return newMethod(); } /** * New implementation introduced in version 2.0.0 */ public CompletableFuture<String> newMethod() { // Implementation }
All resources must be properly cleaned up:
public void shutdown() { // Close resources in reverse initialization order // 1. Shutdown scheduled tasks if (scheduledExecutor != null) { scheduledExecutor.shutdown(); try { if (!scheduledExecutor.awaitTermination(5, TimeUnit.SECONDS)) { scheduledExecutor.shutdownNow(); } } catch (InterruptedException e) { scheduledExecutor.shutdownNow(); Thread.currentThread().interrupt(); } } // 2. Close database connections if (connectionProvider != null) { connectionProvider.close(); } // 3. Clear caches if (cache != null) { cache.invalidateAll(); } logger.info("Resources cleaned up successfully"); }
Be conscious of memory usage:
private final Cache<UUID, PlayerDTO> playerCache = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(30, TimeUnit.MINUTES) .softValues() .build();
Plugins using RVNKCore should register themselves:
public class MyPlugin extends JavaPlugin { private RVNKCoreAPI coreAPI; @Override public void onEnable() { // Get RVNKCore API Plugin corePlugin = getServer().getPluginManager().getPlugin("RVNKCore"); if (corePlugin == null || !(corePlugin instanceof RVNKCore)) { getLogger().severe("RVNKCore not found or incompatible version!"); getServer().getPluginManager().disablePlugin(this); return; } coreAPI = ((RVNKCore) corePlugin).getAPI(); // Register plugin coreAPI.registerPlugin(this, "my-plugin", getDescription().getVersion()); // Initialize with core services initializeWithCore(); } private void initializeWithCore() { // Get required services IPlayerService playerService = coreAPI.getService(IPlayerService.class); IAnnouncementService announcementService = coreAPI.getService(IAnnouncementService.class); // Use services // ... } }
Use the service registry to access core services:
public class MyFeature { private final IPlayerService playerService; private final IAnnouncementService announcementService; public MyFeature(RVNKCoreAPI coreAPI) { this.playerService = coreAPI.getService(IPlayerService.class); this.announcementService = coreAPI.getService(IAnnouncementService.class); } public void doSomething(UUID playerId) { playerService.getPlayer(playerId) .thenAccept(playerOpt -> { if (playerOpt.isPresent()) { PlayerDTO player = playerOpt.get(); // Use player data } }); } }
Organize configuration files by feature:
# Main config.yml database: type: mysql mysql: host: localhost port: 3306 # ... services: player: cache-size: 1000 cache-expiry: 30 announcement: default-type: info broadcast-interval: 300 api: rest-enabled: true port: 8080 auth-required: true
Validate all configuration values:
public class ServiceConfig { private final int cacheSize; private final int cacheExpiry; public ServiceConfig(ConfigurationSection section) { // Get values with validation this.cacheSize = validateCacheSize(section.getInt("cache-size", 1000)); this.cacheExpiry = validateCacheExpiry(section.getInt("cache-expiry", 30)); } private int validateCacheSize(int size) { if (size < 10) { logger.warning("Cache size too small, using minimum value of 10"); return 10; } if (size > 10000) { logger.warning("Cache size too large, using maximum value of 10000"); return 10000; } return size; } private int validateCacheExpiry(int minutes) { if (minutes < 1) { logger.warning("Cache expiry too small, using minimum value of 1 minute"); return 1; } if (minutes > 1440) { logger.warning("Cache expiry too large, using maximum value of 1440 minutes (24 hours)"); return 1440; } return minutes; } // Getters }
For more detailed information, refer to: