diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..a221a9d --- /dev/null +++ b/.drone.yml @@ -0,0 +1,42 @@ +--- +kind: pipeline +type: docker +name: default +steps: + - name: "Build" + image: "eclipse-temurin:17-jdk" + commands: + - "./gradlew assemble" + - name: "Test" + image: "eclipse-temurin:17-jdk" + commands: + - "./gradlew test" + + - name: "Create Release" + image: plugins/gitea-release + settings: + api_key: + from_secret: "gitea_api_key" + base_url: + from_secret: "gitea_base_url" + files: build/libs/DLib-*.jar + when: + event: + - tag + - name: "Publish to Maven" + image: "eclipse-temurin:17-jdk" + commands: + - "./gradlew publishReleasePublicationToMavenRepository" + environment: + MAVEN_REPO_URL: + from_secret: maven_repo_url + MAVEN_REPO_TOKEN: + from_secret: gitea_api_key + when: + event: + - tag +trigger: + event: + - push + - pull_request + - tag \ No newline at end of file diff --git a/.gitignore b/.gitignore index b065a68..0474bbf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +build/ +.idea/ +.gradle/ + # ---> Kotlin # Compiled class file *.class @@ -23,3 +27,4 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* +!gradle/wrapper/gradle-wrapper.jar diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..89d42a0 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,90 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") version "1.7.0" + kotlin("plugin.serialization") version "1.7.0" + id("maven-publish") + id("com.github.johnrengelman.shadow") version "7.1.2" + id("io.papermc.paperweight.userdev") version "1.3.5" +} + +group = "nl.kallestruik" +version = "1.5.0" + +repositories { + mavenCentral() +} + +dependencies { + paperDevBundle("1.19-R0.1-SNAPSHOT") + + implementation(kotlin("reflect")) + implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.1") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0") + implementation("org.jetbrains.kotlin:kotlin-scripting-common") + implementation("org.jetbrains.kotlin:kotlin-scripting-jvm") + implementation("org.jetbrains.kotlin:kotlin-scripting-jvm-host") + + testImplementation(kotlin("test-junit5")) + testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0") +} + +tasks { + build { + dependsOn(shadowJar) + } + assemble { + dependsOn(reobfJar) + } + + test { + useJUnitPlatform() + } + + withType { + kotlinOptions.jvmTarget = "17" + } + + processResources { + expand("version" to project.version) + } +} + +val sourcesJar by tasks.creating(Jar::class) { + archiveClassifier.set("sources") + from(sourceSets["main"].allSource) +} + +publishing { + publications { + create("release") { + from(components["kotlin"]) + artifact(sourcesJar) + } + } + + repositories { + maven { + url = uri(System.getenv("MAVEN_REPO_URL") ?: "") + + credentials(HttpHeaderCredentials::class) { + name = "Authorization" + value = "token ${System.getenv("MAVEN_REPO_TOKEN")}" + } + + authentication { + create("Authentication Header") + } + } + } +} + +kotlin { + sourceSets { + all { + languageSettings.optIn("kotlin.RequiresOptIn") + languageSettings.optIn("kotlinx.serialization.ExperimentalSerializationApi") + } + } +} \ 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..b1159fc --- /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.4-all.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..d90bfa6 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,9 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven("https://papermc.io/repo/repository/maven-public/") + } +} + +rootProject.name = "DLib" + diff --git a/src/main/kotlin/nl/kallestruik/dlib/DLib.kt b/src/main/kotlin/nl/kallestruik/dlib/DLib.kt new file mode 100644 index 0000000..bd6885a --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/DLib.kt @@ -0,0 +1,8 @@ +package nl.kallestruik.dlib + +import org.bukkit.plugin.java.JavaPlugin + +/** + * This class is only here to make sure that paper loads the plugin. + */ +class DLib: JavaPlugin() \ No newline at end of file diff --git a/src/main/kotlin/nl/kallestruik/dlib/DUtil.kt b/src/main/kotlin/nl/kallestruik/dlib/DUtil.kt new file mode 100644 index 0000000..7206ba0 --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/DUtil.kt @@ -0,0 +1,45 @@ +package nl.kallestruik.dlib + +import org.bukkit.util.Vector +import java.util.* + +class DUtil( + private val random: Random +) { + + /** + * Create a random vector with integer offsets within the min and max. + * @param min The minimum offset. + * @param max The maximum offset. + * @param height Whether the height should also have a random offset. + * @return A [Vector] containing the offsets. + */ + fun getRandomLocationOffset(min: Int, max: Int, height: Boolean): Vector { + // Create the vectors with all positive numbers. + val vec = if (height) { + Vector( + min + random.nextInt(max - min), + min + random.nextInt(max - min), + min + random.nextInt(max - min) + ) + } else { + Vector( + min + random.nextInt(max - min), + 0, + min + random.nextInt(max - min) + ) + } + + // Randomly make some components negative instead of positive. + vec.multiply( + Vector( + if (random.nextBoolean()) -1 else 1, + if (random.nextBoolean()) -1 else 1, + if (random.nextBoolean()) -1 else 1 + ) + ) + + // Return the completed vector. + return vec + } +} \ No newline at end of file diff --git a/src/main/kotlin/nl/kallestruik/dlib/MathHelper.kt b/src/main/kotlin/nl/kallestruik/dlib/MathHelper.kt new file mode 100644 index 0000000..fc52cb0 --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/MathHelper.kt @@ -0,0 +1,26 @@ +package nl.kallestruik.dlib + +class MathHelper { + + /** + * Clamp an integer between a min and a max value. + * @param value The value to clamp. + * @param min The minimal value. + * @param max The maximum value. + */ + fun clamp(value: Int, min: Int, max: Int): Int { + return if (value < min) min else value.coerceAtMost(max) + } + + /** + * Force a value to between 0 15 (inclusive) wrapping around to 15 at negative values. + * @param value The value. + * @return A positive int between 0 and 15 (inclusive). + */ + fun chunkAbs(value: Int): Int { + return if (value >= 0) + value % 16 + else + 16 + (value % 16) + } +} \ No newline at end of file diff --git a/src/main/kotlin/nl/kallestruik/dlib/ReflectionUtil.kt b/src/main/kotlin/nl/kallestruik/dlib/ReflectionUtil.kt new file mode 100644 index 0000000..3d27d3c --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/ReflectionUtil.kt @@ -0,0 +1,37 @@ +package nl.kallestruik.dlib + +import java.lang.reflect.Field + +class ReflectionUtil { + + /** + * Get a {@link Field} from the class provided. + * @param clazz The class to get the field from. + * @param fieldName The name of the field to get. + */ + @Throws(NoSuchFieldException::class) + fun getField(clazz: Class<*>, fieldName: String): Field { + return try { + clazz.getDeclaredField(fieldName) + } catch (e: NoSuchFieldException) { + val superClass = clazz.superclass + superClass?.let { getField(it, fieldName) } ?: throw e + } + } + + /** + * Get the value from a field on an object. + * @param instance The instance of the object to take the value from. + * @param fieldName The name of the field to take the value from. + */ + fun getValueFromField(instance: Any, fieldName: String): Any? { + try { + val field = getField(instance.javaClass, fieldName) + field.isAccessible = true //required if field is not normally accessible + return field.get(instance) + } catch (e: NoSuchFieldException) { + e.printStackTrace() + } + return null + } +} \ No newline at end of file diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/ConfigSchemeBuilder.kt b/src/main/kotlin/nl/kallestruik/dlib/config/ConfigSchemeBuilder.kt new file mode 100644 index 0000000..2a722cf --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/ConfigSchemeBuilder.kt @@ -0,0 +1,223 @@ +package nl.kallestruik.dlib.config + +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.json.JsonPrimitive +import nl.kallestruik.dlib.config.annotations.* +import nl.kallestruik.dlib.config.data.ConfigSchema +import nl.kallestruik.dlib.config.data.ConfigValueConstraints +import nl.kallestruik.dlib.config.data.ConfigValueType +import nl.kallestruik.dlib.exceptions.ConfigSchemaGenerationException +import kotlin.math.min +import kotlin.math.max + +fun SerialDescriptor.toConfigSchema(annotations: List = listOf()): ConfigSchema { + val allAnnotations = annotations + this.annotations + return when (this.kind) { + SerialKind.ENUM -> createEnumConfigSchema(this, allAnnotations) + PrimitiveKind.BOOLEAN -> createBooleanConfigSchema(allAnnotations) + PrimitiveKind.BYTE -> createIntegerConfigSchema(allAnnotations, Byte.MAX_VALUE.toLong(), Byte.MIN_VALUE.toLong()) + PrimitiveKind.CHAR -> createStringConfigSchema(allAnnotations, maxLength=1) + PrimitiveKind.SHORT -> createIntegerConfigSchema(allAnnotations, Short.MAX_VALUE.toLong(), Short.MIN_VALUE.toLong()) + PrimitiveKind.INT -> createIntegerConfigSchema(allAnnotations, Int.MAX_VALUE.toLong(), Int.MIN_VALUE.toLong()) + PrimitiveKind.LONG -> createIntegerConfigSchema(allAnnotations, Long.MAX_VALUE, Long.MIN_VALUE) + PrimitiveKind.FLOAT -> createFloatConfigSchema(allAnnotations, Float.MAX_VALUE.toDouble(), -Float.MAX_VALUE.toDouble()) + PrimitiveKind.DOUBLE -> createFloatConfigSchema(allAnnotations, Double.MAX_VALUE, -Double.MAX_VALUE) + PrimitiveKind.STRING -> createStringConfigSchema(allAnnotations) + StructureKind.LIST -> createListConfigSchema(this, allAnnotations) + StructureKind.MAP -> createMapConfigSchema(this, allAnnotations) + StructureKind.OBJECT, StructureKind.CLASS -> createObjectConfigSchema(this, allAnnotations) + else -> throw ConfigSchemaGenerationException("Creating a schema for type ${this.kind} is not currently supported.") + } +} + +private fun createObjectConfigSchema(descriptor: SerialDescriptor, annotations: List): ConfigSchema { + val description = annotations + .filterIsInstance() + .flatMap { it.lines.toList() } + + val children = descriptor.elementDescriptors + .mapIndexed { i, child -> + descriptor.getElementName(i) to child.toConfigSchema(descriptor.getElementAnnotations(i)) + }.toMap() + + return ConfigSchema( + type = ConfigValueType.OBJECT, + children = children, + description = description, + ) +} + +private fun createListConfigSchema(descriptor: SerialDescriptor, annotations: List): ConfigSchema { + val description = annotations + .filterIsInstance() + .flatMap { it.lines.toList() } + + return ConfigSchema( + type = ConfigValueType.LIST, + description = description, + items = descriptor.elementDescriptors.first().toConfigSchema(), + ) +} + +private fun createMapConfigSchema(descriptor: SerialDescriptor, annotations: List): ConfigSchema { + val description = annotations + .filterIsInstance() + .flatMap { it.lines.toList() } + + val allowedKeys = annotations + .filterIsInstance() + .flatMap { annotation -> annotation.keys + .map { listOf(it) } + } + + val allowedKeysExclusive = annotations + .filterIsInstance() + .map { it.exclusiveKeys.toList() } + + if (descriptor.elementDescriptors.first().kind != PrimitiveKind.STRING) { + throw ConfigSchemaGenerationException("Map keys have to be strings.") + } + + return ConfigSchema( + type = ConfigValueType.MAP, + description = description, + items = descriptor.elementDescriptors.last().toConfigSchema(), + constraints = ConfigValueConstraints( + allowedKeys = allowedKeys + allowedKeysExclusive, + ), + ) +} + +private fun createStringConfigSchema(annotations: List, maxLength: Int? = null): ConfigSchema { + val description = annotations + .filterIsInstance() + .flatMap { it.lines.toList() } + + val allowedValues = annotations + .filterIsInstance() + .flatMap { it.values.toList() } + + val default = annotations + .filterIsInstance() + .firstOrNull() + ?.let { JsonPrimitive(it.default) } + + return ConfigSchema( + type = ConfigValueType.STRING, + description = description, + default = default, + constraints = ConfigValueConstraints( + enum = allowedValues.ifEmpty { null }, + maxLength = maxLength, + ), + ) +} + +private fun createEnumConfigSchema(descriptor: SerialDescriptor, annotations: List): ConfigSchema { + val description = annotations + .filterIsInstance() + .flatMap { it.lines.toList() } + + val allowedValues = descriptor.elementNames.toList() + + val default = annotations + .filterIsInstance() + .firstOrNull() + ?.let { JsonPrimitive(it.default) } + + return ConfigSchema( + type = ConfigValueType.STRING, + description = description, + default = default, + constraints = ConfigValueConstraints( + enum = allowedValues.ifEmpty { null }, + ), + ) +} + +private fun createBooleanConfigSchema(annotations: List): ConfigSchema { + val description = annotations + .filterIsInstance() + .flatMap { it.lines.toList() } + + val default = annotations + .filterIsInstance() + .firstOrNull() + ?.let { JsonPrimitive(it.default) } + + return ConfigSchema( + type = ConfigValueType.BOOLEAN, + description = description, + default = default, + ) +} + +private fun createIntegerConfigSchema(annotations: List, maxValue: Long, minValue: Long): ConfigSchema { + val description = annotations + .filterIsInstance() + .flatMap { it.lines.toList() } + + val max = annotations + .filterIsInstance() + .map { it.max } + .reduceOrNull { acc, next -> + return@reduceOrNull max(acc, next) + } ?: maxValue + + val min = annotations + .filterIsInstance() + .map { it.min } + .reduceOrNull { acc, next -> + min(acc, next) + } ?: minValue + + val default = annotations + .filterIsInstance() + .firstOrNull() + ?.let { JsonPrimitive(it.default) } + + return ConfigSchema( + type = ConfigValueType.INTEGER, + description = description, + default = default, + constraints = ConfigValueConstraints( + maxValueInt = max, + minValueInt = min, + ), + ) +} + +private fun createFloatConfigSchema(annotations: List, maxValue: Double, minValue: Double): ConfigSchema { + val description = annotations + .filterIsInstance() + .flatMap { it.lines.toList() } + + val max = annotations + .filterIsInstance() + .map { it.max } + .reduceOrNull { acc, next -> + return@reduceOrNull max(acc, next) + } ?: maxValue + + val min = annotations + .filterIsInstance() + .map { it.min } + .reduceOrNull { acc, next -> + min(acc, next) + } ?: minValue + + val default = annotations + .filterIsInstance() + .firstOrNull() + ?.let { JsonPrimitive(it.default) } + + return ConfigSchema( + type = ConfigValueType.FLOAT, + description = description, + default = default, + constraints = ConfigValueConstraints( + maxValueFloat = max, + minValueFloat = min, + ), + ) +} \ No newline at end of file diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/annotations/AllowedKeys.kt b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/AllowedKeys.kt new file mode 100644 index 0000000..3ee66d8 --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/AllowedKeys.kt @@ -0,0 +1,11 @@ +package nl.kallestruik.dlib.config.annotations + +import kotlinx.serialization.SerialInfo + +@SerialInfo +@Repeatable +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.BINARY) +annotation class AllowedKeys( + vararg val keys: String, +) diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/annotations/AllowedKeysExclusive.kt b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/AllowedKeysExclusive.kt new file mode 100644 index 0000000..3d09188 --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/AllowedKeysExclusive.kt @@ -0,0 +1,11 @@ +package nl.kallestruik.dlib.config.annotations + +import kotlinx.serialization.SerialInfo + +@SerialInfo +@Repeatable +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.BINARY) +annotation class AllowedKeysExclusive( + vararg val exclusiveKeys: String, +) diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/annotations/DefaultBoolean.kt b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/DefaultBoolean.kt new file mode 100644 index 0000000..87c4d84 --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/DefaultBoolean.kt @@ -0,0 +1,10 @@ +package nl.kallestruik.dlib.config.annotations + +import kotlinx.serialization.SerialInfo + +@SerialInfo +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.BINARY) +annotation class DefaultBoolean( + val default: Boolean, +) diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/annotations/DefaultFloat.kt b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/DefaultFloat.kt new file mode 100644 index 0000000..7132647 --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/DefaultFloat.kt @@ -0,0 +1,10 @@ +package nl.kallestruik.dlib.config.annotations + +import kotlinx.serialization.SerialInfo + +@SerialInfo +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.BINARY) +annotation class DefaultFloat( + val default: Double, +) diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/annotations/DefaultInt.kt b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/DefaultInt.kt new file mode 100644 index 0000000..7bf75f7 --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/DefaultInt.kt @@ -0,0 +1,10 @@ +package nl.kallestruik.dlib.config.annotations + +import kotlinx.serialization.SerialInfo + +@SerialInfo +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.BINARY) +annotation class DefaultInt( + val default: Long, +) diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/annotations/DefaultString.kt b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/DefaultString.kt new file mode 100644 index 0000000..2fedf6f --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/DefaultString.kt @@ -0,0 +1,10 @@ +package nl.kallestruik.dlib.config.annotations + +import kotlinx.serialization.SerialInfo + +@SerialInfo +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.BINARY) +annotation class DefaultString( + val default: String, +) diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/annotations/Description.kt b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/Description.kt new file mode 100644 index 0000000..e27d70f --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/Description.kt @@ -0,0 +1,11 @@ +package nl.kallestruik.dlib.config.annotations + +import kotlinx.serialization.SerialInfo + +@SerialInfo +@Repeatable +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.BINARY) +annotation class Description( + vararg val lines: String, +) diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/annotations/DisplayType.kt b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/DisplayType.kt new file mode 100644 index 0000000..af56665 --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/DisplayType.kt @@ -0,0 +1,11 @@ +package nl.kallestruik.dlib.config.annotations + +import kotlinx.serialization.SerialInfo +import nl.kallestruik.dlib.config.data.ConfigDisplayType + +@SerialInfo +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) +@Retention(AnnotationRetention.BINARY) +annotation class DisplayType( + val type: ConfigDisplayType +) diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/annotations/MaterialField.kt b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/MaterialField.kt new file mode 100644 index 0000000..3102d58 --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/MaterialField.kt @@ -0,0 +1,10 @@ +package nl.kallestruik.dlib.config.annotations + +import kotlinx.serialization.SerialInfo + +@SerialInfo +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY, AnnotationTarget.CLASS) +@Retention(AnnotationRetention.BINARY) +annotation class MaterialField( + val field: String, +) diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/annotations/MaxFloat.kt b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/MaxFloat.kt new file mode 100644 index 0000000..f7770e7 --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/MaxFloat.kt @@ -0,0 +1,10 @@ +package nl.kallestruik.dlib.config.annotations + +import kotlinx.serialization.SerialInfo + +@SerialInfo +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.BINARY) +annotation class MaxFloat( + val max: Double, +) diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/annotations/MaxInt.kt b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/MaxInt.kt new file mode 100644 index 0000000..a5f6c7f --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/MaxInt.kt @@ -0,0 +1,10 @@ +package nl.kallestruik.dlib.config.annotations + +import kotlinx.serialization.SerialInfo + +@SerialInfo +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.BINARY) +annotation class MaxInt( + val max: Long, +) diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/annotations/MinFloat.kt b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/MinFloat.kt new file mode 100644 index 0000000..884af8d --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/MinFloat.kt @@ -0,0 +1,10 @@ +package nl.kallestruik.dlib.config.annotations + +import kotlinx.serialization.SerialInfo + +@SerialInfo +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.BINARY) +annotation class MinFloat( + val min: Double, +) diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/annotations/MinInt.kt b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/MinInt.kt new file mode 100644 index 0000000..6af2b59 --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/MinInt.kt @@ -0,0 +1,10 @@ +package nl.kallestruik.dlib.config.annotations + +import kotlinx.serialization.SerialInfo + +@SerialInfo +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.BINARY) +annotation class MinInt( + val min: Long, +) diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/annotations/StaticMaterial.kt b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/StaticMaterial.kt new file mode 100644 index 0000000..866af31 --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/StaticMaterial.kt @@ -0,0 +1,11 @@ +package nl.kallestruik.dlib.config.annotations + +import kotlinx.serialization.SerialInfo +import org.bukkit.Material + +@SerialInfo +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.BINARY) +annotation class StaticMaterial( + val material: Material, +) diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/annotations/StringEnum.kt b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/StringEnum.kt new file mode 100644 index 0000000..87f0bf0 --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/annotations/StringEnum.kt @@ -0,0 +1,11 @@ +package nl.kallestruik.dlib.config.annotations + +import kotlinx.serialization.SerialInfo + +@SerialInfo +@Repeatable +@Target(AnnotationTarget.FIELD, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.BINARY) +annotation class StringEnum( + vararg val values: String, +) diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/data/ConfigDisplaySettings.kt b/src/main/kotlin/nl/kallestruik/dlib/config/data/ConfigDisplaySettings.kt new file mode 100644 index 0000000..6fda58c --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/data/ConfigDisplaySettings.kt @@ -0,0 +1,20 @@ +package nl.kallestruik.dlib.config.data + +import kotlinx.serialization.Serializable + +@Serializable +data class ConfigDisplaySettings( + val type: ConfigDisplayType = ConfigDisplayType.DEFAULT, + val materialField: String? = null, + val staticMaterial: String? = null, + val amountField: String? = null, + val staticAmount: Int? = null, + val inventorySize: Int? = null, +) + +enum class ConfigDisplayType { + DEFAULT, + CHECKLIST, + ITEM, + INVENTORY, +} diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/data/ConfigSchema.kt b/src/main/kotlin/nl/kallestruik/dlib/config/data/ConfigSchema.kt new file mode 100644 index 0000000..5a21a90 --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/data/ConfigSchema.kt @@ -0,0 +1,15 @@ +package nl.kallestruik.dlib.config.data + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonPrimitive + +@Serializable +data class ConfigSchema( + val type: ConfigValueType, + val description: List? = null, + val children: Map? = null, + val items: ConfigSchema? = null, + val default: JsonPrimitive? = null, + val constraints: ConfigValueConstraints = ConfigValueConstraints(), + val display: ConfigDisplaySettings? = null, +) diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/data/ConfigValueConstraints.kt b/src/main/kotlin/nl/kallestruik/dlib/config/data/ConfigValueConstraints.kt new file mode 100644 index 0000000..24dbe66 --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/data/ConfigValueConstraints.kt @@ -0,0 +1,14 @@ +package nl.kallestruik.dlib.config.data + +import kotlinx.serialization.Serializable + +@Serializable +data class ConfigValueConstraints( + val enum: List? = null, + val allowedKeys: List>? = null, + val maxLength: Int? = null, + val maxValueInt: Long? = null, + val minValueInt: Long? = null, + val maxValueFloat: Double? = null, + val minValueFloat: Double? = null, +) \ No newline at end of file diff --git a/src/main/kotlin/nl/kallestruik/dlib/config/data/ConfigValueType.kt b/src/main/kotlin/nl/kallestruik/dlib/config/data/ConfigValueType.kt new file mode 100644 index 0000000..fc778b4 --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/config/data/ConfigValueType.kt @@ -0,0 +1,11 @@ +package nl.kallestruik.dlib.config.data + +enum class ConfigValueType { + STRING, + MAP, + LIST, + OBJECT, + BOOLEAN, + INTEGER, + FLOAT +} \ No newline at end of file diff --git a/src/main/kotlin/nl/kallestruik/dlib/exceptions/ConfigSchemaGenerationException.kt b/src/main/kotlin/nl/kallestruik/dlib/exceptions/ConfigSchemaGenerationException.kt new file mode 100644 index 0000000..d514102 --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/exceptions/ConfigSchemaGenerationException.kt @@ -0,0 +1,3 @@ +package nl.kallestruik.dlib.exceptions + +class ConfigSchemaGenerationException(message: String = ""): RuntimeException(message) \ No newline at end of file diff --git a/src/main/kotlin/nl/kallestruik/dlib/gui/ChestGUI.kt b/src/main/kotlin/nl/kallestruik/dlib/gui/ChestGUI.kt new file mode 100644 index 0000000..2ed67ab --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/gui/ChestGUI.kt @@ -0,0 +1,222 @@ +package nl.kallestruik.dlib.gui + +import kotlinx.coroutines.runBlocking +import net.kyori.adventure.text.Component +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.HandlerList +import org.bukkit.event.Listener +import org.bukkit.event.inventory.ClickType +import org.bukkit.event.inventory.InventoryAction +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.InventoryCloseEvent +import org.bukkit.inventory.InventoryView +import org.bukkit.inventory.ItemStack +import org.bukkit.plugin.Plugin + +fun chestGUI(player: Player, plugin: Plugin, configure: suspend ChestGUI.() -> Unit) { + val chestGUI = ChestGUI() + runBlocking { + chestGUI.configure() + } + + chestGUI.display(player, plugin) +} + +class ChestGUI: Listener { + private data class Configuration( + var border: (suspend ChestGUIEntry.() -> Unit)? = null, + var content: MutableList Unit> = mutableListOf(), + var itemStacks: MutableMap = mutableMapOf(), + var onClose: suspend (gui: ChestGUI, player: Player, event: InventoryCloseEvent) -> Unit = { _, _, _ ->} + ) + + private val configuration = Configuration() + var title: Component = Component.empty() + var rows: Int = 0 + var cancelInteraction: Boolean = true + + private lateinit var view: InventoryView + private lateinit var entries: Array + + + fun onClose(onClose: suspend (gui: ChestGUI, player: Player, event: InventoryCloseEvent) -> Unit) { + configuration.onClose = onClose + } + + fun entry(entry: suspend ChestGUIEntry.() -> Unit) { + configuration.content.add(entry) + } + + fun border(border: suspend ChestGUIEntry.() -> Unit) { + configuration.border = border + } + + fun itemStack(slot: Int, itemStack: ItemStack) { + configuration.itemStacks[slot] = itemStack + } + + fun display(player: Player, plugin: Plugin) { + val inventory = Bukkit.createInventory(null, rows * 9, title) + view = player.openInventory(inventory) ?: return + + entries = arrayOfNulls(rows * 9) + + if (configuration.border != null) { + val border = ChestGUIEntry() + runBlocking { + configuration.border!!.invoke(border) + } + + val borderStack = border.toItemStack() + + // Top/Bottom border + for (x in 0..8) { + view.setItem(x, borderStack) + view.setItem((rows - 1) * 9 + x, borderStack) + } + + // Middle side border + for (y in 1..rows - 2) { + view.setItem(9 * y, borderStack) + view.setItem(9 * y + 8, borderStack) + } + } + + configuration.content.forEach { + val entry = ChestGUIEntry() + runBlocking { + it.invoke(entry) + } + + if (entry.getSlot() < 0) + return@forEach + + entries[entry.getSlot()] = entry + } + + entries.forEach { entry -> + if (entry == null) + return@forEach + + if (entry.x != -1 || entry.y != -1) + return@forEach + + inventory.setItem(entry.getSlot(), entry.toItemStack()) + } + + configuration.itemStacks.forEach { (slot, itemStack) -> + inventory.setItem(slot, itemStack) + } + + plugin.server.pluginManager.registerEvents(this, plugin) + } + + private fun cleanup() { + HandlerList.unregisterAll(this) + } + + /* + Bukkit event handlers + */ + @EventHandler + fun onInventoryClose(event: InventoryCloseEvent) { + if (event.view != this.view) + return + + cleanup() + + if (event.reason == InventoryCloseEvent.Reason.PLUGIN) { + return + } + + if (event.player !is Player) + return + + runBlocking { + configuration.onClose(this@ChestGUI, event.player as Player, event) + } + } + + @EventHandler + fun onClick(event: InventoryClickEvent) { + if (event.view != view) { + return + } + + if (event.action != InventoryAction.PICKUP_ALL) { + if (cancelInteraction) + event.isCancelled = true + return + } + + val slot = view.convertSlot(event.rawSlot) + + if (cancelInteraction) + event.isCancelled = true + + val entry = entries[slot] ?: return + + if (event.whoClicked !is Player) + return + + runBlocking { + when (event.click) { + ClickType.LEFT -> entry.onLeftClick(event) + ClickType.MIDDLE -> entry.onMiddleClick(event) + ClickType.RIGHT -> entry.onRightClick(event) + else -> entry.onOtherClick(event) + } + } + } +} + +data class ChestGUIEntry( + var name: Component = Component.empty(), + var material: Material = Material.AIR, + var amount: Int = 1, + var lore: MutableList = mutableListOf(), + var x: Int = -1, + var y: Int = -1, + var onLeftClick: suspend (event: InventoryClickEvent) -> Unit = {}, + var onMiddleClick: suspend (event: InventoryClickEvent) -> Unit = {}, + var onRightClick: suspend (event: InventoryClickEvent) -> Unit = {}, + var onOtherClick: suspend (event: InventoryClickEvent) -> Unit = {}, +) { + + fun onLeftClick(onLeftClick: suspend (event: InventoryClickEvent) -> Unit) { + this.onLeftClick = onLeftClick + } + + fun onMiddleClick(onMiddleClick: suspend (event: InventoryClickEvent) -> Unit) { + this.onMiddleClick = onMiddleClick + } + + fun onRightClick(onRightClick: suspend (event: InventoryClickEvent) -> Unit) { + this.onRightClick = onRightClick + } + + fun onOtherClick(onOtherClick: suspend (event: InventoryClickEvent) -> Unit) { + this.onOtherClick = onOtherClick + } + + fun lore(line: Component) { + lore.add(line) + } + + fun getSlot(): Int { + return x + y * 9 + } + + fun toItemStack(): ItemStack { + val itemStack = ItemStack(material, amount) + val itemMeta = itemStack.itemMeta + itemMeta.displayName(name) + itemMeta.lore(lore) + + itemStack.itemMeta = itemMeta + return itemStack + } +} \ No newline at end of file diff --git a/src/main/kotlin/nl/kallestruik/dlib/gui/ConfirmationDialog.kt b/src/main/kotlin/nl/kallestruik/dlib/gui/ConfirmationDialog.kt new file mode 100644 index 0000000..0905372 --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/gui/ConfirmationDialog.kt @@ -0,0 +1,159 @@ +package nl.kallestruik.dlib.gui + +import kotlinx.coroutines.runBlocking +import net.kyori.adventure.text.Component +import net.kyori.adventure.text.format.NamedTextColor +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.HandlerList +import org.bukkit.event.Listener +import org.bukkit.event.inventory.InventoryAction +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.InventoryCloseEvent +import org.bukkit.inventory.InventoryView +import org.bukkit.inventory.ItemStack +import org.bukkit.plugin.Plugin + +fun confirmationDialog(player: Player, plugin: Plugin, configure: suspend ConfirmationDialog.() -> Unit) { + val confirmationDialog = ConfirmationDialog() + runBlocking { + confirmationDialog.configure() + } + + confirmationDialog.display(player, plugin) +} + +class ConfirmationDialog: Listener { + private data class Configuration( + var onConfirm: suspend () -> Unit = {}, + var onCancel: suspend () -> Unit = {} + ) + + var title: Component = Component.empty() + var description: Component = Component.empty() + + private val configuration = Configuration() + private lateinit var plugin: Plugin + private lateinit var view: InventoryView + + fun onConfirm(onConfirm: suspend () -> Unit) { + configuration.onConfirm = onConfirm + } + + fun onCancel(onCancel: suspend () -> Unit) { + configuration.onCancel = onCancel + } + + fun display(player: Player, plugin: Plugin) { + player.closeInventory() + this.plugin = plugin + + val inventory = Bukkit.createInventory(null, 3 * 9, title) + view = player.openInventory(inventory) ?: return + + val borderStack = ItemStack(Material.ORANGE_STAINED_GLASS_PANE, 1) + val borderMeta = borderStack.itemMeta + borderMeta.displayName(Component.empty()) + borderStack.itemMeta = borderMeta + + // Top border + for (x in 8 downTo 0) { + view.setItem(x, borderStack) + } + + // Middle sides + view.setItem(9, borderStack) + view.setItem(17, borderStack) + + // Bottom border + for (x in 26 downTo 18) { + view.setItem(x, borderStack) + } + + // Description item + val descriptionItem = ItemStack(Material.PAPER, 1) + val descriptionMeta = descriptionItem.itemMeta + descriptionMeta.displayName(description) + descriptionItem.itemMeta = descriptionMeta + + // Confirm item + val confirmItem = ItemStack(Material.SUNFLOWER, 1) + val confirmMeta = confirmItem.itemMeta + confirmMeta.displayName(Component.text("Confirm").color(NamedTextColor.GREEN)) + confirmItem.itemMeta = confirmMeta + + // Cancel item + val cancelItem = ItemStack(Material.BARRIER, 1) + val cancelMeta = cancelItem.itemMeta + cancelMeta.displayName(Component.text("Cancel").color(NamedTextColor.RED)) + cancelItem.itemMeta = cancelMeta + + // 0 1 2 3 4 5 6 7 8 + // 9 10 11 12 13 14 15 16 17 + // 18 19 20 21 22 23 24 25 26 + + view.setItem(4, descriptionItem) + view.setItem(11, confirmItem) + view.setItem(15, cancelItem) + + plugin.server.pluginManager.registerEvents(this, plugin) + } + + private fun cleanup() { + view.close() + HandlerList.unregisterAll(this) + } + + /* + Bukkit event handlers + */ + @EventHandler + fun onInventoryClose(event: InventoryCloseEvent) { + if (event.view != this.view) + return + + if (event.reason == InventoryCloseEvent.Reason.PLUGIN) { + return + } + + runBlocking { + configuration.onCancel.invoke() + cleanup() + } + } + + @EventHandler + fun onOperationFinish(event: InventoryClickEvent) { + if (event.view != view) { + return + } + + if (event.action != InventoryAction.PICKUP_ALL) { + event.isCancelled = true + return + } + + val slot = view.convertSlot(event.rawSlot) + + if (slot != 11 && slot != 15) { + event.isCancelled = true + return + } + + event.isCancelled = true + + if (slot == 11) { + runBlocking { + configuration.onConfirm() + cleanup() + } + } else { + runBlocking { + configuration.onCancel() + cleanup() + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/nl/kallestruik/dlib/gui/TextInputDialog.kt b/src/main/kotlin/nl/kallestruik/dlib/gui/TextInputDialog.kt new file mode 100644 index 0000000..64bff2a --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/gui/TextInputDialog.kt @@ -0,0 +1,138 @@ +package nl.kallestruik.dlib.gui + +import kotlinx.coroutines.runBlocking +import net.kyori.adventure.text.Component +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.event.EventHandler +import org.bukkit.event.HandlerList +import org.bukkit.event.Listener +import org.bukkit.event.inventory.* +import org.bukkit.inventory.InventoryView +import org.bukkit.inventory.ItemStack +import org.bukkit.plugin.Plugin + +fun textInputDialog(player: Player, plugin: Plugin, configure: suspend TextInputDialog.() -> Unit) { + val textInputDialog = TextInputDialog() + runBlocking { + textInputDialog.configure() + } + + textInputDialog.display(player, plugin) +} + +class TextInputDialog: Listener { + private data class Configuration( + var item: suspend TextInputDialogItem.() -> Unit = {}, + var onFinish: suspend (value: String) -> Unit = {}, + var onCancel: suspend () -> Unit = {} + ) + + private val configuration = Configuration() + var prompt: Component = Component.empty() + + private lateinit var view: InventoryView + private lateinit var plugin: Plugin + + + fun item(entry: suspend TextInputDialogItem.() -> Unit) { + configuration.item = entry + } + + fun onFinish(onFinish: suspend (value: String) -> Unit) { + configuration.onFinish = onFinish + } + + fun onCancel(onCancel: suspend () -> Unit) { + configuration.onCancel = onCancel + } + + fun display(player: Player, plugin: Plugin) { + player.closeInventory() + this.plugin = plugin + view = player.openAnvil(null, true) ?: return + + val entry = TextInputDialogItem() + runBlocking { + configuration.item.invoke(entry) + } + + val itemStack = ItemStack(entry.material, entry.amount) + val itemMeta = itemStack.itemMeta + itemMeta.displayName(prompt) + itemStack.itemMeta = itemMeta + + view.setItem(0, itemStack) + + plugin.server.pluginManager.registerEvents(this, plugin) + } + + private fun cleanup() { + view.close() + HandlerList.unregisterAll(this) + } + + /* + Bukkit event handlers + */ + @EventHandler + fun onInventoryClose(event: InventoryCloseEvent) { + if (event.view != this.view) + return + + if (event.reason == InventoryCloseEvent.Reason.PLUGIN) { + return + } + + runBlocking { + configuration.onCancel.invoke() + cleanup() + } + } + + @EventHandler + fun onAnvilPrepare(event: PrepareAnvilEvent) { + if (event.view != view) + return + + // Always keep the cost at 1. 0 Causes the client to not be able to click it. + event.inventory.repairCost = 1 + } + + @EventHandler + fun onOperationFinish(event: InventoryClickEvent) { + if (event.view != view) { + return + } + + if (event.action != InventoryAction.PICKUP_ALL) { + event.isCancelled = true + return + } + + if (view.convertSlot(event.rawSlot) != 2) { + event.isCancelled = true + return + } + + val item = event.currentItem + val name = item?.itemMeta?.displayName ?: "" + + if (name == "") { + event.isCancelled = true + return + } + + event.isCancelled = true + + runBlocking { + configuration.onFinish.invoke(name) + cleanup() + } + } +} + +data class TextInputDialogItem( + var material: Material = Material.AIR, + var amount: Int = 1, +) \ No newline at end of file diff --git a/src/main/kotlin/nl/kallestruik/dlib/serializers/UUIDSerializer.kt b/src/main/kotlin/nl/kallestruik/dlib/serializers/UUIDSerializer.kt new file mode 100644 index 0000000..548b2be --- /dev/null +++ b/src/main/kotlin/nl/kallestruik/dlib/serializers/UUIDSerializer.kt @@ -0,0 +1,21 @@ +package nl.kallestruik.dlib.serializers + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import java.util.* + +object UUIDSerializer: KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("uuid", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): UUID { + return UUID.fromString(decoder.decodeString()) + } + + override fun serialize(encoder: Encoder, value: UUID) { + encoder.encodeString(value.toString()) + } +} \ No newline at end of file diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..ff037b0 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,4 @@ +name: "DLib" +main: nl.kallestruik.dlib.DLib +version: ${version} +api-version: 1.16 \ No newline at end of file diff --git a/src/test/kotlin/nl/kallestruik/dlib/config/ConfigSchemaBuilderTest.kt b/src/test/kotlin/nl/kallestruik/dlib/config/ConfigSchemaBuilderTest.kt new file mode 100644 index 0000000..381e4f8 --- /dev/null +++ b/src/test/kotlin/nl/kallestruik/dlib/config/ConfigSchemaBuilderTest.kt @@ -0,0 +1,161 @@ +package nl.kallestruik.dlib.config + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonPrimitive +import nl.kallestruik.dlib.config.annotations.* +import kotlin.test.Test +import kotlin.test.assertEquals + + +class ConfigSchemaBuilderTest { + + @Test + fun `config scheme contains all descriptions`() { + val schema = Company.serializer().descriptor.toConfigSchema() + + assertEquals( + listOf("The name of the company."), + schema.children!!["name"]!!.description + ) + assertEquals( + listOf("The person that owns the company."), + schema.children!!["owner"]!!.description + ) + assertEquals( + listOf("All the employees of the company.", "This list should also contain the owner for legacy reasons ;)."), + schema.children!!["employees"]!!.description + ) + + val personSchema = schema.children!!["owner"]!! + + assertEquals( + listOf("The list of qualifications that this person has."), + personSchema.children!!["qualifications"]!!.description + ) + assertEquals( + listOf("The age of the person."), + personSchema.children!!["age"]!!.description + ) + assertEquals( + listOf("Whether the person is currently employed at a company."), + personSchema.children!!["isEmployed"]!!.description + ) + } + + @Test + fun `config schema has correct defaults`() { + val schema = Company.serializer().descriptor.toConfigSchema() + val personSchema = schema.children!!["owner"]!! + + assertEquals(JsonPrimitive("Bob"), personSchema.children!!["firstName"]!!.default) + assertEquals(JsonPrimitive(18.0), personSchema.children!!["age"]!!.default) + assertEquals(JsonPrimitive(true), personSchema.children!!["isEmployed"]!!.default) + + val qualificationSchema = personSchema.children!!["qualifications"]!!.items!! + + assertEquals(JsonPrimitive("EXPERIENCE"), qualificationSchema.children!!["type"]!!.default) + assertEquals(JsonPrimitive(0), qualificationSchema.children!!["yearObtained"]!!.default) + } + + @Test + fun `config schema allowed keys are correct`() { + val schema = Company.serializer().descriptor.toConfigSchema() + + assertEquals( + listOf(listOf("Bob"), listOf("Joe"), listOf("Kon"), listOf("Jane", "Kevin")), + schema.children!!["employees"]!!.constraints.allowedKeys + ) + } + + @Test + fun `config schema enum values are correct`() { + val schema = Company.serializer().descriptor.toConfigSchema() + val personSchema = schema.children!!["owner"]!! + + assertEquals( + listOf("Bob", "Joe", "Kon", "Jane", "Kevin"), + personSchema.children!!["firstName"]!!.constraints.enum + ) + + val qualificationSchema = personSchema.children!!["qualifications"]!!.items!! + + assertEquals( + listOf("DIPLOMA", "EXPERIENCE", "CERTIFICATE"), + qualificationSchema.children!!["type"]!!.constraints.enum + ) + } + + @Test + fun `config schema has correct min and max values`() { + val schema = Company.serializer().descriptor.toConfigSchema() + val personSchema = schema.children!!["owner"]!! + + assertEquals( + 0.0, + personSchema.children!!["age"]!!.constraints.minValueFloat + ) + assertEquals( + 75.0, + personSchema.children!!["age"]!!.constraints.maxValueFloat + ) + + val qualificationSchema = personSchema.children!!["qualifications"]!!.items!! + + assertEquals( + 0, + qualificationSchema.children!!["yearObtained"]!!.constraints.minValueInt + ) + assertEquals( + 9999, + qualificationSchema.children!!["yearObtained"]!!.constraints.maxValueInt + ) + } +} + +@Serializable +data class Company( + @Description("The name of the company.") + val name: String, + @Description("The person that owns the company.") + val owner: Person, + @Description("All the employees of the company.", "This list should also contain the owner for legacy reasons ;).") + @AllowedKeys("Bob", "Joe", "Kon") // Don't question it. I need to test stuff. + @AllowedKeysExclusive("Jane", "Kevin") + val employees: Map, +) + +@Serializable +data class Person( + @DefaultString("Bob") + @StringEnum("Bob", "Joe", "Kon", "Jane", "Kevin") + val firstName: String, + val lastName: String, + @Description("The list of qualifications that this person has.") + val qualifications: List, + @DefaultFloat(18.0) + @MinFloat(0.0) + @MaxFloat(75.0) + @Description("The age of the person.") + val age: Float, + @DefaultBoolean(true) + @Description("Whether the person is currently employed at a company.") + val isEmployed: Boolean, +) + +@Serializable +data class Qualification( + val name: String, + @DefaultString("EXPERIENCE") + val type: QualificationType, + @DefaultInt(0) + @MinInt(0) + @MaxInt(9999) + val yearObtained: Int, +) + +@Serializable +enum class QualificationType { + DIPLOMA, + EXPERIENCE, + CERTIFICATE, +} \ No newline at end of file