From f943617b13c6ffa4957ccafe1a681ad11c477039 Mon Sep 17 00:00:00 2001 From: demengc Date: Mon, 23 Mar 2026 21:54:39 -0400 Subject: [PATCH] docs: audit cooldowns and utilities pages Verify all method names, signatures, and behavioral claims against source code. Fix test/reset semantics documentation, add missing API methods (ofTicks, remainingMillis, getLastTested, setLastTested, copy, getAll, put, standalone Cooldown usage). Add missing Common utilities (checkInt, checkFloat, checkLong, checkDouble, formatDecimal, getOrDefault, getOrError, hasPermission, checkClass, forEachInt). Restructure both pages to use tables for method listings, remove redundant sections (Error Handling, Version-Specific Code, Player Selection), and apply parallel structure throughout. --- docs/core-features/cooldowns.md | 128 ++++++++---- docs/core-features/utilities.md | 341 ++++++++++++++------------------ 2 files changed, 242 insertions(+), 227 deletions(-) diff --git a/docs/core-features/cooldowns.md b/docs/core-features/cooldowns.md index 48f7afa..e882974 100644 --- a/docs/core-features/cooldowns.md +++ b/docs/core-features/cooldowns.md @@ -4,7 +4,11 @@ description: Track cooldowns for players, actions, or any key. # Cooldowns -## Basic Usage +PluginBase provides `Cooldown` (a single timer) and `CooldownMap` (a keyed collection of timers backed by an auto-expiring map). + +## Creating a CooldownMap + +`CooldownMap.create()` accepts a base `Cooldown` that defines the duration for all entries. ```java import dev.demeng.pluginbase.cooldown.Cooldown; @@ -12,27 +16,86 @@ import dev.demeng.pluginbase.cooldown.CooldownMap; import java.util.UUID; import java.util.concurrent.TimeUnit; -// Create cooldown map CooldownMap cooldowns = CooldownMap.create( Cooldown.of(30, TimeUnit.SECONDS) ); +``` + +`Cooldown.ofTicks(long)` is also available for tick-based durations (1 tick = 50ms). + +```java +CooldownMap cooldowns = CooldownMap.create( + Cooldown.ofTicks(600) // 30 seconds +); +``` + +## Test and reset semantics -// Test cooldown +`test(key)` and `testSilently(key)` both return `true` when the cooldown for that key is **not active** (expired or never started). The difference is what happens on a `true` result: + +| Method | Returns `true` when | Side effect on `true` | +|---|---|---| +| `test(key)` | Cooldown is not active | Resets the timer (starts the cooldown) | +| `testSilently(key)` | Cooldown is not active | None | + +When the cooldown **is** active, both methods return `false` and neither resets the timer. + +```java UUID playerId = player.getUniqueId(); + if (cooldowns.test(playerId)) { - // Player can perform action + // Cooldown was not active; timer has now been reset. Text.tell(player, "&aAction performed!"); } else { - // Player is on cooldown + // Cooldown is still active. long remaining = cooldowns.remainingTime(playerId, TimeUnit.SECONDS); Text.tell(player, "&cPlease wait " + remaining + " seconds!"); } ``` -## Complete Example +Use `testSilently()` when you need to check the cooldown state without consuming it (for example, to grey out a UI button). + +## Querying remaining and elapsed time + +| Method | Return type | Description | +|---|---|---| +| `remainingTime(key, TimeUnit)` | `long` | Time remaining until the cooldown expires, in the given unit. Returns `0` if not active. | +| `remainingMillis(key)` | `long` | Time remaining in milliseconds. Returns `0` if not active. | +| `elapsed(key)` | `long` | Milliseconds since the last `test()` or `reset()`. If the key was never tested, returns time since epoch. | + +```java +long remainingSec = cooldowns.remainingTime(playerId, TimeUnit.SECONDS); +long remainingMs = cooldowns.remainingMillis(playerId); +long elapsedMs = cooldowns.elapsed(playerId); +``` + +## Manual management + +```java +// Reset cooldown manually (starts the timer without checking) +cooldowns.reset(playerId); + +// Access the underlying Cooldown instance for a key +Cooldown cd = cooldowns.get(playerId); + +// Get/set the last-tested timestamp (useful for persistence/reconstruction) +OptionalLong lastTested = cooldowns.getLastTested(playerId); +cooldowns.setLastTested(playerId, System.currentTimeMillis()); + +// Insert a pre-built Cooldown (must match the base duration) +Cooldown restored = Cooldown.of(30, TimeUnit.SECONDS); +restored.setLastTested(savedTimestamp); +cooldowns.put(playerId, restored); + +// Get all active cooldowns +Map all = cooldowns.getAll(); +``` + +## Complete example ```java public class TeleportCommand { + private final CooldownMap cooldowns = CooldownMap.create( Cooldown.of(5, TimeUnit.MINUTES) ); @@ -41,57 +104,56 @@ public class TeleportCommand { public void home(Player sender) { UUID uuid = sender.getUniqueId(); - // Check cooldown if (!cooldowns.test(uuid)) { long remaining = cooldowns.remainingTime(uuid, TimeUnit.SECONDS); Text.tell(sender, "&cPlease wait " + remaining + " seconds!"); return; } - // Perform action - Location home = getHome(sender); - sender.teleport(home); + sender.teleport(getHome(sender)); Text.tell(sender, "&aTeleported home!"); } } ``` -## Other Key Types +## Using arbitrary key types + +The type parameter on `CooldownMap` can be any type with proper `equals`/`hashCode` semantics. ```java -// Cooldown by string key CooldownMap messageCooldowns = CooldownMap.create( Cooldown.of(1, TimeUnit.MINUTES) ); -// Per-message cooldown String key = player.getName() + ":welcome"; if (messageCooldowns.test(key)) { Text.tell(player, "&aWelcome message!"); } ``` -## Manual Management +## Standalone Cooldown + +`Cooldown` can be used on its own without a `CooldownMap` when you only need a single timer. + +| Method | Return type | Description | +|---|---|---| +| `of(long, TimeUnit)` | `Cooldown` | Creates a cooldown with the given duration. | +| `ofTicks(long)` | `Cooldown` | Creates a cooldown measured in game ticks. | +| `test()` | `boolean` | Returns `true` if not active, then resets. | +| `testSilently()` | `boolean` | Returns `true` if not active, no reset. | +| `reset()` | `void` | Resets the timer to now. | +| `elapsed()` | `long` | Milliseconds since last reset. | +| `remainingMillis()` | `long` | Milliseconds until expiry (0 if not active). | +| `remainingTime(TimeUnit)` | `long` | Time until expiry in the given unit. | +| `getDuration()` | `long` | The cooldown duration in milliseconds. | +| `getLastTested()` | `OptionalLong` | Timestamp of last reset, or empty if never reset. | +| `setLastTested(long)` | `void` | Sets the last-tested timestamp directly. | +| `copy()` | `Cooldown` | Creates a new instance with the same duration. | ```java -CooldownMap cooldowns = CooldownMap.create( - Cooldown.of(30, TimeUnit.SECONDS) -); - -UUID uuid = player.getUniqueId(); +Cooldown abilityCooldown = Cooldown.of(10, TimeUnit.SECONDS); -// Reset cooldown (starts the cooldown timer) -cooldowns.reset(uuid); - -// Get remaining time -long remaining = cooldowns.remainingTime(uuid, TimeUnit.SECONDS); - -// Get elapsed time in milliseconds -long elapsedMillis = cooldowns.elapsed(uuid); - -// Get the cooldown instance for advanced operations -Cooldown cooldown = cooldowns.get(uuid); - -// Put a specific cooldown instance (must have same duration as base) -cooldowns.put(uuid, Cooldown.of(30, TimeUnit.SECONDS)); +if (abilityCooldown.test()) { + // Use ability +} ``` diff --git a/docs/core-features/utilities.md b/docs/core-features/utilities.md index 003a2e0..bdf9381 100644 --- a/docs/core-features/utilities.md +++ b/docs/core-features/utilities.md @@ -6,276 +6,229 @@ description: Helper utilities for common operations. ## Common -Version checking and plugin utilities. +General-purpose helpers for plugin metadata, version checks, number parsing, and error reporting. -```java -import dev.demeng.pluginbase.Common; +### Plugin and server information + +| Method | Return type | Description | +|---|---|---| +| `getName()` | `String` | Plugin name from plugin.yml. | +| `getVersion()` | `String` | Plugin version from plugin.yml. | +| `getServerMajorVersion()` | `int` | Major MC version (e.g. `20` for 1.20.x). | +| `isServerVersionAtLeast(int)` | `boolean` | `true` if the server's major version >= the argument. | -// Check server version (takes only major version number) +```java if (Common.isServerVersionAtLeast(16)) { - // Use 1.16+ features (like HEX colors) + // Use 1.16+ features } +``` -// Get server's major version as integer -int majorVersion = Common.getServerMajorVersion(); // Returns 20 for 1.20.x - -// Get plugin version string -String version = Common.getVersion(); // Returns plugin version from plugin.yml +### Number parsing -// Get plugin name -String name = Common.getName(); // Returns plugin name from plugin.yml +Each method returns `null` if the string is not a valid number. -// Log errors -try { - // Code that might fail -} catch (Exception ex) { - Common.error(ex, "Failed to load data", true); // Prints to console and optionally disables plugin -} +| Method | Return type | +|---|---| +| `checkInt(String)` | `Integer` | +| `checkLong(String)` | `Long` | +| `checkFloat(String)` | `Float` | +| `checkDouble(String)` | `Double` | -// Log errors and notify players -try { - // Code that might fail -} catch (Exception ex) { - Common.error(ex, "Failed to save data", false, player); // Notifies player about error +```java +Integer level = Common.checkInt("42"); +if (level != null) { + player.setLevel(level); } ``` -## Players +### Null handling -Player-related utilities. +| Method | Return type | Description | +|---|---|---| +| `getOrDefault(T, T)` | `T` | Returns the first argument if non-null, otherwise the second. | +| `getOrError(T, String, boolean)` | `T` | Returns the first argument if non-null, otherwise throws `PluginErrorException`. The boolean controls whether the plugin is disabled. | + +### Other helpers + +| Method | Description | +|---|---| +| `formatDecimal(double)` | Formats a double to 2 decimal places (e.g. `"3.14"`). | +| `hasPermission(CommandSender, String)` | Returns `true` if the sender has the permission, or if the permission is null/empty/`"none"`. | +| `checkClass(String)` | Returns the `Class` if it exists on the classpath, otherwise `null`. | +| `forEachInt(String, IntConsumer)` | Parses an integer sequence (`"1"`, `"1-5"`, or `"1,3,7"`) and runs the consumer for each value. | +| `error(Throwable, String, boolean, CommandSender...)` | Logs an error to console with a formatted block. Optionally disables the plugin and notifies players. | ```java -import dev.demeng.pluginbase.Players; +Common.error(ex, "Failed to load data", true); +Common.error(ex, "Failed to save data", false, player); +``` -// Get all online players -Collection players = Players.all(); +## Players -// Stream all online players -Players.stream().forEach(player -> { - // Do something with each player -}); +Bulk operations on online players. -// Apply action to all players -Players.forEach(player -> { - player.sendMessage("Broadcast message!"); -}); +| Method | Return type | Description | +|---|---|---| +| `all()` | `Collection` | All online players. | +| `stream()` | `Stream` | Stream of all online players. | +| `forEach(Consumer)` | `void` | Applies an action to every online player. | +| `streamInRange(Location, double)` | `Stream` | Stream of players within the given radius of a location. | +| `forEachInRange(Location, double, Consumer)` | `void` | Applies an action to every player within the radius. | -// Get players within radius of a location -Players.streamInRange(location, 50.0).forEach(player -> { - // Do something with nearby players -}); +```java +Players.forEach(p -> p.sendMessage("Server restarting!")); -// Apply action to players within radius -Players.forEachInRange(location, 50.0, player -> { - player.sendMessage("You are near the spawn!"); +Players.forEachInRange(location, 50.0, p -> { + Text.tell(p, "&aYou are near spawn!"); }); ``` ## Locations -Location utilities. +Location manipulation helpers. -```java -import dev.demeng.pluginbase.Locations; +| Method | Return type | Description | +|---|---|---| +| `center(Location)` | `Location` | Rounds to the center of the block (+0.5 on X and Z), preserving yaw/pitch. | +| `toBlockLocation(Location)` | `Location` | Converts to integer coordinates with yaw/pitch zeroed. | -// Center location to block (adds 0.5 to X and Z) +```java Location centered = Locations.center(loc); - -// Convert to block location (integer coordinates) Location blockLoc = Locations.toBlockLocation(loc); ``` ## Sounds -Play sounds easily. - -```java -import dev.demeng.pluginbase.Sounds; -import org.bukkit.Sound; - -// Play vanilla sound to player -Sounds.playVanillaToPlayer(player, Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.0f); +Play vanilla or custom (resource pack) sounds. All `playTo*` methods silently no-op if the sound name is `null` or `"none"`. -// Play vanilla sound at location -Sounds.playVanillaToLocation(location, Sound.BLOCK_NOTE_BLOCK_PLING, 1.0f, 2.0f); +### By name (auto-detecting custom sounds) -// Play custom sound (from resource pack) to player -Sounds.playCustomToPlayer(player, "custom.sound.name", 1.0f, 1.0f); +Pass a `Sound` enum name for vanilla sounds. Prefix with `custom:` for resource pack sounds. -// Play custom sound at location -Sounds.playCustomToLocation(location, "custom.sound.name", 1.0f, 1.0f); +| Method | Parameters | Description | +|---|---|---| +| `playToPlayer` | `Player, String, float, float` | Plays to a player by sound name, volume, pitch. | +| `playToPlayer` | `Player, ConfigurationSection` | Reads `sound`, `volume`, `pitch` keys from the section. | +| `playToLocation` | `Location, String, float, float` | Plays at a location by sound name, volume, pitch. | +| `playToLocation` | `Location, ConfigurationSection` | Reads `sound`, `volume`, `pitch` keys from the section. | -// Play sound by name (supports both vanilla and custom sounds) +```java Sounds.playToPlayer(player, "ENTITY_PLAYER_LEVELUP", 1.0f, 1.0f); -Sounds.playToPlayer(player, "custom:mysound", 1.0f, 1.0f); // Custom sounds use "custom:" prefix - -// Play sound from config section -ConfigurationSection soundConfig = config.getConfigurationSection("sounds.levelup"); -Sounds.playToPlayer(player, soundConfig); -Sounds.playToLocation(location, soundConfig); +Sounds.playToPlayer(player, "custom:myplugin.reward", 1.0f, 1.0f); +Sounds.playToLocation(location, "BLOCK_NOTE_BLOCK_PLING", 1.0f, 2.0f); ``` -## UpdateChecker - -Check for plugin updates from SpigotMC. +Config section format: -```java -import dev.demeng.pluginbase.UpdateChecker; -import dev.demeng.pluginbase.Schedulers; - -// Check for updates asynchronously -Schedulers.async().run(() -> { - UpdateChecker checker = new UpdateChecker(spigotResourceId); - - // Get results - UpdateChecker.Result result = checker.getResult(); - String latestVersion = checker.getLatestVersion(); - - // Notify console or player - checker.notifyResult(null); // Notifies console - checker.notifyResult(player); // Notifies specific player -}); +```yaml +sounds: + levelup: + sound: "ENTITY_PLAYER_LEVELUP" + volume: 1.0 + pitch: 1.0 ``` -## Error Handling - ```java -try { - // Database operation - database.save(data); -} catch (Exception ex) { - // Log with stack trace - Common.error(ex, "Failed to save data", true); - - // Notify player - Text.tell(player, "&cFailed to save data!"); -} +Sounds.playToPlayer(player, config.getConfigurationSection("sounds.levelup")); ``` -## Version-Specific Code +### Direct vanilla/custom methods -```java -if (Common.isServerVersionAtLeast(16)) { - // Use HEX colors (1.16+) - Text.tell(player, "<#FF5733>Custom color!"); -} else { - // Fallback to legacy colors - Text.tell(player, "&cRed color"); -} +These skip the name-based dispatch and accept typed arguments directly. -if (Common.isServerVersionAtLeast(19)) { - // Use 1.19+ features -} else { - // Fallback for older versions -} +| Method | Parameters | +|---|---| +| `playVanillaToPlayer` | `Player, Sound, float, float` | +| `playVanillaToLocation` | `Location, Sound, float, float` | +| `playCustomToPlayer` | `Player, String, float, float` | +| `playCustomToLocation` | `Location, String, float, float` | -// Get major version number -int majorVersion = Common.getServerMajorVersion(); -Text.tell(player, "&7Server version: 1." + majorVersion); +```java +Sounds.playVanillaToPlayer(player, Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.0f); +Sounds.playCustomToPlayer(player, "myplugin.reward", 1.0f, 1.0f); ``` -## Player Selection +Note: the direct custom methods take the raw sound name without the `custom:` prefix. -```java -import java.util.stream.Collectors; -import org.bukkit.Bukkit; -import org.bukkit.World; - -// Get player by name using Bukkit -Player target = Bukkit.getPlayer("Steve"); -if (target == null) { - Text.tell(sender, "&cPlayer not found!"); - return; -} +## UpdateChecker -// Get online VIPs using stream filtering -List vips = Players.stream() - .filter(p -> p.hasPermission("vip")) - .collect(Collectors.toList()); +Checks for plugin updates against the SpigotMC API. The constructor performs a blocking HTTP request, so it should be called asynchronously. -for (Player vip : vips) { - Text.tell(vip, "&6VIP announcement!"); -} +| Method | Return type | Description | +|---|---|---| +| `new UpdateChecker(int)` | `UpdateChecker` | Fetches the latest version for the given SpigotMC resource ID. | +| `getResult()` | `UpdateChecker.Result` | `UP_TO_DATE`, `OUTDATED`, or `ERROR`. | +| `getLatestVersion()` | `String` | The version string from SpigotMC (null on error). | +| `getResourceId()` | `int` | The resource ID passed to the constructor. | +| `notifyResult(CommandSender)` | `void` | Sends an update notification if outdated. Pass `null` for console. | -// Or use forEach directly -Players.stream() - .filter(p -> p.hasPermission("vip")) - .forEach(vip -> Text.tell(vip, "&6VIP announcement!")); - -// Get players in specific world using stream filtering -World world = Bukkit.getWorld("world"); -List survivalPlayers = Players.stream() - .filter(p -> p.getWorld().equals(world)) - .collect(Collectors.toList()); - -// Get players near a location -Location spawn = new Location(world, 0, 64, 0); -Players.forEachInRange(spawn, 50.0, player -> { - Text.tell(player, "&aYou are near spawn!"); +```java +Schedulers.async().run(() -> { + UpdateChecker checker = new UpdateChecker(spigotResourceId); + checker.notifyResult(null); }); ``` ## Time -Parse and format durations and timestamps. +Duration parsing, formatting, and timestamp utilities. + +### Duration parsing + +`Time.parse(String)` accepts human-readable duration strings. Supported units: `y` (years), `mo` (months), `w` (weeks), `d` (days), `h` (hours), `m` (minutes), `s` (seconds). Throws `IllegalArgumentException` on invalid input. ```java -import dev.demeng.pluginbase.Time; -import java.sql.Timestamp; -import java.time.Duration; -import java.util.Optional; +Duration d1 = Time.parse("1h 30m"); +Duration d2 = Time.parse("3d 2h"); +Duration d3 = Time.parse("1y 2mo 3w 4d 5h 6m 7s"); +``` -// Parse duration strings -Duration duration = Time.parse("1h 30m"); // 1 hour 30 minutes -Duration duration2 = Time.parse("3d 2h"); // 3 days 2 hours -Duration duration3 = Time.parse("1y 2mo 3w 4d 5h 6m 7s"); // All units +`Time.parseSafely(String)` returns `Optional` instead of throwing. -// Parse safely (returns Optional) -Optional optional = Time.parseSafely("invalid"); +### Duration formatting -// Format durations -long millis = Duration.ofHours(2).toMillis(); +| Formatter | Example output for 2h 15m | +|---|---| +| `DurationFormatter.LONG` | `2 hours 15 minutes` | +| `DurationFormatter.CONCISE` | `2h 15m` | +| `DurationFormatter.CONCISE_LOW_ACCURACY` | `2h 15m` (max 3 units) | + +```java +long millis = Duration.ofHours(2).plusMinutes(15).toMillis(); String formatted = Time.formatDuration(Time.DurationFormatter.LONG, millis); -// Output: "2 hours" +``` -String concise = Time.formatDuration(Time.DurationFormatter.CONCISE, millis); -// Output: "2h" +### Timestamp formatting and conversion -// Format dates and timestamps -String dateTime = Time.formatDateTime(System.currentTimeMillis()); -String date = Time.formatDate(System.currentTimeMillis()); +| Method | Description | +|---|---| +| `formatDateTime(long)` | Formats using the configured date-time pattern. | +| `formatDate(long)` | Formats using the configured date pattern. | +| `toSqlTimestamp(long)` | Converts epoch millis to `java.sql.Timestamp`. | +| `fromSqlTimestamp(String)` | Parses an SQL timestamp string to epoch millis. | -// Convert to/from SQL timestamps +```java +String dateTime = Time.formatDateTime(System.currentTimeMillis()); Timestamp sqlTime = Time.toSqlTimestamp(System.currentTimeMillis()); -long timestamp = Time.fromSqlTimestamp("2025-12-13 10:30:00"); +long millis = Time.fromSqlTimestamp("2025-12-13 10:30:00"); ``` ## Services -Work with Bukkit's service manager. - -```java -import dev.demeng.pluginbase.Services; -import org.bukkit.plugin.ServicePriority; +Wrapper around Bukkit's `ServicesManager`. -// Register a service -MyService service = new MyService(); -Services.provide(MyService.class, service); +| Method | Return type | Description | +|---|---|---| +| `provide(Class, T)` | `T` | Registers a service at `Normal` priority using the base plugin. | +| `provide(Class, T, ServicePriority)` | `T` | Registers a service at the given priority using the base plugin. | +| `provide(Class, T, Plugin, ServicePriority)` | `T` | Registers a service with an explicit plugin and priority. | +| `get(Class)` | `Optional` | Retrieves a registered service, or empty. | +| `load(Class)` | `T` | Retrieves a registered service, or throws `IllegalStateException`. | -// Register with specific priority -Services.provide(MyService.class, service, ServicePriority.High); +```java +Services.provide(MyService.class, new MyServiceImpl()); -// Get a service (returns Optional) Optional economy = Services.get(Economy.class); -if (economy.isPresent()) { - // Use economy service -} -// Load a service (throws if not found) -try { - Economy economy = Services.load(Economy.class); - // Use economy service -} catch (IllegalStateException ex) { - Text.console("&cEconomy service not found!"); -} +Economy econ = Services.load(Economy.class); ```