├── .gitignore ├── .jitpack.yml ├── LICENSE.md ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── public └── blockify.png ├── settings.gradle └── src └── main ├── java └── codes │ └── kooper │ └── blockify │ ├── Blockify.java │ ├── bstats │ └── Metrics.java │ ├── events │ ├── BlockifyBreakEvent.java │ ├── BlockifyInteractEvent.java │ ├── BlockifyPlaceEvent.java │ ├── CreateStageEvent.java │ ├── DeleteStageEvent.java │ ├── OnBlockChangeSendEvent.java │ ├── PlayerEnterStageEvent.java │ ├── PlayerExitStageEvent.java │ ├── PlayerJoinStageEvent.java │ └── PlayerLeaveStageEvent.java │ ├── listeners │ └── StageBoundListener.java │ ├── managers │ ├── BlockChangeManager.java │ └── StageManager.java │ ├── models │ ├── Audience.java │ ├── Pattern.java │ ├── Stage.java │ └── View.java │ ├── protocol │ ├── BlockDigAdapter.java │ ├── BlockPlaceAdapter.java │ └── ChunkLoadAdapter.java │ ├── types │ ├── BlockifyBlockStage.java │ ├── BlockifyChunk.java │ └── BlockifyPosition.java │ └── utils │ ├── BlockUtils.java │ └── PositionKeyUtil.java └── resources └── plugin.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | .idea 3 | build/ 4 | .gradle 5 | -------------------------------------------------------------------------------- /.jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk17 -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024, Kooper Codes 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | [![](https://img.shields.io/github/license/kooperlol/blockify.svg)](https://github.com/Kooperlol/Blockify/blob/master/LICENSE.md) [![](https://jitpack.io/v/Kooperlol/Blockify.svg)](https://jitpack.io/#Kooperlol/Blockify) [![](https://img.shields.io/badge/Discord-7289DA?style=for-the-badge&logo=discord&logoColor=white)](https://discord.gg/HeH2CuFCjz) 6 | 7 | # About 8 | Ever wondered how servers like FadeCloud or AkumaMC do private farms and mines? 9 | Well, let Blockify take care of it for you! Blockify is a public library that can manage and create client-sided blocks. 10 | 11 | Check out the [Wiki](https://github.com/Kooperlol/Blockify/wiki) to get started, and join the [Discord](https://discord.gg/BKrSKqaAZp) for help. 12 | 13 | ## Features 14 | 1. **Stage Management**: Blockify has different stages for an audience. Each stage has multiple "views", which represent different patterns within a stage. 15 | 2. **Block Interaction Events**: The project handles block interaction events, such as starting to dig a block, as seen in the `BlockDigAdapter` class. 16 | 3. **Block Breaking Events**: Blockify also handles block-breaking events, including checking if a block is breakable and sending block change updates to the player. 17 | 4. **Chunk Loading**: The `ChunkLoadAdapter` class handles chunk-loading events, including sending block changes to the player. 18 | 5. **Game Mode Checks**: The project checks the player's game mode and adjusts block-breaking speed accordingly. 19 | 6. **Memory Management**: Blockify manages memory efficiently by using custom data types like `BlockifyPosition` and `BlockifyChunk`. 20 | 7. **Skript Support**: Blockify has a Skript Addon, [SkBlockify](https://github.com/Kooperlol/SkBlockify), that allows you to use Skript to manage client-sided blocks. 21 | 8. **Custom Events:** Blockify has a custom event `BlockifyBlockBreakEvent` that is called when a block is broken. 22 | 9. **Complex Block Patterns:** Blockify can handle complex block patterns using the `BlockifyPattern` class. In addition, it can handle setting crop ages and other custom block data. 23 | 10. **Custom Mining Speeds:** Blockify allows for custom mining speeds which you can set for a player in an audience. 24 | 25 | ## Credits 26 | - **[Kooper](https://github.com/Kooperlol)**: Project Lead 27 | - **[Zora](https://github.com/ReportCardsMC)**: Mentor 28 | 29 | ## Inspiration 30 | - **[GhostCore](https://github.com/QuarryMC/GhostCore)**: The project was inspired by GhostCore, code that manages client-sided blocks for Quarry (an OP Prison server). 31 | 32 | ## Dependencies 33 | - [PacketEvents](https://github.com/retrooper/packetevents) 34 | 35 | ## Statistics 36 | Check out our [bStats page](https://bstats.org/plugin/bukkit/Blockify/21782) 37 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'maven-publish' 4 | } 5 | 6 | group = 'codes.kooper' 7 | version = '1.1.8-beta' 8 | 9 | repositories { 10 | mavenCentral() 11 | maven { 12 | name = "papermc-repo" 13 | url = "https://repo.papermc.io/repository/maven-public/" 14 | } 15 | maven { 16 | name = "sonatype" 17 | url = "https://oss.sonatype.org/content/groups/public/" 18 | } 19 | maven { 20 | name = "codemc" 21 | url "https://repo.codemc.io/repository/maven-releases/" 22 | } 23 | maven { 24 | name = "jitpack" 25 | url = 'https://jitpack.io' 26 | } 27 | } 28 | 29 | dependencies { 30 | implementation 'org.projectlombok:lombok:1.18.28' 31 | compileOnly "io.papermc.paper:paper-api:1.20.4-R0.1-SNAPSHOT" 32 | compileOnly 'com.github.retrooper:packetevents-spigot:2.4.0' 33 | annotationProcessor 'org.projectlombok:lombok:1.18.28' 34 | } 35 | 36 | def targetJavaVersion = 17 37 | java { 38 | toolchain { 39 | languageVersion = JavaLanguageVersion.of(targetJavaVersion) 40 | } 41 | } 42 | 43 | tasks.withType(JavaCompile).configureEach { 44 | options.annotationProcessorPath = configurations.compileClasspath 45 | if (targetJavaVersion >= 10 || JavaVersion.current().isJava10Compatible()) { 46 | options.release.set(targetJavaVersion) 47 | } 48 | } 49 | 50 | tasks.withType(JavaCompile).configureEach { 51 | options.deprecation = true 52 | options.compilerArgs += '-Xlint:deprecation' 53 | } 54 | 55 | publishing { 56 | publications { 57 | maven(MavenPublication) { 58 | from components.java 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kooperlol/Blockify/7bf8b6b18c9c9345bceeae6614491395073844c2/gradle.properties -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kooperlol/Blockify/7bf8b6b18c9c9345bceeae6614491395073844c2/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original 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 POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Use the maximum available, or set MAX_FD != -1 to use that value. 89 | MAX_FD=maximum 90 | 91 | warn () { 92 | echo "$*" 93 | } >&2 94 | 95 | die () { 96 | echo 97 | echo "$*" 98 | echo 99 | exit 1 100 | } >&2 101 | 102 | # OS specific support (must be 'true' or 'false'). 103 | cygwin=false 104 | msys=false 105 | darwin=false 106 | nonstop=false 107 | case "$( uname )" in #( 108 | CYGWIN* ) cygwin=true ;; #( 109 | Darwin* ) darwin=true ;; #( 110 | MSYS* | MINGW* ) msys=true ;; #( 111 | NONSTOP* ) nonstop=true ;; 112 | esac 113 | 114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 115 | 116 | 117 | # Determine the Java command to use to start the JVM. 118 | if [ -n "$JAVA_HOME" ] ; then 119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 120 | # IBM's JDK on AIX uses strange locations for the executables 121 | JAVACMD=$JAVA_HOME/jre/sh/java 122 | else 123 | JAVACMD=$JAVA_HOME/bin/java 124 | fi 125 | if [ ! -x "$JAVACMD" ] ; then 126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 127 | 128 | Please set the JAVA_HOME variable in your environment to match the 129 | location of your Java installation." 130 | fi 131 | else 132 | JAVACMD=java 133 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 134 | 135 | Please set the JAVA_HOME variable in your environment to match the 136 | location of your Java installation." 137 | fi 138 | 139 | # Increase the maximum file descriptors if we can. 140 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 141 | case $MAX_FD in #( 142 | max*) 143 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 144 | # shellcheck disable=SC3045 145 | MAX_FD=$( ulimit -H -n ) || 146 | warn "Could not query maximum file descriptor limit" 147 | esac 148 | case $MAX_FD in #( 149 | '' | soft) :;; #( 150 | *) 151 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 152 | # shellcheck disable=SC3045 153 | ulimit -n "$MAX_FD" || 154 | warn "Could not set maximum file descriptor limit to $MAX_FD" 155 | esac 156 | fi 157 | 158 | # Collect all arguments for the java command, stacking in reverse order: 159 | # * args from the command line 160 | # * the main class name 161 | # * -classpath 162 | # * -D...appname settings 163 | # * --module-path (only if needed) 164 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 165 | 166 | # For Cygwin or MSYS, switch paths to Windows format before running java 167 | if "$cygwin" || "$msys" ; then 168 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 169 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 170 | 171 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 172 | 173 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 174 | for arg do 175 | if 176 | case $arg in #( 177 | -*) false ;; # don't mess with options #( 178 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 179 | [ -e "$t" ] ;; #( 180 | *) false ;; 181 | esac 182 | then 183 | arg=$( cygpath --path --ignore --mixed "$arg" ) 184 | fi 185 | # Roll the args list around exactly as many times as the number of 186 | # args, so each arg winds up back in the position where it started, but 187 | # possibly modified. 188 | # 189 | # NB: a `for` loop captures its iteration list before it begins, so 190 | # changing the positional parameters here affects neither the number of 191 | # iterations, nor the values presented in `arg`. 192 | shift # remove old arg 193 | set -- "$@" "$arg" # push replacement arg 194 | done 195 | fi 196 | 197 | 198 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 199 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 200 | 201 | # Collect all arguments for the java command; 202 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 203 | # shell script including quotes and variable substitutions, so put them in 204 | # double quotes to make sure that they get re-expanded; and 205 | # * put everything else in single quotes, so that it's not re-expanded. 206 | 207 | set -- \ 208 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 209 | -classpath "$CLASSPATH" \ 210 | org.gradle.wrapper.GradleWrapperMain \ 211 | "$@" 212 | 213 | # Stop when "xargs" is not available. 214 | if ! command -v xargs >/dev/null 2>&1 215 | then 216 | die "xargs is not available" 217 | fi 218 | 219 | # Use "xargs" to parse quoted args. 220 | # 221 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 222 | # 223 | # In Bash we could simply go: 224 | # 225 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 226 | # set -- "${ARGS[@]}" "$@" 227 | # 228 | # but POSIX shell has neither arrays nor command substitution, so instead we 229 | # post-process each arg (as a line of input to sed) to backslash-escape any 230 | # character that might be a shell metacharacter, then use eval to reverse 231 | # that process (while maintaining the separation between arguments), and wrap 232 | # the whole thing up as a single "set" statement. 233 | # 234 | # This will of course break if any of these variables contains a newline or 235 | # an unmatched quote. 236 | # 237 | 238 | eval "set -- $( 239 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 240 | xargs -n1 | 241 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 242 | tr '\n' ' ' 243 | )" '"$@"' 244 | 245 | exec "$JAVACMD" "$@" 246 | -------------------------------------------------------------------------------- /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 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /public/blockify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kooperlol/Blockify/7bf8b6b18c9c9345bceeae6614491395073844c2/public/blockify.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'Blockify' -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/Blockify.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify; 2 | 3 | import codes.kooper.blockify.bstats.Metrics; 4 | import codes.kooper.blockify.listeners.StageBoundListener; 5 | import codes.kooper.blockify.managers.BlockChangeManager; 6 | import codes.kooper.blockify.managers.StageManager; 7 | import codes.kooper.blockify.protocol.BlockDigAdapter; 8 | import codes.kooper.blockify.protocol.BlockPlaceAdapter; 9 | import codes.kooper.blockify.protocol.ChunkLoadAdapter; 10 | import com.github.retrooper.packetevents.PacketEvents; 11 | import com.github.retrooper.packetevents.manager.server.ServerVersion; 12 | import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder; 13 | import lombok.Getter; 14 | import org.bukkit.plugin.java.JavaPlugin; 15 | 16 | @Getter 17 | public final class Blockify extends JavaPlugin { 18 | private StageManager stageManager; 19 | private BlockChangeManager blockChangeManager; 20 | private ServerVersion serverVersion; 21 | 22 | @Override 23 | public void onLoad() { 24 | PacketEvents.setAPI(SpigotPacketEventsBuilder.build(this)); 25 | PacketEvents.getAPI().getSettings().reEncodeByDefault(false).checkForUpdates(true); 26 | PacketEvents.getAPI().load(); 27 | } 28 | 29 | @Override 30 | public void onEnable() { 31 | new Metrics(this, 21782); 32 | serverVersion = PacketEvents.getAPI().getServerManager().getVersion(); 33 | getLogger().info("Blockify has been enabled!"); 34 | 35 | stageManager = new StageManager(); 36 | blockChangeManager = new BlockChangeManager(); 37 | 38 | getServer().getPluginManager().registerEvents(new StageBoundListener(), this); 39 | 40 | PacketEvents.getAPI().getEventManager().registerListeners(new BlockDigAdapter(), new BlockPlaceAdapter(), new ChunkLoadAdapter()); 41 | PacketEvents.getAPI().init(); 42 | } 43 | 44 | @Override 45 | public void onDisable() { 46 | blockChangeManager.shutdown(); 47 | getLogger().info("Blockify has been disabled!"); 48 | } 49 | 50 | public static Blockify getInstance() { 51 | return Blockify.getPlugin(Blockify.class); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/bstats/Metrics.java: -------------------------------------------------------------------------------- 1 | /* 2 | * This Metrics class was auto-generated and can be copied into your project if you are 3 | * not using a build tool like Gradle or Maven for dependency management. 4 | * 5 | * IMPORTANT: You are not allowed to modify this class, except changing the package. 6 | * 7 | * Disallowed modifications include but are not limited to: 8 | * - Remove the option for users to opt-out 9 | * - Change the frequency for data submission 10 | * - Obfuscate the code (every obfuscator should allow you to make an exception for specific files) 11 | * - Reformat the code (if you use a linter, add an exception) 12 | * 13 | * Violations will result in a ban of your plugin and account from bStats. 14 | */ 15 | package codes.kooper.blockify.bstats; 16 | 17 | import java.io.BufferedReader; 18 | import java.io.ByteArrayOutputStream; 19 | import java.io.DataOutputStream; 20 | import java.io.File; 21 | import java.io.IOException; 22 | import java.io.InputStreamReader; 23 | import java.lang.reflect.Method; 24 | import java.net.URL; 25 | import java.nio.charset.StandardCharsets; 26 | import java.util.Arrays; 27 | import java.util.Collection; 28 | import java.util.HashSet; 29 | import java.util.Map; 30 | import java.util.Objects; 31 | import java.util.Set; 32 | import java.util.UUID; 33 | import java.util.concurrent.Callable; 34 | import java.util.concurrent.ScheduledExecutorService; 35 | import java.util.concurrent.ScheduledThreadPoolExecutor; 36 | import java.util.concurrent.TimeUnit; 37 | import java.util.function.BiConsumer; 38 | import java.util.function.Consumer; 39 | import java.util.function.Supplier; 40 | import java.util.logging.Level; 41 | import java.util.stream.Collectors; 42 | import java.util.zip.GZIPOutputStream; 43 | import javax.net.ssl.HttpsURLConnection; 44 | import org.bukkit.Bukkit; 45 | import org.bukkit.configuration.file.YamlConfiguration; 46 | import org.bukkit.entity.Player; 47 | import org.bukkit.plugin.Plugin; 48 | import org.bukkit.plugin.java.JavaPlugin; 49 | 50 | public class Metrics { 51 | 52 | private final Plugin plugin; 53 | 54 | private final MetricsBase metricsBase; 55 | 56 | /** 57 | * Creates a new Metrics instance. 58 | * 59 | * @param plugin Your plugin instance. 60 | * @param serviceId The id of the service. It can be found at What is my plugin id? 62 | */ 63 | public Metrics(JavaPlugin plugin, int serviceId) { 64 | this.plugin = plugin; 65 | // Get the config file 66 | File bStatsFolder = new File(plugin.getDataFolder().getParentFile(), "bStats"); 67 | File configFile = new File(bStatsFolder, "config.yml"); 68 | YamlConfiguration config = YamlConfiguration.loadConfiguration(configFile); 69 | if (!config.isSet("serverUuid")) { 70 | config.addDefault("enabled", true); 71 | config.addDefault("serverUuid", UUID.randomUUID().toString()); 72 | config.addDefault("logFailedRequests", false); 73 | config.addDefault("logSentData", false); 74 | config.addDefault("logResponseStatusText", false); 75 | // Inform the server owners about bStats 76 | config 77 | .options() 78 | .header( 79 | "bStats (https://bStats.org) collects some basic information for plugin authors, like how\n" 80 | + "many people use their plugin and their total player count. It's recommended to keep bStats\n" 81 | + "enabled, but if you're not comfortable with this, you can turn this setting off. There is no\n" 82 | + "performance penalty associated with having metrics enabled, and data sent to bStats is fully\n" 83 | + "anonymous.") 84 | .copyDefaults(true); 85 | try { 86 | config.save(configFile); 87 | } catch (IOException ignored) { 88 | } 89 | } 90 | // Load the data 91 | boolean enabled = config.getBoolean("enabled", true); 92 | String serverUUID = config.getString("serverUuid"); 93 | boolean logErrors = config.getBoolean("logFailedRequests", false); 94 | boolean logSentData = config.getBoolean("logSentData", false); 95 | boolean logResponseStatusText = config.getBoolean("logResponseStatusText", false); 96 | metricsBase = 97 | new MetricsBase( 98 | "bukkit", 99 | serverUUID, 100 | serviceId, 101 | enabled, 102 | this::appendPlatformData, 103 | this::appendServiceData, 104 | submitDataTask -> Bukkit.getScheduler().runTask(plugin, submitDataTask), 105 | plugin::isEnabled, 106 | (message, error) -> this.plugin.getLogger().log(Level.WARNING, message, error), 107 | (message) -> this.plugin.getLogger().log(Level.INFO, message), 108 | logErrors, 109 | logSentData, 110 | logResponseStatusText); 111 | } 112 | 113 | /** Shuts down the underlying scheduler service. */ 114 | public void shutdown() { 115 | metricsBase.shutdown(); 116 | } 117 | 118 | /** 119 | * Adds a custom chart. 120 | * 121 | * @param chart The chart to add. 122 | */ 123 | public void addCustomChart(CustomChart chart) { 124 | metricsBase.addCustomChart(chart); 125 | } 126 | 127 | private void appendPlatformData(JsonObjectBuilder builder) { 128 | builder.appendField("playerAmount", getPlayerAmount()); 129 | builder.appendField("onlineMode", Bukkit.getOnlineMode() ? 1 : 0); 130 | builder.appendField("bukkitVersion", Bukkit.getVersion()); 131 | builder.appendField("bukkitName", Bukkit.getName()); 132 | builder.appendField("javaVersion", System.getProperty("java.version")); 133 | builder.appendField("osName", System.getProperty("os.name")); 134 | builder.appendField("osArch", System.getProperty("os.arch")); 135 | builder.appendField("osVersion", System.getProperty("os.version")); 136 | builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()); 137 | } 138 | 139 | private void appendServiceData(JsonObjectBuilder builder) { 140 | builder.appendField("pluginVersion", plugin.getDescription().getVersion()); 141 | } 142 | 143 | private int getPlayerAmount() { 144 | try { 145 | // Around MC 1.8 the return type was changed from an array to a collection, 146 | // This fixes java.lang.NoSuchMethodError: 147 | // org.bukkit.Bukkit.getOnlinePlayers()Ljava/util/Collection; 148 | Method onlinePlayersMethod = Class.forName("org.bukkit.Server").getMethod("getOnlinePlayers"); 149 | return onlinePlayersMethod.getReturnType().equals(Collection.class) 150 | ? ((Collection) onlinePlayersMethod.invoke(Bukkit.getServer())).size() 151 | : ((Player[]) onlinePlayersMethod.invoke(Bukkit.getServer())).length; 152 | } catch (Exception e) { 153 | // Just use the new method if the reflection failed 154 | return Bukkit.getOnlinePlayers().size(); 155 | } 156 | } 157 | 158 | public static class MetricsBase { 159 | 160 | /** The version of the Metrics class. */ 161 | public static final String METRICS_VERSION = "3.0.2"; 162 | 163 | private static final String REPORT_URL = "https://bStats.org/api/v2/data/%s"; 164 | 165 | private final ScheduledExecutorService scheduler; 166 | 167 | private final String platform; 168 | 169 | private final String serverUuid; 170 | 171 | private final int serviceId; 172 | 173 | private final Consumer appendPlatformDataConsumer; 174 | 175 | private final Consumer appendServiceDataConsumer; 176 | 177 | private final Consumer submitTaskConsumer; 178 | 179 | private final Supplier checkServiceEnabledSupplier; 180 | 181 | private final BiConsumer errorLogger; 182 | 183 | private final Consumer infoLogger; 184 | 185 | private final boolean logErrors; 186 | 187 | private final boolean logSentData; 188 | 189 | private final boolean logResponseStatusText; 190 | 191 | private final Set customCharts = new HashSet<>(); 192 | 193 | private final boolean enabled; 194 | 195 | /** 196 | * Creates a new MetricsBase class instance. 197 | * 198 | * @param platform The platform of the service. 199 | * @param serviceId The id of the service. 200 | * @param serverUuid The server uuid. 201 | * @param enabled Whether or not data sending is enabled. 202 | * @param appendPlatformDataConsumer A consumer that receives a {@code JsonObjectBuilder} and 203 | * appends all platform-specific data. 204 | * @param appendServiceDataConsumer A consumer that receives a {@code JsonObjectBuilder} and 205 | * appends all service-specific data. 206 | * @param submitTaskConsumer A consumer that takes a runnable with the submit task. This can be 207 | * used to delegate the data collection to a another thread to prevent errors caused by 208 | * concurrency. Can be {@code null}. 209 | * @param checkServiceEnabledSupplier A supplier to check if the service is still enabled. 210 | * @param errorLogger A consumer that accepts log message and an error. 211 | * @param infoLogger A consumer that accepts info log messages. 212 | * @param logErrors Whether or not errors should be logged. 213 | * @param logSentData Whether or not the sent data should be logged. 214 | * @param logResponseStatusText Whether or not the response status text should be logged. 215 | */ 216 | public MetricsBase( 217 | String platform, 218 | String serverUuid, 219 | int serviceId, 220 | boolean enabled, 221 | Consumer appendPlatformDataConsumer, 222 | Consumer appendServiceDataConsumer, 223 | Consumer submitTaskConsumer, 224 | Supplier checkServiceEnabledSupplier, 225 | BiConsumer errorLogger, 226 | Consumer infoLogger, 227 | boolean logErrors, 228 | boolean logSentData, 229 | boolean logResponseStatusText) { 230 | ScheduledThreadPoolExecutor scheduler = 231 | new ScheduledThreadPoolExecutor(1, task -> new Thread(task, "bStats-Metrics")); 232 | // We want delayed tasks (non-periodic) that will execute in the future to be 233 | // cancelled when the scheduler is shutdown. 234 | // Otherwise, we risk preventing the server from shutting down even when 235 | // MetricsBase#shutdown() is called 236 | scheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false); 237 | this.scheduler = scheduler; 238 | this.platform = platform; 239 | this.serverUuid = serverUuid; 240 | this.serviceId = serviceId; 241 | this.enabled = enabled; 242 | this.appendPlatformDataConsumer = appendPlatformDataConsumer; 243 | this.appendServiceDataConsumer = appendServiceDataConsumer; 244 | this.submitTaskConsumer = submitTaskConsumer; 245 | this.checkServiceEnabledSupplier = checkServiceEnabledSupplier; 246 | this.errorLogger = errorLogger; 247 | this.infoLogger = infoLogger; 248 | this.logErrors = logErrors; 249 | this.logSentData = logSentData; 250 | this.logResponseStatusText = logResponseStatusText; 251 | checkRelocation(); 252 | if (enabled) { 253 | // WARNING: Removing the option to opt-out will get your plugin banned from 254 | // bStats 255 | startSubmitting(); 256 | } 257 | } 258 | 259 | public void addCustomChart(CustomChart chart) { 260 | this.customCharts.add(chart); 261 | } 262 | 263 | public void shutdown() { 264 | scheduler.shutdown(); 265 | } 266 | 267 | private void startSubmitting() { 268 | final Runnable submitTask = 269 | () -> { 270 | if (!enabled || !checkServiceEnabledSupplier.get()) { 271 | // Submitting data or service is disabled 272 | scheduler.shutdown(); 273 | return; 274 | } 275 | if (submitTaskConsumer != null) { 276 | submitTaskConsumer.accept(this::submitData); 277 | } else { 278 | this.submitData(); 279 | } 280 | }; 281 | // Many servers tend to restart at a fixed time at xx:00 which causes an uneven 282 | // distribution of requests on the 283 | // bStats backend. To circumvent this problem, we introduce some randomness into 284 | // the initial and second delay. 285 | // WARNING: You must not modify and part of this Metrics class, including the 286 | // submit delay or frequency! 287 | // WARNING: Modifying this code will get your plugin banned on bStats. Just 288 | // don't do it! 289 | long initialDelay = (long) (1000 * 60 * (3 + Math.random() * 3)); 290 | long secondDelay = (long) (1000 * 60 * (Math.random() * 30)); 291 | scheduler.schedule(submitTask, initialDelay, TimeUnit.MILLISECONDS); 292 | scheduler.scheduleAtFixedRate( 293 | submitTask, initialDelay + secondDelay, 1000 * 60 * 30, TimeUnit.MILLISECONDS); 294 | } 295 | 296 | private void submitData() { 297 | final JsonObjectBuilder baseJsonBuilder = new JsonObjectBuilder(); 298 | appendPlatformDataConsumer.accept(baseJsonBuilder); 299 | final JsonObjectBuilder serviceJsonBuilder = new JsonObjectBuilder(); 300 | appendServiceDataConsumer.accept(serviceJsonBuilder); 301 | JsonObjectBuilder.JsonObject[] chartData = 302 | customCharts.stream() 303 | .map(customChart -> customChart.getRequestJsonObject(errorLogger, logErrors)) 304 | .filter(Objects::nonNull) 305 | .toArray(JsonObjectBuilder.JsonObject[]::new); 306 | serviceJsonBuilder.appendField("id", serviceId); 307 | serviceJsonBuilder.appendField("customCharts", chartData); 308 | baseJsonBuilder.appendField("service", serviceJsonBuilder.build()); 309 | baseJsonBuilder.appendField("serverUUID", serverUuid); 310 | baseJsonBuilder.appendField("metricsVersion", METRICS_VERSION); 311 | JsonObjectBuilder.JsonObject data = baseJsonBuilder.build(); 312 | scheduler.execute( 313 | () -> { 314 | try { 315 | // Send the data 316 | sendData(data); 317 | } catch (Exception e) { 318 | // Something went wrong! :( 319 | if (logErrors) { 320 | errorLogger.accept("Could not submit bStats metrics data", e); 321 | } 322 | } 323 | }); 324 | } 325 | 326 | private void sendData(JsonObjectBuilder.JsonObject data) throws Exception { 327 | if (logSentData) { 328 | infoLogger.accept("Sent bStats metrics data: " + data.toString()); 329 | } 330 | String url = String.format(REPORT_URL, platform); 331 | HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); 332 | // Compress the data to save bandwidth 333 | byte[] compressedData = compress(data.toString()); 334 | connection.setRequestMethod("POST"); 335 | connection.addRequestProperty("Accept", "application/json"); 336 | connection.addRequestProperty("Connection", "close"); 337 | connection.addRequestProperty("Content-Encoding", "gzip"); 338 | connection.addRequestProperty("Content-Length", String.valueOf(compressedData.length)); 339 | connection.setRequestProperty("Content-Type", "application/json"); 340 | connection.setRequestProperty("User-Agent", "Metrics-Service/1"); 341 | connection.setDoOutput(true); 342 | try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream())) { 343 | outputStream.write(compressedData); 344 | } 345 | StringBuilder builder = new StringBuilder(); 346 | try (BufferedReader bufferedReader = 347 | new BufferedReader(new InputStreamReader(connection.getInputStream()))) { 348 | String line; 349 | while ((line = bufferedReader.readLine()) != null) { 350 | builder.append(line); 351 | } 352 | } 353 | if (logResponseStatusText) { 354 | infoLogger.accept("Sent data to bStats and received response: " + builder); 355 | } 356 | } 357 | 358 | /** Checks that the class was properly relocated. */ 359 | private void checkRelocation() { 360 | // You can use the property to disable the check in your test environment 361 | if (System.getProperty("bstats.relocatecheck") == null 362 | || !System.getProperty("bstats.relocatecheck").equals("false")) { 363 | // Maven's Relocate is clever and changes strings, too. So we have to use this 364 | // little "trick" ... :D 365 | final String defaultPackage = 366 | new String(new byte[] {'o', 'r', 'g', '.', 'b', 's', 't', 'a', 't', 's'}); 367 | final String examplePackage = 368 | new String(new byte[] {'y', 'o', 'u', 'r', '.', 'p', 'a', 'c', 'k', 'a', 'g', 'e'}); 369 | // We want to make sure no one just copy & pastes the example and uses the wrong 370 | // package names 371 | if (MetricsBase.class.getPackage().getName().startsWith(defaultPackage) 372 | || MetricsBase.class.getPackage().getName().startsWith(examplePackage)) { 373 | throw new IllegalStateException("bStats Metrics class has not been relocated correctly!"); 374 | } 375 | } 376 | } 377 | 378 | /** 379 | * Gzips the given string. 380 | * 381 | * @param str The string to gzip. 382 | * @return The gzipped string. 383 | */ 384 | private static byte[] compress(final String str) throws IOException { 385 | if (str == null) { 386 | return null; 387 | } 388 | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 389 | try (GZIPOutputStream gzip = new GZIPOutputStream(outputStream)) { 390 | gzip.write(str.getBytes(StandardCharsets.UTF_8)); 391 | } 392 | return outputStream.toByteArray(); 393 | } 394 | } 395 | 396 | public static class SimplePie extends CustomChart { 397 | 398 | private final Callable callable; 399 | 400 | /** 401 | * Class constructor. 402 | * 403 | * @param chartId The id of the chart. 404 | * @param callable The callable which is used to request the chart data. 405 | */ 406 | public SimplePie(String chartId, Callable callable) { 407 | super(chartId); 408 | this.callable = callable; 409 | } 410 | 411 | @Override 412 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 413 | String value = callable.call(); 414 | if (value == null || value.isEmpty()) { 415 | // Null = skip the chart 416 | return null; 417 | } 418 | return new JsonObjectBuilder().appendField("value", value).build(); 419 | } 420 | } 421 | 422 | public static class MultiLineChart extends CustomChart { 423 | 424 | private final Callable> callable; 425 | 426 | /** 427 | * Class constructor. 428 | * 429 | * @param chartId The id of the chart. 430 | * @param callable The callable which is used to request the chart data. 431 | */ 432 | public MultiLineChart(String chartId, Callable> callable) { 433 | super(chartId); 434 | this.callable = callable; 435 | } 436 | 437 | @Override 438 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 439 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 440 | Map map = callable.call(); 441 | if (map == null || map.isEmpty()) { 442 | // Null = skip the chart 443 | return null; 444 | } 445 | boolean allSkipped = true; 446 | for (Map.Entry entry : map.entrySet()) { 447 | if (entry.getValue() == 0) { 448 | // Skip this invalid 449 | continue; 450 | } 451 | allSkipped = false; 452 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 453 | } 454 | if (allSkipped) { 455 | // Null = skip the chart 456 | return null; 457 | } 458 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 459 | } 460 | } 461 | 462 | public static class AdvancedPie extends CustomChart { 463 | 464 | private final Callable> callable; 465 | 466 | /** 467 | * Class constructor. 468 | * 469 | * @param chartId The id of the chart. 470 | * @param callable The callable which is used to request the chart data. 471 | */ 472 | public AdvancedPie(String chartId, Callable> callable) { 473 | super(chartId); 474 | this.callable = callable; 475 | } 476 | 477 | @Override 478 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 479 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 480 | Map map = callable.call(); 481 | if (map == null || map.isEmpty()) { 482 | // Null = skip the chart 483 | return null; 484 | } 485 | boolean allSkipped = true; 486 | for (Map.Entry entry : map.entrySet()) { 487 | if (entry.getValue() == 0) { 488 | // Skip this invalid 489 | continue; 490 | } 491 | allSkipped = false; 492 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 493 | } 494 | if (allSkipped) { 495 | // Null = skip the chart 496 | return null; 497 | } 498 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 499 | } 500 | } 501 | 502 | public static class SimpleBarChart extends CustomChart { 503 | 504 | private final Callable> callable; 505 | 506 | /** 507 | * Class constructor. 508 | * 509 | * @param chartId The id of the chart. 510 | * @param callable The callable which is used to request the chart data. 511 | */ 512 | public SimpleBarChart(String chartId, Callable> callable) { 513 | super(chartId); 514 | this.callable = callable; 515 | } 516 | 517 | @Override 518 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 519 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 520 | Map map = callable.call(); 521 | if (map == null || map.isEmpty()) { 522 | // Null = skip the chart 523 | return null; 524 | } 525 | for (Map.Entry entry : map.entrySet()) { 526 | valuesBuilder.appendField(entry.getKey(), new int[] {entry.getValue()}); 527 | } 528 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 529 | } 530 | } 531 | 532 | public static class AdvancedBarChart extends CustomChart { 533 | 534 | private final Callable> callable; 535 | 536 | /** 537 | * Class constructor. 538 | * 539 | * @param chartId The id of the chart. 540 | * @param callable The callable which is used to request the chart data. 541 | */ 542 | public AdvancedBarChart(String chartId, Callable> callable) { 543 | super(chartId); 544 | this.callable = callable; 545 | } 546 | 547 | @Override 548 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 549 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 550 | Map map = callable.call(); 551 | if (map == null || map.isEmpty()) { 552 | // Null = skip the chart 553 | return null; 554 | } 555 | boolean allSkipped = true; 556 | for (Map.Entry entry : map.entrySet()) { 557 | if (entry.getValue().length == 0) { 558 | // Skip this invalid 559 | continue; 560 | } 561 | allSkipped = false; 562 | valuesBuilder.appendField(entry.getKey(), entry.getValue()); 563 | } 564 | if (allSkipped) { 565 | // Null = skip the chart 566 | return null; 567 | } 568 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 569 | } 570 | } 571 | 572 | public static class DrilldownPie extends CustomChart { 573 | 574 | private final Callable>> callable; 575 | 576 | /** 577 | * Class constructor. 578 | * 579 | * @param chartId The id of the chart. 580 | * @param callable The callable which is used to request the chart data. 581 | */ 582 | public DrilldownPie(String chartId, Callable>> callable) { 583 | super(chartId); 584 | this.callable = callable; 585 | } 586 | 587 | @Override 588 | public JsonObjectBuilder.JsonObject getChartData() throws Exception { 589 | JsonObjectBuilder valuesBuilder = new JsonObjectBuilder(); 590 | Map> map = callable.call(); 591 | if (map == null || map.isEmpty()) { 592 | // Null = skip the chart 593 | return null; 594 | } 595 | boolean reallyAllSkipped = true; 596 | for (Map.Entry> entryValues : map.entrySet()) { 597 | JsonObjectBuilder valueBuilder = new JsonObjectBuilder(); 598 | boolean allSkipped = true; 599 | for (Map.Entry valueEntry : map.get(entryValues.getKey()).entrySet()) { 600 | valueBuilder.appendField(valueEntry.getKey(), valueEntry.getValue()); 601 | allSkipped = false; 602 | } 603 | if (!allSkipped) { 604 | reallyAllSkipped = false; 605 | valuesBuilder.appendField(entryValues.getKey(), valueBuilder.build()); 606 | } 607 | } 608 | if (reallyAllSkipped) { 609 | // Null = skip the chart 610 | return null; 611 | } 612 | return new JsonObjectBuilder().appendField("values", valuesBuilder.build()).build(); 613 | } 614 | } 615 | 616 | public abstract static class CustomChart { 617 | 618 | private final String chartId; 619 | 620 | protected CustomChart(String chartId) { 621 | if (chartId == null) { 622 | throw new IllegalArgumentException("chartId must not be null"); 623 | } 624 | this.chartId = chartId; 625 | } 626 | 627 | public JsonObjectBuilder.JsonObject getRequestJsonObject( 628 | BiConsumer errorLogger, boolean logErrors) { 629 | JsonObjectBuilder builder = new JsonObjectBuilder(); 630 | builder.appendField("chartId", chartId); 631 | try { 632 | JsonObjectBuilder.JsonObject data = getChartData(); 633 | if (data == null) { 634 | // If the data is null we don't send the chart. 635 | return null; 636 | } 637 | builder.appendField("data", data); 638 | } catch (Throwable t) { 639 | if (logErrors) { 640 | errorLogger.accept("Failed to get data for custom chart with id " + chartId, t); 641 | } 642 | return null; 643 | } 644 | return builder.build(); 645 | } 646 | 647 | protected abstract JsonObjectBuilder.JsonObject getChartData() throws Exception; 648 | } 649 | 650 | public static class SingleLineChart extends CustomChart { 651 | 652 | private final Callable callable; 653 | 654 | /** 655 | * Class constructor. 656 | * 657 | * @param chartId The id of the chart. 658 | * @param callable The callable which is used to request the chart data. 659 | */ 660 | public SingleLineChart(String chartId, Callable callable) { 661 | super(chartId); 662 | this.callable = callable; 663 | } 664 | 665 | @Override 666 | protected JsonObjectBuilder.JsonObject getChartData() throws Exception { 667 | int value = callable.call(); 668 | if (value == 0) { 669 | // Null = skip the chart 670 | return null; 671 | } 672 | return new JsonObjectBuilder().appendField("value", value).build(); 673 | } 674 | } 675 | 676 | /** 677 | * An extremely simple JSON builder. 678 | * 679 | *

While this class is neither feature-rich nor the most performant one, it's sufficient enough 680 | * for its use-case. 681 | */ 682 | public static class JsonObjectBuilder { 683 | 684 | private StringBuilder builder = new StringBuilder(); 685 | 686 | private boolean hasAtLeastOneField = false; 687 | 688 | public JsonObjectBuilder() { 689 | builder.append("{"); 690 | } 691 | 692 | /** 693 | * Appends a null field to the JSON. 694 | * 695 | * @param key The key of the field. 696 | * @return A reference to this object. 697 | */ 698 | public JsonObjectBuilder appendNull(String key) { 699 | appendFieldUnescaped(key, "null"); 700 | return this; 701 | } 702 | 703 | /** 704 | * Appends a string field to the JSON. 705 | * 706 | * @param key The key of the field. 707 | * @param value The value of the field. 708 | * @return A reference to this object. 709 | */ 710 | public JsonObjectBuilder appendField(String key, String value) { 711 | if (value == null) { 712 | throw new IllegalArgumentException("JSON value must not be null"); 713 | } 714 | appendFieldUnescaped(key, "\"" + escape(value) + "\""); 715 | return this; 716 | } 717 | 718 | /** 719 | * Appends an integer field to the JSON. 720 | * 721 | * @param key The key of the field. 722 | * @param value The value of the field. 723 | * @return A reference to this object. 724 | */ 725 | public JsonObjectBuilder appendField(String key, int value) { 726 | appendFieldUnescaped(key, String.valueOf(value)); 727 | return this; 728 | } 729 | 730 | /** 731 | * Appends an object to the JSON. 732 | * 733 | * @param key The key of the field. 734 | * @param object The object. 735 | * @return A reference to this object. 736 | */ 737 | public JsonObjectBuilder appendField(String key, JsonObject object) { 738 | if (object == null) { 739 | throw new IllegalArgumentException("JSON object must not be null"); 740 | } 741 | appendFieldUnescaped(key, object.toString()); 742 | return this; 743 | } 744 | 745 | /** 746 | * Appends a string array to the JSON. 747 | * 748 | * @param key The key of the field. 749 | * @param values The string array. 750 | * @return A reference to this object. 751 | */ 752 | public JsonObjectBuilder appendField(String key, String[] values) { 753 | if (values == null) { 754 | throw new IllegalArgumentException("JSON values must not be null"); 755 | } 756 | String escapedValues = 757 | Arrays.stream(values) 758 | .map(value -> "\"" + escape(value) + "\"") 759 | .collect(Collectors.joining(",")); 760 | appendFieldUnescaped(key, "[" + escapedValues + "]"); 761 | return this; 762 | } 763 | 764 | /** 765 | * Appends an integer array to the JSON. 766 | * 767 | * @param key The key of the field. 768 | * @param values The integer array. 769 | * @return A reference to this object. 770 | */ 771 | public JsonObjectBuilder appendField(String key, int[] values) { 772 | if (values == null) { 773 | throw new IllegalArgumentException("JSON values must not be null"); 774 | } 775 | String escapedValues = 776 | Arrays.stream(values).mapToObj(String::valueOf).collect(Collectors.joining(",")); 777 | appendFieldUnescaped(key, "[" + escapedValues + "]"); 778 | return this; 779 | } 780 | 781 | /** 782 | * Appends an object array to the JSON. 783 | * 784 | * @param key The key of the field. 785 | * @param values The integer array. 786 | * @return A reference to this object. 787 | */ 788 | public JsonObjectBuilder appendField(String key, JsonObject[] values) { 789 | if (values == null) { 790 | throw new IllegalArgumentException("JSON values must not be null"); 791 | } 792 | String escapedValues = 793 | Arrays.stream(values).map(JsonObject::toString).collect(Collectors.joining(",")); 794 | appendFieldUnescaped(key, "[" + escapedValues + "]"); 795 | return this; 796 | } 797 | 798 | /** 799 | * Appends a field to the object. 800 | * 801 | * @param key The key of the field. 802 | * @param escapedValue The escaped value of the field. 803 | */ 804 | private void appendFieldUnescaped(String key, String escapedValue) { 805 | if (builder == null) { 806 | throw new IllegalStateException("JSON has already been built"); 807 | } 808 | if (key == null) { 809 | throw new IllegalArgumentException("JSON key must not be null"); 810 | } 811 | if (hasAtLeastOneField) { 812 | builder.append(","); 813 | } 814 | builder.append("\"").append(escape(key)).append("\":").append(escapedValue); 815 | hasAtLeastOneField = true; 816 | } 817 | 818 | /** 819 | * Builds the JSON string and invalidates this builder. 820 | * 821 | * @return The built JSON string. 822 | */ 823 | public JsonObject build() { 824 | if (builder == null) { 825 | throw new IllegalStateException("JSON has already been built"); 826 | } 827 | JsonObject object = new JsonObject(builder.append("}").toString()); 828 | builder = null; 829 | return object; 830 | } 831 | 832 | /** 833 | * Escapes the given string like stated in https://www.ietf.org/rfc/rfc4627.txt. 834 | * 835 | *

This method escapes only the necessary characters '"', '\'. and '\u0000' - '\u001F'. 836 | * Compact escapes are not used (e.g., '\n' is escaped as "\u000a" and not as "\n"). 837 | * 838 | * @param value The value to escape. 839 | * @return The escaped value. 840 | */ 841 | private static String escape(String value) { 842 | final StringBuilder builder = new StringBuilder(); 843 | for (int i = 0; i < value.length(); i++) { 844 | char c = value.charAt(i); 845 | if (c == '"') { 846 | builder.append("\\\""); 847 | } else if (c == '\\') { 848 | builder.append("\\\\"); 849 | } else if (c <= '\u000F') { 850 | builder.append("\\u000").append(Integer.toHexString(c)); 851 | } else if (c <= '\u001F') { 852 | builder.append("\\u00").append(Integer.toHexString(c)); 853 | } else { 854 | builder.append(c); 855 | } 856 | } 857 | return builder.toString(); 858 | } 859 | 860 | /** 861 | * A super simple representation of a JSON object. 862 | * 863 | *

This class only exists to make methods of the {@link JsonObjectBuilder} type-safe and not 864 | * allow a raw string inputs for methods like {@link JsonObjectBuilder#appendField(String, 865 | * JsonObject)}. 866 | */ 867 | public static class JsonObject { 868 | 869 | private final String value; 870 | 871 | private JsonObject(String value) { 872 | this.value = value; 873 | } 874 | 875 | @Override 876 | public String toString() { 877 | return value; 878 | } 879 | } 880 | } 881 | } -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/events/BlockifyBreakEvent.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.events; 2 | 3 | import codes.kooper.blockify.models.Stage; 4 | import codes.kooper.blockify.models.View; 5 | import codes.kooper.blockify.types.BlockifyPosition; 6 | import lombok.Getter; 7 | import org.bukkit.block.data.BlockData; 8 | import org.bukkit.entity.Player; 9 | import org.bukkit.event.Cancellable; 10 | import org.bukkit.event.Event; 11 | import org.bukkit.event.HandlerList; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | @Getter 15 | public class BlockifyBreakEvent extends Event implements Cancellable { 16 | private static final HandlerList HANDLERS = new HandlerList(); 17 | private boolean cancelled = false; 18 | private final Player player; 19 | private final BlockifyPosition position; 20 | private final BlockData blockData; 21 | private final View view; 22 | private final Stage stage; 23 | 24 | /** 25 | * Event that is called when a player breaks a block in a Blockify stage. 26 | * 27 | * @param player The player that broke the block. 28 | * @param position The position of the block that was broken. 29 | * @param blockData The block data of the block that was broken. 30 | * @param view The view that the player is in. 31 | * @param stage The stage that the player is in. 32 | */ 33 | public BlockifyBreakEvent(Player player, BlockifyPosition position, BlockData blockData, View view, Stage stage) { 34 | this.player = player; 35 | this.position = position; 36 | this.blockData = blockData; 37 | this.view = view; 38 | this.stage = stage; 39 | } 40 | 41 | @Override 42 | public @NotNull HandlerList getHandlers() { 43 | return HANDLERS; 44 | } 45 | 46 | public static HandlerList getHandlerList() { 47 | return HANDLERS; 48 | } 49 | 50 | @Override 51 | public boolean isCancelled() { 52 | return cancelled; 53 | } 54 | 55 | @Override 56 | public void setCancelled(boolean cancel) { 57 | this.cancelled = cancel; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/events/BlockifyInteractEvent.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.events; 2 | 3 | import codes.kooper.blockify.models.Stage; 4 | import codes.kooper.blockify.models.View; 5 | import codes.kooper.blockify.types.BlockifyPosition; 6 | import lombok.Getter; 7 | import org.bukkit.block.data.BlockData; 8 | import org.bukkit.entity.Player; 9 | import org.bukkit.event.Cancellable; 10 | import org.bukkit.event.Event; 11 | import org.bukkit.event.HandlerList; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | @Getter 15 | public class BlockifyInteractEvent extends Event implements Cancellable { 16 | private static final HandlerList HANDLERS = new HandlerList(); 17 | private boolean cancelled = false; 18 | private final Player player; 19 | private final BlockifyPosition position; 20 | private final BlockData blockData; 21 | private final View view; 22 | private final Stage stage; 23 | 24 | /** 25 | * Event that is called when a player interacts with a block in a stage. 26 | * 27 | * @param player The player that interacted with the block. 28 | * @param position The position of the block that was interacted with. 29 | * @param blockData The block data of the block that was interacted with. 30 | * @param view The view that the player is currently in. 31 | * @param stage The stage that the player is currently in. 32 | */ 33 | public BlockifyInteractEvent(Player player, BlockifyPosition position, BlockData blockData, View view, Stage stage) { 34 | this.player = player; 35 | this.position = position; 36 | this.blockData = blockData; 37 | this.view = view; 38 | this.stage = stage; 39 | } 40 | 41 | @Override 42 | public boolean isCancelled() { 43 | return cancelled; 44 | } 45 | 46 | @Override 47 | public void setCancelled(boolean cancel) { 48 | cancelled = cancel; 49 | } 50 | 51 | @Override 52 | public @NotNull HandlerList getHandlers() { 53 | return HANDLERS; 54 | } 55 | 56 | public static HandlerList getHandlerList() { 57 | return HANDLERS; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/events/BlockifyPlaceEvent.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.events; 2 | 3 | import codes.kooper.blockify.models.Stage; 4 | import codes.kooper.blockify.models.View; 5 | import codes.kooper.blockify.types.BlockifyPosition; 6 | import lombok.Getter; 7 | import org.bukkit.entity.Player; 8 | import org.bukkit.event.Event; 9 | import org.bukkit.event.HandlerList; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | @Getter 13 | public class BlockifyPlaceEvent extends Event { 14 | private static final HandlerList HANDLERS = new HandlerList(); 15 | private final Player player; 16 | private final BlockifyPosition position; 17 | private final View view; 18 | private final Stage stage; 19 | 20 | /** 21 | * Event that is called when a player places a block in the Blockify plugin. 22 | * 23 | * @param player The player that placed the block. 24 | * @param position The position of the block that was placed. 25 | * @param view The view that the player is currently in. 26 | * @param stage The stage that the player is currently in. 27 | */ 28 | public BlockifyPlaceEvent(Player player, BlockifyPosition position, View view, Stage stage) { 29 | this.player = player; 30 | this.position = position; 31 | this.view = view; 32 | this.stage = stage; 33 | } 34 | 35 | @Override 36 | public @NotNull HandlerList getHandlers() { 37 | return HANDLERS; 38 | } 39 | 40 | public static HandlerList getHandlerList() { 41 | return HANDLERS; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/events/CreateStageEvent.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.events; 2 | 3 | import codes.kooper.blockify.models.Stage; 4 | import lombok.Getter; 5 | import org.bukkit.event.Event; 6 | import org.bukkit.event.HandlerList; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | @Getter 10 | public class CreateStageEvent extends Event { 11 | private static final HandlerList HANDLERS = new HandlerList(); 12 | private final Stage stage; 13 | 14 | /** 15 | * Event that is called when a stage is created. 16 | * 17 | * @param stage The stage that was created 18 | */ 19 | public CreateStageEvent(Stage stage) { 20 | this.stage = stage; 21 | } 22 | 23 | @Override 24 | public @NotNull HandlerList getHandlers() { 25 | return HANDLERS; 26 | } 27 | 28 | public static HandlerList getHandlerList() { 29 | return HANDLERS; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/events/DeleteStageEvent.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.events; 2 | 3 | import codes.kooper.blockify.models.Stage; 4 | import lombok.Getter; 5 | import org.bukkit.event.Event; 6 | import org.bukkit.event.HandlerList; 7 | import org.jetbrains.annotations.NotNull; 8 | 9 | @Getter 10 | public class DeleteStageEvent extends Event { 11 | 12 | private static final HandlerList HANDLERS = new HandlerList(); 13 | private final Stage stage; 14 | 15 | /** 16 | * Event that is called when a stage is deleted. 17 | * 18 | * @param stage The stage that was deleted. 19 | */ 20 | public DeleteStageEvent(Stage stage) { 21 | this.stage = stage; 22 | } 23 | 24 | @Override 25 | public @NotNull HandlerList getHandlers() { 26 | return HANDLERS; 27 | } 28 | 29 | public static HandlerList getHandlerList() { 30 | return HANDLERS; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/events/OnBlockChangeSendEvent.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.events; 2 | 3 | import codes.kooper.blockify.models.Stage; 4 | import codes.kooper.blockify.types.BlockifyChunk; 5 | import lombok.Getter; 6 | import org.bukkit.block.data.BlockData; 7 | import org.bukkit.event.Event; 8 | import org.bukkit.event.HandlerList; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.util.Map; 12 | 13 | @Getter 14 | public class OnBlockChangeSendEvent extends Event { 15 | 16 | private static final HandlerList HANDLERS = new HandlerList(); 17 | private final Stage stage; 18 | private final Map> blocks; 19 | 20 | /** 21 | * Event that is called when block(s) are being changed. 22 | * @param stage The stage that the block change is happening in. 23 | * @param blocks The blocks that are being changed. 24 | */ 25 | public OnBlockChangeSendEvent(Stage stage, Map> blocks) { 26 | this.stage = stage; 27 | this.blocks = blocks; 28 | } 29 | 30 | 31 | @Override 32 | public @NotNull HandlerList getHandlers() { 33 | return HANDLERS; 34 | } 35 | 36 | public static HandlerList getHandlerList() { 37 | return HANDLERS; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/events/PlayerEnterStageEvent.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.events; 2 | 3 | import codes.kooper.blockify.models.Stage; 4 | import lombok.Getter; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Cancellable; 7 | import org.bukkit.event.Event; 8 | import org.bukkit.event.HandlerList; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | @Getter 12 | public class PlayerEnterStageEvent extends Event implements Cancellable { 13 | private boolean cancelled = false; 14 | private static final HandlerList HANDLERS = new HandlerList(); 15 | private final Stage stage; 16 | private final Player player; 17 | 18 | /** 19 | * Event that is called when a player enters a stage. 20 | * 21 | * @param stage The stage the player entered. 22 | * @param player The player that entered the stage. 23 | */ 24 | public PlayerEnterStageEvent(Stage stage, Player player) { 25 | this.stage = stage; 26 | this.player = player; 27 | } 28 | 29 | @Override 30 | public @NotNull HandlerList getHandlers() { 31 | return HANDLERS; 32 | } 33 | 34 | public static HandlerList getHandlerList() { 35 | return HANDLERS; 36 | } 37 | 38 | @Override 39 | public boolean isCancelled() { 40 | return cancelled; 41 | } 42 | 43 | @Override 44 | public void setCancelled(boolean cancel) { 45 | this.cancelled = cancel; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/events/PlayerExitStageEvent.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.events; 2 | 3 | import codes.kooper.blockify.models.Stage; 4 | import lombok.Getter; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Cancellable; 7 | import org.bukkit.event.Event; 8 | import org.bukkit.event.HandlerList; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | @Getter 12 | public class PlayerExitStageEvent extends Event implements Cancellable { 13 | private boolean cancelled = false; 14 | private static final HandlerList HANDLERS = new HandlerList(); 15 | private final Stage stage; 16 | private final Player player; 17 | 18 | /** 19 | * Event that is called when a player exits a stage. 20 | * 21 | * @param stage The stage the player exited. 22 | * @param player The player that exited the stage. 23 | */ 24 | public PlayerExitStageEvent(Stage stage, Player player) { 25 | this.stage = stage; 26 | this.player = player; 27 | } 28 | 29 | @Override 30 | public @NotNull HandlerList getHandlers() { 31 | return HANDLERS; 32 | } 33 | 34 | public static HandlerList getHandlerList() { 35 | return HANDLERS; 36 | } 37 | 38 | @Override 39 | public boolean isCancelled() { 40 | return cancelled; 41 | } 42 | 43 | @Override 44 | public void setCancelled(boolean cancel) { 45 | this.cancelled = cancel; 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/events/PlayerJoinStageEvent.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.events; 2 | 3 | import codes.kooper.blockify.models.Stage; 4 | import lombok.Getter; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Event; 7 | import org.bukkit.event.HandlerList; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | @Getter 11 | public class PlayerJoinStageEvent extends Event { 12 | 13 | private static final HandlerList HANDLERS = new HandlerList(); 14 | private final Stage stage; 15 | private final Player player; 16 | 17 | /** 18 | * Event that is called when a player joins a stage. 19 | * 20 | * @param stage The stage the player joined. 21 | * @param player The player that joined the stage. 22 | */ 23 | public PlayerJoinStageEvent(Stage stage, Player player) { 24 | this.stage = stage; 25 | this.player = player; 26 | } 27 | 28 | @Override 29 | public @NotNull HandlerList getHandlers() { 30 | return HANDLERS; 31 | } 32 | 33 | public static HandlerList getHandlerList() { 34 | return HANDLERS; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/events/PlayerLeaveStageEvent.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.events; 2 | 3 | import codes.kooper.blockify.models.Stage; 4 | import lombok.Getter; 5 | import org.bukkit.entity.Player; 6 | import org.bukkit.event.Event; 7 | import org.bukkit.event.HandlerList; 8 | import org.jetbrains.annotations.NotNull; 9 | 10 | @Getter 11 | public class PlayerLeaveStageEvent extends Event { 12 | 13 | private static final HandlerList HANDLERS = new HandlerList(); 14 | private final Stage stage; 15 | private final Player player; 16 | 17 | /** 18 | * Event that is called when a player leaves a stage. 19 | * 20 | * @param stage The stage the player is leaving. 21 | * @param player The player that is leaving the stage. 22 | */ 23 | public PlayerLeaveStageEvent(Stage stage, Player player) { 24 | this.stage = stage; 25 | this.player = player; 26 | } 27 | 28 | @Override 29 | public @NotNull HandlerList getHandlers() { 30 | return HANDLERS; 31 | } 32 | 33 | public static HandlerList getHandlerList() { 34 | return HANDLERS; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/listeners/StageBoundListener.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.listeners; 2 | 3 | import codes.kooper.blockify.Blockify; 4 | import codes.kooper.blockify.events.PlayerEnterStageEvent; 5 | import codes.kooper.blockify.events.PlayerExitStageEvent; 6 | import codes.kooper.blockify.events.PlayerJoinStageEvent; 7 | import codes.kooper.blockify.events.PlayerLeaveStageEvent; 8 | import codes.kooper.blockify.models.Stage; 9 | import org.bukkit.entity.Player; 10 | import org.bukkit.event.EventHandler; 11 | import org.bukkit.event.Listener; 12 | import org.bukkit.event.player.PlayerJoinEvent; 13 | import org.bukkit.event.player.PlayerMoveEvent; 14 | import org.bukkit.event.player.PlayerQuitEvent; 15 | 16 | import java.util.List; 17 | 18 | public class StageBoundListener implements Listener { 19 | 20 | /** 21 | * This method is an event handler for PlayerMoveEvent. 22 | * It is triggered when a player moves in the game. 23 | *

24 | * The method retrieves the stages associated with the player and checks if the player's new location 25 | * is within any of these stages. If the player has entered a new stage, a PlayerEnterStageEvent is called. 26 | * If the player has exited a stage, a PlayerExitStageEvent is called. 27 | * 28 | * @param event The PlayerMoveEvent object containing information about the move event. 29 | */ 30 | @EventHandler 31 | public void onPlayerMove(PlayerMoveEvent event) { 32 | Player player = event.getPlayer(); 33 | List stages = Blockify.getInstance().getStageManager().getStages(player); 34 | for (Stage stage : stages) { 35 | if (stage.isLocationWithin(event.getTo())) { 36 | PlayerEnterStageEvent e = new PlayerEnterStageEvent(stage, player); 37 | e.callEvent(); 38 | if (e.isCancelled()) { 39 | event.setCancelled(true); 40 | } 41 | } else if (stage.isLocationWithin(event.getFrom())) { 42 | PlayerExitStageEvent e = new PlayerExitStageEvent(stage, player); 43 | e.callEvent(); 44 | if (e.isCancelled()) { 45 | event.setCancelled(true); 46 | } 47 | } 48 | } 49 | } 50 | 51 | /** 52 | * This method is an event handler for PlayerJoinEvent. 53 | * It is triggered when a player joins the game. 54 | *

55 | * The method retrieves the stages associated with the player and checks if the player's location 56 | * is within any of these stages. If the player is within a stage, a PlayerEnterStageEvent is called. 57 | * 58 | * @param event The PlayerJoinEvent object containing information about the join event. 59 | */ 60 | @EventHandler 61 | public void onPlayerJoin(PlayerJoinEvent event) { 62 | Player player = event.getPlayer(); 63 | List stages = Blockify.getInstance().getStageManager().getStages(player); 64 | for (Stage stage : stages) { 65 | if (stage.isLocationWithin(player.getLocation())) { 66 | new PlayerEnterStageEvent(stage, player).callEvent(); 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * This method is an event handler for PlayerQuitEvent. 73 | * It is triggered when a player quits the game. 74 | *

75 | * The method retrieves the stages associated with the player and calls a PlayerExitStageEvent for each stage. 76 | * 77 | * @param event The PlayerQuitEvent object containing information about the quit event. 78 | */ 79 | @EventHandler 80 | public void onPlayerQuit(PlayerQuitEvent event) { 81 | Player player = event.getPlayer(); 82 | List stages = Blockify.getInstance().getStageManager().getStages(player); 83 | for (Stage stage : stages) { 84 | new PlayerExitStageEvent(stage, player).callEvent(); 85 | } 86 | } 87 | 88 | /** 89 | * This method is an event handler for PlayerJoinStageEvent. 90 | * It is triggered when a player joins a stage. 91 | *

92 | * The method checks if the player's location is within the stage and calls a PlayerEnterStageEvent if it is. 93 | * 94 | * @param event The PlayerJoinStageEvent object containing information about the join stage event. 95 | */ 96 | @EventHandler 97 | public void onPlayerStageJoin(PlayerJoinStageEvent event) { 98 | if (event.getStage().isLocationWithin(event.getPlayer().getLocation())) { 99 | new PlayerEnterStageEvent(event.getStage(), event.getPlayer()).callEvent(); 100 | } 101 | } 102 | 103 | /** 104 | * This method is an event handler for PlayerLeaveStageEvent. 105 | * It is triggered when a player leaves a stage. 106 | *

107 | * The method checks if the player's location is within the stage and calls a PlayerExitStageEvent if it is. 108 | * 109 | * @param event The PlayerLeaveStageEvent object containing information about the leave stage event. 110 | */ 111 | @EventHandler 112 | public void onPlayerStageLeave(PlayerLeaveStageEvent event) { 113 | if (event.getStage().isLocationWithin(event.getPlayer().getLocation())) { 114 | new PlayerExitStageEvent(event.getStage(), event.getPlayer()).callEvent(); 115 | } 116 | } 117 | 118 | /** 119 | * This method is an event handler for PlayerEnterStageEvent. 120 | * It is triggered when a player enters a stage. 121 | *

122 | * The method checks if the stage has players hidden and hides all players in the stage from the player. 123 | * 124 | * @param event The PlayerEnterStageEvent object containing information about the enter stage event. 125 | */ 126 | @EventHandler 127 | public void onPlayerEnterStage(PlayerEnterStageEvent event) { 128 | if (!event.getStage().getAudience().isArePlayersHidden()) return; 129 | for (Player player : event.getStage().getAudience().getOnlinePlayers()) { 130 | if (player == null) continue; 131 | player.hidePlayer(Blockify.getInstance(), event.getPlayer()); 132 | if (!event.getStage().isLocationWithin(player.getLocation())) continue; 133 | event.getPlayer().hidePlayer(Blockify.getInstance(), player); 134 | } 135 | } 136 | 137 | /** 138 | * This method is an event handler for PlayerExitStageEvent. 139 | * It is triggered when a player exits a stage. 140 | *

141 | * The method checks if the stage has players hidden and shows all players in the stage to the player. 142 | * 143 | * @param event The PlayerExitStageEvent object containing information about the exit stage event. 144 | */ 145 | @EventHandler 146 | public void onPlayerExitStage(PlayerExitStageEvent event) { 147 | if (!event.getStage().getAudience().isArePlayersHidden()) return; 148 | for (Player player : event.getStage().getAudience().getOnlinePlayers()) { 149 | if (player == null) continue; 150 | player.showPlayer(Blockify.getInstance(), event.getPlayer()); 151 | } 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/managers/BlockChangeManager.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.managers; 2 | 3 | import codes.kooper.blockify.Blockify; 4 | import codes.kooper.blockify.events.OnBlockChangeSendEvent; 5 | import codes.kooper.blockify.models.Audience; 6 | import codes.kooper.blockify.models.Stage; 7 | import codes.kooper.blockify.models.View; 8 | import codes.kooper.blockify.types.BlockifyChunk; 9 | import codes.kooper.blockify.types.BlockifyPosition; 10 | import codes.kooper.blockify.utils.PositionKeyUtil; 11 | import com.github.retrooper.packetevents.PacketEvents; 12 | import com.github.retrooper.packetevents.protocol.player.User; 13 | import com.github.retrooper.packetevents.protocol.world.chunk.BaseChunk; 14 | import com.github.retrooper.packetevents.protocol.world.chunk.Column; 15 | import com.github.retrooper.packetevents.protocol.world.chunk.LightData; 16 | import com.github.retrooper.packetevents.protocol.world.chunk.impl.v_1_18.Chunk_v1_18; 17 | import com.github.retrooper.packetevents.protocol.world.states.WrappedBlockState; 18 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerChunkData; 19 | import io.github.retrooper.packetevents.util.SpigotConversionUtil; 20 | import io.papermc.paper.math.Position; 21 | import lombok.Getter; 22 | import org.bukkit.Bukkit; 23 | import org.bukkit.Chunk; 24 | import org.bukkit.ChunkSnapshot; 25 | import org.bukkit.Location; 26 | import org.bukkit.block.data.BlockData; 27 | import org.bukkit.entity.Player; 28 | import org.bukkit.scheduler.BukkitTask; 29 | 30 | import java.util.*; 31 | import java.util.concurrent.*; 32 | import java.util.concurrent.atomic.AtomicInteger; 33 | 34 | @Getter 35 | public class BlockChangeManager { 36 | private final ConcurrentHashMap blockChangeTasks; 37 | private final ConcurrentHashMap blockDataToId; 38 | private final ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); 39 | 40 | public BlockChangeManager() { 41 | this.blockChangeTasks = new ConcurrentHashMap<>(); 42 | this.blockDataToId = new ConcurrentHashMap<>(); 43 | } 44 | 45 | /** 46 | * Sends views to the player. 47 | * Call Asynchronously 48 | * 49 | * @param stage the stage 50 | * @param player the player 51 | */ 52 | public void sendViews(Stage stage, Player player) { 53 | for (View view : stage.getViews()) { 54 | sendView(player, view); 55 | } 56 | } 57 | 58 | /** 59 | * Sends a view to the player. 60 | * Call Asynchronously 61 | * 62 | * @param player the player 63 | * @param view the view 64 | */ 65 | public void sendView(Player player, View view) { 66 | Audience audience = Audience.fromPlayers(new HashSet<>(Collections.singletonList(player))); 67 | sendBlockChanges(view.getStage(), audience, view.getBlocks().keySet()); 68 | } 69 | 70 | /** 71 | * Hides a view from the player. 72 | * Call Asynchronously 73 | * 74 | * @param player the player 75 | * @param view the view 76 | */ 77 | public void hideView(Player player, View view) { 78 | Audience audience = Audience.fromPlayers(new HashSet<>(Collections.singletonList(player))); 79 | sendBlockChanges(view.getStage(), audience, view.getBlocks().keySet(), true); 80 | } 81 | 82 | /** 83 | * Hides views from the player. 84 | * Call Asynchronously 85 | * 86 | * @param stage the stage 87 | * @param player the player 88 | */ 89 | public void hideViews(Stage stage, Player player) { 90 | for (View view : stage.getViews()) { 91 | hideView(player, view); 92 | } 93 | } 94 | 95 | /** 96 | * Sends a block change to the audience. 97 | * 98 | * @param stage the stage 99 | * @param audience the audience 100 | * @param position the position 101 | */ 102 | public void sendBlockChange(Stage stage, Audience audience, BlockifyPosition position) { 103 | BlockifyChunk chunk = new BlockifyChunk(position.getX() >> 4, position.getZ() >> 4); 104 | sendBlockChanges(stage, audience, Collections.singleton(chunk)); 105 | } 106 | 107 | /** 108 | * Sends block changes to the audience. 109 | * Call Asynchronously 110 | * 111 | * @param stage the stage 112 | * @param audience the audience 113 | * @param chunks the chunks to send 114 | */ 115 | public void sendBlockChanges(Stage stage, Audience audience, Collection chunks) { 116 | sendBlockChanges(stage, audience, chunks, false); 117 | } 118 | 119 | /** 120 | * Sends block changes to the audience. 121 | * Call Asynchronously 122 | * 123 | * @param stage the stage 124 | * @param audience the audience 125 | * @param chunks the chunks to send 126 | * @param unload whether to unload the chunks 127 | */ 128 | public void sendBlockChanges(Stage stage, Audience audience, Collection chunks, boolean unload) { 129 | Map> blockChanges = getBlockChanges(stage, chunks); 130 | Bukkit.getScheduler().runTask(Blockify.getInstance(), () -> new OnBlockChangeSendEvent(stage, blockChanges).callEvent()); 131 | 132 | // If there is only one block change, send it to the player directly 133 | int blockCount = 0; 134 | for (Map.Entry> entry : blockChanges.entrySet()) { 135 | blockCount += entry.getValue().size(); 136 | } 137 | if (blockCount == 1) { 138 | for (Player onlinePlayer : audience.getOnlinePlayers()) { 139 | if (onlinePlayer.getWorld() != stage.getWorld()) continue; 140 | for (Map.Entry> entry : blockChanges.entrySet()) { 141 | Long position = entry.getValue().keySet().iterator().next(); 142 | BlockData blockData = entry.getValue().values().iterator().next(); 143 | onlinePlayer.sendBlockChange(PositionKeyUtil.toBlockifyPosition(position).toLocation(onlinePlayer.getWorld()), blockData); 144 | } 145 | } 146 | return; 147 | } 148 | 149 | // Less than 3,000 blocks then use the player.sendBlockChanges method 150 | if (blockCount < 3000) { 151 | Map multiBlockChange = new HashMap<>(); 152 | for (BlockifyChunk chunk : chunks) { 153 | if (!stage.getWorld().isChunkLoaded(chunk.x(), chunk.z()) || !blockChanges.containsKey(chunk)) continue; 154 | for (Map.Entry entry : blockChanges.get(chunk).entrySet()) { 155 | multiBlockChange.put(PositionKeyUtil.toBlockifyPosition(entry.getKey()).toPosition(), entry.getValue()); 156 | } 157 | } 158 | for (Player player : audience.getOnlinePlayers()) { 159 | player.sendMultiBlockChange(multiBlockChange); 160 | } 161 | return; 162 | } 163 | 164 | // Send multiple block changes to the players 165 | for (Player onlinePlayer : audience.getOnlinePlayers()) { 166 | Location playerLocation = onlinePlayer.getLocation(); 167 | if (playerLocation.getWorld() != stage.getWorld()) continue; 168 | 169 | // The chunk index is used to keep track of the current chunk being sent 170 | AtomicInteger chunkIndex = new AtomicInteger(0); 171 | // Create an array of chunks to send from the block changes map 172 | List chunksToSend = new ArrayList<>(chunks.stream().toList()); 173 | chunksToSend.sort((chunk1, chunk2) -> { 174 | // Get distance from chunks to player 175 | int x = playerLocation.getBlockX() / 16; 176 | int z = playerLocation.getBlockZ() / 16; 177 | int chunkX1 = chunk1.x(); 178 | int chunkZ1 = chunk1.z(); 179 | int chunkX2 = chunk2.x(); 180 | int chunkZ2 = chunk2.z(); 181 | 182 | // Calculate squared distances (more efficient than using square root) 183 | int distanceSquared1 = (chunkX1 - x) * (chunkX1 - x) + (chunkZ1 - z) * (chunkZ1 - z); 184 | int distanceSquared2 = (chunkX2 - x) * (chunkX2 - x) + (chunkZ2 - z) * (chunkZ2 - z); 185 | 186 | // Compare distances and return accordingly 187 | return Integer.compare(distanceSquared1, distanceSquared2); 188 | }); 189 | 190 | // Create a task to send a chunk to the player every tick 191 | blockChangeTasks.put(onlinePlayer, Bukkit.getScheduler().runTaskTimer(Blockify.getInstance(), () -> { 192 | // Check if player is online, if not, cancel the task 193 | if (!onlinePlayer.isOnline()) { 194 | blockChangeTasks.computeIfPresent(onlinePlayer, (key, task) -> { 195 | task.cancel(); 196 | return null; 197 | }); 198 | return; 199 | } 200 | 201 | // Loop through chunks per tick 202 | for (int i = 0; i < stage.getChunksPerTick(); i++) { 203 | // If the chunk index is greater than the chunks to send length 204 | if (chunkIndex.get() >= chunksToSend.size()) { 205 | // Safely cancel the task and remove it from the map 206 | blockChangeTasks.computeIfPresent(onlinePlayer, (key, task) -> { 207 | task.cancel(); 208 | return null; // Remove the task 209 | }); 210 | return; 211 | } 212 | 213 | // Get the chunk from the chunks to send array 214 | BlockifyChunk chunk = chunksToSend.get(chunkIndex.get()); 215 | chunkIndex.getAndIncrement(); 216 | 217 | // Check if the chunk is loaded; if not, return 218 | if (!stage.getWorld().isChunkLoaded(chunk.x(), chunk.z())) continue; 219 | 220 | // Send the chunk packet to the player 221 | Bukkit.getScheduler().runTaskAsynchronously(Blockify.getInstance(), () -> sendChunkPacket(stage, onlinePlayer, chunk, unload)); 222 | } 223 | }, 0L, 1L)); 224 | } 225 | } 226 | 227 | /** 228 | * Sends a chunk packet to the player. 229 | * This method submits the task to the thread pool for asynchronous execution. 230 | * 231 | * @param stage the stage 232 | * @param player the player 233 | * @param chunk the chunk 234 | * @param unload whether to unload the chunk 235 | */ 236 | public void sendChunkPacket(Stage stage, Player player, BlockifyChunk chunk, boolean unload) { 237 | executorService.submit(() -> processAndSendChunk(stage, player, chunk, unload)); 238 | } 239 | 240 | /** 241 | * Processes the chunk and sends the packet to the player. 242 | * 243 | * @param stage the stage 244 | * @param player the player 245 | * @param chunk the chunk 246 | * @param unload whether to unload the chunk 247 | */ 248 | private void processAndSendChunk(Stage stage, Player player, BlockifyChunk chunk, boolean unload) { 249 | try { 250 | User packetUser = PacketEvents.getAPI().getPlayerManager().getUser(player); 251 | int ySections = packetUser.getTotalWorldHeight() >> 4; 252 | Map blockData = null; 253 | 254 | if (!unload) { 255 | Map> blockChanges = getBlockChanges(stage, Collections.singleton(chunk)); 256 | blockData = blockChanges.get(chunk); 257 | } 258 | 259 | Map blockDataToState = new HashMap<>(); 260 | List chunks = new ArrayList<>(ySections); 261 | Chunk bukkitChunk = player.getWorld().getChunkAt(chunk.x(), chunk.z()); 262 | ChunkSnapshot chunkSnapshot = bukkitChunk.getChunkSnapshot(); 263 | int maxHeight = player.getWorld().getMaxHeight(); 264 | int minHeight = player.getWorld().getMinHeight(); 265 | 266 | // Pre-fetch default block data for the entire chunk to reduce calls to getBlockData() 267 | BlockData[][][][] defaultBlockData = new BlockData[ySections][16][16][16]; 268 | for (int section = 0; section < ySections; section++) { 269 | int baseY = (section << 4) + minHeight; 270 | for (int x = 0; x < 16; x++) { 271 | for (int y = 0; y < 16; y++) { 272 | int worldY = baseY + y; 273 | if (worldY >= minHeight && worldY < maxHeight) { 274 | for (int z = 0; z < 16; z++) { 275 | defaultBlockData[section][x][y][z] = chunkSnapshot.getBlockData(x, worldY, z); 276 | } 277 | } 278 | } 279 | } 280 | } 281 | 282 | // Predefined full light array and bitsets to avoid recreating them for each chunk 283 | byte[] fullLightSection = new byte[2048]; 284 | Arrays.fill(fullLightSection, (byte) 0xFF); 285 | byte[][] fullLightArray = new byte[ySections][]; 286 | BitSet fullBitSet = new BitSet(ySections); 287 | for (int i = 0; i < ySections; i++) { 288 | fullLightArray[i] = fullLightSection; 289 | fullBitSet.set(i); 290 | } 291 | BitSet emptyBitSet = new BitSet(ySections); 292 | 293 | // Process each chunk section 294 | for (int section = 0; section < ySections; section++) { 295 | Chunk_v1_18 baseChunk = new Chunk_v1_18(); 296 | 297 | // Use primitive long keys for block positions to reduce object creation 298 | long baseY = (section << 4) + minHeight; 299 | for (int x = 0; x < 16; x++) { 300 | for (int y = 0; y < 16; y++) { 301 | long worldY = baseY + y; 302 | if (worldY >= minHeight && worldY < maxHeight) { 303 | for (int z = 0; z < 16; z++) { 304 | long positionKey = (((x + ((long) chunk.x() << 4)) & 0x3FFFFFF) << 38) 305 | | ((worldY & 0xFFF) << 26) 306 | | ((z + ((long) chunk.z() << 4)) & 0x3FFFFFF); 307 | BlockData data = null; 308 | 309 | if (!unload && blockData != null) { 310 | data = blockData.get(positionKey); 311 | } 312 | 313 | if (data == null) { 314 | data = defaultBlockData[section][x][y][z]; 315 | } 316 | 317 | WrappedBlockState state = blockDataToState.computeIfAbsent(data, SpigotConversionUtil::fromBukkitBlockData); 318 | baseChunk.set(x, y, z, state); 319 | } 320 | } 321 | } 322 | } 323 | 324 | // Set biome data for the chunk section 325 | int biomeId = baseChunk.getBiomeData().palette.stateToId(1); 326 | int storageSize = baseChunk.getBiomeData().storage.getData().length; 327 | for (int index = 0; index < storageSize; index++) { 328 | baseChunk.getBiomeData().storage.set(index, biomeId); 329 | } 330 | 331 | chunks.add(baseChunk); 332 | } 333 | 334 | // Reuse pre-created light data 335 | LightData lightData = new LightData(); 336 | lightData.setBlockLightArray(fullLightArray); 337 | lightData.setSkyLightArray(fullLightArray); 338 | lightData.setBlockLightCount(ySections); 339 | lightData.setSkyLightCount(ySections); 340 | lightData.setBlockLightMask(fullBitSet); 341 | lightData.setSkyLightMask(fullBitSet); 342 | lightData.setEmptyBlockLightMask(emptyBitSet); 343 | lightData.setEmptySkyLightMask(emptyBitSet); 344 | 345 | Column column = new Column(chunk.x(), chunk.z(), true, chunks.toArray(new BaseChunk[0]), null); 346 | WrapperPlayServerChunkData chunkData = new WrapperPlayServerChunkData(column, lightData); 347 | packetUser.sendPacketSilently(chunkData); 348 | } catch (Exception e) { 349 | // Handle exceptions appropriately, possibly logging them 350 | e.printStackTrace(); 351 | } 352 | } 353 | 354 | private Map> getBlockChanges(Stage stage, Collection chunks) { 355 | Map> blockChanges = new HashMap<>(); 356 | Map> highestZIndexes = new HashMap<>(); 357 | 358 | for (View view : stage.getViews()) { 359 | int zIndex = view.getZIndex(); 360 | for (Map.Entry> entry : view.getBlocks().entrySet()) { 361 | BlockifyChunk chunk = entry.getKey(); 362 | if (!chunks.contains(chunk)) continue; 363 | 364 | highestZIndexes.computeIfAbsent(chunk, k -> new HashMap<>()); 365 | Map chunkZIndexes = highestZIndexes.get(chunk); 366 | Map chunkBlockChanges = blockChanges.computeIfAbsent(chunk, k -> new HashMap<>()); 367 | 368 | for (Map.Entry positionEntry : entry.getValue().entrySet()) { 369 | BlockifyPosition positionKey = positionEntry.getKey(); 370 | BlockData blockData = positionEntry.getValue(); 371 | 372 | chunkZIndexes.compute(PositionKeyUtil.getPositionKey(positionKey.getX(), positionKey.getY(), positionKey.getZ()), (key, currentMaxZIndex) -> { 373 | if (currentMaxZIndex == null || zIndex > currentMaxZIndex) { 374 | chunkBlockChanges.put(PositionKeyUtil.getPositionKey(positionKey.getX(), positionKey.getY(), positionKey.getZ()), blockData); 375 | return zIndex; 376 | } else if (zIndex == currentMaxZIndex) { 377 | chunkBlockChanges.put(PositionKeyUtil.getPositionKey(positionKey.getX(), positionKey.getY(), positionKey.getZ()), blockData); 378 | } 379 | return currentMaxZIndex; 380 | }); 381 | } 382 | } 383 | } 384 | return blockChanges; 385 | } 386 | 387 | /** 388 | * Shutdown the executor service gracefully. 389 | */ 390 | public void shutdown() { 391 | executorService.shutdown(); 392 | try { 393 | if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) { 394 | executorService.shutdownNow(); 395 | if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) 396 | System.err.println("Executor service did not terminate"); 397 | } 398 | } catch (InterruptedException e) { 399 | executorService.shutdownNow(); 400 | Thread.currentThread().interrupt(); 401 | } 402 | } 403 | } -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/managers/StageManager.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.managers; 2 | 3 | import codes.kooper.blockify.Blockify; 4 | import codes.kooper.blockify.events.CreateStageEvent; 5 | import codes.kooper.blockify.events.DeleteStageEvent; 6 | import codes.kooper.blockify.models.Stage; 7 | import lombok.Getter; 8 | import org.bukkit.Bukkit; 9 | import org.bukkit.entity.Player; 10 | 11 | import java.util.ArrayList; 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | @Getter 17 | public class StageManager { 18 | private final Map stages; 19 | 20 | public StageManager() { 21 | this.stages = new HashMap<>(); 22 | } 23 | 24 | /** 25 | * Create a new stage 26 | * @param stage Stage to create 27 | */ 28 | public void createStage(Stage stage) { 29 | if (stages.containsKey(stage.getName())) { 30 | Blockify.getInstance().getLogger().warning("Stage with name " + stage.getName() + " already exists!"); 31 | return; 32 | } 33 | Bukkit.getScheduler().runTask(Blockify.getInstance(), () -> new CreateStageEvent(stage).callEvent()); 34 | stages.put(stage.getName(), stage); 35 | } 36 | 37 | /** 38 | * Get a stage by name 39 | * @param name Name of the stage 40 | * @return Stage 41 | */ 42 | public Stage getStage(String name) { 43 | return stages.get(name); 44 | } 45 | 46 | /** 47 | * Delete a stage by name 48 | * @param name Name of the stage 49 | */ 50 | public void deleteStage(String name) { 51 | new DeleteStageEvent(stages.get(name)).callEvent(); 52 | stages.remove(name); 53 | } 54 | 55 | /** 56 | * Check if a stage exists 57 | * @param name Name of the stage 58 | * @return boolean 59 | */ 60 | public boolean hasStage(String name) { 61 | return stages.containsKey(name); 62 | } 63 | 64 | /** 65 | * Get all stages 66 | * @return List of stages 67 | */ 68 | public List getStages(Player player) { 69 | List stages = new ArrayList<>(); 70 | for (Stage stage : this.stages.values()) { 71 | if (stage.getAudience().getPlayers().contains(player.getUniqueId())) { 72 | stages.add(stage); 73 | } 74 | } 75 | return stages; 76 | } 77 | } -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/models/Audience.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.models; 2 | 3 | import codes.kooper.blockify.Blockify; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import org.bukkit.entity.Player; 7 | 8 | import java.util.*; 9 | import java.util.stream.Collectors; 10 | 11 | @Setter 12 | @Getter 13 | public class Audience { 14 | private boolean arePlayersHidden; 15 | private final Set players; 16 | private final Map miningSpeeds; 17 | 18 | /** 19 | * @param players The set of players 20 | */ 21 | public static Audience fromPlayers(Set players) { 22 | return new Audience(players.stream().map(Player::getUniqueId).collect(Collectors.toSet()), false); 23 | } 24 | 25 | /** 26 | * @param players The set of players 27 | */ 28 | public static Audience fromUUIDs(Set players) { 29 | return new Audience(players, false); 30 | } 31 | 32 | /** 33 | * @param players The set of players 34 | * @param arePlayersHidden Whether the players are hidden 35 | */ 36 | public static Audience fromPlayers(Set players, boolean arePlayersHidden) { 37 | return new Audience(players.stream().map(Player::getUniqueId).collect(Collectors.toSet()), arePlayersHidden); 38 | } 39 | 40 | /** 41 | * @param players The set of players 42 | * @param arePlayersHidden Whether the players are hidden 43 | */ 44 | public static Audience fromUUIDs(Set players, boolean arePlayersHidden) { 45 | return new Audience(players, arePlayersHidden); 46 | } 47 | 48 | /** 49 | * @param players The set of players 50 | * @param arePlayersHidden Whether the players are hidden 51 | */ 52 | private Audience(Set players, boolean arePlayersHidden) { 53 | this.players = players; 54 | this.arePlayersHidden = arePlayersHidden; 55 | this.miningSpeeds = new HashMap<>(); 56 | } 57 | 58 | /** 59 | * @param player The player to add 60 | * @return The set of uuids of players 61 | */ 62 | public Set addPlayer(Player player) { 63 | return addPlayer(player.getUniqueId()); 64 | } 65 | 66 | /** 67 | * @param player The uuid of a player to add 68 | * @return The set of uuids of players 69 | */ 70 | public Set addPlayer(UUID player) { 71 | players.add(player); 72 | return players; 73 | } 74 | 75 | /** 76 | * @param player The player to remove 77 | * @return The set of uuids of players 78 | */ 79 | public Set removePlayer(Player player) { 80 | return removePlayer(player.getUniqueId()); 81 | } 82 | 83 | /** 84 | * @param player The uuid of a player to remove 85 | * @return The set of uuids of players 86 | */ 87 | public Set removePlayer(UUID player) { 88 | players.remove(player); 89 | return players; 90 | } 91 | 92 | /** 93 | * @return A set of online players in the audience 94 | */ 95 | public Set getOnlinePlayers() { 96 | List onlinePlayers = new ArrayList<>(); 97 | for (UUID player : players) { 98 | Player p = Blockify.getInstance().getServer().getPlayer(player); 99 | if (p != null) { 100 | onlinePlayers.add(p); 101 | } 102 | } 103 | return new HashSet<>(onlinePlayers); 104 | } 105 | 106 | 107 | /** 108 | * Sets the mining speed for a player 109 | * @param player The player 110 | * @param speed The speed 111 | */ 112 | public void setMiningSpeed(Player player, float speed) { 113 | setMiningSpeed(player.getUniqueId(), speed); 114 | } 115 | 116 | /** 117 | * Sets the mining speed for a player 118 | * @param player The player's UUID 119 | * @param speed The speed 120 | */ 121 | public void setMiningSpeed(UUID player, float speed) { 122 | if (speed < 0 || speed == 1) { 123 | Blockify.getInstance().getLogger().warning("Invalid mining speed for player " + player + ": " + speed); 124 | return; 125 | } 126 | miningSpeeds.put(player, speed); 127 | } 128 | 129 | /** 130 | * Resets the mining speed for a player 131 | * @param player The player 132 | */ 133 | public void resetMiningSpeed(Player player) { 134 | resetMiningSpeed(player.getUniqueId()); 135 | } 136 | 137 | /** 138 | * Resets the mining speed for a player 139 | * @param player The player's UUID 140 | */ 141 | public void resetMiningSpeed(UUID player) { 142 | miningSpeeds.remove(player); 143 | } 144 | 145 | /** 146 | * Gets the mining speed of a player 147 | * @param player The player 148 | * @return The mining speed 149 | */ 150 | public float getMiningSpeed(Player player) { 151 | return getMiningSpeed(player.getUniqueId()); 152 | } 153 | 154 | /** 155 | * Gets the mining speed of a player 156 | * @param player The player's UUID 157 | * @return The mining speed 158 | */ 159 | public float getMiningSpeed(UUID player) { 160 | return miningSpeeds.getOrDefault(player, 1f); 161 | } 162 | 163 | } -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/models/Pattern.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.models; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | import org.bukkit.block.data.BlockData; 6 | 7 | import java.util.Map; 8 | import java.util.NavigableMap; 9 | import java.util.TreeMap; 10 | import java.util.function.Predicate; 11 | 12 | @Getter 13 | @Setter 14 | public class Pattern { 15 | private final NavigableMap blockDataMap = new TreeMap<>(); 16 | private double totalWeight; 17 | 18 | /** 19 | * Creates a new Pattern with the given BlockData and their respective percentages. 20 | * 21 | * @param blockDataPercentages A map of BlockData and their respective percentages. 22 | * @throws IllegalArgumentException If the percentage values are not in the range [0.0, 1.0], 23 | * if the map is empty, or if the sum of percentages exceeds 1.0. 24 | */ 25 | public Pattern(Map blockDataPercentages) { 26 | Predicate inRange = value -> value >= 0.0 && value <= 1.0; 27 | 28 | if (!blockDataPercentages.values().stream().allMatch(inRange)) { 29 | throw new IllegalArgumentException("Percentage values must be in the range [0.0, 1.0]"); 30 | } 31 | 32 | if (blockDataPercentages.isEmpty()) { 33 | throw new IllegalArgumentException("Pattern must contain at least one BlockData with a non-zero percentage"); 34 | } 35 | 36 | double sum = blockDataPercentages.values().stream().mapToDouble(value -> value).sum(); 37 | if (Math.round(sum * 100000) / 100000.0 > 1.0) { 38 | throw new IllegalArgumentException("Sum of percentages must not exceed 1.0"); 39 | } 40 | 41 | for (Map.Entry entry : blockDataPercentages.entrySet()) { 42 | totalWeight += entry.getValue(); 43 | blockDataMap.put(totalWeight, entry.getKey()); 44 | } 45 | } 46 | 47 | /** 48 | * Returns a random BlockData from the Pattern based on the percentages. 49 | * 50 | * @return A random BlockData from the Pattern. 51 | */ 52 | public BlockData getRandomBlockData() { 53 | double random = Math.random() * totalWeight; 54 | return blockDataMap.higherEntry(random).getValue(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/models/Stage.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.models; 2 | 3 | import codes.kooper.blockify.Blockify; 4 | import codes.kooper.blockify.types.BlockifyChunk; 5 | import codes.kooper.blockify.types.BlockifyPosition; 6 | import lombok.Getter; 7 | import lombok.Setter; 8 | import org.bukkit.Location; 9 | import org.bukkit.World; 10 | 11 | import java.util.HashSet; 12 | import java.util.Set; 13 | import java.util.stream.Collectors; 14 | 15 | @Getter 16 | @Setter 17 | public class Stage { 18 | private final String name; 19 | private final World world; 20 | private BlockifyPosition maxPosition, minPosition; 21 | private final Set views; 22 | private int chunksPerTick; 23 | private final Audience audience; 24 | 25 | /** 26 | * Create a new stage. 27 | * 28 | * @param name Name of the stage 29 | * @param world World the stage is in 30 | * @param pos1 First position of the stage 31 | * @param pos2 Second position of the stage 32 | * @param audience Audience to send blocks to 33 | */ 34 | public Stage(String name, World world, BlockifyPosition pos1, BlockifyPosition pos2, Audience audience) { 35 | this.name = name; 36 | this.world = world; 37 | this.maxPosition = new BlockifyPosition(Math.max(pos1.getX(), pos2.getX()), Math.max(pos1.getY(), pos2.getY()), Math.max(pos1.getZ(), pos2.getZ())); 38 | this.minPosition = new BlockifyPosition(Math.min(pos1.getX(), pos2.getX()), Math.min(pos1.getY(), pos2.getY()), Math.min(pos1.getZ(), pos2.getZ())); 39 | this.views = new HashSet<>(); 40 | this.audience = audience; 41 | this.chunksPerTick = 1; 42 | } 43 | 44 | 45 | /** 46 | * Check if a location is within the stage. 47 | * 48 | * @param location Location to check 49 | * @return True if the location is within the stage 50 | */ 51 | public boolean isLocationWithin(Location location) { 52 | return location.getWorld().equals(world) && location.getBlockX() >= minPosition.getX() && location.getBlockX() <= maxPosition.getX() && location.getBlockY() >= minPosition.getY() && location.getBlockY() <= maxPosition.getY() && location.getBlockZ() >= minPosition.getZ() && location.getBlockZ() <= maxPosition.getZ(); 53 | } 54 | 55 | /** 56 | * Send blocks to the audience. Should be called asynchronously. 57 | */ 58 | public void sendBlocksToAudience() { 59 | Blockify.getInstance().getBlockChangeManager().sendBlockChanges(this, audience, getChunks()); 60 | } 61 | 62 | /** 63 | * Refresh blocks to the audience. Should be called after modifying blocks. 64 | * Should be called asynchronously. 65 | * 66 | * @param blocks Blocks to refresh to the audience. 67 | */ 68 | public void refreshBlocksToAudience(Set blocks) { 69 | Set chunks = blocks.stream().map(BlockifyPosition::toBlockifyChunk).collect(Collectors.toSet()); 70 | Blockify.getInstance().getBlockChangeManager().sendBlockChanges(this, audience, chunks); 71 | } 72 | 73 | /** 74 | * Add a view to the stage. 75 | * 76 | * @param view View to add 77 | */ 78 | public void addView(View view) { 79 | if (views.stream().anyMatch(v -> v.getName().equalsIgnoreCase(view.getName()))) { 80 | Blockify.getInstance().getLogger().warning("View with name " + view.getName() + " already exists in stage " + name + "!"); 81 | return; 82 | } 83 | views.add(view); 84 | } 85 | 86 | /** 87 | * Remove a view from the stage. 88 | * 89 | * @param view View to remove 90 | */ 91 | public void removeView(View view) { 92 | views.remove(view); 93 | } 94 | 95 | /** 96 | * Get a view by name. 97 | * 98 | * @param name Name of the view 99 | * @return View or null if not found 100 | */ 101 | public View getView(String name) { 102 | for (View view : views) { 103 | if (view.getName().equalsIgnoreCase(name)) { 104 | return view; 105 | } 106 | } 107 | return null; 108 | } 109 | 110 | /** 111 | * Get all chunks that are being used by this stage. 112 | * If a lot of chunks are present, it is recommended to use this method asynchronously. 113 | * 114 | * @return Set of chunks 115 | */ 116 | public Set getChunks() { 117 | Set chunks = new HashSet<>(); 118 | for (int x = minPosition.getX() >> 4; x <= maxPosition.getX() >> 4; x++) { 119 | for (int z = minPosition.getZ() >> 4; z <= maxPosition.getZ() >> 4; z++) { 120 | chunks.add(new BlockifyChunk(x, z)); 121 | } 122 | } 123 | return chunks; 124 | } 125 | } -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/models/View.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.models; 2 | 3 | import codes.kooper.blockify.types.BlockifyChunk; 4 | import codes.kooper.blockify.types.BlockifyPosition; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import org.bukkit.block.data.BlockData; 8 | 9 | import java.util.Set; 10 | import java.util.concurrent.ConcurrentHashMap; 11 | 12 | @Getter 13 | @Setter 14 | public class View { 15 | private final ConcurrentHashMap> blocks; 16 | private final Stage stage; 17 | private final String name; 18 | private int zIndex; 19 | private boolean breakable, placeable; 20 | private Pattern pattern; 21 | 22 | /** 23 | * Constructor for the View class. 24 | * 25 | * @param name The name of the view. 26 | * @param stage The stage the view is in. 27 | * @param pattern The pattern of the view. 28 | * @param breakable Whether the view is breakable or not. 29 | */ 30 | public View(String name, Stage stage, Pattern pattern, boolean breakable) { 31 | this.name = name; 32 | this.blocks = new ConcurrentHashMap<>(); 33 | this.stage = stage; 34 | this.breakable = breakable; 35 | this.pattern = pattern; 36 | this.zIndex = 0; 37 | } 38 | 39 | /** 40 | * Get the highest block at a given x and z coordinate. 41 | * 42 | * @param x The x coordinate. 43 | * @param z The z coordinate. 44 | * @return The highest block at the given x and z coordinate. 45 | */ 46 | public BlockifyPosition getHighestBlock(int x, int z) { 47 | for (int y = stage.getMaxPosition().getY(); y >= stage.getMinPosition().getY(); y--) { 48 | BlockifyPosition position = new BlockifyPosition(x, y, z); 49 | if (hasBlock(position) && getBlock(position).getMaterial().isSolid()) { 50 | return position; 51 | } 52 | } 53 | return null; 54 | } 55 | 56 | /** 57 | * Get the lowest block at a given x and z coordinate. 58 | * 59 | * @param x The x coordinate. 60 | * @param z The z coordinate. 61 | * @return The lowest block at the given x and z coordinate. 62 | */ 63 | public BlockifyPosition getLowestBlock(int x, int z) { 64 | for (int y = stage.getMinPosition().getY(); y <= stage.getMaxPosition().getY(); y++) { 65 | BlockifyPosition position = new BlockifyPosition(x, y, z); 66 | if (hasBlock(position) && getBlock(position).getMaterial().isSolid()) { 67 | return position; 68 | } 69 | } 70 | return null; 71 | } 72 | 73 | // Returns all blocks in the view 74 | public ConcurrentHashMap> getBlocks() { 75 | return new ConcurrentHashMap<>(blocks); 76 | } 77 | 78 | /** 79 | * Remove a block from the view. 80 | * 81 | * @param position The block to remove. 82 | */ 83 | public void removeBlock(BlockifyPosition position) { 84 | if (hasBlock(position)) { 85 | blocks.get(position.toBlockifyChunk()).remove(position); 86 | if (blocks.get(position.toBlockifyChunk()).isEmpty()) { 87 | blocks.remove(position.toBlockifyChunk()); 88 | } 89 | } 90 | } 91 | 92 | /** 93 | * Remove a set of blocks from the view. 94 | * Call this method asynchronously if you are removing a large number of blocks. 95 | * 96 | * @param positions The set of blocks to remove. 97 | */ 98 | public void removeBlocks(Set positions) { 99 | for (BlockifyPosition position : positions) { 100 | removeBlock(position); 101 | } 102 | } 103 | 104 | /** 105 | * Remove all blocks from the view. 106 | */ 107 | public void removeAllBlocks() { 108 | blocks.clear(); 109 | } 110 | 111 | /** 112 | * Add a block to the view. 113 | * 114 | * @param position The block to add. 115 | */ 116 | public void addBlock(BlockifyPosition position) { 117 | if (!blocks.containsKey(position.toBlockifyChunk())) { 118 | blocks.put(position.toBlockifyChunk(), new ConcurrentHashMap<>()); 119 | } 120 | blocks.get(position.toBlockifyChunk()).put(position, pattern.getRandomBlockData()); 121 | } 122 | 123 | /** 124 | * Add a set of blocks to the view. 125 | * Call this method asynchronously if you are adding a large number of blocks. 126 | * 127 | * @param positions The set of blocks to add. 128 | */ 129 | public void addBlocks(Set positions) { 130 | for (BlockifyPosition position : positions) { 131 | addBlock(position); 132 | } 133 | } 134 | 135 | /** 136 | * Check if a block is in the view. 137 | * 138 | * @param position The position of the block. 139 | * @return Whether the block is in the view. 140 | */ 141 | public boolean hasBlock(BlockifyPosition position) { 142 | return blocks.containsKey(position.toBlockifyChunk()) && blocks.get(position.toBlockifyChunk()).containsKey(position); 143 | } 144 | 145 | /** 146 | * Check if a set of blocks are in the view. 147 | * Call this method asynchronously if you are checking a large number of blocks. 148 | * 149 | * @param positions The set of blocks to check. 150 | * @return Whether the set of blocks are in the view. 151 | */ 152 | public boolean hasBlocks(Set positions) { 153 | for (BlockifyPosition position : positions) { 154 | if (!hasBlock(position)) { 155 | return false; 156 | } 157 | } 158 | return true; 159 | } 160 | 161 | /** 162 | * Get the block data at a given position. 163 | * 164 | * @param position The position of the block. 165 | * @return The block data at the given position. 166 | */ 167 | public BlockData getBlock(BlockifyPosition position) { 168 | return blocks.get(position.toBlockifyChunk()).get(position); 169 | } 170 | 171 | 172 | /** 173 | * Check if a chunk is in the view. 174 | * 175 | * @param x The x coordinate of the chunk. 176 | * @param z The z coordinate of the chunk. 177 | * @return Whether the chunk is in the view. 178 | */ 179 | public boolean hasChunk(int x, int z) { 180 | return blocks.containsKey(new BlockifyChunk(x, z)); 181 | } 182 | 183 | /** 184 | * Set positions to a given block data. 185 | * Call this method asynchronously if you are setting a large number of blocks. 186 | * 187 | * @param positions The set of positions to set. 188 | * @param blockData The block data. 189 | */ 190 | public void setBlocks(Set positions, BlockData blockData) { 191 | for (BlockifyPosition position : positions) { 192 | setBlock(position, blockData); 193 | } 194 | } 195 | 196 | /** 197 | * Set a position to a given block data. 198 | * 199 | * @param position The position of the block. 200 | * @param blockData The block data. 201 | */ 202 | public void setBlock(BlockifyPosition position, BlockData blockData) { 203 | if (hasBlock(position)) { 204 | blocks.get(position.toBlockifyChunk()).put(position, blockData); 205 | } 206 | } 207 | 208 | /** 209 | * Reset a block to a random block data from the pattern. 210 | * 211 | * @param position The position of the block. 212 | */ 213 | public void resetBlock(BlockifyPosition position) { 214 | if (hasBlock(position)) { 215 | blocks.get(position.toBlockifyChunk()).put(position, pattern.getRandomBlockData()); 216 | } 217 | } 218 | 219 | /** 220 | * Reset a set of blocks to random block data from the pattern. 221 | * Call this method asynchronously if you are resetting a large number of blocks. 222 | * 223 | * @param positions The set of blocks to reset. 224 | */ 225 | public void resetBlocks(Set positions) { 226 | for (BlockifyPosition position : positions) { 227 | resetBlock(position); 228 | } 229 | } 230 | 231 | /** 232 | * Reset all blocks in the view to random block data from the pattern. 233 | * Call this method asynchronously. 234 | */ 235 | public void resetViewBlocks() { 236 | for (BlockifyChunk chunk : blocks.keySet()) { 237 | for (BlockifyPosition position : blocks.get(chunk).keySet()) { 238 | blocks.get(chunk).put(position, pattern.getRandomBlockData()); 239 | } 240 | } 241 | } 242 | 243 | /** 244 | * Changes the pattern of the view. 245 | * 246 | * @param pattern The new pattern. 247 | */ 248 | public void changePattern(Pattern pattern) { 249 | this.pattern = pattern; 250 | } 251 | } -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/protocol/BlockDigAdapter.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.protocol; 2 | 3 | import codes.kooper.blockify.Blockify; 4 | import codes.kooper.blockify.events.BlockifyBreakEvent; 5 | import codes.kooper.blockify.events.BlockifyInteractEvent; 6 | import codes.kooper.blockify.models.Stage; 7 | import codes.kooper.blockify.models.View; 8 | import codes.kooper.blockify.types.BlockifyPosition; 9 | import com.github.retrooper.packetevents.event.SimplePacketListenerAbstract; 10 | import com.github.retrooper.packetevents.event.simple.PacketPlayReceiveEvent; 11 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 12 | import com.github.retrooper.packetevents.protocol.player.DiggingAction; 13 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerDigging; 14 | import org.bukkit.Bukkit; 15 | import org.bukkit.GameMode; 16 | import org.bukkit.Material; 17 | import org.bukkit.block.data.BlockData; 18 | import org.bukkit.entity.Player; 19 | 20 | import java.util.List; 21 | 22 | public class BlockDigAdapter extends SimplePacketListenerAbstract { 23 | 24 | @Override 25 | public void onPacketPlayReceive(PacketPlayReceiveEvent event) { 26 | if (event.getPacketType() == PacketType.Play.Client.PLAYER_DIGGING) { 27 | // Packet wrapper 28 | WrapperPlayClientPlayerDigging wrapper = new WrapperPlayClientPlayerDigging(event); 29 | DiggingAction actionType = wrapper.getAction(); 30 | 31 | // Extract information from wrapper 32 | Player player = (Player) event.getPlayer(); 33 | 34 | // Get stages the player is in. If the player is not in any stages, return. 35 | List stages = Blockify.getInstance().getStageManager().getStages(player); 36 | if (stages == null || stages.isEmpty()) { 37 | return; 38 | } 39 | 40 | BlockifyPosition position = new BlockifyPosition(wrapper.getBlockPosition().getX(), wrapper.getBlockPosition().getY(), wrapper.getBlockPosition().getZ()); 41 | 42 | // Find the block in any stage and view using streams 43 | stages.stream() 44 | .filter(stage -> stage.getWorld() == player.getWorld()) 45 | .flatMap(stage -> stage.getViews().stream()) 46 | .filter(view -> view.hasBlock(position)).min((view1, view2) -> Integer.compare(view2.getZIndex(), view1.getZIndex())) 47 | .ifPresent(view -> { 48 | // Get block data from view 49 | BlockData blockData = view.getBlock(position); 50 | 51 | // Call BlockifyInteractEvent to handle custom interaction 52 | Bukkit.getScheduler().runTask(Blockify.getInstance(), () -> new BlockifyInteractEvent(player, position, blockData, view, view.getStage()).callEvent()); 53 | 54 | // Check if block is breakable, if not, send block change packet to cancel the break 55 | if (!view.isBreakable()) { 56 | event.setCancelled(true); 57 | return; 58 | } 59 | 60 | // Block break functionality 61 | if (actionType == DiggingAction.FINISHED_DIGGING || canInstantBreak(player, blockData)) { 62 | Bukkit.getScheduler().runTask(Blockify.getInstance(), () -> { 63 | // Call BlockifyBreakEvent 64 | BlockifyBreakEvent blockifyBreakEvent = new BlockifyBreakEvent(player, position, blockData, view, view.getStage()); 65 | blockifyBreakEvent.callEvent(); 66 | 67 | // Set to air 68 | player.sendBlockChange(position.toLocation(player.getWorld()), Material.AIR.createBlockData()); 69 | view.setBlock(position, Material.AIR.createBlockData()); 70 | 71 | // If block is not cancelled, break the block, otherwise, revert the block 72 | if (blockifyBreakEvent.isCancelled()) { 73 | player.sendBlockChange(position.toLocation(player.getWorld()), blockData); 74 | view.setBlock(position, blockData); 75 | } 76 | }); 77 | } 78 | }); 79 | } 80 | } 81 | 82 | /** 83 | * Check if player can instantly break block 84 | * 85 | * @param player Player who is digging 86 | * @param blockData BlockData of the block 87 | * @return boolean 88 | */ 89 | private boolean canInstantBreak(Player player, BlockData blockData) { 90 | return blockData.getDestroySpeed(player.getInventory().getItemInMainHand(), true) >= blockData.getMaterial().getHardness() * 30 || player.getGameMode() == GameMode.CREATIVE; 91 | } 92 | } -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/protocol/BlockPlaceAdapter.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.protocol; 2 | 3 | import codes.kooper.blockify.Blockify; 4 | import codes.kooper.blockify.events.BlockifyPlaceEvent; 5 | import codes.kooper.blockify.models.Stage; 6 | import codes.kooper.blockify.models.View; 7 | import codes.kooper.blockify.types.BlockifyPosition; 8 | import com.github.retrooper.packetevents.event.SimplePacketListenerAbstract; 9 | import com.github.retrooper.packetevents.event.simple.PacketPlayReceiveEvent; 10 | import com.github.retrooper.packetevents.event.simple.PacketPlaySendEvent; 11 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 12 | import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientPlayerBlockPlacement; 13 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerBlockChange; 14 | import org.bukkit.Bukkit; 15 | import org.bukkit.entity.Player; 16 | 17 | import java.util.List; 18 | 19 | public class BlockPlaceAdapter extends SimplePacketListenerAbstract { 20 | 21 | @Override 22 | public void onPacketPlayReceive(PacketPlayReceiveEvent event) { 23 | if (event.getPacketType() == PacketType.Play.Client.PLAYER_BLOCK_PLACEMENT) { 24 | // Wrapper for the packet 25 | WrapperPlayClientPlayerBlockPlacement wrapper = new WrapperPlayClientPlayerBlockPlacement(event); 26 | Player player = (Player) event.getPlayer(); 27 | 28 | // Get the stages the player is in. If the player is not in any stages, return. 29 | List stages = Blockify.getInstance().getStageManager().getStages(player); 30 | if (stages == null || stages.isEmpty()) { 31 | return; 32 | } 33 | 34 | BlockifyPosition position = new BlockifyPosition(wrapper.getBlockPosition().getX(), wrapper.getBlockPosition().getY(), wrapper.getBlockPosition().getZ()); 35 | 36 | // Check if the block is in any of the views in the stages 37 | for (Stage stage : stages) { 38 | for (View view : stage.getViews()) { 39 | if (view.hasBlock(position)) { 40 | // Call the event and cancel the placement 41 | Bukkit.getScheduler().runTask(Blockify.getInstance(), () -> new BlockifyPlaceEvent(player, position, view, stage).callEvent()); 42 | event.setCancelled(true); 43 | return; 44 | } 45 | } 46 | } 47 | } 48 | } 49 | 50 | @Override 51 | public void onPacketPlaySend(PacketPlaySendEvent event) { 52 | if (event.getPacketType() == PacketType.Play.Server.BLOCK_CHANGE) { 53 | WrapperPlayServerBlockChange wrapper = new WrapperPlayServerBlockChange(event); 54 | Player player = (Player) event.getPlayer(); 55 | 56 | // Get the stages the player is in. If the player is not in any stages, return. 57 | List stages = Blockify.getInstance().getStageManager().getStages(player); 58 | if (stages == null || stages.isEmpty()) { 59 | return; 60 | } 61 | 62 | BlockifyPosition position = new BlockifyPosition(wrapper.getBlockPosition().getX(), wrapper.getBlockPosition().getY(), wrapper.getBlockPosition().getZ()); 63 | for (Stage stage : stages) { 64 | for (View view : stage.getViews()) { 65 | if (view.hasBlock(position)) { 66 | if (wrapper.getBlockState().getType().getName().equalsIgnoreCase(view.getBlock(position).getMaterial().name())) continue; 67 | event.setCancelled(true); 68 | return; 69 | } 70 | } 71 | } 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/protocol/ChunkLoadAdapter.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.protocol; 2 | 3 | import codes.kooper.blockify.Blockify; 4 | import codes.kooper.blockify.models.Stage; 5 | import codes.kooper.blockify.types.BlockifyChunk; 6 | import com.github.retrooper.packetevents.event.SimplePacketListenerAbstract; 7 | import com.github.retrooper.packetevents.event.simple.PacketPlaySendEvent; 8 | import com.github.retrooper.packetevents.protocol.packettype.PacketType; 9 | import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerChunkData; 10 | import org.bukkit.Bukkit; 11 | import org.bukkit.entity.Player; 12 | 13 | import java.util.List; 14 | 15 | public class ChunkLoadAdapter extends SimplePacketListenerAbstract { 16 | 17 | @Override 18 | public void onPacketPlaySend(PacketPlaySendEvent event) { 19 | if (event.getPacketType() == PacketType.Play.Server.CHUNK_DATA) { 20 | Player player = (Player) event.getPlayer(); 21 | 22 | // Wrapper for the chunk data packet 23 | WrapperPlayServerChunkData chunkData = new WrapperPlayServerChunkData(event); 24 | int chunkX = chunkData.getColumn().getX(); 25 | int chunkZ = chunkData.getColumn().getZ(); 26 | 27 | // Get the stages the player is in. If the player is not in any stages, return. 28 | List stages = Blockify.getInstance().getStageManager().getStages(player); 29 | if (stages == null || stages.isEmpty()) { 30 | return; 31 | } 32 | 33 | // Loop through the stages and views to check if the chunk is in the view. 34 | for (Stage stage : stages) { 35 | 36 | // If the chunk is not in the world, return. 37 | if (!stage.getWorld().equals(player.getWorld())) return; 38 | 39 | if (stage.getChunks().contains(new BlockifyChunk(chunkX, chunkZ))) { 40 | BlockifyChunk blockifyChunk = new BlockifyChunk(chunkX, chunkZ); 41 | 42 | // Cancel the packet to prevent the player from seeing the chunk 43 | event.setCancelled(true); 44 | 45 | // Send the chunk packet to the player 46 | Bukkit.getServer().getScheduler().runTaskAsynchronously(Blockify.getInstance(), () -> Blockify.getInstance().getBlockChangeManager().sendChunkPacket(stage, player, blockifyChunk, false)); 47 | } 48 | } 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/types/BlockifyBlockStage.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.types; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class BlockifyBlockStage { 9 | private final int entityId; 10 | private byte stage; 11 | private long lastUpdated; 12 | private int task = 0; 13 | 14 | /** 15 | * Stores data for a block stage 16 | * @param stage The stage of the block 17 | * @param lastUpdated The last time the block was updated 18 | */ 19 | public BlockifyBlockStage(int entityId, byte stage, long lastUpdated) { 20 | this.entityId = entityId; 21 | this.stage = stage; 22 | this.lastUpdated = lastUpdated; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/types/BlockifyChunk.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.types; 2 | 3 | import org.bukkit.Chunk; 4 | 5 | /** 6 | * Simple class to represent a chunk. 7 | * @param x The x coordinate of the chunk. 8 | * @param z The z coordinate of the chunk. 9 | */ 10 | public record BlockifyChunk(int x, int z) { 11 | 12 | /** 13 | * Stringifies the chunk data. 14 | * 15 | * @return The string representation of the chunk. 16 | */ 17 | @Override 18 | public String toString() { 19 | return "BlockifyChunk{x=" + x + ", z=" + z + "}"; 20 | } 21 | 22 | /** 23 | * Calculate the hash code based on the x and z coordinates. 24 | * 25 | * @return The hash code. 26 | */ 27 | @Override 28 | public int hashCode() { 29 | return x * 31 + z; 30 | } 31 | 32 | /** 33 | * Get the chunk key. 34 | * 35 | * @return The chunk key. 36 | */ 37 | public long getChunkKey() { 38 | return Chunk.getChunkKey(x, z); 39 | } 40 | 41 | /** 42 | * Check if the object is equal to this chunk. 43 | * 44 | * @param o The object to check. 45 | * @return True if the object is equal to this chunk, false otherwise. 46 | */ 47 | @Override 48 | public boolean equals(Object o) { 49 | if (o == this) return true; 50 | if (!(o instanceof BlockifyChunk other)) return false; 51 | return this.x == other.x && this.z == other.z; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/types/BlockifyPosition.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.types; 2 | 3 | import io.papermc.paper.math.BlockPosition; 4 | import io.papermc.paper.math.Position; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | import org.bukkit.Location; 8 | import org.bukkit.World; 9 | import org.bukkit.block.BlockState; 10 | import org.bukkit.block.data.BlockData; 11 | import org.bukkit.util.Vector; 12 | 13 | import java.util.Set; 14 | import java.util.stream.Collectors; 15 | 16 | @Getter 17 | @Setter 18 | public class BlockifyPosition { 19 | private int x, y, z; 20 | 21 | /** 22 | * Create a new BlockifyPosition 23 | * 24 | * @param x The x coordinate 25 | * @param y The y coordinate 26 | * @param z The z coordinate 27 | */ 28 | public BlockifyPosition(int x, int y, int z) { 29 | this.x = x; 30 | this.y = y; 31 | this.z = z; 32 | } 33 | 34 | /** 35 | * Create a new BlockifyPosition 36 | * 37 | * @param location The location to create the BlockifyPosition from 38 | */ 39 | public static BlockifyPosition fromLocation(Location location) { 40 | return new BlockifyPosition(location.getBlockX(), location.getBlockY(), location.getBlockZ()); 41 | } 42 | 43 | /** 44 | * Create a new BlockifyPosition 45 | * 46 | * @param vector The vector to create the BlockifyPosition from 47 | */ 48 | public static BlockifyPosition fromVector(Vector vector) { 49 | return new BlockifyPosition(vector.getBlockX(), vector.getBlockY(), vector.getBlockZ()); 50 | } 51 | 52 | /** 53 | * Creates new BlockifyPositions 54 | * 55 | * @param locations The locations to create the BlockifyPosition from 56 | */ 57 | public static Set fromLocations(Set locations) { 58 | return locations.stream().map(BlockifyPosition::fromLocation).collect(Collectors.toSet()); 59 | } 60 | 61 | /** 62 | * Creates new BlockifyPositions 63 | * 64 | * @param blockPositions The block positions to create the BlockifyPosition from 65 | */ 66 | public static Set fromPositions(Set blockPositions) { 67 | return blockPositions.stream().map(BlockifyPosition::fromPosition).collect(Collectors.toSet()); 68 | } 69 | 70 | /** 71 | * Create a new BlockifyPosition 72 | * 73 | * @param position The position to create the BlockifyPosition from 74 | */ 75 | public static BlockifyPosition fromPosition(Position position) { 76 | return new BlockifyPosition(position.blockX(), position.blockY(), position.blockZ()); 77 | } 78 | 79 | /** 80 | * Converts the BlockifyPosition to a BlockPosition 81 | * 82 | * @return The BlockPosition representation of the BlockifyPosition 83 | */ 84 | public BlockPosition toBlockPosition() { 85 | return Position.block(x, y, z); 86 | } 87 | 88 | /** 89 | * Converts the BlockifyPosition to a BlockifyChunk 90 | * 91 | * @return The BlockifyChunk at the BlockifyPosition. 92 | */ 93 | public BlockifyChunk toBlockifyChunk() { 94 | return new BlockifyChunk(x >> 4, z >> 4); 95 | } 96 | 97 | /** 98 | * Converts the BlockifyPosition to a Location 99 | * 100 | * @param world The world to convert the BlockifyPosition to 101 | * @return The Location representation of the BlockifyPosition 102 | */ 103 | public Location toLocation(World world) { 104 | return new Location(world, x, y, z); 105 | } 106 | 107 | /** 108 | * Converts the BlockifyPosition to a Position 109 | * 110 | * @return The Position representation of the BlockifyPosition 111 | */ 112 | public Position toPosition() { 113 | return Position.block(x, y, z); 114 | } 115 | 116 | /** 117 | * Converts the BlockifyPosition to a Vector 118 | * 119 | * @return The Vector representation of the BlockifyPosition 120 | */ 121 | public Vector toVector() { 122 | return new Vector(x, y, z); 123 | } 124 | 125 | /** 126 | * Get the distance squared between two BlockifyPositions 127 | * 128 | * @param other The other BlockifyPosition 129 | * @return The distance squared between the two BlockifyPositions 130 | */ 131 | public double distanceSquared(BlockifyPosition other) { 132 | return Math.pow(x - other.x, 2) + Math.pow(y - other.y, 2) + Math.pow(z - other.z, 2); 133 | } 134 | 135 | /** 136 | * Get the block state at the BlockifyPosition 137 | * 138 | * @param world The world to get the block state from 139 | * @return The block state at the BlockifyPosition 140 | */ 141 | public BlockState getBlockState(World world, BlockData blockData) { 142 | BlockState state = toLocation(world).getBlock().getState().copy(); 143 | state.setBlockData(blockData); 144 | state.setType(blockData.getMaterial()); 145 | return state; 146 | } 147 | 148 | /** 149 | * Get the distance between two BlockifyPositions 150 | * 151 | * @param other The other BlockifyPosition 152 | * @return The distance between the two BlockifyPositions 153 | */ 154 | public double distance(BlockifyPosition other) { 155 | return Math.sqrt(distanceSquared(other)); 156 | } 157 | 158 | /** 159 | * Get the string representation of the BlockifyPosition 160 | * 161 | * @return The string representation of the BlockifyPosition 162 | */ 163 | @Override 164 | public String toString() { 165 | return "BlockifyPosition{x=" + x + ", y=" + y + ", z=" + z + "}"; 166 | } 167 | 168 | /** 169 | * Check if the BlockifyPosition is equal to another object 170 | * 171 | * @param o The object to compare to 172 | * @return Whether the BlockifyPosition is equal to the object 173 | */ 174 | @Override 175 | public boolean equals(Object o) { 176 | if (o == this) return true; 177 | if (!(o instanceof BlockifyPosition other)) return false; 178 | return this.x == other.x && this.y == other.y && this.z == other.z; 179 | } 180 | 181 | /** 182 | * Get the hash code of the BlockifyPosition 183 | * 184 | * @return The hash code of the BlockifyPosition 185 | */ 186 | @Override 187 | public int hashCode() { 188 | int result = x; 189 | result = 31 * result + y; 190 | result = 31 * result + z; 191 | return result; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/utils/BlockUtils.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.utils; 2 | 3 | import codes.kooper.blockify.types.BlockifyPosition; 4 | import org.bukkit.Location; 5 | import org.bukkit.block.data.Ageable; 6 | import org.bukkit.block.data.BlockData; 7 | 8 | import java.util.ArrayList; 9 | import java.util.HashSet; 10 | import java.util.List; 11 | import java.util.Set; 12 | 13 | public class BlockUtils { 14 | 15 | /** 16 | * Get all the blocks between two positions. 17 | * Call this method asynchronously if you are going to be getting a large amount of blocks. 18 | * 19 | * @param pos1 The first position. 20 | * @param pos2 The second position. 21 | * @return A set of all the blocks between the two positions. 22 | */ 23 | public static Set getBlocksBetween(BlockifyPosition pos1, BlockifyPosition pos2) { 24 | Set positions = new HashSet<>(); 25 | int minX = Math.min(pos1.getX(), pos2.getX()); 26 | int minY = Math.min(pos1.getY(), pos2.getY()); 27 | int minZ = Math.min(pos1.getZ(), pos2.getZ()); 28 | int maxX = Math.max(pos1.getX(), pos2.getX()); 29 | int maxY = Math.max(pos1.getY(), pos2.getY()); 30 | int maxZ = Math.max(pos1.getZ(), pos2.getZ()); 31 | for (int x = minX; x <= maxX; x++) { 32 | for (int y = minY; y <= maxY; y++) { 33 | for (int z = minZ; z <= maxZ; z++) { 34 | positions.add(new BlockifyPosition(x, y, z)); 35 | } 36 | } 37 | } 38 | return positions; 39 | } 40 | 41 | /** 42 | * Get all the locations between two locations. 43 | * Call this method asynchronously if you are going to be getting a large amount of locations. 44 | * 45 | * @param loc1 The first location. 46 | * @param loc2 The second location. 47 | * @return A list of all the locations between the two locations. 48 | */ 49 | public static List getLocationsBetween(Location loc1, Location loc2) { 50 | List locations = new ArrayList<>(); 51 | int minX = Math.min(loc1.getBlockX(), loc2.getBlockX()); 52 | int minY = Math.min(loc1.getBlockY(), loc2.getBlockY()); 53 | int minZ = Math.min(loc1.getBlockZ(), loc2.getBlockZ()); 54 | int maxX = Math.max(loc1.getBlockX(), loc2.getBlockX()); 55 | int maxY = Math.max(loc1.getBlockY(), loc2.getBlockY()); 56 | int maxZ = Math.max(loc1.getBlockZ(), loc2.getBlockZ()); 57 | for (int x = minX; x <= maxX; x++) { 58 | for (int y = minY; y <= maxY; y++) { 59 | for (int z = minZ; z <= maxZ; z++) { 60 | locations.add(new Location(loc1.getWorld(), x, y, z)); 61 | } 62 | } 63 | } 64 | return locations; 65 | } 66 | 67 | /** 68 | * Set the age of a block. 69 | * 70 | * @param blockData The block data. 71 | * @param age The age to set. 72 | * @return The block data with the age set. 73 | */ 74 | public static BlockData setAge(BlockData blockData, int age) { 75 | Ageable ageable = (Ageable) blockData; 76 | ageable.setAge(age); 77 | return ageable; 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /src/main/java/codes/kooper/blockify/utils/PositionKeyUtil.java: -------------------------------------------------------------------------------- 1 | package codes.kooper.blockify.utils; 2 | 3 | import codes.kooper.blockify.types.BlockifyPosition; 4 | 5 | public class PositionKeyUtil { 6 | // Bit masks and shifts 7 | private static final long X_MASK = 0x3FFFFFFL; 8 | private static final long Y_MASK = 0xFFFL; 9 | private static final long Z_MASK = 0x3FFFFFFL; 10 | private static final int X_SHIFT = 38; 11 | private static final int Y_SHIFT = 26; 12 | 13 | public static long getPositionKey(int x, int y, int z) { 14 | return ((x & X_MASK) << X_SHIFT) 15 | | ((y & Y_MASK) << Y_SHIFT) 16 | | (z & Z_MASK); 17 | } 18 | 19 | public static int getX(long positionKey) { 20 | return (int) ((positionKey >> X_SHIFT) & X_MASK); 21 | } 22 | 23 | public static int getY(long positionKey) { 24 | return (int) ((positionKey >> Y_SHIFT) & Y_MASK); 25 | } 26 | 27 | public static int getZ(long positionKey) { 28 | return (int) (positionKey & Z_MASK); 29 | } 30 | 31 | public static BlockifyPosition toBlockifyPosition(long positionKey) { 32 | return new BlockifyPosition(getX(positionKey), getY(positionKey), getZ(positionKey)); 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/resources/plugin.yml: -------------------------------------------------------------------------------- 1 | name: Blockify 2 | version: '1.1.8-beta' 3 | main: codes.kooper.blockify.Blockify 4 | api-version: '1.20' 5 | depend: 6 | - packetevents --------------------------------------------------------------------------------