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