diff --git a/pom.xml b/pom.xml index 468c0c3..c4ab95f 100644 --- a/pom.xml +++ b/pom.xml @@ -100,19 +100,19 @@ org.spigotmc spigot-api - 1.19.4-R0.1-SNAPSHOT + 1.20.1-R0.1-SNAPSHOT provided net.knarcraft knarlib - 1.1 + 1.2.3 compile net.citizensnpcs citizens-main - 2.0.30-SNAPSHOT + 2.0.32-SNAPSHOT jar provided diff --git a/src/main/java/net/knarcraft/minstrel/MinstrelPlugin.java b/src/main/java/net/knarcraft/minstrel/MinstrelPlugin.java index 3c4e5a2..8007b4d 100644 --- a/src/main/java/net/knarcraft/minstrel/MinstrelPlugin.java +++ b/src/main/java/net/knarcraft/minstrel/MinstrelPlugin.java @@ -4,6 +4,7 @@ import net.citizensnpcs.api.CitizensAPI; import net.knarcraft.minstrel.command.MinstrelCommand; import net.knarcraft.minstrel.command.MinstrelTabCompleter; import net.knarcraft.minstrel.listener.MinstrelListener; +import net.knarcraft.minstrel.manager.QueueManager; import net.knarcraft.minstrel.trait.MinstrelTrait; import org.bukkit.Bukkit; import org.bukkit.command.PluginCommand; @@ -39,6 +40,8 @@ public final class MinstrelPlugin extends JavaPlugin { minstrelCommand.setExecutor(new MinstrelCommand()); minstrelCommand.setTabCompleter(new MinstrelTabCompleter()); } + + Bukkit.getScheduler().scheduleSyncRepeatingTask(this, QueueManager::handleQueue, 20, 10); } @Override @@ -57,6 +60,15 @@ public final class MinstrelPlugin extends JavaPlugin { knownMinstrels.add(minstrelTrait); } + /** + * Gets all currently known minstrels + * + * @return

All currently known minstrels

+ */ + public List getMinstrels() { + return new ArrayList<>(knownMinstrels); + } + /** * Gets an instance of this plugin * diff --git a/src/main/java/net/knarcraft/minstrel/command/AddSongCommand.java b/src/main/java/net/knarcraft/minstrel/command/AddSongCommand.java index 693f00e..c681a34 100644 --- a/src/main/java/net/knarcraft/minstrel/command/AddSongCommand.java +++ b/src/main/java/net/knarcraft/minstrel/command/AddSongCommand.java @@ -67,7 +67,7 @@ public class AddSongCommand implements CommandExecutor { //If this is the first song in the playlist, start playing if (playlist.getSongs().size() == 1) { - playlist.play(minstrelTrait); + playlist.changeSong(); } sender.sendMessage("New song added"); return true; diff --git a/src/main/java/net/knarcraft/minstrel/command/ListSongsCommand.java b/src/main/java/net/knarcraft/minstrel/command/ListSongsCommand.java index 768297c..81ce1ab 100644 --- a/src/main/java/net/knarcraft/minstrel/command/ListSongsCommand.java +++ b/src/main/java/net/knarcraft/minstrel/command/ListSongsCommand.java @@ -1,6 +1,5 @@ package net.knarcraft.minstrel.command; -import net.knarcraft.minstrel.music.Song; import net.knarcraft.minstrel.trait.MinstrelTrait; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; @@ -26,12 +25,7 @@ public class ListSongsCommand implements CommandExecutor { @Override public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, @NotNull String[] args) { - StringBuilder builder = new StringBuilder(); - builder.append("Songs:").append("\n"); - for (Song song : minstrelTrait.getPlaylist().getSongs()) { - builder.append(song).append("\n"); - } - sender.sendMessage(builder.toString()); + sender.sendMessage("Songs:" + minstrelTrait.getPlaylist()); return true; } diff --git a/src/main/java/net/knarcraft/minstrel/command/MinstrelCommand.java b/src/main/java/net/knarcraft/minstrel/command/MinstrelCommand.java index eea23aa..6f6abd6 100644 --- a/src/main/java/net/knarcraft/minstrel/command/MinstrelCommand.java +++ b/src/main/java/net/knarcraft/minstrel/command/MinstrelCommand.java @@ -6,6 +6,7 @@ import net.knarcraft.minstrel.trait.MinstrelTrait; import org.bukkit.command.Command; import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; import org.jetbrains.annotations.NotNull; public class MinstrelCommand implements CommandExecutor { @@ -72,9 +73,9 @@ public class MinstrelCommand implements CommandExecutor { sender.sendMessage("The pitch cannot be negative"); } else { minstrelTrait.setPitch(pitch); - if (forceRefresh) { - minstrelTrait.getPlaylist().stop(); - minstrelTrait.getPlaylist().play(minstrelTrait); + if (forceRefresh && sender instanceof Player player) { + minstrelTrait.getPlaylist().stop(player); + minstrelTrait.getPlaylist().play(minstrelTrait, player); } } } catch (NumberFormatException exception) { @@ -105,9 +106,9 @@ public class MinstrelCommand implements CommandExecutor { sender.sendMessage("The volume must be greater than 0"); } else { minstrelTrait.setVolume(volume); - if (forceRefresh) { - minstrelTrait.getPlaylist().stop(); - minstrelTrait.getPlaylist().play(minstrelTrait); + if (forceRefresh && sender instanceof Player player) { + minstrelTrait.getPlaylist().stop(player); + minstrelTrait.getPlaylist().play(minstrelTrait, player); } } } catch (NumberFormatException exception) { diff --git a/src/main/java/net/knarcraft/minstrel/command/RemoveSongCommand.java b/src/main/java/net/knarcraft/minstrel/command/RemoveSongCommand.java index 4a202a2..38c2d67 100644 --- a/src/main/java/net/knarcraft/minstrel/command/RemoveSongCommand.java +++ b/src/main/java/net/knarcraft/minstrel/command/RemoveSongCommand.java @@ -39,7 +39,7 @@ public class RemoveSongCommand implements CommandExecutor { playlist.stop(); playlist.removeSong(index); if (!playlist.getSongs().isEmpty()) { - playlist.play(minstrelTrait); + playlist.changeSong(); } sender.sendMessage("Song removed"); } else { diff --git a/src/main/java/net/knarcraft/minstrel/listener/MinstrelListener.java b/src/main/java/net/knarcraft/minstrel/listener/MinstrelListener.java index 5dcf2b3..c68e3df 100644 --- a/src/main/java/net/knarcraft/minstrel/listener/MinstrelListener.java +++ b/src/main/java/net/knarcraft/minstrel/listener/MinstrelListener.java @@ -1,30 +1,34 @@ package net.knarcraft.minstrel.listener; -import net.citizensnpcs.api.CitizensAPI; import net.citizensnpcs.api.event.NPCRemoveEvent; import net.citizensnpcs.api.npc.NPC; +import net.knarcraft.minstrel.MinstrelPlugin; +import net.knarcraft.minstrel.manager.QueueManager; +import net.knarcraft.minstrel.music.SongEndTime; import net.knarcraft.minstrel.trait.MinstrelTrait; +import net.knarcraft.minstrel.util.SoundHelper; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerMoveEvent; /** * A listener for minstrel-relate events */ public class MinstrelListener implements Listener { - /** - * Listens for any players joining, and makes all minstrels play for them - * - * @param event

The triggered event

- */ @EventHandler - public void playerJoinListener(PlayerJoinEvent event) { - for (NPC npc : CitizensAPI.getNPCRegistry()) { - if (npc.hasTrait(MinstrelTrait.class)) { - MinstrelTrait minstrelTrait = npc.getTraitNullable(MinstrelTrait.class); - minstrelTrait.getPlaylist().play(minstrelTrait, event.getPlayer()); + public void playerMoveListener(PlayerMoveEvent event) { + if (event.getTo() == null || event.getFrom().getBlock().equals(event.getTo().getBlock())) { + return; + } + + for (MinstrelTrait minstrelTrait : MinstrelPlugin.getInstance().getMinstrels()) { + if (!SoundHelper.canHear(event.getPlayer(), minstrelTrait) || + QueueManager.isQueued(event.getPlayer(), minstrelTrait)) { + continue; } + + QueueManager.queue(new SongEndTime(event.getPlayer(), minstrelTrait, 0)); } } diff --git a/src/main/java/net/knarcraft/minstrel/manager/QueueManager.java b/src/main/java/net/knarcraft/minstrel/manager/QueueManager.java new file mode 100644 index 0000000..aff3ec3 --- /dev/null +++ b/src/main/java/net/knarcraft/minstrel/manager/QueueManager.java @@ -0,0 +1,66 @@ +package net.knarcraft.minstrel.manager; + +import net.knarcraft.minstrel.music.SongEndTime; +import net.knarcraft.minstrel.trait.MinstrelTrait; +import net.knarcraft.minstrel.util.SoundHelper; +import org.bukkit.entity.Player; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.PriorityQueue; +import java.util.Queue; +import java.util.Set; + +/** + * A manager keeping track of the queue for playing the next song + */ +public class QueueManager { + + private static final Queue songEndTimes = new PriorityQueue<>(); + private static final Map> queuedMinstrels = new HashMap<>(); + + /** + * Takes care of any expired songs + */ + public static void handleQueue() { + while (!songEndTimes.isEmpty() && songEndTimes.peek().endTime() < System.currentTimeMillis()) { + SongEndTime songEndTime = songEndTimes.remove(); + if (songEndTime == null) { + continue; + } + + // Play the next song if the player is near enough to hear + MinstrelTrait minstrelTrait = songEndTime.minstrelTrait(); + Player player = songEndTime.player(); + if (SoundHelper.canHear(player, minstrelTrait)) { + minstrelTrait.getPlaylist().play(minstrelTrait, player); + } else { + queuedMinstrels.get(player).remove(minstrelTrait); + } + } + } + + /** + * Checks whether the given minstrel has a song queued for the given player + * + * @param player

The player to check

+ * @param minstrelTrait

The minstrel to check

+ * @return

True if already queued

+ */ + public static boolean isQueued(Player player, MinstrelTrait minstrelTrait) { + return queuedMinstrels.get(player) != null && queuedMinstrels.get(player).contains(minstrelTrait); + } + + /** + * Queues the given song end time + * + * @param songEndTime

The song end time to queue

+ */ + public static void queue(SongEndTime songEndTime) { + songEndTimes.add(songEndTime); + queuedMinstrels.computeIfAbsent(songEndTime.player(), k -> new HashSet<>()); + queuedMinstrels.get(songEndTime.player()).add(songEndTime.minstrelTrait()); + } + +} diff --git a/src/main/java/net/knarcraft/minstrel/music/Playlist.java b/src/main/java/net/knarcraft/minstrel/music/Playlist.java index 8faa057..a0dbffb 100644 --- a/src/main/java/net/knarcraft/minstrel/music/Playlist.java +++ b/src/main/java/net/knarcraft/minstrel/music/Playlist.java @@ -1,6 +1,7 @@ package net.knarcraft.minstrel.music; import net.knarcraft.minstrel.MinstrelPlugin; +import net.knarcraft.minstrel.manager.QueueManager; import net.knarcraft.minstrel.trait.MinstrelTrait; import org.bukkit.Bukkit; import org.bukkit.entity.Player; @@ -16,20 +17,16 @@ import java.util.Map; public class Playlist { private final List songs; - private final boolean loop; private int currentlyPlaying = 0; - private int schedulerId = -1; private final Map playerCurrentSong = new HashMap<>(); /** * Instantiates a new playlist * * @param songs

The songs contained in this playlist

- * @param loop

Whether to loop around when this playlist finishes

*/ - public Playlist(List songs, boolean loop) { + public Playlist(List songs) { this.songs = new ArrayList<>(songs); - this.loop = loop; } /** @@ -81,6 +78,7 @@ public class Playlist { song.stop(player); } } + int schedulerId = -1; if (Bukkit.getScheduler().isCurrentlyRunning(schedulerId)) { Bukkit.getScheduler().cancelTask(schedulerId); } @@ -102,43 +100,11 @@ public class Playlist { } stop(player); - Song currentSong = this.songs.get(this.currentlyPlaying - 1); + Song currentSong = this.songs.get(this.currentlyPlaying); playerCurrentSong.put(player, currentSong); currentSong.play(trait, player, trait.getVolume(), trait.getPitch()); - } - - /** - * Plays the next song in this playlist - * - * @param trait

The minstrel to play this playlist for

- */ - public void play(MinstrelTrait trait) { - //If this playlist is empty, do nothing - if (this.songs.size() < 1) { - return; - } - - if (this.currentlyPlaying >= songs.size()) { - if (this.loop) { - this.currentlyPlaying = 0; - } else { - return; - } - } - - //Prevent overlapping music if a song has been played outside the global cycle - for (Player player : playerCurrentSong.keySet()) { - Song currentSong = playerCurrentSong.get(player); - if (currentSong != null) { - currentSong.stop(player); - } - } - - Song currentSong = this.songs.get(this.currentlyPlaying); - currentSong.play(trait, trait.getVolume(), trait.getPitch()); - currentlyPlaying++; - schedulerId = Bukkit.getScheduler().scheduleSyncDelayedTask(MinstrelPlugin.getInstance(), () -> play(trait), - currentSong.getDuration() * 20L); + QueueManager.queue(new SongEndTime(player, trait, System.currentTimeMillis() + + (currentSong.getDuration() * 1000L))); } /** @@ -148,4 +114,31 @@ public class Playlist { this.songs.clear(); } + /** + * Changes to the next song in the playlist + */ + public void changeSong() { + if (this.songs.isEmpty()) { + return; + } + + this.currentlyPlaying++; + this.currentlyPlaying = this.currentlyPlaying % this.songs.size(); + Bukkit.getScheduler().scheduleSyncDelayedTask(MinstrelPlugin.getInstance(), this::changeSong, + this.songs.get(this.currentlyPlaying).getDuration() * 20L); + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < this.songs.size(); i++) { + builder.append("\n"); + if (this.currentlyPlaying == i) { + builder.append(">"); + } + builder.append(this.songs.get(i)); + } + return builder.toString(); + } + } diff --git a/src/main/java/net/knarcraft/minstrel/music/Song.java b/src/main/java/net/knarcraft/minstrel/music/Song.java index b01484e..e3fd9b7 100644 --- a/src/main/java/net/knarcraft/minstrel/music/Song.java +++ b/src/main/java/net/knarcraft/minstrel/music/Song.java @@ -1,8 +1,6 @@ package net.knarcraft.minstrel.music; -import net.knarcraft.minstrel.MinstrelPlugin; import net.knarcraft.minstrel.trait.MinstrelTrait; -import org.bukkit.Bukkit; import org.bukkit.Location; import org.bukkit.SoundCategory; import org.bukkit.entity.Player; @@ -21,7 +19,6 @@ public class Song { private final int durationSeconds; private final SoundCategory category; private final String sound; - private boolean isPlaying = false; private final Set playingFor = new HashSet<>(); /** @@ -53,27 +50,6 @@ public class Song { play(player, trait.getLocation(), volume, pitch); } - /** - * Plays this song at the given minstrel's location - * - *

Volume of 1 means the max volume, which can be heard up to 1 chunk away. Setting it to 2 means it can be - * heard 2 chunks away.

- * - * @param trait

The minstrel to play this song

- * @param volume

The volume to play this song at

- * @param pitch

The pitch to play this song at

- */ - public void play(MinstrelTrait trait, float volume, float pitch) { - for (Player player : Bukkit.getOnlinePlayers()) { - play(player, trait.getLocation(), volume, pitch); - } - - this.isPlaying = true; - //Mark this song as ended, once - Bukkit.getScheduler().runTaskLater(MinstrelPlugin.getInstance(), () -> this.isPlaying = false, - durationSeconds * 20L); - } - /** * Gets the duration of this song, in seconds * @@ -90,7 +66,7 @@ public class Song { */ public void stop(Player player) { //Don't bother stopping if not playing - if (!playingFor.contains(player) && !isPlaying) { + if (!playingFor.contains(player)) { return; } playingFor.remove(player); @@ -119,12 +95,7 @@ public class Song { @Override public String toString() { - String songString = this.category + ":" + this.sound + ":" + this.durationSeconds; - if (this.isPlaying) { - return ">" + songString; - } else { - return songString; - } + return this.category + ":" + this.sound + ":" + this.durationSeconds; } /** diff --git a/src/main/java/net/knarcraft/minstrel/music/SongEndTime.java b/src/main/java/net/knarcraft/minstrel/music/SongEndTime.java new file mode 100644 index 0000000..235e0e7 --- /dev/null +++ b/src/main/java/net/knarcraft/minstrel/music/SongEndTime.java @@ -0,0 +1,37 @@ +package net.knarcraft.minstrel.music; + +import net.knarcraft.minstrel.trait.MinstrelTrait; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; + +/** + * A record of the time when a specific song will end + * + * @param player

The player the song is playing for

+ * @param minstrelTrait

The minstrel playing the song

+ * @param endTime

The time when the song will end

+ */ +public record SongEndTime(Player player, MinstrelTrait minstrelTrait, long endTime) implements Comparable { + + @Override + public boolean equals(Object other) { + if (!(other instanceof SongEndTime songEndTime)) { + return false; + } + + return player.equals(songEndTime.player) && minstrelTrait.equals(songEndTime.minstrelTrait); + } + + @Override + public int compareTo(@NotNull SongEndTime o) { + long difference = this.endTime - o.endTime; + if (difference > 0) { + return 1; + } else if (difference < 0) { + return -1; + } else { + return 0; + } + } + +} diff --git a/src/main/java/net/knarcraft/minstrel/trait/MinstrelTrait.java b/src/main/java/net/knarcraft/minstrel/trait/MinstrelTrait.java index 1157d43..f3d770b 100644 --- a/src/main/java/net/knarcraft/minstrel/trait/MinstrelTrait.java +++ b/src/main/java/net/knarcraft/minstrel/trait/MinstrelTrait.java @@ -16,7 +16,7 @@ import java.util.List; */ public class MinstrelTrait extends Trait { - private final Playlist playlist = new Playlist(new ArrayList<>(), true); + private final Playlist playlist = new Playlist(new ArrayList<>()); private float volume = 1F; private float pitch = 1F; @@ -59,7 +59,7 @@ public class MinstrelTrait extends Trait { } this.volume = (float) key.getDouble("volume", 1D); this.pitch = (float) key.getDouble("pitch", 1D); - this.playlist.play(this); + this.playlist.changeSong(); //Register the minstrel to allow stopping the playback later MinstrelPlugin.getInstance().addMinstrel(this); @@ -143,4 +143,13 @@ public class MinstrelTrait extends Trait { } } + @Override + public boolean equals(Object other) { + if (!(other instanceof MinstrelTrait minstrelTrait)) { + return false; + } + + return npc.equals(minstrelTrait.npc); + } + } diff --git a/src/main/java/net/knarcraft/minstrel/util/SoundHelper.java b/src/main/java/net/knarcraft/minstrel/util/SoundHelper.java new file mode 100644 index 0000000..dea33d2 --- /dev/null +++ b/src/main/java/net/knarcraft/minstrel/util/SoundHelper.java @@ -0,0 +1,21 @@ +package net.knarcraft.minstrel.util; + +import net.knarcraft.minstrel.trait.MinstrelTrait; +import org.bukkit.entity.Player; + +public class SoundHelper { + + /** + * Checks whether the given player can hear the given minstrel + * + * @param player

The player to check

+ * @param minstrelTrait

The minstrel to check

+ * @return

True if the player would hear the minstrel

+ */ + public static boolean canHear(Player player, MinstrelTrait minstrelTrait) { + double distance = player.getLocation().distance(minstrelTrait.getNPC().getStoredLocation()); + // A player can hear a minstrel 15 blocks away, but the distance increases if volume > 1 + return distance < 15 * (Math.max(minstrelTrait.getVolume(), 1)); + } + +}