diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..c251f4a7 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,117 @@ +name: Build + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Get Release Info + id: release-info + uses: GeyserMC/actions/previous-release@master + with: + data: ${{ vars.RELEASEACTION_PREVRELEASE }} + + - name: Checkout repository and submodules + # See https://github.com/actions/checkout/commits + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 + with: + submodules: recursive + + - name: Validate Gradle Wrapper + # See https://github.com/gradle/wrapper-validation-action/commits + uses: gradle/actions/wrapper-validation@db19848a5fa7950289d3668fb053140cf3028d43 # v3.3.2 + + - name: Setup Java + # See https://github.com/actions/setup-java/commits + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 + with: + java-version: 17 + distribution: temurin + + - name: Setup Gradle + # See https://github.com/gradle/actions/commits + uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0 + with: + cache-read-only: ${{ github.ref_name != 'master' && github.ref_name != 'development' }} + + - name: Build Floodgate + run: ./gradlew build + env: + BUILD_NUMBER: ${{ steps.release-info.outputs.curentRelease }} + + - name: Archive artifacts (Floodgate Bungee) + # See https://github.com/actions/upload-artifact/commits + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 + if: success() + with: + name: Floodgate Bungee + path: bungee/build/libs/floodgate-bungee.jar + if-no-files-found: error + + - name: Archive artifacts (Floodgate Spigot) + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 + if: success() + with: + name: Floodgate Spigot + path: spigot/build/libs/floodgate-spigot.jar + if-no-files-found: error + + - name: Archive artifacts (Floodgate Velocity) + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 + if: success() + with: + name: Floodgate Velocity + path: velocity/build/libs/floodgate-velocity.jar + if-no-files-found: error + + - name: Publish to Maven Repository + if: ${{ github.repository == 'GeyserMC/Floodgate' }} + run: ./gradlew publish + env: + BUILD_NUMBER: ${{ steps.release-info.outputs.curentRelease }} + ORG_GRADLE_PROJECT_geysermcUsername: ${{ vars.DEPLOY_USER }} + ORG_GRADLE_PROJECT_geysermcPassword: ${{ secrets.DEPLOY_PASS }} + + - name: Get Version + if: ${{ success() && github.repository == 'GeyserMC/Floodgate' && github.ref_name == 'master' }} + id: get-version + run: | + version=$(cat gradle.properties | grep -o "version=[0-9\\.]*" | cut -d"=" -f2) + echo "VERSION=${version}" >> $GITHUB_OUTPUT + + - name: Get Release Metadata + if: ${{ success() && github.repository == 'GeyserMC/Floodgate' && github.ref_name == 'master' }} + uses: GeyserMC/actions/release@master + id: metadata + with: + appID: ${{ secrets.RELEASE_APP_ID }} + appPrivateKey: ${{ secrets.RELEASE_APP_PK }} + files: | + bungee:bungee/build/libs/floodgate-bungee.jar + spigot:spigot/build/libs/floodgate-spigot.jar + velocity:velocity/build/libs/floodgate-velocity.jar + releaseEnabled: false + saveMetadata: true + releaseProject: 'floodgate' + releaseVersion: ${{ steps.get-version.outputs.VERSION }} + + - name: Publish to Downloads API + if: ${{ success() && github.ref_name == 'master' && github.repository == 'GeyserMC/Floodgate' }} + uses: GeyserMC/actions/upload-release@master + with: + username: ${{ vars.DOWNLOADS_USERNAME }} + privateKey: ${{ secrets.DOWNLOADS_PRIVATE_KEY }} + host: ${{ secrets.DOWNLOADS_SERVER_IP }} + files: | + bungee/build/libs/floodgate-bungee.jar + spigot/build/libs/floodgate-spigot.jar + velocity/build/libs/floodgate-velocity.jar + + - name: Notify Discord + if: ${{ (success() || failure()) && github.repository == 'GeyserMC/Floodgate' }} + uses: GeyserMC/actions/notify-discord@master + with: + discordWebhook: ${{ secrets.DISCORD_WEBHOOK }} + status: ${{ job.status }} + body: ${{ steps.metadata.outputs.body }} diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index 4edc17e3..8d59fecf 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -8,35 +8,57 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - name: Set Build Number + run: | + echo "BUILD_NUMBER=${GITHUB_RUN_NUMBER}" >> $GITHUB_ENV - - name: Set up JDK 1.8 - uses: actions/setup-java@v2 + - name: Checkout repository and submodules + # See https://github.com/actions/checkout/commits + uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29 # v4.1.6 with: - distribution: 'temurin' - java-version: '8' - cache: 'gradle' + submodules: recursive - - name: Build with Maven + - name: Validate Gradle Wrapper + # See https://github.com/gradle/wrapper-validation-action/commits + uses: gradle/actions/wrapper-validation@db19848a5fa7950289d3668fb053140cf3028d43 # v3.3.2 + + - name: Setup Java + # See https://github.com/actions/setup-java/commits + uses: actions/setup-java@99b8673ff64fbf99d8d325f52d9a5bdedb8483e9 # v4.2.1 + with: + java-version: 17 + distribution: temurin + + - name: Setup Gradle + # See https://github.com/gradle/actions/commits + uses: gradle/actions/setup-gradle@db19848a5fa7950289d3668fb053140cf3028d43 # v3.3.2 + with: + cache-read-only: true + + - name: Build Floodgate run: ./gradlew build - name: Archive artifacts (Floodgate Bungee) - uses: actions/upload-artifact@v2 + # See https://github.com/actions/upload-artifact/commits + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3 if: success() with: name: Floodgate Bungee path: bungee/build/libs/floodgate-bungee.jar + if-no-files-found: error - name: Archive artifacts (Floodgate Spigot) - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 if: success() with: name: Floodgate Spigot path: spigot/build/libs/floodgate-spigot.jar + if-no-files-found: error - name: Archive artifacts (Floodgate Velocity) - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 if: success() with: name: Floodgate Velocity - path: velocity/build/libs/floodgate-velocity.jar \ No newline at end of file + path: velocity/build/libs/floodgate-velocity.jar + if-no-files-found: error diff --git a/.gitignore b/.gitignore index 58ae9e78..27c8ac2c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -# Created by https://www.gitignore.io/api/git,java,maven,eclipse,netbeans,jetbrains+all -# Edit at https://www.gitignore.io/?templates=git,java,maven,eclipse,netbeans,jetbrains+all +# Created by https://www.gitignore.io/api/git,java,gradle,eclipse,netbeans,jetbrains+all +# Edit at https://www.gitignore.io/?templates=git,gradle,maven,eclipse,netbeans,jetbrains+all ### Eclipse ### .metadata @@ -52,22 +52,19 @@ local.properties # Annotation Processing .apt_generated/ +.apt_generated_test/ # Scala IDE specific (Scala & Java development for Eclipse) .cache-main .scala_dependencies .worksheet -### Eclipse Patch ### -# Eclipse Core -.project - -# JDT-specific (Eclipse Java Development Tools) -.classpath - -# Annotation Processing -.apt_generated +# Uncomment this line if you wish to ignore the project description file. +# Typically, this file would be tracked if it contains build/dependency configurations: +#.project +### Eclipse Patch ### +# Spring Boot Tooling .sts4-cache/ ### Git ### @@ -109,9 +106,10 @@ local.properties # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* +replay_pid* ### JetBrains+all ### -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff @@ -121,6 +119,9 @@ hs_err_pid* .idea/**/dictionaries .idea/**/shelf +# AWS User-specific +.idea/**/aws.xml + # Generated files .idea/**/contentModel.xml @@ -141,11 +142,14 @@ hs_err_pid* # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr +.idea/artifacts +.idea/compiler.xml +.idea/jarRepositories.xml +.idea/modules.xml +.idea/*.iml +.idea/modules +*.iml +*.ipr # CMake cmake-build-*/ @@ -168,6 +172,9 @@ atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml +# SonarLint plugin +.idea/sonarlint/ + # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties @@ -181,32 +188,13 @@ fabric.properties .idea/caches/build_file_checksums.ser ### JetBrains+all Patch ### -# Ignores the whole .idea folder and all .iml files -# See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. -.idea/ +.idea/* -# Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 - -*.iml -modules.xml -.idea/misc.xml -*.ipr - -# Sonarlint plugin -.idea/sonarlint - -### Maven ### -target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -.mvn/wrapper/maven-wrapper.jar +!.idea/codeStyles +!.idea/runConfigurations ### NetBeans ### **/nbproject/private/ @@ -218,8 +206,29 @@ dist/ nbdist/ .nb-gradle/ -# End of https://www.gitignore.io/api/git,java,maven,eclipse,netbeans,jetbrains+all -gradle/ -**/.gradle/ +### Gradle ### +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# End of https://www.gitignore.io/api/git,java,gradle,eclipse,netbeans,jetbrains+all /core/src/main/resources/languages/ diff --git a/.gitmodules b/.gitmodules index 074c6a2c..f04fc7a9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "core/src/main/resources/languages"] path = core/src/main/resources/languages url = https://github.com/GeyserMC/languages - branch = l10n_floodgate + branch = floodgate \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile deleted file mode 100644 index 0f9cf367..00000000 --- a/Jenkinsfile +++ /dev/null @@ -1,112 +0,0 @@ -pipeline { - agent any - tools { - gradle 'Gradle 7' - jdk 'Java 8' - } - options { - buildDiscarder(logRotator(artifactNumToKeepStr: '5')) - } - stages { - stage ('Build') { - steps { - sh 'git submodule update --init --recursive' - rtGradleRun( - usesPlugin: true, - tool: 'Gradle 7', - buildFile: 'build.gradle.kts', - tasks: 'clean build', - ) - } - post { - success { - archiveArtifacts artifacts: '**/build/libs/floodgate-*.jar', - excludes: '**/floodgate-parent-*.jar', - fingerprint: true - } - } - } - - stage ('Deploy') { - when { - anyOf { - branch "master" - branch "dev/2.1.1" - } - } - - steps { - rtGradleDeployer( - id: "GRADLE_DEPLOYER", - serverId: "opencollab-artifactory", - releaseRepo: "maven-releases", - snapshotRepo: "maven-snapshots" - ) - rtGradleResolver( - id: "GRADLE_RESOLVER", - serverId: "opencollab-artifactory" - ) - rtGradleRun( - usesPlugin: true, - tool: 'Gradle 7', - rootDir: "", - useWrapper: true, - buildFile: 'build.gradle.kts', - tasks: 'artifactoryPublish', - deployerId: "GRADLE_DEPLOYER", - resolverId: "GRADLE_RESOLVER" - ) - rtPublishBuildInfo( - serverId: "opencollab-artifactory" - ) - } - } - } - - post { - always { - script { - def changeLogSets = currentBuild.changeSets - def message = "**Changes:**" - - if (changeLogSets.size() == 0) { - message += "\n*No changes.*" - } else { - def repositoryUrl = scm.userRemoteConfigs[0].url.replace(".git", "") - def count = 0; - def extra = 0; - for (int i = 0; i < changeLogSets.size(); i++) { - def entries = changeLogSets[i].items - for (int j = 0; j < entries.length; j++) { - if (count <= 10) { - def entry = entries[j] - def commitId = entry.commitId.substring(0, 6) - message += "\n - [`${commitId}`](${repositoryUrl}/commit/${entry.commitId}) ${entry.msg}" - count++ - } else { - extra++; - } - } - } - - if (extra != 0) { - message += "\n - ${extra} more commits" - } - } - - env.changes = message - } - deleteDir() - withCredentials([string(credentialsId: 'geyser-discord-webhook', variable: 'DISCORD_WEBHOOK')]) { - discordSend description: "**Build:** [${currentBuild.id}](${env.BUILD_URL})\n**Status:** [${currentBuild.currentResult}](${env.BUILD_URL})\n${changes}\n\n[**Artifacts on Jenkins**](https://ci.opencollab.dev/job/GeyserMC/job/Floodgate)", footer: 'Open Collaboration Jenkins', link: env.BUILD_URL, successful: currentBuild.resultIsBetterOrEqualTo('SUCCESS'), title: "${env.JOB_NAME} #${currentBuild.id}", webhookURL: DISCORD_WEBHOOK - } - } - success { - script { - if (env.BRANCH_NAME == 'master') { - build propagate: false, wait: false, job: 'GeyserMC/Floodgate-Fabric/master', parameters: [booleanParam(name: 'SKIP_DISCORD', value: true)] - } - } - } - } -} \ No newline at end of file diff --git a/README.md b/README.md index 5a9d098e..9b8e6600 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # Floodgate [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) -[![Build Status](https://ci.opencollab.dev/job/GeyserMC/job/Floodgate/job/master/badge/icon)](https://ci.opencollab.dev/job/GeyserMC/job/Floodgate/job/master/) +[![Build Status](https://github.com/GeyserMC/Floodgate/actions/workflows/build.yml/badge.svg?branch=master)](https://github.com/GeyserMC/Floodgate/actions/workflows/build.yml?query=branch%3Amaster) [![Discord](https://img.shields.io/discord/613163671870242838.svg?color=%237289da&label=discord)](http://discord.geysermc.org/) [![HitCount](https://hits.dwyl.com/GeyserMC/Floodgate.svg)](http://hits.dwyl.com/GeyserMC/Floodgate) +[Download](https://geysermc.org/download/?project=floodgate) + Hybrid mode plugin to allow for connections from [Geyser](https://github.com/GeyserMC/Geyser) to join online mode servers. Geyser is an open collaboration project by [CubeCraft Games](https://cubecraft.net). -See the [Floodgate](https://github.com/GeyserMC/Geyser/wiki/Floodgate) page in the Geyser Wiki for more info about the what Floodgate is, how you setup Floodgate and known issues/caveats. - -See the [Floodgate wiki](https://github.com/GeyserMC/Floodgate/wiki) (currently work in progress) for a more in-depth look into Floodgate, how it works and the Floodgate API. +See the [Floodgate](https://geysermc.org/wiki/floodgate/) section in the GeyserMC Wiki for more info about what Floodgate is, how you setup Floodgate and known issues/caveats. Additionally, it includes a more in-depth look into how Floodgate works and the Floodgate API. diff --git a/ap/build.gradle.kts b/ap/build.gradle.kts new file mode 100644 index 00000000..e69de29b diff --git a/ap/src/main/java/org/geysermc/floodgate/ap/AutoBindProcessor.java b/ap/src/main/java/org/geysermc/floodgate/ap/AutoBindProcessor.java new file mode 100644 index 00000000..52bd93d2 --- /dev/null +++ b/ap/src/main/java/org/geysermc/floodgate/ap/AutoBindProcessor.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.ap; + +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; + +@SupportedAnnotationTypes("*") +@SupportedSourceVersion(SourceVersion.RELEASE_8) +public class AutoBindProcessor extends ClassProcessor { + public AutoBindProcessor() { + super("org.geysermc.floodgate.util.AutoBind"); + } +} diff --git a/ap/src/main/java/org/geysermc/floodgate/ap/ClassProcessor.java b/ap/src/main/java/org/geysermc/floodgate/ap/ClassProcessor.java new file mode 100644 index 00000000..3758711a --- /dev/null +++ b/ap/src/main/java/org/geysermc/floodgate/ap/ClassProcessor.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.ap; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import javax.tools.Diagnostic.Kind; +import javax.tools.FileObject; +import javax.tools.StandardLocation; + +/* + * Copied from Geyser + */ +public class ClassProcessor extends AbstractProcessor { + private final String annotationClassName; + + private Path outputPath; + + private final Set locations = new HashSet<>(); + + public ClassProcessor(String annotationClassName) { + this.annotationClassName = annotationClassName; + } + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + + processingEnv.getMessager().printMessage( + Kind.NOTE, "Initializing processor " + annotationClassName + ); + + String outputFile = processingEnv.getOptions().get("metadataOutputFile"); + if (outputFile != null && !outputFile.isEmpty()) { + outputPath = Paths.get(outputFile); + } + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + if (roundEnv.processingOver()) { + if (!roundEnv.errorRaised()) { + complete(); + } + + return false; + } + + if (!contains(annotations, annotationClassName)) { + return false; + } + + for (Element element : roundEnv.getRootElements()) { + if (element.getKind() != ElementKind.CLASS) { + continue; + } + + if (!contains(element.getAnnotationMirrors(), annotationClassName)) { + continue; + } + + TypeElement typeElement = (TypeElement) element; + locations.add(typeElement.getQualifiedName().toString()); + } + return false; + } + + public boolean contains(Collection elements, String className) { + if (elements.isEmpty()) { + return false; + } + + for (TypeElement element : elements) { + if (element.getQualifiedName().contentEquals(className)) { + return true; + } + } + + return false; + } + + public boolean contains(List elements, String className) { + if (elements.isEmpty()) { + return false; + } + + for (AnnotationMirror element : elements) { + if (element.getAnnotationType().toString().equals(className)) { + return true; + } + } + + return false; + } + + public void complete() { + // Read existing annotation list and verify each class still has this annotation + try (BufferedReader reader = createReader()) { + if (reader != null) { + reader.lines().forEach(canonicalName -> { + if (!locations.contains(canonicalName)) { + + TypeElement element = + processingEnv.getElementUtils().getTypeElement(canonicalName); + + if (element != null && element.getKind() == ElementKind.CLASS && + contains(element.getAnnotationMirrors(), annotationClassName)) { + locations.add(canonicalName); + } + } + }); + } + } catch (IOException e) { + e.printStackTrace(); + } + + if (!locations.isEmpty()) { + try (BufferedWriter writer = createWriter()) { + for (String location : locations) { + writer.write(location); + writer.newLine(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } else { + processingEnv.getMessager().printMessage(Kind.NOTE, + "Did not find any classes annotated with " + annotationClassName + ); + } + + processingEnv.getMessager().printMessage( + Kind.NOTE, "Completed processing for " + annotationClassName + ); + } + + private BufferedReader createReader() throws IOException { + if (outputPath != null) { + processingEnv.getMessager().printMessage(Kind.NOTE, + "Reading existing " + annotationClassName + " list from " + outputPath + ); + + return Files.newBufferedReader(outputPath); + } + + FileObject obj = processingEnv.getFiler().getResource( + StandardLocation.CLASS_OUTPUT, "", annotationClassName + ); + + if (obj != null) { + processingEnv.getMessager().printMessage( + Kind.NOTE, + "Reading existing " + annotationClassName + " list from " + obj.toUri() + ); + + try { + return new BufferedReader(obj.openReader(false)); + } catch (NoSuchFileException ignored) {} + } + return null; + } + + private BufferedWriter createWriter() throws IOException { + if (outputPath != null) { + processingEnv.getMessager().printMessage( + Kind.NOTE, "Writing " + annotationClassName + " to " + outputPath + ); + + return Files.newBufferedWriter(outputPath); + } + + FileObject obj = processingEnv.getFiler().createResource( + StandardLocation.CLASS_OUTPUT, "", annotationClassName + ); + + processingEnv.getMessager().printMessage( + Kind.NOTE, "Writing " + annotationClassName + " to " + obj.toUri() + ); + + return new BufferedWriter(obj.openWriter()); + } +} diff --git a/ap/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/ap/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 00000000..53b2e379 --- /dev/null +++ b/ap/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +org.geysermc.floodgate.ap.AutoBindProcessor \ No newline at end of file diff --git a/api/build.gradle.kts b/api/build.gradle.kts index 0bd75df1..70579bbc 100644 --- a/api/build.gradle.kts +++ b/api/build.gradle.kts @@ -1,6 +1,18 @@ +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + dependencies { - api("org.geysermc", "common", Versions.geyserVersion) + api("org.geysermc.geyser", "common", Versions.geyserVersion) api("org.geysermc.cumulus", "cumulus", Versions.cumulusVersion) + api("org.geysermc.event", "events", Versions.eventsVersion) compileOnly("io.netty", "netty-transport", Versions.nettyVersion) } + +tasks { + named("jar") { + archiveClassifier.set("") + } + named("shadowJar") { + archiveClassifier.set("shaded") + } +} diff --git a/api/src/main/java/org/geysermc/floodgate/api/FloodgateApi.java b/api/src/main/java/org/geysermc/floodgate/api/FloodgateApi.java index 45171ab6..a51252a3 100644 --- a/api/src/main/java/org/geysermc/floodgate/api/FloodgateApi.java +++ b/api/src/main/java/org/geysermc/floodgate/api/FloodgateApi.java @@ -28,8 +28,9 @@ import java.util.Collection; import java.util.UUID; import java.util.concurrent.CompletableFuture; -import org.geysermc.cumulus.Form; -import org.geysermc.cumulus.util.FormBuilder; +import org.geysermc.cumulus.form.Form; +import org.geysermc.cumulus.form.util.FormBuilder; +import org.geysermc.floodgate.api.event.FloodgateEventBus; import org.geysermc.floodgate.api.link.PlayerLink; import org.geysermc.floodgate.api.player.FloodgatePlayer; import org.geysermc.floodgate.api.unsafe.Unsafe; @@ -84,8 +85,8 @@ static FloodgateApi getInstance() { /** * Checks if the uuid of the player has the {@link #createJavaPlayerId(long)} format. This - * method can't validate a linked player uuid, since that doesn't equal the format. Use {@link - * #isFloodgatePlayer(UUID)} if you want to include linked accounts. + * method can't validate a linked player uuid, since that doesn't equal the format. Use + * {@link #isFloodgatePlayer(UUID)} if you want to include linked accounts. * * @param uuid the uuid to check * @return true if the given uuid has the correct format. @@ -94,7 +95,23 @@ static FloodgateApi getInstance() { boolean sendForm(UUID uuid, Form form); - boolean sendForm(UUID uuid, FormBuilder formBuilder); + boolean sendForm(UUID uuid, FormBuilder formBuilder); + + boolean closeForm(UUID uuid); + + /** + * @deprecated since Cumulus 1.1 and will be removed when Cumulus 2.0 releases. Please use the + * new form classes instead. + */ + @Deprecated + boolean sendForm(UUID uuid, org.geysermc.cumulus.Form form); + + /** + * @deprecated since Cumulus 1.1 and will be removed when Cumulus 2.0 releases. Please use the + * new form classes instead. + */ + @Deprecated + boolean sendForm(UUID uuid, org.geysermc.cumulus.util.FormBuilder formBuilder); boolean transferPlayer(UUID uuid, String address, int port); @@ -108,9 +125,9 @@ static FloodgateApi getInstance() { CompletableFuture getXuidFor(String gamertag); /** - * Get the xuid of the player that has the given gamertag. It does the same thing as {@link - * #getXuidFor(String)} except that this method will return the xuid in Floodgate uuid format - * instead of just a long + * Get the xuid of the player that has the given gamertag. It does the same thing as + * {@link #getXuidFor(String)} except that this method will return the xuid in Floodgate uuid + * format instead of just a long * * @param gamertag the gamertag of the player * @return the xuid of the player with the given gamertag, or null when there is no player with @@ -134,6 +151,10 @@ default CompletableFuture getUuidFor(String gamertag) { */ CompletableFuture getGamertagFor(long xuid); + default FloodgateEventBus getEventBus() { + return InstanceHolder.getEventBus(); + } + /** * Returns the instance that manages all the linking. */ diff --git a/api/src/main/java/org/geysermc/floodgate/api/InstanceHolder.java b/api/src/main/java/org/geysermc/floodgate/api/InstanceHolder.java index d0c6af24..e9730b10 100644 --- a/api/src/main/java/org/geysermc/floodgate/api/InstanceHolder.java +++ b/api/src/main/java/org/geysermc/floodgate/api/InstanceHolder.java @@ -27,6 +27,7 @@ import java.util.UUID; import lombok.Getter; +import org.geysermc.floodgate.api.event.FloodgateEventBus; import org.geysermc.floodgate.api.handshake.HandshakeHandlers; import org.geysermc.floodgate.api.inject.PlatformInjector; import org.geysermc.floodgate.api.link.PlayerLink; @@ -35,20 +36,22 @@ public final class InstanceHolder { @Getter private static FloodgateApi api; @Getter private static PlayerLink playerLink; + @Getter private static FloodgateEventBus eventBus; - @Getter private static PlatformInjector injector; - @Getter private static PacketHandlers packetHandlers; - @Getter private static HandshakeHandlers handshakeHandlers; + private static PlatformInjector injector; + private static PacketHandlers packetHandlers; + private static HandshakeHandlers handshakeHandlers; private static UUID storedKey; public static boolean set( FloodgateApi floodgateApi, PlayerLink link, + FloodgateEventBus floodgateEventBus, PlatformInjector platformInjector, PacketHandlers packetHandlers, HandshakeHandlers handshakeHandlers, - UUID key) { - + UUID key + ) { if (storedKey != null) { if (!storedKey.equals(key)) { return false; @@ -59,14 +62,37 @@ public static boolean set( api = floodgateApi; playerLink = link; + eventBus = floodgateEventBus; injector = platformInjector; InstanceHolder.packetHandlers = packetHandlers; InstanceHolder.handshakeHandlers = handshakeHandlers; return true; } - @SuppressWarnings("unchecked") - public static T castApi(Class cast) { - return (T) api; + /** + * @deprecated Injector addons will be removed with the launch of Floodgate 3.0. Please look at + * #536 for additional context. + */ + @Deprecated + public static PlatformInjector getInjector() { + return InstanceHolder.injector; + } + + /** + * @deprecated Packet handlers will be removed with the launch of Floodgate 3.0. Please look at + * #536 for additional context. + */ + @Deprecated + public static PacketHandlers getPacketHandlers() { + return InstanceHolder.packetHandlers; + } + + /** + * @deprecated Handshake handlers will be removed with the launch of Floodgate 3.0. Please look at + * #536 for additional context. + */ + @Deprecated + public static HandshakeHandlers getHandshakeHandlers() { + return InstanceHolder.handshakeHandlers; } } diff --git a/api/src/main/java/org/geysermc/floodgate/api/event/FloodgateEventBus.java b/api/src/main/java/org/geysermc/floodgate/api/event/FloodgateEventBus.java new file mode 100644 index 00000000..935cde21 --- /dev/null +++ b/api/src/main/java/org/geysermc/floodgate/api/event/FloodgateEventBus.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.api.event; + +import org.geysermc.event.bus.EventBus; + +public interface FloodgateEventBus extends EventBus> { +} diff --git a/api/src/main/java/org/geysermc/floodgate/api/event/FloodgateSubscriber.java b/api/src/main/java/org/geysermc/floodgate/api/event/FloodgateSubscriber.java new file mode 100644 index 00000000..0d9ab473 --- /dev/null +++ b/api/src/main/java/org/geysermc/floodgate/api/event/FloodgateSubscriber.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.api.event; + +import org.geysermc.event.subscribe.Subscriber; + +public interface FloodgateSubscriber extends Subscriber { +} diff --git a/api/src/main/java/org/geysermc/floodgate/api/event/skin/SkinApplyEvent.java b/api/src/main/java/org/geysermc/floodgate/api/event/skin/SkinApplyEvent.java new file mode 100644 index 00000000..7a4b16c1 --- /dev/null +++ b/api/src/main/java/org/geysermc/floodgate/api/event/skin/SkinApplyEvent.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.api.event.skin; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.checkerframework.common.returnsreceiver.qual.This; +import org.geysermc.event.Cancellable; +import org.geysermc.floodgate.api.player.FloodgatePlayer; + +/** + * An event that's fired when Floodgate receives a player skin. The event will be cancelled by + * default when hasSkin is true, as Floodgate by default only applies skins when the player has no + * skin applied yet. + */ +public interface SkinApplyEvent extends Cancellable { + /** + * Returns the player that will receive the skin. + */ + @NonNull FloodgatePlayer player(); + + /** + * Returns the skin texture currently applied to the player. + */ + @Nullable SkinData currentSkin(); + + /** + * Returns the skin texture to be applied to the player. + */ + @NonNull SkinData newSkin(); + + /** + * Sets the skin texture to be applied to the player + * + * @param skinData the skin to apply + * @return this + */ + @This SkinApplyEvent newSkin(@NonNull SkinData skinData); + + interface SkinData { + /** + * Returns the value of the skin texture. + */ + @NonNull String value(); + + /** + * Returns the signature of the skin texture. + */ + @NonNull String signature(); + } +} diff --git a/api/src/main/java/org/geysermc/floodgate/api/handshake/HandshakeData.java b/api/src/main/java/org/geysermc/floodgate/api/handshake/HandshakeData.java index 44eb1c3c..9c1569a6 100644 --- a/api/src/main/java/org/geysermc/floodgate/api/handshake/HandshakeData.java +++ b/api/src/main/java/org/geysermc/floodgate/api/handshake/HandshakeData.java @@ -31,12 +31,10 @@ import org.geysermc.floodgate.util.LinkedPlayer; /** - * For advanced users only! You shouldn't play with this unless you know what you're doing.
- *
- * This class allows you change specific things of a Bedrock player before it is applied to the - * server. Note that at the time I'm writing this that the HandshakeData is created after requesting - * the player link. So the link is present here, if applicable. + * @deprecated Handshake handlers will be removed with the launch of Floodgate 3.0. Please look at + * #536 for additional context. */ +@Deprecated public interface HandshakeData { /** * Returns the Channel holding the connection between the client and the server. diff --git a/api/src/main/java/org/geysermc/floodgate/api/handshake/HandshakeHandler.java b/api/src/main/java/org/geysermc/floodgate/api/handshake/HandshakeHandler.java index 2aca5498..cab9ef93 100644 --- a/api/src/main/java/org/geysermc/floodgate/api/handshake/HandshakeHandler.java +++ b/api/src/main/java/org/geysermc/floodgate/api/handshake/HandshakeHandler.java @@ -26,14 +26,10 @@ package org.geysermc.floodgate.api.handshake; /** - * This class allows you to change and/or get specific data of the Bedrock client before Floodgate - * does something with this data. This means that Floodgate decrypts the data, then calls the - * handshake handlers and then applies the data to the connection.
- *
- * /!\ Note that this class will be called for both Java and Bedrock connections, but {@link - * HandshakeData#isFloodgatePlayer()} will be false and Floodgate related methods will return null - * for Java players + * @deprecated Handshake handlers will be removed with the launch of Floodgate 3.0. Please look at + * #536 for additional context. */ +@Deprecated @FunctionalInterface public interface HandshakeHandler { /** diff --git a/api/src/main/java/org/geysermc/floodgate/api/handshake/HandshakeHandlers.java b/api/src/main/java/org/geysermc/floodgate/api/handshake/HandshakeHandlers.java index ef19b545..2d69a1ea 100644 --- a/api/src/main/java/org/geysermc/floodgate/api/handshake/HandshakeHandlers.java +++ b/api/src/main/java/org/geysermc/floodgate/api/handshake/HandshakeHandlers.java @@ -25,6 +25,11 @@ package org.geysermc.floodgate.api.handshake; +/** + * @deprecated Handshake handlers will be removed with the launch of Floodgate 3.0. Please look at + * #536 for additional context. + */ +@Deprecated public interface HandshakeHandlers { /** * Register a custom handshake handler. This can be used to check and edit the player during the diff --git a/api/src/main/java/org/geysermc/floodgate/api/inject/InjectorAddon.java b/api/src/main/java/org/geysermc/floodgate/api/inject/InjectorAddon.java index d550cbcb..01fbc3cd 100644 --- a/api/src/main/java/org/geysermc/floodgate/api/inject/InjectorAddon.java +++ b/api/src/main/java/org/geysermc/floodgate/api/inject/InjectorAddon.java @@ -27,6 +27,11 @@ import io.netty.channel.Channel; +/** + * @deprecated Injector addons will be removed with the launch of Floodgate 3.0. Please look at + * #536 for additional context. + */ +@Deprecated public interface InjectorAddon { /** * Called when injecting a specific channel (every client that is connected to the server has @@ -34,7 +39,7 @@ public interface InjectorAddon { * used for third party things. * * @param channel the channel that the injector is injecting - * @param toServer if the the connection is between a proxy and a server + * @param toServer if the connection is between a proxy and a server */ void onInject(Channel channel, boolean toServer); @@ -43,7 +48,7 @@ public interface InjectorAddon { * closed connection (if it is injected), so it'll also run this method for closed connections * between a server and the proxy (when Floodgate is running on a proxy). * - * @param channel the channel that the injecor injected + * @param channel the channel that the injector injected */ default void onChannelClosed(Channel channel) { } diff --git a/api/src/main/java/org/geysermc/floodgate/api/inject/PlatformInjector.java b/api/src/main/java/org/geysermc/floodgate/api/inject/PlatformInjector.java index 5fe765e5..026fb08a 100644 --- a/api/src/main/java/org/geysermc/floodgate/api/inject/PlatformInjector.java +++ b/api/src/main/java/org/geysermc/floodgate/api/inject/PlatformInjector.java @@ -26,21 +26,18 @@ package org.geysermc.floodgate.api.inject; /** - * The global interface of all the Platform Injectors. The injector can be used for various things. - * It is used internally for getting Floodgate data out of the handshake packet and for debug mode, - * but there is also an option to add your own addons. Note that every Floodgate platform that - * supports netty should implement this, but the platform implementation isn't required to implement - * this. + * @deprecated Injector addons will be removed with the launch of Floodgate 3.0. Please look at + * #536 for additional context. */ +@Deprecated public interface PlatformInjector { /** * Injects the server connection. This will allow various addons (like getting the Floodgate * data and debug mode) to work. * - * @return true if the connection has successfully been injected - * @throws Exception if something went wrong while injecting the server connection + * @throws Exception if the platform couldn't be injected */ - boolean inject() throws Exception; + void inject() throws Exception; /** * Some platforms may not be able to remove their injection process. If so, this method will @@ -56,10 +53,9 @@ default boolean canRemoveInjection() { * Removes the injection from the server. Please note that this function should only be used * internally (on plugin shutdown). This method will also remove every added addon. * - * @return true if the injection has successfully been removed - * @throws Exception if something went wrong while removing the injection + * @throws Exception if the platform injection could not be removed */ - boolean removeInjection() throws Exception; + void removeInjection() throws Exception; /** * If the server connection is currently injected. diff --git a/api/src/main/java/org/geysermc/floodgate/api/link/LinkRequestResult.java b/api/src/main/java/org/geysermc/floodgate/api/link/LinkRequestResult.java index ea7c795c..3d8fad03 100644 --- a/api/src/main/java/org/geysermc/floodgate/api/link/LinkRequestResult.java +++ b/api/src/main/java/org/geysermc/floodgate/api/link/LinkRequestResult.java @@ -34,10 +34,6 @@ public enum LinkRequestResult { * An unknown error encountered while creating / verifying the link request. */ UNKNOWN_ERROR, - /** - * @deprecated this result isn't used. Instead the link code is returned - */ - REQUEST_CREATED, /** * The specified bedrock username is already linked to a Java account. */ diff --git a/api/src/main/java/org/geysermc/floodgate/api/logger/FloodgateLogger.java b/api/src/main/java/org/geysermc/floodgate/api/logger/FloodgateLogger.java index d1d533a8..e3afee90 100644 --- a/api/src/main/java/org/geysermc/floodgate/api/logger/FloodgateLogger.java +++ b/api/src/main/java/org/geysermc/floodgate/api/logger/FloodgateLogger.java @@ -80,18 +80,7 @@ public interface FloodgateLogger { void trace(String message, Object... args); /** - * Enables debug mode for the Floodgate logger. - */ - void enableDebug(); - - /** - * Disables debug mode for the Floodgate logger. Debug messages can still be sent after running - * this method, but they will be hidden from the console. - */ - void disableDebug(); - - /** - * Returns if debugging is enabled + * Returns true if debugging is enabled */ boolean isDebug(); } diff --git a/api/src/main/java/org/geysermc/floodgate/api/packet/PacketHandler.java b/api/src/main/java/org/geysermc/floodgate/api/packet/PacketHandler.java index 3170175c..1ddb9cf3 100644 --- a/api/src/main/java/org/geysermc/floodgate/api/packet/PacketHandler.java +++ b/api/src/main/java/org/geysermc/floodgate/api/packet/PacketHandler.java @@ -28,8 +28,10 @@ import io.netty.channel.ChannelHandlerContext; /** - * For advanced users only! You shouldn't play with this unless you know what you're doing. + * @deprecated Packet handlers will be removed with the launch of Floodgate 3.0. Please look at + * #536 for additional context. */ +@Deprecated public interface PacketHandler { /** * Called when a registered packet has been seen. diff --git a/api/src/main/java/org/geysermc/floodgate/api/packet/PacketHandlers.java b/api/src/main/java/org/geysermc/floodgate/api/packet/PacketHandlers.java index e59c66fc..083b16c4 100644 --- a/api/src/main/java/org/geysermc/floodgate/api/packet/PacketHandlers.java +++ b/api/src/main/java/org/geysermc/floodgate/api/packet/PacketHandlers.java @@ -28,6 +28,11 @@ import io.netty.channel.ChannelHandlerContext; import org.geysermc.floodgate.api.util.TriFunction; +/** + * @deprecated Packet handlers will be removed with the launch of Floodgate 3.0. Please look at + * #536 for additional context. + */ +@Deprecated public interface PacketHandlers { /** * Register a specific class for a specific consumer. diff --git a/api/src/main/java/org/geysermc/floodgate/api/player/FloodgatePlayer.java b/api/src/main/java/org/geysermc/floodgate/api/player/FloodgatePlayer.java index 8ade2d12..6205da0d 100644 --- a/api/src/main/java/org/geysermc/floodgate/api/player/FloodgatePlayer.java +++ b/api/src/main/java/org/geysermc/floodgate/api/player/FloodgatePlayer.java @@ -26,8 +26,8 @@ package org.geysermc.floodgate.api.player; import java.util.UUID; -import org.geysermc.cumulus.Form; -import org.geysermc.cumulus.util.FormBuilder; +import org.geysermc.cumulus.form.Form; +import org.geysermc.cumulus.form.util.FormBuilder; import org.geysermc.floodgate.api.FloodgateApi; import org.geysermc.floodgate.util.DeviceOs; import org.geysermc.floodgate.util.InputMode; @@ -49,14 +49,16 @@ public interface FloodgatePlayer { UUID getJavaUniqueId(); /** - * Returns the uuid that the server will use as uuid of that player. Will return {@link - * #getJavaUniqueId()} when not linked or {@link LinkedPlayer#getJavaUniqueId()} when linked. + * Returns the uuid that the server will use as uuid of that player. Will return + * {@link #getJavaUniqueId()} when not linked or {@link LinkedPlayer#getJavaUniqueId()} when + * linked. */ UUID getCorrectUniqueId(); /** - * Returns the username the server will as username for that player. Will return {@link - * #getJavaUsername()} when not linked or {@link LinkedPlayer#getJavaUsername()} when linked. + * Returns the username the server will as username for that player. Will return + * {@link #getJavaUsername()} when not linked or {@link LinkedPlayer#getJavaUsername()} when + * linked. */ String getCorrectUsername(); @@ -115,7 +117,25 @@ default boolean sendForm(Form form) { return FloodgateApi.getInstance().sendForm(getCorrectUniqueId(), form); } - default boolean sendForm(FormBuilder formBuilder) { + default boolean sendForm(FormBuilder formBuilder) { + return sendForm(formBuilder.build()); + } + + /** + * @deprecated since Cumulus 1.1 and will be removed when Cumulus 2.0 releases. Please use the + * new form classes instead. + */ + @Deprecated + default boolean sendForm(org.geysermc.cumulus.Form form) { + return sendForm(form.newForm()); + } + + /** + * @deprecated since Cumulus 1.1 and will be removed when Cumulus 2.0 releases. Please use the + * new form classes instead. + */ + @Deprecated + default boolean sendForm(org.geysermc.cumulus.util.FormBuilder formBuilder) { return sendForm(formBuilder.build()); } @@ -123,20 +143,28 @@ default boolean transfer(String address, int port) { return FloodgateApi.getInstance().transferPlayer(getCorrectUniqueId(), address, port); } + @Deprecated boolean hasProperty(PropertyKey key); + @Deprecated boolean hasProperty(String key); + @Deprecated T getProperty(PropertyKey key); + @Deprecated T getProperty(String key); + @Deprecated T removeProperty(PropertyKey key); + @Deprecated T removeProperty(String key); + @Deprecated T addProperty(PropertyKey key, Object value); + @Deprecated T addProperty(String key, Object value); /** diff --git a/api/src/main/java/org/geysermc/floodgate/api/player/PropertyKey.java b/api/src/main/java/org/geysermc/floodgate/api/player/PropertyKey.java index 04e1d4d0..62bebaf8 100644 --- a/api/src/main/java/org/geysermc/floodgate/api/player/PropertyKey.java +++ b/api/src/main/java/org/geysermc/floodgate/api/player/PropertyKey.java @@ -28,6 +28,7 @@ import lombok.Getter; @Getter +@Deprecated public class PropertyKey { /** * Socket Address returns the InetSocketAddress of the Bedrock player diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 64ac14db..6200d2c9 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -9,9 +9,10 @@ repositories { } dependencies { - implementation("net.kyori", "indra-common", "2.0.6") - implementation("org.jfrog.buildinfo", "build-info-extractor-gradle", "4.26.1") + implementation("net.kyori", "indra-common", "3.0.1") + implementation("net.kyori", "indra-git", "3.0.1") implementation("gradle.plugin.com.github.johnrengelman", "shadow", "7.1.1") + implementation("gradle.plugin.org.jetbrains.gradle.plugin.idea-ext", "gradle-idea-ext", "1.1.7") } tasks.withType { diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts new file mode 100644 index 00000000..30499ad2 --- /dev/null +++ b/build-logic/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "build-logic" \ No newline at end of file diff --git a/build-logic/src/main/kotlin/Versions.kt b/build-logic/src/main/kotlin/Versions.kt index 4251d3e8..5c14da23 100644 --- a/build-logic/src/main/kotlin/Versions.kt +++ b/build-logic/src/main/kotlin/Versions.kt @@ -24,19 +24,20 @@ */ object Versions { - const val geyserVersion = "2.0.0-SNAPSHOT" - const val cumulusVersion = "1.0-SNAPSHOT" + const val geyserVersion = "2.2.1-SNAPSHOT" + const val cumulusVersion = "1.1.2" + const val eventsVersion = "1.1-SNAPSHOT" const val configUtilsVersion = "1.0-SNAPSHOT" - const val spigotVersion = "1.13-R0.1-SNAPSHOT" + const val spigotVersion = "1.19.4-R0.1-SNAPSHOT" const val fastutilVersion = "8.5.3" - const val lombokVersion = "1.18.20" - const val guiceVersion = "5.0.1" + const val guiceVersion = "6.0.0" const val nettyVersion = "4.1.49.Final" const val snakeyamlVersion = "1.28" - const val cloudVersion = "1.5.0" - const val bstatsVersion = "3.0.0" + const val cloudVersion = "2.0.0-beta.11" // for cloud-minecraft + const val cloudCore = "2.0.0-rc.2" + const val bstatsVersion = "3.0.2" const val javaWebsocketVersion = "1.5.2" const val checkerQual = "3.19.0" -} \ No newline at end of file +} diff --git a/build-logic/src/main/kotlin/extensions.kt b/build-logic/src/main/kotlin/extensions.kt index 1b781cb2..11406872 100644 --- a/build-logic/src/main/kotlin/extensions.kt +++ b/build-logic/src/main/kotlin/extensions.kt @@ -23,16 +23,11 @@ * @link https://github.com/GeyserMC/Floodgate */ -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import net.kyori.indra.git.IndraGitExtension import org.gradle.api.Project import org.gradle.api.artifacts.ProjectDependency -import org.gradle.kotlin.dsl.named import org.gradle.kotlin.dsl.the -fun Project.isSnapshot(): Boolean = - version.toString().endsWith("-SNAPSHOT") - fun Project.fullVersion(): String { var version = version.toString() if (version.endsWith("-SNAPSHOT")) { @@ -44,34 +39,46 @@ fun Project.fullVersion(): String { fun Project.lastCommitHash(): String? = the().commit()?.name?.substring(0, 7) -// retrieved from https://wiki.jenkins-ci.org/display/JENKINS/Building+a+software+project -// some properties might be specific to Jenkins fun Project.branchName(): String = - System.getProperty("GIT_BRANCH", "local/dev") -fun Project.buildNumber(): Int = - Integer.parseInt(System.getProperty("BUILD_NUMBER", "-1")) + the().branchName() ?: jenkinsBranchName() ?: "local/dev" -fun Project.buildNumberAsString(): String = - buildNumber().takeIf { it != -1 }?.toString() ?: "??" +fun Project.shouldAddBranchName(): Boolean = + System.getenv("IGNORE_BRANCH")?.toBoolean() ?: (branchName() !in arrayOf("master", "local/dev")) -fun Project.relocate(pattern: String) { - tasks.named("shadowJar") { - relocate(pattern, "org.geysermc.floodgate.shaded.$pattern") - } -} +fun Project.versionWithBranchName(): String = + branchName().replace(Regex("[^0-9A-Za-z-_]"), "-") + '-' + version + +fun buildNumber(): Int = + (System.getenv("BUILD_NUMBER"))?.let { Integer.parseInt(it) } ?: -1 + +fun buildNumberAsString(): String = + buildNumber().takeIf { it != -1 }?.toString() ?: "??" -val providedDependencies = mutableMapOf>() +val providedDependencies = mutableMapOf>>() +val relocatedPackages = mutableMapOf>() fun Project.provided(pattern: String, name: String, version: String, excludedOn: Int = 0b110) { - providedDependencies.getOrPut(project.name) { mutableSetOf() } - .add("${calcExclusion(pattern, 0b100, excludedOn)}:" + - "${calcExclusion(name, 0b10, excludedOn)}:" + - calcExclusion(version, 0b1, excludedOn)) + val format = "${calcExclusion(pattern, 0b100, excludedOn)}:" + + "${calcExclusion(name, 0b10, excludedOn)}:" + + calcExclusion(version, 0b1, excludedOn) + + providedDependencies.getOrPut(project.name) { mutableSetOf() }.add(Pair(format, format)) dependencies.add("compileOnlyApi", "$pattern:$name:$version") } -fun Project.provided(dependency: ProjectDependency) = - provided(dependency.group!!, dependency.name, dependency.version!!) +fun Project.provided(dependency: ProjectDependency) { + providedDependencies.getOrPut(project.name) { mutableSetOf() } + .add(Pair(dependency.group + ":" + dependency.name, dependency)) + dependencies.add("compileOnlyApi", dependency) +} + + +fun Project.relocate(pattern: String) = + relocatedPackages.getOrPut(project.name) { mutableSetOf() } + .add(pattern) private fun calcExclusion(section: String, bit: Int, excludedOn: Int): String = - if (excludedOn and bit > 0) section else "" \ No newline at end of file + if (excludedOn and bit > 0) section else "" + +// todo remove these when we're not using Jenkins anymore +private fun jenkinsBranchName(): String? = System.getenv("BRANCH_NAME") \ No newline at end of file diff --git a/build-logic/src/main/kotlin/floodgate.base-conventions.gradle.kts b/build-logic/src/main/kotlin/floodgate.base-conventions.gradle.kts index 0dba1832..f1d9ab8f 100644 --- a/build-logic/src/main/kotlin/floodgate.base-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/floodgate.base-conventions.gradle.kts @@ -1,7 +1,7 @@ plugins { `java-library` - `maven-publish` // id("net.ltgt.errorprone") + id("net.kyori.indra") id("net.kyori.indra.git") } @@ -9,6 +9,21 @@ dependencies { compileOnly("org.checkerframework", "checker-qual", Versions.checkerQual) } +indra { + github("GeyserMC", "Floodgate") { + ci(true) + issues(true) + scm(true) + } + mitLicense() + + javaVersions { + // without toolchain & strictVersion sun.misc.Unsafe won't be found + minimumToolchain(8) + strictVersions(true) + } +} + tasks { processResources { filesMatching(listOf("plugin.yml", "bungee.yml", "velocity-plugin.json")) { @@ -22,14 +37,4 @@ tasks { ) } } - compileJava { - options.encoding = Charsets.UTF_8.name() - } -} - -java { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - - withSourcesJar() } \ No newline at end of file diff --git a/build-logic/src/main/kotlin/floodgate.database-conventions.gradle.kts b/build-logic/src/main/kotlin/floodgate.database-conventions.gradle.kts index b41417e2..2012ba65 100644 --- a/build-logic/src/main/kotlin/floodgate.database-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/floodgate.database-conventions.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("floodgate.publish-conventions") + id("floodgate.shadow-conventions") } tasks { diff --git a/build-logic/src/main/kotlin/floodgate.generate-templates.gradle.kts b/build-logic/src/main/kotlin/floodgate.generate-templates.gradle.kts new file mode 100644 index 00000000..5f89b302 --- /dev/null +++ b/build-logic/src/main/kotlin/floodgate.generate-templates.gradle.kts @@ -0,0 +1,93 @@ +import org.apache.tools.ant.filters.ReplaceTokens +import org.gradle.plugins.ide.eclipse.model.EclipseModel +import org.gradle.plugins.ide.idea.model.IdeaModel +import org.jetbrains.gradle.ext.ProjectSettings +import org.jetbrains.gradle.ext.TaskTriggersConfig + +plugins { + id("org.jetbrains.gradle.plugin.idea-ext") +} + +registerGenerateTemplateTasks() + +fun Project.registerGenerateTemplateTasks() { + // main and test + extensions.getByType().all { + val javaDestination = layout.buildDirectory.dir("generated/sources/templates/$name") + val javaSrcDir = layout.projectDirectory.dir("src/$name/templates") + val javaGenerateTask = tasks.register( + getTaskName("template", "sources") + ) { + filteringCharset = Charsets.UTF_8.name() + from(javaSrcDir) + into(javaDestination) + filter("tokens" to replacements()) + } + java.srcDir(javaGenerateTask.map { it.outputs }) + + val resourcesDestination = layout.buildDirectory.dir("generated/resources/templates/$name") + val resourcesSrcDir = layout.projectDirectory.dir("src/$name/resourceTemplates") + val resourcesGenerateTask = tasks.register( + getTaskName("template", "resources") + ) { + filteringCharset = Charsets.UTF_8.name() + from(resourcesSrcDir) + into(resourcesDestination) + filter("tokens" to replacements()) + } + resources.srcDir(resourcesGenerateTask.map { it.outputs }) + } + + return configureIdeSync( + tasks.register("allTemplateSources") { + dependsOn(tasks.withType()) + }, + tasks.register("allTemplateResources") { + dependsOn(tasks.withType()) + } + ) +} + +fun Project.configureIdeSync(vararg generateAllTasks: TaskProvider) { + extensions.findByType { + synchronizationTasks(generateAllTasks) + } + + extensions.findByType { + if (project != null) { + (project as ExtensionAware).extensions.configure { + (this as ExtensionAware).extensions.configure { + afterSync(generateAllTasks) + } + } + } + } + + //todo wasn't able to find something for VS(Code) +} + +inline fun ExtensionContainer.findByType(noinline action: T.() -> Unit) { + val extension = findByType(T::class) + if (extension != null) { + action.invoke(extension) + } +} + +abstract class GenerateAnyTemplates : Copy() { + private val replacements = mutableMapOf() + + fun replaceToken(key: String, value: () -> Any) { + replaceToken(key, value.invoke()) + } + + fun replaceToken(key: String, value: Any) { + replacements[key] = value.toString() + } + + fun replacements(): Map { + return replacements + } +} + +open class GenerateResourceTemplates : GenerateAnyTemplates() +open class GenerateSourceTemplates : GenerateAnyTemplates() diff --git a/build-logic/src/main/kotlin/floodgate.publish-conventions.gradle.kts b/build-logic/src/main/kotlin/floodgate.publish-conventions.gradle.kts index 37c815c2..75c054f8 100644 --- a/build-logic/src/main/kotlin/floodgate.publish-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/floodgate.publish-conventions.gradle.kts @@ -1,31 +1,21 @@ plugins { id("floodgate.shadow-conventions") - id("com.jfrog.artifactory") - id("maven-publish") + id("net.kyori.indra.publishing") } -publishing { - publications.create("mavenJava") { - groupId = project.group as String - artifactId = "floodgate-" + project.name - version = project.version as String - - artifact(tasks["shadowJar"]) - artifact(tasks["sourcesJar"]) +indra { + configurePublications { + if (shouldAddBranchName()) { + version = versionWithBranchName() + } } + + publishSnapshotsTo("geysermc", "https://repo.opencollab.dev/maven-snapshots") + publishReleasesTo("geysermc", "https://repo.opencollab.dev/maven-releases") } -artifactory { - publish { - repository { - setRepoKey(if (isSnapshot()) "maven-snapshots" else "maven-releases") - setMavenCompatible(true) - } - defaults { - publishConfigs("archives") - setPublishArtifacts(true) - setPublishPom(true) - setPublishIvy(false) - } - } +publishing { + // skip shadow jar from publishing. Workaround for https://github.com/johnrengelman/shadow/issues/651 + val javaComponent = project.components["java"] as AdhocComponentWithVariants + javaComponent.withVariantsFromConfiguration(configurations["shadowRuntimeElements"]) { skip() } } \ No newline at end of file diff --git a/build-logic/src/main/kotlin/floodgate.shadow-conventions.gradle.kts b/build-logic/src/main/kotlin/floodgate.shadow-conventions.gradle.kts index 5405f480..7f342d8d 100644 --- a/build-logic/src/main/kotlin/floodgate.shadow-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/floodgate.shadow-conventions.gradle.kts @@ -18,15 +18,42 @@ tasks { val sJar: ShadowJar = this doFirst { - providedDependencies[project.name]?.forEach { string -> + providedDependencies[project.name]?.forEach { (name, notation) -> sJar.dependencies { - println("Excluding $string from ${project.name}") - exclude(dependency(string)) + println("Excluding $name from ${project.name}") + exclude(dependency(notation)) } } + + // relocations made in included project dependencies are for whatever reason not + // forwarded to the project implementing the dependency. + // (e.g. a relocation in `core` will relocate for core. But when you include `core` in + // for example Velocity, the relocation will be gone for Velocity) + addRelocations(project, sJar) + } + + val destinationDir = System.getenv("DESTINATION_DIRECTORY"); + if (destinationDir != null) { + destinationDirectory.set(file(destinationDir)) } } named("build") { dependsOn(shadowJar) } -} \ No newline at end of file +} + +fun addRelocations(project: Project, shadowJar: ShadowJar) { + callAddRelocations(project.configurations.api.get(), shadowJar) + callAddRelocations(project.configurations.implementation.get(), shadowJar) + + relocatedPackages[project.name]?.forEach { pattern -> + println("Relocating $pattern for ${shadowJar.project.name}") + shadowJar.relocate(pattern, "org.geysermc.floodgate.shadow.$pattern") + } +} + +fun callAddRelocations(configuration: Configuration, shadowJar: ShadowJar) = + configuration.dependencies.forEach { + if (it is ProjectDependency) + addRelocations(it.dependencyProject, shadowJar) + } \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index b24fcbfa..da89fe19 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,51 +1,43 @@ plugins { `java-library` id("floodgate.build-logic") -// id("com.github.spotbugs") version "4.8.0" apply false id("io.freefair.lombok") version "6.3.0" apply false -// checkstyle } allprojects { group = "org.geysermc.floodgate" - version = "2.1.1-SNAPSHOT" + version = property("version")!! description = "Allows Bedrock players to join Java edition servers while keeping the server in online mode" } -val platforms = setOf( +val deployProjects = setOf( + projects.api, + // for future Floodgate integration + Fabric + projects.core, projects.bungee, projects.spigot, projects.velocity ).map { it.dependencyProject } -//todo re-add pmd and organisation/license/sdcm/issuemanagement stuff +//todo re-add checkstyle when we switch back to 2 space indention +// and take a look again at spotbugs someday subprojects { -// apply(plugin = "com.github.spotbugs") - apply { plugin("java-library") -// plugin("checkstyle") plugin("io.freefair.lombok") plugin("floodgate.build-logic") } -// checkstyle { -// toolVersion = "9.3" -// configFile = rootProject.projectDir.resolve("checkstyle.xml") -// maxErrors = 0 -// maxWarnings = 0 -// } - val relativePath = projectDir.relativeTo(rootProject.projectDir).path if (relativePath.startsWith("database" + File.separator)) { group = rootProject.group as String + ".database" plugins.apply("floodgate.database-conventions") - } else { - when (this) { - in platforms -> plugins.apply("floodgate.publish-conventions") - else -> plugins.apply("floodgate.base-conventions") - } + } + + when (this) { + in deployProjects -> plugins.apply("floodgate.publish-conventions") + else -> plugins.apply("floodgate.base-conventions") } } \ No newline at end of file diff --git a/bungee/build.gradle.kts b/bungee/build.gradle.kts index b362a0ac..711d7a71 100644 --- a/bungee/build.gradle.kts +++ b/bungee/build.gradle.kts @@ -1,20 +1,21 @@ -var bungeeCommit = "bda1605" +var bungeeVersion = "1.21-R0.1-SNAPSHOT" var gsonVersion = "2.8.0" var guavaVersion = "21.0" dependencies { api(projects.core) - implementation("cloud.commandframework", "cloud-bungee", Versions.cloudVersion) + implementation("org.incendo", "cloud-bungee", Versions.cloudVersion) } relocate("com.google.inject") relocate("net.kyori") -relocate("cloud.commandframework") +relocate("org.incendo.cloud") // used in cloud relocate("io.leangen.geantyref") +// since 1.20 +relocate("org.yaml") // these dependencies are already present on the platform -provided("com.github.SpigotMC.BungeeCord", "bungeecord-proxy", bungeeCommit) +provided("net.md-5", "bungeecord-proxy", bungeeVersion) provided("com.google.code.gson", "gson", gsonVersion) provided("com.google.guava", "guava", guavaVersion) -provided("org.yaml", "snakeyaml", Versions.snakeyamlVersion) diff --git a/bungee/src/main/java/org/geysermc/floodgate/inject/bungee/BungeeInjector.java b/bungee/src/main/java/org/geysermc/floodgate/inject/bungee/BungeeInjector.java index 4178490b..ed672f85 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/inject/bungee/BungeeInjector.java +++ b/bungee/src/main/java/org/geysermc/floodgate/inject/bungee/BungeeInjector.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * Copyright (c) 2019-2025 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -26,50 +26,47 @@ package org.geysermc.floodgate.inject.bungee; import io.netty.channel.Channel; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.channel.ChannelOutboundHandlerAdapter; -import io.netty.channel.ChannelPromise; -import java.lang.reflect.Field; import lombok.Getter; import lombok.RequiredArgsConstructor; -import net.md_5.bungee.netty.PipelineUtils; -import net.md_5.bungee.protocol.MinecraftEncoder; -import net.md_5.bungee.protocol.Varint21LengthFieldPrepender; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.api.ProxyServer.Unsafe; +import net.md_5.bungee.protocol.channel.BungeeChannelInitializer; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.inject.CommonPlatformInjector; -import org.geysermc.floodgate.util.BungeeReflectionUtils; -import org.geysermc.floodgate.util.ReflectionUtils; @RequiredArgsConstructor public final class BungeeInjector extends CommonPlatformInjector { - private static final String BUNGEE_INIT = "floodgate-bungee-init"; - private final FloodgateLogger logger; @Getter private boolean injected; @Override - public boolean inject() { + public void inject() { try { - // Can everyone just switch to Velocity please :) - - Field framePrepender = ReflectionUtils.getField(PipelineUtils.class, "framePrepender"); - - // Required in order to inject into both Geyser <-> proxy AND proxy <-> server - // (Instead of just replacing the ChannelInitializer which is only called for - // player <-> proxy) - BungeeCustomPrepender customPrepender = new BungeeCustomPrepender( - this, ReflectionUtils.getCastedValue(null, framePrepender) - ); + ProxyServer.getInstance().unsafe(); + } catch (NoSuchMethodError ignored) { + logger.error("You're running an outdated version of BungeeCord. Please update BungeeCord to the latest version!"); + throw new Error("You're running an outdated version of BungeeCord. Please update BungeeCord to the latest version!"); + } - BungeeReflectionUtils.setFieldValue(null, framePrepender, customPrepender); + Unsafe unsafe = ProxyServer.getInstance().unsafe(); + BungeeChannelInitializer frontend = unsafe.getFrontendChannelInitializer(); + unsafe.setFrontendChannelInitializer(BungeeChannelInitializer.create(channel -> { + if (!frontend.getChannelAcceptor().accept(channel)) { + return false; + } + injectClient(channel, true); + return true; + })); - injected = true; + BungeeChannelInitializer backend = unsafe.getBackendChannelInitializer(); + unsafe.setBackendChannelInitializer(BungeeChannelInitializer.create(channel -> { + if (!backend.getChannelAcceptor().accept(channel)) { + return false; + } + injectClient(channel, false); return true; - } catch (Exception e) { - e.printStackTrace(); - return false; - } + })); + injected = true; } @Override @@ -78,81 +75,13 @@ public boolean canRemoveInjection() { } @Override - public boolean removeInjection() { - logger.error("Floodgate cannot remove itself from Bungee without a reboot"); - return false; + public void removeInjection() { + throw new IllegalStateException( + "Floodgate cannot remove itself from Bungee without a reboot"); } void injectClient(Channel channel, boolean clientToProxy) { - if (!channel.isOpen()) { - return; - } - - if (channel.pipeline().get(MinecraftEncoder.class) == null) { - logger.debug( - "Minecraft encoder not found while injecting! {}", - String.join(", ", channel.pipeline().names()) - ); - return; - } - injectAddonsCall(channel, !clientToProxy); addInjectedClient(channel); } - - @RequiredArgsConstructor - private static final class BungeeCustomPrepender extends Varint21LengthFieldPrepender { - private final BungeeInjector injector; - private final Varint21LengthFieldPrepender original; - - @Override - public void handlerAdded(ChannelHandlerContext ctx) throws Exception { - original.handlerAdded(ctx); - // The Minecraft encoder being in the pipeline isn't present until later - - if (ctx.channel().parent() != null) { - // Client <-> Proxy - ctx.pipeline().addBefore( - PipelineUtils.FRAME_DECODER, BUNGEE_INIT, - new BungeeClientToProxyInjectInitializer(injector) - ); - } else { - // Proxy <-> Server - ctx.pipeline().addLast( - BUNGEE_INIT, new BungeeProxyToServerInjectInitializer(injector) - ); - } - } - } - - @RequiredArgsConstructor - private static final class BungeeClientToProxyInjectInitializer - extends ChannelInboundHandlerAdapter { - - private final BungeeInjector injector; - - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - injector.injectClient(ctx.channel(), true); - - ctx.pipeline().remove(this); - super.channelRead(ctx, msg); - } - } - - @RequiredArgsConstructor - private static final class BungeeProxyToServerInjectInitializer - extends ChannelOutboundHandlerAdapter { - - private final BungeeInjector injector; - - @Override - public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) - throws Exception { - injector.injectClient(ctx.channel(), false); - - ctx.pipeline().remove(this); - super.write(ctx, msg, promise); - } - } } diff --git a/bungee/src/main/java/org/geysermc/floodgate/listener/BungeeListener.java b/bungee/src/main/java/org/geysermc/floodgate/listener/BungeeListener.java index e62a1776..fd1befb6 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/listener/BungeeListener.java +++ b/bungee/src/main/java/org/geysermc/floodgate/listener/BungeeListener.java @@ -39,6 +39,7 @@ import net.md_5.bungee.api.event.PostLoginEvent; import net.md_5.bungee.api.event.PreLoginEvent; import net.md_5.bungee.api.plugin.Listener; +import net.md_5.bungee.api.plugin.Plugin; import net.md_5.bungee.connection.InitialHandler; import net.md_5.bungee.event.EventHandler; import net.md_5.bungee.event.EventPriority; @@ -46,10 +47,10 @@ import org.geysermc.floodgate.api.ProxyFloodgateApi; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; -import org.geysermc.floodgate.config.ProxyFloodgateConfig; import org.geysermc.floodgate.skin.SkinApplier; -import org.geysermc.floodgate.skin.SkinData; +import org.geysermc.floodgate.skin.SkinDataImpl; import org.geysermc.floodgate.util.LanguageManager; +import org.geysermc.floodgate.util.MojangUtils; import org.geysermc.floodgate.util.ReflectionUtils; @SuppressWarnings("ConstantConditions") @@ -66,7 +67,7 @@ public final class BungeeListener implements Listener { checkNotNull(PLAYER_NAME, "Initial name field cannot be null"); } - @Inject private ProxyFloodgateConfig config; + @Inject private Plugin plugin; @Inject private ProxyFloodgateApi api; @Inject private LanguageManager languageManager; @Inject private FloodgateLogger logger; @@ -80,6 +81,8 @@ public final class BungeeListener implements Listener { @Named("kickMessageAttribute") private AttributeKey kickMessageAttribute; + @Inject private MojangUtils mojangUtils; + @EventHandler(priority = EventPriority.LOWEST) public void onPreLogin(PreLoginEvent event) { // well, no reason to check if the player will be kicked anyway @@ -127,13 +130,31 @@ public void onLogin(LoginEvent event) { @EventHandler(priority = EventPriority.LOWEST) public void onPostLogin(PostLoginEvent event) { - // To fix the February 2 2022 Mojang authentication changes - if (!config.isSendFloodgateData()) { - FloodgatePlayer player = api.getPlayer(event.getPlayer().getUniqueId()); - if (player != null && !player.isLinked()) { - skinApplier.applySkin(player, new SkinData("", "")); - } + FloodgatePlayer player = api.getPlayer(event.getPlayer().getUniqueId()); + if (player == null) { + return; + } + + // Skin look up (on Spigot and friends) would result in it failing, so apply a default skin + if (!player.isLinked()) { + skinApplier.applySkin(player, SkinDataImpl.DEFAULT_SKIN); + return; } + + // Floodgate players are seen as offline mode players, meaning we have to look up + // the linked player's textures ourselves + + event.registerIntent(plugin); + + mojangUtils.skinFor(player.getJavaUniqueId()) + .exceptionally(exception -> { + logger.debug("Unexpected skin fetch error for " + player.getJavaUniqueId(), exception); + return SkinDataImpl.DEFAULT_SKIN; + }) + .thenAccept(skin -> { + skinApplier.applySkin(player, skin); + event.completeIntent(plugin); + }); } @EventHandler(priority = EventPriority.HIGHEST) diff --git a/bungee/src/main/java/org/geysermc/floodgate/module/BungeePlatformModule.java b/bungee/src/main/java/org/geysermc/floodgate/module/BungeePlatformModule.java index 7bd1661a..ec256d69 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/module/BungeePlatformModule.java +++ b/bungee/src/main/java/org/geysermc/floodgate/module/BungeePlatformModule.java @@ -25,15 +25,13 @@ package org.geysermc.floodgate.module; -import cloud.commandframework.CommandManager; -import cloud.commandframework.bungee.BungeeCommandManager; -import cloud.commandframework.execution.CommandExecutionCoordinator; import com.google.inject.AbstractModule; import com.google.inject.Provides; import com.google.inject.Singleton; import com.google.inject.name.Named; +import com.google.inject.name.Names; +import java.util.logging.Logger; import lombok.RequiredArgsConstructor; -import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.plugin.Listener; import net.md_5.bungee.api.plugin.Plugin; import org.geysermc.floodgate.BungeePlugin; @@ -46,8 +44,10 @@ import org.geysermc.floodgate.platform.command.CommandUtil; import org.geysermc.floodgate.platform.listener.ListenerRegistration; import org.geysermc.floodgate.platform.pluginmessage.PluginMessageUtils; +import org.geysermc.floodgate.platform.util.PlatformUtils; import org.geysermc.floodgate.player.FloodgateCommandPreprocessor; import org.geysermc.floodgate.player.UserAudience; +import org.geysermc.floodgate.player.audience.FloodgateSenderMapper; import org.geysermc.floodgate.pluginmessage.BungeePluginMessageRegistration; import org.geysermc.floodgate.pluginmessage.BungeePluginMessageUtils; import org.geysermc.floodgate.pluginmessage.BungeeSkinApplier; @@ -55,22 +55,28 @@ import org.geysermc.floodgate.pluginmessage.PluginMessageRegistration; import org.geysermc.floodgate.skin.SkinApplier; import org.geysermc.floodgate.util.BungeeCommandUtil; +import org.geysermc.floodgate.util.BungeePlatformUtils; import org.geysermc.floodgate.util.LanguageManager; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.bungee.BungeeCommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; @RequiredArgsConstructor public final class BungeePlatformModule extends AbstractModule { private final BungeePlugin plugin; - @Provides - @Singleton - public Plugin bungeePlugin() { - return plugin; + @Override + protected void configure() { + bind(PlatformUtils.class).to(BungeePlatformUtils.class); + bind(Logger.class).annotatedWith(Names.named("logger")).toInstance(plugin.getLogger()); + bind(FloodgateLogger.class).to(JavaUtilFloodgateLogger.class); + bind(SkinApplier.class).to(BungeeSkinApplier.class); } @Provides @Singleton - public FloodgateLogger floodgateLogger(LanguageManager languageManager) { - return new JavaUtilFloodgateLogger(plugin.getLogger(), languageManager); + public Plugin bungeePlugin() { + return plugin; } /* @@ -82,9 +88,8 @@ public FloodgateLogger floodgateLogger(LanguageManager languageManager) { public CommandManager commandManager(CommandUtil commandUtil) { CommandManager commandManager = new BungeeCommandManager<>( plugin, - CommandExecutionCoordinator.simpleCoordinator(), - commandUtil::getUserAudience, - audience -> (CommandSender) audience.source() + ExecutionCoordinator.simpleCoordinator(), + new FloodgateSenderMapper<>(commandUtil) ); commandManager.registerCommandPreProcessor(new FloodgateCommandPreprocessor<>(commandUtil)); return commandManager; @@ -116,12 +121,6 @@ public PluginMessageRegistration pluginMessageRegistration() { return new BungeePluginMessageRegistration(); } - @Provides - @Singleton - public SkinApplier skinApplier(FloodgateLogger logger) { - return new BungeeSkinApplier(logger); - } - /* DebugAddon / PlatformInjector */ diff --git a/bungee/src/main/java/org/geysermc/floodgate/pluginmessage/BungeePluginMessageUtils.java b/bungee/src/main/java/org/geysermc/floodgate/pluginmessage/BungeePluginMessageUtils.java index 921ddf23..178657b0 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/pluginmessage/BungeePluginMessageUtils.java +++ b/bungee/src/main/java/org/geysermc/floodgate/pluginmessage/BungeePluginMessageUtils.java @@ -53,21 +53,6 @@ public void onPluginMessage(PluginMessageEvent event) { return; } - UUID targetUuid = null; - String targetUsername = null; - Identity targetIdentity = Identity.UNKNOWN; - - Connection target = event.getReceiver(); - if (target instanceof ProxiedPlayer) { - ProxiedPlayer player = (ProxiedPlayer) target; - targetUuid = player.getUniqueId(); - targetUsername = player.getName(); - targetIdentity = Identity.PLAYER; - - } else if (target instanceof ServerConnection) { - targetIdentity = Identity.SERVER; - } - UUID sourceUuid = null; String sourceUsername = null; Identity sourceIdentity = Identity.UNKNOWN; @@ -83,8 +68,9 @@ public void onPluginMessage(PluginMessageEvent event) { sourceIdentity = Identity.SERVER; } - Result result = channel.handleProxyCall(event.getData(), targetUuid, targetUsername, - targetIdentity, sourceUuid, sourceUsername, sourceIdentity); + Result result = channel.handleProxyCall( + event.getData(), sourceUuid, sourceUsername, sourceIdentity + ); event.setCancelled(!result.isAllowed()); diff --git a/bungee/src/main/java/org/geysermc/floodgate/pluginmessage/BungeeSkinApplier.java b/bungee/src/main/java/org/geysermc/floodgate/pluginmessage/BungeeSkinApplier.java index 3a10b764..fa2aaad2 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/pluginmessage/BungeeSkinApplier.java +++ b/bungee/src/main/java/org/geysermc/floodgate/pluginmessage/BungeeSkinApplier.java @@ -25,34 +25,47 @@ package org.geysermc.floodgate.pluginmessage; +import static com.google.common.base.Preconditions.checkNotNull; import static org.geysermc.floodgate.util.ReflectionUtils.getFieldOfType; -import static org.geysermc.floodgate.util.ReflectionUtils.setValue; +import com.google.inject.Inject; +import com.google.inject.Singleton; import java.lang.reflect.Field; -import lombok.RequiredArgsConstructor; +import java.util.ArrayList; +import java.util.List; import net.md_5.bungee.api.ProxyServer; import net.md_5.bungee.api.connection.ProxiedPlayer; import net.md_5.bungee.connection.InitialHandler; import net.md_5.bungee.connection.LoginResult; -import net.md_5.bungee.connection.LoginResult.Property; +import net.md_5.bungee.protocol.Property; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.floodgate.api.event.skin.SkinApplyEvent; +import org.geysermc.floodgate.api.event.skin.SkinApplyEvent.SkinData; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; +import org.geysermc.floodgate.event.EventBus; +import org.geysermc.floodgate.event.skin.SkinApplyEventImpl; import org.geysermc.floodgate.skin.SkinApplier; -import org.geysermc.floodgate.skin.SkinData; +import org.geysermc.floodgate.skin.SkinDataImpl; +import org.geysermc.floodgate.util.ReflectionUtils; -@RequiredArgsConstructor +@Singleton public final class BungeeSkinApplier implements SkinApplier { - private static final Field LOGIN_RESULT; + private static final Field LOGIN_RESULT_FIELD; static { - LOGIN_RESULT = getFieldOfType(InitialHandler.class, LoginResult.class); + LOGIN_RESULT_FIELD = getFieldOfType(InitialHandler.class, LoginResult.class); + checkNotNull(LOGIN_RESULT_FIELD, "LoginResult field cannot be null"); } - - private final FloodgateLogger logger; + + private final ProxyServer server = ProxyServer.getInstance(); + + @Inject private EventBus eventBus; + @Inject private FloodgateLogger logger; @Override - public void applySkin(FloodgatePlayer uuid, SkinData skinData) { - ProxiedPlayer player = ProxyServer.getInstance().getPlayer(uuid.getCorrectUniqueId()); + public void applySkin(@NonNull FloodgatePlayer floodgatePlayer, @NonNull SkinData skinData) { + ProxiedPlayer player = server.getPlayer(floodgatePlayer.getCorrectUniqueId()); if (player == null) { return; } @@ -69,13 +82,44 @@ public void applySkin(FloodgatePlayer uuid, SkinData skinData) { // expected to be null since LoginResult is the data from hasJoined, // which Floodgate players don't have if (loginResult == null) { - // id and name are unused and properties will be overridden - loginResult = new LoginResult(null, null, null); - setValue(handler, LOGIN_RESULT, loginResult); + // id and name are unused + loginResult = new LoginResult(null, null, new Property[0]); + ReflectionUtils.setValue(handler, LOGIN_RESULT_FIELD, loginResult); } - Property property = new Property("textures", skinData.getValue(), skinData.getSignature()); + Property[] properties = loginResult.getProperties(); + + SkinData currentSkin = currentSkin(properties); + + SkinApplyEvent event = new SkinApplyEventImpl(floodgatePlayer, currentSkin, skinData); + eventBus.fire(event); + + if (event.isCancelled()) { + return; + } - loginResult.setProperties(new Property[]{property}); + loginResult.setProperties(replaceSkin(properties, event.newSkin())); + } + + private SkinData currentSkin(Property[] properties) { + for (Property property : properties) { + if (property.getName().equals("textures")) { + if (!property.getValue().isEmpty()) { + return new SkinDataImpl(property.getValue(), property.getSignature()); + } + } + } + return null; + } + + private Property[] replaceSkin(Property[] properties, SkinData skinData) { + List list = new ArrayList<>(); + for (Property property : properties) { + if (!property.getName().equals("textures")) { + list.add(property); + } + } + list.add(new Property("textures", skinData.value(), skinData.signature())); + return list.toArray(new Property[0]); } } diff --git a/bungee/src/main/java/org/geysermc/floodgate/util/BungeeCommandUtil.java b/bungee/src/main/java/org/geysermc/floodgate/util/BungeeCommandUtil.java index 8d1e7c1c..322a0332 100644 --- a/bungee/src/main/java/org/geysermc/floodgate/util/BungeeCommandUtil.java +++ b/bungee/src/main/java/org/geysermc/floodgate/util/BungeeCommandUtil.java @@ -26,6 +26,7 @@ package org.geysermc.floodgate.util; import java.util.Collection; +import java.util.Locale; import java.util.UUID; import net.md_5.bungee.api.CommandSender; import net.md_5.bungee.api.ProxyServer; @@ -63,7 +64,8 @@ public BungeeCommandUtil(LanguageManager manager, ProxyServer server, FloodgateA ProxiedPlayer player = (ProxiedPlayer) source; UUID uuid = player.getUniqueId(); String username = player.getName(); - String locale = Utils.getLocale(player.getLocale()); + Locale playerLocale = player.getLocale(); // Is null during the PostLoginEvent, which can cause https://github.com/GeyserMC/Floodgate/issues/510 + String locale = Utils.getLocale(playerLocale != null ? playerLocale : Locale.getDefault()); return new PlayerAudience(uuid, username, locale, source, this, true); } diff --git a/bungee/src/main/java/org/geysermc/floodgate/util/BungeePlatformUtils.java b/bungee/src/main/java/org/geysermc/floodgate/util/BungeePlatformUtils.java new file mode 100644 index 00000000..fae9a83c --- /dev/null +++ b/bungee/src/main/java/org/geysermc/floodgate/util/BungeePlatformUtils.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.util; + +import java.lang.reflect.Field; +import java.util.List; +import net.md_5.bungee.api.ProxyServer; +import net.md_5.bungee.protocol.ProtocolConstants; +import org.geysermc.floodgate.platform.util.PlatformUtils; + +@SuppressWarnings("ConstantConditions") +public final class BungeePlatformUtils extends PlatformUtils { + private static final String LATEST_SUPPORTED_VERSION; + private final ProxyServer proxyServer = ProxyServer.getInstance(); + + static { + int protocolNumber = -1; + String versionName = ""; + + for (Field field : ProtocolConstants.class.getFields()) { + if (!field.getName().startsWith("MINECRAFT_")) { + continue; + } + + int fieldValue = ReflectionUtils.castedStaticValue(field); + if (fieldValue > protocolNumber) { + protocolNumber = fieldValue; + versionName = field.getName().substring(10).replace('_', '.'); + } + } + + if (protocolNumber == -1) { + List versions = ProtocolConstants.SUPPORTED_VERSIONS; + versionName = versions.get(versions.size() - 1); + } + LATEST_SUPPORTED_VERSION = versionName; + } + + @Override + public AuthType authType() { + return proxyServer.getConfig().isOnlineMode() ? AuthType.ONLINE : AuthType.OFFLINE; + } + + @Override + public String minecraftVersion() { + return LATEST_SUPPORTED_VERSION; + } + + @Override + public String serverImplementationName() { + return proxyServer.getName(); + } +} diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 8a1d04e0..20a9233c 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -1,20 +1,21 @@ -import net.kyori.blossom.BlossomExtension +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar plugins { - id("net.kyori.blossom") - id("floodgate.shadow-conventions") + id("floodgate.generate-templates") } dependencies { api(projects.api) api("org.geysermc.configutils", "configutils", Versions.configUtilsVersion) + compileOnly(projects.ap) + annotationProcessor(projects.ap) + api("com.google.inject", "guice", Versions.guiceVersion) api("com.nukkitx.fastutil", "fastutil-short-object-maps", Versions.fastutilVersion) api("com.nukkitx.fastutil", "fastutil-int-object-maps", Versions.fastutilVersion) api("org.java-websocket", "Java-WebSocket", Versions.javaWebsocketVersion) - api("cloud.commandframework", "cloud-core", Versions.cloudVersion) - api("org.yaml", "snakeyaml", Versions.snakeyamlVersion) + api("org.incendo", "cloud-core", Versions.cloudCore) api("org.bstats", "bstats-base", Versions.bstatsVersion) } @@ -24,8 +25,16 @@ provided("io.netty", "netty-codec", Versions.nettyVersion) relocate("org.bstats") -configure { - val constantsFile = "src/main/java/org/geysermc/floodgate/util/Constants.java" - replaceToken("\${branch}", branchName(), constantsFile) - replaceToken("\${buildNumber}", buildNumber(), constantsFile) -} +tasks { + templateSources { + replaceToken("floodgateVersion", fullVersion()) + replaceToken("branch", branchName()) + replaceToken("buildNumber", buildNumber()) + } + named("jar") { + archiveClassifier.set("") + } + named("shadowJar") { + archiveClassifier.set("shaded") + } +} \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/floodgate/FloodgatePlatform.java b/core/src/main/java/org/geysermc/floodgate/FloodgatePlatform.java index 245ef777..70487093 100644 --- a/core/src/main/java/org/geysermc/floodgate/FloodgatePlatform.java +++ b/core/src/main/java/org/geysermc/floodgate/FloodgatePlatform.java @@ -28,118 +28,66 @@ import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Module; -import com.google.inject.name.Named; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; import java.util.UUID; import org.geysermc.floodgate.api.FloodgateApi; import org.geysermc.floodgate.api.InstanceHolder; +import org.geysermc.floodgate.api.event.FloodgateEventBus; import org.geysermc.floodgate.api.handshake.HandshakeHandlers; import org.geysermc.floodgate.api.inject.PlatformInjector; import org.geysermc.floodgate.api.link.PlayerLink; -import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.packet.PacketHandlers; import org.geysermc.floodgate.config.FloodgateConfig; -import org.geysermc.floodgate.config.FloodgateConfigHolder; -import org.geysermc.floodgate.config.loader.ConfigLoader; -import org.geysermc.floodgate.link.PlayerLinkLoader; -import org.geysermc.floodgate.module.ConfigLoadedModule; +import org.geysermc.floodgate.event.EventBus; +import org.geysermc.floodgate.event.lifecycle.PostEnableEvent; +import org.geysermc.floodgate.event.lifecycle.ShutdownEvent; import org.geysermc.floodgate.module.PostInitializeModule; -import org.geysermc.floodgate.news.NewsChecker; -import org.geysermc.floodgate.util.PrefixCheckTask; public class FloodgatePlatform { private static final UUID KEY = UUID.randomUUID(); - private final FloodgateApi api; - private final PlatformInjector injector; + @Inject private PlatformInjector injector; - private final FloodgateLogger logger; - - private FloodgateConfig config; - private Injector guice; - - @Inject - public FloodgatePlatform( - FloodgateApi api, - PlatformInjector platformInjector, - FloodgateLogger logger, - Injector guice) { - - this.api = api; - this.injector = platformInjector; - this.logger = logger; - this.guice = guice; - } + @Inject private FloodgateConfig config; + @Inject private Injector guice; @Inject public void init( - @Named("dataDirectory") Path dataDirectory, - ConfigLoader configLoader, - FloodgateConfigHolder configHolder, + FloodgateApi api, + PlayerLink link, + FloodgateEventBus eventBus, PacketHandlers packetHandlers, - HandshakeHandlers handshakeHandlers) { - - if (!Files.isDirectory(dataDirectory)) { - try { - Files.createDirectory(dataDirectory); - } catch (IOException exception) { - logger.error("Failed to create the data folder", exception); - throw new RuntimeException("Failed to create the data folder", exception); - } - } - - config = configLoader.load(); - if (config.isDebug()) { - logger.enableDebug(); - } - - configHolder.set(config); - guice = guice.createChildInjector(new ConfigLoadedModule(config)); - PlayerLink link = guice.getInstance(PlayerLinkLoader.class).load(); - - InstanceHolder.set(api, link, this.injector, packetHandlers, handshakeHandlers, KEY); - - guice.getInstance(NewsChecker.class).start(); + HandshakeHandlers handshakeHandlers + ) { + InstanceHolder.set( + api, link, eventBus, this.injector, packetHandlers, handshakeHandlers, KEY + ); } - public boolean enable(Module... postInitializeModules) { + public void enable(Module... postInitializeModules) throws RuntimeException { if (injector == null) { - logger.error("Failed to find the platform injector!"); - return false; + throw new RuntimeException("Failed to find the platform injector!"); } try { - if (!injector.inject()) { - logger.error("Failed to inject the packet listener!"); - return false; - } + injector.inject(); } catch (Exception exception) { - logger.error("Failed to inject the packet listener!", exception); - return false; + throw new RuntimeException("Failed to inject the packet listener!", exception); } this.guice = guice.createChildInjector(new PostInitializeModule(postInitializeModules)); - PrefixCheckTask.checkAndExecuteDelayed(config, logger); - - return true; + guice.getInstance(EventBus.class).fire(new PostEnableEvent()); } - public boolean disable() { + public void disable() { + guice.getInstance(EventBus.class).fire(new ShutdownEvent()); + if (injector != null && injector.canRemoveInjection()) { try { - if (!injector.removeInjection()) { - logger.error("Failed to remove the injection!"); - } + injector.removeInjection(); } catch (Exception exception) { - logger.error("Failed to remove the injection!", exception); + throw new RuntimeException("Failed to remove the injection!", exception); } } - - guice.getInstance(NewsChecker.class).shutdown(); - api.getPlayerLink().stop(); - return true; } public boolean isProxy() { diff --git a/core/src/main/java/org/geysermc/floodgate/addon/DebugAddon.java b/core/src/main/java/org/geysermc/floodgate/addon/DebugAddon.java index 7249e167..dcf9a7a6 100644 --- a/core/src/main/java/org/geysermc/floodgate/addon/DebugAddon.java +++ b/core/src/main/java/org/geysermc/floodgate/addon/DebugAddon.java @@ -29,9 +29,9 @@ import com.google.inject.name.Named; import io.netty.channel.Channel; import io.netty.channel.ChannelPipeline; +import java.util.concurrent.atomic.AtomicInteger; import org.geysermc.floodgate.addon.debug.ChannelInDebugHandler; import org.geysermc.floodgate.addon.debug.ChannelOutDebugHandler; -import org.geysermc.floodgate.addon.debug.StateChangeDetector; import org.geysermc.floodgate.api.inject.InjectorAddon; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.config.FloodgateConfig; @@ -57,16 +57,14 @@ public final class DebugAddon implements InjectorAddon { public void onInject(Channel channel, boolean toServer) { logger.info("Successfully called onInject. To server? " + toServer); - StateChangeDetector changeDetector = new StateChangeDetector( - channel, packetEncoder, packetDecoder, logger - ); + AtomicInteger packetCount = new AtomicInteger(); channel.pipeline().addBefore( packetEncoder, "floodgate_debug_out", - new ChannelOutDebugHandler(implementationName, toServer, changeDetector, logger) + new ChannelOutDebugHandler(implementationName, toServer, packetCount, logger) ).addBefore( packetDecoder, "floodgate_debug_in", - new ChannelInDebugHandler(implementationName, toServer, changeDetector, logger) + new ChannelInDebugHandler(implementationName, toServer, packetCount, logger) ); } diff --git a/core/src/main/java/org/geysermc/floodgate/addon/debug/ChannelInDebugHandler.java b/core/src/main/java/org/geysermc/floodgate/addon/debug/ChannelInDebugHandler.java index d1b1619c..bf1588e3 100644 --- a/core/src/main/java/org/geysermc/floodgate/addon/debug/ChannelInDebugHandler.java +++ b/core/src/main/java/org/geysermc/floodgate/addon/debug/ChannelInDebugHandler.java @@ -30,25 +30,24 @@ import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.SimpleChannelInboundHandler; +import java.util.concurrent.atomic.AtomicInteger; import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.util.Utils; @Sharable public final class ChannelInDebugHandler extends SimpleChannelInboundHandler { private final String direction; private final FloodgateLogger logger; - - private final boolean toServer; - private final StateChangeDetector changeDetector; + private final AtomicInteger packetCount; public ChannelInDebugHandler( String implementationType, boolean toServer, - StateChangeDetector changeDetector, + AtomicInteger packetCount, FloodgateLogger logger) { this.direction = (toServer ? "Server -> " : "Player -> ") + implementationType; this.logger = logger; - this.toServer = toServer; - this.changeDetector = changeDetector; + this.packetCount = packetCount; } @Override @@ -56,14 +55,12 @@ protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) { try { int index = msg.readerIndex(); - if (changeDetector.shouldPrintPacket(msg, !toServer)) { + if (packetCount.getAndIncrement() < Utils.MAX_DEBUG_PACKET_COUNT) { logger.info("{} {}:\n{}", direction, - changeDetector.getCurrentState(), + packetCount.get(), ByteBufUtil.prettyHexDump(msg) ); - - changeDetector.checkPacket(msg, !toServer); } // reset index diff --git a/core/src/main/java/org/geysermc/floodgate/addon/debug/ChannelOutDebugHandler.java b/core/src/main/java/org/geysermc/floodgate/addon/debug/ChannelOutDebugHandler.java index 05a80673..f4ab7acf 100644 --- a/core/src/main/java/org/geysermc/floodgate/addon/debug/ChannelOutDebugHandler.java +++ b/core/src/main/java/org/geysermc/floodgate/addon/debug/ChannelOutDebugHandler.java @@ -30,25 +30,24 @@ import io.netty.channel.ChannelHandler.Sharable; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.MessageToByteEncoder; +import java.util.concurrent.atomic.AtomicInteger; import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.util.Utils; @Sharable public final class ChannelOutDebugHandler extends MessageToByteEncoder { private final String direction; private final FloodgateLogger logger; - - private final boolean toServer; - private final StateChangeDetector changeDetector; + private final AtomicInteger packetCount; public ChannelOutDebugHandler( String implementationType, boolean toServer, - StateChangeDetector changeDetector, + AtomicInteger packetCount, FloodgateLogger logger) { this.direction = implementationType + (toServer ? " -> Server" : " -> Player"); this.logger = logger; - this.toServer = toServer; - this.changeDetector = changeDetector; + this.packetCount = packetCount; } @Override @@ -56,16 +55,13 @@ protected void encode(ChannelHandlerContext ctx, ByteBuf msg, ByteBuf out) { try { int index = msg.readerIndex(); - if (changeDetector.shouldPrintPacket(msg, toServer)) { + if (packetCount.getAndIncrement() < Utils.MAX_DEBUG_PACKET_COUNT) { logger.info( "{} {}:\n{}", direction, - changeDetector.getCurrentState(), + packetCount.get(), ByteBufUtil.prettyHexDump(msg) ); - - // proxy acts as a client when it connects to a server - changeDetector.checkPacket(msg, toServer); } // reset index diff --git a/core/src/main/java/org/geysermc/floodgate/addon/debug/StateChangeDetector.java b/core/src/main/java/org/geysermc/floodgate/addon/debug/StateChangeDetector.java deleted file mode 100644 index 2c60a1c8..00000000 --- a/core/src/main/java/org/geysermc/floodgate/addon/debug/StateChangeDetector.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Floodgate - */ - -package org.geysermc.floodgate.addon.debug; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelPipeline; -import java.nio.charset.StandardCharsets; -import org.geysermc.floodgate.api.logger.FloodgateLogger; -import org.geysermc.floodgate.util.Constants; -import org.geysermc.floodgate.util.Utils; - -public class StateChangeDetector { - private static volatile int pluginMessageToClientId = -1; - private static volatile int pluginMessageToServerId = -1; - - private final Channel channel; - private final FloodgateLogger logger; - private final String packetEncoderName; - private final String packetDecoderName; - - private volatile boolean enableCompressionNext; - private volatile State currentState = State.HANDSHAKE; - - public StateChangeDetector( - Channel channel, - String packetEncoderName, - String packetDecoderName, - FloodgateLogger logger) { - this.channel = channel; - this.packetEncoderName = packetEncoderName; - this.packetDecoderName = packetDecoderName; - this.logger = logger; - } - - /** - * Checks (and acts) if the current packet is one of the packets that we need to switch states. - * - * @param packet the packet to check - * @param fromClient if the packet is clientbound or serverbound - */ - public void checkPacket(ByteBuf packet, boolean fromClient) { - int index = packet.readerIndex(); - - if (enableCompressionNext) { - // data length - Utils.readVarInt(packet); - - fixCompressionPipes(); - enableCompressionNext = false; - } - - int packetId = Utils.readVarInt(packet); - - if (fromClient) { - if (currentState == State.HANDSHAKE && packetId == Constants.HANDSHAKE_PACKET_ID) { - // have to read the content to determine the next state - - // protocol version - Utils.readVarInt(packet); - // server address - int hostLength = Utils.readVarInt(packet); - // read server address + port (short = 2 bytes) - packet.readerIndex(packet.readerIndex() + hostLength + 2); - // next state - currentState = State.getById(Utils.readVarInt(packet)); - } - } else { - if (currentState == State.LOGIN) { - if (packetId == Constants.LOGIN_SUCCESS_PACKET_ID) { - currentState = State.PLAY; - } - if (packetId == Constants.SET_COMPRESSION_PACKET_ID) { - enableCompressionNext = true; - } - } - } - - // reset index - packet.readerIndex(index); - } - - private void fixCompressionPipes() { - // The previous packet was the compression packet, meaning that starting with this - // packet the data can already be compressed. The compression handler has been added - // directly before the packet encoder and decoder, so we have to reclaim that place. - // If we don't, we'll see the compressed data. - - ChannelPipeline pipeline = channel.pipeline(); - - ChannelHandler outDebug = pipeline.remove(ChannelOutDebugHandler.class); - ChannelHandler inDebug = pipeline.remove(ChannelInDebugHandler.class); - - pipeline.addBefore(packetEncoderName, "floodgate_debug_out", outDebug); - pipeline.addBefore(packetDecoderName, "floodgate_debug_in", inDebug); - } - - public boolean shouldPrintPacket(ByteBuf packet, boolean clientbound) { - return Constants.PRINT_ALL_PACKETS || - currentState == State.HANDSHAKE || currentState == State.LOGIN || - currentState != State.STATUS && shouldPrintPlayPacket(packet, clientbound); - } - - public boolean shouldPrintPlayPacket(ByteBuf packet, boolean clientbound) { - int index = packet.readerIndex(); - - int packetId = Utils.readVarInt(packet); - - // we're only interested in the plugin message packets - - // use cached packet ids - if (clientbound && pluginMessageToClientId != -1) { - return pluginMessageToClientId == packetId; - } - if (!clientbound && pluginMessageToServerId != -1) { - return pluginMessageToServerId == packetId; - } - - boolean shouldPrint = false; - - if (packet.isReadable()) { - // format plugin message packet: channel - remaining data - try { - int channelLength = Utils.readVarInt(packet); - - if (channelLength >= 1 && channelLength <= 128 && - packet.isReadable(channelLength)) { - - byte[] channelBytes = new byte[channelLength]; - packet.readBytes(channelBytes); - - String channelName = new String(channelBytes, StandardCharsets.UTF_8); - if (channelName.contains(":")) { - // some other packets will still match, - // but since plugin message packets are send early on - // we can almost know for certain that this is a plugin message packet - printIdentified(clientbound, packetId); - if (clientbound) { - pluginMessageToClientId = packetId; - } else { - pluginMessageToServerId = packetId; - } - shouldPrint = true; - } - } - } catch (RuntimeException ignored) { - // not the plugin message packet - } - } - - // reset index - packet.readerIndex(index); - - return shouldPrint; - } - - private void printIdentified(boolean clientbound, int packetId) { - logger.info( - "Identified plugin message packet ({}) as {} ({})", - clientbound ? "clientbound" : "serverbound", - packetId, - Integer.toHexString(packetId) - ); - } - - public State getCurrentState() { - return currentState; - } -} diff --git a/core/src/main/java/org/geysermc/floodgate/api/ProxyFloodgateApi.java b/core/src/main/java/org/geysermc/floodgate/api/ProxyFloodgateApi.java index a3336e09..ede56730 100644 --- a/core/src/main/java/org/geysermc/floodgate/api/ProxyFloodgateApi.java +++ b/core/src/main/java/org/geysermc/floodgate/api/ProxyFloodgateApi.java @@ -25,24 +25,14 @@ package org.geysermc.floodgate.api; +import com.google.inject.Inject; import java.nio.charset.StandardCharsets; -import org.geysermc.floodgate.api.logger.FloodgateLogger; -import org.geysermc.floodgate.config.FloodgateConfigHolder; import org.geysermc.floodgate.crypto.FloodgateCipher; -import org.geysermc.floodgate.pluginmessage.PluginMessageManager; import org.geysermc.floodgate.util.BedrockData; public final class ProxyFloodgateApi extends SimpleFloodgateApi { - private final FloodgateCipher cipher; - - public ProxyFloodgateApi( - PluginMessageManager pluginMessageManager, - FloodgateConfigHolder configHolder, - FloodgateLogger logger, - FloodgateCipher cipher) { - super(pluginMessageManager, configHolder, logger); - this.cipher = cipher; - } + @Inject + private FloodgateCipher cipher; public byte[] createEncryptedData(BedrockData bedrockData) { try { diff --git a/core/src/main/java/org/geysermc/floodgate/api/SimpleFloodgateApi.java b/core/src/main/java/org/geysermc/floodgate/api/SimpleFloodgateApi.java index 8974396f..baf05da8 100644 --- a/core/src/main/java/org/geysermc/floodgate/api/SimpleFloodgateApi.java +++ b/core/src/main/java/org/geysermc/floodgate/api/SimpleFloodgateApi.java @@ -30,41 +30,41 @@ import com.google.common.collect.ImmutableSet; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.inject.Inject; import java.util.Collection; -import java.util.HashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; -import lombok.RequiredArgsConstructor; -import org.geysermc.cumulus.Form; -import org.geysermc.cumulus.util.FormBuilder; +import org.geysermc.cumulus.form.Form; +import org.geysermc.cumulus.form.util.FormBuilder; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; import org.geysermc.floodgate.api.unsafe.Unsafe; -import org.geysermc.floodgate.config.FloodgateConfigHolder; +import org.geysermc.floodgate.config.FloodgateConfig; import org.geysermc.floodgate.pluginmessage.PluginMessageManager; import org.geysermc.floodgate.pluginmessage.channel.FormChannel; import org.geysermc.floodgate.pluginmessage.channel.TransferChannel; import org.geysermc.floodgate.util.Constants; -import org.geysermc.floodgate.util.HttpUtils; +import org.geysermc.floodgate.util.HttpClient; import org.geysermc.floodgate.util.Utils; -@RequiredArgsConstructor public class SimpleFloodgateApi implements FloodgateApi { - private final Map players = new HashMap<>(); + private final Map players = new ConcurrentHashMap<>(); private final Cache pendingRemove = CacheBuilder.newBuilder() .expireAfterWrite(20, TimeUnit.SECONDS) .build(); - private final PluginMessageManager pluginMessageManager; - private final FloodgateConfigHolder configHolder; - private final FloodgateLogger logger; + @Inject private PluginMessageManager pluginMessageManager; + @Inject private FloodgateConfig config; + @Inject private HttpClient httpClient; + @Inject private FloodgateLogger logger; @Override public String getPlayerPrefix() { - return configHolder.get().getUsernamePrefix(); + return config.getUsernamePrefix(); } @Override @@ -121,7 +121,22 @@ public boolean sendForm(UUID uuid, Form form) { } @Override - public boolean sendForm(UUID uuid, FormBuilder formBuilder) { + public boolean sendForm(UUID uuid, FormBuilder formBuilder) { + return sendForm(uuid, formBuilder.build()); + } + + @Override + public boolean closeForm(UUID uuid) { + return pluginMessageManager.getChannel(FormChannel.class).closeForm(uuid); + } + + @Override + public boolean sendForm(UUID uuid, org.geysermc.cumulus.Form form) { + return sendForm(uuid, form.newForm()); + } + + @Override + public boolean sendForm(UUID uuid, org.geysermc.cumulus.util.FormBuilder formBuilder) { return sendForm(uuid, formBuilder.build()); } @@ -138,7 +153,7 @@ public CompletableFuture getXuidFor(String gamertag) { return Utils.failedFuture(new IllegalStateException("Received an invalid gamertag")); } - return HttpUtils.asyncGet(Constants.GET_XUID_URL + gamertag) + return httpClient.asyncGet(Constants.GET_XUID_URL + gamertag) .thenApply(result -> { JsonObject response = result.getResponse(); @@ -153,7 +168,7 @@ public CompletableFuture getXuidFor(String gamertag) { @Override public CompletableFuture getGamertagFor(long xuid) { - return HttpUtils.asyncGet(Constants.GET_GAMERTAG_URL + xuid) + return httpClient.asyncGet(Constants.GET_GAMERTAG_URL + xuid) .thenApply(result -> { JsonObject response = result.getResponse(); diff --git a/core/src/main/java/org/geysermc/floodgate/command/LinkAccountCommand.java b/core/src/main/java/org/geysermc/floodgate/command/LinkAccountCommand.java index 60392de7..7c06429d 100644 --- a/core/src/main/java/org/geysermc/floodgate/command/LinkAccountCommand.java +++ b/core/src/main/java/org/geysermc/floodgate/command/LinkAccountCommand.java @@ -26,12 +26,8 @@ package org.geysermc.floodgate.command; import static org.geysermc.floodgate.command.CommonCommandMessage.CHECK_CONSOLE; +import static org.incendo.cloud.parser.standard.StringParser.stringParser; -import cloud.commandframework.ArgumentDescription; -import cloud.commandframework.Command; -import cloud.commandframework.CommandManager; -import cloud.commandframework.arguments.standard.StringArgument; -import cloud.commandframework.context.CommandContext; import com.google.inject.Inject; import lombok.Getter; import lombok.NoArgsConstructor; @@ -46,8 +42,13 @@ import org.geysermc.floodgate.platform.command.TranslatableMessage; import org.geysermc.floodgate.player.UserAudience; import org.geysermc.floodgate.player.UserAudience.PlayerAudience; -import org.geysermc.floodgate.player.audience.ProfileAudienceArgument; +import org.geysermc.floodgate.player.audience.PlayerAudienceArgument; +import org.geysermc.floodgate.player.audience.ProfileAudience; import org.geysermc.floodgate.util.Constants; +import org.incendo.cloud.Command; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.description.Description; @NoArgsConstructor public final class LinkAccountCommand implements FloodgateCommand { @@ -55,20 +56,19 @@ public final class LinkAccountCommand implements FloodgateCommand { @Inject private FloodgateLogger logger; @Override - public Command buildCommand(CommandManager commandManager) { + public Command buildCommand(CommandManager commandManager) { return commandManager.commandBuilder("linkaccount", - ArgumentDescription.of("Link your Java account with your Bedrock account")) + Description.of("Link your Java account with your Bedrock account")) .senderType(PlayerAudience.class) .permission(Permission.COMMAND_LINK.get()) - .argument(ProfileAudienceArgument.of("player", true)) - .argument(StringArgument.optional("code")) + .argument(PlayerAudienceArgument.ofAnyUsernameBoth("player")) + .optional("code", stringParser()) .handler(this::execute) .build(); } - @Override - public void execute(CommandContext context) { - UserAudience sender = context.getSender(); + public void execute(CommandContext context) { + UserAudience sender = context.sender(); PlayerLink link = api.getPlayerLink(); @@ -89,6 +89,10 @@ public void execute(CommandContext context) { return; } + ProfileAudience targetUser = context.get("player"); + // allowUuid is false so username cannot be null + String targetName = targetUser.username(); + // when the player is a Bedrock player if (api.isFloodgatePlayer(sender.uuid())) { if (!context.contains("code")) { @@ -96,8 +100,6 @@ public void execute(CommandContext context) { return; } - UserAudience targetUser = context.get("player"); - String targetName = targetUser.username(); String code = context.get("code"); link.verifyLinkRequest(sender.uuid(), targetName, sender.username(), code) @@ -136,9 +138,6 @@ public void execute(CommandContext context) { return; } - UserAudience targetUser = context.get("player"); - String targetName = targetUser.username(); - link.createLinkRequest(sender.uuid(), sender.username(), targetName) .whenComplete((result, throwable) -> { if (throwable != null || result == LinkRequestResult.UNKNOWN_ERROR) { diff --git a/core/src/main/java/org/geysermc/floodgate/command/TestCommand.java b/core/src/main/java/org/geysermc/floodgate/command/TestCommand.java index f950da04..10213c78 100644 --- a/core/src/main/java/org/geysermc/floodgate/command/TestCommand.java +++ b/core/src/main/java/org/geysermc/floodgate/command/TestCommand.java @@ -25,14 +25,14 @@ package org.geysermc.floodgate.command; -import cloud.commandframework.Command; -import cloud.commandframework.CommandManager; -import cloud.commandframework.context.CommandContext; import org.geysermc.floodgate.api.FloodgateApi; import org.geysermc.floodgate.config.FloodgateConfig; import org.geysermc.floodgate.platform.command.FloodgateCommand; import org.geysermc.floodgate.player.UserAudience; import org.geysermc.floodgate.util.Constants; +import org.incendo.cloud.Command; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; public class TestCommand implements FloodgateCommand { @Override @@ -43,10 +43,9 @@ public Command buildCommand(CommandManager commandMa .build(); } - @Override public void execute(CommandContext context) { int players = FloodgateApi.getInstance().getPlayers().size(); - context.getSender().sendMessage(String.valueOf(players)); + context.sender().sendMessage(String.valueOf(players)); } @Override diff --git a/core/src/main/java/org/geysermc/floodgate/command/UnlinkAccountCommand.java b/core/src/main/java/org/geysermc/floodgate/command/UnlinkAccountCommand.java index 1c5b0444..3ba4682b 100644 --- a/core/src/main/java/org/geysermc/floodgate/command/UnlinkAccountCommand.java +++ b/core/src/main/java/org/geysermc/floodgate/command/UnlinkAccountCommand.java @@ -27,10 +27,6 @@ import static org.geysermc.floodgate.command.CommonCommandMessage.CHECK_CONSOLE; -import cloud.commandframework.ArgumentDescription; -import cloud.commandframework.Command; -import cloud.commandframework.CommandManager; -import cloud.commandframework.context.CommandContext; import com.google.inject.Inject; import lombok.Getter; import lombok.NoArgsConstructor; @@ -44,24 +40,27 @@ import org.geysermc.floodgate.player.UserAudience.PlayerAudience; import org.geysermc.floodgate.util.Constants; import org.geysermc.floodgate.command.util.Permission; +import org.incendo.cloud.Command; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.description.Description; @NoArgsConstructor public final class UnlinkAccountCommand implements FloodgateCommand { @Inject private FloodgateApi api; @Override - public Command buildCommand(CommandManager commandManager) { + public Command buildCommand(CommandManager commandManager) { return commandManager.commandBuilder("unlinkaccount", - ArgumentDescription.of("Unlink your Java account from your Bedrock account")) + Description.of("Unlink your Java account from your Bedrock account")) .senderType(PlayerAudience.class) .permission(Permission.COMMAND_UNLINK.get()) .handler(this::execute) .build(); } - @Override - public void execute(CommandContext context) { - UserAudience sender = context.getSender(); + public void execute(CommandContext context) { + UserAudience sender = context.sender(); PlayerLink link = api.getPlayerLink(); diff --git a/core/src/main/java/org/geysermc/floodgate/command/WhitelistCommand.java b/core/src/main/java/org/geysermc/floodgate/command/WhitelistCommand.java index cce707cc..54c5efcb 100644 --- a/core/src/main/java/org/geysermc/floodgate/command/WhitelistCommand.java +++ b/core/src/main/java/org/geysermc/floodgate/command/WhitelistCommand.java @@ -27,10 +27,6 @@ import static org.geysermc.floodgate.command.CommonCommandMessage.CHECK_CONSOLE; -import cloud.commandframework.ArgumentDescription; -import cloud.commandframework.Command; -import cloud.commandframework.CommandManager; -import cloud.commandframework.context.CommandContext; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.inject.Inject; @@ -44,37 +40,42 @@ import org.geysermc.floodgate.platform.command.CommandUtil; import org.geysermc.floodgate.platform.command.FloodgateCommand; import org.geysermc.floodgate.platform.command.TranslatableMessage; -import org.geysermc.floodgate.platform.util.PlatformUtils.PlayerType; +import org.geysermc.floodgate.platform.util.PlayerType; import org.geysermc.floodgate.player.UserAudience; +import org.geysermc.floodgate.player.audience.PlayerAudienceArgument; import org.geysermc.floodgate.player.audience.ProfileAudience; -import org.geysermc.floodgate.player.audience.ProfileAudienceArgument; import org.geysermc.floodgate.util.Constants; -import org.geysermc.floodgate.util.HttpUtils; +import org.geysermc.floodgate.util.HttpClient; +import org.incendo.cloud.Command; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.description.Description; public class WhitelistCommand implements FloodgateCommand { @Inject private FloodgateConfig config; + @Inject private HttpClient httpClient; @Inject private FloodgateLogger logger; @Override public Command buildCommand(CommandManager commandManager) { Command.Builder builder = commandManager.commandBuilder("fwhitelist", - ArgumentDescription.of("Easy way to whitelist Bedrock players")) + Description.of("Easy way to whitelist Bedrock players")) .permission(Permission.COMMAND_WHITELIST.get()); commandManager.command(builder .literal("add", "a") - .argument(ProfileAudienceArgument.of("player", true, true, PlayerType.ONLY_BEDROCK)) + .argument(PlayerAudienceArgument.ofAnyIdentifierBedrock("player")) .handler(context -> performCommand(context, true))); return builder .literal("remove", "r") - .argument(ProfileAudienceArgument.of("player", true, true, PlayerType.ONLY_BEDROCK)) + .argument(PlayerAudienceArgument.ofAnyIdentifierBedrock("player")) .handler(context -> performCommand(context, false)) .build(); } public void performCommand(CommandContext context, boolean add) { - UserAudience sender = context.getSender(); + UserAudience sender = context.sender(); ProfileAudience profile = context.get("player"); UUID uuid = profile.uuid(); String name = profile.username(); @@ -113,7 +114,7 @@ public void performCommand(CommandContext context, boolean add) { name = name.substring(config.getUsernamePrefix().length()); } - if (name.length() < 1 || name.length() > 16) { + if (name.isEmpty() || name.length() > 16) { sender.sendMessage(Message.INVALID_USERNAME); return; } @@ -128,7 +129,7 @@ public void performCommand(CommandContext context, boolean add) { final String strippedName = name; // We need to get the UUID of the player if it's not manually specified - HttpUtils.asyncGet(Constants.GET_XUID_URL + name) + httpClient.asyncGet(Constants.GET_XUID_URL + name) .whenComplete((result, error) -> { if (error != null) { sender.sendMessage(Message.API_UNAVAILABLE); @@ -180,11 +181,6 @@ public void performCommand(CommandContext context, boolean add) { }); } - @Override - public void execute(CommandContext context) { - // ignored, all the logic is in the other method - } - @Override public boolean shouldRegister(FloodgateConfig config) { // currently only Spigot (our only non-Proxy platform) has a whitelist build-in. diff --git a/core/src/main/java/org/geysermc/floodgate/command/main/FirewallCheckSubcommand.java b/core/src/main/java/org/geysermc/floodgate/command/main/FirewallCheckSubcommand.java index 66f439b7..be64d92a 100644 --- a/core/src/main/java/org/geysermc/floodgate/command/main/FirewallCheckSubcommand.java +++ b/core/src/main/java/org/geysermc/floodgate/command/main/FirewallCheckSubcommand.java @@ -27,23 +27,43 @@ import static org.geysermc.floodgate.util.Constants.COLOR_CHAR; -import cloud.commandframework.context.CommandContext; import com.google.gson.JsonElement; +import com.google.inject.Inject; import it.unimi.dsi.fastutil.Pair; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BooleanSupplier; +import org.geysermc.floodgate.command.util.Permission; +import org.geysermc.floodgate.platform.command.FloodgateSubCommand; import org.geysermc.floodgate.player.UserAudience; import org.geysermc.floodgate.util.Constants; -import org.geysermc.floodgate.util.HttpUtils; -import org.geysermc.floodgate.util.HttpUtils.HttpResponse; +import org.geysermc.floodgate.util.HttpClient; +import org.geysermc.floodgate.util.HttpClient.HttpResponse; import org.geysermc.floodgate.util.Utils; +import org.incendo.cloud.context.CommandContext; -final class FirewallCheckSubcommand { - private FirewallCheckSubcommand() {} +final class FirewallCheckSubcommand extends FloodgateSubCommand { + @Inject + private HttpClient httpClient; - static void executeFirewall(CommandContext context) { - UserAudience sender = context.getSender(); + @Override + public String name() { + return "firewall"; + } + + @Override + public String description() { + return "Check if your outgoing firewall allows Floodgate to work properly"; + } + + @Override + public Permission permission() { + return Permission.COMMAND_MAIN_FIREWALL; + } + + @Override + public void execute(CommandContext context) { + UserAudience sender = context.sender(); executeChecks( globalApiCheck(sender) ).whenComplete((response, $) -> @@ -54,12 +74,12 @@ static void executeFirewall(CommandContext context) { ); } - private static BooleanSupplier globalApiCheck(UserAudience sender) { + private BooleanSupplier globalApiCheck(UserAudience sender) { return executeFirewallText( sender, "global api", () -> { HttpResponse response = - HttpUtils.get(Constants.HEALTH_URL, JsonElement.class); + httpClient.get(Constants.HEALTH_URL, JsonElement.class); if (!response.isCodeOk()) { throw new IllegalStateException(String.format( @@ -70,7 +90,7 @@ private static BooleanSupplier globalApiCheck(UserAudience sender) { }); } - private static BooleanSupplier executeFirewallText( + private BooleanSupplier executeFirewallText( UserAudience sender, String name, Runnable runnable) { return () -> { sender.sendMessage(COLOR_CHAR + "eTesting " + name + "..."); @@ -86,9 +106,7 @@ private static BooleanSupplier executeFirewallText( }; } - private static CompletableFuture> executeChecks( - BooleanSupplier... checks) { - + private CompletableFuture> executeChecks(BooleanSupplier... checks) { return CompletableFuture.supplyAsync(() -> { AtomicInteger okCount = new AtomicInteger(); AtomicInteger failCount = new AtomicInteger(); diff --git a/core/src/main/java/org/geysermc/floodgate/command/main/MainCommand.java b/core/src/main/java/org/geysermc/floodgate/command/main/MainCommand.java index 093289fe..801676e4 100644 --- a/core/src/main/java/org/geysermc/floodgate/command/main/MainCommand.java +++ b/core/src/main/java/org/geysermc/floodgate/command/main/MainCommand.java @@ -27,33 +27,38 @@ import static org.geysermc.floodgate.util.Constants.COLOR_CHAR; -import cloud.commandframework.ArgumentDescription; -import cloud.commandframework.Command; -import cloud.commandframework.Command.Builder; -import cloud.commandframework.CommandManager; -import cloud.commandframework.context.CommandContext; import java.util.Locale; -import java.util.function.Consumer; -import lombok.RequiredArgsConstructor; import org.geysermc.floodgate.command.util.Permission; import org.geysermc.floodgate.platform.command.FloodgateCommand; +import org.geysermc.floodgate.platform.command.FloodgateSubCommand; +import org.geysermc.floodgate.platform.command.SubCommands; import org.geysermc.floodgate.player.UserAudience; +import org.incendo.cloud.Command; +import org.incendo.cloud.Command.Builder; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.context.CommandContext; +import org.incendo.cloud.description.Description; + +public final class MainCommand extends SubCommands implements FloodgateCommand { + public MainCommand() { + defineSubCommand(FirewallCheckSubcommand.class); + defineSubCommand(VersionSubcommand.class); + } -public final class MainCommand implements FloodgateCommand { @Override public Command buildCommand(CommandManager commandManager) { Builder builder = commandManager.commandBuilder( "floodgate", - ArgumentDescription.of("A set of Floodgate related actions in one command")) + Description.of("A set of Floodgate related actions in one command")) .senderType(UserAudience.class) .permission(Permission.COMMAND_MAIN.get()) .handler(this::execute); - for (SubCommand subCommand : SubCommand.VALUES) { + for (FloodgateSubCommand subCommand : subCommands()) { commandManager.command(builder - .literal(subCommand.name().toLowerCase(Locale.ROOT), subCommand.description) - .permission(subCommand.permission.get()) - .handler(subCommand.executor::accept) + .literal(subCommand.name().toLowerCase(Locale.ROOT), Description.of(subCommand.description())) + .permission(subCommand.permission().get()) + .handler(subCommand::execute) ); } @@ -61,31 +66,18 @@ public Command buildCommand(CommandManager commandMa return builder.build(); } - @Override public void execute(CommandContext context) { StringBuilder helpMessage = new StringBuilder("Available subcommands are:\n"); - for (SubCommand subCommand : SubCommand.VALUES) { - if (context.getSender().hasPermission(subCommand.permission.get())) { + for (FloodgateSubCommand subCommand : subCommands()) { + if (context.sender().hasPermission(subCommand.permission().get())) { helpMessage.append('\n').append(COLOR_CHAR).append('b') .append(subCommand.name().toLowerCase(Locale.ROOT)) .append(COLOR_CHAR).append("f - ").append(COLOR_CHAR).append('7') - .append(subCommand.description); + .append(subCommand.description()); } } - context.getSender().sendMessage(helpMessage.toString()); - } - - @RequiredArgsConstructor - enum SubCommand { - FIREWALL("Check if your outgoing firewall allows Floodgate to work properly", - Permission.COMMAND_MAIN_FIREWALL, FirewallCheckSubcommand::executeFirewall); - - static final SubCommand[] VALUES = values(); - - final String description; - final Permission permission; - final Consumer> executor; + context.sender().sendMessage(helpMessage.toString()); } } diff --git a/core/src/main/java/org/geysermc/floodgate/command/main/VersionSubcommand.java b/core/src/main/java/org/geysermc/floodgate/command/main/VersionSubcommand.java new file mode 100644 index 00000000..961c1735 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/command/main/VersionSubcommand.java @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.command.main; + +import static org.geysermc.floodgate.util.Constants.COLOR_CHAR; + +import com.google.gson.JsonObject; +import com.google.inject.Inject; +import com.google.inject.name.Named; +import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.command.WhitelistCommand.Message; +import org.geysermc.floodgate.command.util.Permission; +import org.geysermc.floodgate.platform.command.FloodgateSubCommand; +import org.geysermc.floodgate.player.UserAudience; +import org.geysermc.floodgate.util.Constants; +import org.geysermc.floodgate.util.HttpClient; +import org.incendo.cloud.context.CommandContext; + +public class VersionSubcommand extends FloodgateSubCommand { + @Inject + private HttpClient httpClient; + + @Inject + private FloodgateLogger logger; + + @Inject + @Named("implementationName") + private String implementationName; + + @Override + public String name() { + return "version"; + } + + @Override + public String description() { + return "Displays version information about Floodgate"; + } + + @Override + public Permission permission() { + return Permission.COMMAND_MAIN_VERSION; + } + + @Override + public void execute(CommandContext context) { + UserAudience sender = context.sender(); + sender.sendMessage(String.format( + COLOR_CHAR + "7You're currently on " + COLOR_CHAR + "b%s" + + COLOR_CHAR + "7 (branch: " + COLOR_CHAR + "b%s" + COLOR_CHAR + "7)\n" + + COLOR_CHAR + "eFetching latest build info...", + Constants.VERSION, Constants.GIT_BRANCH + )); + + //noinspection ConstantValue + if (!Constants.GIT_MAIN_BRANCH.equals(Constants.GIT_BRANCH)) { + sender.sendMessage(String.format( + COLOR_CHAR + "7Detected that you aren't on the %s branch, " + + "so we can't fetch the latest version.", + Constants.GIT_MAIN_BRANCH + )); + return; + } + + httpClient.asyncGet( + String.format(Constants.LATEST_VERSION_URL, Constants.PROJECT_NAME), + JsonObject.class + ).whenComplete((result, error) -> { + if (error != null) { + sender.sendMessage(COLOR_CHAR + "cCould not retrieve latest version info!"); + error.printStackTrace(); + return; + } + + JsonObject response = result.getResponse(); + + logger.debug("code: {}, response: ", result.getHttpCode(), String.valueOf(response)); + + if (result.getHttpCode() == 404) { + sender.sendMessage( + COLOR_CHAR + "cGot a 404 (not found) while requesting the latest version." + + " Are you using a custom Floodgate version?" + ); + return; + } + + if (!result.isCodeOk()) { + //todo make it more generic instead of using a Whitelist command strings + logger.error( + "Got an error from requesting the latest Floodgate version: {}", + String.valueOf(response) + ); + sender.sendMessage(Message.UNEXPECTED_ERROR); + return; + } + + int buildNumber = response.get("build").getAsInt(); + + if (buildNumber > Constants.BUILD_NUMBER) { + sender.sendMessage(String.format( + COLOR_CHAR + "7There is a newer version of Floodgate available!\n" + + COLOR_CHAR + "7You are " + COLOR_CHAR + "e%s " + COLOR_CHAR + "7builds behind.\n" + + COLOR_CHAR + "7Download the latest Floodgate version here: " + COLOR_CHAR + "b%s", + buildNumber - Constants.BUILD_NUMBER, + String.format(Constants.LATEST_DOWNLOAD_URL, implementationName) + )); + return; + } + if (buildNumber == Constants.BUILD_NUMBER) { + sender.sendMessage(COLOR_CHAR + "aYou're running the latest version of Floodgate!"); + return; + } + sender.sendMessage(COLOR_CHAR + "cCannot check version for custom Floodgate versions!"); + }); + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/command/util/Permission.java b/core/src/main/java/org/geysermc/floodgate/command/util/Permission.java index 90f897fb..87c2ec52 100644 --- a/core/src/main/java/org/geysermc/floodgate/command/util/Permission.java +++ b/core/src/main/java/org/geysermc/floodgate/command/util/Permission.java @@ -31,6 +31,7 @@ public enum Permission { COMMAND_MAIN("floodgate.command.floodgate", TRUE), COMMAND_MAIN_FIREWALL(COMMAND_MAIN, "firewall", OP), + COMMAND_MAIN_VERSION(COMMAND_MAIN, "version", OP), COMMAND_LINK("floodgate.command.linkaccount", TRUE), COMMAND_UNLINK("floodgate.command.unlinkaccount", TRUE), COMMAND_WHITELIST("floodgate.command.fwhitelist", OP), diff --git a/core/src/main/java/org/geysermc/floodgate/config/loader/ConfigLoader.java b/core/src/main/java/org/geysermc/floodgate/config/ConfigLoader.java similarity index 81% rename from core/src/main/java/org/geysermc/floodgate/config/loader/ConfigLoader.java rename to core/src/main/java/org/geysermc/floodgate/config/ConfigLoader.java index b261e63f..739e1767 100644 --- a/core/src/main/java/org/geysermc/floodgate/config/loader/ConfigLoader.java +++ b/core/src/main/java/org/geysermc/floodgate/config/ConfigLoader.java @@ -23,7 +23,7 @@ * @link https://github.com/GeyserMC/Floodgate */ -package org.geysermc.floodgate.config.loader; +package org.geysermc.floodgate.config; import java.nio.file.Files; import java.nio.file.Path; @@ -33,24 +33,20 @@ import lombok.RequiredArgsConstructor; import org.geysermc.configutils.ConfigUtilities; import org.geysermc.configutils.file.codec.PathFileCodec; -import org.geysermc.configutils.file.template.ResourceTemplateReader; +import org.geysermc.configutils.file.template.TemplateReader; import org.geysermc.configutils.updater.change.Changes; -import org.geysermc.floodgate.api.logger.FloodgateLogger; -import org.geysermc.floodgate.config.FloodgateConfig; -import org.geysermc.floodgate.config.ProxyFloodgateConfig; import org.geysermc.floodgate.crypto.FloodgateCipher; import org.geysermc.floodgate.crypto.KeyProducer; @Getter @RequiredArgsConstructor public final class ConfigLoader { - private final Path dataFolder; + private final Path dataDirectory; private final Class configClass; private final KeyProducer keyProducer; private final FloodgateCipher cipher; - - private final FloodgateLogger logger; + private final TemplateReader reader; @SuppressWarnings("unchecked") public T load() { @@ -67,9 +63,9 @@ public T load() { ConfigUtilities utilities = ConfigUtilities.builder() - .fileCodec(PathFileCodec.of(dataFolder)) + .fileCodec(PathFileCodec.of(dataDirectory)) .configFile("config.yml") - .templateReader(ResourceTemplateReader.of(getClass())) + .templateReader(reader) .template(templateFile) .changes(Changes.builder() .version(1, Changes.versionBuilder() @@ -102,21 +98,16 @@ public void generateKey(Path keyPath) { String decrypted = cipher.decryptToString(encrypted); if (!test.equals(decrypted)) { - logger.error("Whoops, we tested the generated Floodgate keys but " + - "the decrypted test message doesn't match the original.\n" + + throw new RuntimeException("Failed to decrypt test message.\n" + "Original message: " + test + "." + "Decrypted message: " + decrypted + ".\n" + "The encrypted message itself: " + new String(encrypted) ); - throw new RuntimeException( - "Tested the generated public and private key but, " + - "the decrypted message doesn't match the original!" - ); } Files.write(keyPath, key.getEncoded()); } catch (Exception exception) { - logger.error("Error while creating key", exception); + throw new RuntimeException("Error while creating key", exception); } } } diff --git a/core/src/main/java/org/geysermc/floodgate/config/FloodgateConfig.java b/core/src/main/java/org/geysermc/floodgate/config/FloodgateConfig.java index 9fb105d8..4a87b0d4 100644 --- a/core/src/main/java/org/geysermc/floodgate/config/FloodgateConfig.java +++ b/core/src/main/java/org/geysermc/floodgate/config/FloodgateConfig.java @@ -29,11 +29,9 @@ import java.nio.file.Files; import java.nio.file.Path; import java.security.Key; -import java.util.UUID; import lombok.Getter; import org.geysermc.configutils.loader.callback.CallbackResult; import org.geysermc.configutils.loader.callback.GenericPostInitializeCallback; -import org.geysermc.floodgate.config.loader.ConfigLoader; /** * The global Floodgate configuration file used in every platform. Some platforms have their own @@ -42,7 +40,7 @@ @Getter public class FloodgateConfig implements GenericPostInitializeCallback { private String keyFileName; - private String usernamePrefix; + private String usernamePrefix = ""; private boolean replaceSpaces; private String defaultLocale; @@ -54,7 +52,9 @@ public class FloodgateConfig implements GenericPostInitializeCallback= 16) { + usernamePrefix = "."; + } + return CallbackResult.ok(); } @@ -99,6 +107,6 @@ public static class PlayerLinkConfig { @Getter public static class MetricsConfig { private boolean enabled; - private UUID uuid; + private String uuid; } } diff --git a/core/src/main/java/org/geysermc/floodgate/event/EventBus.java b/core/src/main/java/org/geysermc/floodgate/event/EventBus.java new file mode 100644 index 00000000..c8e6f9bc --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/event/EventBus.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.event; + +import com.google.inject.Singleton; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.event.PostOrder; +import org.geysermc.event.bus.impl.EventBusImpl; +import org.geysermc.event.subscribe.Subscribe; +import org.geysermc.event.subscribe.Subscriber; +import org.geysermc.floodgate.api.event.FloodgateEventBus; +import org.geysermc.floodgate.api.event.FloodgateSubscriber; + +@Singleton +@SuppressWarnings("unchecked") +public final class EventBus extends EventBusImpl> + implements FloodgateEventBus { + @Override + protected > B makeSubscription( + @NonNull Class eventClass, + @NonNull Subscribe subscribe, + @NonNull H listener, + @NonNull BiConsumer handler + ) { + return (B) new EventSubscriber<>( + eventClass, subscribe.postOrder(), subscribe.ignoreCancelled(), listener, handler + ); + } + + @Override + protected > B makeSubscription( + @NonNull Class eventClass, + @NonNull Consumer handler, + @NonNull PostOrder postOrder + ) { + return (B) new EventSubscriber<>(eventClass, handler, postOrder); + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/event/EventSubscriber.java b/core/src/main/java/org/geysermc/floodgate/event/EventSubscriber.java new file mode 100644 index 00000000..f01c209b --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/event/EventSubscriber.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.event; + +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.event.PostOrder; +import org.geysermc.event.subscribe.impl.SubscriberImpl; +import org.geysermc.floodgate.api.event.FloodgateSubscriber; + +public final class EventSubscriber extends SubscriberImpl implements FloodgateSubscriber { + EventSubscriber( + @NonNull Class eventClass, + @NonNull Consumer handler, + @NonNull PostOrder postOrder + ) { + super(eventClass, handler, postOrder); + } + + EventSubscriber( + Class eventClass, + PostOrder postOrder, + boolean ignoreCancelled, + H handlerInstance, + BiConsumer handler + ) { + super(eventClass, postOrder, ignoreCancelled, handlerInstance, handler); + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/event/lifecycle/PostEnableEvent.java b/core/src/main/java/org/geysermc/floodgate/event/lifecycle/PostEnableEvent.java new file mode 100644 index 00000000..f274ff16 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/event/lifecycle/PostEnableEvent.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.event.lifecycle; + +public class PostEnableEvent { +} diff --git a/core/src/main/java/org/geysermc/floodgate/event/lifecycle/ShutdownEvent.java b/core/src/main/java/org/geysermc/floodgate/event/lifecycle/ShutdownEvent.java new file mode 100644 index 00000000..30e782c5 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/event/lifecycle/ShutdownEvent.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.event.lifecycle; + +public class ShutdownEvent { +} diff --git a/core/src/main/java/org/geysermc/floodgate/event/skin/SkinApplyEventImpl.java b/core/src/main/java/org/geysermc/floodgate/event/skin/SkinApplyEventImpl.java new file mode 100644 index 00000000..52148986 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/event/skin/SkinApplyEventImpl.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.event.skin; + +import java.util.Objects; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.event.util.AbstractCancellable; +import org.geysermc.floodgate.api.event.skin.SkinApplyEvent; +import org.geysermc.floodgate.api.player.FloodgatePlayer; + +public class SkinApplyEventImpl extends AbstractCancellable implements SkinApplyEvent { + private final FloodgatePlayer player; + private final SkinData currentSkin; + private SkinData newSkin; + + public SkinApplyEventImpl( + @NonNull FloodgatePlayer player, + @Nullable SkinData currentSkin, + @NonNull SkinData newSkin + ) { + this.player = Objects.requireNonNull(player); + this.currentSkin = currentSkin; + this.newSkin = Objects.requireNonNull(newSkin); + } + + @Override + public @NonNull FloodgatePlayer player() { + return player; + } + + public @Nullable SkinData currentSkin() { + return currentSkin; + } + + public @NonNull SkinData newSkin() { + return newSkin; + } + + public SkinApplyEventImpl newSkin(@NonNull SkinData skinData) { + this.newSkin = Objects.requireNonNull(skinData); + return this; + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/event/util/ListenerAnnotationMatcher.java b/core/src/main/java/org/geysermc/floodgate/event/util/ListenerAnnotationMatcher.java new file mode 100644 index 00000000..e0881e23 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/event/util/ListenerAnnotationMatcher.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.event.util; + +import com.google.inject.TypeLiteral; +import com.google.inject.matcher.AbstractMatcher; +import org.geysermc.event.Listener; + +public class ListenerAnnotationMatcher extends AbstractMatcher> { + @Override + public boolean matches(TypeLiteral typeLiteral) { + Class rawType = typeLiteral.getRawType(); + return rawType.isAnnotationPresent(Listener.class); + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/inject/CommonPlatformInjector.java b/core/src/main/java/org/geysermc/floodgate/inject/CommonPlatformInjector.java index 48449372..1547a2be 100644 --- a/core/src/main/java/org/geysermc/floodgate/inject/CommonPlatformInjector.java +++ b/core/src/main/java/org/geysermc/floodgate/inject/CommonPlatformInjector.java @@ -26,18 +26,17 @@ package org.geysermc.floodgate.inject; import io.netty.channel.Channel; +import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Set; -import lombok.AccessLevel; -import lombok.Getter; +import java.util.WeakHashMap; import org.geysermc.floodgate.api.inject.InjectorAddon; import org.geysermc.floodgate.api.inject.PlatformInjector; public abstract class CommonPlatformInjector implements PlatformInjector { - @Getter(AccessLevel.PROTECTED) - private final Set injectedClients = new HashSet<>(); + private final Set injectedClients = + Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>())); private final Map, InjectorAddon> addons = new HashMap<>(); @@ -49,6 +48,10 @@ public boolean removeInjectedClient(Channel channel) { return injectedClients.remove(channel); } + public Set injectedClients() { + return injectedClients; + } + @Override public boolean addAddon(InjectorAddon addon) { return addons.putIfAbsent(addon.getClass(), addon) == null; @@ -77,7 +80,7 @@ public void injectAddonsCall(Channel channel, boolean proxyToServer) { } /** - * Method to loop throguh all the addons and call {@link InjectorAddon#onChannelClosed(Channel)} + * Method to loop through all the addons and call {@link InjectorAddon#onChannelClosed(Channel)} * if {@link InjectorAddon#shouldInject()} * * @param channel the channel that was injected diff --git a/core/src/main/java/org/geysermc/floodgate/link/CommonPlayerLink.java b/core/src/main/java/org/geysermc/floodgate/link/CommonPlayerLink.java index 0973b8d9..9324a097 100644 --- a/core/src/main/java/org/geysermc/floodgate/link/CommonPlayerLink.java +++ b/core/src/main/java/org/geysermc/floodgate/link/CommonPlayerLink.java @@ -34,6 +34,8 @@ import java.util.concurrent.Executors; import lombok.AccessLevel; import lombok.Getter; +import org.geysermc.event.Listener; +import org.geysermc.event.subscribe.Subscribe; import org.geysermc.floodgate.api.FloodgateApi; import org.geysermc.floodgate.api.link.LinkRequest; import org.geysermc.floodgate.api.link.PlayerLink; @@ -41,11 +43,13 @@ import org.geysermc.floodgate.config.FloodgateConfig; import org.geysermc.floodgate.database.config.DatabaseConfig; import org.geysermc.floodgate.database.config.DatabaseConfigLoader; +import org.geysermc.floodgate.event.lifecycle.ShutdownEvent; import org.geysermc.floodgate.util.InjectorHolder; +@Listener public abstract class CommonPlayerLink implements PlayerLink { @Getter(AccessLevel.PROTECTED) - private final ExecutorService executorService = Executors.newFixedThreadPool(11); + private final ExecutorService executorService = Executors.newCachedThreadPool(); @Getter private boolean enabled; @Getter private boolean allowLinking; @@ -102,4 +106,9 @@ public String getName() { public void stop() { executorService.shutdown(); } + + @Subscribe + public void onShutdown(ShutdownEvent ignored) { + stop(); + } } diff --git a/core/src/main/java/org/geysermc/floodgate/link/GlobalPlayerLinking.java b/core/src/main/java/org/geysermc/floodgate/link/GlobalPlayerLinking.java index 1ef1227e..bc57c61b 100644 --- a/core/src/main/java/org/geysermc/floodgate/link/GlobalPlayerLinking.java +++ b/core/src/main/java/org/geysermc/floodgate/link/GlobalPlayerLinking.java @@ -29,19 +29,23 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.google.inject.Inject; import java.util.UUID; import java.util.concurrent.CompletableFuture; import lombok.Getter; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.floodgate.api.link.LinkRequestResult; import org.geysermc.floodgate.api.link.PlayerLink; -import org.geysermc.floodgate.util.HttpUtils; -import org.geysermc.floodgate.util.HttpUtils.DefaultHttpResponse; +import org.geysermc.floodgate.util.HttpClient; +import org.geysermc.floodgate.util.HttpClient.DefaultHttpResponse; import org.geysermc.floodgate.util.LinkedPlayer; import org.geysermc.floodgate.util.Utils; @Getter public class GlobalPlayerLinking extends CommonPlayerLink { + @Inject + private HttpClient httpClient; + private PlayerLink databaseImpl; public void setDatabaseImpl(PlayerLink databaseImpl) { @@ -94,7 +98,7 @@ private CompletableFuture getLinkedPlayer0(@NonNull UUID bedrockId return CompletableFuture.supplyAsync( () -> { DefaultHttpResponse response = - HttpUtils.get(GET_BEDROCK_LINK + bedrockId.getLeastSignificantBits()); + httpClient.get(GET_BEDROCK_LINK + bedrockId.getLeastSignificantBits()); // either the global api is down or it failed to return link if (!response.isCodeOk()) { @@ -144,7 +148,7 @@ private CompletableFuture isLinkedPlayer0(@NonNull UUID bedrockId) { return CompletableFuture.supplyAsync( () -> { DefaultHttpResponse response = - HttpUtils.get(GET_BEDROCK_LINK + bedrockId.getLeastSignificantBits()); + httpClient.get(GET_BEDROCK_LINK + bedrockId.getLeastSignificantBits()); if (!response.isCodeOk()) { getLogger().error( diff --git a/core/src/main/java/org/geysermc/floodgate/link/PlayerLinkLoader.java b/core/src/main/java/org/geysermc/floodgate/link/PlayerLinkHolder.java similarity index 73% rename from core/src/main/java/org/geysermc/floodgate/link/PlayerLinkLoader.java rename to core/src/main/java/org/geysermc/floodgate/link/PlayerLinkHolder.java index b8febe29..4b140c2f 100644 --- a/core/src/main/java/org/geysermc/floodgate/link/PlayerLinkLoader.java +++ b/core/src/main/java/org/geysermc/floodgate/link/PlayerLinkHolder.java @@ -32,6 +32,7 @@ import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Singleton; +import com.google.inject.name.Named; import com.google.inject.name.Names; import java.io.IOException; import java.io.InputStream; @@ -42,18 +43,23 @@ import java.nio.file.Path; import java.util.List; import java.util.stream.Collectors; -import javax.annotation.Nonnull; -import javax.inject.Named; +import java.util.stream.Stream; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.event.Listener; +import org.geysermc.event.subscribe.Subscribe; import org.geysermc.floodgate.api.link.PlayerLink; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.config.FloodgateConfig; +import org.geysermc.floodgate.config.FloodgateConfig.PlayerLinkConfig; +import org.geysermc.floodgate.event.lifecycle.ShutdownEvent; import org.geysermc.floodgate.util.Constants; import org.geysermc.floodgate.util.InjectorHolder; import org.geysermc.floodgate.util.Utils; +@Listener @Singleton @SuppressWarnings("unchecked") -public final class PlayerLinkLoader { +public final class PlayerLinkHolder { @Inject private Injector injector; @Inject private FloodgateConfig config; @Inject private FloodgateLogger logger; @@ -62,30 +68,41 @@ public final class PlayerLinkLoader { @Named("dataDirectory") private Path dataDirectory; - @Nonnull - public PlayerLink load() { + private URLClassLoader classLoader; + private PlayerLink instance; + + public @NonNull PlayerLink load() { + if (instance != null) { + return instance; + } + instance = load0(); + return instance; + } + + private @NonNull PlayerLink load0() { if (config == null) { throw new IllegalStateException("Config cannot be null!"); } - FloodgateConfig.PlayerLinkConfig lConfig = config.getPlayerLink(); - if (!lConfig.isEnabled()) { + PlayerLinkConfig linkConfig = config.getPlayerLink(); + if (!linkConfig.isEnabled()) { return new DisabledPlayerLink(); } List files; - try { - files = Files.list(dataDirectory) - .filter(path -> Files.isRegularFile(path) && path.toString().endsWith("jar")) + try (Stream list = Files.list(dataDirectory)) { + files = list + .filter(path -> Files.isRegularFile(path) && path.toString().endsWith(".jar")) .collect(Collectors.toList()); } catch (IOException exception) { logger.error("Failed to list possible database implementations", exception); return new DisabledPlayerLink(); } - // we can skip the rest if global linking is enabled and no database implementations has been - // found, or when global linking is enabled and own player linking is disabled. - if (lConfig.isEnableGlobalLinking() && (files.isEmpty() || !lConfig.isEnableOwnLinking())) { + // we can skip the rest if global linking is enabled and no database implementations has + // been found, or when global linking is enabled and own player linking is disabled. + if (linkConfig.isEnableGlobalLinking() && + (files.isEmpty() || !linkConfig.isEnableOwnLinking())) { return injector.getInstance(GlobalPlayerLinking.class); } @@ -100,7 +117,7 @@ public PlayerLink load() { // We only want to load one database implementation if (files.size() > 1) { boolean found = false; - databaseName = lConfig.getType(); + databaseName = linkConfig.getType(); String expectedName = "floodgate-" + databaseName + "-database.jar"; for (Path path : files) { @@ -111,14 +128,18 @@ public PlayerLink load() { } if (!found) { - logger.error("Failed to find an implementation for type: {}", lConfig.getType()); + logger.error( + "Failed to find an implementation for type: {}", linkConfig.getType() + ); return new DisabledPlayerLink(); } } else { String name = implementationPath.getFileName().toString(); if (!Utils.isValidDatabaseName(name)) { - logger.error("Found database {} but the name doesn't match {}", - name, Constants.DATABASE_NAME_FORMAT); + logger.error( + "Found database {} but the name doesn't match {}", + name, Constants.DATABASE_NAME_FORMAT + ); return new DisabledPlayerLink(); } int firstSplit = name.indexOf('-') + 1; @@ -133,24 +154,22 @@ public PlayerLink load() { // we don't have a way to close this properly since we have no stop method and we have // to be able to load classes on the fly, but that doesn't matter anyway since Floodgate // doesn't support reloading - URLClassLoader classLoader = new URLClassLoader( + classLoader = new URLClassLoader( new URL[]{pluginUrl}, - PlayerLinkLoader.class.getClassLoader() + PlayerLinkHolder.class.getClassLoader() ); String mainClassName; - JsonObject linkConfig; - - try (InputStream linkConfigStream = - classLoader.getResourceAsStream("init.json")) { + JsonObject dbInitConfig; + try (InputStream linkConfigStream = classLoader.getResourceAsStream("init.json")) { requireNonNull(linkConfigStream, "Implementation should have an init file"); - linkConfig = new Gson().fromJson( + dbInitConfig = new Gson().fromJson( new InputStreamReader(linkConfigStream), JsonObject.class ); - mainClassName = linkConfig.get("mainClass").getAsString(); + mainClassName = dbInitConfig.get("mainClass").getAsString(); } Class mainClass = @@ -167,7 +186,7 @@ public PlayerLink load() { Names.named("databaseClassLoader")).toInstance(classLoader); binder.bind(JsonObject.class) .annotatedWith(Names.named("databaseInitData")) - .toInstance(linkConfig); + .toInstance(dbInitConfig); binder.bind(InjectorHolder.class) .toInstance(injectorHolder); }); @@ -176,7 +195,7 @@ public PlayerLink load() { PlayerLink instance = linkInjector.getInstance(mainClass); // we use our own internal PlayerLinking when global linking is enabled - if (lConfig.isEnableGlobalLinking()) { + if (linkConfig.isEnableGlobalLinking()) { GlobalPlayerLinking linking = linkInjector.getInstance(GlobalPlayerLinking.class); linking.setDatabaseImpl(instance); linking.load(); @@ -186,8 +205,10 @@ public PlayerLink load() { return instance; } } catch (ClassCastException exception) { - logger.error("The database implementation ({}) doesn't extend the PlayerLink class!", - implementationPath.getFileName().toString(), exception); + logger.error( + "The database implementation ({}) doesn't extend the PlayerLink class!", + implementationPath.getFileName().toString(), exception + ); return new DisabledPlayerLink(); } catch (Exception exception) { if (init) { @@ -198,4 +219,12 @@ public PlayerLink load() { return new DisabledPlayerLink(); } } + + @Subscribe + public void onShutdown(ShutdownEvent ignored) throws Exception { + instance.stop(); + if (classLoader != null) { + classLoader.close(); + } + } } diff --git a/core/src/main/java/org/geysermc/floodgate/logger/JavaUtilFloodgateLogger.java b/core/src/main/java/org/geysermc/floodgate/logger/JavaUtilFloodgateLogger.java index f3151ef1..97f1af3f 100644 --- a/core/src/main/java/org/geysermc/floodgate/logger/JavaUtilFloodgateLogger.java +++ b/core/src/main/java/org/geysermc/floodgate/logger/JavaUtilFloodgateLogger.java @@ -27,17 +27,29 @@ import static org.geysermc.floodgate.util.MessageFormatter.format; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; import java.util.logging.Level; import java.util.logging.Logger; -import lombok.RequiredArgsConstructor; import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.config.FloodgateConfig; import org.geysermc.floodgate.util.LanguageManager; -@RequiredArgsConstructor +@Singleton public final class JavaUtilFloodgateLogger implements FloodgateLogger { - private final Logger logger; - private final LanguageManager languageManager; - private Level originLevel; + @Inject + @Named("logger") + private Logger logger; + private LanguageManager languageManager; + + @Inject + private void init(LanguageManager languageManager, FloodgateConfig config) { + this.languageManager = languageManager; + if (config.isDebug()) { + logger.setLevel(Level.ALL); + } + } @Override public void error(String message, Object... args) { @@ -74,19 +86,6 @@ public void trace(String message, Object... args) { logger.finer(format(message, args)); } - @Override - public void enableDebug() { - originLevel = logger.getLevel(); - logger.setLevel(Level.ALL); - } - - @Override - public void disableDebug() { - if (originLevel != null) { - logger.setLevel(originLevel); - } - } - @Override public boolean isDebug() { return logger.getLevel() == Level.ALL; diff --git a/core/src/main/java/org/geysermc/floodgate/skin/SkinData.java b/core/src/main/java/org/geysermc/floodgate/module/AutoBindModule.java similarity index 66% rename from core/src/main/java/org/geysermc/floodgate/skin/SkinData.java rename to core/src/main/java/org/geysermc/floodgate/module/AutoBindModule.java index 84162710..1c2933de 100644 --- a/core/src/main/java/org/geysermc/floodgate/skin/SkinData.java +++ b/core/src/main/java/org/geysermc/floodgate/module/AutoBindModule.java @@ -23,25 +23,17 @@ * @link https://github.com/GeyserMC/Floodgate */ -package org.geysermc.floodgate.skin; +package org.geysermc.floodgate.module; -import com.google.gson.JsonObject; -import lombok.Getter; -import lombok.RequiredArgsConstructor; +import com.google.inject.AbstractModule; +import org.geysermc.floodgate.util.AutoBind; +import org.geysermc.floodgate.util.Utils; -@Getter -@RequiredArgsConstructor -public class SkinData { - private final String value; - private final String signature; - - public static SkinData from(JsonObject data) { - if (data.has("signature") && !data.get("signature").isJsonNull()) { - return new SkinData( - data.get("value").getAsString(), - data.get("signature").getAsString() - ); +public class AutoBindModule extends AbstractModule { + @Override + protected void configure() { + for (Class clazz : Utils.getGeneratedClassesForAnnotation(AutoBind.class)) { + bind(clazz).asEagerSingleton(); } - return new SkinData(data.get("value").getAsString(), null); } } diff --git a/core/src/main/java/org/geysermc/floodgate/module/CommonModule.java b/core/src/main/java/org/geysermc/floodgate/module/CommonModule.java index 36e8140a..cddb93f7 100644 --- a/core/src/main/java/org/geysermc/floodgate/module/CommonModule.java +++ b/core/src/main/java/org/geysermc/floodgate/module/CommonModule.java @@ -28,49 +28,113 @@ import com.google.inject.AbstractModule; import com.google.inject.Provides; import com.google.inject.Singleton; +import com.google.inject.TypeLiteral; import com.google.inject.name.Named; +import com.google.inject.name.Names; +import com.google.inject.spi.InjectionListener; +import com.google.inject.spi.TypeEncounter; +import com.google.inject.spi.TypeListener; import io.netty.util.AttributeKey; import java.nio.file.Path; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; import lombok.RequiredArgsConstructor; +import org.geysermc.configutils.file.template.ResourceTemplateReader; +import org.geysermc.configutils.file.template.TemplateReader; +import org.geysermc.event.PostOrder; import org.geysermc.floodgate.addon.data.HandshakeHandlersImpl; import org.geysermc.floodgate.api.FloodgateApi; import org.geysermc.floodgate.api.SimpleFloodgateApi; +import org.geysermc.floodgate.api.event.FloodgateEventBus; import org.geysermc.floodgate.api.handshake.HandshakeHandlers; import org.geysermc.floodgate.api.inject.PlatformInjector; +import org.geysermc.floodgate.api.link.PlayerLink; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.packet.PacketHandlers; import org.geysermc.floodgate.api.player.FloodgatePlayer; +import org.geysermc.floodgate.config.ConfigLoader; import org.geysermc.floodgate.config.FloodgateConfig; -import org.geysermc.floodgate.config.FloodgateConfigHolder; -import org.geysermc.floodgate.config.loader.ConfigLoader; import org.geysermc.floodgate.crypto.AesCipher; import org.geysermc.floodgate.crypto.AesKeyProducer; import org.geysermc.floodgate.crypto.Base64Topping; import org.geysermc.floodgate.crypto.FloodgateCipher; import org.geysermc.floodgate.crypto.KeyProducer; +import org.geysermc.floodgate.event.EventBus; +import org.geysermc.floodgate.event.lifecycle.ShutdownEvent; +import org.geysermc.floodgate.event.util.ListenerAnnotationMatcher; import org.geysermc.floodgate.inject.CommonPlatformInjector; -import org.geysermc.floodgate.news.NewsChecker; +import org.geysermc.floodgate.link.PlayerLinkHolder; import org.geysermc.floodgate.packet.PacketHandlersImpl; -import org.geysermc.floodgate.platform.command.CommandUtil; import org.geysermc.floodgate.player.FloodgateHandshakeHandler; import org.geysermc.floodgate.pluginmessage.PluginMessageManager; -import org.geysermc.floodgate.skin.SkinApplier; import org.geysermc.floodgate.skin.SkinUploadManager; import org.geysermc.floodgate.util.Constants; +import org.geysermc.floodgate.util.HttpClient; import org.geysermc.floodgate.util.LanguageManager; @RequiredArgsConstructor public class CommonModule extends AbstractModule { + private final EventBus eventBus = new EventBus(); private final Path dataDirectory; + private final TemplateReader reader; + + public CommonModule(Path dataDirectory) { + this.dataDirectory = dataDirectory; + this.reader = ResourceTemplateReader.of(ConfigLoader.class); + } @Override protected void configure() { + bind(EventBus.class).toInstance(eventBus); + bind(FloodgateEventBus.class).to(EventBus.class); + // register every class that has the Listener annotation + bindListener(new ListenerAnnotationMatcher(), new TypeListener() { + @Override + public void hear(TypeLiteral type, TypeEncounter encounter) { + encounter.register((InjectionListener) eventBus::register); + } + }); + + ExecutorService commonPool = Executors.newCachedThreadPool(); + ScheduledExecutorService commonScheduledPool = Executors.newSingleThreadScheduledExecutor(); + + eventBus.subscribe(ShutdownEvent.class, ignored -> { + commonPool.shutdown(); + commonScheduledPool.shutdown(); + }, PostOrder.LAST); + + bind(ExecutorService.class) + .annotatedWith(Names.named("commonPool")) + .toInstance(commonPool); + bind(ScheduledExecutorService.class) + .annotatedWith(Names.named("commonScheduledPool")) + .toInstance(commonScheduledPool); + + bind(HttpClient.class).in(Singleton.class); + bind(FloodgateApi.class).to(SimpleFloodgateApi.class); bind(PlatformInjector.class).to(CommonPlatformInjector.class); + bind(HandshakeHandlers.class).to(HandshakeHandlersImpl.class); + bind(HandshakeHandlersImpl.class).in(Singleton.class); bind(PacketHandlers.class).to(PacketHandlersImpl.class); bind(PacketHandlersImpl.class).asEagerSingleton(); + + install(new AutoBindModule()); + } + + @Provides + @Singleton + public FloodgateConfig floodgateConfig(ConfigLoader configLoader) { + return configLoader.load(); + } + + @Provides + @Singleton + public PlayerLink playerLink(PlayerLinkHolder linkLoader) { + return linkLoader.load(); } @Provides @@ -92,34 +156,13 @@ public Path dataDirectory() { return dataDirectory; } - @Provides - @Singleton - public FloodgateConfigHolder configHolder() { - return new FloodgateConfigHolder(); - } - @Provides @Singleton public ConfigLoader configLoader( @Named("configClass") Class configClass, KeyProducer producer, - FloodgateCipher cipher, - FloodgateLogger logger) { - return new ConfigLoader(dataDirectory, configClass, producer, cipher, logger); - } - - @Provides - @Singleton - public LanguageManager languageLoader( - FloodgateConfigHolder configHolder, - FloodgateLogger logger) { - return new LanguageManager(configHolder, logger); - } - - @Provides - @Singleton - public HandshakeHandlersImpl handshakeHandlers() { - return new HandshakeHandlersImpl(); + FloodgateCipher cipher) { + return new ConfigLoader(dataDirectory, configClass, producer, cipher, reader); } @Provides @@ -128,13 +171,14 @@ public FloodgateHandshakeHandler handshakeHandler( HandshakeHandlersImpl handshakeHandlers, SimpleFloodgateApi api, FloodgateCipher cipher, - FloodgateConfigHolder configHolder, + FloodgateConfig config, SkinUploadManager skinUploadManager, @Named("playerAttribute") AttributeKey playerAttribute, - FloodgateLogger logger) { + FloodgateLogger logger, + LanguageManager languageManager) { - return new FloodgateHandshakeHandler(handshakeHandlers, api, cipher, configHolder, - skinUploadManager, playerAttribute, logger); + return new FloodgateHandshakeHandler(handshakeHandlers, api, cipher, config, + skinUploadManager, playerAttribute, logger, languageManager); } @Provides @@ -145,17 +189,16 @@ public PluginMessageManager pluginMessageManager() { @Provides @Singleton - public SkinUploadManager skinUploadManager( - FloodgateApi api, - SkinApplier skinApplier, - FloodgateLogger logger) { - return new SkinUploadManager(api, skinApplier, logger); + @Named("gitBranch") + public String gitBranch() { + return Constants.GIT_BRANCH; } @Provides @Singleton - public NewsChecker newsChecker(CommandUtil commandUtil, FloodgateLogger logger) { - return new NewsChecker(commandUtil, logger, Constants.GIT_BRANCH, Constants.BUILD_NUMBER); + @Named("buildNumber") + public int buildNumber() { + return Constants.BUILD_NUMBER; } @Provides diff --git a/core/src/main/java/org/geysermc/floodgate/module/ProxyCommonModule.java b/core/src/main/java/org/geysermc/floodgate/module/ProxyCommonModule.java index bae8cf22..77d52d2c 100644 --- a/core/src/main/java/org/geysermc/floodgate/module/ProxyCommonModule.java +++ b/core/src/main/java/org/geysermc/floodgate/module/ProxyCommonModule.java @@ -31,12 +31,8 @@ import java.nio.file.Path; import org.geysermc.floodgate.api.ProxyFloodgateApi; import org.geysermc.floodgate.api.SimpleFloodgateApi; -import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.config.FloodgateConfig; -import org.geysermc.floodgate.config.FloodgateConfigHolder; import org.geysermc.floodgate.config.ProxyFloodgateConfig; -import org.geysermc.floodgate.crypto.FloodgateCipher; -import org.geysermc.floodgate.pluginmessage.PluginMessageManager; public final class ProxyCommonModule extends CommonModule { public ProxyCommonModule(Path dataDirectory) { @@ -46,23 +42,21 @@ public ProxyCommonModule(Path dataDirectory) { @Override protected void configure() { super.configure(); + bind(SimpleFloodgateApi.class).to(ProxyFloodgateApi.class); + bind(ProxyFloodgateApi.class).in(Singleton.class); } @Provides @Singleton - @Named("configClass") - public Class floodgateConfigClass() { - return ProxyFloodgateConfig.class; + public ProxyFloodgateConfig proxyFloodgateConfig(FloodgateConfig config) { + return (ProxyFloodgateConfig) config; } @Provides @Singleton - public ProxyFloodgateApi proxyFloodgateApi( - PluginMessageManager pluginMessageManager, - FloodgateConfigHolder configHolder, - FloodgateLogger logger, - FloodgateCipher cipher) { - return new ProxyFloodgateApi(pluginMessageManager, configHolder, logger, cipher); + @Named("configClass") + public Class floodgateConfigClass() { + return ProxyFloodgateConfig.class; } } diff --git a/core/src/main/java/org/geysermc/floodgate/module/ServerCommonModule.java b/core/src/main/java/org/geysermc/floodgate/module/ServerCommonModule.java index b1b4ab5a..9afb2253 100644 --- a/core/src/main/java/org/geysermc/floodgate/module/ServerCommonModule.java +++ b/core/src/main/java/org/geysermc/floodgate/module/ServerCommonModule.java @@ -29,30 +29,30 @@ import com.google.inject.Singleton; import com.google.inject.name.Named; import java.nio.file.Path; +import org.geysermc.configutils.file.template.TemplateReader; import org.geysermc.floodgate.api.SimpleFloodgateApi; -import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.config.FloodgateConfig; -import org.geysermc.floodgate.config.FloodgateConfigHolder; -import org.geysermc.floodgate.pluginmessage.PluginMessageManager; public final class ServerCommonModule extends CommonModule { public ServerCommonModule(Path dataDirectory) { super(dataDirectory); } + // Used in floodgate-fabric to provide it's own reader implementation + public ServerCommonModule(Path dataDirectory, TemplateReader reader) { + super(dataDirectory, reader); + } + + @Override + protected void configure() { + super.configure(); + bind(SimpleFloodgateApi.class).in(Singleton.class); + } + @Provides @Singleton @Named("configClass") public Class floodgateConfigClass() { return FloodgateConfig.class; } - - @Provides - @Singleton - public SimpleFloodgateApi floodgateApi( - PluginMessageManager pluginMessageManager, - FloodgateConfigHolder configHolder, - FloodgateLogger logger) { - return new SimpleFloodgateApi(pluginMessageManager, configHolder, logger); - } } diff --git a/core/src/main/java/org/geysermc/floodgate/news/NewsChecker.java b/core/src/main/java/org/geysermc/floodgate/news/NewsChecker.java index 901321c3..71f9d667 100644 --- a/core/src/main/java/org/geysermc/floodgate/news/NewsChecker.java +++ b/core/src/main/java/org/geysermc/floodgate/news/NewsChecker.java @@ -27,42 +27,50 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; +import com.google.inject.Inject; +import com.google.inject.name.Named; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.command.util.Permission; import org.geysermc.floodgate.news.data.AnnouncementData; import org.geysermc.floodgate.news.data.BuildSpecificData; import org.geysermc.floodgate.news.data.CheckAfterData; import org.geysermc.floodgate.platform.command.CommandUtil; +import org.geysermc.floodgate.util.AutoBind; import org.geysermc.floodgate.util.Constants; -import org.geysermc.floodgate.util.HttpUtils; -import org.geysermc.floodgate.util.HttpUtils.HttpResponse; -import org.geysermc.floodgate.command.util.Permission; +import org.geysermc.floodgate.util.HttpClient; +import org.geysermc.floodgate.util.HttpClient.HttpResponse; +@AutoBind public class NewsChecker { - private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); - private final CommandUtil commandUtil; - private final FloodgateLogger logger; - private final Map activeNewsItems = new HashMap<>(); - private final String branch; - private final int build; + @Inject + @Named("commonScheduledPool") + private ScheduledExecutorService executorService; + + @Inject + private CommandUtil commandUtil; + @Inject + private HttpClient httpClient; + @Inject + private FloodgateLogger logger; + + @Inject + @Named("gitBranch") + private String branch; + @Inject + @Named("buildNumber") + private int build; private boolean firstCheck; - public NewsChecker(CommandUtil commandUtil, FloodgateLogger logger, String branch, int build) { - this.commandUtil = commandUtil; - this.logger = logger; - this.branch = branch; - this.build = build; - } - + @Inject public void start() { executorService.scheduleWithFixedDelay(this::checkNews, 0, 30, TimeUnit.MINUTES); } @@ -72,10 +80,10 @@ private void schedule(long delayMs) { } private void checkNews() { - HttpResponse response = - HttpUtils.getSilent( - Constants.NEWS_OVERVIEW_URL + Constants.NEWS_PROJECT_NAME, - JsonArray.class); + HttpResponse response = httpClient.getSilent( + Constants.NEWS_OVERVIEW_URL + Constants.PROJECT_NAME, + JsonArray.class + ); JsonArray array = response.getResponse(); @@ -165,7 +173,7 @@ public void addNews(NewsItem item) { switch (item.getType()) { case ANNOUNCEMENT: - if (!item.getDataAs(AnnouncementData.class).isAffected(Constants.NEWS_PROJECT_NAME)) { + if (!item.getDataAs(AnnouncementData.class).isAffected(Constants.PROJECT_NAME)) { return; } break; @@ -193,8 +201,4 @@ private void activateNews(NewsItem item) { handleNewsItem(null, item, action); } } - - public void shutdown() { - executorService.shutdown(); - } } diff --git a/core/src/main/java/org/geysermc/floodgate/platform/command/CommandUtil.java b/core/src/main/java/org/geysermc/floodgate/platform/command/CommandUtil.java index bf9129a1..17bf555b 100644 --- a/core/src/main/java/org/geysermc/floodgate/platform/command/CommandUtil.java +++ b/core/src/main/java/org/geysermc/floodgate/platform/command/CommandUtil.java @@ -25,8 +25,8 @@ package org.geysermc.floodgate.platform.command; -import static org.geysermc.floodgate.platform.util.PlatformUtils.PlayerType.ALL_PLAYERS; -import static org.geysermc.floodgate.platform.util.PlatformUtils.PlayerType.ONLY_BEDROCK; +import static org.geysermc.floodgate.platform.util.PlayerType.ALL_PLAYERS; +import static org.geysermc.floodgate.platform.util.PlayerType.ONLY_BEDROCK; import java.util.ArrayList; import java.util.Collection; @@ -38,7 +38,7 @@ import org.checkerframework.checker.nullness.qual.NonNull; import org.checkerframework.checker.nullness.qual.Nullable; import org.geysermc.floodgate.api.FloodgateApi; -import org.geysermc.floodgate.platform.util.PlatformUtils.PlayerType; +import org.geysermc.floodgate.platform.util.PlayerType; import org.geysermc.floodgate.player.UserAudience; import org.geysermc.floodgate.player.audience.ProfileAudience; import org.geysermc.floodgate.util.LanguageManager; @@ -112,11 +112,6 @@ public abstract class CommandUtil { return usernames; } - /** - * - * @param uuid - * @return - */ public abstract Object getPlayerByUuid(@NonNull UUID uuid); public Object getPlayerByUuid(@NonNull UUID uuid, PlayerType limitTo) { @@ -125,15 +120,15 @@ public Object getPlayerByUuid(@NonNull UUID uuid, PlayerType limitTo) { public abstract Object getPlayerByUsername(@NonNull String username); - public Object getPlayerByUsername(@NonNull String username, PlayerType limitTo) { - return applyPlayerTypeFilter(getPlayerByUsername(username), limitTo, username); + public Object getPlayerByUsername(@NonNull String username, PlayerType filter) { + return applyPlayerTypeFilter(getPlayerByUsername(username), filter, username); } protected Object applyPlayerTypeFilter(Object player, PlayerType filter, Object fallback) { if (filter == ALL_PLAYERS || player instanceof String || player instanceof UUID) { return player; } - return (filter == ONLY_BEDROCK) == api.isFloodgatePlayer(getUuidFromSource(player)) + return (filter == ONLY_BEDROCK) == api.isFloodgateId(getUuidFromSource(player)) ? player : fallback; } diff --git a/core/src/main/java/org/geysermc/floodgate/platform/command/FloodgateCommand.java b/core/src/main/java/org/geysermc/floodgate/platform/command/FloodgateCommand.java index f61c336e..80a115da 100644 --- a/core/src/main/java/org/geysermc/floodgate/platform/command/FloodgateCommand.java +++ b/core/src/main/java/org/geysermc/floodgate/platform/command/FloodgateCommand.java @@ -25,11 +25,10 @@ package org.geysermc.floodgate.platform.command; -import cloud.commandframework.Command; -import cloud.commandframework.CommandManager; -import cloud.commandframework.context.CommandContext; import org.geysermc.floodgate.config.FloodgateConfig; import org.geysermc.floodgate.player.UserAudience; +import org.incendo.cloud.Command; +import org.incendo.cloud.CommandManager; /** The base class for every Floodgate command. */ public interface FloodgateCommand { @@ -39,14 +38,7 @@ public interface FloodgateCommand { * @param commandManager the manager to create a command * @return the command to register */ - Command buildCommand(CommandManager commandManager); - - /** - * Called when the command created in {@link #buildCommand(CommandManager)} is executed. - * - * @param context the context of the executed command - */ - void execute(CommandContext context); + Command buildCommand(CommandManager commandManager); /** * Called by the CommandRegister to check if the command should be added given the config. diff --git a/core/src/main/java/org/geysermc/floodgate/platform/command/FloodgateSubCommand.java b/core/src/main/java/org/geysermc/floodgate/platform/command/FloodgateSubCommand.java new file mode 100644 index 00000000..fcf4b905 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/platform/command/FloodgateSubCommand.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.platform.command; + +import org.geysermc.floodgate.command.util.Permission; +import org.geysermc.floodgate.player.UserAudience; +import org.incendo.cloud.context.CommandContext; + +public abstract class FloodgateSubCommand { + public abstract String name(); + + public abstract String description(); + + public abstract Permission permission(); + + public abstract void execute(CommandContext context); +} diff --git a/spigot/src/main/java/org/geysermc/floodgate/SpigotPlatform.java b/core/src/main/java/org/geysermc/floodgate/platform/command/SubCommands.java similarity index 60% rename from spigot/src/main/java/org/geysermc/floodgate/SpigotPlatform.java rename to core/src/main/java/org/geysermc/floodgate/platform/command/SubCommands.java index c3ba5abf..75ece406 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/SpigotPlatform.java +++ b/core/src/main/java/org/geysermc/floodgate/platform/command/SubCommands.java @@ -23,33 +23,32 @@ * @link https://github.com/GeyserMC/Floodgate */ -package org.geysermc.floodgate; +package org.geysermc.floodgate.platform.command; import com.google.inject.Inject; import com.google.inject.Injector; -import com.google.inject.Module; -import org.bukkit.Bukkit; -import org.bukkit.plugin.java.JavaPlugin; -import org.geysermc.floodgate.api.FloodgateApi; -import org.geysermc.floodgate.api.inject.PlatformInjector; -import org.geysermc.floodgate.api.logger.FloodgateLogger; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; -public final class SpigotPlatform extends FloodgatePlatform { - @Inject private JavaPlugin plugin; +public abstract class SubCommands { + private final Set> toRegister = new HashSet<>(); + private Set subCommands; - @Inject - public SpigotPlatform(FloodgateApi api, PlatformInjector platformInjector, - FloodgateLogger logger, Injector injector) { - super(api, platformInjector, logger, injector); + public void defineSubCommand(Class subCommandClass) { + toRegister.add(subCommandClass); } - @Override - public boolean enable(Module... postInitializeModules) { - boolean success = super.enable(postInitializeModules); - if (!success) { - Bukkit.getPluginManager().disablePlugin(plugin); - return false; + @Inject + public void registerSubCommands(Injector injector) { + Set subCommandSet = new HashSet<>(); + for (Class subCommand : toRegister) { + subCommandSet.add(injector.getInstance(subCommand)); } - return true; + subCommands = Collections.unmodifiableSet(subCommandSet); + } + + protected Set subCommands() { + return subCommands; } } diff --git a/core/src/main/java/org/geysermc/floodgate/platform/util/PlatformUtils.java b/core/src/main/java/org/geysermc/floodgate/platform/util/PlatformUtils.java index 4807c017..d9901848 100644 --- a/core/src/main/java/org/geysermc/floodgate/platform/util/PlatformUtils.java +++ b/core/src/main/java/org/geysermc/floodgate/platform/util/PlatformUtils.java @@ -25,37 +25,26 @@ package org.geysermc.floodgate.platform.util; -import java.util.Collection; -import org.geysermc.floodgate.platform.command.CommandUtil; -import org.geysermc.floodgate.platform.command.TranslatableMessage; +import lombok.RequiredArgsConstructor; -public interface PlatformUtils { +@RequiredArgsConstructor +public abstract class PlatformUtils { /** - * Send a message to the specified player, no matter what platform Floodgate is running on. - * - * @param player the player to send the message to - * @param message the command message - * @param locale the locale of the player - * @param args the arguments + * Returns the authentication type used on the platform */ - void sendMessage(Object player, String locale, TranslatableMessage message, Object... args); + public abstract AuthType authType(); /** - * Same as {@link CommandUtil#sendMessage(Object, String, TranslatableMessage, Object...)} except it - * kicks the player. - * - * @param player the player to send the message to - * @param message the command message - * @param locale the locale of the player - * @param args the arguments + * Returns the Minecraft version the server is based on (or the most recent supported version + * for proxy platforms) */ - void kickPlayer(Object player, String locale, TranslatableMessage message, Object... args); + public abstract String minecraftVersion(); - Collection getOnlineUsernames(PlayerType limitTo); + public abstract String serverImplementationName(); - enum PlayerType { - ALL_PLAYERS, - ONLY_BEDROCK, - ONLY_JAVA + public enum AuthType { + ONLINE, + PROXIED, + OFFLINE } } diff --git a/core/src/main/java/org/geysermc/floodgate/platform/util/PlayerType.java b/core/src/main/java/org/geysermc/floodgate/platform/util/PlayerType.java new file mode 100644 index 00000000..f8c0c97c --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/platform/util/PlayerType.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.platform.util; + +public enum PlayerType { + ALL_PLAYERS, + ONLY_BEDROCK, + ONLY_JAVA +} diff --git a/core/src/main/java/org/geysermc/floodgate/player/FloodgateCommandPreprocessor.java b/core/src/main/java/org/geysermc/floodgate/player/FloodgateCommandPreprocessor.java index 104516de..54e68079 100644 --- a/core/src/main/java/org/geysermc/floodgate/player/FloodgateCommandPreprocessor.java +++ b/core/src/main/java/org/geysermc/floodgate/player/FloodgateCommandPreprocessor.java @@ -25,14 +25,14 @@ package org.geysermc.floodgate.player; -import cloud.commandframework.execution.preprocessor.CommandPreprocessingContext; -import cloud.commandframework.execution.preprocessor.CommandPreprocessor; import lombok.RequiredArgsConstructor; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.floodgate.platform.command.CommandUtil; +import org.incendo.cloud.execution.preprocessor.CommandPreprocessingContext; +import org.incendo.cloud.execution.preprocessor.CommandPreprocessor; /** - * Command preprocessor which decorated incoming {@link cloud.commandframework.context.CommandContext} + * Command preprocessor which decorated incoming {@link org.incendo.cloud.context.CommandContext} * with Floodgate specific objects * * @param Command sender type @@ -44,6 +44,6 @@ public final class FloodgateCommandPreprocessor implements CommandPreprocesso @Override public void accept(@NonNull CommandPreprocessingContext context) { - context.getCommandContext().store("CommandUtil", commandUtil); + context.commandContext().store("CommandUtil", commandUtil); } } diff --git a/core/src/main/java/org/geysermc/floodgate/player/FloodgateHandshakeHandler.java b/core/src/main/java/org/geysermc/floodgate/player/FloodgateHandshakeHandler.java index 950d999c..b84ff1ba 100644 --- a/core/src/main/java/org/geysermc/floodgate/player/FloodgateHandshakeHandler.java +++ b/core/src/main/java/org/geysermc/floodgate/player/FloodgateHandshakeHandler.java @@ -48,11 +48,13 @@ import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; import org.geysermc.floodgate.api.player.PropertyKey; -import org.geysermc.floodgate.config.FloodgateConfigHolder; +import org.geysermc.floodgate.config.FloodgateConfig; import org.geysermc.floodgate.crypto.FloodgateCipher; import org.geysermc.floodgate.skin.SkinUploadManager; import org.geysermc.floodgate.util.BedrockData; +import org.geysermc.floodgate.util.Constants; import org.geysermc.floodgate.util.InvalidFormatException; +import org.geysermc.floodgate.util.LanguageManager; import org.geysermc.floodgate.util.LinkedPlayer; import org.geysermc.floodgate.util.Utils; @@ -60,27 +62,30 @@ public final class FloodgateHandshakeHandler { private final HandshakeHandlersImpl handshakeHandlers; private final SimpleFloodgateApi api; private final FloodgateCipher cipher; - private final FloodgateConfigHolder configHolder; + private final FloodgateConfig config; private final SkinUploadManager skinUploadManager; private final AttributeKey playerAttribute; private final FloodgateLogger logger; + private final LanguageManager languageManager; public FloodgateHandshakeHandler( HandshakeHandlersImpl handshakeHandlers, SimpleFloodgateApi api, FloodgateCipher cipher, - FloodgateConfigHolder configHolder, + FloodgateConfig config, SkinUploadManager skinUploadManager, AttributeKey playerAttribute, - FloodgateLogger logger) { + FloodgateLogger logger, + LanguageManager languageManager) { this.handshakeHandlers = handshakeHandlers; this.api = api; this.cipher = cipher; - this.configHolder = configHolder; + this.config = config; this.skinUploadManager = skinUploadManager; this.playerAttribute = playerAttribute; this.logger = logger; + this.languageManager = languageManager; } /** @@ -134,7 +139,7 @@ public CompletableFuture handle( ); } catch (Exception e) { // all the other exceptions are caused by invalid/tempered Floodgate data - if (configHolder.get().isDebug()) { + if (config.isDebug()) { e.printStackTrace(); } @@ -207,11 +212,16 @@ private HandshakeResult handlePart2( try { HandshakeData handshakeData = new HandshakeDataImpl( - channel, true, bedrockData.clone(), configHolder.get(), + channel, true, bedrockData.clone(), config, linkedPlayer != null ? linkedPlayer.clone() : null, hostname); - if (configHolder.get().getPlayerLink().isRequireLink() && linkedPlayer == null) { - handshakeData.setDisconnectReason("floodgate.core.not_linked"); + if (config.getPlayerLink().isRequireLink() && linkedPlayer == null) { + String reason = languageManager.getString( + "floodgate.core.not_linked", + bedrockData.getLanguageCode(), + Constants.LINK_INFO_URL + ); + handshakeData.setDisconnectReason(reason); } handshakeHandlers.callHandshakeHandlers(handshakeData); @@ -245,7 +255,7 @@ private HandshakeResult callHandlerAndReturnResult( String hostname) { HandshakeData handshakeData = new HandshakeDataImpl(channel, bedrockData != null, - bedrockData, configHolder.get(), null, hostname); + bedrockData, config, null, hostname); handshakeHandlers.callHandshakeHandlers(handshakeData); return new HandshakeResult(resultType, handshakeData, bedrockData, null); diff --git a/core/src/main/java/org/geysermc/floodgate/player/FloodgatePlayerImpl.java b/core/src/main/java/org/geysermc/floodgate/player/FloodgatePlayerImpl.java index acfcab75..5945f66e 100644 --- a/core/src/main/java/org/geysermc/floodgate/player/FloodgatePlayerImpl.java +++ b/core/src/main/java/org/geysermc/floodgate/player/FloodgatePlayerImpl.java @@ -32,7 +32,6 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; import org.geysermc.floodgate.api.FloodgateApi; -import org.geysermc.floodgate.api.InstanceHolder; import org.geysermc.floodgate.api.ProxyFloodgateApi; import org.geysermc.floodgate.api.handshake.HandshakeData; import org.geysermc.floodgate.api.player.FloodgatePlayer; @@ -72,7 +71,7 @@ public final class FloodgatePlayerImpl implements FloodgatePlayer { private Map stringToPropertyKey; static FloodgatePlayerImpl from(BedrockData data, HandshakeData handshakeData) { - FloodgateApi api = InstanceHolder.getApi(); + FloodgateApi api = FloodgateApi.getInstance(); UUID javaUniqueId = Utils.getJavaUuid(data.getXuid()); diff --git a/core/src/main/java/org/geysermc/floodgate/player/audience/FloodgateSenderMapper.java b/core/src/main/java/org/geysermc/floodgate/player/audience/FloodgateSenderMapper.java new file mode 100644 index 00000000..632ae5aa --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/player/audience/FloodgateSenderMapper.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.player.audience; + + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.floodgate.platform.command.CommandUtil; +import org.geysermc.floodgate.player.UserAudience; +import org.incendo.cloud.SenderMapper; + +public class FloodgateSenderMapper implements SenderMapper { + + private final CommandUtil commandUtil; + + public FloodgateSenderMapper(CommandUtil commandUtil) { + this.commandUtil = commandUtil; + } + + @Override + public @NonNull UserAudience map(@NonNull T base) { + return this.commandUtil.getUserAudience(base); + } + + @SuppressWarnings("unchecked") + @Override + public @NonNull T reverse(@NonNull UserAudience mapped) { + return (T) mapped.source(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/floodgate/player/audience/InvalidPlayerIdentifierException.java b/core/src/main/java/org/geysermc/floodgate/player/audience/InvalidPlayerIdentifierException.java new file mode 100644 index 00000000..d6692680 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/player/audience/InvalidPlayerIdentifierException.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.player.audience; + +import org.checkerframework.checker.nullness.qual.NonNull; + +public final class InvalidPlayerIdentifierException extends IllegalArgumentException { + + public InvalidPlayerIdentifierException(@NonNull String message) { + super(message); + } + + @Override + public @NonNull Throwable fillInStackTrace() { + return this; + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/player/audience/PlayerAudienceArgument.java b/core/src/main/java/org/geysermc/floodgate/player/audience/PlayerAudienceArgument.java new file mode 100644 index 00000000..765fa75f --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/player/audience/PlayerAudienceArgument.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.player.audience; + +import static org.incendo.cloud.parser.standard.StringParser.quotedStringParser; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.geysermc.floodgate.platform.command.CommandUtil; +import org.geysermc.floodgate.platform.util.PlayerType; +import org.geysermc.floodgate.player.UserAudience; +import org.geysermc.floodgate.util.BrigadierUtils; +import org.incendo.cloud.component.CommandComponent; +import org.incendo.cloud.parser.ArgumentParseResult; +import org.incendo.cloud.suggestion.Suggestion; + +public class PlayerAudienceArgument { + + public static CommandComponent.Builder ofAnyIdentifierBedrock(String name) { + return of(name, true, true, PlayerType.ONLY_BEDROCK); + } + + public static CommandComponent.Builder ofAnyUsernameBoth(String name) { + return of(name, false, true, PlayerType.ALL_PLAYERS); + } + + private static CommandComponent.Builder of(String name, boolean allowUuid, boolean allowOffline, PlayerType limitTo) { + return CommandComponent.builder() + .name(name) + .parser(quotedStringParser().flatMapSuccess(ProfileAudience.class, + (context, input) -> { + CommandUtil commandUtil = context.get("CommandUtil"); + + ProfileAudience profileAudience; + if (input.length() > 16) { + // This must be a UUID. + if (!allowUuid) { + return ArgumentParseResult.failureFuture( + new InvalidPlayerIdentifierException( + "UUID is not allowed here")); + } + + if (input.length() != 32 && input.length() != 36) { + // Neither UUID without dashes nor with dashes. + return ArgumentParseResult.failureFuture( + new InvalidPlayerIdentifierException( + "Expected player name/UUID")); + } + + try { + // We only want to make sure the UUID is valid here. + Object player = commandUtil.getPlayerByUuid( + UUID.fromString(input), limitTo); + profileAudience = commandUtil.getProfileAudience(player, + allowOffline); + } catch (final IllegalArgumentException ignored) { + return ArgumentParseResult.failureFuture( + new InvalidPlayerIdentifierException( + "Invalid UUID '" + input + "'")); + } + } else { + // This is a username. + Object player = commandUtil.getPlayerByUsername(input, limitTo); + profileAudience = commandUtil.getProfileAudience(player, + allowOffline); + } + + if (profileAudience == null) { + return ArgumentParseResult.failureFuture( + new InvalidPlayerIdentifierException( + "Invalid player '" + input + "'")); + } + + return ArgumentParseResult.successFuture(profileAudience); + })) + .suggestionProvider((context, input) -> { + CommandUtil commandUtil = context.get("CommandUtil"); + + boolean quoted = input.remainingInput().startsWith("\""); + List suggestions = new ArrayList<>(); + for (final String player : commandUtil.getOnlineUsernames(limitTo)) { + suggestions.add( + Suggestion.suggestion(BrigadierUtils.escapeIfRequired(player, quoted))); + } + return CompletableFuture.completedFuture(suggestions); + }); + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/player/audience/ProfileAudienceArgument.java b/core/src/main/java/org/geysermc/floodgate/player/audience/ProfileAudienceArgument.java deleted file mode 100644 index d9ed8e47..00000000 --- a/core/src/main/java/org/geysermc/floodgate/player/audience/ProfileAudienceArgument.java +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Floodgate - */ - -package org.geysermc.floodgate.player.audience; - -import cloud.commandframework.arguments.CommandArgument; -import cloud.commandframework.arguments.parser.ArgumentParseResult; -import cloud.commandframework.arguments.parser.ArgumentParser; -import cloud.commandframework.context.CommandContext; -import com.google.common.collect.ImmutableList; -import java.util.List; -import java.util.Locale; -import java.util.Queue; -import java.util.UUID; -import lombok.RequiredArgsConstructor; -import org.checkerframework.checker.nullness.qual.NonNull; -import org.geysermc.floodgate.platform.command.CommandUtil; -import org.geysermc.floodgate.platform.util.PlatformUtils.PlayerType; -import org.geysermc.floodgate.player.UserAudience; - -public class ProfileAudienceArgument extends CommandArgument { - private ProfileAudienceArgument(@NonNull String name, ProfileAudienceParser parser) { - super(true, name, parser, ProfileAudience.class); - } - - public static ProfileAudienceArgument of( - String name, - boolean allowUuid, - boolean allowOffline, - PlayerType limitTo) { - return new ProfileAudienceArgument(name, - new ProfileAudienceParser(allowUuid, allowOffline, limitTo)); - } - - public static ProfileAudienceArgument of( - String name, - boolean allowOffline, - PlayerType limitTo) { - return of(name, false, allowOffline, limitTo); - } - - public static ProfileAudienceArgument ofOnline(String name, PlayerType limitTo) { - return of(name, false, false, limitTo); - } - - public static ProfileAudienceArgument ofOnline(String name, boolean allowUuid) { - return of(name, allowUuid, false, PlayerType.ALL_PLAYERS); - } - - public static CommandArgument ofOnline(String name) { - return of(name, false, false, PlayerType.ALL_PLAYERS); - } - - public static ProfileAudienceArgument of(String name, boolean allowOffline) { - return of(name, false, allowOffline, PlayerType.ALL_PLAYERS); - } - - @RequiredArgsConstructor - public static final class ProfileAudienceParser - implements ArgumentParser { - - private final boolean allowUuid; - private final boolean allowOffline; - private final PlayerType limitTo; - - @Override - public @NonNull ArgumentParseResult parse( - @NonNull CommandContext<@NonNull UserAudience> commandContext, - @NonNull Queue<@NonNull String> inputQueue) { - CommandUtil commandUtil = commandContext.get("CommandUtil"); - - String input = inputQueue.poll(); - if (input == null || input.length() < 3) { - return ArgumentParseResult.failure( - new NullPointerException("Expected player name/UUID")); - } - - if (input.startsWith("\"")) { - if (input.endsWith("\"")) { - // Remove quotes from both sides of this string - input = input.substring(1); - input = input.substring(0, input.length() - 1); - } else { - // Multi-line - StringBuilder builder = new StringBuilder(input); - while (!inputQueue.isEmpty()) { - String string = inputQueue.remove(); - builder.append(' ').append(string); - if (string.endsWith("\"")) { - break; - } - } - - if (builder.lastIndexOf("\"") != builder.length() - 1) { - return ArgumentParseResult.failure( - new InvalidPlayerIdentifierException("Malformed string provided; " + - "no end quotes found!")); - } - - builder.deleteCharAt(0); - builder.deleteCharAt(builder.length() - 1); - input = builder.toString(); - } - } - - ProfileAudience profileAudience; - - if (input.length() > 16) { - // This must be a UUID. - if (!allowUuid) { - return ArgumentParseResult.failure( - new InvalidPlayerIdentifierException("UUID is not allowed here")); - } - - if (input.length() != 32 && input.length() != 36) { - // Neither UUID without dashes nor with dashes. - return ArgumentParseResult.failure( - new InvalidPlayerIdentifierException("Expected player name/UUID")); - } - - try { - // We only want to make sure the UUID is valid here. - Object player = commandUtil.getPlayerByUuid(UUID.fromString(input), limitTo); - profileAudience = commandUtil.getProfileAudience(player, allowOffline); - } catch (final IllegalArgumentException ignored) { - return ArgumentParseResult.failure( - new InvalidPlayerIdentifierException("Invalid UUID '" + input + "'")); - } - } else { - // This is a username. - Object player = commandUtil.getPlayerByUsername(input, limitTo); - profileAudience = commandUtil.getProfileAudience(player, allowOffline); - } - - if (profileAudience == null) { - return ArgumentParseResult.failure( - new InvalidPlayerIdentifierException("Invalid player '" + input + "'")); - } - - return ArgumentParseResult.success(profileAudience); - } - - @Override - public @NonNull List suggestions( - @NonNull CommandContext commandContext, - @NonNull String input) { - CommandUtil commandUtil = commandContext.get("CommandUtil"); - String trimmedInput = input.trim(); - - if (trimmedInput.isEmpty()) { - return ImmutableList.copyOf(commandUtil.getOnlineUsernames(limitTo)); - } - - String lowercaseInput = input.toLowerCase(Locale.ROOT); - ImmutableList.Builder builder = ImmutableList.builder(); - - for (final String player : commandUtil.getOnlineUsernames(limitTo)) { - if (player.toLowerCase(Locale.ROOT).startsWith(lowercaseInput)) { - builder.add(player); - } - } - - return builder.build(); - } - - @Override - public boolean isContextFree() { - return true; - } - } - - public static final class InvalidPlayerIdentifierException extends IllegalArgumentException { - private static final long serialVersionUID = -6500019324607183855L; - - public InvalidPlayerIdentifierException(@NonNull String message) { - super(message); - } - - @Override - public @NonNull Throwable fillInStackTrace() { - return this; - } - } -} diff --git a/core/src/main/java/org/geysermc/floodgate/pluginmessage/PluginMessageChannel.java b/core/src/main/java/org/geysermc/floodgate/pluginmessage/PluginMessageChannel.java index 28f8c068..067edd31 100644 --- a/core/src/main/java/org/geysermc/floodgate/pluginmessage/PluginMessageChannel.java +++ b/core/src/main/java/org/geysermc/floodgate/pluginmessage/PluginMessageChannel.java @@ -36,14 +36,12 @@ public interface PluginMessageChannel { Result handleProxyCall( byte[] data, - UUID targetUuid, - String targetUsername, - Identity targetIdentity, UUID sourceUuid, String sourceUsername, - Identity sourceIdentity); + Identity sourceIdentity + ); - Result handleServerCall(byte[] data, UUID targetUuid, String targetUsername); + Result handleServerCall(byte[] data, UUID playerUuid, String playerUsername); @Getter @RequiredArgsConstructor(access = AccessLevel.PRIVATE) diff --git a/core/src/main/java/org/geysermc/floodgate/pluginmessage/channel/FormChannel.java b/core/src/main/java/org/geysermc/floodgate/pluginmessage/channel/FormChannel.java index 5f546d8e..187d0edc 100644 --- a/core/src/main/java/org/geysermc/floodgate/pluginmessage/channel/FormChannel.java +++ b/core/src/main/java/org/geysermc/floodgate/pluginmessage/channel/FormChannel.java @@ -28,17 +28,22 @@ import com.google.common.base.Charsets; import com.google.inject.Inject; import it.unimi.dsi.fastutil.shorts.Short2ObjectMap; +import it.unimi.dsi.fastutil.shorts.Short2ObjectMaps; import it.unimi.dsi.fastutil.shorts.Short2ObjectOpenHashMap; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; -import org.geysermc.cumulus.Form; +import org.geysermc.cumulus.form.Form; +import org.geysermc.cumulus.form.impl.FormDefinition; +import org.geysermc.cumulus.form.impl.FormDefinitions; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.config.FloodgateConfig; import org.geysermc.floodgate.platform.pluginmessage.PluginMessageUtils; import org.geysermc.floodgate.pluginmessage.PluginMessageChannel; public class FormChannel implements PluginMessageChannel { - private final Short2ObjectMap
storedForms = new Short2ObjectOpenHashMap<>(); + private final FormDefinitions formDefinitions = FormDefinitions.instance(); + private final Short2ObjectMap storedForms = + Short2ObjectMaps.synchronize(new Short2ObjectOpenHashMap<>()); private final AtomicInteger nextFormId = new AtomicInteger(0); @Inject private PluginMessageUtils pluginMessageUtils; @@ -53,13 +58,10 @@ public String getIdentifier() { @Override public Result handleProxyCall( byte[] data, - UUID targetUuid, - String targetUsername, - Identity targetIdentity, UUID sourceUuid, String sourceUsername, - Identity sourceIdentity) { - + Identity sourceIdentity + ) { if (sourceIdentity == Identity.SERVER) { // send it to the client return Result.forward(); @@ -86,11 +88,15 @@ public Result handleProxyCall( } @Override - public Result handleServerCall(byte[] data, UUID targetUuid, String targetUsername) { + public Result handleServerCall(byte[] data, UUID playerUuid, String playerUsername) { callResponseConsumer(data); return Result.handled(); } + public boolean closeForm(UUID player) { + return pluginMessageUtils.sendMessage(player, getIdentifier(), new byte[0]); + } + public boolean sendForm(UUID player, Form form) { byte[] formData = createFormData(form); return pluginMessageUtils.sendMessage(player, getIdentifier(), formData); @@ -103,10 +109,15 @@ public byte[] createFormData(Form form) { } storedForms.put(formId, form); - byte[] jsonData = form.getJsonData().getBytes(Charsets.UTF_8); + FormDefinition definition = formDefinitions.definitionFor(form); + + byte[] jsonData = + definition.codec() + .jsonData(form) + .getBytes(Charsets.UTF_8); byte[] data = new byte[jsonData.length + 3]; - data[0] = (byte) form.getType().ordinal(); + data[0] = (byte) definition.formType().ordinal(); data[1] = (byte) (formId >> 8 & 0xFF); data[2] = (byte) (formId & 0xFF); System.arraycopy(jsonData, 0, data, 3, jsonData.length); @@ -117,7 +128,12 @@ protected boolean callResponseConsumer(byte[] data) { Form storedForm = storedForms.remove(getFormId(data)); if (storedForm != null) { String responseData = new String(data, 2, data.length - 2, Charsets.UTF_8); - storedForm.getResponseHandler().accept(responseData); + try { + formDefinitions.definitionFor(storedForm) + .handleFormResponse(storedForm, responseData); + } catch (Exception e) { + logger.error("Error while processing form response!", e); + } return true; } return false; diff --git a/core/src/main/java/org/geysermc/floodgate/pluginmessage/channel/PacketChannel.java b/core/src/main/java/org/geysermc/floodgate/pluginmessage/channel/PacketChannel.java index a6da65c5..2296f35e 100644 --- a/core/src/main/java/org/geysermc/floodgate/pluginmessage/channel/PacketChannel.java +++ b/core/src/main/java/org/geysermc/floodgate/pluginmessage/channel/PacketChannel.java @@ -40,23 +40,26 @@ public String getIdentifier() { } @Override - public Result handleProxyCall(byte[] data, UUID targetUuid, String targetUsername, - Identity targetIdentity, UUID sourceUuid, String sourceUsername, - Identity sourceIdentity) { + public Result handleProxyCall( + byte[] data, + UUID sourceUuid, + String sourceUsername, + Identity sourceIdentity + ) { if (sourceIdentity == Identity.SERVER) { // send it to the client return Result.forward(); } if (sourceIdentity == Identity.PLAYER) { - return handleServerCall(data, targetUuid, targetUsername); + return handleServerCall(data, sourceUuid, sourceUsername); } return Result.handled(); } @Override - public Result handleServerCall(byte[] data, UUID targetUuid, String targetUsername) { + public Result handleServerCall(byte[] data, UUID playerUuid, String playerUsername) { return Result.kick("Cannot send packets from Geyser/Floodgate to Floodgate"); } diff --git a/core/src/main/java/org/geysermc/floodgate/pluginmessage/channel/SkinChannel.java b/core/src/main/java/org/geysermc/floodgate/pluginmessage/channel/SkinChannel.java index 2e82eceb..8e9e64b0 100644 --- a/core/src/main/java/org/geysermc/floodgate/pluginmessage/channel/SkinChannel.java +++ b/core/src/main/java/org/geysermc/floodgate/pluginmessage/channel/SkinChannel.java @@ -25,7 +25,6 @@ package org.geysermc.floodgate.pluginmessage.channel; -import com.google.gson.JsonObject; import com.google.inject.Inject; import java.nio.charset.StandardCharsets; import java.util.UUID; @@ -36,7 +35,7 @@ import org.geysermc.floodgate.config.ProxyFloodgateConfig; import org.geysermc.floodgate.pluginmessage.PluginMessageChannel; import org.geysermc.floodgate.skin.SkinApplier; -import org.geysermc.floodgate.skin.SkinData; +import org.geysermc.floodgate.skin.SkinDataImpl; public class SkinChannel implements PluginMessageChannel { @Inject private FloodgateApi api; @@ -51,16 +50,13 @@ public String getIdentifier() { @Override public Result handleProxyCall( byte[] data, - UUID targetUuid, - String targetUsername, - Identity targetIdentity, UUID sourceUuid, String sourceUsername, - Identity sourceIdentity) { - + Identity sourceIdentity + ) { // we can only get skins from Geyser (client) if (sourceIdentity == Identity.PLAYER) { - Result result = handleServerCall(data, targetUuid, targetUsername); + Result result = handleServerCall(data, sourceUuid, sourceUsername); // aka translate 'handled' into 'forward' when send-floodgate-data is enabled if (!result.isAllowed() && result.getReason() == null) { if (config.isProxy() && ((ProxyFloodgateConfig) config).isSendFloodgateData()) { @@ -78,8 +74,8 @@ public Result handleProxyCall( } @Override - public Result handleServerCall(byte[] data, UUID targetUuid, String targetUsername) { - FloodgatePlayer floodgatePlayer = api.getPlayer(targetUuid); + public Result handleServerCall(byte[] data, UUID playerUuid, String playerUsername) { + FloodgatePlayer floodgatePlayer = api.getPlayer(playerUuid); if (floodgatePlayer == null) { return Result.kick("Player sent skins data for a non-Floodgate player"); } @@ -92,18 +88,10 @@ public Result handleServerCall(byte[] data, UUID targetUuid, String targetUserna return Result.kick("Got invalid skin data"); } - if (floodgatePlayer.isLinked()) { - return Result.handled(); - } - String value = split[0]; String signature = split[1]; - JsonObject result = new JsonObject(); - result.addProperty("value", value); - result.addProperty("signature", signature); - - SkinData skinData = new SkinData(value, signature); + SkinDataImpl skinData = new SkinDataImpl(value, signature); floodgatePlayer.addProperty(PropertyKey.SKIN_UPLOADED, skinData); skinApplier.applySkin(floodgatePlayer, skinData); diff --git a/core/src/main/java/org/geysermc/floodgate/pluginmessage/channel/TransferChannel.java b/core/src/main/java/org/geysermc/floodgate/pluginmessage/channel/TransferChannel.java index 94e9982e..7ca754ed 100644 --- a/core/src/main/java/org/geysermc/floodgate/pluginmessage/channel/TransferChannel.java +++ b/core/src/main/java/org/geysermc/floodgate/pluginmessage/channel/TransferChannel.java @@ -42,27 +42,24 @@ public String getIdentifier() { @Override public Result handleProxyCall( byte[] data, - UUID targetUuid, - String targetUsername, - Identity targetIdentity, UUID sourceUuid, String sourceUsername, - Identity sourceIdentity) { - + Identity sourceIdentity + ) { if (sourceIdentity == Identity.SERVER) { // send it to the client return Result.forward(); } if (sourceIdentity == Identity.PLAYER) { - handleServerCall(data, targetUuid, targetUsername); + handleServerCall(data, sourceUuid, sourceUsername); } return Result.handled(); } @Override - public Result handleServerCall(byte[] data, UUID targetUuid, String targetUsername) { + public Result handleServerCall(byte[] data, UUID playerUuid, String playerUsername) { return Result.kick("I'm sorry, I'm unable to transfer a server :("); } diff --git a/core/src/main/java/org/geysermc/floodgate/register/CommandRegister.java b/core/src/main/java/org/geysermc/floodgate/register/CommandRegister.java index 2eed7d26..11ebd0fe 100644 --- a/core/src/main/java/org/geysermc/floodgate/register/CommandRegister.java +++ b/core/src/main/java/org/geysermc/floodgate/register/CommandRegister.java @@ -25,7 +25,6 @@ package org.geysermc.floodgate.register; -import cloud.commandframework.CommandManager; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Key; @@ -33,6 +32,7 @@ import org.geysermc.floodgate.config.FloodgateConfig; import org.geysermc.floodgate.platform.command.FloodgateCommand; import org.geysermc.floodgate.player.UserAudience; +import org.incendo.cloud.CommandManager; /** * This class is responsible for registering commands to the command register of the platform that diff --git a/core/src/main/java/org/geysermc/floodgate/skin/SkinApplier.java b/core/src/main/java/org/geysermc/floodgate/skin/SkinApplier.java index 0386b723..c8e7684c 100644 --- a/core/src/main/java/org/geysermc/floodgate/skin/SkinApplier.java +++ b/core/src/main/java/org/geysermc/floodgate/skin/SkinApplier.java @@ -25,8 +25,16 @@ package org.geysermc.floodgate.skin; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.floodgate.api.event.skin.SkinApplyEvent.SkinData; import org.geysermc.floodgate.api.player.FloodgatePlayer; public interface SkinApplier { - void applySkin(FloodgatePlayer floodgatePlayer, SkinData skinData); + /** + * Apply a skin to a {@link FloodgatePlayer player} + * + * @param floodgatePlayer player to apply skin to + * @param skinData data for skin to apply to player + */ + void applySkin(@NonNull FloodgatePlayer floodgatePlayer, @NonNull SkinData skinData); } diff --git a/core/src/main/java/org/geysermc/floodgate/skin/SkinDataImpl.java b/core/src/main/java/org/geysermc/floodgate/skin/SkinDataImpl.java new file mode 100644 index 00000000..1e2247e2 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/skin/SkinDataImpl.java @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.skin; + +import com.google.gson.JsonObject; +import java.util.Objects; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.floodgate.api.event.skin.SkinApplyEvent.SkinData; +import org.geysermc.floodgate.util.Constants; + +public class SkinDataImpl implements SkinData { + public static final SkinData DEFAULT_SKIN = new SkinDataImpl( + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE, + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE + ); + + private final String value; + private final String signature; + + public SkinDataImpl(@NonNull String value, @NonNull String signature) { + this.value = Objects.requireNonNull(value); + this.signature = Objects.requireNonNull(signature); + } + + public static SkinData from(JsonObject data) { + return new SkinDataImpl( + data.get("value").getAsString(), + data.get("signature").getAsString() + ); + } + + @Override + public @NonNull String value() { + return value; + } + + @Override + public @NonNull String signature() { + return signature; + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/skin/SkinUploadManager.java b/core/src/main/java/org/geysermc/floodgate/skin/SkinUploadManager.java index a6c49395..61db1ab6 100644 --- a/core/src/main/java/org/geysermc/floodgate/skin/SkinUploadManager.java +++ b/core/src/main/java/org/geysermc/floodgate/skin/SkinUploadManager.java @@ -25,21 +25,26 @@ package org.geysermc.floodgate.skin; +import com.google.inject.Inject; +import com.google.inject.Singleton; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import lombok.AllArgsConstructor; +import org.geysermc.event.Listener; +import org.geysermc.event.subscribe.Subscribe; import org.geysermc.floodgate.api.FloodgateApi; import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.event.lifecycle.ShutdownEvent; -@AllArgsConstructor +@Listener +@Singleton public final class SkinUploadManager { private final Int2ObjectMap connections = Int2ObjectMaps.synchronize(new Int2ObjectOpenHashMap<>()); - private final FloodgateApi api; - private final SkinApplier applier; - private final FloodgateLogger logger; + @Inject private FloodgateApi api; + @Inject private SkinApplier applier; + @Inject private FloodgateLogger logger; public void addConnectionIfNeeded(int id, String verifyCode) { connections.computeIfAbsent(id, (ignored) -> { @@ -53,4 +58,16 @@ public void addConnectionIfNeeded(int id, String verifyCode) { public void removeConnection(int id, SkinUploadSocket socket) { connections.remove(id, socket); } + + public void closeAllSockets() { + for (SkinUploadSocket socket : connections.values()) { + socket.close(); + } + connections.clear(); + } + + @Subscribe + public void onShutdown(ShutdownEvent ignored) { + closeAllSockets(); + } } diff --git a/core/src/main/java/org/geysermc/floodgate/skin/SkinUploadSocket.java b/core/src/main/java/org/geysermc/floodgate/skin/SkinUploadSocket.java index 87c04459..5018f8fa 100644 --- a/core/src/main/java/org/geysermc/floodgate/skin/SkinUploadSocket.java +++ b/core/src/main/java/org/geysermc/floodgate/skin/SkinUploadSocket.java @@ -35,6 +35,7 @@ import javax.net.ssl.SSLException; import lombok.Getter; import org.geysermc.floodgate.api.FloodgateApi; +import org.geysermc.floodgate.api.event.skin.SkinApplyEvent.SkinData; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; import org.geysermc.floodgate.api.player.PropertyKey; @@ -61,8 +62,8 @@ public SkinUploadSocket( SkinUploadManager uploadManager, FloodgateApi api, SkinApplier applier, - FloodgateLogger logger) { - + FloodgateLogger logger + ) { super(getWebsocketUri(id, verifyCode)); this.id = id; this.verifyCode = verifyCode; @@ -83,7 +84,7 @@ private static URI getWebsocketUri(int id, String verifyCode) { } @Override - public void onOpen(ServerHandshake handshakedata) { + public void onOpen(ServerHandshake ignored) { setConnectionLostTimeout(11); } @@ -114,10 +115,14 @@ public void onMessage(String data) { player.getCorrectUsername()); return; } + + SkinData skinData = SkinDataImpl.from(message.getAsJsonObject("data")); + applier.applySkin(player, skinData); + + // legacy stuff, + // will be removed shortly after or during the Floodgate-Geyser integration if (!player.isLinked()) { - SkinData skinData = SkinData.from(message.getAsJsonObject("data")); player.addProperty(PropertyKey.SKIN_UPLOADED, skinData); - applier.applySkin(player, skinData); } } break; diff --git a/core/src/main/java/org/geysermc/floodgate/util/AutoBind.java b/core/src/main/java/org/geysermc/floodgate/util/AutoBind.java new file mode 100644 index 00000000..6c1eec2e --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/util/AutoBind.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.util; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Automatically binds an instance of itself as an eager singleton during the post-initialise stage + * of the FloodgatePlatform. Add the annotation to a class, and it should automatically create an + * instance and inject it for you. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface AutoBind { +} diff --git a/core/src/main/java/org/geysermc/floodgate/util/BrigadierUtils.java b/core/src/main/java/org/geysermc/floodgate/util/BrigadierUtils.java new file mode 100644 index 00000000..50c1eb5d --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/util/BrigadierUtils.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.util; + +/* +Code taken from Brigadier's StringArgumentType and StringReader + */ +public final class BrigadierUtils { + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public static boolean isAllowedInUnquotedString(char c) { + return c >= '0' && c <= '9' || + c >= 'A' && c <= 'Z' || + c >= 'a' && c <= 'z' || + c == '_' || c == '-' || + c == '.' || c == '+'; + } + + public static String escapeIfRequired(String input, boolean quoted) { + if (quoted) { + return escape(input); + } + + for (final char c : input.toCharArray()) { + if (!isAllowedInUnquotedString(c)) { + return "\"" + input + "\""; + } + } + return input; + } + + private static String escape(final String input) { + final StringBuilder result = new StringBuilder("\""); + + for (int i = 0; i < input.length(); i++) { + final char c = input.charAt(i); + if (c == '\\' || c == '"') { + result.append('\\'); + } + result.append(c); + } + + result.append("\""); + return result.toString(); + } +} \ No newline at end of file diff --git a/core/src/main/java/org/geysermc/floodgate/util/HttpUtils.java b/core/src/main/java/org/geysermc/floodgate/util/HttpClient.java similarity index 76% rename from core/src/main/java/org/geysermc/floodgate/util/HttpUtils.java rename to core/src/main/java/org/geysermc/floodgate/util/HttpClient.java index 56ef2540..960dc1b1 100644 --- a/core/src/main/java/org/geysermc/floodgate/util/HttpUtils.java +++ b/core/src/main/java/org/geysermc/floodgate/util/HttpClient.java @@ -27,13 +27,15 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Getter; @@ -42,29 +44,36 @@ // resources are properly closed and ignoring the original stack trace is intended @SuppressWarnings({"PMD.CloseResource", "PMD.PreserveStackTrace"}) -public class HttpUtils { - private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor(); - - private static final Gson GSON = new Gson(); +@Singleton +public class HttpClient { private static final String USER_AGENT = "GeyserMC/Floodgate"; - public static CompletableFuture asyncGet(String urlString) { - return CompletableFuture.supplyAsync(() -> get(urlString), EXECUTOR_SERVICE); + private final Gson gson = new Gson(); + @Inject + @Named("commonPool") + private ExecutorService executorService; + + public CompletableFuture asyncGet(String urlString) { + return CompletableFuture.supplyAsync(() -> get(urlString), executorService); + } + + public CompletableFuture> asyncGet(String urlString, Class response) { + return CompletableFuture.supplyAsync(() -> get(urlString, response), executorService); } - public static DefaultHttpResponse get(String urlString) { + public DefaultHttpResponse get(String urlString) { return readDefaultResponse(request(urlString)); } - public static HttpResponse get(String urlString, Class clazz) { + public HttpResponse get(String urlString, Class clazz) { return readResponse(request(urlString), clazz); } - public static HttpResponse getSilent(String urlString, Class clazz) { + public HttpResponse getSilent(String urlString, Class clazz) { return readResponseSilent(request(urlString), clazz); } - private static HttpURLConnection request(String urlString) { + private HttpURLConnection request(String urlString) { HttpURLConnection connection; try { @@ -88,18 +97,20 @@ private static HttpURLConnection request(String urlString) { } @NonNull - private static HttpResponse readResponse(HttpURLConnection connection, Class clazz) { + private HttpResponse readResponse(HttpURLConnection connection, Class clazz) { InputStreamReader streamReader = createReader(connection); if (streamReader == null) { return new HttpResponse<>(-1, null); } + int responseCode = -1; try { - int responseCode = connection.getResponseCode(); - T response = GSON.fromJson(streamReader, clazz); + responseCode = connection.getResponseCode(); + T response = gson.fromJson(streamReader, clazz); return new HttpResponse<>(responseCode, response); } catch (Exception ignored) { - return new HttpResponse<>(-1, null); + // e.g. when the response isn't JSON + return new HttpResponse<>(responseCode, null); } finally { try { streamReader.close(); @@ -109,9 +120,7 @@ private static HttpResponse readResponse(HttpURLConnection connection, Cl } @NonNull - private static HttpResponse readResponseSilent( - HttpURLConnection connection, - Class clazz) { + private HttpResponse readResponseSilent(HttpURLConnection connection, Class clazz) { try { return readResponse(connection, clazz); } catch (Exception ignored) { @@ -120,7 +129,7 @@ private static HttpResponse readResponseSilent( } @NonNull - private static DefaultHttpResponse readDefaultResponse(HttpURLConnection connection) { + private DefaultHttpResponse readDefaultResponse(HttpURLConnection connection) { InputStreamReader streamReader = createReader(connection); if (streamReader == null) { return new DefaultHttpResponse(-1, null); @@ -128,7 +137,7 @@ private static DefaultHttpResponse readDefaultResponse(HttpURLConnection connect try { int responseCode = connection.getResponseCode(); - JsonObject response = GSON.fromJson(streamReader, JsonObject.class); + JsonObject response = gson.fromJson(streamReader, JsonObject.class); return new DefaultHttpResponse(responseCode, response); } catch (Exception exception) { throw new RuntimeException("Failed to read response", exception); @@ -141,16 +150,12 @@ private static DefaultHttpResponse readDefaultResponse(HttpURLConnection connect } @Nullable - private static InputStreamReader createReader(HttpURLConnection connection) { + private InputStreamReader createReader(HttpURLConnection connection) { InputStream stream; try { stream = connection.getInputStream(); } catch (Exception exception) { - try { - stream = connection.getErrorStream(); - } catch (Exception exception1) { - throw new RuntimeException("Both the input and the error stream failed?!"); - } + stream = connection.getErrorStream(); } // it's null for example when it couldn't connect to the server diff --git a/core/src/main/java/org/geysermc/floodgate/util/LanguageManager.java b/core/src/main/java/org/geysermc/floodgate/util/LanguageManager.java index 92c9a395..a3d659bf 100644 --- a/core/src/main/java/org/geysermc/floodgate/util/LanguageManager.java +++ b/core/src/main/java/org/geysermc/floodgate/util/LanguageManager.java @@ -26,30 +26,27 @@ package org.geysermc.floodgate.util; import com.google.common.base.Joiner; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; +import com.google.inject.Inject; +import com.google.inject.Singleton; import java.net.URL; -import java.nio.charset.StandardCharsets; import java.text.MessageFormat; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Properties; import lombok.Getter; -import lombok.RequiredArgsConstructor; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.config.FloodgateConfig; -import org.geysermc.floodgate.config.FloodgateConfigHolder; /** * Manages translations for strings in Floodgate */ -@RequiredArgsConstructor +@Singleton public final class LanguageManager { private final Map localeMappings = new HashMap<>(); - private final FloodgateConfigHolder configHolder; - private final FloodgateLogger logger; + + @Inject private FloodgateConfig config; + @Inject private FloodgateLogger logger; /** * The locale used in console and as a fallback @@ -71,24 +68,15 @@ private static String formatLocale(String locale) { } } - public boolean isLoaded() { - return logger != null && defaultLocale != null; - } - /** * Tries to load the log's locale file once a string has been requested */ + @Inject private void init() { if (!loadLocale("en_US")) {// Fallback logger.error("Failed to load the fallback language. This will likely cause errors!"); } - FloodgateConfig config = configHolder.get(); - if (config == null) { - // :thonk: - return; - } - defaultLocale = formatLocale(config.getDefaultLocale()); if (isValidLanguage(defaultLocale)) { @@ -125,21 +113,11 @@ public boolean loadLocale(String locale) { return true; } - InputStream localeStream = LanguageManager.class.getClassLoader().getResourceAsStream( - "languages/texts/" + formatLocale + ".properties"); - - // load the locale - if (localeStream != null) { - Properties localeProp = new Properties(); + Properties properties = + Utils.readProperties("languages/texts/" + formatLocale + ".properties"); - try (Reader reader = new InputStreamReader(localeStream, StandardCharsets.UTF_8)) { - localeProp.load(reader); - } catch (Exception e) { - throw new AssertionError("Failed to load Floodgate locale", e); - } - - // insert the locale into the mappings - localeMappings.put(formatLocale, localeProp); + if (properties != null) { + localeMappings.put(formatLocale, properties); return true; } @@ -167,14 +145,6 @@ public String getLogString(String key, Object... values) { * @return translated string or "key arg1, arg2 (etc.)" if it was not found in the given locale */ public String getString(String key, String locale, Object... values) { - if (!isLoaded()) { - init(); - // we can skip everything if the LanguageManager can't be loaded yet - if (!isLoaded()) { - return formatNotFound(key, values); - } - } - Properties properties = localeMappings.get(locale); String formatString = null; diff --git a/core/src/main/java/org/geysermc/floodgate/util/Metrics.java b/core/src/main/java/org/geysermc/floodgate/util/Metrics.java new file mode 100644 index 00000000..7e388940 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/util/Metrics.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.util; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import java.util.Collections; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.bstats.MetricsBase; +import org.bstats.charts.DrilldownPie; +import org.bstats.charts.SimplePie; +import org.bstats.charts.SingleLineChart; +import org.bstats.json.JsonObjectBuilder; +import org.geysermc.event.Listener; +import org.geysermc.event.subscribe.Subscribe; +import org.geysermc.floodgate.api.FloodgateApi; +import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.config.FloodgateConfig; +import org.geysermc.floodgate.config.FloodgateConfig.MetricsConfig; +import org.geysermc.floodgate.event.lifecycle.ShutdownEvent; +import org.geysermc.floodgate.platform.util.PlatformUtils; + +@Listener +@AutoBind +public final class Metrics { + private final MetricsBase metricsBase; + + @Inject + Metrics(FloodgateConfig config, PlatformUtils platformUtils, FloodgateApi api, + @Named("implementationName") String implementationName, FloodgateLogger logger) { + + MetricsConfig metricsConfig = config.getMetrics(); + + metricsBase = new MetricsBase( + "server-implementation", + metricsConfig.getUuid(), + Constants.METRICS_ID, + metricsConfig.isEnabled(), + this::appendPlatformData, + jsonObjectBuilder -> { /* NOP */ }, + null, + () -> true, // remove this if/when we add some form of reload support + logger::error, + logger::info, + Constants.DEBUG_MODE, + Constants.DEBUG_MODE, + Constants.DEBUG_MODE + ); + + metricsBase.addCustomChart( + new SingleLineChart("players", api::getPlayerCount) + ); + + metricsBase.addCustomChart( + new DrilldownPie("player_count", () -> { + int playerCount = api.getPlayerCount(); + // 0 = 0 - 4, 9 = 5 - 9, etc. + int category = playerCount / 5 * 5; + String categoryName = category + " - " + (category + 4); + + return Collections.singletonMap( + implementationName, + Collections.singletonMap(categoryName, 1) + ); + }) + ); + + metricsBase.addCustomChart( + new SimplePie("authentication", + () -> platformUtils.authType().name().toLowerCase(Locale.ROOT)) + ); + + metricsBase.addCustomChart( + new SimplePie("floodgate_version", () -> Constants.VERSION) + ); + + metricsBase.addCustomChart( + new DrilldownPie("platform", () -> Collections.singletonMap( + implementationName, + Collections.singletonMap(platformUtils.serverImplementationName(), 1) + ))); + + metricsBase.addCustomChart( + new DrilldownPie("minecraft_version", () -> { + // e.g.: 1.16.5 => (Spigot, 1) + return Collections.singletonMap( + implementationName, + Collections.singletonMap(platformUtils.minecraftVersion(), 1) + ); + }) + ); + + // Source: Geyser + metricsBase.addCustomChart(new DrilldownPie("java_version", () -> { + Map> map = new HashMap<>(); + String javaVersion = System.getProperty("java.version"); + Map entry = new HashMap<>(); + entry.put(javaVersion, 1); + + String majorVersion = javaVersion.split("\\.")[0]; + String release; + + int indexOf = javaVersion.lastIndexOf('.'); + + if (majorVersion.equals("1")) { + release = "Java " + javaVersion.substring(0, indexOf); + } else { + Matcher versionMatcher = Pattern.compile("\\d+").matcher(majorVersion); + if (versionMatcher.find()) { + majorVersion = versionMatcher.group(0); + } + release = "Java " + majorVersion; + } + map.put(release, entry); + return map; + })); + } + + private void appendPlatformData(JsonObjectBuilder builder) { + builder.appendField("osName", System.getProperty("os.name")); + builder.appendField("osArch", System.getProperty("os.arch")); + builder.appendField("osVersion", System.getProperty("os.version")); + builder.appendField("coreCount", Runtime.getRuntime().availableProcessors()); + } + + @Subscribe + public void onShutdown(ShutdownEvent ignored) { + metricsBase.shutdown(); + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/util/MojangUtils.java b/core/src/main/java/org/geysermc/floodgate/util/MojangUtils.java new file mode 100644 index 00000000..423ff44b --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/util/MojangUtils.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2019-2024 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.util; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.google.inject.name.Named; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import lombok.NonNull; +import org.geysermc.floodgate.api.event.skin.SkinApplyEvent.SkinData; +import org.geysermc.floodgate.skin.SkinDataImpl; +import org.geysermc.floodgate.util.HttpClient.HttpResponse; + +@Singleton +public class MojangUtils { + private final Cache SKIN_CACHE = CacheBuilder.newBuilder() + .expireAfterWrite(5, TimeUnit.MINUTES) + .maximumSize(500) + .build(); + + @Inject private HttpClient httpClient; + @Inject + @Named("commonPool") + private ExecutorService commonPool; + + public CompletableFuture<@NonNull SkinData> skinFor(UUID playerId) { + return CompletableFuture.supplyAsync(() -> { + try { + return SKIN_CACHE.get(playerId, () -> fetchSkinFor(playerId)); + } catch (ExecutionException exception) { + throw new RuntimeException(exception.getCause()); + } + }, commonPool); + } + + private @NonNull SkinData fetchSkinFor(UUID playerId) { + HttpResponse httpResponse = httpClient.get( + String.format(Constants.PROFILE_WITH_PROPERTIES_URL, playerId.toString())); + + if (httpResponse.getHttpCode() != 200) { + return SkinDataImpl.DEFAULT_SKIN; + } + + JsonObject response = httpResponse.getResponse(); + + if (response == null) { + return SkinDataImpl.DEFAULT_SKIN; + } + + JsonArray properties = response.getAsJsonArray("properties"); + + if (properties.size() == 0) { + return SkinDataImpl.DEFAULT_SKIN; + } + + for (JsonElement property : properties) { + if (!property.isJsonObject()) { + continue; + } + + JsonObject propertyObject = property.getAsJsonObject(); + + if (!propertyObject.has("name") + || !propertyObject.has("value") + || !propertyObject.has("signature") + || !propertyObject.get("name").getAsString().equals("textures")) { + continue; + } + + return new SkinDataImpl( + propertyObject.get("value").getAsString(), + propertyObject.get("signature").getAsString() + ); + } + + return SkinDataImpl.DEFAULT_SKIN; + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/util/PostEnableMessages.java b/core/src/main/java/org/geysermc/floodgate/util/PostEnableMessages.java new file mode 100644 index 00000000..e526c470 --- /dev/null +++ b/core/src/main/java/org/geysermc/floodgate/util/PostEnableMessages.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.util; + +import com.google.inject.Inject; +import com.google.inject.name.Named; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import org.geysermc.event.Listener; +import org.geysermc.event.subscribe.Subscribe; +import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.config.FloodgateConfig; +import org.geysermc.floodgate.event.lifecycle.PostEnableEvent; + +@AutoBind +@Listener +public final class PostEnableMessages { + private final List messages = new ArrayList<>(); + + @Inject private FloodgateConfig config; + @Inject private FloodgateLogger logger; + @Inject + @Named("commonScheduledPool") + private ScheduledExecutorService executorService; + + public void add(String[] message, Object... args) { + StringBuilder builder = new StringBuilder(); + + builder.append("\n**********************************\n"); + for (String part : message) { + builder.append("* ").append(part).append('\n'); + } + builder.append("**********************************"); + + messages.add(MessageFormatter.format(builder.toString(), args)); + } + + @Inject + private void init() { + registerPrefixMessages(); + } + + private void registerPrefixMessages() { + String prefix = config.getRawUsernamePrefix(); + + if (prefix.isEmpty()) { + add(new String[]{ + "You specified an empty prefix in your Floodgate config for Bedrock players!", + "Should a Java player join and a Bedrock player join with the same username, unwanted results and conflicts will happen!", + "We strongly recommend using . as the prefix, but other alternatives that will not conflict include: +, - and *" + }); + } else if (!Utils.isUniquePrefix(prefix)) { + add(new String[]{ + "The prefix you entered in your Floodgate config ({}) could lead to username conflicts!", + "Should a Java player join with the username {}Notch, and a Bedrock player join as Notch (who will be given the name {}Notch), unwanted results will happen!", + "We strongly recommend using . as the prefix, but other alternatives that will not conflict include: +, - and *" + }, prefix, prefix, prefix, prefix); + } + + if (prefix.length() >= 16) { + add(new String[]{ + "The prefix you entered in your Floodgate config ({}) is longer than a Java username can be!", + "Because of this, we reset the prefix to the default Floodgate prefix (.)" + }, prefix); + } else if (prefix.length() > 2) { + // we only have to warn them if we haven't replaced the prefix + add(new String[]{ + "The prefix you entered in your Floodgate config ({}) is long! ({} characters)", + "A prefix is there to prevent username conflicts. However, a long prefix makes the chance of username conflicts higher.", + "We strongly recommend using . as the prefix, but other alternatives that will not conflict include: +, - and *" + }, prefix, prefix.length()); + } + } + + @Subscribe + public void onPostEnable(PostEnableEvent ignored) { + // normally proxies don't have a lot of plugins, so proxies don't need to sleep as long + executorService.schedule( + () -> messages.forEach(logger::warn), + config.isProxy() ? 2 : 5, + TimeUnit.SECONDS + ); + } +} diff --git a/core/src/main/java/org/geysermc/floodgate/util/PrefixCheckTask.java b/core/src/main/java/org/geysermc/floodgate/util/PrefixCheckTask.java deleted file mode 100644 index 4631171f..00000000 --- a/core/src/main/java/org/geysermc/floodgate/util/PrefixCheckTask.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * @author GeyserMC - * @link https://github.com/GeyserMC/Floodgate - */ - -package org.geysermc.floodgate.util; - -import org.geysermc.floodgate.api.logger.FloodgateLogger; -import org.geysermc.floodgate.config.FloodgateConfig; - -public final class PrefixCheckTask { - public static void checkAndExecuteDelayed(FloodgateConfig config, FloodgateLogger logger) { - if (Utils.isUniquePrefix(config.getUsernamePrefix())) { - return; - } - - new Thread(() -> { - // normally proxies don't have a lot of plugins, so proxies don't need to sleep as long - try { - Thread.sleep(config.isProxy() ? 1000 : 2000); - } catch (InterruptedException ignored) { - } - - if (config.getUsernamePrefix().isEmpty()) { - logger.warn("\n" + - "**********************************\n" + - "* You specified an empty prefix in your Floodgate config for Bedrock players!\n" + - "* Should a Java player join and a Bedrock player join with the same username, unwanted results and conflicts will happen!\n" + - "* We strongly recommend using . as the prefix, but other alternatives that will not conflict include: +, - and *\n" + - "**********************************"); - return; - } - - logger.warn( - "\n" + - "**********************************\n" + - "* The prefix you entered in your Floodgate config ({}) could lead to username conflicts!\n" + - "* Should a Java player join with the username {}Notch, and a Bedrock player join as Notch (who will be given the name {}Notch), unwanted results will happen!\n" + - "* We strongly recommend using . as the prefix, but other alternatives that will not conflict include: +, - and *\n" + - "**********************************", - config.getUsernamePrefix(), config.getUsernamePrefix(), - config.getUsernamePrefix(), config.getUsernamePrefix()); - }).start(); - } -} diff --git a/core/src/main/java/org/geysermc/floodgate/util/ReflectionUtils.java b/core/src/main/java/org/geysermc/floodgate/util/ReflectionUtils.java index 28c5abaf..709dfd43 100644 --- a/core/src/main/java/org/geysermc/floodgate/util/ReflectionUtils.java +++ b/core/src/main/java/org/geysermc/floodgate/util/ReflectionUtils.java @@ -46,6 +46,10 @@ public final class ReflectionUtils { @Setter private static String prefix; + private static String applyPrefix(String className) { + return prefix + "." + className; + } + /** * Get a class that is prefixed with the prefix provided in {@link #setPrefix(String)}. Calling * this method is equal to calling {@link #getClass(String)} with prefix.classname @@ -56,13 +60,13 @@ public final class ReflectionUtils { */ @Nullable public static Class getPrefixedClass(String className) { - return getClass(prefix + "." + className); + return getClass(applyPrefix(className)); } @Nullable public static Class getPrefixedClassSilently(String className) { try { - return Class.forName(prefix + "." + className); + return Class.forName(applyPrefix(className)); } catch (ClassNotFoundException ignored) { return null; } @@ -109,6 +113,28 @@ public static Class getClassOrThrow(String className) { } } + public static Class getClassOrFallbackPrefixed(String className, String fallbackClassName) { + return getClassOrFallback(applyPrefix(className), applyPrefix(fallbackClassName)); + } + + public static Class getClassOrFallback(String className, String fallbackClassName) { + Class clazz = getClassSilently(className); + + if (clazz != null) { + if (Constants.DEBUG_MODE) { + System.out.println("Found class (primary): " + clazz.getName()); + } + return clazz; + } + + // do throw an exception when both classes couldn't be found + clazz = ReflectionUtils.getClassOrThrow(fallbackClassName); + if (Constants.DEBUG_MODE) { + System.out.println("Found class (fallback): " + clazz.getName()); + } + return clazz; + } + @Nullable public static Constructor getConstructor(Class clazz, boolean declared, Class... parameters) { try { @@ -143,7 +169,7 @@ public static T newInstance(Constructor constructor, Object... parameters * * @param clazz the class name to get the field from * @param fieldName the name of the field - * @param declared if the field is declared or public. + * @param declared if the field is declared. * @return the field if found, otherwise null */ @Nullable @@ -180,7 +206,7 @@ public static Field getField(Class clazz, String fieldName) { * * @param clazz the class to search the field from * @param fieldType the type of the field - * @param declared if the field is declared or public + * @param declared if the field is declared * @return the field if it has been found, otherwise null */ @Nullable @@ -229,6 +255,25 @@ public static Object getValue(Object instance, Field field) { } } + /** + * Get the value of a boolean field. This method first makes the field accessible and then gets + * the value.
This method will return false instead of throwing an exception, but it'll log + * the stacktrace to the console. + * + * @param instance the instance to get the value from + * @param field the field to get the value from + * @return the value when succeeded, otherwise null + */ + public static boolean getBooleanValue(Object instance, Field field) { + makeAccessible(field); + try { + return field.getBoolean(instance); + } catch (IllegalArgumentException | IllegalAccessException exception) { + exception.printStackTrace(); + return false; + } + } + /** * Get the value of the given field by finding the field and then get the value of it. * @@ -242,7 +287,7 @@ public static Object getValue(Object instance, String fieldName) { } /** - * Get the value of a field and cast it to . + * Get the value of a field and cast it to T. * * @param instance the instance to get the value from * @param field the field to get the value from @@ -256,7 +301,7 @@ public static T getCastedValue(Object instance, Field field) { } /** - * Get the value of a field and cast it to . + * Get the value of a field and cast it to T. * * @param instance the instance to get the value from * @param fieldName the field to get the value from @@ -269,6 +314,21 @@ public static T getCastedValue(Object instance, String fieldName) { return (T) getValue(instance, getField(instance.getClass(), fieldName)); } + @Nullable + public static T castedStaticValue(Field field) { + return getCastedValue(null, field); + } + + public static boolean castedStaticBooleanValue(Field field) { + makeAccessible(field); + try { + return field.getBoolean(null); + } catch (IllegalArgumentException | IllegalAccessException exception) { + exception.printStackTrace(); + return false; + } + } + /** * Set the value of a field. This method make the field accessible and then sets the value.
* This method doesn't throw an exception when failed, but it'll log the error to the console. @@ -311,7 +371,7 @@ public static boolean setValue(Object instance, String fieldName, Object value) * * @param clazz the class to get the method from * @param method the name of the method to find - * @param declared if the the method is declared or public + * @param declared if the the method is declared * @param arguments the classes of the method arguments * @return the requested method if it has been found, otherwise null */ @@ -364,7 +424,7 @@ public static Method getMethod(Object instance, String methodName, Class... a } @Nullable - public static Method getMethod( + public static Method getMethodThatReturns( Class clazz, Class returnType, boolean declared, @@ -396,7 +456,7 @@ public static Method getMethod( * * @param clazz the class to search the method in * @param methodName the name of the method - * @param declared if the method is declared or public + * @param declared if the method is declared * @return the method if it has been found, otherwise null */ @Nullable @@ -415,7 +475,7 @@ public static Method getMethodByName(Class clazz, String methodName, boolean * * @param clazz the class to search the method in * @param paramType the type of one of the method parameters - * @param declared if the method is declared or public + * @param declared if the method is declared * @return the method if it has been found, otherwise null */ @Nullable diff --git a/core/src/main/java/org/geysermc/floodgate/util/Utils.java b/core/src/main/java/org/geysermc/floodgate/util/Utils.java index d6efda3a..cf4b7f1a 100644 --- a/core/src/main/java/org/geysermc/floodgate/util/Utils.java +++ b/core/src/main/java/org/geysermc/floodgate/util/Utils.java @@ -25,7 +25,6 @@ package org.geysermc.floodgate.util; -import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelPipeline; import java.io.BufferedReader; @@ -33,22 +32,21 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; -import java.io.Reader; import java.io.StringWriter; -import java.nio.charset.Charset; -import java.nio.charset.CharsetDecoder; +import java.lang.annotation.Annotation; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; import java.util.Locale; import java.util.Properties; +import java.util.Set; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.regex.Pattern; +import java.util.stream.Collectors; public class Utils { - private static final Pattern NON_UNIQUE_PREFIX = Pattern.compile("^[a-zA-Z0-9_]{0,16}$"); + private static final Pattern NON_UNIQUE_PREFIX = Pattern.compile("^\\w{0,16}$"); private static final Pattern DATABASE_NAME = Pattern.compile(Constants.DATABASE_NAME_FORMAT); + public static final int MAX_DEBUG_PACKET_COUNT = 25; /** * This method is used in Addons.
Most addons can be removed once the player associated to @@ -66,33 +64,21 @@ public static void removeHandler(ChannelPipeline pipeline, String handler) { } } - public static List readAllLines(String resourcePath) throws IOException { - InputStream stream = Utils.class.getClassLoader().getResourceAsStream(resourcePath); - try (BufferedReader reader = newBufferedReader(stream, StandardCharsets.UTF_8)) { - List result = new ArrayList<>(); - for (; ; ) { - String line = reader.readLine(); - if (line == null) { - break; - } - result.add(line); - } - return result; - } - } - - public static BufferedReader newBufferedReader(InputStream inputStream, Charset charset) { - CharsetDecoder decoder = charset.newDecoder(); - Reader reader = new InputStreamReader(inputStream, decoder); - return new BufferedReader(reader); - } - + /** + * Reads a properties resource file + * @param resourceFile the resource file to read + * @return the properties file if the resource exists, otherwise null + * @throws AssertionError if something went wrong while readin the resource file + */ public static Properties readProperties(String resourceFile) { Properties properties = new Properties(); try (InputStream is = Utils.class.getClassLoader().getResourceAsStream(resourceFile)) { - properties.load(is); + if (is == null) { + return null; + } + properties.load(new InputStreamReader(is, StandardCharsets.UTF_8)); } catch (IOException e) { - e.printStackTrace(); + throw new AssertionError("Failed to read properties file", e); } return properties; } @@ -117,21 +103,6 @@ public static boolean isValidDatabaseName(String databaseName) { return DATABASE_NAME.matcher(databaseName).matches(); } - public static int readVarInt(ByteBuf buffer) { - int out = 0; - int count = 0; - byte current; - do { - current = buffer.readByte(); - out |= (current & 0x7F) << (count++ * 7); - - if (count > 5) { - throw new RuntimeException("VarInt is bigger then allowed"); - } - } while ((current & 0x80) != 0); - return out; - } - public static String getStackTrace(Throwable throwable) { StringWriter writer = new StringWriter(); throwable.printStackTrace(new PrintWriter(writer)); @@ -151,4 +122,42 @@ public static CompletableFuture failedFuture(Throwable ex) { future.completeExceptionally(ex); return future; } + + /** + * Returns a set of all the classes that are annotated by a given annotation. + * Keep in mind that these are from a set of generated annotations generated + * at compile time by the annotation processor, meaning that arbitrary annotations + * cannot be passed into this method and expected to get a set of classes back. + * + * @param annotationClass the annotation class + * @return a set of all the classes annotated by the given annotation + */ + public static Set> getGeneratedClassesForAnnotation(Class annotationClass) { + return getGeneratedClassesForAnnotation(annotationClass.getName()); + } + + /** + * Returns a set of all the classes that are annotated by a given annotation. + * Keep in mind that these are from a set of generated annotations generated + * at compile time by the annotation processor, meaning that arbitrary annotations + * cannot be passed into this method and expected to have a set of classes + * returned back. + * + * @param input the fully qualified name of the annotation + * @return a set of all the classes annotated by the given annotation + */ + public static Set> getGeneratedClassesForAnnotation(String input) { + try (InputStream annotatedClass = Utils.class.getClassLoader().getResourceAsStream(input); + BufferedReader reader = new BufferedReader(new InputStreamReader(annotatedClass))) { + return reader.lines().map(className -> { + try { + return Class.forName(className); + } catch (ClassNotFoundException ex) { + throw new RuntimeException("Failed to find class for annotation " + input, ex); + } + }).collect(Collectors.toSet()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/core/src/main/resources/languages b/core/src/main/resources/languages index 38cb4a52..df599a9b 160000 --- a/core/src/main/resources/languages +++ b/core/src/main/resources/languages @@ -1 +1 @@ -Subproject commit 38cb4a52df713cb0bc1738370aa8135c01f0cabc +Subproject commit df599a9bc9d7fce93a586591d6c0d625eefe4463 diff --git a/core/src/main/java/org/geysermc/floodgate/util/Constants.java b/core/src/main/templates/org/geysermc/floodgate/util/Constants.java similarity index 58% rename from core/src/main/java/org/geysermc/floodgate/util/Constants.java rename to core/src/main/templates/org/geysermc/floodgate/util/Constants.java index 86fbc65b..1eec715a 100644 --- a/core/src/main/java/org/geysermc/floodgate/util/Constants.java +++ b/core/src/main/templates/org/geysermc/floodgate/util/Constants.java @@ -26,10 +26,13 @@ package org.geysermc.floodgate.util; public final class Constants { - public static final int BUILD_NUMBER = Integer.parseInt("${buildNumber}"); - public static final String GIT_BRANCH = "${branch}"; + public static final String VERSION = "@floodgateVersion@"; + public static final int BUILD_NUMBER = Integer.parseInt("@buildNumber@"); + public static final String GIT_BRANCH = "@branch@"; + public static final String GIT_MAIN_BRANCH = "master"; + public static final int METRICS_ID = 14649; - public static final char COLOR_CHAR = '§'; + public static final char COLOR_CHAR = '\u00A7'; public static final boolean DEBUG_MODE = false; public static final boolean PRINT_ALL_PACKETS = false; @@ -46,14 +49,20 @@ public final class Constants { public static final String NEWS_OVERVIEW_URL = "http" + API_BASE_URL + "/v2/news/"; public static final String GET_BEDROCK_LINK = "http" + API_BASE_URL + "/v2/link/bedrock/"; + public static final String PROJECT_NAME = "floodgate"; public static final String LINK_INFO_URL = "https://link.geysermc.org/"; + public static final String LATEST_DOWNLOAD_URL = + "https://geysermc.org/download#%s"; + public static final String LATEST_VERSION_URL = + "https://download.geysermc.org/v2/projects/%s/versions/latest/builds/latest"; - public static final String NEWS_PROJECT_NAME = "floodgate"; + public static final String PROFILE_WITH_PROPERTIES_URL = + "https://sessionserver.mojang.com/session/minecraft/profile/%s?unsigned=false"; public static final String NTP_SERVER = "time.cloudflare.com"; public static final String INTERNAL_ERROR_MESSAGE = - "An internal error happened while handling Floodgate data." + + "An internal error happened while handling Floodgate data." + " Try logging in again or contact a server administrator if the issue persists."; public static final String UNSUPPORTED_DATA_VERSION = "Received an unsupported Floodgate data version." + @@ -64,4 +73,7 @@ public final class Constants { public static final int HANDSHAKE_PACKET_ID = 0; public static final int LOGIN_SUCCESS_PACKET_ID = 2; public static final int SET_COMPRESSION_PACKET_ID = 3; + + public static final String DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE = "ewogICJ0aW1lc3RhbXAiIDogMTcxNTcxNzM1NTI2MywKICAicHJvZmlsZUlkIiA6ICIyMWUzNjdkNzI1Y2Y0ZTNiYjI2OTJjNGEzMDBhNGRlYiIsCiAgInByb2ZpbGVOYW1lIiA6ICJHZXlzZXJNQyIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS8zMWY0NzdlYjFhN2JlZWU2MzFjMmNhNjRkMDZmOGY2OGZhOTNhMzM4NmQwNDQ1MmFiMjdmNDNhY2RmMWI2MGNiIgogICAgfQogIH0KfQ"; + public static final String DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE = "dFKIZ5d6vNqCSe1IFGiVLjt3cnW8qh4qNP2umg9zqkX9bvAQawuR1iuO1kCD/+ye8A6GQFv2wRCdxdrjp5+Vrr0SsWqMnsYDN8cEg6CD18mAnaKI1TYDuGbdJaqLyGqN5wqSMdHxchs9iovFkde5ir4aYdvHkA11vOTi11L4kUzETGzJ4iKVuZOv4dq+B7wFAWqp4n8QZfhixyvemFazQHlLmxnuhU+jhpZMvYY9MAaRAJonfy/wJe9LymbTe0EJ8N+NwZQDrEUzgfBFo4OIGDqRZwvydInCqkjhPMtHCSL25VOKwcFocYpRYbk4eIKM4CLjYlBiQGki+XKsPaljwjVhnT0jUupSf7yraGb3T0CsVBjhDbIIIp9nytlbO0GvxHu0TzYjkr4Iji0do5jlCKQ/OasXcL21wd6ozw0t1QZnnzxi9ewSuyYVY9ErmWdkww1OtCIgJilceEBwNAB8+mhJ062WFaYPgJQAmOREM8InW33dbbeENMFhQi4LIO5P7p9ye3B4Lrwm20xtd9wJk3lewzcs8ezh0LUF6jPSDQDivgSKU49mLCTmOi+WZh8zKjjxfVEtNZON2W+3nct0LiWBVsQ55HzlvF0FFxuRVm6pxi6MQK2ernv3DQl0hUqyQ1+RV9nfZXTQOAUzwLjKx3t2zKqyZIiNEKLE+iAXrsE="; } diff --git a/database/mongo/src/main/java/org/geysermc/floodgate/database/MongoDbDatabase.java b/database/mongo/src/main/java/org/geysermc/floodgate/database/MongoDbDatabase.java index 1f1d6eb8..19f9058b 100644 --- a/database/mongo/src/main/java/org/geysermc/floodgate/database/MongoDbDatabase.java +++ b/database/mongo/src/main/java/org/geysermc/floodgate/database/MongoDbDatabase.java @@ -40,6 +40,7 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.time.Instant; +import java.util.ArrayList; import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; @@ -336,12 +337,7 @@ private UUID bytesToUUID(String uuidBytes) { } public boolean collectionNotExists(final String collectionName) { - try (MongoCursor collectionNames = database.listCollectionNames().cursor()) { - if (collectionNames.hasNext() && collectionNames.next().equals(collectionName)) { - return false; - } - } - return true; + return !database.listCollectionNames().into(new ArrayList<>()).contains(collectionName); } } diff --git a/database/mysql/.editorconfig b/database/mysql/.editorconfig new file mode 100644 index 00000000..1f0feacf --- /dev/null +++ b/database/mysql/.editorconfig @@ -0,0 +1,4 @@ +[*] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 \ No newline at end of file diff --git a/database/mysql/build.gradle.kts b/database/mysql/build.gradle.kts index cc59658b..3975a2e0 100644 --- a/database/mysql/build.gradle.kts +++ b/database/mysql/build.gradle.kts @@ -1,10 +1,17 @@ -val mariadbClientVersion = "2.7.4" - dependencies { - provided(projects.core) - implementation("org.mariadb.jdbc", "mariadb-java-client" , mariadbClientVersion) + provided(projects.core) + + // update HikariCP when we move to Java 11+ + implementation("com.zaxxer", "HikariCP", "4.0.3") + + implementation("com.mysql", "mysql-connector-j", "8.0.32") { + exclude("com.google.protobuf", "protobuf-java") + } } description = "The Floodgate database extension for MySQL" -relocate("org.mariadb") +// relocate everything from mysql-connector-j and HikariCP +relocate("com.mysql") +relocate("com.zaxxer.hikari") +relocate("org.slf4j") diff --git a/database/mysql/src/main/java/org/geysermc/floodgate/database/MysqlDatabase.java b/database/mysql/src/main/java/org/geysermc/floodgate/database/MysqlDatabase.java index 16797c6b..60c19716 100644 --- a/database/mysql/src/main/java/org/geysermc/floodgate/database/MysqlDatabase.java +++ b/database/mysql/src/main/java/org/geysermc/floodgate/database/MysqlDatabase.java @@ -25,6 +25,8 @@ package org.geysermc.floodgate.database; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.sql.Connection; @@ -43,309 +45,296 @@ import org.geysermc.floodgate.link.CommonPlayerLink; import org.geysermc.floodgate.link.LinkRequestImpl; import org.geysermc.floodgate.util.LinkedPlayer; -import org.mariadb.jdbc.MariaDbPoolDataSource; public class MysqlDatabase extends CommonPlayerLink { - private MariaDbPoolDataSource pool; - - @Override - public void load() { - getLogger().info("Connecting to a MySQL-like database..."); - try { - Class.forName("org.mariadb.jdbc.Driver"); - MysqlConfig databaseconfig = getConfig(MysqlConfig.class); - - pool = new MariaDbPoolDataSource(); - - String hostname = databaseconfig.getHostname(); - if (hostname.contains(":")) { - String[] split = hostname.split(":"); - - pool.setServerName(split[0]); - try { - pool.setPortNumber(Integer.parseInt(split[1])); - } catch (NumberFormatException exception) { - getLogger().info("{} is not a valid port! Will use the default port", split[1]); - } - } else { - pool.setServerName(hostname); - } - - pool.setUser(databaseconfig.getUsername()); - pool.setPassword(databaseconfig.getPassword()); - pool.setDatabaseName(databaseconfig.getDatabase()); - pool.setMinPoolSize(2); - pool.setMaxPoolSize(10); - - try (Connection connection = pool.getConnection()) { - try (Statement statement = connection.createStatement()) { - statement.executeUpdate( - "CREATE TABLE IF NOT EXISTS `LinkedPlayers` ( " + - "`bedrockId` BINARY(16) NOT NULL , " + - "`javaUniqueId` BINARY(16) NOT NULL , " + - "`javaUsername` VARCHAR(16) NOT NULL , " + - " PRIMARY KEY (`bedrockId`) , " + - " INDEX (`bedrockId`, `javaUniqueId`)" + - ") ENGINE = InnoDB;" - ); - statement.executeUpdate( - "CREATE TABLE IF NOT EXISTS `LinkedPlayersRequest` ( " + - "`javaUsername` VARCHAR(16) NOT NULL , `javaUniqueId` BINARY(16) NOT NULL , " + - "`linkCode` VARCHAR(16) NOT NULL , " + - "`bedrockUsername` VARCHAR(16) NOT NULL ," + - "`requestTime` BIGINT NOT NULL , " + - " PRIMARY KEY (`javaUsername`), INDEX(`requestTime`)" + - " ) ENGINE = InnoDB;" - ); - } - } - getLogger().info("Connected to MySQL-like database."); - } catch (ClassNotFoundException exception) { - getLogger().error("The required class to load the MySQL database wasn't found"); - } catch (SQLException exception) { - getLogger().error("Error while loading database", exception); + private HikariDataSource dataSource; + + @Override + public void load() { + getLogger().info("Connecting to a MySQL-like database..."); + try { + MysqlConfig config = getConfig(MysqlConfig.class); + + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setDriverClassName("com.mysql.cj.jdbc.Driver"); + hikariConfig.setJdbcUrl("jdbc:mysql://" + config.getHostname() + "/" + config.getDatabase()); + hikariConfig.setUsername(config.getUsername()); + hikariConfig.setPassword(config.getPassword()); + hikariConfig.setPoolName("floodgate-linking-mysql"); + hikariConfig.setMinimumIdle(5); + hikariConfig.setMaximumPoolSize(10); + + dataSource = new HikariDataSource(hikariConfig); + + try (Connection connection = dataSource.getConnection()) { + try (Statement statement = connection.createStatement()) { + statement.executeUpdate( + "CREATE TABLE IF NOT EXISTS `LinkedPlayers` ( " + + "`bedrockId` BINARY(16) NOT NULL , " + + "`javaUniqueId` BINARY(16) NOT NULL , " + + "`javaUsername` VARCHAR(16) NOT NULL , " + + " PRIMARY KEY (`bedrockId`) , " + + " INDEX (`bedrockId`, `javaUniqueId`)" + + ") ENGINE = InnoDB;" + ); + statement.executeUpdate( + "CREATE TABLE IF NOT EXISTS `LinkedPlayersRequest` ( " + + "`javaUsername` VARCHAR(16) NOT NULL , `javaUniqueId` BINARY(16) NOT NULL , " + + "`linkCode` VARCHAR(16) NOT NULL , " + + "`bedrockUsername` VARCHAR(16) NOT NULL ," + + "`requestTime` BIGINT NOT NULL , " + + " PRIMARY KEY (`javaUsername`), INDEX(`requestTime`)" + + " ) ENGINE = InnoDB;" + ); } + } + getLogger().info("Connected to MySQL-like database."); + } catch (SQLException exception) { + getLogger().error("Error while loading database", exception); } - - @Override - public void stop() { - super.stop(); - pool.close(); - } - - @Override - @NonNull - public CompletableFuture getLinkedPlayer(@NonNull UUID bedrockId) { - return CompletableFuture.supplyAsync(() -> { - try (Connection connection = pool.getConnection()) { - try (PreparedStatement query = connection.prepareStatement( - "SELECT * FROM `LinkedPlayers` WHERE `bedrockId` = ?" - )) { - query.setBytes(1, uuidToBytes(bedrockId)); - try (ResultSet result = query.executeQuery()) { - if (!result.next()) { - return null; - } - String javaUsername = result.getString("javaUsername"); - UUID javaUniqueId = bytesToUUID(result.getBytes("javaUniqueId")); - return LinkedPlayer.of(javaUsername, javaUniqueId, bedrockId); - } - } - } catch (SQLException exception) { - getLogger().error("Error while getting LinkedPlayer", exception); - throw new CompletionException("Error while getting LinkedPlayer", exception); - } - }, getExecutorService()); - } - - @Override - @NonNull - public CompletableFuture isLinkedPlayer(@NonNull UUID playerId) { - return CompletableFuture.supplyAsync(() -> { - try (Connection connection = pool.getConnection()) { - try (PreparedStatement query = connection.prepareStatement( - "SELECT * FROM `LinkedPlayers` WHERE `bedrockId` = ? OR `javaUniqueId` = ?" - )) { - byte[] uuidBytes = uuidToBytes(playerId); - query.setBytes(1, uuidBytes); - query.setBytes(2, uuidBytes); - try (ResultSet result = query.executeQuery()) { - return result.next(); - } - } - } catch (SQLException exception) { - getLogger().error("Error while checking if player is a LinkedPlayer", exception); - throw new CompletionException( - "Error while checking if player is a LinkedPlayer", exception - ); + } + + @Override + public void stop() { + super.stop(); + dataSource.close(); + } + + @Override + @NonNull + public CompletableFuture getLinkedPlayer(@NonNull UUID bedrockId) { + return CompletableFuture.supplyAsync(() -> { + try (Connection connection = dataSource.getConnection()) { + try (PreparedStatement query = connection.prepareStatement( + "SELECT * FROM `LinkedPlayers` WHERE `bedrockId` = ?" + )) { + query.setBytes(1, uuidToBytes(bedrockId)); + try (ResultSet result = query.executeQuery()) { + if (!result.next()) { + return null; } - }, getExecutorService()); - } - - @Override - @NonNull - public CompletableFuture linkPlayer( - @NonNull UUID bedrockId, - @NonNull UUID javaId, - @NonNull String javaUsername) { - return CompletableFuture.runAsync( - () -> linkPlayer0(bedrockId, javaId, javaUsername), - getExecutorService()); - } - - private void linkPlayer0(UUID bedrockId, UUID javaId, String javaUsername) { - try (Connection connection = pool.getConnection()) { - try (PreparedStatement query = connection.prepareStatement( - "INSERT INTO `LinkedPlayers` VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE " + - "`javaUniqueId`=VALUES(`javaUniqueId`), " + - "`javaUsername`=VALUES(`javaUsername`);" - )) { - query.setBytes(1, uuidToBytes(bedrockId)); - query.setBytes(2, uuidToBytes(javaId)); - query.setString(3, javaUsername); - query.executeUpdate(); - } - } catch (SQLException exception) { - getLogger().error("Error while linking player", exception); - throw new CompletionException("Error while linking player", exception); + String javaUsername = result.getString("javaUsername"); + UUID javaUniqueId = bytesToUUID(result.getBytes("javaUniqueId")); + return LinkedPlayer.of(javaUsername, javaUniqueId, bedrockId); + } } - } - - @Override - @NonNull - public CompletableFuture unlinkPlayer(@NonNull UUID javaId) { - return CompletableFuture.runAsync(() -> { - try (Connection connection = pool.getConnection()) { - try (PreparedStatement query = connection.prepareStatement( - "DELETE FROM `LinkedPlayers` WHERE `javaUniqueId` = ? OR `bedrockId` = ?" - )) { - byte[] uuidBytes = uuidToBytes(javaId); - query.setBytes(1, uuidBytes); - query.setBytes(2, uuidBytes); - query.executeUpdate(); - } - } catch (SQLException exception) { - getLogger().error("Error while unlinking player", exception); - throw new CompletionException("Error while unlinking player", exception); - } - }, getExecutorService()); - } - - @Override - @NonNull - public CompletableFuture createLinkRequest( - @NonNull UUID javaId, - @NonNull String javaUsername, - @NonNull String bedrockUsername) { - return CompletableFuture.supplyAsync(() -> { - String linkCode = createCode(); - - createLinkRequest0(javaUsername, javaId, linkCode, bedrockUsername); - - return linkCode; - }, getExecutorService()); - } - - private void createLinkRequest0( - String javaUsername, - UUID javaId, - String linkCode, - String bedrockUsername) { - try (Connection connection = pool.getConnection()) { - try (PreparedStatement query = connection.prepareStatement( - "INSERT INTO `LinkedPlayersRequest` VALUES (?, ?, ?, ?, ?) " + - "ON DUPLICATE KEY UPDATE " + - "`javaUniqueId`=VALUES(`javaUniqueId`), " + - "`linkCode`=VALUES(`linkCode`), " + - "`bedrockUsername`=VALUES(`bedrockUsername`), " + - "`requestTime`=VALUES(`requestTime`);" - )) { - query.setString(1, javaUsername); - query.setBytes(2, uuidToBytes(javaId)); - query.setString(3, linkCode); - query.setString(4, bedrockUsername); - query.setLong(5, Instant.now().getEpochSecond()); - query.executeUpdate(); - } - } catch (SQLException exception) { - getLogger().error("Error while linking player", exception); - throw new CompletionException("Error while linking player", exception); + } catch (SQLException exception) { + getLogger().error("Error while getting LinkedPlayer", exception); + throw new CompletionException("Error while getting LinkedPlayer", exception); + } + }, getExecutorService()); + } + + @Override + @NonNull + public CompletableFuture isLinkedPlayer(@NonNull UUID playerId) { + return CompletableFuture.supplyAsync(() -> { + try (Connection connection = dataSource.getConnection()) { + try (PreparedStatement query = connection.prepareStatement( + "SELECT * FROM `LinkedPlayers` WHERE `bedrockId` = ? OR `javaUniqueId` = ?" + )) { + byte[] uuidBytes = uuidToBytes(playerId); + query.setBytes(1, uuidBytes); + query.setBytes(2, uuidBytes); + try (ResultSet result = query.executeQuery()) { + return result.next(); + } } + } catch (SQLException exception) { + getLogger().error("Error while checking if player is a LinkedPlayer", exception); + throw new CompletionException( + "Error while checking if player is a LinkedPlayer", exception + ); + } + }, getExecutorService()); + } + + @Override + @NonNull + public CompletableFuture linkPlayer( + @NonNull UUID bedrockId, + @NonNull UUID javaId, + @NonNull String javaUsername) { + return CompletableFuture.runAsync( + () -> linkPlayer0(bedrockId, javaId, javaUsername), + getExecutorService()); + } + + private void linkPlayer0(UUID bedrockId, UUID javaId, String javaUsername) { + try (Connection connection = dataSource.getConnection()) { + try (PreparedStatement query = connection.prepareStatement( + "INSERT INTO `LinkedPlayers` VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE " + + "`javaUniqueId`=VALUES(`javaUniqueId`), " + + "`javaUsername`=VALUES(`javaUsername`);" + )) { + query.setBytes(1, uuidToBytes(bedrockId)); + query.setBytes(2, uuidToBytes(javaId)); + query.setString(3, javaUsername); + query.executeUpdate(); + } + } catch (SQLException exception) { + getLogger().error("Error while linking player", exception); + throw new CompletionException("Error while linking player", exception); } - - private void removeLinkRequest(String javaUsername) { - try (Connection connection = pool.getConnection()) { - try (PreparedStatement query = connection.prepareStatement( - "DELETE FROM `LinkedPlayersRequest` WHERE `javaUsername` = ?" - )) { - query.setString(1, javaUsername); - query.executeUpdate(); - } - } catch (SQLException exception) { - getLogger().error("Error while cleaning up LinkRequest", exception); + } + + @Override + @NonNull + public CompletableFuture unlinkPlayer(@NonNull UUID javaId) { + return CompletableFuture.runAsync(() -> { + try (Connection connection = dataSource.getConnection()) { + try (PreparedStatement query = connection.prepareStatement( + "DELETE FROM `LinkedPlayers` WHERE `javaUniqueId` = ? OR `bedrockId` = ?" + )) { + byte[] uuidBytes = uuidToBytes(javaId); + query.setBytes(1, uuidBytes); + query.setBytes(2, uuidBytes); + query.executeUpdate(); } + } catch (SQLException exception) { + getLogger().error("Error while unlinking player", exception); + throw new CompletionException("Error while unlinking player", exception); + } + }, getExecutorService()); + } + + @Override + @NonNull + public CompletableFuture createLinkRequest( + @NonNull UUID javaId, + @NonNull String javaUsername, + @NonNull String bedrockUsername + ) { + return CompletableFuture.supplyAsync(() -> { + String linkCode = createCode(); + + createLinkRequest0(javaUsername, javaId, linkCode, bedrockUsername); + + return linkCode; + }, getExecutorService()); + } + + private void createLinkRequest0( + String javaUsername, + UUID javaId, + String linkCode, + String bedrockUsername + ) { + try (Connection connection = dataSource.getConnection()) { + try (PreparedStatement query = connection.prepareStatement( + "INSERT INTO `LinkedPlayersRequest` VALUES (?, ?, ?, ?, ?) " + + "ON DUPLICATE KEY UPDATE " + + "`javaUniqueId`=VALUES(`javaUniqueId`), " + + "`linkCode`=VALUES(`linkCode`), " + + "`bedrockUsername`=VALUES(`bedrockUsername`), " + + "`requestTime`=VALUES(`requestTime`);" + )) { + query.setString(1, javaUsername); + query.setBytes(2, uuidToBytes(javaId)); + query.setString(3, linkCode); + query.setString(4, bedrockUsername); + query.setLong(5, Instant.now().getEpochSecond()); + query.executeUpdate(); + } + } catch (SQLException exception) { + getLogger().error("Error while linking player", exception); + throw new CompletionException("Error while linking player", exception); } - - @Override - @NonNull - public CompletableFuture verifyLinkRequest( - @NonNull UUID bedrockId, - @NonNull String javaUsername, - @NonNull String bedrockUsername, - @NonNull String code) { - return CompletableFuture.supplyAsync(() -> { - LinkRequest request = getLinkRequest0(javaUsername); - - if (request == null || !isRequestedPlayer(request, bedrockId)) { - return LinkRequestResult.NO_LINK_REQUESTED; - } - - if (!request.getLinkCode().equals(code)) { - return LinkRequestResult.INVALID_CODE; - } - - // link request can be removed. Doesn't matter if the request is expired or not - removeLinkRequest(javaUsername); - - if (request.isExpired(getVerifyLinkTimeout())) { - return LinkRequestResult.REQUEST_EXPIRED; - } - - linkPlayer0(bedrockId, request.getJavaUniqueId(), javaUsername); - return LinkRequestResult.LINK_COMPLETED; - }, getExecutorService()); - } - - private LinkRequest getLinkRequest0(String javaUsername) { - try (Connection connection = pool.getConnection()) { - try (PreparedStatement query = connection.prepareStatement( - "SELECT * FROM `LinkedPlayersRequest` WHERE `javaUsername` = ?" - )) { - query.setString(1, javaUsername); - - try (ResultSet result = query.executeQuery()) { - if (result.next()) { - UUID javaId = bytesToUUID(result.getBytes(2)); - String linkCode = result.getString(3); - String bedrockUsername = result.getString(4); - long requestTime = result.getLong(5); - return new LinkRequestImpl(javaUsername, javaId, linkCode, bedrockUsername, - requestTime); - } - } - } - } catch (SQLException exception) { - getLogger().error("Error while getLinkRequest", exception); - throw new CompletionException("Error while getLinkRequest", exception); - } - return null; + } + + private void removeLinkRequest(String javaUsername) { + try (Connection connection = dataSource.getConnection()) { + try (PreparedStatement query = connection.prepareStatement( + "DELETE FROM `LinkedPlayersRequest` WHERE `javaUsername` = ?" + )) { + query.setString(1, javaUsername); + query.executeUpdate(); + } + } catch (SQLException exception) { + getLogger().error("Error while cleaning up LinkRequest", exception); } - - public void cleanLinkRequests() { - try (Connection connection = pool.getConnection()) { - try (PreparedStatement query = connection.prepareStatement( - "DELETE FROM `LinkedPlayersRequest` WHERE `requestTime` < ?" - )) { - query.setLong(1, Instant.now().getEpochSecond() - getVerifyLinkTimeout()); - query.executeUpdate(); - } - } catch (SQLException exception) { - getLogger().error("Error while cleaning up link requests", exception); + } + + @Override + @NonNull + public CompletableFuture verifyLinkRequest( + @NonNull UUID bedrockId, + @NonNull String javaUsername, + @NonNull String bedrockUsername, + @NonNull String code + ) { + return CompletableFuture.supplyAsync(() -> { + LinkRequest request = getLinkRequest0(javaUsername); + + if (request == null || !isRequestedPlayer(request, bedrockId)) { + return LinkRequestResult.NO_LINK_REQUESTED; + } + + if (!request.getLinkCode().equals(code)) { + return LinkRequestResult.INVALID_CODE; + } + + // link request can be removed. Doesn't matter if the request is expired or not + removeLinkRequest(javaUsername); + + if (request.isExpired(getVerifyLinkTimeout())) { + return LinkRequestResult.REQUEST_EXPIRED; + } + + linkPlayer0(bedrockId, request.getJavaUniqueId(), javaUsername); + return LinkRequestResult.LINK_COMPLETED; + }, getExecutorService()); + } + + private LinkRequest getLinkRequest0(String javaUsername) { + try (Connection connection = dataSource.getConnection()) { + try (PreparedStatement query = connection.prepareStatement( + "SELECT * FROM `LinkedPlayersRequest` WHERE `javaUsername` = ?" + )) { + query.setString(1, javaUsername); + + try (ResultSet result = query.executeQuery()) { + if (result.next()) { + UUID javaId = bytesToUUID(result.getBytes(2)); + String linkCode = result.getString(3); + String bedrockUsername = result.getString(4); + long requestTime = result.getLong(5); + return new LinkRequestImpl(javaUsername, javaId, linkCode, bedrockUsername, + requestTime); + } } + } + } catch (SQLException exception) { + getLogger().error("Error while getLinkRequest", exception); + throw new CompletionException("Error while getLinkRequest", exception); } - - private byte[] uuidToBytes(UUID uuid) { - byte[] uuidBytes = new byte[16]; - ByteBuffer.wrap(uuidBytes) - .order(ByteOrder.BIG_ENDIAN) - .putLong(uuid.getMostSignificantBits()) - .putLong(uuid.getLeastSignificantBits()); - return uuidBytes; - } - - private UUID bytesToUUID(byte[] uuidBytes) { - ByteBuffer buf = ByteBuffer.wrap(uuidBytes); - return new UUID(buf.getLong(), buf.getLong()); + return null; + } + + public void cleanLinkRequests() { + try (Connection connection = dataSource.getConnection()) { + try (PreparedStatement query = connection.prepareStatement( + "DELETE FROM `LinkedPlayersRequest` WHERE `requestTime` < ?" + )) { + query.setLong(1, Instant.now().getEpochSecond() - getVerifyLinkTimeout()); + query.executeUpdate(); + } + } catch (SQLException exception) { + getLogger().error("Error while cleaning up link requests", exception); } - + } + + private byte[] uuidToBytes(UUID uuid) { + byte[] uuidBytes = new byte[16]; + ByteBuffer.wrap(uuidBytes) + .order(ByteOrder.BIG_ENDIAN) + .putLong(uuid.getMostSignificantBits()) + .putLong(uuid.getLeastSignificantBits()); + return uuidBytes; + } + + private UUID bytesToUUID(byte[] uuidBytes) { + ByteBuffer buf = ByteBuffer.wrap(uuidBytes); + return new UUID(buf.getLong(), buf.getLong()); + } } diff --git a/database/mysql/src/main/java/org/geysermc/floodgate/database/config/MysqlConfig.java b/database/mysql/src/main/java/org/geysermc/floodgate/database/config/MysqlConfig.java index 0a2e0c26..b85748e7 100644 --- a/database/mysql/src/main/java/org/geysermc/floodgate/database/config/MysqlConfig.java +++ b/database/mysql/src/main/java/org/geysermc/floodgate/database/config/MysqlConfig.java @@ -29,8 +29,8 @@ @Getter public class MysqlConfig implements DatabaseConfig { - private String hostname = "localhost"; - private String database = "floodgate"; - private String username = "floodgate"; - private String password; + private String hostname = "localhost"; + private String database = "floodgate"; + private String username = "floodgate"; + private String password; } diff --git a/gradle.properties b/gradle.properties index af7d8325..69e11adc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,5 @@ org.gradle.configureondemand=true org.gradle.caching=true -org.gradle.parallel=true \ No newline at end of file +org.gradle.parallel=true + +version=2.2.4-SNAPSHOT \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e6e5897..070cb702 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c7873..c53aefaa 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright © 2015-2021 the original authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -32,10 +32,10 @@ # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # diff --git a/settings.gradle.kts b/settings.gradle.kts index 11653aab..eee1e4e4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,8 +1,11 @@ +@file:Suppress("UnstableApiUsage") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { + mavenLocal() + // Geyser, Cumulus etc. maven("https://repo.opencollab.dev/maven-releases") { mavenContent { releasesOnly() } @@ -12,7 +15,13 @@ dependencyResolutionManagement { } // Paper, Velocity - maven("https://papermc.io/repo/repository/maven-public") +// maven("https://repo.papermc.io/repository/maven-releases") { +// mavenContent { releasesOnly() } +// } +// maven("https://repo.papermc.io/repository/maven-snapshots") { +// mavenContent { snapshotsOnly() } +// } + maven("https://repo.papermc.io/repository/maven-public") // Spigot maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots") { mavenContent { snapshotsOnly() } @@ -41,7 +50,8 @@ pluginManagement { gradlePluginPortal() } plugins { - id("net.kyori.blossom") version "1.2.0" + id("net.kyori.indra") + id("net.kyori.indra.git") } includeBuild("build-logic") } @@ -49,6 +59,7 @@ pluginManagement { rootProject.name = "floodgate-parent" include(":api") +include(":ap") include(":core") include(":bungee") include(":spigot") diff --git a/spigot/build.gradle.kts b/spigot/build.gradle.kts index 8bd8b9c0..6e95bae1 100644 --- a/spigot/build.gradle.kts +++ b/spigot/build.gradle.kts @@ -2,28 +2,42 @@ var authlibVersion = "1.5.21" var guavaVersion = "21.0" var gsonVersion = "2.8.5" +indra { + javaVersions { + // For Folia + target(8) + minimumToolchain(17) + } +} + dependencies { api(projects.core) - implementation("cloud.commandframework", "cloud-bukkit", Versions.cloudVersion) + implementation("org.incendo", "cloud-paper", Versions.cloudVersion) // hack to make pre 1.12 work implementation("com.google.guava", "guava", guavaVersion) + + compileOnlyApi("dev.folia", "folia-api", Versions.spigotVersion) { + attributes { + attribute(TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE, 17) + } + } } relocate("com.google.inject") relocate("net.kyori") -relocate("cloud.commandframework") +relocate("org.incendo.cloud") relocate("io.leangen.geantyref") // used in cloud // hack to make pre 1.12 work relocate("com.google.common") relocate("com.google.guava") // hack to make (old versions? of) Paper work relocate("it.unimi") +// since 1.20 +relocate("org.yaml") // these dependencies are already present on the platform -provided("com.destroystokyo.paper", "paper-api", Versions.spigotVersion) provided("com.mojang", "authlib", authlibVersion) provided("io.netty", "netty-transport", Versions.nettyVersion) provided("io.netty", "netty-codec", Versions.nettyVersion) provided("com.google.code.gson", "gson", gsonVersion) -provided("org.yaml", "snakeyaml", Versions.snakeyamlVersion) diff --git a/spigot/src/main/java/org/geysermc/floodgate/SpigotPlugin.java b/spigot/src/main/java/org/geysermc/floodgate/SpigotPlugin.java index 6254cc17..a12c41a8 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/SpigotPlugin.java +++ b/spigot/src/main/java/org/geysermc/floodgate/SpigotPlugin.java @@ -27,6 +27,7 @@ import com.google.inject.Guice; import com.google.inject.Injector; +import org.bukkit.Bukkit; import org.bukkit.plugin.java.JavaPlugin; import org.geysermc.floodgate.api.handshake.HandshakeHandlers; import org.geysermc.floodgate.api.logger.FloodgateLogger; @@ -43,7 +44,7 @@ import org.geysermc.floodgate.util.SpigotProtocolSupportListener; public final class SpigotPlugin extends JavaPlugin { - private SpigotPlatform platform; + private FloodgatePlatform platform; private Injector injector; @Override @@ -54,7 +55,7 @@ public void onLoad() { new SpigotPlatformModule(this) ); - platform = injector.getInstance(SpigotPlatform.class); + platform = injector.getInstance(FloodgatePlatform.class); long endCtm = System.currentTimeMillis(); injector.getInstance(FloodgateLogger.class) @@ -73,7 +74,6 @@ public void onEnable() { (usePaperListener ? new PaperListenerModule() : new SpigotListenerModule()) ); - //todo add proper support for disabling things on shutdown and enabling this on enable injector.getInstance(HandshakeHandlers.class) .addHandshakeHandler(injector.getInstance(SpigotHandshakeHandler.class)); diff --git a/spigot/src/main/java/org/geysermc/floodgate/addon/data/SpigotDataHandler.java b/spigot/src/main/java/org/geysermc/floodgate/addon/data/SpigotDataHandler.java index bc5742db..383fa7e9 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/addon/data/SpigotDataHandler.java +++ b/spigot/src/main/java/org/geysermc/floodgate/addon/data/SpigotDataHandler.java @@ -29,17 +29,26 @@ import static org.geysermc.floodgate.util.ReflectionUtils.setValue; import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; import io.netty.channel.Channel; import io.netty.util.AttributeKey; +import java.lang.reflect.InvocationTargetException; import java.net.InetSocketAddress; import org.geysermc.floodgate.api.player.FloodgatePlayer; import org.geysermc.floodgate.config.FloodgateConfig; import org.geysermc.floodgate.player.FloodgateHandshakeHandler; import org.geysermc.floodgate.player.FloodgateHandshakeHandler.HandshakeResult; import org.geysermc.floodgate.util.ClassNames; +import org.geysermc.floodgate.util.Constants; import org.geysermc.floodgate.util.ProxyUtils; public final class SpigotDataHandler extends CommonDataHandler { + private static final Property DEFAULT_TEXTURE_PROPERTY = new Property( + "textures", + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE, + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE + ); + private Object networkManager; private FloodgatePlayer player; private boolean proxyData; @@ -57,9 +66,27 @@ protected void setNewIp(Channel channel, InetSocketAddress newIp) { } @Override - protected Object setHostname(Object handshakePacket, String hostname) { - setValue(handshakePacket, ClassNames.HANDSHAKE_HOST, hostname); - return handshakePacket; + protected Object setHostname(Object handshakePacket, String hostname) throws IllegalStateException { + if (ClassNames.IS_PRE_1_20_2) { + // 1.20.1 and below + setValue(handshakePacket, ClassNames.HANDSHAKE_HOST, hostname); + + return handshakePacket; + } else { + // 1.20.2 and above + try { + Object[] components = new Object[]{ + ClassNames.HANDSHAKE_PROTOCOL.get(handshakePacket), + hostname, + ClassNames.HANDSHAKE_PORT.get(handshakePacket), + ClassNames.HANDSHAKE_INTENTION.get(handshakePacket) + }; + + return ClassNames.HANDSHAKE_PACKET_CONSTRUCTOR.newInstance(components); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new IllegalStateException("Failed to create new Handshake packet", e); + } + } } @Override @@ -148,25 +175,43 @@ private boolean checkAndHandleLogin(Object packet) throws Exception { } } - // set the player his GameProfile, we can't change the username without this GameProfile gameProfile = new GameProfile( player.getCorrectUniqueId(), player.getCorrectUsername() ); - setValue(packetListener, ClassNames.LOGIN_PROFILE, gameProfile); - // we have to fake the offline player (login) cycle - // just like on Spigot: - - // LoginListener#initUUID - // new LoginHandler().fireEvents(); - - // and the tick of LoginListener will do the rest + if (!player.isLinked()) { + // Otherwise game server will try to fetch the skin from Mojang. + // No need to worry that this overrides proxy data, because those won't reach this + // method / are already removed (in the case of username validation) + gameProfile.getProperties().put("textures", DEFAULT_TEXTURE_PROPERTY); + } - ClassNames.INIT_UUID.invoke(packetListener); + // we have to fake the offline player (login) cycle - Object loginHandler = - ClassNames.LOGIN_HANDLER_CONSTRUCTOR.newInstance(packetListener); - ClassNames.FIRE_LOGIN_EVENTS.invoke(loginHandler); + if (ClassNames.IS_PRE_1_20_2) { + // 1.20.1 and below + // - set profile, otherwise the username doesn't change + // - LoginListener#initUUID + // - new LoginHandler().fireEvents(); + // and the tick of LoginListener will do the rest + + Object loginHandler = ClassNames.LOGIN_HANDLER_CONSTRUCTOR.newInstance(packetListener); + setValue(packetListener, ClassNames.LOGIN_PROFILE, gameProfile); + ClassNames.INIT_UUID.invoke(packetListener); + ClassNames.FIRE_LOGIN_EVENTS.invoke(loginHandler); + } else if (!ClassNames.IS_POST_LOGIN_HANDLER) { + // 1.20.2 until somewhere in 1.20.4 we can directly register the profile + + Object loginHandler = ClassNames.LOGIN_HANDLER_CONSTRUCTOR.newInstance(packetListener); + ClassNames.FIRE_LOGIN_EVENTS_GAME_PROFILE.invoke(loginHandler, gameProfile); + } else { + // somewhere during 1.20.4 md_5 moved stuff to CraftBukkit + + // LoginListener#callPlayerPreLoginEvents(GameProfile) + // LoginListener#startClientVerification(GameProfile) + ClassNames.CALL_PLAYER_PRE_LOGIN_EVENTS.invoke(packetListener, gameProfile); + ClassNames.START_CLIENT_VERIFICATION.invoke(packetListener, gameProfile); + } ctx.pipeline().remove(this); return true; diff --git a/spigot/src/main/java/org/geysermc/floodgate/inject/spigot/SpigotInjector.java b/spigot/src/main/java/org/geysermc/floodgate/inject/spigot/SpigotInjector.java index 9ea99b1a..17812624 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/inject/spigot/SpigotInjector.java +++ b/spigot/src/main/java/org/geysermc/floodgate/inject/spigot/SpigotInjector.java @@ -25,25 +25,28 @@ package org.geysermc.floodgate.inject.spigot; +import com.google.inject.Inject; +import com.google.inject.Singleton; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import java.lang.reflect.Field; -import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; import java.util.List; import lombok.Getter; -import lombok.RequiredArgsConstructor; +import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.inject.CommonPlatformInjector; import org.geysermc.floodgate.util.ClassNames; import org.geysermc.floodgate.util.ReflectionUtils; -@RequiredArgsConstructor +@Singleton public final class SpigotInjector extends CommonPlatformInjector { + @Inject private FloodgateLogger logger; + private Object serverConnection; private String injectedFieldName; @@ -51,54 +54,56 @@ public final class SpigotInjector extends CommonPlatformInjector { @Override @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") - public boolean inject() throws Exception { + public void inject() throws Exception { if (isInjected()) { - return true; + return; + } + + Object serverConnection = getServerConnection(); + if (serverConnection == null) { + throw new RuntimeException("Unable to find server connection"); } - if (getServerConnection() != null) { - for (Field field : serverConnection.getClass().getDeclaredFields()) { - if (field.getType() == List.class) { - field.setAccessible(true); + for (Field field : serverConnection.getClass().getDeclaredFields()) { + if (field.getType() == List.class) { + field.setAccessible(true); - ParameterizedType parameterType = ((ParameterizedType) field.getGenericType()); - Type listType = parameterType.getActualTypeArguments()[0]; + ParameterizedType parameterType = ((ParameterizedType) field.getGenericType()); + Type listType = parameterType.getActualTypeArguments()[0]; - // the list we search has ChannelFuture as type - if (listType != ChannelFuture.class) { - continue; - } + // the list we search has ChannelFuture as type + if (listType != ChannelFuture.class) { + continue; + } - injectedFieldName = field.getName(); - List newList = new CustomList((List) field.get(serverConnection)) { - @Override - public void onAdd(Object object) { - try { - injectClient((ChannelFuture) object); - } catch (Exception exception) { - exception.printStackTrace(); - } + injectedFieldName = field.getName(); + List newList = new CustomList((List) field.get(serverConnection)) { + @Override + public void onAdd(Object object) { + try { + injectClient((ChannelFuture) object); + } catch (Exception exception) { + exception.printStackTrace(); } - }; - - // inject existing - synchronized (newList) { - for (Object object : newList) { - try { - injectClient((ChannelFuture) object); - } catch (Exception exception) { - exception.printStackTrace(); - } + } + }; + + // inject existing + synchronized (newList) { + for (Object object : newList) { + try { + injectClient((ChannelFuture) object); + } catch (Exception exception) { + exception.printStackTrace(); } } - - field.set(serverConnection, newList); - injected = true; - return true; } + + field.set(serverConnection, newList); + injected = true; + return; } } - return false; } public void injectClient(ChannelFuture future) { @@ -120,36 +125,48 @@ protected void initChannel(Channel channel) { } @Override - public boolean removeInjection() throws Exception { + public void removeInjection() { if (!isInjected()) { - return true; - } - - // remove injection from clients - for (Channel channel : getInjectedClients()) { - removeAddonsCall(channel); + return; } - getInjectedClients().clear(); - // and change the list back to the original + // let's change the list back to the original first + // so that new connections are not handled through our custom list Object serverConnection = getServerConnection(); if (serverConnection != null) { Field field = ReflectionUtils.getField(serverConnection.getClass(), injectedFieldName); - List list = (List) ReflectionUtils.getValue(serverConnection, field); + Object value = ReflectionUtils.getValue(serverConnection, field); - if (list instanceof CustomList) { - CustomList customList = (CustomList) list; + if (value instanceof CustomList) { + // all we have to do is replace the list with the original list. + // the original list is up-to-date, so we don't have to clear/add/whatever anything + CustomList customList = (CustomList) value; ReflectionUtils.setValue(serverConnection, field, customList.getOriginalList()); - customList.clear(); - customList.addAll(list); + return; } + + // we could replace all references of CustomList that are directly in 'value', but that + // only brings you so far. ProtocolLib for example stores the original value + // (which would be our CustomList e.g.) in a separate object + logger.debug( + "Unable to remove all references of Floodgate due to {}! ", + value.getClass().getName() + ); + } + + // remove injection from clients + for (Channel channel : injectedClients()) { + removeAddonsCall(channel); } + //todo make sure that all references are removed from the channels, + // except from one AttributeKey with Floodgate player data which could be used + // after reloading + injected = false; - return true; } - public Object getServerConnection() throws IllegalAccessException, InvocationTargetException { + private Object getServerConnection() { if (serverConnection != null) { return serverConnection; } @@ -158,14 +175,11 @@ public Object getServerConnection() throws IllegalAccessException, InvocationTar // method by CraftBukkit to get the instance of the MinecraftServer Object minecraftServerInstance = ReflectionUtils.invokeStatic(minecraftServer, "getServer"); - for (Method method : minecraftServer.getDeclaredMethods()) { - if (ClassNames.SERVER_CONNECTION.equals(method.getReturnType())) { - // making sure that it's a getter - if (method.getParameterTypes().length == 0) { - serverConnection = method.invoke(minecraftServerInstance); - } - } - } + Method method = ReflectionUtils.getMethodThatReturns( + minecraftServer, ClassNames.SERVER_CONNECTION, true + ); + + serverConnection = ReflectionUtils.invoke(minecraftServerInstance, method); return serverConnection; } diff --git a/spigot/src/main/java/org/geysermc/floodgate/listener/PaperProfileListener.java b/spigot/src/main/java/org/geysermc/floodgate/listener/PaperProfileListener.java index 5672ead8..836b9fb2 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/listener/PaperProfileListener.java +++ b/spigot/src/main/java/org/geysermc/floodgate/listener/PaperProfileListener.java @@ -35,8 +35,15 @@ import org.bukkit.event.Listener; import org.geysermc.floodgate.api.SimpleFloodgateApi; import org.geysermc.floodgate.api.player.FloodgatePlayer; +import org.geysermc.floodgate.util.Constants; public final class PaperProfileListener implements Listener { + private static final ProfileProperty DEFAULT_TEXTURE_PROPERTY = new ProfileProperty( + "textures", + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE, + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE + ); + @Inject private SimpleFloodgateApi api; @EventHandler @@ -59,7 +66,8 @@ public void onFill(PreFillProfileEvent event) { } Set properties = new HashSet<>(event.getPlayerProfile().getProperties()); - properties.add(new ProfileProperty("textures", "", "")); + properties.add(DEFAULT_TEXTURE_PROPERTY); + event.setProperties(properties); } } diff --git a/spigot/src/main/java/org/geysermc/floodgate/listener/SpigotListener.java b/spigot/src/main/java/org/geysermc/floodgate/listener/SpigotListener.java index 3574af44..8c19dd74 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/listener/SpigotListener.java +++ b/spigot/src/main/java/org/geysermc/floodgate/listener/SpigotListener.java @@ -30,7 +30,7 @@ import org.bukkit.event.EventHandler; import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; -import org.bukkit.event.player.PlayerLoginEvent; +import org.bukkit.event.player.PlayerJoinEvent; import org.bukkit.event.player.PlayerQuitEvent; import org.geysermc.floodgate.api.SimpleFloodgateApi; import org.geysermc.floodgate.api.logger.FloodgateLogger; @@ -43,11 +43,8 @@ public final class SpigotListener implements Listener { @Inject private FloodgateLogger logger; @EventHandler(priority = EventPriority.MONITOR) - public void onPlayerLogin(PlayerLoginEvent event) { + public void onPlayerJoin(PlayerJoinEvent event) { UUID uniqueId = event.getPlayer().getUniqueId(); - if (event.getResult() != PlayerLoginEvent.Result.ALLOWED) { - return; - } // if there was another player with the same uuid online, // he would've been disconnected by now diff --git a/spigot/src/main/java/org/geysermc/floodgate/module/SpigotCommandModule.java b/spigot/src/main/java/org/geysermc/floodgate/module/SpigotCommandModule.java index 4db0c9f6..d616181e 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/module/SpigotCommandModule.java +++ b/spigot/src/main/java/org/geysermc/floodgate/module/SpigotCommandModule.java @@ -25,15 +25,11 @@ package org.geysermc.floodgate.module; -import cloud.commandframework.CommandManager; -import cloud.commandframework.bukkit.BukkitCommandManager; -import cloud.commandframework.execution.CommandExecutionCoordinator; import com.google.inject.Provides; import com.google.inject.Singleton; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.bukkit.Bukkit; -import org.bukkit.command.CommandSender; import org.bukkit.permissions.PermissionDefault; import org.bukkit.plugin.PluginManager; import org.geysermc.floodgate.SpigotPlugin; @@ -41,6 +37,10 @@ import org.geysermc.floodgate.platform.command.CommandUtil; import org.geysermc.floodgate.player.FloodgateCommandPreprocessor; import org.geysermc.floodgate.player.UserAudience; +import org.geysermc.floodgate.player.audience.FloodgateSenderMapper; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.paper.LegacyPaperCommandManager; @RequiredArgsConstructor public final class SpigotCommandModule extends CommandModule { @@ -56,11 +56,10 @@ protected void configure() { @Singleton @SneakyThrows public CommandManager commandManager(CommandUtil commandUtil) { - CommandManager commandManager = new BukkitCommandManager<>( + CommandManager commandManager = new LegacyPaperCommandManager<>( plugin, - CommandExecutionCoordinator.simpleCoordinator(), - commandUtil::getUserAudience, - audience -> (CommandSender) audience.source() + ExecutionCoordinator.simpleCoordinator(), + new FloodgateSenderMapper<>(commandUtil) ); commandManager.registerCommandPreProcessor(new FloodgateCommandPreprocessor<>(commandUtil)); return commandManager; diff --git a/spigot/src/main/java/org/geysermc/floodgate/module/SpigotPlatformModule.java b/spigot/src/main/java/org/geysermc/floodgate/module/SpigotPlatformModule.java index 94d8f12a..1c03dc95 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/module/SpigotPlatformModule.java +++ b/spigot/src/main/java/org/geysermc/floodgate/module/SpigotPlatformModule.java @@ -29,6 +29,8 @@ import com.google.inject.Provides; import com.google.inject.Singleton; import com.google.inject.name.Named; +import com.google.inject.name.Names; +import java.util.logging.Logger; import lombok.RequiredArgsConstructor; import org.bukkit.event.Listener; import org.bukkit.plugin.java.JavaPlugin; @@ -42,6 +44,7 @@ import org.geysermc.floodgate.platform.command.CommandUtil; import org.geysermc.floodgate.platform.listener.ListenerRegistration; import org.geysermc.floodgate.platform.pluginmessage.PluginMessageUtils; +import org.geysermc.floodgate.platform.util.PlatformUtils; import org.geysermc.floodgate.pluginmessage.PluginMessageRegistration; import org.geysermc.floodgate.pluginmessage.SpigotPluginMessageRegistration; import org.geysermc.floodgate.pluginmessage.SpigotPluginMessageUtils; @@ -49,22 +52,27 @@ import org.geysermc.floodgate.skin.SkinApplier; import org.geysermc.floodgate.util.LanguageManager; import org.geysermc.floodgate.util.SpigotCommandUtil; +import org.geysermc.floodgate.util.SpigotPlatformUtils; import org.geysermc.floodgate.util.SpigotVersionSpecificMethods; @RequiredArgsConstructor public final class SpigotPlatformModule extends AbstractModule { private final SpigotPlugin plugin; - @Provides - @Singleton - public JavaPlugin javaPlugin() { - return plugin; + @Override + protected void configure() { + bind(SpigotPlugin.class).toInstance(plugin); + bind(PlatformUtils.class).to(SpigotPlatformUtils.class); + bind(CommonPlatformInjector.class).to(SpigotInjector.class); + bind(Logger.class).annotatedWith(Names.named("logger")).toInstance(plugin.getLogger()); + bind(FloodgateLogger.class).to(JavaUtilFloodgateLogger.class); + bind(SkinApplier.class).to(SpigotSkinApplier.class); } @Provides @Singleton - public FloodgateLogger floodgateLogger(LanguageManager languageManager) { - return new JavaUtilFloodgateLogger(plugin.getLogger(), languageManager); + public JavaPlugin javaPlugin() { + return plugin; } /* @@ -78,7 +86,7 @@ public CommandUtil commandUtil( SpigotVersionSpecificMethods versionSpecificMethods, LanguageManager languageManager) { return new SpigotCommandUtil( - languageManager, plugin.getServer(), api, versionSpecificMethods, plugin); + languageManager, plugin.getServer(), api, versionSpecificMethods); } @Provides @@ -91,12 +99,6 @@ public ListenerRegistration listenerRegistration() { DebugAddon / PlatformInjector */ - @Provides - @Singleton - public CommonPlatformInjector platformInjector() { - return new SpigotInjector(); - } - @Provides @Named("packetEncoder") public String packetEncoder() { @@ -137,12 +139,6 @@ public PluginMessageRegistration pluginMessageRegister() { return new SpigotPluginMessageRegistration(plugin); } - @Provides - @Singleton - public SkinApplier skinApplier(SpigotVersionSpecificMethods versionSpecificMethods) { - return new SpigotSkinApplier(versionSpecificMethods, plugin); - } - @Provides @Singleton public SpigotVersionSpecificMethods versionSpecificMethods() { diff --git a/spigot/src/main/java/org/geysermc/floodgate/pluginmessage/SpigotSkinApplier.java b/spigot/src/main/java/org/geysermc/floodgate/pluginmessage/SpigotSkinApplier.java index 86f64c96..d312afdc 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/pluginmessage/SpigotSkinApplier.java +++ b/spigot/src/main/java/org/geysermc/floodgate/pluginmessage/SpigotSkinApplier.java @@ -25,32 +25,31 @@ package org.geysermc.floodgate.pluginmessage; +import com.google.inject.Inject; +import com.google.inject.Singleton; import com.mojang.authlib.GameProfile; import com.mojang.authlib.properties.Property; import com.mojang.authlib.properties.PropertyMap; import org.bukkit.Bukkit; import org.bukkit.entity.Player; -import org.geysermc.floodgate.SpigotPlugin; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.floodgate.api.event.skin.SkinApplyEvent; +import org.geysermc.floodgate.api.event.skin.SkinApplyEvent.SkinData; import org.geysermc.floodgate.api.player.FloodgatePlayer; +import org.geysermc.floodgate.event.EventBus; +import org.geysermc.floodgate.event.skin.SkinApplyEventImpl; import org.geysermc.floodgate.skin.SkinApplier; -import org.geysermc.floodgate.skin.SkinData; import org.geysermc.floodgate.util.ClassNames; import org.geysermc.floodgate.util.ReflectionUtils; import org.geysermc.floodgate.util.SpigotVersionSpecificMethods; +@Singleton public final class SpigotSkinApplier implements SkinApplier { - private final SpigotVersionSpecificMethods versionSpecificMethods; - private final SpigotPlugin plugin; - - public SpigotSkinApplier( - SpigotVersionSpecificMethods versionSpecificMethods, - SpigotPlugin plugin) { - this.versionSpecificMethods = versionSpecificMethods; - this.plugin = plugin; - } + @Inject private SpigotVersionSpecificMethods versionSpecificMethods; + @Inject private EventBus eventBus; @Override - public void applySkin(FloodgatePlayer floodgatePlayer, SkinData skinData) { + public void applySkin(@NonNull FloodgatePlayer floodgatePlayer, @NonNull SkinData skinData) { applySkin0(floodgatePlayer, skinData, true); } @@ -60,9 +59,10 @@ private void applySkin0(FloodgatePlayer floodgatePlayer, SkinData skinData, bool // player is probably not logged in yet if (player == null) { if (firstTry) { - Bukkit.getScheduler().runTaskLater(plugin, + versionSpecificMethods.schedule( () -> applySkin0(floodgatePlayer, skinData, false), - 10 * 1000); + 10 * 20 + ); } return; } @@ -73,20 +73,35 @@ private void applySkin0(FloodgatePlayer floodgatePlayer, SkinData skinData, bool throw new IllegalStateException("The GameProfile cannot be null! " + player.getName()); } + // Need to be careful here - getProperties() returns an authlib PropertyMap, which extends + // MultiMap from Guava. Floodgate relocates Guava. PropertyMap properties = profile.getProperties(); - properties.removeAll("textures"); - Property property = new Property("textures", skinData.getValue(), skinData.getSignature()); - properties.put("textures", property); + SkinData currentSkin = versionSpecificMethods.currentSkin(properties); + + SkinApplyEvent event = new SkinApplyEventImpl(floodgatePlayer, currentSkin, skinData); + event.setCancelled(floodgatePlayer.isLinked()); - // By running as a task, we don't run into async issues - plugin.getServer().getScheduler().runTask(plugin, () -> { + eventBus.fire(event); + + if (event.isCancelled()) { + return; + } + + replaceSkin(properties, event.newSkin()); + + versionSpecificMethods.maybeSchedule(() -> { for (Player p : Bukkit.getOnlinePlayers()) { if (!p.equals(player) && p.canSee(player)) { - versionSpecificMethods.hidePlayer(p, player); - versionSpecificMethods.showPlayer(p, player); + versionSpecificMethods.hideAndShowPlayer(p, player); } } }); } + + private void replaceSkin(PropertyMap properties, SkinData skinData) { + properties.removeAll("textures"); + Property property = new Property("textures", skinData.value(), skinData.signature()); + properties.put("textures", property); + } } diff --git a/spigot/src/main/java/org/geysermc/floodgate/util/ClassNames.java b/spigot/src/main/java/org/geysermc/floodgate/util/ClassNames.java index 60d38a38..029b89b1 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/util/ClassNames.java +++ b/spigot/src/main/java/org/geysermc/floodgate/util/ClassNames.java @@ -25,9 +25,17 @@ package org.geysermc.floodgate.util; +import static org.geysermc.floodgate.util.ReflectionUtils.castedStaticBooleanValue; +import static org.geysermc.floodgate.util.ReflectionUtils.getBooleanValue; +import static org.geysermc.floodgate.util.ReflectionUtils.getClassOrFallback; +import static org.geysermc.floodgate.util.ReflectionUtils.getClassSilently; +import static org.geysermc.floodgate.util.ReflectionUtils.getConstructor; import static org.geysermc.floodgate.util.ReflectionUtils.getField; import static org.geysermc.floodgate.util.ReflectionUtils.getFieldOfType; import static org.geysermc.floodgate.util.ReflectionUtils.getMethod; +import static org.geysermc.floodgate.util.ReflectionUtils.getValue; +import static org.geysermc.floodgate.util.ReflectionUtils.invoke; +import static org.geysermc.floodgate.util.ReflectionUtils.makeAccessible; import com.google.common.base.Preconditions; import com.mojang.authlib.GameProfile; @@ -36,80 +44,106 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.net.SocketAddress; +import java.util.function.BooleanSupplier; +import javax.annotation.CheckForNull; import org.bukkit.Bukkit; import org.bukkit.OfflinePlayer; import org.checkerframework.checker.nullness.qual.Nullable; @SuppressWarnings("PMD.SystemPrintln") public class ClassNames { - public static final String SPIGOT_MAPPING_PREFIX; - public static final Class MINECRAFT_SERVER; public static final Class SERVER_CONNECTION; public static final Class HANDSHAKE_PACKET; public static final Class LOGIN_START_PACKET; public static final Class LOGIN_LISTENER; - public static final Class LOGIN_HANDLER; + @Nullable public static final Class CLIENT_INTENT; public static final Constructor CRAFT_OFFLINE_PLAYER_CONSTRUCTOR; - public static final Constructor LOGIN_HANDLER_CONSTRUCTOR; + @Nullable public static final Constructor LOGIN_HANDLER_CONSTRUCTOR; + @Nullable public static final Constructor HANDSHAKE_PACKET_CONSTRUCTOR; public static final Field SOCKET_ADDRESS; public static final Field HANDSHAKE_HOST; public static final Field LOGIN_PROFILE; public static final Field PACKET_LISTENER; - @Nullable - public static final Field PAPER_DISABLE_USERNAME_VALIDATION; + + @Nullable public static final Field HANDSHAKE_PORT; + @Nullable public static final Field HANDSHAKE_PROTOCOL; + @Nullable public static final Field HANDSHAKE_INTENTION; + + @Nullable public static final Field PAPER_DISABLE_USERNAME_VALIDATION; + @Nullable public static final BooleanSupplier PAPER_VELOCITY_SUPPORT; public static final Method GET_PROFILE_METHOD; public static final Method LOGIN_DISCONNECT; public static final Method NETWORK_EXCEPTION_CAUGHT; - public static final Method INIT_UUID; - public static final Method FIRE_LOGIN_EVENTS; + @Nullable public static final Method INIT_UUID; + @Nullable public static final Method FIRE_LOGIN_EVENTS; + @Nullable public static final Method FIRE_LOGIN_EVENTS_GAME_PROFILE; + @Nullable public static final Method CALL_PLAYER_PRE_LOGIN_EVENTS; + @Nullable public static final Method START_CLIENT_VERIFICATION; + + public static final Field BUNGEE; + + public static final boolean IS_FOLIA; + public static final boolean IS_PRE_1_20_2; + public static final boolean IS_POST_LOGIN_HANDLER; static { - String version = Bukkit.getServer().getClass().getPackage().getName().split("\\.")[3]; - SPIGOT_MAPPING_PREFIX = "net.minecraft.server." + version; + // ahhhhhhh, this class should really be reworked at this point + + String[] versionSplit = Bukkit.getServer().getClass().getPackage().getName().split("\\."); + // Paper, since 1.20.5, no longer relocates CraftBukkit classes + // and NMS classes aren't relocated for a few versions now (both Spigot & Paper) + if (versionSplit.length <= 3 && getClassSilently("net.minecraft.server.MinecraftServer") == null) { + throw new IllegalStateException( + "Was unable to find net.minecraft.server.MinecraftServer. " + + "We don't support Mojmap yet" + ); + } + // Makes it that we don't have to lookup both the new and the old + // 'org.bukkit.craftbukkit. + version + CraftPlayer' will be .CraftPlayer on new + // versions and .v1_8R3.CraftPlayer on older versions + String version = versionSplit.length > 3 ? versionSplit[3] + '.' : ""; + String nmsPackage = "net.minecraft.server." + version; // SpigotSkinApplier Class craftPlayerClass = ReflectionUtils.getClass( - "org.bukkit.craftbukkit." + version + ".entity.CraftPlayer"); - GET_PROFILE_METHOD = ReflectionUtils.getMethod(craftPlayerClass, "getProfile"); + "org.bukkit.craftbukkit." + version + "entity.CraftPlayer"); + GET_PROFILE_METHOD = getMethod(craftPlayerClass, "getProfile"); checkNotNull(GET_PROFILE_METHOD, "Get profile method"); - String nmsPackage = SPIGOT_MAPPING_PREFIX + '.'; - - // SpigotInjector - MINECRAFT_SERVER = getClassOrFallBack( + MINECRAFT_SERVER = getClassOrFallback( "net.minecraft.server.MinecraftServer", nmsPackage + "MinecraftServer" ); - SERVER_CONNECTION = getClassOrFallBack( + SERVER_CONNECTION = getClassOrFallback( "net.minecraft.server.network.ServerConnection", nmsPackage + "ServerConnection" ); // WhitelistUtils Class craftServerClass = ReflectionUtils.getClass( - "org.bukkit.craftbukkit." + version + ".CraftServer"); + "org.bukkit.craftbukkit." + version + "CraftServer"); Class craftOfflinePlayerClass = ReflectionUtils.getCastedClass( - "org.bukkit.craftbukkit." + version + ".CraftOfflinePlayer"); + "org.bukkit.craftbukkit." + version + "CraftOfflinePlayer"); CRAFT_OFFLINE_PLAYER_CONSTRUCTOR = ReflectionUtils.getConstructor( craftOfflinePlayerClass, true, craftServerClass, GameProfile.class); // SpigotDataHandler - Class networkManager = getClassOrFallBack( + Class networkManager = getClassOrFallback( "net.minecraft.network.NetworkManager", nmsPackage + "NetworkManager" ); SOCKET_ADDRESS = getFieldOfType(networkManager, SocketAddress.class, false); - HANDSHAKE_PACKET = getClassOrFallBack( + HANDSHAKE_PACKET = getClassOrFallback( "net.minecraft.network.protocol.handshake.PacketHandshakingInSetProtocol", nmsPackage + "PacketHandshakingInSetProtocol" ); @@ -117,12 +151,12 @@ public class ClassNames { HANDSHAKE_HOST = getFieldOfType(HANDSHAKE_PACKET, String.class); checkNotNull(HANDSHAKE_HOST, "Handshake host"); - LOGIN_START_PACKET = getClassOrFallBack( + LOGIN_START_PACKET = getClassOrFallback( "net.minecraft.network.protocol.login.PacketLoginInStart", nmsPackage + "PacketLoginInStart" ); - LOGIN_LISTENER = getClassOrFallBack( + LOGIN_LISTENER = getClassOrFallback( "net.minecraft.server.network.LoginListener", nmsPackage + "LoginListener" ); @@ -140,56 +174,174 @@ public class ClassNames { ); // there are multiple no-arg void methods + // Pre 1.20.2 uses initUUID so if it's null, we're on 1.20.2 or later INIT_UUID = getMethod(LOGIN_LISTENER, "initUUID"); - checkNotNull(INIT_UUID, "initUUID from LoginListener"); + IS_PRE_1_20_2 = INIT_UUID != null; - Class packetListenerClass = getClassOrFallBack( - "net.minecraft.network.PacketListener", - nmsPackage + "PacketListener" + // somewhere during 1.20.4 md_5 moved PreLogin logic to CraftBukkit + CALL_PLAYER_PRE_LOGIN_EVENTS = getMethod( + LOGIN_LISTENER, + "callPlayerPreLoginEvents", + GameProfile.class ); - PACKET_LISTENER = getFieldOfType(networkManager, packetListenerClass); + IS_POST_LOGIN_HANDLER = CALL_PLAYER_PRE_LOGIN_EVENTS != null; + + if (IS_PRE_1_20_2) { + Class packetListenerClass = getClassOrFallback( + "net.minecraft.network.PacketListener", + nmsPackage + "PacketListener" + ); + + PACKET_LISTENER = getFieldOfType(networkManager, packetListenerClass); + } else { + // We get the field by name on 1.20.2+ as there are now multiple fields of this type in network manager + + // PacketListener packetListener of NetworkManager + PACKET_LISTENER = getField(networkManager, "q"); + makeAccessible(PACKET_LISTENER); + } checkNotNull(PACKET_LISTENER, "Packet listener"); - LOGIN_HANDLER = getClassOrFallBack( - "net.minecraft.server.network.LoginListener$LoginHandler", - nmsPackage + "LoginListener$LoginHandler" - ); + if (IS_POST_LOGIN_HANDLER) { + makeAccessible(CALL_PLAYER_PRE_LOGIN_EVENTS); - LOGIN_HANDLER_CONSTRUCTOR = - ReflectionUtils.getConstructor(LOGIN_HANDLER, true, LOGIN_LISTENER); - checkNotNull(LOGIN_HANDLER_CONSTRUCTOR, "LoginHandler constructor"); + START_CLIENT_VERIFICATION = getMethod(LOGIN_LISTENER, "b", GameProfile.class); + checkNotNull(START_CLIENT_VERIFICATION, "startClientVerification"); + makeAccessible(START_CLIENT_VERIFICATION); - FIRE_LOGIN_EVENTS = getMethod(LOGIN_HANDLER, "fireEvents"); - checkNotNull(FIRE_LOGIN_EVENTS, "fireEvents from LoginHandler"); + LOGIN_HANDLER_CONSTRUCTOR = null; + FIRE_LOGIN_EVENTS = null; + FIRE_LOGIN_EVENTS_GAME_PROFILE = null; + } else { + Class loginHandler = getClassOrFallback( + "net.minecraft.server.network.LoginListener$LoginHandler", + nmsPackage + "LoginListener$LoginHandler" + ); + + LOGIN_HANDLER_CONSTRUCTOR = + ReflectionUtils.getConstructor(loginHandler, true, LOGIN_LISTENER); + checkNotNull(LOGIN_HANDLER_CONSTRUCTOR, "LoginHandler constructor"); + + FIRE_LOGIN_EVENTS = getMethod(loginHandler, "fireEvents"); + + // LoginHandler().fireEvents(GameProfile) + FIRE_LOGIN_EVENTS_GAME_PROFILE = getMethod(loginHandler, "fireEvents", + GameProfile.class); + checkNotNull(FIRE_LOGIN_EVENTS, FIRE_LOGIN_EVENTS_GAME_PROFILE, + "fireEvents from LoginHandler", "fireEvents(GameProfile) from LoginHandler"); + + START_CLIENT_VERIFICATION = null; + } PAPER_DISABLE_USERNAME_VALIDATION = getField(LOGIN_LISTENER, "iKnowThisMayNotBeTheBestIdeaButPleaseDisableUsernameValidation"); + if (Constants.DEBUG_MODE) { System.out.println("Paper disable username validation field exists? " + (PAPER_DISABLE_USERNAME_VALIDATION != null)); } - } - - private static Class getClassOrFallBack(String className, String fallbackName) { - Class clazz = ReflectionUtils.getClassSilently(className); - if (clazz != null) { - if (Constants.DEBUG_MODE) { - System.out.println("Found class (primary): " + clazz.getName()); + // ProxyUtils + Class spigotConfig = ReflectionUtils.getClass("org.spigotmc.SpigotConfig"); + checkNotNull(spigotConfig, "Spigot config"); + + BUNGEE = getField(spigotConfig, "bungee"); + checkNotNull(BUNGEE, "Bungee field"); + + Class paperConfigNew = getClassSilently( + "io.papermc.paper.configuration.GlobalConfiguration"); + if (paperConfigNew != null) { + // 1.19 and later + Method paperConfigGet = checkNotNull(getMethod(paperConfigNew, "get"), + "GlobalConfiguration get"); + Field paperConfigProxies = checkNotNull(getField(paperConfigNew, "proxies"), + "Proxies field"); + Field paperConfigVelocity = checkNotNull( + getField(paperConfigProxies.getType(), "velocity"), + "velocity field"); + Field paperVelocityEnabled = checkNotNull( + getField(paperConfigVelocity.getType(), "enabled"), + "Velocity enabled field"); + PAPER_VELOCITY_SUPPORT = () -> { + Object paperConfigInstance = invoke(null, paperConfigGet); + Object proxiesInstance = getValue(paperConfigInstance, paperConfigProxies); + Object velocityInstance = getValue(proxiesInstance, paperConfigVelocity); + return getBooleanValue(velocityInstance, paperVelocityEnabled); + }; + } else { + // Pre-1.19 + Class paperConfig = getClassSilently( + "com.destroystokyo.paper.PaperConfig"); + + if (paperConfig != null) { + Field velocitySupport = getField(paperConfig, "velocitySupport"); + // velocitySupport field is null pre-1.13 + PAPER_VELOCITY_SUPPORT = velocitySupport != null ? + () -> castedStaticBooleanValue(velocitySupport) : null; + } else { + PAPER_VELOCITY_SUPPORT = null; } - return clazz; } - // do throw an exception when both classes couldn't be found - clazz = ReflectionUtils.getClassOrThrow(fallbackName); - if (Constants.DEBUG_MODE) { - System.out.println("Found class (fallback): " + clazz.getName()); + IS_FOLIA = ReflectionUtils.getClassSilently( + "io.papermc.paper.threadedregions.RegionizedServer" + ) != null; + + if (!IS_PRE_1_20_2) { + // PacketHandshakingInSetProtocol is now a record + // This means its fields are now private and final + // We therefore must use reflection to obtain the constructor + CLIENT_INTENT = getClassOrFallback( + "net.minecraft.network.protocol.handshake.ClientIntent", + nmsPackage + "ClientIntent" + ); + checkNotNull(CLIENT_INTENT, "Client intent enum"); + + HANDSHAKE_PACKET_CONSTRUCTOR = getConstructor(HANDSHAKE_PACKET, false, int.class, + String.class, int.class, CLIENT_INTENT); + checkNotNull(HANDSHAKE_PACKET_CONSTRUCTOR, "Handshake packet constructor"); + + Field a = getField(HANDSHAKE_PACKET, "a"); + checkNotNull(a, "Handshake \"a\" field (protocol version, or stream codec)"); + + if (a.getType().isPrimitive()) { // 1.20.2 - 1.20.4: a is the protocol version (int) + HANDSHAKE_PROTOCOL = a; + HANDSHAKE_PORT = getField(HANDSHAKE_PACKET, "c"); + } else { // 1.20.5: a is the stream_codec thing, so everything is shifted + HANDSHAKE_PROTOCOL = getField(HANDSHAKE_PACKET, "b"); + HANDSHAKE_PORT = getField(HANDSHAKE_PACKET, "d"); + } + + checkNotNull(HANDSHAKE_PROTOCOL, "Handshake protocol"); + makeAccessible(HANDSHAKE_PROTOCOL); + + checkNotNull(HANDSHAKE_PORT, "Handshake port"); + makeAccessible(HANDSHAKE_PORT); + + HANDSHAKE_INTENTION = getFieldOfType(HANDSHAKE_PACKET, CLIENT_INTENT); + checkNotNull(HANDSHAKE_INTENTION, "Handshake intention"); + makeAccessible(HANDSHAKE_INTENTION); + } else { + CLIENT_INTENT = null; + HANDSHAKE_PACKET_CONSTRUCTOR = null; + HANDSHAKE_PORT = null; + HANDSHAKE_PROTOCOL = null; + HANDSHAKE_INTENTION = null; } + } - return clazz; + private static T checkNotNull(@CheckForNull T toCheck, @CheckForNull String objectName) { + return Preconditions.checkNotNull(toCheck, objectName + " cannot be null"); } - private static void checkNotNull(Object toCheck, String objectName) { - Preconditions.checkNotNull(toCheck, objectName + " cannot be null"); + // Ensure one of two is not null + private static T checkNotNull( + @CheckForNull T toCheck, + @CheckForNull T toCheck2, + @CheckForNull String objectName, + @CheckForNull String objectName2 + ) { + return Preconditions.checkNotNull(toCheck != null ? toCheck : toCheck2, + objectName2 + " cannot be null if " + objectName + " is null"); } } diff --git a/spigot/src/main/java/org/geysermc/floodgate/util/ProxyUtils.java b/spigot/src/main/java/org/geysermc/floodgate/util/ProxyUtils.java index 55d2bb3d..d9d7791d 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/util/ProxyUtils.java +++ b/spigot/src/main/java/org/geysermc/floodgate/util/ProxyUtils.java @@ -25,45 +25,21 @@ package org.geysermc.floodgate.util; -import static com.google.common.base.Preconditions.checkNotNull; -import static org.geysermc.floodgate.util.ReflectionUtils.getField; - -import java.lang.reflect.Field; - -@SuppressWarnings("ConstantConditions") public final class ProxyUtils { - private static final Field IS_BUNGEE_DATA; - private static final Field IS_MODERN_FORWARDING; - - static { - Class spigotConfig = ReflectionUtils.getClass("org.spigotmc.SpigotConfig"); - IS_BUNGEE_DATA = getField(spigotConfig, "bungee"); - checkNotNull(IS_BUNGEE_DATA, "bungee field cannot be null. Are you using CraftBukkit?"); - - Field velocitySupport; - try { - Class paperConfig = Class.forName("com.destroystokyo.paper.PaperConfig"); - velocitySupport = getField(paperConfig, "velocitySupport"); - } catch (ClassNotFoundException e) { - // We're not on a platform that has modern forwarding - velocitySupport = null; // NOPMD - there's really not a better way around this unless you want to use an optional - } - IS_MODERN_FORWARDING = velocitySupport; - } public static boolean isProxyData() { return isBungeeData() || isVelocitySupport(); } private static boolean isBungeeData() { - return ReflectionUtils.getCastedValue(null, IS_BUNGEE_DATA); + return ReflectionUtils.castedStaticBooleanValue(ClassNames.BUNGEE); } private static boolean isVelocitySupport() { - if (IS_MODERN_FORWARDING == null) { + if (ClassNames.PAPER_VELOCITY_SUPPORT == null) { return false; } - return ReflectionUtils.getCastedValue(null, IS_MODERN_FORWARDING); + return ClassNames.PAPER_VELOCITY_SUPPORT.getAsBoolean(); } } \ No newline at end of file diff --git a/spigot/src/main/java/org/geysermc/floodgate/util/SpigotCommandUtil.java b/spigot/src/main/java/org/geysermc/floodgate/util/SpigotCommandUtil.java index da3d65bc..d6028abe 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/util/SpigotCommandUtil.java +++ b/spigot/src/main/java/org/geysermc/floodgate/util/SpigotCommandUtil.java @@ -27,11 +27,10 @@ import java.util.Collection; import java.util.UUID; -import org.bukkit.Bukkit; +import org.bukkit.OfflinePlayer; import org.bukkit.Server; import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; -import org.bukkit.plugin.java.JavaPlugin; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.floodgate.api.FloodgateApi; import org.geysermc.floodgate.platform.command.CommandUtil; @@ -42,19 +41,17 @@ public final class SpigotCommandUtil extends CommandUtil { private final Server server; private final SpigotVersionSpecificMethods versionSpecificMethods; - private final JavaPlugin plugin; private UserAudience console; public SpigotCommandUtil( LanguageManager manager, Server server, FloodgateApi api, - SpigotVersionSpecificMethods versionSpecificMethods, - JavaPlugin plugin) { + SpigotVersionSpecificMethods versionSpecificMethods + ) { super(manager, api); this.server = server; this.versionSpecificMethods = versionSpecificMethods; - this.plugin = plugin; } @Override @@ -120,17 +117,17 @@ public void sendMessage(Object target, String message) { public void kickPlayer(Object player, String message) { // can also be console if (player instanceof Player) { - Bukkit.getScheduler().runTask(plugin, () -> ((Player) player).kickPlayer(message)); + versionSpecificMethods.schedule(() -> ((Player) player).kickPlayer(message), 0); } } @Override public boolean whitelistPlayer(UUID uuid, String username) { - return WhitelistUtils.addPlayer(uuid, username); + return WhitelistUtils.addPlayer(uuid, username, versionSpecificMethods); } @Override public boolean removePlayerFromWhitelist(UUID uuid, String username) { - return WhitelistUtils.removePlayer(uuid, username); + return WhitelistUtils.removePlayer(uuid, username, versionSpecificMethods); } } diff --git a/spigot/src/main/java/org/geysermc/floodgate/util/SpigotPlatformUtils.java b/spigot/src/main/java/org/geysermc/floodgate/util/SpigotPlatformUtils.java new file mode 100644 index 00000000..538c8ce6 --- /dev/null +++ b/spigot/src/main/java/org/geysermc/floodgate/util/SpigotPlatformUtils.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Floodgate + */ + +package org.geysermc.floodgate.util; + +import org.bukkit.Bukkit; +import org.geysermc.floodgate.platform.util.PlatformUtils; + +public class SpigotPlatformUtils extends PlatformUtils { + @Override + public AuthType authType() { + if (Bukkit.getOnlineMode()) { + return AuthType.ONLINE; + } + return ProxyUtils.isProxyData() ? AuthType.PROXIED : AuthType.OFFLINE; + } + + @Override + public String minecraftVersion() { + return Bukkit.getServer().getVersion().split("\\(MC: ")[1].split("\\)")[0]; + } + + @Override + public String serverImplementationName() { + return Bukkit.getServer().getName(); + } +} diff --git a/spigot/src/main/java/org/geysermc/floodgate/util/SpigotVersionSpecificMethods.java b/spigot/src/main/java/org/geysermc/floodgate/util/SpigotVersionSpecificMethods.java index f8e67616..7569c615 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/util/SpigotVersionSpecificMethods.java +++ b/spigot/src/main/java/org/geysermc/floodgate/util/SpigotVersionSpecificMethods.java @@ -25,20 +25,35 @@ package org.geysermc.floodgate.util; +import com.mojang.authlib.properties.Property; +import com.mojang.authlib.properties.PropertyMap; +import java.lang.reflect.Method; +import java.util.concurrent.TimeUnit; import org.bukkit.entity.Player; import org.bukkit.plugin.Plugin; import org.geysermc.floodgate.SpigotPlugin; +import org.geysermc.floodgate.api.event.skin.SkinApplyEvent; +import org.geysermc.floodgate.skin.SkinDataImpl; public final class SpigotVersionSpecificMethods { - private static final boolean NEW_GET_LOCALE; + private static final Method GET_SPIGOT; + private static final Method OLD_GET_LOCALE; private static final boolean NEW_VISIBILITY; + private static final Method NEW_PROPERTY_VALUE; + private static final Method NEW_PROPERTY_SIGNATURE; + static { - NEW_GET_LOCALE = ReflectionUtils.getMethod(Player.class, "getLocale") != null; + GET_SPIGOT = ReflectionUtils.getMethod(Player.class, "spigot"); + OLD_GET_LOCALE = ReflectionUtils.getMethod(Player.Spigot.class, "getLocale"); + NEW_VISIBILITY = null != ReflectionUtils.getMethod( Player.class, "hidePlayer", Plugin.class, Player.class ); + + NEW_PROPERTY_VALUE = ReflectionUtils.getMethod(Property.class, "value"); + NEW_PROPERTY_SIGNATURE = ReflectionUtils.getMethod(Property.class, "signature"); } private final SpigotPlugin plugin; @@ -48,27 +63,71 @@ public SpigotVersionSpecificMethods(SpigotPlugin plugin) { } public String getLocale(Player player) { - if (NEW_GET_LOCALE) { + if (OLD_GET_LOCALE == null) { return player.getLocale(); } - return player.spigot().getLocale(); + Object spigot = ReflectionUtils.invoke(player, GET_SPIGOT); + return ReflectionUtils.castedInvoke(spigot, OLD_GET_LOCALE); } - @SuppressWarnings("deprecation") - public void hidePlayer(Player hideFor, Player playerToHide) { - if (NEW_VISIBILITY) { - hideFor.hidePlayer(plugin, playerToHide); + public void hideAndShowPlayer(Player on, Player target) { + // In Folia we don't have to schedule this as there is no concept of a single main thread. + // Instead, we have to schedule the task per player. + if (ClassNames.IS_FOLIA) { + on.getScheduler().execute(plugin, () -> hideAndShowPlayer0(on, target), null, 0); return; } - hideFor.hidePlayer(playerToHide); + hideAndShowPlayer0(on, target); + } + + public SkinApplyEvent.SkinData currentSkin(PropertyMap properties) { + for (Property property : properties.get("textures")) { + String value; + String signature; + if (NEW_PROPERTY_VALUE != null) { + value = ReflectionUtils.castedInvoke(property, NEW_PROPERTY_VALUE); + signature = ReflectionUtils.castedInvoke(property, NEW_PROPERTY_SIGNATURE); + } else { + value = property.getValue(); + signature = property.getSignature(); + } + + //noinspection DataFlowIssue + if (!value.isEmpty()) { + return new SkinDataImpl(value, signature); + } + } + return null; + } + + public void schedule(Runnable runnable, long delay) { + if (ClassNames.IS_FOLIA) { + plugin.getServer().getAsyncScheduler().runDelayed( + plugin, $ -> runnable.run(), delay * 50, TimeUnit.MILLISECONDS + ); + return; + } + plugin.getServer().getScheduler().runTaskLater(plugin, runnable, delay); } @SuppressWarnings("deprecation") - public void showPlayer(Player showFor, Player playerToShow) { + private void hideAndShowPlayer0(Player source, Player target) { if (NEW_VISIBILITY) { - showFor.showPlayer(plugin, playerToShow); + source.hidePlayer(plugin, target); + source.showPlayer(plugin, target); + return; + } + source.hidePlayer(target); + source.showPlayer(target); + } + + public void maybeSchedule(Runnable runnable) { + // In Folia we don't have to schedule this as there is no concept of a single main thread. + // Instead, we have to schedule the task per player. + if (ClassNames.IS_FOLIA) { + runnable.run(); return; } - showFor.showPlayer(playerToShow); + plugin.getServer().getScheduler().runTask(plugin, runnable); } } diff --git a/spigot/src/main/java/org/geysermc/floodgate/util/WhitelistUtils.java b/spigot/src/main/java/org/geysermc/floodgate/util/WhitelistUtils.java index 6747c28c..d5779565 100644 --- a/spigot/src/main/java/org/geysermc/floodgate/util/WhitelistUtils.java +++ b/spigot/src/main/java/org/geysermc/floodgate/util/WhitelistUtils.java @@ -36,11 +36,12 @@ public final class WhitelistUtils { /** * Whitelist the given Bedrock player. * - * @param uuid the UUID of the Bedrock player to be whitelisted - * @param username the username of the Bedrock player to be whitelisted - * @return true if the player has been whitelisted, false if the player is already whitelisted + * @param uuid the UUID of the Bedrock player to be removed + * @param username the username of the Bedrock player to be removed + * @param versionSpecificMethods a reference to the SpigotVersionSpecificMethods used in SpigotCommandUtil + * @return true if the player has been removed from the whitelist, false if the player wasn't */ - public static boolean addPlayer(UUID uuid, String username) { + public static boolean addPlayer(UUID uuid, String username, SpigotVersionSpecificMethods versionSpecificMethods) { GameProfile profile = new GameProfile(uuid, username); OfflinePlayer player = ReflectionUtils.newInstance( @@ -50,7 +51,7 @@ public static boolean addPlayer(UUID uuid, String username) { if (player.isWhitelisted()) { return false; } - player.setWhitelisted(true); + setWhitelist(player, true, versionSpecificMethods); return true; } @@ -59,10 +60,11 @@ public static boolean addPlayer(UUID uuid, String username) { * * @param uuid the UUID of the Bedrock player to be removed * @param username the username of the Bedrock player to be removed + * @param versionSpecificMethods a reference to the SpigotVersionSpecificMethods used in SpigotCommandUtil * @return true if the player has been removed from the whitelist, false if the player wasn't * whitelisted */ - public static boolean removePlayer(UUID uuid, String username) { + public static boolean removePlayer(UUID uuid, String username, SpigotVersionSpecificMethods versionSpecificMethods) { GameProfile profile = new GameProfile(uuid, username); OfflinePlayer player = ReflectionUtils.newInstance( @@ -72,7 +74,11 @@ public static boolean removePlayer(UUID uuid, String username) { if (!player.isWhitelisted()) { return false; } - player.setWhitelisted(false); + setWhitelist(player, false, versionSpecificMethods); return true; } + + static void setWhitelist(OfflinePlayer player, boolean whitelist, SpigotVersionSpecificMethods versionSpecificMethods) { + versionSpecificMethods.maybeSchedule(() -> player.setWhitelisted(whitelist)); + } } diff --git a/spigot/src/main/resources/plugin.yml b/spigot/src/main/resources/plugin.yml index c9a5d232..0c9fe9f1 100644 --- a/spigot/src/main/resources/plugin.yml +++ b/spigot/src/main/resources/plugin.yml @@ -4,4 +4,5 @@ version: ${version} author: ${author} website: ${url} main: org.geysermc.floodgate.SpigotPlugin -api-version: 1.13 \ No newline at end of file +api-version: 1.13 +folia-supported: true \ No newline at end of file diff --git a/velocity/build.gradle.kts b/velocity/build.gradle.kts index 5fa61663..dd2d8b5b 100644 --- a/velocity/build.gradle.kts +++ b/velocity/build.gradle.kts @@ -1,14 +1,21 @@ -var velocityVersion = "3.0.1" +var velocityVersion = "3.2.0-SNAPSHOT" var log4jVersion = "2.11.2" var gsonVersion = "2.8.8" var guavaVersion = "25.1-jre" +indra { + javaVersions { + // For Velocity API + target(11) + } +} + dependencies { api(projects.core) - api("cloud.commandframework", "cloud-velocity", Versions.cloudVersion) + implementation("org.incendo", "cloud-velocity", Versions.cloudVersion) } -relocate("cloud.commandframework") +relocate("org.incendo.cloud") // used in cloud relocate("io.leangen.geantyref") diff --git a/velocity/src/main/java/org/geysermc/floodgate/VelocityPlugin.java b/velocity/src/main/java/org/geysermc/floodgate/VelocityPlugin.java index 259d29a7..d7c16754 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/VelocityPlugin.java +++ b/velocity/src/main/java/org/geysermc/floodgate/VelocityPlugin.java @@ -29,6 +29,7 @@ import com.google.inject.Injector; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.proxy.ProxyInitializeEvent; +import com.velocitypowered.api.event.proxy.ProxyShutdownEvent; import com.velocitypowered.api.plugin.annotation.DataDirectory; import java.nio.file.Path; import org.geysermc.floodgate.api.logger.FloodgateLogger; @@ -69,4 +70,9 @@ public void onProxyInitialization(ProxyInitializeEvent event) { new PluginMessageModule() ); } + + @Subscribe + public void onProxyShutdown(ProxyShutdownEvent event) { + platform.disable(); + } } diff --git a/velocity/src/main/java/org/geysermc/floodgate/addon/data/VelocityProxyDataHandler.java b/velocity/src/main/java/org/geysermc/floodgate/addon/data/VelocityProxyDataHandler.java index a19b71ed..0c67c8a2 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/addon/data/VelocityProxyDataHandler.java +++ b/velocity/src/main/java/org/geysermc/floodgate/addon/data/VelocityProxyDataHandler.java @@ -27,13 +27,17 @@ import static com.google.common.base.Preconditions.checkNotNull; import static org.geysermc.floodgate.util.ReflectionUtils.getCastedValue; +import static org.geysermc.floodgate.util.ReflectionUtils.getClassOrFallbackPrefixed; import static org.geysermc.floodgate.util.ReflectionUtils.getField; +import static org.geysermc.floodgate.util.ReflectionUtils.getMethodByName; import static org.geysermc.floodgate.util.ReflectionUtils.getPrefixedClass; +import static org.geysermc.floodgate.util.ReflectionUtils.invoke; import static org.geysermc.floodgate.util.ReflectionUtils.setValue; import io.netty.channel.Channel; import io.netty.util.AttributeKey; import java.lang.reflect.Field; +import java.lang.reflect.Method; import java.net.InetSocketAddress; import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; @@ -48,6 +52,11 @@ public final class VelocityProxyDataHandler extends CommonDataHandler { private static final Field HANDSHAKE_SERVER_ADDRESS; private static final Field REMOTE_ADDRESS; + private static final Class SERVER_LOGIN_PACKET; + private static final Method GET_SESSION_HANDLER; + private static final Class INITIAL_LOGIN_SESSION_HANDLER; + private static final Field FORCE_KEY_AUTHENTICATION; + static { Class iic = getPrefixedClass("connection.client.InitialInboundConnection"); checkNotNull(iic, "InitialInboundConnection class cannot be null"); @@ -55,7 +64,10 @@ public final class VelocityProxyDataHandler extends CommonDataHandler { HANDSHAKE = getField(iic, "handshake"); checkNotNull(HANDSHAKE, "Handshake field cannot be null"); - HANDSHAKE_PACKET = getPrefixedClass("protocol.packet.Handshake"); + HANDSHAKE_PACKET = getClassOrFallbackPrefixed( + "protocol.packet.HandshakePacket", + "protocol.packet.Handshake" + ); checkNotNull(HANDSHAKE_PACKET, "Handshake packet class cannot be null"); HANDSHAKE_SERVER_ADDRESS = getField(HANDSHAKE_PACKET, "serverAddress"); @@ -63,6 +75,29 @@ public final class VelocityProxyDataHandler extends CommonDataHandler { Class minecraftConnection = getPrefixedClass("connection.MinecraftConnection"); REMOTE_ADDRESS = getField(minecraftConnection, "remoteAddress"); + checkNotNull(REMOTE_ADDRESS, "remoteAddress cannot be null"); + + SERVER_LOGIN_PACKET = getClassOrFallbackPrefixed( + "protocol.packet.ServerLoginPacket", + "protocol.packet.ServerLogin" + ); + checkNotNull(SERVER_LOGIN_PACKET, "ServerLogin packet class cannot be null"); + + + Method sessionHandler = getMethodByName(minecraftConnection, "getSessionHandler", true); + if (sessionHandler == null) { + // We are 1.20.2+ + sessionHandler = getMethodByName(minecraftConnection, "getActiveSessionHandler", true); + } + GET_SESSION_HANDLER = sessionHandler; + checkNotNull(GET_SESSION_HANDLER, "getSessionHandler method cannot be null"); + + INITIAL_LOGIN_SESSION_HANDLER = + getPrefixedClass("connection.client.InitialLoginSessionHandler"); + checkNotNull(INITIAL_LOGIN_SESSION_HANDLER, "InitialLoginSessionHandler cannot be null"); + + // allowed to be null if it's an old Velocity version + FORCE_KEY_AUTHENTICATION = getField(INITIAL_LOGIN_SESSION_HANDLER, "forceKeyAuthentication"); } private final FloodgateLogger logger; @@ -94,19 +129,35 @@ protected boolean shouldRemoveHandler(HandshakeResult result) { FloodgatePlayer player = result.getFloodgatePlayer(); logger.info("Floodgate player who is logged in as {} {} joined", player.getCorrectUsername(), player.getCorrectUniqueId()); + + // the way Velocity stores whether to force key authentication + boolean forceKeyAuthentication = Boolean.getBoolean("auth.forceSecureProfiles"); + // we need the login packet to bypass the 'force key authentication' + return !forceKeyAuthentication; } return super.shouldRemoveHandler(result); } @Override public boolean channelRead(Object packet) { - // we're only interested in the Handshake packet. - // it should be the first packet but you never know if (HANDSHAKE_PACKET.isInstance(packet)) { handle(packet, getCastedValue(packet, HANDSHAKE_SERVER_ADDRESS)); // otherwise, it'll get read twice. once by the packet queue and once by this method return false; } + + // at this point we know that forceKeyAuthentication is enabled + if (SERVER_LOGIN_PACKET.isInstance(packet)) { + Object minecraftConnection = ctx.pipeline().get("handler"); + Object sessionHandler = invoke(minecraftConnection, GET_SESSION_HANDLER); + if (!INITIAL_LOGIN_SESSION_HANDLER.isInstance(sessionHandler)) { + logger.error("Expected player's session handler to be InitialLoginSessionHandler"); + return true; + } + if (FORCE_KEY_AUTHENTICATION != null) { + setValue(sessionHandler, FORCE_KEY_AUTHENTICATION, false); + } + } return true; } } diff --git a/velocity/src/main/java/org/geysermc/floodgate/addon/data/VelocityServerDataHandler.java b/velocity/src/main/java/org/geysermc/floodgate/addon/data/VelocityServerDataHandler.java index fb61f143..b68fafaa 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/addon/data/VelocityServerDataHandler.java +++ b/velocity/src/main/java/org/geysermc/floodgate/addon/data/VelocityServerDataHandler.java @@ -28,6 +28,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static org.geysermc.floodgate.util.ReflectionUtils.castedInvoke; import static org.geysermc.floodgate.util.ReflectionUtils.getCastedValue; +import static org.geysermc.floodgate.util.ReflectionUtils.getClassOrFallbackPrefixed; import static org.geysermc.floodgate.util.ReflectionUtils.getField; import static org.geysermc.floodgate.util.ReflectionUtils.getMethod; import static org.geysermc.floodgate.util.ReflectionUtils.getPrefixedClass; @@ -55,7 +56,10 @@ public final class VelocityServerDataHandler extends ChannelOutboundHandlerAdapt private static final Method GET_PLAYER; static { - HANDSHAKE_PACKET = getPrefixedClass("protocol.packet.Handshake"); + HANDSHAKE_PACKET = getClassOrFallbackPrefixed( + "protocol.packet.HandshakePacket", + "protocol.packet.Handshake" + ); checkNotNull(HANDSHAKE_PACKET, "Handshake packet class cannot be null"); HANDSHAKE_ADDRESS = getField(HANDSHAKE_PACKET, "serverAddress"); diff --git a/velocity/src/main/java/org/geysermc/floodgate/inject/velocity/VelocityInjector.java b/velocity/src/main/java/org/geysermc/floodgate/inject/velocity/VelocityInjector.java index a37768bb..4d122cf1 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/inject/velocity/VelocityInjector.java +++ b/velocity/src/main/java/org/geysermc/floodgate/inject/velocity/VelocityInjector.java @@ -36,21 +36,19 @@ import java.lang.reflect.Method; import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.inject.CommonPlatformInjector; @RequiredArgsConstructor public final class VelocityInjector extends CommonPlatformInjector { private final ProxyServer server; - private final FloodgateLogger logger; @Getter private boolean injected; @Override @SuppressWarnings("rawtypes") - public boolean inject() { + public void inject() { if (isInjected()) { - return true; + return; } Object connectionManager = getValue(server, "cm"); @@ -72,7 +70,8 @@ public boolean inject() { Method backendSetter = getMethod(backendInitializerHolder, "set", ChannelInitializer.class); invoke(backendInitializerHolder, backendSetter, new VelocityChannelInitializer(this, backendInitializer, true)); - return injected = true; + + injected = true; } @Override @@ -81,9 +80,9 @@ public boolean canRemoveInjection() { } @Override - public boolean removeInjection() { - logger.error("Floodgate cannot remove itself from Bungee without a reboot"); - return false; + public void removeInjection() { + throw new IllegalStateException( + "Floodgate cannot remove itself from Velocity without a reboot"); } @RequiredArgsConstructor diff --git a/velocity/src/main/java/org/geysermc/floodgate/listener/VelocityListener.java b/velocity/src/main/java/org/geysermc/floodgate/listener/VelocityListener.java index 8000f520..cd625957 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/listener/VelocityListener.java +++ b/velocity/src/main/java/org/geysermc/floodgate/listener/VelocityListener.java @@ -36,6 +36,7 @@ import com.google.common.cache.CacheBuilder; import com.google.inject.Inject; import com.google.inject.name.Named; +import com.velocitypowered.api.event.Continuation; import com.velocitypowered.api.event.PostOrder; import com.velocitypowered.api.event.Subscribe; import com.velocitypowered.api.event.connection.DisconnectEvent; @@ -48,7 +49,7 @@ import io.netty.channel.Channel; import io.netty.util.AttributeKey; import java.lang.reflect.Field; -import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.concurrent.TimeUnit; import net.kyori.adventure.text.Component; @@ -56,12 +57,16 @@ import org.geysermc.floodgate.api.logger.FloodgateLogger; import org.geysermc.floodgate.api.player.FloodgatePlayer; import org.geysermc.floodgate.config.ProxyFloodgateConfig; +import org.geysermc.floodgate.skin.SkinDataImpl; +import org.geysermc.floodgate.util.Constants; import org.geysermc.floodgate.util.LanguageManager; +import org.geysermc.floodgate.util.MojangUtils; public final class VelocityListener { private static final Field INITIAL_MINECRAFT_CONNECTION; private static final Field INITIAL_CONNECTION_DELEGATE; private static final Field CHANNEL; + private static final Property DEFAULT_TEXTURE_PROPERTY; static { Class initialConnection = getPrefixedClass("connection.client.InitialInboundConnection"); @@ -82,6 +87,12 @@ public final class VelocityListener { } CHANNEL = getFieldOfType(minecraftConnection, Channel.class); + + DEFAULT_TEXTURE_PROPERTY = new Property( + "textures", + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_TEXTURE, + Constants.DEFAULT_MINECRAFT_JAVA_SKIN_SIGNATURE + ); } private final Cache playerCache = @@ -103,6 +114,9 @@ public final class VelocityListener { @Named("kickMessageAttribute") private AttributeKey kickMessageAttribute; + @Inject + private MojangUtils mojangUtils; + @Subscribe(order = PostOrder.EARLY) public void onPreLogin(PreLoginEvent event) { FloodgatePlayer player = null; @@ -139,22 +153,40 @@ public void onPreLogin(PreLoginEvent event) { } @Subscribe(order = PostOrder.EARLY) - public void onGameProfileRequest(GameProfileRequestEvent event) { + public void onGameProfileRequest(GameProfileRequestEvent event, Continuation continuation) { FloodgatePlayer player = playerCache.getIfPresent(event.getConnection()); - if (player != null) { - playerCache.invalidate(event.getConnection()); + if (player == null) { + continuation.resume(); + return; + } + playerCache.invalidate(event.getConnection()); - GameProfile profile = new GameProfile( + // Skin look up (on Spigot and friends) would result in it failing, so apply a default skin + if (!player.isLinked()) { + event.setGameProfile(new GameProfile( player.getCorrectUniqueId(), player.getCorrectUsername(), - Collections.emptyList() - ); - // The texture properties addition is to fix the February 2 2022 Mojang authentication changes - if (!config.isSendFloodgateData() && !player.isLinked()) { - profile = profile.addProperty(new Property("textures", "", "")); - } - event.setGameProfile(profile); + List.of(DEFAULT_TEXTURE_PROPERTY) + )); + continuation.resume(); + return; } + + // Floodgate players are seen as offline mode players, meaning we have to look up + // the linked player's textures ourselves + + mojangUtils.skinFor(player.getJavaUniqueId()) + .exceptionally(exception -> { + logger.debug("Unexpected skin fetch error for " + player.getJavaUniqueId(), exception); + return SkinDataImpl.DEFAULT_SKIN; + }).thenAccept(skin -> { + event.setGameProfile(new GameProfile( + player.getCorrectUniqueId(), + player.getCorrectUsername(), + List.of(new Property("textures", skin.value(), skin.signature())) + )); + continuation.resume(); + }); } @Subscribe(order = PostOrder.LAST) diff --git a/velocity/src/main/java/org/geysermc/floodgate/logger/Slf4jFloodgateLogger.java b/velocity/src/main/java/org/geysermc/floodgate/logger/Slf4jFloodgateLogger.java index 3b5dd543..17efe8a6 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/logger/Slf4jFloodgateLogger.java +++ b/velocity/src/main/java/org/geysermc/floodgate/logger/Slf4jFloodgateLogger.java @@ -27,17 +27,27 @@ import static org.geysermc.floodgate.util.MessageFormatter.format; -import lombok.RequiredArgsConstructor; +import com.google.inject.Inject; +import com.google.inject.Singleton; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.config.Configurator; import org.geysermc.floodgate.api.logger.FloodgateLogger; +import org.geysermc.floodgate.config.FloodgateConfig; import org.geysermc.floodgate.util.LanguageManager; import org.slf4j.Logger; -@RequiredArgsConstructor +@Singleton public final class Slf4jFloodgateLogger implements FloodgateLogger { - private final Logger logger; - private final LanguageManager languageManager; + @Inject private Logger logger; + private LanguageManager languageManager; + + @Inject + private void init(LanguageManager languageManager, FloodgateConfig config) { + this.languageManager = languageManager; + if (config.isDebug() && !logger.isDebugEnabled()) { + Configurator.setLevel(logger.getName(), Level.DEBUG); + } + } @Override public void error(String message, Object... args) { @@ -74,20 +84,6 @@ public void trace(String message, Object... args) { logger.trace(message, args); } - @Override - public void enableDebug() { - if (!logger.isDebugEnabled()) { - Configurator.setLevel(logger.getName(), Level.DEBUG); - } - } - - @Override - public void disableDebug() { - if (logger.isDebugEnabled()) { - Configurator.setLevel(logger.getName(), Level.INFO); - } - } - @Override public boolean isDebug() { return logger.isDebugEnabled(); diff --git a/velocity/src/main/java/org/geysermc/floodgate/module/VelocityPlatformModule.java b/velocity/src/main/java/org/geysermc/floodgate/module/VelocityPlatformModule.java index 209c07c6..91ced577 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/module/VelocityPlatformModule.java +++ b/velocity/src/main/java/org/geysermc/floodgate/module/VelocityPlatformModule.java @@ -25,17 +25,12 @@ package org.geysermc.floodgate.module; -import cloud.commandframework.CommandManager; -import cloud.commandframework.execution.CommandExecutionCoordinator; -import cloud.commandframework.velocity.CloudInjectionModule; -import cloud.commandframework.velocity.VelocityCommandManager; import com.google.inject.AbstractModule; import com.google.inject.Injector; import com.google.inject.Key; import com.google.inject.Provides; import com.google.inject.Singleton; import com.google.inject.name.Named; -import com.velocitypowered.api.command.CommandSource; import com.velocitypowered.api.event.EventManager; import com.velocitypowered.api.proxy.ProxyServer; import lombok.RequiredArgsConstructor; @@ -48,17 +43,22 @@ import org.geysermc.floodgate.platform.command.CommandUtil; import org.geysermc.floodgate.platform.listener.ListenerRegistration; import org.geysermc.floodgate.platform.pluginmessage.PluginMessageUtils; +import org.geysermc.floodgate.platform.util.PlatformUtils; import org.geysermc.floodgate.player.FloodgateCommandPreprocessor; import org.geysermc.floodgate.player.UserAudience; +import org.geysermc.floodgate.player.audience.FloodgateSenderMapper; import org.geysermc.floodgate.pluginmessage.PluginMessageManager; import org.geysermc.floodgate.pluginmessage.PluginMessageRegistration; import org.geysermc.floodgate.pluginmessage.VelocityPluginMessageRegistration; import org.geysermc.floodgate.pluginmessage.VelocityPluginMessageUtils; import org.geysermc.floodgate.skin.SkinApplier; -import org.geysermc.floodgate.util.LanguageManager; import org.geysermc.floodgate.util.VelocityCommandUtil; +import org.geysermc.floodgate.util.VelocityPlatformUtils; import org.geysermc.floodgate.util.VelocitySkinApplier; -import org.slf4j.Logger; +import org.incendo.cloud.CommandManager; +import org.incendo.cloud.execution.ExecutionCoordinator; +import org.incendo.cloud.velocity.CloudInjectionModule; +import org.incendo.cloud.velocity.VelocityCommandManager; @RequiredArgsConstructor public final class VelocityPlatformModule extends AbstractModule { @@ -67,6 +67,9 @@ public final class VelocityPlatformModule extends AbstractModule { @Override protected void configure() { bind(CommandUtil.class).to(VelocityCommandUtil.class); + bind(PlatformUtils.class).to(VelocityPlatformUtils.class); + bind(FloodgateLogger.class).to(Slf4jFloodgateLogger.class); + bind(SkinApplier.class).to(VelocitySkinApplier.class); } @Provides @@ -74,9 +77,8 @@ protected void configure() { public CommandManager commandManager(CommandUtil commandUtil) { Injector child = guice.createChildInjector(new CloudInjectionModule<>( UserAudience.class, - CommandExecutionCoordinator.simpleCoordinator(), - commandUtil::getUserAudience, - audience -> (CommandSource) audience.source() + ExecutionCoordinator.simpleCoordinator(), + new FloodgateSenderMapper<>(commandUtil) )); CommandManager commandManager = @@ -86,12 +88,6 @@ public CommandManager commandManager(CommandUtil commandUtil) { return commandManager; } - @Provides - @Singleton - public FloodgateLogger floodgateLogger(Logger logger, LanguageManager languageManager) { - return new Slf4jFloodgateLogger(logger, languageManager); - } - /* Commands / Listeners */ @@ -116,20 +112,14 @@ public PluginMessageRegistration pluginMessageRegistration(ProxyServer proxy) { return new VelocityPluginMessageRegistration(proxy); } - @Provides - @Singleton - public SkinApplier skinApplier(ProxyServer server) { - return new VelocitySkinApplier(server); - } - /* DebugAddon / PlatformInjector */ @Provides @Singleton - public CommonPlatformInjector platformInjector(ProxyServer server, FloodgateLogger logger) { - return new VelocityInjector(server, logger); + public CommonPlatformInjector platformInjector(ProxyServer server) { + return new VelocityInjector(server); } @Provides diff --git a/velocity/src/main/java/org/geysermc/floodgate/pluginmessage/VelocityPluginMessageUtils.java b/velocity/src/main/java/org/geysermc/floodgate/pluginmessage/VelocityPluginMessageUtils.java index 482e5712..f6a8b75f 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/pluginmessage/VelocityPluginMessageUtils.java +++ b/velocity/src/main/java/org/geysermc/floodgate/pluginmessage/VelocityPluginMessageUtils.java @@ -33,7 +33,6 @@ import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.proxy.ServerConnection; import com.velocitypowered.api.proxy.messages.ChannelIdentifier; -import com.velocitypowered.api.proxy.messages.ChannelMessageSink; import com.velocitypowered.api.proxy.messages.ChannelMessageSource; import com.velocitypowered.api.proxy.messages.MinecraftChannelIdentifier; import java.util.UUID; @@ -64,21 +63,6 @@ public void onPluginMessage(PluginMessageEvent event) { return; } - UUID targetUuid = null; - String targetUsername = null; - Identity targetIdentity = Identity.UNKNOWN; - - ChannelMessageSink target = event.getTarget(); - if (target instanceof Player) { - Player player = (Player) target; - targetUuid = player.getUniqueId(); - targetUsername = player.getUsername(); - targetIdentity = Identity.PLAYER; - - } else if (target instanceof ServerConnection) { - targetIdentity = Identity.SERVER; - } - UUID sourceUuid = null; String sourceUsername = null; Identity sourceIdentity = Identity.UNKNOWN; @@ -94,8 +78,9 @@ public void onPluginMessage(PluginMessageEvent event) { sourceIdentity = Identity.SERVER; } - Result result = channel.handleProxyCall(event.getData(), targetUuid, targetUsername, - targetIdentity, sourceUuid, sourceUsername, sourceIdentity); + Result result = channel.handleProxyCall( + event.getData(), sourceUuid, sourceUsername, sourceIdentity + ); event.setResult(result.isAllowed() ? ForwardResult.forward() : ForwardResult.handled()); diff --git a/velocity/src/main/java/org/geysermc/floodgate/util/VelocityCommandUtil.java b/velocity/src/main/java/org/geysermc/floodgate/util/VelocityCommandUtil.java index 8edf09da..a2c355f3 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/util/VelocityCommandUtil.java +++ b/velocity/src/main/java/org/geysermc/floodgate/util/VelocityCommandUtil.java @@ -90,13 +90,13 @@ protected Collection getOnlinePlayers() { @Override public Object getPlayerByUuid(@NonNull UUID uuid) { Optional player = server.getPlayer(uuid); - return player.isPresent() ? player : uuid; + return player.isPresent() ? player.get() : uuid; } @Override public Object getPlayerByUsername(@NonNull String username) { Optional player = server.getPlayer(username); - return player.isPresent() ? player : username; + return player.isPresent() ? player.get() : username; } @Override diff --git a/core/src/main/java/org/geysermc/floodgate/config/FloodgateConfigHolder.java b/velocity/src/main/java/org/geysermc/floodgate/util/VelocityPlatformUtils.java similarity index 63% rename from core/src/main/java/org/geysermc/floodgate/config/FloodgateConfigHolder.java rename to velocity/src/main/java/org/geysermc/floodgate/util/VelocityPlatformUtils.java index a4fd63b4..0340cfac 100644 --- a/core/src/main/java/org/geysermc/floodgate/config/FloodgateConfigHolder.java +++ b/velocity/src/main/java/org/geysermc/floodgate/util/VelocityPlatformUtils.java @@ -23,37 +23,29 @@ * @link https://github.com/GeyserMC/Floodgate */ -package org.geysermc.floodgate.config; +package org.geysermc.floodgate.util; -import org.geysermc.floodgate.util.FloodgateInfoHolder; +import com.google.inject.Inject; +import com.velocitypowered.api.network.ProtocolVersion; +import com.velocitypowered.api.proxy.ProxyServer; +import org.geysermc.floodgate.platform.util.PlatformUtils; -public class FloodgateConfigHolder { - private FloodgateConfig config; +public final class VelocityPlatformUtils extends PlatformUtils { + @Inject + private ProxyServer server; - public boolean has() { - return config != null; + @Override + public AuthType authType() { + return server.getConfiguration().isOnlineMode() ? AuthType.ONLINE : AuthType.OFFLINE; } - public boolean isProxy() { - return config instanceof ProxyFloodgateConfig; + @Override + public String minecraftVersion() { + return ProtocolVersion.MAXIMUM_VERSION.getMostRecentSupportedVersion(); } - public FloodgateConfig get() { - return config; - } - - public ProxyFloodgateConfig getAsProxy() { - return getAs(); - } - - @SuppressWarnings("unchecked") - public T getAs() { - return (T) config; - } - - public void set(FloodgateConfig config) { - this.config = config; - // for Geyser dump - FloodgateInfoHolder.setConfig(config); + @Override + public String serverImplementationName() { + return server.getVersion().getName(); } } diff --git a/velocity/src/main/java/org/geysermc/floodgate/util/VelocitySkinApplier.java b/velocity/src/main/java/org/geysermc/floodgate/util/VelocitySkinApplier.java index 2d925725..f9818cbc 100644 --- a/velocity/src/main/java/org/geysermc/floodgate/util/VelocitySkinApplier.java +++ b/velocity/src/main/java/org/geysermc/floodgate/util/VelocitySkinApplier.java @@ -25,25 +25,60 @@ package org.geysermc.floodgate.util; +import com.google.inject.Inject; +import com.google.inject.Singleton; import com.velocitypowered.api.proxy.ProxyServer; import com.velocitypowered.api.util.GameProfile.Property; import java.util.ArrayList; import java.util.List; -import lombok.RequiredArgsConstructor; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.floodgate.api.event.skin.SkinApplyEvent; +import org.geysermc.floodgate.api.event.skin.SkinApplyEvent.SkinData; import org.geysermc.floodgate.api.player.FloodgatePlayer; +import org.geysermc.floodgate.event.EventBus; +import org.geysermc.floodgate.event.skin.SkinApplyEventImpl; import org.geysermc.floodgate.skin.SkinApplier; -import org.geysermc.floodgate.skin.SkinData; +import org.geysermc.floodgate.skin.SkinDataImpl; -@RequiredArgsConstructor +@Singleton public class VelocitySkinApplier implements SkinApplier { - private final ProxyServer server; + @Inject private ProxyServer server; + @Inject private EventBus eventBus; @Override - public void applySkin(FloodgatePlayer floodgatePlayer, SkinData skinData) { + public void applySkin(@NonNull FloodgatePlayer floodgatePlayer, @NonNull SkinData skinData) { server.getPlayer(floodgatePlayer.getCorrectUniqueId()).ifPresent(player -> { List properties = new ArrayList<>(player.getGameProfileProperties()); - properties.add(new Property("textures", skinData.getValue(), skinData.getSignature())); + + SkinData currentSkin = currentSkin(properties); + + SkinApplyEvent event = new SkinApplyEventImpl(floodgatePlayer, currentSkin, skinData); + event.setCancelled(floodgatePlayer.isLinked()); + + eventBus.fire(event); + + if (event.isCancelled()) { + return; + } + + replaceSkin(properties, event.newSkin()); player.setGameProfileProperties(properties); }); } + + private SkinData currentSkin(List properties) { + for (Property property : properties) { + if (property.getName().equals("textures")) { + if (!property.getValue().isEmpty()) { + return new SkinDataImpl(property.getValue(), property.getSignature()); + } + } + } + return null; + } + + private void replaceSkin(List properties, SkinData skinData) { + properties.removeIf(property -> property.getName().equals("textures")); + properties.add(new Property("textures", skinData.value(), skinData.signature())); + } } diff --git a/velocity/src/main/resources/velocity-plugin.json b/velocity/src/main/resources/velocity-plugin.json index a4df39d2..dc7d5e1f 100644 --- a/velocity/src/main/resources/velocity-plugin.json +++ b/velocity/src/main/resources/velocity-plugin.json @@ -1 +1 @@ -{"id": "${id}", "name": "${name}", "version": "${version}", "description": "${description}", "url": "$url}", "authors": ["${author}"], "main": "org.geysermc.floodgate.VelocityPlugin"} \ No newline at end of file +{"id": "${id}", "name": "${name}", "version": "${version}", "description": "${description}", "url": "${url}", "authors": ["${author}"], "main": "org.geysermc.floodgate.VelocityPlugin"} \ No newline at end of file