commit 2e9d684622a424e632e2300a42c117d9d06d0d92 Author: TONY_All Date: Wed Feb 2 10:03:32 2022 +0800 Init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f35d9b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +### Idea Gradle template +### IDEA ### +.idea +*.iml + +### Gradle ### +.gradle +build diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..1e8ef2f --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,37 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") version "1.6.10" + id("io.izzel.taboolib") version "1.34" +} + +group = "cc.maxmc.servux" +version = "1.0.0" + +taboolib { + description { + contributors { + name("TONY_All") + } + } + install("common") + install("platform-bukkit") + version = "6.0.7-26" +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("com.google.code.gson:gson:2.8.9") + compileOnly(kotlin("stdlib")) + compileOnly("it.unimi.dsi:fastutil:8.5.6") + compileOnly("ink.ptms.core:v11800:11800:api") + compileOnly("ink.ptms.core:v11800:11800:mapped") + compileOnly("ink.ptms.core:v11800:11800:universal") +} + +tasks.withType { + kotlinOptions.jvmTarget = "16" +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..7fc6f1f --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7454180 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..69a9715 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..744e882 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MSYS* | MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..afe6831 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,3 @@ + +rootProject.name = "ServuxSpigot" + diff --git a/src/main/kotlin/cc/maxmc/servux/ServuxServer.kt b/src/main/kotlin/cc/maxmc/servux/ServuxServer.kt new file mode 100644 index 0000000..295c8fb --- /dev/null +++ b/src/main/kotlin/cc/maxmc/servux/ServuxServer.kt @@ -0,0 +1,6 @@ +package cc.maxmc.servux + +import taboolib.platform.BukkitPlugin + +object ServuxServer: BukkitPlugin() + diff --git a/src/main/kotlin/cc/maxmc/servux/dataproviders/DataProviderBase.kt b/src/main/kotlin/cc/maxmc/servux/dataproviders/DataProviderBase.kt new file mode 100644 index 0000000..4644186 --- /dev/null +++ b/src/main/kotlin/cc/maxmc/servux/dataproviders/DataProviderBase.kt @@ -0,0 +1,17 @@ +package cc.maxmc.servux.dataproviders + +import net.minecraft.resources.MinecraftKey +import kotlin.math.max + +abstract class DataProviderBase protected constructor( + override val name: String, + override val networkChannel: MinecraftKey, + override val protocolVersion: Int, + override val description: String +) : IDataProvider { + override var isEnabled = false + override var tickRate = 40 + protected set(tickRate) { + field = max(tickRate, 1) + } +} \ No newline at end of file diff --git a/src/main/kotlin/cc/maxmc/servux/dataproviders/IDataProvider.kt b/src/main/kotlin/cc/maxmc/servux/dataproviders/IDataProvider.kt new file mode 100644 index 0000000..981ce2e --- /dev/null +++ b/src/main/kotlin/cc/maxmc/servux/dataproviders/IDataProvider.kt @@ -0,0 +1,80 @@ +package cc.maxmc.servux.dataproviders + +import net.minecraft.resources.MinecraftKey + +interface IDataProvider { + /** + * Returns the simple name for this data provider. + * This should preferably be a lower case alphanumeric string with no + * other special characters than '-' and '_'. + * This name will be used in the enable/disable commands as the argument + * and also as the config file key/identifier. + * + * @return + */ + val name: String + + /** + * Returns the description of this data provider. + * Used in the command to list the available providers and to check the status + * of a given provider. + * + * @return + */ + val description: String + + /** + * Returns the network channel name used by this data provider to listen + * for incoming data requests and to respond and send the requested data. + * + * @return + */ + val networkChannel: MinecraftKey + + /** + * Returns the current protocol version this provider supports + * + * @return + */ + val protocolVersion: Int + /** + * Returns true if this data provider is currently enabled. + * + * @return + */ + /** + * Enables or disables this data provider + * + * @param enabled + */ + var isEnabled: Boolean + + /** + * Returns whether or not this data provider should get ticked to periodically send some data, + * or if it's only listening for incoming requests and responds to them directly. + * + * @return + */ + fun shouldTick(): Boolean { + return false + } + + /** + * Returns the interval in game ticks that this data provider should be ticked at + * + * @return + */ + val tickRate: Int + + /** + * Called at the given tick rate + * + * @param tickCounter The current server tick (since last server start) + */ + fun tick(tickCounter: Int) {} + /** + * Returns the network packet handler used for this data provider. + * @return + */ + // IPluginChannelHandler getPacketHandler(); +} \ No newline at end of file diff --git a/src/main/kotlin/cc/maxmc/servux/dataproviders/StructureDataProvider.kt b/src/main/kotlin/cc/maxmc/servux/dataproviders/StructureDataProvider.kt new file mode 100644 index 0000000..cee94ec --- /dev/null +++ b/src/main/kotlin/cc/maxmc/servux/dataproviders/StructureDataProvider.kt @@ -0,0 +1,203 @@ +package cc.maxmc.servux.dataproviders + +import cc.maxmc.servux.network.packet.StructureDataPacketHandler +import cc.maxmc.servux.util.ChunkPos +import cc.maxmc.servux.util.PlayerDimensionPosition +import cc.maxmc.servux.util.Timeout +import it.unimi.dsi.fastutil.longs.LongOpenHashSet +import it.unimi.dsi.fastutil.longs.LongSet +import net.minecraft.nbt.NBTTagCompound +import net.minecraft.nbt.NBTTagList +import net.minecraft.server.MinecraftServer +import net.minecraft.world.level.ChunkCoordIntPair +import net.minecraft.world.level.levelgen.feature.StructureGenerator +import net.minecraft.world.level.levelgen.structure.StructureStart +import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext +import org.bukkit.Bukkit +import org.bukkit.Chunk +import org.bukkit.World +import org.bukkit.craftbukkit.v1_18_R1.CraftChunk +import org.bukkit.craftbukkit.v1_18_R1.CraftWorld +import org.bukkit.entity.Player +import java.util.* +import kotlin.math.abs + +object StructureDataProvider : DataProviderBase("structure_bounding_boxes", + StructureDataPacketHandler.CHANNEL, + StructureDataPacketHandler.PROTOCOL_VERSION, + "Structure Bounding Boxes data for structures such as Witch Huts, Ocean Monuments, Nether Fortresses etc.") { + + val registeredPlayers = HashMap() + val timeouts = HashMap>() + val metadata: NBTTagCompound = NBTTagCompound() + const val timeout = 30 * 20 + const val updateInterval = 40 + var retainDistance = 0 + + override fun shouldTick(): Boolean = true + + override fun tick(tickCounter: Int) { + if (tickCounter % updateInterval == 0) { + if (registeredPlayers.isNotEmpty()) { + // System.out.printf("=======================\n"); + // System.out.printf("tick: %d - %s\n", tickCounter, this.isEnabled()); + retainDistance = Bukkit.getViewDistance() + 2 + val uuidIter = registeredPlayers.keys.iterator() + while (uuidIter.hasNext()) { + val uuid = uuidIter.next() + val player: Player = Bukkit.getPlayer(uuid) ?: return run { + timeouts.remove(uuid) + uuidIter.remove() + } + this.checkForDimensionChange(player) + this.refreshTrackedChunks(player, tickCounter) + } + } + } + } + + private fun checkForDimensionChange(player: Player) { + val uuid: UUID = player.uniqueId + val playerPos: PlayerDimensionPosition? = registeredPlayers[uuid] + if (playerPos == null || playerPos.dimensionChanged(player)) { + timeouts.remove(uuid) + registeredPlayers.computeIfAbsent(uuid) { + PlayerDimensionPosition(player) + }.setPosition(player) + } + } + + private fun refreshTrackedChunks(player: Player, tickCounter: Int) { + val uuid: UUID = player.uniqueId + val map: MutableMap? = timeouts[uuid] + if (map != null) { + // System.out.printf("refreshTrackedChunks: timeouts: %d\n", map.size()); + this.sendAndRefreshExpiredStructures(player, map, tickCounter) + } + } + + private fun sendAndRefreshExpiredStructures( + player: Player, + map: MutableMap, + tickCounter: Int, + ) { + val positionsToUpdate: MutableSet = HashSet() + map.forEach { (key, timeout) -> + if (timeout.needsUpdate(tickCounter, this.timeout)) { + positionsToUpdate.add(key) + } + } + if (positionsToUpdate.isNotEmpty()) { + val world = player.world + val center = player.location.chunk + val references: MutableMap, LongSet> = HashMap() + for (pos in positionsToUpdate) { + if (this.isOutOfRange(pos, center)) { + map.remove(pos) + } else { + this.getStructureReferencesFromChunk(pos.x, pos.z, world, references) + val timeout = map[pos] + timeout?.lastSync = tickCounter + } + } + + // System.out.printf("sendAndRefreshExpiredStructures: positionsToUpdate: %d -> references: %d, to: %d\n", positionsToUpdate.size(), references.size(), this.timeout); + if (references.isNotEmpty()) { + this.sendStructures(player, references, tickCounter) + } + } + } + + private fun isOutOfRange(pos: ChunkPos, center: Chunk): Boolean { + val chunkRadius = retainDistance + return abs(pos.x - center.x) > chunkRadius || + abs(pos.z - center.z) > chunkRadius + } + + private fun getStructureReferencesFromChunk( + chunkX: Int, + chunkZ: Int, + world: World, + references: MutableMap, LongSet>, + ) { + if (!world.isChunkLoaded(chunkX, chunkZ)) { + return + } + + val chunk = ((world.getChunkAt(chunkX, chunkZ) as CraftChunk).handle) as net.minecraft.world.level.chunk.Chunk + chunk.allReferences.forEach { (feature, startChunks) -> + if (!startChunks.isEmpty() && feature != StructureGenerator.MINESHAFT) { + references.merge(feature, startChunks) { oldSet, entrySet -> + val newSet = LongOpenHashSet(oldSet) + newSet.addAll(entrySet) + return@merge newSet + } + } + } + } + + private fun sendStructures( + player: Player, + references: Map, LongSet>, + tickCounter: Int, + ) { + val world: World = player.world + val starts: Map> = this.getStructureStartsFromReferences(world, references) + if (starts.isNotEmpty()) { + this.addOrRefreshTimeouts(player.uniqueId, references, tickCounter) + val structureList: NBTTagList = getStructureList(starts, world) + // System.out.printf("sendStructures: starts: %d -> structureList: %d. refs: %s\n", starts.size(), structureList.size(), references.keySet()); + val tag = NBTTagCompound() + tag.put("Structures", structureList) + PacketSplitter.sendPacketTypeAndCompound(StructureDataPacketHandler.CHANNEL, + StructureDataPacketHandler.PACKET_S2C_STRUCTURE_DATA, + tag, + player) + } + } + + private fun getStructureStartsFromReferences( + world: World, + references: Map, LongSet>, + ): Map> { + val starts = HashMap>() + references.forEach { (feature, startChunks) -> + val iter = startChunks.iterator() + while (iter.hasNext()) { + val pos = ChunkPos(iter.nextLong()) + if (!world.isChunkLoaded(pos.x, pos.z)) { + continue + } + val chunk = (world.getChunkAt(pos.x, + pos.z) as CraftChunk).handle as net.minecraft.world.level.chunk.Chunk + val start: StructureStart<*> = chunk.getStartForFeature(feature) ?: return@forEach + starts[pos] = start + } + } + + // System.out.printf("getStructureStartsFromReferences: references: %d -> starts: %d\n", references.size(), starts.size()); + return starts + } + + private fun addOrRefreshTimeouts(uuid: UUID, references: Map, LongSet>, tickCounter: Int) { + // System.out.printf("addOrRefreshTimeouts: references: %d\n", references.size()); + val map: MutableMap = timeouts.computeIfAbsent(uuid + ) { HashMap() } + for (chunks: LongSet in references.values) { + for (chunkPosLong in chunks) { + val pos = ChunkPos(chunkPosLong) + map.computeIfAbsent(pos) { + Timeout(tickCounter) + }.lastSync = tickCounter + } + } + } + + private fun getStructureList(structures: Map>, world: World): NBTTagList { + val list = NBTTagList() + structures.forEach { (pos, value) -> + list.add(value.createTag(StructurePieceSerializationContext.fromLevel((world as CraftWorld).handle), ChunkCoordIntPair(pos.x, pos.z))) + } + return list + } +} \ No newline at end of file diff --git a/src/main/kotlin/cc/maxmc/servux/network/packet/StructureDataPacketHandler.java b/src/main/kotlin/cc/maxmc/servux/network/packet/StructureDataPacketHandler.java new file mode 100644 index 0000000..25b229f --- /dev/null +++ b/src/main/kotlin/cc/maxmc/servux/network/packet/StructureDataPacketHandler.java @@ -0,0 +1,30 @@ +package cc.maxmc.servux.network.packet; + +import cc.maxmc.servux.dataproviders.StructureDataProvider; +import net.minecraft.resources.MinecraftKey; +import org.bukkit.entity.Player; + +public class StructureDataPacketHandler { + public static final MinecraftKey CHANNEL = new MinecraftKey("servux:structures"); + public static final StructureDataPacketHandler INSTANCE = new StructureDataPacketHandler(); + + public static final int PROTOCOL_VERSION = 1; + public static final int PACKET_S2C_METADATA = 1; + public static final int PACKET_S2C_STRUCTURE_DATA = 2; + + public MinecraftKey getChannel() { + return CHANNEL; + } + + public boolean isSubscribable() { + return true; + } + + public boolean subscribe(Player player) { + return StructureDataProvider.register(netHandler.player); + } + + public boolean unsubscribe(Player player) { + return StructureDataProvider.unregister(netHandler.player); + } +} diff --git a/src/main/kotlin/cc/maxmc/servux/util/ChunkPos.kt b/src/main/kotlin/cc/maxmc/servux/util/ChunkPos.kt new file mode 100644 index 0000000..a9a4d02 --- /dev/null +++ b/src/main/kotlin/cc/maxmc/servux/util/ChunkPos.kt @@ -0,0 +1,5 @@ +package cc.maxmc.servux.util + +data class ChunkPos(var x: Int, var z: Int) { + constructor(pos: Long) : this(pos.toInt(), (pos shr 32).toInt()) +} diff --git a/src/main/kotlin/cc/maxmc/servux/util/JsonUtils.kt b/src/main/kotlin/cc/maxmc/servux/util/JsonUtils.kt new file mode 100644 index 0000000..f208149 --- /dev/null +++ b/src/main/kotlin/cc/maxmc/servux/util/JsonUtils.kt @@ -0,0 +1,330 @@ +package cc.maxmc.servux.util + +import com.google.gson.* +import org.bukkit.util.Vector +import taboolib.common.platform.function.info +import taboolib.common.platform.function.warning +import taboolib.platform.BukkitPlugin +import java.io.File +import java.io.FileReader +import java.io.FileWriter +import java.io.IOException + +object JsonUtils { + val GSON = GsonBuilder().setPrettyPrinting().create() + fun getNestedObject(parent: JsonObject, key: String?, create: Boolean): JsonObject? { + return if (!parent.has(key) || !parent[key].isJsonObject) { + if (!create) { + return null + } + val obj = JsonObject() + parent.add(key, obj) + obj + } else { + parent[key].asJsonObject + } + } + + fun hasBoolean(obj: JsonObject, name: String?): Boolean { + val el = obj[name] + if (el != null && el.isJsonPrimitive) { + try { + el.asBoolean + return true + } catch (_: Exception) { } + } + return false + } + + fun hasInteger(obj: JsonObject, name: String?): Boolean { + val el = obj[name] + if (el != null && el.isJsonPrimitive) { + try { + el.asInt + return true + } catch (e: Exception) { + } + } + return false + } + + fun hasLong(obj: JsonObject, name: String?): Boolean { + val el = obj[name] + if (el != null && el.isJsonPrimitive) { + try { + el.asLong + return true + } catch (_: Exception) { + } + } + return false + } + + fun hasFloat(obj: JsonObject, name: String?): Boolean { + val el = obj[name] + if (el != null && el.isJsonPrimitive) { + try { + el.asFloat + return true + } catch (_: Exception) { + } + } + return false + } + + fun hasDouble(obj: JsonObject, name: String?): Boolean { + val el = obj[name] + if (el != null && el.isJsonPrimitive) { + try { + el.asDouble + return true + } catch (_: Exception) { } + } + return false + } + + fun hasString(obj: JsonObject, name: String?): Boolean { + val el = obj[name] + if (el != null && el.isJsonPrimitive) { + try { + el.asString + return true + } catch (_: Exception) { } + } + return false + } + + fun hasObject(obj: JsonObject, name: String?): Boolean { + val el = obj[name] + return el != null && el.isJsonObject + } + + fun hasArray(obj: JsonObject, name: String?): Boolean { + val el = obj[name] + return el != null && el.isJsonArray + } + + fun getBooleanOrDefault(obj: JsonObject, name: String?, defaultValue: Boolean): Boolean { + if (obj.has(name) && obj[name].isJsonPrimitive) { + try { + return obj[name].asBoolean + } catch (e: Exception) { + } + } + return defaultValue + } + + fun getIntegerOrDefault(obj: JsonObject, name: String?, defaultValue: Int): Int { + if (obj.has(name) && obj[name].isJsonPrimitive) { + try { + return obj[name].asInt + } catch (e: Exception) { + } + } + return defaultValue + } + + fun getLongOrDefault(obj: JsonObject, name: String?, defaultValue: Long): Long { + if (obj.has(name) && obj[name].isJsonPrimitive) { + try { + return obj[name].asLong + } catch (e: Exception) { + } + } + return defaultValue + } + + fun getFloatOrDefault(obj: JsonObject, name: String?, defaultValue: Float): Float { + if (obj.has(name) && obj[name].isJsonPrimitive) { + try { + return obj[name].asFloat + } catch (e: Exception) { + } + } + return defaultValue + } + + fun getDoubleOrDefault(obj: JsonObject, name: String?, defaultValue: Double): Double { + if (obj.has(name) && obj[name].isJsonPrimitive) { + try { + return obj[name].asDouble + } catch (e: Exception) { + } + } + return defaultValue + } + + fun getStringOrDefault(obj: JsonObject, name: String?, defaultValue: String?): String? { + if (obj.has(name) && obj[name].isJsonPrimitive) { + try { + return obj[name].asString + } catch (e: Exception) { + } + } + return defaultValue + } + + fun getBoolean(obj: JsonObject, name: String?): Boolean { + return getBooleanOrDefault(obj, name, false) + } + + fun getInteger(obj: JsonObject, name: String?): Int { + return getIntegerOrDefault(obj, name, 0) + } + + fun getLong(obj: JsonObject, name: String?): Long { + return getLongOrDefault(obj, name, 0) + } + + fun getFloat(obj: JsonObject, name: String?): Float { + return getFloatOrDefault(obj, name, 0f) + } + + fun getDouble(obj: JsonObject, name: String?): Double { + return getDoubleOrDefault(obj, name, 0.0) + } + + fun getString(obj: JsonObject, name: String?): String? { + return getStringOrDefault(obj, name, null) + } + + fun hasBlockPos(obj: JsonObject, name: String?): Boolean { + return blockPosFromJson(obj, name) != null + } + + fun blockPosToJson(pos: Vector): JsonArray { + val arr = JsonArray() + arr.add(pos.x) + arr.add(pos.y) + arr.add(pos.z) + return arr + } + + fun blockPosFromJson(obj: JsonObject, name: String?): Vector? { + if (hasArray(obj, name)) { + val arr = obj.getAsJsonArray(name) + if (arr.size() == 3) { + try { + return Vector(arr[0].asInt, arr[1].asInt, arr[2].asInt) + } catch (ignored: Exception) { + } + } + } + return null + } + + fun hasVec3d(obj: JsonObject, name: String?): Boolean { + return vec3dFromJson(obj, name) != null + } + + fun vec3dToJson(vec: Vector): JsonArray { + val arr = JsonArray() + arr.add(vec.x) + arr.add(vec.y) + arr.add(vec.z) + return arr + } + + fun vec3dFromJson(obj: JsonObject, name: String?): Vector? { + if (hasArray(obj, name)) { + val arr = obj.getAsJsonArray(name) + if (arr.size() == 3) { + try { + return Vector(arr[0].asDouble, arr[1].asDouble, arr[2].asDouble) + } catch (e: Exception) { + } + } + } + return null + } + + // https://stackoverflow.com/questions/29786197/gson-jsonobject-copy-value-affected-others-jsonobject-instance + fun deepCopy(jsonObject: JsonObject): JsonObject { + val result = JsonObject() + for ((key, value) in jsonObject.entrySet()) { + result.add(key, deepCopy(value)) + } + return result + } + + fun deepCopy(jsonArray: JsonArray): JsonArray { + val result = JsonArray() + for (e in jsonArray) { + result.add(deepCopy(e)) + } + return result + } + + fun deepCopy(jsonElement: JsonElement): JsonElement { + return if (jsonElement.isJsonPrimitive || jsonElement.isJsonNull) { + jsonElement // these are immutable anyway + } else if (jsonElement.isJsonObject) { + deepCopy(jsonElement.asJsonObject) + } else if (jsonElement.isJsonArray) { + deepCopy(jsonElement.asJsonArray) + } else { + throw UnsupportedOperationException("Unsupported element: $jsonElement") + } + } + + fun parseJsonFromString(str: String?): JsonElement? { + try { + val parser = JsonParser() + return parser.parse(str) + } catch (e: Exception) { + } + return null + } + + fun parseJsonFile(file: File?): JsonElement? { + if (file != null && file.exists() && file.isFile && file.canRead()) { + val fileName = file.absolutePath + try { + val parser = JsonParser() + val reader = FileReader(file) + val element = parser.parse(reader) + reader.close() + return element + } catch (e: Exception) { + warning("Failed to parse the JSON file '" + fileName + "'" + e.message) + } + } + return null + } + + /** + * Converts the given JsonElement tree into its string representation. + * If **compact** is true, then it's written in one line without spaces or line breaks. + * + * @param element + * @param compact + * @return + */ + fun jsonToString(element: JsonElement?, compact: Boolean): String { + val gson = if (compact) Gson() else GSON + return gson.toJson(element) + } + + fun writeJsonToFile(root: JsonElement?, file: File): Boolean { + return writeJsonToFile(GSON, root, file) + } + + fun writeJsonToFile(gson: Gson, root: JsonElement?, file: File): Boolean { + var writer: FileWriter? = null + try { + writer = FileWriter(file) + writer.write(gson.toJson(root)) + writer.close() + return true + } catch (e: IOException) { + warning("Failed to write JSON data to file '" + file.absolutePath + "'" + e.message) + } finally { + try { + writer?.close() + } catch (e: Exception) { + warning("Failed to close JSON file" + e.message) + } + } + return false + } +} \ No newline at end of file diff --git a/src/main/kotlin/cc/maxmc/servux/util/PlayerDimensionPosition.kt b/src/main/kotlin/cc/maxmc/servux/util/PlayerDimensionPosition.kt new file mode 100644 index 0000000..796426e --- /dev/null +++ b/src/main/kotlin/cc/maxmc/servux/util/PlayerDimensionPosition.kt @@ -0,0 +1,34 @@ +package cc.maxmc.servux.util + +import org.bukkit.World +import org.bukkit.entity.Player +import org.bukkit.util.BlockVector +import org.bukkit.util.Vector +import kotlin.math.abs + +class PlayerDimensionPosition(player: Player) { + lateinit var world: World + lateinit var pos: BlockVector + + init { + setPosition(player) + } + + fun dimensionChanged(player: Player): Boolean { + return world != player.world + } + + fun needsUpdate(player: Player, distanceThreshold: Int): Boolean { + if (player.world != world) { + return true + } + val pos: Vector = player.location.toVector().toBlockVector() + return abs(pos.x - this.pos.x) > distanceThreshold || abs(pos.y - this.pos.y) > distanceThreshold || abs( + pos.z - this.pos.z) > distanceThreshold + } + + fun setPosition(player: Player) { + world = player.world + pos = player.location.toVector().toBlockVector() + } +} \ No newline at end of file diff --git a/src/main/kotlin/cc/maxmc/servux/util/Timeout.kt b/src/main/kotlin/cc/maxmc/servux/util/Timeout.kt new file mode 100644 index 0000000..19790cd --- /dev/null +++ b/src/main/kotlin/cc/maxmc/servux/util/Timeout.kt @@ -0,0 +1,9 @@ +package cc.maxmc.servux.util + +class Timeout(var lastSync: Int) { + + fun needsUpdate(currentTick: Int, timeout: Int): Boolean { + return currentTick - lastSync >= timeout + } + +} \ No newline at end of file