diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..087fa3a --- /dev/null +++ b/.gitignore @@ -0,0 +1,160 @@ +# Created by .ignore support plugin (hsz.mobi) +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/dictionaries +.idea/**/shelf + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml +.idea/ + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ +cmake-build-release/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests +### Java template +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +### Eclipse template + +.metadata +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath +.recommenders + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# CDT- autotools +.autotools + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ + +# Scala IDE specific (Scala & Java development for Eclipse) +.cache-main +.scala_dependencies +.worksheet +### Gradle template +.gradle +/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties +### Kotlin template +# Compiled class file + +# Log file + +# BlueJ files + +# Mobile Tools for Java (J2ME) + +# Package Files # + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..2ba8a04 --- /dev/null +++ b/build.gradle @@ -0,0 +1,35 @@ +plugins { + id 'java' + id 'org.jetbrains.kotlin.jvm' version '1.3.21' +} + +group 'io.github.thefrontier' +version '1.0-SNAPSHOT' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() + jcenter() + maven { url 'https://jitpack.io' } + maven { + name = 'sponge-repo' + url = 'https://repo.spongepowered.org/maven' + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + implementation "org.jetbrains.kotlin:kotlin-reflect" + + implementation 'org.spongepowered:spongeapi:7.1.0' + + implementation 'com.github.TheFrontier:SKE:8618c9d738' +} + +compileKotlin { + kotlinOptions.jvmTarget = "1.8" +} +compileTestKotlin { + kotlinOptions.jvmTarget = "1.8" +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..29e08e8 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +kotlin.code.style=official \ 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..28861d2 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..bc744c7 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Sun Mar 17 16:31:25 CDT 2019 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.10-all.zip diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..cccdd3d --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## 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="" + +# 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 + ;; + 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, switch paths to Windows format before running java +if $cygwin ; 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=$((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" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@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 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= + +@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 init + +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 init + +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 + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +: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 %CMD_LINE_ARGS% + +: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 b/settings.gradle new file mode 100644 index 0000000..165d2a8 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'skc' + diff --git a/src/main/kotlin/frontier/skc/ParameterMapping.kt b/src/main/kotlin/frontier/skc/ParameterMapping.kt new file mode 100644 index 0000000..4e02183 --- /dev/null +++ b/src/main/kotlin/frontier/skc/ParameterMapping.kt @@ -0,0 +1,7 @@ +package frontier.skc + +import org.spongepowered.api.command.args.CommandElement +import org.spongepowered.api.text.Text +import kotlin.reflect.KParameter + +typealias ParameterMapping = (KParameter) -> ((Text) -> CommandElement)? \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/ParameterMappings.kt b/src/main/kotlin/frontier/skc/ParameterMappings.kt new file mode 100644 index 0000000..3057665 --- /dev/null +++ b/src/main/kotlin/frontier/skc/ParameterMappings.kt @@ -0,0 +1,137 @@ +package frontier.skc + +import com.flowpowered.math.vector.Vector3d +import frontier.skc.annotation.OrSource +import frontier.skc.annotation.RemainingJoined +import frontier.skc.annotation.Serialized +import frontier.skc.annotation.Source +import frontier.skc.util.CommandSourceCommandElement +import frontier.skc.util.directlyMatchOnType +import frontier.skc.util.isSubtypeOf +import frontier.skc.util.matchOnType +import frontier.ske.gameRegistry +import frontier.ske.getType +import org.spongepowered.api.CatalogType +import org.spongepowered.api.command.CommandSource +import org.spongepowered.api.command.args.CommandElement +import org.spongepowered.api.command.args.GenericArguments +import org.spongepowered.api.entity.living.player.Player +import org.spongepowered.api.entity.living.player.User +import org.spongepowered.api.plugin.PluginContainer +import org.spongepowered.api.text.Text +import org.spongepowered.api.text.serializer.TextSerializer +import org.spongepowered.api.text.serializer.TextSerializers +import org.spongepowered.api.world.DimensionType +import org.spongepowered.api.world.Location +import org.spongepowered.api.world.World +import org.spongepowered.api.world.storage.WorldProperties +import java.math.BigDecimal +import java.math.BigInteger +import kotlin.reflect.KClass +import kotlin.reflect.KParameter +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.isSubclassOf + +object ParameterMappings { + + val PLAYER: ParameterMapping = matchOnType { + when { + it.findAnnotation() != null -> GenericArguments::playerOrSource + else -> GenericArguments::player + } + } + + val USER: ParameterMapping = matchOnType { + when { + it.findAnnotation() != null -> GenericArguments::userOrSource + else -> GenericArguments::user + } + } + + val STRING: ParameterMapping = matchOnType { + when (it.findAnnotation()?.raw) { + true -> GenericArguments::remainingRawJoinedStrings + false -> GenericArguments::remainingJoinedStrings + else -> GenericArguments::string + } + } + + val BOOLEAN: ParameterMapping = directlyMatchOnType(GenericArguments::bool) + + val INT: ParameterMapping = directlyMatchOnType(GenericArguments::integer) + + val LONG: ParameterMapping = directlyMatchOnType(GenericArguments::longNum) + + val DOUBLE: ParameterMapping = directlyMatchOnType(GenericArguments::doubleNum) + + val BIGINTEGER: ParameterMapping = directlyMatchOnType(GenericArguments::bigInteger) + + val BIGDECIMAL: ParameterMapping = directlyMatchOnType(GenericArguments::bigDecimal) + + val LOCATION: ParameterMapping = directlyMatchOnType>(GenericArguments::location) + + val VECTOR3D: ParameterMapping = directlyMatchOnType(GenericArguments::vector3d) + + val WORLD: ParameterMapping = directlyMatchOnType(GenericArguments::world) + + val DIMENSION: ParameterMapping = directlyMatchOnType(GenericArguments::dimension) + + val PLUGIN: ParameterMapping = directlyMatchOnType(GenericArguments::plugin) + + val TEXT: ParameterMapping = matchOnType { + val serializer = it.findAnnotation()?.let { serialized -> + gameRegistry.getType(serialized.id) + } ?: TextSerializers.FORMATTING_CODE + + val allRemaining = it.findAnnotation() + + return@matchOnType { key -> + GenericArguments.text(key, serializer, allRemaining != null) + } + } + + @Suppress("UNCHECKED_CAST") + val CATALOG_TYPE: ParameterMapping = { + when (it.type.isSubtypeOf()) { + true -> { + val clazz = (it.type.classifier as KClass).java + + { key -> + GenericArguments.catalogedElement(key, clazz) + } + } + else -> null + } + } + + @Suppress("UNCHECKED_CAST") + fun commandSource(parameter: KParameter): ((Text) -> CommandElement)? { + val type = parameter.type.classifier as? KClass<*> ?: return null + + return if (parameter.findAnnotation() != null && type.isSubclassOf(CommandSource::class)) { + { key -> CommandSourceCommandElement(key, type as KClass) } + } else { + null + } + } + + val DEFAULT = listOf( + ::commandSource, + PLAYER, + USER, + STRING, + BOOLEAN, + INT, + LONG, + DOUBLE, + BIGINTEGER, + BIGDECIMAL, + LOCATION, + VECTOR3D, + WORLD, + DIMENSION, + PLUGIN, + TEXT, + CATALOG_TYPE + ) +} \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/SKCCommand.kt b/src/main/kotlin/frontier/skc/SKCCommand.kt new file mode 100644 index 0000000..f8667b1 --- /dev/null +++ b/src/main/kotlin/frontier/skc/SKCCommand.kt @@ -0,0 +1,41 @@ +package frontier.skc + +import com.google.inject.Injector +import frontier.skc.annotation.Command +import frontier.skc.util.newSpec +import frontier.ske.commandManager +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.full.findAnnotation + +class SKCCommand( + private val mappings: List = ParameterMappings.DEFAULT, + private val injector: Injector? = null +) { + + inline fun register(plugin: Any) = register(plugin, T::class) + + fun register() { + + } + + fun register(plugin: Any, clazz: KClass<*>) { + val command = requireNotNull(clazz.findAnnotation()) { + "${clazz.simpleName} must be annotated with @Command" + } + + val spec = clazz.newSpec(mappings, injector).build() + + commandManager.register(plugin, spec, *command.aliases) + } + + fun register(plugin: Any, function: KFunction<*>) { + val command = requireNotNull(function.findAnnotation()) { + "${function.name} must be annotated with @Command" + } + + val spec = function.newSpec(mappings).build() + + commandManager.register(plugin, spec, *command.aliases) + } +} \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/annotation/Command.kt b/src/main/kotlin/frontier/skc/annotation/Command.kt new file mode 100644 index 0000000..012a405 --- /dev/null +++ b/src/main/kotlin/frontier/skc/annotation/Command.kt @@ -0,0 +1,4 @@ +package frontier.skc.annotation + +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +annotation class Command(vararg val aliases: String) diff --git a/src/main/kotlin/frontier/skc/annotation/Description.kt b/src/main/kotlin/frontier/skc/annotation/Description.kt new file mode 100644 index 0000000..ff29c2a --- /dev/null +++ b/src/main/kotlin/frontier/skc/annotation/Description.kt @@ -0,0 +1,4 @@ +package frontier.skc.annotation + +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS) +annotation class Description(val value: String) \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/annotation/Executor.kt b/src/main/kotlin/frontier/skc/annotation/Executor.kt new file mode 100644 index 0000000..ccb0a3f --- /dev/null +++ b/src/main/kotlin/frontier/skc/annotation/Executor.kt @@ -0,0 +1,4 @@ +package frontier.skc.annotation + +@Target(AnnotationTarget.FUNCTION) +annotation class Executor \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/annotation/Flag.kt b/src/main/kotlin/frontier/skc/annotation/Flag.kt new file mode 100644 index 0000000..aa83ee7 --- /dev/null +++ b/src/main/kotlin/frontier/skc/annotation/Flag.kt @@ -0,0 +1,4 @@ +package frontier.skc.annotation + +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class Flag(vararg val specs: String) \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/annotation/OrSource.kt b/src/main/kotlin/frontier/skc/annotation/OrSource.kt new file mode 100644 index 0000000..29b5156 --- /dev/null +++ b/src/main/kotlin/frontier/skc/annotation/OrSource.kt @@ -0,0 +1,4 @@ +package frontier.skc.annotation + +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class OrSource diff --git a/src/main/kotlin/frontier/skc/annotation/Permission.kt b/src/main/kotlin/frontier/skc/annotation/Permission.kt new file mode 100644 index 0000000..e55af48 --- /dev/null +++ b/src/main/kotlin/frontier/skc/annotation/Permission.kt @@ -0,0 +1,4 @@ +package frontier.skc.annotation + +@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS, AnnotationTarget.VALUE_PARAMETER) +annotation class Permission(val value: String) \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/annotation/Regex.kt b/src/main/kotlin/frontier/skc/annotation/Regex.kt new file mode 100644 index 0000000..4034b69 --- /dev/null +++ b/src/main/kotlin/frontier/skc/annotation/Regex.kt @@ -0,0 +1,7 @@ +package frontier.skc.annotation + +import org.intellij.lang.annotations.Language +import javax.annotation.RegEx + +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class Regex(@RegEx @Language("regexp") val regex: String) \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/annotation/RemainingJoined.kt b/src/main/kotlin/frontier/skc/annotation/RemainingJoined.kt new file mode 100644 index 0000000..5885413 --- /dev/null +++ b/src/main/kotlin/frontier/skc/annotation/RemainingJoined.kt @@ -0,0 +1,4 @@ +package frontier.skc.annotation + +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class RemainingJoined(val raw: Boolean = false) \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/annotation/Serialized.kt b/src/main/kotlin/frontier/skc/annotation/Serialized.kt new file mode 100644 index 0000000..f8870fb --- /dev/null +++ b/src/main/kotlin/frontier/skc/annotation/Serialized.kt @@ -0,0 +1,4 @@ +package frontier.skc.annotation + +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class Serialized(val id: String = "sponge:formatting_code_&") \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/annotation/Source.kt b/src/main/kotlin/frontier/skc/annotation/Source.kt new file mode 100644 index 0000000..2ed4293 --- /dev/null +++ b/src/main/kotlin/frontier/skc/annotation/Source.kt @@ -0,0 +1,4 @@ +package frontier.skc.annotation + +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class Source \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/annotation/Weak.kt b/src/main/kotlin/frontier/skc/annotation/Weak.kt new file mode 100644 index 0000000..db2f219 --- /dev/null +++ b/src/main/kotlin/frontier/skc/annotation/Weak.kt @@ -0,0 +1,4 @@ +package frontier.skc.annotation + +@Target(AnnotationTarget.VALUE_PARAMETER) +annotation class Weak \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/util/CommandSourceCommandElement.kt b/src/main/kotlin/frontier/skc/util/CommandSourceCommandElement.kt new file mode 100644 index 0000000..87a9403 --- /dev/null +++ b/src/main/kotlin/frontier/skc/util/CommandSourceCommandElement.kt @@ -0,0 +1,28 @@ +package frontier.skc.util + +import frontier.ske.text.not +import org.spongepowered.api.command.CommandException +import org.spongepowered.api.command.CommandSource +import org.spongepowered.api.command.args.CommandArgs +import org.spongepowered.api.command.args.CommandContext +import org.spongepowered.api.command.args.CommandElement +import org.spongepowered.api.text.Text +import kotlin.reflect.KClass + +class CommandSourceCommandElement(key: Text, private val required: KClass = CommandSource::class) : + CommandElement(key) { + + override fun parseValue(source: CommandSource, args: CommandArgs): Any? { + if (!required.isInstance(source)) { + throw CommandException(!"You must be a ${required.simpleName} to use that command!") + } + + return source + } + + override fun complete(src: CommandSource, args: CommandArgs, context: CommandContext): List = + emptyList() + + override fun getUsage(src: CommandSource): Text = + Text.EMPTY +} \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/util/ConstantNoUsageCommandElement.kt b/src/main/kotlin/frontier/skc/util/ConstantNoUsageCommandElement.kt new file mode 100644 index 0000000..f00f817 --- /dev/null +++ b/src/main/kotlin/frontier/skc/util/ConstantNoUsageCommandElement.kt @@ -0,0 +1,19 @@ +package frontier.skc.util + +import org.spongepowered.api.command.CommandSource +import org.spongepowered.api.command.args.CommandArgs +import org.spongepowered.api.command.args.CommandContext +import org.spongepowered.api.command.args.CommandElement +import org.spongepowered.api.text.Text + +class ConstantNoUsageCommandElement(key: Text, private val value: Any) : CommandElement(key) { + + override fun parseValue(source: CommandSource, args: CommandArgs): Any? = + value + + override fun complete(src: CommandSource, args: CommandArgs, context: CommandContext): List = + emptyList() + + override fun getUsage(src: CommandSource): Text = + Text.EMPTY +} \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/util/KAnnotatedElement.kt b/src/main/kotlin/frontier/skc/util/KAnnotatedElement.kt new file mode 100644 index 0000000..d3498ef --- /dev/null +++ b/src/main/kotlin/frontier/skc/util/KAnnotatedElement.kt @@ -0,0 +1,16 @@ +package frontier.skc.util + +import frontier.skc.annotation.Description +import frontier.skc.annotation.Permission +import frontier.ske.text.unaryPlus +import org.spongepowered.api.command.spec.CommandSpec +import kotlin.reflect.KAnnotatedElement +import kotlin.reflect.full.findAnnotation + +inline fun List.findAnnotation(): T? = + this.firstOrNull { it is T } as T? + +fun KAnnotatedElement.applyAnnotations(spec: CommandSpec.Builder) { + this.findAnnotation()?.let { spec.permission(it.value) } + this.findAnnotation()?.let { spec.description(+it.value) } +} \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/util/KClass.kt b/src/main/kotlin/frontier/skc/util/KClass.kt new file mode 100644 index 0000000..d2ba8eb --- /dev/null +++ b/src/main/kotlin/frontier/skc/util/KClass.kt @@ -0,0 +1,56 @@ +package frontier.skc.util + +import com.google.inject.Injector +import frontier.skc.ParameterMapping +import frontier.skc.annotation.Command +import frontier.skc.annotation.Executor +import org.spongepowered.api.command.spec.CommandSpec +import kotlin.reflect.KClass +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.functions + +fun KClass<*>.newSpec(mappings: List, injector: Injector? = null): CommandSpec.Builder { + val spec = CommandSpec.builder() + + val instance = this.objectInstance ?: requireNotNull(injector?.getInstance(this.java)) { + "Could not instantiate Command for ${this.simpleName}: must be an object, or provide an injector if class" + } + + val finalMappings = mappings + ObjectInstanceParameterMapping(this, instance) + + this.checkCommand() + this.applyAnnotations(spec) + + // Register child objects/classes. + for (childClass in this.nestedClasses) { + val aliases = childClass.findAnnotation()?.aliases ?: continue + val childSpec = childClass.newSpec(finalMappings).build() + spec.child(childSpec, *aliases) + } + + var hasDefault = false + + // Register child functions and the default executor, if available. + for (childFunction in this.functions) { + if (childFunction.findAnnotation() != null) { + // Found a default executor + require(!hasDefault) { "${this.simpleName} already has a default executor." } + + spec.arguments(childFunction.mapParameters(finalMappings)) + spec.executor(childFunction.createExecutor()) + + hasDefault = true + } + + val aliases = childFunction.findAnnotation()?.aliases ?: continue + val childSpec = childFunction.newSpec(finalMappings).build() + spec.child(childSpec, *aliases) + } + + return spec +} + +fun KClass<*>.checkCommand() { + require(!this.isAbstract) { "Unsupported class modifier: abstract" } + require(!this.isSealed) { "Unsupported class modifier: sealed" } +} \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/util/KFunction.kt b/src/main/kotlin/frontier/skc/util/KFunction.kt new file mode 100644 index 0000000..16eaeff --- /dev/null +++ b/src/main/kotlin/frontier/skc/util/KFunction.kt @@ -0,0 +1,81 @@ +package frontier.skc.util + +import frontier.skc.ParameterMapping +import frontier.skc.annotation.Flag +import frontier.skc.annotation.Permission +import org.spongepowered.api.command.CommandResult +import org.spongepowered.api.command.args.CommandElement +import org.spongepowered.api.command.args.GenericArguments +import org.spongepowered.api.command.spec.CommandExecutor +import org.spongepowered.api.command.spec.CommandSpec +import java.util.* +import kotlin.reflect.KFunction +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.jvm.isAccessible + +fun KFunction<*>.newSpec(mappings: List): CommandSpec.Builder { + val spec = CommandSpec.builder() + + this.checkCommand() + this.applyAnnotations(spec) + + spec.arguments(this.mapParameters(mappings)) + spec.executor(this.createExecutor()) + + return spec +} + +fun KFunction<*>.checkCommand() { + require(this.isAccessible) { "Function must be accessible." } + + require(this.returnType.isSubtypeOf() || this.returnType.isSubtypeOf()) { + "Unsupported return type (${this.returnType}), must be CommandResult or Unit" + } + + require(!this.isAbstract) { "Unsupported function modifier: abstract" } + require(!this.isExternal) { "Unsupported function modifier: external" } + require(!this.isInfix) { "Unsupported function modifier: infix" } + require(!this.isInline) { "Unsupported function modifier: inline" } + require(!this.isOperator) { "Unsupported function modifier: operator" } + require(!this.isSuspend) { "Unsupported function modifier: suspend" } +} + +fun KFunction<*>.mapParameters(mappings: List): CommandElement { + val flags = GenericArguments.flags() + val elements = LinkedList() + + for (parameter in this.parameters) { + val flag = parameter.findAnnotation() + + if (flag != null) { + // Flag element. + if (parameter.type.isType()) { + // Boolean flag. + val permission = parameter.findAnnotation() + + when (permission) { + null -> flags.flag(*flag.specs) + else -> flags.permissionFlag(permission.value, *flag.specs) + } + } else { + // Value flag. + flags.valueFlag(mappings.match(parameter), *flag.specs) + } + } else { + // Non-flag element. + elements += mappings.match(parameter) + } + } + + return flags.buildWith(GenericArguments.seq(*elements.toTypedArray())) +} + +fun KFunction<*>.createExecutor(): CommandExecutor = CommandExecutor { _, ctx -> + val result = this.callBy(this.parameters.buildCallingArguments(ctx)) + + if (result is CommandResult) { + result + } else { + CommandResult.success() + } +} \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/util/KParameter.kt b/src/main/kotlin/frontier/skc/util/KParameter.kt new file mode 100644 index 0000000..dbf8e89 --- /dev/null +++ b/src/main/kotlin/frontier/skc/util/KParameter.kt @@ -0,0 +1,27 @@ +package frontier.skc.util + +import frontier.ske.text.not +import org.spongepowered.api.command.CommandException +import org.spongepowered.api.command.args.CommandContext +import kotlin.reflect.KParameter + +val KParameter.effectiveName: String get() = this.name ?: "arg${this.index}" + +fun List.buildCallingArguments(ctx: CommandContext): Map { + val values = hashMapOf() + + for (parameter in this) { + if (ctx.hasAny(parameter.effectiveName)) { + // A value is available, use it. + values[parameter] = ctx.requireOne(parameter.effectiveName) + } else if (parameter.type.isMarkedNullable) { + // No value is available, but the type is nullable, so set it to null. + values[parameter] = null + } else if (!parameter.isOptional) { + // There is no available value to use! What do we do?! + throw CommandException(!"No value found for parameter '${parameter.effectiveName}'") + } + } + + return values +} \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/util/KType.kt b/src/main/kotlin/frontier/skc/util/KType.kt new file mode 100644 index 0000000..bdfeef7 --- /dev/null +++ b/src/main/kotlin/frontier/skc/util/KType.kt @@ -0,0 +1,11 @@ +package frontier.skc.util + +import kotlin.reflect.KType +import kotlin.reflect.full.createType +import kotlin.reflect.full.isSubtypeOf + +inline fun KType.isSubtypeOf() = + this.isSubtypeOf(T::class.createType()) + +inline fun KType.isType() = + this.classifier == T::class \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/util/ObjectInstanceParameterMapping.kt b/src/main/kotlin/frontier/skc/util/ObjectInstanceParameterMapping.kt new file mode 100644 index 0000000..47b8358 --- /dev/null +++ b/src/main/kotlin/frontier/skc/util/ObjectInstanceParameterMapping.kt @@ -0,0 +1,14 @@ +package frontier.skc.util + +import frontier.skc.ParameterMapping +import org.spongepowered.api.command.args.CommandElement +import org.spongepowered.api.text.Text +import kotlin.reflect.KClass +import kotlin.reflect.KParameter + +class ObjectInstanceParameterMapping(private val clazz: KClass<*>, private val instance: Any) : ParameterMapping { + override fun invoke(parameter: KParameter): ((Text) -> CommandElement)? = when (parameter.type.classifier) { + clazz -> { key -> ConstantNoUsageCommandElement(key, instance) } + else -> null + } +} \ No newline at end of file diff --git a/src/main/kotlin/frontier/skc/util/ParameterMatching.kt b/src/main/kotlin/frontier/skc/util/ParameterMatching.kt new file mode 100644 index 0000000..0805ca3 --- /dev/null +++ b/src/main/kotlin/frontier/skc/util/ParameterMatching.kt @@ -0,0 +1,52 @@ +package frontier.skc.util + +import frontier.skc.ParameterMapping +import frontier.skc.annotation.Permission +import frontier.skc.annotation.Weak +import frontier.ske.text.not +import org.spongepowered.api.command.args.CommandElement +import org.spongepowered.api.command.args.GenericArguments +import org.spongepowered.api.text.Text +import kotlin.reflect.KParameter +import kotlin.reflect.full.findAnnotation + +fun List.match(parameter: KParameter): CommandElement { + var element: CommandElement? = null + + for (mapper in this) { + element = mapper(parameter)?.invoke(!parameter.effectiveName) + } + + var result = requireNotNull(element) { + "Could not find a ParameterMapping that matches ${parameter.type}" + } + + parameter.findAnnotation()?.let { + result = GenericArguments.requiringPermission(result, it.value) + } + + if (parameter.isOptional || parameter.type.isMarkedNullable) { + result = when { + parameter.findAnnotation() != null -> GenericArguments.optionalWeak(result) + else -> GenericArguments.optional(result) + } + } + + return result +} + +inline fun matchOnType(crossinline process: ParameterMapping): ParameterMapping = + { parameter -> + when (parameter.type.classifier) { + T::class -> process(parameter) + else -> null + } + } + +inline fun directlyMatchOnType(noinline init: (Text) -> CommandElement): ParameterMapping = + { parameter -> + when (parameter.type.classifier) { + T::class -> init + else -> null + } + } \ No newline at end of file