├── .gitattributes ├── .gitignore ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── readme.md └── src └── main ├── java └── me │ └── Navoei │ └── customdiscsplugin │ ├── CustomDiscs.java │ ├── HopperManager.java │ ├── JukeboxStateManager.java │ ├── PlayerManager.java │ ├── VoicePlugin.java │ ├── command │ ├── CustomDiscCommand.java │ └── SubCommands │ │ ├── CreateSubCommand.java │ │ ├── DownloadSubCommand.java │ │ └── SetRangeSubCommand.java │ ├── event │ └── JukeBox.java │ └── language │ └── Lang.java └── resources ├── config.yml ├── lang.yml ├── music_disc_category.png └── plugin.yml /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle/ 2 | .idea/ 3 | build/ 4 | run/ -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id "com.gradleup.shadow" version "8.3.5" 4 | } 5 | 6 | sourceCompatibility = JavaLanguageVersion.of(java_version as int) 7 | targetCompatibility = JavaLanguageVersion.of(java_version as int) 8 | 9 | archivesBaseName = archives_base_name 10 | version = plugin_version 11 | group = maven_group 12 | 13 | processResources { 14 | filesMatching("plugin.yml") { 15 | expand "plugin_version": plugin_version, 16 | "bukkit_api_version": bukkit_api_version 17 | } 18 | } 19 | 20 | dependencies { 21 | 22 | implementation("com.googlecode.soundlibs:mp3spi:${mp3spi_version}") { 23 | exclude group: 'junit', module: 'junit' 24 | } 25 | shadow("com.googlecode.soundlibs:mp3spi:${mp3spi_version}") { 26 | exclude group: 'junit', module: 'junit' 27 | } 28 | 29 | implementation 'com.google.code.findbugs:jsr305:3.0.2' 30 | 31 | implementation 'org.jflac:jflac-codec:1.5.2' 32 | shadow 'org.jflac:jflac-codec:1.5.2' 33 | 34 | // To use this dependency, you need to compile bukkit by yourself 35 | // See https://www.spigotmc.org/wiki/buildtools/ 36 | // implementation "org.bukkit:craftbukkit:${bukkit_version}" 37 | 38 | // Use this dependency if you don't want to compile bukkit 39 | implementation "io.papermc.paper:paper-api:${bukkit_version}" 40 | implementation "de.maxhenkel.voicechat:voicechat-api:${voicechat_api_version}" 41 | shadow "dev.jorel:commandapi-bukkit-shade:${command_api_version}" 42 | 43 | compileOnly group: "com.comphenix.protocol", name: "ProtocolLib", version: "5.3.0"; 44 | } 45 | 46 | repositories { 47 | mavenCentral() 48 | maven { url "https://repo.dmulloy2.net/repository/public/" } 49 | maven { 50 | name = "henkelmax.public" 51 | url = 'https://maven.maxhenkel.de/repository/public' 52 | } 53 | // You need this maven repository if you want to use the paper dependency 54 | maven { 55 | name = "papermc" 56 | url = uri("https://repo.papermc.io/repository/maven-public/") 57 | } 58 | maven { url "https://repo.dmulloy2.net/repository/public/" } 59 | maven { url = "https://repo.codemc.org/repository/maven-public/" } 60 | 61 | mavenLocal() 62 | } 63 | 64 | shadowJar { 65 | configurations = [project.configurations.shadow] 66 | archiveClassifier.set("dev") 67 | 68 | //relocate 'javazoom', "me.navoei.${mod_id}.javazoom" 69 | //relocate 'org.tritonus', "me.navoei.${mod_id}.tritonus" 70 | 71 | // By documentation, it was recommented to relocate to not cause issues with other plugins that shade CommandAPI 72 | relocate("dev.jorel.commandapi", "me.navoei.${mod_id}.commandapi") 73 | } 74 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx2G 2 | 3 | java_version=21 4 | 5 | mp3spi_version=1.9.5.4 6 | 7 | bukkit_api_version=1.21 8 | bukkit_version=1.21.5-R0.1-SNAPSHOT 9 | mod_id=customdiscsplugin 10 | 11 | # Target an older API to make it compatible with older versions of Simple Voice Chat 12 | voicechat_api_version=2.3.3 13 | command_api_version=10.0.1 14 | 15 | plugin_version=4.4 16 | maven_group=me.Navoei.customdiscsplugin 17 | archives_base_name=custom-discs -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Navoei/CustomDiscs/85148b489ada5fb27545e99a3ffcc72a27f57a81/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun Aug 11 17:52:06 EDT 2024 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MSYS* | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | ARGV=("$@") 176 | eval set -- $DEFAULT_JVM_OPTS 177 | 178 | IFS=$' 179 | ' read -rd '' -a JAVA_OPTS_ARR <<< "$(echo $JAVA_OPTS | xargs -n1)" 180 | IFS=$' 181 | ' read -rd '' -a GRADLE_OPTS_ARR <<< "$(echo $GRADLE_OPTS | xargs -n1)" 182 | 183 | exec "$JAVACMD" "$@" "${JAVA_OPTS_ARR[@]}" "${GRADLE_OPTS_ARR[@]}" "-Dorg.gradle.appname=$APP_BASE_NAME" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "${ARGV[@]}" 184 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Custom Discs v4.4 for Paper 1.21.5 2 | 3 | A Paper fork of henkelmax's Audio Player. Special thanks to Athar42 for maintaining this plugin. 4 | - Play custom music discs using the Simple Voice Chat API. (The voice chat mod is required on the client and server.) 5 | - Use ```/customdisc``` or ```/cd``` to view available commands. 6 | - Music files should go into ```plugins/CustomDiscs/musicdata/``` 7 | - Music files must be in the ```.wav```, ```.flac```, or ```.mp3``` format. 8 | 9 | Downloading Files: 10 | - To download a file use the command ```/cd download ```. The link used to download a file must be a direct link (meaning the file must automatically begin downloading when accessing the link). Files must have the correct extension specified. An UnsupportedAudioFileException will be thrown in the server's console if the file extension is not correct (for example when giving a wav file the mp3 extension). Below is an example of how to use the command and a link to get direct downloads from Google Drive. 11 | - Example: ```/cd download https://example.com/mysong mysong.mp3``` 12 | - Direct Google Drive links: https://lonedev6.github.io/gddl/ 13 | 14 | Set the range of a disc: 15 | - To set the active range of a playable disc, just use the command ```/cd range ```. The range can be between 1 and the max value set in the config file (default : 256) 16 | - Example: ```/cd range 100``` 17 | 18 | Permission Nodes (Required to run the commands. Playing discs does not require a permission.): 19 | - ```customdiscs.create``` to create a disc 20 | - ```customdiscs.download``` to download a file 21 | - ```customdiscs.range``` to set the range of the disc 22 | 23 | Dependencies: 24 | - This plugin depends on the latest version of ProtocolLib for 1.21.5 and SimpleVoiceChatBukkit version 2.5.30. 25 | 26 | 27 | https://user-images.githubusercontent.com/64107368/178426026-c454ac66-5133-4f3a-9af9-7f674e022423.mp4 28 | 29 | Default Config.yml: 30 | ``` 31 | # [Music Disc Config] 32 | 33 | # The distance from which music discs can be heard in blocks. 34 | music-disc-distance: 16 35 | 36 | # The max distance from which music discs can be heard in blocks. 37 | music-disc-max-distance: 256 38 | 39 | # The master volume of music discs from 0-1. (You can set values like 0.5 for 50% volume). 40 | music-disc-volume: 1 41 | 42 | # The maximum download size in megabytes. 43 | max-download-size: 50 44 | 45 | #Custom Discs Help Page 46 | help: 47 | - "&8-[&6CustomDiscs Help Page&8]-" 48 | - "&aAuthor&7: &6Navoei" 49 | - "&aContributors&7: &6alfw / &6Athar42" 50 | - "&fGit&0Hub&7: &9&ohttps://github.com/Navoei/CustomDiscs" 51 | ``` 52 | 53 | Default Lang.yml: 54 | ``` 55 | prefix: "&8[&6CustomDiscs&8]&r" 56 | no-permission: "&cYou do not have permission to execute this command." 57 | invalid-filename: "&cThis is an invalid filename!" 58 | no-disc-name-provided: "&cYou must provide a name for your disc." 59 | invalid-format: "&cFile must be in wav, flac, or mp3 format!" 60 | file-not-found: "&cFile not found!" 61 | invalid-arguments: "&cInvalid arguments. &7(&a%command_syntax%&7)" 62 | not-holding-disc: "&cYou must hold a disc in your main hand." 63 | create-filename: "&7Your filename is: &a\"%filename%\"." 64 | create-custom-name: "&7Your custom name is: &a\"%custom_name%\"." 65 | downloading-file: "&7Downloading file..." 66 | file-too-large: "&cThe file is larger than %max_download_size%MB." 67 | successful-download: "&aFile successfully downloaded to &7%file_path%&a." 68 | create-disc: "&aCreate a disc by doing &7/cd create %filename% \"Custom Lore\"&a." 69 | download-error: "&cAn error has occurred while downloading." 70 | now-playing: "&6Now playing: %song_name%" 71 | disc-converted: "&aConverted disc to new format! &fThis is due to changes in newer Minecraft versions which introduced &7JukeboxPlayableComponent&f." 72 | invalid-range: "&cYou need to chose a range between 1 and %range_value%" 73 | create-custom-range: "&7Your range is set to: &a\"%custom_range%\"." 74 | ``` 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/main/java/me/Navoei/customdiscsplugin/CustomDiscs.java: -------------------------------------------------------------------------------- 1 | package me.Navoei.customdiscsplugin; 2 | 3 | import com.comphenix.protocol.PacketType; 4 | import com.comphenix.protocol.ProtocolLibrary; 5 | import com.comphenix.protocol.ProtocolManager; 6 | import com.comphenix.protocol.events.ListenerPriority; 7 | import com.comphenix.protocol.events.PacketAdapter; 8 | import com.comphenix.protocol.events.PacketContainer; 9 | import com.comphenix.protocol.events.PacketEvent; 10 | import de.maxhenkel.voicechat.api.BukkitVoicechatService; 11 | import dev.jorel.commandapi.CommandAPI; 12 | import dev.jorel.commandapi.CommandAPIBukkitConfig; 13 | import me.Navoei.customdiscsplugin.command.CustomDiscCommand; 14 | import me.Navoei.customdiscsplugin.event.JukeBox; 15 | import me.Navoei.customdiscsplugin.language.Lang; 16 | import org.bukkit.NamespacedKey; 17 | import org.bukkit.block.Jukebox; 18 | import org.bukkit.configuration.file.YamlConfiguration; 19 | import org.bukkit.persistence.PersistentDataType; 20 | import org.bukkit.plugin.java.JavaPlugin; 21 | 22 | import javax.annotation.Nullable; 23 | import java.io.*; 24 | import java.util.Objects; 25 | import java.util.logging.Level; 26 | import java.util.logging.Logger; 27 | 28 | public final class CustomDiscs extends JavaPlugin { 29 | static CustomDiscs instance; 30 | 31 | @Nullable 32 | private VoicePlugin voicechatPlugin; 33 | private Logger log; 34 | public static YamlConfiguration LANG; 35 | public static File LANG_FILE; 36 | public float musicDiscDistance; 37 | public float musicDiscMaxDistance; 38 | public float musicDiscVolume; 39 | 40 | @Override 41 | public void onLoad() { 42 | CustomDiscs.instance = this; 43 | CommandAPI.onLoad(new CommandAPIBukkitConfig(this).verboseOutput(true)); 44 | new CustomDiscCommand(this).register("customdiscs"); 45 | } 46 | 47 | @Override 48 | public void onEnable() { 49 | log = getLogger(); 50 | 51 | CommandAPI.onEnable(); 52 | 53 | BukkitVoicechatService service = getServer().getServicesManager().load(BukkitVoicechatService.class); 54 | 55 | this.saveDefaultConfig(); 56 | loadLang(); 57 | 58 | File musicData = new File(this.getDataFolder(), "musicdata"); 59 | if (!(musicData.exists())) { 60 | musicData.mkdirs(); 61 | } 62 | 63 | if (service != null) { 64 | voicechatPlugin = new VoicePlugin(); 65 | service.registerPlugin(voicechatPlugin); 66 | log.info("Successfully registered CustomDiscs plugin"); 67 | } else { 68 | log.info("Failed to register CustomDiscs plugin"); 69 | } 70 | 71 | getServer().getPluginManager().registerEvents(new JukeBox(), this); 72 | getServer().getPluginManager().registerEvents(new HopperManager(), this); 73 | 74 | musicDiscDistance = Objects.requireNonNull(getConfig().getInt("music-disc-distance")); 75 | musicDiscMaxDistance = Objects.requireNonNull(getConfig().getInt("music-disc-max-distance")); 76 | musicDiscVolume = Float.parseFloat(Objects.requireNonNull(getConfig().getString("music-disc-volume"))); 77 | 78 | ProtocolManager protocolManager = ProtocolLibrary.getProtocolManager(); 79 | 80 | protocolManager.addPacketListener(new PacketAdapter(this, ListenerPriority.NORMAL, PacketType.Play.Server.WORLD_EVENT) { 81 | @Override 82 | public void onPacketSending(PacketEvent event) { 83 | PacketContainer packet = event.getPacket(); 84 | 85 | if (packet.getIntegers().read(0).toString().equals("1010")) { 86 | Jukebox jukebox = (Jukebox) packet.getBlockPositionModifier().read(0).toLocation(event.getPlayer().getWorld()).getBlock().getState(); 87 | 88 | if (!jukebox.getRecord().hasItemMeta()) return; 89 | 90 | if (jukebox.getRecord().getItemMeta().getPersistentDataContainer().has(new NamespacedKey(this.plugin, "customdisc"), PersistentDataType.STRING)) { 91 | event.setCancelled(true); 92 | } 93 | 94 | //Start the jukebox state manager. 95 | //This keeps the jukebox powered while custom song is playing, 96 | //which perfectly emulates the vanilla behavior of discs. 97 | JukeboxStateManager.start(jukebox); 98 | } 99 | } 100 | }); 101 | 102 | } 103 | 104 | @Override 105 | public void onDisable() { 106 | CommandAPI.onDisable(); 107 | if (voicechatPlugin != null) { 108 | getServer().getServicesManager().unregister(voicechatPlugin); 109 | log.info("Successfully unregistered CustomDiscs plugin"); 110 | } 111 | } 112 | 113 | public static CustomDiscs getInstance() { 114 | return instance; 115 | } 116 | 117 | /** 118 | * Load the lang.yml file. 119 | * 120 | * @return The lang.yml config. 121 | */ 122 | public void loadLang() { 123 | File lang = new File(getDataFolder(), "lang.yml"); 124 | if (!lang.exists()) { 125 | try { 126 | getDataFolder().mkdir(); 127 | lang.createNewFile(); 128 | InputStream defConfigStream = this.getResource("lang.yml"); 129 | if (defConfigStream != null) { 130 | copyInputStreamToFile(defConfigStream, lang); 131 | YamlConfiguration defConfig = YamlConfiguration.loadConfiguration(lang); 132 | defConfig.save(lang); 133 | Lang.setFile(defConfig); 134 | } 135 | } catch (IOException e) { 136 | e.printStackTrace(); // So they notice 137 | log.severe("Failed to create lang.yml for CustomDiscs."); 138 | log.severe("Now disabling..."); 139 | this.setEnabled(false); // Without it loaded, we can't send them messages 140 | } 141 | } 142 | YamlConfiguration conf = YamlConfiguration.loadConfiguration(lang); 143 | for (Lang item : Lang.values()) { 144 | if (conf.getString(item.getPath()) == null) { 145 | conf.set(item.getPath(), item.getDefault()); 146 | } 147 | } 148 | Lang.setFile(conf); 149 | LANG = conf; 150 | LANG_FILE = lang; 151 | try { 152 | conf.save(getLangFile()); 153 | } catch (IOException e) { 154 | log.log(Level.WARNING, "Failed to save lang.yml for CustomDiscs"); 155 | log.log(Level.WARNING, "Now disabling..."); 156 | e.printStackTrace(); 157 | } 158 | } 159 | 160 | /** 161 | * Gets the lang.yml config. 162 | * 163 | * @return The lang.yml config. 164 | */ 165 | public YamlConfiguration getLang() { 166 | return LANG; 167 | } 168 | 169 | /** 170 | * Get the lang.yml file. 171 | * 172 | * @return The lang.yml file. 173 | */ 174 | public File getLangFile() { 175 | return LANG_FILE; 176 | } 177 | 178 | public static void copyInputStreamToFile(InputStream input, File file) { 179 | 180 | try (OutputStream output = new FileOutputStream(file)) { 181 | input.transferTo(output); 182 | } catch (IOException ioException) { 183 | ioException.printStackTrace(); 184 | } 185 | 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/main/java/me/Navoei/customdiscsplugin/HopperManager.java: -------------------------------------------------------------------------------- 1 | package me.Navoei.customdiscsplugin; 2 | 3 | import io.papermc.paper.datacomponent.DataComponentTypes; 4 | import io.papermc.paper.datacomponent.item.TooltipDisplay; 5 | import me.Navoei.customdiscsplugin.language.Lang; 6 | import net.kyori.adventure.text.Component; 7 | import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; 8 | import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; 9 | import org.bukkit.Material; 10 | import org.bukkit.NamespacedKey; 11 | import org.bukkit.block.Block; 12 | import org.bukkit.block.BlockState; 13 | //import org.bukkit.block.Container; 14 | import org.bukkit.block.Jukebox; 15 | import org.bukkit.event.EventHandler; 16 | import org.bukkit.event.EventPriority; 17 | import org.bukkit.event.Listener; 18 | import org.bukkit.event.inventory.InventoryMoveItemEvent; 19 | import org.bukkit.event.inventory.InventoryType; 20 | import org.bukkit.event.world.ChunkLoadEvent; 21 | import org.bukkit.inventory.ItemStack; 22 | import org.bukkit.persistence.PersistentDataContainer; 23 | import org.bukkit.inventory.meta.ItemMeta; 24 | import org.bukkit.inventory.meta.components.JukeboxPlayableComponent; 25 | import org.bukkit.persistence.PersistentDataType; 26 | 27 | import java.nio.file.Path; 28 | import java.util.Objects; 29 | import java.util.Optional; 30 | 31 | import org.bukkit.entity.minecart.HopperMinecart; 32 | import org.bukkit.inventory.InventoryHolder; 33 | 34 | // Used only if logger is needed 35 | //import java.util.logging.Logger; 36 | //import org.bukkit.Bukkit; 37 | 38 | public class HopperManager implements Listener { 39 | 40 | CustomDiscs customDiscs = CustomDiscs.getInstance(); 41 | 42 | PlayerManager playerManager = PlayerManager.instance(); 43 | 44 | //private static final Logger logger = Bukkit.getLogger(); // or Logger.getLogger("Minecraft"); 45 | 46 | @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) 47 | public void onJukeboxInsertFromHopper(InventoryMoveItemEvent event) { 48 | //logger.warning("Enter : onJukeboxInsertFromHopper"); 49 | if (event.getDestination().getLocation() == null) return; 50 | if (!event.getDestination().getType().equals(InventoryType.JUKEBOX)) return; 51 | if (!isCustomMusicDisc(event.getItem())) return; 52 | 53 | Component songNameComponent = Objects.requireNonNull(event.getItem().getItemMeta().lore()).get(0).asComponent(); 54 | String songName = PlainTextComponentSerializer.plainText().serialize(songNameComponent); 55 | Component customActionBarSongPlaying = LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.NOW_PLAYING.toString().replace("%song_name%", songName)); 56 | 57 | ItemMeta discMeta = event.getItem().getItemMeta(); 58 | String soundFileName = discMeta.getPersistentDataContainer().get(new NamespacedKey(customDiscs, "customdisc"), PersistentDataType.STRING); 59 | 60 | PersistentDataContainer persistentDataContainer = event.getItem().getItemMeta().getPersistentDataContainer(); 61 | float range = CustomDiscs.getInstance().musicDiscDistance; 62 | NamespacedKey customSoundRangeKey = new NamespacedKey(customDiscs, "range"); 63 | 64 | if(persistentDataContainer.has(customSoundRangeKey, PersistentDataType.FLOAT)) { 65 | float soundRange = Optional.ofNullable(persistentDataContainer.get(customSoundRangeKey, PersistentDataType.FLOAT)).orElse(0f); 66 | range = Math.min(soundRange, CustomDiscs.getInstance().musicDiscMaxDistance); 67 | } 68 | 69 | if (!event.getItem().hasData(DataComponentTypes.TOOLTIP_DISPLAY) || !event.getItem().getData(DataComponentTypes.TOOLTIP_DISPLAY).hiddenComponents().contains(DataComponentTypes.JUKEBOX_PLAYABLE)) { 70 | event.getItem().setData(DataComponentTypes.TOOLTIP_DISPLAY, TooltipDisplay.tooltipDisplay().addHiddenComponents(DataComponentTypes.JUKEBOX_PLAYABLE).build()); 71 | } 72 | 73 | Path soundFilePath = Path.of(customDiscs.getDataFolder().getPath(), "musicdata", soundFileName); 74 | assert VoicePlugin.voicechatServerApi != null; 75 | playerManager.playAudio(VoicePlugin.voicechatServerApi, soundFilePath, event.getDestination().getLocation().getBlock(), customActionBarSongPlaying, range); 76 | 77 | } 78 | 79 | @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) 80 | public void onJukeboxEjectToHopperMinecart(InventoryMoveItemEvent event) { 81 | //logger.warning("Enter : onJukeboxEjectToHopper"); 82 | 83 | InventoryHolder holderSource = event.getSource().getHolder(); 84 | InventoryHolder holderDestination = event.getDestination().getHolder(); 85 | 86 | if (event.getSource().getLocation() == null) return; 87 | if (!event.getSource().getType().equals(InventoryType.JUKEBOX)) return; 88 | if (event.getItem().getItemMeta() == null) return; 89 | if (!isCustomMusicDisc(event.getItem())) return; 90 | 91 | if (holderDestination instanceof HopperMinecart) { 92 | stopDisc(((BlockState) holderSource).getBlock()); 93 | } 94 | 95 | } 96 | 97 | @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) 98 | public void onChunkLoad(ChunkLoadEvent event) { 99 | //logger.warning("Enter : onChunkLoad"); 100 | for (BlockState blockState : event.getChunk().getTileEntities()) { 101 | if (blockState instanceof Jukebox jukebox) { 102 | if (!jukebox.hasRecord()) return; 103 | if (!PlayerManager.instance().isAudioPlayerPlaying(blockState.getLocation()) && isCustomMusicDisc(jukebox.getRecord())) { 104 | //Set the block type to force an update. 105 | jukebox.stopPlaying(); 106 | } 107 | } 108 | } 109 | } 110 | 111 | public void discToHopper(Block block) { 112 | if (block == null) return; 113 | if (!block.getLocation().getChunk().isLoaded()) return; 114 | if (!block.getType().equals(Material.JUKEBOX)) return; 115 | 116 | Jukebox jukebox = (Jukebox) block.getState(); 117 | jukebox.stopPlaying(); 118 | } 119 | 120 | private boolean isCustomMusicDisc(ItemStack item) { 121 | //logger.warning("Enter : isCustomMusicDisc"); 122 | return item.getItemMeta().getPersistentDataContainer().has(new NamespacedKey(customDiscs, "customdisc"), PersistentDataType.STRING); 123 | } 124 | 125 | private void stopDisc(Block block) { 126 | playerManager.stopLocationalAudio(block.getLocation()); 127 | } 128 | 129 | private static HopperManager instance; 130 | 131 | public static HopperManager instance() { 132 | //logger.warning("Enter : HopperManager Instance"); 133 | if (instance == null) { 134 | instance = new HopperManager(); 135 | } 136 | return instance; 137 | } 138 | 139 | 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/me/Navoei/customdiscsplugin/JukeboxStateManager.java: -------------------------------------------------------------------------------- 1 | package me.Navoei.customdiscsplugin; 2 | 3 | import org.bukkit.Location; 4 | import org.bukkit.block.Jukebox; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class JukeboxStateManager { 10 | 11 | static CustomDiscs plugin = CustomDiscs.getInstance(); 12 | static PlayerManager playerManager = PlayerManager.instance(); 13 | static List jukeboxLocations = new ArrayList<>(); 14 | 15 | public static void start(Jukebox jukebox) { 16 | if (jukeboxLocations.contains(jukebox.getLocation()) || !playerManager.isAudioPlayerPlaying(jukebox.getLocation())) return; 17 | jukeboxLocations.add(jukebox.getLocation()); 18 | plugin.getServer().getRegionScheduler().runAtFixedRate(plugin, jukebox.getLocation(), scheduledTask -> { 19 | if (playerManager.isAudioPlayerPlaying(jukebox.getLocation())) { 20 | if (!jukebox.isPlaying()) { 21 | jukebox.startPlaying(); 22 | } 23 | } else { 24 | jukebox.stopPlaying(); 25 | jukeboxLocations.remove(jukebox.getLocation()); 26 | scheduledTask.cancel(); 27 | } 28 | }, 1, 1); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/me/Navoei/customdiscsplugin/PlayerManager.java: -------------------------------------------------------------------------------- 1 | package me.Navoei.customdiscsplugin; 2 | 3 | import de.maxhenkel.voicechat.api.ServerPlayer; 4 | import de.maxhenkel.voicechat.api.VoicechatConnection; 5 | import de.maxhenkel.voicechat.api.VoicechatServerApi; 6 | import de.maxhenkel.voicechat.api.audiochannel.AudioChannel; 7 | import de.maxhenkel.voicechat.api.audiochannel.AudioPlayer; 8 | import de.maxhenkel.voicechat.api.audiochannel.LocationalAudioChannel; 9 | import javazoom.spi.mpeg.sampled.convert.MpegFormatConversionProvider; 10 | import javazoom.spi.mpeg.sampled.file.MpegAudioFileReader; 11 | import net.kyori.adventure.text.Component; 12 | import net.kyori.adventure.text.TextComponent; 13 | import net.kyori.adventure.text.format.NamedTextColor; 14 | import org.bukkit.Location; 15 | import org.bukkit.block.Block; 16 | import org.bukkit.entity.Player; 17 | import org.jflac.sound.spi.Flac2PcmAudioInputStream; 18 | import org.jflac.sound.spi.FlacAudioFileReader; 19 | 20 | import javax.annotation.Nullable; 21 | import javax.sound.sampled.*; 22 | import java.io.File; 23 | import java.io.IOException; 24 | import java.nio.file.Path; 25 | import java.util.Arrays; 26 | import java.util.Collection; 27 | import java.util.Map; 28 | import java.util.UUID; 29 | import java.util.concurrent.ConcurrentHashMap; 30 | import java.util.concurrent.ExecutorService; 31 | import java.util.concurrent.Executors; 32 | import java.util.concurrent.atomic.AtomicBoolean; 33 | import java.util.concurrent.atomic.AtomicReference; 34 | 35 | public class PlayerManager { 36 | 37 | CustomDiscs plugin = CustomDiscs.getInstance(); 38 | private final Map playerMap; 39 | private final ExecutorService executorService; 40 | private static final AudioFormat FORMAT = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, 48000F, 16, 1, 2, 48000F, false); 41 | 42 | public PlayerManager() { 43 | this.playerMap = new ConcurrentHashMap<>(); 44 | this.executorService = Executors.newSingleThreadExecutor(r -> { 45 | Thread thread = new Thread(r, "AudioPlayerThread"); 46 | thread.setDaemon(true); 47 | return thread; 48 | }); 49 | } 50 | 51 | public void playAudio(VoicechatServerApi api, Path soundFilePath, Block block, Component actionbarComponent, float range) { 52 | UUID id = UUID.nameUUIDFromBytes(block.getLocation().toString().getBytes()); 53 | 54 | LocationalAudioChannel audioChannel = api.createLocationalAudioChannel(id, api.fromServerLevel(block.getWorld()), api.createPosition(block.getLocation().getX() + 0.5d, block.getLocation().getY() + 0.5d, block.getLocation().getZ() + 0.5d)); 55 | 56 | if (audioChannel == null) return; 57 | 58 | audioChannel.setCategory(VoicePlugin.MUSIC_DISC_CATEGORY); 59 | audioChannel.setDistance(range); 60 | 61 | Collection playersInRange = api.getPlayersInRange(api.fromServerLevel(block.getWorld()), audioChannel.getLocation(), range); 62 | 63 | for (ServerPlayer serverPlayer : playersInRange) { 64 | Player bukkitPlayer = (Player) serverPlayer.getPlayer(); 65 | bukkitPlayer.sendActionBar(actionbarComponent); 66 | } 67 | 68 | AtomicBoolean stopped = new AtomicBoolean(); 69 | AtomicReference player = new AtomicReference<>(); 70 | PlayerReference playerReference = new PlayerReference(() -> { 71 | synchronized (stopped) { 72 | stopped.set(true); 73 | de.maxhenkel.voicechat.api.audiochannel.AudioPlayer audioPlayer = player.get(); 74 | if (audioPlayer != null) { 75 | audioPlayer.stopPlaying(); 76 | } 77 | } 78 | }, player, soundFilePath); 79 | 80 | playerMap.put(id, playerReference); 81 | 82 | executorService.execute(() -> { 83 | AudioPlayer audioPlayer = null; 84 | AudioInputStream inputStream = null; 85 | try { 86 | inputStream = getAudioInputStream(soundFilePath, FORMAT); 87 | audioPlayer = playChannel(api, audioChannel, block, inputStream, playersInRange); 88 | } catch (UnsupportedAudioFileException | IOException e) { 89 | throw new RuntimeException(e); 90 | } 91 | if (audioPlayer == null) { 92 | playerMap.remove(id); 93 | return; 94 | } 95 | AudioInputStream finalInputStream = inputStream; 96 | audioPlayer.setOnStopped(() -> { 97 | try { 98 | finalInputStream.close(); 99 | } catch (IOException e) { 100 | throw new RuntimeException(e); 101 | } 102 | //plugin.getServer().getRegionScheduler().run(plugin, block.getLocation(), scheduledTask -> HopperManager.instance().discToHopper(block)); 103 | if (playerMap.containsValue(playerReference)) { 104 | playerMap.remove(id); 105 | } 106 | }); 107 | synchronized (stopped) { 108 | if (!stopped.get()) { 109 | player.set(audioPlayer); 110 | } else { 111 | audioPlayer.stopPlaying(); 112 | } 113 | } 114 | }); 115 | 116 | } 117 | 118 | @Nullable 119 | private de.maxhenkel.voicechat.api.audiochannel.AudioPlayer playChannel(VoicechatServerApi api, AudioChannel audioChannel, Block block, AudioInputStream inputStream, Collection playersInRange) throws UnsupportedAudioFileException, IOException { 120 | //short[] audio = readSoundFile(soundFilePath); 121 | AudioPlayer audioPlayer = api.createAudioPlayer(audioChannel, api.createEncoder(), () -> { 122 | try { 123 | return readSoundFile(inputStream); 124 | } catch (Exception e) { 125 | plugin.getLogger().info("Error Occurred At: " + block.getLocation()); 126 | for (ServerPlayer serverPlayer : playersInRange) { 127 | Player bukkitPlayer = (Player) serverPlayer.getPlayer(); 128 | TextComponent textComponent = Component.text("An error has occurred while trying to play this disc.").color(NamedTextColor.RED); 129 | bukkitPlayer.sendMessage(textComponent); 130 | } 131 | e.printStackTrace(); 132 | return null; 133 | } 134 | }); 135 | audioPlayer.startPlaying(); 136 | return audioPlayer; 137 | } 138 | 139 | public AudioInputStream getAudioInputStream(Path file, AudioFormat audioFormat) throws UnsupportedAudioFileException, IOException { 140 | AudioInputStream finalInputStream = null; 141 | if (getFileExtension(file.toFile().toString()).equals("wav")) { 142 | AudioInputStream inputStream = AudioSystem.getAudioInputStream(file.toFile()); 143 | finalInputStream = AudioSystem.getAudioInputStream(audioFormat, inputStream); 144 | } else if (getFileExtension(file.toFile().toString()).equals("mp3")) { 145 | 146 | AudioInputStream inputStream = new MpegAudioFileReader().getAudioInputStream(file.toFile()); 147 | AudioFormat baseFormat = inputStream.getFormat(); 148 | AudioFormat decodedFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, baseFormat.getSampleRate(), 16, baseFormat.getChannels(), baseFormat.getChannels() * 2, baseFormat.getFrameRate(), false); 149 | AudioInputStream convertedInputStream = new MpegFormatConversionProvider().getAudioInputStream(decodedFormat, inputStream); 150 | finalInputStream = AudioSystem.getAudioInputStream(audioFormat, convertedInputStream); 151 | 152 | } else if (getFileExtension(file.toFile().toString()).equals("flac")) { 153 | AudioInputStream inputStream = new FlacAudioFileReader().getAudioInputStream(file.toFile()); 154 | AudioFormat baseFormat = inputStream.getFormat(); 155 | AudioFormat decodedFormat = new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, baseFormat.getSampleRate(), 16, baseFormat.getChannels(), baseFormat.getChannels() * 2, baseFormat.getFrameRate(), false); 156 | AudioInputStream convertedInputStream = new Flac2PcmAudioInputStream(inputStream, decodedFormat, inputStream.getFrameLength()); 157 | finalInputStream = AudioSystem.getAudioInputStream(audioFormat, convertedInputStream); 158 | } 159 | return finalInputStream; 160 | } 161 | 162 | private static short[] readSoundFile(AudioInputStream inputStream) throws IOException { 163 | byte[] audioPacket = getAudioPacket(inputStream); 164 | if (audioPacket == null) return null; 165 | return VoicePlugin.voicechatApi.getAudioConverter().bytesToShorts(audioPacket); 166 | } 167 | 168 | private static byte[] getAudioPacket(AudioInputStream inputStream) throws IOException { 169 | byte[] audioPacket = inputStreamToPackets(inputStream); 170 | return adjustVolume(audioPacket, CustomDiscs.getInstance().musicDiscVolume); 171 | } 172 | 173 | private static byte[] inputStreamToPackets(AudioInputStream inputStream) throws IOException { 174 | int FRAME_SIZE_BYTES = 1920; 175 | byte[] buffer = new byte[FRAME_SIZE_BYTES]; // Buffer to hold 960 bytes of audio data 176 | int bytesRead = inputStream.read(buffer); 177 | // If fewer than 960 bytes are read, pad with zeros 178 | if (bytesRead == -1) return null; 179 | if (bytesRead < FRAME_SIZE_BYTES) { 180 | for (int i = bytesRead; i < FRAME_SIZE_BYTES; i++) { 181 | buffer[i] = 0; // Pad with zero 182 | } 183 | } 184 | return buffer; 185 | } 186 | 187 | private static byte[] adjustVolume(byte[] audioSamples, double volume) { 188 | 189 | if (audioSamples == null) return null; 190 | 191 | if (volume > 1d || volume < 0d) { 192 | CustomDiscs.getInstance().getLogger().info("Error: The volume must be between 0 and 1 in the config!"); 193 | return null; 194 | } 195 | 196 | byte[] array = new byte[audioSamples.length]; 197 | for (int i = 0; i < array.length; i+=2) { 198 | // convert byte pair to int 199 | short buf1 = audioSamples[i+1]; 200 | short buf2 = audioSamples[i]; 201 | 202 | buf1 = (short) ((buf1 & 0xff) << 8); 203 | buf2 = (short) (buf2 & 0xff); 204 | 205 | short res= (short) (buf1 | buf2); 206 | res = (short) (res * volume); 207 | 208 | // convert back 209 | array[i] = (byte) res; 210 | array[i+1] = (byte) (res >> 8); 211 | 212 | } 213 | return array; 214 | } 215 | 216 | 217 | public void stopLocationalAudio(Location blockLocation) { 218 | UUID id = UUID.nameUUIDFromBytes(blockLocation.toString().getBytes()); 219 | PlayerReference player = playerMap.get(id); 220 | if (player != null) { 221 | player.onStop.stop(); 222 | } 223 | } 224 | 225 | //public static float getLengthSeconds(Path file) throws UnsupportedAudioFileException, IOException { 226 | // short[] audio = readSoundFile(file); 227 | // return (float) audio.length / FORMAT.getSampleRate(); 228 | //} 229 | 230 | public boolean isAudioPlayerPlaying(Location blockLocation) { 231 | UUID id = UUID.nameUUIDFromBytes(blockLocation.toString().getBytes()); 232 | return playerMap.containsKey(id); 233 | } 234 | 235 | private static String getFileExtension(String s) { 236 | int index = s.lastIndexOf("."); 237 | if (index > 0) { 238 | return s.substring(index + 1); 239 | } else { 240 | return ""; 241 | } 242 | } 243 | 244 | private static PlayerManager instance; 245 | 246 | public static PlayerManager instance() { 247 | if (instance == null) { 248 | instance = new PlayerManager(); 249 | } 250 | return instance; 251 | } 252 | 253 | private interface Stoppable { 254 | void stop(); 255 | } 256 | 257 | private record PlayerReference(Stoppable onStop, 258 | AtomicReference player, 259 | Path soundFilePath) { 260 | } 261 | 262 | } 263 | -------------------------------------------------------------------------------- /src/main/java/me/Navoei/customdiscsplugin/VoicePlugin.java: -------------------------------------------------------------------------------- 1 | package me.Navoei.customdiscsplugin; 2 | 3 | import de.maxhenkel.voicechat.api.VoicechatApi; 4 | import de.maxhenkel.voicechat.api.VoicechatPlugin; 5 | import de.maxhenkel.voicechat.api.VoicechatServerApi; 6 | import de.maxhenkel.voicechat.api.VolumeCategory; 7 | import de.maxhenkel.voicechat.api.events.EventRegistration; 8 | import de.maxhenkel.voicechat.api.events.VoicechatServerStartedEvent; 9 | 10 | import javax.annotation.Nullable; 11 | import javax.imageio.ImageIO; 12 | import java.awt.image.BufferedImage; 13 | import java.net.URL; 14 | import java.util.Enumeration; 15 | 16 | public class VoicePlugin implements VoicechatPlugin { 17 | 18 | public static String MUSIC_DISC_CATEGORY = "music_discs"; 19 | 20 | public static String GOAT_HORN_CATEGORY = "goat_horns"; 21 | 22 | public static String PLAYER_HEAD_CATEGORY = "player_heads"; 23 | 24 | public static VoicechatApi voicechatApi; 25 | @Nullable 26 | public static VoicechatServerApi voicechatServerApi; 27 | @Nullable 28 | public static VolumeCategory musicDiscs; 29 | @Nullable 30 | public static VolumeCategory goatHorns; 31 | @Nullable 32 | public static VolumeCategory playerHeads; 33 | 34 | /** 35 | * @return the unique ID for this voice chat plugin 36 | */ 37 | @Override 38 | public String getPluginId() { 39 | return null; 40 | } 41 | 42 | /** 43 | * Called when the voice chat initializes the plugin. 44 | * 45 | * @param api the voice chat API 46 | */ 47 | @Override 48 | public void initialize(VoicechatApi api) { 49 | voicechatApi = api; 50 | } 51 | 52 | /** 53 | * Called once by the voice chat to register all events. 54 | * 55 | * @param registration the event registration 56 | */ 57 | @Override 58 | public void registerEvents(EventRegistration registration) { 59 | registration.registerEvent(VoicechatServerStartedEvent.class, this::onServerStarted); 60 | } 61 | 62 | public void onServerStarted(VoicechatServerStartedEvent event) { 63 | voicechatServerApi = event.getVoicechat(); 64 | 65 | musicDiscs = voicechatServerApi.volumeCategoryBuilder() 66 | .setId(MUSIC_DISC_CATEGORY) 67 | .setName("Music Discs") 68 | .setDescription("The volume of music discs") 69 | .setIcon(getMusicDiscIcon()) 70 | .build(); 71 | voicechatServerApi.registerVolumeCategory(musicDiscs); 72 | 73 | } 74 | 75 | private int[][] getMusicDiscIcon() { 76 | try { 77 | Enumeration resources = CustomDiscs.getInstance().getClass().getClassLoader().getResources("music_disc_category.png"); 78 | 79 | while (resources.hasMoreElements()) { 80 | BufferedImage bufferedImage = ImageIO.read(resources.nextElement().openStream()); 81 | if (bufferedImage.getWidth() != 16) { 82 | continue; 83 | } 84 | if (bufferedImage.getHeight() != 16) { 85 | continue; 86 | } 87 | int[][] image = new int[16][16]; 88 | for (int x = 0; x < bufferedImage.getWidth(); x++) { 89 | for (int y = 0; y < bufferedImage.getHeight(); y++) { 90 | image[x][y] = bufferedImage.getRGB(x, y); 91 | } 92 | } 93 | return image; 94 | } 95 | 96 | } catch (Exception e) { 97 | e.printStackTrace(); 98 | } 99 | return null; 100 | } 101 | 102 | } -------------------------------------------------------------------------------- /src/main/java/me/Navoei/customdiscsplugin/command/CustomDiscCommand.java: -------------------------------------------------------------------------------- 1 | package me.Navoei.customdiscsplugin.command; 2 | 3 | import dev.jorel.commandapi.CommandAPICommand; 4 | import dev.jorel.commandapi.executors.CommandArguments; 5 | import me.Navoei.customdiscsplugin.CustomDiscs; 6 | import me.Navoei.customdiscsplugin.command.SubCommands.CreateSubCommand; 7 | import me.Navoei.customdiscsplugin.command.SubCommands.DownloadSubCommand; 8 | import me.Navoei.customdiscsplugin.command.SubCommands.SetRangeSubCommand; 9 | import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; 10 | import net.kyori.adventure.text.format.NamedTextColor; 11 | import org.bukkit.command.ConsoleCommandSender; 12 | import org.bukkit.configuration.file.FileConfiguration; 13 | import org.bukkit.entity.Player; 14 | 15 | public class CustomDiscCommand extends CommandAPICommand { 16 | private final CustomDiscs plugin; 17 | 18 | public CustomDiscCommand(CustomDiscs plugin) { 19 | super("customdisc"); 20 | this.plugin = plugin; 21 | 22 | this.withAliases("cd"); 23 | this.withFullDescription("The custom discs command."); 24 | 25 | this.withSubcommand(new CreateSubCommand(plugin)); 26 | this.withSubcommand(new DownloadSubCommand(plugin)); 27 | this.withSubcommand(new SetRangeSubCommand(plugin)); 28 | 29 | this.executesPlayer(this::onCommandPlayer); 30 | this.executesConsole(this::onCommandConsole); 31 | } 32 | 33 | private int onCommandPlayer(Player player, CommandArguments arguments) { 34 | FileConfiguration config = this.plugin.getConfig(); 35 | for (String message : config.getStringList("help")) { 36 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(message)); 37 | } 38 | 39 | return 1; 40 | } 41 | 42 | private int onCommandConsole(ConsoleCommandSender executor, CommandArguments arguments) { 43 | executor.sendMessage(NamedTextColor.RED + "Only players can use this command : '"+arguments+"'!"); 44 | return 1; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/me/Navoei/customdiscsplugin/command/SubCommands/CreateSubCommand.java: -------------------------------------------------------------------------------- 1 | package me.Navoei.customdiscsplugin.command.SubCommands; 2 | 3 | import dev.jorel.commandapi.CommandAPICommand; 4 | import dev.jorel.commandapi.arguments.ArgumentSuggestions; 5 | import dev.jorel.commandapi.arguments.StringArgument; 6 | import dev.jorel.commandapi.arguments.TextArgument; 7 | import dev.jorel.commandapi.executors.CommandArguments; 8 | import io.papermc.paper.datacomponent.DataComponentTypes; 9 | import io.papermc.paper.datacomponent.item.TooltipDisplay; 10 | import me.Navoei.customdiscsplugin.CustomDiscs; 11 | import me.Navoei.customdiscsplugin.language.Lang; 12 | import net.kyori.adventure.text.Component; 13 | import net.kyori.adventure.text.TextComponent; 14 | import net.kyori.adventure.text.format.NamedTextColor; 15 | import net.kyori.adventure.text.format.TextDecoration; 16 | import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; 17 | import org.bukkit.*; 18 | import org.bukkit.command.ConsoleCommandSender; 19 | import org.bukkit.entity.Player; 20 | import org.bukkit.inventory.ItemStack; 21 | import org.bukkit.inventory.meta.ItemMeta; 22 | import org.bukkit.persistence.PersistentDataContainer; 23 | import org.bukkit.persistence.PersistentDataType; 24 | 25 | import javax.annotation.Nullable; 26 | import java.io.File; 27 | import java.util.ArrayList; 28 | import java.util.Arrays; 29 | import java.util.List; 30 | import java.util.Objects; 31 | 32 | public class CreateSubCommand extends CommandAPICommand { 33 | private final CustomDiscs plugin; 34 | 35 | public CreateSubCommand(CustomDiscs plugin) { 36 | super("create"); 37 | this.plugin = plugin; 38 | 39 | this.withFullDescription(NamedTextColor.GRAY + "Creates a custom music disc."); 40 | this.withUsage("/customdisc create \"Custom Lore\""); 41 | 42 | this.withArguments(new StringArgument("filename").replaceSuggestions(ArgumentSuggestions.stringCollection((sender) -> { 43 | File musicDataFolder = new File(this.plugin.getDataFolder(), "musicdata"); 44 | if (!musicDataFolder.isDirectory()) { 45 | return List.of(); 46 | } 47 | 48 | File[] files = musicDataFolder.listFiles(); 49 | if (files == null) { 50 | return List.of(); 51 | } 52 | 53 | return Arrays.stream(files).filter(file -> !file.isDirectory()).map(File::getName).toList(); 54 | }))); 55 | 56 | this.withArguments(new TextArgument("song_name")); 57 | 58 | this.executesPlayer(this::onCommandPlayer); 59 | this.executesConsole(this::onCommandConsole); 60 | } 61 | 62 | private int onCommandPlayer(Player player, CommandArguments arguments) { 63 | 64 | ItemStack item = player.getInventory().getItemInMainHand(); 65 | 66 | if (!isMusicDisc(item)) { 67 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.NOT_HOLDING_DISC.toString())); 68 | return 1; 69 | } 70 | 71 | if (!player.hasPermission("customdiscs.create")) { 72 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.NO_PERMISSION.toString())); 73 | return 1; 74 | } 75 | 76 | // Find file, if file not there then say "file not there" 77 | String filename = Objects.requireNonNull(arguments.getByClass("filename", String.class)); 78 | if (filename.contains("../")) { 79 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.INVALID_FILENAME.toString())); 80 | return 1; 81 | } 82 | 83 | File getDirectory = new File(this.plugin.getDataFolder(), "musicdata"); 84 | File songFile = new File(getDirectory.getPath(), filename); 85 | if (songFile.exists()) { 86 | if (!getFileExtension(filename).equals("wav") && !getFileExtension(filename).equals("mp3") && !getFileExtension(filename).equals("flac")) { 87 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.INVALID_FORMAT.toString())); 88 | return 1; 89 | } 90 | } else { 91 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.FILE_NOT_FOUND.toString())); 92 | return 1; 93 | } 94 | 95 | String song_name = Objects.requireNonNull(arguments.getByClass("song_name", String.class)); 96 | 97 | ItemStack disc = new ItemStack(player.getInventory().getItemInMainHand()); 98 | disc.setData(DataComponentTypes.TOOLTIP_DISPLAY, TooltipDisplay.tooltipDisplay().addHiddenComponents(DataComponentTypes.JUKEBOX_PLAYABLE).build()); 99 | ItemMeta meta = disc.getItemMeta(); 100 | @Nullable List itemLore = new ArrayList<>(); 101 | final TextComponent customLoreSong = Component.text().decoration(TextDecoration.ITALIC, false).content(song_name).color(NamedTextColor.GRAY).build(); 102 | itemLore.add(customLoreSong); 103 | meta.lore(itemLore); 104 | 105 | PersistentDataContainer data = meta.getPersistentDataContainer(); 106 | data.set(new NamespacedKey(this.plugin, "customdisc"), PersistentDataType.STRING, filename); 107 | player.getInventory().getItemInMainHand().setItemMeta(meta); 108 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.CREATE_FILENAME.toString().replace("%filename%", filename))); 109 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.CREATE_CUSTOM_NAME.toString().replace("%custom_name%", song_name))); 110 | return 1; 111 | } 112 | 113 | private int onCommandConsole(ConsoleCommandSender executor, CommandArguments arguments) { 114 | executor.sendMessage(NamedTextColor.RED + "Only players can use this command : '"+arguments+"'!"); 115 | return 1; 116 | } 117 | 118 | private String getFileExtension(String s) { 119 | int index = s.lastIndexOf("."); 120 | if (index > 0) { 121 | return s.substring(index + 1); 122 | } else { 123 | return ""; 124 | } 125 | } 126 | 127 | public static boolean isMusicDisc(ItemStack item) { 128 | return item.getType().toString().contains("MUSIC_DISC"); 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/me/Navoei/customdiscsplugin/command/SubCommands/DownloadSubCommand.java: -------------------------------------------------------------------------------- 1 | package me.Navoei.customdiscsplugin.command.SubCommands; 2 | 3 | import dev.jorel.commandapi.CommandAPICommand; 4 | import dev.jorel.commandapi.arguments.StringArgument; 5 | import dev.jorel.commandapi.arguments.TextArgument; 6 | import dev.jorel.commandapi.executors.CommandArguments; 7 | import me.Navoei.customdiscsplugin.CustomDiscs; 8 | import me.Navoei.customdiscsplugin.language.Lang; 9 | import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; 10 | import net.kyori.adventure.text.format.NamedTextColor; 11 | import org.bukkit.Bukkit; 12 | import org.bukkit.command.ConsoleCommandSender; 13 | import org.bukkit.entity.Player; 14 | import org.codehaus.plexus.util.FileUtils; 15 | 16 | import java.io.File; 17 | import java.io.IOException; 18 | import java.net.URL; 19 | import java.net.URLConnection; 20 | import java.nio.file.Path; 21 | import java.util.Objects; 22 | 23 | public class DownloadSubCommand extends CommandAPICommand { 24 | private final CustomDiscs plugin; 25 | 26 | public DownloadSubCommand(CustomDiscs plugin) { 27 | super("download"); 28 | this.plugin = plugin; 29 | 30 | this.withFullDescription(NamedTextColor.GRAY + "Downloads a file from a given URL."); 31 | this.withUsage("/customdisc download "); 32 | 33 | this.withArguments(new TextArgument("url")); 34 | this.withArguments(new StringArgument("filename")); 35 | 36 | this.executesPlayer(this::onCommandPlayer); 37 | this.executesConsole(this::onCommandConsole); 38 | } 39 | 40 | private int onCommandPlayer(Player player, CommandArguments arguments) { 41 | if (!player.hasPermission("customdiscs.download")) { 42 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.NO_PERMISSION.toString())); 43 | return 1; 44 | } 45 | 46 | Bukkit.getScheduler().runTaskAsynchronously(this.plugin, () -> { 47 | try { 48 | URL fileURL = new URL(Objects.requireNonNull(arguments.getByClass("url", String.class))); 49 | String filename = Objects.requireNonNull(arguments.getByClass("filename", String.class)); 50 | if (filename.contains("../")) { 51 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.INVALID_FILENAME.toString())); 52 | return; 53 | } 54 | 55 | System.out.println(filename); 56 | 57 | if (!getFileExtension(filename).equals("wav") && !getFileExtension(filename).equals("mp3") && !getFileExtension(filename).equals("flac")) { 58 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.INVALID_FORMAT.toString())); 59 | return; 60 | } 61 | 62 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.DOWNLOADING_FILE.toString())); 63 | 64 | URLConnection connection = fileURL.openConnection(); 65 | if (connection != null) { 66 | long size = connection.getContentLengthLong() / 1048576; 67 | if (size > this.plugin.getConfig().getInt("max-download-size", 50)) { 68 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.FILE_TOO_LARGE.toString().replace("%max_download_size%", String.valueOf(this.plugin.getConfig().getInt("max-download-size", 50))))); 69 | return; 70 | } 71 | } 72 | 73 | Path downloadPath = Path.of(this.plugin.getDataFolder().getPath(), "musicdata", filename); 74 | File downloadFile = new File(downloadPath.toUri()); 75 | FileUtils.copyURLToFile(fileURL, downloadFile); 76 | 77 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.SUCCESSFUL_DOWNLOAD.toString().replace("%file_path%", "plugins/CustomDiscs/musicdata/" + filename))); 78 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.CREATE_DISC.toString().replace("%filename%", filename))); 79 | } catch (IOException e) { 80 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.DOWNLOAD_ERROR.toString())); 81 | e.printStackTrace(); 82 | } 83 | }); 84 | 85 | return 1; 86 | } 87 | 88 | private int onCommandConsole(ConsoleCommandSender executor, CommandArguments arguments) { 89 | executor.sendMessage(NamedTextColor.RED + "Only players can use this command : '"+arguments+"'!"); 90 | return 1; 91 | } 92 | 93 | private String getFileExtension(String s) { 94 | int index = s.lastIndexOf("."); 95 | if (index > 0) { 96 | return s.substring(index + 1); 97 | } else { 98 | return ""; 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/me/Navoei/customdiscsplugin/command/SubCommands/SetRangeSubCommand.java: -------------------------------------------------------------------------------- 1 | package me.Navoei.customdiscsplugin.command.SubCommands; 2 | 3 | import dev.jorel.commandapi.CommandAPICommand; 4 | import dev.jorel.commandapi.arguments.FloatArgument; 5 | import dev.jorel.commandapi.executors.CommandArguments; 6 | import me.Navoei.customdiscsplugin.CustomDiscs; 7 | import me.Navoei.customdiscsplugin.language.Lang; 8 | import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; 9 | import net.kyori.adventure.text.format.NamedTextColor; 10 | //import org.bukkit.Bukkit; 11 | import org.bukkit.command.ConsoleCommandSender; 12 | import org.bukkit.entity.Player; 13 | //import org.bukkit.Material; 14 | import org.bukkit.NamespacedKey; 15 | //import org.bukkit.inventory.ItemFlag; 16 | import org.bukkit.event.player.PlayerInteractEvent; 17 | import org.bukkit.inventory.ItemStack; 18 | import org.bukkit.inventory.meta.ItemMeta; 19 | import org.bukkit.persistence.PersistentDataContainer; 20 | import org.bukkit.persistence.PersistentDataType; 21 | 22 | import java.util.Objects; 23 | import org.bukkit.Bukkit; 24 | 25 | public class SetRangeSubCommand extends CommandAPICommand { 26 | private final CustomDiscs plugin; 27 | 28 | public SetRangeSubCommand(CustomDiscs plugin) { 29 | super("range"); 30 | this.plugin = plugin; 31 | 32 | this.withFullDescription(NamedTextColor.GRAY + "Set the range of a disc to the defined value (range from 1 to "+ this.plugin.musicDiscMaxDistance +")."); 33 | this.withUsage("/cd range "); 34 | 35 | this.withArguments(new FloatArgument("range")); 36 | 37 | this.executesPlayer(this::onCommandPlayer); 38 | this.executesConsole(this::onCommandConsole); 39 | } 40 | 41 | private int onCommandPlayer(Player player, CommandArguments arguments) { 42 | 43 | ItemStack item = player.getInventory().getItemInMainHand(); 44 | 45 | if (!isCustomDisc(item)) { 46 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.NOT_HOLDING_DISC.toString())); 47 | return 1; 48 | } 49 | 50 | if (!player.hasPermission("customdiscs.range")) { 51 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.NO_PERMISSION.toString())); 52 | return 1; 53 | } 54 | 55 | Float range = Objects.requireNonNull(arguments.getByClass("range", Float.class)); 56 | 57 | if ( range < 1 || range > this.plugin.musicDiscMaxDistance) { 58 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.INVALID_RANGE.toString().replace("%range_value%", Float.toString(this.plugin.musicDiscMaxDistance)))); 59 | return 1; 60 | } 61 | 62 | ItemMeta meta = item.getItemMeta(); 63 | PersistentDataContainer data = meta.getPersistentDataContainer(); 64 | data.set(new NamespacedKey(this.plugin, "range"), PersistentDataType.FLOAT, range); 65 | player.getInventory().getItemInMainHand().setItemMeta(meta); 66 | player.sendMessage(LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.CREATE_CUSTOM_RANGE.toString().replace("%custom_range%", Float.toString(range)))); 67 | 68 | return 1; 69 | } 70 | 71 | private int onCommandConsole(ConsoleCommandSender executor, CommandArguments arguments) { 72 | executor.sendMessage(NamedTextColor.RED + "Only players can use this command : '"+arguments+"'!"); 73 | return 1; 74 | } 75 | 76 | public boolean isCustomDisc(ItemStack item) { 77 | if (item==null) return false; 78 | return item.getType().toString().contains("MUSIC_DISC") && item.getItemMeta().getPersistentDataContainer().has(new NamespacedKey(plugin, "customdisc")); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/me/Navoei/customdiscsplugin/event/JukeBox.java: -------------------------------------------------------------------------------- 1 | package me.Navoei.customdiscsplugin.event; 2 | 3 | import io.papermc.paper.datacomponent.DataComponentTypes; 4 | import io.papermc.paper.datacomponent.item.TooltipDisplay; 5 | import me.Navoei.customdiscsplugin.CustomDiscs; 6 | import me.Navoei.customdiscsplugin.PlayerManager; 7 | import me.Navoei.customdiscsplugin.VoicePlugin; 8 | import me.Navoei.customdiscsplugin.language.Lang; 9 | import net.kyori.adventure.text.Component; 10 | import net.kyori.adventure.text.format.NamedTextColor; 11 | import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; 12 | import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer; 13 | import org.bukkit.Material; 14 | import org.bukkit.NamespacedKey; 15 | import org.bukkit.block.Block; 16 | import org.bukkit.block.Jukebox; 17 | import org.bukkit.entity.Player; 18 | import org.bukkit.event.EventHandler; 19 | import org.bukkit.event.EventPriority; 20 | import org.bukkit.event.Listener; 21 | import org.bukkit.event.block.Action; 22 | import org.bukkit.event.block.BlockBreakEvent; 23 | import org.bukkit.event.entity.EntityExplodeEvent; 24 | import org.bukkit.event.player.PlayerInteractEvent; 25 | import org.bukkit.inventory.ItemStack; 26 | import org.bukkit.inventory.meta.ItemMeta; 27 | import org.bukkit.inventory.meta.components.JukeboxPlayableComponent; 28 | import org.bukkit.persistence.PersistentDataContainer; 29 | import org.bukkit.persistence.PersistentDataType; 30 | 31 | import java.io.FileNotFoundException; 32 | import java.io.IOException; 33 | import java.nio.file.Path; 34 | import java.util.Objects; 35 | import java.util.Optional; 36 | 37 | public class JukeBox implements Listener{ 38 | 39 | CustomDiscs customDiscs = CustomDiscs.getInstance(); 40 | PlayerManager playerManager = PlayerManager.instance(); 41 | 42 | @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) 43 | public void onInsert(PlayerInteractEvent event) throws IOException { 44 | 45 | Player player = event.getPlayer(); 46 | Block block = event.getClickedBlock(); 47 | 48 | if (event.getAction() != Action.RIGHT_CLICK_BLOCK || event.getClickedBlock() == null || event.getItem() == null || event.getItem().getItemMeta() == null || block == null) return; 49 | if (event.getClickedBlock().getType() != Material.JUKEBOX) return; 50 | if (player.isSneaking()) return; 51 | 52 | if (isCustomMusicDisc(event.getItem()) && !jukeboxContainsDisc(block)) { 53 | 54 | ItemMeta discMeta = event.getItem().getItemMeta(); 55 | String soundFileName = discMeta.getPersistentDataContainer().get(new NamespacedKey(customDiscs, "customdisc"), PersistentDataType.STRING); 56 | 57 | PersistentDataContainer persistentDataContainer = discMeta.getPersistentDataContainer(); 58 | float range = CustomDiscs.getInstance().musicDiscDistance; 59 | NamespacedKey customSoundRangeKey = new NamespacedKey(customDiscs, "range"); 60 | 61 | if(persistentDataContainer.has(customSoundRangeKey, PersistentDataType.FLOAT)) { 62 | float soundRange = Optional.ofNullable(persistentDataContainer.get(customSoundRangeKey, PersistentDataType.FLOAT)).orElse(0f); 63 | range = Math.min(soundRange, CustomDiscs.getInstance().musicDiscMaxDistance); 64 | } 65 | 66 | if (!event.getItem().hasData(DataComponentTypes.TOOLTIP_DISPLAY) || !event.getItem().getData(DataComponentTypes.TOOLTIP_DISPLAY).hiddenComponents().contains(DataComponentTypes.JUKEBOX_PLAYABLE)) { 67 | event.getItem().setData(DataComponentTypes.TOOLTIP_DISPLAY, TooltipDisplay.tooltipDisplay().addHiddenComponents(DataComponentTypes.JUKEBOX_PLAYABLE).build()); 68 | Component textComponent = LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.PREFIX + Lang.DISC_CONVERTED.toString()); 69 | player.sendMessage(textComponent); 70 | } 71 | 72 | Path soundFilePath = Path.of(customDiscs.getDataFolder().getPath(), "musicdata", soundFileName); 73 | 74 | if (soundFilePath.toFile().exists()) { 75 | Component songNameComponent = Objects.requireNonNull(discMeta.lore()).get(0).asComponent(); 76 | String songName = PlainTextComponentSerializer.plainText().serialize(songNameComponent); 77 | Component customActionBarSongPlaying = LegacyComponentSerializer.legacyAmpersand().deserialize(Lang.NOW_PLAYING.toString().replace("%song_name%", songName)); 78 | 79 | assert VoicePlugin.voicechatServerApi != null; 80 | playerManager.playAudio(VoicePlugin.voicechatServerApi, soundFilePath, block, customActionBarSongPlaying, range); 81 | } else { 82 | player.sendMessage(NamedTextColor.RED + "Sound file not found."); 83 | event.setCancelled(true); 84 | throw new FileNotFoundException("Sound file is missing!"); 85 | } 86 | } 87 | } 88 | 89 | @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) 90 | public void onEject(PlayerInteractEvent event) { 91 | 92 | Player player = event.getPlayer(); 93 | Block block = event.getClickedBlock(); 94 | 95 | if (event.getAction() != Action.RIGHT_CLICK_BLOCK || event.getClickedBlock() == null || block == null) return; 96 | if (event.getClickedBlock().getType() != Material.JUKEBOX) return; 97 | 98 | if (jukeboxContainsDisc(block)) { 99 | ItemStack itemInvolvedInEvent; 100 | if (event.getMaterial().equals(Material.AIR)) { 101 | 102 | if (!player.getInventory().getItemInMainHand().getType().equals(Material.AIR)) { 103 | itemInvolvedInEvent = player.getInventory().getItemInMainHand(); 104 | } else if (!player.getInventory().getItemInOffHand().getType().equals(Material.AIR)) { 105 | itemInvolvedInEvent = player.getInventory().getItemInOffHand(); 106 | } else { 107 | itemInvolvedInEvent = new ItemStack(Material.AIR); 108 | } 109 | 110 | } else { 111 | itemInvolvedInEvent = new ItemStack(event.getMaterial()); 112 | } 113 | 114 | if (player.isSneaking() && !itemInvolvedInEvent.getType().equals(Material.AIR)) return; 115 | stopDisc(block); 116 | } 117 | } 118 | 119 | @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) 120 | public void onJukeboxBreak(BlockBreakEvent event) { 121 | 122 | Block block = event.getBlock(); 123 | 124 | if (block.getType() != Material.JUKEBOX) return; 125 | Jukebox jukebox = (Jukebox) block.getState(); 126 | if (!isCustomMusicDisc(jukebox.getRecord())) return; 127 | 128 | stopDisc(block); 129 | } 130 | 131 | @EventHandler(priority = EventPriority.HIGHEST, ignoreCancelled = true) 132 | public void onJukeboxExplode(EntityExplodeEvent event) { 133 | 134 | for (Block explodedBlock : event.blockList()) { 135 | if (explodedBlock.getType() == Material.JUKEBOX) { 136 | Jukebox jukebox = (Jukebox) explodedBlock.getState(); 137 | if (!isCustomMusicDisc(jukebox.getRecord())) return; 138 | stopDisc(explodedBlock); 139 | } 140 | } 141 | 142 | } 143 | 144 | public boolean jukeboxContainsDisc(Block b) { 145 | Jukebox jukebox = (Jukebox) b.getLocation().getBlock().getState(); 146 | return jukebox.getRecord().getType() != Material.AIR; 147 | } 148 | 149 | public boolean isCustomMusicDisc(ItemStack itemStack) { 150 | if (itemStack == null) return false; 151 | if (itemStack.getItemMeta() == null) return false; 152 | return itemStack.getItemMeta().getPersistentDataContainer().has(new NamespacedKey(customDiscs, "customdisc")); 153 | } 154 | 155 | private void stopDisc(Block block) { 156 | playerManager.stopLocationalAudio(block.getLocation()); 157 | } 158 | 159 | } -------------------------------------------------------------------------------- /src/main/java/me/Navoei/customdiscsplugin/language/Lang.java: -------------------------------------------------------------------------------- 1 | package me.Navoei.customdiscsplugin.language; 2 | 3 | import org.bukkit.configuration.file.YamlConfiguration; 4 | 5 | public enum Lang { 6 | PREFIX("prefix", "&8[&6CustomDiscs&8]&r"), 7 | NO_PERMISSION("no-permission", "&rYou do not have permission to execute this command."), 8 | INVALID_FILENAME("invalid-filename", "&rThis is an invalid filename!"), 9 | INVALID_FORMAT("invalid-format", "&rFile must be in wav, flac, or mp3 format!"), 10 | FILE_NOT_FOUND("file-not-found", "&rFile not found!"), 11 | INVALID_ARGUMENTS("invalid-arguments", "&rInsufficient arguments. &7(&a%command_syntax%&7)"), 12 | NOT_HOLDING_DISC("not-holding-disc", "&rYou must hold a disc in your main hand."), 13 | CREATE_FILENAME("create-filename", "&7Your filename is: &a\"%filename%\"."), 14 | CREATE_CUSTOM_NAME("create-custom-name", "&7Your custom name is: &a\"%custom_name%\"."), 15 | DOWNLOADING_FILE("downloading-file", "&7Downloading file..."), 16 | FILE_TOO_LARGE("file-too-large", "&rThe file is larger than %max_download_size%MB."), 17 | SUCCESSFUL_DOWNLOAD("successful-download", "&aFile successfully downloaded to &7%file_path%&a."), 18 | CREATE_DISC("create-disc", "&aCreate a disc by doing &7/cd create filename.extension \"Custom Lore\"&a."), 19 | DOWNLOAD_ERROR("download-error", "&rAn error has occurred while downloading."), 20 | NOW_PLAYING("now-playing","&6Now playing: %song_name%"), 21 | DISC_CONVERTED("disc-converted", "&aConverted disc to new format! &fThis is due to changes in newer Minecraft versions which introduced &7JukeboxPlayableComponent&f."), 22 | INVALID_RANGE("invalid-range","&rYou need to chose a range between 1 and %range_value%"), 23 | CREATE_CUSTOM_RANGE("create-custom-range", "&7Your range is set to: &a\"%custom_range%\"."); 24 | 25 | private final String path; 26 | private final String def; 27 | private static YamlConfiguration LANG; 28 | 29 | /** 30 | * Lang enum constructor. 31 | * @param path The string path. 32 | * @param start The default string. 33 | */ 34 | Lang(String path, String start) { 35 | this.path = path; 36 | this.def = start; 37 | } 38 | 39 | /** 40 | * Set the {@code YamlConfiguration} to use. 41 | * @param config The config to set. 42 | */ 43 | public static void setFile(YamlConfiguration config) { 44 | LANG = config; 45 | } 46 | 47 | @Override 48 | public String toString() { 49 | if (this == PREFIX) 50 | return LANG.getString(this.path, def) + " "; 51 | return LANG.getString(this.path, def); 52 | } 53 | 54 | /** 55 | * Get the default value of the path. 56 | * @return The default value of the path. 57 | */ 58 | public String getDefault() { 59 | return this.def; 60 | } 61 | 62 | /** 63 | * Get the path to the string. 64 | * @return The path to the string. 65 | */ 66 | public String getPath() { 67 | return this.path; 68 | } 69 | 70 | //Component textComponent = LegacyComponentSerializer.legacyAmpersand().deserialize(PlaceholderAPI.setPlaceholders(player, Lang.PREFIX + Lang.COMBAT.toString())); 71 | //player.sendMessage(textComponent); 72 | } 73 | -------------------------------------------------------------------------------- /src/main/resources/config.yml: -------------------------------------------------------------------------------- 1 | # [Music Disc Config] 2 | 3 | # The distance from which music discs can be heard in blocks. 4 | music-disc-distance: 16 5 | 6 | # The max distance from which music discs can be heard in blocks. 7 | music-disc-max-distance: 256 8 | 9 | # The master volume of music discs from 0-1. (You can set values like 0.5 for 50% volume). 10 | music-disc-volume: 1 11 | 12 | # The maximum download size in megabytes. 13 | max-download-size: 50 14 | 15 | #Custom Discs Help Page 16 | help: 17 | - "&8-[&6CustomDiscs Help Page&8]-" 18 | - "&aAuthor&7: &6Navoei" 19 | - "&aContributors&7: &6alfw / &6Athar42" 20 | - "&fGit&0Hub&7: &9&ohttps://github.com/Navoei/CustomDiscs" -------------------------------------------------------------------------------- /src/main/resources/lang.yml: -------------------------------------------------------------------------------- 1 | prefix: "&8[&6CustomDiscs&8]&r" 2 | no-permission: "&cYou do not have permission to execute this command." 3 | invalid-filename: "&cThis is an invalid filename!" 4 | no-disc-name-provided: "&cYou must provide a name for your disc." 5 | invalid-format: "&cFile must be in wav, flac, or mp3 format!" 6 | file-not-found: "&cFile not found!" 7 | invalid-arguments: "&cInvalid arguments. &7(&a%command_syntax%&7)" 8 | not-holding-disc: "&cYou must hold a disc in your main hand." 9 | create-filename: "&7Your filename is: &a\"%filename%\"." 10 | create-custom-name: "&7Your custom name is: &a\"%custom_name%\"." 11 | downloading-file: "&7Downloading file..." 12 | file-too-large: "&cThe file is larger than %max_download_size%MB." 13 | successful-download: "&aFile successfully downloaded to &7%file_path%&a." 14 | create-disc: "&aCreate a disc by doing &7/cd create %filename% \"Custom Lore\"&a." 15 | download-error: "&cAn error has occurred while downloading." 16 | now-playing: "&6Now playing: %song_name%" 17 | disc-converted: "&aConverted disc to new format! &fThis is due to changes in newer Minecraft versions which introduced &7ToolTipDisplay&f." 18 | invalid-range: "&cYou need to chose a range between 1 and %range_value%" 19 | create-custom-range: "&7Your range is set to: &a\"%custom_range%\"." -------------------------------------------------------------------------------- /src/main/resources/music_disc_category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Navoei/CustomDiscs/85148b489ada5fb27545e99a3ffcc72a27f57a81/src/main/resources/music_disc_category.png -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: CustomDiscs 2 | version: '${plugin_version}' 3 | main: me.Navoei.customdiscsplugin.CustomDiscs 4 | api-version: '${bukkit_api_version}' 5 | prefix: CustomDiscs 6 | authors: [ "Navoei", "Athar42", "alfw" ] 7 | description: A plugin which uses the Simple Voice Chat API to add custom music discs. 8 | depend: [ "voicechat", "ProtocolLib" ] 9 | dependencies: 10 | server: 11 | voicechat: 12 | load: BEFORE 13 | required: true 14 | join-classpath: true 15 | ProtocolLib: 16 | load: BEFORE 17 | required: true 18 | join-classpath: true --------------------------------------------------------------------------------