diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5fbfac0..9d72813 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: | @@ -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 @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index e0adde7..d9c814c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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__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 diff --git a/README.md b/README.md index 65415c8..d83a161 100644 --- a/README.md +++ b/README.md @@ -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__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 diff --git a/docs/api/README.md b/docs/api/README.md index 23ef9a5..216089a 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -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(); diff --git a/docs/api/model/Countdown.md b/docs/api/model/Countdown.md index 052468e..a967a1d 100644 --- a/docs/api/model/Countdown.md +++ b/docs/api/model/Countdown.md @@ -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 getEndCommands()` - commands executed when countdown ends. - `ZoneId getZoneId()` - timezone used when resolving fixed/recurring dates. @@ -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`. diff --git a/docs/configuration.md b/docs/configuration.md index 89f9a18..3a33df8 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -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`: @@ -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: diff --git a/docs/feature/gui.md b/docs/feature/gui.md index 930286d..548b0bb 100644 --- a/docs/feature/gui.md +++ b/docs/feature/gui.md @@ -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 diff --git a/docs/index.md b/docs/index.md index 52ccaaa..a5c1804 100644 --- a/docs/index.md +++ b/docs/index.md @@ -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. diff --git a/docs/topics/bbcode-topic.txt b/docs/topics/bbcode-topic.txt index 18af91d..18950ea 100644 --- a/docs/topics/bbcode-topic.txt +++ b/docs/topics/bbcode-topic.txt @@ -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]. @@ -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__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] diff --git a/docs/topics/markdown-topic.md b/docs/topics/markdown-topic.md index 09f2ca6..93f9c1f 100644 --- a/docs/topics/markdown-topic.md +++ b/docs/topics/markdown-topic.md @@ -2,7 +2,7 @@ **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** --- @@ -10,8 +10,9 @@ - **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. diff --git a/src/main/java/com/skyblockexp/ezcountdown/api/model/Countdown.java b/src/main/java/com/skyblockexp/ezcountdown/api/model/Countdown.java index 2792050..4f2b83d 100644 --- a/src/main/java/com/skyblockexp/ezcountdown/api/model/Countdown.java +++ b/src/main/java/com/skyblockexp/ezcountdown/api/model/Countdown.java @@ -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 endCommands; @@ -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; @@ -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; @@ -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; @@ -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 getEndCommands() { return java.util.List.copyOf(endCommands); } diff --git a/src/main/java/com/skyblockexp/ezcountdown/api/model/CountdownBuilder.java b/src/main/java/com/skyblockexp/ezcountdown/api/model/CountdownBuilder.java index 4c4f601..9b9d156 100644 --- a/src/main/java/com/skyblockexp/ezcountdown/api/model/CountdownBuilder.java +++ b/src/main/java/com/skyblockexp/ezcountdown/api/model/CountdownBuilder.java @@ -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 endCommands = List.of(); private ZoneId zoneId = ZoneId.systemDefault(); private boolean autoRestart = false; @@ -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 commands) { this.endCommands = commands == null ? List.of() : List.copyOf(commands); return this; @@ -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); diff --git a/src/main/java/com/skyblockexp/ezcountdown/gui/EditorMenu.java b/src/main/java/com/skyblockexp/ezcountdown/gui/EditorMenu.java index b808031..16be27b 100644 --- a/src/main/java/com/skyblockexp/ezcountdown/gui/EditorMenu.java +++ b/src/main/java/com/skyblockexp/ezcountdown/gui/EditorMenu.java @@ -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"); @@ -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); } diff --git a/src/main/java/com/skyblockexp/ezcountdown/listener/actions/EditEndSoundAction.java b/src/main/java/com/skyblockexp/ezcountdown/listener/actions/EditEndSoundAction.java new file mode 100644 index 0000000..2dd7779 --- /dev/null +++ b/src/main/java/com/skyblockexp/ezcountdown/listener/actions/EditEndSoundAction.java @@ -0,0 +1,55 @@ +package com.skyblockexp.ezcountdown.listener.actions; + +import com.skyblockexp.ezcountdown.api.model.Countdown; +import com.skyblockexp.ezcountdown.listener.ChatInputListener; +import com.skyblockexp.ezcountdown.manager.CountdownManager; +import com.skyblockexp.ezcountdown.manager.MessageManager; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; + +import java.util.Locale; +import java.util.Optional; + +public final class EditEndSoundAction implements GuiAction { + private final CountdownManager manager; + private final MessageManager messageManager; + private final ChatInputListener chatInputListener; + private final com.skyblockexp.ezcountdown.bootstrap.Registry registry; + + public EditEndSoundAction(CountdownManager manager, MessageManager messageManager, ChatInputListener chatInputListener, com.skyblockexp.ezcountdown.bootstrap.Registry registry) { + this.manager = manager; + this.messageManager = messageManager; + this.chatInputListener = chatInputListener; + this.registry = registry; + } + + @Override + public ActionResult handle(InventoryClickEvent event, Player player, String cdName, Optional countdownOpt) { + if (countdownOpt.isEmpty()) return ActionResult.none(); + Countdown cd = countdownOpt.get(); + player.sendMessage(org.bukkit.ChatColor.GRAY + "Enter end sound (Bukkit Sound enum) or 'none' to disable."); + SoundEditorHelper.sendAvailableSounds(player); + chatInputListener.request(player, input -> { + String value = input == null ? "" : input.trim(); + if (value.equalsIgnoreCase("none") || value.isBlank()) { + cd.setEndSound(null); + manager.save(); + player.sendMessage(messageManager.message("gui.edit.saved", java.util.Map.of("name", cdName))); + registry.scheduler().runTask(() -> registry.gui().editorMenu().openEditor(player, cd)); + return; + } + try { + Sound.valueOf(value.toUpperCase(Locale.ROOT)); + cd.setEndSound(value.toUpperCase(Locale.ROOT)); + manager.save(); + player.sendMessage(messageManager.message("gui.edit.saved", java.util.Map.of("name", cdName))); + registry.scheduler().runTask(() -> registry.gui().editorMenu().openEditor(player, cd)); + } catch (IllegalArgumentException ex) { + player.sendMessage(org.bukkit.ChatColor.RED + "Invalid sound name: " + value); + SoundEditorHelper.sendAvailableSounds(player); + } + }); + return ActionResult.handled(); + } +} diff --git a/src/main/java/com/skyblockexp/ezcountdown/listener/actions/EditStartSoundAction.java b/src/main/java/com/skyblockexp/ezcountdown/listener/actions/EditStartSoundAction.java new file mode 100644 index 0000000..322163f --- /dev/null +++ b/src/main/java/com/skyblockexp/ezcountdown/listener/actions/EditStartSoundAction.java @@ -0,0 +1,55 @@ +package com.skyblockexp.ezcountdown.listener.actions; + +import com.skyblockexp.ezcountdown.api.model.Countdown; +import com.skyblockexp.ezcountdown.listener.ChatInputListener; +import com.skyblockexp.ezcountdown.manager.CountdownManager; +import com.skyblockexp.ezcountdown.manager.MessageManager; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryClickEvent; + +import java.util.Locale; +import java.util.Optional; + +public final class EditStartSoundAction implements GuiAction { + private final CountdownManager manager; + private final MessageManager messageManager; + private final ChatInputListener chatInputListener; + private final com.skyblockexp.ezcountdown.bootstrap.Registry registry; + + public EditStartSoundAction(CountdownManager manager, MessageManager messageManager, ChatInputListener chatInputListener, com.skyblockexp.ezcountdown.bootstrap.Registry registry) { + this.manager = manager; + this.messageManager = messageManager; + this.chatInputListener = chatInputListener; + this.registry = registry; + } + + @Override + public ActionResult handle(InventoryClickEvent event, Player player, String cdName, Optional countdownOpt) { + if (countdownOpt.isEmpty()) return ActionResult.none(); + Countdown cd = countdownOpt.get(); + player.sendMessage(org.bukkit.ChatColor.GRAY + "Enter start sound (Bukkit Sound enum) or 'none' to disable."); + SoundEditorHelper.sendAvailableSounds(player); + chatInputListener.request(player, input -> { + String value = input == null ? "" : input.trim(); + if (value.equalsIgnoreCase("none") || value.isBlank()) { + cd.setStartSound(null); + manager.save(); + player.sendMessage(messageManager.message("gui.edit.saved", java.util.Map.of("name", cdName))); + registry.scheduler().runTask(() -> registry.gui().editorMenu().openEditor(player, cd)); + return; + } + try { + Sound.valueOf(value.toUpperCase(Locale.ROOT)); + cd.setStartSound(value.toUpperCase(Locale.ROOT)); + manager.save(); + player.sendMessage(messageManager.message("gui.edit.saved", java.util.Map.of("name", cdName))); + registry.scheduler().runTask(() -> registry.gui().editorMenu().openEditor(player, cd)); + } catch (IllegalArgumentException ex) { + player.sendMessage(org.bukkit.ChatColor.RED + "Invalid sound name: " + value); + SoundEditorHelper.sendAvailableSounds(player); + } + }); + return ActionResult.handled(); + } +} diff --git a/src/main/java/com/skyblockexp/ezcountdown/listener/actions/GuiActionRegistry.java b/src/main/java/com/skyblockexp/ezcountdown/listener/actions/GuiActionRegistry.java index 6c001a1..902fd65 100644 --- a/src/main/java/com/skyblockexp/ezcountdown/listener/actions/GuiActionRegistry.java +++ b/src/main/java/com/skyblockexp/ezcountdown/listener/actions/GuiActionRegistry.java @@ -24,6 +24,8 @@ public class GuiActionRegistry { private final EditStartMessageAction editStartMessageAction; private final EditStartCountdownTargetAction editStartCountdownTargetAction; private final EditEndMessageAction editEndMessageAction; + private final EditStartSoundAction editStartSoundAction; + private final EditEndSoundAction editEndSoundAction; private final ToggleDisplayTypeAction toggleDisplayTypeAction; private final CommandsEditorActions commandsEditorActions; @@ -40,6 +42,8 @@ public GuiActionRegistry(CountdownManager manager, MessageManager messageManager this.editStartMessageAction = new EditStartMessageAction(manager, messageManager, chatInputListener, registry); this.editStartCountdownTargetAction = new EditStartCountdownTargetAction(manager, messageManager, chatInputListener, registry); this.editEndMessageAction = new EditEndMessageAction(manager, messageManager, chatInputListener, registry); + this.editStartSoundAction = new EditStartSoundAction(manager, messageManager, chatInputListener, registry); + this.editEndSoundAction = new EditEndSoundAction(manager, messageManager, chatInputListener, registry); this.toggleDisplayTypeAction = new ToggleDisplayTypeAction(manager, messageManager, displayEditor); this.commandsEditorActions = new CommandsEditorActions(manager, messageManager, chatInputListener, commandsEditor, registry); } @@ -64,6 +68,8 @@ public Optional forEditorSlot(int slot) { case 6 -> Optional.of(editStartMessageAction); case 7 -> Optional.of(editStartCountdownTargetAction); case 8 -> Optional.of(editEndMessageAction); + case 9 -> Optional.of(editStartSoundAction); + case 10 -> Optional.of(editEndSoundAction); default -> Optional.empty(); }; } diff --git a/src/main/java/com/skyblockexp/ezcountdown/listener/actions/SoundEditorHelper.java b/src/main/java/com/skyblockexp/ezcountdown/listener/actions/SoundEditorHelper.java new file mode 100644 index 0000000..ca1821a --- /dev/null +++ b/src/main/java/com/skyblockexp/ezcountdown/listener/actions/SoundEditorHelper.java @@ -0,0 +1,50 @@ +package com.skyblockexp.ezcountdown.listener.actions; + +import org.bukkit.ChatColor; +import org.bukkit.Sound; +import org.bukkit.entity.Player; + +import java.util.Arrays; + +final class SoundEditorHelper { + private SoundEditorHelper() {} + + static void sendAvailableSounds(Player player) { + String[] names = Arrays.stream(Sound.values()) + .map(SoundEditorHelper::soundName) + .sorted() + .toArray(String[]::new); + + player.sendMessage(ChatColor.AQUA + "Available sounds (" + names.length + "):"); + player.sendMessage(ChatColor.DARK_GRAY + "Use one exact enum value; input is case-insensitive."); + + final int perLine = 6; + for (int i = 0; i < names.length; i += perLine) { + StringBuilder line = new StringBuilder(ChatColor.YELLOW.toString()); + int end = Math.min(i + perLine, names.length); + for (int j = i; j < end; j++) { + if (j > i) { + line.append(ChatColor.GRAY).append(", ").append(ChatColor.YELLOW); + } + line.append(names[j]); + } + player.sendMessage(line.toString()); + } + } + + private static String soundName(Sound sound) { + if (sound == null) { + return ""; + } + try { + java.lang.reflect.Method nameMethod = sound.getClass().getMethod("name"); + Object value = nameMethod.invoke(sound); + if (value instanceof String s) { + return s; + } + } catch (Exception ignored) { + // Fallback for API variants where Sound is not a Java enum type. + } + return String.valueOf(sound); + } +} diff --git a/src/main/java/com/skyblockexp/ezcountdown/manager/CountdownManager.java b/src/main/java/com/skyblockexp/ezcountdown/manager/CountdownManager.java index dcfb909..3b488fc 100644 --- a/src/main/java/com/skyblockexp/ezcountdown/manager/CountdownManager.java +++ b/src/main/java/com/skyblockexp/ezcountdown/manager/CountdownManager.java @@ -24,6 +24,7 @@ import com.skyblockexp.ezcountdown.integration.discord.DiscordWebhookSender; import com.skyblockexp.ezcountdown.compat.scheduler.TaskHandle; import org.bukkit.Bukkit; +import org.bukkit.Sound; import com.skyblockexp.ezcountdown.bootstrap.Registry; import com.skyblockexp.ezcountdown.manager.LocationManager; import com.skyblockexp.ezcountdown.type.CountdownTypeHandler; @@ -459,6 +460,7 @@ private void fireStart(Countdown countdown) { displayManager.broadcastMessage(messageManager.formatWithPrefix(message, Map.of("name", countdown.getName()))); } + playConfiguredSound(countdown.getStartSound(), countdown.getName(), "start"); // Teleport players if configured String teleportLocation = getTeleportLocation(countdown, "start"); if (teleportLocation != null) { @@ -492,6 +494,7 @@ private void fireEnd(Countdown countdown) { displayManager.broadcastMessage(messageManager.formatWithPrefix(message, Map.of("name", countdown.getName()))); } + playConfiguredSound(countdown.getEndSound(), countdown.getName(), "end"); // Teleport players if configured String teleportLocation = getTeleportLocation(countdown, "end"); if (teleportLocation != null) { @@ -633,6 +636,26 @@ private void executeEndCommands(Countdown countdown) { } } + private void playConfiguredSound(String configuredSound, String countdownName, String phase) { + if (configuredSound == null || configuredSound.isBlank()) { + return; + } + final Sound sound; + try { + sound = Sound.valueOf(configuredSound.toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException ex) { + registry.plugin().getLogger().warning("Invalid " + phase + " sound '" + configuredSound + "' for countdown '" + countdownName + "'."); + return; + } + for (Player player : Bukkit.getOnlinePlayers()) { + try { + player.playSound(player.getLocation(), sound, 1.0f, 1.0f); + } catch (Exception ignored) { + // Best effort; continue delivering to other players. + } + } + } + private String normalizeName(String name) { return name == null ? "" : name.toLowerCase(Locale.ROOT); } diff --git a/src/main/java/com/skyblockexp/ezcountdown/storage/YamlCountdownStorage.java b/src/main/java/com/skyblockexp/ezcountdown/storage/YamlCountdownStorage.java index e47a0fb..ec4a4e4 100644 --- a/src/main/java/com/skyblockexp/ezcountdown/storage/YamlCountdownStorage.java +++ b/src/main/java/com/skyblockexp/ezcountdown/storage/YamlCountdownStorage.java @@ -100,6 +100,8 @@ public void saveCountdowns(Collection countdowns) { section.set("messages.format", countdown.getFormatMessage()); section.set("messages.start", countdown.getStartMessage()); section.set("messages.end", countdown.getEndMessage()); + section.set("sounds.start", countdown.getStartSound()); + section.set("sounds.end", countdown.getEndSound()); section.set("commands.end", countdown.getEndCommands()); section.set("zone", countdown.getZoneId().getId()); section.set("timezone", countdown.getZoneId().getId()); @@ -152,6 +154,8 @@ private Countdown parseCountdown(String name, ConfigurationSection section, List String format = section.getString("messages.format", defaults.formatMessage()); String start = section.getString("messages.start", defaults.startMessage()); String end = section.getString("messages.end", defaults.endMessage()); + String startSound = section.getString("sounds.start", null); + String endSound = section.getString("sounds.end", null); List endCommands = section.getStringList("commands.end").stream().filter(command -> command != null && !command.isBlank()).toList(); String zoneKey = section.isSet("timezone") ? section.getString("timezone") : section.getString("zone", defaults.zoneId().getId()); ZoneId zoneId = ZoneId.of(zoneKey); @@ -189,6 +193,8 @@ private Countdown parseCountdown(String name, ConfigurationSection section, List } Countdown countdown = new Countdown(name, type, displayTypes, updateInterval, visibility, format, start, end, endCommands, zoneId, autoRestart, startCountdown, restartDelay, alignToClock, alignInterval, missedPolicy, bossColor, bossStyle); + countdown.setStartSound(startSound); + countdown.setEndSound(endSound); countdown.setRunning(section.getBoolean("running", type != CountdownType.MANUAL)); switch (type) { diff --git a/src/main/java/com/skyblockexp/ezcountdown/type/DurationHandler.java b/src/main/java/com/skyblockexp/ezcountdown/type/DurationHandler.java index 9e4d80a..5b58071 100644 --- a/src/main/java/com/skyblockexp/ezcountdown/type/DurationHandler.java +++ b/src/main/java/com/skyblockexp/ezcountdown/type/DurationHandler.java @@ -40,6 +40,8 @@ public Countdown parse(String name, ConfigurationSection section, CountdownDefau String format = section.getString("messages.format", defaults.formatMessage()); String start = section.getString("messages.start", defaults.startMessage()); String end = section.getString("messages.end", defaults.endMessage()); + String startSound = section.getString("sounds.start", null); + String endSound = section.getString("sounds.end", null); List endCommands = section.getStringList("commands.end").stream().filter(c -> c != null && !c.isBlank()).collect(Collectors.toList()); String zoneKey = section.isSet("timezone") ? section.getString("timezone") : section.getString("zone", defaults.zoneId().getId()); ZoneId zone = ZoneId.of(zoneKey); @@ -56,6 +58,8 @@ public Countdown parse(String name, ConfigurationSection section, CountdownDefau BarStyle bossStyle = parseBossBarStyle(section.getString("display.bossbar.style", "SOLID")); Countdown countdown = new Countdown(name, getType(), displayTypes, updateInterval, visibility, format, start, end, endCommands, zone, autoRestart, startCountdown, restartDelay, alignToClock, alignInterval, missedPolicy, bossColor, bossStyle); + countdown.setStartSound(startSound); + countdown.setEndSound(endSound); countdown.setRunning(section.getBoolean("running", defaults.startOnCreate())); String durationValue = section.getString("duration", "0s"); long seconds = parseDurationLegacy(durationValue); diff --git a/src/main/java/com/skyblockexp/ezcountdown/type/FixedDateHandler.java b/src/main/java/com/skyblockexp/ezcountdown/type/FixedDateHandler.java index 4fdcdd6..a73c2fb 100644 --- a/src/main/java/com/skyblockexp/ezcountdown/type/FixedDateHandler.java +++ b/src/main/java/com/skyblockexp/ezcountdown/type/FixedDateHandler.java @@ -44,6 +44,8 @@ public Countdown parse(String name, ConfigurationSection section, CountdownDefau String format = section.getString("messages.format", defaults.formatMessage()); String start = section.getString("messages.start", defaults.startMessage()); String end = section.getString("messages.end", defaults.endMessage()); + String startSound = section.getString("sounds.start", null); + String endSound = section.getString("sounds.end", null); List endCommands = section.getStringList("commands.end").stream().filter(c -> c != null && !c.isBlank()).collect(Collectors.toList()); String zoneKey = section.isSet("timezone") ? section.getString("timezone") : section.getString("zone", defaults.zoneId().getId()); ZoneId zone = ZoneId.of(zoneKey); @@ -60,6 +62,8 @@ public Countdown parse(String name, ConfigurationSection section, CountdownDefau BarStyle bossStyle = parseBossBarStyle(section.getString("display.bossbar.style", "SOLID")); Countdown countdown = new Countdown(name, getType(), displayTypes, updateInterval, visibility, format, start, end, endCommands, zone, autoRestart, startCountdown, restartDelay, alignToClock, alignInterval, missedPolicy, bossColor, bossStyle); + countdown.setStartSound(startSound); + countdown.setEndSound(endSound); countdown.setRunning(section.getBoolean("running", defaults.startOnCreate())); String target = section.getString("target"); if (target == null) throw new IllegalArgumentException("Missing target date for fixed date countdown."); diff --git a/src/main/java/com/skyblockexp/ezcountdown/type/ManualHandler.java b/src/main/java/com/skyblockexp/ezcountdown/type/ManualHandler.java index 9129a37..bb0f0e0 100644 --- a/src/main/java/com/skyblockexp/ezcountdown/type/ManualHandler.java +++ b/src/main/java/com/skyblockexp/ezcountdown/type/ManualHandler.java @@ -40,6 +40,8 @@ public Countdown parse(String name, ConfigurationSection section, CountdownDefau String format = section.getString("messages.format", defaults.formatMessage()); String start = section.getString("messages.start", defaults.startMessage()); String end = section.getString("messages.end", defaults.endMessage()); + String startSound = section.getString("sounds.start", null); + String endSound = section.getString("sounds.end", null); List endCommands = section.getStringList("commands.end").stream().filter(c -> c != null && !c.isBlank()).collect(Collectors.toList()); String zoneKey = section.isSet("timezone") ? section.getString("timezone") : section.getString("zone", defaults.zoneId().getId()); ZoneId zone = ZoneId.of(zoneKey); @@ -56,6 +58,8 @@ public Countdown parse(String name, ConfigurationSection section, CountdownDefau BarStyle bossStyle = parseBossBarStyle(section.getString("display.bossbar.style", "SOLID")); Countdown countdown = new Countdown(name, getType(), displayTypes, updateInterval, visibility, format, start, end, endCommands, zone, autoRestart, startCountdown, restartDelay, alignToClock, alignInterval, missedPolicy, bossColor, bossStyle); + countdown.setStartSound(startSound); + countdown.setEndSound(endSound); countdown.setRunning(section.getBoolean("running", false)); String durationValue = section.getString("duration", "0s"); long seconds = parseDurationLegacy(durationValue); diff --git a/src/main/java/com/skyblockexp/ezcountdown/type/RecurringHandler.java b/src/main/java/com/skyblockexp/ezcountdown/type/RecurringHandler.java index b667cc2..9fadd40 100644 --- a/src/main/java/com/skyblockexp/ezcountdown/type/RecurringHandler.java +++ b/src/main/java/com/skyblockexp/ezcountdown/type/RecurringHandler.java @@ -41,6 +41,8 @@ public Countdown parse(String name, ConfigurationSection section, CountdownDefau String format = section.getString("messages.format", defaults.formatMessage()); String start = section.getString("messages.start", defaults.startMessage()); String end = section.getString("messages.end", defaults.endMessage()); + String startSound = section.getString("sounds.start", null); + String endSound = section.getString("sounds.end", null); List endCommands = section.getStringList("commands.end").stream().filter(c -> c != null && !c.isBlank()).collect(Collectors.toList()); String zoneKey = section.isSet("timezone") ? section.getString("timezone") : section.getString("zone", defaults.zoneId().getId()); ZoneId zone = ZoneId.of(zoneKey); @@ -61,6 +63,8 @@ public Countdown parse(String name, ConfigurationSection section, CountdownDefau BarStyle bossStyle = parseBossBarStyle(section.getString("display.bossbar.style", "SOLID")); Countdown countdown = new Countdown(name, getType(), displayTypes, updateInterval, visibility, format, start, end, endCommands, zone, autoRestart, startCountdown, restartDelay, alignToClock, alignInterval, missedPolicy, bossColor, bossStyle); + countdown.setStartSound(startSound); + countdown.setEndSound(endSound); countdown.setRunning(section.getBoolean("running", true)); countdown.setRecurringMonth(section.getInt("recurring.month", 1)); countdown.setRecurringDay(section.getInt("recurring.day", 1)); diff --git a/src/main/java/com/skyblockexp/ezcountdown/util/CountdownCloner.java b/src/main/java/com/skyblockexp/ezcountdown/util/CountdownCloner.java index 3df4160..3b56e52 100644 --- a/src/main/java/com/skyblockexp/ezcountdown/util/CountdownCloner.java +++ b/src/main/java/com/skyblockexp/ezcountdown/util/CountdownCloner.java @@ -13,6 +13,8 @@ public static void copyRuntimeFields(Countdown src, Countdown dest) { dest.setRecurringMonth(src.getRecurringMonth()); dest.setRecurringDay(src.getRecurringDay()); dest.setRecurringTime(src.getRecurringTime()); + dest.setStartSound(src.getStartSound()); + dest.setEndSound(src.getEndSound()); dest.setRunning(src.isRunning()); } } diff --git a/src/main/resources/countdowns.yml b/src/main/resources/countdowns.yml index ef19365..b71f15e 100644 --- a/src/main/resources/countdowns.yml +++ b/src/main/resources/countdowns.yml @@ -23,6 +23,11 @@ countdowns: # format: "{translate:example.format}" # start: "{translate:example.start}" # end: "{translate:example.end}" + # sounds: + # # Optional Bukkit Sound enum names to play for all online players. + # # Example: ENTITY_PLAYER_LEVELUP + # start: "ENTITY_PLAYER_LEVELUP" + # end: "BLOCK_NOTE_BLOCK_PLING" # commands: # end: # - "say Countdown finished!" diff --git a/src/test/java/com/skyblockexp/ezcountdown/api/model/CountdownBuilderTest.java b/src/test/java/com/skyblockexp/ezcountdown/api/model/CountdownBuilderTest.java index f317e53..a3e09ee 100644 --- a/src/test/java/com/skyblockexp/ezcountdown/api/model/CountdownBuilderTest.java +++ b/src/test/java/com/skyblockexp/ezcountdown/api/model/CountdownBuilderTest.java @@ -52,4 +52,17 @@ void buildRecurringCountdown_setsRecurringFields() { assertEquals(25, c.getRecurringDay()); assertEquals(t, c.getRecurringTime()); } + + @Test + void buildCountdown_setsStartAndEndSounds() { + Countdown c = CountdownBuilder.builder("sound-test") + .type(CountdownType.DURATION) + .startSound("ENTITY_PLAYER_LEVELUP") + .endSound("BLOCK_NOTE_BLOCK_PLING") + .durationSeconds(30) + .build(); + + assertEquals("ENTITY_PLAYER_LEVELUP", c.getStartSound()); + assertEquals("BLOCK_NOTE_BLOCK_PLING", c.getEndSound()); + } } diff --git a/src/test/java/com/skyblockexp/ezcountdown/gui/GuiSoundEditorFeatureTest.java b/src/test/java/com/skyblockexp/ezcountdown/gui/GuiSoundEditorFeatureTest.java new file mode 100644 index 0000000..1f0fe44 --- /dev/null +++ b/src/test/java/com/skyblockexp/ezcountdown/gui/GuiSoundEditorFeatureTest.java @@ -0,0 +1,157 @@ +package com.skyblockexp.ezcountdown.gui; + +import com.skyblockexp.ezcountdown.api.model.Countdown; +import com.skyblockexp.ezcountdown.api.model.CountdownType; +import com.skyblockexp.ezcountdown.listener.ChatInputListener; +import com.skyblockexp.ezcountdown.test.MockBukkitTestBase; +import org.bukkit.event.inventory.ClickType; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; +import org.bukkit.event.inventory.InventoryType; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.time.ZoneId; +import java.util.EnumSet; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GuiSoundEditorFeatureTest extends MockBukkitTestBase { + + @SuppressWarnings("unchecked") + private static Map> pendingMap(ChatInputListener listener) throws Exception { + Field f = ChatInputListener.class.getDeclaredField("pending"); + f.setAccessible(true); + return (Map>) f.get(listener); + } + + private static boolean hasPending(ChatInputListener listener, UUID id) throws Exception { + return pendingMap(listener).containsKey(id); + } + + private static Consumer getPending(ChatInputListener listener, UUID id) throws Exception { + return pendingMap(listener).get(id); + } + + @Test + public void editorSlotStartSound_registersChatInputRequest() throws Exception { + Countdown cd = new Countdown("gui-sound-start", CountdownType.MANUAL, + EnumSet.noneOf(com.skyblockexp.ezcountdown.display.DisplayType.class), + 1, null, "{formatted}", "start", "end", java.util.List.of(), ZoneId.systemDefault()); + cd.setDurationSeconds(10); + manager.createCountdown(cd); + + var player = addPlayer("gui-sound-player-start"); + registry.gui().editorMenu().openEditor(player, cd); + + InventoryClickEvent click = new InventoryClickEvent( + player.getOpenInventory(), InventoryType.SlotType.CONTAINER, + 9, ClickType.LEFT, InventoryAction.PICKUP_ALL); + plugin.getServer().getPluginManager().callEvent(click); + + assertTrue(hasPending(registry.gui().chatInputListener(), player.getUniqueId()), + "Expected start sound slot to register chat input callback"); + } + + @Test + public void editorStartSoundInput_validValue_updatesCountdown() throws Exception { + Countdown cd = new Countdown("gui-sound-apply-start", CountdownType.MANUAL, + EnumSet.noneOf(com.skyblockexp.ezcountdown.display.DisplayType.class), + 1, null, "{formatted}", "start", "end", java.util.List.of(), ZoneId.systemDefault()); + cd.setDurationSeconds(10); + manager.createCountdown(cd); + + var player = addPlayer("gui-sound-player-apply-start"); + registry.gui().editorMenu().openEditor(player, cd); + + InventoryClickEvent click = new InventoryClickEvent( + player.getOpenInventory(), InventoryType.SlotType.CONTAINER, + 9, ClickType.LEFT, InventoryAction.PICKUP_ALL); + plugin.getServer().getPluginManager().callEvent(click); + + Consumer cb = getPending(registry.gui().chatInputListener(), player.getUniqueId()); + assertNotNull(cb, "Expected pending callback for start sound input"); + + String validSound = "BLOCK.NOTE_BLOCK.PLING"; + cb.accept(validSound); + + assertEquals(validSound, cd.getStartSound()); + } + + @Test + public void editorStartSoundInput_none_clearsCountdownSound() throws Exception { + Countdown cd = new Countdown("gui-sound-clear-start", CountdownType.MANUAL, + EnumSet.noneOf(com.skyblockexp.ezcountdown.display.DisplayType.class), + 1, null, "{formatted}", "start", "end", java.util.List.of(), ZoneId.systemDefault()); + cd.setDurationSeconds(10); + cd.setStartSound("BLOCK.NOTE_BLOCK.PLING"); + manager.createCountdown(cd); + + var player = addPlayer("gui-sound-player-clear-start"); + registry.gui().editorMenu().openEditor(player, cd); + + InventoryClickEvent click = new InventoryClickEvent( + player.getOpenInventory(), InventoryType.SlotType.CONTAINER, + 9, ClickType.LEFT, InventoryAction.PICKUP_ALL); + plugin.getServer().getPluginManager().callEvent(click); + + Consumer cb = getPending(registry.gui().chatInputListener(), player.getUniqueId()); + assertNotNull(cb, "Expected pending callback for start sound input"); + + cb.accept("none"); + + assertNull(cd.getStartSound()); + } + + @Test + public void editorSlotEndSound_registersChatInputRequest() throws Exception { + Countdown cd = new Countdown("gui-sound-end", CountdownType.MANUAL, + EnumSet.noneOf(com.skyblockexp.ezcountdown.display.DisplayType.class), + 1, null, "{formatted}", "start", "end", java.util.List.of(), ZoneId.systemDefault()); + cd.setDurationSeconds(10); + manager.createCountdown(cd); + + var player = addPlayer("gui-sound-player-end"); + registry.gui().editorMenu().openEditor(player, cd); + + InventoryClickEvent click = new InventoryClickEvent( + player.getOpenInventory(), InventoryType.SlotType.CONTAINER, + 10, ClickType.LEFT, InventoryAction.PICKUP_ALL); + plugin.getServer().getPluginManager().callEvent(click); + + assertTrue(hasPending(registry.gui().chatInputListener(), player.getUniqueId()), + "Expected end sound slot to register chat input callback"); + assertNotNull(player.getOpenInventory()); + } + + @Test + public void editorEndSoundInput_validValue_updatesCountdown() throws Exception { + Countdown cd = new Countdown("gui-sound-apply-end", CountdownType.MANUAL, + EnumSet.noneOf(com.skyblockexp.ezcountdown.display.DisplayType.class), + 1, null, "{formatted}", "start", "end", java.util.List.of(), ZoneId.systemDefault()); + cd.setDurationSeconds(10); + manager.createCountdown(cd); + + var player = addPlayer("gui-sound-player-apply-end"); + registry.gui().editorMenu().openEditor(player, cd); + + InventoryClickEvent click = new InventoryClickEvent( + player.getOpenInventory(), InventoryType.SlotType.CONTAINER, + 10, ClickType.LEFT, InventoryAction.PICKUP_ALL); + plugin.getServer().getPluginManager().callEvent(click); + + Consumer cb = getPending(registry.gui().chatInputListener(), player.getUniqueId()); + assertNotNull(cb, "Expected pending callback for end sound input"); + + String validSound = "BLOCK.NOTE_BLOCK.PLING"; + cb.accept(validSound); + + assertEquals(validSound, cd.getEndSound()); + } +} diff --git a/src/test/java/com/skyblockexp/ezcountdown/listener/actions/SoundEditorHelperUnitTest.java b/src/test/java/com/skyblockexp/ezcountdown/listener/actions/SoundEditorHelperUnitTest.java new file mode 100644 index 0000000..2fd9c1f --- /dev/null +++ b/src/test/java/com/skyblockexp/ezcountdown/listener/actions/SoundEditorHelperUnitTest.java @@ -0,0 +1,24 @@ +package com.skyblockexp.ezcountdown.listener.actions; + +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class SoundEditorHelperUnitTest { + + @Test + public void sendAvailableSounds_sendsHeaderAndKnownSoundNames() { + Player player = mock(Player.class); + + SoundEditorHelper.sendAvailableSounds(player); + + verify(player, atLeastOnce()).sendMessage(contains("Available sounds (")); + assertTrue(Sound.values().length > 0, "Expected at least one Bukkit sound enum value"); + } +} diff --git a/src/test/java/com/skyblockexp/ezcountdown/storage/YamlCountdownStorageRoundtripTest.java b/src/test/java/com/skyblockexp/ezcountdown/storage/YamlCountdownStorageRoundtripTest.java index de27686..3b2dc98 100644 --- a/src/test/java/com/skyblockexp/ezcountdown/storage/YamlCountdownStorageRoundtripTest.java +++ b/src/test/java/com/skyblockexp/ezcountdown/storage/YamlCountdownStorageRoundtripTest.java @@ -50,6 +50,30 @@ public void saveAndLoadRoundtrip() throws Exception { tmp.delete(); } + @Test + public void saveAndLoadRoundtrip_preservesStartAndEndSounds() throws Exception { + File tmp = File.createTempFile("countdowns-sounds", ".yml"); + tmp.delete(); + tmp.getParentFile().mkdirs(); + + CountdownDefaults defaults = new CountdownDefaults(EnumSet.noneOf(DisplayType.class), 1, null, "{formatted}", "start", "end", true, ZoneId.of("UTC")); + YamlCountdownStorage storage = new YamlCountdownStorage(defaults, tmp, java.util.logging.Logger.getLogger("test-sounds")); + + Countdown cd = new Countdown("soundy", CountdownType.MANUAL, EnumSet.noneOf(DisplayType.class), 1, null, "{formatted}", "s", "e", List.of(), ZoneId.of("UTC")); + cd.setStartSound("ENTITY_PLAYER_LEVELUP"); + cd.setEndSound("BLOCK_NOTE_BLOCK_PLING"); + + storage.saveCountdowns(List.of(cd)); + + YamlCountdownStorage reloader = new YamlCountdownStorage(defaults, tmp, java.util.logging.Logger.getLogger("test-sounds-reload")); + var loaded = reloader.loadCountdowns(); + Countdown loadedCd = loaded.stream().filter(c -> c.getName().equals("soundy")).findFirst().orElseThrow(); + + assertEquals("ENTITY_PLAYER_LEVELUP", loadedCd.getStartSound()); + assertEquals("BLOCK_NOTE_BLOCK_PLING", loadedCd.getEndSound()); + tmp.delete(); + } + /** * Regression test for: duration countdown resets to full duration on server restart. *