package me.chayapak1.chomens_bot.plugins; import me.chayapak1.chomens_bot.Bot; import me.chayapak1.chomens_bot.data.bossbar.BotBossBar; import me.chayapak1.chomens_bot.data.player.PlayerEntry; import me.chayapak1.chomens_bot.song.Loop; import me.chayapak1.chomens_bot.song.Note; import me.chayapak1.chomens_bot.song.Song; import me.chayapak1.chomens_bot.song.SongLoaderThread; import me.chayapak1.chomens_bot.util.ColorUtilities; import me.chayapak1.chomens_bot.util.LoggerUtilities; import me.chayapak1.chomens_bot.util.MathUtilities; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.util.HSVLike; import org.cloudburstmc.math.vector.Vector3d; import org.geysermc.mcprotocollib.network.event.session.DisconnectedEvent; import org.geysermc.mcprotocollib.protocol.data.game.BossBarColor; import org.geysermc.mcprotocollib.protocol.data.game.BossBarDivision; import java.io.IOException; import java.net.URL; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; // Author: _ChipMC_ & chayapak <3 public class MusicPlayerPlugin extends Bot.Listener implements CorePlugin.Listener { public static final String SELECTOR = "@a[tag=!nomusic,tag=!chomens_bot_nomusic,tag=!custompitch]"; public static final String CUSTOM_PITCH_SELECTOR = "@a[tag=!nomusic,tag=!chomens_bot_nomusic,tag=custompitch]"; public static final String BOTH_SELECTOR = "@a[tag=!nomusic,tag=!chomens_bot_nomusic]"; private final Bot bot; public static final Path SONG_DIR = Path.of("songs"); static { try { if (!Files.exists(SONG_DIR)) Files.createDirectory(SONG_DIR); } catch (IOException e) { LoggerUtilities.error(e); } } public Song currentSong; public final List songQueue = new ArrayList<>(); public SongLoaderThread loaderThread = null; public Loop loop = Loop.OFF; // sus nightcore stuff,..,.,. public float pitch = 0; public float speed = 1; public int amplify = 1; public boolean rainbow = false; // nbs easter egg private float rainbowHue = 0F; public String instrument = "off"; private int urlLimit = 0; public boolean locked = false; // this can be set through servereval private final String bossBarName = "music"; public BossBarColor bossBarColor; public String currentLyrics = ""; public MusicPlayerPlugin (Bot bot) { this.bot = bot; bot.addListener(this); bot.core.addListener(this); bot.executor.scheduleAtFixedRate(this::onTick, 0, 50, TimeUnit.MILLISECONDS); bot.executor.scheduleAtFixedRate(() -> urlLimit = 0, 0, bot.config.music.urlRatelimit.seconds, TimeUnit.SECONDS); } public void loadSong (Path location, PlayerEntry sender) { startLoadingSong( location.getFileName().toString(), new SongLoaderThread(location, bot, sender.profile.getName()) ); } public void loadSong (byte[] data, PlayerEntry sender) { startLoadingSong( sender.profile.getName() + "'s song item", new SongLoaderThread(data, bot, sender.profile.getName()) ); } public void loadSong (URL location, PlayerEntry sender) { if (urlLimit >= bot.config.music.urlRatelimit.limit) { bot.chat.tellraw(Component.text("URL loading is being rate limited!").color(NamedTextColor.RED)); return; } urlLimit++; startLoadingSong( location.toString(), new SongLoaderThread(location, bot, sender.profile.getName()) ); } private void startLoadingSong (String songName, SongLoaderThread loaderThread) { if (songQueue.size() > 500) return; this.loaderThread = loaderThread; bot.chat.tellraw( Component .translatable( "Loading %s", Component.text(songName, ColorUtilities.getColorByString(bot.config.colorPalette.secondary)) ) .color(ColorUtilities.getColorByString(bot.config.colorPalette.defaultColor)), BOTH_SELECTOR ); this.loaderThread.start(); } @Override public void coreReady () { if (currentSong != null) currentSong.play(); } // this needs a separate ticker because we need // the song to be playing without lag private void onTick () { try { if (!bot.loggedIn) return; if (currentSong == null) { if (songQueue.isEmpty()) return; // this line addBossBar(); currentSong = songQueue.getFirst(); // songQueue.poll(); bot.chat.tellraw( Component.translatable( "Now playing %s", Component.empty().append(Component.text(currentSong.name)).color(ColorUtilities.getColorByString(bot.config.colorPalette.secondary)) ).color(ColorUtilities.getColorByString(bot.config.colorPalette.defaultColor)), BOTH_SELECTOR ); currentSong.play(); } if (currentSong.paused) return; if (!currentSong.finished()) { handleLyrics(); BotBossBar bossBar = bot.bossbar.get(bossBarName); if (bossBar == null && bot.bossbar.enabled) bossBar = addBossBar(); if (bot.bossbar.enabled && bot.options.useCore) { bossBar.setTitle(generateBossBar()); bossBar.setColor(bossBarColor); bossBar.setValue((int) Math.floor(((double) (currentSong.time * speed) / 1000))); bossBar.setMax((long) (currentSong.length * speed) / 1000); } if (currentSong.paused || bot.core.isRateLimited()) return; handlePlaying(); } else { currentLyrics = ""; if (loop == Loop.CURRENT) { currentSong.loop(); return; } bot.chat.tellraw( Component.translatable( "Finished playing %s", Component.empty().append(Component.text(currentSong.name)).color(ColorUtilities.getColorByString(bot.config.colorPalette.secondary)) ).color(ColorUtilities.getColorByString(bot.config.colorPalette.defaultColor)), BOTH_SELECTOR ); if (loop == Loop.ALL) { skip(); return; } songQueue.removeFirst(); if (songQueue.isEmpty()) { stopPlaying(); return; } if (currentSong.size() > 0) { currentSong = songQueue.getFirst(); currentSong.setTime(0); currentSong.play(); } } } catch (Exception e) { bot.logger.error(e); } } public void skip () { if (loop == Loop.ALL) { songQueue.add(songQueue.removeFirst()); // bot.music.queue.push(bot.music.queue.shift()) in js } else { songQueue.removeFirst(); } if (songQueue.isEmpty()) { stopPlaying(); return; } currentSong = songQueue.getFirst(); currentSong.setTime(0); currentSong.play(); } public BotBossBar addBossBar () { if (currentSong == null) return null; rainbow = false; final BotBossBar bossBar = new BotBossBar( Component.empty(), BOTH_SELECTOR, BossBarColor.LIME, BossBarDivision.NONE, true, (int) currentSong.length / 1000, 0, bot ); bot.bossbar.add(bossBarName, bossBar); return bossBar; } private void handleLyrics () { // please help, this is many attempts trying to get this working // midi lyrics are very weird // i need some karaoke players to see how this works // final Map lyrics = currentSong.lyrics; // // if (lyrics.isEmpty()) return; // // final List lyricsList = new ArrayList<>(); // // for (Map.Entry entry : lyrics.entrySet()) { // final long time = entry.getKey(); // String _lyric = entry.getValue(); // // if (time > currentSong.time) continue; // //// StringBuilder lyric = new StringBuilder(); //// //// for (char character : _lyric.toCharArray()) { //// if ((character != '\n' && character != '\r' && character < ' ') || character == '�') continue; //// //// lyric.append(character); //// } //// //// String stringLyric = lyric.toString(); //// //// if (stringLyric.startsWith("\\") || stringLyric.startsWith("/")) { //// lyricsList.clear(); //// //// stringLyric = stringLyric.substring(1); //// } // // lyricsList.add(_lyric); // } // // final String joined = String.join("", lyricsList); // currentLyrics = joined.substring(Math.max(0, joined.length() - 25)); } public void removeBossBar() { final BotBossBar bossBar = bot.bossbar.get(bossBarName); if (bossBar != null) bossBar.setTitle(Component.text("No song is currently playing")); bot.bossbar.remove(bossBarName); } public Component generateBossBar () { TextColor nameColor; if (rainbow) { final int increment = 360 / 20; nameColor = TextColor.color(HSVLike.hsvLike(rainbowHue / 360.0f, 1, 1)); rainbowHue = (rainbowHue + increment) % 360; bossBarColor = BossBarColor.YELLOW; } else if (pitch > 0) { nameColor = NamedTextColor.LIGHT_PURPLE; bossBarColor = BossBarColor.PURPLE; } else if (pitch < 0) { nameColor = NamedTextColor.AQUA; bossBarColor = BossBarColor.CYAN; } else { nameColor = NamedTextColor.GREEN; bossBarColor = BossBarColor.YELLOW; } Component component = Component.empty() .append(Component.empty().append(Component.text(currentSong.name)).color(nameColor)) .append(Component.text(" | ").color(NamedTextColor.DARK_GRAY)) .append( Component .translatable("%s / %s", formatTime((long) (currentSong.time * speed)).color(NamedTextColor.GRAY), formatTime((long) (currentSong.length * speed)).color(NamedTextColor.GRAY)).color(NamedTextColor.DARK_GRAY) ); if (!bot.core.hasRateLimit() && !currentLyrics.isEmpty()) { component = component .append(Component.text(" | ", NamedTextColor.DARK_GRAY)) .append(Component.text(currentLyrics).color(NamedTextColor.BLUE)); } if (currentSong.paused) { return component .append(Component.text(" | ", NamedTextColor.DARK_GRAY)) .append(Component.text("Paused", NamedTextColor.LIGHT_PURPLE)); } if (loop != Loop.OFF) { return component .append(Component.translatable(" | ", NamedTextColor.DARK_GRAY)) .append(Component.translatable("Looping " + ((loop == Loop.CURRENT) ? "current" : "all"), NamedTextColor.LIGHT_PURPLE)); } return component; } public Component formatTime (long millis) { final int seconds = (int) millis / 1000; final String minutePart = String.valueOf(seconds / 60); final String unpaddedSecondPart = String.valueOf(seconds % 60); return Component.translatable( "%s:%s", Component.text(minutePart), Component.text(unpaddedSecondPart.length() < 2 ? "0" + unpaddedSecondPart : unpaddedSecondPart) ); } public void stopPlaying () { removeBossBar(); currentSong = null; } @Override public void disconnected (DisconnectedEvent event) { if (currentSong != null) currentSong.pause(); // nice. } public void handlePlaying () { if (currentSong == null) return; try { currentSong.advanceTime(); while (currentSong.reachedNextNote()) { final Note note = currentSong.getNextNote(); if (note.isRainbowToggle) { rainbow = !rainbow; continue; } if (note.volume == 0) continue; double key = note.shiftedPitch; final Vector3d blockPosition = getBlockPosition(note); final double notShiftedFloatingPitch = 0.5 * Math.pow(2, (note.pitch + (pitch / 10)) / 12); key += 33; final boolean isMoreOrLessOctave = key < 33 || key > 57; final boolean shouldCustomPitch = currentSong.nbs ? isMoreOrLessOctave : note.pitch != note.shiftedPitch || note.shiftedInstrument != note.instrument; final double volume = note.volume; if (shouldCustomPitch) { bot.core.run( "minecraft:execute as " + CUSTOM_PITCH_SELECTOR + " at @s run playsound " + (!instrument.equals("off") ? instrument : note.instrument.sound) + ".pitch." + notShiftedFloatingPitch + " record @s ^" + blockPosition.getX() + " ^" + blockPosition.getY() + " ^" + blockPosition.getZ() + " " + volume + " " + 0 ); } // these 2 lines are totallynotskidded from https://github.com/OpenNBS/OpenNoteBlockStudio/blob/master/scripts/selection_transpose/selection_transpose.gml // so huge thanks to them uwu while (key < 33) key += 12; // 1 octave has 12 notes, so we just keep moving octaves here while (key > 57) key -= 12; key -= 33; double floatingPitch = 0.5 * Math.pow(2, (key + (pitch / 10)) / 12); for (int i = 0; i < amplify; i++) { bot.core.run( "minecraft:execute as " + (shouldCustomPitch ? SELECTOR : BOTH_SELECTOR) + " at @s run playsound " + (!instrument.equals("off") ? instrument : note.shiftedInstrument.sound) + " record @s ^" + blockPosition.getX() + " ^" + blockPosition.getY() + " ^" + blockPosition.getZ() + " " + volume + " " + MathUtilities.clamp(floatingPitch, 0, 2) ); } } } catch (Exception e) { bot.logger.error(e); } } private Vector3d getBlockPosition (Note note) { Vector3d blockPosition; if (currentSong.nbs) { double value; if (note.stereo == 100 && note.panning != 100) value = note.panning; else if (note.panning == 100 && note.stereo != 100) value = note.stereo; else value = (double) (note.stereo + note.panning) / 2; double xPos; if (value > 100) xPos = (value - 100) / -100; else if (value == 100) xPos = 0; else xPos = ((value - 100) * -1) / 100; blockPosition = Vector3d.from(xPos, 0, 0); } else { final double originalPitch = note.originalPitch; double xPos = -(double) originalPitch / 768; if (originalPitch > 25) xPos = Math.abs(xPos); double yPos = -(double) originalPitch / 35; if (originalPitch < 75) yPos = -yPos; double zPos = -(double) originalPitch / 40; if (originalPitch < 75) zPos = -zPos; blockPosition = Vector3d.from(xPos, yPos, zPos); } return blockPosition; } }