Adds proper playback of any song

Adds several commands to control songs, volumes and pitches
Moves code into proper packages
Removes test song
Adds loading and saving of minstrel data
This commit is contained in:
Kristian Knarvik 2022-10-30 19:29:56 +01:00
parent bfa49448ab
commit 2babceeb87
12 changed files with 582 additions and 66 deletions

View File

@ -1,8 +1,12 @@
package net.knarcraft.minstrel;
import net.citizensnpcs.api.CitizensAPI;
import net.knarcraft.minstrel.command.MinstrelCommand;
import net.knarcraft.minstrel.command.MinstrelTabCompleter;
import net.knarcraft.minstrel.listener.PlayerListener;
import net.knarcraft.minstrel.trait.MinstrelTrait;
import org.bukkit.Bukkit;
import org.bukkit.SoundCategory;
import org.bukkit.command.PluginCommand;
import org.bukkit.plugin.PluginManager;
import org.bukkit.plugin.java.JavaPlugin;
@ -15,7 +19,8 @@ import java.util.List;
public final class MinstrelPlugin extends JavaPlugin {
private static MinstrelPlugin instance;
private static Playlist testPlaylist;
private final List<MinstrelTrait> knownMinstrels = new ArrayList<>();
@Override
public void onEnable() {
@ -26,28 +31,30 @@ public final class MinstrelPlugin extends JavaPlugin {
CitizensAPI.getTraitFactory().registerTrait(
net.citizensnpcs.api.trait.TraitInfo.create(MinstrelTrait.class).withName("minstrel"));
Song testSong = new Song(SoundCategory.RECORDS, "minecraft:records.custom.medieval_3_g_mixolydian", 114);
List<Song> songs = new ArrayList<>();
songs.add(testSong);
testPlaylist = new Playlist(songs, true);
PluginManager pluginManager = Bukkit.getPluginManager();
pluginManager.registerEvents(new PlayerListener(), this);
PluginCommand minstrelCommand = this.getCommand("minstrel");
if (minstrelCommand != null) {
minstrelCommand.setExecutor(new MinstrelCommand());
minstrelCommand.setTabCompleter(new MinstrelTabCompleter());
}
}
@Override
public void onDisable() {
// Plugin shutdown logic
//TODO: Stop all songs in all playlists
for (MinstrelTrait minstrelTrait : knownMinstrels) {
minstrelTrait.getPlaylist().stop();
}
}
/**
* Gets the playlist which is currently hard-coded for testing
* Adds the given minstrel to the list of known minstrels
*
* @return <p>The test playlist</p>
* @param minstrelTrait <p>The minstrel to add</p>
*/
public static Playlist getTestPlaylist() {
return testPlaylist;
public void addMinstrel(MinstrelTrait minstrelTrait) {
knownMinstrels.add(minstrelTrait);
}
/**

View File

@ -1,44 +0,0 @@
package net.knarcraft.minstrel;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.util.DataKey;
import org.bukkit.Location;
/**
* The minstrel trait itself, which contains all NPC data for this trait
*/
public class MinstrelTrait extends Trait {
public MinstrelTrait() {
super("minstrel");
}
/**
* Gets the location of this minstrel
*
* @return <p>The location of this minstrel</p>
*/
public Location getLocation() {
return this.getNPC().getStoredLocation();
}
/**
* Loads all config values stored in citizens' config file for this NPC
*
* @param key <p>The data key used for the config root</p>
*/
@Override
public void load(DataKey key) {
//TODO: Actually load the playlist set to this minstrel
MinstrelPlugin.getTestPlaylist().play(this);
}
public float getVolume() {
return 1F;
}
public float getPitch() {
return 1F;
}
}

View File

@ -0,0 +1,75 @@
package net.knarcraft.minstrel.command;
import net.knarcraft.minstrel.music.Playlist;
import net.knarcraft.minstrel.music.Song;
import net.knarcraft.minstrel.trait.MinstrelTrait;
import org.bukkit.SoundCategory;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
/**
* The command for adding a new song to a minstrel
*/
public class AddSongCommand implements CommandExecutor {
private final MinstrelTrait minstrelTrait;
/**
* Instantiates a new add song command
*
* @param minstrelTrait <p>The minstrel to run this command on</p>
*/
public AddSongCommand(MinstrelTrait minstrelTrait) {
this.minstrelTrait = minstrelTrait;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
if (args.length < 4) {
return false;
}
String categoryString = args[1];
String songIdString = args[2];
String durationString = args[3];
Playlist playlist = minstrelTrait.getPlaylist();
//category identifier duration
SoundCategory category = null;
int duration;
if (!categoryString.equalsIgnoreCase("null")) {
try {
category = SoundCategory.valueOf(categoryString);
} catch (IllegalArgumentException exception) {
sender.sendMessage("The specified category cannot be recognized");
return false;
}
}
try {
duration = Integer.parseInt(durationString);
} catch (NumberFormatException exception) {
sender.sendMessage("The duration specified is not a valid number");
return false;
}
if (duration < 1) {
sender.sendMessage("The duration specified is not positive");
return false;
}
if (songIdString.trim().isEmpty()) {
sender.sendMessage("The song identifier is empty");
return false;
}
Song song = new Song(category, songIdString, duration);
playlist.addSong(song);
if (playlist.getSongs().size() == 1) {
playlist.play(minstrelTrait);
}
sender.sendMessage("New song added");
return true;
}
}

View File

@ -0,0 +1,38 @@
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;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
/**
* The command for listing a minstrel's current songs
*/
public class ListSongsCommand implements CommandExecutor {
private final MinstrelTrait minstrelTrait;
/**
* Instantiates a new list songs command
*
* @param minstrelTrait <p>The minstrel to run this command on</p>
*/
public ListSongsCommand(MinstrelTrait minstrelTrait) {
this.minstrelTrait = minstrelTrait;
}
@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());
return true;
}
}

View File

@ -0,0 +1,114 @@
package net.knarcraft.minstrel.command;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.npc.NPC;
import net.knarcraft.minstrel.trait.MinstrelTrait;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
public class MinstrelCommand implements CommandExecutor {
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
NPC npc = CitizensAPI.getDefaultNPCSelector().getSelected(sender);
if (npc == null || !npc.hasTrait(MinstrelTrait.class)) {
sender.sendMessage("You must select a minstrel NPC before running this command");
return true;
}
MinstrelTrait minstrelTrait = npc.getTraitNullable(MinstrelTrait.class);
if (args.length < 1) {
return false;
}
switch (args[0].toLowerCase()) {
case "addsong":
return new AddSongCommand(minstrelTrait).onCommand(sender, command, label, args);
case "removesong":
return new RemoveSongCommand(minstrelTrait).onCommand(sender, command, label, args);
case "listsongs":
return new ListSongsCommand(minstrelTrait).onCommand(sender, command, label, args);
case "pitch":
return updatePitch(minstrelTrait, args.length > 1 ? args[1] : null, sender);
case "volume":
return updateVolume(minstrelTrait, args.length > 1 ? args[1] : null, sender);
}
/* Sub-commands:
AddSong category identifier duration (remember to run play again)
RemoveSong index
ListSongs
Global sub-commands
StopAll - Stops all minstrels from playing
PlayALl - Starts playing for all minstrels
//TODO: Perhaps split this into two plugins instead? Or make this a generic plugin which happens to add a Minstrel trait?
CreatePlaylist name
AddSong playlist category identifier duration
Play playlist - Plays the specified playlist at the executor's location
*/
return false;
}
/**
* Updates the pitch for a minstrel if possible
*
* @param minstrelTrait <p>The minstrel to update the pitch for</p>
* @param newPitch <p>The new pitch for the minstrel</p>
* @param sender <p>The sender to send error/success messages to</p>
* @return <p>True if the pitch was successfully updated</p>
*/
private boolean updatePitch(MinstrelTrait minstrelTrait, String newPitch, CommandSender sender) {
if (newPitch == null) {
sender.sendMessage("Current pitch: " + minstrelTrait.getPitch());
return true;
}
try {
float pitch = Float.parseFloat(newPitch);
if (pitch < 0) {
sender.sendMessage("The pitch cannot be negative");
} else {
minstrelTrait.setPitch(pitch);
}
} catch (NumberFormatException exception) {
sender.sendMessage("The given pitch is not a number!");
return false;
}
sender.sendMessage("Pitch set to " + newPitch);
return true;
}
/**
* Updates the volume for a minstrel if possible
*
* @param minstrelTrait <p>The minstrel to update the pitch for</p>
* @param newVolume <p>The new volume for the minstrel</p>
* @param sender <p>The sender to send error/success messages to</p>
* @return <p>True if the volume was successfully updated</p>
*/
private boolean updateVolume(MinstrelTrait minstrelTrait, String newVolume, CommandSender sender) {
if (newVolume == null) {
sender.sendMessage("Current volume: " + minstrelTrait.getVolume());
return true;
}
try {
float volume = Float.parseFloat(newVolume);
if (volume <= 0) {
sender.sendMessage("The volume must be greater than 0");
} else {
minstrelTrait.setVolume(volume);
}
} catch (NumberFormatException exception) {
sender.sendMessage("The given volume is not a number!");
return false;
}
sender.sendMessage("Volume set to " + newVolume);
return true;
}
}

View File

@ -0,0 +1,52 @@
package net.knarcraft.minstrel.command;
import org.bukkit.SoundCategory;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.TabCompleter;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
public class MinstrelTabCompleter implements TabCompleter {
private final List<String> baseCommands;
private final List<String> soundCategories;
public MinstrelTabCompleter() {
baseCommands = new ArrayList<>();
baseCommands.add("addsong");
baseCommands.add("removesong");
baseCommands.add("listsongs");
baseCommands.add("pitch");
baseCommands.add("volume");
soundCategories = new ArrayList<>();
for (SoundCategory category : SoundCategory.values()) {
soundCategories.add(category.name());
}
}
@Nullable
@Override
public List<String> onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
if (args.length < 2) {
return baseCommands;
}
switch (args[0].toLowerCase()) {
case "addsong":
if (args.length == 2) {
return soundCategories;
} else if (args.length == 3) {
List<String> exampleSongNames = new ArrayList<>();
exampleSongNames.add("minecraft:records.custom.medieval_3_g_mixolydian");
exampleSongNames.add("minecraft:block.amethyst_block.step");
return exampleSongNames;
}
}
return null;
}
}

View File

@ -0,0 +1,50 @@
package net.knarcraft.minstrel.command;
import net.knarcraft.minstrel.music.Playlist;
import net.knarcraft.minstrel.trait.MinstrelTrait;
import org.bukkit.command.Command;
import org.bukkit.command.CommandExecutor;
import org.bukkit.command.CommandSender;
import org.jetbrains.annotations.NotNull;
/**
* The command for removing a song from a minstrel
*/
public class RemoveSongCommand implements CommandExecutor {
private final MinstrelTrait minstrelTrait;
/**
* Instantiates a new remove song command
*
* @param minstrelTrait <p>The minstrel to run this command on</p>
*/
public RemoveSongCommand(MinstrelTrait minstrelTrait) {
this.minstrelTrait = minstrelTrait;
}
@Override
public boolean onCommand(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label,
@NotNull String[] args) {
Playlist playlist = minstrelTrait.getPlaylist();
int index;
try {
index = Integer.parseInt(args[1]);
} catch (NumberFormatException exception) {
sender.sendMessage("The given index is not an integer");
return false;
}
if (index >= 0 && playlist.getSongs().size() > index) {
playlist.removeSong(index);
//Stop any minstrels from playing the removed song
minstrelTrait.getPlaylist().stop();
minstrelTrait.getPlaylist().play(minstrelTrait);
sender.sendMessage("Song removed");
} else {
sender.sendMessage("The specified index is outside the bounds of the playlist");
return false;
}
return true;
}
}

View File

@ -1,7 +1,8 @@
package net.knarcraft.minstrel;
package net.knarcraft.minstrel.listener;
import net.citizensnpcs.api.CitizensAPI;
import net.citizensnpcs.api.npc.NPC;
import net.knarcraft.minstrel.trait.MinstrelTrait;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerJoinEvent;
@ -15,7 +16,8 @@ public class PlayerListener implements Listener {
public void playerJoinListener(PlayerJoinEvent event) {
for (NPC npc : CitizensAPI.getNPCRegistry()) {
if (npc.hasTrait(MinstrelTrait.class)) {
MinstrelPlugin.getTestPlaylist().play(npc.getTraitNullable(MinstrelTrait.class), event.getPlayer());
MinstrelTrait minstrelTrait = npc.getTraitNullable(MinstrelTrait.class);
minstrelTrait.getPlaylist().play(minstrelTrait, event.getPlayer());
}
}
}

View File

@ -1,5 +1,7 @@
package net.knarcraft.minstrel;
package net.knarcraft.minstrel.music;
import net.knarcraft.minstrel.MinstrelPlugin;
import net.knarcraft.minstrel.trait.MinstrelTrait;
import org.bukkit.Bukkit;
import org.bukkit.entity.Player;
@ -14,7 +16,7 @@ public class Playlist {
private final List<Song> songs;
private final boolean loop;
private int currentlyPlaying = 0;
private int schedulerId = 0;
private int schedulerId = -1;
/**
* Instantiates a new playlist
@ -54,6 +56,33 @@ public class Playlist {
this.songs.remove(index);
}
/**
* Stops all songs in this playlist for the given player
*
* @param player <p>The player to stop the playlist for</p>
*/
public void stop(Player player) {
for (Song song : this.songs) {
if (song.isPlaying()) {
song.stop(player);
}
}
}
/**
* Stops all songs in this playlist for all players, and aborts scheduling for the next song
*/
public void stop() {
for (Song song : this.songs) {
if (song.isPlaying()) {
for (Player player : Bukkit.getOnlinePlayers()) {
song.stop(player);
}
}
}
Bukkit.getScheduler().cancelTask(schedulerId);
}
/**
* Plays the current song for the given player
*
@ -69,10 +98,7 @@ public class Playlist {
return;
}
for (Song song : this.songs) {
song.stop(player);
}
stop(player);
Song currentSong = this.songs.get(this.currentlyPlaying - 1);
currentSong.play(trait, player, trait.getVolume(), trait.getPitch());
}
@ -113,4 +139,11 @@ public class Playlist {
}, currentSong.getDuration(), 20);
}
/**
* Clears this playlist, removing any existing songs
*/
public void clear() {
this.songs.clear();
}
}

View File

@ -1,5 +1,7 @@
package net.knarcraft.minstrel;
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;
@ -112,4 +114,35 @@ public class Song {
}
}
@Override
public String toString() {
String songString = this.category + ":" + this.sound + ":" + this.durationSeconds;
if (this.isPlaying) {
return ">" + songString;
} else {
return songString;
}
}
/**
* Gets the category of this song
*
* <p>Note: The category is only used to tell which volume slider affects the song volume, and which type of sounds
* may prevent this from playing. RECORDS is preferable for minstrels.</p>
*
* @return <p>The category of this song</p>
*/
public SoundCategory getCategory() {
return category;
}
/**
* Gets the identifier for this song's sound
*
* @return <p>The identifier for this song's sound</p>
*/
public String getSound() {
return this.sound;
}
}

View File

@ -0,0 +1,146 @@
package net.knarcraft.minstrel.trait;
import net.citizensnpcs.api.trait.Trait;
import net.citizensnpcs.api.util.DataKey;
import net.knarcraft.minstrel.MinstrelPlugin;
import net.knarcraft.minstrel.music.Playlist;
import net.knarcraft.minstrel.music.Song;
import org.bukkit.Location;
import org.bukkit.SoundCategory;
import java.util.ArrayList;
import java.util.List;
/**
* The minstrel trait itself, which contains all NPC data for this trait
*/
public class MinstrelTrait extends Trait {
private final Playlist playlist = new Playlist(new ArrayList<>(), true);
private float volume = 1F;
private float pitch = 1F;
/**
* Instantiates a new minstrel trait
*/
public MinstrelTrait() {
super("minstrel");
}
/**
* Gets the location of this minstrel
*
* @return <p>The location of this minstrel</p>
*/
public Location getLocation() {
return this.getNPC().getStoredLocation();
}
/**
* Loads all config values stored in citizens' config file for this NPC
*
* @param key <p>The data key used for the config root</p>
*/
@Override
public void load(DataKey key) {
this.playlist.clear();
for (DataKey songKey : key.getRelative("playlist").getSubKeys()) {
String category = songKey.getString("category");
SoundCategory soundCategory;
if (category.equalsIgnoreCase("null")) {
soundCategory = null;
} else {
soundCategory = SoundCategory.valueOf(category);
}
String soundIdentifier = songKey.getString("identifier");
int songDuration = songKey.getInt("duration");
Song song = new Song(soundCategory, soundIdentifier, songDuration);
this.playlist.addSong(song);
}
this.volume = (float) key.getDouble("volume", 1D);
this.pitch = (float) key.getDouble("pitch", 1D);
this.playlist.play(this);
//Register the minstrel to allow stopping the playback later
MinstrelPlugin.getInstance().addMinstrel(this);
}
/**
* Saves all of this NPC's config values to citizens' config file
*
* @param key <p>The data key used for the config root</p>
*/
@Override
public void save(DataKey key) {
//Saves the songs in the playlist, the volume and the pitch
key.setRaw("playlist", null);
List<Song> songs = this.playlist.getSongs();
for (int i = 0; i < songs.size(); i++) {
Song song = songs.get(i);
String songKey = "playlist." + i;
if (song.getCategory() == null) {
key.setString(songKey + ".category", "null");
} else {
key.setString(songKey + ".category", song.getCategory().name());
}
key.setString(songKey + ".identifier", song.getSound());
key.setInt(songKey + ".duration", song.getDuration());
}
key.setRaw("volume", this.getVolume());
key.setRaw("pitch", this.getPitch());
}
/**
* Gets the volume to use for this minstrel
*
* @return <p>The volume of this minstrel</p>
*/
public float getVolume() {
return this.volume;
}
/**
* Gets the pitch to use for this minstrel
*
* @return <p>The pitch of this minstrel</p>
*/
public float getPitch() {
return this.pitch;
}
/**
* Gets this minstrel's playlist
*
* @return <p>This minstrel's playlist</p>
*/
public Playlist getPlaylist() {
return this.playlist;
}
/**
* Sets the volume for this minstrel
*
* @param newVolume <p>The volume for this minstrel</p>
*/
public void setVolume(float newVolume) {
if (newVolume > 0) {
this.volume = newVolume;
} else {
throw new IllegalArgumentException("Volume cannot be negative!");
}
}
/**
* Sets the pitch for this minstrel
*
* @param newPitch <p>The new pitch for this minstrel</p>
*/
public void setPitch(float newPitch) {
if (newPitch > 0) {
this.pitch = newPitch;
} else {
throw new IllegalArgumentException("Pitch cannot be negative!");
}
}
}

View File

@ -6,3 +6,13 @@ prefix: Minstrel
depend: [ Citizens ]
authors: [ EpicKnarvik97 ]
description: Adds a minstrel trait to Citizens NPCs
commands:
minstrel:
permission: minstrel.admin
usage: /<command> <addsong/removesong/listsongs/pitch/volume> [argument]
description: Used to see, or alter, a minstrel's songs
permissions:
minstrel.admin:
description: Allows configuring minstrels
default: op