├── .gitattributes ├── .gitignore ├── LICENSE.txt ├── README.md ├── build.gradle ├── build.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── fi │ └── dy │ └── masa │ └── servux │ ├── Reference.java │ ├── Servux.java │ ├── dataproviders │ ├── DataProviderBase.java │ ├── DataProviderManager.java │ ├── IDataProvider.java │ └── StructureDataProvider.java │ ├── mixin │ ├── IMixinCustomPayloadC2SPacket.java │ ├── MixinMinecraftServer.java │ ├── MixinServerPlayNetworkHandler.java │ └── MixinThreadedAnvilChunkStorage.java │ ├── network │ ├── IPluginChannelHandler.java │ ├── PacketSplitter.java │ ├── ServerPacketChannelHandler.java │ ├── packet │ │ └── StructureDataPacketHandler.java │ └── util │ │ └── PacketUtils.java │ └── util │ ├── JsonUtils.java │ ├── PlayerDimensionPosition.java │ └── Timeout.java └── resources ├── fabric.mod.json └── mixins.servux.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # text stuff 2 | * text=auto 3 | *.bat text eol=crlf 4 | *.groovy text eol=lf 5 | *.java text eol=crlf 6 | *.md text 7 | *.properties text eol=lf 8 | *.scala text eol=lf 9 | *.sh text eol=lf 10 | .gitattributes text eol=lf 11 | .gitignore text eol=lf 12 | build.gradle text eol=lf 13 | gradlew text eol=lf 14 | gradle/wrapper/gradle-wrapper.properties text eol=crlf 15 | COPYING.txt text eol=lf 16 | COPYING.LESSER.txt text eol=lf 17 | README.md text eol=lf 18 | 19 | #binary 20 | *.dat binary 21 | *.bin binary 22 | *.png binary 23 | *.exe binary 24 | *.dll binary 25 | *.zip binary 26 | *.jar binary 27 | *.7z binary 28 | *.db binary 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .settings 3 | bin/ 4 | build/ 5 | eclipse/ 6 | logs/ 7 | .classpath 8 | .project 9 | build.number -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Servux 2 | ============== 3 | Servux is a server-side mod that provides extra support/features for some client-side mods when playing on a server. 4 | 5 | **Servux itself is never needed on the clients or in single player**, 6 | it's only needed/useful on the dedicated server side in multiplayer. 7 | 8 | In version 0.1.x it only has one thing, which is sending structure bounding boxes for MiniHUD so that it can render those also in multiplayer. 9 | 10 | For compiled builds (= downloads), see https://www.curseforge.com/minecraft/mc-mods/servux 11 | 12 | Compiling 13 | ========= 14 | * Clone the repository 15 | * Open a command prompt/terminal to the repository directory 16 | * run 'gradlew build' 17 | * The built jar file will be in build/libs/ -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'fabric-loom' version '0.12-SNAPSHOT' 3 | } 4 | 5 | sourceCompatibility = JavaVersion.VERSION_17 6 | targetCompatibility = JavaVersion.VERSION_17 7 | 8 | ext.configFile = file "build.properties" 9 | 10 | configFile.withReader { 11 | def prop = new Properties() 12 | prop.load(it) 13 | project.ext.config = new ConfigSlurper().parse prop 14 | } 15 | 16 | dependencies { 17 | minecraft "com.mojang:minecraft:${config.minecraft_version}" 18 | mappings "net.fabricmc:yarn:${config.yarn_mappings_version}:v2" 19 | modImplementation "net.fabricmc:fabric-loader:${config.fabric_loader_version}" 20 | implementation "com.google.code.findbugs:jsr305:3.0.2" 21 | 22 | // Fabric API. This is technically optional, but you probably want it anyway. 23 | //modCompile "net.fabricmc.fabric-api:fabric-api:" + config.fabric_version 24 | } 25 | 26 | group = config.group + "." + config.mod_id 27 | archivesBaseName = config.mod_file_name + '-' + config.minecraft_version_out 28 | version = config.mod_version 29 | 30 | if (version.endsWith('-dev')) { 31 | version += "." + new Date().format('yyyyMMdd.HHmmss') 32 | } 33 | 34 | processResources { 35 | // Exclude the GIMP image files 36 | exclude '**/*.xcf' 37 | exclude '**/xcf' 38 | 39 | // this will ensure that this task is redone when the versions change. 40 | //inputs.property "minecraft_version", project.config.minecraft_version 41 | 42 | inputs.property "mod_version", config.mod_version 43 | 44 | filesMatching("fabric.mod.json") { 45 | expand "mod_version": config.mod_version 46 | } 47 | } 48 | 49 | tasks.withType(JavaCompile).configureEach { 50 | // ensure that the encoding is set to UTF-8, no matter what the system default is 51 | // this fixes some edge cases with special characters not displaying correctly 52 | // see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html 53 | // If Javadoc is generated, this must be specified in that task too. 54 | it.options.encoding = "UTF-8" 55 | 56 | // Minecraft 1.18 (1.18-pre2) upwards uses Java 17. 57 | it.options.release = 17 58 | } 59 | -------------------------------------------------------------------------------- /build.properties: -------------------------------------------------------------------------------- 1 | # Thu Jan 14 08:33:00 EET 2016 2 | group = fi.dy.masa 3 | mod_id = servux 4 | mod_name = Servux 5 | author = masa 6 | mod_file_name = servux-fabric 7 | 8 | # Current mod version 9 | mod_version = 0.1.0 10 | 11 | # Minecraft, Forge and MCP mappings versions 12 | minecraft_version_out = 1.19.2 13 | minecraft_version = 1.19.2 14 | yarn_mappings_version = 1.19.1+build.1 15 | 16 | fabric_loader_version = 0.14.9 17 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maruohon/servux/643d053ae9b0b44ad07f78d97442020406c2d369/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Aug 22 17:36:22 EDT 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { 4 | name = 'Fabric' 5 | url = 'https://maven.fabricmc.net/' 6 | } 7 | gradlePluginPortal() 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/servux/Reference.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.servux; 2 | 3 | public class Reference 4 | { 5 | public static final String MOD_ID = "servux"; 6 | public static final String MOD_NAME = "ServuX"; 7 | public static final String MOD_VERSION = Servux.getModVersionString(MOD_ID); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/servux/Servux.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.servux; 2 | 3 | import org.apache.logging.log4j.LogManager; 4 | import org.apache.logging.log4j.Logger; 5 | import fi.dy.masa.servux.dataproviders.DataProviderManager; 6 | import fi.dy.masa.servux.dataproviders.StructureDataProvider; 7 | import net.fabricmc.api.ModInitializer; 8 | 9 | public class Servux implements ModInitializer 10 | { 11 | public static final Logger logger = LogManager.getLogger(Reference.MOD_ID); 12 | 13 | @Override 14 | public void onInitialize() 15 | { 16 | DataProviderManager.INSTANCE.registerDataProvider(StructureDataProvider.INSTANCE); 17 | DataProviderManager.INSTANCE.readFromConfig(); 18 | } 19 | 20 | public static String getModVersionString(String modId) 21 | { 22 | for (net.fabricmc.loader.api.ModContainer container : net.fabricmc.loader.api.FabricLoader.getInstance().getAllMods()) 23 | { 24 | if (container.getMetadata().getId().equals(modId)) 25 | { 26 | return container.getMetadata().getVersion().getFriendlyString(); 27 | } 28 | } 29 | 30 | return "?"; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/servux/dataproviders/DataProviderBase.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.servux.dataproviders; 2 | 3 | import net.minecraft.util.Identifier; 4 | 5 | public abstract class DataProviderBase implements IDataProvider 6 | { 7 | protected final Identifier networkChannel; 8 | protected final String name; 9 | protected final String description; 10 | protected final int protocolVersion; 11 | protected boolean enabled; 12 | private int tickRate = 40; 13 | 14 | protected DataProviderBase(String name, Identifier channel, int protocolVersion, String description) 15 | { 16 | this.name = name; 17 | this.networkChannel = channel; 18 | this.protocolVersion = protocolVersion; 19 | this.description = description; 20 | } 21 | 22 | @Override 23 | public String getName() 24 | { 25 | return this.name; 26 | } 27 | 28 | @Override 29 | public String getDescription() 30 | { 31 | return this.description; 32 | } 33 | 34 | @Override 35 | public Identifier getNetworkChannel() 36 | { 37 | return this.networkChannel; 38 | } 39 | 40 | @Override 41 | public int getProtocolVersion() 42 | { 43 | return this.protocolVersion; 44 | } 45 | 46 | @Override 47 | public boolean isEnabled() 48 | { 49 | return this.enabled; 50 | } 51 | 52 | @Override 53 | public void setEnabled(boolean enabled) 54 | { 55 | this.enabled = enabled; 56 | } 57 | 58 | protected void setTickRate(int tickRate) 59 | { 60 | this.tickRate = Math.max(tickRate, 1); 61 | } 62 | 63 | @Override 64 | public final int getTickRate() 65 | { 66 | return this.tickRate; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/servux/dataproviders/DataProviderManager.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.servux.dataproviders; 2 | 3 | import java.io.File; 4 | import java.util.ArrayList; 5 | import java.util.HashMap; 6 | import com.google.common.collect.ImmutableList; 7 | import com.google.gson.JsonElement; 8 | import com.google.gson.JsonObject; 9 | import com.google.gson.JsonPrimitive; 10 | import net.minecraft.server.MinecraftServer; 11 | import fi.dy.masa.servux.network.IPluginChannelHandler; 12 | import fi.dy.masa.servux.network.ServerPacketChannelHandler; 13 | import fi.dy.masa.servux.util.JsonUtils; 14 | 15 | public class DataProviderManager 16 | { 17 | public static final DataProviderManager INSTANCE = new DataProviderManager(); 18 | 19 | protected final HashMap providers = new HashMap<>(); 20 | protected ImmutableList providersImmutable = ImmutableList.of(); 21 | protected ArrayList providersTicking = new ArrayList<>(); 22 | 23 | public ImmutableList getAllProviders() 24 | { 25 | return this.providersImmutable; 26 | } 27 | 28 | /** 29 | * Registers the given data provider, if it's not already registered 30 | * @param provider 31 | * @return true if the provider did not exist yet and was successfully registered 32 | */ 33 | public boolean registerDataProvider(IDataProvider provider) 34 | { 35 | String name = provider.getName(); 36 | 37 | if (this.providers.containsKey(name) == false) 38 | { 39 | this.providers.put(name, provider); 40 | this.providersImmutable = ImmutableList.copyOf(this.providers.values()); 41 | //System.out.printf("registerDataProvider: %s\n", provider); 42 | return true; 43 | } 44 | 45 | return false; 46 | } 47 | 48 | public boolean setProviderEnabled(String providerName, boolean enabled) 49 | { 50 | IDataProvider provider = this.providers.get(providerName); 51 | return provider != null && this.setProviderEnabled(provider, enabled); 52 | } 53 | 54 | public boolean setProviderEnabled(IDataProvider provider, boolean enabled) 55 | { 56 | boolean wasEnabled = provider.isEnabled(); 57 | enabled = true; // FIXME TODO remove debug 58 | 59 | if (enabled || wasEnabled != enabled) 60 | { 61 | //System.out.printf("setProviderEnabled: %s (%s)\n", enabled, provider); 62 | provider.setEnabled(enabled); 63 | this.updatePacketHandlerRegistration(provider); 64 | 65 | if (enabled && provider.shouldTick() && this.providersTicking.contains(provider) == false) 66 | { 67 | this.providersTicking.add(provider); 68 | } 69 | else 70 | { 71 | this.providersTicking.remove(provider); 72 | } 73 | 74 | return true; 75 | } 76 | 77 | return false; 78 | } 79 | 80 | public void tickProviders(MinecraftServer server, int tickCounter) 81 | { 82 | if (this.providersTicking.isEmpty() == false) 83 | { 84 | for (IDataProvider provider : this.providersTicking) 85 | { 86 | if ((tickCounter % provider.getTickRate()) == 0) 87 | { 88 | provider.tick(server, tickCounter); 89 | } 90 | } 91 | } 92 | } 93 | 94 | protected void registerEnabledPacketHandlers() 95 | { 96 | for (IDataProvider provider : this.providersImmutable) 97 | { 98 | this.updatePacketHandlerRegistration(provider); 99 | } 100 | } 101 | 102 | protected void updatePacketHandlerRegistration(IDataProvider provider) 103 | { 104 | IPluginChannelHandler handler = provider.getPacketHandler(); 105 | 106 | if (provider.isEnabled()) 107 | { 108 | ServerPacketChannelHandler.INSTANCE.registerServerChannelHandler(handler); 109 | } 110 | else 111 | { 112 | ServerPacketChannelHandler.INSTANCE.unregisterServerChannelHandler(handler); 113 | } 114 | } 115 | 116 | public void readFromConfig() 117 | { 118 | JsonElement el = JsonUtils.parseJsonFile(this.getConfigFile()); 119 | JsonObject obj = null; 120 | 121 | if (el != null && el.isJsonObject()) 122 | { 123 | JsonObject root = el.getAsJsonObject(); 124 | 125 | if (JsonUtils.hasObject(root, "DataProviderToggles")) 126 | { 127 | obj = JsonUtils.getNestedObject(root, "DataProviderToggles", false); 128 | } 129 | } 130 | 131 | for (IDataProvider provider : this.providersImmutable) 132 | { 133 | String name = provider.getName(); 134 | boolean enabled = obj != null && JsonUtils.getBooleanOrDefault(obj, name, false); 135 | this.setProviderEnabled(provider, enabled); 136 | } 137 | } 138 | 139 | public void writeToConfig() 140 | { 141 | JsonObject root = new JsonObject(); 142 | JsonObject objToggles = new JsonObject(); 143 | 144 | for (IDataProvider provider : this.providersImmutable) 145 | { 146 | String name = provider.getName(); 147 | objToggles.add(name, new JsonPrimitive(provider.isEnabled())); 148 | } 149 | 150 | root.add("DataProviderToggles", objToggles); 151 | 152 | JsonUtils.writeJsonToFile(root, this.getConfigFile()); 153 | } 154 | 155 | protected File getConfigFile() 156 | { 157 | return new File("servux.json"); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/servux/dataproviders/IDataProvider.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.servux.dataproviders; 2 | 3 | import net.minecraft.server.MinecraftServer; 4 | import net.minecraft.util.Identifier; 5 | import fi.dy.masa.servux.network.IPluginChannelHandler; 6 | 7 | public interface IDataProvider 8 | { 9 | /** 10 | * Returns the simple name for this data provider. 11 | * This should preferably be a lower case alphanumeric string with no 12 | * other special characters than '-' and '_'. 13 | * This name will be used in the enable/disable commands as the argument 14 | * and also as the config file key/identifier. 15 | * @return 16 | */ 17 | String getName(); 18 | 19 | /** 20 | * Returns the description of this data provider. 21 | * Used in the command to list the available providers and to check the status 22 | * of a given provider. 23 | * @return 24 | */ 25 | String getDescription(); 26 | 27 | /** 28 | * Returns the network channel name used by this data provider to listen 29 | * for incoming data requests and to respond and send the requested data. 30 | * @return 31 | */ 32 | Identifier getNetworkChannel(); 33 | 34 | /** 35 | * Returns the current protocol version this provider supports 36 | * @return 37 | */ 38 | int getProtocolVersion(); 39 | 40 | /** 41 | * Returns true if this data provider is currently enabled. 42 | * @return 43 | */ 44 | boolean isEnabled(); 45 | 46 | /** 47 | * Enables or disables this data provider 48 | * @param enabled 49 | */ 50 | void setEnabled(boolean enabled); 51 | 52 | /** 53 | * Returns whether or not this data provider should get ticked to periodically send some data, 54 | * or if it's only listening for incoming requests and responds to them directly. 55 | * @return 56 | */ 57 | default boolean shouldTick() 58 | { 59 | return false; 60 | } 61 | 62 | /** 63 | * Returns the interval in game ticks that this data provider should be ticked at 64 | * @return 65 | */ 66 | int getTickRate(); 67 | 68 | /** 69 | * Called at the given tick rate 70 | * @param server 71 | * @param tickCounter The current server tick (since last server start) 72 | */ 73 | default void tick(MinecraftServer server, int tickCounter) 74 | { 75 | } 76 | 77 | /** 78 | * Returns the network packet handler used for this data provider. 79 | * @return 80 | */ 81 | IPluginChannelHandler getPacketHandler(); 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/servux/dataproviders/StructureDataProvider.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.servux.dataproviders; 2 | 3 | import java.util.HashMap; 4 | import java.util.HashSet; 5 | import java.util.Iterator; 6 | import java.util.Map; 7 | import java.util.Set; 8 | import java.util.UUID; 9 | import it.unimi.dsi.fastutil.longs.LongIterator; 10 | import it.unimi.dsi.fastutil.longs.LongOpenHashSet; 11 | import it.unimi.dsi.fastutil.longs.LongSet; 12 | import net.minecraft.nbt.NbtCompound; 13 | import net.minecraft.nbt.NbtList; 14 | import net.minecraft.server.MinecraftServer; 15 | import net.minecraft.server.network.ServerPlayerEntity; 16 | import net.minecraft.server.world.ServerWorld; 17 | import net.minecraft.structure.StructureContext; 18 | import net.minecraft.structure.StructureStart; 19 | import net.minecraft.util.math.ChunkPos; 20 | import net.minecraft.world.World; 21 | import net.minecraft.world.chunk.Chunk; 22 | import net.minecraft.world.chunk.ChunkStatus; 23 | import net.minecraft.world.chunk.WorldChunk; 24 | import net.minecraft.world.gen.structure.Structure; 25 | import fi.dy.masa.servux.network.IPluginChannelHandler; 26 | import fi.dy.masa.servux.network.PacketSplitter; 27 | import fi.dy.masa.servux.network.packet.StructureDataPacketHandler; 28 | import fi.dy.masa.servux.util.PlayerDimensionPosition; 29 | import fi.dy.masa.servux.util.Timeout; 30 | 31 | public class StructureDataProvider extends DataProviderBase 32 | { 33 | public static final StructureDataProvider INSTANCE = new StructureDataProvider(); 34 | 35 | protected final Map registeredPlayers = new HashMap<>(); 36 | protected final Map> timeouts = new HashMap<>(); 37 | protected final NbtCompound metadata = new NbtCompound(); 38 | protected int timeout = 30 * 20; 39 | protected int updateInterval = 40; 40 | protected int retainDistance; 41 | 42 | protected StructureDataProvider() 43 | { 44 | super("structure_bounding_boxes", 45 | StructureDataPacketHandler.CHANNEL, StructureDataPacketHandler.PROTOCOL_VERSION, 46 | "Structure Bounding Boxes data for structures such as Witch Huts, Ocean Monuments, Nether Fortresses etc."); 47 | 48 | this.metadata.putString("id", StructureDataPacketHandler.CHANNEL.toString()); 49 | this.metadata.putInt("timeout", this.timeout); 50 | this.metadata.putInt("version", StructureDataPacketHandler.PROTOCOL_VERSION); 51 | } 52 | 53 | @Override 54 | public boolean shouldTick() 55 | { 56 | return true; 57 | } 58 | 59 | @Override 60 | public IPluginChannelHandler getPacketHandler() 61 | { 62 | return StructureDataPacketHandler.INSTANCE; 63 | } 64 | 65 | @Override 66 | public void tick(MinecraftServer server, int tickCounter) 67 | { 68 | if ((tickCounter % this.updateInterval) == 0) 69 | { 70 | if (this.registeredPlayers.isEmpty() == false) 71 | { 72 | // System.out.printf("=======================\n"); 73 | // System.out.printf("tick: %d - %s\n", tickCounter, this.isEnabled()); 74 | this.retainDistance = server.getPlayerManager().getViewDistance() + 2; 75 | Iterator uuidIter = this.registeredPlayers.keySet().iterator(); 76 | 77 | while (uuidIter.hasNext()) 78 | { 79 | UUID uuid = uuidIter.next(); 80 | ServerPlayerEntity player = server.getPlayerManager().getPlayer(uuid); 81 | 82 | if (player != null) 83 | { 84 | this.checkForDimensionChange(player); 85 | this.refreshTrackedChunks(player, tickCounter); 86 | } 87 | else 88 | { 89 | this.timeouts.remove(uuid); 90 | uuidIter.remove(); 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | public void onStartedWatchingChunk(ServerPlayerEntity player, WorldChunk chunk) 98 | { 99 | UUID uuid = player.getUuid(); 100 | 101 | if (this.registeredPlayers.containsKey(uuid)) 102 | { 103 | this.addChunkTimeoutIfHasReferences(uuid, chunk, player.getServer().getTicks()); 104 | } 105 | } 106 | 107 | public boolean register(ServerPlayerEntity player) 108 | { 109 | // System.out.printf("register\n"); 110 | boolean registered = false; 111 | UUID uuid = player.getUuid(); 112 | 113 | if (this.registeredPlayers.containsKey(uuid) == false) 114 | { 115 | PacketSplitter.sendPacketTypeAndCompound(StructureDataPacketHandler.CHANNEL, StructureDataPacketHandler.PACKET_S2C_METADATA, this.metadata, player); 116 | 117 | this.registeredPlayers.put(uuid, new PlayerDimensionPosition(player)); 118 | int tickCounter = player.getServer().getTicks(); 119 | this.initialSyncStructuresToPlayerWithinRange(player, player.getServer().getPlayerManager().getViewDistance(), tickCounter); 120 | 121 | registered = true; 122 | } 123 | 124 | return registered; 125 | } 126 | 127 | public boolean unregister(ServerPlayerEntity player) 128 | { 129 | // System.out.printf("unregister\n"); 130 | return this.registeredPlayers.remove(player.getUuid()) != null; 131 | } 132 | 133 | protected void initialSyncStructuresToPlayerWithinRange(ServerPlayerEntity player, int chunkRadius, int tickCounter) 134 | { 135 | UUID uuid = player.getUuid(); 136 | ChunkPos center = player.getWatchedSection().toChunkPos(); 137 | Map references = 138 | this.getStructureReferencesWithinRange(player.getWorld(), center, chunkRadius); 139 | 140 | this.timeouts.remove(uuid); 141 | this.registeredPlayers.computeIfAbsent(uuid, (u) -> new PlayerDimensionPosition(player)).setPosition(player); 142 | 143 | // System.out.printf("initialSyncStructuresToPlayerWithinRange: references: %d\n", references.size()); 144 | this.sendStructures(player, references, tickCounter); 145 | } 146 | 147 | protected void addChunkTimeoutIfHasReferences(final UUID uuid, WorldChunk chunk, final int tickCounter) 148 | { 149 | final ChunkPos pos = chunk.getPos(); 150 | 151 | if (this.chunkHasStructureReferences(pos.x, pos.z, chunk.getWorld())) 152 | { 153 | final Map map = this.timeouts.computeIfAbsent(uuid, (u) -> new HashMap<>()); 154 | final int timeout = this.timeout; 155 | 156 | //System.out.printf("addChunkTimeoutIfHasReferences: %s\n", pos); 157 | // Set the timeout so it's already expired and will cause the chunk to be sent on the next update tick 158 | map.computeIfAbsent(pos, (p) -> new Timeout(tickCounter - timeout)); 159 | } 160 | } 161 | 162 | protected void checkForDimensionChange(ServerPlayerEntity player) 163 | { 164 | UUID uuid = player.getUuid(); 165 | PlayerDimensionPosition playerPos = this.registeredPlayers.get(uuid); 166 | 167 | if (playerPos == null || playerPos.dimensionChanged(player)) 168 | { 169 | this.timeouts.remove(uuid); 170 | this.registeredPlayers.computeIfAbsent(uuid, (u) -> new PlayerDimensionPosition(player)).setPosition(player); 171 | } 172 | } 173 | 174 | protected void addOrRefreshTimeouts(final UUID uuid, 175 | final Map references, 176 | final int tickCounter) 177 | { 178 | // System.out.printf("addOrRefreshTimeouts: references: %d\n", references.size()); 179 | Map map = this.timeouts.computeIfAbsent(uuid, (u) -> new HashMap<>()); 180 | 181 | for (LongSet chunks : references.values()) 182 | { 183 | for (Long chunkPosLong : chunks) 184 | { 185 | final ChunkPos pos = new ChunkPos(chunkPosLong); 186 | map.computeIfAbsent(pos, (p) -> new Timeout(tickCounter)).setLastSync(tickCounter); 187 | } 188 | } 189 | } 190 | 191 | protected void refreshTrackedChunks(ServerPlayerEntity player, int tickCounter) 192 | { 193 | UUID uuid = player.getUuid(); 194 | Map map = this.timeouts.get(uuid); 195 | 196 | if (map != null) 197 | { 198 | // System.out.printf("refreshTrackedChunks: timeouts: %d\n", map.size()); 199 | this.sendAndRefreshExpiredStructures(player, map, tickCounter); 200 | } 201 | } 202 | 203 | protected boolean isOutOfRange(ChunkPos pos, ChunkPos center) 204 | { 205 | int chunkRadius = this.retainDistance; 206 | 207 | return Math.abs(pos.x - center.x) > chunkRadius || 208 | Math.abs(pos.z - center.z) > chunkRadius; 209 | } 210 | 211 | protected void sendAndRefreshExpiredStructures(ServerPlayerEntity player, Map map, int tickCounter) 212 | { 213 | Set positionsToUpdate = new HashSet<>(); 214 | 215 | for (Map.Entry entry : map.entrySet()) 216 | { 217 | Timeout timeout = entry.getValue(); 218 | 219 | if (timeout.needsUpdate(tickCounter, this.timeout)) 220 | { 221 | positionsToUpdate.add(entry.getKey()); 222 | } 223 | } 224 | 225 | if (positionsToUpdate.isEmpty() == false) 226 | { 227 | ServerWorld world = player.getWorld(); 228 | ChunkPos center = player.getWatchedSection().toChunkPos(); 229 | Map references = new HashMap<>(); 230 | 231 | for (ChunkPos pos : positionsToUpdate) 232 | { 233 | if (this.isOutOfRange(pos, center)) 234 | { 235 | map.remove(pos); 236 | } 237 | else 238 | { 239 | this.getStructureReferencesFromChunk(pos.x, pos.z, world, references); 240 | 241 | Timeout timeout = map.get(pos); 242 | 243 | if (timeout != null) 244 | { 245 | timeout.setLastSync(tickCounter); 246 | } 247 | } 248 | } 249 | 250 | // System.out.printf("sendAndRefreshExpiredStructures: positionsToUpdate: %d -> references: %d, to: %d\n", positionsToUpdate.size(), references.size(), this.timeout); 251 | 252 | if (references.isEmpty() == false) 253 | { 254 | this.sendStructures(player, references, tickCounter); 255 | } 256 | } 257 | } 258 | 259 | protected void getStructureReferencesFromChunk(int chunkX, int chunkZ, World world, Map references) 260 | { 261 | if (world.isChunkLoaded(chunkX, chunkZ) == false) 262 | { 263 | return; 264 | } 265 | 266 | Chunk chunk = world.getChunk(chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS, false); 267 | 268 | if (chunk == null) 269 | { 270 | return; 271 | } 272 | 273 | for (Map.Entry entry : chunk.getStructureReferences().entrySet()) 274 | { 275 | Structure feature = entry.getKey(); 276 | LongSet startChunks = entry.getValue(); 277 | 278 | // TODO add an option && feature != StructureFeature.MINESHAFT 279 | if (startChunks.isEmpty() == false) 280 | { 281 | references.merge(feature, startChunks, (oldSet, entrySet) -> { 282 | LongOpenHashSet newSet = new LongOpenHashSet(oldSet); 283 | newSet.addAll(entrySet); 284 | return newSet; 285 | }); 286 | } 287 | } 288 | } 289 | 290 | protected boolean chunkHasStructureReferences(int chunkX, int chunkZ, World world) 291 | { 292 | if (world.isChunkLoaded(chunkX, chunkZ) == false) 293 | { 294 | return false; 295 | } 296 | 297 | Chunk chunk = world.getChunk(chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS, false); 298 | 299 | if (chunk == null) 300 | { 301 | return false; 302 | } 303 | 304 | for (Map.Entry entry : chunk.getStructureReferences().entrySet()) 305 | { 306 | // TODO add an option entry.getKey() != StructureFeature.MINESHAFT && 307 | if (entry.getValue().isEmpty() == false) 308 | { 309 | return true; 310 | } 311 | } 312 | 313 | return false; 314 | } 315 | 316 | protected Map 317 | getStructureStartsFromReferences(ServerWorld world, Map references) 318 | { 319 | Map starts = new HashMap<>(); 320 | 321 | for (Map.Entry entry : references.entrySet()) 322 | { 323 | Structure structure = entry.getKey(); 324 | LongSet startChunks = entry.getValue(); 325 | LongIterator iter = startChunks.iterator(); 326 | 327 | while (iter.hasNext()) 328 | { 329 | ChunkPos pos = new ChunkPos(iter.nextLong()); 330 | 331 | if (world.isChunkLoaded(pos.x, pos.z) == false) 332 | { 333 | continue; 334 | } 335 | 336 | Chunk chunk = world.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS, false); 337 | 338 | if (chunk == null) 339 | { 340 | continue; 341 | } 342 | 343 | StructureStart start = chunk.getStructureStart(structure); 344 | 345 | if (start != null) 346 | { 347 | starts.put(pos, start); 348 | } 349 | } 350 | } 351 | 352 | // System.out.printf("getStructureStartsFromReferences: references: %d -> starts: %d\n", references.size(), starts.size()); 353 | return starts; 354 | } 355 | 356 | protected Map 357 | getStructureReferencesWithinRange(ServerWorld world, ChunkPos center, int chunkRadius) 358 | { 359 | Map references = new HashMap<>(); 360 | 361 | for (int cx = center.x - chunkRadius; cx <= center.x + chunkRadius; ++cx) 362 | { 363 | for (int cz = center.z - chunkRadius; cz <= center.z + chunkRadius; ++cz) 364 | { 365 | this.getStructureReferencesFromChunk(cx, cz, world, references); 366 | } 367 | } 368 | 369 | // System.out.printf("getStructureReferencesWithinRange: references: %d\n", references.size()); 370 | return references; 371 | } 372 | 373 | protected void sendStructures(ServerPlayerEntity player, 374 | Map references, 375 | int tickCounter) 376 | { 377 | ServerWorld world = player.getWorld(); 378 | Map starts = this.getStructureStartsFromReferences(world, references); 379 | 380 | if (starts.isEmpty() == false) 381 | { 382 | this.addOrRefreshTimeouts(player.getUuid(), references, tickCounter); 383 | 384 | NbtList structureList = this.getStructureList(starts, world); 385 | // System.out.printf("sendStructures: starts: %d -> structureList: %d. refs: %s\n", starts.size(), structureList.size(), references.keySet()); 386 | 387 | NbtCompound tag = new NbtCompound(); 388 | tag.put("Structures", structureList); 389 | 390 | PacketSplitter.sendPacketTypeAndCompound(StructureDataPacketHandler.CHANNEL, StructureDataPacketHandler.PACKET_S2C_STRUCTURE_DATA, tag, player); 391 | } 392 | } 393 | 394 | protected NbtList getStructureList(Map structures, ServerWorld world) 395 | { 396 | NbtList list = new NbtList(); 397 | StructureContext ctx = StructureContext.from(world); 398 | 399 | for (Map.Entry entry : structures.entrySet()) 400 | { 401 | ChunkPos pos = entry.getKey(); 402 | list.add(entry.getValue().toNbt(ctx, pos)); 403 | } 404 | 405 | return list; 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/servux/mixin/IMixinCustomPayloadC2SPacket.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.servux.mixin; 2 | 3 | import org.spongepowered.asm.mixin.Mixin; 4 | import org.spongepowered.asm.mixin.gen.Accessor; 5 | import net.minecraft.network.PacketByteBuf; 6 | import net.minecraft.network.packet.c2s.play.CustomPayloadC2SPacket; 7 | import net.minecraft.util.Identifier; 8 | 9 | @Mixin(CustomPayloadC2SPacket.class) 10 | public interface IMixinCustomPayloadC2SPacket 11 | { 12 | @Accessor("channel") 13 | Identifier servux_getChannel(); 14 | 15 | @Accessor("data") 16 | PacketByteBuf servux_getData(); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/servux/mixin/MixinMinecraftServer.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.servux.mixin; 2 | 3 | import java.util.function.BooleanSupplier; 4 | import org.spongepowered.asm.mixin.Mixin; 5 | import org.spongepowered.asm.mixin.Shadow; 6 | import org.spongepowered.asm.mixin.injection.At; 7 | import org.spongepowered.asm.mixin.injection.Inject; 8 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 9 | import net.minecraft.server.MinecraftServer; 10 | import net.minecraft.util.profiler.Profiler; 11 | import fi.dy.masa.servux.dataproviders.DataProviderManager; 12 | 13 | @Mixin(MinecraftServer.class) 14 | public abstract class MixinMinecraftServer 15 | { 16 | @Shadow private Profiler profiler; 17 | @Shadow private int ticks; 18 | 19 | @Inject(method = "tick", at = @At("RETURN")) 20 | private void servux_onTickEnd(BooleanSupplier supplier, CallbackInfo ci) 21 | { 22 | this.profiler.push("servux_tick"); 23 | DataProviderManager.INSTANCE.tickProviders((MinecraftServer) (Object) this, this.ticks); 24 | this.profiler.pop(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/servux/mixin/MixinServerPlayNetworkHandler.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.servux.mixin; 2 | 3 | import org.spongepowered.asm.mixin.Mixin; 4 | import org.spongepowered.asm.mixin.injection.At; 5 | import org.spongepowered.asm.mixin.injection.Inject; 6 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 7 | import net.minecraft.network.packet.c2s.play.CustomPayloadC2SPacket; 8 | import net.minecraft.server.network.ServerPlayNetworkHandler; 9 | import fi.dy.masa.servux.network.ServerPacketChannelHandler; 10 | 11 | @Mixin(value = ServerPlayNetworkHandler.class, priority = 998) 12 | public abstract class MixinServerPlayNetworkHandler 13 | { 14 | @Inject(method = "onCustomPayload", at = @At("HEAD")) 15 | private void servux_handleCustomPayload(CustomPayloadC2SPacket packet, CallbackInfo ci) 16 | { 17 | ServerPacketChannelHandler.INSTANCE.processPacketFromClient(packet, (ServerPlayNetworkHandler) (Object) this); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/servux/mixin/MixinThreadedAnvilChunkStorage.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.servux.mixin; 2 | 3 | import org.apache.commons.lang3.mutable.MutableObject; 4 | import org.spongepowered.asm.mixin.Mixin; 5 | import org.spongepowered.asm.mixin.injection.At; 6 | import org.spongepowered.asm.mixin.injection.Inject; 7 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 8 | import net.minecraft.network.packet.s2c.play.ChunkDataS2CPacket; 9 | import net.minecraft.server.network.ServerPlayerEntity; 10 | import net.minecraft.server.world.ThreadedAnvilChunkStorage; 11 | import net.minecraft.world.chunk.WorldChunk; 12 | import fi.dy.masa.servux.dataproviders.StructureDataProvider; 13 | 14 | @Mixin(ThreadedAnvilChunkStorage.class) 15 | public abstract class MixinThreadedAnvilChunkStorage 16 | { 17 | @Inject(method = "sendChunkDataPackets", at = @At("HEAD")) 18 | private void servux_onSendChunkPacket(ServerPlayerEntity player, 19 | MutableObject cachedDataPacket, 20 | WorldChunk chunk, 21 | CallbackInfo ci) 22 | { 23 | if (StructureDataProvider.INSTANCE.isEnabled()) 24 | { 25 | StructureDataProvider.INSTANCE.onStartedWatchingChunk(player, chunk); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/servux/network/IPluginChannelHandler.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.servux.network; 2 | 3 | import net.minecraft.network.PacketByteBuf; 4 | import net.minecraft.server.network.ServerPlayNetworkHandler; 5 | import net.minecraft.util.Identifier; 6 | 7 | public interface IPluginChannelHandler 8 | { 9 | Identifier getChannel(); 10 | 11 | default void onPacketReceived(PacketByteBuf buf, ServerPlayNetworkHandler netHandler) 12 | { 13 | } 14 | 15 | default boolean isSubscribable() 16 | { 17 | return false; 18 | } 19 | 20 | default boolean subscribe(ServerPlayNetworkHandler netHandler) 21 | { 22 | return false; 23 | } 24 | 25 | default boolean unsubscribe(ServerPlayNetworkHandler netHandler) 26 | { 27 | return false; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/servux/network/PacketSplitter.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.servux.network; 2 | 3 | import java.util.HashMap; 4 | import java.util.Map; 5 | import java.util.function.Consumer; 6 | import javax.annotation.Nullable; 7 | import io.netty.buffer.Unpooled; 8 | import org.apache.commons.lang3.tuple.Pair; 9 | import net.minecraft.nbt.NbtCompound; 10 | import net.minecraft.network.PacketByteBuf; 11 | import net.minecraft.network.listener.PacketListener; 12 | import net.minecraft.network.packet.s2c.play.CustomPayloadS2CPacket; 13 | import net.minecraft.server.network.ServerPlayNetworkHandler; 14 | import net.minecraft.server.network.ServerPlayerEntity; 15 | import net.minecraft.util.Identifier; 16 | import fi.dy.masa.servux.network.util.PacketUtils; 17 | 18 | /** 19 | * Network packet splitter code from QuickCarpet by skyrising 20 | * @author skyrising 21 | */ 22 | public class PacketSplitter 23 | { 24 | public static final int MAX_TOTAL_PER_PACKET_S2C = 1048576; 25 | public static final int MAX_PAYLOAD_PER_PACKET_S2C = MAX_TOTAL_PER_PACKET_S2C - 5; 26 | public static final int DEFAULT_MAX_RECEIVE_SIZE_C2S = 1048576; 27 | 28 | private static final Map, ReadingSession> READING_SESSIONS = new HashMap<>(); 29 | 30 | public static void send(PacketByteBuf packet, Identifier channel, ServerPlayNetworkHandler networkHandler) 31 | { 32 | send(packet, MAX_PAYLOAD_PER_PACKET_S2C, buf -> networkHandler.sendPacket(new CustomPayloadS2CPacket(channel, buf))); 33 | } 34 | 35 | private static void send(PacketByteBuf packet, int payloadLimit, Consumer sender) 36 | { 37 | int len = packet.writerIndex(); 38 | 39 | packet.readerIndex(0); 40 | 41 | for (int offset = 0; offset < len; offset += payloadLimit) 42 | { 43 | int thisLen = Math.min(len - offset, payloadLimit); 44 | PacketByteBuf buf = new PacketByteBuf(Unpooled.buffer(thisLen)); 45 | 46 | if (offset == 0) 47 | { 48 | buf.writeVarInt(len); 49 | } 50 | 51 | buf.writeBytes(packet, thisLen); 52 | sender.accept(buf); 53 | } 54 | 55 | packet.release(); 56 | } 57 | 58 | @Nullable 59 | public static PacketByteBuf receive(Identifier channel, PacketByteBuf data, ServerPlayNetworkHandler networkHandler) 60 | { 61 | return receive(channel, data, DEFAULT_MAX_RECEIVE_SIZE_C2S, networkHandler); 62 | } 63 | 64 | @Nullable 65 | private static PacketByteBuf receive(Identifier channel, PacketByteBuf data, int maxLength, ServerPlayNetworkHandler networkHandler) 66 | { 67 | Pair key = Pair.of(networkHandler, channel); 68 | return READING_SESSIONS.computeIfAbsent(key, ReadingSession::new).receive(data, maxLength); 69 | } 70 | 71 | /** 72 | * Sends a packet type ID as a VarInt, and then the given Compound tag. 73 | */ 74 | public static void sendPacketTypeAndCompound(Identifier channel, int packetType, NbtCompound data, ServerPlayerEntity player) 75 | { 76 | sendPacketTypeAndCompound(channel, packetType, data, player.networkHandler); 77 | } 78 | 79 | /** 80 | * Sends a packet type ID as a VarInt, and then the given Compound tag. 81 | */ 82 | public static void sendPacketTypeAndCompound(Identifier channel, int packetType, NbtCompound data, ServerPlayNetworkHandler networkHandler) 83 | { 84 | PacketByteBuf buf = new PacketByteBuf(Unpooled.buffer()); 85 | buf.writeVarInt(packetType); 86 | buf.writeNbt(data); 87 | 88 | send(buf, channel, networkHandler); 89 | } 90 | 91 | private static class ReadingSession 92 | { 93 | private final Pair key; 94 | private int expectedSize = -1; 95 | private PacketByteBuf received; 96 | 97 | private ReadingSession(Pair key) 98 | { 99 | this.key = key; 100 | } 101 | 102 | @Nullable 103 | private PacketByteBuf receive(PacketByteBuf data, int maxLength) 104 | { 105 | data.readerIndex(0); 106 | data = PacketUtils.slice(data); 107 | 108 | if (this.expectedSize < 0) 109 | { 110 | this.expectedSize = data.readVarInt(); 111 | 112 | if (this.expectedSize > maxLength) 113 | { 114 | throw new IllegalArgumentException("Payload too large"); 115 | } 116 | 117 | this.received = new PacketByteBuf(Unpooled.buffer(this.expectedSize)); 118 | } 119 | 120 | this.received.writeBytes(data.readBytes(data.readableBytes())); 121 | 122 | if (this.received.writerIndex() >= this.expectedSize) 123 | { 124 | READING_SESSIONS.remove(this.key); 125 | return this.received; 126 | } 127 | 128 | return null; 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/servux/network/ServerPacketChannelHandler.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.servux.network; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.function.BiConsumer; 7 | import com.google.common.base.Charsets; 8 | import net.minecraft.network.PacketByteBuf; 9 | import net.minecraft.network.packet.c2s.play.CustomPayloadC2SPacket; 10 | import net.minecraft.server.network.ServerPlayNetworkHandler; 11 | import net.minecraft.util.Identifier; 12 | import fi.dy.masa.servux.mixin.IMixinCustomPayloadC2SPacket; 13 | import fi.dy.masa.servux.network.util.PacketUtils; 14 | 15 | public class ServerPacketChannelHandler 16 | { 17 | public static final Identifier REGISTER = new Identifier("minecraft:register"); 18 | public static final Identifier UNREGISTER = new Identifier("minecraft:unregister"); 19 | 20 | public static final ServerPacketChannelHandler INSTANCE = new ServerPacketChannelHandler(); 21 | 22 | private final HashMap handlers = new HashMap<>(); 23 | private final HashMap subscribableHandlers = new HashMap<>(); 24 | 25 | private ServerPacketChannelHandler() 26 | { 27 | } 28 | 29 | public void registerServerChannelHandler(IPluginChannelHandler handler) 30 | { 31 | synchronized (this.handlers) 32 | { 33 | List toRegister = new ArrayList<>(); 34 | Identifier channel = handler.getChannel(); 35 | 36 | if (this.handlers.containsKey(channel) == false) 37 | { 38 | this.handlers.put(channel, handler); 39 | toRegister.add(channel); 40 | 41 | if (handler.isSubscribable()) 42 | { 43 | this.subscribableHandlers.put(channel, handler); 44 | } 45 | } 46 | } 47 | } 48 | 49 | public void unregisterServerChannelHandler(IPluginChannelHandler handler) 50 | { 51 | synchronized (this.handlers) 52 | { 53 | List toUnRegister = new ArrayList<>(); 54 | Identifier channel = handler.getChannel(); 55 | 56 | if (this.handlers.remove(channel, handler)) 57 | { 58 | toUnRegister.add(channel); 59 | this.subscribableHandlers.remove(channel); 60 | } 61 | } 62 | } 63 | 64 | public void processPacketFromClient(CustomPayloadC2SPacket packet, ServerPlayNetworkHandler netHandler) 65 | { 66 | IMixinCustomPayloadC2SPacket accessor = ((IMixinCustomPayloadC2SPacket) packet); 67 | Identifier channel = accessor.servux_getChannel(); 68 | PacketByteBuf data = accessor.servux_getData(); 69 | 70 | IPluginChannelHandler handler; 71 | 72 | synchronized (this.handlers) 73 | { 74 | handler = this.handlers.get(channel); 75 | } 76 | 77 | if (handler != null) 78 | { 79 | final PacketByteBuf slice = PacketUtils.retainedSlice(data); 80 | this.schedule(() -> this.handleReceivedData(channel, slice, handler, netHandler), netHandler); 81 | } 82 | else if (channel.equals(REGISTER)) 83 | { 84 | final List channels = getChannels(data); 85 | this.schedule(() -> this.updateRegistrationForChannels(channels, IPluginChannelHandler::subscribe, netHandler), netHandler); 86 | } 87 | else if (channel.equals(UNREGISTER)) 88 | { 89 | final List channels = getChannels(data); 90 | this.schedule(() -> this.updateRegistrationForChannels(channels, IPluginChannelHandler::unsubscribe, netHandler), netHandler); 91 | } 92 | } 93 | 94 | private void updateRegistrationForChannels(List channels, 95 | BiConsumer action, 96 | ServerPlayNetworkHandler netHandler) 97 | { 98 | for (Identifier channel : channels) 99 | { 100 | IPluginChannelHandler handler = this.subscribableHandlers.get(channel); 101 | 102 | if (handler != null) 103 | { 104 | action.accept(handler, netHandler); 105 | } 106 | } 107 | } 108 | 109 | private void handleReceivedData(Identifier channel, PacketByteBuf data, 110 | IPluginChannelHandler handler, ServerPlayNetworkHandler netHandler) 111 | { 112 | PacketByteBuf buf = PacketSplitter.receive(channel, data, netHandler); 113 | data.release(); 114 | 115 | // Finished the complete packet 116 | if (buf != null) 117 | { 118 | handler.onPacketReceived(buf, netHandler); 119 | buf.release(); 120 | } 121 | } 122 | 123 | private void schedule(Runnable task, ServerPlayNetworkHandler netHandler) 124 | { 125 | netHandler.getPlayer().getServer().execute(task); 126 | } 127 | 128 | private static List getChannels(PacketByteBuf buf) 129 | { 130 | buf.readerIndex(0); 131 | buf = PacketUtils.slice(buf); 132 | 133 | byte[] bytes = new byte[buf.readableBytes()]; 134 | buf.readBytes(bytes); 135 | String channelString = new String(bytes, Charsets.UTF_8); 136 | List channels = new ArrayList<>(); 137 | 138 | for (String channel : channelString.split("\0")) 139 | { 140 | try 141 | { 142 | Identifier id = new Identifier(channel); 143 | channels.add(id); 144 | } 145 | catch (Exception ignore) {} 146 | } 147 | 148 | return channels; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/servux/network/packet/StructureDataPacketHandler.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.servux.network.packet; 2 | 3 | import net.minecraft.server.network.ServerPlayNetworkHandler; 4 | import net.minecraft.util.Identifier; 5 | import fi.dy.masa.servux.dataproviders.StructureDataProvider; 6 | import fi.dy.masa.servux.network.IPluginChannelHandler; 7 | 8 | public class StructureDataPacketHandler implements IPluginChannelHandler 9 | { 10 | public static final Identifier CHANNEL = new Identifier("servux:structures"); 11 | public static final StructureDataPacketHandler INSTANCE = new StructureDataPacketHandler(); 12 | 13 | public static final int PROTOCOL_VERSION = 1; 14 | public static final int PACKET_S2C_METADATA = 1; 15 | public static final int PACKET_S2C_STRUCTURE_DATA = 2; 16 | 17 | @Override 18 | public Identifier getChannel() 19 | { 20 | return CHANNEL; 21 | } 22 | 23 | @Override 24 | public boolean isSubscribable() 25 | { 26 | return true; 27 | } 28 | 29 | @Override 30 | public boolean subscribe(ServerPlayNetworkHandler netHandler) 31 | { 32 | return StructureDataProvider.INSTANCE.register(netHandler.getPlayer()); 33 | } 34 | 35 | @Override 36 | public boolean unsubscribe(ServerPlayNetworkHandler netHandler) 37 | { 38 | return StructureDataProvider.INSTANCE.unregister(netHandler.getPlayer()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/servux/network/util/PacketUtils.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.servux.network.util; 2 | 3 | import java.util.Objects; 4 | import net.minecraft.network.PacketByteBuf; 5 | import io.netty.buffer.ByteBuf; 6 | 7 | public class PacketUtils 8 | { 9 | /** 10 | * Wraps the newly created buf from {@code buf.slice()} in a PacketByteBuf. 11 | * 12 | * @param buf the original ByteBuf 13 | * @return a slice of the buffer 14 | * @see ByteBuf#slice() 15 | */ 16 | public static PacketByteBuf slice(ByteBuf buf) 17 | { 18 | Objects.requireNonNull(buf, "ByteBuf cannot be null"); 19 | return new PacketByteBuf(buf.slice()); 20 | } 21 | 22 | /** 23 | * Wraps the newly created buf from {@code buf.retainedSlice()} in a PacketByteBuf. 24 | * 25 | * @param buf the original ByteBuf 26 | * @return a slice of the buffer 27 | * @see ByteBuf#retainedSlice() 28 | */ 29 | public static PacketByteBuf retainedSlice(ByteBuf buf) 30 | { 31 | Objects.requireNonNull(buf, "ByteBuf cannot be null"); 32 | return new PacketByteBuf(buf.retainedSlice()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/servux/util/JsonUtils.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.servux.util; 2 | 3 | import java.io.File; 4 | import java.io.FileReader; 5 | import java.io.FileWriter; 6 | import java.io.IOException; 7 | import java.util.Map; 8 | import javax.annotation.Nonnull; 9 | import javax.annotation.Nullable; 10 | import com.google.gson.Gson; 11 | import com.google.gson.GsonBuilder; 12 | import com.google.gson.JsonArray; 13 | import com.google.gson.JsonElement; 14 | import com.google.gson.JsonObject; 15 | import com.google.gson.JsonParser; 16 | import net.minecraft.util.math.BlockPos; 17 | import net.minecraft.util.math.Vec3d; 18 | import net.minecraft.util.math.Vec3i; 19 | import fi.dy.masa.servux.Servux; 20 | 21 | public class JsonUtils 22 | { 23 | public static final Gson GSON = new GsonBuilder().setPrettyPrinting().create(); 24 | 25 | @Nullable 26 | public static JsonObject getNestedObject(JsonObject parent, String key, boolean create) 27 | { 28 | if (parent.has(key) == false || parent.get(key).isJsonObject() == false) 29 | { 30 | if (create == false) 31 | { 32 | return null; 33 | } 34 | 35 | JsonObject obj = new JsonObject(); 36 | parent.add(key, obj); 37 | return obj; 38 | } 39 | else 40 | { 41 | return parent.get(key).getAsJsonObject(); 42 | } 43 | } 44 | 45 | public static boolean hasBoolean(JsonObject obj, String name) 46 | { 47 | JsonElement el = obj.get(name); 48 | 49 | if (el != null && el.isJsonPrimitive()) 50 | { 51 | try 52 | { 53 | el.getAsBoolean(); 54 | return true; 55 | } 56 | catch (Exception ignore) {} 57 | } 58 | 59 | return false; 60 | } 61 | 62 | public static boolean hasInteger(JsonObject obj, String name) 63 | { 64 | JsonElement el = obj.get(name); 65 | 66 | if (el != null && el.isJsonPrimitive()) 67 | { 68 | try 69 | { 70 | el.getAsInt(); 71 | return true; 72 | } 73 | catch (Exception ignore) {} 74 | } 75 | 76 | return false; 77 | } 78 | 79 | public static boolean hasLong(JsonObject obj, String name) 80 | { 81 | JsonElement el = obj.get(name); 82 | 83 | if (el != null && el.isJsonPrimitive()) 84 | { 85 | try 86 | { 87 | el.getAsLong(); 88 | return true; 89 | } 90 | catch (Exception ignore) {} 91 | } 92 | 93 | return false; 94 | } 95 | 96 | public static boolean hasFloat(JsonObject obj, String name) 97 | { 98 | JsonElement el = obj.get(name); 99 | 100 | if (el != null && el.isJsonPrimitive()) 101 | { 102 | try 103 | { 104 | el.getAsFloat(); 105 | return true; 106 | } 107 | catch (Exception ignore) {} 108 | } 109 | 110 | return false; 111 | } 112 | 113 | public static boolean hasDouble(JsonObject obj, String name) 114 | { 115 | JsonElement el = obj.get(name); 116 | 117 | if (el != null && el.isJsonPrimitive()) 118 | { 119 | try 120 | { 121 | el.getAsDouble(); 122 | return true; 123 | } 124 | catch (Exception ignore) {} 125 | } 126 | 127 | return false; 128 | } 129 | 130 | public static boolean hasString(JsonObject obj, String name) 131 | { 132 | JsonElement el = obj.get(name); 133 | 134 | if (el != null && el.isJsonPrimitive()) 135 | { 136 | try 137 | { 138 | el.getAsString(); 139 | return true; 140 | } 141 | catch (Exception ignore) {} 142 | } 143 | 144 | return false; 145 | } 146 | 147 | public static boolean hasObject(JsonObject obj, String name) 148 | { 149 | JsonElement el = obj.get(name); 150 | return el != null && el.isJsonObject(); 151 | } 152 | 153 | public static boolean hasArray(JsonObject obj, String name) 154 | { 155 | JsonElement el = obj.get(name); 156 | return el != null && el.isJsonArray(); 157 | } 158 | 159 | public static boolean getBooleanOrDefault(JsonObject obj, String name, boolean defaultValue) 160 | { 161 | if (obj.has(name) && obj.get(name).isJsonPrimitive()) 162 | { 163 | try 164 | { 165 | return obj.get(name).getAsBoolean(); 166 | } 167 | catch (Exception ignore) {} 168 | } 169 | 170 | return defaultValue; 171 | } 172 | 173 | public static int getIntegerOrDefault(JsonObject obj, String name, int defaultValue) 174 | { 175 | if (obj.has(name) && obj.get(name).isJsonPrimitive()) 176 | { 177 | try 178 | { 179 | return obj.get(name).getAsInt(); 180 | } 181 | catch (Exception ignore) {} 182 | } 183 | 184 | return defaultValue; 185 | } 186 | 187 | public static long getLongOrDefault(JsonObject obj, String name, long defaultValue) 188 | { 189 | if (obj.has(name) && obj.get(name).isJsonPrimitive()) 190 | { 191 | try 192 | { 193 | return obj.get(name).getAsLong(); 194 | } 195 | catch (Exception ignore) {} 196 | } 197 | 198 | return defaultValue; 199 | } 200 | 201 | public static float getFloatOrDefault(JsonObject obj, String name, float defaultValue) 202 | { 203 | if (obj.has(name) && obj.get(name).isJsonPrimitive()) 204 | { 205 | try 206 | { 207 | return obj.get(name).getAsFloat(); 208 | } 209 | catch (Exception ignore) {} 210 | } 211 | 212 | return defaultValue; 213 | } 214 | 215 | public static double getDoubleOrDefault(JsonObject obj, String name, double defaultValue) 216 | { 217 | if (obj.has(name) && obj.get(name).isJsonPrimitive()) 218 | { 219 | try 220 | { 221 | return obj.get(name).getAsDouble(); 222 | } 223 | catch (Exception ignore) {} 224 | } 225 | 226 | return defaultValue; 227 | } 228 | 229 | public static String getStringOrDefault(JsonObject obj, String name, String defaultValue) 230 | { 231 | if (obj.has(name) && obj.get(name).isJsonPrimitive()) 232 | { 233 | try 234 | { 235 | return obj.get(name).getAsString(); 236 | } 237 | catch (Exception ignore) {} 238 | } 239 | 240 | return defaultValue; 241 | } 242 | 243 | public static boolean getBoolean(JsonObject obj, String name) 244 | { 245 | return getBooleanOrDefault(obj, name, false); 246 | } 247 | 248 | public static int getInteger(JsonObject obj, String name) 249 | { 250 | return getIntegerOrDefault(obj, name, 0); 251 | } 252 | 253 | public static long getLong(JsonObject obj, String name) 254 | { 255 | return getLongOrDefault(obj, name, 0); 256 | } 257 | 258 | public static float getFloat(JsonObject obj, String name) 259 | { 260 | return getFloatOrDefault(obj, name, 0); 261 | } 262 | 263 | public static double getDouble(JsonObject obj, String name) 264 | { 265 | return getDoubleOrDefault(obj, name, 0); 266 | } 267 | 268 | @Nullable 269 | public static String getString(JsonObject obj, String name) 270 | { 271 | return getStringOrDefault(obj, name, null); 272 | } 273 | 274 | public static boolean hasBlockPos(JsonObject obj, String name) 275 | { 276 | return blockPosFromJson(obj, name) != null; 277 | } 278 | 279 | public static JsonArray blockPosToJson(Vec3i pos) 280 | { 281 | JsonArray arr = new JsonArray(); 282 | 283 | arr.add(pos.getX()); 284 | arr.add(pos.getY()); 285 | arr.add(pos.getZ()); 286 | 287 | return arr; 288 | } 289 | 290 | @Nullable 291 | public static BlockPos blockPosFromJson(JsonObject obj, String name) 292 | { 293 | if (hasArray(obj, name)) 294 | { 295 | JsonArray arr = obj.getAsJsonArray(name); 296 | 297 | if (arr.size() == 3) 298 | { 299 | try 300 | { 301 | return new BlockPos(arr.get(0).getAsInt(), arr.get(1).getAsInt(), arr.get(2).getAsInt()); 302 | } 303 | catch (Exception ignore) {} 304 | } 305 | } 306 | 307 | return null; 308 | } 309 | 310 | public static boolean hasVec3d(JsonObject obj, String name) 311 | { 312 | return vec3dFromJson(obj, name) != null; 313 | } 314 | 315 | public static JsonArray vec3dToJson(Vec3d vec) 316 | { 317 | JsonArray arr = new JsonArray(); 318 | 319 | arr.add(vec.x); 320 | arr.add(vec.y); 321 | arr.add(vec.z); 322 | 323 | return arr; 324 | } 325 | 326 | @Nullable 327 | public static Vec3d vec3dFromJson(JsonObject obj, String name) 328 | { 329 | if (hasArray(obj, name)) 330 | { 331 | JsonArray arr = obj.getAsJsonArray(name); 332 | 333 | if (arr.size() == 3) 334 | { 335 | try 336 | { 337 | return new Vec3d(arr.get(0).getAsDouble(), arr.get(1).getAsDouble(), arr.get(2).getAsDouble()); 338 | } 339 | catch (Exception ignore) {} 340 | } 341 | } 342 | 343 | return null; 344 | } 345 | 346 | // https://stackoverflow.com/questions/29786197/gson-jsonobject-copy-value-affected-others-jsonobject-instance 347 | @Nonnull 348 | public static JsonObject deepCopy(@Nonnull JsonObject jsonObject) 349 | { 350 | JsonObject result = new JsonObject(); 351 | 352 | for (Map.Entry entry : jsonObject.entrySet()) 353 | { 354 | result.add(entry.getKey(), deepCopy(entry.getValue())); 355 | } 356 | 357 | return result; 358 | } 359 | 360 | @Nonnull 361 | public static JsonArray deepCopy(@Nonnull JsonArray jsonArray) 362 | { 363 | JsonArray result = new JsonArray(); 364 | 365 | for (JsonElement e : jsonArray) 366 | { 367 | result.add(deepCopy(e)); 368 | } 369 | 370 | return result; 371 | } 372 | 373 | @Nonnull 374 | public static JsonElement deepCopy(@Nonnull JsonElement jsonElement) 375 | { 376 | if (jsonElement.isJsonPrimitive() || jsonElement.isJsonNull()) 377 | { 378 | return jsonElement; // these are immutable anyway 379 | } 380 | else if (jsonElement.isJsonObject()) 381 | { 382 | return deepCopy(jsonElement.getAsJsonObject()); 383 | } 384 | else if (jsonElement.isJsonArray()) 385 | { 386 | return deepCopy(jsonElement.getAsJsonArray()); 387 | } 388 | else 389 | { 390 | throw new UnsupportedOperationException("Unsupported element: " + jsonElement); 391 | } 392 | } 393 | 394 | @Nullable 395 | public static JsonElement parseJsonFromString(String str) 396 | { 397 | try 398 | { 399 | return JsonParser.parseString(str); 400 | } 401 | catch (Exception ignore) {} 402 | 403 | return null; 404 | } 405 | 406 | @Nullable 407 | public static JsonElement parseJsonFile(File file) 408 | { 409 | if (file != null && file.exists() && file.isFile() && file.canRead()) 410 | { 411 | String fileName = file.getAbsolutePath(); 412 | 413 | try (FileReader reader = new FileReader(file)) 414 | { 415 | return JsonParser.parseReader(reader); 416 | } 417 | catch (Exception e) 418 | { 419 | Servux.logger.error("Failed to parse the JSON file '{}'", fileName, e); 420 | } 421 | } 422 | 423 | return null; 424 | } 425 | 426 | /** 427 | * Converts the given JsonElement tree into its string representation. 428 | * If compact is true, then it's written in one line without spaces or line breaks. 429 | */ 430 | public static String jsonToString(JsonElement element, boolean compact) 431 | { 432 | Gson gson = compact ? new Gson() : GSON; 433 | return gson.toJson(element); 434 | } 435 | 436 | public static boolean writeJsonToFile(JsonElement root, File file) 437 | { 438 | return writeJsonToFile(GSON, root, file); 439 | } 440 | 441 | public static boolean writeJsonToFile(Gson gson, JsonElement root, File file) 442 | { 443 | FileWriter writer = null; 444 | 445 | try 446 | { 447 | writer = new FileWriter(file); 448 | writer.write(gson.toJson(root)); 449 | writer.close(); 450 | 451 | return true; 452 | } 453 | catch (IOException e) 454 | { 455 | Servux.logger.warn("Failed to write JSON data to file '{}'", file.getAbsolutePath(), e); 456 | } 457 | finally 458 | { 459 | try 460 | { 461 | if (writer != null) 462 | { 463 | writer.close(); 464 | } 465 | } 466 | catch (Exception e) 467 | { 468 | Servux.logger.warn("Failed to close JSON file", e); 469 | } 470 | } 471 | 472 | return false; 473 | } 474 | } 475 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/servux/util/PlayerDimensionPosition.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.servux.util; 2 | 3 | import net.minecraft.entity.player.PlayerEntity; 4 | import net.minecraft.util.math.BlockPos; 5 | import net.minecraft.world.dimension.DimensionType; 6 | 7 | public class PlayerDimensionPosition 8 | { 9 | protected DimensionType dimensionType; 10 | protected BlockPos pos; 11 | 12 | public PlayerDimensionPosition(PlayerEntity player) 13 | { 14 | this.setPosition(player); 15 | } 16 | 17 | public boolean dimensionChanged(PlayerEntity player) 18 | { 19 | return this.dimensionType != player.getEntityWorld().getDimension(); 20 | } 21 | 22 | public boolean needsUpdate(PlayerEntity player, int distanceThreshold) 23 | { 24 | if (player.getEntityWorld().getDimension() != this.dimensionType) 25 | { 26 | return true; 27 | } 28 | 29 | BlockPos pos = player.getBlockPos(); 30 | 31 | return Math.abs(pos.getX() - this.pos.getX()) > distanceThreshold || 32 | Math.abs(pos.getY() - this.pos.getY()) > distanceThreshold || 33 | Math.abs(pos.getZ() - this.pos.getZ()) > distanceThreshold; 34 | } 35 | 36 | public void setPosition(PlayerEntity player) 37 | { 38 | this.dimensionType = player.getEntityWorld().getDimension(); 39 | this.pos = player.getBlockPos(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/fi/dy/masa/servux/util/Timeout.java: -------------------------------------------------------------------------------- 1 | package fi.dy.masa.servux.util; 2 | 3 | public class Timeout 4 | { 5 | protected int lastSync; 6 | 7 | public Timeout(int currentTick) 8 | { 9 | this.lastSync = currentTick; 10 | } 11 | 12 | public boolean needsUpdate(int currentTick, int timeout) 13 | { 14 | return currentTick - this.lastSync >= timeout; 15 | } 16 | 17 | public void setLastSync(int tickCounter) 18 | { 19 | this.lastSync = tickCounter; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "servux", 4 | "name": "Servux", 5 | "version": "${mod_version}", 6 | 7 | "description": "Server-side support and integration for masa's client mods", 8 | "license": "LGPLv3", 9 | "authors": [ 10 | "masa" 11 | ], 12 | "contact": { 13 | "homepage": "https://www.curseforge.com/minecraft/mc-mods/servux", 14 | "sources": "https://github.com/maruohon/servux", 15 | "twitter": "https://twitter.com/maruohon", 16 | "discord": "https://discordapp.com/channels/211786369951989762/453662800460644354/" 17 | }, 18 | 19 | "environment": "server", 20 | 21 | "entrypoints": { 22 | "main": [ 23 | "fi.dy.masa.servux.Servux" 24 | ] 25 | }, 26 | 27 | "mixins": [ 28 | "mixins.servux.json" 29 | ], 30 | 31 | "depends": { 32 | "minecraft": ">=1.19" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/resources/mixins.servux.json: -------------------------------------------------------------------------------- 1 | { 2 | "required": true, 3 | "package": "fi.dy.masa.servux.mixin", 4 | "compatibilityLevel": "JAVA_17", 5 | "minVersion": "0.8", 6 | "mixins": [ 7 | "IMixinCustomPayloadC2SPacket", 8 | "MixinMinecraftServer", 9 | "MixinServerPlayNetworkHandler", 10 | "MixinThreadedAnvilChunkStorage" 11 | ], 12 | "injectors": { 13 | "defaultRequire": 1 14 | } 15 | } --------------------------------------------------------------------------------