Schedules next song individually for each player #4
All checks were successful
KnarCraft/Minstrel/pipeline/head This commit looks good

This commit is contained in:
Kristian Knarvik 2023-08-26 02:56:14 +02:00
parent fa2c1eda08
commit a243b3fc77
13 changed files with 211 additions and 103 deletions

View File

@ -100,19 +100,19 @@
<dependency> <dependency>
<groupId>org.spigotmc</groupId> <groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId> <artifactId>spigot-api</artifactId>
<version>1.19.4-R0.1-SNAPSHOT</version> <version>1.20.1-R0.1-SNAPSHOT</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>net.knarcraft</groupId> <groupId>net.knarcraft</groupId>
<artifactId>knarlib</artifactId> <artifactId>knarlib</artifactId>
<version>1.1</version> <version>1.2.3</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>net.citizensnpcs</groupId> <groupId>net.citizensnpcs</groupId>
<artifactId>citizens-main</artifactId> <artifactId>citizens-main</artifactId>
<version>2.0.30-SNAPSHOT</version> <version>2.0.32-SNAPSHOT</version>
<type>jar</type> <type>jar</type>
<scope>provided</scope> <scope>provided</scope>
<exclusions> <exclusions>

View File

@ -4,6 +4,7 @@ import net.citizensnpcs.api.CitizensAPI;
import net.knarcraft.minstrel.command.MinstrelCommand; import net.knarcraft.minstrel.command.MinstrelCommand;
import net.knarcraft.minstrel.command.MinstrelTabCompleter; import net.knarcraft.minstrel.command.MinstrelTabCompleter;
import net.knarcraft.minstrel.listener.MinstrelListener; import net.knarcraft.minstrel.listener.MinstrelListener;
import net.knarcraft.minstrel.manager.QueueManager;
import net.knarcraft.minstrel.trait.MinstrelTrait; import net.knarcraft.minstrel.trait.MinstrelTrait;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.command.PluginCommand; import org.bukkit.command.PluginCommand;
@ -39,6 +40,8 @@ public final class MinstrelPlugin extends JavaPlugin {
minstrelCommand.setExecutor(new MinstrelCommand()); minstrelCommand.setExecutor(new MinstrelCommand());
minstrelCommand.setTabCompleter(new MinstrelTabCompleter()); minstrelCommand.setTabCompleter(new MinstrelTabCompleter());
} }
Bukkit.getScheduler().scheduleSyncRepeatingTask(this, QueueManager::handleQueue, 20, 10);
} }
@Override @Override
@ -57,6 +60,15 @@ public final class MinstrelPlugin extends JavaPlugin {
knownMinstrels.add(minstrelTrait); knownMinstrels.add(minstrelTrait);
} }
/**
* Gets all currently known minstrels
*
* @return <p>All currently known minstrels</p>
*/
public List<MinstrelTrait> getMinstrels() {
return new ArrayList<>(knownMinstrels);
}
/** /**
* Gets an instance of this plugin * Gets an instance of this plugin
* *

View File

@ -67,7 +67,7 @@ public class AddSongCommand implements CommandExecutor {
//If this is the first song in the playlist, start playing //If this is the first song in the playlist, start playing
if (playlist.getSongs().size() == 1) { if (playlist.getSongs().size() == 1) {
playlist.play(minstrelTrait); playlist.changeSong();
} }
sender.sendMessage("New song added"); sender.sendMessage("New song added");
return true; return true;

View File

@ -1,6 +1,5 @@
package net.knarcraft.minstrel.command; package net.knarcraft.minstrel.command;
import net.knarcraft.minstrel.music.Song;
import net.knarcraft.minstrel.trait.MinstrelTrait; import net.knarcraft.minstrel.trait.MinstrelTrait;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandExecutor;
@ -26,12 +25,7 @@ public class ListSongsCommand implements CommandExecutor {
@Override @Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) { @NotNull String[] args) {
StringBuilder builder = new StringBuilder(); sender.sendMessage("Songs:" + minstrelTrait.getPlaylist());
builder.append("Songs:").append("\n");
for (Song song : minstrelTrait.getPlaylist().getSongs()) {
builder.append(song).append("\n");
}
sender.sendMessage(builder.toString());
return true; return true;
} }

View File

@ -6,6 +6,7 @@ import net.knarcraft.minstrel.trait.MinstrelTrait;
import org.bukkit.command.Command; import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor; import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class MinstrelCommand implements CommandExecutor { public class MinstrelCommand implements CommandExecutor {
@ -72,9 +73,9 @@ public class MinstrelCommand implements CommandExecutor {
sender.sendMessage("The pitch cannot be negative"); sender.sendMessage("The pitch cannot be negative");
} else { } else {
minstrelTrait.setPitch(pitch); minstrelTrait.setPitch(pitch);
if (forceRefresh) { if (forceRefresh && sender instanceof Player player) {
minstrelTrait.getPlaylist().stop(); minstrelTrait.getPlaylist().stop(player);
minstrelTrait.getPlaylist().play(minstrelTrait); minstrelTrait.getPlaylist().play(minstrelTrait, player);
} }
} }
} catch (NumberFormatException exception) { } catch (NumberFormatException exception) {
@ -105,9 +106,9 @@ public class MinstrelCommand implements CommandExecutor {
sender.sendMessage("The volume must be greater than 0"); sender.sendMessage("The volume must be greater than 0");
} else { } else {
minstrelTrait.setVolume(volume); minstrelTrait.setVolume(volume);
if (forceRefresh) { if (forceRefresh && sender instanceof Player player) {
minstrelTrait.getPlaylist().stop(); minstrelTrait.getPlaylist().stop(player);
minstrelTrait.getPlaylist().play(minstrelTrait); minstrelTrait.getPlaylist().play(minstrelTrait, player);
} }
} }
} catch (NumberFormatException exception) { } catch (NumberFormatException exception) {

View File

@ -39,7 +39,7 @@ public class RemoveSongCommand implements CommandExecutor {
playlist.stop(); playlist.stop();
playlist.removeSong(index); playlist.removeSong(index);
if (!playlist.getSongs().isEmpty()) { if (!playlist.getSongs().isEmpty()) {
playlist.play(minstrelTrait); playlist.changeSong();
} }
sender.sendMessage("Song removed"); sender.sendMessage("Song removed");
} else { } else {

View File

@ -1,30 +1,34 @@
package net.knarcraft.minstrel.listener; package net.knarcraft.minstrel.listener;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.event.NPCRemoveEvent; import net.citizensnpcs.api.event.NPCRemoveEvent;
import net.citizensnpcs.api.npc.NPC; 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.trait.MinstrelTrait;
import net.knarcraft.minstrel.util.SoundHelper;
import org.bukkit.event.EventHandler; import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener; import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerMoveEvent;
/** /**
* A listener for minstrel-relate events * A listener for minstrel-relate events
*/ */
public class MinstrelListener implements Listener { public class MinstrelListener implements Listener {
/**
* Listens for any players joining, and makes all minstrels play for them
*
* @param event <p>The triggered event</p>
*/
@EventHandler @EventHandler
public void playerJoinListener(PlayerJoinEvent event) { public void playerMoveListener(PlayerMoveEvent event) {
for (NPC npc : CitizensAPI.getNPCRegistry()) { if (event.getTo() == null || event.getFrom().getBlock().equals(event.getTo().getBlock())) {
if (npc.hasTrait(MinstrelTrait.class)) { return;
MinstrelTrait minstrelTrait = npc.getTraitNullable(MinstrelTrait.class); }
minstrelTrait.getPlaylist().play(minstrelTrait, event.getPlayer());
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));
} }
} }

View File

@ -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<SongEndTime> songEndTimes = new PriorityQueue<>();
private static final Map<Player, Set<MinstrelTrait>> 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 <p>The player to check</p>
* @param minstrelTrait <p>The minstrel to check</p>
* @return <p>True if already queued</p>
*/
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 <p>The song end time to queue</p>
*/
public static void queue(SongEndTime songEndTime) {
songEndTimes.add(songEndTime);
queuedMinstrels.computeIfAbsent(songEndTime.player(), k -> new HashSet<>());
queuedMinstrels.get(songEndTime.player()).add(songEndTime.minstrelTrait());
}
}

View File

@ -1,6 +1,7 @@
package net.knarcraft.minstrel.music; package net.knarcraft.minstrel.music;
import net.knarcraft.minstrel.MinstrelPlugin; import net.knarcraft.minstrel.MinstrelPlugin;
import net.knarcraft.minstrel.manager.QueueManager;
import net.knarcraft.minstrel.trait.MinstrelTrait; import net.knarcraft.minstrel.trait.MinstrelTrait;
import org.bukkit.Bukkit; import org.bukkit.Bukkit;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@ -16,20 +17,16 @@ import java.util.Map;
public class Playlist { public class Playlist {
private final List<Song> songs; private final List<Song> songs;
private final boolean loop;
private int currentlyPlaying = 0; private int currentlyPlaying = 0;
private int schedulerId = -1;
private final Map<Player, Song> playerCurrentSong = new HashMap<>(); private final Map<Player, Song> playerCurrentSong = new HashMap<>();
/** /**
* Instantiates a new playlist * Instantiates a new playlist
* *
* @param songs <p>The songs contained in this playlist</p> * @param songs <p>The songs contained in this playlist</p>
* @param loop <p>Whether to loop around when this playlist finishes</p>
*/ */
public Playlist(List<Song> songs, boolean loop) { public Playlist(List<Song> songs) {
this.songs = new ArrayList<>(songs); this.songs = new ArrayList<>(songs);
this.loop = loop;
} }
/** /**
@ -81,6 +78,7 @@ public class Playlist {
song.stop(player); song.stop(player);
} }
} }
int schedulerId = -1;
if (Bukkit.getScheduler().isCurrentlyRunning(schedulerId)) { if (Bukkit.getScheduler().isCurrentlyRunning(schedulerId)) {
Bukkit.getScheduler().cancelTask(schedulerId); Bukkit.getScheduler().cancelTask(schedulerId);
} }
@ -102,43 +100,11 @@ public class Playlist {
} }
stop(player); stop(player);
Song currentSong = this.songs.get(this.currentlyPlaying - 1); Song currentSong = this.songs.get(this.currentlyPlaying);
playerCurrentSong.put(player, currentSong); playerCurrentSong.put(player, currentSong);
currentSong.play(trait, player, trait.getVolume(), trait.getPitch()); currentSong.play(trait, player, trait.getVolume(), trait.getPitch());
} QueueManager.queue(new SongEndTime(player, trait, System.currentTimeMillis() +
(currentSong.getDuration() * 1000L)));
/**
* Plays the next song in this playlist
*
* @param trait <p>The minstrel to play this playlist for</p>
*/
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);
} }
/** /**
@ -148,4 +114,31 @@ public class Playlist {
this.songs.clear(); 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();
}
} }

View File

@ -1,8 +1,6 @@
package net.knarcraft.minstrel.music; package net.knarcraft.minstrel.music;
import net.knarcraft.minstrel.MinstrelPlugin;
import net.knarcraft.minstrel.trait.MinstrelTrait; import net.knarcraft.minstrel.trait.MinstrelTrait;
import org.bukkit.Bukkit;
import org.bukkit.Location; import org.bukkit.Location;
import org.bukkit.SoundCategory; import org.bukkit.SoundCategory;
import org.bukkit.entity.Player; import org.bukkit.entity.Player;
@ -21,7 +19,6 @@ public class Song {
private final int durationSeconds; private final int durationSeconds;
private final SoundCategory category; private final SoundCategory category;
private final String sound; private final String sound;
private boolean isPlaying = false;
private final Set<Player> playingFor = new HashSet<>(); private final Set<Player> playingFor = new HashSet<>();
/** /**
@ -53,27 +50,6 @@ public class Song {
play(player, trait.getLocation(), volume, pitch); play(player, trait.getLocation(), volume, pitch);
} }
/**
* Plays this song at the given minstrel's location
*
* <p>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.</p>
*
* @param trait <p>The minstrel to play this song</p>
* @param volume <p>The volume to play this song at</p>
* @param pitch <p>The pitch to play this song at</p>
*/
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 * Gets the duration of this song, in seconds
* *
@ -90,7 +66,7 @@ public class Song {
*/ */
public void stop(Player player) { public void stop(Player player) {
//Don't bother stopping if not playing //Don't bother stopping if not playing
if (!playingFor.contains(player) && !isPlaying) { if (!playingFor.contains(player)) {
return; return;
} }
playingFor.remove(player); playingFor.remove(player);
@ -119,12 +95,7 @@ public class Song {
@Override @Override
public String toString() { public String toString() {
String songString = this.category + ":" + this.sound + ":" + this.durationSeconds; return this.category + ":" + this.sound + ":" + this.durationSeconds;
if (this.isPlaying) {
return ">" + songString;
} else {
return songString;
}
} }
/** /**

View File

@ -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 <p>The player the song is playing for</p>
* @param minstrelTrait <p>The minstrel playing the song</p>
* @param endTime <p>The time when the song will end</p>
*/
public record SongEndTime(Player player, MinstrelTrait minstrelTrait, long endTime) implements Comparable<SongEndTime> {
@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;
}
}
}

View File

@ -16,7 +16,7 @@ import java.util.List;
*/ */
public class MinstrelTrait extends Trait { 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 volume = 1F;
private float pitch = 1F; private float pitch = 1F;
@ -59,7 +59,7 @@ public class MinstrelTrait extends Trait {
} }
this.volume = (float) key.getDouble("volume", 1D); this.volume = (float) key.getDouble("volume", 1D);
this.pitch = (float) key.getDouble("pitch", 1D); this.pitch = (float) key.getDouble("pitch", 1D);
this.playlist.play(this); this.playlist.changeSong();
//Register the minstrel to allow stopping the playback later //Register the minstrel to allow stopping the playback later
MinstrelPlugin.getInstance().addMinstrel(this); 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);
}
} }

View File

@ -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 <p>The player to check</p>
* @param minstrelTrait <p>The minstrel to check</p>
* @return <p>True if the player would hear the minstrel</p>
*/
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));
}
}