commit 9eca6d191046aeac4239a32f151afb99e6691e28 Author: Gabriel Tofvesson Date: Sun Sep 19 03:35:55 2021 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..82a5d36 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +# Project exclude paths +/.gradle/ +/build/ +/debug/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..659bf43 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..611e7c8 --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..1369a44 --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..cc57d5f --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules/SpigotPortals.main.iml b/.idea/modules/SpigotPortals.main.iml new file mode 100644 index 0000000..fa63d4b --- /dev/null +++ b/.idea/modules/SpigotPortals.main.iml @@ -0,0 +1,12 @@ + + + + + + + SPIGOT + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..797acea --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..d7add72 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,133 @@ +import kr.entree.spigradle.kotlin.spigot + +plugins { + kotlin("jvm") version "1.5.30" + id("kr.entree.spigradle") version "2.2.4" +} + +group = "dev.w1zzrd" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + compileOnly(spigot("1.17.1")) + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.5.30") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.2") +} + +tasks.getByName("test") { + useJUnitPlatform() +} + +tasks.getByName("compileKotlin") { + kotlinOptions { + jvmTarget = "16" + } +} + +tasks.getByName("compileTestKotlin") { + kotlinOptions { + jvmTarget = "16" + } +} + +spigot { + description = "Simple portals plugin" + depends = listOf("Kotlin") + load = kr.entree.spigradle.data.Load.STARTUP + + commands { + create("portals") { + aliases = listOf("p", "portal") + description = "Create a portal" + permission = "portals.create" + permissionMessage = "You do not have permission to create portals" + } + } + + permissions { + create("portals.create") { + description = "Allows portal creation" + defaults = "true" + } + + create("portals.list") { + description = "Allows listing portals" + defaults = "true" + } + + create("portals.list.other") { + description = "Allows listing other players' portals" + defaults = "op" + } + + create("portals.tp") { + description = "Allows teleporting to a portal" + defaults = "op" + } + + create("portals.tp.other") { + description = "Allows teleporting to other players' portals" + defaults = "op" + } + + create("portals.modify.remove") { + description = "Allows portal removal" + defaults = "true" + } + + create("portals.modify.edit") { + description = "Allows portal position and orientation editing" + defaults = "true" + } + + create("portals.modify.target") { + description = "Allows targeting/un-targeting a portal as a destination" + defaults = "true" + } + + create("portals.modify.allow") { + description = "Allows another player to use a portal" + defaults = "true" + } + + create("portals.modify.other") { + description = "Allows modification of other players' portals" + defaults = "op" + } + + create("portals.modify.*") { + description = "Wildcard portal modification" + defaults = "op" + children = mapOf( + "portals.modify.remove" to true, + "portals.modify.edit" to true, + "portals.modify.target" to true, + "portals.modify.allow" to true, + "portals.modify.other" to true + ) + } + + create("portals.*") { + description = "Top-level wildcard" + defaults = "op" + children = mapOf( + "portals.create" to true, + "portals.list" to true, + "portals.list.other" to true, + "portals.tp" to true, + "portals.tp.other" to true, + "portals.modify.*" to true + ) + } + } + + debug { + jvmArgs("-Xmx4G") + buildVersion = "1.17.1" + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..e161f0e --- /dev/null +++ b/gradle.properties @@ -0,0 +1,2 @@ +kotlin.code.style=official +org.gradle.jvmargs=--illegal-access=permit \ No newline at end of file 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 100644 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..c7e0d93 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "SpigotPortals" + diff --git a/src/main/kotlin/Buffers.kt b/src/main/kotlin/Buffers.kt new file mode 100644 index 0000000..786c30a --- /dev/null +++ b/src/main/kotlin/Buffers.kt @@ -0,0 +1,151 @@ +import java.nio.ByteBuffer +import kotlin.math.min +import kotlin.reflect.KMutableProperty0 +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty0 +import kotlin.reflect.KProperty1 + +fun ByteBuffer.writePackedRange(value: Double, min: Double, max: Double) { + packedULong = ((value - min)/max).toULong() +} + +fun ByteBuffer.writePackedRange(value: Float, min: Float, max: Float) { + packedULong = ((value - min)/max).toULong() +} + +var ByteBuffer.packedULong: ULong + get() = readPacked().first + set(value) { value.toPacked(this) } + +var ByteBuffer.packedUInt: UInt + get() = packedULong.toUInt() + set(value) { packedULong = value.toULong() } + +var ByteBuffer.packedUShort: UShort + get() = packedULong.toUShort() + set(value) { packedULong = value.toULong() } + +var ByteBuffer.packedLong: Long + get() = readPacked().first.deInterlace() + set(value) { value.interlace().toPacked(this) } + +var ByteBuffer.packedInt: Int + get() = packedLong.toInt() + set(value) { packedLong = value.toLong() } + +var ByteBuffer.packedShort: Short + get() = packedLong.toShort() + set(value) { packedLong = value.toLong() } + +var ByteBuffer.packedChar: Char + get() = packedInt.toChar() + set(value) { packedInt = value.code } + +fun ByteBuffer.readPackedRangeDouble(min: Double, max: Double) = (packedLong * max) + min +fun ByteBuffer.readPackedRangeFloat(min: Float, max: Float) = (packedLong * max) + min + + +class ReallocatingBuffer(buffer: ByteBuffer, val growthFactor: Float = 1.0f) { + // Handles reads/writes + private abstract inner class ReallocatingAccessor( + private val getter: () -> T, + private val setter: (T) -> Unit + ) { + protected abstract fun sizeOf(value: T): Int + + constructor(property: KMutableProperty0): this(property::get, property::set) + + operator fun getValue(thisRef: Any?, property: KProperty<*>) = getter() + operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + ensureSize(size) + setter(value) + } + } + + private inner class StaticReallocatingAccessor( + getter: () -> T, + setter: (T) -> Unit, + private val size: Int + ): ReallocatingAccessor(getter, setter) { + constructor(property: KMutableProperty0, size: Int): this(property::get, property::set, size) + + override fun sizeOf(value: T) = size + } + + private inner class VarIntReallocatingAccessor( + getter: () -> T, + setter: (T) -> Unit, + private val sizeGetter: T.() -> Int + ): ReallocatingAccessor(getter, setter) { + constructor(getter: () -> T, setter: (T) -> Unit, sizeGetter: KProperty1): this(getter, setter, sizeGetter::get) + constructor(property: KMutableProperty0, sizeGetter: T.() -> Int): this(property::get, property::set, sizeGetter) + constructor(property: KMutableProperty0, sizeGetter: KProperty1): this(property::get, property::set, sizeGetter::get) + + override fun sizeOf(value: T) = sizeGetter(value) + } + + var buffer = buffer + private set + + var byte: Byte by StaticReallocatingAccessor(buffer::get, buffer::put, 1) + var char: Char by StaticReallocatingAccessor(buffer::getChar, buffer::putChar, 2) + var short: Short by StaticReallocatingAccessor(buffer::getShort, buffer::putShort, 2) + var int: Int by StaticReallocatingAccessor(buffer::getInt, buffer::putInt, 4) + var long: Long by StaticReallocatingAccessor(buffer::getLong, buffer::putLong, 8) + var uShort: UShort by StaticReallocatingAccessor({ buffer.short.toUShort() }, { buffer.putShort(it.toShort()) }, 2) + var uInt: UInt by StaticReallocatingAccessor({ buffer.int.toUInt() }, { buffer.putInt(it.toInt()) }, 4) + var uLong: ULong by StaticReallocatingAccessor({ buffer.long.toULong() }, { buffer.putLong(it.toLong()) }, 8) + var float: Float by StaticReallocatingAccessor(buffer::getFloat, buffer::putFloat, 4) + var double: Double by StaticReallocatingAccessor(buffer::getDouble, buffer::putDouble, 4) + + var packedChar: Char by VarIntReallocatingAccessor(buffer::packedChar, Char::varIntSize) + var packedShort: Short by VarIntReallocatingAccessor(buffer::packedShort, Short::varIntSize) + var packedInt: Int by VarIntReallocatingAccessor(buffer::packedInt, Int::varIntSize) + var packedLong: Long by VarIntReallocatingAccessor(buffer::packedLong, Long::varIntSize) + var packedUShort: UShort by VarIntReallocatingAccessor(buffer::packedUShort, UShort::varIntSize) + var packedUInt: UInt by VarIntReallocatingAccessor(buffer::packedUInt, UInt::varIntSize) + var packedULong: ULong by VarIntReallocatingAccessor(buffer::packedULong, ULong::varIntSize) + + var position: Int + get() = buffer.position() + set(value) { buffer.position(value) } + + var size: Int + get() = buffer.capacity() + set(value) { + if (buffer.capacity() != value) { + val oldPosition = position + val newBuffer = if(buffer.isDirect) ByteBuffer.allocateDirect(value) else ByteBuffer.allocate(value) + + position = 0 + newBuffer.put(buffer) + position = min(oldPosition, value) + + buffer = newBuffer + } + } + + private fun ensureSize(newBytes: Int) { + if (position + newBytes > size) + size = (size * (growthFactor + 1f)).toInt() + } + + + fun putPackedFloat(value: Float, min: Float, max: Float) { + ensureSize(value.varIntSize(min, max)) + buffer.writePackedRange(value, min, max) + } + + fun putPackedDouble(value: Double, min: Double, max: Double) { + ensureSize(value.varIntSize(min, max)) + buffer.writePackedRange(value, min, max) + } + + fun getPackedFloat(min: Float, max: Float) = buffer.readPackedRangeFloat(min, max) + fun getPackedDouble(min: Double, max: Double) = buffer.readPackedRangeDouble(min, max) + + fun ensureAtLeast(minSize: Int) { + if (minSize > size) + size = minSize + } +} \ No newline at end of file diff --git a/src/main/kotlin/Numbers.kt b/src/main/kotlin/Numbers.kt new file mode 100644 index 0000000..d41ab5f --- /dev/null +++ b/src/main/kotlin/Numbers.kt @@ -0,0 +1,106 @@ +import java.nio.ByteBuffer + +private val threadLocalBuffer = ThreadLocal.withInitial { ByteBuffer.allocateDirect(9) } + +private fun ByteBuffer.putUByte(value: ULong) = put((value and 0xFFUL).toByte()) +private fun String.parseHex(index: Int) = ((this[index * 2].digitToInt(16) shl 4) or (this[1 + index * 2].digitToInt(16))).toULong() and 0xFFUL + +fun Long.interlace() = (this shr 63).toULong() xor (this shl 1).toULong() +fun ULong.deInterlace() = (this shr 1).toLong() xor ((this shl 63).toLong() shr 63) + +fun ULong.toPacked(target: ByteBuffer): Int { + if (this <= 240UL) { + target.putUByte(this) + return 1 + } + else if (this <= 2287UL) { + target.putUByte(((this - 240UL) shr 8) + 241UL) + target.putUByte(this - 240UL) + return 2 + } + else if (this <= 67823UL) { + target.putUByte(249UL) + target.putUByte((this - 2288UL) shr 8) + target.putUByte(this - 2288UL) + return 3 + } + else { + var header = 255UL + var match = 0x00FF_FFFF_FFFF_FFFFUL + + while (this <= match) { + --header + match = match shr 8 + } + + target.putUByte(header) + for (i in 0 until (header - 247UL).toInt()) + target.putUByte(this shr (i shl 3)) + + return (header - 247UL).toInt() + } +} + +fun ULong.toPackedString(): String { + val buffer = threadLocalBuffer.get().position(0) + val len = toPacked(buffer) + + val builder = StringBuilder(len * 2) + for (i in 0 until len) { + builder.append(((buffer[i].toInt() ushr 4) and 0xF).toString(16)) + builder.append((buffer[i].toInt() and 0xF).toString(16)) + } + + return builder.toString() +} + +fun ByteBuffer.readPacked(): Pair { + val header = get().toULong() and 0xFFUL + if (header <= 240UL) return header to 1 + else if (header <= 248UL) return (240UL + ((header - 241UL) shl 8) + (get().toULong() and 0xFFUL)) to 2 + else if (header == 249UL) return (2288UL + ((get().toULong() and 0xFFUL) shl 8) + (get().toULong() and 0xFFUL)) to 3 + + var res = (get().toULong() and 0xFFUL) or ((get().toULong() and 0xFFUL) shl 8) or ((get().toULong() and 0xFFUL) shl 16) + + for (cmp in 3 until (header - 247UL).toInt()) + res = res or ((get().toULong() and 0xFFUL) shl (cmp shl 3)) + + return res to (header - 246UL).toInt() +} + +fun String.readPacked(): Pair { + val header = parseHex(0) + if (header <= 240UL) return header to 1 + else if (header <= 248UL) return (240UL + ((header - 241UL) shl 8) + parseHex(1)) to 2 + else if (header == 249UL) return (2288UL + (parseHex(1) shl 8) + parseHex(2)) to 3 + + var res = parseHex(1) or (parseHex(2) shl 8) or (parseHex(3) shl 16) + + for (cmp in 3 until (header - 247UL).toInt()) + res = res or (parseHex(cmp + 1) shl (cmp shl 3)) + + return res to (header - 247UL).toInt() +} + +val ULong.varIntSize + get() = + if (this <= 240UL) 1 + else if(this <= 2287UL) 2 + else if(this <= 67823UL) 2 + else if(this <= 16777215UL) 2 + else if(this <= 4294967295UL) 2 + else if(this <= 1099511627775UL) 2 + else if(this <= 281474976710655UL) 2 + else if(this <= 72057594037927935UL) 2 + else 9 + +val UInt.varIntSize get() = toULong().varIntSize +val UShort.varIntSize get() = toULong().varIntSize + +val Long.varIntSize get() = interlace().varIntSize +val Int.varIntSize get() = toLong().interlace().varIntSize +val Short.varIntSize get() = toLong().interlace().varIntSize +val Char.varIntSize get() = code.varIntSize + +fun Float.varIntSize(min: Float, max: Float) = ((this - min)/max).toULong().varIntSize +fun Double.varIntSize(min: Double, max: Double) = ((this - min)/max).toULong().varIntSize \ No newline at end of file diff --git a/src/main/kotlin/Ordering.kt b/src/main/kotlin/Ordering.kt new file mode 100644 index 0000000..131f9b9 --- /dev/null +++ b/src/main/kotlin/Ordering.kt @@ -0,0 +1,22 @@ +fun T.compareByOrder(other: T, vararg comparables: T.() -> Comparable<*>): Int { + for (comparable in comparables) { + @Suppress("UNCHECKED_CAST") + val result = (comparable(this) as Comparable).compareTo(comparable(other)) + + if (result != 0) + return result + } + + return 0 +} + +fun compareValues(vararg pairs: Pair<() -> Comparable<*>, () -> Comparable<*>>): Int { + for ((a, b) in pairs) { + val result = (a() as Comparable).compareTo(b()) + + if (result != 0) + return result + } + + return 0 +} \ No newline at end of file diff --git a/src/main/kotlin/Portal.kt b/src/main/kotlin/Portal.kt new file mode 100644 index 0000000..bfd0746 --- /dev/null +++ b/src/main/kotlin/Portal.kt @@ -0,0 +1,220 @@ +import org.bukkit.Location +import org.bukkit.OfflinePlayer +import org.bukkit.World +import org.bukkit.entity.Player +import java.nio.ByteBuffer +import java.util.* +import kotlin.Comparator +import kotlin.collections.ArrayList +import kotlin.experimental.and +import kotlin.experimental.inv +import kotlin.experimental.or + +private val PLAYER_COMPARATOR = Comparator { a, b -> a.uniqueId.compareTo(b.uniqueId) } +val LOCATION_COMPARATOR = Comparator { a, b -> a.compareByOrder(b, { world!!.uid }, Location::getBlockX, Location::getBlockY, Location::getBlockZ) } +val PORTAL_COMPARATOR = Comparator { a, b -> a.compareByOrder(b, { world.uid }, Portal::x, Portal::y, Portal::z, Portal::id) } + + +private val threadLocalInputBuffer = ThreadLocal.withInitial { ReallocatingBuffer(ByteBuffer.allocate(96)) } +private val threadLocalOutputBuffer = ThreadLocal.withInitial { ReallocatingBuffer(ByteBuffer.allocate(96)) } + +private val method_encode0 = run { + val method = Base64::class.java.getDeclaredMethod( + "encode0", + ByteArray::class.java, + Int::class.java, + Int::class.java, + ByteArray::class.java + ) + method.isAccessible = true + return@run method +} + +private val method_encodedOutLength = run { + val method = Base64::class.java.getDeclaredMethod("encodedOutLength", Int::class.java, Boolean::class.java) + method.isAccessible = true + return@run method +} + +enum class PortalResult { + NO_LINK, + DISALLOWED, + SUCCESS +} + +enum class PortalFlag { + PUBLIC, LINKED, NO_EXCLUSIONS; + + private val flagBit: Byte + get() = (1 shl ordinal).toByte() + + fun isFlagSet(flagMap: Byte) = flagMap and flagBit != 0.toByte() + fun setFlag(flagMap: Byte) = flagMap or flagBit + fun unSetFlag(flagMap: Byte) = flagMap and flagBit.inv() + + fun setFlagValue(flagMap: Byte, set: Boolean) = + if (set) setFlag(flagMap) + else unSetFlag(flagMap) +} + +fun applyFlags(flags: Byte, flagMap: Map): Byte { + var result = flags + for ((flag, value) in flagMap) + result = flag.setFlagValue(result, value) + return result +} + +fun flagMapOf(vararg flags: PortalFlag): Byte { + var result: Byte = 0 + + for (flag in flags) + result = flag.setFlag(result) + + return result +} + +fun readFlags(flagMap: Byte): List { + val list = ArrayList() + + for (flag in PortalFlag.values()) + if (flag.isFlagSet(flagMap)) + list += flag + + return list +} + + +class Portal( + val id: UUID, + val owner: OfflinePlayer, + val world: World, + val x: Int, + val y: Int, + val z: Int, + var yaw: Float, + var pitch: Float, + private var flags: Byte, + link: UUID?, + val accessExclusions: SortedList +) { + init { + flags = applyFlags( + flags, + mapOf( + PortalFlag.NO_EXCLUSIONS to accessExclusions.isEmpty(), + PortalFlag.LINKED to (link != null) + ) + ) + } + + var public: Boolean + get() = PortalFlag.PUBLIC.isFlagSet(flags) + set(value) { + accessExclusions.clear() + flags = PortalFlag.PUBLIC.setFlagValue(flags, value) + } + + var link = link + private set(value) { + flags = PortalFlag.LINKED.setFlagValue(flags, value != null) + field = value + } + + internal val location: Location + get() = Location(world, x.toDouble(), y.toDouble(), z.toDouble(), yaw, pitch) + + fun canEnter(player: OfflinePlayer) = + player.uniqueId == owner.uniqueId || (accessExclusions.contains(player) != public) + + fun enterPortal(player: Player, portalMapper: MapFunction): PortalResult { + val remoteLink = link + + return if (remoteLink == null) PortalResult.NO_LINK + else if (!canEnter(player)) PortalResult.DISALLOWED + else { + val portal = portalMapper(remoteLink) + if (portal == null) { + link = null + return PortalResult.NO_LINK + } + + player.teleport(portal.location) + PortalResult.SUCCESS + } + } + + fun toCompressedString(worldMapper: MapFunction, playerMapper: MapFunction): String { + val buffer = threadLocalInputBuffer.get() + buffer.position = 0 + + buffer.long = id.mostSignificantBits + buffer.long = id.leastSignificantBits + buffer.packedUInt = playerMapper(owner) + buffer.packedUInt = worldMapper(world) + buffer.packedInt = x + buffer.packedInt = y + buffer.packedInt = z + buffer.putPackedFloat(yaw, 0f, 360f) + buffer.putPackedFloat(pitch, 0f, 360f) + buffer.byte = flags + if (PortalFlag.LINKED.isFlagSet(flags)) { + val link = link!! + buffer.long = link.mostSignificantBits + buffer.long = link.leastSignificantBits + } + if (accessExclusions.size > 0) { + for (player in accessExclusions) + buffer.packedUInt = playerMapper(player) + } + + val outputBuffer = threadLocalOutputBuffer.get() + outputBuffer.position = 0 + val encoder = Base64.getEncoder().withoutPadding() + + outputBuffer.ensureAtLeast(method_encodedOutLength.invoke(encoder, buffer.position, true) as Int) + val len = method_encode0.invoke(encoder, buffer.buffer.array(), 0, outputBuffer.buffer.array()) as Int + + + return buffer.position.toString(16).padStart(8, '0') + String(outputBuffer.buffer.array(), 0, len) + } +} + +fun readCompressedPortal( + data: String, + worldMapper: MapFunction, + playerMapper: MapFunction +): Portal? { + val inputBuffer = threadLocalInputBuffer.get() + inputBuffer.position = 0 + + val dataLen = data.substring(0 until 8).toInt(16) + + inputBuffer.ensureAtLeast(dataLen) + + Base64.getDecoder().decode(data.substring(8).toByteArray(Charsets.ISO_8859_1), inputBuffer.buffer.array()) + + val flags: Byte + return Portal( + UUID(inputBuffer.long, inputBuffer.long), + playerMapper(inputBuffer.packedUInt) ?: return null, + worldMapper(inputBuffer.packedUInt) ?: return null, + inputBuffer.packedInt, + inputBuffer.packedInt, + inputBuffer.packedInt, + inputBuffer.getPackedFloat(0f, 360f), + inputBuffer.getPackedFloat(0f, 360f), + run { + flags = inputBuffer.byte + return@run flags + }, + if (PortalFlag.LINKED.isFlagSet(flags)) UUID(inputBuffer.long, inputBuffer.long) + else null, + if (PortalFlag.NO_EXCLUSIONS.isFlagSet(flags)) SortedList.create(comparator = PLAYER_COMPARATOR) + else run { + val collect = SortedList.create(comparator = PLAYER_COMPARATOR) + while (inputBuffer.position < dataLen) + collect += playerMapper(inputBuffer.packedUInt) ?: continue + return@run collect + } + ) +} \ No newline at end of file diff --git a/src/main/kotlin/PortalManager.kt b/src/main/kotlin/PortalManager.kt new file mode 100644 index 0000000..8459b81 --- /dev/null +++ b/src/main/kotlin/PortalManager.kt @@ -0,0 +1,72 @@ +import org.bukkit.Location +import org.bukkit.configuration.ConfigurationSection +import org.bukkit.event.EventHandler +import org.bukkit.event.player.PlayerMoveEvent +import java.util.* +import kotlin.collections.ArrayList + +private const val PATH_PLAYERS = "players" +private const val PATH_WORLDS = "worlds" +private const val PATH_PORTALS = "portals" + + +class PortalManager(private val data: ConfigurationSection, private val config: () -> ConfigurationSection) { + private val players = PlayerMapper(data, PATH_PLAYERS) + private val worlds = WorldMapper(data, PATH_WORLDS) + private var portals = SortedList.create(comparator = PORTAL_COMPARATOR) + + fun reload() { + players.reload() + worlds.reload() + + val portalList = ArrayList() + data.getStringList(PATH_PORTALS).forEach { + portalList += readCompressedPortal(it, worlds::getValue, players::getValue) ?: return@forEach + } + portals = SortedList.create(PORTAL_COMPARATOR, portalList) + } + + fun save() { + players.save() + worlds.save() + data.set(PATH_PORTALS, portals.map { it.toCompressedString(worlds::getIndex, players::getIndex) }) + } + + + @EventHandler + fun onPlayerMove(moveEvent: PlayerMoveEvent) { + fun UUID.portalMapper() = portals.firstOrNull { it.id == this } + val to = moveEvent.to + + if (!moveEvent.isCancelled && to != null) { + val found = getPortalsAt(to) + + found?.firstOrNull { it.owner.uniqueId == moveEvent.player.uniqueId } + ?.enterPortal(moveEvent.player, UUID::portalMapper) + ?: found?.firstOrNull { it.enterPortal(moveEvent.player, UUID::portalMapper) == PortalResult.SUCCESS } + } + } + + + // This is a very hot function: allocate with extreme care! + fun getPortalsAt(location: Location): LinkedList? { + fun portalFinder(portal: Portal) = + compareValues( + location.world!!::getUID to portal.world::getUID, + location::getBlockX to portal::x, + location::getBlockY to portal::y, + location::getBlockZ to portal::z + ) + + // Don't allocate list unless there is data + var index = portals.binarySearch(comparison = ::portalFinder) + if (index < 0) return null + + val result = LinkedList() + + do result += portals[index] + while (++index < portals.size && portalFinder(portals[index]) == 0) + + return result + } +} \ No newline at end of file diff --git a/src/main/kotlin/PortalsPlugin.kt b/src/main/kotlin/PortalsPlugin.kt new file mode 100644 index 0000000..c20e194 --- /dev/null +++ b/src/main/kotlin/PortalsPlugin.kt @@ -0,0 +1,23 @@ +import kr.entree.spigradle.annotations.SpigotPlugin +import org.bukkit.plugin.java.JavaPlugin +import java.io.File + +@SpigotPlugin +class PortalsPlugin: JavaPlugin() { + private val data = YamlFile.loadFile(File(dataFolder, "data.yml")) + private val portalManager = PortalManager(data) { config } + + override fun onEnable() { + super.onEnable() + + saveDefaultConfig() + data.load() + } + + override fun onDisable() { + super.onDisable() + + data.save() + saveConfig() + } +} \ No newline at end of file diff --git a/src/main/kotlin/SortedList.kt b/src/main/kotlin/SortedList.kt new file mode 100644 index 0000000..ec86ae9 --- /dev/null +++ b/src/main/kotlin/SortedList.kt @@ -0,0 +1,87 @@ +import java.util.* + +class SortedList private constructor( + private val underlying: MutableList, + private val comparator: Comparator +): MutableList by underlying { + + companion object { + fun create( + type: Class, + underlying: MutableList, + comparator: Comparator + ) = SortedList(Collections.checkedList(underlying, type), comparator) + + inline fun > create( + underlying: MutableList = ArrayList(), + comparator: Comparator = Comparator { a, b -> a.compareTo(b) } + ) = create(T::class.java, underlying, comparator) + + inline fun create(comparator: Comparator, underlying: MutableList = ArrayList()) = + create(T::class.java, underlying, comparator) + } + + init { + if (underlying.size > 0) + underlying.sortWith(comparator) + } + + override fun add(element: E): Boolean { + val index = underlying.binarySearch(element, comparator) + + if (index < 0) + underlying.add(-(index + 1), element) + else + underlying[index] = element + + return index < 0 + } + + override fun add(index: Int, element: E) = + throw UnsupportedOperationException("Cannot insert at index for sorted list") + + override fun addAll(elements: Collection): Boolean { + var result = false + + for (element in elements) + result = add(element) || result + + return result + } + + override fun addAll(index: Int, elements: Collection) = + throw UnsupportedOperationException("Cannot insert at index for sorted list") + + override fun contains(element: E) = underlying.binarySearch(element, comparator) >= 0 + + override fun containsAll(elements: Collection): Boolean { + for (element in elements) + if (!contains(element)) + return false + + return true + } + + override fun remove(element: E): Boolean { + val index = underlying.binarySearch(element, comparator) + + if (index >= 0) { + underlying.removeAt(index) + return true + } + + return false + } + + override fun removeAt(index: Int) = + underlying.removeAt(index) + + override fun removeAll(elements: Collection): Boolean { + var result = false + + for (element in elements) + result = remove(element) || result + + return result + } +} \ No newline at end of file diff --git a/src/main/kotlin/UUIDMapper.kt b/src/main/kotlin/UUIDMapper.kt new file mode 100644 index 0000000..4234c93 --- /dev/null +++ b/src/main/kotlin/UUIDMapper.kt @@ -0,0 +1,80 @@ +import org.bukkit.Bukkit +import org.bukkit.OfflinePlayer +import org.bukkit.World +import org.bukkit.configuration.ConfigurationSection +import java.nio.ByteBuffer +import java.util.* + +typealias MapFunction = K.() -> V + +private val threadLocalBuffer = ThreadLocal.withInitial { ByteBuffer.allocate(16) } + +private val STRING_TO_UUID: MapFunction = { + val buffer = threadLocalBuffer.get().position(0) + + Base64.getDecoder().decode(toByteArray(Charsets.ISO_8859_1), buffer.array()) + + UUID(buffer.long, buffer.long) +} + +private val UUID_TO_STRING: MapFunction = { + Base64.getEncoder() + .withoutPadding() + .encodeToString( + threadLocalBuffer.get() + .position(0) + .putLong(mostSignificantBits) + .putLong(leastSignificantBits) + .array() + ) +} + +open class UUIDMapper( + private val fromUUID: MapFunction, + private val toUUID: MapFunction, + private val dataStore: ConfigurationSection, + private val dataStorePath: String, + private var underlying: MutableList = ArrayList() +) { + init { + reload() + } + + private fun T.toSavedString() = toUUID().UUID_TO_STRING() + private fun String.toMappedType() = STRING_TO_UUID().fromUUID() + + fun reload() { + underlying = dataStore.getStringList(dataStorePath) + } + + fun save() { + dataStore.set(dataStorePath, underlying) + } + + fun getValue(index: UInt) = underlying[index.toInt()].toMappedType() + fun getIndex(value: T): UInt { + val saved = value.toSavedString() + val index = underlying.indexOf(saved) + + return if (index < 0) { + underlying.add(saved) + (underlying.size - 1).toUInt() + } else { + index.toUInt() + } + } +} + +class PlayerMapper(dataStore: ConfigurationSection, dataStorePath: String): UUIDMapper( + { Bukkit.getServer().getPlayer(this) }, + OfflinePlayer::getUniqueId, + dataStore, + dataStorePath +) + +class WorldMapper(dataStore: ConfigurationSection, dataStorePath: String): UUIDMapper( + { Bukkit.getServer().getWorld(this) }, + World::getUID, + dataStore, + dataStorePath +) \ No newline at end of file diff --git a/src/main/kotlin/YamlFile.kt b/src/main/kotlin/YamlFile.kt new file mode 100644 index 0000000..dcb5c2c --- /dev/null +++ b/src/main/kotlin/YamlFile.kt @@ -0,0 +1,19 @@ +import org.bukkit.configuration.Configuration +import org.bukkit.configuration.ConfigurationSection +import org.bukkit.configuration.file.YamlConfiguration +import java.io.File + +class YamlFile private constructor( + private val file: File, + private val yamlConfiguration: YamlConfiguration = YamlConfiguration.loadConfiguration(file) +): ConfigurationSection, Configuration by yamlConfiguration { + companion object { + fun loadFile(file: File) = YamlFile(file) + } + + fun load() { + if (file.isFile) yamlConfiguration.load(file) + } + + fun save() = yamlConfiguration.save(file) +} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..0a66eae --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1 @@ +chunkLoadDestination: true