From 6f326e16a6a01c6962cd16e5a5290852f3b56197 Mon Sep 17 00:00:00 2001 From: IanTapply22 Date: Sun, 1 Dec 2024 22:42:23 -0500 Subject: [PATCH] Create basic NBS song playing system --- .../com/iantapply/wynncraft/Wynncraft.java | 9 + .../wynncraft/command/CommandCore.java | 4 + .../command/commands/nbs/NbsCommand.java | 45 +++++ .../command/commands/nbs/NbsCore.java | 12 ++ .../command/commands/nbs/NbsPlayCommand.java | 60 ++++++ .../command/commands/nbs/NbsStopCommand.java | 41 ++++ .../wynncraft/event/PlayerJoinEvent.java | 9 +- .../wynncraft/nbs/CustomInstrument.java | 21 ++ .../iantapply/wynncraft/nbs/Instrument.java | 18 ++ .../com/iantapply/wynncraft/nbs/NBSCore.java | 45 +++++ .../wynncraft/nbs/enums/NotePitch.java | 48 +++++ .../iantapply/wynncraft/nbs/enums/Sound.java | 66 +++++++ .../nbs/handling/NBSFormatDecoder.java | 182 ++++++++++++++++++ .../wynncraft/nbs/handling/NBSLayer.java | 21 ++ .../wynncraft/nbs/handling/NBSNote.java | 40 ++++ .../wynncraft/nbs/handling/NBSSong.java | 58 ++++++ .../wynncraft/nbs/players/NBSSongPlayer.java | 171 ++++++++++++++++ .../nbs/players/PlayerOrientedSongPlayer.java | 40 ++++ .../wynncraft/nbs/utils/InstrumentUtils.java | 86 +++++++++ .../wynncraft/nbs/utils/Interpolator.java | 87 +++++++++ .../wynncraft/nbs/utils/VersionUtils.java | 76 ++++++++ src/main/resources/plugin.yml | 6 +- 22 files changed, 1143 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/iantapply/wynncraft/command/commands/nbs/NbsCommand.java create mode 100644 src/main/java/com/iantapply/wynncraft/command/commands/nbs/NbsCore.java create mode 100644 src/main/java/com/iantapply/wynncraft/command/commands/nbs/NbsPlayCommand.java create mode 100644 src/main/java/com/iantapply/wynncraft/command/commands/nbs/NbsStopCommand.java create mode 100644 src/main/java/com/iantapply/wynncraft/nbs/CustomInstrument.java create mode 100644 src/main/java/com/iantapply/wynncraft/nbs/Instrument.java create mode 100644 src/main/java/com/iantapply/wynncraft/nbs/NBSCore.java create mode 100644 src/main/java/com/iantapply/wynncraft/nbs/enums/NotePitch.java create mode 100644 src/main/java/com/iantapply/wynncraft/nbs/enums/Sound.java create mode 100644 src/main/java/com/iantapply/wynncraft/nbs/handling/NBSFormatDecoder.java create mode 100644 src/main/java/com/iantapply/wynncraft/nbs/handling/NBSLayer.java create mode 100644 src/main/java/com/iantapply/wynncraft/nbs/handling/NBSNote.java create mode 100644 src/main/java/com/iantapply/wynncraft/nbs/handling/NBSSong.java create mode 100644 src/main/java/com/iantapply/wynncraft/nbs/players/NBSSongPlayer.java create mode 100644 src/main/java/com/iantapply/wynncraft/nbs/players/PlayerOrientedSongPlayer.java create mode 100644 src/main/java/com/iantapply/wynncraft/nbs/utils/InstrumentUtils.java create mode 100644 src/main/java/com/iantapply/wynncraft/nbs/utils/Interpolator.java create mode 100644 src/main/java/com/iantapply/wynncraft/nbs/utils/VersionUtils.java diff --git a/src/main/java/com/iantapply/wynncraft/Wynncraft.java b/src/main/java/com/iantapply/wynncraft/Wynncraft.java index ee0aa8e..44feb99 100644 --- a/src/main/java/com/iantapply/wynncraft/Wynncraft.java +++ b/src/main/java/com/iantapply/wynncraft/Wynncraft.java @@ -2,8 +2,11 @@ import com.iantapply.wynncraft.command.CommandCore; import com.iantapply.wynncraft.database.DatabaseCore; +import com.iantapply.wynncraft.event.PlayerJoinEvent; import com.iantapply.wynncraft.logger.Logger; +import com.iantapply.wynncraft.nbs.NBSCore; import lombok.Getter; +import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; public final class Wynncraft extends JavaPlugin { @@ -11,6 +14,8 @@ public final class Wynncraft extends JavaPlugin { public static Wynncraft instance; @Getter public CommandCore commandCore; + @Getter + public NBSCore nbsCore; @Override public void onEnable() { @@ -25,6 +30,10 @@ public void onEnable() { this.commandCore.initialize(); this.commandCore.registerCommands(); + this.nbsCore = new NBSCore(); + + Bukkit.getServer().getPluginManager().registerEvents(new PlayerJoinEvent(), this); + Logger.logStartup(); } diff --git a/src/main/java/com/iantapply/wynncraft/command/CommandCore.java b/src/main/java/com/iantapply/wynncraft/command/CommandCore.java index bb6f5ae..3822012 100644 --- a/src/main/java/com/iantapply/wynncraft/command/CommandCore.java +++ b/src/main/java/com/iantapply/wynncraft/command/CommandCore.java @@ -6,6 +6,7 @@ import com.iantapply.wynncraft.command.commands.guild.GuildCommandsCore; import com.iantapply.wynncraft.command.commands.housing.HousingCommandsCore; import com.iantapply.wynncraft.command.commands.ignore.IgnoreCommandsCore; +import com.iantapply.wynncraft.command.commands.nbs.NbsCore; import com.iantapply.wynncraft.command.commands.party.PartyCommandsCore; import com.iantapply.wynncraft.command.commands.toggle.ToggleCommandsCore; import com.iantapply.wynncraft.logger.Logger; @@ -66,6 +67,8 @@ public void initialize() { CosmeticCommandsCore cosmeticCommandsCore = new CosmeticCommandsCore(); ToggleCommandsCore toggleCommandsCore = new ToggleCommandsCore(); + NbsCore nbsCore = new NbsCore(); + gameCommandsCore.initialize(); partyCommandsCore.initialize(); ignoreCommandsCore.initialize(); @@ -74,6 +77,7 @@ public void initialize() { guildCommandsCore.initialize(); cosmeticCommandsCore.initialize(); toggleCommandsCore.initialize(); + nbsCore.initialize(); } /** diff --git a/src/main/java/com/iantapply/wynncraft/command/commands/nbs/NbsCommand.java b/src/main/java/com/iantapply/wynncraft/command/commands/nbs/NbsCommand.java new file mode 100644 index 0000000..3b5f805 --- /dev/null +++ b/src/main/java/com/iantapply/wynncraft/command/commands/nbs/NbsCommand.java @@ -0,0 +1,45 @@ +package com.iantapply.wynncraft.command.commands.nbs; + +import com.iantapply.wynncraft.command.WynncraftCommand; +import org.bukkit.command.CommandSender; + +import java.util.ArrayList; + +public class NbsCommand extends WynncraftCommand { + @Override + public String name() { + return "nbs"; + } + + @Override + public String syntax() { + return "nbs "; + } + + @Override + public String description() { + return "Handles the testing and playing of NBS files."; + } + + @Override + public ArrayList subcommands() { + ArrayList subcommands = new ArrayList<>(); + subcommands.add(new NbsPlayCommand()); + subcommands.add(new NbsStopCommand()); + + return subcommands; + } + + @Override + public int minArgs() { + return 1; + } + + @Override + public int maxArgs() { + return 2; + } + + @Override + public void execute(CommandSender sender, String[] args) {} +} diff --git a/src/main/java/com/iantapply/wynncraft/command/commands/nbs/NbsCore.java b/src/main/java/com/iantapply/wynncraft/command/commands/nbs/NbsCore.java new file mode 100644 index 0000000..b62e7ed --- /dev/null +++ b/src/main/java/com/iantapply/wynncraft/command/commands/nbs/NbsCore.java @@ -0,0 +1,12 @@ +package com.iantapply.wynncraft.command.commands.nbs; + +import com.iantapply.wynncraft.Wynncraft; +import com.iantapply.wynncraft.nbs.players.PlayerOrientedSongPlayer; + +public class NbsCore { + public static PlayerOrientedSongPlayer player; + + public void initialize() { + Wynncraft.getInstance().getCommandCore().stageCommand(new NbsCommand()); + } +} diff --git a/src/main/java/com/iantapply/wynncraft/command/commands/nbs/NbsPlayCommand.java b/src/main/java/com/iantapply/wynncraft/command/commands/nbs/NbsPlayCommand.java new file mode 100644 index 0000000..4abd246 --- /dev/null +++ b/src/main/java/com/iantapply/wynncraft/command/commands/nbs/NbsPlayCommand.java @@ -0,0 +1,60 @@ +package com.iantapply.wynncraft.command.commands.nbs; + +import com.iantapply.wynncraft.Wynncraft; +import com.iantapply.wynncraft.command.WynncraftCommand; +import com.iantapply.wynncraft.nbs.handling.NBSFormatDecoder; +import com.iantapply.wynncraft.nbs.handling.NBSSong; +import com.iantapply.wynncraft.nbs.players.PlayerOrientedSongPlayer; +import org.bukkit.SoundCategory; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; + +public class NbsPlayCommand extends WynncraftCommand { + @Override + public String name() { + return "play"; + } + + @Override + public String syntax() { + return "play "; + } + + @Override + public String description() { + return "Plays an NBS file from the plugins folder."; + } + + @Override + public int minArgs() { + return 1; + } + + @Override + public int maxArgs() { + return 1; + } + + @Override + public void execute(CommandSender sender, String[] args) { + try { + Player player = (Player) sender; + File songFile = new File("./plugins/" + args[0]); + InputStream inputStreamSong = new FileInputStream(songFile); + NBSSong song = NBSFormatDecoder.parse(inputStreamSong); + NbsCore.player = new PlayerOrientedSongPlayer(Wynncraft.getInstance().getNbsCore(), song); + NbsCore.player.setPlayer(player); + NbsCore.player.setSoundCategory(SoundCategory.RECORDS); + + NbsCore.player.addPlayer(player); + NbsCore.player.setPlaying(true); + sender.sendMessage("Started playing NBS song: " + args[0]); + } catch (Exception e) { + sender.sendMessage("The provided file does not exist. Please provide a valid NBS file in the plugins directory."); + } + } +} diff --git a/src/main/java/com/iantapply/wynncraft/command/commands/nbs/NbsStopCommand.java b/src/main/java/com/iantapply/wynncraft/command/commands/nbs/NbsStopCommand.java new file mode 100644 index 0000000..3fee2c5 --- /dev/null +++ b/src/main/java/com/iantapply/wynncraft/command/commands/nbs/NbsStopCommand.java @@ -0,0 +1,41 @@ +package com.iantapply.wynncraft.command.commands.nbs; + +import com.iantapply.wynncraft.command.WynncraftCommand; +import org.bukkit.command.CommandSender; + +public class NbsStopCommand extends WynncraftCommand { + @Override + public String name() { + return "stop"; + } + + @Override + public String syntax() { + return "stop"; + } + + @Override + public String description() { + return "Stops playing the currently playing NBS song."; + } + + @Override + public int minArgs() { + return 0; + } + + @Override + public int maxArgs() { + return 0; + } + + @Override + public void execute(CommandSender sender, String[] args) { + try { + NbsCore.player.destroy(); + sender.sendMessage("Stopped playing NBS song."); + } catch(Exception e) { + sender.sendMessage("Could not stop playing NBS song."); + } + } +} diff --git a/src/main/java/com/iantapply/wynncraft/event/PlayerJoinEvent.java b/src/main/java/com/iantapply/wynncraft/event/PlayerJoinEvent.java index c8f57b1..e5ca1b7 100644 --- a/src/main/java/com/iantapply/wynncraft/event/PlayerJoinEvent.java +++ b/src/main/java/com/iantapply/wynncraft/event/PlayerJoinEvent.java @@ -1,4 +1,11 @@ package com.iantapply.wynncraft.event; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; -public class PlayerJoinEvent { +public class PlayerJoinEvent implements Listener { + + @EventHandler + public void onPlayerJoin(org.bukkit.event.player.PlayerJoinEvent event) { + // TODO: Create a model that will store the players data upon joining, that including the stats from DB and NBS player instance + } } diff --git a/src/main/java/com/iantapply/wynncraft/nbs/CustomInstrument.java b/src/main/java/com/iantapply/wynncraft/nbs/CustomInstrument.java new file mode 100644 index 0000000..ff4f517 --- /dev/null +++ b/src/main/java/com/iantapply/wynncraft/nbs/CustomInstrument.java @@ -0,0 +1,21 @@ +package com.iantapply.wynncraft.nbs; + +import com.iantapply.wynncraft.nbs.enums.Sound; +import lombok.Getter; + +@Getter +public class CustomInstrument{ + private final byte index; + private final String name; + private final String soundFileName; + private org.bukkit.Sound sound; + + public CustomInstrument(byte index, String name, String soundFileName) { + this.index = index; + this.name = name; + this.soundFileName = soundFileName.replaceAll(".ogg", ""); + if (this.soundFileName.equalsIgnoreCase("pling")){ + this.sound = Sound.NOTE_PLING.bukkitSound(); + } + } +} diff --git a/src/main/java/com/iantapply/wynncraft/nbs/Instrument.java b/src/main/java/com/iantapply/wynncraft/nbs/Instrument.java new file mode 100644 index 0000000..da1b0b7 --- /dev/null +++ b/src/main/java/com/iantapply/wynncraft/nbs/Instrument.java @@ -0,0 +1,18 @@ +package com.iantapply.wynncraft.nbs; + +import com.iantapply.wynncraft.nbs.utils.InstrumentUtils; + +public class Instrument { + + public static org.bukkit.Sound getInstrument(byte instrument) { + return org.bukkit.Sound.valueOf(getInstrumentName(instrument)); + } + + public static String getInstrumentName(byte instrument) { + return InstrumentUtils.getInstrumentName(instrument); + } + + public static org.bukkit.Instrument getBukkitInstrument(byte instrument) { + return InstrumentUtils.getBukkitInstrument(instrument); + } +} diff --git a/src/main/java/com/iantapply/wynncraft/nbs/NBSCore.java b/src/main/java/com/iantapply/wynncraft/nbs/NBSCore.java new file mode 100644 index 0000000..1b5aad9 --- /dev/null +++ b/src/main/java/com/iantapply/wynncraft/nbs/NBSCore.java @@ -0,0 +1,45 @@ +package com.iantapply.wynncraft.nbs; + +import com.iantapply.wynncraft.nbs.players.NBSSongPlayer; +import lombok.Getter; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.HashMap; + +@Getter +public class NBSCore { + public HashMap> playingSongs = new HashMap<>(); + public HashMap playerVolume = new HashMap<>(); + public NBSCore instance; + + public NBSCore() { + this.instance = this; + } + + public boolean isReceivingSong(Player p) { + return ((this.playingSongs.get(p.getName()) != null) && (!this.playingSongs.get(p.getName()).isEmpty())); + } + + public void stopPlaying(Player p) { + if (this.playingSongs.get(p.getName()) == null) { + return; + } + for (NBSSongPlayer s : this.playingSongs.get(p.getName())) { + s.removePlayer(p); + } + } + + public void setPlayerVolume(Player p, byte volume) { + this.playerVolume.put(p.getName(), volume); + } + + public byte getPlayerVolume(Player p) { + Byte b = this.playerVolume.get(p.getName()); + if (b == null) { + b = 100; + playerVolume.put(p.getName(), b); + } + return b; + } +} diff --git a/src/main/java/com/iantapply/wynncraft/nbs/enums/NotePitch.java b/src/main/java/com/iantapply/wynncraft/nbs/enums/NotePitch.java new file mode 100644 index 0000000..0f43bd5 --- /dev/null +++ b/src/main/java/com/iantapply/wynncraft/nbs/enums/NotePitch.java @@ -0,0 +1,48 @@ +package com.iantapply.wynncraft.nbs.enums; + +public enum NotePitch { + + NOTE_0(0, 0.5F), + NOTE_1(1, 0.53F), + NOTE_2(2, 0.56F), + NOTE_3(3, 0.6F), + NOTE_4(4, 0.63F), + NOTE_5(5, 0.67F), + NOTE_6(6, 0.7F), + NOTE_7(7, 0.76F), + NOTE_8(8, 0.8F), + NOTE_9(9, 0.84F), + NOTE_10(10, 0.9F), + NOTE_11(11, 0.94F), + NOTE_12(12, 1.0F), + NOTE_13(13, 1.06F), + NOTE_14(14, 1.12F), + NOTE_15(15, 1.18F), + NOTE_16(16, 1.26F), + NOTE_17(17, 1.34F), + NOTE_18(18, 1.42F), + NOTE_19(19, 1.5F), + NOTE_20(20, 1.6F), + NOTE_21(21, 1.68F), + NOTE_22(22, 1.78F), + NOTE_23(23, 1.88F), + NOTE_24(24, 2.0F); + + public final int note; + public final float pitch; + + NotePitch(int note, float pitch) { + this.note = note; + this.pitch = pitch; + } + + public static float getPitch(int note) { + for (NotePitch notePitch : values()) { + if (notePitch.note == note) { + return notePitch.pitch; + } + } + + return 0.0F; + } +} diff --git a/src/main/java/com/iantapply/wynncraft/nbs/enums/Sound.java b/src/main/java/com/iantapply/wynncraft/nbs/enums/Sound.java new file mode 100644 index 0000000..e32b7bd --- /dev/null +++ b/src/main/java/com/iantapply/wynncraft/nbs/enums/Sound.java @@ -0,0 +1,66 @@ +package com.iantapply.wynncraft.nbs.enums; + +import java.util.HashMap; +import java.util.Map; + +// TODO: Omit usage of valueOf and use PaperMC registry +public enum Sound { + + NOTE_PIANO("NOTE_PIANO", "BLOCK_NOTE_HARP", "BLOCK_NOTE_BLOCK_HARP"), + NOTE_BASS("NOTE_BASS", "BLOCK_NOTE_BASS", "BLOCK_NOTE_BLOCK_BASS"), + NOTE_BASS_DRUM("NOTE_BASS_DRUM", "BLOCK_NOTE_BASEDRUM", "BLOCK_NOTE_BLOCK_BASEDRUM"), + NOTE_SNARE_DRUM("NOTE_SNARE_DRUM", "BLOCK_NOTE_SNARE", "BLOCK_NOTE_BLOCK_SNARE"), + NOTE_STICKS("NOTE_STICKS", "BLOCK_NOTE_HAT", "BLOCK_NOTE_BLOCK_HAT"), + NOTE_BASS_GUITAR("NOTE_BASS_GUITAR", "BLOCK_NOTE_GUITAR", "BLOCK_NOTE_BLOCK_GUITAR"), + NOTE_FLUTE("NOTE_FLUTE", "BLOCK_NOTE_FLUTE", "BLOCK_NOTE_BLOCK_FLUTE"), + NOTE_BELL("NOTE_BELL", "BLOCK_NOTE_BELL", "BLOCK_NOTE_BLOCK_BELL"), + NOTE_CHIME("NOTE_CHIME", "BLOCK_NOTE_CHIME", "BLOCK_NOTE_BLOCK_CHIME"), + NOTE_XYLOPHONE("NOTE_XYLOPHONE", "BLOCK_NOTE_XYLOPHONE", "BLOCK_NOTE_BLOCK_XYLOPHONE"), + NOTE_PLING("NOTE_PLING", "BLOCK_NOTE_PLING", "BLOCK_NOTE_BLOCK_PLING"), + NOTE_IRON_XYLOPHONE("BLOCK_NOTE_BLOCK_IRON_XYLOPHONE"), + NOTE_COW_BELL("BLOCK_NOTE_BLOCK_COW_BELL"), + NOTE_DIDGERIDOO("BLOCK_NOTE_BLOCK_DIDGERIDOO"), + NOTE_BIT("BLOCK_NOTE_BLOCK_BIT"), + NOTE_BANJO("BLOCK_NOTE_BLOCK_BANJO"); + + private final String[] versionDependentNames; + private org.bukkit.Sound cached; + private static final Map cachedSoundMap = new HashMap<>(); + + Sound(String... versionDependentNames) { + this.versionDependentNames = versionDependentNames; + } + + public static org.bukkit.Sound getFromBukkitName(String bukkitSoundName) { + org.bukkit.Sound sound = cachedSoundMap.get(bukkitSoundName.toUpperCase()); + if (sound != null) + return sound; + + return org.bukkit.Sound.valueOf(bukkitSoundName); + } + + private org.bukkit.Sound getSound() { + if (cached != null) return cached; + for (String name : versionDependentNames) { + try { + return cached = org.bukkit.Sound.valueOf(name); + } catch (IllegalArgumentException ignore2) { + // try next + } + } + return null; + } + + public org.bukkit.Sound bukkitSound() { + if (getSound() != null) { + return getSound(); + } + throw new IllegalArgumentException("Found no valid sound name for " + this.name()); + } + + static { + for (Sound sound : values()) + for (String soundName : sound.versionDependentNames) + cachedSoundMap.put(soundName.toUpperCase(), sound.getSound()); + } +} diff --git a/src/main/java/com/iantapply/wynncraft/nbs/handling/NBSFormatDecoder.java b/src/main/java/com/iantapply/wynncraft/nbs/handling/NBSFormatDecoder.java new file mode 100644 index 0000000..7f6204b --- /dev/null +++ b/src/main/java/com/iantapply/wynncraft/nbs/handling/NBSFormatDecoder.java @@ -0,0 +1,182 @@ +package com.iantapply.wynncraft.nbs.handling; + +import com.iantapply.wynncraft.logger.Logger; +import com.iantapply.wynncraft.logger.LoggingLevel; +import com.iantapply.wynncraft.nbs.CustomInstrument; +import com.iantapply.wynncraft.nbs.utils.VersionUtils; +import com.iantapply.wynncraft.nbs.utils.InstrumentUtils; + +import java.io.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; + +// TODO: Better error handling and rework/lighten up +public class NBSFormatDecoder { + + public static NBSSong parse(File decodeFile) { + try { + return parse(new FileInputStream(decodeFile), decodeFile); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + return null; + } + + public static NBSSong parse(InputStream inputStream) { + return parse(inputStream, null); + } + + private static NBSSong parse(InputStream inputStream, File songFile) { + HashMap layerHashMap = new HashMap<>(); + try { + DataInputStream dataInputStream = new DataInputStream(inputStream); + short length = readShort(dataInputStream); + int firstcustominstrument = 10; + int firstcustominstrumentdiff; + int nbsversion = 0; + if (length == 0) { + nbsversion = dataInputStream.readByte(); + firstcustominstrument = dataInputStream.readByte(); + if (nbsversion >= 3) { + length = readShort(dataInputStream); + } + } + firstcustominstrumentdiff = InstrumentUtils.getCustomInstrumentFirstIndex() - firstcustominstrument; + short songHeight = readShort(dataInputStream); + String title = readString(dataInputStream); + String author = readString(dataInputStream); + readString(dataInputStream); // original author + String description = readString(dataInputStream); + float speed = readShort(dataInputStream) / 100f; + dataInputStream.readBoolean(); // auto-save + dataInputStream.readByte(); // auto-save duration + dataInputStream.readByte(); // x/4ths, time signature + readInt(dataInputStream); // minutes spent on project + readInt(dataInputStream); // left clicks (why?) + readInt(dataInputStream); // right clicks (why?) + readInt(dataInputStream); // blocks added + readInt(dataInputStream); // blocks removed + readString(dataInputStream); // .mid/.schematic file name + if (nbsversion >= 4) { + dataInputStream.readByte(); // loop on/off + dataInputStream.readByte(); // max loop count + readShort(dataInputStream); // loop start tick + } + short tick = -1; + while (true) { + short jumpTicks = readShort(dataInputStream); + if (jumpTicks == 0) break; + tick += jumpTicks; + short layer = -1; + while (true) { + short jumpLayers = readShort(dataInputStream); + if (jumpLayers == 0) break; + layer += jumpLayers; + byte instrument = dataInputStream.readByte(); + + if (firstcustominstrumentdiff > 0 && instrument >= firstcustominstrument){ + instrument += firstcustominstrumentdiff; + } + + byte key = dataInputStream.readByte(); + + if (nbsversion >= 4) { + dataInputStream.readByte(); // note block velocity + dataInputStream.readByte(); // note block panning + readShort(dataInputStream); // note block pitch + } + + setNote(layer, tick, instrument /* instrument */, + key/* note */, layerHashMap); + } + } + + if (nbsversion > 0 && nbsversion < 3) { + length = tick; + } + + for (int i = 0; i < songHeight; i++) { + NBSLayer layer = layerHashMap.get(i); + + String name = readString(dataInputStream); + if (nbsversion >= 4){ + dataInputStream.readByte(); // layer lock + } + + byte volume = dataInputStream.readByte(); + if (nbsversion >= 2){ + dataInputStream.readByte(); // layer stereo + } + if (layer != null) { + layer.setName(name); + layer.setVolume(volume); + } + } + + byte customInstrumentAmount = dataInputStream.readByte(); + CustomInstrument[] customInstrumentsArray = new CustomInstrument[customInstrumentAmount]; + + for (int index = 0; index < customInstrumentAmount; index++) { + customInstrumentsArray[index] = new CustomInstrument((byte) index, + readString(dataInputStream), readString(dataInputStream)); + dataInputStream.readByte(); // pitch + dataInputStream.readByte(); // key + } + + if (firstcustominstrumentdiff < 0){ + ArrayList customInstruments = VersionUtils.getVersionCustomInstrumentsForSong(firstcustominstrument); + customInstruments.addAll(Arrays.asList(customInstrumentsArray)); + customInstrumentsArray = customInstruments.toArray(customInstrumentsArray); + } else { + firstcustominstrument += firstcustominstrumentdiff; + } + + return new NBSSong(speed, layerHashMap, songHeight, length, title, + author, description, songFile, firstcustominstrument, customInstrumentsArray); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } catch (EOFException e) { + Logger.log(LoggingLevel.ERROR, "File is corrupted: " + e.getMessage()); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + + private static void setNote(int layer, int ticks, byte instrument, byte key, HashMap layerHashMap) { + NBSLayer l = layerHashMap.get(layer); + if (l == null) { + l = new NBSLayer(); + layerHashMap.put(layer, l); + } + l.setNote(ticks, new NBSNote(instrument, key)); + } + + private static short readShort(DataInputStream dis) throws IOException { + int byte1 = dis.readUnsignedByte(); + int byte2 = dis.readUnsignedByte(); + return (short) (byte1 + (byte2 << 8)); + } + + private static int readInt(DataInputStream dis) throws IOException { + int byte1 = dis.readUnsignedByte(); + int byte2 = dis.readUnsignedByte(); + int byte3 = dis.readUnsignedByte(); + int byte4 = dis.readUnsignedByte(); + return (byte1 + (byte2 << 8) + (byte3 << 16) + (byte4 << 24)); + } + + private static String readString(DataInputStream dis) throws IOException { + int length = readInt(dis); + StringBuilder sb = new StringBuilder(length); + for (; length > 0; --length) { + char c = (char) dis.readByte(); + if (c == (char) 0x0D) { + c = ' '; + } + sb.append(c); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/iantapply/wynncraft/nbs/handling/NBSLayer.java b/src/main/java/com/iantapply/wynncraft/nbs/handling/NBSLayer.java new file mode 100644 index 0000000..ee848b5 --- /dev/null +++ b/src/main/java/com/iantapply/wynncraft/nbs/handling/NBSLayer.java @@ -0,0 +1,21 @@ +package com.iantapply.wynncraft.nbs.handling; + +import lombok.Getter; +import lombok.Setter; + +import java.util.HashMap; + +@Getter @Setter +public class NBSLayer { + private HashMap hashMap = new HashMap<>(); + private byte volume = 100; + private String name = ""; + + public NBSNote getNote(int tick) { + return hashMap.get(tick); + } + + public void setNote(int tick, NBSNote note) { + hashMap.put(tick, note); + } +} diff --git a/src/main/java/com/iantapply/wynncraft/nbs/handling/NBSNote.java b/src/main/java/com/iantapply/wynncraft/nbs/handling/NBSNote.java new file mode 100644 index 0000000..4eab75f --- /dev/null +++ b/src/main/java/com/iantapply/wynncraft/nbs/handling/NBSNote.java @@ -0,0 +1,40 @@ +package com.iantapply.wynncraft.nbs.handling; + +import lombok.Getter; +import lombok.Setter; + +@Getter +public class NBSNote { + @Setter + private byte instrument; + @Setter + private byte key; + private byte velocity; + @Setter + private int panning; + @Setter + private short pitch; + + public NBSNote(byte instrument, byte key) { + this(instrument, key, (byte) 100, (byte) 100, (short) 0); + } + + public NBSNote(byte instrument, byte key, byte velocity, int panning, short pitch) { + this.instrument = instrument; + this.key = key; + this.velocity = velocity; + this.panning = panning; + this.pitch = pitch; + } + + /** + * Sets note velocity (volume) + * @param velocity number from 0 to 100 + */ + public void setVelocity(byte velocity) { + if (velocity < 0) velocity = 0; + if (velocity > 100) velocity = 100; + + this.velocity = velocity; + } +} diff --git a/src/main/java/com/iantapply/wynncraft/nbs/handling/NBSSong.java b/src/main/java/com/iantapply/wynncraft/nbs/handling/NBSSong.java new file mode 100644 index 0000000..f40ced6 --- /dev/null +++ b/src/main/java/com/iantapply/wynncraft/nbs/handling/NBSSong.java @@ -0,0 +1,58 @@ +package com.iantapply.wynncraft.nbs.handling; + +import com.iantapply.wynncraft.nbs.CustomInstrument; +import lombok.Getter; + +import java.io.File; +import java.util.HashMap; + +@Getter +public class NBSSong { + private final HashMap layerHashMap; + private final short songHeight; + private final short length; + private final String title; + private final File path; + private final String author; + private final String originalAuthor; + private final String description; + private final float speed; + private final float delay; + private final CustomInstrument[] customInstruments; + private final int firstCustomInstrumentIndex; + private final boolean isStereo; + + public NBSSong(float speed, HashMap layerHashMap, + short songHeight, final short length, String title, String author, + String description, File path, int firstCustomInstrumentIndex, CustomInstrument[] customInstruments) { + this(speed, layerHashMap, songHeight, length, title, author, description, path, firstCustomInstrumentIndex, customInstruments, false); + } + + public NBSSong(float speed, HashMap layerHashMap, + short songHeight, final short length, String title, String author, + String description, File path, int firstCustomInstrumentIndex, CustomInstrument[] customInstruments, boolean isStereo) { + this(speed, layerHashMap, songHeight, length, title, author, "", description, path, firstCustomInstrumentIndex, customInstruments, isStereo); + } + + public NBSSong(float speed, HashMap layerHashMap, + short songHeight, final short length, String title, String author, String originalAuthor, + String description, File path, int firstCustomInstrumentIndex, CustomInstrument[] customInstruments, boolean isStereo) { + this.speed = speed; + delay = 20 / speed; + this.layerHashMap = layerHashMap; + this.songHeight = songHeight; + this.length = length; + this.title = title; + this.author = author; + this.originalAuthor = originalAuthor; + this.description = description; + this.path = path; + this.firstCustomInstrumentIndex = firstCustomInstrumentIndex; + this.customInstruments = customInstruments; + this.isStereo = isStereo; + } + + public boolean isStereo() { + return isStereo; + } +} diff --git a/src/main/java/com/iantapply/wynncraft/nbs/players/NBSSongPlayer.java b/src/main/java/com/iantapply/wynncraft/nbs/players/NBSSongPlayer.java new file mode 100644 index 0000000..34304bf --- /dev/null +++ b/src/main/java/com/iantapply/wynncraft/nbs/players/NBSSongPlayer.java @@ -0,0 +1,171 @@ +package com.iantapply.wynncraft.nbs.players; + +import com.iantapply.wynncraft.nbs.NBSCore; +import com.iantapply.wynncraft.nbs.handling.NBSSong; +import com.iantapply.wynncraft.nbs.utils.Interpolator; +import lombok.Getter; +import lombok.Setter; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +// TODO: Make custom events for song playing events +@Getter @Setter +public abstract class NBSSongPlayer { + protected NBSSong song; + protected NBSCore core; + protected boolean playing = false; + protected short tick = -1; + protected ArrayList playerList = new ArrayList<>(); + protected boolean loop; + protected boolean autoDestroy = false; + protected boolean destroyed = false; + protected Thread playerThread; + protected byte fadeTarget = 100; + protected byte volume = 100; + protected byte fadeStart = volume; + protected int fadeDuration = 60; + protected int fadeDone = 0; + + public NBSSongPlayer(NBSCore core, NBSSong song) { + this.song = song; + this.core = core; + createThread(); + } + + protected void calculateFade() { + if (fadeDone == fadeDuration) return; + + double targetVolume = Interpolator.interpLinear(new double[]{0, fadeStart, fadeDuration, fadeTarget}, fadeDone); + setVolume((byte) targetVolume); + fadeDone++; + } + + protected void createThread() { + playerThread = new Thread(() -> { + while (!destroyed) { + long startTime = System.currentTimeMillis(); + synchronized (NBSSongPlayer.this) { + if (playing) { + calculateFade(); + tick++; + if (tick > song.getLength()) { + if(loop){ + tick = 0; + continue; + } + playing = false; + tick = -1; + // TODO: Send custom emit song end event + if (autoDestroy) { + destroy(); + return; + } + } + for (String s : playerList) { + Player p = Bukkit.getPlayerExact(s); + if (p == null) { + // offline... + continue; + } + playTick(p, tick); + } + } + } + long duration = System.currentTimeMillis() - startTime; + float delayMillis = song.getDelay() * 50; + if (duration < delayMillis) { + try { + Thread.sleep((long) (delayMillis - duration)); + } catch (InterruptedException e) { + // do nothing + } + } + } + }); + playerThread.setPriority(Thread.MAX_PRIORITY); + playerThread.start(); + } + + public List getPlayerList() { + return Collections.unmodifiableList(playerList); + } + + public void addPlayer(Player p) { + synchronized (this) { + if (!playerList.contains(p.getName())) { + playerList.add(p.getName()); + ArrayList songs = this.core.getPlayingSongs() + .get(p.getName()); + if (songs == null) { + songs = new ArrayList<>(); + } + songs.add(this); + this.core.getPlayingSongs().put(p.getName(), songs); + } + } + } + public void setLoop(boolean loop) { + synchronized (this) + { + this.loop = loop; + } + } + + public boolean isLoop() { + synchronized (this) + { + return this.loop; + } + } + public boolean getAutoDestroy() { + synchronized (this) { + return autoDestroy; + } + } + + public void setAutoDestroy(boolean value) { + synchronized (this) { + autoDestroy = value; + } + } + + public abstract void playTick(Player p, int tick); + + public void destroy() { + synchronized (this) { + + // TODO: Call song destroy custom emit event and cancel thread via thread ID + destroyed = true; + playing = false; + setTick((short) -1); + } + } + + public void setPlaying(boolean playing) { + this.playing = playing; + if (!playing) { + // TODO: Call song stopped event + } + } + + public void removePlayer(Player p) { + synchronized (this) { + playerList.remove(p.getName()); + if (this.core.getPlayingSongs().get(p.getName()) == null) { + return; + } + ArrayList songs = new ArrayList<>( + this.core.getPlayingSongs().get(p.getName())); + songs.remove(this); + this.core.getPlayingSongs().put(p.getName(), songs); + if (playerList.isEmpty() && autoDestroy) { + // TODO: Call song end event emit + destroy(); + } + } + } +} diff --git a/src/main/java/com/iantapply/wynncraft/nbs/players/PlayerOrientedSongPlayer.java b/src/main/java/com/iantapply/wynncraft/nbs/players/PlayerOrientedSongPlayer.java new file mode 100644 index 0000000..a73e550 --- /dev/null +++ b/src/main/java/com/iantapply/wynncraft/nbs/players/PlayerOrientedSongPlayer.java @@ -0,0 +1,40 @@ +package com.iantapply.wynncraft.nbs.players; + + +import com.iantapply.wynncraft.nbs.*; +import com.iantapply.wynncraft.nbs.enums.NotePitch; +import com.iantapply.wynncraft.nbs.handling.NBSLayer; +import com.iantapply.wynncraft.nbs.handling.NBSNote; +import com.iantapply.wynncraft.nbs.handling.NBSSong; +import lombok.Getter; +import lombok.Setter; +import org.bukkit.SoundCategory; +import org.bukkit.entity.Player; + +@Getter @Setter +public class PlayerOrientedSongPlayer extends NBSSongPlayer { + private Player player; + private SoundCategory soundCategory = SoundCategory.RECORDS; + + public PlayerOrientedSongPlayer(NBSCore core, NBSSong song) { + super(core, song); + } + + @Override + public void playTick(Player p, int tick) { + + byte playerVolume = this.core.getPlayerVolume(p); + + for (NBSLayer l : song.getLayerHashMap().values()) { + NBSNote note = l.getNote(tick); + if (note == null) { + continue; + } + p.playSound(player, + Instrument.getInstrument(note.getInstrument()), + soundCategory, + (l.getVolume() * (int) volume * (int) playerVolume) / 1000000f, + NotePitch.getPitch(note.getKey() - 33)); + } + } +} diff --git a/src/main/java/com/iantapply/wynncraft/nbs/utils/InstrumentUtils.java b/src/main/java/com/iantapply/wynncraft/nbs/utils/InstrumentUtils.java new file mode 100644 index 0000000..8f6e90c --- /dev/null +++ b/src/main/java/com/iantapply/wynncraft/nbs/utils/InstrumentUtils.java @@ -0,0 +1,86 @@ +package com.iantapply.wynncraft.nbs.utils; + +import com.iantapply.wynncraft.nbs.enums.Sound; +import org.bukkit.Instrument; + +public class InstrumentUtils { + + public static String getInstrumentName(byte instrument) { + return switch (instrument) { + case 0 -> Sound.getFromBukkitName("BLOCK_NOTE_BLOCK_HARP").toString(); + case 1 -> Sound.getFromBukkitName("BLOCK_NOTE_BLOCK_BASS").toString(); + case 2 -> Sound.getFromBukkitName("BLOCK_NOTE_BLOCK_BASEDRUM").toString(); + case 3 -> Sound.getFromBukkitName("BLOCK_NOTE_BLOCK_SNARE").toString(); + case 4 -> Sound.getFromBukkitName("BLOCK_NOTE_BLOCK_HAT").toString(); + case 5 -> Sound.getFromBukkitName("BLOCK_NOTE_BLOCK_GUITAR").toString(); + case 6 -> Sound.getFromBukkitName("BLOCK_NOTE_BLOCK_FLUTE").toString(); + case 7 -> Sound.getFromBukkitName("BLOCK_NOTE_BLOCK_BELL").toString(); + case 8 -> Sound.getFromBukkitName("BLOCK_NOTE_BLOCK_CHIME").toString(); + case 9 -> Sound.getFromBukkitName("BLOCK_NOTE_BLOCK_XYLOPHONE").toString(); + case 10 -> Sound.getFromBukkitName("BLOCK_NOTE_BLOCK_IRON_XYLOPHONE").toString(); + case 11 -> Sound.getFromBukkitName("BLOCK_NOTE_BLOCK_COW_BELL").toString(); + case 12 -> Sound.getFromBukkitName("BLOCK_NOTE_BLOCK_DIDGERIDOO").toString(); + case 13 -> Sound.getFromBukkitName("BLOCK_NOTE_BLOCK_BIT").toString(); + case 14 -> Sound.getFromBukkitName("BLOCK_NOTE_BLOCK_BANJO").toString(); + case 15 -> Sound.getFromBukkitName("BLOCK_NOTE_BLOCK_PLING").toString(); + default -> Sound.getFromBukkitName("BLOCK_NOTE_BLOCK_HARP").toString(); + }; + } + + public static Instrument getBukkitInstrument(byte instrument) { + switch (instrument) { + case 0: + return Instrument.PIANO; + case 1: + return Instrument.BASS_GUITAR; + case 2: + return Instrument.BASS_DRUM; + case 3: + return Instrument.SNARE_DRUM; + case 4: + return Instrument.STICKS; + default: { + if (VersionUtils.getServerVersion() >= 0.0112f) { + return switch (instrument) { + case 5 -> Instrument.valueOf("GUITAR"); + case 6 -> Instrument.valueOf("FLUTE"); + case 7 -> Instrument.valueOf("BELL"); + case 8 -> Instrument.valueOf("CHIME"); + case 9 -> Instrument.valueOf("XYLOPHONE"); + default -> { + if (VersionUtils.getServerVersion() >= 0.0114f) { + switch (instrument) { + case 10: + yield Instrument.valueOf("IRON_XYLOPHONE"); + case 11: + yield Instrument.valueOf("COW_BELL"); + case 12: + yield Instrument.valueOf("DIDGERIDOO"); + case 13: + yield Instrument.valueOf("BIT"); + case 14: + yield Instrument.valueOf("BANJO"); + case 15: + yield Instrument.valueOf("PLING"); + } + } + yield Instrument.PIANO; + } + }; + } + return Instrument.PIANO; + } + } + } + + public static byte getCustomInstrumentFirstIndex() { + if (VersionUtils.getServerVersion() >= 0.0114f) { + return 16; + } + if (VersionUtils.getServerVersion() >= 0.0112f) { + return 10; + } + return 5; + } + +} diff --git a/src/main/java/com/iantapply/wynncraft/nbs/utils/Interpolator.java b/src/main/java/com/iantapply/wynncraft/nbs/utils/Interpolator.java new file mode 100644 index 0000000..b190d49 --- /dev/null +++ b/src/main/java/com/iantapply/wynncraft/nbs/utils/Interpolator.java @@ -0,0 +1,87 @@ +package com.iantapply.wynncraft.nbs.utils; + + +import java.util.Arrays; + +/** + *

Static methods for doing useful math


+ * + * @author : $Author: brian $ + * @version : $Revision: 1.1 $ + *

+ *


+ * The Monterey Bay Aquarium Research Institute (MBARI) provides this + * documentation and code "as is", with no warranty, express or + * implied, of its quality or consistency. It is provided without support and + * without obligation on the part of MBARI to assist in its use, correction, + * modification, or enhancement. This information should not be published or + * distributed to third parties without specific written permission from + * MBARI.


+ *

+ * Copyright 2002 MBARI.
+ * MBARI Proprietary Information. All rights reserved.



+ */ +public class Interpolator { + + public static double[] interpLinear(double[] x, double[] y, double[] xi) throws IllegalArgumentException { + + if (x.length != y.length) { + throw new IllegalArgumentException("X and Y must be the same length"); + } + if (x.length == 1) { + throw new IllegalArgumentException("X must contain more than one value"); + } + double[] dx = new double[x.length - 1]; + double[] dy = new double[x.length - 1]; + double[] slope = new double[x.length - 1]; + double[] intercept = new double[x.length - 1]; + + // Calculate the line equation (i.e. slope and intercept) between each point + for (int i = 0; i < x.length - 1; i++) { + dx[i] = x[i + 1] - x[i]; + if (dx[i] == 0) { + throw new IllegalArgumentException("X must be montotonic. A duplicate " + "x-value was found"); + } + if (dx[i] < 0) { + throw new IllegalArgumentException("X must be sorted"); + } + dy[i] = y[i + 1] - y[i]; + slope[i] = dy[i] / dx[i]; + intercept[i] = y[i] - x[i] * slope[i]; + } + + // Perform the interpolation here + double[] yi = new double[xi.length]; + for (int i = 0; i < xi.length; i++) { + if ((xi[i] > x[x.length - 1]) || (xi[i] < x[0])) { + yi[i] = Double.NaN; + } else { + int loc = Arrays.binarySearch(x, xi[i]); + if (loc < -1) { + loc = -loc - 2; + yi[i] = slope[loc] * xi[i] + intercept[loc]; + } else { + yi[i] = y[loc]; + } + } + } + + return yi; + } + + public static double interpLinear(double[] xy, double xx) { + if (xy.length % 2 != 0) { + throw new IllegalArgumentException("XY must be divisible by two."); + } + double[] x = new double[xy.length/2]; + double[] y = new double[x.length]; + for (int i = 0; i < xy.length; i++) { + if (i % 2 == 0) { + x[i/2] = xy[i]; + } else { + y[i/2] = xy[i]; + } + } + return interpLinear(x, y, new double[] {xx})[0]; + } +} diff --git a/src/main/java/com/iantapply/wynncraft/nbs/utils/VersionUtils.java b/src/main/java/com/iantapply/wynncraft/nbs/utils/VersionUtils.java new file mode 100644 index 0000000..92b3798 --- /dev/null +++ b/src/main/java/com/iantapply/wynncraft/nbs/utils/VersionUtils.java @@ -0,0 +1,76 @@ +package com.iantapply.wynncraft.nbs.utils; + +import com.iantapply.wynncraft.nbs.CustomInstrument; + +import java.util.ArrayList; + +import org.bukkit.Bukkit; + +public class VersionUtils { + private static float serverVersion = -1; + + public static ArrayList getVersionCustomInstruments(float serverVersion){ + ArrayList instruments = new ArrayList<>(); + if (serverVersion == 0.0112f){ + instruments.add(new CustomInstrument((byte) 0, "Guitar", "block.note_block.guitar.ogg")); + instruments.add(new CustomInstrument((byte) 0, "Flute", "block.note_block.flute.ogg")); + instruments.add(new CustomInstrument((byte) 0, "Bell", "block.note_block.bell.ogg")); + instruments.add(new CustomInstrument((byte) 0, "Chime", "block.note_block.icechime.ogg")); + instruments.add(new CustomInstrument((byte) 0, "Xylophone", "block.note_block.xylobone.ogg")); + return instruments; + } + + if (serverVersion == 0.0114f){ + instruments.add(new CustomInstrument((byte) 0, "Iron Xylophone", "block.note_block.iron_xylophone.ogg")); + instruments.add(new CustomInstrument((byte) 0, "Cow Bell", "block.note_block.cow_bell.ogg")); + instruments.add(new CustomInstrument((byte) 0, "Didgeridoo", "block.note_block.didgeridoo.ogg")); + instruments.add(new CustomInstrument((byte) 0, "Bit", "block.note_block.bit.ogg")); + instruments.add(new CustomInstrument((byte) 0, "Banjo", "block.note_block.banjo.ogg")); + instruments.add(new CustomInstrument((byte) 0, "Pling", "block.note_block.pling.ogg")); + return instruments; + } + return instruments; + } + + public static ArrayList getVersionCustomInstrumentsForSong(int firstCustomInstrumentIndex){ + ArrayList instruments = new ArrayList<>(); + + if (getServerVersion() < 0.0112f){ + if (firstCustomInstrumentIndex == 10) { + instruments.addAll(getVersionCustomInstruments(0.0112f)); + } else if (firstCustomInstrumentIndex == 16){ + instruments.addAll(getVersionCustomInstruments(0.0112f)); + instruments.addAll(getVersionCustomInstruments(0.0114f)); + } + } else if (getServerVersion() < 0.0114f){ + if (firstCustomInstrumentIndex == 16){ + instruments.addAll(getVersionCustomInstruments(0.0114f)); + } + } + + return instruments; + } + + public static float getServerVersion(){ + if (serverVersion != -1){ + return serverVersion; + } + + String versionInfo = Bukkit.getServer().getVersion(); + int start = versionInfo.lastIndexOf('('); + int end = versionInfo.lastIndexOf(')'); + + String[] versionParts = versionInfo.substring(start + 5, end).split("\\."); + + StringBuilder versionString = new StringBuilder("0."); + for (String part : versionParts){ + if (part.length() == 1){ + versionString.append("0"); + } + + versionString.append(part); + } + serverVersion = Float.parseFloat(versionString.toString()); + return serverVersion; + } +} diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml index cc44047..ad12aef 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -132,4 +132,8 @@ commands: housing: description: "Handles all housing related actions." usage: "/housing " - aliases: [is] \ No newline at end of file + aliases: [is] + # NBS Commands + nbs: + description: "Handles the testing and playing of NBS files." + usage: "/nbs " \ No newline at end of file