diff --git a/.gitignore b/.gitignore index 6742be5..398c0da 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ .idea *.iml -dependency-reduced-pom.xml -**/target/** +**/out/** +**/build/** +.gradle pom.xml.versionsBackup **/spigot/** !**/archetype-resources/spigot/** diff --git a/.maven.xml b/.maven.xml deleted file mode 100644 index 4bd6f9a..0000000 --- a/.maven.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - ossrh - ${env.SONATYPE_USERNAME} - ${env.SONATYPE_PASSWORD} - - - - - - ossrh - - true - - - gpg - - ${env.GPG_PASSPHRASE} - - - - diff --git a/.travis.yml b/.travis.yml index 0dcd791..c7c692d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,20 +1,30 @@ language: java jdk: openjdk8 +install: skip + before_install: - - echo $GPG_SECRET_KEYS | base64 --decode | $GPG_EXECUTABLE --import - - echo $GPG_OWNERTRUST | base64 --decode | $GPG_EXECUTABLE --import-ownertrust - - export MAVEN_OPTS=-Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn -before_deploy: - - mvn help:evaluate -N -Dexpression=project.version|grep -v '\[' - - export project_version=$(mvn help:evaluate -N -Dexpression=project.version|grep -v '\[') + - chmod +x gradlew + +before_cache: + - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ + +cache: + directories: + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ + +script: + - ./gradlew build --scan -s + deploy: provider: script - skip_cleanup: true - ## Build and release to maven central on tagged version - script: mvn deploy --settings .maven.xml -DskipTests=true -B -U -Prelease + cleanup: false + script: + - ./gradlew bintrayPublish -Dorg.gradle.project.bintray.user=$BINTRAY_USER -Dorg.gradle.project.bintray.key=$BINTRAY_KEY + # TODO add in publishing for plugin + # - ./ -Dorg.gradle.project.gradle.publish.key=$PLUGIN_PORTAL_API_KEY -Dorg.gradle.project.gradle.publish.secret=$PLUGIN_PORTAL_API_SECRET on: tags: true branch: master name: $project_version -install: - mvn --settings .maven.xml clean install -DskipTests=true -Dgpg.skip -Dmaven.javadoc.skip=true -B -V diff --git a/README.md b/README.md index d46be2b..b6fb261 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# mcspring [![Build Status](https://travis-ci.org/kylepls/mcspring.svg?branch=master)](https://travis-ci.org/kylepls/mcspring) ![Maven Central](https://img.shields.io/maven-central/v/in.kyle.mcspring/mcspring) +# mcspring [![Build Status](https://travis-ci.org/kylepls/mcspring.svg?branch=master)](https://travis-ci.org/kylepls/mcspring) ![Bintray](https://img.shields.io/bintray/v/mcspring/maven/mcspring) Writing Bukkit plugins is a nightmare. I often lay awake in my bed late at night unable to sleep because Bukkit made events an annotation but commands are created by implementing a class. @@ -9,7 +9,7 @@ Writing Bukkit plugins is a nightmare. I often lay awake in my bed late at night These are solved problems. Spring Boot took care of this issue ages ago. So how about we ditch this ridiculous programming model and hop on the Spring train. -```java +```kotlin @Component class Test { // We don't have to extend JavaPlugin. The plugin.yml is also generated for us. @@ -69,15 +69,13 @@ class Test { // We don't have to extend JavaPlugin. The plugin.yml is also gener * Schedulers are defined with `@Scheduler`. Another thing to schlep away somewhere. * `@EventHandler` now registers itself. About damn time. * Like money? Vault support is in the box `in.kyle.mcspring.economy.EconomyService` -* Want my hot take on sub-command handing? We've got you covered (see the wiki) +* Want my hot take on sub-command handling? We've got you covered (see the wiki) +* I've also added some other optionionated libraries to the project with some pretty neat functionality. Check out mcspring-chat-actions, mcspring-chat, mcspring-guis, mcspring-nms. ## Getting Started -I went ahead and wrote a full tutorial series for you newcomers. Get started [here](https://github.com/kylepls/mcspring/wiki/Getting-Setup) +Please use the simple-factions project as a reference. This can be found in the `/mcspring-examples` folder. -If you think you're too smart for the beginner tutorial; go to the -[wiki](https://github.com/kylepls/mcspring/wiki) and piece it together. - -If you're really really smart; check out the example plugins in the `mcspring-example` folder. +Each subproject has attached documentation. --- diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..3974385 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + `mcspring-docs` +} + +allprojects { + group = "in.kyle.mcspring" + version = "0.1.0" +} + +subprojects { + apply(plugin = "mcspring-build") + apply(plugin = "mcspring-publish") +} + +defaultTasks("build") diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..09aa405 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + `kotlin-dsl` +} + +configure { + experimentalWarning.set(false) +} + +repositories { + jcenter() + mavenCentral() +} + +dependencies { + implementation("org.springframework.boot:spring-boot-gradle-plugin:2.3.1.RELEASE") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72") + implementation("io.spring.gradle:dependency-management-plugin:1.0.9.RELEASE") + implementation("org.jetbrains.dokka:dokka-gradle-plugin:0.10.1") + implementation("com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5") + implementation("gradle.plugin.com.eden:orchidPlugin:0.20.0") { + repositories { + maven("https://plugins.gradle.org/m2/") + } + } +} + diff --git a/buildSrc/src/main/kotlin/mcspring-build.gradle.kts b/buildSrc/src/main/kotlin/mcspring-build.gradle.kts new file mode 100644 index 0000000..94b4b29 --- /dev/null +++ b/buildSrc/src/main/kotlin/mcspring-build.gradle.kts @@ -0,0 +1,41 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") +} + +repositories { + jcenter() + mavenCentral() + mavenLocal() + maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") + maven("https://oss.sonatype.org/content/repositories/snapshots") +} + +dependencies { + val spigotVersion = "1.15.2-R0.1-SNAPSHOT" + compileOnly("org.spigotmc:spigot-api:$spigotVersion") + + implementation(kotlin("stdlib")) + + testImplementation("org.mockito:mockito-core:2.+") + testImplementation("io.mockk:mockk:1.10.0") + testImplementation("org.junit.jupiter:junit-jupiter:5.6.2") + testImplementation("org.spigotmc:spigot-api:$spigotVersion") + + val kotestVersion = "4.1.0.RC2" + testImplementation("io.kotest:kotest-runner-junit5-jvm:$kotestVersion") + testImplementation("io.kotest:kotest-assertions-core-jvm:$kotestVersion") + testImplementation("io.kotest:kotest-property-jvm:$kotestVersion") + testImplementation("io.kotest:kotest-runner-console-jvm:$kotestVersion") + testImplementation("io.kotest:kotest-extensions-spring:$kotestVersion") +} + +tasks.test { + useJUnitPlatform() +} + +tasks.withType().configureEach { + kotlinOptions.suppressWarnings = true + kotlinOptions.jvmTarget = "1.8" +} diff --git a/buildSrc/src/main/kotlin/mcspring-docs.gradle.kts b/buildSrc/src/main/kotlin/mcspring-docs.gradle.kts new file mode 100644 index 0000000..8217ace --- /dev/null +++ b/buildSrc/src/main/kotlin/mcspring-docs.gradle.kts @@ -0,0 +1,29 @@ +plugins { + id("org.jetbrains.dokka") + id("com.eden.orchidPlugin") +} + +repositories { + jcenter() +} + +dependencies { + orchidRuntimeOnly("io.github.javaeden.orchid:OrchidDocs:0.20.0") + orchidRuntimeOnly("io.github.javaeden.orchid:OrchidSourceDoc:0.20.0") + orchidRuntimeOnly("io.github.javaeden.orchid:OrchidKotlindoc:0.20.0") + orchidRuntimeOnly("io.github.javaeden.orchid:OrchidPluginDocs:0.20.0") +} + +tasks.withType(org.jetbrains.dokka.gradle.DokkaTask::class) { + outputFormat = "html" + configuration { + externalDocumentationLink { + url = uri("https://hub.spigotmc.org/javadocs/bukkit/").toURL() + url = uri("http://milkbowl.github.io/VaultAPI/").toURL() + } + } +} + +orchid { + theme = "Editorial" +} diff --git a/buildSrc/src/main/kotlin/mcspring-publish.gradle.kts b/buildSrc/src/main/kotlin/mcspring-publish.gradle.kts new file mode 100644 index 0000000..1a3e966 --- /dev/null +++ b/buildSrc/src/main/kotlin/mcspring-publish.gradle.kts @@ -0,0 +1,85 @@ +import com.jfrog.bintray.gradle.BintrayExtension + +plugins { + kotlin("jvm") + `maven-publish` + id("com.jfrog.bintray") +} + +val sourcesJar by tasks.creating(Jar::class) { + archiveClassifier.set("sources") + from(sourceSets.getByName("main").allSource) + dependsOn(tasks.classes) +} + +val artifactName = project.name +val artifactGroup = project.group.toString() +val artifactVersion = project.version.toString() + +val pomUrl = "https://github.com/kylepls/mcspring" +val pomScmUrl = "https://github.com/kylepls/mcspring" +val pomIssueUrl = "https://github.com/kylepls/mcspring/issues" +val pomDesc = "https://github.com/kylepls/mcspring" + +val githubRepo = "kylepls/mcspring" +val githubReadme = "README.md" + +val pomLicenseName = "MIT" +val pomLicenseUrl = "https://opensource.org/licenses/mit-license.php" +val pomLicenseDist = "repo" + +val pomDeveloperId = "kylepls" +val pomDeveloperName = "Kyle" + +publishing { + publications { + create("mcspring") { + groupId = artifactGroup + artifactId = artifactName + version = artifactVersion + from(components["java"]) + artifact(sourcesJar) { + classifier = "sources" + } + + pom.withXml { + asNode().apply { + appendNode("description", pomDesc) + appendNode("name", rootProject.name) + appendNode("url", pomUrl) + appendNode("licenses").appendNode("license").apply { + appendNode("name", pomLicenseName) + appendNode("url", pomLicenseUrl) + appendNode("distribution", pomLicenseDist) + } + appendNode("developers").appendNode("developer").apply { + appendNode("id", pomDeveloperId) + appendNode("name", pomDeveloperName) + } + appendNode("scm").apply { + appendNode("url", pomScmUrl) + } + } + } + } + } +} + +bintray { + user = project.findProperty("bintray.user")?.toString() ?: System.getProperty("bintray.user") + key = project.findProperty("bintray.key")?.toString() ?: System.getProperty("bintray.key") + setPublications("mcspring") + override = true + publish = true + + pkg(delegateClosureOf { + repo = "maven" + name = "mcspring" + userOrg = "mcspring" + setLabels("kotlin", "spigot", "spring") + vcsUrl = "https://github.com/kylepls/mcspring" + }) +} + +tasks.findByName("build")!!.dependsOn(tasks.findByName("publishToMavenLocal")) + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..62d4c05 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..a4f0001 --- /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-6.4.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..fbd7c51 --- /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 + ;; + 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..5093609 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,104 @@ +@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 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/mcspring-api/mcspring-base/README.md b/mcspring-api/mcspring-base/README.md new file mode 100644 index 0000000..6776d17 --- /dev/null +++ b/mcspring-api/mcspring-base/README.md @@ -0,0 +1,36 @@ +mcspring-base + +--- + +_A lightweight Spring Boot wrapper for Bukkit_ + +This is the "core" of mcspring. Most of the magic sauce is in here (and the gradle plugin). + +* Adds support for `@Scheduled`, `@EventHandler` and plugin dependency annotations. +* Bootstraps Spring on top of the plugin system. + +### Quickstart + +#### Annotations: +* `@PluginDepend` - Add this annotation to a Spring component to add plugin dependencies to the generated `plugin.yml` +* `@SoftPluginDepend` - Same as above but for soft dependencies. + +#### Events: +Register a class as a component and event handlers will be automatically registered. + +```kotlin +@Component +class Demo { + @EventHandler + fun move(e: PlayerMoveEvent) { + println(e.getPlayer().getName().toString() + " moved") + } +} +``` + +#### Scheduling +There are 2 ways to schedule things depending on your use case. + +#1: Global Scheduling Annotation: See [The @Scheduled Annotation in Spring](https://www.baeldung.com/spring-scheduled-tasks) + +#2: A convenience wrapper for the Bukkit scheduler. See: in.kyle.mcspring.scheduler.SchedulerService. diff --git a/mcspring-api/mcspring-base/build.gradle.kts b/mcspring-api/mcspring-base/build.gradle.kts new file mode 100644 index 0000000..a5b344a --- /dev/null +++ b/mcspring-api/mcspring-base/build.gradle.kts @@ -0,0 +1,24 @@ +plugins { + id("org.jetbrains.dokka") +} + +repositories { + mavenCentral() +} + +configurations.getByName("api") { +// exclude("com.google.code.gson") +} + +dependencies { +// compileOnly("org.apache.logging.log4j:log4j-core:2.12.1") + api(platform("org.springframework.boot:spring-boot-dependencies:2.3.1.RELEASE")) + + api("org.springframework.boot:spring-boot-loader") { + exclude("org.springframework.boot", "spring-boot-dependencies") + } + api("org.springframework.boot:spring-boot-starter") { + exclude("org.springframework.boot", "spring-boot-dependencies") + exclude("org.yaml", "snakeyaml") + } +} diff --git a/mcspring-api/mcspring-base/pom.xml b/mcspring-api/mcspring-base/pom.xml deleted file mode 100644 index 420d80f..0000000 --- a/mcspring-api/mcspring-base/pom.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - mcspring-api - in.kyle.mcspring - 0.0.9 - - - 4.0.0 - - mcspring-base - - - - spigot-repo - https://hub.spigotmc.org/nexus/content/repositories/snapshots/ - - - - - - org.spigotmc - spigot-api - - - in.kyle.mcspring - mcspring-jar-loader - ${project.version} - - - org.springframework.boot - spring-boot-loader - - - org.springframework.boot - spring-boot-starter - - - org.springframework.boot - spring-boot-starter-logging - - - - - org.apache.logging.log4j - log4j-core - 2.12.1 - provided - - - org.springframework.boot - spring-boot-starter-aop - - - org.springframework.boot - spring-boot-starter-test - test - - - org.projectlombok - lombok - - - diff --git a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/SpringPlugin.java b/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/SpringPlugin.java deleted file mode 100644 index 7988d2d..0000000 --- a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/SpringPlugin.java +++ /dev/null @@ -1,83 +0,0 @@ -package in.kyle.mcspring; - -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.core.Logger; -import org.bukkit.plugin.Plugin; -import org.springframework.boot.Banner; -import org.springframework.boot.builder.SpringApplicationBuilder; -import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.io.DefaultResourceLoader; -import org.springframework.core.io.ResourceLoader; - -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.Map; - -import lombok.Getter; -import lombok.RequiredArgsConstructor; - -@RequiredArgsConstructor -public class SpringPlugin { - - private static final LinkedHashMap SETUP_PLUGINS = new LinkedHashMap<>(); - - private final Plugin plugin; - @Getter - private ConfigurableApplicationContext context; - - public static void setup(Plugin plugin, Class config) { - setupLogger(); - SpringPlugin springPlugin = new SpringPlugin(plugin); - springPlugin.initSpring(config); - SETUP_PLUGINS.put(plugin, springPlugin); - } - - public static void teardown(Plugin plugin) { - SpringPlugin springPlugin = SETUP_PLUGINS.remove(plugin); - if (springPlugin != null) { - springPlugin.onDisable(plugin); - } - } - - public final void onDisable(Plugin plugin) { - if (context != null) { - context.close(); - context = null; - SETUP_PLUGINS.remove(plugin); - } - } - - private void initSpring(Class config) { - SpringApplicationBuilder builder = new SpringApplicationBuilder(); - ClassLoader classLoader = plugin.getClass().getClassLoader(); - ResourceLoader loader = new DefaultResourceLoader(classLoader); - Class[] sources = new Class[]{config, SpringSpigotSupport.class}; - if (!SETUP_PLUGINS.isEmpty()) { - SpringPlugin parent = findParentCandidate(); - builder.parent(parent.getContext()); - sources = Arrays.copyOfRange(sources, 0, 1); - } - context = builder.sources(sources) - .resourceLoader(loader) - .bannerMode(Banner.Mode.OFF) - .properties("spigot.plugin=" + plugin.getName()) - .logStartupInfo(true) - .run(); - } - - private static void setupLogger() { - if (SETUP_PLUGINS.isEmpty()) { - Logger rootLogger = (Logger) LogManager.getRootLogger(); - rootLogger.setLevel(Level.ALL); - } - } - - private SpringPlugin findParentCandidate() { - return SETUP_PLUGINS.entrySet() - .stream() - .reduce((a, b) -> b) - .map(Map.Entry::getValue) - .orElse(null); - } -} diff --git a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/SpringSpigotSupport.java b/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/SpringSpigotSupport.java deleted file mode 100644 index 4f8cf8e..0000000 --- a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/SpringSpigotSupport.java +++ /dev/null @@ -1,77 +0,0 @@ -package in.kyle.mcspring; - -import org.bukkit.Bukkit; -import org.bukkit.Server; -import org.bukkit.configuration.file.FileConfiguration; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.PluginDescriptionFile; -import org.bukkit.plugin.PluginLoader; -import org.bukkit.plugin.PluginManager; -import org.bukkit.plugin.messaging.Messenger; -import org.bukkit.scheduler.BukkitScheduler; -import org.bukkit.scoreboard.ScoreboardManager; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.EnableAspectJAutoProxy; -import org.springframework.scheduling.annotation.EnableScheduling; - -import java.util.logging.Logger; - -@Configuration -@ComponentScan(basePackageClasses = SpringPlugin.class) -@EnableScheduling -@EnableAspectJAutoProxy -class SpringSpigotSupport { - - @Bean - Plugin plugin(@Value("${spigot.plugin}") String pluginName) { - return Bukkit.getPluginManager().getPlugin(pluginName); - } - - @Bean(destroyMethod = "") - Server server(Plugin plugin) { - return plugin.getServer(); - } - - @Bean - Logger logger(Plugin plugin) { - return plugin.getLogger(); - } - - @Bean - PluginManager pluginManager(Server server) { - return server.getPluginManager(); - } - - @Bean - ScoreboardManager scoreboardManager(Server server) { - return server.getScoreboardManager(); - } - - @Bean - Messenger messenger(Server server) { - return server.getMessenger(); - } - - @Bean - FileConfiguration configuration(Plugin plugin) { - return plugin.getConfig(); - } - - @Bean - PluginDescriptionFile description(Plugin plugin) { - return plugin.getDescription(); - } - - @Bean - BukkitScheduler scheduler(Server server) { - return server.getScheduler(); - } - - @Bean - PluginLoader pluginLoader(Plugin plugin) { - return plugin.getPluginLoader(); - } -} diff --git a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/BukkitCommandRegistration.java b/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/BukkitCommandRegistration.java deleted file mode 100644 index 9c33f9e..0000000 --- a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/BukkitCommandRegistration.java +++ /dev/null @@ -1,36 +0,0 @@ -package in.kyle.mcspring.command; - -import org.bukkit.plugin.Plugin; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.stereotype.Component; - -import java.lang.reflect.Method; -import java.util.Arrays; - -import lombok.RequiredArgsConstructor; -import lombok.var; - -@Component -@ConditionalOnBean(Plugin.class) -@RequiredArgsConstructor -class BukkitCommandRegistration implements CommandRegistration { - - private final CommandController controller; - private final CommandFactory commandFactory; - - @Override - public void register(Command command, Method method, Object object) { - String name = command.value(); - var bukkitCommand = commandFactory.makeCommand(method, object, name); - bukkitCommand.setAliases(Arrays.asList(command.aliases())); - bukkitCommand.setDescription(command.description()); - bukkitCommand.setUsage(command.usage()); - bukkitCommand.setPermission(command.permission()); - bukkitCommand.setPermissionMessage(command.permissionMessage()); - controller.registerCommand(bukkitCommand); - } - - interface CommandFactory { - org.bukkit.command.Command makeCommand(Method method, Object object, String name); - } -} diff --git a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/Command.java b/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/Command.java deleted file mode 100644 index 8c45394..0000000 --- a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/Command.java +++ /dev/null @@ -1,23 +0,0 @@ -package in.kyle.mcspring.command; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -public @interface Command { - - String value(); - - String[] aliases() default {}; - - String description() default ""; - - String usage() default ""; - - String permission() default ""; - - String permissionMessage() default ""; -} diff --git a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/CommandController.java b/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/CommandController.java deleted file mode 100644 index d4edc40..0000000 --- a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/CommandController.java +++ /dev/null @@ -1,39 +0,0 @@ -package in.kyle.mcspring.command; - -import org.bukkit.Bukkit; -import org.bukkit.command.Command; -import org.bukkit.command.CommandMap; -import org.bukkit.plugin.Plugin; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.context.annotation.Bean; -import org.springframework.stereotype.Controller; - -import java.lang.reflect.Field; - -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; - -@Controller -@RequiredArgsConstructor -@ConditionalOnBean(Plugin.class) -class CommandController { - - private final CommandMap commandMap = getCommandMap(); - private final Plugin plugin; - - public void registerCommand(Command command) { - commandMap.register(command.getLabel(), plugin.getName(), command); - } - - @SneakyThrows - private CommandMap getCommandMap() { - Field bukkitCommandMap = Bukkit.getServer().getClass().getDeclaredField("commandMap"); - bukkitCommandMap.setAccessible(true); - return (CommandMap) bukkitCommandMap.get(Bukkit.getServer()); - } - - @Bean - CommandMap commandMap() { - return commandMap; - } -} diff --git a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/CommandRegistration.java b/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/CommandRegistration.java deleted file mode 100644 index 0a8a4f4..0000000 --- a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/CommandRegistration.java +++ /dev/null @@ -1,7 +0,0 @@ -package in.kyle.mcspring.command; - -import java.lang.reflect.Method; - -public interface CommandRegistration { - void register(Command command, Method method, Object object); -} diff --git a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/CommandResolver.java b/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/CommandResolver.java deleted file mode 100644 index 16d6309..0000000 --- a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/CommandResolver.java +++ /dev/null @@ -1,17 +0,0 @@ -package in.kyle.mcspring.command; - -import org.bukkit.command.CommandSender; - -import lombok.Value; - -public interface CommandResolver { - - Resolver makeResolver(Command command); - - @Value - class Command { - CommandSender sender; - String[] args; - String label; - } -} diff --git a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/CommandScanner.java b/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/CommandScanner.java deleted file mode 100644 index fdd9811..0000000 --- a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/CommandScanner.java +++ /dev/null @@ -1,37 +0,0 @@ -package in.kyle.mcspring.command; - -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.stereotype.Component; - -import java.lang.reflect.Method; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import in.kyle.mcspring.util.SpringScanner; -import lombok.AllArgsConstructor; -import lombok.var; - -@Component -@AllArgsConstructor -class CommandScanner implements ApplicationContextAware { - - private final SpringScanner scanner; - private final CommandRegistration commandRegistration; - private final Set registeredCommands = new HashSet<>(); - - @Override - public void setApplicationContext(ApplicationContext ctx) throws BeansException { - Map scan = scanner.scanMethods(Command.class); - for (var e : scan.entrySet()) { - if (!registeredCommands.contains(e.getKey())) { - Command command = e.getKey().getAnnotation(Command.class); - commandRegistration.register(command, e.getKey(), e.getValue()); - registeredCommands.add(e.getKey()); - } - } - } - -} diff --git a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/Resolver.java b/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/Resolver.java deleted file mode 100644 index c46e9b7..0000000 --- a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/Resolver.java +++ /dev/null @@ -1,8 +0,0 @@ -package in.kyle.mcspring.command; - -import java.lang.reflect.Parameter; -import java.util.Optional; - -public interface Resolver { - Optional resolve(Parameter parameter); -} diff --git a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/SimpleCommandFactory.java b/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/SimpleCommandFactory.java deleted file mode 100644 index 2446180..0000000 --- a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/SimpleCommandFactory.java +++ /dev/null @@ -1,61 +0,0 @@ -package in.kyle.mcspring.command; - -import org.bukkit.command.CommandExecutor; -import org.bukkit.command.PluginCommand; -import org.bukkit.plugin.Plugin; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.stereotype.Component; - -import java.lang.reflect.Method; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.var; - -@Component -@RequiredArgsConstructor -@ConditionalOnBean(Plugin.class) -class SimpleCommandFactory implements BukkitCommandRegistration.CommandFactory { - - private final SimpleMethodInjection methodInjection; - private final Set commandResolvers; - private final Plugin plugin; - - @SneakyThrows - public org.bukkit.command.Command makeCommand(Method method, Object object, String name) { - var constructor = PluginCommand.class.getDeclaredConstructor(String.class, Plugin.class); - constructor.setAccessible(true); - PluginCommand command = constructor.newInstance(name, plugin); - CommandExecutor executor = makeExecutor(method, object); - command.setExecutor(executor); - return command; - } - - CommandExecutor makeExecutor(Method method, Object object) { - return (commandSender, bukkitCommand, label, args) -> { - try { - CommandResolver.Command command = - new CommandResolver.Command(commandSender, args, label); - List contextResolvers = commandResolvers.stream() - .map(r -> r.makeResolver(command)) - .collect(Collectors.toList()); - Object result = methodInjection.invoke(method, - object, - contextResolvers, - commandSender, - args, - label); - if (result != null) { - commandSender.sendMessage(result.toString()); - } - } catch (RuntimeException exception) { - throw new RuntimeException("Could not invoke method " + method.getName(), - exception); - } - return true; - }; - } -} diff --git a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/SimpleMethodInjection.java b/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/SimpleMethodInjection.java deleted file mode 100644 index 88aa719..0000000 --- a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/SimpleMethodInjection.java +++ /dev/null @@ -1,82 +0,0 @@ -package in.kyle.mcspring.command; - -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Component; -import org.springframework.util.ClassUtils; - -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.var; - -/** - * Used to inject method parameters - * Does not support annotated parameters - */ -@Lazy -@Component -@RequiredArgsConstructor -public class SimpleMethodInjection { - - private final List resolvers; - - @SneakyThrows - public Object invoke(Method method, Object object, List resolvers, Object... contextObjects) { - Object[] params = getParameters(method, resolvers, contextObjects); - method.setAccessible(true); - if (params.length != 0) { - return method.invoke(object, params); - } else { - return method.invoke(object); - } - } - - private List makeResolvers(Object... contextObjects) { - return Stream.of(contextObjects) - .filter(Objects::nonNull) - .map(o -> (Resolver) parameter -> ClassUtils.isAssignable(parameter.getType(), - o.getClass()) - ? Optional.of(o) - : Optional.empty()) - .collect(Collectors.toList()); - } - - public Object[] getParameters(Method method, List contextResolvers, Object... contextObjects) { - Parameter[] parameters = method.getParameters(); - Object[] params = new Object[parameters.length]; - - List methodResolvers = new ArrayList<>(); - methodResolvers.addAll(contextResolvers); - methodResolvers.addAll(makeResolvers(contextObjects)); - methodResolvers.addAll(resolvers); - for (int i = 0; i < parameters.length; i++) { - Parameter parameter = parameters[i]; - - for (int j = 0; j < methodResolvers.size(); j++) { - Resolver methodResolver = methodResolvers.get(j); - Optional resolved = methodResolver.resolve(parameter); - if (resolved.isPresent()) { - params[i] = resolved.get(); - var removed = methodResolvers.remove(j); - methodResolvers.add(removed); - break; - } - } - - if (params[i] == null) { - throw new RuntimeException( - "Unable to resolve parameter " + parameter.getType() + " for " + - method.getName()); - } - } - return params; - } -} diff --git a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/SimpleSpringResolver.java b/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/SimpleSpringResolver.java deleted file mode 100644 index 8ad17f6..0000000 --- a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/command/SimpleSpringResolver.java +++ /dev/null @@ -1,26 +0,0 @@ -package in.kyle.mcspring.command; - -import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.context.ApplicationContext; -import org.springframework.stereotype.Component; - -import java.lang.reflect.Parameter; -import java.util.Optional; - -import lombok.AllArgsConstructor; - -@Component -@AllArgsConstructor -class SimpleSpringResolver implements Resolver { - - private final ApplicationContext context; - - @Override - public Optional resolve(Parameter parameter) { - try { - return Optional.of(context.getBean(parameter.getType())); - } catch (NoSuchBeanDefinitionException e) { - return Optional.empty(); - } - } -} diff --git a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/event/EventHandlerSupport.java b/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/event/EventHandlerSupport.java deleted file mode 100644 index 4213e56..0000000 --- a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/event/EventHandlerSupport.java +++ /dev/null @@ -1,45 +0,0 @@ -package in.kyle.mcspring.event; - -import org.bukkit.event.Event; -import org.bukkit.event.EventHandler; -import org.bukkit.plugin.EventExecutor; -import org.bukkit.plugin.Plugin; -import org.springframework.aop.support.AopUtils; -import org.springframework.beans.BeansException; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.stereotype.Component; - -import java.lang.reflect.Method; - -import in.kyle.mcspring.util.SpringScanner; -import lombok.AllArgsConstructor; -import lombok.SneakyThrows; - -@Component -@AllArgsConstructor -@ConditionalOnBean(Plugin.class) -class EventHandlerSupport implements ApplicationContextAware { - - private final EventService eventService; - private final SpringScanner scanner; - - @Override - public void setApplicationContext(ApplicationContext ctx) throws BeansException { - scanner.scanMethods(EventHandler.class).forEach(this::register); - } - - private void register(Method method, Object container) { - eventService.registerEvent(method, createEventExecutor(container, method)); - } - - private EventExecutor createEventExecutor(Object listenerBean, Method method) { - return (listener, event) -> triggerEvent(method, listenerBean, event); - } - - @SneakyThrows - private void triggerEvent(Method method, Object listenerBean, Event event) { - AopUtils.invokeJoinpointUsingReflection(listenerBean, method, new Object[]{event}); - } -} diff --git a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/event/EventService.java b/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/event/EventService.java deleted file mode 100644 index 6b26004..0000000 --- a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/event/EventService.java +++ /dev/null @@ -1,44 +0,0 @@ -package in.kyle.mcspring.event; - -import org.bukkit.Server; -import org.bukkit.event.Event; -import org.bukkit.event.EventHandler; -import org.bukkit.event.Listener; -import org.bukkit.plugin.EventExecutor; -import org.bukkit.plugin.Plugin; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; - -import java.lang.reflect.Method; - -import lombok.AllArgsConstructor; -import lombok.val; - -@Lazy -@Service -@AllArgsConstructor -@ConditionalOnBean(Plugin.class) -class EventService { - - private final Server server; - private final Plugin plugin; - - void registerEvent(Method method, EventExecutor executor) { - val handler = method.getAnnotation(EventHandler.class); - val eventType = (Class) method.getParameters()[0].getType(); - - server.getPluginManager() - .registerEvent(eventType, - makeListener(), - handler.priority(), - executor, - plugin, - handler.ignoreCancelled()); - } - - private Listener makeListener() { - return new Listener() { - }; - } -} diff --git a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/scheduler/ScheduledAnnotationSupport.java b/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/scheduler/ScheduledAnnotationSupport.java deleted file mode 100644 index d224a2a..0000000 --- a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/scheduler/ScheduledAnnotationSupport.java +++ /dev/null @@ -1,100 +0,0 @@ -package in.kyle.mcspring.scheduler; - -import org.bukkit.plugin.Plugin; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.context.annotation.Lazy; -import org.springframework.scheduling.Trigger; -import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; -import org.springframework.stereotype.Component; -import org.springframework.util.concurrent.ListenableFuture; - -import java.util.Date; -import java.util.concurrent.Callable; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledFuture; - -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; - -@Lazy -@Component -@AllArgsConstructor -@ConditionalOnBean(Plugin.class) -class ScheduledAnnotationSupport extends ThreadPoolTaskScheduler { - - private final SchedulerService scheduler; - - @Override - public ScheduledFuture schedule(Runnable task, Trigger trigger) { - return super.schedule(wrapSync(task), trigger); - } - - @Override - public ScheduledFuture schedule(Runnable task, Date startTime) { - return super.schedule(wrapSync(task), startTime); - } - - @Override - public ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period) { - return super.scheduleAtFixedRate(wrapSync(task), startTime, period); - } - - @Override - public ScheduledFuture scheduleAtFixedRate(Runnable task, long period) { - return super.scheduleAtFixedRate(wrapSync(task), period); - } - - @Override - public ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay) { - return super.scheduleWithFixedDelay(wrapSync(task), startTime, delay); - } - - @Override - public ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay) { - return super.scheduleWithFixedDelay(wrapSync(task), delay); - } - - @Override - public void execute(Runnable task) { - super.execute(task); - } - - @Override - public Future submit(Runnable task) { - return super.submit(task); - } - - @Override - public Future submit(Callable task) { - return super.submit(task); - } - - @Override - public ListenableFuture submitListenable(Runnable task) { - return super.submitListenable(task); - } - - @Override - public ListenableFuture submitListenable(Callable task) { - return super.submitListenable(task); - } - - private Runnable wrapSync(Runnable task) { - return new WrappedRunnable(scheduler, task); - } - - @AllArgsConstructor - @EqualsAndHashCode(onlyExplicitlyIncluded = true) - static class WrappedRunnable implements Runnable { - - private final SchedulerService scheduler; - - @EqualsAndHashCode.Include - private final Runnable runnable; - - @Override - public void run() { - scheduler.syncTask(runnable); - } - } -} diff --git a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/scheduler/SchedulerService.java b/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/scheduler/SchedulerService.java deleted file mode 100644 index 41b38ec..0000000 --- a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/scheduler/SchedulerService.java +++ /dev/null @@ -1,56 +0,0 @@ -package in.kyle.mcspring.scheduler; - -import org.bukkit.plugin.Plugin; -import org.bukkit.scheduler.BukkitScheduler; -import org.bukkit.scheduler.BukkitTask; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; - -import lombok.RequiredArgsConstructor; - -@Lazy -@Service -@RequiredArgsConstructor -@ConditionalOnBean(Plugin.class) -public class SchedulerService { - - private final BukkitScheduler scheduler; - private final Plugin plugin; - - public BukkitTask asyncTask(Runnable task) { - return scheduler.runTaskAsynchronously(plugin, task); - } - - public BukkitTask syncTask(Runnable task) { - return scheduler.runTask(plugin, task); - } - - public BukkitTask asyncDelayedTask(Runnable task, long delayTicks) { - return scheduler.runTaskLaterAsynchronously(plugin, task, delayTicks); - } - - public BukkitTask syncDelayedTask(Runnable task, long delayTicks) { - return scheduler.runTaskLater(plugin, task, delayTicks); - } - - public BukkitTask asyncRepeatingTask(Runnable task, long delayTicks, long periodTicks) { - return scheduler.runTaskTimerAsynchronously(plugin, task, delayTicks, periodTicks); - } - - public BukkitTask syncRepeatingTask(Runnable task, long delayTicks, long periodTicks) { - return scheduler.runTaskTimer(plugin, task, delayTicks, periodTicks); - } - - public void cancelTask(BukkitTask task) { - scheduler.cancelTask(task.getTaskId()); - } - - public boolean isCurrentlyRunning(BukkitTask task) { - return scheduler.isCurrentlyRunning(task.getTaskId()); - } - - public boolean isQueued(BukkitTask task) { - return scheduler.isQueued(task.getTaskId()); - } -} diff --git a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/util/SpringScanner.java b/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/util/SpringScanner.java deleted file mode 100644 index a9512ad..0000000 --- a/mcspring-api/mcspring-base/src/main/java/in/kyle/mcspring/util/SpringScanner.java +++ /dev/null @@ -1,43 +0,0 @@ -package in.kyle.mcspring.util; - -import org.springframework.aop.support.AopUtils; -import org.springframework.context.ApplicationContext; -import org.springframework.stereotype.Component; - -import java.lang.annotation.Annotation; -import java.lang.reflect.Method; -import java.util.HashMap; -import java.util.Map; - -import lombok.AllArgsConstructor; - -@Component -@AllArgsConstructor -public class SpringScanner { - - private final ApplicationContext context; - - public Map scanMethods(Class annotation) { - HashMap methods = new HashMap<>(); - for (String beanName : context.getBeanDefinitionNames()) { - Object object = context.getBean(beanName); - - Class clazz = getRealClass(object); - - for (Method method : clazz.getDeclaredMethods()) { - if (method.isAnnotationPresent(annotation)) { - methods.put(method, object); - } - } - } - return methods; - } - - private Class getRealClass(Object object) { - Class clazz = object.getClass(); - if (AopUtils.isAopProxy(clazz)) { - clazz = AopUtils.getTargetClass(object); - } - return clazz; - } -} diff --git a/mcspring-api/mcspring-base/src/main/java/org/springframework/boot/loader/in/kyle/mcspring/javaplugin/SpringJavaPlugin.java b/mcspring-api/mcspring-base/src/main/java/org/springframework/boot/loader/in/kyle/mcspring/javaplugin/SpringJavaPlugin.java new file mode 100644 index 0000000..678695a --- /dev/null +++ b/mcspring-api/mcspring-base/src/main/java/org/springframework/boot/loader/in/kyle/mcspring/javaplugin/SpringJavaPlugin.java @@ -0,0 +1,108 @@ +// Package name for hiding the class in the final jar +package org.springframework.boot.loader.in.kyle.mcspring.javaplugin; + +import java.lang.reflect.Field; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Collection; +import java.util.Iterator; + +import org.bukkit.plugin.java.JavaPlugin; +import org.springframework.boot.loader.JarLauncher; +import org.springframework.boot.loader.archive.Archive; +import org.springframework.lang.NonNull; + +import in.kyle.mcspring.SpringLoader; + +// This has to be written in Java for loading +public class SpringJavaPlugin extends JavaPlugin { + + private SpringLoader impl; + + @Override + public void onEnable() { + try { + new McSpringLoader().launch(getClassLoader()); + + impl = new SpringLoader(this, getClassLoader()); + impl.onEnable(); + } catch (Exception e) { + getLogger().info("MCSpring failed to load"); + throw new RuntimeException(e); + } + } + + @Override + public void onDisable() { + impl.onDisable(); + } + + static class McSpringLoader extends JarLauncher { + public void launch(ClassLoader classLoader) throws Exception { + Iterator activeArchives = getClassPathArchivesIterator(); + // Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); + // addURL.setAccessible(true); + UnsafeClassLoaderAccess unsafe = new UnsafeClassLoaderAccess((URLClassLoader) classLoader); + while (activeArchives.hasNext()) { + // addURL.invoke(classLoader, activeArchives.next().getUrl()); + unsafe.addURL(activeArchives.next().getUrl()); + } + } + + @Override + protected String getMainClass() { + return ""; + } + } + + // https://github.com/slimjar/slimjar + private static class UnsafeClassLoaderAccess { + private static final sun.misc.Unsafe UNSAFE; + + static { + try { + Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); + unsafeField.setAccessible(true); + UNSAFE = (sun.misc.Unsafe) unsafeField.get(null); + } catch (Throwable t) { + throw new Error("Unable to find unsafe", t); + } + } + + private final Collection unopenedURLs; + private final Collection pathURLs; + + @SuppressWarnings("unchecked") + UnsafeClassLoaderAccess(URLClassLoader classLoader) { + Collection unopenedURLs; + Collection pathURLs; + try { + Object ucp = fetchField(URLClassLoader.class, classLoader, "ucp"); + unopenedURLs = (Collection) fetchField(ucp.getClass(), ucp, "unopenedUrls"); + pathURLs = (Collection) fetchField(ucp.getClass(), ucp, "path"); + } catch (Throwable e) { + unopenedURLs = null; + pathURLs = null; + } + + this.unopenedURLs = unopenedURLs; + this.pathURLs = pathURLs; + } + + private static Object fetchField(final Class clazz, final Object object, final String name) + throws NoSuchFieldException { + Field field = clazz.getDeclaredField(name); + long offset = UNSAFE.objectFieldOffset(field); + return UNSAFE.getObject(object, offset); + } + + public void addURL(@NonNull URL url) { + if (this.unopenedURLs == null || this.pathURLs == null) { + throw new NullPointerException("unopenedURLs or pathURLs"); + } + + this.unopenedURLs.add(url); + this.pathURLs.add(url); + } + } +} diff --git a/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/SpringLoader.kt b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/SpringLoader.kt new file mode 100644 index 0000000..35635bb --- /dev/null +++ b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/SpringLoader.kt @@ -0,0 +1,82 @@ +package `in`.kyle.mcspring + +import org.bukkit.plugin.java.JavaPlugin +import org.springframework.boot.Banner +import org.springframework.boot.builder.SpringApplicationBuilder +import org.springframework.boot.loader.JarLauncher +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.core.io.DefaultResourceLoader +import org.yaml.snakeyaml.Yaml +import java.net.URL +import java.net.URLClassLoader + +class SpringLoader( + private val javaPlugin: JavaPlugin, + private val classLoader: ClassLoader +) { + + private var context: ConfigurableApplicationContext? = null + private val logger by lazy { javaPlugin.logger } + + private val scanThreads by lazy { + Runtime.getRuntime().availableProcessors() + } + + fun onEnable() { + try { + initSpring() + } catch (exception: Exception) { + logger.warning("MCSpring Failed to load ${javaPlugin.name}") + throw exception + } + } + + private fun initSpring() { + val pluginYmlResource = javaPlugin.getResource("plugin.yml") ?: error("plugin.yml not found???") + @Suppress("UNCHECKED_CAST") + val yaml = Yaml().loadAs(pluginYmlResource, Map::class.java) as Map + val main = yaml["spring-boot-main"] as? String + ?: error("Spring boot main not found in plugin.yml") + + val config = Class.forName(main) + val builder = SpringApplicationBuilder() + var sources = arrayOf(config, SpringSpigotSupport::class.java) + + // TODO search +// if (SETUP_PLUGINS.isNotEmpty()) { +// val parent = SETUP_PLUGINS.entries.reduce { _, b -> b }.value +// builder.parent(parent.context) +// sources = sources.copyOfRange(0, 1) +// } + + // this is sad :( + val cl = Thread.currentThread().contextClassLoader + Thread.currentThread().contextClassLoader = classLoader + + context = builder.sources(*sources) + .resourceLoader(DefaultResourceLoader(classLoader)) + .bannerMode(Banner.Mode.OFF) + .properties("spigot.plugin=${javaPlugin.name}") + .logStartupInfo(true) + .run() + Thread.currentThread().contextClassLoader = cl + } + + fun onDisable() { + if (context != null) { + context!!.close() + context = null + } + } + + class McSpringLoader : JarLauncher() { + fun launch(classLoader: ClassLoader?) { + val activeArchives = classPathArchivesIterator + val addURL = URLClassLoader::class.java.getDeclaredMethod("addURL", URL::class.java) + addURL.isAccessible = true + activeArchives.forEach { addURL(classLoader, it.url) } + } + + override fun getMainClass() = "" + } +} diff --git a/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/SpringSpigotSupport.kt b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/SpringSpigotSupport.kt new file mode 100644 index 0000000..d2e7a65 --- /dev/null +++ b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/SpringSpigotSupport.kt @@ -0,0 +1,74 @@ +package `in`.kyle.mcspring + +import org.bukkit.Bukkit +import org.bukkit.Server +import org.bukkit.configuration.file.FileConfiguration +import org.bukkit.plugin.Plugin +import org.bukkit.plugin.PluginDescriptionFile +import org.bukkit.plugin.PluginLoader +import org.bukkit.plugin.PluginManager +import org.bukkit.plugin.messaging.Messenger +import org.bukkit.scheduler.BukkitScheduler +import org.bukkit.scoreboard.ScoreboardManager +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.Configuration +import org.springframework.scheduling.annotation.EnableScheduling +import java.util.logging.Logger + +@Configuration +@ComponentScan(basePackageClasses = [SpringSpigotSupport::class]) +@EnableScheduling +open class SpringSpigotSupport { + + @Bean + open fun plugin(@Value("\${spigot.plugin}") pluginName: String): Plugin { + return Bukkit.getPluginManager().getPlugin(pluginName)!! + } + + @Bean(destroyMethod = "") + open fun server(plugin: Plugin): Server { + return plugin.server + } + + @Bean + open fun logger(plugin: Plugin): Logger { + return plugin.logger + } + + @Bean + open fun pluginManager(server: Server): PluginManager { + return server.pluginManager + } + + @Bean + open fun scoreboardManager(server: Server): ScoreboardManager? { + return server.scoreboardManager + } + + @Bean + open fun messenger(server: Server): Messenger { + return server.messenger + } + + @Bean + open fun configuration(plugin: Plugin): FileConfiguration { + return plugin.config + } + + @Bean + open fun description(plugin: Plugin): PluginDescriptionFile { + return plugin.description + } + + @Bean + open fun scheduler(server: Server): BukkitScheduler { + return server.scheduler + } + + @Bean + open fun pluginLoader(plugin: Plugin): PluginLoader { + return plugin.pluginLoader + } +} diff --git a/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/annotation/PluginDepend.kt b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/annotation/PluginDepend.kt new file mode 100644 index 0000000..6c4296a --- /dev/null +++ b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/annotation/PluginDepend.kt @@ -0,0 +1,8 @@ +package `in`.kyle.mcspring.annotation + +/** + * Declares a dependency in the `plugin.yml` + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) +annotation class PluginDepend(vararg val plugins: String) diff --git a/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/annotation/SoftPluginDepend.kt b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/annotation/SoftPluginDepend.kt new file mode 100644 index 0000000..bb72823 --- /dev/null +++ b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/annotation/SoftPluginDepend.kt @@ -0,0 +1,8 @@ +package `in`.kyle.mcspring.annotation + +/** + * Declares a dependency in the `plugin.yml` + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.CLASS) +annotation class SoftPluginDepend(vararg val plugins: String) diff --git a/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/annotation/SpringPlugin.kt b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/annotation/SpringPlugin.kt new file mode 100644 index 0000000..d0211fa --- /dev/null +++ b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/annotation/SpringPlugin.kt @@ -0,0 +1,16 @@ +package `in`.kyle.mcspring.annotation + +import `in`.kyle.mcspring.SpringSpigotSupport +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.context.annotation.Import +import java.lang.annotation.Inherited + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +@Inherited +@SpringBootApplication +@Import(SpringSpigotSupport::class) +@EnableAutoConfiguration +annotation class SpringPlugin diff --git a/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/event/EventHandlerSupport.kt b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/event/EventHandlerSupport.kt new file mode 100644 index 0000000..8b4946a --- /dev/null +++ b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/event/EventHandlerSupport.kt @@ -0,0 +1,34 @@ +package `in`.kyle.mcspring.event + +import `in`.kyle.mcspring.util.SpringScanner +import org.bukkit.event.Event +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.plugin.EventExecutor +import org.bukkit.plugin.Plugin +import org.springframework.aop.support.AopUtils +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.context.ApplicationContext +import org.springframework.context.ApplicationContextAware +import org.springframework.stereotype.Component +import java.lang.reflect.Method + +@Component +internal class EventHandlerSupport( + private val eventService: EventService, + private val scanner: SpringScanner +) : ApplicationContextAware { + + override fun setApplicationContext(ctx: ApplicationContext) { + scanner.scanMethods(EventHandler::class.java).forEach { + val executor = makeExecutor(it.key, it.value) + eventService.registerEvent(it.key, executor) + } + } + + private fun makeExecutor(method: Method, obj: Any): EventExecutor { + return EventExecutor { _: Listener, event: Event -> + AopUtils.invokeJoinpointUsingReflection(obj, method, arrayOf(event)) + } + } +} diff --git a/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/event/EventService.kt b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/event/EventService.kt new file mode 100644 index 0000000..189e660 --- /dev/null +++ b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/event/EventService.kt @@ -0,0 +1,34 @@ +package `in`.kyle.mcspring.event + +import org.bukkit.Server +import org.bukkit.event.Event +import org.bukkit.event.EventHandler +import org.bukkit.event.Listener +import org.bukkit.plugin.EventExecutor +import org.bukkit.plugin.Plugin +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.context.annotation.Lazy +import org.springframework.stereotype.Service +import java.lang.reflect.Method + +@Lazy +@Service +internal class EventService( + private val server: Server, + private val plugin: Plugin +) { + fun registerEvent(method: Method, executor: EventExecutor) { + require(method.parameters.size == 1) {"Listener can only have 1 parameter: $method"} + val handler = method.getAnnotation(EventHandler::class.java) + + @Suppress("UNCHECKED_CAST") + val eventType = method.parameters[0].type as Class + server.pluginManager + .registerEvent(eventType, + object : Listener {}, + handler.priority, + executor, + plugin, + handler.ignoreCancelled) + } +} diff --git a/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/scheduler/ScheduledAnnotationSupport.kt b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/scheduler/ScheduledAnnotationSupport.kt new file mode 100644 index 0000000..5c82071 --- /dev/null +++ b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/scheduler/ScheduledAnnotationSupport.kt @@ -0,0 +1,50 @@ +package `in`.kyle.mcspring.scheduler + +import org.springframework.context.annotation.Lazy +import org.springframework.scheduling.Trigger +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler +import org.springframework.stereotype.Component +import java.util.* +import java.util.concurrent.ScheduledFuture + +@Lazy +@Component +internal class ScheduledAnnotationSupport( + private val scheduler: SchedulerService +) : ThreadPoolTaskScheduler() { + + override fun schedule(task: Runnable, trigger: Trigger): ScheduledFuture<*> { + @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + return super.schedule(wrapSync(task), trigger) + } + + override fun schedule(task: Runnable, startTime: Date): ScheduledFuture<*> { + return super.schedule(wrapSync(task), startTime) + } + + override fun scheduleAtFixedRate(task: Runnable, startTime: Date, period: Long): ScheduledFuture<*> { + return super.scheduleAtFixedRate(wrapSync(task), startTime, period) + } + + override fun scheduleAtFixedRate(task: Runnable, period: Long): ScheduledFuture<*> { + return super.scheduleAtFixedRate(wrapSync(task), period) + } + + override fun scheduleWithFixedDelay(task: Runnable, startTime: Date, delay: Long): ScheduledFuture<*> { + return super.scheduleWithFixedDelay(wrapSync(task), startTime, delay) + } + + override fun scheduleWithFixedDelay(task: Runnable, delay: Long): ScheduledFuture<*> { + return super.scheduleWithFixedDelay(wrapSync(task), delay) + } + + private fun wrapSync(task: Runnable): Runnable { + return WrappedRunnable(scheduler, task) + } + + data class WrappedRunnable(val scheduler: SchedulerService, val runnable: Runnable) : Runnable { + override fun run() { + scheduler.syncTask(runnable) + } + } +} diff --git a/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/scheduler/SchedulerService.kt b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/scheduler/SchedulerService.kt new file mode 100644 index 0000000..1d2b8b3 --- /dev/null +++ b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/scheduler/SchedulerService.kt @@ -0,0 +1,82 @@ +package `in`.kyle.mcspring.scheduler + +import org.bukkit.plugin.Plugin +import org.bukkit.scheduler.BukkitScheduler +import org.bukkit.scheduler.BukkitTask +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.context.annotation.Lazy +import org.springframework.stereotype.Service + +/** + * Convenience methods for Bukkit scheduling + * @see [BukkitScheduler] + */ +@Lazy +@Service +class SchedulerService( + private val scheduler: BukkitScheduler, + private val plugin: Plugin +) { + /** + * @see BukkitScheduler.runTaskAsynchronously + */ + fun asyncTask(task: Runnable): BukkitTask { + return scheduler.runTaskAsynchronously(plugin, task) + } + + /** + * @see BukkitScheduler.runTask + */ + fun syncTask(task: Runnable): BukkitTask { + return scheduler.runTask(plugin, task) + } + + /** + * @see BukkitScheduler.runTaskLaterAsynchronously + */ + fun asyncDelayedTask(task: Runnable, delayTicks: Long): BukkitTask { + return scheduler.runTaskLaterAsynchronously(plugin, task, delayTicks) + } + + /** + * @see BukkitScheduler.scheduleSyncDelayedTask + */ + fun syncDelayedTask(task: Runnable, delayTicks: Long): BukkitTask { + return scheduler.runTaskLater(plugin, task, delayTicks) + } + + /** + * @see BukkitScheduler.runTaskTimerAsynchronously + */ + fun asyncRepeatingTask(task: Runnable, delayTicks: Long, periodTicks: Long): BukkitTask { + return scheduler.runTaskTimerAsynchronously(plugin, task, delayTicks, periodTicks) + } + + /** + * @see BukkitScheduler.runTaskTimer + */ + fun syncRepeatingTask(task: Runnable, delayTicks: Long, periodTicks: Long): BukkitTask { + return scheduler.runTaskTimer(plugin, task, delayTicks, periodTicks) + } + + /** + * @see BukkitScheduler.cancelTask + */ + fun cancelTask(task: BukkitTask) { + scheduler.cancelTask(task.taskId) + } + + /** + * @see BukkitScheduler.isCurrentlyRunning + */ + fun isCurrentlyRunning(task: BukkitTask): Boolean { + return scheduler.isCurrentlyRunning(task.taskId) + } + + /** + * @see BukkitScheduler.isQueued + */ + fun isQueued(task: BukkitTask): Boolean { + return scheduler.isQueued(task.taskId) + } +} diff --git a/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/util/SpringScanner.kt b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/util/SpringScanner.kt new file mode 100644 index 0000000..5f4e152 --- /dev/null +++ b/mcspring-api/mcspring-base/src/main/kotlin/in/kyle/mcspring/util/SpringScanner.kt @@ -0,0 +1,31 @@ +package `in`.kyle.mcspring.util + +import org.springframework.aop.support.AopUtils +import org.springframework.context.ApplicationContext +import org.springframework.stereotype.Component +import java.lang.reflect.Method + +@Component +class SpringScanner(private val context: ApplicationContext) { + + fun scanMethods(vararg annotations: Class): Map { + val methods = mutableMapOf() + for (beanName in context.beanDefinitionNames) { + val obj = context.getBean(beanName) + for (method in getRealClass(obj).declaredMethods) { + if (annotations.any { method.isAnnotationPresent(it) }) { + methods[method] = obj + } + } + } + return methods + } + + private fun getRealClass(obj: Any): Class<*> { + return if (AopUtils.isAopProxy(obj.javaClass)) { + AopUtils.getTargetClass(obj) + } else { + obj.javaClass + } + } +} diff --git a/mcspring-api/mcspring-base/src/main/resources/META-INF/spring.factories b/mcspring-api/mcspring-base/src/main/resources/META-INF/spring.factories index 01adb48..9e737e5 100644 --- a/mcspring-api/mcspring-base/src/main/resources/META-INF/spring.factories +++ b/mcspring-api/mcspring-base/src/main/resources/META-INF/spring.factories @@ -1 +1 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=in.kyle.mcspring.test.SpringSpigotSupport +org.springframework.boot.autoconfigure.EnableAutoConfiguration=in.kyle.mcspring.SpringSpigotSupport diff --git a/mcspring-api/mcspring-chat-actions/README.md b/mcspring-api/mcspring-chat-actions/README.md new file mode 100644 index 0000000..14942dd --- /dev/null +++ b/mcspring-api/mcspring-chat-actions/README.md @@ -0,0 +1,15 @@ +mcspring-chat-actions +--- +Simplifies the process of adding interactable elements to chat messages. + +### Examples + +```kotlin +player.sendMessage { + "hover for details".red() hover { + ("click to run ".gray()) + ("/test".yellow()) + } command "/test" +} +``` + +_This may be used independently of mcspring_ diff --git a/mcspring-api/mcspring-chat-actions/build.gradle.kts b/mcspring-api/mcspring-chat-actions/build.gradle.kts new file mode 100644 index 0000000..eee9b8b --- /dev/null +++ b/mcspring-api/mcspring-chat-actions/build.gradle.kts @@ -0,0 +1,4 @@ +dependencies { + compile(project(":mcspring-api:mcspring-chat")) + compile(project(":mcspring-api:mcspring-rx")) +} diff --git a/mcspring-api/mcspring-chat-actions/src/main/kotlin/in/kyle/mcspring/chat/CommandActionListener.kt b/mcspring-api/mcspring-chat-actions/src/main/kotlin/in/kyle/mcspring/chat/CommandActionListener.kt new file mode 100644 index 0000000..905f7ae --- /dev/null +++ b/mcspring-api/mcspring-chat-actions/src/main/kotlin/in/kyle/mcspring/chat/CommandActionListener.kt @@ -0,0 +1,63 @@ +package `in`.kyle.mcspring.chat + +import `in`.kyle.mcspring.rx.observeEvent +import `in`.kyle.mcspring.rx.syncScheduler +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.subjects.PublishSubject +import org.bukkit.entity.Player +import org.bukkit.event.player.AsyncPlayerChatEvent +import org.bukkit.event.player.PlayerCommandPreprocessEvent +import org.bukkit.plugin.java.JavaPlugin +import java.util.* +import java.util.concurrent.TimeUnit + +object CommandActionListener { + + private val actions = mutableMapOf>() + private val ttls = mutableMapOf() + private var registered = false + + fun createCommand(): Pair> { + if (!registered) { + registerListener() + registered = true + } + prune() + + val command = UUID.randomUUID().toString() + val subject = PublishSubject.create() + + actions[command] = subject + ttls[command] = System.currentTimeMillis() + + return Pair(command, subject.doOnDispose { unregisterCommand(command) }) + } + + fun unregisterCommand(string: String) { + actions.remove(string)?.onComplete() + ttls.remove(string) + } + + private fun prune() { + val toRemove = ttls.filterValues { (System.currentTimeMillis() - it) > TimeUnit.DAYS.toMillis(1) } + toRemove.keys.forEach { + unregisterCommand(it) + } + } + + private fun registerListener() { + val plugin = JavaPlugin.getProvidingPlugin(CommandActionListener::class.java) + plugin.observeEvent(PlayerCommandPreprocessEvent::class) + .filter { it.message.startsWith("/") } + .subscribe { + val command = it.message.substring(1) + val subject = actions[command] + if (subject != null) { + it.isCancelled = true + plugin.syncScheduler().scheduleDirect { + subject.onNext(it.player) + } + } + } + } +} diff --git a/mcspring-api/mcspring-chat-actions/src/main/kotlin/in/kyle/mcspring/chat/Support.kt b/mcspring-api/mcspring-chat-actions/src/main/kotlin/in/kyle/mcspring/chat/Support.kt new file mode 100644 index 0000000..903e1c4 --- /dev/null +++ b/mcspring-api/mcspring-chat-actions/src/main/kotlin/in/kyle/mcspring/chat/Support.kt @@ -0,0 +1,30 @@ +package `in`.kyle.mcspring.chat + +import `in`.kyle.mcspring.chat.StringSupport.toTextComponent +import `in`.kyle.mcspring.chat.TextComponentSupport.command +import net.md_5.bungee.api.chat.TextComponent +import org.bukkit.entity.Player + +infix fun TextComponent.onClick(lambda: (Player) -> Unit): TextComponent { + val (command, subject) = CommandActionListener.createCommand() + subject.subscribe { lambda(it) } + return this command "/$command" +} + +infix fun TextComponent.onClickOnce(lambda: (Player) -> Unit): TextComponent { + val (command, subject) = CommandActionListener.createCommand() + subject.subscribe { + lambda(it) + CommandActionListener.unregisterCommand(command) + } + return this command "/$command" +} + +infix fun String.onClick(lambda: (Player) -> Unit): TextComponent { + return toTextComponent() onClick lambda +} + +infix fun String.onClickOnce(lambda: (Player) -> Unit): TextComponent { + return toTextComponent() onClickOnce lambda +} + diff --git a/mcspring-api/mcspring-chat/README.md b/mcspring-api/mcspring-chat/README.md new file mode 100644 index 0000000..b472dee --- /dev/null +++ b/mcspring-api/mcspring-chat/README.md @@ -0,0 +1,15 @@ +mcspring-chat +--- +Provides some basic wrappers around the TextComponent API + +### Examples + +```kotlin +player.sendMessage { + "hover for details".red() hover { + ("click to run ".gray()) + ("/test".yellow()) + } command "/test" +} +``` + +_This may be used independently of mcspring_ diff --git a/mcspring-api/mcspring-chat/build.gradle.kts b/mcspring-api/mcspring-chat/build.gradle.kts new file mode 100644 index 0000000..e69de29 diff --git a/mcspring-api/mcspring-chat/src/main/kotlin/in/kyle/mcspring/chat/ColorHelpers.kt b/mcspring-api/mcspring-chat/src/main/kotlin/in/kyle/mcspring/chat/ColorHelpers.kt new file mode 100644 index 0000000..7c6c8e2 --- /dev/null +++ b/mcspring-api/mcspring-chat/src/main/kotlin/in/kyle/mcspring/chat/ColorHelpers.kt @@ -0,0 +1,54 @@ +package `in`.kyle.mcspring.chat + +import `in`.kyle.mcspring.chat.StringSupport.color +import `in`.kyle.mcspring.chat.TextComponentSupport.color +import net.md_5.bungee.api.chat.TextComponent +import org.bukkit.ChatColor.* + +fun String.translateColorCodes() = translateAlternateColorCodes('&', this) + +fun String.black() = this color BLACK +fun String.darkBlue() = this color DARK_BLUE +fun String.darkGreen() = this color DARK_GREEN +fun String.darkAqua() = this color DARK_AQUA +fun String.darkRed() = this color DARK_RED +fun String.darkPurple() = this color DARK_PURPLE +fun String.gold() = this color GOLD +fun String.gray() = this color GRAY +fun String.darkGray() = this color DARK_GRAY +fun String.blue() = this color BLUE +fun String.green() = this color GREEN +fun String.aqua() = this color AQUA +fun String.red() = this color RED +fun String.lightPurple() = this color LIGHT_PURPLE +fun String.yellow() = this color YELLOW +fun String.white() = this color WHITE +fun String.magic() = this color MAGIC +fun String.bold() = this color BOLD +fun String.strikethrough() = this color STRIKETHROUGH +fun String.underline() = this color UNDERLINE +fun String.italic() = this color ITALIC +fun String.reset() = this color RESET + +fun TextComponent.black() = this color BLACK +fun TextComponent.darkBlue() = this color DARK_BLUE +fun TextComponent.darkGreen() = this color DARK_GREEN +fun TextComponent.darkAqua() = this color DARK_AQUA +fun TextComponent.darkRed() = this color DARK_RED +fun TextComponent.darkPurple() = this color DARK_PURPLE +fun TextComponent.gold() = this color GOLD +fun TextComponent.gray() = this color GRAY +fun TextComponent.darkGray() = this color DARK_GRAY +fun TextComponent.blue() = this color BLUE +fun TextComponent.green() = this color GREEN +fun TextComponent.aqua() = this color AQUA +fun TextComponent.red() = this color RED +fun TextComponent.lightPurple() = this color LIGHT_PURPLE +fun TextComponent.yellow() = this color YELLOW +fun TextComponent.white() = this color WHITE +fun TextComponent.magic() = this color MAGIC +fun TextComponent.bold() = this color BOLD +fun TextComponent.strikethrough() = this color STRIKETHROUGH +fun TextComponent.underline() = this color UNDERLINE +fun TextComponent.italic() = this color ITALIC +fun TextComponent.reset() = this color RESET diff --git a/mcspring-api/mcspring-chat/src/main/kotlin/in/kyle/mcspring/chat/Message.kt b/mcspring-api/mcspring-chat/src/main/kotlin/in/kyle/mcspring/chat/Message.kt new file mode 100644 index 0000000..e88f931 --- /dev/null +++ b/mcspring-api/mcspring-chat/src/main/kotlin/in/kyle/mcspring/chat/Message.kt @@ -0,0 +1,108 @@ +package `in`.kyle.mcspring.chat + +import `in`.kyle.mcspring.chat.CommandSenderSupport.sendMessage +import `in`.kyle.mcspring.chat.StringSupport.toTextComponent +import `in`.kyle.mcspring.chat.TextComponentSupport.color +import `in`.kyle.mcspring.chat.TextComponentSupport.command +import `in`.kyle.mcspring.chat.TextComponentSupport.hover +import `in`.kyle.mcspring.chat.TextComponentSupport.suggest +import net.md_5.bungee.api.chat.ClickEvent +import net.md_5.bungee.api.chat.HoverEvent +import net.md_5.bungee.api.chat.TextComponent +import org.bukkit.ChatColor +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player + +object PlayerSupport { + fun Player.sendMessage(vararg components: Any) = (this as CommandSender).sendMessage(*components) + fun Player.sendMessage(vararg components: TextComponent) = (this as CommandSender).sendMessage(*components) + fun Player.sendMessage(lambda: () -> TextComponent) = (this as CommandSender).sendMessage(lambda) +} + +object CommandSenderSupport { + fun CommandSender.sendMessage(vararg components: Any) { + sendMessage(*components.map(::convertComponent).toTypedArray()) + } + + private fun convertComponent(component: Any) = when (component) { + is TextComponent -> component + is String -> component.toTextComponent() + else -> error("Invalid component for message: ${component::class.simpleName}:$component. Only strings and TextComponents are supported") + } + + fun CommandSender.sendMessage(vararg components: TextComponent) { + spigot().sendMessage(*components) + } + + fun CommandSender.sendMessage(lambda: () -> TextComponent) { + spigot().sendMessage(lambda()) + } +} + +object StringSupport { + + fun String.toTextComponent(): TextComponent { + return TextComponent(translateColorCodes()) + } + + infix fun String.color(color: ChatColor): TextComponent { + return toTextComponent() color color + } + + infix fun String.hover(hover: String): TextComponent { + return toTextComponent() hover hover + } + + infix fun String.command(command: String): TextComponent { + return toTextComponent() command command + } + + infix fun String.suggest(command: String): TextComponent { + return toTextComponent() suggest command + } +} + +object TextComponentSupport { + infix fun TextComponent.color(color: ChatColor): TextComponent { + this.color = color.asBungee() + return TextComponent(this).apply { this.color = color.asBungee() } + } + + infix fun TextComponent.hover(hover: TextComponent): TextComponent { + return TextComponent(this).apply { + this.hoverEvent = HoverEvent(HoverEvent.Action.SHOW_TEXT, arrayOf(hover)) + } + } + + infix fun TextComponent.hover(lambda: () -> TextComponent): TextComponent { + return hover(lambda()) + } + + infix fun TextComponent.hover(hover: String): TextComponent { + return hover(TextComponent(hover)) + } + + infix fun TextComponent.command(command: String): TextComponent { + return TextComponent(this).apply { + this.clickEvent = ClickEvent(ClickEvent.Action.RUN_COMMAND, command) + } + } + + infix fun TextComponent.suggest(command: String): TextComponent { + return TextComponent(this).apply { + this.clickEvent = ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, command) + } + } + + operator fun TextComponent.plus(text: String): TextComponent { + return plus(TextComponent(text)) + } + + operator fun TextComponent.plus(text: TextComponent): TextComponent { + return TextComponent(this, text) + } + + operator fun TextComponent.plus(texts: Collection): TextComponent { + return TextComponent(this, *texts.toTypedArray()) + } +} diff --git a/mcspring-api/mcspring-chat/src/test/kotlin/in/kyle/mcspring/chat/TestMessage.kt b/mcspring-api/mcspring-chat/src/test/kotlin/in/kyle/mcspring/chat/TestMessage.kt new file mode 100644 index 0000000..59f1f39 --- /dev/null +++ b/mcspring-api/mcspring-chat/src/test/kotlin/in/kyle/mcspring/chat/TestMessage.kt @@ -0,0 +1,59 @@ +package `in`.kyle.mcspring.chat + +import `in`.kyle.mcspring.chat.StringSupport.color +import `in`.kyle.mcspring.chat.StringSupport.command +import `in`.kyle.mcspring.chat.StringSupport.hover +import `in`.kyle.mcspring.chat.StringSupport.suggest +import `in`.kyle.mcspring.chat.StringSupport.toTextComponent +import `in`.kyle.mcspring.chat.TextComponentSupport.plus +import io.kotest.matchers.shouldBe +import net.md_5.bungee.api.ChatColor +import net.md_5.bungee.api.chat.ClickEvent +import net.md_5.bungee.api.chat.ClickEvent.Action.* +import net.md_5.bungee.api.chat.ComponentBuilder +import net.md_5.bungee.api.chat.HoverEvent +import net.md_5.bungee.api.chat.HoverEvent.Action.SHOW_TEXT +import net.md_5.bungee.api.chat.TextComponent +import org.bukkit.ChatColor.RED +import org.junit.jupiter.api.Test + +class TestMessage { + + @Test + fun testHover() { + "test" hover "hover" shouldBe + TextComponent("test").apply { + hoverEvent = HoverEvent(SHOW_TEXT, arrayOf(TextComponent("hover"))) + } + } + + @Test + fun testColor() { + "test" color RED shouldBe + ComponentBuilder("test").color(ChatColor.RED).create()[0] + } + + @Test + fun testPlus() { + val component = "hello ".toTextComponent() + "world" + // ofc the equality checking doesn't work... + component.toString() shouldBe + TextComponent(TextComponent("hello "), TextComponent("world")).toString() + } + + @Test + fun testCommand() { + "test" command "/test" shouldBe + TextComponent("test").apply { + clickEvent = ClickEvent(RUN_COMMAND, "/test") + } + } + + @Test + fun testSuggest() { + "test" suggest "/test" shouldBe + TextComponent("test").apply { + clickEvent = ClickEvent(SUGGEST_COMMAND, "/test") + } + } +} diff --git a/mcspring-api/mcspring-commands-dsl/README.md b/mcspring-api/mcspring-commands-dsl/README.md new file mode 100644 index 0000000..c9df245 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/README.md @@ -0,0 +1,117 @@ +mcspring-commands-dsl +--- +_My hot take on command parsing, completely optional._ + +### Project Goals +One of the worst parts of writing plugins is writing command parsers. + +One of the primary problems when writing parsers as a conditional tree is the tendency of depth + spiraling out of control. What could be a simple command could turn into a nested cataclysm of conditional branching. + +Here's some real code that I wrote not too long ago. The problems here are not worth listing out. +```kotlin +if (party.hasAdminPermissions(player)) { + if (args.size == 2) { // party size + val size = args[1].toIntOrNull() + if (size != null) { + if (size <= 12) { + if (size >= party.members.size) { + party.size = size + showPartyInfo(player, party) + } else { + player.sendMessage("Your party has too many members for that size. You must kick some players first. ($size < ${party.members.size}") + } + } else { + player.sendMessage("Max party size is 12") + } + } else { + player.sendMessage("Invalid party size: ${args[1]}") + } + } else { + player.sendMessage("Usage: /$label size ") + } +} else { + player.sendMessage("You must be a party admin to do this") +} +``` + +Flattening this mess is easy with some non-standard control flow. Next is to clean up the + redundant mess of if-return blocks. I started by commenting the type of task being performed by + each block. This will be useful later. + +```kotlin +// Condition +if (!party.hasAdminPermissions(player)) { + player.sendMessage("You must be a party admin to do this") + return +} + +// Usage message +if (args.size != 2) { // party size + player.sendMessage("Usage: /$label size ") + return +} + +// Argument parser +val size = args[1].toIntOrNull() +if (size == null) { + player.sendMessage("Invalid party size: ${args[1]}") + return +} + +// Argument condition +if (size > 12) { // TODO config + player.sendMessage("Max party size is 12") + return +} + +// Argument condition +if (size < party.members.size) { + player.sendMessage("Your party has too many members for that size. You must kick some players first. ($size < ${party.members.size}") + return +} + +// Command execution +party.size = size +showPartyInfo(player, party) +``` + +These statements are often highly repetitive in nature. Abstracting out the general concepts to a + DSL is more-or-less straightforward: + +```kotlin +require(party.hasAdminPermissions(sender as Player)) { + // execution + message("You must be a party admin to do this") + // Dead end +} + +val size by intArg { + parser { + lessThanEqual(12) { + // execution + message("Invalid party size ${args[1]}") + // Dead end + } + lessThanEqual(party.members.size) { + // execution + message("Your party has too many members for that size. You must kick some players first. (${args[1]} < ${party.members.size}") + // Dead end + } + } +} + +then { + // execution + party.size = size + showPartyInfo(player, party) +} +``` + +This approach has some neat advantages over a traditional command structure. First, there is a + clear structure to how the command is parsed. Second, this format natively lends itself to + automatic tab completion. That means that any command written in this DSL format is natively + tab-completable. + +Tab completions are handled by running the command DSL but ignoring execution sections (see + comments). diff --git a/mcspring-api/mcspring-commands-dsl/build.gradle.kts b/mcspring-api/mcspring-commands-dsl/build.gradle.kts new file mode 100644 index 0000000..df14890 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/build.gradle.kts @@ -0,0 +1,4 @@ +dependencies { + compileOnly(project(":mcspring-api:mcspring-base")) + testImplementation(project(":mcspring-api:mcspring-base")) +} diff --git a/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/CommandBuilder.kt b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/CommandBuilder.kt new file mode 100644 index 0000000..754e302 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/CommandBuilder.kt @@ -0,0 +1,219 @@ +package `in`.kyle.mcspring.commands.dsl + +import `in`.kyle.mcspring.commands.dsl.parsers.* +import `in`.kyle.mcspring.commands.dsl.parsers.numbers.DoubleParser +import `in`.kyle.mcspring.commands.dsl.parsers.numbers.IntParser +import org.bukkit.command.CommandSender +import org.bukkit.command.ConsoleCommandSender +import org.bukkit.entity.Player + +@DslMarker +annotation class CommandParserBuilder + +class CommandMeta { + lateinit var name: String + var description: String = "" + var usageMessage: String = "" + var permission: String = "" + var permissionMessage: String = "" + var aliases: List = listOf() + lateinit var executor: CommandExecutor + + fun validate() { + require(this::name.isInitialized) { "Command name not set" } + require(this::executor.isInitialized) { "Command executor not set for command $name" } + } +} + +data class CommandContext( + val sender: CommandSender, + val label: String, + val args: List, + val tabCompletions: MutableList = mutableListOf(), + internal var argIndex: Int = 0, + internal val runExecutors: Boolean = true +) { + val nextArg: String? + get() = args.getOrNull(argIndex) +} + +data class CommandExecutor(val provider: (CommandContext) -> ParsedCommand) + +@CommandParserBuilder +class CommandBuilder(context: CommandContext) : ContextReciever(context) { + + private val parsedArgs: MutableList> = mutableListOf() + + fun stringArg(lambda: ValueBuilder.() -> Unit = {}) = valueArg(lambda) { StringParser(context, it) } + fun intArg(lambda: ValueBuilder.() -> Unit) = valueArg(lambda) { IntParser(context, it) } + fun doubleArg(lambda: ValueBuilder.() -> Unit) = valueArg(lambda) { DoubleParser(context, it) } + fun booleanArg(lambda: ValueBuilder.() -> Unit) = valueArg(lambda) { BooleanParser(context, it) } + fun playerArg(lambda: ValueBuilder.() -> Unit = {}) = valueArg(lambda) { PlayerParser(context, it) } + + fun mapArg(lambda: ValueBuilder>.() -> Unit) = valueArg(lambda) { MapParser(context, it) } + + fun subcommand(lambda: SubcommandBuilder.() -> Unit) { + arg(SubcommandBuilder(context), lambda) + } + + fun require(predicate: () -> Boolean, lambda: ContextReciever.() -> Unit = {}) = require(predicate(), lambda) + + fun require(value: Boolean, lambda: ContextReciever.() -> Unit) { + if (!value) { + lambda(this) + commandMissing() + } + } + + fun requirePlayer(lambda: ContextReciever.() -> Unit) = require({ sender is Player }, lambda) + + fun requireConsole(lambda: ContextReciever.() -> Unit) = require({ sender is ConsoleCommandSender }, lambda) + + private fun > valueArg( + lambda: ValueBuilder.() -> Unit, + parserSupplier: (String?) -> P + ) = arg(ValueBuilder(context, parserSupplier), lambda) + + private fun > arg(builder: T, lambda: T.() -> Unit): R { + context.tabCompletions.clear() + val arg = builder.apply(lambda).build() + parsedArgs.add(arg) + context.argIndex++ + if (arg.returnValue == null) { + commandMissing() + } + return arg.returnValue + } + + fun then(lambda: ContextReciever.() -> Unit) { + if (context.runExecutors) { + lambda(this) + } + commandComplete() + } + + fun build(): ParsedCommand = ParsedCommand(parsedArgs) +} + +@CommandParserBuilder +abstract class ArgBuilder( + context: CommandContext +) : ContextReciever(context) { + + internal open var returnValue: R? = null + + open fun invalid(lambda: ContextReciever.(arg: String) -> Unit) { + val nextArg = context.nextArg + if (returnValue == null && nextArg != null) { + if (context.runExecutors) { + lambda(nextArg) + } + commandInvalid() + } + } + + abstract fun build(): ValueArg +} + +class ValueBuilder>( + context: CommandContext, + private val parserSupplier: (String?) -> T +) : ArgBuilder(context) { + + private var hasRunParser = false + + fun default(lambda: ContextReciever.() -> R?) { + if (returnValue == null) { + returnValue = lambda() + } + } + + fun parser(lambda: T.() -> Unit) { + hasRunParser = true + if (returnValue == null) { + val parser = parserSupplier(context.nextArg) + parser.lambda() + returnValue = parser.returnValue + } + } + + fun missing(lambda: ContextReciever.() -> Unit) { + if (!hasNextArg()) { + if (context.runExecutors) { + lambda() + } + commandMissing() + } + } + + override fun invalid(lambda: ContextReciever.(arg: String) -> Unit) { + runBaseParse() + super.invalid(lambda) + } + + private fun runBaseParse() { + if (!hasRunParser) { + parser {} + } + } + + override fun build(): ValueArg { + runBaseParse() + val temp = returnValue + if (temp != null) { + return ValueArg(temp) + } else { + error(""" + Error parsing $context, make sure to implement the following in your DSL: + missing - run when the argument is missing. E.g.: Missing the argument in /tp + invalid - run when the argument is invalid. E.g.: "askdf" being passed for an int arg. + """.trimIndent()) + } + } +} + +class SubcommandBuilder(context: CommandContext) : ArgBuilder(context) { + + private val subCommands: MutableMap, ParsedCommand> = mutableMapOf() + + fun on(vararg values: String, commandExecutor: CommandExecutor) = on({ it in values }, values.toList(), commandExecutor) + fun on(vararg values: String, command: CommandBuilder.() -> Unit) { + on({ it in values }, values.toList(), command(command)) + } + + fun on( + predicate: (String) -> Boolean, + tabCompletions: Iterable, + commandExecutor: CommandExecutor + ) { + context.tabCompletions.addAll(tabCompletions) + val argString = context.nextArg ?: return + + if (predicate(argString)) { + returnValue = argString + context.argIndex++ + subCommands[tabCompletions.toList()] = commandExecutor.provider(context) + commandComplete() + } else { + val fakeContext = context.copy(runExecutors = false) + val parsed = commandExecutor.provider(fakeContext) + subCommands[tabCompletions.toList()] = parsed + } + } + + fun missing(lambda: SubcommandMissingBuilder.() -> Unit) { + if (!hasNextArg()) { + if (context.runExecutors) { + lambda(SubcommandMissingBuilder(subCommands, context)) + } + commandComplete() + } + } + + override fun build() = SubCommandArg(subCommands, returnValue!!) + + class SubcommandMissingBuilder( + val subCommands: Map, ParsedCommand>, + context: CommandContext + ) : ContextReciever(context) +} diff --git a/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/CommandFacade.kt b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/CommandFacade.kt new file mode 100644 index 0000000..7b2e09f --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/CommandFacade.kt @@ -0,0 +1,17 @@ +package `in`.kyle.mcspring.commands.dsl + +fun commandMeta(lambda: CommandMeta.() -> Unit) = CommandMeta().apply { lambda(this) } + + +infix fun CommandMeta.commandExecutor(lambda: CommandBuilder.() -> Unit): CommandMeta { + executor = command(lambda) + return this +} + +fun command(lambda: CommandBuilder.() -> Unit) = + CommandExecutor { context: CommandContext -> + CommandBuilder(context).let { + it.lambda() + it.build() + } + } diff --git a/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/CommandModel.kt b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/CommandModel.kt new file mode 100644 index 0000000..df08d90 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/CommandModel.kt @@ -0,0 +1,53 @@ +package `in`.kyle.mcspring.commands.dsl + +import org.bukkit.ChatColor + +open class ContextReciever(val context: CommandContext) { + val sender = context.sender + val label = context.label + val args = context.args + + fun message(message: String) { + if (context.runExecutors) { + sender.sendMessage(ChatColor.translateAlternateColorCodes('&', message)) + } + } + + fun tabCompletions(vararg strings: String) = context.tabCompletions.addAll(strings) + fun tabCompletions(strings: List) = context.tabCompletions.addAll(strings) + + fun hasNextArg() = context.argIndex < context.args.size + + fun commandMissing() { + throw BreakParseException.ParseMissingException() + } + + fun commandInvalid() { + throw BreakParseException.ParseInvalidException() + } + + fun commandComplete() { + throw BreakParseException.ParseCompletedException() + } + + sealed class BreakParseException : RuntimeException() { + class ParseCompletedException : BreakParseException() + class ParseMissingException : BreakParseException() + class ParseInvalidException : BreakParseException() + } +} + +data class ParsedCommand( + val args: List> +) + +open class ValueArg( + val returnValue: R +) + +class SubCommandArg( + val subCommands: Map, ParsedCommand>, + returnValue: String +): ValueArg(returnValue) + + diff --git a/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/mcspring/Command.kt b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/mcspring/Command.kt new file mode 100644 index 0000000..d87c318 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/mcspring/Command.kt @@ -0,0 +1,16 @@ +package `in`.kyle.mcspring.commands.dsl.mcspring + +/** + * Defines a command to be handled by a certain method. + * Values from this annotation will be reflected in the `plugin.yml`. + */ +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +annotation class Command( + val value: String, + val aliases: Array = [], + val description: String = "", + val usage: String = "", + val permission: String = "", + val permissionMessage: String = "" +) diff --git a/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/mcspring/CommandDslRegistration.kt b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/mcspring/CommandDslRegistration.kt new file mode 100644 index 0000000..21485b4 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/mcspring/CommandDslRegistration.kt @@ -0,0 +1,58 @@ +package `in`.kyle.mcspring.commands.dsl.mcspring + +import `in`.kyle.mcspring.commands.dsl.CommandExecutor +import `in`.kyle.mcspring.commands.dsl.CommandMeta +import `in`.kyle.mcspring.commands.dsl.util.CommandUtils +import `in`.kyle.mcspring.commands.dsl.util.CommandMapWrapper +import `in`.kyle.mcspring.util.SpringScanner +import org.bukkit.plugin.Plugin +import org.springframework.context.ApplicationContext +import org.springframework.context.ApplicationContextAware +import org.springframework.context.annotation.Bean +import org.springframework.stereotype.Component +import java.lang.reflect.Method + +@Component +class CommandDslRegistration( + private val plugin: Plugin, + private val scanner: SpringScanner +) : ApplicationContextAware { + + private val registeredCommands: MutableSet = mutableSetOf() + + override fun setApplicationContext(ctx: ApplicationContext) { + registerAnnotatedCommands() + registerMetaCommands() + } + + private fun registerAnnotatedCommands() { + scanner.scanMethods(Command::class.java) + .filterKeys { it !in registeredCommands } + .filterKeys { it.returnType == CommandExecutor::class.java } + .forEach { (key, value) -> + val command = key.getAnnotation(Command::class.java) + val meta = CommandMeta().apply { + name = command.value + description = command.description + usageMessage = command.usage + permission = command.permission + permissionMessage = command.permissionMessage + aliases = command.aliases.toMutableList() + executor = key.invoke(value) as CommandExecutor + } + CommandUtils.register(plugin, meta) + } + } + + private fun registerMetaCommands() { + scanner.scanMethods().filterKeys { it !in registeredCommands } + .filterKeys { it.returnType == CommandMeta::class.java } + .forEach { (key, value) -> + val meta = key.invoke(value) as CommandMeta + CommandUtils.register(plugin, meta) + } + } + + @Bean + fun commandMap() = CommandMapWrapper.commandMap +} diff --git a/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/BaseParser.kt b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/BaseParser.kt new file mode 100644 index 0000000..fbc13f3 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/BaseParser.kt @@ -0,0 +1,44 @@ +package `in`.kyle.mcspring.commands.dsl.parsers + +import `in`.kyle.mcspring.commands.dsl.CommandContext +import `in`.kyle.mcspring.commands.dsl.CommandParserBuilder +import `in`.kyle.mcspring.commands.dsl.ContextReciever + +class RunInvalidBlock : RuntimeException() + +typealias ConditionFail = (String) -> Unit + +@CommandParserBuilder +abstract class BaseParser( + context: CommandContext, + private val stringArg: String? +) : ContextReciever(context) { + + internal var returnValue: R? = null + + protected fun mapped(hints: Iterable = emptyList(), mapper: (String) -> R?) { + context.tabCompletions.addAll(hints) + if (returnValue == null) { + returnValue = stringArg?.let { mapper(it) } + } + } + + protected fun require( + hints: List = emptyList(), + predicate: (R) -> Boolean, + conditionFail: ConditionFail = {} + ) { + context.tabCompletions.addAll(hints) + val condition = predicate(returnValue!!) + if (returnValue != null && !condition) { + if (conditionFail == {}) { + returnValue = null + throw RunInvalidBlock() + } else if (context.runExecutors) { + conditionFail(stringArg!!) + commandInvalid() + } + } + } +} + diff --git a/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/BooleanParser.kt b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/BooleanParser.kt new file mode 100644 index 0000000..53fcc19 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/BooleanParser.kt @@ -0,0 +1,16 @@ +package `in`.kyle.mcspring.commands.dsl.parsers + +import `in`.kyle.mcspring.commands.dsl.CommandContext + + +class BooleanParser(context: CommandContext, stringArg: String?) : BaseParser(context, stringArg) { + init { + val booleans = listOf("true", "false") + context.tabCompletions.addAll(booleans) + + val lowered = stringArg?.toLowerCase() + if (lowered in booleans) { + returnValue = lowered!!.toBoolean() + } + } +} diff --git a/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/MapParser.kt b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/MapParser.kt new file mode 100644 index 0000000..867cb10 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/MapParser.kt @@ -0,0 +1,11 @@ +package `in`.kyle.mcspring.commands.dsl.parsers + +import `in`.kyle.mcspring.commands.dsl.CommandContext + +class MapParser(context: CommandContext, stringArg: String?) : BaseParser(context, stringArg) { + + fun map(vararg pairs: Pair) = map(mapOf(*pairs)) + fun map(map: Map) = mapped(map.keys) { map[it] } + fun map(mapper: (String) -> R?) = mapped(mapper = mapper) + +} diff --git a/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/PlayerParser.kt b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/PlayerParser.kt new file mode 100644 index 0000000..502a2c4 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/PlayerParser.kt @@ -0,0 +1,18 @@ +package `in`.kyle.mcspring.commands.dsl.parsers + +import `in`.kyle.mcspring.commands.dsl.CommandContext +import org.bukkit.Bukkit +import org.bukkit.entity.Player + +class PlayerParser( + context: CommandContext, + stringArg: String? +) : BaseParser(context, stringArg) { + init { + val players = Bukkit.getOnlinePlayers() + context.tabCompletions.addAll(players.map { it.name }) + if (stringArg != null) { + returnValue = Bukkit.getPlayer(stringArg) + } + } +} diff --git a/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/StringParser.kt b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/StringParser.kt new file mode 100644 index 0000000..89c8142 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/StringParser.kt @@ -0,0 +1,20 @@ +package `in`.kyle.mcspring.commands.dsl.parsers + +import `in`.kyle.mcspring.commands.dsl.CommandContext + +class StringParser(context: CommandContext, stringArg: String?) : BaseParser(context, stringArg) { + init { + returnValue = stringArg + } + + fun anyOf(vararg string: String, conditionFail: ConditionFail) = anyOf(string.toList(), conditionFail) + fun anyOf(strings: List, conditionFail: ConditionFail) = require(strings, { it in strings }, conditionFail) + fun anyString() = mapped { it } + fun anyStringMatching(regex: Regex, conditionFail: ConditionFail) = require(predicate = { it.matches(regex) }, conditionFail = conditionFail) + fun anyInt(conditionFail: ConditionFail) = require(predicate = { it.toIntOrNull() != null }, conditionFail = conditionFail) + fun anyDouble(conditionFail: ConditionFail) = require(predicate = { it.toDoubleOrNull() != null }, conditionFail = conditionFail) + fun anyFloat(conditionFail: ConditionFail) = require(predicate = { it.toFloatOrNull() != null }, conditionFail = conditionFail) + fun anyByte(conditionFail: ConditionFail) = require(predicate = { it.toByteOrNull() != null }, conditionFail = conditionFail) + fun anyShort(conditionFail: ConditionFail) = require(predicate = { it.toShortOrNull() != null }, conditionFail = conditionFail) + fun anyBoolean(conditionFail: ConditionFail) = anyOf("true", "false", conditionFail = conditionFail) +} diff --git a/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/numbers/DoubleParser.kt b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/numbers/DoubleParser.kt new file mode 100644 index 0000000..2006ad1 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/numbers/DoubleParser.kt @@ -0,0 +1,9 @@ +package `in`.kyle.mcspring.commands.dsl.parsers.numbers + +import `in`.kyle.mcspring.commands.dsl.CommandContext + +class DoubleParser(context: CommandContext, stringArg: String?) : NumberParser(context, stringArg) { + override fun zero() = 0.0 + override fun toNumber(s: String): Double? = s.toDoubleOrNull() + override fun parse(s: String) = s.toDoubleOrNull() +} diff --git a/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/numbers/IntParser.kt b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/numbers/IntParser.kt new file mode 100644 index 0000000..9c743cb --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/numbers/IntParser.kt @@ -0,0 +1,13 @@ +package `in`.kyle.mcspring.commands.dsl.parsers.numbers + +import `in`.kyle.mcspring.commands.dsl.CommandContext +import `in`.kyle.mcspring.commands.dsl.parsers.ConditionFail + +class IntParser(context: CommandContext, stringArg: String?) : NumberParser(context, stringArg) { + override fun zero() = 0 + override fun toNumber(s: String): Int? = s.toIntOrNull() + fun even(conditionFail: ConditionFail) = require(predicate = { it % 2 == 0 && it != 0 }, conditionFail = conditionFail) + fun odd(conditionFail: ConditionFail) = require(predicate = { it % 2 == 1 && it != 0 }, conditionFail = conditionFail) + fun between(range: IntRange, conditionFail: ConditionFail) = require(predicate = { it in range }, conditionFail = conditionFail) + override fun parse(s: String) = s.toIntOrNull() +} diff --git a/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/numbers/NumberParser.kt b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/numbers/NumberParser.kt new file mode 100644 index 0000000..8e1477d --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/parsers/numbers/NumberParser.kt @@ -0,0 +1,27 @@ +package `in`.kyle.mcspring.commands.dsl.parsers.numbers + +import `in`.kyle.mcspring.commands.dsl.parsers.BaseParser +import `in`.kyle.mcspring.commands.dsl.CommandContext +import `in`.kyle.mcspring.commands.dsl.parsers.ConditionFail + +abstract class NumberParser(context: CommandContext, stringArg: String?) : BaseParser(context, stringArg) + where T : Number, + T : Comparable { + + init { + returnValue = if (stringArg != null) this.parse(stringArg) else null + } + + internal abstract fun toNumber(s: String): T? + internal abstract fun zero(): T + internal abstract fun parse(s: String): T? + + fun between(lower: T, upper: T, conditionFail: ConditionFail) = require(predicate = { it > lower && it < upper }, conditionFail = conditionFail) + fun greaterThan(value: T, conditionFail: ConditionFail) = require(predicate = { it > value }, conditionFail = conditionFail) + fun greaterThanEqual(value: T, conditionFail: ConditionFail) = require(predicate = { it >= value }, conditionFail = conditionFail) + fun lessThan(value: T, conditionFail: ConditionFail) = require(predicate = { it < value }, conditionFail = conditionFail) + fun lessThanEqual(value: T, conditionFail: ConditionFail) = require(predicate = { it <= value }, conditionFail = conditionFail) + fun positive(conditionFail: ConditionFail) = require(predicate = { it > zero() }, conditionFail = conditionFail) + fun negative(conditionFail: ConditionFail) = require(predicate = { it < zero() }, conditionFail = conditionFail) + fun nonZero(conditionFail: ConditionFail) = require(predicate = { it != zero() }, conditionFail = conditionFail) +} diff --git a/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/util/CommandMapWrapper.kt b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/util/CommandMapWrapper.kt new file mode 100644 index 0000000..6853273 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/util/CommandMapWrapper.kt @@ -0,0 +1,47 @@ +package `in`.kyle.mcspring.commands.dsl.util + +import org.bukkit.Bukkit +import org.bukkit.command.Command +import org.bukkit.command.CommandExecutor +import org.bukkit.command.CommandMap +import org.bukkit.command.TabCompleter +import org.bukkit.plugin.Plugin +import org.bukkit.plugin.java.JavaPlugin +import org.slf4j.LoggerFactory + +object CommandMapWrapper { + + val logger by lazy { LoggerFactory.getLogger(CommandMapWrapper::class.java) } + + val commandMap by lazy { + val bukkitCommandMap = Bukkit.getServer().javaClass.getDeclaredField("commandMap") + bukkitCommandMap.isAccessible = true + bukkitCommandMap[Bukkit.getServer()] as CommandMap + } + + fun registerCommand(plugin: Plugin, command: Command) { + require(plugin is JavaPlugin) { "Plugin must be an instance of JavaPlugin to register commands" } + val pluginCommand = plugin.getCommand(command.name) + if (pluginCommand != null) { + pluginCommand.apply { + val (executor, tabCompleter) = delegateTabsAndCommands(command) + setExecutor(executor) + setTabCompleter(tabCompleter) + } + } else { + val registered = commandMap.register(command.label, plugin.name, command) + if (!registered) { + logger.warn("""Could not register ${command.label} for ${plugin.name}. + There is a command label conflict. + Using aliases instead.""".trimIndent()) + } + } + } + + private fun delegateTabsAndCommands(command: Command): Pair { + return Pair( + CommandExecutor { sender, _, label, args -> command.execute(sender, label, args) }, + TabCompleter { sender, _, alias, args -> command.tabComplete(sender, alias, args) } + ) + } +} diff --git a/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/util/CommandUtils.kt b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/util/CommandUtils.kt new file mode 100644 index 0000000..6b612b1 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/main/kotlin/in/kyle/mcspring/commands/dsl/util/CommandUtils.kt @@ -0,0 +1,87 @@ +package `in`.kyle.mcspring.commands.dsl.util + +import `in`.kyle.mcspring.commands.dsl.CommandContext +import `in`.kyle.mcspring.commands.dsl.CommandMeta +import `in`.kyle.mcspring.commands.dsl.ContextReciever +import `in`.kyle.mcspring.commands.dsl.ContextReciever.* +import `in`.kyle.mcspring.commands.dsl.ContextReciever.BreakParseException.* +import `in`.kyle.mcspring.commands.dsl.ParsedCommand +import org.bukkit.command.Command +import org.bukkit.command.CommandException +import org.bukkit.command.CommandSender +import org.bukkit.plugin.Plugin + +class NoValidExecutorException(string: String) : CommandException(string) + +object CommandUtils { + + private val noValidExecutorException = NoValidExecutorException(""" + It appears that this command executed without hitting a terminal block. + A terminal block is an endpoint for a command. Such would be `missing`, `invalid`, + or `then`. Make sure your command executes one of these blocks. + """.trimIndent()) + + fun register(plugin: Plugin, meta: CommandMeta) { + meta.validate() + CommandMapWrapper.registerCommand(plugin, makeBukkitCommand(meta).apply { + aliases = meta.aliases + description = meta.description + usage = meta.usageMessage + meta.permission.takeIf { it.isNotBlank() } + ?.run { permission = meta.permission } + meta.permissionMessage.takeIf { it.isNotBlank() } + ?.run { permissionMessage = meta.permissionMessage } + }) + } + + fun runCommand(context: CommandContext, provider: (CommandContext) -> ParsedCommand) { + try { + provider(context) + throw noValidExecutorException + } catch (_: BreakParseException) { + } + } + + fun getCompletions(context: CommandContext, provider: (CommandContext) -> ParsedCommand): List { + val completions = try { + provider(context) + throw noValidExecutorException + } catch (e: BreakParseException) { + when (e) { + is ParseCompletedException -> listOf() + else -> context.tabCompletions + } + } + + // make sure this is the last arg + return if (context.argIndex+1 == context.args.size || context.args.isEmpty()) { + val arg = context.nextArg ?: "" + completions.filter { it.toLowerCase().startsWith(arg.toLowerCase()) } + } else { + emptyList() + } + } + + private fun makeBukkitCommand(meta: CommandMeta) = object : Command(meta.name) { + override fun execute( + sender: CommandSender, + commandLabel: String, + args: Array + ): Boolean { + val context = CommandContext(sender, label, args.toList()) + val provider = meta.executor.provider + runCommand(context, provider) + return true + } + + override fun tabComplete( + sender: CommandSender, + alias: String, + args: Array + ): List { + val provider = meta.executor.provider + val context = CommandContext(sender, label, args.toList(), runExecutors = false) + return getCompletions(context, provider) + } + } +} diff --git a/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/CommandTestSupport.kt b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/CommandTestSupport.kt new file mode 100644 index 0000000..12357f6 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/CommandTestSupport.kt @@ -0,0 +1,51 @@ +package `in`.kyle.mcspring.commands.dsl + +import `in`.kyle.mcspring.commands.dsl.util.CommandUtils +import org.bukkit.entity.Player +import org.mockito.ArgumentMatchers +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.mock + +class TestException : RuntimeException() + +object CommandTestSupport { + + fun runCommand(command: String, lambda: CommandBuilder.() -> Unit): String { + val (context, messages) = makeContext(command) + val provider = command(lambda).provider + CommandUtils.runCommand(context, provider) + return messages.joinToString("\n") + } + + fun getCompletions(command: String, lambda: CommandBuilder.() -> Unit): List { + val (context, _) = makeContext(command) + val provider = command(lambda).provider + return CommandUtils.getCompletions(context, provider) + } + + fun makeContext( + command: String, + runExecutors: Boolean = true + ): Pair> { + val (player, messages) = makeTestPlayer() + val args = command.split(" ").filter { it.isNotBlank() } + val context = CommandContext( + player, + args.getOrElse(0) { "" }, + args.toMutableList(), + runExecutors = runExecutors + ) + return Pair(context, messages) + } + + private fun makeTestPlayer(): Pair> { + val sender = mock(Player::class.java) + val messages = mutableListOf() + doAnswer { messages.add(it.getArgument(0)); null } + .`when`(sender) + .sendMessage(ArgumentMatchers.anyString()) + return Pair(sender, messages) + } + + fun makeBuilder(command: String) = CommandBuilder(makeContext(command).first) +} diff --git a/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/TestArgBuilder.kt b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/TestArgBuilder.kt new file mode 100644 index 0000000..4802426 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/TestArgBuilder.kt @@ -0,0 +1,36 @@ +package `in`.kyle.mcspring.commands.dsl + +import `in`.kyle.mcspring.commands.dsl.CommandTestSupport.makeContext +import io.kotest.assertions.fail +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FreeSpec + +class TestArgBuilder : FreeSpec({ + + class Test(context: CommandContext) : ArgBuilder(context) { + override fun build(): ValueArg { + return null as ValueArg + } + } + + "invalid block should run properly" - { + "invalid block runs when invalid" - { + val test = Test(makeContext("test").first) + test.returnValue = null + + shouldThrow { + test.invalid { + throw TestException() + } + } + } + + "invalid block does not run when valid" - { + val test = Test(makeContext("test").first) + test.returnValue = "" + test.invalid { + fail("should not run") + } + } + } +}) diff --git a/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/TestCommandBuilder.kt b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/TestCommandBuilder.kt new file mode 100644 index 0000000..1a7b1a1 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/TestCommandBuilder.kt @@ -0,0 +1,59 @@ +package `in`.kyle.mcspring.commands.dsl + +import `in`.kyle.mcspring.commands.dsl.CommandTestSupport.runCommand +import io.kotest.assertions.fail +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import io.mockk.mockk + +class TestCommandBuilder : FreeSpec({ + + "require should work" - { + "require(true) should not fail" - { + runCommand("test") { + require({ true }) { fail("should not run") } + commandComplete() + } + } + + "require(false) should fail" - { + runCommand("test") { + require({ false }) { message("should run") } + } shouldBe "should run" + } + } + + "missing arg should work" - { + shouldThrow { + runCommand("") { + stringArg { + missing { + throw TestException() + } + } + } + } + } + + "then should only run while executors are enabled" - { + "test disabled" - { + shouldThrow { + CommandBuilder(CommandContext(mockk(), "test", listOf(), mutableListOf(), runExecutors = false)).apply { + then { + fail("should not run") + } + } + } + } + "test enabled" - { + shouldThrow { + runCommand("test") { + then { + throw TestException() + } + } + } + } + } +}) diff --git a/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/TestSubcommandBuilder.kt b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/TestSubcommandBuilder.kt new file mode 100644 index 0000000..2879cb2 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/TestSubcommandBuilder.kt @@ -0,0 +1,50 @@ +package `in`.kyle.mcspring.commands.dsl + +import `in`.kyle.mcspring.commands.dsl.CommandTestSupport.makeContext +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe + +class TestSubcommandBuilder : FreeSpec({ + + "should run missing block" - { + val context = makeContext("").first + val builder = SubcommandBuilder(context) + + shouldThrow { + builder.apply { + on("test") {} + missing { + throw TestException() + } + } + } + } + + "should run invalid block" - { + val context = makeContext("arg1").first + val builder = SubcommandBuilder(context) + + shouldThrow { + builder.apply { + invalid { + throw TestException() + } + } + } + } + + "should run on block" - { + val context = makeContext("sub1").first + val builder = SubcommandBuilder(context) + + shouldThrow { + builder.apply { + on("sub1") { + context.argIndex shouldBe 1 + throw TestException() + } + } + } + } +}) diff --git a/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/TestTabCompletions.kt b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/TestTabCompletions.kt new file mode 100644 index 0000000..b884d61 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/TestTabCompletions.kt @@ -0,0 +1,66 @@ +package `in`.kyle.mcspring.commands.dsl + +import `in`.kyle.mcspring.commands.dsl.CommandTestSupport.getCompletions +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe + +class TestTabCompletions : FreeSpec({ + "should return completions for command" - { + getCompletions("") { + tabCompletions("one", "two", "three") + commandMissing() + } shouldBe listOf("one", "two", "three") + } + + "should return partial completions" - { + getCompletions("on") { + tabCompletions("one", "two", "three") + commandMissing() + } shouldBe listOf("one") + } + + "completed commands return no completions" - { + getCompletions("") { + tabCompletions("one", "two", "three") + commandComplete() + } shouldBe emptyList() + } + + "multi-arg commands should reset completions" - { + getCompletions("one") { + mapArg { + parser { + map("one" to 1) + } + } + mapArg { + parser { + map("two" to 1) + } + missing {} + } + + commandComplete() + } shouldBe emptyList() + } + + "invalid arg values should stop parsing" - { + getCompletions("on tw") { + mapArg { + parser { + map("one" to 1) + } + invalid {} + } + + mapArg { + parser { + map("two" to 1) + } + missing {} + } + + commandComplete() + } shouldBe emptyList() + } +}) diff --git a/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/TestValueBuilder.kt b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/TestValueBuilder.kt new file mode 100644 index 0000000..718e56b --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/TestValueBuilder.kt @@ -0,0 +1,48 @@ +package `in`.kyle.mcspring.commands.dsl + +import `in`.kyle.mcspring.commands.dsl.CommandTestSupport.makeContext +import `in`.kyle.mcspring.commands.dsl.parsers.numbers.IntParser +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe + +class TestValueBuilder : FreeSpec({ + + "default should set the return value" - { + val context = makeContext("one").first + val builder = ValueBuilder(context) { IntParser(context, it) } + + builder.apply { + default { 1 } + default { 2 } + } + + builder.returnValue shouldBe 1 + } + + "default should not overwrite existing" - { + val context = makeContext("one").first + val builder = ValueBuilder(context) { IntParser(context, it) } + builder.returnValue = 99 + + builder.apply { + default { 1 } + } + + builder.returnValue shouldBe 99 + } + + + "missing block should run when missing arg" - { + val context = makeContext("").first + val builder = ValueBuilder(context) { IntParser(context, it) } + + shouldThrow { + builder.apply { + missing { + throw TestException() + } + } + } + } +}) diff --git a/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/parsers/TestBooleanParser.kt b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/parsers/TestBooleanParser.kt new file mode 100644 index 0000000..774a462 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/parsers/TestBooleanParser.kt @@ -0,0 +1,31 @@ +package `in`.kyle.mcspring.commands.dsl.parsers + +import `in`.kyle.mcspring.commands.dsl.CommandTestSupport.makeBuilder +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.filterNot +import io.kotest.property.arbitrary.stringPattern +import io.kotest.property.checkAll + +class TestBooleanParser : FreeSpec({ + + "true should parse" - { + makeBuilder("true").booleanArg { } shouldBe true + makeBuilder("TrUe").booleanArg { } shouldBe true + } + + "false should parse" - { + makeBuilder("false").booleanArg { } shouldBe false + makeBuilder("FaLsE").booleanArg { } shouldBe false + } + + "no other string should parse" - { + Arb.stringPattern("[a-zA-Z]{1,256}").filterNot { it in arrayOf("true", "false") }.checkAll { + shouldThrow { + makeBuilder(it).booleanArg { } + } + } + } +}) diff --git a/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/parsers/TestMapParser.kt b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/parsers/TestMapParser.kt new file mode 100644 index 0000000..e4eebc2 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/parsers/TestMapParser.kt @@ -0,0 +1,65 @@ +package `in`.kyle.mcspring.commands.dsl.parsers + +import `in`.kyle.mcspring.commands.dsl.CommandTestSupport.getCompletions +import `in`.kyle.mcspring.commands.dsl.CommandTestSupport.makeBuilder +import `in`.kyle.mcspring.commands.dsl.TestException +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import java.lang.RuntimeException + +class TestMapParser : FreeSpec({ + + "test map vararg" - { + makeBuilder("one").mapArg { + parser { + map("one" to 1) + } + } shouldBe 1 + } + + "test map map" - { + makeBuilder("one").mapArg { + parser { + map(mapOf("one" to 1)) + } + } shouldBe 1 + } + + "missing map arg should not error" - { + shouldThrow { + makeBuilder("").mapArg { + parser { + map("one" to 1) + map("two" to 2) + map("three" to 3) + } + missing { + throw TestException() + } + } + } + } + + "test tab completions" - { + getCompletions(" ") { + mapArg { + parser { + map("one" to 1) + map("two" to 2) + map("three" to 3) + } + invalid { } + missing { } + } + } shouldBe listOf("one", "two", "three") + } + + "test map function" - { + makeBuilder("1").mapArg { + parser { + map { it.toIntOrNull() } + } + } shouldBe 1 + } +}) diff --git a/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/parsers/TestStringParser.kt b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/parsers/TestStringParser.kt new file mode 100644 index 0000000..56c9d37 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/parsers/TestStringParser.kt @@ -0,0 +1,67 @@ +package `in`.kyle.mcspring.commands.dsl.parsers + +import `in`.kyle.mcspring.commands.dsl.CommandTestSupport.runCommand +import `in`.kyle.mcspring.commands.dsl.TestException +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.arbitrary.stringPattern +import io.kotest.property.checkAll +import org.junit.jupiter.api.fail + +class TestStringParser : FreeSpec({ + + "simple string parses" - { + runCommand("test") { + val arg0 = stringArg { } + arg0 shouldBe "test" + commandComplete() + } + } + + "missing called on empty string" - { + shouldThrow { + runCommand("") { + stringArg { + invalid { fail("should not run") } + missing { throw TestException() } + } + fail("should not run") + } + } + } + + "multiple strings are parses correctly" - { + runCommand("arg1 arg2") { + val arg0 = stringArg { + missing { fail("should not run") } + invalid { fail("should not run") } + } + + arg0 shouldBe "arg1" + context.argIndex shouldBe 1 + + val arg1 = stringArg { + missing { fail("should not run") } + invalid { fail("should not run") } + } + arg1 shouldBe "arg2" + + commandComplete() + } + } + + "test all strings parse correctly" - { + Arb.stringPattern("[a-zA-Z0-9]{1,256}").checkAll { + runCommand(it) { + val arg1 = stringArg { + missing { fail("should not run") } + invalid { fail("should not run") } + } + arg1 shouldBe it + commandComplete() + } + } + } +}) diff --git a/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/parsers/numbers/TestDoubleParser.kt b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/parsers/numbers/TestDoubleParser.kt new file mode 100644 index 0000000..efe1871 --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/parsers/numbers/TestDoubleParser.kt @@ -0,0 +1,18 @@ +package `in`.kyle.mcspring.commands.dsl.parsers.numbers + +import `in`.kyle.mcspring.commands.dsl.CommandTestSupport.makeBuilder +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import io.kotest.property.checkAll + +class TestDoubleParser : FreeSpec({ + "should parse simple double" - { + makeBuilder("0.0001").doubleArg { } shouldBe 0.0001 + } + + "should parse all doubles" - { + checkAll { + makeBuilder(it.toString()).doubleArg { } shouldBe it + } + } +}) diff --git a/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/parsers/numbers/TestIntParser.kt b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/parsers/numbers/TestIntParser.kt new file mode 100644 index 0000000..9f5326f --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/parsers/numbers/TestIntParser.kt @@ -0,0 +1,36 @@ +package `in`.kyle.mcspring.commands.dsl.parsers.numbers + +import `in`.kyle.mcspring.commands.dsl.CommandTestSupport.runCommand +import io.kotest.assertions.fail +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import io.kotest.property.checkAll + +class TestIntParser : FreeSpec({ + + "should parse int" - { + "should parse simple int" - { + runCommand("123") { + val arg0 = intArg { + invalid { fail("should not run") } + missing { fail("should not run") } + } + arg0 shouldBe 123 + commandComplete() + } + } + + "should parse any int" - { + checkAll { + runCommand("$it") { + val testArg = intArg { + invalid { fail("should not run") } + missing { fail("should not run") } + } + testArg shouldBe it + commandComplete() + } + } + } + } +}) diff --git a/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/parsers/numbers/TestNumberParser.kt b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/parsers/numbers/TestNumberParser.kt new file mode 100644 index 0000000..094047c --- /dev/null +++ b/mcspring-api/mcspring-commands-dsl/src/test/kotlin/in/kyle/mcspring/commands/dsl/parsers/numbers/TestNumberParser.kt @@ -0,0 +1,134 @@ +package `in`.kyle.mcspring.commands.dsl.parsers.numbers + +import `in`.kyle.mcspring.commands.dsl.CommandTestSupport.makeBuilder +import `in`.kyle.mcspring.commands.dsl.ContextReciever.BreakParseException +import io.kotest.assertions.fail +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe + +class TestNumberParser : FreeSpec({ + + "between should work" - { + "between pass" - { + makeArg { + between(0, 100) { fail("should not run") } + } shouldBe 10 + } + "between fail" - { + makeArg { + shouldThrow { + between(0, 1) {} + } + } + } + } + + "greaterThan should work" - { + "gt pass" - { + makeArg { + greaterThan(0) { fail("should not run") } + } shouldBe 10 + } + "gt fail" - { + makeArg { + shouldThrow { + greaterThan(100) {} + } + } + } + } + + "greaterThanEqual should work" - { + "gte pass" - { + makeArg { + greaterThanEqual(10) { fail("should not run") } + } shouldBe 10 + } + "gte fail" - { + makeArg { + shouldThrow { + greaterThanEqual(100) {} + } + } + } + } + + "lessThan should work" - { + "lt pass" - { + makeArg { + lessThan(100) { fail("should not run") } + } shouldBe 10 + } + "lt fail" - { + makeArg { + shouldThrow { + lessThan(0) {} + } + } + } + } + + "lessThanEqual should work" - { + "lte pass" - { + makeArg { + lessThanEqual(10) { fail("should not run") } + } shouldBe 10 + } + "lte fail" - { + makeArg { + shouldThrow { + lessThanEqual(0) {} + } + } + } + } + + "positive should work" - { + "positive pass" - { + makeArg { + positive { fail("should not run") } + } shouldBe 10 + } + "positive fail" - { + makeArg(-10) { + shouldThrow { + positive {} + } + } + } + } + + "negative should work" - { + "negative pass" - { + makeArg(-1) { + negative { fail("should not run") } + } shouldBe -1 + } + "negative fail" - { + makeArg { + shouldThrow { + negative {} + } + } + } + } + + "nonzero should work" - { + "nonzero pass" - { + makeArg { + nonZero { fail("should not run") } + } shouldBe 10 + } + "nonzero fail" - { + makeArg(value = 0) { + shouldThrow { + nonZero {} + } + } + } + } +}) + +private fun makeArg(value: Int = 10, lambda: IntParser.() -> Unit) = + makeBuilder(value.toString()).intArg { parser(lambda) } diff --git a/mcspring-api/mcspring-e2e/README.md b/mcspring-api/mcspring-e2e/README.md new file mode 100644 index 0000000..2f5656a --- /dev/null +++ b/mcspring-api/mcspring-e2e/README.md @@ -0,0 +1,22 @@ +mcspring-e2e +--- +This is my attempt to create E2E testing for plugins. It's not complete, but it's a cool concept. +Maybe you should create a PR? + +### Examples + +This isn't complete yet, but the idea is the following: +1. Instrument a JUnit test to start a Spigot instance +```kotlin +class MyTest : SpigotServerTest { + @Test + fun test() { + // wait for Spigot to start + // attach and run some code as a plugin + // verify the behavior of your existing plugin + // kill Spigot + } +} +``` + +_This may be used independently of mcspring_ diff --git a/mcspring-api/mcspring-e2e/build.gradle.kts b/mcspring-api/mcspring-e2e/build.gradle.kts new file mode 100644 index 0000000..e24116a --- /dev/null +++ b/mcspring-api/mcspring-e2e/build.gradle.kts @@ -0,0 +1,27 @@ +import java.net.URL + +val spigotVersion by extra { "1.16.1" } + +dependencies { + implementation("org.junit.jupiter:junit-jupiter:5.6.2") + implementation("io.mockk:mockk:1.10.0") + val paper = urlFile( + "https://papermc.io/api/v1/paper/$spigotVersion/latest/download", + "paper-$spigotVersion" + ) + compileOnly(paper) + runtimeOnly(paper) +} + +fun urlFile(url: String, name: String): ConfigurableFileCollection { + val file = File("$buildDir/downloads/${name}.jar") + file.parentFile.mkdirs() + if (!file.exists()) { + URL(url).openStream().use { downloadStream -> + file.outputStream().use { fileOut -> + downloadStream.copyTo(fileOut) + } + } + } + return files(file.absolutePath) +} diff --git a/mcspring-api/mcspring-e2e/src/main/kotlin/in/kyle/mcspring/e2e/FileDsl.kt b/mcspring-api/mcspring-e2e/src/main/kotlin/in/kyle/mcspring/e2e/FileDsl.kt new file mode 100644 index 0000000..c202dd3 --- /dev/null +++ b/mcspring-api/mcspring-e2e/src/main/kotlin/in/kyle/mcspring/e2e/FileDsl.kt @@ -0,0 +1,29 @@ +package `in`.kyle.mcspring.e2e + +import java.io.File + +class FileDsl(private val file: File) { + + fun delete(string: String) { + val toDelete = file.resolve(string) + if (toDelete.isFile) { + toDelete.delete() + } else { + toDelete.deleteRecursively() + } + } + + fun create(string: String) = file.apply { mkdirs() }.resolve(string).createNewFile() + + fun folder(string: String, lambda: FileDsl.() -> Unit) = FileDsl(file.resolve(string)).lambda() + + fun write(string: String) = file.writeText(string) + + operator fun String.unaryPlus() = create(this) + + operator fun String.unaryMinus() = delete(this) + + operator fun String.invoke(lambda: FileDsl.() -> Unit) = folder(this, lambda) +} + +operator fun File.invoke(lambda: FileDsl.() -> Unit) = FileDsl(this).lambda() diff --git a/mcspring-api/mcspring-e2e/src/main/kotlin/in/kyle/mcspring/e2e/SpigotServerTest.kt b/mcspring-api/mcspring-e2e/src/main/kotlin/in/kyle/mcspring/e2e/SpigotServerTest.kt new file mode 100644 index 0000000..019b540 --- /dev/null +++ b/mcspring-api/mcspring-e2e/src/main/kotlin/in/kyle/mcspring/e2e/SpigotServerTest.kt @@ -0,0 +1,120 @@ +package `in`.kyle.mcspring.e2e + +import io.mockk.every +import io.mockk.mockk +import io.papermc.paperclip.Paperclip +import org.bukkit.Bukkit +import org.bukkit.plugin.Plugin +import org.bukkit.plugin.PluginDescriptionFile +import org.junit.jupiter.api.extension.* +import org.junit.platform.commons.JUnitException +import java.io.File +import java.lang.reflect.Method + +class SpigotServerTest : BeforeAllCallback, AfterAllCallback, InvocationInterceptor { + + override fun beforeAll(context: ExtensionContext) { + startSpigot() + } + + override fun afterAll(context: ExtensionContext) { + println("Shutting down") + Bukkit.getServer().shutdown() + cleanupFiles() + // let junit clean up the zombie threads + } + + override fun interceptTestMethod( + invocation: InvocationInterceptor.Invocation, + invocationContext: ReflectiveInvocationContext, + extensionContext: ExtensionContext? + ) { + val plugin = mockk() + every { plugin.isEnabled } returns true + every { plugin.name } returns "test-instance" + every { plugin.description } returns PluginDescriptionFile("test-instance", "0.0.1", "main-class") + every { plugin.logger } returns Bukkit.getLogger() + every { plugin.server } returns Bukkit.getServer() + + var hasRun = false + Bukkit.getServer().scheduler.runTask(plugin, Runnable { + invocation.proceed() + hasRun = true + }) + + while (!hasRun) { + Thread.sleep(1) + } + println("Run test") + } + + private fun startSpigot() { + try { + startSpigotServer() + waitForSpigotToStart() + } catch (e: Exception) { + throw JUnitException("Could not start Spigot server", e) + } + } + + private fun findSpigotMain(): Method { + return try { + Class.forName("org.bukkit.craftbukkit.Main") + .declaredMethods + .first { it.name == "main" } + } catch (e: ClassNotFoundException) { + error(""" + Could not find Spigot jar main class on the runtime classpath. + """.trimIndent()) + } + } + + private fun startSpigotServer() { + System.setProperty("IReallyKnowWhatIAmDoingISwear", "true") + System.setProperty("com.mojang.eula.agree", "true") + + val args = arrayOf("-nogui", "-o=false") + Thread({ + try { + Paperclip.main(args) + } catch (e: Exception) { + println(e) + } + }, "spigot-thread").start() + } + + private fun waitForSpigotToStart() { + @Suppress("UNNECESSARY_SAFE_CALL", "SENSELESS_COMPARISON") + while (Bukkit.getServer() == null || Bukkit.getWorlds().isEmpty()) { + Thread.sleep(1) + } + + println("Spigot Server ${Bukkit.getVersion()} ready") + } + + private fun cleanupFiles() { + File("")() { + -"logs" + -"plugins" + -"cache" + -"world" + -"banned-ips.json" + -"banned-players.json" + -"bukkit.yml" + -"commands.yml" + -"eula.txt" + -"help.yml" + -"ops.json" + -"paper.yml" + -"server.properties" + -"spigot.yml" + -"version_history.json" + -"whitelist.json" + -"permissions.yml" + -"usercache.json" + -"world_nether" + -"world_the_end" + -"crash-reports" + } + } +} diff --git a/mcspring-api/mcspring-gradle-plugin/build.gradle.kts b/mcspring-api/mcspring-gradle-plugin/build.gradle.kts new file mode 100644 index 0000000..5377c2e --- /dev/null +++ b/mcspring-api/mcspring-gradle-plugin/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + `kotlin-dsl` + id("com.gradle.plugin-publish") version "0.12.0" + id("java-gradle-plugin") +} + +val createClasspathManifest = tasks.create("createClasspathManifest") { + val outputDir = buildDir.resolve(name) + + inputs.files(sourceSets.main.get().runtimeClasspath) + outputs.dir(outputDir) + logger.info("Dir to $outputDir") + + doLast { + outputDir.mkdirs() + val target = outputDir.resolve("plugin-classpath.txt") + target.writeText(sourceSets.main.get().runtimeClasspath.joinToString("\n")) + logger.info("Wrote to $target") + } +} + +pluginBundle { + website = "https://github.com/kylepls/mcspring" + vcsUrl = "https://github.com/kylepls/mcspring.git" + tags = listOf("bukkit", "spring") +} + +gradlePlugin { + plugins { + register("mcspring-gradle-plugin") { + id = "in.kyle.mcspring" + implementationClass = "$id.BuildPlugin" + displayName = "mcspring gradle plugin" + description = "Handles the generation of plugin.yml files and applies Spring package formatting" + } + } +} + +dependencies { + implementation("io.github.classgraph:classgraph:4.8.83") + implementation("org.yaml:snakeyaml:1.26") + implementation("com.github.jengelman.gradle.plugins:shadow:5.2.0") + implementation("org.springframework.boot:spring-boot-gradle-plugin:2.3.1.RELEASE") + implementation(project(":mcspring-api:mcspring-base")) + implementation(project(":mcspring-api:mcspring-commands-dsl")) + + testImplementation(gradleTestKit()) + testImplementation("org.junit.jupiter:junit-jupiter:5.6.2") + implementation("io.github.classgraph:classgraph:4.8.83") + testImplementation(project(":mcspring-api:mcspring-base")) + testImplementation(project(":mcspring-api:mcspring-commands-dsl")) + + val kotestVersion = "4.1.0.RC2" + testImplementation("io.kotest:kotest-runner-junit5-jvm:$kotestVersion") + testImplementation("io.kotest:kotest-assertions-core-jvm:$kotestVersion") + testImplementation("io.kotest:kotest-property-jvm:$kotestVersion") + testImplementation("io.kotest:kotest-runner-console-jvm:$kotestVersion") + + testRuntimeOnly(files(createClasspathManifest)) +} + +tasks.withType() { + manifest { + attributes("Implementation-Version" to archiveVersion.get()) + } +} diff --git a/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/BuildPlugin.kt b/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/BuildPlugin.kt new file mode 100644 index 0000000..dc29e97 --- /dev/null +++ b/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/BuildPlugin.kt @@ -0,0 +1,89 @@ +package `in`.kyle.mcspring + +import `in`.kyle.mcspring.tasks.BuildPluginJar +import `in`.kyle.mcspring.tasks.BuildPluginYml +import `in`.kyle.mcspring.tasks.DownloadJar +import `in`.kyle.mcspring.tasks.SetupSpigot +import org.codehaus.groovy.ast.ClassHelper +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.tasks.Copy +import org.gradle.kotlin.dsl.create +import org.gradle.kotlin.dsl.register +import java.io.File + +class BuildPlugin : Plugin { + + override fun apply(project: Project) { + project.extensions.create("mcspring", project) + + project.tasks.register("buildPluginYml") { + description = "Generates a and plugin.yml" + dependsOn(project.tasks.findByName("classes")) + } + + project.tasks.register("downloadJar") { + description = "Download the Bukkit jar" + } + + project.tasks.register("setupSpigot") { + description = "Adds default Spigot configuration settings" + } + + project.tasks.register("buildPluginJar") { + description = "Builds the plugin jar with dependencies" + } + + project.tasks.create("copyJarToSpigot") { + description = "Copy the built plugin jar to the Spigot directory" + val props = project.extensions.mcspring + from(project.buildDir / "libs") + val pluginsDir = File(props.spigotDirectory) / "plugins" + doFirst { + pluginsDir.mkdirs() + } + into(pluginsDir) + } + + project.tasks.register("buildServer") { + group = "mcspring" + description = "Performs a complete sever assembly (this is what you're looking for)" + dependsOn( + "downloadJar", + "setupSpigot", + "buildPluginYml", + "buildPluginJar", + "copyJarToSpigot" + ) + } + + project.tasks.named("jar") { onlyIf { false } } + + registerTestDependencies(project) + registerMcSpringDependencies(project) + } + + private fun registerTestDependencies(project: Project) { + project.afterEvaluate { + val extension = project.extensions.mcspring + project.dependencies.add( + "testImplementation", + project.paperRunner(extension.spigotVersion) + ) + } + } + + private fun registerMcSpringDependencies(project: Project) { + project.configurations.all { + resolutionStrategy.eachDependency { + if (requested.group == "in.kyle.mcspring" && requested.version?.isBlank() == true) { + val version = BuildPlugin::class.java.`package`.implementationVersion + useVersion(version) + because(""" + mcspring dependencies with missing versions are set to the build plugin's version. + """.trimIndent()) + } + } + } + } +} diff --git a/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/Extensions.kt b/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/Extensions.kt new file mode 100644 index 0000000..a4bd1f6 --- /dev/null +++ b/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/Extensions.kt @@ -0,0 +1,47 @@ +package `in`.kyle.mcspring + +import org.gradle.api.Project +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.plugins.ExtensionContainer +import org.gradle.api.plugins.JavaPluginConvention +import org.gradle.api.tasks.SourceSet +import org.gradle.kotlin.dsl.DependencyHandlerScope +import org.gradle.kotlin.dsl.findByType +import java.io.File +import java.net.URL + +fun Project.getMainSourceSet(): SourceSet { + val convention = try { + project.convention.getPlugin(JavaPluginConvention::class.java) + } catch (e: IllegalStateException) { + error("Kotlin/Java/Other plugin not found. Make sure to add one of these.") + } + return convention.sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME) +} + +fun DependencyHandlerScope.mcspring(name: String) = "in.kyle.mcspring:mcspring-$name" + +fun Project.paperRunner(version: String): ConfigurableFileCollection { + return urlFile(this, + "https://papermc.io/api/v1/paper/$version/latest/download", + "paper-runner-$version") +} + +private fun urlFile(project: Project, url: String, name: String): ConfigurableFileCollection { + val file = File("${project.buildDir}/downloads/${name}.jar") + file.parentFile.mkdirs() + if (!file.exists()) { + URL(url).openStream().use { downloadStream -> + file.outputStream().use { fileOut -> + downloadStream.copyTo(fileOut) + } + } + } + return project.files(file.absolutePath) +} + +operator fun File.div(string: String) = resolve(string) + +val ExtensionContainer.mcspring: McSpringExtension + get() = findByType()!! + diff --git a/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/McSpringExtension.kt b/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/McSpringExtension.kt new file mode 100644 index 0000000..cfbadcb --- /dev/null +++ b/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/McSpringExtension.kt @@ -0,0 +1,31 @@ +package `in`.kyle.mcspring + +import org.gradle.api.Project + +open class McSpringExtension(project: Project) { + + var spigotVersion: String = "1.15.2" + var spigotDownloadUrl: String = "https://papermc.io/api/v1/paper/$spigotVersion/latest/download" + var spigotDirectory: String = project.projectDir.resolve("spigot").absolutePath + + var pluginMainPackage: String? = null + var pluginName: String? = null + var pluginVersion: String? = null + var pluginDescription: String? = project.description + var pluginLoad: Any? = null + var pluginAuthor: String? = null + var pluginAuthors: List? = null + var pluginWebsite: String? = null + var pluginDatabase: Boolean? = null + var pluginPrefix: String? = null + var pluginLoadBefore: List? = null + var pluginApiVersion: String? = null + + fun latestApiVersion() { + pluginApiVersion = "1.13" + } + + enum class Load { + STARTUP, POSTWORLD + } +} diff --git a/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/tasks/BuildPluginJar.kt b/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/tasks/BuildPluginJar.kt new file mode 100644 index 0000000..b4c879b --- /dev/null +++ b/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/tasks/BuildPluginJar.kt @@ -0,0 +1,79 @@ +package `in`.kyle.mcspring.tasks + +import `in`.kyle.mcspring.div +import `in`.kyle.mcspring.getMainSourceSet +import `in`.kyle.mcspring.mcspring +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.bundling.ZipEntryCompression +import org.gradle.kotlin.dsl.create +import org.springframework.boot.gradle.dsl.SpringBootExtension +import org.springframework.boot.gradle.tasks.bundling.BootJar +import java.util.concurrent.Callable + +open class BuildPluginJar : ShadowJar() { + + private val springJarTask by lazy { + project.tasks.create("springJar") { + doFirst { + val libs = project.buildDir / "libs" + libs.deleteRecursively() + libs.mkdirs() + } + + mainClassName = "ignored" + classpath(Callable { project.getMainSourceSet().runtimeClasspath }) + + isExcludeDevtools = false + + archiveFileName.set("spring.jar") + dependsOn(project.tasks.named("classes")) + } + } + + private val javaPluginMain = Callable { + project.getMainSourceSet().runtimeClasspath.filter { + "mcspring-base" in it.name + }.map { file -> + project.zipTree(file).matching { + include { + it.isDirectory || "in/kyle/mcspring/javaplugin" in it.path + } + } + } + } + + init { + SpringBootExtension(project).buildInfo() + setup() + } + + private fun setup() { + entryCompression = ZipEntryCompression.STORED + includeEmptyDirs = false + val name = project.extensions.mcspring.pluginName ?: project.name + archiveBaseName.set(name) + archiveClassifier.set("") + + val springJar = project.buildDir / "libs" / "spring.jar" + from(springJar) + from(javaPluginMain) + + relocate("BOOT-INF/classes/", "") + + doLast { + springJar.delete() + } + + dependsOn("buildPluginYml", springJarTask) + } + + @TaskAction + override fun copy() { + relocate( + "org.springframework.boot.loader.in.kyle.mcspring.javaplugin", + project.extensions.mcspring.pluginMainPackage + ) + super.copy() + } +} diff --git a/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/tasks/BuildPluginYml.kt b/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/tasks/BuildPluginYml.kt new file mode 100644 index 0000000..a2c0aff --- /dev/null +++ b/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/tasks/BuildPluginYml.kt @@ -0,0 +1,236 @@ +package `in`.kyle.mcspring.tasks + +import `in`.kyle.mcspring.McSpringExtension +import `in`.kyle.mcspring.annotation.PluginDepend +import `in`.kyle.mcspring.annotation.SoftPluginDepend +import `in`.kyle.mcspring.commands.dsl.mcspring.Command +import `in`.kyle.mcspring.div +import `in`.kyle.mcspring.getMainSourceSet +import `in`.kyle.mcspring.mcspring +import io.github.classgraph.AnnotationParameterValueList +import io.github.classgraph.ClassGraph +import org.apache.log4j.Logger +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.tasks.TaskAction +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.yaml.snakeyaml.Yaml +import java.net.URLClassLoader +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import kotlin.reflect.KClass + +open class BuildPluginYml : DefaultTask() { + + private val logger = Logger.getLogger(BuildPluginYml::class.java) + private val attributes = mutableMapOf() + + @TaskAction + fun buildYml() { + val props = project.extensions.mcspring + logInfo(props) + + props.apply { + writeMainAttributes() + + fun writeNonNull(key: String, value: Any?) = value?.apply { attributes[key] = this } + + writeNonNull("api-version", pluginApiVersion) + writeNonNull("description", pluginDescription) + writeNonNull("load", pluginLoad?.toString()?.toLowerCase()) + writeNonNull("author", pluginAuthor) + writeNonNull("authors", pluginAuthors) + writeNonNull("website", pluginWebsite) + writeNonNull("database", pluginDatabase) + writeNonNull("prefix", pluginPrefix) + writeNonNull("loadbefore", pluginLoadBefore) + } + + logger.info("Getting plugin sources, ${project.getMainSourceSet().runtimeClasspath.files}") + + project.getMainSourceSet().runtimeClasspath.files + .apply { + val classLoader = URLClassLoader(this.map { it.toURI().toURL() }.toTypedArray()) + addDependencies(classLoader) + addCommands(classLoader) + addSpringBootMain(classLoader) + classLoader.close() + } + + writeYmlFile() + } + + private fun McSpringExtension.writeMainAttributes() { + val name = pluginName ?: project.name + val version = pluginVersion ?: project.version + if (pluginMainPackage == null) { + pluginMainPackage = "${project.group}.${project.name}" + logger.info("No main package specified, using $pluginMainPackage") + } + val main = "$pluginMainPackage.SpringJavaPlugin" + + if (!name.matches("[a-zA-Z0-9_-]+".toRegex())) { + throw GradleException( + """ + Invalid plugin name: $name + The plugin name must consist of all alphanumeric characters and underscores (a-z,A-Z,0-9,_) + + Update the project name + OR + Update the generated name by setting the `pluginName` in the mcspring extension block. + """.trimIndent() + ) + } + + val pattern = + "(\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*\\.)*\\p{javaJavaIdentifierStart}\\p{javaJavaIdentifierPart}*" + if (!main.matches(pattern.toRegex())) { + throw GradleException( + """ + Invalid main class location: $main + Refer to the Java specification for valid package/class names. + The main class location is set using: {project group}.{project location}.SpringJavaPlugin + + Update the project group and project name + OR + Update the generated main package by setting the `pluginMainPackage` in the mcspring extension block. + """.trimIndent() + ) + } + + attributes["name"] = name + attributes["version"] = version + attributes["main"] = main + } + + private fun writeYmlFile() { + val outputFile = project.buildDir / "resources" / "main" + outputFile.mkdirs() + val pluginYml = outputFile / "plugin.yml" + logger.info("Building to $pluginYml") + + val formatter = DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm:SS") + val nowFormatted = LocalDateTime.now().format(formatter) + if (pluginYml.exists()) pluginYml.delete() + pluginYml.createNewFile() + pluginYml.appendText( + """ + # File auto generated by mcspring on $nowFormatted + # https://github.com/kylepls/mcspring + + """.trimIndent() + ) + pluginYml.appendText(Yaml().dumpAsMap(attributes)) + } + + private fun addDependencies(classLoader: ClassLoader) { + val scanResult = ClassGraph() + .overrideClassLoaders(classLoader) + .enableAnnotationInfo() + .scan() + scanResult.use { + fun getPluginDependencies(annotation: String) = + scanResult.allClasses.filter { it.isStandardClass && it.hasAnnotation(annotation) } + .map { it.getAnnotationInfo(annotation).parameterValues } + .flatMap { (it["plugins"].value as Array).toList() } + + fun addAnnotationAttributeList(string: String, clazz: KClass<*>) = + getPluginDependencies(clazz.qualifiedName!!) + .takeIf { it.isNotEmpty() } + ?.apply { attributes[string] = this } + + addAnnotationAttributeList("softdepend", SoftPluginDepend::class) + addAnnotationAttributeList("depend", PluginDepend::class) + } + } + + private fun addCommands(classLoader: ClassLoader) { + val scanResult = ClassGraph() + .overrideClassLoaders(classLoader) + .enableAnnotationInfo() + .enableMethodInfo() + .enableClassInfo() + .scan() + + val annotations = scanResult.use { + fun getAnnotations(annotation: KClass<*>): List { + val methods = scanResult.getClassesWithMethodAnnotation(annotation.qualifiedName!!) + .flatMap { it.methodInfo.filter { it.hasAnnotation(annotation.qualifiedName!!) } } + val annotations = + methods.map { it.getAnnotationInfo(annotation.qualifiedName!!) } + .map { it.parameterValues } + return annotations + } + + getAnnotations(Command::class) + } + + val commands = annotations.map { + val meta = mutableMapOf() + + meta["description"] = it.getValue("description") + meta["aliases"] = it.getValue("aliases") + meta["permission"] = it.getValue("permission") + meta["permission-message"] = it.getValue("permissionMessage") + meta["usage"] = it.getValue("usage") + + val name = it["value"].value + val filteredMeta = meta.filterValues { value -> + when (value) { + is Array<*> -> value.isNotEmpty() + is String -> value.isNotEmpty() + else -> true + } + } + Pair(name, filteredMeta) + }.associate { it } + + if (commands.isNotEmpty()) { + attributes["commands"] = commands + } + } + + private fun addSpringBootMain(classLoader: ClassLoader) { + val scanResult = ClassGraph() + .overrideClassLoaders(classLoader) + .enableAnnotationInfo() + .scan(4) + val mains = scanResult.use { + scanResult + .allStandardClasses + .filter { it.hasAnnotation(SpringBootApplication::class.qualifiedName) } + .map { it.name } + } + require(mains.size == 1) { + """ + There should be 1 main class on the classpath: $mains + Make sure to annotate a class with @SpringBootApplication + This serves as an entry point for mcspring. + """.trimIndent() + } + attributes["spring-boot-main"] = mains.first() + } + + private fun logInfo(extension: McSpringExtension) { + extension.apply { + logger.info("Spigot Version: $spigotVersion") + logger.info("Spigot Download URL: $spigotDownloadUrl") + logger.info("Spigot Directory: $spigotDirectory") + logger.info("Plugin Main Package: $pluginMainPackage") + logger.info("Project Name: ${project.name}") + logger.info("Project Version: ${project.version}") + logger.info("Project Group: ${project.group}") + logger.info("Plugin Name: $pluginName") + logger.info("Plugin Version: $pluginVersion") + logger.info("Plugin Description: $pluginDescription") + logger.info("Plugin Load: $pluginLoad") + logger.info("Plugin Author: $pluginAuthor") + logger.info("Plugin Authors: $pluginAuthors") + logger.info("Plugin Website: $pluginWebsite") + logger.info("Plugin Database: $pluginDatabase") + logger.info("Plugin Prefix: $pluginPrefix") + logger.info("Plugin Load Before: $pluginLoadBefore") + logger.info("Plugin API Version: $pluginApiVersion") + } + } +} diff --git a/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/tasks/DownloadJar.kt b/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/tasks/DownloadJar.kt new file mode 100644 index 0000000..0a6f9bf --- /dev/null +++ b/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/tasks/DownloadJar.kt @@ -0,0 +1,30 @@ +package `in`.kyle.mcspring.tasks + +import `in`.kyle.mcspring.McSpringExtension +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.findByType +import java.io.File +import java.net.URI + +open class DownloadJar : DefaultTask() { + + @TaskAction + fun download() { + val props = project.extensions.findByType() + requireNotNull(props) { "mcspring not defined in build.gradle" } + + val target = File(props.spigotDirectory).resolve("spigot.jar") + target.parentFile.mkdirs() + if (target.exists()) { + logger.info("Jar already exists, skipping download") + return + } + + target.outputStream().use { os -> + URI(props.spigotDownloadUrl).toURL().openStream().use { + it.copyTo(os) + } + } + } +} diff --git a/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/tasks/SetupSpigot.kt b/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/tasks/SetupSpigot.kt new file mode 100644 index 0000000..6bbe2eb --- /dev/null +++ b/mcspring-api/mcspring-gradle-plugin/src/main/kotlin/in/kyle/mcspring/tasks/SetupSpigot.kt @@ -0,0 +1,36 @@ +package `in`.kyle.mcspring.tasks + +import org.gradle.api.DefaultTask +import org.gradle.api.tasks.TaskAction +import java.io.File + +open class SetupSpigot : DefaultTask() { + + @TaskAction + fun setup() { + val directory = project.projectDir.resolve("spigot") + require(directory.exists()) {"$directory does not yet exist, run downloadJar first"} + + copyResource("/bukkit.yml", directory) + copyResource("/eula.txt", directory) + copyResource("/server.properties", directory) + copyResource("/spigot.yml", directory) + copyResource("/README.md", directory) + } + + private fun copyResource(path: String, directory: File) { + val target = directory.resolve(path.substring(1)) + if (target.exists()) { + logger.info("Target file $target already exists, skipping...") + return + } + + target.parentFile.mkdirs() + target.outputStream().use { os -> + javaClass.getResourceAsStream(path).use { + requireNotNull(it) {"Could not find $path"} + it.copyTo(os) + } + } + } +} diff --git a/mcspring-api/mcspring-gradle-plugin/src/main/resources/README.md b/mcspring-api/mcspring-gradle-plugin/src/main/resources/README.md new file mode 100644 index 0000000..f7dafec --- /dev/null +++ b/mcspring-api/mcspring-gradle-plugin/src/main/resources/README.md @@ -0,0 +1,14 @@ +mcspring server +--- + +1. Run the `buildServer` gradle task to generate the required files. +This will recompile the plugin jar and copy it over for you. +2. Create a new JAR run configuration in Intellij +3. Select the downloaded `spigot.jar` in the `spigot` folder as the target +4. Change the run environment to the `spigot` folder +5. Add the following **VM flag** `-DIReallyKnowWhatIAmDoingISwear` +6. Add the following **Program Argument** `--nogui` + +![](https://i.imgur.com/waoHVTz.png) + +7. To update the plugin on the server, run the `buildServer` task and restart the server. diff --git a/mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/spigot/bukkit.yml b/mcspring-api/mcspring-gradle-plugin/src/main/resources/bukkit.yml similarity index 94% rename from mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/spigot/bukkit.yml rename to mcspring-api/mcspring-gradle-plugin/src/main/resources/bukkit.yml index fb6a72b..ca883f2 100644 --- a/mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/spigot/bukkit.yml +++ b/mcspring-api/mcspring-gradle-plugin/src/main/resources/bukkit.yml @@ -1,6 +1,6 @@ settings: allow-end: false - warn-on-overload: true + warn-on-overload: false permissions-file: permissions.yml update-folder: update plugin-profiling: false diff --git a/mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/spigot/eula.txt b/mcspring-api/mcspring-gradle-plugin/src/main/resources/eula.txt similarity index 100% rename from mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/spigot/eula.txt rename to mcspring-api/mcspring-gradle-plugin/src/main/resources/eula.txt diff --git a/mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/spigot/server.properties b/mcspring-api/mcspring-gradle-plugin/src/main/resources/server.properties similarity index 100% rename from mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/spigot/server.properties rename to mcspring-api/mcspring-gradle-plugin/src/main/resources/server.properties diff --git a/mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/spigot/spigot.yml b/mcspring-api/mcspring-gradle-plugin/src/main/resources/spigot.yml similarity index 81% rename from mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/spigot/spigot.yml rename to mcspring-api/mcspring-gradle-plugin/src/main/resources/spigot.yml index af31f36..a1b9af3 100644 --- a/mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/spigot/spigot.yml +++ b/mcspring-api/mcspring-gradle-plugin/src/main/resources/spigot.yml @@ -1,15 +1,3 @@ -# This is the main configuration file for Spigot. -# As you can see, there's tons to configure. Some options may impact gameplay, so use -# with caution, and make sure you know what each option does before configuring. -# For a reference for any variable inside this file, check out the Spigot wiki at -# http://www.spigotmc.org/wiki/spigot-configuration/ -# -# If you need help with the configuration or have any questions related to Spigot, -# join us at the IRC or drop by our forums and leave a post. -# -# IRC: #spigot @ irc.spi.gt ( http://www.spigotmc.org/pages/irc/ ) -# Forums: http://www.spigotmc.org/ - config-version: 11 settings: debug: false diff --git a/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/GradleContext.kt b/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/GradleContext.kt new file mode 100644 index 0000000..0a3b474 --- /dev/null +++ b/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/GradleContext.kt @@ -0,0 +1,76 @@ +package `in`.kyle.mcspring + +import org.yaml.snakeyaml.Yaml +import java.io.File + +class GradleContext private constructor() { + + val folder = createTempDir() + val buildFile by lazy { folder / "build.gradle" } + val kotlinSourceFolder by lazy { folder / "src" / "main" / "kotlin" } + val pluginYml by lazy { folder / "build" / "resources" / "main" / "plugin.yml" } + val libsFolder by lazy { folder / "build" / "libs" } + val spigotFolder by lazy { folder / "spigot" } + + val pluginYmlContents: Map + get() = Yaml().load(pluginYml.inputStream()) + + fun runTask(goal: String) = runGradle(folder, goal).task(":$goal")!! + + private fun writeMainClass() { + val srcFile = kotlinSourceFolder / "ExampleMain.kt" + srcFile += """ + |import org.springframework.boot.autoconfigure.SpringBootApplication + |@SpringBootApplication + |open class ExampleMain + """.trimMargin() + } + + companion object { + fun setup(): GradleContext { + val context = GradleContext() + context.writeMainClass() + writeBaseGradleConfig(context.buildFile) + return context + } + } +} + +private fun writeBaseGradleConfig(file: File) { + file += """ + |plugins { + | id("org.jetbrains.kotlin.jvm") version "1.3.72" + | id("in.kyle.mcspring") + |} + | + |repositories { + | mavenLocal() + | jcenter() + |} + |dependencies { + | implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + | implementation("in.kyle.mcspring:mcspring-base:+") + | implementation("in.kyle.mcspring:mcspring-commands-dsl:+") + |} + |mcspring { + | pluginName = "test" + | pluginMainPackage = "test.plugin" + |} + """.trimMargin() + val settings = file.parentFile / "settings.gradle.kts" + settings += """ + |plugins { + | id("com.gradle.enterprise").version("3.3.4") + |} + |gradleEnterprise { + | buildScan { + | termsOfServiceUrl = "https://gradle.com/terms-of-service" + | termsOfServiceAgree = "yes" + | } + |} + """.trimMargin() + val gradleProperties = file.parentFile / "gradle.properties" + gradleProperties += """ + org.gradle.jvmargs=-Xmx1024m + """.trimIndent() +} diff --git a/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/PluginTestSupport.kt b/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/PluginTestSupport.kt new file mode 100644 index 0000000..481eb74 --- /dev/null +++ b/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/PluginTestSupport.kt @@ -0,0 +1,34 @@ +package `in`.kyle.mcspring + +import org.gradle.testkit.runner.BuildResult +import org.gradle.testkit.runner.GradleRunner +import java.io.File + +fun runGradle(folder: File, vararg args: String): BuildResult { + return GradleRunner.create() + .withProjectDir(folder) + .withArguments( + *args, + "--stacktrace", + "--info", + "--scan", + "-s" + ) + .withPluginClasspath(getPluginClasspath()) + .withDebug(true) + .build() +} + +private fun getPluginClasspath(): List { + val pluginClasspathResource = {}::class.java.classLoader.getResource("plugin-classpath.txt") + ?: error("Did not find plugin classpath resource, run `testClasses` build task.") + return pluginClasspathResource.readText().lines().map { File(it) } +} + +operator fun File.plusAssign(string: String) { + parentFile.mkdirs() + appendText("\n$string") +} + +operator fun File.div(string: String) = resolve(string) + diff --git a/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/TestBuildPlugin.kt b/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/TestBuildPlugin.kt new file mode 100644 index 0000000..a915f42 --- /dev/null +++ b/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/TestBuildPlugin.kt @@ -0,0 +1,23 @@ +package `in`.kyle.mcspring + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FreeSpec +import org.gradle.testkit.runner.UnexpectedBuildFailure + +class TestBuildPlugin : FreeSpec({ + + "should throw exception if missing source plugin" - { + val folder = createTempDir() + val buildFile = folder.resolve("build.gradle.kts") + + buildFile += """ + |plugins { + | id("in.kyle.mcspring") + |} + """.trimMargin() + + shouldThrow { + runGradle(folder, "build") + } + } +}) diff --git a/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/tasks/TestBuildPluginJar.kt b/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/tasks/TestBuildPluginJar.kt new file mode 100644 index 0000000..c740f36 --- /dev/null +++ b/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/tasks/TestBuildPluginJar.kt @@ -0,0 +1,54 @@ +package `in`.kyle.mcspring.tasks + +import `in`.kyle.mcspring.* +import io.github.classgraph.ClassGraph +import io.kotest.assertions.assertSoftly +import io.kotest.core.spec.style.FreeSpec +import io.kotest.inspectors.forAll +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import org.gradle.testkit.runner.TaskOutcome +import java.net.URLClassLoader + +class TestBuildPluginJar : FreeSpec({ + + "should create proper plugin jar" - { + val gradle = GradleContext.setup() + + (gradle.kotlinSourceFolder / "Base.kt") += "fun test() { }" + + val task = gradle.runTask("buildPluginJar") + task.outcome shouldBe TaskOutcome.SUCCESS + + val pluginJar = gradle.libsFolder.listFiles()?.firstOrNull() + ?: error("Plugin jar not generated") + + val classLoader = URLClassLoader(arrayOf(pluginJar.toURI().toURL())) + val scan = ClassGraph() + .enableClassInfo() + .overrideClassLoaders(classLoader) + .scan() + scan.use { + "should have plugin.yml" - { + scan.getResourcesWithPath("plugin.yml") shouldHaveSize 1 + } + + "all jar files should be in lib directory" - { + scan.getResourcesWithExtension("jar").map { it.path }.forAll { + it.startsWith("BOOT-INF/lib/") + } + } + + "plugin main should be relocated" - { + assertSoftly { + scan.allClasses.filter { it.packageName == "test.plugin" } + .map { it.name } shouldBe listOf("test.plugin.SpringJavaPlugin") + } + } + + "should have source files" - { + scan.allClasses.filter { it.name == "BaseKt" } shouldHaveSize 1 + } + } + } +}) diff --git a/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/tasks/TestBuildPluginYml.kt b/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/tasks/TestBuildPluginYml.kt new file mode 100644 index 0000000..1d31f0b --- /dev/null +++ b/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/tasks/TestBuildPluginYml.kt @@ -0,0 +1,123 @@ +package `in`.kyle.mcspring.tasks + +import `in`.kyle.mcspring.GradleContext +import `in`.kyle.mcspring.div +import `in`.kyle.mcspring.plusAssign +import io.kotest.assertions.assertSoftly +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldEndWith +import org.gradle.testkit.runner.TaskOutcome + +class TestBuildPluginYml : FreeSpec({ + + "should write minimum required info" - { + val gradle = GradleContext.setup() + gradle.buildFile += """version = "29" """ + + val task = gradle.runTask("buildPluginYml") + task.outcome shouldBe TaskOutcome.SUCCESS + + gradle.pluginYmlContents.assertSoftly { + it.size shouldBe 4 + it["name"] shouldBe "test" + it["version"] shouldBe "29" + it["main"].toString() shouldEndWith ".SpringJavaPlugin" + it["spring-boot-main"] shouldBe "ExampleMain" + } + } + + "should write all user-specified info properly" - { + val gradle = GradleContext.setup() + gradle.buildFile += """ + |mcspring { + | pluginDescription = "test description" + | pluginLoad = "startup" + | pluginAuthor = "kyle" + | pluginWebsite = "github" + | pluginDatabase = true + | pluginPrefix = "prefix" + | pluginAuthors = ["kyle1", "kyle2"] + | pluginLoadBefore = ["other"] + |} + """.trimMargin() + + val task = gradle.runTask("buildPluginYml") + task.outcome shouldBe TaskOutcome.SUCCESS + + gradle.pluginYmlContents.assertSoftly { + it["description"] shouldBe "test description" + it["load"] shouldBe "startup" + it["author"] shouldBe "kyle" + it["authors"] shouldBe listOf("kyle1", "kyle2") + it["website"] shouldBe "github" + it["database"] shouldBe true + it["prefix"] shouldBe "prefix" + it["loadbefore"] shouldBe listOf("other") + } + } + + "should write dependency tags" - { + val gradle = GradleContext.setup() + + (gradle.kotlinSourceFolder / "Example.kt") += """ + |import `in`.kyle.mcspring.annotation.* + |@PluginDepend("plugin") + |@SoftPluginDepend("plugin2") + |class Example + """.trimMargin() + + val task = gradle.runTask("buildPluginYml") + task.outcome shouldBe TaskOutcome.SUCCESS + + gradle.pluginYmlContents.assertSoftly { + it["depend"] shouldBe listOf("plugin") + it["softdepend"] shouldBe listOf("plugin2") + } + } + + "should write commands" - { + val gradle = GradleContext.setup() + + (gradle.kotlinSourceFolder / "Command.kt") += """ + |import `in`.kyle.mcspring.commands.dsl.mcspring.Command + |@Command( + | value = "test", + | aliases = ["t"], + | description = "a test command", + | usage = "/test", + | permission = "test", + | permissionMessage = "no permission" + |) + |fun test() { } + """.trimMargin() + + val task = gradle.runTask("buildPluginYml") + task.outcome shouldBe TaskOutcome.SUCCESS + + gradle.pluginYmlContents["commands"] shouldBe mapOf( + "test" to mapOf( + "description" to "a test command", + "aliases" to listOf("t"), + "permission" to "test", + "permission-message" to "no permission", + "usage" to "/test" + ) + ) + } + + "should add minimal info to commands" - { + val gradle = GradleContext.setup() + + (gradle.kotlinSourceFolder / "Command.kt") += """ + |import `in`.kyle.mcspring.commands.dsl.mcspring.Command + |@Command("test") + |fun test() { } + """.trimMargin() + + val task = gradle.runTask("buildPluginYml") + task.outcome shouldBe TaskOutcome.SUCCESS + + gradle.pluginYmlContents["commands"] shouldBe mapOf("test" to emptyMap()) + } +}) diff --git a/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/tasks/TestDownloadJar.kt b/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/tasks/TestDownloadJar.kt new file mode 100644 index 0000000..526962d --- /dev/null +++ b/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/tasks/TestDownloadJar.kt @@ -0,0 +1,22 @@ +package `in`.kyle.mcspring.tasks + +import `in`.kyle.mcspring.GradleContext +import `in`.kyle.mcspring.div +import `in`.kyle.mcspring.runGradle +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.file.shouldNotBeEmpty +import io.kotest.matchers.shouldBe +import org.gradle.testkit.runner.TaskOutcome + +class TestDownloadJar : FreeSpec({ + + "should download the Spigot jar" - { + val gradle = GradleContext.setup() + + val task = gradle.runTask("downloadJar") + task.outcome shouldBe TaskOutcome.SUCCESS + + val spigot = gradle.spigotFolder / "spigot.jar" + spigot.shouldNotBeEmpty() + } +}) diff --git a/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/tasks/TestSetupSpigot.kt b/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/tasks/TestSetupSpigot.kt new file mode 100644 index 0000000..deaefab --- /dev/null +++ b/mcspring-api/mcspring-gradle-plugin/src/test/kotlin/in/kyle/mcspring/tasks/TestSetupSpigot.kt @@ -0,0 +1,31 @@ +package `in`.kyle.mcspring.tasks + +import `in`.kyle.mcspring.GradleContext +import `in`.kyle.mcspring.div +import io.kotest.assertions.assertSoftly +import io.kotest.core.spec.style.FreeSpec +import io.kotest.matchers.file.shouldNotBeEmpty +import io.kotest.matchers.shouldBe +import org.gradle.testkit.runner.TaskOutcome + +class TestSetupSpigot : FreeSpec({ + + "setup spigot should make required files" - { + val gradle = GradleContext.setup() + val spigotJar = gradle.spigotFolder / "spigot.jar" + spigotJar.parentFile.mkdirs() + spigotJar.createNewFile() + + val task = gradle.runTask("setupSpigot") + task.outcome shouldBe TaskOutcome.SUCCESS + + val spigot = gradle.spigotFolder + assertSoftly { + (spigot / "bukkit.yml").shouldNotBeEmpty() + (spigot / "eula.txt").shouldNotBeEmpty() + (spigot / "README.md").shouldNotBeEmpty() + (spigot / "server.properties").shouldNotBeEmpty() + (spigot / "spigot.yml").shouldNotBeEmpty() + } + } +}) diff --git a/mcspring-api/mcspring-guis/README.md b/mcspring-api/mcspring-guis/README.md new file mode 100644 index 0000000..17b5d7a --- /dev/null +++ b/mcspring-api/mcspring-guis/README.md @@ -0,0 +1,3 @@ +mcspring-guis +--- +Provides a functional GUI api. diff --git a/mcspring-api/mcspring-guis/build.gradle.kts b/mcspring-api/mcspring-guis/build.gradle.kts new file mode 100644 index 0000000..d072cf6 --- /dev/null +++ b/mcspring-api/mcspring-guis/build.gradle.kts @@ -0,0 +1,5 @@ +dependencies { + api("org.apache.logging.log4j:log4j-api-kotlin:1.0.0") + implementation(project(":mcspring-api:mcspring-chat")) + implementation(project(":mcspring-api:mcspring-rx")) +} diff --git a/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/FunctionalGui.kt b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/FunctionalGui.kt new file mode 100644 index 0000000..f4dc7da --- /dev/null +++ b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/FunctionalGui.kt @@ -0,0 +1,41 @@ +package `in`.kyle.mcspring.guis + +import io.reactivex.rxjava3.disposables.CompositeDisposable +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin + +abstract class FunctionalGui( + val player: Player, + val plugin: Plugin +) { + + val redrawLambdas = mutableSetOf Unit>() + val setupLambdas = mutableSetOf Unit>() + + abstract val gui: GuiType + + protected abstract fun makeDrawer(): Drawer + protected abstract fun makeSetup(): Setup + + fun runSetupLambdas() { + val setup = makeSetup() + setupLambdas.forEach { setup.it() } + } + + fun runRedrawLambdas() { + val drawer = makeDrawer() + redrawLambdas.forEach { drawer.it() } + } + + fun open() = gui.open() +} + +interface FunctionalGuiSetup : FunctionalGuiDrawer { + val listenerSubscription: CompositeDisposable + val globalSubscription: CompositeDisposable +} + +interface FunctionalGuiDrawer { + val plugin: Plugin + val player: Player +} diff --git a/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/GuiBase.kt b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/GuiBase.kt new file mode 100644 index 0000000..33e2e5a --- /dev/null +++ b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/GuiBase.kt @@ -0,0 +1,164 @@ +package `in`.kyle.mcspring.guis + +import `in`.kyle.mcspring.guis.chat.ChatGui +import `in`.kyle.mcspring.guis.hotbar.HotbarGui +import `in`.kyle.mcspring.guis.inventory.InventoryGui +import `in`.kyle.mcspring.guis.inventory.InventorySize +import `in`.kyle.mcspring.rx.observeEvent +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.disposables.Disposable +import org.apache.logging.log4j.kotlin.Logging +import org.bukkit.entity.Player +import org.bukkit.event.player.PlayerQuitEvent +import org.bukkit.event.server.PluginDisableEvent +import org.bukkit.plugin.Plugin +import org.bukkit.plugin.java.JavaPlugin + +object Gui : Logging { + + internal val currentGuis = mutableMapOf() + internal val plugin = JavaPlugin.getProvidingPlugin(InventoryGui::class.java) + + fun getActiveGui(player: Player): GuiBase? { + val parentGui = currentGuis[player] + logger.debug { "Active GUI lookup for $player: $parentGui" } + return parentGui + } + + @Suppress("UNCHECKED_CAST") + fun < + Drawer : FunctionalGuiDrawer, + Setup : FunctionalGuiSetup, + GuiType : GuiBase + > setup( + gui: FunctionalGui, + lambda: GuiBuilder.() -> Unit + ): Disposable { + val builder = GuiBuilder() + builder.lambda() + builder.setup?.apply { gui.setupLambdas.add(this) } + builder.redraw?.apply { gui.redrawLambdas.add(this) } + return gui.open() + } + + abstract class Chat( + plugin: Plugin, + player: Player, + parent: GuiBase? = getActiveGui(player) + ) : ChatGui(plugin, player, parent) + + abstract class Hotbar( + plugin: Plugin, + player: Player, + parent: GuiBase? = getActiveGui(player) + ) : HotbarGui(plugin, player, parent) + + abstract class Inventory( + plugin: Plugin, + player: Player, + title: String, + size: Int = InventorySize.ONE_LINE, + parent: GuiBase? = getActiveGui(player) + ) : InventoryGui(plugin, player, title, size, parent) +} + +abstract class GuiBase( + val plugin: Plugin, + val player: Player, + var parent: GuiBase? = null +) : Disposable, Logging { + + lateinit var globalSubscription: CompositeDisposable + lateinit var listenerSubscription: CompositeDisposable + + protected open fun initialize() {} + protected open fun setup() {} + protected abstract fun registerListeners(): CompositeDisposable + + abstract fun redraw() + abstract fun clear() + + open fun open(): Disposable { + logger.debug { "Opening gui for ${player.name}" } + require(!this::globalSubscription.isInitialized) { "GUI can only be opened once" } + if (parent == null) { + parent = Gui.currentGuis[player] + } + logger.debug { + parent?.let { "Created new GUI with parent ${it::class.simpleName}" } + ?: "Created new GUI with no parent" + } + require(parent?.isDisabled() + ?: true) { "Parent must be disabled/closed before a new GUI can be opened" } + globalSubscription = makeDefaultSubscription() + initialize() + enable() + return globalSubscription + } + + fun isDisabled() = listenerSubscription.isDisposed + + override fun isDisposed() = globalSubscription.isDisposed + + override fun dispose() = close() + + open fun close() { + logger.debug { "Closing gui for ${player.name}" } + require(!globalSubscription.isDisposed) { "GUI already closed" } + globalSubscription.dispose() + } + + open fun enable() { + logger.debug { "Enabling gui ${this::class.simpleName} for ${player.name}" } + logger.debug { "Setting current GUI for $player" } + Gui.currentGuis[player] = this + require(this::globalSubscription.isInitialized) { "GUI enable called before open" } + require(!this::listenerSubscription.isInitialized || listenerSubscription.isDisposed) { + "Cannot enable, already enabled" + } + listenerSubscription = registerListeners() + setup() + redraw() + } + + open fun disable() { + logger.debug { "Disabling gui ${this::class.simpleName} for ${player.name}" } + require(this::globalSubscription.isInitialized) { "GUI disable called before open" } + require(!listenerSubscription.isDisposed) { "Cannot disable, already disabled" } + listenerSubscription.dispose() + globalSubscription.remove(listenerSubscription) + clear() + } + + private fun makeDefaultSubscription(): CompositeDisposable { + val subscription = CompositeDisposable() + + subscription.add(plugin.observeEvent(PlayerQuitEvent::class) + .filter { it.player == player } + .subscribe { subscription.dispose() }) + + subscription.add(plugin.observeEvent(PluginDisableEvent::class) + .filter { it.plugin == plugin } + .subscribe { subscription.dispose() }) + + subscription.add(Disposable.fromRunnable { + logger.debug { "Running dispose for ${this::class.simpleName}: ${player.name}" } + if (!isDisabled()) { + logger.debug { "GUI ${this::class.simpleName} not disabled, disabling..." } + disable() + } + + logger.debug { "Removing current GUI ${this::class.simpleName} for $player" } + Gui.currentGuis.remove(player) + + val tempParent = parent + if (tempParent != null) { + logger.debug { "Parent GUI found, enabling ${tempParent::class.simpleName}..." } + tempParent.enable() + } else { + logger.debug { "No parent GUI, skipping parent enable" } + } + }) + return subscription + } +} diff --git a/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/GuiBuilder.kt b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/GuiBuilder.kt new file mode 100644 index 0000000..66059d6 --- /dev/null +++ b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/GuiBuilder.kt @@ -0,0 +1,18 @@ +package `in`.kyle.mcspring.guis + +@Target(AnnotationTarget.CLASS) +annotation class DslMark + +@DslMark +class GuiBuilder { + internal var setup: (Setup.() -> Unit)? = null + internal var redraw: (Drawer.() -> Unit)? = null + + fun setup(lambda: Setup.() -> Unit) { + setup = lambda + } + + fun redraw(lambda: Drawer.() -> Unit) { + redraw = lambda + } +} diff --git a/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/chat/ChatGui.kt b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/chat/ChatGui.kt new file mode 100644 index 0000000..dbe1d98 --- /dev/null +++ b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/chat/ChatGui.kt @@ -0,0 +1,36 @@ +package `in`.kyle.mcspring.guis.chat + +import `in`.kyle.mcspring.guis.GuiBase +import `in`.kyle.mcspring.rx.observeEvent +import `in`.kyle.mcspring.rx.syncScheduler +import io.reactivex.rxjava3.disposables.CompositeDisposable +import org.bukkit.entity.Player +import org.bukkit.event.player.AsyncPlayerChatEvent +import org.bukkit.plugin.Plugin + +abstract class ChatGui( + plugin: Plugin, + player: Player, + parent: GuiBase? = null +) : GuiBase(plugin, player, parent) { + + abstract fun onMessage(message: String) + + override fun registerListeners(): CompositeDisposable { + val subscription = CompositeDisposable() + subscription.add(plugin.observeEvent(AsyncPlayerChatEvent::class) + .filter { it.player == player } + .doOnNext { it.isCancelled = true } + .observeOn(plugin.syncScheduler()) + .subscribeOn(plugin.syncScheduler()) + .map { it.message } + .subscribe(::onMessage)) + return subscription + } + + override fun clear() { + repeat(101) { + player.sendMessage(" ") + } + } +} diff --git a/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/chat/FunctionalChatGui.kt b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/chat/FunctionalChatGui.kt new file mode 100644 index 0000000..00d938d --- /dev/null +++ b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/chat/FunctionalChatGui.kt @@ -0,0 +1,57 @@ +package `in`.kyle.mcspring.guis.chat + +import `in`.kyle.mcspring.guis.* +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.disposables.Disposable +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin + +class FunctionalChatGui( + plugin: Plugin, + player: Player, + parent: GuiBase? +) : FunctionalGui(player, plugin) { + + override val gui: ChatGui = object : ChatGui(plugin, player, parent) { + override fun onMessage(message: String) = messageLambdas.forEach { it(message) } + override fun setup() = runSetupLambdas() + override fun redraw() = runRedrawLambdas() + } + + internal val messageLambdas = mutableListOf<(String) -> Unit>() + + override fun makeDrawer() = ChatGuiDrawer(this) + + override fun makeSetup() = ChatGuiSetup(this) + +} + +open class ChatGuiDrawer(private val chat: FunctionalChatGui): FunctionalGuiDrawer { + + override val plugin = chat.plugin + override val player = chat.player + + fun message(lambda: (String) -> Unit) { + chat.messageLambdas.add(lambda) + } + + fun clear() = chat.gui.clear() + fun redraw() = chat.gui.redraw() + fun close() = chat.gui.close() + fun enable() = chat.gui.enable() + fun disable() = chat.gui.disable() + +} + +class ChatGuiSetup(chat: FunctionalChatGui) : ChatGuiDrawer(chat), FunctionalGuiSetup { + override val listenerSubscription = chat.gui.listenerSubscription + override val globalSubscription = chat.gui.globalSubscription +} + +fun Gui.chat( + player: Player, + lambda: GuiBuilder.() -> Unit +): Disposable { + val functionalChatGui = FunctionalChatGui(plugin, player, getActiveGui(player)) + return setup(functionalChatGui, lambda) +} diff --git a/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/hotbar/FunctionalHotbarGui.kt b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/hotbar/FunctionalHotbarGui.kt new file mode 100644 index 0000000..bc4aad7 --- /dev/null +++ b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/hotbar/FunctionalHotbarGui.kt @@ -0,0 +1,74 @@ +package `in`.kyle.mcspring.guis.hotbar + +import `in`.kyle.mcspring.guis.GuiBase +import `in`.kyle.mcspring.guis.FunctionalGui +import `in`.kyle.mcspring.guis.FunctionalGuiDrawer +import `in`.kyle.mcspring.guis.FunctionalGuiSetup +import `in`.kyle.mcspring.guis.item.ItemBuilder +import org.bukkit.entity.Player +import org.bukkit.inventory.ItemStack +import org.bukkit.plugin.Plugin + +class FunctionalHotbarGui( + plugin: Plugin, + player: Player, + parent: GuiBase? +) : FunctionalGui(player, plugin) { + + internal val actions = mutableMapOf Unit>() + + override val gui = object : HotbarGui(plugin, player, parent) { + override fun onClick(slot: Int): Boolean { + val action = actions[slot] + action?.invoke() + return action != null + } + + override fun setup() = runSetupLambdas() + override fun redraw() = runRedrawLambdas() + } + + override fun makeDrawer() = HotbarGuiDrawer(this) + override fun makeSetup() = HotbarGuiSetup(this) +} + +class HotbarButtonBuilder { + + internal lateinit var item: ItemStack + internal var click: (() -> Unit)? = null + + fun onClick(click: () -> Unit) { + this.click = click + } + + fun itemStack(lambda: ItemBuilder.() -> Unit) { + this.item = ItemBuilder.create(lambda) + } +} + + +open class HotbarGuiDrawer(private val hotbar: FunctionalHotbarGui) : FunctionalGuiDrawer { + + override val plugin = hotbar.plugin + override val player = hotbar.player + + fun button(slot: Int, lambda: HotbarButtonBuilder.() -> Unit) { + val builder = HotbarButtonBuilder().apply(lambda) + val click = builder.click + if (click != null) { + hotbar.actions[slot] = click + } + player.inventory.setItem(slot, builder.item) + } + + fun redraw() = hotbar.gui.redraw() + fun close() = hotbar.gui.close() + fun clear() = hotbar.gui.clear() + fun enable() = hotbar.gui.enable() + fun disable() = hotbar.gui.disable() +} + +class HotbarGuiSetup(hotbar: FunctionalHotbarGui) : HotbarGuiDrawer(hotbar), FunctionalGuiSetup { + override val listenerSubscription = hotbar.gui.listenerSubscription + override val globalSubscription = hotbar.gui.globalSubscription +} diff --git a/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/hotbar/HotbarGui.kt b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/hotbar/HotbarGui.kt new file mode 100644 index 0000000..9ab87f1 --- /dev/null +++ b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/hotbar/HotbarGui.kt @@ -0,0 +1,86 @@ +package `in`.kyle.mcspring.guis.hotbar + +import `in`.kyle.mcspring.guis.GuiBase +import `in`.kyle.mcspring.rx.observeEvent +import io.reactivex.rxjava3.disposables.CompositeDisposable +import org.bukkit.Material +import org.bukkit.entity.Player +import org.bukkit.event.Cancellable +import org.bukkit.event.block.Action +import org.bukkit.event.block.BlockBreakEvent +import org.bukkit.event.block.BlockPlaceEvent +import org.bukkit.event.inventory.InventoryClickEvent +import org.bukkit.event.inventory.InventoryCreativeEvent +import org.bukkit.event.inventory.InventoryType +import org.bukkit.event.player.PlayerDropItemEvent +import org.bukkit.event.player.PlayerInteractEvent +import org.bukkit.inventory.ItemStack +import org.bukkit.plugin.Plugin + +abstract class HotbarGui(plugin: Plugin, player: Player, parent: GuiBase?) : GuiBase(plugin, player, parent) { + + private var lastClickTime = -1L + + private val clickEventHandler = { event: Cancellable -> + val slot = player.inventory.heldItemSlot + if (notSpamClick()) { + onClick(slot) + event.isCancelled = true + } + } + + abstract fun onClick(slot: Int): Boolean + + private fun notSpamClick(): Boolean { + return if (lastClickTime == -1L) { + lastClickTime = System.currentTimeMillis() + true + } else { + val duration = System.currentTimeMillis() - lastClickTime + if (duration > 100) { + lastClickTime = System.currentTimeMillis() + true + } else { + false + } + } + } + + override fun registerListeners(): CompositeDisposable { + val listeners = CompositeDisposable() + listeners.add(plugin.observeEvent(PlayerDropItemEvent::class) + .filter { it.player == player } + .subscribe(clickEventHandler)) + + listeners.add(plugin.observeEvent(InventoryCreativeEvent::class, InventoryClickEvent::class) + .filter { player == it.whoClicked } + .filter { it.clickedInventory?.type == InventoryType.PLAYER } + .subscribe { + it.isCancelled = true + player.setItemOnCursor(ItemStack(Material.AIR)) + player.updateInventory() + }) + + listeners.add(plugin.observeEvent(PlayerInteractEvent::class) + .filter { player == it.player } + .filter { it.action !== Action.PHYSICAL } + .subscribe(clickEventHandler)) + + listeners.add(plugin.observeEvent(BlockPlaceEvent::class) + .filter { it.player == player } + .subscribe(clickEventHandler)) + + listeners.add(plugin.observeEvent(BlockBreakEvent::class) + .filter { it.player == player } + .subscribe(clickEventHandler)) + + return listeners + } + + override fun clear() { + for (i in 0..8) { + player.inventory.setItem(i, null) + } + player.updateInventory() + } +} diff --git a/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/inventory/FunctionalInventoryGui.kt b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/inventory/FunctionalInventoryGui.kt new file mode 100644 index 0000000..88e2367 --- /dev/null +++ b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/inventory/FunctionalInventoryGui.kt @@ -0,0 +1,68 @@ +package `in`.kyle.mcspring.guis.inventory + +import `in`.kyle.mcspring.guis.* +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.disposables.Disposable +import org.bukkit.entity.Player +import org.bukkit.plugin.Plugin + +class FunctionalInventoryGui( + player: Player, + title: String, + size: Int, + plugin: Plugin, + parent: GuiBase? +) : FunctionalGui(player, plugin) { + + internal val actions = mutableMapOf Unit>() + + override val gui = object : InventoryGui(plugin, player, title, size, parent) { + override fun onClick(slot: Int) = actions[slot]?.invoke() ?: Unit + override fun setup() = runSetupLambdas() + override fun redraw() = runRedrawLambdas() + } + + override fun makeDrawer() = InventoryGuiDrawer(this) + override fun makeSetup() = InventoryGuiSetup(this) +} + +class InventoryGuiSetup( + inventory: FunctionalInventoryGui +) : InventoryGuiDrawer(inventory), FunctionalGuiSetup { + override val listenerSubscription: CompositeDisposable = inventory.gui.listenerSubscription + override val globalSubscription: CompositeDisposable = inventory.gui.globalSubscription +} + +open class InventoryGuiDrawer(val inventory: FunctionalInventoryGui) : FunctionalGuiDrawer { + override val plugin = inventory.gui.plugin + override val player = inventory.player + + val lastItemIndex: Int + get() = inventory.gui.lastItemIndex + + fun button(x: Int, y: Int, lambda: InventoryItemBuilder.() -> Unit) = inventory.gui.button(x, y, lambda) + + fun button(slot: Int, lambda: InventoryItemBuilder.() -> Unit) = inventory.gui.button(slot, lambda) + + fun title(lambda: () -> String) { + inventory.gui.title = lambda() + } + + fun redraw() = inventory.gui.redraw() + fun close() = inventory.gui.close() + fun enable() = inventory.gui.enable() + fun disable(closeInventory: Boolean = true) = inventory.gui.disable(closeInventory) + fun closeInventoryOnDisable(boolean: Boolean) { + inventory.gui.closeInventoryOnDisable = boolean + } +} + +fun Gui.inventory( + player: Player, + title: String, + size: Int = InventorySize.ONE_LINE, + lambda: GuiBuilder.() -> Unit +): Disposable { + val functionalInventoryGui = FunctionalInventoryGui(player, title, size, plugin, getActiveGui(player)) + return setup(functionalInventoryGui, lambda) +} diff --git a/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/inventory/InventoryGui.kt b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/inventory/InventoryGui.kt new file mode 100644 index 0000000..1b713dc --- /dev/null +++ b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/inventory/InventoryGui.kt @@ -0,0 +1,177 @@ +package `in`.kyle.mcspring.guis.inventory + +import `in`.kyle.mcspring.chat.translateColorCodes +import `in`.kyle.mcspring.guis.GuiBase +import `in`.kyle.mcspring.guis.item.ItemBuilder +import `in`.kyle.mcspring.rx.observeEvent +import `in`.kyle.mcspring.rx.syncScheduler +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.disposables.Disposable +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import org.bukkit.event.inventory.* +import org.bukkit.inventory.Inventory +import org.bukkit.inventory.ItemStack +import org.bukkit.plugin.Plugin +import java.util.concurrent.TimeUnit + +abstract class InventoryGui( + plugin: Plugin, + player: Player, + title: String, + val size: Int, + parent: GuiBase? = null +) : GuiBase(plugin, player, parent) { + + private val validClicks = listOf(ClickType.LEFT, ClickType.RIGHT, ClickType.MIDDLE) + + private val actions = mutableMapOf Unit>() + + lateinit var bukkitInventory: Inventory + var closeInventoryOnDisable = true + var title = title + set(value) { + updateViewingTitle(value) + field = value + } + + val lastItemIndex: Int by lazy { bukkitInventory.size - 1 } + + open fun onClick(slot: Int) {} + + override fun close() { + require(player.openInventory.topInventory == bukkitInventory) { "Inventory already closed" } + super.close() + } + + override fun open(): Disposable { + val tempParent = parent + if (tempParent is InventoryGui) { + updateViewingTitle(title) + bukkitInventory = tempParent.bukkitInventory + } else { + bukkitInventory = Bukkit.createInventory(player, size, title.translateColorCodes()) + player.openInventory(bukkitInventory) + player.updateInventory() + } + return super.open() + } + + override fun disable() { + super.disable() + if (closeInventoryOnDisable) { + Bukkit.getScheduler().runTask(plugin, Runnable { player.closeInventory() }) + } + } + + fun disable(closeInventory: Boolean) { + val temp = closeInventoryOnDisable + closeInventoryOnDisable = closeInventory + disable() + closeInventoryOnDisable = temp + } + + override fun enable() { + if (player.openInventory.topInventory != bukkitInventory) { + player.openInventory(bukkitInventory) + } + super.enable() + } + + override fun clear() { + bukkitInventory.clear() + player.updateInventory() + } + + override fun registerListeners(): CompositeDisposable { + val listeners = CompositeDisposable() + listeners.add(plugin.observeEvent(InventoryClickEvent::class) + .filter { isCorrectPlayer(it) } + .subscribe { + if (it.click in validClicks) { + val slot = it.rawSlot + actions[slot]?.invoke() + onClick(slot) + } + it.isCancelled = true + }) + + listeners.add(plugin.observeEvent(InventoryDragEvent::class) + .filter { it.inventory == bukkitInventory } + .subscribe { it.isCancelled = true }) + + listeners.add(plugin.observeEvent(InventoryCloseEvent::class) + .filter { it.player == player } + .subscribe { + listeners.add(plugin.syncScheduler().scheduleDirect({ + it.player.openInventory(bukkitInventory) + }, 100, TimeUnit.MILLISECONDS)) + }) + return listeners + } + + fun button(x: Int, y: Int, lambda: InventoryItemBuilder.() -> Unit) { + require(x < 9 && x > -1) { "x is out of range! $x" } + button(x + y * 9, lambda) + } + + fun button(slot: Int, lambda: InventoryItemBuilder.() -> Unit) { + require(slot < bukkitInventory.size) { + "Slot out of bounds $slot < ${bukkitInventory.size}" + } + + val builder = InventoryItemBuilder() + builder.lambda() + + builder.click?.let { actions[slot] = it } + bukkitInventory.setItem(slot, builder.itemStack) + } + + private fun isCorrectPlayer(event: InventoryEvent): Boolean { + return event.inventory.holder != null + && event.inventory.holder == player + && this::bukkitInventory.isInitialized + && event.inventory == bukkitInventory + } + + private fun updateViewingTitle(title: String) { + InventoryTitleHelper.sendInventoryTitle(player, title.translateColorCodes()) + } +} + +class InventoryItemBuilder { + + internal var itemStack: ItemStack? = null + internal var click: (() -> Unit)? = null + + fun itemStack(lambda: ItemBuilder.() -> Unit) { + this.itemStack = ItemBuilder.create(lambda) + } + + fun itemStack(itemStack: ItemStack) { + this.itemStack = itemStack + } + + fun onClick(lambda: () -> Unit) { + click = lambda + } +} + +@Suppress("MemberVisibilityCanBePrivate") +object InventorySize { + const val ONE_LINE = 9 * 1 + const val TWO_LINE = 9 * 2 + const val THREE_LINE = 9 * 3 + const val FOUR_LINE = 9 * 4 + const val FIVE_LINE = 9 * 5 + const val SIX_LINE = 9 * 6 + + fun of(requiredSlots: Int) = arrayOf( + ONE_LINE, + TWO_LINE, + THREE_LINE, + FOUR_LINE, + FIVE_LINE, + SIX_LINE + ).first { it >= requiredSlots } +} diff --git a/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/inventory/InventoryTitleHelper.kt b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/inventory/InventoryTitleHelper.kt new file mode 100644 index 0000000..ba7c414 --- /dev/null +++ b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/inventory/InventoryTitleHelper.kt @@ -0,0 +1,322 @@ +@file:Suppress("LocalVariableName") + +package `in`.kyle.mcspring.guis.inventory + +import com.google.common.base.Preconditions +import org.bukkit.Bukkit +import org.bukkit.entity.Player +import org.bukkit.event.inventory.InventoryType +import org.bukkit.inventory.Inventory +import java.lang.reflect.Constructor +import java.lang.reflect.Field +import java.lang.reflect.Method + +/** + * A inventory helper that allows you to change the title of the current + * opened inventory. + * https://gist.githubusercontent.com/Cybermaxke/7f0a315aea70c9d62535/raw/545b01be4234422e81b1ce0a9606083c261906ba/InventoryTitleHelper + */ +object InventoryTitleHelper { + // Methods + private var m_Player_GetHandle: Method? = null + private var m_PlayerConnection_sendPacket: Method? = null + private var m_CraftChatMessage_fromString: Method? = null + private var m_EntityPlayer_updateInventory: Method? = null + + // Fields + private var f_EntityPlayer_playerConnection: Field? = null + private var f_EntityPlayer_activeContainer: Field? = null + private var f_Container_windowId: Field? = null + + // Constructors + private var c_PacketOpenWindow: Constructor<*>? = null + + // The version of the server (nms version like v1_5_R3) + private var nms_version: String? = null + private var nms_package: String? = null + private var crb_package: String? = null + + /** + * Sends a new inventory title to the client. + * + * @param player the player + * @param title the new title + */ + fun sendInventoryTitle(player: Player, title: String) { + Preconditions.checkNotNull(player, "player") + try { + sendInventoryTitle0( + player, + title + ) + } catch (e: Exception) { + e.printStackTrace() + } + } + + private fun sendInventoryTitle0(player: Player, title: String) { + val inventory = player.openInventory.topInventory + if (m_Player_GetHandle == null) { + m_Player_GetHandle = player.javaClass.getMethod("getHandle") + } + val nms_EntityPlayer = m_Player_GetHandle!!.invoke(player) + if (f_EntityPlayer_playerConnection == null) { + f_EntityPlayer_playerConnection = + nms_EntityPlayer.javaClass.getField("playerConnection") + } + val nms_PlayerConnection = + f_EntityPlayer_playerConnection!![nms_EntityPlayer] + if (f_EntityPlayer_activeContainer == null) { + f_EntityPlayer_activeContainer = nms_EntityPlayer.javaClass.getField("activeContainer") + } + val nms_Container = f_EntityPlayer_activeContainer!![nms_EntityPlayer] + if (f_Container_windowId == null) { + f_Container_windowId = nms_Container.javaClass.getField("windowId") + } + val windowId = f_Container_windowId!!.getInt(nms_Container) + val version = nmsVersion + if (version!!.startsWith("v1_5_") || version.startsWith("v1_6_")) { + sendPacket15a16a17( + nms_PlayerConnection, + nms_EntityPlayer, + nms_Container, + windowId, + inventory, + title, + false + ) + } else if (version.startsWith("v1_7_")) { + sendPacket15a16a17( + nms_PlayerConnection, + nms_EntityPlayer, + nms_Container, + windowId, + inventory, + title, + true + ) + } else if (version == "v1_8_R1" || version == "v1_8_R2") { + sendPacket18( + nms_PlayerConnection, + nms_EntityPlayer, + nms_Container, + windowId, + inventory, + title + ) + } + } + + private fun sendPacket15a16a17( + nms_playerConnection: Any, + nms_EntityPlayer: Any, + nms_Container: Any, + windowId: Int, + inventory: Inventory, + title: String, + flag: Boolean + ) { + var title: String? = title + if (c_PacketOpenWindow == null) { + c_PacketOpenWindow = if (flag) { + findNmsClass("PacketPlayOutOpenWindow") + .getConstructor( + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + String::class.java, + Int::class.javaPrimitiveType, + Boolean::class.javaPrimitiveType + ) + } else { + findNmsClass("Packet100OpenWindow") + .getConstructor( + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + String::class.java, + Int::class.javaPrimitiveType, + Boolean::class.javaPrimitiveType + ) + } + } + val id: Int + val size: Int + when (inventory.type) { + InventoryType.ANVIL -> { + id = 8 + size = 9 + } + InventoryType.BEACON -> { + id = 7 + size = 1 + } + InventoryType.BREWING -> { + id = 5 + size = 4 + } + InventoryType.CRAFTING -> return + InventoryType.CREATIVE -> return + InventoryType.DISPENSER -> { + id = 3 + size = 9 + } + InventoryType.DROPPER -> { + id = 10 + size = 9 + } + InventoryType.ENCHANTING -> { + id = 4 + size = 9 + } + InventoryType.ENDER_CHEST, InventoryType.CHEST -> { + id = 0 + size = inventory.size + } + InventoryType.FURNACE -> { + id = 2 + size = 2 + } + InventoryType.HOPPER -> { + id = 9 + size = 5 + } + InventoryType.MERCHANT -> { + id = 6 + size = 3 + } + InventoryType.PLAYER -> return + InventoryType.WORKBENCH -> { + id = 1 + size = 9 + } + else -> return + } + if (title != null && title.length > 32) { + title = title.substring(0, 32) + } + if (m_EntityPlayer_updateInventory == null) { + m_EntityPlayer_updateInventory = + nms_EntityPlayer.javaClass.getMethod("updateInventory", + findNmsClass("Container") + ) + } + val packet = c_PacketOpenWindow!!.newInstance( + windowId, + id, + title ?: "", + size, + true + ) + sendPacket( + nms_playerConnection, + packet + ) + m_EntityPlayer_updateInventory!!.invoke(nms_EntityPlayer, nms_Container) + } + + private fun sendPacket18( + nms_playerConnection: Any, + nms_EntityPlayer: Any, + nms_Container: Any, + windowId: Int, + inventory: Inventory, + title: String + ) { + if (c_PacketOpenWindow == null) { + c_PacketOpenWindow = + findNmsClass("PacketPlayOutOpenWindow") + .getConstructor( + Int::class.javaPrimitiveType, + String::class.java, + findNmsClass("IChatBaseComponent"), + Int::class.javaPrimitiveType + ) + } + val id: String + var size = 0 + when (inventory.type) { + InventoryType.ANVIL -> id = "minecraft:anvil" + InventoryType.BEACON -> id = "minecraft:beacon" + InventoryType.BREWING -> id = "minecraft:brewing_stand" + InventoryType.CRAFTING -> return + InventoryType.CREATIVE -> return + InventoryType.DISPENSER -> id = "minecraft:dispenser" + InventoryType.DROPPER -> id = "minecraft:dropper" + InventoryType.ENCHANTING -> id = "minecraft:enchanting_table" + InventoryType.ENDER_CHEST, InventoryType.CHEST -> { + id = "minecraft:chest" + size = inventory.size + } + InventoryType.FURNACE -> id = "minecraft:furnace" + InventoryType.HOPPER -> id = "minecraft:hopper" + InventoryType.MERCHANT -> { + id = "minecraft:villager" + size = 3 + } + InventoryType.PLAYER -> return + InventoryType.WORKBENCH -> id = "minecraft:crafting_table" + else -> return + } + if (m_CraftChatMessage_fromString == null) { + m_CraftChatMessage_fromString = + findCrbClass("util.CraftChatMessage") + .getMethod("fromString", String::class.java) + } + if (m_EntityPlayer_updateInventory == null) { + m_EntityPlayer_updateInventory = + nms_EntityPlayer.javaClass.getMethod("updateInventory", + findNmsClass("Container") + ) + } + val nms_title = + (m_CraftChatMessage_fromString!!.invoke(null, title) as Array)[0] + val nms_packet = c_PacketOpenWindow!!.newInstance(windowId, id, nms_title, size) + sendPacket( + nms_playerConnection, + nms_packet + ) + m_EntityPlayer_updateInventory!!.invoke(nms_EntityPlayer, nms_Container) + } + + private fun sendPacket(playerConnection: Any, packet: Any) { + if (m_PlayerConnection_sendPacket == null) { + m_PlayerConnection_sendPacket = + playerConnection.javaClass.getMethod("sendPacket", + findNmsClass("Packet") + ) + } + m_PlayerConnection_sendPacket!!.invoke(playerConnection, packet) + } + + private val nmsVersion: String? + get() { + if (nms_version == null) { + nms_version = + Bukkit.getServer().javaClass.getPackage().name.replace("org.bukkit.craftbukkit.", "") + } + return nms_version + } + + private val nmsPackage: String? + get() { + if (nms_package == null) { + nms_package = "net.minecraft.server.$nmsVersion" + } + return nms_package + } + + private val crbPackage: String? + get() { + if (crb_package == null) { + crb_package = "org.bukkit.craftbukkit.$nmsVersion" + } + return crb_package + } + + private fun findNmsClass(name: String): Class<*> { + return Class.forName("$nmsPackage.$name") + } + + private fun findCrbClass(name: String): Class<*> { + return Class.forName("$crbPackage.$name") + } +} diff --git a/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/item/ItemBuilder.kt b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/item/ItemBuilder.kt new file mode 100644 index 0000000..7022064 --- /dev/null +++ b/mcspring-api/mcspring-guis/src/main/kotlin/in/kyle/mcspring/guis/item/ItemBuilder.kt @@ -0,0 +1,98 @@ +package `in`.kyle.mcspring.guis.item + +import `in`.kyle.mcspring.chat.translateColorCodes +import org.bukkit.DyeColor +import org.bukkit.Material +import org.bukkit.inventory.ItemFlag +import org.bukkit.inventory.ItemStack +import org.bukkit.inventory.meta.SkullMeta +import org.bukkit.material.Dye + +class ItemBuilder { + + var material = Material.AIR + var amount = 1 + var name: String? = null + set(value) { + field = value?.translateColorCodes() + } + var skullOwner: String? = null + var lore = listOf() + set(lines) { + field = lines.map { it.translateColorCodes() }.toMutableList() + } + + var dyeColor: DyeColor? = null + + fun skullOwner(name: String) { + material = Material.PLAYER_HEAD + skullOwner = name + } + + fun lore(vararg lines: String) { + this.lore = lines.toList() + } + + fun build(): ItemStack { + var itemStack= if (dyeColor != null) { + createDyeItemStack() + } else { + ItemStack(material, amount) + } + tryAddItemName(itemStack) + tryAddSkullMeta(itemStack) + tryAddLore(itemStack) + itemStack = + resetItemFlags(itemStack) + return itemStack + } + + private fun tryAddLore(itemStack: ItemStack) { + if (lore.isNotEmpty()) { + val itemMeta = itemStack.itemMeta + itemMeta!!.lore = lore.map { it.translateColorCodes() } + itemStack.itemMeta = itemMeta + } + } + + private fun tryAddSkullMeta(itemStack: ItemStack) { + if (skullOwner != null) { + val skullMeta = itemStack.itemMeta as SkullMeta + skullMeta.owner = skullOwner!!.trim { it <= ' ' } + itemStack.itemMeta = skullMeta + } + } + + private fun tryAddItemName(itemStack: ItemStack) { + if (name != null) { + val itemMeta = itemStack.itemMeta + itemMeta!!.setDisplayName(name) + itemStack.itemMeta = itemMeta + } + } + + private fun createDyeItemStack(): ItemStack { + val itemStack: ItemStack + val dye = Dye(material) + dye.color = dyeColor + itemStack = dye.toItemStack(amount) + return itemStack + } + + companion object { + fun create(lambda: ItemBuilder.() -> Unit): ItemStack { + val builder = ItemBuilder() + builder.lambda() + return builder.build() + } + + private fun resetItemFlags(item: ItemStack): ItemStack { + if (item.hasItemMeta()) { + val itemMeta = item.itemMeta + itemMeta!!.addItemFlags(ItemFlag.HIDE_ATTRIBUTES, ItemFlag.HIDE_POTION_EFFECTS) + item.itemMeta = itemMeta + } + return item + } + } +} diff --git a/mcspring-api/mcspring-jar-loader/pom.xml b/mcspring-api/mcspring-jar-loader/pom.xml deleted file mode 100644 index b851777..0000000 --- a/mcspring-api/mcspring-jar-loader/pom.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - mcspring-api - in.kyle.mcspring - 0.0.9 - - 4.0.0 - - mcspring-jar-loader - - - - org.projectlombok - lombok - - - org.spigotmc - spigot-api - - - org.springframework.boot - spring-boot-loader - - - \ No newline at end of file diff --git a/mcspring-api/mcspring-jar-loader/src/main/java/org/springframework/boot/loader/mcspring/McSpringLoader.java b/mcspring-api/mcspring-jar-loader/src/main/java/org/springframework/boot/loader/mcspring/McSpringLoader.java deleted file mode 100644 index 0c3a572..0000000 --- a/mcspring-api/mcspring-jar-loader/src/main/java/org/springframework/boot/loader/mcspring/McSpringLoader.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.springframework.boot.loader.mcspring; - -import org.springframework.boot.loader.JarLauncher; -import org.springframework.boot.loader.archive.Archive; - -import java.lang.reflect.Method; -import java.net.URL; -import java.net.URLClassLoader; -import java.util.List; - -import lombok.SneakyThrows; - -// Packaging is so that it all blends in with the Spring loader -public class McSpringLoader extends JarLauncher { - - @SneakyThrows - public void launch(ClassLoader classLoader) { - List activeArchives = getClassPathArchives(); - Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class); - addURL.setAccessible(true); - for (Archive archive : activeArchives) { - addURL.invoke(classLoader, archive.getUrl()); - } - } - -// @SneakyThrows -// public void close() { -// getArchive().close(); -// } - - @Override - protected String getMainClass() { - return ""; - } -} diff --git a/mcspring-api/mcspring-nms/README.md b/mcspring-api/mcspring-nms/README.md new file mode 100644 index 0000000..dbeb67a --- /dev/null +++ b/mcspring-api/mcspring-nms/README.md @@ -0,0 +1,66 @@ +mcspring-guis +--- +Provides a straightforward NMS API. + +Here's the idea, we define an interface for the reflective access we need. Then we can use the interface as a proxy object to interact with the objects at runtime. + +### To use... +1. First define the object you want reflective access to, in this instance the OBC world. +2. Add a method that matches the method signature in the OBC class. This method can have another _accessor_ class in-place of the OBC/NMS class. + +```kotlin +@ForClass("{obc}.CraftWorld") // the path to the class, {obc} and {nms} are filled in at runtime +interface CraftWorldAccessor { + + fun save(any: Any): Any + + fun addEntity(entity: NmsEntityAccessor, reason: CreatureSpawnEvent.SpawnReason): Entity + + fun addEntity(entity: CraftEntityAccessor, reason: CreatureSpawnEvent.SpawnReason) = addEntity(entity.getHandle(), reason) + + fun createEntity(location: Location, entityClass: Class): NmsEntity + +} + +@ForClass("{obc}.entity.CraftEntity") +interface CraftEntity { + + fun save(): NbtTagCompound + + fun getNbtString() = save().toString() + + fun getHandle(): NmsEntity + + @StaticMethod + fun getEntity(server: Server, entity: NmsEntity): Entity + + @StaticMethod + fun getEntity(nmsEntity: NmsEntity): Entity = getEntity(Bukkit.getServer(), nmsEntity) + + @UnwrapMethod + fun unwrap(): Entity + +} + +// helper method, not needed +val World.craftWorld + get() = this.obc() +``` + +Then you just have to call the `obc/nms` or `obcStatic/nmsStatic` methods to obtain an instance of the accessor. + +```kotlin + fun makeEntityNms( + entityType: Class, + bukkitWorld: World, + nbt: NbtTagCompound? = null + ): T { + val craftWorld = bukkitWorld.obc() // get the OBC world + val nmsEntity = craftWorld.createEntity(bukkitWorld.spawnLocation, entityType) // call the createEntity method on the accessor object - calls the OBC method, returns an accessor object + if (nbt != null) { + nmsEntity.load(nbt) + } + val bukkitEntity = obcStatic().getEntity(nmsEntity) // obtains an accessor for calling static class methods + return bukkitEntity as T + } +``` diff --git a/mcspring-api/mcspring-nms/build.gradle.kts b/mcspring-api/mcspring-nms/build.gradle.kts new file mode 100644 index 0000000..10654dd --- /dev/null +++ b/mcspring-api/mcspring-nms/build.gradle.kts @@ -0,0 +1,5 @@ +plugins { + `kotlin-dsl` + id("com.gradle.plugin-publish") version "0.12.0" + id("java-gradle-plugin") +} diff --git a/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/MinecraftReflectionAnnotations.kt b/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/MinecraftReflectionAnnotations.kt new file mode 100644 index 0000000..cdeb7f8 --- /dev/null +++ b/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/MinecraftReflectionAnnotations.kt @@ -0,0 +1,19 @@ +package `in`.kyle.mcspring.nms + +import kotlin.reflect.KClass + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +annotation class ForClass(val name: String = "", val clazz: KClass<*> = Any::class) + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +annotation class ObfuscatedName + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +annotation class StaticMethod + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +annotation class UnwrapMethod diff --git a/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/MinecraftReflectionFacade.kt b/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/MinecraftReflectionFacade.kt new file mode 100644 index 0000000..47e9c73 --- /dev/null +++ b/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/MinecraftReflectionFacade.kt @@ -0,0 +1,28 @@ +package `in`.kyle.mcspring.nms + +import `in`.kyle.mcspring.nms.internal.MinecraftReflectionProxyUtils +import `in`.kyle.mcspring.nms.internal.MinecraftReflectionProxyUtils.tryUnwrapProxyArgs +import `in`.kyle.mcspring.nms.internal.MinecraftReflectionProxyUtils.unwrapReflectionProxyType +import `in`.kyle.mcspring.nms.internal.ReflectionInvocationSearchCriteria + +inline fun Any.obc(): T = this.nms() +inline fun Any.nms(): T = MinecraftReflectionProxyUtils.createProxy(this, T::class.java) + +object StaticObject + +inline fun obcStatic(): T = StaticObject.obc() +inline fun nmsStatic(): T = StaticObject.nms() + +inline fun obcConstructor(vararg args: Any) = nmsConstructor(args) +inline fun nmsConstructor(vararg args: Any): T { + val type = unwrapReflectionProxyType(T::class.java) + + val passArgs = tryUnwrapProxyArgs(args).requireNoNulls() + + val constructor = ReflectionInvocationSearchCriteria( + parameterCount = args.size, + parameterTypes = passArgs.map { it.javaClass } + ).matchSingleConstructor(type) + + return constructor.newInstance(*passArgs).nms() +} diff --git a/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/internal/FriendlyStrings.kt b/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/internal/FriendlyStrings.kt new file mode 100644 index 0000000..ef3647e --- /dev/null +++ b/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/internal/FriendlyStrings.kt @@ -0,0 +1,17 @@ +package `in`.kyle.mcspring.nms.internal + +import `in`.kyle.mcspring.nms.internal.MinecraftPackages.replacePackagesWithPrefix +import java.lang.reflect.Method + +internal object FriendlyStrings { + fun getMethodString(method: Method): String { + val returnType = replacePackagesWithPrefix(method.returnType.canonicalName) + val methodName = "${replacePackagesWithPrefix(method.declaringClass.canonicalName)}.${method.name}" + + val args = method.parameterTypes + .joinToString(", ") { replacePackagesWithPrefix(it.canonicalName) } + .let { "($it)" } + + return "$returnType $methodName$args" + } +} diff --git a/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/internal/MinecraftPackages.kt b/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/internal/MinecraftPackages.kt new file mode 100644 index 0000000..6536db2 --- /dev/null +++ b/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/internal/MinecraftPackages.kt @@ -0,0 +1,37 @@ +package `in`.kyle.mcspring.nms.internal + +import org.bukkit.Bukkit + +internal object MinecraftPackages { + + val OBC_PREFIX = Bukkit.getServer().javaClass.getPackage().name + val NMS_PREFIX = OBC_PREFIX.replace("org.bukkit.craftbukkit", "net.minecraft.server") + val VERSION = OBC_PREFIX.replace("org.bukkit.craftbukkit", "").replace(".", "") + + private val replaceVariables = mapOf( + "{obc}" to OBC_PREFIX, + "{nms}" to NMS_PREFIX, + "{version}" to VERSION + ) + + private val classCache = mutableMapOf>() + + fun getClass(nameWithVariables: String): Class<*> { + return classCache.getOrPut(nameWithVariables) { + val canonicalClass = replaceVariables.toList() + .fold(nameWithVariables) { acc, (variable, replacement) -> + acc.replace(variable, replacement) + } + Class.forName(canonicalClass) + } + } + + fun replacePackagesWithPrefix(string: String): String { + return replaceVariables.toList() + .associate { (key, value) -> value to key } + .toList() + .fold(string) { acc, (variable, replacement) -> + acc.replace(variable, replacement) + } + } +} diff --git a/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/internal/MinecraftReflectionProxyInvocationHandler.kt b/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/internal/MinecraftReflectionProxyInvocationHandler.kt new file mode 100644 index 0000000..4673808 --- /dev/null +++ b/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/internal/MinecraftReflectionProxyInvocationHandler.kt @@ -0,0 +1,127 @@ +package `in`.kyle.mcspring.nms.internal + +import `in`.kyle.mcspring.nms.ObfuscatedName +import `in`.kyle.mcspring.nms.StaticMethod +import `in`.kyle.mcspring.nms.UnwrapMethod +import `in`.kyle.mcspring.nms.internal.MinecraftReflectionProxyUtils.isReflectionProxyType +import `in`.kyle.mcspring.nms.internal.MinecraftReflectionProxyUtils.tryUnwrapProxyArgs +import `in`.kyle.mcspring.nms.internal.MinecraftReflectionProxyUtils.tryUnwrapProxyClass +import `in`.kyle.mcspring.nms.internal.MinecraftReflectionProxyUtils.unwrapReflectionProxyType +import java.lang.invoke.MethodHandles +import java.lang.reflect.InvocationHandler +import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.declaredMemberProperties +import kotlin.reflect.full.memberProperties +import kotlin.reflect.jvm.javaGetter + +internal class MinecraftReflectionProxyInvocationHandler( + val instance: Any, + private val interfaceClass: Class +) : InvocationHandler { + + init { + require(!MinecraftReflectionProxyUtils.isReflectionProxyInstance(instance)) { "Cannot create nested proxy types" } + } + + override fun invoke(proxy: Any, proxyMethod: Method, args: Array?): Any? { + return when { + proxyMethod.isDefault -> invokeDefaultMethod(proxy, proxyMethod, args) + proxyMethod.isAnnotationPresent(UnwrapMethod::class.java) -> instance + else -> callMethodOrProperty(proxyMethod, args) + } + } + + private fun callMethodOrProperty(proxyMethod: Method, args: Array?): Any? { + val kotlinProperty = findKotlinProperty(proxyMethod) + return if (kotlinProperty != null) { + val clazz = unwrapReflectionProxyType(interfaceClass) + val fieldType = tryUnwrapProxyClass(kotlinProperty.javaGetter!!.returnType) + val properties = + interfaceClass.kotlin.declaredMemberProperties.filter { + unwrapReflectionProxyType(it.javaGetter!!.returnType) == fieldType + } + val index = properties.indexOf(kotlinProperty) + require(index >= 0) { "Could not find property $kotlinProperty on kotlin class ${interfaceClass.kotlin}" } + val field = + clazz.declaredFields.filter { it.type == fieldType }.elementAtOrNull(index) + ?: error( + """ + ${index + 1} field(s) of type $fieldType not found on class $clazz + was expecting at least ${index + 1} + """.trimIndent() + ) + + field.isAccessible = true + if (proxyMethod.name.startsWith("get")) { + val result = field[instance] + return if (isReflectionProxyType(proxyMethod.returnType)) { + MinecraftReflectionProxyUtils.createProxy(result, proxyMethod.returnType) + } else { + result + } + } else { + field[instance] = args!![0] + } + } else { + callRealMethod(proxyMethod, args) + } + } + + private fun findKotlinProperty(proxyMethod: Method): KProperty1? { + return if (proxyMethod.name.length > 3) { + interfaceClass.kotlin.memberProperties.singleOrNull { + val propertyName = proxyMethod.name.substring(3) + it.name.equals(propertyName, ignoreCase = true) + } + } else { + null + } + } + + private fun callRealMethod(proxyMethod: Method, args: Array?): Any? { + val actualMethod = lookupRealMethod(proxyMethod) + val mappedArgs = tryUnwrapProxyArgs(args) + + return try { + val result = actualMethod.invoke(instance, *mappedArgs) + if (isReflectionProxyType(proxyMethod.returnType)) { + MinecraftReflectionProxyUtils.createProxy(result, proxyMethod.returnType) + } else { + result + } + } catch (e: Exception) { + val methodString = FriendlyStrings.getMethodString(actualMethod) + val argTypes = mappedArgs.map { it?.javaClass } + throw InvocationTargetException(e, "Could not invoke $methodString with $argTypes") + } + } + + private fun lookupRealMethod(proxyMethod: Method): Method { + val isIgnoreName = proxyMethod.isAnnotationPresent(ObfuscatedName::class.java) + val isStatic = proxyMethod.isAnnotationPresent(StaticMethod::class.java) + + val searchCriteria = ReflectionInvocationSearchCriteria( + proxyMethod.name.takeUnless { isIgnoreName }, + proxyMethod.parameterCount, + proxyMethod.returnType, + proxyMethod.parameterTypes.toList(), + isStatic + ) + + val clazz = unwrapReflectionProxyType(interfaceClass) + return searchCriteria.matchSingleMethod(clazz).apply { isAccessible = true } + } + + private fun invokeDefaultMethod(proxy: Any, method: Method, args: Array?): Any? { + method.isAccessible = true + return MethodHandles.privateLookupIn(interfaceClass, MethodHandles.lookup()) + .`in`(interfaceClass) + .unreflectSpecial(method, interfaceClass) + .bindTo(proxy) + .invokeWithArguments(args?.toList() ?: emptyList()) + } + + override fun toString() = "MinecraftReflection[$interfaceClass: $instance]" +} diff --git a/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/internal/MinecraftReflectionProxyUtils.kt b/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/internal/MinecraftReflectionProxyUtils.kt new file mode 100644 index 0000000..4a9abfa --- /dev/null +++ b/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/internal/MinecraftReflectionProxyUtils.kt @@ -0,0 +1,72 @@ +package `in`.kyle.mcspring.nms.internal + +import `in`.kyle.mcspring.nms.ForClass +import java.lang.reflect.Proxy + +object MinecraftReflectionProxyUtils { + + fun createProxy(obj: Any, clazz: Class): T { + val classLoader = object : Any() {}.javaClass.classLoader + @Suppress("UNCHECKED_CAST") + return Proxy.newProxyInstance( + classLoader, + arrayOf(clazz), + MinecraftReflectionProxyInvocationHandler(obj, clazz) + ) as T + } + + fun isReflectionProxyInstance(obj: Any) = try { + Proxy.getInvocationHandler(obj) is MinecraftReflectionProxyInvocationHandler<*> + } catch (_: IllegalArgumentException) { + false + } + + fun isReflectionProxyType(type: Class<*>) = type.isAnnotationPresent(ForClass::class.java) && type.isInterface + + fun unwrapReflectionProxyType(proxyType: Class<*>): Class<*> { + val forClassAnnotation = proxyType.getAnnotation(ForClass::class.java) + ?: error("ForClass annotation missing on ${proxyType.canonicalName}") + val className = forClassAnnotation.name + return if (className.isNotEmpty()) { + MinecraftPackages.getClass(className) + } else { + forClassAnnotation.clazz.java + } + } + + fun unwrapReflectionProxyInstance(proxy: Any): Any { + val invocationHandler = Proxy.getInvocationHandler(proxy) + return if (invocationHandler is MinecraftReflectionProxyInvocationHandler<*>) { + invocationHandler.instance + } else { + proxy + } + } + + fun tryUnwrapProxyArgs(args: Array?): Array { + return args?.map(MinecraftReflectionProxyUtils::tryUnwrapProxyArg) + ?.toTypedArray() + ?: emptyArray() + } + + private fun tryUnwrapProxyArg(arg: Any?): Any? { + if (arg != null) { + if (isReflectionProxyInstance(arg)) { + return unwrapReflectionProxyInstance(arg) + } + } + return arg + } + + fun tryUnwrapProxyClasses(classes: List>): List> { + return classes.map(this::tryUnwrapProxyClass) + } + + fun tryUnwrapProxyClass(clazz: Class<*>): Class<*> { + return if (isReflectionProxyType(clazz)) { + unwrapReflectionProxyType(clazz) + } else { + clazz + } + } +} diff --git a/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/internal/ReflectionInvocationSearchCriteria.kt b/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/internal/ReflectionInvocationSearchCriteria.kt new file mode 100644 index 0000000..a02eaf1 --- /dev/null +++ b/mcspring-api/mcspring-nms/src/main/kotlin/in/kyle/mcspring/nms/internal/ReflectionInvocationSearchCriteria.kt @@ -0,0 +1,69 @@ +package `in`.kyle.mcspring.nms.internal + +import java.lang.reflect.Constructor +import java.lang.reflect.Method +import java.lang.reflect.Modifier + +data class ReflectionInvocationSearchCriteria( + private val name: String? = null, + private val parameterCount: Int? = null, + private val returnType: Class<*>? = null, + private val parameterTypes: List>? = null, + private val static: Boolean? = null +) { + + fun matchSingleMethod(clazz: Class<*>): Method { + val matchMethods = matchMethods(clazz) + return matchMethods.singleOrNull() + ?: error("Did not find exact match for criteria: $this on ${clazz.name}, matches: $matchMethods") + } + + fun matchSingleConstructor(clazz: Class<*>): Constructor<*> { + val matchConstructors = matchConstructors(clazz) + return matchConstructors.singleOrNull() + ?: error("Did not find exact match for criteria: $this on ${clazz.name}, matches: $matchConstructors") + } + + fun matchMethods(clazz: Class<*>): List { + return clazz.declaredMethods.filter { matchesMethod(clazz, it) } + } + + fun matchConstructors(clazz: Class<*>): List> { + return clazz.declaredConstructors.filter(::matchesConstructor) + } + + fun matchesMethod(clazz: Class<*>, method: Method): Boolean { + return (name == null || method.name == name) + && (parameterCount == null || method.parameterCount == parameterCount) + && (returnType == null || typeMatches(returnType, method.returnType)) + && (parameterTypes == null || typeArraysMatch(parameterTypes, method.parameterTypes.toList())) + && (static == null || Modifier.isStatic(method.modifiers) == static) + && method.declaringClass == clazz + } + + fun matchesConstructor(constructor: Constructor<*>): Boolean { + require(name == null) { "Cannot have named constructor" } + require(returnType == null) { "Constructor return type implicitly given" } + return (parameterCount == null || constructor.parameterCount == parameterCount) + && (parameterTypes == null || typeArraysMatch(parameterTypes, constructor.parameterTypes.toList())) + } + + private fun typeMatches(proxyType: Class<*>, targetType: Class<*>): Boolean { + return typeArraysMatch(listOf(proxyType), listOf(targetType)) + } + + private fun typeArraysMatch(proxyTypes: List>, targetTypes: List>): Boolean { + return proxyTypes.zip(targetTypes) + .all { (proxyType, targetType) -> + if (MinecraftReflectionProxyUtils.isReflectionProxyType(proxyType)) { + targetType.isAssignableFrom( + MinecraftReflectionProxyUtils.unwrapReflectionProxyType( + proxyType + ) + ) + } else { + true + } + } + } +} diff --git a/mcspring-api/mcspring-rx/README.md b/mcspring-api/mcspring-rx/README.md new file mode 100644 index 0000000..a1f0937 --- /dev/null +++ b/mcspring-api/mcspring-rx/README.md @@ -0,0 +1,5 @@ +mcspring-rx +--- +Adds RXJava3 Support for Bukkit events and scheduled tasks + +_This may be used independently of mcspring_ diff --git a/mcspring-api/mcspring-rx/build.gradle.kts b/mcspring-api/mcspring-rx/build.gradle.kts new file mode 100644 index 0000000..0ac580d --- /dev/null +++ b/mcspring-api/mcspring-rx/build.gradle.kts @@ -0,0 +1,5 @@ +val rxJava = "3.0.4" +dependencies { + api("io.reactivex.rxjava3:rxjava:$rxJava") + implementation("io.reactivex.rxjava3:rxjava:$rxJava") +} diff --git a/mcspring-api/mcspring-rx/src/main/kotlin/in/kyle/mcspring/rx/EventSupport.kt b/mcspring-api/mcspring-rx/src/main/kotlin/in/kyle/mcspring/rx/EventSupport.kt new file mode 100644 index 0000000..3604b01 --- /dev/null +++ b/mcspring-api/mcspring-rx/src/main/kotlin/in/kyle/mcspring/rx/EventSupport.kt @@ -0,0 +1,67 @@ +package `in`.kyle.mcspring.rx + +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.ObservableEmitter +import io.reactivex.rxjava3.disposables.Disposable +import org.bukkit.Bukkit +import org.bukkit.event.Event +import org.bukkit.event.EventPriority +import org.bukkit.event.HandlerList +import org.bukkit.event.Listener +import org.bukkit.event.server.PluginDisableEvent +import org.bukkit.plugin.EventExecutor +import org.bukkit.plugin.Plugin +import kotlin.reflect.KClass + +fun Plugin.observeEvent( + vararg classes: KClass, + priority: EventPriority = EventPriority.NORMAL, + ignoredCanceled: Boolean = false +): Observable { + val listener = object : Listener {} + return Observable.create { subscriber -> + val executor = createExecutor( + subscriber, + classes.toList() + ) + + val pluginManager = Bukkit.getPluginManager() + classes.forEach { + pluginManager.registerEvent(it.java, listener, priority, executor, this, ignoredCanceled) + } + + registerDisable(subscriber, this, listener) + subscriber.setDisposable(Disposable.fromRunnable { + HandlerList.unregisterAll(listener) + }) + } +} + +private fun createExecutor( + subscriber: ObservableEmitter, + classes: List> +): EventExecutor { + return EventExecutor { _, event -> + if (classes.all { it.java.isAssignableFrom(event::class.java) }) { + subscriber.onNext(event as T) + } + } +} + +private fun registerDisable( + subscriber: ObservableEmitter<*>, + plugin: Plugin, + listener: Listener +) { + Bukkit.getPluginManager().registerEvent( + PluginDisableEvent::class.java, + listener, + EventPriority.MONITOR, + { _, event -> + if ((event as PluginDisableEvent).plugin == plugin) { + subscriber.onComplete() + } + }, + plugin + ) +} diff --git a/mcspring-api/mcspring-rx/src/main/kotlin/in/kyle/mcspring/rx/SchedulerSupport.kt b/mcspring-api/mcspring-rx/src/main/kotlin/in/kyle/mcspring/rx/SchedulerSupport.kt new file mode 100644 index 0000000..746506a --- /dev/null +++ b/mcspring-api/mcspring-rx/src/main/kotlin/in/kyle/mcspring/rx/SchedulerSupport.kt @@ -0,0 +1,79 @@ +package `in`.kyle.mcspring.rx + +import io.reactivex.rxjava3.core.Scheduler +import io.reactivex.rxjava3.disposables.CompositeDisposable +import io.reactivex.rxjava3.disposables.Disposable +import org.bukkit.plugin.Plugin +import org.bukkit.scheduler.BukkitTask +import java.util.concurrent.TimeUnit +import kotlin.math.roundToLong + +private val asyncSchedulers = mutableMapOf() +private val syncSchedulers = mutableMapOf() + +fun Plugin.asyncScheduler(): BukkitScheduler { + return asyncSchedulers.getOrPut(this) { + BukkitScheduler(this, true) + } +} + +fun Plugin.syncScheduler(): BukkitScheduler { + return syncSchedulers.getOrPut(this) { + BukkitScheduler(this, false) + } +} + +class BukkitScheduler(private val plugin: Plugin, private val async: Boolean) : Scheduler() { + + override fun createWorker() = BukkitWorker(plugin, async) + + class BukkitWorker(private val plugin: Plugin, private val async: Boolean) : Worker() { + + private val composite = CompositeDisposable() + + override fun isDisposed() = composite.isDisposed + + override fun schedule(run: Runnable, initialDelay: Long, unit: TimeUnit): Disposable { + val initialTicks = timeUnitToBukkitTicks(initialDelay, unit) + val bukkitTask = bukkitSchedule(run, initialTicks) + + return Disposable.fromRunnable { + bukkitTask.cancel() + }.apply { composite.add(this) } + } + + override fun dispose() = composite.dispose() + + override fun schedulePeriodically(run: Runnable, initialDelay: Long, period: Long, unit: TimeUnit): Disposable { + val initialTicks = timeUnitToBukkitTicks(initialDelay, unit) + val periodTicks = timeUnitToBukkitTicks(period, unit) + val bukkitTask = bukkitSchedulePeriodically(run, initialTicks, periodTicks) + + return Disposable.fromRunnable { + bukkitTask.cancel() + }.apply { composite.add(this) } + } + + private fun timeUnitToBukkitTicks(delayTime: Long, timeUnit: TimeUnit): Long { + return (timeUnit.toMillis(delayTime) * 0.02).roundToLong() + } + + private fun bukkitSchedulePeriodically(runnable: Runnable, delay: Long, period: Long): BukkitTask { + val scheduler = plugin.server.scheduler + return if (async) { + scheduler.runTaskTimerAsynchronously(plugin, runnable, delay, period) + } else { + scheduler.runTaskTimer(plugin, runnable, delay, period) + } + } + + private fun bukkitSchedule(runnable: Runnable, delay: Long): BukkitTask { + val scheduler = plugin.server.scheduler + return if (async) { + scheduler.runTaskLaterAsynchronously(plugin, runnable, delay) + } else { + scheduler.runTaskLater(plugin, runnable, delay) + } + } + } +} diff --git a/mcspring-api/mcspring-subcommands/pom.xml b/mcspring-api/mcspring-subcommands/pom.xml deleted file mode 100644 index 73591cc..0000000 --- a/mcspring-api/mcspring-subcommands/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - mcspring-api - in.kyle.mcspring - 0.0.9 - - 4.0.0 - - mcspring-subcommands - - - - ${project.parent.groupId} - mcspring-base - - - org.bukkit - bukkit - ${spigot.version} - provided - - - org.projectlombok - lombok - - - in.kyle.mcspring - mcspring-test - ${project.parent.version} - test - - - diff --git a/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/command/TabCommandFactory.java b/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/command/TabCommandFactory.java deleted file mode 100644 index 486ac06..0000000 --- a/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/command/TabCommandFactory.java +++ /dev/null @@ -1,65 +0,0 @@ -package in.kyle.mcspring.command; - -import in.kyle.mcspring.subcommands.PluginCommand; -import org.bukkit.command.Command; -import org.bukkit.command.TabCompleter; -import org.bukkit.plugin.Plugin; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.context.annotation.Primary; -import org.springframework.stereotype.Component; - -import java.lang.reflect.Method; -import java.util.Set; -import java.util.function.Consumer; - -import in.kyle.mcspring.subcommands.tab.TabDiscovery; -import lombok.SneakyThrows; -import lombok.var; - -@Primary -@Component -@ConditionalOnBean(Plugin.class) -class TabCommandFactory extends SimpleCommandFactory { - - private final TabDiscovery tabDiscovery; - - public TabCommandFactory(SimpleMethodInjection injection, - Set commandResolvers, - Plugin plugin, - TabDiscovery tabDiscovery) { - super(injection, commandResolvers, plugin); - this.tabDiscovery = tabDiscovery; - } - - @Override - public Command makeCommand(Method method, Object object, String name) { - var command = (org.bukkit.command.PluginCommand) super.makeCommand(method, object, name); - if (method.getParameterCount() == 1 && method.getParameters()[0].getType() - .isAssignableFrom(PluginCommand.class)) { - command.setTabCompleter(makeTabCompleter(method, object)); - } - return command; - } - - private Consumer methodToConsumer(Method method, - Object object) { - return pluginCommand -> { - method.setAccessible(true); - invoke(method, object, pluginCommand); - }; - } - - @SneakyThrows - private Object invoke(Method method, - Object object, - PluginCommand command) { - return method.invoke(object, command); - } - - private TabCompleter makeTabCompleter(Method method, Object object) { - return (sender, command, s, strings) -> { - var consumer = methodToConsumer(method, object); - return tabDiscovery.getCompletions(sender, String.join(" ", strings), consumer); - }; - } -} diff --git a/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/subcommands/Executors.java b/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/subcommands/Executors.java deleted file mode 100644 index 8dffded..0000000 --- a/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/subcommands/Executors.java +++ /dev/null @@ -1,78 +0,0 @@ -package in.kyle.mcspring.subcommands; - -import java.io.Serializable; -import java.lang.invoke.SerializedLambda; -import java.lang.reflect.Method; -import java.util.stream.Stream; - -import lombok.SneakyThrows; - -public interface Executors { - - @SneakyThrows - default Method getMethod(int argCount) { - Method writeReplace = this.getClass().getDeclaredMethod("writeReplace"); - writeReplace.setAccessible(true); - SerializedLambda sl = (SerializedLambda) writeReplace.invoke(this); - String methodName = sl.getImplMethodName(); - Class clazz = Class.forName(sl.getImplClass().replace("/", ".")); - return Stream.of(clazz.getMethods(), clazz.getDeclaredMethods()) - .flatMap(Stream::of) - .filter(m -> m.getName().equals(methodName)) - .filter(m -> m.getParameters().length == argCount) - .findFirst() - .orElseThrow(() -> new RuntimeException("Method not found")); - } - - interface E1 extends Executors, Serializable { - void handle(A a1); - } - - interface E2 extends Executors, Serializable { - void handle(A a, B b); - } - - interface E3 extends Executors, Serializable { - void handle(A a, B b, C c); - } - - interface E4 extends Executors, Serializable { - void handle(A a, B b, C c, D d); - } - - interface E5 extends Executors, Serializable { - void handle(A a, B b, C c, D d, E e); - } - - interface E6 extends Executors, Serializable { - void handle(A a, B b, C c, D d, E e, F f); - } - - interface O0 extends Executors, Serializable { - Object handle(); - } - - interface O1 extends Executors, Serializable { - Object handle(A a1); - } - - interface O2 extends Executors, Serializable { - Object handle(A a, B b); - } - - interface O3 extends Executors, Serializable { - Object handle(A a, B b, C c); - } - - interface O4 extends Executors, Serializable { - Object handle(A a, B b, C c, D d); - } - - interface O5 extends Executors, Serializable { - Object handle(A a, B b, C c, D d, E e); - } - - interface O6 extends Executors, Serializable { - void handle(A a, B b, C c, D d, E e, F f); - } -} diff --git a/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/subcommands/PluginCommand.java b/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/subcommands/PluginCommand.java deleted file mode 100644 index 0462a3f..0000000 --- a/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/subcommands/PluginCommand.java +++ /dev/null @@ -1,172 +0,0 @@ -package in.kyle.mcspring.subcommands; - -import in.kyle.mcspring.command.SimpleMethodInjection; -import org.bukkit.command.CommandSender; - -import java.util.List; - -public class PluginCommand extends PluginCommandBase { - - public PluginCommand(SimpleMethodInjection injection, - CommandSender sender, - List parts, - List injections) { - super(injection, sender, parts, injections); - } - - public void on(String command, Executors.O1 e) { - callOn(command, e, 1); - } - - public void on(String command, Executors.O2 e) { - callOn(command, e, 2); - } - - public void on(String command, Executors.O3 e) { - callOn(command, e, 3); - } - - public void on(String command, Executors.O4 e) { - callOn(command, e, 4); - } - - public void on(String command, Executors.O5 e) { - callOn(command, e, 5); - } - - public void on(String command, Executors.O6 e) { - callOn(command, e, 6); - } - - public void on(String command, Executors.O0 executors) { - callOn(command, executors, 0); - } - - public void on(String command, Executors.E1 e) { - callOn(command, e, 1); - } - - public void on(String command, Executors.E2 e) { - callOn(command, e, 2); - } - - public void on(String command, Executors.E3 e) { - callOn(command, e, 3); - } - - public void on(String command, Executors.E4 e) { - callOn(command, e, 4); - } - - public void on(String command, Executors.E5 e) { - callOn(command, e, 5); - } - - public void on(String command, Executors.E6 e) { - callOn(command, e, 6); - } - - public void then(Executors.O0 e) { - then(() -> invoke(e, 0)); - } - - public void then(Executors.O1 e) { - then(() -> invoke(e, 1)); - } - - public void then(Executors.O2 e) { - then(() -> invoke(e, 2)); - } - - public void then(Executors.O3 e) { - then(() -> invoke(e, 3)); - } - - public void then(Executors.O4 e) { - then(() -> invoke(e, 4)); - } - - public void then(Executors.O5 e) { - then(() -> invoke(e, 5)); - } - - public void then(Executors.O6 e) { - then(() -> invoke(e, 6)); - } - - public void then(Executors.E1 e) { - then(() -> invoke(e, 1)); - } - - public void then(Executors.E2 e) { - then(() -> invoke(e, 2)); - } - - public void then(Executors.E3 e) { - then(() -> invoke(e, 3)); - } - - public void then(Executors.E4 e) { - then(() -> invoke(e, 4)); - } - - public void then(Executors.E5 e) { - then(() -> invoke(e, 5)); - } - - public void then(Executors.E6 e) { - then(() -> invoke(e, 6)); - } - - public void otherwise(Executors.O0 e) { - otherwise(() -> invoke(e, 0)); - } - - public void otherwise(Executors.O1 e) { - otherwise(() -> invoke(e, 1)); - } - - public void otherwise(Executors.O2 e) { - otherwise(() -> invoke(e, 2)); - } - - public void otherwise(Executors.O3 e) { - otherwise(() -> invoke(e, 3)); - } - - public void otherwise(Executors.O4 e) { - otherwise(() -> invoke(e, 4)); - } - - public void otherwise(Executors.O5 e) { - otherwise(() -> invoke(e, 5)); - } - - public void otherwise(Executors.O6 e) { - otherwise(() -> invoke(e, 6)); - } - - public void otherwise(Executors.E1 e) { - otherwise(() -> invoke(e, 1)); - } - - public void otherwise(Executors.E2 e) { - otherwise(() -> invoke(e, 2)); - } - - public void otherwise(Executors.E3 e) { - otherwise(() -> invoke(e, 3)); - } - - public void otherwise(Executors.E4 e) { - otherwise(() -> invoke(e, 4)); - } - - public void otherwise(Executors.E5 e) { - otherwise(() -> invoke(e, 5)); - } - - public void otherwise(Executors.E6 e) { - otherwise(() -> invoke(e, 6)); - } -} diff --git a/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/subcommands/PluginCommandBase.java b/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/subcommands/PluginCommandBase.java deleted file mode 100644 index e52452a..0000000 --- a/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/subcommands/PluginCommandBase.java +++ /dev/null @@ -1,261 +0,0 @@ -package in.kyle.mcspring.subcommands; - -import in.kyle.mcspring.command.SimpleMethodInjection; -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import org.bukkit.Bukkit; -import org.bukkit.ChatColor; -import org.bukkit.command.CommandSender; -import org.bukkit.entity.Player; - -import java.lang.reflect.Method; -import java.util.*; -import java.util.function.*; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -@RequiredArgsConstructor -public class PluginCommandBase { - - final SimpleMethodInjection injection; - final CommandSender sender; - final List parts; - final List injections; - State state = State.CLEAN; - - public void on(String command, Consumer executor) { - if (hasExecutablePart()) { - String part = parts.get(0); - if (command.equalsIgnoreCase(part)) { - parts.remove(0); - executor.accept(copy()); - state = State.EXECUTED; - } - } - } - - private void ifThen(Predicate ifPredicate, Executors executors, int argCount) { - if (state == State.CLEAN && ifPredicate.test(sender)) { - then(() -> invoke(executors, argCount)); - state = State.EXECUTED; - } - } - - public void ifThen(Predicate ifPredicate, Executors.E1 e1) { - ifThen(ifPredicate, e1, 1); - } - - public void withPlayerSender(String error) { - if (state == State.CLEAN && !(sender instanceof Player)) { - sendMessage(error); - state = State.EXECUTED; - } - } - - public void onInvalid(Function help) { - if (hasExecutablePart()) { - String message = help.apply(parts.get(0)); - sendMessage(message); - state = State.EXECUTED; - } - } - - public void otherwise(String message) { - otherwise(() -> message); - } - - public void otherwise(Supplier supplier) { - if (state == State.MISSING_ARG || state == State.CLEAN) { - String message = supplier.get(); - sendMessage(message); - state = State.EXECUTED; - } - } - - public void with(Function> processor, Function error) { - if (hasExecutablePart()) { - String part = parts.remove(0); - Optional apply = processor.apply(part); - if (apply.isPresent()) { - injections.add(apply.get()); - } else { - String message = error.apply(part); - sendMessage(message); - state = State.INVALID_ARG; - } - } else { - state = State.MISSING_ARG; - } - } - - public void withString() { - with(Optional::of, null); - } - - public void withSentence() { - if (hasExecutablePart()) { - String part = String.join(" ", parts); - injections.add(part); - parts.clear(); - } - } - - public void withOfflinePlayer(String notPlayer) { - withOfflinePlayer(s -> notPlayer); - } - - public void withOfflinePlayer(Function notPlayer) { - with(s -> Optional.ofNullable(Bukkit.getOfflinePlayer(s)), notPlayer); - } - - public void withPlayer(String notPlayer) { - withPlayer(s -> notPlayer); - } - - public void withPlayer(Function notPlayer) { - with(s -> Optional.ofNullable(Bukkit.getPlayerExact(s)), notPlayer); - } - - public void withWorld(String notWorld) { - withWorld(s -> notWorld); - } - - public void withWorld(Function notWorld) { - with(s -> Bukkit.getWorlds() - .stream() - .filter(w -> w.getName().equalsIgnoreCase(s)) - .findFirst(), notWorld); - } - - public void withMap(Map options, Function invalidArg) { - if (hasExecutablePart()) { - String part = parts.remove(0).toLowerCase(); - if (options.containsKey(part)) { - injections.add(options.get(part)); - } else { - String message = invalidArg.apply(part); - sendMessage(message); - state = State.INVALID_ARG; - } - } else { - state = State.MISSING_ARG; - } - } - - public void withAny(Collection options, Function invalidArg) { - BinaryOperator merge = (a, b) -> { - throw new RuntimeException("Duplicate option " + a); - }; - Map optionsMap = options.stream() - .collect(Collectors.toMap(Function.identity(), - Function.identity(), - merge, - LinkedHashMap::new)); - withMap(optionsMap, invalidArg); - } - - public void withAny(Function invalidArg, String... options) { - withAny(Arrays.asList(options), invalidArg); - } - - public void withOnlinePlayer(Function playerNotFound) { - with(s -> Optional.ofNullable(Bukkit.getPlayer(s)), playerNotFound); - } - - public void withInt(String notInteger) { - withInt(a -> notInteger); - } - - public void withInt(Function notInteger) { - with(this::tryParseInt, notInteger); - } - - public void withDouble(String notDouble) { - withDouble(a -> notDouble); - } - - public void withDouble(Function notDouble) { - with(this::tryParseDouble, notDouble); - } - - private Optional tryParseInt(String intString) { - try { - return Optional.of(Integer.parseInt(intString)); - } catch (NumberFormatException e) { - return Optional.empty(); - } - } - - private Optional tryParseDouble(String intString) { - try { - return Optional.of(Double.parseDouble(intString)); - } catch (NumberFormatException e) { - return Optional.empty(); - } - } - - protected void callOn(String command, Executors executors, int argSize) { - if(hasExecutablePart()) { - if(parts.get(0).equalsIgnoreCase(command)) { - parts.remove(0); - then(() -> invoke(executors, argSize)); - } - } else { - state = State.MISSING_ARG; - } - } - - public void otherwise(Runnable r) { - if (state == State.MISSING_ARG || state == State.CLEAN) { - r.run(); - state = State.EXECUTED; - } - } - - public void then(Runnable r) { - if (parts.size() == 0 && state == State.CLEAN) { - r.run(); - state = State.EXECUTED; - } - } - - void sendMessage(String message) { - sender.sendMessage(ChatColor.translateAlternateColorCodes('&', message)); - } - - PluginCommand copy() { - return new PluginCommand(injection, sender, parts, injections); - } - - @SneakyThrows - protected void invoke(Executors executors, int argCount) { - Method method = executors.getMethod(argCount); - injections.add(sender); - Object[] objects = injections.toArray(new Object[0]); - Object[] parameters = injection.getParameters(method, Collections.emptyList(), objects); - Method handleMethod = getHandleMethod(executors); - handleMethod.setAccessible(true); - Object output = handleMethod.invoke(executors, parameters); - if (output != null) { - sendMessage(output.toString()); - } - } - - boolean hasExecutablePart() { - return parts.size() > 0 && state == State.CLEAN; - } - - private Method getHandleMethod(Executors executors) { - return Stream.of(executors.getClass().getDeclaredMethods()) - .filter(m -> m.getName().equals("handle")) - .findFirst() - .orElseThrow(RuntimeException::new); - } - - public enum State { - CLEAN, - MISSING_ARG, - INVALID_ARG, - EXECUTED - } -} diff --git a/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/subcommands/PluginCommandResolver.java b/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/subcommands/PluginCommandResolver.java deleted file mode 100644 index 2ef1ef7..0000000 --- a/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/subcommands/PluginCommandResolver.java +++ /dev/null @@ -1,30 +0,0 @@ -package in.kyle.mcspring.subcommands; - -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Optional; - -import in.kyle.mcspring.command.CommandResolver; -import in.kyle.mcspring.command.Resolver; -import in.kyle.mcspring.command.SimpleMethodInjection; -import lombok.AllArgsConstructor; - -@Component -@AllArgsConstructor -class PluginCommandResolver implements CommandResolver { - - private final SimpleMethodInjection injection; - - @Override - public Resolver makeResolver(Command command) { - return parameter -> parameter.getType().isAssignableFrom(PluginCommand.class) ? Optional.of( - makeCommand(command)) : Optional.empty(); - } - - private PluginCommand makeCommand(Command command) { - ArrayList args = new ArrayList<>(Arrays.asList(command.getArgs())); - return new PluginCommand(injection, command.getSender(), args, new ArrayList<>()); - } -} diff --git a/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/subcommands/PluginCommandTabCompletable.java b/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/subcommands/PluginCommandTabCompletable.java deleted file mode 100644 index 0f3c076..0000000 --- a/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/subcommands/PluginCommandTabCompletable.java +++ /dev/null @@ -1,102 +0,0 @@ -package in.kyle.mcspring.subcommands; - -import org.bukkit.command.CommandSender; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import in.kyle.mcspring.command.SimpleMethodInjection; -import lombok.Getter; -import lombok.var; - -public class PluginCommandTabCompletable extends PluginCommand { - - @Getter - private final List tabCompletionOptions = new ArrayList<>(); - @Getter - private PluginCommandTabCompletable child; - - public PluginCommandTabCompletable(SimpleMethodInjection injection, - CommandSender sender, - List parts) { - super(injection, sender, parts, Collections.emptyList()); - } - - public State getState() { - return state; - } - - public boolean hasChild() { - return child != null; - } - - @Override - public void on(String command, Consumer executor) { - tabCompletionOptions.add(command); - super.on(command, executor); - } - - @Override - public void onInvalid(Function help) { - state = State.INVALID_ARG; - } - - @Override - public void with(Function> processor, Function error) { - tabCompletionOptions.clear(); - if (hasExecutablePart()) { - parts.remove(0); - state = State.INVALID_ARG; - } else { - state = State.MISSING_ARG; - } - } - - @Override - public void withMap(Map options, Function invalidArg) { - if (hasExecutablePart()) { - Set validOptions = options.keySet(); - String part = parts.remove(0).toLowerCase(); - if (!validOptions.contains(part)) { - state = State.INVALID_ARG; - } - } else { - state = State.MISSING_ARG; - tabCompletionOptions.addAll(options.keySet()); - } - } - - @Override - protected void callOn(String command, Executors executors, int argSize) { - tabCompletionOptions.add(command); - super.callOn(command, executors, argSize); - } - - @Override - public void otherwise(Supplier supplier) { - } - - - @Override - public void then(Runnable r) { - } - - @Override - PluginCommand copy() { - var command = - new PluginCommandTabCompletable(injection, sender, parts); - if (child == null) { - child = command; - return command; - } else { - throw new IllegalStateException("Command cannot have 2 children"); - } - } -} diff --git a/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/subcommands/tab/TabDiscovery.java b/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/subcommands/tab/TabDiscovery.java deleted file mode 100644 index 014453a..0000000 --- a/mcspring-api/mcspring-subcommands/src/main/java/in/kyle/mcspring/subcommands/tab/TabDiscovery.java +++ /dev/null @@ -1,51 +0,0 @@ -package in.kyle.mcspring.subcommands.tab; - -import org.bukkit.command.CommandSender; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.function.Consumer; - -import in.kyle.mcspring.command.SimpleMethodInjection; -import in.kyle.mcspring.subcommands.PluginCommand; -import in.kyle.mcspring.subcommands.PluginCommandTabCompletable; -import lombok.RequiredArgsConstructor; -import lombok.var; - -@Component -@RequiredArgsConstructor -public class TabDiscovery { - - private final SimpleMethodInjection injection; - - public List getCompletions(CommandSender sender, - String commandString, - Consumer consumer) { - if (!commandString.isEmpty() && !commandString.endsWith(" ")) { - // TODO: 2020-02-25 Enable partial completions - return Collections.emptyList(); - } - var parts = new ArrayList<>(Arrays.asList(commandString.split(" "))); - if (parts.get(0).isEmpty()) { - parts.remove(0); - } - var command = new PluginCommandTabCompletable(injection, sender, parts); - consumer.accept(command); - - return getCompletions(command); - } - - private List getCompletions(PluginCommandTabCompletable command) { - if (command.hasChild()) { - return getCompletions(command.getChild()); - } else if (command.getState() == PluginCommand.State.MISSING_ARG || - command.getState() == PluginCommand.State.CLEAN) { - return command.getTabCompletionOptions(); - } else { - return Collections.emptyList(); - } - } -} diff --git a/mcspring-api/mcspring-subcommands/src/test/java/in/kyle/mcspring/subcommands/TestConsole.java b/mcspring-api/mcspring-subcommands/src/test/java/in/kyle/mcspring/subcommands/TestConsole.java deleted file mode 100644 index 7c0d04c..0000000 --- a/mcspring-api/mcspring-subcommands/src/test/java/in/kyle/mcspring/subcommands/TestConsole.java +++ /dev/null @@ -1,28 +0,0 @@ -package in.kyle.mcspring.subcommands; - -import org.bukkit.command.CommandSender; -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.function.Consumer; - -import in.kyle.mcspring.command.SimpleMethodInjection; -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class TestConsole { - - private final SimpleMethodInjection injection; - - public void run(CommandSender sender, String commandString, Consumer consumer) { - List parts = new ArrayList<>(Arrays.asList(commandString.split(" "))); - if (parts.get(0).isEmpty()) { - parts.remove(0); - } - PluginCommand command = new PluginCommand(injection, sender, parts, new ArrayList<>()); - consumer.accept(command); - } -} diff --git a/mcspring-api/mcspring-subcommands/src/test/java/in/kyle/mcspring/subcommands/TestPluginCommand.java b/mcspring-api/mcspring-subcommands/src/test/java/in/kyle/mcspring/subcommands/TestPluginCommand.java deleted file mode 100644 index 1f6ceb0..0000000 --- a/mcspring-api/mcspring-subcommands/src/test/java/in/kyle/mcspring/subcommands/TestPluginCommand.java +++ /dev/null @@ -1,208 +0,0 @@ -package in.kyle.mcspring.subcommands; - -import org.bukkit.command.CommandSender; -import org.bukkit.permissions.ServerOperator; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -import in.kyle.api.bukkit.entity.TestPlayer; -import in.kyle.mcspring.test.MCSpringTest; - -import static org.assertj.core.api.Assertions.assertThat; - -@MCSpringTest -class TestPluginCommand { - - @Autowired - TestConsole console; - @Autowired - TestPlayer sender; - List outputMessages; - - @BeforeEach - void setup() { - outputMessages = new ArrayList<>(); - sender.getMessages().subscribe(outputMessages::add); - } - - @Test - void testDirectExecutor() { - class Test { - - void exec1(PluginCommand command) { - command.on("test", this::handler1); - } - - void exec2(PluginCommand command) { - command.withString(); - command.on("test2", this::handler2); - } - - void handler1(CommandSender sender) { - sender.sendMessage("Hello World"); - } - - void handler2(CommandSender sender, String string) { - sender.sendMessage(string); - } - } - - Test test = new Test(); - console.run(sender, "test", test::exec1); - assertThat(outputMessages).containsExactly("Hello World"); - outputMessages.clear(); - console.run(sender, "hello test2", test::exec2); - assertThat(outputMessages).containsExactly("hello"); - } - - @Test - void testSenderArg() { - class Test { - void root(PluginCommand command) { - command.then(this::exec); - } - - void exec(CommandSender sender) { - sender.sendMessage("Hello World"); - } - } - Test test = new Test(); - console.run(sender, "", test::root); - assertThat(outputMessages).containsExactly("Hello World"); - } - - @Test - void testIf() { - class Test { - void root(PluginCommand command) { - command.ifThen(ServerOperator::isOp, this::exec); - } - - void exec(CommandSender sender) { - sender.sendMessage("Works"); - } - } - Test test = new Test(); - console.run(sender, "", test::root); - assertThat(outputMessages).isEmpty(); - - sender.setOp(true); - console.run(sender, "", test::root); - assertThat(outputMessages).containsExactly("Works"); - } - - @Test - void testCommandSingleSentenceArg() { - class Test { - void root(PluginCommand command) { - command.withSentence(); - command.then(this::exec); - } - - void exec(String sentence) { - sender.sendMessage(sentence); - } - } - Test test = new Test(); - console.run(sender, "Hello to you world", test::root); - assertThat(outputMessages).containsExactly("Hello to you world"); - } - - @Test - void testCommandIntArgs() { - AtomicBoolean ran = new AtomicBoolean(); - class Test { - void root(PluginCommand command) { - command.withInt("error"); - command.withInt("error"); - command.withInt("error"); - command.then(this::exec); - } - - void exec(int i1, int i2, int i3) { - assertThat(i1).isEqualTo(1); - assertThat(i2).isEqualTo(2); - assertThat(i3).isEqualTo(3); - ran.set(true); - } - } - Test test = new Test(); - console.run(sender, "1 2 3", test::root); - assertThat(outputMessages).isEmpty(); - assertThat(ran).isTrue(); - } - - @Test - void testCommandBranching() { - class Test { - void root(PluginCommand command) { - command.on("a", this::a); - } - - void a(PluginCommand command) { - command.on("b", this::b); - command.on("c", this::c); - } - - private void b(PluginCommand command) { - command.then(this::exec); - } - - private void c(PluginCommand command) { - } - - void exec(CommandSender sender) { - sender.sendMessage("Works"); - } - } - Test test = new Test(); - console.run(sender, "a b", test::root); - assertThat(outputMessages).containsExactly("Works"); - outputMessages.clear(); - console.run(sender, "a c", test::root); - assertThat(outputMessages).isEmpty(); - } - - @Test - void testOtherwise() { - class Test { - void root(PluginCommand command) { - command.on("a", this::a); - command.otherwise("no subcommand at root"); - } - - void a(PluginCommand command) { - command.withInt("error"); - command.otherwise("should run if int passed or missing arg"); - } - } - Test test = new Test(); - console.run(sender, "", test::root); - assertThat(outputMessages).containsExactly("no subcommand at root"); - outputMessages.clear(); - - console.run(sender, "a", test::root); - assertThat(outputMessages).containsExactly("should run if int passed or missing arg"); - outputMessages.clear(); - - console.run(sender, "a 2", test::root); - assertThat(outputMessages).containsExactly("should run if int passed or missing arg"); - } - - @Test - void testWithError() { - class Test { - void root(PluginCommand command) { - command.withInt(s -> s + " is not an int"); - } - } - Test test = new Test(); - console.run(sender, "swag", test::root); - assertThat(outputMessages).containsExactly("swag is not an int"); - } -} diff --git a/mcspring-api/mcspring-subcommands/src/test/java/in/kyle/mcspring/subcommands/TestTabCompletion.java b/mcspring-api/mcspring-subcommands/src/test/java/in/kyle/mcspring/subcommands/TestTabCompletion.java deleted file mode 100644 index 768d411..0000000 --- a/mcspring-api/mcspring-subcommands/src/test/java/in/kyle/mcspring/subcommands/TestTabCompletion.java +++ /dev/null @@ -1,166 +0,0 @@ -package in.kyle.mcspring.subcommands; - -import in.kyle.mcspring.test.MCSpringTest; -import org.bukkit.command.CommandSender; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; -import java.util.function.Function; - -import in.kyle.api.bukkit.entity.TestPlayer; -import in.kyle.mcspring.subcommands.tab.TabDiscovery; -import lombok.var; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; - -@MCSpringTest -public class TestTabCompletion { - - @Autowired - TestPlayer sender; - @Autowired - TabDiscovery tabDiscovery; - List outputMessages; - - @BeforeEach - void setup() { - outputMessages = new ArrayList<>(); - sender.getMessages().subscribe(outputMessages::add); - } - - @Test - void testTabWithDirectExecution() { - class Test { - - void root(PluginCommand command) { - command.on("test", this::exec); - } - - void exec(String string) { - fail("Should not run"); - } - } - Test test = new Test(); - var completions = tabDiscovery.getCompletions(sender, "", test::root); - assertThat(completions).containsExactly("test"); - } - - @Test - void testNoTab() { - class Test { - void root(PluginCommand command) { - command.then(this::exec); - } - - void exec(CommandSender sender) { - fail("Should not run"); - } - } - - Test test = new Test(); - var completions = tabDiscovery.getCompletions(sender, "", test::root); - - assertThat(outputMessages).isEmpty(); - assertThat(completions).isEmpty(); - } - - @Test - void testSimpleSubs() { - class Test { - void root(PluginCommand command) { - Consumer dontRun = cmd -> fail("should not run"); - command.on("a", dontRun); - command.on("b", dontRun); - command.on("c", dontRun); - command.then(this::exec); - } - - void exec(CommandSender sender) { - fail("Should not run"); - } - } - - Test test = new Test(); - var completions = tabDiscovery.getCompletions(sender, "", test::root); - - assertThat(outputMessages).isEmpty(); - assertThat(completions).containsSequence("a", "b", "c"); - } - - @Test - void testAny() { - class Test { - void root(PluginCommand command) { - Function f = ignored -> { - fail("should not run"); - return ""; - }; - command.withAny(f, "a", "b", "c"); - command.withAny(f, "d", "e", "f"); - command.then(this::exec); - } - - void exec(CommandSender sender) { - fail("Should not run"); - } - } - - Test test = new Test(); - var completions = tabDiscovery.getCompletions(sender, "", test::root); - - assertThat(outputMessages).isEmpty(); - assertThat(completions).containsSequence("a", "b", "c", "d", "e", "f"); - - } - - @Test - void testOnTake1() { - class Test { - void root(PluginCommand command) { - Consumer doesNothing = cmd -> {}; - command.on("a", doesNothing); - command.on("b", doesNothing); - command.on("c", doesNothing); - command.then(this::exec); - } - - void exec(CommandSender sender) { - fail("Should not run"); - } - } - - Test test = new Test(); - var completions = tabDiscovery.getCompletions(sender, "", test::root); - assertThat(completions).containsSequence("a", "b", "c"); - - completions = tabDiscovery.getCompletions(sender, "a", test::root); - assertThat(completions).isEmpty(); - } - - @Test - void testInvalidStop() { - class Test { - void root(PluginCommand command) { - Consumer doesNothing = cmd -> fail("should not run"); - command.on("a", doesNothing); - command.on("b", doesNothing); - command.on("c", doesNothing); - command.onInvalid(s -> String.format("%s is not valid", s)); - command.then(this::exec); - } - - void exec(CommandSender sender) { - fail("Should not run"); - } - } - - Test test = new Test(); - var completions = tabDiscovery.getCompletions(sender, "g", test::root); - assertThat(completions).isEmpty(); - } -} diff --git a/mcspring-api/mcspring-subcommands/src/test/resources/application.properties b/mcspring-api/mcspring-subcommands/src/test/resources/application.properties deleted file mode 100644 index 1494a77..0000000 --- a/mcspring-api/mcspring-subcommands/src/test/resources/application.properties +++ /dev/null @@ -1,2 +0,0 @@ -main=test -debug=true diff --git a/mcspring-api/mcspring-test/pom.xml b/mcspring-api/mcspring-test/pom.xml deleted file mode 100644 index 7f1245d..0000000 --- a/mcspring-api/mcspring-test/pom.xml +++ /dev/null @@ -1,70 +0,0 @@ - - - - mcspring-api - in.kyle.mcspring - 0.0.9 - - 4.0.0 - - mcspring-test - - - - jitpack.io - https://jitpack.io - - - - - - org.spigotmc - spigot-api - - - in.kyle.mcspring - mcspring-base - ${project.parent.version} - - - org.springframework.boot - spring-boot-test - - - org.projectlombok - lombok - - - com.github.kylepls - BukkitTest - 0.0.1 - - - org.assertj - assertj-core - 3.15.0 - - - org.springframework.boot - spring-boot-starter-test - ${spring.version} - - - junit - junit - - - org.junit.vintage - junit-vintage-engine - - - - - org.junit.jupiter - junit-jupiter-engine - 5.3.2 - - - diff --git a/mcspring-api/mcspring-test/src/main/java/in/kyle/mcspring/test/MCSpringTest.java b/mcspring-api/mcspring-test/src/main/java/in/kyle/mcspring/test/MCSpringTest.java deleted file mode 100644 index cd498ac..0000000 --- a/mcspring-api/mcspring-test/src/main/java/in/kyle/mcspring/test/MCSpringTest.java +++ /dev/null @@ -1,16 +0,0 @@ -package in.kyle.mcspring.test; - -import org.springframework.boot.test.context.SpringBootTest; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Inherited; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -@Target(ElementType.TYPE) -@Retention(RetentionPolicy.RUNTIME) -@Inherited -@SpringBootTest(classes = SpringSpigotSupport.class) -public @interface MCSpringTest { -} diff --git a/mcspring-api/mcspring-test/src/main/java/in/kyle/mcspring/test/SpringSpigotSupport.java b/mcspring-api/mcspring-test/src/main/java/in/kyle/mcspring/test/SpringSpigotSupport.java deleted file mode 100644 index 81d8ea0..0000000 --- a/mcspring-api/mcspring-test/src/main/java/in/kyle/mcspring/test/SpringSpigotSupport.java +++ /dev/null @@ -1,45 +0,0 @@ -package in.kyle.mcspring.test; - -import org.springframework.boot.SpringBootConfiguration; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.info.BuildProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.Scope; - -import java.util.Properties; - -import in.kyle.api.bukkit.TestServer; -import in.kyle.api.bukkit.entity.TestPlayer; -import in.kyle.api.generate.api.Generator; -import in.kyle.mcspring.SpringPlugin; -import in.kyle.mcspring.command.SimpleMethodInjection; - -@SpringBootConfiguration -@EnableAutoConfiguration -@ComponentScan(basePackageClasses = {SpringSpigotSupport.class, SpringPlugin.class}) -@Import(SimpleMethodInjection.class) -class SpringSpigotSupport { - - @Bean - Generator generator() { - return Generator.create(); - } - - @Bean - @Scope("prototype") - TestPlayer player(Generator generator) { - return generator.create(TestPlayer.class); - } - - @Bean - TestServer server(Generator generator) { - return generator.create(TestServer.class); - } - - @Bean - BuildProperties buildProperties() { - return new BuildProperties(new Properties()); - } -} diff --git a/mcspring-api/mcspring-test/src/main/java/in/kyle/mcspring/test/command/TestCommandExecutor.java b/mcspring-api/mcspring-test/src/main/java/in/kyle/mcspring/test/command/TestCommandExecutor.java deleted file mode 100644 index a2f4721..0000000 --- a/mcspring-api/mcspring-test/src/main/java/in/kyle/mcspring/test/command/TestCommandExecutor.java +++ /dev/null @@ -1,49 +0,0 @@ -package in.kyle.mcspring.test.command; - -import org.springframework.stereotype.Component; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.stream.Collectors; - -import in.kyle.api.bukkit.TestCommandSender; -import in.kyle.api.bukkit.entity.TestPlayer; -import in.kyle.api.generate.api.Generator; -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -public class TestCommandExecutor { - - private final TestCommandRegistration registration; - - public List run(String command) { - TestPlayer player = Generator.create().create(TestPlayer.class); - return run(player, command); - } - - public List run(TestCommandSender sender, String command) { - command = command.trim(); - List parts = new ArrayList<>(Arrays.asList(command.split(" "))); - if (parts.get(0).isEmpty()) { - parts.remove(0); - } - - if (parts.size() != 0) { - String label = parts.get(0); - String[] args = parts.subList(1, parts.size()).toArray(new String[0]); - - List output = new ArrayList<>(); - sender.getMessages().subscribe(output::add); - - registration.run(label, sender, label, args); - - return output.stream() - .flatMap(s -> Arrays.stream(s.split("\n"))) - .collect(Collectors.toList()); - } else { - throw new RuntimeException("Empty command"); - } - } -} diff --git a/mcspring-api/mcspring-test/src/main/java/in/kyle/mcspring/test/command/TestCommandRegistration.java b/mcspring-api/mcspring-test/src/main/java/in/kyle/mcspring/test/command/TestCommandRegistration.java deleted file mode 100644 index 4fa3cbb..0000000 --- a/mcspring-api/mcspring-test/src/main/java/in/kyle/mcspring/test/command/TestCommandRegistration.java +++ /dev/null @@ -1,68 +0,0 @@ -package in.kyle.mcspring.test.command; - -import org.bukkit.command.CommandSender; -import org.springframework.stereotype.Component; - -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import in.kyle.mcspring.command.Command; -import in.kyle.mcspring.command.CommandRegistration; -import in.kyle.mcspring.command.CommandResolver; -import in.kyle.mcspring.command.SimpleMethodInjection; -import lombok.RequiredArgsConstructor; -import lombok.var; - -@Component -@RequiredArgsConstructor -class TestCommandRegistration implements CommandRegistration { - - private final SimpleMethodInjection injection; - private final Set commandResolvers; - - private final Map commandExecutors = new HashMap<>(); - - @Override - public void register(Command command, Method method, Object object) { - var executor = makeExecutor(method, object); - getAllNames(command).forEach(key -> commandExecutors.put(key, executor)); - } - - private List getAllNames(Command command) { - List commands = new ArrayList<>(Arrays.asList(command.aliases())); - commands.add(command.value()); - return commands; - } - - private Executor makeExecutor(Method method, Object object) { - return (sender, label, args) -> { - var temp = new CommandResolver.Command(sender, args, label); - var contextResolvers = commandResolvers.stream() - .map(r -> r.makeResolver(temp)) - .collect(Collectors.toList()); - Object result = injection.invoke(method, object, contextResolvers, sender, args, label); - if (result != null) { - sender.sendMessage(result.toString()); - } - }; - } - - public void run(String command, CommandSender sender, String label, String[] args) { - if (commandExecutors.containsKey(command)) { - commandExecutors.get(command).execute(sender, label, args); - } else { - throw new RuntimeException( - "Command " + command + " is not registered. Make sure to @Import it"); - } - } - - interface Executor { - void execute(CommandSender sender, String label, String[] args); - } -} diff --git a/mcspring-api/mcspring-vault/build.gradle.kts b/mcspring-api/mcspring-vault/build.gradle.kts new file mode 100644 index 0000000..ea55dc5 --- /dev/null +++ b/mcspring-api/mcspring-vault/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("org.jetbrains.dokka") +} + +repositories { + maven { url = uri("https://jitpack.io") } +} + +dependencies { + api(project(":mcspring-api:mcspring-base")) + api("com.github.MilkBowl:VaultAPI:1.7") { + exclude(group = "org.bukkit", module = "bukkit") + } +} diff --git a/mcspring-api/mcspring-vault/pom.xml b/mcspring-api/mcspring-vault/pom.xml deleted file mode 100644 index 4f13e62..0000000 --- a/mcspring-api/mcspring-vault/pom.xml +++ /dev/null @@ -1,59 +0,0 @@ - - - - mcspring-api - in.kyle.mcspring - 0.0.9 - - 4.0.0 - - - 1.7.3 - - mcspring-vault - - - - vault-repo - http://nexus.hc.to/content/repositories/pub_releases - - - - - - ${project.parent.groupId} - mcspring-base - - - net.milkbowl.vault - Vault - ${vault.version} - provided - - - * - * - - - - - org.spigotmc - spigot-api - - - - - - - org.apache.maven.plugins - maven-source-plugin - - - org.apache.maven.plugins - maven-javadoc-plugin - - - - diff --git a/mcspring-api/mcspring-vault/src/main/java/in/kyle/mcspring/economy/EconomyException.java b/mcspring-api/mcspring-vault/src/main/java/in/kyle/mcspring/economy/EconomyException.java deleted file mode 100644 index be27449..0000000 --- a/mcspring-api/mcspring-vault/src/main/java/in/kyle/mcspring/economy/EconomyException.java +++ /dev/null @@ -1,8 +0,0 @@ -package in.kyle.mcspring.economy; - -public class EconomyException extends RuntimeException { - - EconomyException(String message) { - super(message); - } -} diff --git a/mcspring-api/mcspring-vault/src/main/java/in/kyle/mcspring/economy/EconomyService.java b/mcspring-api/mcspring-vault/src/main/java/in/kyle/mcspring/economy/EconomyService.java deleted file mode 100644 index 085616e..0000000 --- a/mcspring-api/mcspring-vault/src/main/java/in/kyle/mcspring/economy/EconomyService.java +++ /dev/null @@ -1,25 +0,0 @@ -package in.kyle.mcspring.economy; - -import org.bukkit.OfflinePlayer; - -import java.math.BigDecimal; - -public interface EconomyService { - - void deposit(OfflinePlayer player, BigDecimal amount); - - void withdraw(OfflinePlayer player, BigDecimal amount); - - void transfer(OfflinePlayer origin, OfflinePlayer destination, BigDecimal amount); - - boolean hasAmount(OfflinePlayer player, BigDecimal amount); - - void createAccount(OfflinePlayer player); - - String format(BigDecimal amount); - - BigDecimal getBalance(OfflinePlayer player); - - boolean hasAccount(OfflinePlayer player); - -} diff --git a/mcspring-api/mcspring-vault/src/main/java/in/kyle/mcspring/economy/VaultEconomyService.java b/mcspring-api/mcspring-vault/src/main/java/in/kyle/mcspring/economy/VaultEconomyService.java deleted file mode 100644 index d09b204..0000000 --- a/mcspring-api/mcspring-vault/src/main/java/in/kyle/mcspring/economy/VaultEconomyService.java +++ /dev/null @@ -1,71 +0,0 @@ -package in.kyle.mcspring.economy; - -import net.milkbowl.vault.economy.Economy; -import net.milkbowl.vault.economy.EconomyResponse; - -import org.bukkit.OfflinePlayer; -import org.bukkit.Server; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Service; - -import java.math.BigDecimal; - -@Lazy -@Service -@ConditionalOnClass(Economy.class) -class VaultEconomyService implements EconomyService { - - private final Economy economy; - - public VaultEconomyService(Server server) { - economy = server.getServicesManager().getRegistration(Economy.class).getProvider(); - } - - @Override - public void deposit(OfflinePlayer player, BigDecimal amount) { - assertEconomyResponse(economy.depositPlayer(player, amount.doubleValue())); - } - - @Override - public void withdraw(OfflinePlayer player, BigDecimal amount) { - assertEconomyResponse(economy.withdrawPlayer(player, amount.doubleValue())); - } - - @Override - public void transfer(OfflinePlayer origin, OfflinePlayer destination, BigDecimal amount) { - withdraw(origin, amount); - deposit(destination, amount); - } - - @Override - public boolean hasAmount(OfflinePlayer player, BigDecimal amount) { - return economy.has(player, amount.doubleValue()); - } - - @Override - public void createAccount(OfflinePlayer player) { - economy.createPlayerAccount(player); - } - - @Override - public String format(BigDecimal amount) { - return economy.format(amount.doubleValue()); - } - - @Override - public BigDecimal getBalance(OfflinePlayer player) { - return BigDecimal.valueOf(economy.getBalance(player)); - } - - @Override - public boolean hasAccount(OfflinePlayer player) { - return economy.hasAccount(player); - } - - private void assertEconomyResponse(EconomyResponse response) { - if (response.type != EconomyResponse.ResponseType.SUCCESS) { - throw new EconomyException(response.errorMessage); - } - } -} diff --git a/mcspring-api/mcspring-vault/src/main/kotlin/in/kyle/mcspring/vault/economy/EconomyException.kt b/mcspring-api/mcspring-vault/src/main/kotlin/in/kyle/mcspring/vault/economy/EconomyException.kt new file mode 100644 index 0000000..66e07f6 --- /dev/null +++ b/mcspring-api/mcspring-vault/src/main/kotlin/in/kyle/mcspring/vault/economy/EconomyException.kt @@ -0,0 +1,3 @@ +package `in`.kyle.mcspring.vault.economy + +class EconomyException internal constructor(message: String) : RuntimeException(message) diff --git a/mcspring-api/mcspring-vault/src/main/kotlin/in/kyle/mcspring/vault/economy/EconomyService.kt b/mcspring-api/mcspring-vault/src/main/kotlin/in/kyle/mcspring/vault/economy/EconomyService.kt new file mode 100644 index 0000000..96fbef5 --- /dev/null +++ b/mcspring-api/mcspring-vault/src/main/kotlin/in/kyle/mcspring/vault/economy/EconomyService.kt @@ -0,0 +1,60 @@ +package `in`.kyle.mcspring.vault.economy + +import org.bukkit.OfflinePlayer +import java.math.BigDecimal + +// https://github.com/MilkBowl/VaultAPI/blob/master/src/main/java/net/milkbowl/vault/economy/Economy.java +/** + * A wrapper for the [VaultAPI](http://milkbowl.github.io/VaultAPI/) + */ +interface EconomyService { + + /** + * Attempts to create a player account for the given player + * @return if the account creation was successful + */ + fun createAccount(player: OfflinePlayer) + + /** + * Deposit a **non-negative** balance to a player + * @throws EconomyException if the transaction fails + */ + fun deposit(player: OfflinePlayer, amount: BigDecimal) + + /** + * Format amount into a human readable String This provides translation into + * economy specific formatting to improve consistency between plugins. + * @return Human-readable string describing amount + */ + fun format(amount: BigDecimal): String + + /** + * Gets balance of a player + * @return Amount currently held in players account + */ + fun getBalance(player: OfflinePlayer): BigDecimal + + /** + * Checks if the player account has the **non-negative** amount + * @return True if [player] has [amount], False else wise + */ + fun hasAmount(player: OfflinePlayer, amount: BigDecimal): Boolean + + /** + * Checks if this player has an account on the server yet + * This will always return true if the player has joined the server at least once + * as all major economy plugins auto-generate a player account when the player joins the server + * @return if the player has an account + */ + fun hasAccount(player: OfflinePlayer): Boolean + + /** + * Transfer an amount between two players + */ + fun transfer(origin: OfflinePlayer, destination: OfflinePlayer, amount: BigDecimal) + + /** + * Withdraw a **non-negative** amount from a player + */ + fun withdraw(player: OfflinePlayer, amount: BigDecimal) +} diff --git a/mcspring-api/mcspring-vault/src/main/kotlin/in/kyle/mcspring/vault/economy/VaultEconomyService.kt b/mcspring-api/mcspring-vault/src/main/kotlin/in/kyle/mcspring/vault/economy/VaultEconomyService.kt new file mode 100644 index 0000000..eb56462 --- /dev/null +++ b/mcspring-api/mcspring-vault/src/main/kotlin/in/kyle/mcspring/vault/economy/VaultEconomyService.kt @@ -0,0 +1,72 @@ +package `in`.kyle.mcspring.vault.economy + +import `in`.kyle.mcspring.annotation.PluginDepend +import net.milkbowl.vault.economy.Economy +import net.milkbowl.vault.economy.EconomyResponse +import org.bukkit.OfflinePlayer +import org.bukkit.Server +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass +import org.springframework.context.annotation.Lazy +import org.springframework.stereotype.Service +import java.math.BigDecimal + +@Suppress("unused") +@Lazy +@Service +@ConditionalOnClass(Economy::class) +@PluginDepend("Vault") +internal class VaultEconomyService(server: Server) : EconomyService { + + private val economy: Economy = server.servicesManager.getRegistration(Economy::class.java)!!.provider + + override fun deposit(player: OfflinePlayer, amount: BigDecimal) { + requirePositive(amount) + assertEconomyResponse(economy.depositPlayer(player, amount.toDouble())) + } + + override fun withdraw(player: OfflinePlayer, amount: BigDecimal) { + requirePositive(amount) + assertEconomyResponse(economy.withdrawPlayer(player, amount.toDouble())) + } + + override fun transfer(origin: OfflinePlayer, destination: OfflinePlayer, amount: BigDecimal) { + requirePositive(amount) + require(hasAmount(origin, amount)) { "Player ${origin.name} does not have the required amount $amount" } + withdraw(origin, amount) + if (!hasAccount(destination)) { + createAccount(destination) + } + deposit(destination, amount) + } + + override fun hasAmount(player: OfflinePlayer, amount: BigDecimal): Boolean { + requirePositive(amount) + return economy.has(player, amount.toDouble()) + } + + override fun createAccount(player: OfflinePlayer) { + assert(economy.createPlayerAccount(player)) { "Failed to create account for ${player.name}" } + } + + override fun format(amount: BigDecimal): String { + return economy.format(amount.toDouble()) + } + + override fun getBalance(player: OfflinePlayer): BigDecimal { + return BigDecimal.valueOf(economy.getBalance(player)) + } + + override fun hasAccount(player: OfflinePlayer): Boolean { + return economy.hasAccount(player) + } + + private fun assertEconomyResponse(response: EconomyResponse) { + if (response.type != EconomyResponse.ResponseType.SUCCESS) { + throw EconomyException(response.errorMessage) + } + } + + private fun requirePositive(decimal: BigDecimal) { + require(decimal.toDouble() > 0) { "The decimal amount must be positive, got: $decimal" } + } +} diff --git a/mcspring-api/pom.xml b/mcspring-api/pom.xml deleted file mode 100644 index f238746..0000000 --- a/mcspring-api/pom.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - mcspring-parent - in.kyle.mcspring - 0.0.9 - - - pom - - - mcspring-subcommands - mcspring-base - mcspring-vault - mcspring-jar-loader - mcspring-test - - - 4.0.0 - - mcspring-api - - - - - ${project.parent.groupId} - mcspring-parent - - - - diff --git a/mcspring-build/mcspring-annotations/goddammit-maven/META-INF/services/javax.annotation.processing.Processor b/mcspring-build/mcspring-annotations/goddammit-maven/META-INF/services/javax.annotation.processing.Processor deleted file mode 100644 index bcce3a5..0000000 --- a/mcspring-build/mcspring-annotations/goddammit-maven/META-INF/services/javax.annotation.processing.Processor +++ /dev/null @@ -1 +0,0 @@ -in.kyle.mcspring.processor.AnnotationProcessor diff --git a/mcspring-build/mcspring-annotations/pom.xml b/mcspring-build/mcspring-annotations/pom.xml deleted file mode 100644 index 66efbe7..0000000 --- a/mcspring-build/mcspring-annotations/pom.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - mcspring-build - in.kyle.mcspring - 0.0.9 - - - 4.0.0 - - mcspring-annotations - - - - commons-io - commons-io - 2.6 - - - in.kyle.mcspring - mcspring-base - provided - - - commons-lang - commons-lang - 2.6 - compile - - - org.spigotmc - spigot-api - compile - - - - - - - - maven-resources-plugin - 3.1.0 - - - copy-resources - process-classes - - copy-resources - - - ${project.build.outputDirectory} - - - ${basedir}/goddammit-maven - false - - - - - - - - - diff --git a/mcspring-build/mcspring-annotations/src/main/java/in/kyle/mcspring/processor/AnnotationProcessor.java b/mcspring-build/mcspring-annotations/src/main/java/in/kyle/mcspring/processor/AnnotationProcessor.java deleted file mode 100644 index 75036f3..0000000 --- a/mcspring-build/mcspring-annotations/src/main/java/in/kyle/mcspring/processor/AnnotationProcessor.java +++ /dev/null @@ -1,172 +0,0 @@ -package in.kyle.mcspring.processor; - -import org.apache.commons.lang.exception.ExceptionUtils; -import org.springframework.stereotype.Component; -import org.springframework.stereotype.Controller; -import org.springframework.stereotype.Service; - -import java.io.IOException; -import java.io.Writer; -import java.lang.annotation.Annotation; -import java.util.Arrays; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; - -import javax.annotation.processing.AbstractProcessor; -import javax.annotation.processing.RoundEnvironment; -import javax.annotation.processing.SupportedAnnotationTypes; -import javax.annotation.processing.SupportedSourceVersion; -import javax.lang.model.SourceVersion; -import javax.lang.model.element.Element; -import javax.lang.model.element.TypeElement; -import javax.tools.Diagnostic; -import javax.tools.FileObject; -import javax.tools.StandardLocation; - -import in.kyle.mcspring.processor.annotation.PluginDepend; -import in.kyle.mcspring.processor.annotation.SpringPlugin; -import in.kyle.mcspring.processor.util.MainClassCreator; - -// Adapted from https://hub.spigotmc.org/stash/projects/SPIGOT/repos/plugin-annotations/browse -// /src/main/java/org/bukkit/plugin/java/annotation/PluginAnnotationProcessor.java -@SupportedAnnotationTypes("*") -@SupportedSourceVersion(SourceVersion.RELEASE_8) -public class AnnotationProcessor extends AbstractProcessor { - - private Writer yml; - private String mainClass = "PluginMain"; - private boolean created = false; - - private void setArtifactId() { - processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, processingEnv.getOptions().toString()); - mainClass = Objects.requireNonNull(processingEnv.getOptions().get("artifactId")) - .replace("-", "") - .replace(".", ""); - } - - @Override - public boolean process(Set annotations, RoundEnvironment env) { - if (created) { - return true; - } - try { - setArtifactId(); - this.processingEnv.getMessager() - .printMessage(Diagnostic.Kind.NOTE, "Generating Plugin Data..."); - process(env); - created = true; - } catch (Exception e) { - this.processingEnv.getMessager() - .printMessage(Diagnostic.Kind.ERROR, ExceptionUtils.getStackTrace(e)); - return false; - } - return true; - } - - private Set findPackage(RoundEnvironment env) { - Set elements = env.getElementsAnnotatedWith(Component.class); - elements.addAll((Set) env.getElementsAnnotatedWith(Controller.class)); - elements.addAll((Set) env.getElementsAnnotatedWith(Service.class)); - Set packages = new HashSet<>(); - for (Element element : elements) { - if (element instanceof TypeElement) { - TypeElement te = (TypeElement) element; - String packageName = getPackageFromFqn(te.getQualifiedName().toString()); - packages.add(packageName); - } - } - return packages; - } - - private String getRootPackage(Set packages) { - return packages.stream().min(Comparator.comparingInt(String::length)).orElse("ignore"); - } - - private String getPackageFromFqn(String fqn) { - if (fqn.contains(".")) { - return fqn.substring(0, fqn.lastIndexOf(".")); - } else { - return fqn; - } - } - - private void process(RoundEnvironment env) throws Exception { - FileObject ymlFile = processingEnv.getFiler() - .createResource(StandardLocation.CLASS_OUTPUT, "", "plugin.yml"); - yml = ymlFile.openWriter(); - Set packages = findPackage(env); - String rootPackage = "org.springframework.boot.loader"; - String appendRoot = getRootPackage(packages); - if (!appendRoot.isEmpty()) { - rootPackage += "." + appendRoot; - } - mainClass = rootPackage + "." + mainClass; - FileObject main = processingEnv.getFiler().createSourceFile(mainClass); - MainClassCreator.generateMain(main, mainClass, rootPackage, packages); - - addRequired(env); - addDependencies(env); - yml.flush(); - yml.close(); - } - - private void addRequired(RoundEnvironment env) throws IOException { - yml.write(String.format("main: %s\n", mainClass)); - String name = processOne(env, - SpringPlugin.class, - SpringPlugin::name, - "spring-plugin-default-name"); - yml.write(String.format("name: %s\n", name)); - String version = processOne(env, SpringPlugin.class, SpringPlugin::version, "0.0.1"); - yml.write(String.format("version: %s\n", version)); - String description = processOne(env, SpringPlugin.class, SpringPlugin::description, ""); - yml.write(String.format("description: %s\n", description)); - } - - private void addDependencies(RoundEnvironment env) throws IOException { - Set hardDepend = new HashSet<>(); - Set softDepend = new HashSet<>(); - processAll(env, PluginDepend.class, depend -> { - if (depend.soft()) { - return softDepend.addAll(Arrays.asList(depend.plugins())); - } else { - return hardDepend.addAll(Arrays.asList(depend.plugins())); - } - }); - if (!hardDepend.isEmpty()) { - String dependString = String.join(", ", hardDepend); - yml.write(String.format("depend: [%s]\n", dependString)); - } - if (!softDepend.isEmpty()) { - String dependString = String.join(", ", softDepend); - yml.write(String.format("softdepend: [%s]\n", dependString)); - } - } - - private static R processOne(RoundEnvironment env, - Class annotation, - Function function, - R defaultValue) { - List rs = processAll(env, annotation, function); - if (!rs.isEmpty()) { - return rs.get(0); - } else { - return defaultValue; - } - } - - private static List processAll(RoundEnvironment env, - Class annotation, - Function consumer) { - Set elements = env.getElementsAnnotatedWith(annotation); - return elements.stream() - .flatMap(e -> Arrays.stream(e.getAnnotationsByType(annotation))) - .map(consumer) - .collect(Collectors.toList()); - } -} diff --git a/mcspring-build/mcspring-annotations/src/main/java/in/kyle/mcspring/processor/annotation/PluginAuthor.java b/mcspring-build/mcspring-annotations/src/main/java/in/kyle/mcspring/processor/annotation/PluginAuthor.java deleted file mode 100644 index 820307b..0000000 --- a/mcspring-build/mcspring-annotations/src/main/java/in/kyle/mcspring/processor/annotation/PluginAuthor.java +++ /dev/null @@ -1,5 +0,0 @@ -package in.kyle.mcspring.processor.annotation; - -public @interface PluginAuthor { - String[] authors(); -} diff --git a/mcspring-build/mcspring-annotations/src/main/java/in/kyle/mcspring/processor/annotation/PluginDepend.java b/mcspring-build/mcspring-annotations/src/main/java/in/kyle/mcspring/processor/annotation/PluginDepend.java deleted file mode 100644 index 869945d..0000000 --- a/mcspring-build/mcspring-annotations/src/main/java/in/kyle/mcspring/processor/annotation/PluginDepend.java +++ /dev/null @@ -1,6 +0,0 @@ -package in.kyle.mcspring.processor.annotation; - -public @interface PluginDepend { - String[] plugins(); - boolean soft() default false; -} diff --git a/mcspring-build/mcspring-annotations/src/main/java/in/kyle/mcspring/processor/annotation/SpringPlugin.java b/mcspring-build/mcspring-annotations/src/main/java/in/kyle/mcspring/processor/annotation/SpringPlugin.java deleted file mode 100644 index f0426ba..0000000 --- a/mcspring-build/mcspring-annotations/src/main/java/in/kyle/mcspring/processor/annotation/SpringPlugin.java +++ /dev/null @@ -1,7 +0,0 @@ -package in.kyle.mcspring.processor.annotation; - -public @interface SpringPlugin { - String name(); - String version() default "0.0.1"; - String description() default "A Spring plugin"; -} diff --git a/mcspring-build/mcspring-annotations/src/main/java/in/kyle/mcspring/processor/util/MainClassCreator.java b/mcspring-build/mcspring-annotations/src/main/java/in/kyle/mcspring/processor/util/MainClassCreator.java deleted file mode 100644 index a545509..0000000 --- a/mcspring-build/mcspring-annotations/src/main/java/in/kyle/mcspring/processor/util/MainClassCreator.java +++ /dev/null @@ -1,37 +0,0 @@ -package in.kyle.mcspring.processor.util; - -import org.apache.commons.io.IOUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Writer; -import java.util.Set; -import java.util.stream.Collectors; - -import javax.tools.FileObject; - -public class MainClassCreator { - public static void generateMain(FileObject main, - String fqn, - String packageName, - Set packages) throws IOException { - InputStream resource = MainClassCreator.class.getResourceAsStream("/Main.java"); - try (Writer writer = main.openWriter()) { - String template = IOUtils.toString(new InputStreamReader(resource)); - String name = fqn; - if (fqn.contains(".")) { - template = String.format("package %s;\n", packageName) + template; - name = fqn.substring(fqn.lastIndexOf(".") + 1); - } - String scans = makeScanStrings(packages); - writer.write(template.replace("{name}", name).replace("{scans}", scans)); - } - } - - private static String makeScanStrings(Set strings) { - return strings.stream() - .map(s -> String.format("\"%s\"", s)) - .collect(Collectors.joining(",\n ")); - } -} diff --git a/mcspring-build/mcspring-annotations/src/main/resources/Main.java b/mcspring-build/mcspring-annotations/src/main/resources/Main.java deleted file mode 100644 index bca3692..0000000 --- a/mcspring-build/mcspring-annotations/src/main/resources/Main.java +++ /dev/null @@ -1,34 +0,0 @@ -import org.springframework.boot.autoconfigure.SpringBootApplication; - -// @formatter::off -import org.bukkit.plugin.java.JavaPlugin; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.loader.mcspring.McSpringLoader; - -import in.kyle.mcspring.SpringPlugin; -// @formatter::on - -public class {name} extends JavaPlugin { - - private McSpringLoader loader; - - public void onEnable() { - try { - new McSpringLoader().launch(getClassLoader()); - SpringPlugin.setup(this, MainPluginConfig.class); - } catch (Exception ignored){ - getLogger().warning("MCSpring Failed to load " + getName()); - // error will be logged by Spring - } - } - - public void onDisable() { - SpringPlugin.teardown(this); -// if (loader != null) loader.close(); - loader = null; - } - - @SpringBootApplication(scanBasePackages = {{scans}}) - static class MainPluginConfig { - } -} diff --git a/mcspring-build/mcspring-archetype/pom.xml b/mcspring-build/mcspring-archetype/pom.xml deleted file mode 100644 index 38fdcc4..0000000 --- a/mcspring-build/mcspring-archetype/pom.xml +++ /dev/null @@ -1,76 +0,0 @@ - - - - mcspring-build - in.kyle.mcspring - 0.0.9 - - - 4.0.0 - - mcspring-archetype - - - - in.kyle.mcspring - mcspring-plugin-manager - ${project.parent.version} - - - - - - - org.apache.maven.archetype - archetype-packaging - 3.1.2 - - - - - src/main/resources - true - - archetype-resources/pom.xml - - - - src/main/resources - false - - archetype-resources/pom.xml - - - - - - org.apache.maven.plugins - maven-resources-plugin - 3.1.0 - - \ - - - - maven-dependency-plugin - - - prepare-package - - copy-dependencies - - - - ${project.build.outputDirectory}/archetype-resources/spigot/plugins - - mcspring-plugin-manager - true - - - - - - - diff --git a/mcspring-build/mcspring-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/mcspring-build/mcspring-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml deleted file mode 100644 index 8c27f28..0000000 --- a/mcspring-build/mcspring-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - src/main/java - - **/*.java - - - - src/main/resources - - ** - - - - spigot - - bukkit.yml - eula.txt - server.properties - spigot.yml - README.md - plugins/** - - - - diff --git a/mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/pom.xml b/mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/pom.xml deleted file mode 100644 index 0fbad3d..0000000 --- a/mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/pom.xml +++ /dev/null @@ -1,34 +0,0 @@ - - 4.0.0 - - - mcspring-starter - ${project.groupId} - ${project.version} - - - \${groupId} - \${artifactId} - \${version} - jar - - - 1.15.1-R0.1-SNAPSHOT - - - - - - org.projectlombok - lombok - 1.18.12 - provided - - - ${project.groupId} - mcspring-subcommands - - - diff --git a/mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/spigot/README.md b/mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/spigot/README.md deleted file mode 100644 index 91612d4..0000000 --- a/mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/spigot/README.md +++ /dev/null @@ -1,16 +0,0 @@ -This is a simple Spigot installation. - -To start the server in Intellij: -1. Go to `Edit Run Configurations` (next to the run button in the dropdown) -2. Add a new JAR run configuration by clicking the plus button and selecting `JAR Application` -3. Set the `Path to JAR` as the downloaded spigot.jar. If you do not have a spigot.jar, run a maven install. -4. Add the VM Option `-DIReallyKnowWhatIAmDoingISwear` -5. Set the `Working Directory` as the spigot folder - -You will then be able to run spigot by clicking the run button in IJ. - -Further Notes: -* You are also able to launch Spigot in debug mode and set breakpoints. -* Running a maven install will automatically copy the latest plugin jar into your plugins folder. -* Once you perform a new install, restart the server to get the latest version. McSpring does not - yet support reloading altered jars. diff --git a/mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/src/main/java/ExampleCommand.java b/mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/src/main/java/ExampleCommand.java deleted file mode 100644 index 4e99434..0000000 --- a/mcspring-build/mcspring-archetype/src/main/resources/archetype-resources/src/main/java/ExampleCommand.java +++ /dev/null @@ -1,30 +0,0 @@ -package ${groupId}; - -import org.springframework.stereotype.Component; - -import javax.annotation.PostConstruct; - -import in.kyle.mcspring.command.Command; - -// Remember to annotate Spring beans with @Component -// If you forget this, your class will not run. -@Component -class ExampleCommand { - // Do not extend JavaPlugin, you will regret it - // To get an instance of Plugin, have Spring inject it. - - // Use this in-place of an onEnable method. You can make as many of these methods as you like. - // for onDisable see the @PreDestroy annotation. - @PostConstruct - void onEnable() { - - } - - // Commands will be automatically set up. Do not create a plugin.yml - // The return value of a command method will be sent to the CommandSender using Object::toString - // void methods simplly will not send a message - @Command("test") - String test() { - return "The command works!"; - } -} diff --git a/mcspring-build/mcspring-plugin-layout/pom.xml b/mcspring-build/mcspring-plugin-layout/pom.xml deleted file mode 100644 index d0b6971..0000000 --- a/mcspring-build/mcspring-plugin-layout/pom.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - - mcspring-build - in.kyle.mcspring - 0.0.9 - - 4.0.0 - - mcspring-plugin-layout - - - - org.springframework.boot - spring-boot-loader-tools - - - in.kyle.mcspring - mcspring-jar-loader - ${project.version} - - - - - - - maven-dependency-plugin - - - prepare-package - - copy-dependencies - - - - ${project.build.outputDirectory}/META-INF/loader - - mcspring-jar-loader - true - - - - - - - diff --git a/mcspring-build/mcspring-plugin-layout/src/main/java/in/kyle/mcspring/layout/McSpringLayout.java b/mcspring-build/mcspring-plugin-layout/src/main/java/in/kyle/mcspring/layout/McSpringLayout.java deleted file mode 100644 index 32a2210..0000000 --- a/mcspring-build/mcspring-plugin-layout/src/main/java/in/kyle/mcspring/layout/McSpringLayout.java +++ /dev/null @@ -1,43 +0,0 @@ -package in.kyle.mcspring.layout; - -import org.springframework.boot.loader.tools.CustomLoaderLayout; -import org.springframework.boot.loader.tools.Layout; -import org.springframework.boot.loader.tools.LibraryScope; -import org.springframework.boot.loader.tools.LoaderClassesWriter; - -import java.io.IOException; - -public class McSpringLayout implements Layout, CustomLoaderLayout { - - private static final String NESTED_LOADER_JAR = "META-INF/loader/mcspring-jar-loader.jar"; - - @Override - public String getLauncherClassName() { - return ""; - } - - @Override - public String getLibraryDestination(String libraryName, LibraryScope scope) { - if (scope == LibraryScope.COMPILE) { - return "BOOT-INF/lib/"; - } else { - return null; - } - } - - @Override - public String getClassesLocation() { - return ""; - } - - @Override - public boolean isExecutable() { - return true; - } - - @Override - public void writeLoadedClasses(LoaderClassesWriter writer) throws IOException { - writer.writeLoaderClasses(); - writer.writeLoaderClasses(NESTED_LOADER_JAR); - } -} diff --git a/mcspring-build/mcspring-plugin-layout/src/main/java/in/kyle/mcspring/layout/McSpringLayoutFactory.java b/mcspring-build/mcspring-plugin-layout/src/main/java/in/kyle/mcspring/layout/McSpringLayoutFactory.java deleted file mode 100644 index 68833ff..0000000 --- a/mcspring-build/mcspring-plugin-layout/src/main/java/in/kyle/mcspring/layout/McSpringLayoutFactory.java +++ /dev/null @@ -1,18 +0,0 @@ -package in.kyle.mcspring.layout; - -import org.springframework.boot.loader.tools.Layout; -import org.springframework.boot.loader.tools.LayoutFactory; - -import java.io.File; - -public class McSpringLayoutFactory implements LayoutFactory { - - // required by spring-boot - @SuppressWarnings("unused") - private String name = "mcspring"; - - @Override - public Layout getLayout(File file) { - return new McSpringLayout(); - } -} diff --git a/mcspring-build/mcspring-plugin-manager/pom.xml b/mcspring-build/mcspring-plugin-manager/pom.xml deleted file mode 100644 index c01e245..0000000 --- a/mcspring-build/mcspring-plugin-manager/pom.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - - - mcspring-starter - in.kyle.mcspring - 0.0.9 - ../mcspring-starter - - - 4.0.0 - - mcspring-plugin-manager - - - - in.kyle.mcspring - mcspring-subcommands - - - org.projectlombok - lombok - - - org.springframework.boot - spring-boot-starter-test - test - - - in.kyle.mcspring - mcspring-test - ${project.parent.version} - - - diff --git a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/Main.java b/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/Main.java deleted file mode 100644 index 5659770..0000000 --- a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/Main.java +++ /dev/null @@ -1,9 +0,0 @@ -package in.kyle.mcspring.manager; - -import in.kyle.mcspring.processor.annotation.SpringPlugin; - -// TODO: 2020-03-07 Pull this information from the pom.xml in a build plugin -@SpringPlugin(name = "mcspring-plugin-manager", - description = "Provides simple debugging and plugin management functions") -interface Main { -} diff --git a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandAbout.java b/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandAbout.java deleted file mode 100644 index 27b12ad..0000000 --- a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandAbout.java +++ /dev/null @@ -1,22 +0,0 @@ -package in.kyle.mcspring.manager.commands; - -import org.bukkit.Server; -import org.springframework.boot.info.BuildProperties; -import org.springframework.core.SpringVersion; -import org.springframework.stereotype.Component; - -import in.kyle.mcspring.command.Command; - -@Component -class CommandAbout { - @Command(value = "about", - description = "Provides information about current library versions being used") - String about(BuildProperties properties, Server server) { - return String.format( - "Plugin Name: %s\nPlugin Version: %s\nSpring Version: %s\nBukkit Version: %s", - properties.getName(), - properties.getVersion(), - SpringVersion.getVersion(), - server.getBukkitVersion()); - } -} diff --git a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandClassLoader.java b/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandClassLoader.java deleted file mode 100644 index 906a0f8..0000000 --- a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandClassLoader.java +++ /dev/null @@ -1,31 +0,0 @@ -package in.kyle.mcspring.manager.commands; - -import org.springframework.stereotype.Component; - -import in.kyle.mcspring.command.Command; -import in.kyle.mcspring.subcommands.PluginCommand; - -@Component -class CommandClassLoader { - - @Command(value = "classloader", - description = "Show ClassLoader information for a specific class", - usage = "/classloader ") - void classLoader(PluginCommand command) { - command.withString(); - command.then(this::executeClassLoader); - command.otherwise("Usage: /classloader "); - } - - String executeClassLoader(String clazz) { - try { - Class aClass = Class.forName(clazz); - String classLoader = aClass.getClassLoader().toString(); - String protectionDomain = - aClass.getProtectionDomain().getCodeSource().getLocation().toString(); - return String.format("ClassLoader: %s\nDomain: %s", classLoader, protectionDomain); - } catch (ClassNotFoundException e) { - return String.format("Class %s not found", clazz); - } - } -} diff --git a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandGamemode.java b/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandGamemode.java deleted file mode 100644 index 746a34b..0000000 --- a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandGamemode.java +++ /dev/null @@ -1,47 +0,0 @@ -package in.kyle.mcspring.manager.commands; - -import org.bukkit.GameMode; -import org.bukkit.entity.Player; -import org.springframework.stereotype.Component; - -import java.util.HashMap; -import java.util.Map; - -import in.kyle.mcspring.command.Command; -import in.kyle.mcspring.subcommands.PluginCommand; - -@Component -class CommandGamemode { - - @Command(value = "gamemode", - aliases = "gm", - description = "Set your game mode", - usage = "/gamemode ") - void gamemode(PluginCommand command) { - command.withPlayerSender("Only players can run this command."); - Map gamemodes = new HashMap<>(); - for (GameMode value : GameMode.values()) { - gamemodes.put(value.name().toLowerCase(), value); - gamemodes.put(String.valueOf(value.getValue()), value); - } - command.withMap(gamemodes, s -> String.format("%s is not a valid game mode", s)); - command.then(this::gamemodeExecutor); - - command.otherwise("Usage: /gamemode "); - } - - @Command(value = "gmc", description = "Set your game mode to creative") - String gmc(Player sender) { - return gamemodeExecutor(sender, GameMode.CREATIVE); - } - - @Command(value = "gms", description = "Set your game mode to survival") - String gms(Player sender) { - return gamemodeExecutor(sender, GameMode.SURVIVAL); - } - - String gamemodeExecutor(Player sender, GameMode gameMode) { - sender.setGameMode(gameMode); - return String.format("Game mode set to %s", gameMode.name().toLowerCase()); - } -} diff --git a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandHeal.java b/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandHeal.java deleted file mode 100644 index 14c96b9..0000000 --- a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandHeal.java +++ /dev/null @@ -1,26 +0,0 @@ -package in.kyle.mcspring.manager.commands; - -import org.bukkit.entity.Player; -import org.springframework.stereotype.Component; - -import in.kyle.mcspring.command.Command; -import in.kyle.mcspring.subcommands.PluginCommand; - -@Component -class CommandHeal { - - @Command(value = "heal", - description = "Heal yourself or another player", - usage = "/heal ?") - void heal(PluginCommand command) { - command.withPlayerSender("Sender must be a player"); - command.withPlayer(s -> String.format("Player %s not found", s)); - command.then(this::executeHeal); - command.otherwise(this::executeHeal); - } - - String executeHeal(Player target) { - target.setHealth(target.getMaxHealth()); - return String.format("Healed %s", target.getName()); - } -} diff --git a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandOp.java b/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandOp.java deleted file mode 100644 index 8800360..0000000 --- a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandOp.java +++ /dev/null @@ -1,26 +0,0 @@ -package in.kyle.mcspring.manager.commands; - -import org.bukkit.command.CommandSender; -import org.springframework.stereotype.Component; - -import in.kyle.mcspring.command.Command; -import in.kyle.mcspring.subcommands.PluginCommand; - -@Component -class CommandOp { - - @Command(value = "op", - description = "Toggle yourself or another players OP status", - usage = "/op ?") - void op(PluginCommand command) { - command.withPlayer(s -> String.format("Player %s not found", s)); - command.then(this::toggleOp); - - command.otherwise(this::toggleOp); - } - - String toggleOp(CommandSender target) { - target.setOp(!target.isOp()); - return String.format("%s is %s op", target.getName(), target.isOp() ? "now" : "no longer"); - } -} diff --git a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandPlugin.java b/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandPlugin.java deleted file mode 100644 index 8ff826a..0000000 --- a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandPlugin.java +++ /dev/null @@ -1,80 +0,0 @@ -package in.kyle.mcspring.manager.commands; - -import org.bukkit.plugin.Plugin; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.stereotype.Component; - -import java.nio.file.Path; -import java.util.function.Function; -import java.util.stream.Collectors; - -import in.kyle.mcspring.command.Command; -import in.kyle.mcspring.manager.controller.PluginController; -import in.kyle.mcspring.subcommands.PluginCommand; -import lombok.RequiredArgsConstructor; -import lombok.var; - -@Component -@RequiredArgsConstructor -@ConditionalOnBean(Plugin.class) -class CommandPlugin { - - private final PluginController pluginController; - - @Command(value = "plugin", - aliases = "pl", - description = "Load/unload/reload a specific plugin", - usage = "/plugin ") - void plugin(PluginCommand command) { - command.on("load", this::load); - command.on("unload", this::unload); - command.on("list", this::list); - command.otherwise("Usage: /plugin "); - } - - private void load(PluginCommand command) { - command.withMap(pluginController.getLoadablePlugins(), - s -> String.format("Plugin %s not found or is already loaded", s)); - command.then(this::executeLoad); - command.otherwise("Usage: /plugin load "); - } - - private void unload(PluginCommand command) { - var plugins = pluginController.getPlugins() - .stream() - .collect(Collectors.toMap(org.bukkit.plugin.Plugin::getName, Function.identity())); - command.withMap(plugins, s -> String.format("Plugin %s is not loaded", s)); - command.then(this::executeDisable); - command.otherwise("Usage: /plugin unload "); - } - - private void list(PluginCommand command) { - command.then(this::executeListPlugins); - } - - private String executeListPlugins() { - return pluginController.getAllPlugins() - .entrySet() - .stream() - .map(e -> String.format("%s%s", e.getValue() ? "&1" : "&4", e.getKey())) - .collect(Collectors.joining(" ")); - } - - private String executeLoad(Path jar) { - var pluginOptional = pluginController.load(jar); - if (pluginOptional.isPresent()) { - return String.format("Plugin %s enabled", pluginOptional.get().getName()); - } else { - return String.format("&4Could not load %s see log for details", jar); - } - } - - private String executeDisable(org.bukkit.plugin.Plugin plugin) { - boolean disabled = pluginController.unload(plugin); - if (disabled) { - return String.format("Plugin %s disabled", plugin); - } else { - return String.format("Could not disable %s, see log for details", plugin); - } - } -} diff --git a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandSpeed.java b/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandSpeed.java deleted file mode 100644 index 1ddbe40..0000000 --- a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/commands/CommandSpeed.java +++ /dev/null @@ -1,27 +0,0 @@ -package in.kyle.mcspring.manager.commands; - -import org.bukkit.entity.Player; -import org.springframework.stereotype.Component; - -import in.kyle.mcspring.command.Command; -import in.kyle.mcspring.subcommands.PluginCommand; - -@Component -class CommandSpeed { - - @Command(value = "speed", - description = "Set your movement and fly speed", - usage = "/speed ") - void speed(PluginCommand command) { - command.withPlayerSender("Sender must be a player"); - command.withDouble("Speed value must be an integer"); - command.then(this::speedExecutor); - command.otherwise("Usage: /speed "); - } - - String speedExecutor(Player sender, double speed) { - sender.setFlySpeed((float) speed); - sender.setWalkSpeed((float) speed); - return String.format("Speed set to %f", speed); - } -} diff --git a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/controller/BukkitPluginUnloader.java b/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/controller/BukkitPluginUnloader.java deleted file mode 100644 index f917e57..0000000 --- a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/controller/BukkitPluginUnloader.java +++ /dev/null @@ -1,96 +0,0 @@ -package in.kyle.mcspring.manager.controller; - -import org.bukkit.command.Command; -import org.bukkit.command.CommandMap; -import org.bukkit.command.PluginCommand; -import org.bukkit.command.SimpleCommandMap; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.PluginManager; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.context.annotation.Lazy; -import org.springframework.stereotype.Component; - -import java.lang.reflect.Field; -import java.net.URLClassLoader; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import javax.annotation.PostConstruct; - -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.var; - -@Lazy -@Component -@RequiredArgsConstructor -@ConditionalOnBean(Plugin.class) -@SuppressWarnings("unchecked") -class BukkitPluginUnloader { - - private final PluginManager pluginManager; - private final CommandMap commandMap; - private Map commands; - private List plugins; - private Map names; - - @PostConstruct - @SneakyThrows - void setup() { - plugins = getDeclaredField(pluginManager, "plugins"); - names = getDeclaredField(pluginManager, "lookupNames"); - Field knownCommands = SimpleCommandMap.class.getDeclaredField("knownCommands"); - knownCommands.setAccessible(true); - commands = (Map) knownCommands.get(commandMap); - } - - @SneakyThrows - boolean unload(Plugin plugin) { - pluginManager.disablePlugin(plugin); - - synchronized (pluginManager) { - plugins.remove(plugin); - names.remove(plugin.getName()); - unregisterCommands(plugin); - - closeClassLoader(plugin.getClass().getClassLoader()); - } - System.gc(); - - return true; - } - - private void unregisterCommands(Plugin plugin) { - var unregister = commands.entrySet() - .stream() - .filter(e -> e.getValue() instanceof PluginCommand) - .filter(e -> ((PluginCommand) e.getValue()).getPlugin() == plugin) - .peek(e -> e.getValue().unregister(commandMap)) - .collect(Collectors.toSet()); - unregister.forEach(e -> commands.remove(e.getKey())); - } - - @SneakyThrows - private void closeClassLoader(ClassLoader classLoader) { - if (classLoader instanceof URLClassLoader) { - setDeclaredField(classLoader, "plugin", null); - setDeclaredField(classLoader, "pluginInit", null); - ((URLClassLoader) classLoader).close(); - } - } - - @SneakyThrows - private T getDeclaredField(Object object, String fieldName) { - Field field = object.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - return (T) field.get(object); - } - - @SneakyThrows - private void setDeclaredField(Object object, String fieldName, Object value) { - Field field = object.getClass().getDeclaredField(fieldName); - field.setAccessible(true); - field.set(object, value); - } -} diff --git a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/controller/LogFileController.java b/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/controller/LogFileController.java deleted file mode 100644 index 99fe27a..0000000 --- a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/controller/LogFileController.java +++ /dev/null @@ -1,33 +0,0 @@ -package in.kyle.mcspring.manager.controller; - -import org.bukkit.plugin.Plugin; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.stereotype.Controller; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; - -import javax.annotation.PostConstruct; - -import lombok.SneakyThrows; - -@Controller -@ConditionalOnBean(Plugin.class) -class LogFileController { - - private final Path logsFolder = Paths.get("logs"); - - @PostConstruct - @SneakyThrows - void setup() { - Files.list(logsFolder) - .filter(p -> p.toString().endsWith(".log.gz")) - .forEach(this::delete); - } - - @SneakyThrows - private void delete(Path f) { - Files.delete(f); - } -} diff --git a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/controller/PluginController.java b/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/controller/PluginController.java deleted file mode 100644 index 054ac93..0000000 --- a/mcspring-build/mcspring-plugin-manager/src/main/java/in/kyle/mcspring/manager/controller/PluginController.java +++ /dev/null @@ -1,100 +0,0 @@ -package in.kyle.mcspring.manager.controller; - -import org.bukkit.plugin.InvalidDescriptionException; -import org.bukkit.plugin.Plugin; -import org.bukkit.plugin.PluginLoader; -import org.bukkit.plugin.PluginManager; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.stereotype.Controller; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.Arrays; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -import lombok.RequiredArgsConstructor; -import lombok.SneakyThrows; -import lombok.var; - -@Controller -@RequiredArgsConstructor -@ConditionalOnBean(Plugin.class) -public class PluginController { - - private final PluginManager pluginManager; - private final PluginLoader pluginLoader; - private final BukkitPluginUnloader unloader; - private final Logger logger; - - public Optional load(Path jar) { - try { - Plugin plugin = pluginManager.loadPlugin(jar.toFile()); - Optional optionalPlugin = Optional.ofNullable(plugin); - optionalPlugin.ifPresent(Plugin::onLoad); - return optionalPlugin; - } catch (Exception e) { - logger.log(Level.SEVERE, "Could not load " + jar.toAbsolutePath()); - logger.log(Level.SEVERE, e, () -> ""); - return Optional.empty(); - } - } - - public boolean unload(Plugin plugin) { - if (pluginManager.isPluginEnabled(plugin)) { - try { - return unloader.unload(plugin); - } catch (Exception e) { - logger.log(Level.SEVERE, "Could not unload " + plugin.getName()); - logger.log(Level.SEVERE, e, () -> ""); - } - } - return false; - } - - @SneakyThrows - public Map getLoadablePlugins() { - Path pluginsFolder = Paths.get("plugins"); - return Files.list(pluginsFolder) - .filter(p -> p.toString().endsWith(".jar")) - .filter(jar -> getPluginName(jar).isPresent()) - .collect(Collectors.toMap(j -> getPluginName(j).get(), Function.identity())); - } - - public Optional getPlugin(String name) { - return Optional.ofNullable(pluginManager.getPlugin(name)); - } - - public Set getPlugins() { - return new HashSet<>(Arrays.asList(pluginManager.getPlugins())); - } - - public Map getAllPlugins() { - Map allPlugins = new HashMap<>(); - getLoadablePlugins().forEach((k,v)->allPlugins.put(k, false)); - getPlugins().forEach(p -> allPlugins.put(p.getName(), p.isEnabled())); - return allPlugins; - } - - private boolean isEnabled(String pluginName) { - return getPlugin(pluginName).map(Plugin::isEnabled).orElse(false); - } - - private Optional getPluginName(Path jar) { - try { - var description = pluginLoader.getPluginDescription(jar.toFile()); - return Optional.of(description.getName()); - } catch (InvalidDescriptionException e) { - return Optional.empty(); - } - } - -} diff --git a/mcspring-build/mcspring-plugin-manager/src/test/java/in/kyle/mcspring/manager/commands/TestCommandAbout.java b/mcspring-build/mcspring-plugin-manager/src/test/java/in/kyle/mcspring/manager/commands/TestCommandAbout.java deleted file mode 100644 index 5b25654..0000000 --- a/mcspring-build/mcspring-plugin-manager/src/test/java/in/kyle/mcspring/manager/commands/TestCommandAbout.java +++ /dev/null @@ -1,28 +0,0 @@ -package in.kyle.mcspring.manager.commands; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.List; - -import in.kyle.mcspring.test.MCSpringTest; -import in.kyle.mcspring.test.command.TestCommandExecutor; - -import static org.assertj.core.api.Assertions.assertThat; - -@MCSpringTest -class TestCommandAbout { - - @Autowired - TestCommandExecutor commandExecutor; - - @Test - void testAbout() { - List output = commandExecutor.run("about"); - assertThat(output).hasSize(4); - assertThat(output.get(0)).matches("Plugin Name: [^ ]+"); - assertThat(output.get(1)).matches("Plugin Version: [^ ]+"); - assertThat(output.get(2)).matches("Spring Version: [^ ]+"); - assertThat(output.get(3)).matches("Bukkit Version: [^ ]+"); - } -} diff --git a/mcspring-build/mcspring-plugin-manager/src/test/java/in/kyle/mcspring/manager/commands/TestCommandClassLoader.java b/mcspring-build/mcspring-plugin-manager/src/test/java/in/kyle/mcspring/manager/commands/TestCommandClassLoader.java deleted file mode 100644 index 1ebec91..0000000 --- a/mcspring-build/mcspring-plugin-manager/src/test/java/in/kyle/mcspring/manager/commands/TestCommandClassLoader.java +++ /dev/null @@ -1,30 +0,0 @@ -package in.kyle.mcspring.manager.commands; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.List; - -import in.kyle.mcspring.test.MCSpringTest; -import in.kyle.mcspring.test.command.TestCommandExecutor; - -import static org.assertj.core.api.Assertions.assertThat; - -@MCSpringTest -class TestCommandClassLoader { - - @Autowired - TestCommandExecutor commandExecutor; - - @Test - void testClassLoader() { - List output = commandExecutor.run("classloader " + getClass().getName()); - assertThat(output).hasSize(2); - assertThat(output.get(0)).isEqualTo( - "ClassLoader: " + getClass().getClassLoader().toString()); - assertThat(output.get(1)).isEqualTo("Domain: " + getClass().getProtectionDomain() - .getCodeSource() - .getLocation() - .toString()); - } -} diff --git a/mcspring-build/mcspring-plugin-manager/src/test/java/in/kyle/mcspring/manager/commands/TestCommandGamemode.java b/mcspring-build/mcspring-plugin-manager/src/test/java/in/kyle/mcspring/manager/commands/TestCommandGamemode.java deleted file mode 100644 index 356393d..0000000 --- a/mcspring-build/mcspring-plugin-manager/src/test/java/in/kyle/mcspring/manager/commands/TestCommandGamemode.java +++ /dev/null @@ -1,58 +0,0 @@ -package in.kyle.mcspring.manager.commands; - -import org.bukkit.GameMode; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.List; - -import in.kyle.api.bukkit.entity.TestPlayer; -import in.kyle.mcspring.test.MCSpringTest; -import in.kyle.mcspring.test.command.TestCommandExecutor; - -import static org.assertj.core.api.Assertions.assertThat; - -@MCSpringTest -public class TestCommandGamemode { - - @Autowired - TestCommandExecutor executor; - - @Autowired - TestPlayer sender; - - @BeforeEach - void setup() { - sender.setGameMode(GameMode.SURVIVAL); - } - - @Test - void testGmc() { - List output = executor.run(sender, "gmc"); - assertThat(output).first().asString().isEqualTo("Game mode set to creative"); - assertThat(sender.getGameMode()).isEqualTo(GameMode.CREATIVE); - } - - @Test - void testGms() { - sender.setGameMode(GameMode.SPECTATOR); - List output = executor.run(sender, "gms"); - assertThat(output).first().asString().isEqualTo("Game mode set to survival"); - assertThat(sender.getGameMode()).isEqualTo(GameMode.SURVIVAL); - } - - @Test - void testGamemode() { - List output = executor.run(sender, "gm creative"); - assertThat(output).first().asString().isEqualTo("Game mode set to creative"); - assertThat(sender.getGameMode()).isEqualTo(GameMode.CREATIVE); - } - - @Test - void testGamemodeNumeric() { - List output = executor.run(sender, "gm 1"); - assertThat(output).first().asString().isEqualTo("Game mode set to creative"); - assertThat(sender.getGameMode()).isEqualTo(GameMode.CREATIVE); - } -} diff --git a/mcspring-build/mcspring-plugin-manager/src/test/java/in/kyle/mcspring/manager/commands/TestCommandHeal.java b/mcspring-build/mcspring-plugin-manager/src/test/java/in/kyle/mcspring/manager/commands/TestCommandHeal.java deleted file mode 100644 index 81759cd..0000000 --- a/mcspring-build/mcspring-plugin-manager/src/test/java/in/kyle/mcspring/manager/commands/TestCommandHeal.java +++ /dev/null @@ -1,24 +0,0 @@ -package in.kyle.mcspring.manager.commands; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.List; - -import in.kyle.mcspring.test.MCSpringTest; -import in.kyle.mcspring.test.command.TestCommandExecutor; - -import static org.assertj.core.api.Assertions.assertThat; - -@MCSpringTest -class TestCommandHeal { - - @Autowired - TestCommandExecutor commandExecutor; - - @Test - void testHeal() { - List output = commandExecutor.run("heal"); - assertThat(output).first().asString().matches("Healed [^ ]+"); - } -} diff --git a/mcspring-build/mcspring-plugin-manager/src/test/java/in/kyle/mcspring/manager/commands/TestCommandOp.java b/mcspring-build/mcspring-plugin-manager/src/test/java/in/kyle/mcspring/manager/commands/TestCommandOp.java deleted file mode 100644 index 7c9492e..0000000 --- a/mcspring-build/mcspring-plugin-manager/src/test/java/in/kyle/mcspring/manager/commands/TestCommandOp.java +++ /dev/null @@ -1,35 +0,0 @@ -package in.kyle.mcspring.manager.commands; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.List; - -import in.kyle.mcspring.test.MCSpringTest; -import in.kyle.mcspring.test.command.TestCommandExecutor; - -import static org.assertj.core.api.Assertions.assertThat; - -@MCSpringTest -class TestCommandOp { - - @Autowired - TestCommandExecutor executor; - - @Test - void testOpSelf() { - List messages = executor.run("op"); - assertThat(messages).hasSize(1); - assertThat(messages.get(0)).matches("[^ ]+ is now op"); - } - - @Test - void testOpOther() { - // TODO: 2020-03-13 Need better testing instrumentation - - // server.getOnlinePlayers().add(target); - // List messages = executor.run("op " + target.getName()); - // assertThat(messages).hasSize(1); - // assertThat(messages.get(0)).matches(target.getName() + " is now op"); - } -} diff --git a/mcspring-build/mcspring-plugin-manager/src/test/java/in/kyle/mcspring/manager/commands/TestCommandSpeed.java b/mcspring-build/mcspring-plugin-manager/src/test/java/in/kyle/mcspring/manager/commands/TestCommandSpeed.java deleted file mode 100644 index c0b0c56..0000000 --- a/mcspring-build/mcspring-plugin-manager/src/test/java/in/kyle/mcspring/manager/commands/TestCommandSpeed.java +++ /dev/null @@ -1,40 +0,0 @@ -package in.kyle.mcspring.manager.commands; - -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; - -import java.util.List; - -import in.kyle.api.bukkit.entity.TestPlayer; -import in.kyle.mcspring.test.MCSpringTest; -import in.kyle.mcspring.test.command.TestCommandExecutor; - -import static org.assertj.core.api.Assertions.assertThat; - -@MCSpringTest -class TestCommandSpeed { - - @Autowired - TestCommandExecutor executor; - - @Autowired - TestPlayer sender; - - @Test - void testSpeed() { - List messages = executor.run(sender, "speed 10"); - assertThat(messages).first().asString().matches("Speed set to [^ ]+"); - assertThat(sender.getWalkSpeed()).isEqualTo(10F); - assertThat(sender.getFlySpeed()).isEqualTo(10F); - } - - @Test - void testSpeedUsage() { - sender.setWalkSpeed(0); - sender.setFlySpeed(0); - List messages = executor.run(sender, "speed"); - assertThat(messages).first().asString().startsWith("Usage: "); - assertThat(sender.getWalkSpeed()).isEqualTo(0); - assertThat(sender.getFlySpeed()).isEqualTo(0); - } -} diff --git a/mcspring-build/mcspring-starter/pom.xml b/mcspring-build/mcspring-starter/pom.xml deleted file mode 100644 index 7f0999a..0000000 --- a/mcspring-build/mcspring-starter/pom.xml +++ /dev/null @@ -1,209 +0,0 @@ - - - - mcspring-build - in.kyle.mcspring - 0.0.9 - - - pom - - - ../../mcspring-examples - ../mcspring-plugin-manager - - 4.0.0 - - mcspring-starter - - - - - in.kyle.mcspring - mcspring-base - 0.0.9 - - - in.kyle.mcspring - mcspring-subcommands - 0.0.9 - - - in.kyle.mcspring - mcspring-annotations - 0.0.9 - provided - - - - - - - - - org.springframework.boot - spring-boot-maven-plugin - ${spring.version} - - ignored - - mcspring - - - - - in.kyle.mcspring - mcspring-plugin-layout - - 0.0.9 - - - - - build-info - - repackage - build-info - - - - - - maven-antrun-plugin - 1.8 - - - package - - run - - - - - - - - - - - com.coderplus.maven.plugins - copy-rename-maven-plugin - 1.0.1 - - - init - package - - copy - - - - ${project.build.directory}/${project.artifactId}.jar - - - ${project.basedir}/spigot/plugins/${project.artifactId}.jar - - - - - copy-plugin - package - - copy - - - - ${project.build.directory}/${project.artifactId}.jar - - - ${project.basedir}/spigot/plugins/update/${project.artifactId}.jar - - - - - - - com.googlecode.maven-download-plugin - download-maven-plugin - 1.3.0 - - - generate-resources - - wget - - - https://cdn.getbukkit.org/spigot/spigot-1.15.2.jar - spigot.jar - ${project.basedir}/spigot - - - - - - - - - - - - true - - generate-yml - - UTF-8 - UTF-8 - 1.8 - 1.8 - - - - - org.springframework.boot - spring-boot-maven-plugin - - - org.apache.maven.plugins - maven-antrun-plugin - - - com.coderplus.maven.plugins - copy-rename-maven-plugin - - - com.googlecode.maven-download-plugin - download-maven-plugin - - - ${project.artifactId} - - - - in.kyle.mcspring - mcspring-annotations - - - in.kyle.mcspring - mcspring-base - - - - org.spigotmc - spigot-api - ${spigot.version} - provided - - - - org.bukkit - bukkit - ${spigot.version} - provided - - - - - diff --git a/mcspring-build/pom.xml b/mcspring-build/pom.xml deleted file mode 100644 index 328dfbf..0000000 --- a/mcspring-build/pom.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - mcspring-parent - in.kyle.mcspring - 0.0.9 - - - pom - - - mcspring-plugin-layout - mcspring-archetype - mcspring-annotations - mcspring-starter - - - 4.0.0 - mcspring-build - diff --git a/mcspring-examples/build.gradle.kts b/mcspring-examples/build.gradle.kts new file mode 100644 index 0000000..d9ab36d --- /dev/null +++ b/mcspring-examples/build.gradle.kts @@ -0,0 +1,38 @@ +import org.jetbrains.kotlin.js.translate.context.Namer.kotlin + +plugins { + kotlin("jvm") version "1.3.72" + id("in.kyle.mcspring") version "0.1.0" apply false +} + +allprojects { + tasks.withType().configureEach { + kotlinOptions.suppressWarnings = true + kotlinOptions.jvmTarget = "1.8" + } +} + +subprojects { + apply(plugin = "org.jetbrains.kotlin.jvm") + apply(plugin = "in.kyle.mcspring") + + repositories { + mavenLocal() + mavenCentral() + maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") + maven("https://oss.sonatype.org/content/repositories/snapshots") + } + + dependencies { + val spigotVersion = "1.15.2-R0.1-SNAPSHOT" + compileOnly("org.spigotmc:spigot-api:$spigotVersion") + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.6.0") + + testImplementation("org.spigotmc:spigot-api:$spigotVersion") + testImplementation("org.junit.jupiter:junit-jupiter:5.8.2") + } + + tasks.test { + useJUnitPlatform() + } +} diff --git a/mcspring-examples/gradle/wrapper/gradle-wrapper.jar b/mcspring-examples/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f3d88b1 Binary files /dev/null and b/mcspring-examples/gradle/wrapper/gradle-wrapper.jar differ diff --git a/mcspring-examples/gradle/wrapper/gradle-wrapper.properties b/mcspring-examples/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ba94df8 --- /dev/null +++ b/mcspring-examples/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/mcspring-examples/gradlew b/mcspring-examples/gradlew new file mode 100644 index 0000000..2fe81a7 --- /dev/null +++ b/mcspring-examples/gradlew @@ -0,0 +1,183 @@ +#!/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 + ;; + 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/mcspring-examples/gradlew.bat b/mcspring-examples/gradlew.bat new file mode 100644 index 0000000..9618d8d --- /dev/null +++ b/mcspring-examples/gradlew.bat @@ -0,0 +1,100 @@ +@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 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 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/mcspring-examples/mcspring-plugin-manager/build.gradle.kts b/mcspring-examples/mcspring-plugin-manager/build.gradle.kts new file mode 100644 index 0000000..f6203fb --- /dev/null +++ b/mcspring-examples/mcspring-plugin-manager/build.gradle.kts @@ -0,0 +1,12 @@ +import `in`.kyle.mcspring.mcspring + +dependencies { + implementation(mcspring("base")) + implementation(mcspring("commands-dsl")) + testImplementation(mcspring("e2e")) +} + +mcspring { + pluginAuthor = "kylepls" + pluginMainPackage = "in.kyle.mcspring.pluginmanager" +} diff --git a/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/McSpringPluginManager.kt b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/McSpringPluginManager.kt new file mode 100644 index 0000000..c5e586e --- /dev/null +++ b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/McSpringPluginManager.kt @@ -0,0 +1,6 @@ +package `in`.kyle.mcspring.manager + +import org.springframework.boot.autoconfigure.SpringBootApplication + +@SpringBootApplication +open class McSpringPluginManager diff --git a/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandAbout.kt b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandAbout.kt new file mode 100644 index 0000000..8652480 --- /dev/null +++ b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandAbout.kt @@ -0,0 +1,30 @@ +package `in`.kyle.mcspring.manager.commands + +import `in`.kyle.mcspring.commands.dsl.command +import `in`.kyle.mcspring.commands.dsl.commandExecutor +import `in`.kyle.mcspring.commands.dsl.mcspring.Command +import org.bukkit.Server +import org.springframework.boot.info.BuildProperties +import org.springframework.core.SpringVersion +import org.springframework.stereotype.Component + +@Component +internal class CommandAbout( + private val properties: BuildProperties, + private val server: Server +) { + + @Command(value = "about", + description = "Provides information about current library versions in use") + fun about() = command { + then { + val aboutString = """ + Plugin Name: ${properties.name} + Plugin Version: ${properties.version} + Spring Version: ${SpringVersion.getVersion()} + Bukkit Version: ${server.bukkitVersion} + """.trimIndent() + message(aboutString) + } + } +} diff --git a/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandClassLoader.kt b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandClassLoader.kt new file mode 100644 index 0000000..5ff986f --- /dev/null +++ b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandClassLoader.kt @@ -0,0 +1,36 @@ +package `in`.kyle.mcspring.manager.commands + +import `in`.kyle.mcspring.commands.dsl.command +import `in`.kyle.mcspring.commands.dsl.commandExecutor +import `in`.kyle.mcspring.commands.dsl.mcspring.Command +import org.springframework.stereotype.Component + +@Component +internal class CommandClassLoader { + + @Command( + value = "classloader", + aliases = ["cl"], + description = "Show ClassLoader information for a specific class", + usage = "/classloader " + ) + fun classLoader() = command { + val className = stringArg { + missing { + message("Usage: /$label ") + } + } + + then { message(executeClassLoader(className)) } + } + + private fun executeClassLoader(clazz: String): String { + val aClass = Class.forName(clazz) + val classLoader = aClass.classLoader.toString() + val protectionDomain = aClass.protectionDomain.codeSource.location.toString() + return """ + ClassLoader: $classLoader + Domain: $protectionDomain + """.trimIndent() + } +} diff --git a/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandGamemode.kt b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandGamemode.kt new file mode 100644 index 0000000..eb209dd --- /dev/null +++ b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandGamemode.kt @@ -0,0 +1,46 @@ +package `in`.kyle.mcspring.manager.commands + +import `in`.kyle.mcspring.commands.dsl.command +import `in`.kyle.mcspring.commands.dsl.commandExecutor +import `in`.kyle.mcspring.commands.dsl.mcspring.Command +import org.bukkit.GameMode +import org.bukkit.entity.Player +import org.springframework.stereotype.Component +import kotlin.contracts.ExperimentalContracts + +@ExperimentalContracts +@Component +internal class CommandGamemode { + + @Command( + value = "gamemode", + aliases = ["gm"], + description = "Set your game mode", + usage = "/gamemode " + ) + fun gamemode() = command { + requirePlayer { message("Only players can run this command.") } + + val gameMode = mapArg { + parser { + map(GameMode.values().associateBy { it.name.toLowerCase() }) + map(GameMode.values().associateBy { it.value.toString() }) + } + missing { message("Usage: /$label ") } + invalid { message("Invalid game mode $it") } + } + + then { message(gamemodeExecutor(sender as Player, gameMode)) } + } + + @Command(value = "gmc", description = "Set your game mode to creative") + fun gmc() = command { then { message(gamemodeExecutor(sender as Player, GameMode.CREATIVE)) } } + + @Command(value = "gms", description = "Set your game mode to survival") + fun gms() = command { then { message(gamemodeExecutor(sender as Player, GameMode.SURVIVAL)) } } + + private fun gamemodeExecutor(target: Player, gameMode: GameMode): String { + target.gameMode = gameMode + return "Game mode set to ${gameMode.name.toLowerCase()}" + } +} diff --git a/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandHeal.kt b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandHeal.kt new file mode 100644 index 0000000..add6178 --- /dev/null +++ b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandHeal.kt @@ -0,0 +1,41 @@ +package `in`.kyle.mcspring.manager.commands + +import `in`.kyle.mcspring.commands.dsl.command +import `in`.kyle.mcspring.commands.dsl.commandExecutor +import `in`.kyle.mcspring.commands.dsl.mcspring.Command +import org.bukkit.entity.Player +import org.springframework.stereotype.Component + +@Component +internal class CommandHeal { + + @Command( + value = "heal", + description = "Heal yourself or another player", + usage = "/heal ?" + ) + fun heal() = command { + requirePlayer { message("Sender must be a player") } + + val target = playerArg { + default { sender as Player } + invalid { message("Target player $it not found") } + } + + val health = doubleArg { + default { 20.0 } + parser { + between(0.0, 20.0) { message("Heal value must be between 0 and 20") } + } + invalid { message("Invalid health amount $it") } + } + + then { message(executeHeal(target, health)) } + } + + private fun executeHeal(target: Player, health: Double): String { + @Suppress("DEPRECATION") + target.health += health + return "Healed ${target.name} by $health" + } +} diff --git a/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandOp.kt b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandOp.kt new file mode 100644 index 0000000..897ad0c --- /dev/null +++ b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandOp.kt @@ -0,0 +1,31 @@ +package `in`.kyle.mcspring.manager.commands + +import `in`.kyle.mcspring.commands.dsl.command +import `in`.kyle.mcspring.commands.dsl.commandExecutor +import `in`.kyle.mcspring.commands.dsl.mcspring.Command +import org.bukkit.command.CommandSender +import org.bukkit.entity.Player +import org.springframework.stereotype.Component + +@Component +internal class CommandOp { + + @Command( + value = "op", + description = "Toggle yourself or another players OP status", + usage = "/op ?" + ) + fun op() = command { + val target = playerArg { + default { sender as? Player } + invalid { message("Player $it not found") } + } + then { message(toggleOp(target)) } + } + + private fun toggleOp(target: CommandSender): String { + target.isOp = !target.isOp + val modifier = if (target.isOp) "now" else "no longer" + return "${target.name} is $modifier op" + } +} diff --git a/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandPlugin.kt b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandPlugin.kt new file mode 100644 index 0000000..fc8f25f --- /dev/null +++ b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandPlugin.kt @@ -0,0 +1,80 @@ +package `in`.kyle.mcspring.manager.commands + +import `in`.kyle.mcspring.commands.dsl.command +import `in`.kyle.mcspring.manager.controller.PluginController +import `in`.kyle.mcspring.commands.dsl.commandExecutor +import `in`.kyle.mcspring.commands.dsl.mcspring.Command +import org.bukkit.plugin.Plugin +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.stereotype.Component +import java.nio.file.Path + +@Component +internal class CommandPlugin(private val pluginController: PluginController) { + + @Command( + value = "plugin", + aliases = ["pl"], + description = "Load/unload/reload a specific plugin", + usage = "/plugin " + ) + fun plugin() = command { + subcommand { + on("load", commandExecutor = load()) + on("unload", commandExecutor = unload()) + on("list") { then { message(executeListPlugins()) } } + + missing { + val subs = subCommands.keys.joinToString(separator = "|") { it.first() } + message("Usage: $label <$subs>") + } + } + } + + private fun load() = command { + val path = mapArg { + parser { + map(pluginController.loadablePlugins) + } + invalid { message("Plugin $it not found or it is already loaded") } + } + then { executeLoad(path) } + } + + private fun unload() = command { + val plugin = mapArg { + parser { + map(pluginController.plugins.associateBy({ it.name }, { it })) + } + invalid { message("Plugin $it is not loaded") } + missing { message("Usage: $label ${args[0]} ") } + } + then { message(executeDisable(plugin)) } + } + + private fun executeListPlugins(): String { + val colors = mapOf(true to "&1", false to "&2") + return pluginController.allPlugins + .map { "${colors[it.value]}${it.key}" } + .joinToString(separator = " ") + } + + private fun executeLoad(jar: Path): String { + val plugin = pluginController.load(jar) + + return if (plugin != null) { + "Plugin ${plugin.name} enabled" + } else { + "&4Could not load $jar see log for details" + } + } + + private fun executeDisable(plugin: Plugin): String { + val disabled = pluginController.unload(plugin) + return if (disabled) { + "Plugin $plugin disabled" + } else { + "Could not disable $plugin, see log for details" + } + } +} diff --git a/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandReload.kt b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandReload.kt new file mode 100644 index 0000000..5bff977 --- /dev/null +++ b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandReload.kt @@ -0,0 +1,25 @@ +package `in`.kyle.mcspring.manager.commands + +import `in`.kyle.mcspring.commands.dsl.command +import `in`.kyle.mcspring.commands.dsl.commandExecutor +import `in`.kyle.mcspring.commands.dsl.mcspring.Command +import org.bukkit.Bukkit +import org.bukkit.plugin.Plugin +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.stereotype.Component + +@Component +class CommandReload { + + @Command( + value = "reload", + aliases = ["rl"], + description = "Reload the server" + ) + fun reload() = command { + then { + sender.sendMessage("Reloading the server...") + Bukkit.getServer().reload() + } + } +} diff --git a/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandSpeed.kt b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandSpeed.kt new file mode 100644 index 0000000..5c17e07 --- /dev/null +++ b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/commands/CommandSpeed.kt @@ -0,0 +1,38 @@ +package `in`.kyle.mcspring.manager.commands + +import `in`.kyle.mcspring.commands.dsl.command +import `in`.kyle.mcspring.commands.dsl.commandExecutor +import `in`.kyle.mcspring.commands.dsl.mcspring.Command +import org.bukkit.entity.Player +import org.springframework.stereotype.Component + +@Component +internal class CommandSpeed { + + @Command( + value = "speed", + description = "Set your movement and fly speed", + usage = "/speed " + ) + fun speed() = command { + requirePlayer { message("Sender must be a player") } + val player = sender as Player + val speed = doubleArg { + parser { + between(0.0, 10.0) { message("Speed must be between 0 and 10") } + } + invalid { message("Speed $it is not a valid speed") } + missing { + message("Fly Speed = ${player.flySpeed * 10}") + message("Walk Speed = ${player.walkSpeed * 10}") + } + } + then { message(speedExecutor(player, speed / 10)) } + } + + private fun speedExecutor(sender: Player, speed: Double): String { + sender.flySpeed = speed.toFloat() + sender.walkSpeed = speed.toFloat() + return "Speed set to $speed" + } +} diff --git a/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/controller/BukkitPluginUnloader.kt b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/controller/BukkitPluginUnloader.kt new file mode 100644 index 0000000..fd61ace --- /dev/null +++ b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/controller/BukkitPluginUnloader.kt @@ -0,0 +1,87 @@ +package `in`.kyle.mcspring.manager.controller + +import org.bukkit.command.Command +import org.bukkit.command.CommandMap +import org.bukkit.command.PluginCommand +import org.bukkit.command.SimpleCommandMap +import org.bukkit.event.Event +import org.bukkit.plugin.Plugin +import org.bukkit.plugin.PluginManager +import org.bukkit.plugin.RegisteredListener +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.context.annotation.Lazy +import org.springframework.stereotype.Component +import java.net.URLClassLoader +import java.util.* +import javax.annotation.PostConstruct + +@Lazy +@Component +class BukkitPluginUnloader( + private val pluginManager: PluginManager, + private val commandMap: CommandMap +) { + + private lateinit var commands: MutableMap + private lateinit var plugins: MutableList + private lateinit var names: MutableMap + private lateinit var listeners: MutableMap> + + @PostConstruct + fun setup() { + plugins = getDeclaredField(pluginManager, "plugins") + names = getDeclaredField(pluginManager, "lookupNames") + listeners = getDeclaredField(pluginManager, "listeners") + val knownCommands = SimpleCommandMap::class.java.getDeclaredField("knownCommands").apply { + isAccessible = true + } + @Suppress("UNCHECKED_CAST") + commands = knownCommands.get(commandMap) as MutableMap + } + + fun unload(plugin: Plugin): Boolean { + pluginManager.disablePlugin(plugin) + synchronized(pluginManager) { + plugins.remove(plugin) + names.remove(plugin.name) + unregisterListeners(plugin) + unregisterCommands(plugin) + closeClassLoader(plugin.javaClass.classLoader) + } + System.gc() + return true + } + + private fun unregisterCommands(plugin: Plugin) { + val unregister = commands.entries + .filter { (it.value as? PluginCommand)?.plugin === plugin } + .toSet() + unregister.forEach { it.value.unregister(commandMap) } + unregister.forEach { commands.remove(it.key) } + } + + private fun unregisterListeners(plugin: Plugin) { + listeners.entries.removeIf { entry -> entry.value.all { it.plugin == plugin } } + } + + private fun closeClassLoader(classLoader: ClassLoader) { + if (classLoader is URLClassLoader) { + setDeclaredField(classLoader, "plugin", null) + setDeclaredField(classLoader, "pluginInit", null) + classLoader.close() + } + } + + private fun getDeclaredField(obj: Any, fieldName: String): T { + val field = obj.javaClass.getDeclaredField(fieldName) + field.isAccessible = true + @Suppress("UNCHECKED_CAST") + return field[obj] as T + } + + private fun setDeclaredField(obj: Any, fieldName: String, value: Any?) { + val field = obj.javaClass.getDeclaredField(fieldName) + field.isAccessible = true + field[obj] = value + } +} diff --git a/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/controller/LogFileController.kt b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/controller/LogFileController.kt new file mode 100644 index 0000000..35a1833 --- /dev/null +++ b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/controller/LogFileController.kt @@ -0,0 +1,21 @@ +package `in`.kyle.mcspring.manager.controller + +import org.bukkit.plugin.Plugin +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.stereotype.Controller +import java.nio.file.Files +import java.nio.file.Paths +import javax.annotation.PostConstruct + +@Controller +internal class LogFileController { + + private val logsFolder = Paths.get("logs") + + @PostConstruct + fun setup() { + Files.list(logsFolder) + .filter { it.toString().endsWith(".log.gz") } + .forEach { Files.delete(it) } + } +} diff --git a/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/controller/PluginController.kt b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/controller/PluginController.kt new file mode 100644 index 0000000..b681bf4 --- /dev/null +++ b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/controller/PluginController.kt @@ -0,0 +1,103 @@ +package `in`.kyle.mcspring.manager.controller + +import org.bukkit.plugin.InvalidDescriptionException +import org.bukkit.plugin.Plugin +import org.bukkit.plugin.PluginLoader +import org.bukkit.plugin.PluginManager +import org.springframework.stereotype.Controller +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.util.logging.Level +import java.util.logging.Logger +import java.util.stream.Collectors + +@Controller +class PluginController( + private val pluginManager: PluginManager, + private val pluginLoader: PluginLoader, + private val unloader: BukkitPluginUnloader, + private val logger: Logger +) { + + val pluginsFolder = Paths.get("plugins") + + val loadablePlugins: Map + get() { + return Files.list(pluginsFolder) + .filter { it.toString().endsWith(".jar") } + .filter { getPluginName(it) != null } + .collect(Collectors.toMap({ getPluginName(it) }, { it })) + } + + val plugins: Set + get() = pluginManager.plugins.toSet() + + val allPlugins: Map + get() { + val allPlugins = mutableMapOf() + loadablePlugins.forEach { allPlugins[it.key] = false } + plugins.forEach { allPlugins[it.name] = it.isEnabled } + return allPlugins + } + + fun load(jar: Path): Plugin? { + return try { + val plugin: Plugin? = pluginManager.loadPlugin(jar.toFile()) + plugin?.onLoad() + plugin + } catch (e: Exception) { + logger.log(Level.SEVERE, "Could not load " + jar.toAbsolutePath()) + logger.log(Level.SEVERE, e) { "" } + null + } + } + + fun unload(plugin: Plugin): Boolean { + if (pluginManager.isPluginEnabled(plugin)) { + try { + return unloader.unload(plugin) + } catch (e: Exception) { + logger.log(Level.SEVERE, "Could not unload " + plugin.name) + logger.log(Level.SEVERE, e) { "" } + } + } + return false + } + + fun isPluginJar(jar: Path): Boolean { + return try { + pluginLoader.getPluginDescription(jar.toFile()) + true + } catch (_: InvalidDescriptionException) { + false + } + } + + fun reload(jar: Path) { + val description = pluginLoader.getPluginDescription(jar.toFile()) + val name = description.name + val plugin = getPlugin(name) + requireNotNull(plugin) {"Plugin $name is not loaded and therefore cannot be reloaded."} + unload(plugin) + load(jar) + } + + fun getPlugin(name: String): Plugin? { + return pluginManager.getPlugin(name) + } + + fun isEnabled(pluginName: String): Boolean { + return getPlugin(pluginName)?.isEnabled ?: false + } + + private fun getPluginName(jar: Path): String? { + val description = try { + pluginLoader.getPluginDescription(jar.toFile()) + } catch (e: InvalidDescriptionException) { + null + } + + return description?.name + } +} diff --git a/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/controller/PluginFileWatcher.kt b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/controller/PluginFileWatcher.kt new file mode 100644 index 0000000..805da49 --- /dev/null +++ b/mcspring-examples/mcspring-plugin-manager/src/main/kotlin/in/kyle/mcspring/manager/controller/PluginFileWatcher.kt @@ -0,0 +1,47 @@ +package `in`.kyle.mcspring.manager.controller + +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service +import java.nio.file.FileSystems +import java.nio.file.Path +import java.nio.file.StandardWatchEventKinds.ENTRY_CREATE +import java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY +import java.nio.file.WatchKey +import java.nio.file.WatchService +import javax.annotation.PostConstruct + +@Service +class PluginFileWatcher( + private val pluginController: PluginController +) { + + lateinit var watcher: WatchService + lateinit var key: WatchKey + + @PostConstruct + fun setup() { + startWatcher() + } + + private fun startWatcher() { + watcher = FileSystems.getDefault().newWatchService() + key = pluginController.pluginsFolder.register(watcher, ENTRY_CREATE, ENTRY_MODIFY) + } + + @Scheduled(fixedRate = 100) + fun pollWatcher() { + for (event in key.pollEvents()) { + val context = event.context() + require(context is Path) { "Context must be a path" } + + if (context.endsWith(".jar") && context == pluginController.pluginsFolder.resolve(context.fileName)) { + if (pluginController.isPluginJar(context)) { + when (event.kind()) { + ENTRY_CREATE -> pluginController.load(context) + ENTRY_MODIFY -> pluginController.reload(context) + } + } + } + } + } +} diff --git a/mcspring-examples/mcspring-plugin-manager/src/test/kotlin/in/kyle/mcspring/manager/Test.kt b/mcspring-examples/mcspring-plugin-manager/src/test/kotlin/in/kyle/mcspring/manager/Test.kt new file mode 100644 index 0000000..92afc12 --- /dev/null +++ b/mcspring-examples/mcspring-plugin-manager/src/test/kotlin/in/kyle/mcspring/manager/Test.kt @@ -0,0 +1,24 @@ +package `in`.kyle.mcspring.manager + +import `in`.kyle.mcspring.e2e.SpigotServerTest +import org.bukkit.Bukkit +import org.bukkit.Material +import org.bukkit.util.Vector +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +//@ExtendWith(SpigotServerTest::class) +class Test { + + @Test + fun testSetABlock() { +// val server = Bukkit.getServer() +// val world = server.worlds[0] +// +// val location = Vector(0, 0, 0).toLocation(world) +// location.block.type = Material.ORANGE_WOOL +// +// Assertions.assertEquals(Material.ORANGE_WOOL, location.block.type) + } +} diff --git a/mcspring-examples/mcspring-plugin-manager/src/test/kotlin_disabled/in/kyle/mcspring/manager/commands/TestCommandClassLoader.kt b/mcspring-examples/mcspring-plugin-manager/src/test/kotlin_disabled/in/kyle/mcspring/manager/commands/TestCommandClassLoader.kt new file mode 100644 index 0000000..dccd5a7 --- /dev/null +++ b/mcspring-examples/mcspring-plugin-manager/src/test/kotlin_disabled/in/kyle/mcspring/manager/commands/TestCommandClassLoader.kt @@ -0,0 +1,19 @@ +package `in`.kyle.mcspring.manager.commands + +import `in`.kyle.mcspring.test.MCSpringTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired + +// TODO +@MCSpringTest +internal class TestCommandClassLoader { +// +// @Test +// fun testClassLoader(@Autowired commandExecutor: TestCommandExecutor) { +// val output = commandExecutor.run("classloader " + javaClass.name) +// assertThat(output).hasSize(2) +// assertThat(output[0]).isEqualTo("ClassLoader: ${javaClass.classLoader}") +// assertThat(output[1]).isEqualTo("Domain: ${javaClass.protectionDomain.codeSource.location}") +// } +} diff --git a/mcspring-examples/mcspring-plugin-manager/src/test/kotlin_disabled/in/kyle/mcspring/manager/commands/TestCommandGamemode.kt b/mcspring-examples/mcspring-plugin-manager/src/test/kotlin_disabled/in/kyle/mcspring/manager/commands/TestCommandGamemode.kt new file mode 100644 index 0000000..fd401ea --- /dev/null +++ b/mcspring-examples/mcspring-plugin-manager/src/test/kotlin_disabled/in/kyle/mcspring/manager/commands/TestCommandGamemode.kt @@ -0,0 +1,28 @@ +package `in`.kyle.mcspring.manager.commands + +import `in`.kyle.mcspring.test.MCSpringTest + +// TODO +@MCSpringTest +class TestCommandGamemode { +// +// @Autowired +// lateinit var executor: TestCommandExecutor +// +// @ParameterizedTest +// @CsvSource( +// "gmc, CREATIVE", +// "gms, SURVIVAL", +// "gm creative, CREATIVE", +// "gm survival, SURVIVAL", +// "gm 1, CREATIVE", +// "gm 0, SURVIVAL" +// ) +// fun testGamemodes(command: String, targetGameMode: GameMode) { +// val (sender, messages) = executor.makeTestPlayer() +// executor.run(sender, command) +// assertThat(messages).first().asString() +// .isEqualTo("Game mode set to ${targetGameMode.name.toLowerCase()}") +// verify(sender, times(1)).gameMode = targetGameMode +// } +} diff --git a/mcspring-examples/mcspring-plugin-manager/src/test/kotlin_disabled/in/kyle/mcspring/manager/commands/TestCommandSpeed.kt b/mcspring-examples/mcspring-plugin-manager/src/test/kotlin_disabled/in/kyle/mcspring/manager/commands/TestCommandSpeed.kt new file mode 100644 index 0000000..1728a48 --- /dev/null +++ b/mcspring-examples/mcspring-plugin-manager/src/test/kotlin_disabled/in/kyle/mcspring/manager/commands/TestCommandSpeed.kt @@ -0,0 +1,34 @@ +package `in`.kyle.mcspring.manager.commands + +import `in`.kyle.mcspring.test.MCSpringTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.Mockito.* +import org.springframework.beans.factory.annotation.Autowired + +// TODO +@MCSpringTest +internal class TestCommandSpeed() { +// +// @Autowired +// lateinit var executor: TestCommandExecutor +// +// @Test +// fun testSpeed() { +// val (sender, messages) = executor.makeTestPlayer() +// executor.run(sender, "speed 10") +// assertThat(messages).first().asString().matches("Speed set to [^ ]+") +// verify(sender, times(1)).walkSpeed = 10F +// verify(sender, times(1)).flySpeed = 10F +// } +// +// @Test +// fun testSpeedUsage() { +// val (sender, messages) = executor.makeTestPlayer() +// executor.run(sender, "speed") +// assertThat(messages).first().asString().matches("Usage: .+") +// verify(sender, never()).walkSpeed = anyFloat() +// verify(sender, never()).flySpeed = anyFloat() +// } +} diff --git a/mcspring-examples/pom.xml b/mcspring-examples/pom.xml deleted file mode 100644 index 946e408..0000000 --- a/mcspring-examples/pom.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - mcspring-starter - in.kyle.mcspring - 0.0.9 - ../mcspring-build/mcspring-starter - - - - simple-factions - simple-factions-addon - - - 4.0.0 - - pom - mcspring-examples - - - - - ${project.parent.groupId} - mcspring-starter - - - - diff --git a/mcspring-examples/settings.gradle.kts b/mcspring-examples/settings.gradle.kts new file mode 100644 index 0000000..1b932af --- /dev/null +++ b/mcspring-examples/settings.gradle.kts @@ -0,0 +1,12 @@ +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + mavenCentral() + jcenter() + } +} + +include(":mcspring-plugin-manager") +include(":simple-factions") +include(":simple-factions-addon") diff --git a/mcspring-examples/simple-factions-addon/build.gradle.kts b/mcspring-examples/simple-factions-addon/build.gradle.kts new file mode 100644 index 0000000..96aac72 --- /dev/null +++ b/mcspring-examples/simple-factions-addon/build.gradle.kts @@ -0,0 +1,3 @@ +dependencies { + compileOnly(project(":simple-factions")) +} diff --git a/mcspring-examples/simple-factions-addon/pom.xml b/mcspring-examples/simple-factions-addon/pom.xml deleted file mode 100644 index ea38461..0000000 --- a/mcspring-examples/simple-factions-addon/pom.xml +++ /dev/null @@ -1,36 +0,0 @@ - - 4.0.0 - - - mcspring-examples - in.kyle.mcspring - 0.0.9 - - - simple-factions-addon - jar - 0.69 - - - UTF-8 - 1.15.1-R0.1-SNAPSHOT - 1.8 - 1.8 - true - - - - - in.kyle.mcspring - simple-factions - ${project.parent.version} - provided - - - org.projectlombok - lombok - - - diff --git a/mcspring-examples/simple-factions-addon/src/main/java/test/other/stats/FactionsStats.java b/mcspring-examples/simple-factions-addon/src/main/java/test/other/stats/FactionsStats.java deleted file mode 100644 index d1967c0..0000000 --- a/mcspring-examples/simple-factions-addon/src/main/java/test/other/stats/FactionsStats.java +++ /dev/null @@ -1,32 +0,0 @@ -package test.other.stats; - -import org.bukkit.event.EventHandler; -import org.bukkit.event.player.PlayerMoveEvent; -import in.kyle.mcspring.command.Command; -import in.kyle.mcspring.processor.annotation.PluginDepend; -import in.kyle.mcspring.processor.annotation.SpringPlugin; - -import org.example.factions.api.FactionsApi; -import org.springframework.stereotype.Component; - -import lombok.RequiredArgsConstructor; - -@Component -@RequiredArgsConstructor -@PluginDepend(plugins = "factions") -@SpringPlugin(name = "faction-stats", description = "Shows stats for factions") -class FactionsStats { - - private final FactionsApi factionsApi; - - @Command("stats") - String test() { -// return "test"; - return String.format("There are %d factions", factionsApi.getFactions().size()); - } - - @EventHandler - void move(PlayerMoveEvent e) { - System.out.println(e.getPlayer().getName() + " moved"); - } -} diff --git a/mcspring-examples/simple-factions-addon/src/main/kotlin/test/other/stats/FactionsStats.kt b/mcspring-examples/simple-factions-addon/src/main/kotlin/test/other/stats/FactionsStats.kt new file mode 100644 index 0000000..552a170 --- /dev/null +++ b/mcspring-examples/simple-factions-addon/src/main/kotlin/test/other/stats/FactionsStats.kt @@ -0,0 +1,25 @@ +package test.other.stats + +import lombok.RequiredArgsConstructor +import org.bukkit.event.EventHandler +import org.bukkit.event.player.PlayerMoveEvent +import org.example.factions.api.FactionsApi +import org.springframework.stereotype.Component + +@Component +@RequiredArgsConstructor +@PluginDepend(plugins = "factions") +internal class FactionsStats { + private val factionsApi: FactionsApi? = null + + @Command("stats") + fun test(): String { +// return "test"; + return String.format("There are %d factions", factionsApi!!.factions.size) + } + + @EventHandler + fun move(e: PlayerMoveEvent) { + println(e.getPlayer().getName().toString() + " moved") + } +} diff --git a/mcspring-examples/simple-factions/build.gradle.kts b/mcspring-examples/simple-factions/build.gradle.kts new file mode 100644 index 0000000..ab53060 --- /dev/null +++ b/mcspring-examples/simple-factions/build.gradle.kts @@ -0,0 +1,4 @@ +dependencies { + implementation("in.kyle.mcspring:mcspring-base:+") + implementation("in.kyle.mcspring:mcspring-commands-dsl:+") +} diff --git a/mcspring-examples/simple-factions/pom.xml b/mcspring-examples/simple-factions/pom.xml deleted file mode 100644 index 925a784..0000000 --- a/mcspring-examples/simple-factions/pom.xml +++ /dev/null @@ -1,31 +0,0 @@ - - 4.0.0 - - - mcspring-examples - in.kyle.mcspring - 0.0.9 - - - simple-factions - jar - - - 1.15.1-R0.1-SNAPSHOT - 1.8 - 1.8 - - - - - in.kyle.mcspring - mcspring-subcommands - - - org.projectlombok - lombok - - - diff --git a/mcspring-examples/simple-factions/src/main/java/org/example/factions/api/Faction.java b/mcspring-examples/simple-factions/src/main/kotlin/org/example/factions/api/Faction.java similarity index 91% rename from mcspring-examples/simple-factions/src/main/java/org/example/factions/api/Faction.java rename to mcspring-examples/simple-factions/src/main/kotlin/org/example/factions/api/Faction.java index f1a0404..3d4ab4c 100644 --- a/mcspring-examples/simple-factions/src/main/java/org/example/factions/api/Faction.java +++ b/mcspring-examples/simple-factions/src/main/kotlin/org/example/factions/api/Faction.java @@ -6,7 +6,6 @@ import java.util.Map; import java.util.UUID; -import in.kyle.mcspring.processor.annotation.SpringPlugin; import lombok.Getter; @Getter diff --git a/mcspring-examples/simple-factions/src/main/java/org/example/factions/api/FactionsApi.java b/mcspring-examples/simple-factions/src/main/kotlin/org/example/factions/api/FactionsApi.java similarity index 76% rename from mcspring-examples/simple-factions/src/main/java/org/example/factions/api/FactionsApi.java rename to mcspring-examples/simple-factions/src/main/kotlin/org/example/factions/api/FactionsApi.java index 66f6155..a5badb6 100644 --- a/mcspring-examples/simple-factions/src/main/java/org/example/factions/api/FactionsApi.java +++ b/mcspring-examples/simple-factions/src/main/kotlin/org/example/factions/api/FactionsApi.java @@ -1,12 +1,11 @@ package org.example.factions.api; import org.bukkit.entity.Player; -import in.kyle.mcspring.processor.annotation.SpringPlugin; import java.util.List; import java.util.Optional; -@SpringPlugin(name = "factions", description = "A simple factions plugin") +//@SpringPlugin(name = "factions", description = "A simple factions plugin") public interface FactionsApi { void addFaction(Faction faction); diff --git a/mcspring-examples/simple-factions/src/main/java/org/example/factions/commands/FactionCommand.java b/mcspring-examples/simple-factions/src/main/kotlin/org/example/factions/commands/FactionCommand.java similarity index 92% rename from mcspring-examples/simple-factions/src/main/java/org/example/factions/commands/FactionCommand.java rename to mcspring-examples/simple-factions/src/main/kotlin/org/example/factions/commands/FactionCommand.java index dcfe5a0..0c53924 100644 --- a/mcspring-examples/simple-factions/src/main/java/org/example/factions/commands/FactionCommand.java +++ b/mcspring-examples/simple-factions/src/main/kotlin/org/example/factions/commands/FactionCommand.java @@ -10,7 +10,7 @@ import java.util.stream.Collectors; import in.kyle.mcspring.command.Command; -import in.kyle.mcspring.subcommands.PluginCommand; +import in.kyle.mcspring.commandDsl.plugincommand.api.PluginCommand; import lombok.RequiredArgsConstructor; @Component @@ -23,17 +23,13 @@ class FactionCommand { void faction(PluginCommand command) { command.on("create", this::create); command.on("delete", this::delete); - command.on("list", this::list); + command.on("list", this::factionList); command.on("join", this::join); - command.on("mine", this::info); + command.on("mine", this::factionMine); command.onInvalid(s -> String.format("Invalid sub-command %s", s)); command.otherwise("Usage: /faction "); } - private void info(PluginCommand command) { - command.then(this::factionMine); - } - private void join(PluginCommand command) { command.withMap(factions.getFactions() .stream() @@ -57,10 +53,6 @@ private void delete(PluginCommand command) { command.otherwise("Usage: /faction delete "); } - private void list(PluginCommand command) { - command.then(this::factionList); - } - private String factionJoin(Player sender, Faction faction) { if (!factions.isFactionMember(sender)) { faction.getMembers().put(sender.getUniqueId(), Faction.Rank.MEMBER); diff --git a/mcspring-examples/simple-factions/src/main/java/org/example/factions/controller/FactionsController.java b/mcspring-examples/simple-factions/src/main/kotlin/org/example/factions/controller/FactionsController.java similarity index 100% rename from mcspring-examples/simple-factions/src/main/java/org/example/factions/controller/FactionsController.java rename to mcspring-examples/simple-factions/src/main/kotlin/org/example/factions/controller/FactionsController.java diff --git a/pom.xml b/pom.xml deleted file mode 100644 index f3ae3b8..0000000 --- a/pom.xml +++ /dev/null @@ -1,220 +0,0 @@ - - - 4.0.0 - - - mcspring-build - mcspring-api - - - in.kyle.mcspring - mcspring-parent - pom - 0.0.9 - mcspring - Adds Spring support to Bukkit plugins - https://github.com/kylepls/mc-spring - - - UTF-8 - 8 - ${java.version} - ${java.version} - 1.15.1-R0.1-SNAPSHOT - 2.2.4.RELEASE - - - - - MIT License - http://www.opensource.org/licenses/mit-license.php - repo - - - - - GitHub - https://github.com/kylepls/mcspring/issues - - - - - api.wiki - https://github.com/phillip-kruger/apiee/wiki - - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - - https://github.com/kylepls/mcspring - scm:git:git://github.com/kylepls/mcspring.git - scm:git:ssh://git@github.com:kylepls/mcspring.git - - - - - Kyle - mail@kyle.in - kylepls - - - - - - spigot-repo - https://hub.spigotmc.org/nexus/content/repositories/snapshots/ - - - - - - - org.projectlombok - lombok - 1.18.12 - provided - - - org.springframework.boot - spring-boot-dependencies - ${spring.version} - pom - import - - - in.kyle.mcspring - mcspring-base - ${project.parent.version} - - - junit - junit - 4.12 - test - - - org.spigotmc - spigot-api - ${spigot.version} - provided - - - net.md-5 - bungeecord-chat - - - - - - - - - - - release - - - release - - - - org.apache.maven.plugins - maven-source-plugin - - - org.apache.maven.plugins - maven-javadoc-plugin - - - org.sonatype.plugins - nexus-staging-maven-plugin - - - org.apache.maven.plugins - maven-gpg-plugin - - - - - - - - - - - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.8 - true - - ossrh - https://oss.sonatype.org/ - true - - - - org.apache.maven.plugins - maven-gpg-plugin - 1.6 - - - sign-artifacts - verify - - sign - - - - - - org.apache.maven.plugins - maven-compiler-plugin - 3.8.1 - - true - false - - -AartifactId=${project.artifactId} - - - - - org.apache.maven.plugins - maven-source-plugin - 3.2.1 - - - attach-sources - - jar - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 3.1.1 - - - attach-javadocs - - jar - - - - - - - - diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..daee2ec --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,24 @@ +plugins { + id("com.gradle.enterprise").version("3.3.4") +} + +rootProject.name = "mcspring" + +include(":mcspring-api") +include(":mcspring-api:mcspring-commands-dsl") +include(":mcspring-api:mcspring-base") +include(":mcspring-api:mcspring-vault") +include(":mcspring-api:mcspring-rx") +include(":mcspring-api:mcspring-guis") +include(":mcspring-api:mcspring-chat") +include(":mcspring-api:mcspring-chat-actions") +include(":mcspring-api:mcspring-gradle-plugin") +include(":mcspring-api:mcspring-e2e") +include(":mcspring-api:mcspring-nms") + +gradleEnterprise { + buildScan { + termsOfServiceUrl = "https://gradle.com/terms-of-service" + termsOfServiceAgree = "yes" + } +}