Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,18 @@ jobs:
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: ${{ matrix.java }}
java-version: '21'
cache: maven

- name: Build plugin JAR
run: mvn -B -ntp -DskipTests package

- name: Switch to runtime JDK ${{ matrix.java }}
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: ${{ matrix.java }}

- name: Resolve latest ${{ matrix.platform }} build for MC ${{ matrix.mc-prefix }}.x
id: server
run: |
Expand Down Expand Up @@ -214,7 +220,7 @@ jobs:
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: ${{ matrix.java }}
java-version: '21'
cache: maven

- name: Configure Git for BuildTools
Expand All @@ -225,6 +231,12 @@ jobs:
- name: Build plugin JAR
run: mvn -B -ntp -DskipTests package

- name: Switch to runtime JDK ${{ matrix.java }}
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: ${{ matrix.java }}

- name: Restore cached Spigot JAR (MC ${{ matrix.mc-prefix }}, Java ${{ matrix.java }})
id: spigot-cache
uses: actions/cache@v5
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- **Placeholder formatted output regression tests** - added unit coverage in `EzCountdownPlaceholderExpansionUnitTest` to verify `%ezcountdown_<name>_formatted%` honors `display.time-format.hide-leading-zeros` for both enabled and disabled configurations.
- **Optional countdown start/end sounds** - countdowns can now define `sounds.start` and `sounds.end` with Bukkit `Sound` enum names in `countdowns.yml`.
- **GUI sound editors** - the in-game editor now includes dedicated actions to configure and validate start/end sounds, including `none` to disable either sound.

### Changed

- **Runtime start/end flow** - when a configured start/end sound is valid, EzCountdown now plays it to online players at countdown start/end alongside existing message, teleport, firework, and command actions.

## [2.0.1] - 2026-05-22

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ EzCountdown provides configurable countdown timers for events, launches, and mai
## Features
- Fixed date, duration, recurring, and manual countdown modes.
- Display options: action bar, boss bar (1.9+), chat, title, and scoreboard.
- Optional start/end sounds per countdown (`sounds.start`, `sounds.end`) using Bukkit `Sound` names.
- PlaceholderAPI support for `%ezcountdown_<name>_days%`, `_hours`, `_minutes`, `_seconds`, and `_formatted`.
- Customizable messages (messages.yml), permissions, and update intervals.
- Run console commands when countdowns finish.
- GUI editor support for start/end messages, display modes, end commands, and start/end sounds.
- **Developer API**: send ephemeral one-shot notifications from your plugin without touching YAML (see [API overview](docs/api/README.md)).

## Commands
Expand Down
2 changes: 2 additions & 0 deletions docs/api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ Countdown countdown = CountdownBuilder.builder("launch")
.formatMessage("countdown.format")
.startMessage("Server Launching")
.endMessage("Server Launched")
.startSound("ENTITY_PLAYER_LEVELUP")
.endSound("BLOCK_NOTE_BLOCK_PLING")
.zoneId(ZoneId.systemDefault())
.duration(Duration.ofMinutes(10))
.build();
Expand Down
4 changes: 4 additions & 0 deletions docs/api/model/Countdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ Important accessors and behavior:
- `String getFormatMessage()` - format string used for display messages.
- `String getStartMessage()` - message broadcasted on start (if configured).
- `String getEndMessage()` - message broadcasted on end (if configured).
- `String getStartSound()` / `void setStartSound(String)` - optional Bukkit `Sound` enum name for start.
- `String getEndSound()` / `void setEndSound(String)` - optional Bukkit `Sound` enum name for end.
- `List<String> getEndCommands()` - commands executed when countdown ends.
- `ZoneId getZoneId()` - timezone used when resolving fixed/recurring dates.

Expand All @@ -39,3 +41,5 @@ Runtime fields (mutable):
Notes

- The constructor establishes the immutable configuration; runtime fields are updated by the plugin when starting/stopping the countdown.
- `startSound` / `endSound` are optional mutable fields. Passing `null` or blank disables that phase sound.
- YAML-backed countdowns map these fields to `sounds.start` and `sounds.end`.
49 changes: 49 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,52 @@ Set per-countdown inside `countdowns.yml` under `display.bossbar`:

---

## Start/end sounds

Set optional per-countdown sounds in `countdowns.yml`:

| Key | Type | Default | Description |
|---|---|---|---|
| `sounds.start` | string | not set | Bukkit `Sound` enum name played when the countdown starts |
| `sounds.end` | string | not set | Bukkit `Sound` enum name played when the countdown ends |

Notes:

- Sound names must match Bukkit `Sound` enum values (for example `ENTITY_PLAYER_LEVELUP`).
- If a sound key is missing or blank, no sound is played for that phase.
- If an invalid sound is configured, EzCountdown logs a warning and continues other start/end actions.
- The GUI sound editor now prints the full list of sounds available on your running server version.

Common choices:

- `ENTITY_PLAYER_LEVELUP` - positive "start" cue.
- `BLOCK_NOTE_BLOCK_PLING` - short neutral confirmation.
- `ENTITY_EXPERIENCE_ORB_PICKUP` - subtle reward-like ping.
- `UI_BUTTON_CLICK` - lightweight interface-style sound.
- `BLOCK_BELL_USE` - event bell/chime.
- `ENTITY_FIREWORK_ROCKET_BLAST` - celebratory end sound.
- `BLOCK_RESPAWN_ANCHOR_CHARGE` - energetic build-up tone.
- `ENTITY_ENDER_DRAGON_GROWL` - dramatic finale.

Full references:

- Spigot `Sound` enum: https://hub.spigotmc.org/javadocs/spigot/org/bukkit/Sound.html
- Paper `Sound` enum: https://jd.papermc.io/paper/1.21/org/bukkit/Sound.html

Example:

```yaml
countdowns:
arena_event:
type: DURATION
duration: "15m"
sounds:
start: "ENTITY_PLAYER_LEVELUP"
end: "BLOCK_NOTE_BLOCK_PLING"
```

---

## Clock-aligned recurring options

Set these per-countdown in `countdowns.yml`:
Expand Down Expand Up @@ -118,6 +164,9 @@ countdowns:
format: "New Year in {formatted}"
start: "Countdown started!"
end: "Happy New Year!"
sounds:
start: "ENTITY_PLAYER_LEVELUP"
end: "BLOCK_NOTE_BLOCK_PLING"
commands_on_end:
- "broadcast &6Happy New Year!"
teleport:
Expand Down
1 change: 1 addition & 0 deletions docs/feature/gui.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Opening the GUI
- `Main` - see a list of configured countdowns, their running state, and quick start/stop/delete actions.
- `Display Editor` - pick which `display.types` will show for a countdown and edit per-display settings. For the `BOSS_BAR` display the editor also exposes `color` and `style` options so you can match event themes or improve visibility; the GUI validates input and will show an error if an invalid option is selected.
- `Messages` - edit `messages.format`, `messages.start`, and `messages.end` templates with live preview.
- `Sounds` - edit `sounds.start` and `sounds.end` using Bukkit `Sound` enum names. The editor validates values, supports `none` to disable either sound, and prints the full list of available sounds from your current server version before input.
- `Commands Editor` - add, remove, or reorder `commands.end` to run console commands when a countdown completes.

Behaviour & saving
Expand Down
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ A feature-rich countdown plugin for Paper/Spigot servers. Create countdowns for
- **Discord webhooks** - post start/end notifications to a Discord channel.
- **In-game GUI** - manage countdowns visually without editing YAML.
- **Firework shows** - trigger configurable firework displays on start or end.
- **Start/end sounds** - optionally play configurable Bukkit sounds when countdowns start or finish.
- **Teleport actions** - move all online players to a named location when a countdown starts or ends.
- **Console commands on end** - run any command when a countdown completes.
- **Developer API** - create, start, stop, listen to countdown events, and send per-player notifications from other plugins.
Expand Down
4 changes: 3 additions & 1 deletion docs/topics/bbcode-topic.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[CENTER][SIZE=6][B]EzCountdown[/B][/SIZE]
[SIZE=3]Run flash sales, timed events, and launches with stunning countdowns - ActionBar, BossBar, Titles & Placeholders![/SIZE]
[SIZE=2]Spigot, Paper, or Bukkit 1.7-1.21.* • Action bar, boss bar, title, chat, scoreboard • PlaceholderAPI ready[/SIZE][/CENTER]
[SIZE=2]Paper/Spigot 1.18+ • Java 17+ • Action bar, boss bar, title, chat, scoreboard • PlaceholderAPI ready[/SIZE][/CENTER]

Found an issue or have a question? Please contact me through [URL='https://discord.gg/yWP95XfmBS']the EzPlugins Discord server[/URL].

Expand All @@ -9,10 +9,12 @@ Found an issue or have a question? Please contact me through [URL='https://disco
[*][B]Perfect for flash sales & events[/B] - Instantly set up countdowns for shop discounts, launches, or any special event.
[*][B]Multiple countdown types[/B] - Fixed dates, durations, recurring schedules, or manual timers. Great for launches, resets, or surprise sales.
[*][B]Flexible displays[/B] - Show timers in action bar, boss bar, title, chat, or scoreboard. Make sure every player sees the excitement.
[*][B]Start/end sounds[/B] - Configure optional [icode]sounds.start[/icode] and [icode]sounds.end[/icode] per countdown using Bukkit sound names.
[*][B]Firework shows[/B] - Optional, configurable firework displays for celebration moments. ([COLOR=#ff0000]new[/COLOR])
[*][B]Permission control[/B] - Limit visibility or management to specific ranks. Keep control in the hands of your staff or VIPs.
[*][B]PlaceholderAPI support[/B] - Use %ezcountdown_<name>_formatted% and time-part placeholders in any plugin or message.
[*][B]Config-driven[/B] - Tune update intervals, messaging, and defaults in YAML. No coding required, just edit and reload.
[*][B]GUI sound editor[/B] - Set or disable countdown sounds directly in the editor with input validation.
[*][B]Translation variables[/B] - Centralize message text in `messages.yml` and reference keys from `countdowns.yml` using `{translate:key.path}` so owners can reset countdowns without losing localized text.
[/LIST]

Expand Down
5 changes: 3 additions & 2 deletions docs/topics/markdown-topic.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@

**EzCountdown** is the ultimate custom countdown timer plugin for Minecraft servers. Whether you’re launching a new map, running a flash sale, or celebrating a special event, EzCountdown lets you create, manage, and display any countdown you want, your way.

**Compatible with Spigot, Paper, and Bukkit 1.7–1.21 · Java 8+ · Action bar, boss bar, title, chat & scoreboard displays · PlaceholderAPI ready**
**Compatible with Paper/Spigot 1.18+ · Java 17+ · Action bar, boss bar, title, chat & scoreboard displays · PlaceholderAPI ready**

---

## Why EzCountdown?

- **Create any countdown** – Set up timers for launches, events, sales, or anything you can imagine. Fixed dates, durations, recurring, or manual, your choice.
- **Flexible displays** – Show countdowns in the action bar, boss bar, title, chat, or scoreboard. Make your timers visible everywhere players look.
- **Start/end sounds** – Configure optional `sounds.start` and `sounds.end` per countdown using Bukkit sound names.
- **Easy configuration** – Define countdowns in YAML, customize messages, and reload instantly - no coding required.
- **Easy configuration** – Define countdowns in YAML, customize messages, and reload instantly - no coding required.
- **GUI sound editor** – Set or disable start/end sounds directly from the in-game editor with validation.
- **Translation variables** – You can keep message text in `messages.yml` and reference it from `countdowns.yml` with the `{translate:key.path}` token. This lets you centralize translations and makes resetting `countdowns.yml` safe because messages are pulled from `messages.yml` at runtime.
- **Permission control** – Limit who can see or manage each countdown.
- **PlaceholderAPI support** – Use countdown placeholders in any plugin or message.
Expand Down
28 changes: 28 additions & 0 deletions src/main/java/com/skyblockexp/ezcountdown/api/model/Countdown.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ public final class Countdown {
/** Message template shown when the countdown ends. */
private final String endMessage;

/** Optional Bukkit sound name to play when the countdown starts. */
private String startSound;

/** Optional Bukkit sound name to play when the countdown ends. */
private String endSound;

/** Console commands to execute when the countdown completes. */
private final java.util.List<String> endCommands;

Expand Down Expand Up @@ -129,6 +135,8 @@ public Countdown(String name,
this.formatMessage = formatMessage;
this.startMessage = startMessage;
this.endMessage = endMessage;
this.startSound = null;
this.endSound = null;
this.endCommands = endCommands == null ? java.util.List.of() : java.util.List.copyOf(endCommands);
this.zoneId = zoneId;
this.alignToClock = false;
Expand Down Expand Up @@ -188,6 +196,8 @@ public Countdown(String name,
this.formatMessage = formatMessage;
this.startMessage = startMessage;
this.endMessage = endMessage;
this.startSound = null;
this.endSound = null;
this.endCommands = endCommands == null ? java.util.List.of() : java.util.List.copyOf(endCommands);
this.zoneId = zoneId;
this.autoRestart = autoRestart;
Expand Down Expand Up @@ -229,6 +239,8 @@ public Countdown(String name,
this.formatMessage = formatMessage;
this.startMessage = startMessage;
this.endMessage = endMessage;
this.startSound = null;
this.endSound = null;
this.endCommands = endCommands == null ? java.util.List.of() : java.util.List.copyOf(endCommands);
this.zoneId = zoneId;
this.autoRestart = autoRestart;
Expand Down Expand Up @@ -267,6 +279,22 @@ public Countdown(String name,
/** @return end message template */
public String getEndMessage() { return endMessage; }

/** @return optional sound name to play on start, or null when disabled */
public String getStartSound() { return startSound; }

/** Set optional start sound name; null/blank disables start sound. */
public void setStartSound(String startSound) {
this.startSound = (startSound == null || startSound.isBlank()) ? null : startSound;
}

/** @return optional sound name to play on end, or null when disabled */
public String getEndSound() { return endSound; }

/** Set optional end sound name; null/blank disables end sound. */
public void setEndSound(String endSound) {
this.endSound = (endSound == null || endSound.isBlank()) ? null : endSound;
}

/** @return immutable copy of end commands */
public java.util.List<String> getEndCommands() { return java.util.List.copyOf(endCommands); }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public final class CountdownBuilder {
private String formatMessage = null;
private String startMessage = null;
private String endMessage = null;
private String startSound = null;
private String endSound = null;
private List<String> endCommands = List.of();
private ZoneId zoneId = ZoneId.systemDefault();
private boolean autoRestart = false;
Expand Down Expand Up @@ -99,6 +101,16 @@ public CountdownBuilder endMessage(String msg) {
return this;
}

public CountdownBuilder startSound(String sound) {
this.startSound = sound;
return this;
}

public CountdownBuilder endSound(String sound) {
this.endSound = sound;
return this;
}

public CountdownBuilder endCommands(List<String> commands) {
this.endCommands = commands == null ? List.of() : List.copyOf(commands);
return this;
Expand Down Expand Up @@ -188,6 +200,8 @@ public Countdown build() {
if (recurringMonth > 0) countdown.setRecurringMonth(recurringMonth);
if (recurringDay > 0) countdown.setRecurringDay(recurringDay);
if (recurringTime != null) countdown.setRecurringTime(recurringTime);
countdown.setStartSound(startSound);
countdown.setEndSound(endSound);
if (ephemeral) countdown.setEphemeral(true);
if (targetPlayers != null) countdown.setTargetPlayers(targetPlayers);

Expand Down
26 changes: 25 additions & 1 deletion src/main/java/com/skyblockexp/ezcountdown/gui/EditorMenu.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ public EditorMenu(CountdownManager manager, ChatInputListener chatInputListener,

public void openEditor(Player player, Countdown countdown) {
String title = PREFIX + countdown.getName();
Inventory inv = Bukkit.createInventory(null, 9, title);
Inventory inv = Bukkit.createInventory(null, 18, title);
ItemStack run = new ItemStack(countdown.isRunning() ? MaterialCompat.resolve("LIME_CONCRETE", "LIME_WOOL", "WOOL") : MaterialCompat.resolve("RED_CONCRETE", "RED_WOOL", "WOOL"));
ItemMeta rm = run.getItemMeta();
rm.setDisplayName((countdown.isRunning() ? ChatColor.GREEN : ChatColor.RED) + "Toggle Running");
Expand Down Expand Up @@ -107,6 +107,30 @@ public void openEditor(Player player, Countdown countdown) {
end.setItemMeta(em);
inv.setItem(8, end);

ItemStack startSound = new ItemStack(Material.NOTE_BLOCK);
ItemMeta ssm = startSound.getItemMeta();
ssm.setDisplayName(ChatColor.AQUA + "Edit Start Sound");
String startSoundName = countdown.getStartSound() == null ? "(none)" : countdown.getStartSound();
ssm.setLore(List.of(
ChatColor.GRAY + "Current: " + startSoundName,
ChatColor.DARK_GRAY + "Type a Bukkit Sound enum name",
ChatColor.DARK_GRAY + "Type 'none' to disable"
));
startSound.setItemMeta(ssm);
inv.setItem(9, startSound);

ItemStack endSound = new ItemStack(MaterialCompat.resolve("JUKEBOX", "NOTE_BLOCK"));
ItemMeta esm = endSound.getItemMeta();
esm.setDisplayName(ChatColor.RED + "Edit End Sound");
String endSoundName = countdown.getEndSound() == null ? "(none)" : countdown.getEndSound();
esm.setLore(List.of(
ChatColor.GRAY + "Current: " + endSoundName,
ChatColor.DARK_GRAY + "Type a Bukkit Sound enum name",
ChatColor.DARK_GRAY + "Type 'none' to disable"
));
endSound.setItemMeta(esm);
inv.setItem(10, endSound);

player.openInventory(inv);
}

Expand Down
Loading
Loading