diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..126da71 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,68 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# ******** NOTE ******** + +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '33 0 * * 0' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 0000000..58e1c59 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,26 @@ +# This workflow will build a Java project with Gradle +# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle + +name: Java CI with Gradle + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build diff --git a/.gitignore b/.gitignore index 406cc73..b95f7f4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ /.gradle/ /.idea/ /build/ -/gradle/ /out/ *.iml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d82f196 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: java +after_success: + - wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh + - chmod +x send.sh + - ./send.sh success $WEBHOOK_URL +after_failure: + - wget https://raw.githubusercontent.com/DiscordHooks/travis-ci-discord-webhook/master/send.sh + - chmod +x send.sh + - ./send.sh failure $WEBHOOK_URL \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a904633 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-2020 windy, sarhatabaot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0fdbb3 --- /dev/null +++ b/README.md @@ -0,0 +1,71 @@ +# Composer [![Codacy Badge](https://api.codacy.com/project/badge/Grade/e14287e74fb44aeeb1c294ff7959076e)](https://www.codacy.com/app/sarhatabaot/Composer?utm_source=github.com&utm_medium=referral&utm_content=sarhatabaot/Composer&utm_campaign=Badge_Grade) [![Build Status](https://travis-ci.org/sarhatabaot/Composer.svg?branch=api-7)](https://travis-ci.org/sarhatabaot/Composer) + +#### This version has been updated to work with Sponge API v7. +Composer is a full featured note block music importer and player. Import Note Block Studio 151 files by placing them in the config/composer/tracks/ directory and the plugin will automatically load them on start. + +Composer comes with a full music player built in as shown below. There are commands for each action you can perform within this menu as well as the text itself being clickable. + +![Composer Example](./images/composer-overview.png) + +## Features +* Per player music player for loaded tracks. +* Play, pause, shuffle, skip, back actions. +* Flexible API to allow developers to create compositions programmatically or import other file formats. + +## Installation +1. Place the composer-x.x-SNAPSHOT.jar file in your mods/ folder. +2. Place your .nbs files in the config/composer/tracks/ directory. +3. Run the server. + + +## Playlists +As of v4.1.0 Composer support playlists. To use playlists enable the option in default.conf +``` +use-playlists=true +default-playlist="default" +``` +### Creating a playlist +1. Under config/composer/playlists create a folder with the desired name. +2. Place your .nbs files in the folder you created. +3. Run the server. + +### Set a playlist +Run the `/music playlist ` command. + +### Setting a default playlist +Change the config option to the playlist name. +``` +default-playlist="default" +``` +## Commands +Note: The -p lets you target a player other than yourself. +* `/composer` or `/music`: Base command for plugin. These can be used interchangeably. +* `/music list` + Aliases: `list-tracks`, `tracks`, `track-list` +* `/music play [-p ] ` + Aliases: `start`, `>` +* `/music play-once [-p ] ` +* `/music pause [-p ]` + Aliases: `||` +* `/music stop` +* `/music resume [-p ]` +* `/music shuffle [-p ]` +* `/music queue [-p ]`: Shows this player’s play queue + Aliases: `order` +* `/music next [-p ]` + Aliases: `skip`, `>|` +* `/music previous [-p ]` + Aliases: `back`, `|<` +* `/music playlist [-p ]`: Sets an active playlist. +* `/music list-playlist` + Aliases: `playlists` +* `/music loop-track`: Loops the track. +* `/music loop-playlist`: Loops the playlist. + + + +## Permissions +* `composer.musicplayer`: Allows access to music player commands. +* `composer.musicplayer.others`: Allows use of -p flag. + + diff --git a/build.gradle b/build.gradle index 0f2db67..3d61f4b 100644 --- a/build.gradle +++ b/build.gradle @@ -1,18 +1,32 @@ +import org.apache.tools.ant.filters.ReplaceTokens + plugins { - id 'org.spongepowered.plugin' version '0.7' + id 'java' + id 'org.spongepowered.plugin' version '0.9.0' + id "io.freefair.lombok" version "4.1.3" } +compileJava.options.encoding = 'UTF-8' + group = 'se.walkercrou' -version = '2.0.0' +version = '4.1.4' description = 'Note block music importer and player.' -sponge.plugin { - id = 'composer' - meta { - name = "Composer" +sourceCompatibility = targetCompatibility = '1.8' + + +processResources { + from(sourceSets.main.resources.srcDirs) { + filter ReplaceTokens, tokens: [version: version] } } +repositories { + mavenCentral() +} + dependencies { - compile 'org.spongepowered:spongeapi:5.0.0' + compileOnly 'org.spongepowered:spongeapi:7.4.0' + annotationProcessor 'org.spongepowered:spongeapi:7.4.0' + compile 'org.jetbrains:annotations:23.1.0' } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..5c2d1cf Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..070cb70 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 91a7e26..8e25e6c 100755 --- a/gradlew +++ b/gradlew @@ -1,4 +1,20 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ############################################################################## ## @@ -6,20 +22,38 @@ ## ############################################################################## -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -30,6 +64,7 @@ die ( ) { cygwin=false msys=false darwin=false +nonstop=false case "`uname`" in CYGWIN* ) cygwin=true @@ -40,31 +75,11 @@ case "`uname`" in MINGW* ) msys=true ;; + NONSTOP* ) + nonstop=true + ;; esac -# For Cygwin, ensure paths are in UNIX format before anything is touched. -if $cygwin ; then - [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` -fi - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >&- -APP_HOME="`pwd -P`" -cd "$SAVED" >&- - CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar # Determine the Java command to use to start the JVM. @@ -90,7 +105,7 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then MAX_FD_LIMIT=`ulimit -H -n` if [ $? -eq 0 ] ; then if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then @@ -114,6 +129,7 @@ fi if $cygwin ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` @@ -154,11 +170,19 @@ if $cygwin ; then esac fi -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " } -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index aec9973..24467a1 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -8,14 +24,14 @@ @rem Set local scope for the variables with windows NT shell if "%OS%"=="Windows_NT" setlocal -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - set DIRNAME=%~dp0 if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome @@ -46,10 +62,9 @@ echo location of your Java installation. goto fail :init -@rem Get command-line arguments, handling Windowz variants +@rem Get command-line arguments, handling Windows variants if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args :win9xME_args @rem Slurp the command line arguments. @@ -60,11 +75,6 @@ set _SKIP=2 if "x%~1" == "x" goto execute set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ :execute @rem Setup the command line diff --git a/images/composer-overview.png b/images/composer-overview.png new file mode 100644 index 0000000..f275efa Binary files /dev/null and b/images/composer-overview.png differ diff --git a/lombok.config b/lombok.config new file mode 100644 index 0000000..6aa51d7 --- /dev/null +++ b/lombok.config @@ -0,0 +1,2 @@ +# This file is generated by the 'io.freefair.lombok' Gradle plugin +config.stopBubbling = true diff --git a/src/main/java/se/walkercrou/composer/Composer.java b/src/main/java/se/walkercrou/composer/Composer.java index 6efd72d..f82db8e 100644 --- a/src/main/java/se/walkercrou/composer/Composer.java +++ b/src/main/java/se/walkercrou/composer/Composer.java @@ -1,17 +1,23 @@ package se.walkercrou.composer; import com.google.inject.Inject; +import lombok.Getter; +import lombok.Setter; import ninja.leaping.configurate.ConfigurationNode; import ninja.leaping.configurate.commented.CommentedConfigurationNode; import ninja.leaping.configurate.loader.ConfigurationLoader; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.spongepowered.api.config.DefaultConfig; import org.spongepowered.api.entity.living.player.Player; import org.spongepowered.api.event.Listener; import org.spongepowered.api.event.game.state.GameStartedServerEvent; import org.spongepowered.api.plugin.Plugin; +import org.spongepowered.api.scheduler.Task; import se.walkercrou.composer.cmd.ComposerCommands; import se.walkercrou.composer.cmd.TestCommands; +import se.walkercrou.composer.exception.CorruptedFileException; +import se.walkercrou.composer.exception.OldNbsVersionException; import se.walkercrou.composer.nbs.MusicPlayer; import se.walkercrou.composer.nbs.NoteBlockStudioSong; @@ -26,100 +32,184 @@ /** * Main class for Composer plugin. */ -@Plugin(id = "composer", authors = { "windy" }) +@Plugin(id = "composer", + name = "Composer", + url = "https://github.com/sarhatabaot/Composer", + authors = {"windy", "sarhatabaot"}) public class Composer { + @Getter + @Setter + private static Composer instance; - @Inject public Logger log; - @Inject @DefaultConfig(sharedRoot = false) private Path configPath; - @Inject @DefaultConfig(sharedRoot = false) private ConfigurationLoader configLoader; - - private ConfigurationNode config; - private final List nbsTracks = new ArrayList<>(); - private final Map musicPlayers = new HashMap<>(); - - @Listener - public void onGameStarted(GameStartedServerEvent event) { - setupConfig(); - if (config.getNode("debugMode").getBoolean()) - new TestCommands(this).register(); - new ComposerCommands(this).register(); - loadTracks(); - } - - /** - * Returns the specified player's {@link MusicPlayer}. If one does not exist a new one will be created. - * - * @param player player - * @return music player - */ - public MusicPlayer getMusicPlayer(Player player) { - UUID playerId = player.getUniqueId(); - MusicPlayer mp = musicPlayers.get(playerId); - if (mp == null) - musicPlayers.put(playerId, mp = new MusicPlayer(this, getNbsTracks())); - return mp; - } - - /** - * Returns the currently loaded {@link NoteBlockStudioSong}s. - * - * @return list of tracks - */ - public List getNbsTracks() { - return Collections.unmodifiableList(nbsTracks); - } - - private void loadTracks() { - File file = new File(configPath.toFile().getParentFile(), "tracks"); - if (!file.exists()) - file.mkdirs(); - - new Thread(() -> { - double progress = 0; - int total = file.list((d, n) -> n.endsWith(".nbs")).length; - progress(progress); - try (DirectoryStream stream = Files.newDirectoryStream(file.toPath(), "*.nbs")) { - for (Path path : stream) { - try { - nbsTracks.add(NoteBlockStudioSong.read(path.toFile())); - progress(++progress / total * 100); - } catch (IOException e) { - log.error("Could not read file (file is likely malformed): " + path, e); - } - } - } catch (IOException e) { - log.error("An error occurred while loading the tracks.", e); - } - - }).start(); - - } - - private void progress(double p) { - log.info("Loading tracks: " + (int) p + "%"); - } - - private void setupConfig() { - File file = configPath.toFile(); - if (!file.exists()) - createDefaultConfig(file); - try { - config = configLoader.load(); - } catch (IOException e) { - e.printStackTrace(); - log.error("An error occurred while loading the config file.", e); - } - } - - private void createDefaultConfig(File file) { - try { - file.getParentFile().mkdirs(); - file.createNewFile(); - Files.copy(Composer.class.getResourceAsStream("/assets/se/walkercrou/composer/default.conf"), configPath, - StandardCopyOption.REPLACE_EXISTING); - } catch (IOException e) { - e.printStackTrace(); - log.error("An error occurred while creating default config.", e); - } - } + @Inject + @Getter + private Logger logger; + @Inject + @DefaultConfig(sharedRoot = false) + private Path configPath; + @Inject + @DefaultConfig(sharedRoot = false) + private ConfigurationLoader configLoader; + + @Getter + private ConfigurationNode config; + + @Getter + private final Map playlists = new HashMap<>(); + + private final List nbsTracks = new ArrayList<>(); + private final Map musicPlayers = new HashMap<>(); + + @Listener + public void onGameStarted(GameStartedServerEvent event) { + setupConfig(); + setInstance(this); + if (config.getNode("debugMode").getBoolean()) + new TestCommands(this).register(); + new ComposerCommands(this).register(); + Task.Builder taskBuilder = Task.builder(); + taskBuilder.execute(new LoadTracksRunnable()).submit(this); + } + + /** + * Returns the specified player's {@link MusicPlayer}. + * If one does not exist a new one will be created. + * + * @param player player + * @return music player + */ + public MusicPlayer getMusicPlayer(Player player) { + UUID playerId = player.getUniqueId(); + MusicPlayer mp = musicPlayers.get(playerId); + if (mp == null) { + if(config.getNode("use-playlists").getBoolean()) + mp = new MusicPlayer(this, playlists.get(config.getNode("default-playlist").getString()).getTracks()); + else + mp = new MusicPlayer(this, nbsTracks); + musicPlayers.put(playerId, mp); + } + return mp; + } + + /** + * Returns the currently loaded {@link NoteBlockStudioSong}s. + * + * @return list of tracks + */ + public List getNbsTracks() { + return Collections.unmodifiableList(nbsTracks); + } + + public class LoadTracksRunnable implements Runnable { + + + @Override + public void run() { + File file = new File(configPath.toFile().getParentFile(), "tracks"); + if (!file.exists()) { + if(file.mkdirs()) + logger.info("Created tracks folder."); + else logger.warn("Could not create tracks folder."); + } + + if (config.getNode("use-playlists").getBoolean()) { + File playlistsFile = new File(configPath.toFile().getParent(), "playlists"); + if (!playlistsFile.exists()) { + if(playlistsFile.mkdirs()) + logger.info("Created playlists folder."); + else logger.warn("Could not create playlists folder"); + } + file = new File(configPath.toFile().getParentFile(), "playlists/" + config.getNode("default-playlist").getString()); + if (!file.exists()) { + if(file.mkdirs()) + logger.info("Created default playlist folder."); + else logger.warn("Could not create default playlist folder."); + } + for(File playlistDir: playlistsFile.listFiles()){ + if(playlistDir.isDirectory()) { + loadTracks(playlistDir); + logger.info("Loaded "+playlistDir.getName()); + } + + } + } else { + loadTracks(file); + } + + } + + + private void loadTracks(final File file) { + double progress = 0; + int total = Objects.requireNonNull(file.list((d, n) -> n.endsWith(".nbs"))).length; + progress(progress); + try (DirectoryStream stream = Files.newDirectoryStream(file.toPath(), "*.nbs")) { + List songs = new ArrayList<>(); + for (Path path : stream) { + NoteBlockStudioSong song = addTrack(path); + if (song != null) { + progress(++progress / total * 100); + songs.add(song); + } + } + addTracks(file.getName(),songs); + logger.info(String.format("Loaded %d/%d tracks.", songs.size(), total)); + } catch (IOException e) { + logger.error("An error occurred while loading the tracks.", e); + } + } + + + private void progress(double p) { + getLogger().info("Loading tracks: " + (int) p + "%"); + } + + public void addTracks(final String name, final List songs){ + if(config.getNode("use-playlists").getBoolean()){ + playlists.put(name,new Playlist(songs)); + } else { + nbsTracks.addAll(songs); + } + } + + + @Nullable + private NoteBlockStudioSong addTrack(final Path path) { + try { + return NoteBlockStudioSong.read(path.toFile()); + } catch (IOException | CorruptedFileException e) { + logger.error("Could not read file (file is likely malformed): "+ path); + logger.debug(e.getMessage()); + } catch (OldNbsVersionException e) { + logger.error(e.getMessage()); + logger.error("Please update your NBS file to a new format, using NBS studio or something similar."); + } + return null; + } + } + + + private void setupConfig() { + File file = configPath.toFile(); + if (!file.exists()) + createDefaultConfig(file); + try { + config = configLoader.load(); + } catch (IOException e) { + getLogger().info(e.getMessage()); + getLogger().error("An error occurred while loading the config file.", e); + } + } + + private void createDefaultConfig(File file) { + try { + file.getParentFile().mkdirs(); + file.createNewFile(); + Files.copy(Composer.class.getResourceAsStream("/assets/se/walkercrou/composer/default.conf"), configPath, + StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + getLogger().error(e.getMessage()); + logger.error("An error occurred while creating default config.", e); + } + } } diff --git a/src/main/java/se/walkercrou/composer/Playlist.java b/src/main/java/se/walkercrou/composer/Playlist.java new file mode 100644 index 0000000..92d79ee --- /dev/null +++ b/src/main/java/se/walkercrou/composer/Playlist.java @@ -0,0 +1,20 @@ +package se.walkercrou.composer; + + +import se.walkercrou.composer.nbs.NoteBlockStudioSong; + +import java.util.List; + +//pluginfolder/playlists/foldername/songs.. +public class Playlist { + private final List tracks; + + public Playlist(final List tracks) { + this.tracks = tracks; + } + + + public List getTracks() { + return tracks; + } +} diff --git a/src/main/java/se/walkercrou/composer/cmd/ComposerCommands.java b/src/main/java/se/walkercrou/composer/cmd/ComposerCommands.java index 1df9bcf..20910d4 100644 --- a/src/main/java/se/walkercrou/composer/cmd/ComposerCommands.java +++ b/src/main/java/se/walkercrou/composer/cmd/ComposerCommands.java @@ -1,5 +1,6 @@ package se.walkercrou.composer.cmd; +import org.jetbrains.annotations.NotNull; import org.spongepowered.api.Sponge; import org.spongepowered.api.command.CommandException; import org.spongepowered.api.command.CommandPermissionException; @@ -9,11 +10,15 @@ import org.spongepowered.api.command.spec.CommandSpec; import org.spongepowered.api.entity.living.player.Player; import org.spongepowered.api.text.Text; +import org.spongepowered.api.text.format.TextColors; +import org.spongepowered.api.util.annotation.NonnullByDefault; import se.walkercrou.composer.Composer; +import se.walkercrou.composer.Playlist; import se.walkercrou.composer.util.TextUtil; import se.walkercrou.composer.nbs.MusicPlayer; import static org.spongepowered.api.command.args.GenericArguments.*; +import static se.walkercrou.composer.util.TextUtil.getPlaylistName; /** * Main commands for plugin. @@ -63,18 +68,57 @@ public class ComposerCommands { .description(Text.of("Goes back to the previous song in the queue.")) .executor(this::previousTrack) .build(); + private final CommandSpec selectPlaylist = CommandSpec.builder() + .arguments(new PlaylistCommandElement(Text.of("playlist")) + ,optional(player(Text.of("player")))) + .description(Text.of("Selects a playlist")) + .executor(this::selectPlaylist) + .build(); + private final CommandSpec listPlaylists = CommandSpec.builder() + .description(Text.of("Lists all available playlists.")) + .executor(this::listPlaylists) + .build(); + private final CommandSpec loopTrack = CommandSpec.builder() + .arguments(optional(player(Text.of("player")))) + .description(Text.of("Loops a track")) + .executor(this::loopTrack) + .build(); + private final CommandSpec loopPlaylist = CommandSpec.builder() + .arguments(optional(player(Text.of("player")))) + .description(Text.of("Loops a playlist")) + .executor(this::loopPlaylist) + .build(); + private final CommandSpec stop = CommandSpec.builder() + .arguments(optional(player(Text.of("player")))) + .description(Text.of("Stops a track")) + .executor(this::stopTrack) + .build(); + private final CommandSpec playOnce = CommandSpec.builder() + .arguments(flags() + .valueFlag(player(Text.of("player")), "p") + .buildWith(integer(Text.of("trackNumber"))) + ) + .description(Text.of("Plays a track once.")) + .executor(this::playOnce) + .build(); private final CommandSpec base = CommandSpec.builder() .permission("composer.musicplayer") .description(Text.of("Main parent command for plugin.")) .executor(this::listTracks) .child(list, "list", "list-tracks", "tracks", "track-list") .child(play, "play", "start", ">") - .child(pause, "pause", "stop", "||") + .child(pause, "pause", "||") + .child(stop,"stop") .child(resume, "resume") .child(shuffle, "shuffle") .child(queue, "queue", "order") .child(next, "next", "skip", ">|") .child(previous, "previous", "back", "|<") + .child(loopTrack,"loop","loop-track") + .child(loopPlaylist,"loop-all","loop-playlist") + .child(selectPlaylist,"playlist") + .child(listPlaylists,"list-playlist","playlists") + .child(playOnce,"play-once") .build(); public ComposerCommands(Composer plugin) { @@ -85,31 +129,38 @@ public void register() { Sponge.getCommandManager().register(plugin, base, "music", "composer"); } - public CommandResult previousTrack(CommandSource src, CommandContext context) throws CommandException { + @NonnullByDefault + private CommandResult previousTrack(CommandSource src, CommandContext context) throws CommandException { Player player = getPlayer(src, context); plugin.getMusicPlayer(player).previous(player); return CommandResult.success(); } - public CommandResult nextTrack(CommandSource src, CommandContext context) throws CommandException { + @NonnullByDefault + private CommandResult nextTrack(CommandSource src, CommandContext context) throws CommandException { Player player = getPlayer(src, context); plugin.getMusicPlayer(player).next(player); return CommandResult.success(); } - public CommandResult printPlayQueue(CommandSource src, CommandContext context) throws CommandException { + @NonnullByDefault + private CommandResult printPlayQueue(CommandSource src, CommandContext context) throws CommandException { Player player = getPlayer(src, context); - TextUtil.trackList(plugin.getMusicPlayer(player).getTracks()).sendTo(src); + final String title = getPlaylistName(plugin.getMusicPlayer(player).getPlaylist()); + TextUtil.trackList(plugin.getMusicPlayer(player).getTracks(),title).sendTo(src); return CommandResult.success(); } - public CommandResult shuffleTracks(CommandSource src, CommandContext context) throws CommandException { + @NonnullByDefault + private CommandResult shuffleTracks(CommandSource src, CommandContext context) throws CommandException { Player player = getPlayer(src, context); plugin.getMusicPlayer(player).shuffle(player); return CommandResult.success(); } - public CommandResult resumeTrack(CommandSource src, CommandContext context) throws CommandException { + @NonnullByDefault + //TODO: Doesn't resume + private CommandResult resumeTrack(CommandSource src, CommandContext context) throws CommandException { Player player = getPlayer(src, context); MusicPlayer mp = plugin.getMusicPlayer(player); if (mp.isPlaying()) @@ -118,27 +169,116 @@ public CommandResult resumeTrack(CommandSource src, CommandContext context) thro return CommandResult.success(); } - public CommandResult pauseTrack(CommandSource src, CommandContext context) throws CommandException { + @NonnullByDefault + private CommandResult pauseTrack(CommandSource src, CommandContext context) throws CommandException { Player player = getPlayer(src, context); MusicPlayer mp = plugin.getMusicPlayer(player); if (!mp.isPlaying()) throw new CommandException(Text.of("No music playing.")); mp.pause(); + player.sendMessage(Text.builder("Paused: ") + .color(TextColors.GOLD) + .append(TextUtil.track(mp.getCurrentTrack()).build()) + .build()); return CommandResult.success(); } - public CommandResult playTrack(CommandSource src, CommandContext context) throws CommandException { - int trackIndex = context.getOne("trackNumber").get() - 1; + @NonnullByDefault + private CommandResult playTrack(CommandSource src, CommandContext context) throws CommandException { + int trackIndex = context.getOne("trackNumber").orElse(1) - 1; Player player = getPlayer(src, context); plugin.getMusicPlayer(player).play(player, trackIndex); return CommandResult.success(); } - public CommandResult listTracks(CommandSource source, CommandContext context) throws CommandException { - TextUtil.trackList(plugin.getNbsTracks()).sendTo(source); + @NonnullByDefault + private @NotNull CommandResult selectPlaylist(CommandSource src, CommandContext context) throws CommandException { + Player player = getPlayer(src,context); + Playlist playlist = context.getOne("playlist").orElse(null); + if(playlist == null) + throw new CommandException(Text.of("This playlist doesn't exist.")); + + plugin.getMusicPlayer(player).setPlaylist(playlist); + player.sendMessage(Text.builder("Selected: ") + .color(TextColors.GOLD) + .append(Text.of(getPlaylistName(playlist))) + .build()); return CommandResult.success(); } + + @NonnullByDefault + private CommandResult listTracks(CommandSource source, CommandContext context) throws CommandException { + final String title = getPlaylistName(plugin.getMusicPlayer((Player)source).getPlaylist()); + if(Composer.getInstance().getConfig().getNode("use-playlists").getBoolean()) + TextUtil.trackList(plugin.getMusicPlayer((Player)source).getTracks(), title).sendTo(source); + else + TextUtil.trackList(plugin.getNbsTracks(), title).sendTo(source); + return CommandResult.success(); + } + + @NonnullByDefault + private CommandResult loopTrack(final CommandSource source, final CommandContext context) throws CommandException { + Player player = getPlayer(source,context); + final MusicPlayer musicPlayer = plugin.getMusicPlayer(player); + musicPlayer.setLoopTrack(!musicPlayer.isLoopTrack()); + musicPlayer.getCurrentTrack().toScore().onFinish(() -> { + try { + previousTrack(source,context); + } catch (CommandException e) { + e.printStackTrace(); + } + }); + player.sendMessage(Text.builder("Turned track looping ") + .color(TextColors.GOLD) + .append(Text.of(onOrOff(musicPlayer.isLoopTrack()))).build()); + return CommandResult.success(); + } + + @NonnullByDefault + private CommandResult loopPlaylist(final CommandSource source, final CommandContext context) throws CommandException { + Player player = getPlayer(source,context); + final MusicPlayer musicPlayer = plugin.getMusicPlayer(player); + musicPlayer.setLoopPlaylist(!musicPlayer.isLoopPlaylist()); + player.sendMessage(Text.builder("Turned playlist looping ") + .color(TextColors.GOLD) + .append(onOrOff(musicPlayer.isLoopPlaylist()).build()).build()); + return CommandResult.success(); + } + + private Text.Builder onOrOff(boolean value) { + return value ? Text.builder("on").color(TextColors.GREEN) : Text.builder("off").color(TextColors.RED); + } + + @NonnullByDefault + private CommandResult stopTrack(final CommandSource source, final CommandContext context) throws CommandException { + Player player = getPlayer(source,context); + plugin.getMusicPlayer(player).stop(player); + return CommandResult.success(); + } + + @NonnullByDefault + private CommandResult listPlaylists(final CommandSource source,final CommandContext context) throws CommandException { + Player player = getPlayer(source,context); + if(!Composer.getInstance().getConfig().getNode("use-playlists").getBoolean()){ + player.sendMessage(Text.builder("Playlists are disabled.").color(TextColors.RED).build()); + return CommandResult.success(); + } + + TextUtil.playlistList().sendTo(source); + return CommandResult.success(); + } + + @NonnullByDefault + private CommandResult playOnce(final CommandSource source, final CommandContext context) throws CommandException { + Player player = getPlayer(source,context); + int trackIndex = context.getOne("trackNumber").orElse(1) - 1; + final MusicPlayer musicPlayer = plugin.getMusicPlayer(player); + musicPlayer.play(player,trackIndex, true); + return CommandResult.success(); + } + + private Player getPlayer(CommandSource src, CommandContext context) throws CommandException { Player player = context.getOne("player").orElse(null); if (player == null) { diff --git a/src/main/java/se/walkercrou/composer/cmd/PlaylistCommandElement.java b/src/main/java/se/walkercrou/composer/cmd/PlaylistCommandElement.java new file mode 100644 index 0000000..f6616da --- /dev/null +++ b/src/main/java/se/walkercrou/composer/cmd/PlaylistCommandElement.java @@ -0,0 +1,39 @@ +package se.walkercrou.composer.cmd; + + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.api.command.CommandSource; +import org.spongepowered.api.command.args.ArgumentParseException; +import org.spongepowered.api.command.args.CommandArgs; +import org.spongepowered.api.command.args.CommandContext; +import org.spongepowered.api.command.args.CommandElement; + +import org.spongepowered.api.text.Text; +import se.walkercrou.composer.Composer; + +import java.util.ArrayList; +import java.util.List; + +public class PlaylistCommandElement extends CommandElement { + private final Composer composer = Composer.getInstance(); //probably should use inject here instead + protected PlaylistCommandElement(@Nullable final Text key) { + super(key); + } + + @Nullable + @Override + protected Object parseValue(final @NotNull CommandSource source, final @NotNull CommandArgs args) throws ArgumentParseException { + if(composer.getPlaylists().get(args.peek()) != null){ + return composer.getPlaylists().get(args.next()); + } + return null; + } + + @Override + public @NotNull List complete(final @NotNull CommandSource src, final @NotNull CommandArgs args, final @NotNull CommandContext context) { + return new ArrayList<>(composer.getPlaylists().keySet()); + } + + +} diff --git a/src/main/java/se/walkercrou/composer/cmd/TestCommands.java b/src/main/java/se/walkercrou/composer/cmd/TestCommands.java index 1a04bd8..67e80de 100644 --- a/src/main/java/se/walkercrou/composer/cmd/TestCommands.java +++ b/src/main/java/se/walkercrou/composer/cmd/TestCommands.java @@ -1,16 +1,16 @@ package se.walkercrou.composer.cmd; import static org.spongepowered.api.effect.sound.SoundTypes.BLOCK_NOTE_BASS; -import static se.walkercrou.composer.Note.HALF; -import static se.walkercrou.composer.Note.QUARTER; -import static se.walkercrou.composer.Note.WHOLE; -import static se.walkercrou.composer.Pitch.A0; -import static se.walkercrou.composer.Pitch.A1; -import static se.walkercrou.composer.Pitch.B0; -import static se.walkercrou.composer.Pitch.B1; -import static se.walkercrou.composer.Pitch.D2; -import static se.walkercrou.composer.Pitch.G0; -import static se.walkercrou.composer.Pitch.G1; +import static se.walkercrou.composer.score.Note.HALF; +import static se.walkercrou.composer.score.Note.QUARTER; +import static se.walkercrou.composer.score.Note.WHOLE; +import static se.walkercrou.composer.util.PitchUtils.A0; +import static se.walkercrou.composer.util.PitchUtils.A1; +import static se.walkercrou.composer.util.PitchUtils.B0; +import static se.walkercrou.composer.util.PitchUtils.B1; +import static se.walkercrou.composer.util.PitchUtils.D2; +import static se.walkercrou.composer.util.PitchUtils.G0; +import static se.walkercrou.composer.util.PitchUtils.G1; import org.spongepowered.api.Sponge; import org.spongepowered.api.command.CommandException; @@ -24,11 +24,14 @@ import org.spongepowered.api.effect.sound.SoundTypes; import org.spongepowered.api.entity.living.player.Player; import org.spongepowered.api.text.Text; +import org.spongepowered.api.util.annotation.NonnullByDefault; import se.walkercrou.composer.Composer; -import se.walkercrou.composer.Measure; -import se.walkercrou.composer.Note; -import se.walkercrou.composer.Score; -import se.walkercrou.composer.TimeSignature; +import se.walkercrou.composer.exception.CorruptedFileException; +import se.walkercrou.composer.exception.OldNbsVersionException; +import se.walkercrou.composer.score.Measure; +import se.walkercrou.composer.score.Note; +import se.walkercrou.composer.score.Score; +import se.walkercrou.composer.score.TimeSignature; import se.walkercrou.composer.nbs.NoteBlockStudioSong; import java.io.File; @@ -77,14 +80,15 @@ public void register() { cm.register(plugin, nbs, "nbs"); } - public CommandResult readNbsFile(CommandSource src, CommandContext context) throws CommandException { + @NonnullByDefault + private CommandResult readNbsFile(CommandSource src, CommandContext context) throws CommandException { if (!(src instanceof Player)) throw new CommandException(Text.of("Only players may run this command.")); NoteBlockStudioSong nbs; try { nbs = NoteBlockStudioSong.read(new File(context.getOne("file").get())); - } catch (IOException e) { + } catch (IOException| CorruptedFileException | OldNbsVersionException e) { e.printStackTrace(); throw new CommandException(Text.of("Error reading NBS file"), e); } @@ -95,14 +99,16 @@ public CommandResult readNbsFile(CommandSource src, CommandContext context) thro return CommandResult.success(); } - public CommandResult playNote(CommandSource src, CommandContext context, SoundType type) { + @NonnullByDefault + private CommandResult playNote(CommandSource src, CommandContext context, SoundType type) { Player player = (Player) src; - double pitch = context.getOne("pitch").get(); + double pitch = context.getOne("pitch").orElse(2.0); player.getWorld().playSound(type, player.getLocation().getPosition(), 2, pitch); return CommandResult.success(); } - public CommandResult playSong(CommandSource src, CommandContext context) throws CommandException { + @NonnullByDefault + private CommandResult playSong(CommandSource src, CommandContext context) throws CommandException { if (!(src instanceof Player)) throw new CommandException(Text.of("Only players may run this command.")); diff --git a/src/main/java/se/walkercrou/composer/exception/CorruptedFileException.java b/src/main/java/se/walkercrou/composer/exception/CorruptedFileException.java new file mode 100644 index 0000000..e170714 --- /dev/null +++ b/src/main/java/se/walkercrou/composer/exception/CorruptedFileException.java @@ -0,0 +1,10 @@ +package se.walkercrou.composer.exception; + +import lombok.RequiredArgsConstructor; + +@RequiredArgsConstructor +public class CorruptedFileException extends Exception { + public CorruptedFileException(final String message) { + super(message); + } +} \ No newline at end of file diff --git a/src/main/java/se/walkercrou/composer/exception/OldNbsVersionException.java b/src/main/java/se/walkercrou/composer/exception/OldNbsVersionException.java new file mode 100644 index 0000000..75a0426 --- /dev/null +++ b/src/main/java/se/walkercrou/composer/exception/OldNbsVersionException.java @@ -0,0 +1,10 @@ +package se.walkercrou.composer.exception; + +/** + * @author sarhatabaot + */ +public class OldNbsVersionException extends Exception{ + public OldNbsVersionException(final String fileName) { + super(String.format("File %s is using the classic (version 0) of NBS.", fileName)); + } +} diff --git a/src/main/java/se/walkercrou/composer/nbs/MusicPlayer.java b/src/main/java/se/walkercrou/composer/nbs/MusicPlayer.java index 1a2ac1d..a90db71 100644 --- a/src/main/java/se/walkercrou/composer/nbs/MusicPlayer.java +++ b/src/main/java/se/walkercrou/composer/nbs/MusicPlayer.java @@ -4,10 +4,10 @@ import org.spongepowered.api.text.Text; import org.spongepowered.api.text.format.TextColors; import se.walkercrou.composer.Composer; -import se.walkercrou.composer.Score; +import se.walkercrou.composer.Playlist; +import se.walkercrou.composer.score.Score; import se.walkercrou.composer.util.TextUtil; -import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -15,142 +15,217 @@ * Plays music for a {@link Player}. */ public class MusicPlayer { - private final Composer plugin; - private final List tracks; - private int currentTrack = 0; - private Score currentSong; - private boolean playing = false; - - /** - * Creates a new MusicPlayer with the specified tracks. - * - * @param plugin context - * @param tracks tracks - */ - public MusicPlayer(Composer plugin, List tracks) { - this.plugin = plugin; - this.tracks = new ArrayList<>(tracks); - } - - /** - * Returns this player's current track. - * - * @return current track - */ - public NoteBlockStudioSong getCurrentTrack() { - return tracks.get(currentTrack); - } - - /** - * Returns the tracks in this player. - * - * @return tracks in player - */ - public List getTracks() { - return Collections.unmodifiableList(tracks); - } - - /** - * Returns true if currently playing - * - * @return true if playing - */ - public boolean isPlaying() { - return playing; - } - - /** - * Starts playing or resumes the specified track. - * - * @param player player - * @param trackIndex index of track - */ - public void play(Player player, int trackIndex) { - if (trackIndex != currentTrack) { - currentTrack = trackIndex; - if (currentSong != null) { - currentSong.pause(); - currentSong = null; - } - } - - if (currentSong == null) - currentSong = tracks.get(currentTrack).toScore().onFinish(() -> next(player)); - - player.sendMessage(Text.builder("Now playing: ") - .color(TextColors.GOLD) - .append(TextUtil.track(getCurrentTrack()).build()) - .build()); - - currentSong.play(plugin, player); - playing = true; - } - - /** - * Starts playing or resumes the current track. - * - * @param player player - */ - public void play(Player player) { - play(player, currentTrack); - } - - /** - * Pauses the player. - */ - public void pause() { - playing = false; - if (currentSong != null) - currentSong.pause(); - } - - /** - * Shuffles the player tracks and starts playing track zero. - * - * @param player player - */ - public void shuffle(Player player) { - Collections.shuffle(tracks); - if (currentSong != null) { - currentSong.pause(); - currentSong = null; - } - currentTrack = 0; - play(player); - } - - /** - * Skips the specified amount of tracks and starts playing. - * - * @param player player - * @param jumps tracks to skip - */ - public void skip(Player player, int jumps) { - int newIndex = currentTrack + jumps; - if (newIndex < 0 || newIndex >= tracks.size()) { - pause(); - currentSong = null; - currentTrack = 0; - return; - } - play(player, newIndex); - } - - /** - * Skips one track. - * - * @param player player - */ - public void next(Player player) { - skip(player, 1); - } - - /** - * Goes back one track. - * - * @param player player - */ - public void previous(Player player) { - skip(player, -1); - } + private final Composer plugin; + private Playlist tracks; + private int currentTrack = 0; + private Score currentSong; + private boolean playing = false; + + private boolean loopTrack; + private boolean loopPlaylist; + + /** + * Creates a new MusicPlayer with the specified tracks. + * + * @param plugin context + * @param tracks tracks + */ + public MusicPlayer(Composer plugin, List tracks) { + this.plugin = plugin; + this.tracks = new Playlist(tracks); + } + + /** + * Returns this player's current track. + * + * @return current track + */ + public NoteBlockStudioSong getCurrentTrack() { + return tracks.getTracks().get(currentTrack); + } + + public void setPlaylist(final Playlist playlist) { + this.tracks = playlist; + } + + public Playlist getPlaylist() { + return this.tracks; + } + + /** + * Returns the tracks in this player. + * + * @return tracks in player + */ + public List getTracks() { + return Collections.unmodifiableList(tracks.getTracks()); + } + + /** + * Returns true if currently playing + * + * @return true if playing + */ + public boolean isPlaying() { + return playing; + } + + + /** + * Starts playing or resumes the specified track. + * + * @param playOnce play once + * @param player player + * @param trackIndex index of track + */ + public void play(Player player, int trackIndex, boolean playOnce) { + if (trackIndex != currentTrack) { + currentTrack = trackIndex; + if (currentSong != null) { + currentSong.pause(); + currentSong = null; + } + } else if (loopTrack) { + currentSong = tracks.getTracks().get(currentTrack).toScore().onFinish(() -> next(player)); + } else if (currentSong != null && playing) { + currentSong.pause(); + playing = false; + player.sendMessage(Text.builder("Paused: ") + .color(TextColors.GOLD) + .append(TextUtil.track(getCurrentTrack()).build()) + .build()); + return; + } + if (playOnce) + currentSong = tracks.getTracks().get(currentTrack).toScore().onFinish(() -> stop(player)); + else { + currentSong = tracks.getTracks().get(currentTrack).toScore().onFinish(() -> next(player)); + } + + player.sendMessage(Text.builder("Now playing: ") + .color(TextColors.GOLD) + .append(TextUtil.track(getCurrentTrack()).build()) + .build()); + + currentSong.play(plugin, player); + playing = true; + } + + /** + * Starts playing or resumes the specified track. + * + * @param player player + * @param trackIndex index + */ + public void play(Player player, int trackIndex) { + play(player, trackIndex, false); + } + + /** + * Starts playing or resumes the current track. + * + * @param player player + */ + public void play(Player player) { + play(player, currentTrack, false); + } + + /** + * Pauses the {@link MusicPlayer}. + */ + public void pause() { + playing = false; + if (currentSong != null) { + currentSong.pause(); + } + } + + /** + * Stops the {@link MusicPlayer} + * + * @param player player + */ + public void stop(Player player) { + playing = false; + pause(); + int current = currentTrack; + currentSong.onFinish(() -> { + currentSong.finish(); + play(player, current); + }); + player.sendMessage(Text.builder("Stopped: ") + .color(TextColors.GOLD) + .append(TextUtil.track(getCurrentTrack()).build()) + .build()); + } + + /** + * Shuffles the player tracks and starts playing track zero. + * + * @param player player + */ + public void shuffle(Player player) { + Collections.shuffle(tracks.getTracks()); + if (currentSong != null) { + currentSong.pause(); + currentSong = null; + } + currentTrack = 0; + play(player); + } + + /** + * Skips the specified amount of tracks and starts playing. + * + * @param player player + * @param jumps tracks to skip + */ + public void skip(Player player, int jumps) { + int newIndex = (loopTrack) ? currentTrack : currentTrack + jumps; + if (newIndex < 0 || newIndex >= tracks.getTracks().size()) { + if (loopPlaylist) { + newIndex = 0; + } else { + pause(); + currentSong = null; + currentTrack = 0; + return; + } + } + play(player, newIndex); + } + + /** + * Skips one track. + * + * @param player player + */ + public void next(Player player) { + skip(player, 1); + } + + /** + * Goes back one track. + * + * @param player player + */ + public void previous(Player player) { + skip(player, -1); + } + + public void setLoopTrack(final boolean loopTrack) { + this.loopTrack = loopTrack; + } + + public void setLoopPlaylist(final boolean loopPlaylist) { + this.loopPlaylist = loopPlaylist; + } + + public boolean isLoopTrack() { + return loopTrack; + } + + public boolean isLoopPlaylist() { + return loopPlaylist; + } } diff --git a/src/main/java/se/walkercrou/composer/nbs/NoteBlockStudioSong.java b/src/main/java/se/walkercrou/composer/nbs/NoteBlockStudioSong.java index f6c1a87..222b027 100644 --- a/src/main/java/se/walkercrou/composer/nbs/NoteBlockStudioSong.java +++ b/src/main/java/se/walkercrou/composer/nbs/NoteBlockStudioSong.java @@ -1,15 +1,18 @@ package se.walkercrou.composer.nbs; import com.google.common.io.ByteStreams; +import lombok.Getter; import org.apache.commons.lang3.builder.ToStringBuilder; import org.spongepowered.api.effect.sound.SoundType; import org.spongepowered.api.effect.sound.SoundTypes; -import se.walkercrou.composer.Layer; -import se.walkercrou.composer.Measure; -import se.walkercrou.composer.Note; -import se.walkercrou.composer.Pitch; -import se.walkercrou.composer.Score; -import se.walkercrou.composer.TimeSignature; +import se.walkercrou.composer.exception.CorruptedFileException; +import se.walkercrou.composer.exception.OldNbsVersionException; +import se.walkercrou.composer.score.Layer; +import se.walkercrou.composer.score.Measure; +import se.walkercrou.composer.score.Note; +import se.walkercrou.composer.util.PitchUtils; +import se.walkercrou.composer.score.Score; +import se.walkercrou.composer.score.TimeSignature; import java.io.File; import java.io.FileInputStream; @@ -18,36 +21,45 @@ import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import java.nio.file.Files; /** * Represents song data imported from the Note Block Studio file format (.nbs). * * @see http://www.stuffbydavid.com/mcnbs */ +@Getter public class NoteBlockStudioSong { // ---- Header --- - public short lengthTicks; - public short height; // amount of layers - public String name; - public String author, ogAuthor; - public String description; - public double tempoTicksPerSecond; - public boolean autoSave; - public byte autoSaveDuration; - public byte timeSignature; - public int minutesSpent; - public int leftClicks, rightClicks; - public int blocksAdded, blocksRemoved; - public String importedFileName; + private short zeroBytes; + private byte version; + private byte instruments; + private short lengthTicks; + private short height; // amount of layers + private String name; + private String author; + private String ogAuthor; + private String description; + private double tempoTicksPerSecond; + private boolean autoSave; + private byte autoSaveDuration; + private byte timeSignature; + private int minutesSpent; + private int leftClicks; + private int rightClicks; + private int blocksAdded; + private int blocksRemoved; + private String importedFileName; + private byte loop; + private byte maxLoop; + private short loopStartTick; // ---- Note Blocks ---- - public NoteBlock[][] noteBlocks; + private NoteBlock[][] noteBlocks; // ---- Layer info ---- - public LayerInfo[] layerInfo; + private LayerInfo[] layerInfo; - private NoteBlockStudioSong() { - } /** * Converts this song into a {@link Score}. @@ -71,14 +83,14 @@ public Score toScore() { currentMeasure[beat - 1] = Note.rest(Note.QUARTER); else { // make sure key is within two octave range - int key = note.key; + int key = note.getKey(); while (key < 33) key += 12; while (key > 57) key -= 12; key -= 33; - currentMeasure[beat - 1] = new Note(note.getInstrument(), Pitch.TWO_OCTAVES[key], Note.QUARTER, + currentMeasure[beat - 1] = new Note(note.getInstrument(), PitchUtils.TWO_OCTAVES[key], Note.QUARTER, layerInfo[i].volume / 100d); } @@ -107,28 +119,39 @@ public String toString() { * @return song data * @throws IOException */ - public static NoteBlockStudioSong read(File file) throws IOException { + public static NoteBlockStudioSong read(File file) throws IOException, CorruptedFileException, OldNbsVersionException { if (!file.exists()) throw new FileNotFoundException(); NoteBlockStudioSong result = new NoteBlockStudioSong(); - InputStream in = new FileInputStream(file); + InputStream in = Files.newInputStream(file.toPath()); byte[] bytes = ByteStreams.toByteArray(in); ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + checkVersion(result,buffer,file.getName()); + readHeader(result, buffer); readNoteBlocks(result, buffer); readLayerInfo(result, buffer); return result; } + private static void checkVersion(final NoteBlockStudioSong result, ByteBuffer buffer, final String fileName) throws OldNbsVersionException{ + result.zeroBytes = buffer.getShort(); + if(result.zeroBytes != 0) { + throw new OldNbsVersionException(fileName); + } + } + private static String getString(ByteBuffer in) throws IOException { int len = in.getInt(); - String str = ""; + StringBuilder str = new StringBuilder(); for (int i = 0; i < len; i++) - str += (char) in.get(); - return str; + str.append((char) in.get()); + return str.toString(); } private static void readHeader(NoteBlockStudioSong result, ByteBuffer buffer) throws IOException { + result.version = buffer.get(); + result.instruments = buffer.get(); result.lengthTicks = buffer.getShort(); result.height = buffer.getShort(); result.name = getString(buffer); @@ -145,9 +168,12 @@ private static void readHeader(NoteBlockStudioSong result, ByteBuffer buffer) th result.blocksAdded = buffer.getInt(); result.blocksRemoved = buffer.getInt(); result.importedFileName = getString(buffer); + result.loop = buffer.get(); + result.maxLoop = buffer.get(); + result.loopStartTick = buffer.getShort(); } - private static void readNoteBlocks(NoteBlockStudioSong result, ByteBuffer buffer) throws IOException { + private static void readNoteBlocks(NoteBlockStudioSong result, ByteBuffer buffer) throws IOException, CorruptedFileException { result.noteBlocks = new NoteBlock[result.height + 1][result.lengthTicks + 1]; short tick = -1; short jumps; @@ -164,27 +190,47 @@ private static void readNoteBlocks(NoteBlockStudioSong result, ByteBuffer buffer layer += jumps; byte instrument = buffer.get(); byte key = buffer.get(); - result.noteBlocks[layer][tick] = new NoteBlock(instrument, key); + byte volume = buffer.get(); + byte panning = buffer.get(); + short pitch = buffer.getShort(); + try { + result.noteBlocks[layer][tick] = new NoteBlock(instrument, key,volume,panning,pitch); + } catch (ArrayIndexOutOfBoundsException e){ + throw new CorruptedFileException("Most likely a corrupted file.."); + } } } } + private static void readLayerInfo(NoteBlockStudioSong result, ByteBuffer buffer) throws IOException { + String name = getString(buffer); + byte lock = buffer.get(); + byte volume = buffer.get(); + byte stereo = buffer.get(); + result.layerInfo = new LayerInfo[result.height + 1]; for (int i = 0; i < result.height; i++) - result.layerInfo[i] = new LayerInfo(getString(buffer), buffer.get()); + result.layerInfo[i] = new LayerInfo(name,lock,volume,stereo); } /** * Represents a single note block within the song. */ + @Getter public static class NoteBlock { - public final byte instrument; - public final byte key; + private final byte instrument; + private final byte key; + private final byte volume; + private final byte panning; + private final short pitch; - private NoteBlock(byte instrument, byte key) { + private NoteBlock(byte instrument, byte key, byte volume, byte panning, short pitch) { this.instrument = instrument; this.key = key; + this.volume = volume; + this.panning = panning; + this.pitch = pitch; } /** @@ -205,6 +251,16 @@ public SoundType getInstrument() { return SoundTypes.BLOCK_NOTE_SNARE; case 4: return SoundTypes.BLOCK_NOTE_PLING; + case 5: + return SoundTypes.BLOCK_NOTE_GUITAR; + case 6: + return SoundTypes.BLOCK_NOTE_FLUTE; + case 7: + return SoundTypes.BLOCK_NOTE_BELL; + case 8: + return SoundTypes.BLOCK_NOTE_CHIME; + case 9: + return SoundTypes.BLOCK_NOTE_XYLOPHONE; } } @@ -215,15 +271,20 @@ public String toString() { } /** - * Represents some meta data relating to a layer of the song. + * Represents some metadata relating to a layer of the song. */ + @Getter public static class LayerInfo { - public final String name; - public final byte volume; + private final String name; + private final byte lock; + private final byte volume; + private final byte stereo; - private LayerInfo(String name, byte volume) { + private LayerInfo(String name, byte lock, byte volume, byte stereo) { this.name = name; + this.lock = lock; this.volume = volume; + this.stereo = stereo; } @Override diff --git a/src/main/java/se/walkercrou/composer/Layer.java b/src/main/java/se/walkercrou/composer/score/Layer.java similarity index 98% rename from src/main/java/se/walkercrou/composer/Layer.java rename to src/main/java/se/walkercrou/composer/score/Layer.java index 46b4bf3..63ffb53 100644 --- a/src/main/java/se/walkercrou/composer/Layer.java +++ b/src/main/java/se/walkercrou/composer/score/Layer.java @@ -1,4 +1,4 @@ -package se.walkercrou.composer; +package se.walkercrou.composer.score; import com.flowpowered.math.vector.Vector3d; import org.spongepowered.api.effect.Viewer; @@ -15,6 +15,13 @@ public class Layer { private final TimeSignature time; private final List measures; + private int currentBeat = 1; + private int currentMeasure = 1; + private int hold = 1; + private int noteIndex = 0; + private boolean finished = false; + + private Layer(TimeSignature time, List measures) { this.time = time; this.measures = measures; @@ -29,12 +36,6 @@ public List getMeasures() { return measures; } - private int currentBeat = 1; - private int currentMeasure = 1; - private int hold = 1; - private int noteIndex = 0; - private boolean finished = false; - protected boolean onStep(Viewer viewer, Vector3d pos, int currentStep, int stepsPerBeat) { if (finished) return true; // no more measures to play diff --git a/src/main/java/se/walkercrou/composer/Measure.java b/src/main/java/se/walkercrou/composer/score/Measure.java similarity index 92% rename from src/main/java/se/walkercrou/composer/Measure.java rename to src/main/java/se/walkercrou/composer/score/Measure.java index 30152e8..cd22112 100644 --- a/src/main/java/se/walkercrou/composer/Measure.java +++ b/src/main/java/se/walkercrou/composer/score/Measure.java @@ -1,4 +1,4 @@ -package se.walkercrou.composer; +package se.walkercrou.composer.score; /** * Represents a single measure within a {@link Score}. diff --git a/src/main/java/se/walkercrou/composer/Note.java b/src/main/java/se/walkercrou/composer/score/Note.java similarity index 91% rename from src/main/java/se/walkercrou/composer/Note.java rename to src/main/java/se/walkercrou/composer/score/Note.java index 8149308..29b73c4 100644 --- a/src/main/java/se/walkercrou/composer/Note.java +++ b/src/main/java/se/walkercrou/composer/score/Note.java @@ -1,9 +1,10 @@ -package se.walkercrou.composer; +package se.walkercrou.composer.score; import com.flowpowered.math.vector.Vector3d; import org.spongepowered.api.effect.Viewer; import org.spongepowered.api.effect.sound.SoundType; import org.spongepowered.api.effect.sound.SoundTypes; +import se.walkercrou.composer.util.PitchUtils; /** * Represents a single note in a {@link Measure}. @@ -21,7 +22,7 @@ public class Note { private final double volume; /** - * Creates new Note with the specified {@link Pitch} value and type. The note type corresponds to how many beats + * Creates new Note with the specified {@link PitchUtils} value and type. The note type corresponds to how many beats * it should last. For instance, in common time, a quarter note (type 4) lasts one beat. * * @param instrument of note @@ -52,7 +53,7 @@ public Note(double pitch, int type) { * Returns the "Minecraft" pitch of this note. * * @return pitch - * @see Pitch + * @see PitchUtils */ public double getPitch() { return pitch; diff --git a/src/main/java/se/walkercrou/composer/Score.java b/src/main/java/se/walkercrou/composer/score/Score.java similarity index 89% rename from src/main/java/se/walkercrou/composer/Score.java rename to src/main/java/se/walkercrou/composer/score/Score.java index 0507b4b..304f7b7 100644 --- a/src/main/java/se/walkercrou/composer/Score.java +++ b/src/main/java/se/walkercrou/composer/score/Score.java @@ -1,4 +1,4 @@ -package se.walkercrou.composer; +package se.walkercrou.composer.score; import com.flowpowered.math.vector.Vector3d; import org.spongepowered.api.Sponge; @@ -14,12 +14,17 @@ * Represents a musical score to be played in game. */ public class Score { - private final String title, artist; + private final String title; + private final String artist; private final int tempoBmp; private final TimeSignature time; private final List layers; private Runnable onFinish; + private Task task; + private int stepsPerBeat; + private int currentStep = 1; + private Score(String title, String artist, int tempoBmp, TimeSignature time, List layers) { this.title = title; this.artist = artist; @@ -127,14 +132,28 @@ public void play(Object context, Player viewer) { play(context, viewer, null); } + public void resume(Object context, Player viewer, Integer step) { + if(task == null) { + currentStep = step == null ? 0 : step; + play(context, viewer); + } + } + /** * Pauses the song. */ + /*TODO Actually stops the song. + We should store the "paused" position for the player. + Stop the song, and if the paused position is not 0, start it from there. + Pause will effectively stop the song, but the start exec will continue + from the correct position + */ public void pause() { if (task != null) task.cancel(); } + /** * Stops the song. */ @@ -144,15 +163,12 @@ public void finish() { onFinish.run(); } - private Task task; - private int stepsPerBeat; - private int currentStep = 1; private void nextStep(Viewer viewer, Vector3d pos) { boolean finished = true; for (Layer layer : layers) finished &= layer.onStep(viewer, pos, currentStep, stepsPerBeat); - if (currentStep == stepsPerBeat) + if (currentStep == stepsPerBeat) //? currentStep = 1; else currentStep++; @@ -164,7 +180,8 @@ private void nextStep(Viewer viewer, Vector3d pos) { * Builder class for {@link Score} object. */ public static class Builder { - private String title, artist; + private String title; + private String artist; private int tempoBmp; private TimeSignature time; protected final List layers = new ArrayList<>(); diff --git a/src/main/java/se/walkercrou/composer/TimeSignature.java b/src/main/java/se/walkercrou/composer/score/TimeSignature.java similarity index 92% rename from src/main/java/se/walkercrou/composer/TimeSignature.java rename to src/main/java/se/walkercrou/composer/score/TimeSignature.java index f378e8c..36b3de1 100644 --- a/src/main/java/se/walkercrou/composer/TimeSignature.java +++ b/src/main/java/se/walkercrou/composer/score/TimeSignature.java @@ -1,4 +1,4 @@ -package se.walkercrou.composer; +package se.walkercrou.composer.score; /** * Represents a musical time signature within a {@link Score}. @@ -13,7 +13,8 @@ public class TimeSignature { */ public static final TimeSignature CUT = new TimeSignature(2, 2); - private final int beatsPerMeasure, singleBeatNote; + private final int beatsPerMeasure; + private final int singleBeatNote; /** * Creates a new TimeSignature. The first parameter represents the upper number in a traditional key signature diff --git a/src/main/java/se/walkercrou/composer/Pitch.java b/src/main/java/se/walkercrou/composer/util/PitchUtils.java similarity index 93% rename from src/main/java/se/walkercrou/composer/Pitch.java rename to src/main/java/se/walkercrou/composer/util/PitchUtils.java index 1287c07..5eaa16c 100644 --- a/src/main/java/se/walkercrou/composer/Pitch.java +++ b/src/main/java/se/walkercrou/composer/util/PitchUtils.java @@ -1,11 +1,11 @@ -package se.walkercrou.composer; +package se.walkercrou.composer.util; import org.spongepowered.api.effect.sound.SoundType; /** * Represents a pitch value for a {@link SoundType}. Minecraft's accepted pitches span two octaves from F#0 to F#2. */ -public final class Pitch { +public final class PitchUtils { public static final double FSHARP0 = 0; public static final double G0 = 0.53; public static final double GSHARP0 = 0.56; @@ -53,6 +53,7 @@ public final class Pitch { B1, C2, CSHARP2, D2, DSHARP2, E2, F2, FSHARP2 }; - private Pitch() { + private PitchUtils() { + throw new IllegalStateException("Utility class"); } } diff --git a/src/main/java/se/walkercrou/composer/util/TextUtil.java b/src/main/java/se/walkercrou/composer/util/TextUtil.java index 6bd20c0..83d277f 100644 --- a/src/main/java/se/walkercrou/composer/util/TextUtil.java +++ b/src/main/java/se/walkercrou/composer/util/TextUtil.java @@ -8,6 +8,8 @@ import org.spongepowered.api.text.action.TextActions; import org.spongepowered.api.text.format.TextColors; import org.spongepowered.api.text.format.TextStyles; +import se.walkercrou.composer.Composer; +import se.walkercrou.composer.Playlist; import se.walkercrou.composer.nbs.NoteBlockStudioSong; import java.util.ArrayList; @@ -17,78 +19,152 @@ * Utility class for text handling. */ public final class TextUtil { - private TextUtil() { - } - - /** - * Returns a pagination builder for the specified tracks. - * - * @param tracks to build list for - * @return pagination builder - * @throws CommandException if list is empty - */ - public static PaginationList.Builder trackList(List tracks) throws CommandException { - List trackListings = new ArrayList<>(); - for (int i = 0; i < tracks.size(); i++) { - trackListings.add(TextUtil.track(tracks.get(i)) - .onClick(TextActions.runCommand("/music > " + (i + 1))) - .build()); - } - - if (trackListings.isEmpty()) - throw new CommandException(Text.of("There are no tracks currently loaded.")); - - return Sponge.getServiceManager().provide(PaginationService.class).get().builder() - .contents(trackListings) - .title(Text.builder("Tracks").color(TextColors.GOLD).build()) - .header(Text.builder("Play") - .color(TextColors.DARK_GREEN) - .style(TextStyles.BOLD) - .onClick(TextActions.runCommand("/music resume")) - .append(Text.of(" ")) - .append(Text.builder("Pause") - .color(TextColors.YELLOW) - .onClick(TextActions.runCommand("/music ||")) - .build()) - .append(Text.of(" ")) - .append(Text.builder("Shuffle") - .color(TextColors.AQUA) - .onClick(TextActions.runCommand("/music shuffle")) - .build()) - .append(Text.of(" ")) - .append(Text.builder("«") - .color(TextColors.LIGHT_PURPLE) - .style(TextStyles.RESET) - .onClick(TextActions.runCommand("/music |<")) - .build()) - .append(Text.of(" ")) - .append(Text.builder("»") - .color(TextColors.LIGHT_PURPLE) - .style(TextStyles.RESET) - .onClick(TextActions.runCommand("/music >|")) - .build()) - .build()) - .footer(Text.builder("Click a track to start playing.").color(TextColors.GRAY).build()) - .padding(Text.of("-")); - } - - /** - * Returns a track listing Text for the specified track. - * - * @param track listing - * @return listing - */ - public static Text.Builder track(NoteBlockStudioSong track) { - return Text.builder(strOrUnknown(track.name)) - .color(TextColors.GREEN) - .append(Text.builder(" by ").color(TextColors.GRAY).build()) - .append(Text.builder(strOrUnknown(track.ogAuthor).equals("Unknown") - ? strOrUnknown(track.author) : track.ogAuthor) - .color(TextColors.GREEN) - .build()); - } - - private static String strOrUnknown(String str) { - return str == null || str.isEmpty() ? "Unknown" : str; - } + private TextUtil() { + throw new IllegalStateException("Util class"); + } + + private static Text.Builder playText(Text.Builder builder, int i) { + return builder.onClick(TextActions.runCommand("/music > " + (i + 1))) + .onHover(TextActions.showText(Text.of("Click to play."))); + } + + private static Text.Builder playOnceText(int i) { + return Text.builder("Play Once") + .color(TextColors.DARK_GRAY) + .onClick(TextActions.runCommand("/music play-once " + (i + 1))) + .onHover(TextActions.showText(Text.of("Click to play once."))); + } + + private static Text doubleSpace() { + return Text.builder().append(Text.of(" ")).build(); + } + + /** + * Returns a pagination builder for the specified tracks. + * + * @param tracks to build list for + * @return pagination builder + * @throws CommandException if list is empty + */ + public static PaginationList.Builder trackList(List tracks, final String title) throws CommandException { + List trackListings = new ArrayList<>(); + for (int i = 0; i < tracks.size(); i++) { + trackListings.add(playText(TextUtil.track(tracks.get(i)), i) + .append(Text.of(" ")) + .append(playOnceText(i).build()) + .build()); + } + + if (trackListings.isEmpty()) + throw new CommandException(Text.of("There are no tracks currently loaded.")); + + return Sponge.getServiceManager().provide(PaginationService.class).get().builder() + .contents(trackListings) + .title(Text.builder(title).color(TextColors.GOLD).build()) + .header( + Text.builder() + .append(Text.of("Play")) + .color(TextColors.DARK_GREEN) + .style(TextStyles.BOLD) + .onClick(TextActions.runCommand("/music resume")) + .onHover(TextActions.showText(Text.of("Resume a track."))) + .append(doubleSpace()).onHover(null) + .append(Text.builder("Pause") + .color(TextColors.YELLOW) + .onClick(TextActions.runCommand("/music pause")) + .onHover(TextActions.showText(Text.of("Pause a track."))) + .build()) + .append(doubleSpace()) + .append(Text.builder("Stop") + .color(TextColors.RED) + .onClick(TextActions.runCommand("/music stop")) + .onHover(TextActions.showText(Text.of("Stop a track."))) + .build()) + .append(doubleSpace()) + .append(Text.builder("Shuffle") + .color(TextColors.AQUA) + .onClick(TextActions.runCommand("/music shuffle")) + .onHover(TextActions.showText(Text.of("Shuffle the tracklist."))) + .build()) + .append(doubleSpace()) + .append(Text.builder("Loop Track") + .color(TextColors.AQUA) + .onClick(TextActions.runCommand("/music loop")) + .onHover(TextActions.showText(Text.of("Repeat the track."))) + .build()) + .append(doubleSpace()) + .append(Text.builder("Loop Playlist") + .color(TextColors.AQUA) + .onClick(TextActions.runCommand("/music loop-all")) + .onHover(TextActions.showText(Text.of("Repeat the playlist."))) + .build()) + .append(doubleSpace()) + .append(Text.builder("«") + .color(TextColors.LIGHT_PURPLE) + .style(TextStyles.RESET) + .onClick(TextActions.runCommand("/music |<")) + .onHover(TextActions.showText(Text.of("Previous"))) + .build()) + .append(doubleSpace()) + .append(Text.builder("»") + .color(TextColors.LIGHT_PURPLE) + .style(TextStyles.RESET) + .onClick(TextActions.runCommand("/music >|")) + .onHover(TextActions.showText(Text.of("Next"))) + .build()) + .build()) + .footer(Text.builder("Click a track to start playing.").color(TextColors.GRAY).build()) + .padding(Text.of("-")); + } + + public static PaginationList.Builder playlistList() throws CommandException { + List playlistListing = new ArrayList<>(); + for (Playlist playlist : Composer.getInstance().getPlaylists().values()) { + playlistListing.add(TextUtil.playlist(playlist) + .onClick(TextActions.runCommand("/music playlist " + getPlaylistName(playlist))).build()); + } + + if (playlistListing.isEmpty()) + throw new CommandException(Text.of("There aren't any loaded playlists.")); + + return Sponge.getServiceManager().provide(PaginationService.class).get().builder() + .contents(playlistListing) + .title(Text.builder("Playlists").color(TextColors.GOLD).build()) + .footer(Text.builder("Click a playlist to select it.").color(TextColors.GRAY).build()) + .padding(Text.of("-")); + + } + + /** + * Returns a track listing Text for the specified track. + * + * @param track listing + * @return listing + */ + public static Text.Builder track(NoteBlockStudioSong track) { + return Text.builder(strOrUnknown(track.getName())) + .color(TextColors.GREEN) + .append(Text.builder(" by ").color(TextColors.GRAY).build()) + .append(Text.builder(strOrUnknown(track.getOgAuthor()).equals("Unknown") + ? strOrUnknown(track.getOgAuthor()) : track.getOgAuthor()) + .color(TextColors.GREEN) + .build()); + } + + public static Text.Builder playlist(Playlist playlist) { + return Text.builder(getPlaylistName(playlist)) + .color(TextColors.GREEN) + .onHover(TextActions.showText(Text.of(playlist.getTracks().size() + " tracks"))); + } + + public static String getPlaylistName(final Playlist playlist) { + return Composer.getInstance().getPlaylists().keySet() + .stream() + .filter(key -> playlist.equals(Composer.getInstance().getPlaylists().get(key))) + .findFirst().orElse("Tracks"); + } + + private static String strOrUnknown(String str) { + return str == null || str.isEmpty() ? "Unknown" : str; + } } diff --git a/src/main/resources/assets/se/walkercrou/composer/default.conf b/src/main/resources/assets/se/walkercrou/composer/default.conf index b291a88..e70b046 100644 --- a/src/main/resources/assets/se/walkercrou/composer/default.conf +++ b/src/main/resources/assets/se/walkercrou/composer/default.conf @@ -1 +1,3 @@ debugMode=false +use-playlists=false +default-playlist="default" \ No newline at end of file