diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7fd7a5c..5fbfac0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,16 +16,16 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: '21' - name: Cache Maven local repository - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.m2/repository key: ${{ runner.os }}-m2-${{ matrix.paper-version }}-${{ hashFiles('**/pom.xml') }} @@ -47,16 +47,16 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: '21' - name: Cache Maven local repository - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.m2/repository key: ${{ runner.os }}-m2-feature-${{ matrix.paper-version }}-${{ hashFiles('**/pom.xml') }} @@ -65,3 +65,248 @@ jobs: - name: Run feature tests run: mvn -B -Dpaper.version=${{ matrix.paper-version }} -Dmockbukkit.artifactId=${{ matrix.mockbukkit-artifactId }} -Dmockbukkit.version=${{ matrix.mockbukkit-version }} -Pfeature-tests -Dtest=*FeatureTest test + + # ───────────────────────────────────────────────────────────────────────── + # Smoke tests — Paper & Folia (downloaded from api.papermc.io) + # Matrix: platform × mc-prefix × java + # ───────────────────────────────────────────────────────────────────────── + smoke-papermc: + name: "Smoke · ${{ matrix.platform }} / MC ${{ matrix.mc-prefix }} / Java ${{ matrix.java }}" + runs-on: ubuntu-latest + needs: [unit-tests, feature-tests] + strategy: + fail-fast: false + matrix: + platform: [paper, folia] + mc-prefix: ["1.21", "26.1"] + java: [21, 25] + exclude: + # MC 26.1 requires Java 25+ + - mc-prefix: "26.1" + java: 21 + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: ${{ matrix.java }} + cache: maven + + - name: Build plugin JAR + run: mvn -B -ntp -DskipTests package + + - name: Resolve latest ${{ matrix.platform }} build for MC ${{ matrix.mc-prefix }}.x + id: server + run: | + PLATFORM="${{ matrix.platform }}" + PREFIX="${{ matrix.mc-prefix }}" + VERSIONS_JSON=$(curl -fsSL "https://fill.papermc.io/v3/projects/${PLATFORM}") + MC_VERSION=$(PREFIX="${PREFIX}" python3 -c "import sys,os,json;d=json.loads(sys.stdin.read());p=os.environ['PREFIX'];vs=d.get('versions',{});fam=p if p in vs else next((f for f in vs if f.startswith(p+'.')),None);print(vs[fam][0] if fam and vs.get(fam) else '')" <<< "$VERSIONS_JSON") + if [[ -z "$MC_VERSION" ]]; then + echo "::error::No ${PLATFORM} builds found matching MC ${PREFIX}.x." + echo "available=false" >> "$GITHUB_OUTPUT" + exit 1 + fi + BUILDS_JSON=$(curl -fsSL "https://fill.papermc.io/v3/projects/${PLATFORM}/versions/${MC_VERSION}/builds") + BUILD_INFO=$(python3 -c "import sys,json;bs=json.loads(sys.stdin.read());st=[b for b in bs if b.get('channel')=='STABLE'];c=st[0] if st else (bs[0] if bs else None);[print(c['id']),print(c['downloads']['server:default']['url'])] if c else None" <<< "$BUILDS_JSON") + BUILD=$(echo "$BUILD_INFO" | sed -n '1p') + DOWNLOAD_URL=$(echo "$BUILD_INFO" | sed -n '2p') + JAR_NAME=$(basename "$DOWNLOAD_URL") + if [[ -z "$BUILD" || -z "$DOWNLOAD_URL" ]]; then + echo "::error::No builds found for ${PLATFORM} ${MC_VERSION}." + echo "available=false" >> "$GITHUB_OUTPUT" + exit 1 + fi + echo "mc_version=${MC_VERSION}" >> "$GITHUB_OUTPUT" + echo "build=${BUILD}" >> "$GITHUB_OUTPUT" + echo "jar_name=${JAR_NAME}" >> "$GITHUB_OUTPUT" + echo "download_url=${DOWNLOAD_URL}" >> "$GITHUB_OUTPUT" + echo "available=true" >> "$GITHUB_OUTPUT" + echo "Resolved: ${PLATFORM} ${MC_VERSION} build ${BUILD} (${JAR_NAME})" + + - name: Download ${{ matrix.platform }} server JAR + if: steps.server.outputs.available == 'true' + run: | + mkdir -p smoke-server/plugins + curl -fsSL "${{ steps.server.outputs.download_url }}" -o smoke-server/server.jar + + - name: Copy plugin JAR + if: steps.server.outputs.available == 'true' + run: cp target/EzCountdown-*.jar smoke-server/plugins/EzCountdown.jar + + - name: Configure server + if: steps.server.outputs.available == 'true' + run: | + echo "eula=true" > smoke-server/eula.txt + printf '%s\n' \ + "online-mode=false" \ + "level-type=flat" \ + "generate-structures=false" \ + "max-players=0" \ + > smoke-server/server.properties + + - name: Run server and wait for ready + if: steps.server.outputs.available == 'true' + run: | + cd smoke-server + java -Xms512m -Xmx1g -jar server.jar --nogui > server.log 2>&1 & + SERVER_PID=$! + ELAPSED=0 + while [ $ELAPSED -lt 120 ]; do + sleep 2; ELAPSED=$((ELAPSED + 2)) + if grep -q "Done (" server.log 2>/dev/null; then + echo "Server ready after ${ELAPSED}s"; break + fi + if ! kill -0 $SERVER_PID 2>/dev/null; then + echo "Server process exited after ${ELAPSED}s"; break + fi + done + kill $SERVER_PID 2>/dev/null || true + wait $SERVER_PID 2>/dev/null || true + + - name: Assert no EzCountdown errors + if: steps.server.outputs.available == 'true' + run: | + LOG=smoke-server/server.log + if grep -qiE "SEVERE.*EzCountdown|Could not load.*EzCountdown|Error initialising.*EzCountdown" "$LOG"; then + echo "::error::EzCountdown logged errors on ${{ matrix.platform }} MC ${{ matrix.mc-prefix }} (Java ${{ matrix.java }}):" + grep -iE "SEVERE.*EzCountdown|Could not load.*EzCountdown|Error initialising.*EzCountdown" "$LOG" + exit 1 + fi + if grep -qiE "Enabling EzCountdown|EzCountdown.*enabled" "$LOG"; then + echo "Smoke test passed — EzCountdown loaded on ${{ matrix.platform }} MC ${{ steps.server.outputs.mc_version }} (Java ${{ matrix.java }})." + else + echo "::warning::Plugin load confirmation not found — server may not have finished loading within the timeout." + fi + + - name: Upload server log + if: always() + uses: actions/upload-artifact@v7 + with: + name: smoke-log-${{ matrix.platform }}-mc${{ matrix.mc-prefix }}-java${{ matrix.java }} + path: smoke-server/server.log + if-no-files-found: ignore + retention-days: 7 + + # ───────────────────────────────────────────────────────────────────────── + # Smoke tests — Spigot (compiled via BuildTools, result cached by MC+Java) + # Matrix: mc-prefix × java + # ───────────────────────────────────────────────────────────────────────── + smoke-spigot: + name: "Smoke · spigot / MC ${{ matrix.mc-prefix }} / Java ${{ matrix.java }}" + runs-on: ubuntu-latest + needs: [unit-tests, feature-tests] + strategy: + fail-fast: false + matrix: + mc-prefix: ["1.21"] + java: [21, 25] + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: ${{ matrix.java }} + cache: maven + + - name: Configure Git for BuildTools + run: | + git config --global user.email "ci@github.com" + git config --global user.name "CI" + + - name: Build plugin JAR + run: mvn -B -ntp -DskipTests package + + - name: Restore cached Spigot JAR (MC ${{ matrix.mc-prefix }}, Java ${{ matrix.java }}) + id: spigot-cache + uses: actions/cache@v5 + with: + path: spigot-build/spigot-${{ matrix.mc-prefix }}.jar + key: spigot-jar-mc${{ matrix.mc-prefix }}-java${{ matrix.java }}-v1 + + - name: Build Spigot via BuildTools + if: steps.spigot-cache.outputs.cache-hit != 'true' + id: buildtools + run: | + mkdir -p spigot-build/work + cd spigot-build/work + curl -fsSL \ + "https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar" \ + -o BuildTools.jar + java -jar BuildTools.jar --rev "${{ matrix.mc-prefix }}" 2>&1 | tee buildtools.log || { + echo "::warning::BuildTools failed for MC ${{ matrix.mc-prefix }} on Java ${{ matrix.java }} — version may not be supported yet. Spigot smoke test will be skipped." + echo "failed=true" >> "$GITHUB_OUTPUT" + exit 0 + } + SPIGOT_JAR=$(ls spigot-${{ matrix.mc-prefix }}*.jar 2>/dev/null | head -1) + if [[ -z "$SPIGOT_JAR" ]]; then + SPIGOT_JAR=$(ls spigot*.jar 2>/dev/null | grep -v craftbukkit | head -1) + fi + if [[ -n "$SPIGOT_JAR" ]]; then + cp "$SPIGOT_JAR" "../spigot-${{ matrix.mc-prefix }}.jar" + echo "::notice::Cached ${SPIGOT_JAR} as spigot-${{ matrix.mc-prefix }}.jar" + else + echo "::warning::BuildTools completed but no Spigot JAR found — smoke test will be skipped." + echo "failed=true" >> "$GITHUB_OUTPUT" + fi + + - name: Run Spigot smoke test + run: | + SPIGOT_JAR="spigot-build/spigot-${{ matrix.mc-prefix }}.jar" + if [[ ! -f "$SPIGOT_JAR" ]]; then + echo "::warning::Spigot JAR not available for MC ${{ matrix.mc-prefix }} — BuildTools could not build this version. Smoke test skipped." + exit 0 + fi + mkdir -p smoke-server/plugins + cp "$SPIGOT_JAR" smoke-server/server.jar + cp target/EzCountdown-*.jar smoke-server/plugins/EzCountdown.jar + echo "eula=true" > smoke-server/eula.txt + printf '%s\n' \ + "online-mode=false" \ + "level-type=flat" \ + "generate-structures=false" \ + "max-players=0" \ + > smoke-server/server.properties + cd smoke-server + java -Xms512m -Xmx1g -jar server.jar --nogui > server.log 2>&1 & + SERVER_PID=$! + ELAPSED=0 + while [ $ELAPSED -lt 120 ]; do + sleep 2; ELAPSED=$((ELAPSED + 2)) + if grep -q "Done (" server.log 2>/dev/null; then + echo "Server ready after ${ELAPSED}s"; break + fi + if ! kill -0 $SERVER_PID 2>/dev/null; then + echo "Server process exited after ${ELAPSED}s"; break + fi + done + kill $SERVER_PID 2>/dev/null || true + wait $SERVER_PID 2>/dev/null || true + LOG=server.log + if grep -qiE "SEVERE.*EzCountdown|Could not load.*EzCountdown|Error initialising.*EzCountdown" "$LOG"; then + echo "::error::EzCountdown logged errors on Spigot MC ${{ matrix.mc-prefix }} (Java ${{ matrix.java }}):" + grep -iE "SEVERE.*EzCountdown|Could not load.*EzCountdown|Error initialising.*EzCountdown" "$LOG" + exit 1 + fi + if grep -qiE "Enabling EzCountdown|EzCountdown.*enabled" "$LOG"; then + echo "Smoke test passed — EzCountdown loaded on Spigot MC ${{ matrix.mc-prefix }} (Java ${{ matrix.java }})." + else + echo "::warning::Plugin load confirmation not found — server may not have finished loading within the timeout." + fi + + - name: Upload Spigot server log + if: always() + uses: actions/upload-artifact@v7 + with: + name: smoke-log-spigot-mc${{ matrix.mc-prefix }}-java${{ matrix.java }} + path: smoke-server/server.log + if-no-files-found: ignore + retention-days: 7 diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml index 5f5d24a..9597741 100644 --- a/.github/workflows/maven-publish.yml +++ b/.github/workflows/maven-publish.yml @@ -16,9 +16,9 @@ jobs: packages: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up JDK 21 - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: java-version: '21' distribution: 'temurin' diff --git a/CHANGELOG.md b/CHANGELOG.md index b2b3a6a..b60842f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,13 +7,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.0.0] - 2026-05-30 + +### Added + +- **Folia support** - the plugin now runs on Folia (marked `folia-supported: true` in `plugin.yml`). Folia's `GlobalRegionScheduler` is used automatically when detected at runtime; Paper/Spigot continue to use the Bukkit scheduler. +- **`compat/` package hierarchy** - clean OOP abstraction layer: + - `compat.platform.PlatformDetector` - detects Folia vs. Paper/Spigot at startup via `RegionizedServer` class presence. + - `compat.scheduler.SchedulerAdapter` / `TaskHandle` - platform-agnostic scheduler interface (`runTask`, `runTaskTimer`, `runTaskLater`, `runTaskAsync`). + - `compat.scheduler.BukkitSchedulerAdapter` - `SchedulerAdapter` backed by `BukkitScheduler` (Paper/Spigot). + - `compat.scheduler.FoliaSchedulerAdapter` - `SchedulerAdapter` backed by `GlobalRegionScheduler` (Folia); only class-loaded when Folia is detected. + - `compat.scheduler.SchedulerAdapterFactory` - chooses the correct adapter with graceful fallback if Folia classes are missing. + - `compat.version.ServerVersionUtil` - canonical location for runtime Minecraft version detection (replaces `util.ServerVersionUtil`). + - `compat.material.MaterialCompat` - canonical location for cross-version material resolution (replaces `util.MaterialCompat`). +- **Startup platform log** - `PluginBootstrap` now logs the active scheduler adapter class on enable (e.g. `Scheduler: BukkitSchedulerAdapter`). + +### Changed + +- `util.ServerVersionUtil` and `util.MaterialCompat` are now `@Deprecated` delegation stubs that forward to their canonical `compat.*` counterparts; they will be removed in a future release. +- All internal scheduler usages (`CountdownManager`, `FireworkShowManager`, `PatternScheduler`, `ChatInputListener`, all `listener.actions.*` GUI action classes, `SpigotIntegration`, `SpigotUpdateChecker`) now go through `SchedulerAdapter` instead of `Bukkit.getScheduler()` / `BukkitRunnable` directly. +- **Scoreboard API modernised** - `ScoreboardDisplay` now delegates to `compat.scoreboard.ScoreboardCompat`. On Paper 1.20.3+ (`Criteria.DUMMY` available) the modern `registerNewObjective(String, Criteria, Component)` overload is used; older builds fall back to the legacy string overload. The deprecated `Scoreboard.resetScores(String)` calls are replaced by objective unregister/re-register via `ScoreboardCompat.resetObjective`. +- **Title API modernised** - `TitleDisplay` now delegates to `compat.title.TitleCompat`. On Paper 1.18+ the Adventure `Player.showTitle(Title)` / `Player.clearTitle()` APIs are preferred over the deprecated `sendTitle(String...)` / `resetTitle()`. `TitleValidator` now also accepts `showTitle` as a valid title capability. A Spigot/legacy string fallback is retained. + +- **Per-player notification targeting** - `NotificationBuilder.players(Collection)` restricts a notification to specific players. `EzCountdownApi.sendNotification(Notification, Collection)` provides an inline alternative without building a `Notification` first. +- **`Countdown.isVisibleTo(Player)`** - centralises per-player visibility logic (permission gate + target-player set) in one method; all display handlers now use it instead of duplicated inline checks. +- **`CountdownBuilder.targetPlayers(Collection)`** - restrict a persistent countdown's display to specific players. +- **Developer-friendly exception hierarchy** - new `com.skyblockexp.ezcountdown.api.exception` package: + - `EzCountdownException` - base unchecked exception; catch this for all EzCountdown API errors. + - `CountdownNotFoundException` - thrown when referencing a countdown name that does not exist; carries `getCountdownName()`. + - `DuplicateCountdownException` - thrown when creating a countdown whose name already exists; carries `getCountdownName()`. + - `InvalidConfigurationException` - thrown by `NotificationBuilder.build()` and future builder validators when configuration is invalid; replaces the generic `IllegalStateException`. +- **`api-version` bumped to `1.18`** in `plugin.yml`; minimum supported Minecraft version is Paper/Spigot 1.18. Dialog display continues to require Paper 1.21.7+. +- **Broadened Java compatibility** - release JARs now target Java 17 bytecode (previously Java 21), so the plugin runs on Paper 1.18 - 1.20.4 (Java 17) as well as Paper 1.20.5+ (Java 21). The `jdk21` Maven profile also targets Java 17 bytecode. +- **`ServerVersionUtil`** - new utility class (`com.skyblockexp.ezcountdown.util.ServerVersionUtil`) for runtime Minecraft version detection; enables future conditional feature gating without hard API dependencies. +- **Startup guard** - `onEnable` now logs the detected MC and Java version and disables the plugin with a clear error message if the server is too old (MC < 1.18 or Java < 17). + +### Changed + +- **All display handlers** (`ActionBarDisplay`, `TitleDisplay`, `ChatDisplay`, `BossBarDisplay`, `ScoreboardDisplay`, `DialogDisplay`) use `countdown.isVisibleTo(player)` instead of duplicated inline permission checks. +- **`NotificationBuilder.build()`** now throws `InvalidConfigurationException` (a subclass of `EzCountdownException`) instead of a plain `IllegalStateException`. +- **`ScoreboardDisplay`** catch blocks now include `NoSuchMethodError` so the scoreboard falls back to chat if the String-criteria `registerNewObjective` overload is ever removed in a future Paper build. +- Project version bumped from `1.4.3` to `2.0.0`. + +### Fixed + +- **DURATION countdown resets to full duration on `/countdown reload`** - `resumeRunningCountdowns()` previously called `handler.onStart()`, which always sets `targetInstant` to `now + fullDuration`, discarding the `target_epoch` saved in storage. It now calls `handler.ensureTarget()` instead, which is a no-op when a target is already present. The same guard was added to the legacy fallback path for handler-less countdown types. End-commands that fired once before a reload will no longer fire again unexpectedly due to the countdown silently restarting. + ## [1.4.2] - 2026-05-16 ### Added -- **Configurable time format** — new `display.time-format` section in `config.yml`. - - `pattern` (default `"{days}d {hours}h {minutes}m {seconds}s"`) — customize the token layout used for the `{formatted}` placeholder and `%ezcountdown__formatted%` PAPI expansion. - - `hide-leading-zeros` (default `true`) — when enabled, leading space-delimited segments whose unit value is zero are suppressed. For example, `0d 0h 5m 3s` is displayed as `5m 3s`. +- **Configurable time format** - new `display.time-format` section in `config.yml`. + - `pattern` (default `"{days}d {hours}h {minutes}m {seconds}s"`) - customize the token layout used for the `{formatted}` placeholder and `%ezcountdown__formatted%` PAPI expansion. + - `hide-leading-zeros` (default `true`) - when enabled, leading space-delimited segments whose unit value is zero are suppressed. For example, `0d 0h 5m 3s` is displayed as `5m 3s`. - Applies to countdown display messages, discord webhook `{time_left}`, PlaceholderAPI, and the GUI preview action. - Hot-reloads with `/countdown reload`. @@ -27,10 +73,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- **Notification API** — `EzCountdownApi.sendNotification(Notification)` fires a one-shot ephemeral timed display from any plugin without creating a persistent countdown or touching `countdowns.yml`. -- **`Notification` model** — lightweight immutable value object with factory methods `Notification.ofSeconds(long)`, `Notification.of(Duration)`, and a fluent `NotificationBuilder` (display types, format message, start/end messages). -- **`NotificationBuilder`** — fluent builder for `Notification`; all fields have sensible defaults (`ACTION_BAR` display, `{formatted}` message); `build()` validates duration > 0. -- **Ephemeral countdown support** — `Countdown.isEphemeral()` / `CountdownBuilder.ephemeral(boolean)`; ephemeral countdowns are removed from memory automatically when they end and are never written to storage. +- **Notification API** - `EzCountdownApi.sendNotification(Notification)` fires a one-shot ephemeral timed display from any plugin without creating a persistent countdown or touching `countdowns.yml`. +- **`Notification` model** - lightweight immutable value object with factory methods `Notification.ofSeconds(long)`, `Notification.of(Duration)`, and a fluent `NotificationBuilder` (display types, format message, start/end messages). +- **`NotificationBuilder`** - fluent builder for `Notification`; all fields have sensible defaults (`ACTION_BAR` display, `{formatted}` message); `build()` validates duration > 0. +- **Ephemeral countdown support** - `Countdown.isEphemeral()` / `CountdownBuilder.ephemeral(boolean)`; ephemeral countdowns are removed from memory automatically when they end and are never written to storage. ## [1.3.2] - 2026-04-27 diff --git a/docs/_sass/color_schemes/ezcountdown.scss b/docs/_sass/color_schemes/ezcountdown.scss index 782cf91..0b7e63f 100644 --- a/docs/_sass/color_schemes/ezcountdown.scss +++ b/docs/_sass/color_schemes/ezcountdown.scss @@ -1,4 +1,4 @@ -// EzCountdown — dark/green color scheme for just-the-docs +// EzCountdown - dark/green color scheme for just-the-docs // // Palette: // Background #111111 (body) @@ -6,7 +6,7 @@ // Elevated #1e1e1e (code blocks, search, table rows) // Border #2a2a2a // Text #c8d4cc (body) / #ffffff (headings) -// Accent #4ade80 (green — links, nav highlight, buttons) +// Accent #4ade80 (green - links, nav highlight, buttons) // Accent dim #22c55e (hover state) $color-scheme: dark; diff --git a/docs/api/EzCountdownApi.md b/docs/api/EzCountdownApi.md index 216222a..0742767 100644 --- a/docs/api/EzCountdownApi.md +++ b/docs/api/EzCountdownApi.md @@ -8,33 +8,57 @@ nav_order: 2 Public service interface exposed by the plugin: `com.skyblockexp.ezcountdown.api.EzCountdownApi` -Methods +## Methods + +### Countdown lifecycle - `boolean startCountdown(String name)` - - Start a configured countdown by name. Returns `true` if started successfully. + - Start a configured countdown by name. Returns `true` if started successfully, `false` if the countdown was already running or does not exist. - `boolean stopCountdown(String name)` - - Stop a running countdown by name. Returns `true` if stopped. + - Stop a running countdown by name. Returns `true` if stopped, `false` if it was already stopped or does not exist. - `Optional getCountdown(String name)` - - Retrieve a countdown configuration/runtime instance by name. + - Retrieve a countdown configuration and runtime state by name. - `Collection listCountdowns()` - List all countdowns currently known to the plugin. +### Countdown management + - `boolean createCountdown(Countdown countdown)` - - Create and persist a new countdown configuration. Returns `true` on success. + - Create and persist a new countdown configuration. Returns `true` on success, `false` if a countdown with the same name already exists. + +- `boolean createCountdown(CountdownType type, long durationSeconds, Collection targetPlayers)` + - Convenience overload: create an ephemeral countdown of the given type and duration, visible only to the specified players. Returns `true` on success. - `boolean deleteCountdown(String name)` - Delete a configured countdown by name. Returns `true` when deleted. +### Notifications + - `Optional sendNotification(Notification notification)` - - Fire a one-shot, ephemeral timed display notification without creating a persistent countdown. The notification runs for its configured duration then disappears automatically — it is never written to `countdowns.yml`. Returns an `Optional` containing the generated internal name on success, or an empty `Optional` on collision. + - Fire a one-shot ephemeral timed display notification. The notification runs for its configured duration then is removed automatically - it is never written to `countdowns.yml` and does not appear in `/countdown list`. Returns an `Optional` containing the generated internal name on success, or an empty `Optional` on name collision. + +- `Optional sendNotification(Notification notification, Collection players)` + - Same as above, but restrict the notification to the specified players. Equivalent to building a `Notification` with `NotificationBuilder.players(players)`. + +## Exceptions -Usage example (service lookup): +Methods return boolean flags or Optional on expected failures (e.g. countdown not found). The API also defines a typed exception hierarchy for use in builders and validation: + +| Exception | Package | Description | +|---|---|---| +| `EzCountdownException` | `api.exception` | Base class for all EzCountdown API exceptions | +| `CountdownNotFoundException` | `api.exception` | A countdown name does not exist; carries `getCountdownName()` | +| `DuplicateCountdownException` | `api.exception` | A countdown with this name already exists; carries `getCountdownName()` | +| `InvalidConfigurationException` | `api.exception` | Thrown by `NotificationBuilder.build()` when duration is missing or invalid | + +## Usage example ```java -RegisteredServiceProvider rsp = Bukkit.getServicesManager().getRegistration(EzCountdownApi.class); +RegisteredServiceProvider rsp = + Bukkit.getServicesManager().getRegistration(EzCountdownApi.class); if (rsp != null) { EzCountdownApi api = rsp.getProvider(); api.createCountdown(myCountdown); @@ -42,8 +66,15 @@ if (rsp != null) { } ``` -Notes +Per-player notification: + +```java +List targets = List.of(player1, player2); +api.sendNotification(Notification.ofSeconds(30), targets); +``` + +## Notes - Changes made via the API are persisted using the plugin's `countdowns.yml` storage. -- Methods return boolean flags to indicate success; check plugin logs or events for failure reasons. -- Countdowns created by `sendNotification` are ephemeral: they run in memory only and are deleted automatically when they end. They are never stored in `countdowns.yml` and do not appear in `/countdown list`. +- Ephemeral countdowns created by `sendNotification` are held in memory only; they are deleted automatically when they end and are never stored in `countdowns.yml`. +- `createCountdown(CountdownType, long, Collection)` creates an ephemeral (in-memory) countdown visible only to the provided players. diff --git a/docs/api/README.md b/docs/api/README.md index c52de25..23ef9a5 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -1,6 +1,6 @@ --- title: Developer API -nav_order: 4 +nav_order: 5 has_children: true --- @@ -8,8 +8,8 @@ has_children: true ## Prerequisites -- Java 21 (matches Paper API classfile version used by the plugin). -- Access to the GitHub Packages repository +- Java 17 or newer. +- Access to the GitHub Packages repository. ## Quick start @@ -17,7 +17,7 @@ Follow these steps to add and use the EzCountdown API from your plugin. ### Installation -1) Add the GitHub Packages repository to your `pom.xml` (replace owner/repo if different): +1) Add the GitHub Packages repository to your `pom.xml`: ```xml @@ -29,30 +29,34 @@ Follow these steps to add and use the EzCountdown API from your plugin. ``` -2) Add the dependency (use the published version tag): +2) Add the dependency: ```xml - com.skyblockexp + com.github.ez-plugins ezcountdown - 1.3.1 + 2.0.0 + provided ``` +> Use `provided` - the plugin jar is already on the server; you do not want to shade it into your own jar. + ## Service lookup ### Getting the API -Use Bukkit's `ServicesManager` to obtain a reference to the `EzCountdownApi` service. The example below shows the common pattern used in other plugins: +Use Bukkit's `ServicesManager` to obtain a reference to the `EzCountdownApi` service: ```java import com.skyblockexp.ezcountdown.api.EzCountdownApi; import org.bukkit.Bukkit; import org.bukkit.plugin.RegisteredServiceProvider; -RegisteredServiceProvider rsp = Bukkit.getServicesManager().getRegistration(EzCountdownApi.class); +RegisteredServiceProvider rsp = + Bukkit.getServicesManager().getRegistration(EzCountdownApi.class); if (rsp == null) { - // EzCountdown not available + // EzCountdown is not installed or not loaded yet return; } EzCountdownApi api = rsp.getProvider(); @@ -62,22 +66,13 @@ EzCountdownApi api = rsp.getProvider(); ### Start/Stop -Start an existing countdown by id: - ```java api.startCountdown("example-countdown"); -``` - -Stop a running countdown: - -```java api.stopCountdown("example-countdown"); ``` ### Create -Create a countdown using `CountdownBuilder`: - ```java import com.skyblockexp.ezcountdown.api.model.Countdown; import com.skyblockexp.ezcountdown.api.model.CountdownType; @@ -104,27 +99,23 @@ api.startCountdown("launch"); ### Inspect -Inspect or list countdowns: - ```java Optional maybe = api.getCountdown("launch"); Collection all = api.listCountdowns(); ``` -### Send a notification +### Send a notification (global) -`sendNotification` fires a one-shot ephemeral display that runs for the specified duration and then vanishes — no YAML entry is created and no `/countdown list` entry appears. - -The simplest usage: +`sendNotification` fires a one-shot ephemeral display that runs for the specified duration and then vanishes - no YAML entry is created and no `/countdown list` entry appears. ```java import com.skyblockexp.ezcountdown.api.model.Notification; -// Show an action bar countdown for 30 seconds (plugin defaults for display). +// Show an action bar countdown for 30 seconds using plugin defaults. api.sendNotification(Notification.ofSeconds(30)); ``` -Use the builder for full control: +Full builder example: ```java import com.skyblockexp.ezcountdown.api.model.Notification; @@ -135,28 +126,69 @@ import java.util.EnumSet; Notification notif = Notification.builder() .duration(Duration.ofMinutes(5)) .displays(EnumSet.of(DisplayType.ACTION_BAR, DisplayType.BOSS_BAR)) - .message("{formatted}") // optional: format message key - .startMessage("Event starts soon!") // optional: broadcast on start - .endMessage("Event started!") // optional: broadcast on end + .message("{formatted}") + .startMessage("Event starts soon!") + .endMessage("Event started!") .build(); Optional handle = api.sendNotification(notif); + +// Stop early if needed: +handle.ifPresent(name -> api.stopCountdown(name)); ``` -`sendNotification` returns the generated internal name wrapped in `Optional.of(...)` on success, or `Optional.empty()` on the rare name collision. You can use the name to stop the notification early: +### Send a notification to specific players + +Two ways to send a notification to a subset of online players: + +**Option A - via the builder:** ```java -handle.ifPresent(name -> api.stopCountdown(name)); +import com.skyblockexp.ezcountdown.api.model.Notification; +import java.util.List; + +List vipPlayers = /* your player collection */; + +Notification notif = Notification.builder() + .duration(60) + .message("VIP event in {formatted}") + .players(vipPlayers) // restrict to these players + .build(); + +api.sendNotification(notif); ``` -## Javadoc & API reference +**Option B - inline (without a pre-built Notification):** + +```java +api.sendNotification(Notification.ofSeconds(60), vipPlayers); +``` -See the generated API docs for full type and method details: `docs/api/EzCountdownApi.md` and the `model/` and `event/` pages in this folder. If you want, I can add a GitHub Pages workflow to publish hosted Javadoc automatically on release. +### Exceptions + +EzCountdown throws typed exceptions from the `com.skyblockexp.ezcountdown.api.exception` package: + +| Exception | When thrown | +|---|---| +| `EzCountdownException` | Base class - catch this to handle any EzCountdown error | +| `CountdownNotFoundException` | A countdown name does not exist | +| `DuplicateCountdownException` | Creating a countdown with an already-used name | +| `InvalidConfigurationException` | `NotificationBuilder.build()` called without a valid duration | + +```java +import com.skyblockexp.ezcountdown.api.exception.EzCountdownException; +import com.skyblockexp.ezcountdown.api.exception.CountdownNotFoundException; + +try { + Notification notif = Notification.builder().build(); // missing duration! +} catch (InvalidConfigurationException e) { + getLogger().warning("Bad notification config: " + e.getMessage()); +} +``` ## Further reading -- API reference: [docs/api/EzCountdownApi.md](docs/api/EzCountdownApi.md) -- Events: [docs/api/event](docs/api/event) -- Models: [docs/api/model](docs/api/model) -- Notification model: [docs/api/model/Notification.md](docs/api/model/Notification.md) +- [EzCountdownApi interface](EzCountdownApi) - full method list +- [Events](event/) - `CountdownStartEvent`, `CountdownTickEvent`, `CountdownEndEvent` +- [Models](model/) - `Countdown`, `Notification`, `CountdownType` diff --git a/docs/api/model/Notification.md b/docs/api/model/Notification.md index 8e74609..2c807fe 100644 --- a/docs/api/model/Notification.md +++ b/docs/api/model/Notification.md @@ -16,17 +16,18 @@ Notifications are used exclusively with [`EzCountdownApi.sendNotification`](../E ## Factory methods -- `static Notification ofSeconds(long seconds)` — Create a notification with the given duration (seconds) and all other settings at their defaults. -- `static Notification of(Duration duration)` — Create a notification from a `java.time.Duration` with all other settings at their defaults. -- `static NotificationBuilder builder()` — Start a fluent `NotificationBuilder`. +- `static Notification ofSeconds(long seconds)` - Create a notification with the given duration (seconds) and all other settings at their defaults. +- `static Notification of(Duration duration)` - Create a notification from a `java.time.Duration` with all other settings at their defaults. +- `static NotificationBuilder builder()` - Start a fluent `NotificationBuilder`. ## Accessors -- `long getDurationSeconds()` — Duration of the notification in seconds (always > 0). -- `EnumSet getDisplayTypes()` — Which displays are shown. Defaults to `ACTION_BAR`. -- `String getFormatMessage()` — Format message key. Defaults to `{formatted}`. -- `String getStartMessage()` — Optional message broadcast when the notification starts. May be `null`. -- `String getEndMessage()` — Optional message broadcast when the notification ends. May be `null`. +- `long getDurationSeconds()` - Duration of the notification in seconds (always > 0). +- `EnumSet getDisplayTypes()` - Which displays are shown. Defaults to `ACTION_BAR`. +- `String getFormatMessage()` - Format message key. Defaults to `{formatted}`. +- `String getStartMessage()` - Optional message broadcast when the notification starts. May be `null`. +- `String getEndMessage()` - Optional message broadcast when the notification ends. May be `null`. +- `Set getTargetPlayers()` - Set of player UUIDs that will receive this notification. `null` means all online players. ## Default values @@ -36,6 +37,7 @@ Notifications are used exclusively with [`EzCountdownApi.sendNotification`](../E | `formatMessage` | `{formatted}` | | `startMessage` | `null` (no broadcast) | | `endMessage` | `null` (no broadcast) | +| `targetPlayers` | `null` (all online players) | ## NotificationBuilder @@ -51,13 +53,49 @@ Obtained via `Notification.builder()`. All methods return `this` for chaining. | `message(String)` | Set the format message; `null` or blank resets to `{formatted}` | | `startMessage(String)` | Set the start broadcast; blank treated as `null` | | `endMessage(String)` | Set the end broadcast; blank treated as `null` | -| `build()` | Build and return the `Notification`. Throws `IllegalStateException` if duration was never set or is ≤ 0. | +| `players(Collection)` | Restrict the notification to these players; `null` or empty targets all online players | +| `build()` | Build and return the `Notification`. Throws `InvalidConfigurationException` if duration was never set or is ≤ 0. | ## Examples One-liner (action bar for 30 s, plugin defaults): ```java +Notification n = Notification.ofSeconds(30); +api.sendNotification(n); +``` + +Full builder - action bar + boss bar for 5 minutes: + +```java +Notification n = Notification.builder() + .duration(Duration.ofMinutes(5)) + .displays(EnumSet.of(DisplayType.ACTION_BAR, DisplayType.BOSS_BAR)) + .message("{formatted}") + .startMessage("Event starts soon!") + .endMessage("Event started!") + .build(); + +Optional handle = api.sendNotification(n); +``` + +Target specific players only: + +```java +List vipPlayers = getVipPlayers(); + +Notification n = Notification.builder() + .duration(60) + .message("VIP reward in {formatted}") + .players(vipPlayers) + .build(); + +api.sendNotification(n); + +// Or inline: +api.sendNotification(Notification.ofSeconds(60), vipPlayers); +``` + Notification notif = Notification.ofSeconds(30); api.sendNotification(notif); ``` diff --git a/docs/commands.md b/docs/commands.md index b11e136..90cd2d5 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -4,74 +4,133 @@ parent: Server Owners nav_order: 1 --- -# Commands (Quick Reference for Server Owners) +# Commands -This page lists the most important commands staff will use to manage countdowns. Keep these in a staff guide or paste them into your control panel. +All commands use the `/countdown` base (aliases: `/ezcountdown`, `/ezcd`). -Core commands +## Command reference + +### `/countdown create` + +Create a new countdown. The command syntax depends on the type: + +| Type | Syntax | Example | +|---|---|---| +| Fixed date | `/countdown create ` | `/countdown create new_year 2026-01-01 00:00` | +| Duration | `/countdown create duration