diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0a788a8..921adad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/setup-java@v3 with: - java-version: 17 + java-version: 21 distribution: temurin - name: Build @@ -27,23 +27,12 @@ jobs: arguments: build cache-read-only: ${{ github.ref_name != 'master' && github.ref_name != 'development' }} - - name: Build image - if: ${{ github.repository == 'GeyserMC/GlobalLinkServer' && github.ref_name == 'master' }} - run: docker build . -t $IMAGE_NAME --label "run-number=${GITHUB_RUN_ID}" - - name: Registry login - if: ${{ github.repository == 'GeyserMC/GlobalLinkServer' && github.ref_name == 'master' }} - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin - - name: Push image - if: ${{ github.repository == 'GeyserMC/GlobalLinkServer' && github.ref_name == 'master' }} - run: | - IMAGE_ID=ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME - IMAGE_ID=$(echo $IMAGE_ID | tr '[A-Z]' '[a-z]') - VERSION=latest - - echo IMAGE_ID=$IMAGE_ID - echo VERSION=$VERSION - docker tag $IMAGE_NAME $IMAGE_ID:$VERSION - docker push $IMAGE_ID:$VERSION + - name: Archive Artifacts + uses: GeyserMC/actions/upload-multi-artifact@master + if: success() + with: + artifacts: | + GlobalLinkPlugin:build/libs/GlobalLinkPlugin.jar - name: Notify Discord if: ${{ (success() || failure()) && github.repository == 'GeyserMC/GlobalLinkServer' }} diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 98c2fec..0000000 --- a/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM eclipse-temurin:21-jre - -WORKDIR /app -COPY build/libs/GlobalLinkServer.jar GlobalLinkServer.jar - -CMD ["java", "-jar", "GlobalLinkServer.jar"] diff --git a/LICENSE b/LICENSE index 6efc431..648eb0d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License -Copyright (c) 2021-2024 GeyserMC +Copyright (c) 2021-2025 GeyserMC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 0000000..982f6f2 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# GlobalLinkServer Plugin + +> [!IMPORTANT] +> This repo contains the code for the Geyser Global Linking Server plugin. +> If you want to just link your account, join link.geysermc.org on Minecraft Java or Bedrock. +> For more information visit https://link.geysermc.org/ + +### `server.properties` +```properties +allow-nether=false +generate-structures=false +generator-settings={"biome"\:"minecraft\:the_void","layers"\:[{"block"\:"minecraft\:air","height"\:1}]} +level-type=minecraft\:flat +spawn-protection=200 +``` + +### `spigot.yml` +```yaml +commands: + send-namespaced: false +``` + + +### `bukkit.yml` +```yaml +settings: + allow-end: false +``` + diff --git a/build.gradle.kts b/build.gradle.kts index 62eb7ac..e2a33ae 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,33 +1,22 @@ plugins { - application alias(libs.plugins.indra) alias(libs.plugins.indra.licenser.spotless) + alias(libs.plugins.paperweight) + alias(libs.plugins.runpaper) } group = "org.geysermc.globallinkserver" dependencies { - implementation(libs.gson) // newer version required for record support - implementation(libs.fastutil.common) + paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT") - implementation(libs.bundles.protocol) - implementation(libs.mcprotocollib) { - exclude("io.netty", "netty-all") - } - - // mcprotocollib won't work without this - implementation(libs.netty.handler) - - implementation(libs.adventure.text.legacy) + compileOnly(libs.floodgate.api) implementation(libs.mariadb.client) + implementation(libs.bundles.fastutil) compileOnly(libs.checker.qual) } -application { - mainClass.set("org.geysermc.globallinkserver.GlobalLinkServer") -} - indra { github("GeyserMC", "GlobalLinkServer") { ci(true) @@ -38,27 +27,39 @@ indra { mitLicense() javaVersions { - target(17) + target(21) } spotless { java { - palantirJavaFormat() + // Broken until paperweight updates to a newer version of spotless (6.23.3+) +// palantirJavaFormat() formatAnnotations() } ratchetFrom("origin/master") } } +repositories { + mavenLocal() + + maven("https://repo.opencollab.dev/main") + maven("https://repo.papermc.io/repository/maven-public/") + + mavenCentral() + + maven("https://jitpack.io") { + content { includeGroupByRegex("com\\.github\\..*") } + } +} + +paperweight.reobfArtifactConfiguration = io.papermc.paperweight.userdev.ReobfArtifactConfiguration.MOJANG_PRODUCTION + tasks.jar { duplicatesStrategy = DuplicatesStrategy.EXCLUDE from(configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }) - archiveBaseName = "GlobalLinkServer" + archiveBaseName = "GlobalLinkPlugin" archiveVersion = "" archiveClassifier = "" - - manifest { - attributes["Main-Class"] = application.mainClass - } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index bd60cdf..718a495 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=1.0-SNAPSHOT \ No newline at end of file +version=2.0-SNAPSHOT \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 375193f..56cb898 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,33 +1,30 @@ [versions] -gson = "2.10.1" -protocol = "3.0.0.Beta5-20241022.154658-14" -netty = "4.1.110.Final" -fastutil = "8.5.2" -mcprotocollib = "86903ec" # Revert from jitpack after release -adventure-text = "4.15.0-20231207.074016-23" # Match version to MCPL +floodgate = "2.2.3-SNAPSHOT" mariadb-client = "2.7.3" +fastutil = "8.5.2" checker-qual = "3.21.1" + indra = "3.1.2" +paperweight = "2.0.0-beta.12" +runpaper = "2.3.1" [libraries] -gson = { module = "com.google.code.gson:gson", version.ref = "gson" } -protocol-codec = { module = "org.cloudburstmc.protocol:bedrock-codec", version.ref = "protocol" } -protocol-connection = { module = "org.cloudburstmc.protocol:bedrock-connection", version.ref = "protocol" } -protocol-common = { module = "org.cloudburstmc.protocol:common", version.ref = "protocol" } - -netty-handler = { group = "io.netty", name = "netty-handler", version.ref = "netty" } - -fastutil-common = { module = "com.nukkitx.fastutil:fastutil-common", version.ref = "fastutil" } -mcprotocollib = { module = "com.github.GeyserMC:mcprotocollib", version.ref = "mcprotocollib" } -adventure-text-legacy = { module = "net.kyori:adventure-text-serializer-legacy", version.ref = "adventure-text" } +floodgate-api = { group = "org.geysermc.floodgate", name = "api", version.ref = "floodgate" } mariadb-client = { module = "org.mariadb.jdbc:mariadb-java-client", version.ref = "mariadb-client" } +fastutil-int-int-maps = { group = "com.nukkitx.fastutil", name = "fastutil-int-int-maps", version.ref = "fastutil" } +fastutil-int-object-maps = { group = "com.nukkitx.fastutil", name = "fastutil-int-object-maps", version.ref = "fastutil" } +fastutil-object-int-maps = { group = "com.nukkitx.fastutil", name = "fastutil-object-int-maps", version.ref = "fastutil" } +fastutil-object-object-maps = { group = "com.nukkitx.fastutil", name = "fastutil-object-object-maps", version.ref = "fastutil" } + checker-qual = { module = "org.checkerframework:checker-qual", version.ref = "checker-qual" } [plugins] indra = { id = "net.kyori.indra", version.ref = "indra" } indra-publishing = { id = "net.kyori.indra.publishing", version.ref = "indra" } indra-licenser-spotless = { id = "net.kyori.indra.licenser.spotless", version.ref = "indra" } +paperweight = { id = "io.papermc.paperweight.userdev", version.ref = "paperweight" } +runpaper = { id = "xyz.jpenilla.run-paper", version.ref = "runpaper" } [bundles] -protocol = ["protocol-codec", "protocol-connection", "protocol-common"] \ No newline at end of file +fastutil = [ "fastutil-int-int-maps", "fastutil-int-object-maps", "fastutil-object-int-maps", "fastutil-object-object-maps" ] diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 033e24c..a4b76b9 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 62f495d..cea7a79 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index fcb6fca..f5feea6 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -83,7 +85,9 @@ done # This is normally unused # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} -APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -144,7 +148,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then case $MAX_FD in #( max*) # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 MAX_FD=$( ulimit -H -n ) || warn "Could not query maximum file descriptor limit" esac @@ -152,7 +156,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then '' | soft) :;; #( *) # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC3045 + # shellcheck disable=SC2039,SC3045 ulimit -n "$MAX_FD" || warn "Could not set maximum file descriptor limit to $MAX_FD" esac @@ -201,11 +205,11 @@ fi # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' -# Collect all arguments for the java command; -# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of -# shell script including quotes and variable substitutions, so put them in -# double quotes to make sure that they get re-expanded; and -# * put everything else in single quotes, so that it's not re-expanded. +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ diff --git a/gradlew.bat b/gradlew.bat index 93e3f59..9d21a21 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## @@ -43,11 +45,11 @@ set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 if %ERRORLEVEL% equ 0 goto execute -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail @@ -57,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe if exist "%JAVA_EXE%" goto execute -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 goto fail diff --git a/settings.gradle.kts b/settings.gradle.kts index 3747c5f..3f8d8b3 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,17 +1,9 @@ @file:Suppress("UnstableApiUsage") -dependencyResolutionManagement { - repositoriesMode = RepositoriesMode.FAIL_ON_PROJECT_REPOS +pluginManagement { repositories { - mavenLocal() - - maven("https://repo.opencollab.dev/main") - - mavenCentral() - - maven("https://jitpack.io") { - content { includeGroupByRegex("com\\.github\\..*") } - } + gradlePluginPortal() + maven("https://repo.papermc.io/repository/maven-public/") } } diff --git a/src/main/java/org/geysermc/globallinkserver/Components.java b/src/main/java/org/geysermc/globallinkserver/Components.java new file mode 100644 index 0000000..d515372 --- /dev/null +++ b/src/main/java/org/geysermc/globallinkserver/Components.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2025 GeyserMC + * Licensed under the MIT license + * @link https://github.com/GeyserMC/GlobalLinkServer + */ +package org.geysermc.globallinkserver; + +import java.util.UUID; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public final class Components { + private Components() {} + + private static final String PLATFORM_BEDROCK = "Bedrock"; + private static final String PLATFORM_JAVA = "Java"; + + public static final Component MOTD = Component.text("GeyserMC ", NamedTextColor.GREEN) + .append(Component.text("Link ", NamedTextColor.AQUA)) + .append(Component.text("Server", NamedTextColor.WHITE)); + + public static final Component LINK_INSTRUCTION = Component.text( + "You are not linked. To link, run the ", NamedTextColor.AQUA) + .append(Component.text("`/link`", NamedTextColor.GREEN)) + .append(Component.text(" command.", NamedTextColor.AQUA)); + + public static final Component UNLINK_INSTRUCTION = Component.text( + "You are currently linked. To unlink, use ", NamedTextColor.AQUA) + .append(Component.text("`/unlink`", NamedTextColor.RED)) + .append(Component.text(".")); + + public static final Component LINK_INFO_UNAVAILABLE = + Component.text("Your linking information is currently unavailable. Please wait!", NamedTextColor.RED); + public static final Component KICK_IDLE = Component.text("You have been idle for too long!"); + public static final Component LINK_ALREADY_LINKED = Component.text( + "You are already linked! You need to unlink first before linking again.", NamedTextColor.RED); + public static final Component LINK_CODE_INVALID_RANGE = Component.text("Invalid link code!", NamedTextColor.RED); + public static final Component LINK_REQUEST_REPLACED = Component.text( + "You already had an active link request, so your old request has been invalidated.", NamedTextColor.AQUA); + public static final Component LINK_REQUEST_NOT_FOUND = + Component.text("Could not find the provided link. Has it expired?", NamedTextColor.RED); + public static final Component LINK_REQUEST_SAME_PLATFORM = Component.text( + "You can only link a Java account to a Bedrock account. ", NamedTextColor.RED) + .append(Component.text("Try to start the linking process again!")); + public static final Component LINK_CREATE_ERROR = Component.text( + "An unknown error occurred while linking your account. Try it again later!", NamedTextColor.RED); + public static final Component LINK_CREATE_SUCCESS = + Component.text("You are now successfully linked! :)", NamedTextColor.GREEN); + + public static final Component UNLINK_NOT_LINKED = + Component.text("You are not linked to any account!", NamedTextColor.RED); + public static final Component UNLINK_ERROR = Component.text( + "An unknown error occurred while unlinking your account. Try it again later!", NamedTextColor.RED); + public static final Component UNLINK_SUCCESS = + Component.text("You are successfully unlinked.", NamedTextColor.GREEN); + + public static final Component INFO_NOT_LINKED = UNLINK_NOT_LINKED.color(NamedTextColor.AQUA); + public static final Component INFO_UNAVAILABLE = Component.text("Failed to find current link!", NamedTextColor.RED); + + public static Component linkStarted(String otherPlatform, String code) { + return Component.text("Please join on %s and run ".formatted(otherPlatform), NamedTextColor.GREEN) + .append(Component.text("`/link " + code + "`", NamedTextColor.AQUA)); + } + + public static Component infoLinkInfo(String username, UUID uuid, boolean bedrock) { + return Component.text( + String.format( + "You are currently linked to the %s player %s (%s).", + bedrock ? PLATFORM_BEDROCK : PLATFORM_JAVA, username, uuid), + NamedTextColor.GREEN); + } + + public static Component cleanupLinkRequestExpired(int code) { + return Component.text( + "Your link (%s) has expired! Run the link account again if you need a new code.".formatted(code), + NamedTextColor.RED); + } +} diff --git a/src/main/java/org/geysermc/globallinkserver/GlobalLinkServer.java b/src/main/java/org/geysermc/globallinkserver/GlobalLinkServer.java index c2ac3cb..2fa69c3 100644 --- a/src/main/java/org/geysermc/globallinkserver/GlobalLinkServer.java +++ b/src/main/java/org/geysermc/globallinkserver/GlobalLinkServer.java @@ -1,46 +1,253 @@ /* - * Copyright (c) 2021-2024 GeyserMC + * Copyright (c) 2021-2025 GeyserMC * Licensed under the MIT license * @link https://github.com/GeyserMC/GlobalLinkServer */ package org.geysermc.globallinkserver; -import java.util.Timer; -import java.util.TimerTask; -import java.util.logging.Logger; -import org.geysermc.globallinkserver.bedrock.BedrockServer; -import org.geysermc.globallinkserver.config.Config; +import com.destroystokyo.paper.event.server.PaperServerListPingEvent; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.event.player.AsyncChatEvent; +import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager; +import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import org.bukkit.Bukkit; +import org.bukkit.GameMode; +import org.bukkit.GameRule; +import org.bukkit.World; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.block.Action; +import org.bukkit.event.entity.FoodLevelChangeEvent; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.player.PlayerCommandPreprocessEvent; +import org.bukkit.event.player.PlayerCommandSendEvent; +import org.bukkit.event.player.PlayerInteractEvent; +import org.bukkit.event.player.PlayerQuitEvent; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.java.JavaPlugin; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.geysermc.floodgate.api.FloodgateApi; import org.geysermc.globallinkserver.config.ConfigReader; -import org.geysermc.globallinkserver.java.JavaServer; +import org.geysermc.globallinkserver.handler.CommandHandler; +import org.geysermc.globallinkserver.handler.JoinHandler; +import org.geysermc.globallinkserver.handler.MoveInactivityHandler; +import org.geysermc.globallinkserver.handler.TeleportToSpawnHandler; import org.geysermc.globallinkserver.link.LinkManager; -import org.geysermc.globallinkserver.player.PlayerManager; +import org.geysermc.globallinkserver.manager.DatabaseManager; +import org.geysermc.globallinkserver.manager.PlayerManager; +import org.geysermc.globallinkserver.service.LinkInfoService; +import org.geysermc.globallinkserver.service.LinkLookupService; +import org.geysermc.globallinkserver.util.MultiConditionSet; +import org.geysermc.globallinkserver.util.Utils; -public class GlobalLinkServer { - private static final Timer TIMER = new Timer(); - public static final Logger LOGGER = Logger.getGlobal(); +@SuppressWarnings("UnstableApiUsage") +public class GlobalLinkServer extends JavaPlugin implements Listener { + private static final Set PERMITTED_COMMANDS = + Set.of("link", "linkaccount", "linkinfo", "info", "unlink", "unlinkaccount", "help"); - public static void main(String... args) { - // Make logging more simple, adopted from https://stackoverflow.com/a/5937929 - System.setProperty( - "java.util.logging.SimpleFormatter.format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS %5$s%6$s%n"); + private final MultiConditionSet playerIdleTracker = new MultiConditionSet<>(15_000, uuid -> { + Player player = Bukkit.getPlayer(uuid); + if (player != null) { + Bukkit.getScheduler().callSyncMethod(this, () -> { + player.kick(Components.KICK_IDLE); + return null; + }); + } + }); - Config config = ConfigReader.readConfig(); + private LinkLookupService linkLookupService; + private LinkInfoService linkInfoService; - PlayerManager playerManager = new PlayerManager(); - LinkManager linkManager = new LinkManager(config); + @Override + public void onEnable() { + var config = ConfigReader.readConfig(this); - new JavaServer(playerManager, linkManager).startServer(config); - new BedrockServer(playerManager, linkManager).startServer(config); + var playerManager = new PlayerManager(FloodgateApi.getInstance()); + var databaseManager = new DatabaseManager(config); + var linkManager = new LinkManager(playerManager, databaseManager); + linkLookupService = new LinkLookupService(playerManager, databaseManager); + linkInfoService = new LinkInfoService(linkLookupService, playerManager); - TimerTask task = new TimerTask() { - @Override - public void run() { - linkManager.cleanupTempLinks(playerManager); + var commandUtils = new CommandHandler(linkLookupService, linkInfoService, linkManager, playerManager, this); + + // clean up link requests every 30s + Bukkit.getScheduler().scheduleSyncRepeatingTask(this, linkManager::cleanupLinkRequests, 60 * 20, 30 * 20); + + Bukkit.getScheduler().scheduleSyncRepeatingTask(this, this::broadcastLinkStatusActionbar, 10, 15); + + var pluginManager = getServer().getPluginManager(); + pluginManager.registerEvents(this, this); + pluginManager.registerEvents(new JoinHandler(linkLookupService, playerIdleTracker, this), this); + pluginManager.registerEvents(new MoveInactivityHandler(playerIdleTracker), this); + pluginManager.registerEvents(new TeleportToSpawnHandler(config.spawn()), this); + + // if the player has an active link request, don't kick the player + playerIdleTracker.addRemovalCondition(uuid -> !linkManager.hasActiveLinkRequest(uuid)); + + LifecycleEventManager<@NonNull Plugin> manager = this.getLifecycleManager(); + manager.registerEventHandler(LifecycleEvents.COMMANDS, event -> { + final Commands commands = event.registrar(); + commands.register( + Commands.literal("link") + .requires(ctx -> ctx.getSender() instanceof Player) + .executes(commandUtils::startLink) + .then(Commands.argument("code", IntegerArgumentType.integer(0, 9999)) + .executes(commandUtils::linkWithCode)) + .build(), + "Use this command to link your Java and Bedrock account.", + List.of("linkaccount")); + commands.register( + Commands.literal("unlink") + .requires(ctx -> ctx.getSender() instanceof Player) + .executes(commandUtils::unlink) + .build(), + "Use this command to unlink your Java and Bedrock account.", + List.of("unlinkaccount")); + commands.register( + Commands.literal("linkinfo") + .requires(ctx -> ctx.getSender() instanceof Player) + .executes(ctx -> { + linkInfoService.sendCurrentLinkInfo(Utils.contextExecutor(ctx)); + return 1; + }) + .build(), + "Use this command to show information whether you are currently linked.", + List.of("info")); + }); + + // Set game rules + World world = config.spawn().getWorld(); + world.setGameRule(GameRule.ANNOUNCE_ADVANCEMENTS, false); + world.setGameRule(GameRule.DO_MOB_SPAWNING, false); + world.setGameRule(GameRule.DO_WEATHER_CYCLE, false); + world.setGameRule(GameRule.DO_DAYLIGHT_CYCLE, false); + world.setGameRule(GameRule.FALL_DAMAGE, false); + world.setGameRule(GameRule.DO_IMMEDIATE_RESPAWN, true); + world.setGameRule(GameRule.RANDOM_TICK_SPEED, 0); + world.setGameRule(GameRule.SHOW_DEATH_MESSAGES, false); + + // Make nighttime + world.setTime(18000); + + // Other changes + getServer().motd(Components.MOTD); + + getServer().clearRecipes(); + getServer().setDefaultGameMode(GameMode.ADVENTURE); + + getLogger().info("Started Global Linking plugin!"); + } + + private void broadcastLinkStatusActionbar() { + Bukkit.getOnlinePlayers().forEach(player -> { + if (linkLookupService.isLookupCompleted(player)) { + if (linkLookupService.isLinkedCached(player)) { + player.sendActionBar(Components.UNLINK_INSTRUCTION); + } else { + player.sendActionBar(Components.LINK_INSTRUCTION); + } + } + }); + } + + @Override + public void onDisable() { + playerIdleTracker.close(); + } + + @EventHandler + public void onCommands(PlayerCommandSendEvent event) { + if (event.getPlayer().isOp()) { + return; + } + + var toRemove = new ArrayList(); + for (String command : event.getCommands()) { + if (!PERMITTED_COMMANDS.contains(command)) { + toRemove.add(command); } - }; - TIMER.scheduleAtFixedRate(task, 0L, 60_000L); + } + event.getCommands().removeAll(toRemove); + } + + @EventHandler + public void preCommand(PlayerCommandPreprocessEvent event) { + Player player = event.getPlayer(); + if (player.isOp()) { + return; + } + + String command = event.getMessage(); + if (command.startsWith("/")) { + command = command.substring(1); + } + + if (command.equalsIgnoreCase("help")) { + event.setCancelled(true); + + if (!linkLookupService.isLookupCompleted(player)) { + player.sendMessage(Components.LINK_INFO_UNAVAILABLE); + return; + } + + if (linkLookupService.isLinkedCached(player)) { + player.sendMessage(Components.UNLINK_INSTRUCTION); + } else { + player.sendMessage(Components.LINK_INSTRUCTION); + } + return; + } + + for (String permitted : PERMITTED_COMMANDS) { + if (command.startsWith(permitted)) { + return; + } + } + event.setCancelled(true); + } + + @EventHandler + public void onPlayerLeave(PlayerQuitEvent event) { + event.quitMessage(null); + playerIdleTracker.remove(event.getPlayer().getUniqueId()); + linkLookupService.invalidate(event.getPlayer()); + } + + @EventHandler + public void onFoodLevelChange(FoodLevelChangeEvent event) { + if (event.getFoodLevel() < event.getEntity().getFoodLevel()) { + event.setCancelled(true); + } + } + + @EventHandler + public void onPlayerInteract(PlayerInteractEvent event) { + // Prevent crop trampling + if (event.getAction() == Action.PHYSICAL) { + event.setCancelled(true); + } + } + + @EventHandler + public void onDeath(PlayerDeathEvent event) { + event.deathMessage(null); + } + + @EventHandler + public void onPlayerChat(AsyncChatEvent event) { + event.setCancelled(true); + } - LOGGER.info( - "Started Global Linking Server on java: " + config.javaPort() + ", bedrock: " + config.bedrockPort()); + @EventHandler + public void onServerListPing(PaperServerListPingEvent event) { + event.getListedPlayers().clear(); + event.setNumPlayers(0); + event.setMaxPlayers(1); } } diff --git a/src/main/java/org/geysermc/globallinkserver/Server.java b/src/main/java/org/geysermc/globallinkserver/Server.java deleted file mode 100644 index a9a3d1e..0000000 --- a/src/main/java/org/geysermc/globallinkserver/Server.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2021-2021 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/GlobalLinkServer - */ - -package org.geysermc.globallinkserver; - -import org.geysermc.globallinkserver.config.Config; - -public interface Server { - /** - * Starts the server using the server configuration. - * - * @param config the configuration to use - * @return true if the server started successfully, false otherwise - */ - boolean startServer(Config config); - - void shutdown(); -} diff --git a/src/main/java/org/geysermc/globallinkserver/bedrock/BedrockPlayer.java b/src/main/java/org/geysermc/globallinkserver/bedrock/BedrockPlayer.java deleted file mode 100644 index bcbb011..0000000 --- a/src/main/java/org/geysermc/globallinkserver/bedrock/BedrockPlayer.java +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright (c) 2021-2023 GeyserMC - * - * 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/GlobalLinkServer - */ -package org.geysermc.globallinkserver.bedrock; - -import io.netty.buffer.Unpooled; - -import java.util.Collections; -import java.util.UUID; -import org.cloudburstmc.math.vector.Vector2f; -import org.cloudburstmc.math.vector.Vector3f; -import org.cloudburstmc.math.vector.Vector3i; -import org.cloudburstmc.nbt.NbtMap; -import org.cloudburstmc.protocol.bedrock.BedrockServerSession; -import org.cloudburstmc.protocol.bedrock.data.AuthoritativeMovementMode; -import org.cloudburstmc.protocol.bedrock.data.ChatRestrictionLevel; -import org.cloudburstmc.protocol.bedrock.data.EduSharedUriResource; -import org.cloudburstmc.protocol.bedrock.data.GamePublishSetting; -import org.cloudburstmc.protocol.bedrock.data.GameRuleData; -import org.cloudburstmc.protocol.bedrock.data.GameType; -import org.cloudburstmc.protocol.bedrock.data.PlayerPermission; -import org.cloudburstmc.protocol.bedrock.data.SpawnBiomeType; -import org.cloudburstmc.protocol.bedrock.data.command.CommandData; -import org.cloudburstmc.protocol.bedrock.data.command.CommandOverloadData; -import org.cloudburstmc.protocol.bedrock.data.command.CommandParam; -import org.cloudburstmc.protocol.bedrock.data.command.CommandParamData; -import org.cloudburstmc.protocol.bedrock.data.command.CommandPermission; -import org.cloudburstmc.protocol.bedrock.packet.AvailableCommandsPacket; -import org.cloudburstmc.protocol.bedrock.packet.BiomeDefinitionListPacket; -import org.cloudburstmc.protocol.bedrock.packet.LevelChunkPacket; -import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket; -import org.cloudburstmc.protocol.bedrock.packet.SetEntityMotionPacket; -import org.cloudburstmc.protocol.bedrock.packet.StartGamePacket; -import org.cloudburstmc.protocol.bedrock.packet.TextPacket; -import org.cloudburstmc.protocol.bedrock.util.ChainValidationResult; -import org.cloudburstmc.protocol.common.util.OptionalBoolean; -import org.geysermc.globallinkserver.bedrock.util.PaletteUtils; -import org.geysermc.globallinkserver.player.Player; -import org.geysermc.globallinkserver.util.Utils; - -public class BedrockPlayer implements Player { - private final BedrockServerSession session; - private final UUID uniqueId; - private final String xuid; - private final String username; - - private int linkId; - - public BedrockPlayer(BedrockServerSession session, ChainValidationResult.IdentityData identity) { - this.session = session; - this.xuid = identity.xuid; - this.uniqueId = new UUID(0, Utils.parseLong(xuid)); - this.username = identity.displayName; - } - - @Override - public void sendMessage(String message) { - TextPacket packet = new TextPacket(); - packet.setPlatformChatId(""); - packet.setSourceName(""); - packet.setXuid(xuid); - packet.setType(TextPacket.Type.SYSTEM); - packet.setNeedsTranslation(false); - packet.setMessage(formatMessage(message)); - session.sendPacket(packet); - } - - @Override - public void disconnect(String reason) { - session.disconnect(formatMessage(reason)); - } - - /** - * Send a few packets to get the client to load into the world - */ - public void sendStartGame() { - // A lot of this likely doesn't need to be changed - StartGamePacket startGamePacket = new StartGamePacket(); - startGamePacket.setUniqueEntityId(1); - startGamePacket.setRuntimeEntityId(1); - startGamePacket.setPlayerGameType(GameType.CREATIVE); - startGamePacket.setPlayerPosition(Vector3f.from(0, 64 + 2, 0)); - startGamePacket.setRotation(Vector2f.ONE); - startGamePacket.setPlayerPropertyData(NbtMap.EMPTY); - - startGamePacket.setSeed(0L); - startGamePacket.setDimensionId(2); - startGamePacket.setGeneratorId(1); - startGamePacket.setSpawnBiomeType(SpawnBiomeType.DEFAULT); - startGamePacket.setCustomBiomeName(""); - startGamePacket.setForceExperimentalGameplay(OptionalBoolean.empty()); - startGamePacket.setLevelGameType(GameType.CREATIVE); - startGamePacket.setDifficulty(0); - startGamePacket.setDefaultSpawn(Vector3i.ZERO); - startGamePacket.setAchievementsDisabled(true); - startGamePacket.setCurrentTick(-1); - startGamePacket.setEduEditionOffers(0); - startGamePacket.setEduFeaturesEnabled(false); - startGamePacket.setEducationProductionId(""); - startGamePacket.setEduSharedUriResource(EduSharedUriResource.EMPTY); - startGamePacket.setRainLevel(0); - startGamePacket.setLightningLevel(0); - startGamePacket.setMultiplayerGame(true); - startGamePacket.setBroadcastingToLan(true); - startGamePacket.getGamerules().add(new GameRuleData<>("showcoordinates", false)); - startGamePacket.setPlatformBroadcastMode(GamePublishSetting.PUBLIC); - startGamePacket.setXblBroadcastMode(GamePublishSetting.PUBLIC); - startGamePacket.setCommandsEnabled(true); - startGamePacket.setChatRestrictionLevel(ChatRestrictionLevel.NONE); - startGamePacket.setTexturePacksRequired(false); - startGamePacket.setBonusChestEnabled(false); - startGamePacket.setStartingWithMap(false); - startGamePacket.setTrustingPlayers(true); - startGamePacket.setDefaultPlayerPermission(PlayerPermission.VISITOR); - startGamePacket.setServerChunkTickRange(4); - startGamePacket.setBehaviorPackLocked(false); - startGamePacket.setResourcePackLocked(false); - startGamePacket.setFromLockedWorldTemplate(false); - startGamePacket.setUsingMsaGamertagsOnly(false); - startGamePacket.setFromWorldTemplate(false); - startGamePacket.setWorldTemplateOptionLocked(false); - - startGamePacket.setServerEngine(""); - startGamePacket.setLevelId(""); - startGamePacket.setLevelName("GlobalLinkServer"); - startGamePacket.setPremiumWorldTemplateId(""); - startGamePacket.setWorldTemplateId(new UUID(0, 0)); - startGamePacket.setCurrentTick(0); - startGamePacket.setEnchantmentSeed(0); - startGamePacket.setMultiplayerCorrelationId(""); - startGamePacket.setVanillaVersion("*"); - - startGamePacket.setAuthoritativeMovementMode(AuthoritativeMovementMode.CLIENT); - startGamePacket.setRewindHistorySize(0); - startGamePacket.setServerAuthoritativeBlockBreaking(false); - - startGamePacket.setServerId(""); - startGamePacket.setWorldId(""); - startGamePacket.setScenarioId(""); - - session.sendPacket(startGamePacket); - - // Send an empty chunk - LevelChunkPacket data = new LevelChunkPacket(); - data.setChunkX(0); - data.setChunkZ(0); - data.setSubChunksLength(0); - data.setData(Unpooled.wrappedBuffer(PaletteUtils.EMPTY_LEVEL_CHUNK_DATA)); - data.setCachingEnabled(false); - session.sendPacket(data); - - // Send the biomes - BiomeDefinitionListPacket biomeDefinitionListPacket = new BiomeDefinitionListPacket(); - biomeDefinitionListPacket.setDefinitions(PaletteUtils.BIOMES_PALETTE); - session.sendPacket(biomeDefinitionListPacket); - - // Let the client know the player can spawn - PlayStatusPacket playStatusPacket = new PlayStatusPacket(); - playStatusPacket.setStatus(PlayStatusPacket.Status.PLAYER_SPAWN); - session.sendPacket(playStatusPacket); - - // Freeze the player - SetEntityMotionPacket setEntityMotionPacket = new SetEntityMotionPacket(); - setEntityMotionPacket.setRuntimeEntityId(1); - setEntityMotionPacket.setMotion(Vector3f.ZERO); - session.sendPacket(setEntityMotionPacket); - - // Send the available commands - AvailableCommandsPacket availableCommandsPacket = new AvailableCommandsPacket(); - - CommandParamData linkIdParam = new CommandParamData(); - linkIdParam.setName("linkId"); - linkIdParam.setType(CommandParam.INT); - linkIdParam.setOptional(true); - - availableCommandsPacket.getCommands().add(new CommandData( - "linkaccount", - "Link your account", - Collections.EMPTY_SET, - CommandPermission.ANY, - null, - Collections.EMPTY_LIST, - new CommandOverloadData[] { - new CommandOverloadData( - false, - new CommandParamData[] { - linkIdParam - } - ) - } - )); - - availableCommandsPacket.getCommands().add(new CommandData( - "unlinkaccount", - "Unlink your account", - Collections.EMPTY_SET, - CommandPermission.ANY, - null, - Collections.EMPTY_LIST, - new CommandOverloadData[] {} - )); - - session.sendPacket(availableCommandsPacket); - } - - public BedrockServerSession session() { - return session; - } - - @Override - public UUID uniqueId() { - return uniqueId; - } - - public String xuid() { - return xuid; - } - - @Override - public String username() { - return username; - } - - @Override - public int linkId() { - return linkId; - } - - @Override - public void linkId(int linkId) { - this.linkId = linkId; - } -} diff --git a/src/main/java/org/geysermc/globallinkserver/bedrock/BedrockServer.java b/src/main/java/org/geysermc/globallinkserver/bedrock/BedrockServer.java deleted file mode 100644 index 6e1b5a9..0000000 --- a/src/main/java/org/geysermc/globallinkserver/bedrock/BedrockServer.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) 2021-2023 GeyserMC - * - * 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/GlobalLinkServer - */ -package org.geysermc.globallinkserver.bedrock; - -import java.net.InetSocketAddress; -import java.util.concurrent.ThreadLocalRandom; - -import org.cloudburstmc.protocol.bedrock.BedrockPong; -import org.cloudburstmc.protocol.bedrock.BedrockServerSession; -import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; -import org.cloudburstmc.protocol.bedrock.netty.initializer.BedrockServerInitializer; -import org.geysermc.globallinkserver.Server; -import org.geysermc.globallinkserver.bedrock.util.BedrockVersionUtils; -import org.geysermc.globallinkserver.config.Config; -import org.geysermc.globallinkserver.link.LinkManager; -import org.geysermc.globallinkserver.player.PlayerManager; - -public class BedrockServer implements Server { - private final PlayerManager playerManager; - private final LinkManager linkManager; - - private NettyServer server; - - public BedrockServer(PlayerManager playerManager, LinkManager linkManager) { - this.playerManager = playerManager; - this.linkManager = linkManager; - } - - @Override - public boolean startServer(Config config) { - if (server != null) { - return false; - } - - BedrockCodec latestCodec = BedrockVersionUtils.LATEST_CODEC; - server = new NettyServer( - new BedrockPong() - .edition("MCPE") - .motd("Global Linking") - .subMotd("Server") - .playerCount(0) - .maximumPlayerCount(1) - .gameType("Survival") - .ipv4Port(config.bedrockPort()) - .protocolVersion(latestCodec.getProtocolVersion()) - .version(latestCodec.getMinecraftVersion()) - .serverId(ThreadLocalRandom.current().nextLong()), - new ServerInitializer()); - server.bind(new InetSocketAddress(config.bindIp(), config.bedrockPort())) - .awaitUninterruptibly(); - return true; - } - - @Override - public void shutdown() { - server.shutdown(); - server = null; - } - - class ServerInitializer extends BedrockServerInitializer { - @Override - protected void initSession(BedrockServerSession session) { - session.setPacketHandler(new PacketHandler(session, playerManager, linkManager)); - } - } -} diff --git a/src/main/java/org/geysermc/globallinkserver/bedrock/NettyServer.java b/src/main/java/org/geysermc/globallinkserver/bedrock/NettyServer.java deleted file mode 100644 index da3814b..0000000 --- a/src/main/java/org/geysermc/globallinkserver/bedrock/NettyServer.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) 2021-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/GlobalLinkServer - */ - -package org.geysermc.globallinkserver.bedrock; - -import io.netty.bootstrap.ServerBootstrap; -import io.netty.channel.ChannelFuture; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.nio.NioDatagramChannel; -import org.cloudburstmc.netty.channel.raknet.RakChannelFactory; -import org.cloudburstmc.netty.channel.raknet.config.RakChannelOption; -import org.cloudburstmc.protocol.bedrock.BedrockPong; -import org.cloudburstmc.protocol.bedrock.netty.initializer.BedrockServerInitializer; - -import java.net.InetSocketAddress; - -public class NettyServer { - private final EventLoopGroup group; - private final ServerBootstrap bootstrap; - - private ChannelFuture future; - - public NettyServer(BedrockPong pong, BedrockServerInitializer serverInitializer) { - group = new NioEventLoopGroup(); - bootstrap = new ServerBootstrap() - .channelFactory(RakChannelFactory.server(NioDatagramChannel.class)) - .option(RakChannelOption.RAK_GUID, pong.serverId()) - .option(RakChannelOption.RAK_ADVERTISEMENT, pong.toByteBuf()) - .group(group) - .childHandler(serverInitializer); - } - - public ChannelFuture bind(InetSocketAddress address) { - return future = bootstrap.bind(address); - } - - public void shutdown() { - group.shutdownGracefully(); - future.channel().closeFuture().syncUninterruptibly(); - } -} diff --git a/src/main/java/org/geysermc/globallinkserver/bedrock/PacketHandler.java b/src/main/java/org/geysermc/globallinkserver/bedrock/PacketHandler.java deleted file mode 100644 index ca13859..0000000 --- a/src/main/java/org/geysermc/globallinkserver/bedrock/PacketHandler.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (c) 2021-2023 GeyserMC - * - * 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/GlobalLinkServer - */ -package org.geysermc.globallinkserver.bedrock; - -import org.cloudburstmc.protocol.bedrock.BedrockServerSession; -import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; -import org.cloudburstmc.protocol.bedrock.data.PacketCompressionAlgorithm; -import org.cloudburstmc.protocol.bedrock.packet.BedrockPacket; -import org.cloudburstmc.protocol.bedrock.packet.BedrockPacketHandler; -import org.cloudburstmc.protocol.bedrock.packet.ClientCacheStatusPacket; -import org.cloudburstmc.protocol.bedrock.packet.CommandRequestPacket; -import org.cloudburstmc.protocol.bedrock.packet.LoginPacket; -import org.cloudburstmc.protocol.bedrock.packet.NetworkSettingsPacket; -import org.cloudburstmc.protocol.bedrock.packet.PlayStatusPacket; -import org.cloudburstmc.protocol.bedrock.packet.RequestNetworkSettingsPacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackClientResponsePacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePackStackPacket; -import org.cloudburstmc.protocol.bedrock.packet.ResourcePacksInfoPacket; -import org.cloudburstmc.protocol.bedrock.packet.SetLocalPlayerAsInitializedPacket; -import org.cloudburstmc.protocol.bedrock.util.ChainValidationResult; -import org.cloudburstmc.protocol.common.PacketSignal; -import org.geysermc.globallinkserver.bedrock.util.BedrockVersionUtils; -import org.geysermc.globallinkserver.link.LinkManager; -import org.geysermc.globallinkserver.player.PlayerManager; -import org.geysermc.globallinkserver.util.CommandUtils; -import org.geysermc.globallinkserver.util.Utils; - -public class PacketHandler implements BedrockPacketHandler { - private final BedrockServerSession session; - private final PlayerManager playerManager; - private final LinkManager linkManager; - - private BedrockPlayer player; - private long lastCommand; - - /** - * In Protocol V554 and above, RequestNetworkSettingsPacket is sent before LoginPacket. - */ - private boolean networkSettingsRequested = false; - - public PacketHandler(BedrockServerSession session, PlayerManager playerManager, LinkManager linkManager) { - this.session = session; - this.playerManager = playerManager; - this.linkManager = linkManager; - } - - @Override - public void onDisconnect(String reason) { - if (player != null) { - playerManager.removeBedrockPlayer(player); - } - } - - @Override - public PacketSignal handlePacket(BedrockPacket packet) { - BedrockPacketHandler.super.handlePacket(packet); - return PacketSignal.HANDLED; // Avoids warning spam about all the packets we ignore and don't handle - } - - private boolean setCorrectCodec(int protocolVersion) { - BedrockCodec packetCodec = BedrockVersionUtils.bedrockCodec(protocolVersion); - if (packetCodec == null) { - // Protocol version is not supported - PlayStatusPacket status = new PlayStatusPacket(); - if (protocolVersion > BedrockVersionUtils.latestProtocolVersion()) { - status.setStatus(PlayStatusPacket.Status.LOGIN_FAILED_SERVER_OLD); - } else { - status.setStatus(PlayStatusPacket.Status.LOGIN_FAILED_CLIENT_OLD); - } - - session.sendPacketImmediately(status); - session.disconnect(); - return false; - } - - session.setCodec(packetCodec); - return true; - } - - @Override - public PacketSignal handle(RequestNetworkSettingsPacket packet) { - if (!setCorrectCodec(packet.getProtocolVersion())) { - return PacketSignal.HANDLED; // Unsupported version, client has been disconnected - } - - // New since 1.19.30 - sent before login packet - PacketCompressionAlgorithm algorithm = PacketCompressionAlgorithm.ZLIB; - - NetworkSettingsPacket responsePacket = new NetworkSettingsPacket(); - responsePacket.setCompressionAlgorithm(algorithm); - responsePacket.setCompressionThreshold(512); - session.sendPacketImmediately(responsePacket); - - session.setCompression(algorithm); - networkSettingsRequested = true; - return PacketSignal.HANDLED; - } - - @Override - public PacketSignal handle(LoginPacket packet) { - if (!networkSettingsRequested) { - // This is expected for pre-1.19.30 - PlayStatusPacket statusPacket = new PlayStatusPacket(); - statusPacket.setStatus(PlayStatusPacket.Status.LOGIN_FAILED_CLIENT_OLD); - session.sendPacketImmediately(statusPacket); - - session.disconnect(); - return PacketSignal.HANDLED; - } - - try { - ChainValidationResult.IdentityData extraData = - Utils.validateAndEncryptConnection(session, packet.getChain(), packet.getExtra()); - - PlayStatusPacket status = new PlayStatusPacket(); - status.setStatus(PlayStatusPacket.Status.LOGIN_SUCCESS); - session.sendPacket(status); - - ResourcePacksInfoPacket info = new ResourcePacksInfoPacket(); - session.sendPacket(info); - - player = playerManager.addBedrockPlayer(session, extraData); - } catch (AssertionError | Exception error) { - session.disconnect("disconnect.loginFailed"); - } - return PacketSignal.HANDLED; - } - - @Override - public PacketSignal handle(ClientCacheStatusPacket packet) { - return PacketSignal.HANDLED; - } - - @Override - public PacketSignal handle(ResourcePackClientResponsePacket packet) { - switch (packet.getStatus()) { - case COMPLETED: - player.sendStartGame(); - break; - case HAVE_ALL_PACKS: - ResourcePackStackPacket stack = new ResourcePackStackPacket(); - stack.setExperimentsPreviouslyToggled(false); - stack.setForcedToAccept(false); - stack.setGameVersion("*"); - session.sendPacket(stack); - break; - default: - session.disconnect("disconnectionScreen.resourcePack"); - break; - } - return PacketSignal.HANDLED; - } - - @Override - public PacketSignal handle(SetLocalPlayerAsInitializedPacket packet) { - player.sendJoinMessages(); - return PacketSignal.HANDLED; - } - - @Override - public PacketSignal handle(CommandRequestPacket packet) { - String message = packet.getCommand(); - if (message.startsWith("/")) { - long now = System.currentTimeMillis(); - if (now - lastCommand < 4_000) { - player.sendMessage("&cYou're sending commands too fast"); - } else { - lastCommand = now; - CommandUtils.handleCommand(linkManager, playerManager, player, message); - } - } else { - player.sendMessage("&7The darkness doesn't know how to respond to your message"); - } - return PacketSignal.HANDLED; - } -} diff --git a/src/main/java/org/geysermc/globallinkserver/bedrock/util/BedrockVersionUtils.java b/src/main/java/org/geysermc/globallinkserver/bedrock/util/BedrockVersionUtils.java deleted file mode 100644 index 02c8e8f..0000000 --- a/src/main/java/org/geysermc/globallinkserver/bedrock/util/BedrockVersionUtils.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) 2021-2024 GeyserMC - * - * 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/GlobalLinkServer - */ -package org.geysermc.globallinkserver.bedrock.util; - -import java.util.ArrayList; -import java.util.List; -import org.cloudburstmc.protocol.bedrock.codec.BedrockCodec; -import org.cloudburstmc.protocol.bedrock.codec.v589.Bedrock_v589; -import org.cloudburstmc.protocol.bedrock.codec.v594.Bedrock_v594; -import org.cloudburstmc.protocol.bedrock.codec.v618.Bedrock_v618; -import org.cloudburstmc.protocol.bedrock.codec.v622.Bedrock_v622; -import org.cloudburstmc.protocol.bedrock.codec.v630.Bedrock_v630; -import org.cloudburstmc.protocol.bedrock.codec.v649.Bedrock_v649; -import org.cloudburstmc.protocol.bedrock.codec.v662.Bedrock_v662; -import org.cloudburstmc.protocol.bedrock.codec.v671.Bedrock_v671; -import org.cloudburstmc.protocol.bedrock.codec.v685.Bedrock_v685; -import org.cloudburstmc.protocol.bedrock.codec.v686.Bedrock_v686; -import org.cloudburstmc.protocol.bedrock.codec.v712.Bedrock_v712; -import org.cloudburstmc.protocol.bedrock.codec.v729.Bedrock_v729; -import org.cloudburstmc.protocol.bedrock.codec.v748.Bedrock_v748; - -/** - * Contains information about the supported Bedrock protocols in GlobalLinkServer. - */ -public class BedrockVersionUtils { - /** - * A list of all supported Bedrock versions that can join GlobalLinkServer - */ - public static final List SUPPORTED_BEDROCK_CODECS = new ArrayList<>() {{ - add(Bedrock_v589.CODEC); - add(Bedrock_v594.CODEC); - add(Bedrock_v618.CODEC); - add(Bedrock_v622.CODEC); - add(Bedrock_v630.CODEC); - add(Bedrock_v649.CODEC); - add(Bedrock_v662.CODEC); - add(Bedrock_v671.CODEC); - add(Bedrock_v685.CODEC); - add(Bedrock_v686.CODEC); - add(Bedrock_v712.CODEC); - add(Bedrock_v729.CODEC); - add(Bedrock_v748.CODEC); - }}; - - /** - * Default Bedrock codec that should act as a fallback. Should represent the latest available - * release of the game that GlobalLinkServer supports. - */ - public static final BedrockCodec LATEST_CODEC = SUPPORTED_BEDROCK_CODECS.get(SUPPORTED_BEDROCK_CODECS.size() - 1); - - /** - * Gets the {@link BedrockCodec} of the given protocol version. - * @param protocolVersion The protocol version to attempt to find - * @return The packet codec, or null if the client's protocol is unsupported - */ - public static BedrockCodec bedrockCodec(int protocolVersion) { - for (BedrockCodec packetCodec : SUPPORTED_BEDROCK_CODECS) { - if (packetCodec.getProtocolVersion() == protocolVersion) { - return packetCodec; - } - } - return null; - } - - public static int latestProtocolVersion() { - return LATEST_CODEC.getProtocolVersion(); - } -} diff --git a/src/main/java/org/geysermc/globallinkserver/bedrock/util/PaletteUtils.java b/src/main/java/org/geysermc/globallinkserver/bedrock/util/PaletteUtils.java deleted file mode 100644 index c7b7f75..0000000 --- a/src/main/java/org/geysermc/globallinkserver/bedrock/util/PaletteUtils.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2021-2021 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/GlobalLinkServer - */ - -package org.geysermc.globallinkserver.bedrock.util; - -import org.cloudburstmc.nbt.*; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; - -/** - * This class is mostly copied from Geyser - */ -public class PaletteUtils { - public static final NbtMap BIOMES_PALETTE; - public static final byte[] EMPTY_LEVEL_CHUNK_DATA; - - private static final NbtMap EMPTY_TAG = NbtMap.EMPTY; - - static { - /* Load biomes */ - // Build a fake plains biome entry - NbtMapBuilder plainsBuilder = NbtMap.builder(); - plainsBuilder.putFloat("blue_spores", 0f); - plainsBuilder.putFloat("white_ash", 0f); - plainsBuilder.putFloat("ash", 0f); - plainsBuilder.putFloat("temperature", 0f); - plainsBuilder.putFloat("red_spores", 0f); - plainsBuilder.putFloat("downfall", 0f); - - plainsBuilder.put("minecraft:overworld_generation_rules", NbtMap.EMPTY); - plainsBuilder.put("minecraft:climate", NbtMap.EMPTY); - plainsBuilder.put("tags", NbtList.EMPTY); - - // Add the fake plains to the map - NbtMapBuilder biomesBuilder = NbtMap.builder(); - biomesBuilder.put("plains", plainsBuilder.build()); - - // Build the biomes palette - BIOMES_PALETTE = biomesBuilder.build(); - - /* Create empty chunk data */ - try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - outputStream.write(new byte[258]); // Biomes + Border Size + Extra Data Size - - try (NBTOutputStream nbtOutputStream = NbtUtils.createNetworkWriter(outputStream)) { - nbtOutputStream.writeTag(EMPTY_TAG); - } - - EMPTY_LEVEL_CHUNK_DATA = outputStream.toByteArray(); - } catch (IOException e) { - throw new AssertionError("Unable to generate empty level chunk data"); - } - } -} diff --git a/src/main/java/org/geysermc/globallinkserver/config/Config.java b/src/main/java/org/geysermc/globallinkserver/config/Config.java index 465edc5..a47bec6 100644 --- a/src/main/java/org/geysermc/globallinkserver/config/Config.java +++ b/src/main/java/org/geysermc/globallinkserver/config/Config.java @@ -1,37 +1,21 @@ /* - * Copyright (c) 2021-2023 GeyserMC - * - * 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 + * Copyright (c) 2021-2025 GeyserMC + * Licensed under the MIT license * @link https://github.com/GeyserMC/GlobalLinkServer */ package org.geysermc.globallinkserver.config; -import com.google.gson.annotations.SerializedName; +import org.bukkit.Location; public record Config( - @SerializedName("bind-ip") String bindIp, - @SerializedName("bind-port-java") int javaPort, - @SerializedName("bind-port-bedrock") int bedrockPort, - // database related - String hostname, - String username, - String password, - String database) {} + Database database, + Location spawn +) { + public record Database( + String hostname, + String username, + String password, + String database, + int maxPoolSize + ) {} +} diff --git a/src/main/java/org/geysermc/globallinkserver/config/ConfigReader.java b/src/main/java/org/geysermc/globallinkserver/config/ConfigReader.java index a7c186d..9472dc7 100644 --- a/src/main/java/org/geysermc/globallinkserver/config/ConfigReader.java +++ b/src/main/java/org/geysermc/globallinkserver/config/ConfigReader.java @@ -1,46 +1,37 @@ /* - * Copyright (c) 2021-2024 GeyserMC + * Copyright (c) 2021-2025 GeyserMC * Licensed under the MIT license * @link https://github.com/GeyserMC/GlobalLinkServer */ package org.geysermc.globallinkserver.config; -import static org.geysermc.globallinkserver.GlobalLinkServer.LOGGER; - -import com.google.gson.Gson; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; +import java.util.Objects; +import org.bukkit.Location; +import org.bukkit.plugin.java.JavaPlugin; +import org.jspecify.annotations.NullMarked; +@NullMarked public class ConfigReader { - private static final Gson GSON = new Gson(); - private static final Path CONFIG_PATH = Paths.get("config.json"); + public static Config readConfig(JavaPlugin plugin) { + plugin.saveDefaultConfig(); + var config = plugin.getConfig(); - public static Config readConfig() { - LOGGER.info("Reading config from " + CONFIG_PATH.toAbsolutePath()); - String data = configContent(); - if (data == null) { - createConfig(); - } - data = configContent(); + var databaseSection = Objects.requireNonNull(config.getConfigurationSection("database")); - return GSON.fromJson(data, Config.class); - } + var database = new Config.Database( + databaseSection.getString("hostname"), + databaseSection.getString("username"), + databaseSection.getString("password"), + databaseSection.getString("database"), + databaseSection.getInt("max-pool-size")); - private static String configContent() { - try { - return Files.readString(CONFIG_PATH); - } catch (IOException exception) { - return null; - } - } + var locationSection = Objects.requireNonNull(config.getConfigurationSection("spawn")); + var spawnLocation = Location.deserialize(locationSection.getValues(false)); - private static void createConfig() { - try { - Files.copy(ConfigReader.class.getResourceAsStream("/config.json"), CONFIG_PATH); - } catch (IOException exception) { - throw new RuntimeException("Failed to copy config", exception); + if (!spawnLocation.isWorldLoaded()) { + throw new IllegalArgumentException("World %s is not loaded".formatted(locationSection.getString("world"))); } + + return new Config(database, spawnLocation); } } diff --git a/src/main/java/org/geysermc/globallinkserver/handler/CommandHandler.java b/src/main/java/org/geysermc/globallinkserver/handler/CommandHandler.java new file mode 100644 index 0000000..3bf9317 --- /dev/null +++ b/src/main/java/org/geysermc/globallinkserver/handler/CommandHandler.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2021-2025 GeyserMC + * Licensed under the MIT license + * @link https://github.com/GeyserMC/GlobalLinkServer + */ +package org.geysermc.globallinkserver.handler; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.context.CommandContext; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.geysermc.globallinkserver.Components; +import org.geysermc.globallinkserver.link.FullLink; +import org.geysermc.globallinkserver.link.Link; +import org.geysermc.globallinkserver.link.LinkManager; +import org.geysermc.globallinkserver.link.LinkRequest; +import org.geysermc.globallinkserver.manager.PlayerManager; +import org.geysermc.globallinkserver.service.LinkInfoService; +import org.geysermc.globallinkserver.service.LinkLookupService; +import org.jspecify.annotations.NullMarked; + +import static org.geysermc.globallinkserver.util.Utils.contextExecutor; + +@NullMarked +@SuppressWarnings("UnstableApiUsage") +public final class CommandHandler { + private final LinkLookupService linkLookupService; + private final LinkInfoService linkInfoService; + private final LinkManager linkManager; + private final PlayerManager playerManager; + private final Plugin plugin; + + public CommandHandler(LinkLookupService linkLookupService, LinkInfoService linkInfoService, LinkManager linkManager, PlayerManager playerManager, Plugin plugin) { + this.linkLookupService = linkLookupService; + this.linkInfoService = linkInfoService; + this.linkManager = linkManager; + this.playerManager = playerManager; + this.plugin = plugin; + } + + public int startLink(CommandContext ctx) { + Player player = contextExecutor(ctx); + + if (linkLookupService.isLinkedCached(player)) { + player.sendMessage(Components.LINK_ALREADY_LINKED); + linkInfoService.sendCurrentLinkInfo(player); + return Command.SINGLE_SUCCESS; + } + + if (linkManager.removeActiveLinkRequest(player)) { + player.sendMessage(Components.LINK_REQUEST_REPLACED); + } + + String code = String.format("%04d", linkManager.createTempLink(player)); + String otherPlatform = playerManager.isBedrockPlayer(player) ? "Java" : "Bedrock"; + player.sendMessage(Components.linkStarted(otherPlatform, code)); + return Command.SINGLE_SUCCESS; + } + + public int linkWithCode(CommandContext ctx) { + int code = IntegerArgumentType.getInteger(ctx, "code"); + Player player = contextExecutor(ctx); + + if (code < 0 || code > 9999) { + player.sendMessage(Components.LINK_CODE_INVALID_RANGE); + return Command.SINGLE_SUCCESS; + } + + LinkRequest linkRequest = linkManager.linkRequestByCode(code); + if (linkRequest == null) { + player.sendMessage(Components.LINK_REQUEST_NOT_FOUND); + return Command.SINGLE_SUCCESS; + } + + boolean isRequesterBedrock = playerManager.isBedrockId(linkRequest.requesterUuid()); + boolean isCompleteLink = isRequesterBedrock != playerManager.isBedrockId(player.getUniqueId()); + + if (!isCompleteLink) { + player.sendMessage(Components.LINK_REQUEST_SAME_PLATFORM); + return Command.SINGLE_SUCCESS; + } + + var completedLink = Link.fromRequest(linkRequest, player.getUniqueId(), player.getName(), isRequesterBedrock); + + linkManager.finaliseLink(completedLink).whenComplete((result, error) -> { + if (error != null || !result) { + if (error != null) { + error.printStackTrace(); + } + System.out.println(result); + player.sendMessage(Components.LINK_CREATE_ERROR); + return; + } + + var requestPlayer = linkRequest.requester(); + + Bukkit.getScheduler().callSyncMethod(plugin, () -> { + player.kick(Components.LINK_CREATE_SUCCESS); + + if (requestPlayer != null) { + requestPlayer.kick(Components.LINK_CREATE_SUCCESS); + } + + return null; + }); + }); + return Command.SINGLE_SUCCESS; + } + + public int unlink(CommandContext ctx) { + Player player = contextExecutor(ctx); + + FullLink currentLink = linkLookupService.cachedLookup(player); + if (currentLink == null) { + player.sendMessage(Components.UNLINK_NOT_LINKED); + return Command.SINGLE_SUCCESS; + } + + linkManager.unlinkAccount(player).whenComplete((result, error) -> { + if (error != null) { + error.printStackTrace(); + player.sendMessage(Components.UNLINK_ERROR); + return; + } + + Bukkit.getScheduler().callSyncMethod(plugin, () -> { + if (result) { + player.kick(Components.UNLINK_SUCCESS); + + // Lookup whether the player's link is online, kick em too + Player otherLink = Bukkit.getServer().getPlayer(currentLink.getOpposed(player)); + if (otherLink != null) { + otherLink.kick(Components.UNLINK_SUCCESS); + } + } else { + // Technically impossible + player.kick(Components.UNLINK_NOT_LINKED); + } + + return null; + }); + }); + return Command.SINGLE_SUCCESS; + } +} diff --git a/src/main/java/org/geysermc/globallinkserver/handler/JoinHandler.java b/src/main/java/org/geysermc/globallinkserver/handler/JoinHandler.java new file mode 100644 index 0000000..553c241 --- /dev/null +++ b/src/main/java/org/geysermc/globallinkserver/handler/JoinHandler.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2025 GeyserMC + * Licensed under the MIT license + * @link https://github.com/GeyserMC/GlobalLinkServer + */ +package org.geysermc.globallinkserver.handler; + +import java.util.UUID; +import org.bukkit.Bukkit; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.plugin.Plugin; +import org.geysermc.globallinkserver.Components; +import org.geysermc.globallinkserver.service.LinkLookupService; +import org.geysermc.globallinkserver.util.MultiConditionSet; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public final class JoinHandler implements Listener { + private final LinkLookupService linkLookupService; + private final MultiConditionSet playerIdleTracker; + private final Plugin plugin; + + public JoinHandler(LinkLookupService linkLookupService, MultiConditionSet playerIdleTracker, Plugin plugin) { + this.linkLookupService = linkLookupService; + this.playerIdleTracker = playerIdleTracker; + this.plugin = plugin; + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + var player = event.getPlayer(); + event.joinMessage(null); + + player.setPersistent(false); + player.setAllowFlight(true); + + // Hide all players from each other + Bukkit.getOnlinePlayers().forEach(otherPlayer -> { + player.hidePlayer(plugin, otherPlayer); + otherPlayer.hidePlayer(plugin, player); + }); + + playerIdleTracker.add(player.getUniqueId()); + + linkLookupService.lookup(player).whenComplete(($, throwable) -> { + if (throwable != null) { + player.sendMessage(Components.INFO_UNAVAILABLE); + throwable.printStackTrace(); + } + }); + } +} diff --git a/src/main/java/org/geysermc/globallinkserver/handler/MoveInactivityHandler.java b/src/main/java/org/geysermc/globallinkserver/handler/MoveInactivityHandler.java new file mode 100644 index 0000000..490002b --- /dev/null +++ b/src/main/java/org/geysermc/globallinkserver/handler/MoveInactivityHandler.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025 GeyserMC + * Licensed under the MIT license + * @link https://github.com/GeyserMC/GlobalLinkServer + */ +package org.geysermc.globallinkserver.handler; + +import it.unimi.dsi.fastutil.objects.Object2LongMap; +import it.unimi.dsi.fastutil.objects.Object2LongMaps; +import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap; +import java.util.UUID; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerMoveEvent; +import org.geysermc.globallinkserver.util.MultiConditionSet; + +public final class MoveInactivityHandler implements Listener { + private static final long TIME_TILL_IDLE_MILLIS = 15 * 60 * 1000; // 15 minutes + + private final Object2LongMap lastMoveAction = Object2LongMaps.synchronize(new Object2LongOpenHashMap<>()); + + public MoveInactivityHandler(MultiConditionSet playerIdleTracker) { + playerIdleTracker + .addRemovalCondition(key -> { + long lastMovement = lastMoveAction.getLong(key); + // if not present, the value will be 0. It should never happen, so if that happens we remove them. + return System.currentTimeMillis() - lastMovement >= TIME_TILL_IDLE_MILLIS; + }) + .addRemovalListener(lastMoveAction::removeLong); + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + // just to make sure that there aren't any weird edge cases of a player not firing a PlayerMoveEvent immediately + lastMoveAction.put(event.getPlayer().getUniqueId(), System.currentTimeMillis()); + } + + @EventHandler + public void onPlayerMove(PlayerMoveEvent event) { + int diffX = event.getFrom().getBlockX() - event.getTo().getBlockX(); + int diffY = event.getFrom().getBlockZ() - event.getTo().getBlockZ(); + if (Math.abs(diffX) > 0 || Math.abs(diffY) > 0) { + lastMoveAction.put(event.getPlayer().getUniqueId(), System.currentTimeMillis()); + } + } +} diff --git a/src/main/java/org/geysermc/globallinkserver/handler/TeleportToSpawnHandler.java b/src/main/java/org/geysermc/globallinkserver/handler/TeleportToSpawnHandler.java new file mode 100644 index 0000000..d522abd --- /dev/null +++ b/src/main/java/org/geysermc/globallinkserver/handler/TeleportToSpawnHandler.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025 GeyserMC + * Licensed under the MIT license + * @link https://github.com/GeyserMC/GlobalLinkServer + */ +package org.geysermc.globallinkserver.handler; + +import org.bukkit.Location; +import org.bukkit.attribute.Attribute; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.entity.EntityDamageEvent; +import org.bukkit.event.entity.PlayerDeathEvent; +import org.bukkit.event.player.PlayerJoinEvent; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public final class TeleportToSpawnHandler implements Listener { + private final Location spawn; + + public TeleportToSpawnHandler(Location spawn) { + this.spawn = spawn; + } + + @EventHandler + public void onPlayerJoin(PlayerJoinEvent event) { + var player = event.getPlayer(); + player.setRespawnLocation(spawn); + teleport(player); + } + + @EventHandler + public void onEntityDamage(EntityDamageEvent event) { + if (event.getEntity() instanceof Player player) { + event.setCancelled(true); + + if (event.getCause() == EntityDamageEvent.DamageCause.VOID) { + teleport(player); + } + } + } + + @EventHandler + public void onPlayerDeath(PlayerDeathEvent event) { + event.setCancelled(true); + teleport(event.getEntity()); + } + + private void teleport(Player player) { + player.teleport(spawn); + player.setFallDistance(0); + //noinspection DataFlowIssue we know it can't be null + player.setHealth(player.getAttribute(Attribute.MAX_HEALTH).getValue()); + } +} diff --git a/src/main/java/org/geysermc/globallinkserver/java/JavaPlayer.java b/src/main/java/org/geysermc/globallinkserver/java/JavaPlayer.java deleted file mode 100644 index 81d9dea..0000000 --- a/src/main/java/org/geysermc/globallinkserver/java/JavaPlayer.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (c) 2021-2024 GeyserMC - * - * 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/GlobalLinkServer - */ -package org.geysermc.globallinkserver.java; - -import java.util.UUID; -import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; -import org.geysermc.globallinkserver.player.Player; -import org.geysermc.mcprotocollib.auth.GameProfile; -import org.geysermc.mcprotocollib.network.Session; -import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundSystemChatPacket; -import org.geysermc.mcprotocollib.protocol.packet.login.clientbound.ClientboundLoginDisconnectPacket; - -public class JavaPlayer implements Player { - private static final LegacyComponentSerializer LEGACY_SERIALIZER = LegacyComponentSerializer.legacyAmpersand(); - - private final Session session; - private final GameProfile profile; - - private int linkId; - - public JavaPlayer(Session session, GameProfile profile) { - this.session = session; - this.profile = profile; - } - - @Override - public void sendMessage(String message) { - session.send(new ClientboundSystemChatPacket(LEGACY_SERIALIZER.deserialize(message), false)); - } - - @Override - public void disconnect(String reason) { - session.send(new ClientboundLoginDisconnectPacket(LEGACY_SERIALIZER.deserialize(reason))); - session.disconnect(LEGACY_SERIALIZER.deserialize(reason)); - } - - @Override - public UUID uniqueId() { - return profile.getId(); - } - - @Override - public String username() { - return profile.getName(); - } - - @Override - public int linkId() { - return linkId; - } - - @Override - public void linkId(int linkId) { - this.linkId = linkId; - } -} diff --git a/src/main/java/org/geysermc/globallinkserver/java/JavaServer.java b/src/main/java/org/geysermc/globallinkserver/java/JavaServer.java deleted file mode 100644 index ab3ec1d..0000000 --- a/src/main/java/org/geysermc/globallinkserver/java/JavaServer.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Copyright (c) 2021-2024 GeyserMC - * Licensed under the MIT license - * @link https://github.com/GeyserMC/GlobalLinkServer - */ -package org.geysermc.globallinkserver.java; - -import static org.geysermc.mcprotocollib.protocol.codec.MinecraftCodec.CODEC; - -import java.util.BitSet; -import java.util.Collections; -import java.util.List; -import java.util.OptionalInt; -import net.kyori.adventure.key.Key; -import net.kyori.adventure.text.Component; -import org.cloudburstmc.math.vector.Vector3i; -import org.cloudburstmc.nbt.NbtMap; -import org.geysermc.globallinkserver.config.Config; -import org.geysermc.globallinkserver.link.LinkManager; -import org.geysermc.globallinkserver.player.PlayerManager; -import org.geysermc.mcprotocollib.auth.SessionService; -import org.geysermc.mcprotocollib.network.Server; -import org.geysermc.mcprotocollib.network.event.server.ServerAdapter; -import org.geysermc.mcprotocollib.network.event.server.ServerClosedEvent; -import org.geysermc.mcprotocollib.network.event.server.SessionAddedEvent; -import org.geysermc.mcprotocollib.network.event.session.ConnectedEvent; -import org.geysermc.mcprotocollib.network.tcp.TcpServer; -import org.geysermc.mcprotocollib.protocol.MinecraftConstants; -import org.geysermc.mcprotocollib.protocol.MinecraftProtocol; -import org.geysermc.mcprotocollib.protocol.data.game.command.CommandNode; -import org.geysermc.mcprotocollib.protocol.data.game.command.CommandParser; -import org.geysermc.mcprotocollib.protocol.data.game.command.CommandType; -import org.geysermc.mcprotocollib.protocol.data.game.command.properties.IntegerProperties; -import org.geysermc.mcprotocollib.protocol.data.game.entity.player.GameMode; -import org.geysermc.mcprotocollib.protocol.data.game.entity.player.PlayerSpawnInfo; -import org.geysermc.mcprotocollib.protocol.data.game.level.LightUpdateData; -import org.geysermc.mcprotocollib.protocol.data.game.level.block.BlockEntityInfo; -import org.geysermc.mcprotocollib.protocol.data.game.level.notify.GameEvent; -import org.geysermc.mcprotocollib.protocol.data.status.PlayerInfo; -import org.geysermc.mcprotocollib.protocol.data.status.ServerStatusInfo; -import org.geysermc.mcprotocollib.protocol.data.status.VersionInfo; -import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundCommandsPacket; -import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.ClientboundLoginPacket; -import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.player.ClientboundPlayerAbilitiesPacket; -import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.player.ClientboundPlayerPositionPacket; -import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundGameEventPacket; -import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundLevelChunkWithLightPacket; -import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.level.ClientboundSetDefaultSpawnPositionPacket; - -public class JavaServer implements org.geysermc.globallinkserver.Server { - private final PlayerManager playerManager; - private final LinkManager linkManager; - - private final ClientboundLevelChunkWithLightPacket cachedChunk = cachedChunk(); - - private final ServerStatusInfo pong = new ServerStatusInfo( - Component.text("Global Link Server"), - new PlayerInfo(1, 0, Collections.emptyList()), - new VersionInfo(CODEC.getMinecraftVersion(), CODEC.getProtocolVersion()), - null, - false); - - private Server server; - - public JavaServer(PlayerManager playerManager, LinkManager linkManager) { - this.playerManager = playerManager; - this.linkManager = linkManager; - } - - @Override - public boolean startServer(Config config) { - if (server != null) { - return false; - } - - server = new TcpServer(config.bindIp(), config.javaPort(), MinecraftProtocol::new); - - server.setGlobalFlag(MinecraftConstants.SESSION_SERVICE_KEY, new SessionService()); - server.setGlobalFlag(MinecraftConstants.VERIFY_USERS_KEY, true); - server.setGlobalFlag(MinecraftConstants.SERVER_INFO_BUILDER_KEY, session -> pong); - server.setGlobalFlag(MinecraftConstants.SERVER_LOGIN_HANDLER_KEY, session -> { - session.send(new ClientboundCommandsPacket( - new CommandNode[] { - new CommandNode( - CommandType.ROOT, true, new int[] {1, 3}, OptionalInt.empty(), null, null, null, null), - new CommandNode( - CommandType.LITERAL, - true, - new int[] {2}, - OptionalInt.empty(), - "linkaccount", - null, - null, - null), - new CommandNode( - CommandType.ARGUMENT, - true, - new int[0], - OptionalInt.empty(), - "code", - CommandParser.INTEGER, - new IntegerProperties(0, 9999), - null), - new CommandNode( - CommandType.LITERAL, - true, - new int[0], - OptionalInt.empty(), - "unlinkaccount", - null, - null, - null) - }, - 0)); - - session.send(new ClientboundLoginPacket( - 0, - false, - new Key[] {Key.key("minecraft:the_end")}, - 1, - 0, - 0, - false, - false, - false, - new PlayerSpawnInfo( - 2, - Key.key("minecraft:the_end"), - 100, - GameMode.SPECTATOR, - GameMode.SPECTATOR, - false, - false, - null, - 100), - true)); - - session.send(new ClientboundPlayerAbilitiesPacket(false, false, true, false, 0f, 0f)); - - // this packet is also required to let our player spawn, but the location itself doesn't matter - session.send(new ClientboundSetDefaultSpawnPositionPacket(Vector3i.ZERO, 0)); - - // we have to listen to the teleport confirm on the PacketHandler to prevent respawn request packet spam, - // so send it after calling ConnectedEvent which adds the PacketHandler as listener - session.send(new ClientboundPlayerPositionPacket(0, 0, 0, 0, 0, 0)); - - // these packets are required since 1.20.3 - session.send(new ClientboundGameEventPacket(GameEvent.LEVEL_CHUNKS_LOAD_START, null)); - session.send(cachedChunk); - - // Manually call the connect event - session.callEvent(new ConnectedEvent(session)); - }); - server.setGlobalFlag(MinecraftConstants.SERVER_COMPRESSION_THRESHOLD, 256); // default - - server.addListener(new ServerAdapter() { - @Override - public void serverClosed(ServerClosedEvent event) { - super.serverClosed(event); - } - - @Override - public void sessionAdded(SessionAddedEvent event) { - event.getSession().addListener(new PacketHandler(event.getSession(), linkManager, playerManager)); - } - }); - - server.bind(); - return true; - } - - @Override - public void shutdown() { - server.close(); - server = null; - } - - private ClientboundLevelChunkWithLightPacket cachedChunk() { - // 8 bytes for every section: - // short - block count - // for both blocks and biomes: - // byte - bits per entry - // varint(1) - block ID (0, air) - // varint(1) - data length - // times 16 for the 16 chunk sections that the end biome has - byte[] chunkData = new byte[8 * 16]; - - // just setting everything to empty seems to do the trick - var lightData = - new LightUpdateData(new BitSet(), new BitSet(), new BitSet(), new BitSet(), List.of(), List.of()); - // same applies for the heightmaps - return new ClientboundLevelChunkWithLightPacket( - 0, 0, chunkData, NbtMap.EMPTY, new BlockEntityInfo[0], lightData); - } -} diff --git a/src/main/java/org/geysermc/globallinkserver/java/PacketHandler.java b/src/main/java/org/geysermc/globallinkserver/java/PacketHandler.java deleted file mode 100644 index 0cce0de..0000000 --- a/src/main/java/org/geysermc/globallinkserver/java/PacketHandler.java +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright (c) 2021-2024 GeyserMC - * - * 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/GlobalLinkServer - */ -package org.geysermc.globallinkserver.java; - -import org.geysermc.globallinkserver.link.LinkManager; -import org.geysermc.globallinkserver.player.PlayerManager; -import org.geysermc.globallinkserver.util.CommandUtils; -import org.geysermc.mcprotocollib.auth.GameProfile; -import org.geysermc.mcprotocollib.network.Session; -import org.geysermc.mcprotocollib.network.event.session.ConnectedEvent; -import org.geysermc.mcprotocollib.network.event.session.DisconnectedEvent; -import org.geysermc.mcprotocollib.network.event.session.SessionAdapter; -import org.geysermc.mcprotocollib.network.packet.Packet; -import org.geysermc.mcprotocollib.protocol.MinecraftConstants; -import org.geysermc.mcprotocollib.protocol.packet.ingame.clientbound.entity.player.ClientboundSetHealthPacket; -import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.ServerboundChatCommandPacket; -import org.geysermc.mcprotocollib.protocol.packet.ingame.serverbound.level.ServerboundAcceptTeleportationPacket; - -public class PacketHandler extends SessionAdapter { - private final Session session; - private final LinkManager linkManager; - private final PlayerManager playerManager; - - private JavaPlayer player; - private long lastCommand; - - public PacketHandler(Session session, LinkManager linkManager, PlayerManager playerManager) { - this.session = session; - this.linkManager = linkManager; - this.playerManager = playerManager; - } - - @Override - public void packetReceived(Session session, Packet packet) { - try { - if (packet instanceof ServerboundChatCommandPacket) { - long now = System.currentTimeMillis(); - if (now - lastCommand < 4_000) { - player.sendMessage("&cYou're sending commands too fast"); - return; - } - lastCommand = now; - String message = "/" + ((ServerboundChatCommandPacket) packet).getCommand(); - CommandUtils.handleCommand(linkManager, playerManager, player, message); - } - - if (packet instanceof ServerboundAcceptTeleportationPacket) { - // if we keep the health on 0, the client will spam us respawn request packets :/ - session.send(new ClientboundSetHealthPacket(1, 0, 0)); - } - } catch (Exception e) { - e.printStackTrace(); - } - } - - @Override - public void connected(ConnectedEvent event) { - try { - GameProfile profile = event.getSession().getFlag(MinecraftConstants.PROFILE_KEY); - - player = playerManager.addJavaPlayer(session, profile); - player.sendJoinMessages(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - @Override - public void disconnected(DisconnectedEvent event) { - if (player != null) { - playerManager.removeJavaPlayer(player); - } - } -} diff --git a/src/main/java/org/geysermc/globallinkserver/link/FullLink.java b/src/main/java/org/geysermc/globallinkserver/link/FullLink.java new file mode 100644 index 0000000..2b82312 --- /dev/null +++ b/src/main/java/org/geysermc/globallinkserver/link/FullLink.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2021-2025 GeyserMC + * Licensed under the MIT license + * @link https://github.com/GeyserMC/GlobalLinkServer + */ +package org.geysermc.globallinkserver.link; + +import org.bukkit.entity.Player; + +import java.util.UUID; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public record FullLink(UUID bedrockId, String bedrockUsername, UUID javaId, String javaUsername) { + public UUID getOpposed(Player player) { + return player.getUniqueId().equals(bedrockId) ? javaId : bedrockId; + } +} diff --git a/src/main/java/org/geysermc/globallinkserver/link/Link.java b/src/main/java/org/geysermc/globallinkserver/link/Link.java index fd29588..d1ca5bd 100644 --- a/src/main/java/org/geysermc/globallinkserver/link/Link.java +++ b/src/main/java/org/geysermc/globallinkserver/link/Link.java @@ -1,60 +1,23 @@ /* - * Copyright (c) 2021-2023 GeyserMC - * - * 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 + * Copyright (c) 2025 GeyserMC + * Licensed under the MIT license * @link https://github.com/GeyserMC/GlobalLinkServer */ package org.geysermc.globallinkserver.link; import java.util.UUID; +import org.jspecify.annotations.NullMarked; -public class Link { - private UUID bedrockId; - private UUID javaId; - private String javaUsername; - - public UUID bedrockId() { - return bedrockId; - } - - public Link bedrockId(UUID bedrockId) { - this.bedrockId = bedrockId; - return this; - } - - public UUID javaId() { - return javaId; - } - - public Link javaId(UUID javaId) { - this.javaId = javaId; - return this; - } - - public String javaUsername() { - return javaUsername; +@NullMarked +public record Link(UUID javaId, String javaUsername, long bedrockId) { + public Link(UUID javaId, String javaUsername, UUID bedrockId) { + this(javaId, javaUsername, bedrockId.getLeastSignificantBits()); } - public Link javaUsername(String javaUsername) { - this.javaUsername = javaUsername; - return this; + public static Link fromRequest(LinkRequest left, UUID rightId, String rightName, boolean isLeftBedrock) { + if (isLeftBedrock) { + return new Link(rightId, rightName, left.requesterUuid()); + } + return new Link(left.requesterUuid(), left.requesterUsername(), rightId); } } diff --git a/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java b/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java index d58fb66..42fb70a 100644 --- a/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java +++ b/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java @@ -1,84 +1,56 @@ /* - * Copyright (c) 2021-2023 GeyserMC - * - * 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 + * Copyright (c) 2021-2025 GeyserMC + * Licensed under the MIT license * @link https://github.com/GeyserMC/GlobalLinkServer */ package org.geysermc.globallinkserver.link; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; -import it.unimi.dsi.fastutil.ints.IntArraySet; -import it.unimi.dsi.fastutil.ints.IntSet; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Iterator; -import java.util.List; import java.util.Random; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import org.geysermc.globallinkserver.config.Config; -import org.geysermc.globallinkserver.java.JavaPlayer; -import org.geysermc.globallinkserver.player.Player; -import org.geysermc.globallinkserver.player.PlayerManager; -import org.mariadb.jdbc.MariaDbPoolDataSource; - -public class LinkManager { - private static final int TEMP_LINK_DURATION = 60_000 * 15; // 15 min - private final Int2ObjectMap tempLinks = new Int2ObjectOpenHashMap<>(); - - private final ExecutorService executorService = Executors.newFixedThreadPool(3); - private final MariaDbPoolDataSource dataSource; +import org.bukkit.entity.Player; +import org.geysermc.globallinkserver.Components; +import org.geysermc.globallinkserver.manager.DatabaseManager; +import org.geysermc.globallinkserver.manager.PlayerManager; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public final class LinkManager { + private static final int PENDING_LINK_TTL_MILLIS = 15 * 60 * 1000; // 15 min + + private final PlayerManager playerManager; + private final DatabaseManager database; + + private final Int2ObjectMap linkRequests = new Int2ObjectOpenHashMap<>(); + private final Object2IntMap linkRequestForPlayer = new Object2IntOpenHashMap<>() { + { + defaultReturnValue(-1); + } + }; private final Random random = new Random(); - public LinkManager(Config config) { - try { - Class.forName("org.mariadb.jdbc.Driver"); - dataSource = new MariaDbPoolDataSource("jdbc:mariadb://" + config.hostname() + "/" + config.database() - + "?user=" + config.username() + "&password=" + config.password() + "&minPoolSize=1&maxPoolSize=3"); - } catch (ClassNotFoundException exception) { - throw new RuntimeException("Cannot find required class to load the MariaDB database"); - } + public LinkManager(PlayerManager playerManager, DatabaseManager database) { + this.playerManager = playerManager; + this.database = database; } public int createTempLink(Player player) { - TempLink link = new TempLink(); - if (player instanceof JavaPlayer) { - link.javaId(player.uniqueId()); - link.javaUsername(player.username()); - } else { - link.bedrockId(player.uniqueId()); - } - link.expiryTime(System.currentTimeMillis() + TEMP_LINK_DURATION); - link.code(createCode()); + var linkRequest = new LinkRequest(createCode(), PENDING_LINK_TTL_MILLIS, player); - tempLinks.put(link.code(), link); - - player.linkId(link.code()); - - return link.code(); + linkRequests.put(linkRequest.code(), linkRequest); + linkRequestForPlayer.put(player.getUniqueId(), linkRequest.code()); + return linkRequest.code(); } private int createCode() { @@ -86,7 +58,7 @@ private int createCode() { while (code == -1) { code = random.nextInt(9999 + 1); // the bound is exclusive - TempLink link = tempLinks.get(code); + LinkRequest link = linkRequests.get(code); if (isLinkValid(link)) { code = -1; } @@ -94,54 +66,67 @@ private int createCode() { return code; } - public TempLink tempLinkById(int linkId) { - TempLink link = tempLinks.remove(linkId); + public @Nullable LinkRequest linkRequestByCode(int code) { + LinkRequest link = linkRequests.remove(code); return isLinkValid(link) ? link : null; } - private boolean isLinkValid(TempLink link) { + private boolean isLinkValid(@Nullable LinkRequest link) { long currentMillis = System.currentTimeMillis(); - return link != null && currentMillis - link.expiryTime() < TEMP_LINK_DURATION; + return link != null && currentMillis < link.expiryTime(); + } + + public boolean removeActiveLinkRequest(Player player) { + int code = linkRequestForPlayer.removeInt(player.getUniqueId()); + if (code != -1) { + linkRequests.remove(code); + } + return code != -1; } - public void removeTempLink(int linkId) { - tempLinks.remove(linkId); + public boolean hasActiveLinkRequest(UUID uuid) { + int code = linkRequestForPlayer.getInt(uuid); + if (code == -1) { + return false; + } + var request = linkRequests.get(code); + //noinspection ConstantValue ?? + return request != null && isLinkValid(request); } - public CompletableFuture finaliseLink(TempLink tempLink) { + public CompletableFuture finaliseLink(Link linkRequest) { return CompletableFuture.supplyAsync( () -> { - try (Connection connection = dataSource.getConnection()) { + try (Connection connection = database.connection()) { try (PreparedStatement query = connection.prepareStatement( "INSERT INTO `links` (`java_id`, `bedrock_id`, `java_name`) VALUES (?, ?, ?) " + "ON DUPLICATE KEY UPDATE " + "`java_id` = VALUES(`java_id`)," + "`bedrock_id` = VALUES(`bedrock_id`)," + "`java_name` = VALUES(`java_name`);")) { - query.setString(1, tempLink.javaId().toString()); - query.setLong(2, tempLink.bedrockId().getLeastSignificantBits()); - query.setString(3, tempLink.javaUsername()); + query.setString(1, linkRequest.javaId().toString()); + query.setLong(2, linkRequest.bedrockId()); + query.setString(3, linkRequest.javaUsername()); return query.executeUpdate() != 0; } } catch (SQLException exception) { throw new CompletionException("Error while linking player", exception); } }, - executorService); + database.executor()); } public CompletableFuture unlinkAccount(Player player) { return CompletableFuture.supplyAsync( () -> { - try (Connection connection = dataSource.getConnection()) { - + try (Connection connection = database.connection()) { PreparedStatement query; - if (player instanceof JavaPlayer) { - query = connection.prepareStatement("DELETE FROM `links` WHERE `java_id` = ?;"); - query.setString(1, player.uniqueId().toString()); - } else { + if (playerManager.isBedrockPlayer(player)) { query = connection.prepareStatement("DELETE FROM `links` WHERE `bedrock_id` = ?;"); - query.setLong(1, player.uniqueId().getLeastSignificantBits()); + query.setLong(1, player.getUniqueId().getLeastSignificantBits()); + } else { + query = connection.prepareStatement("DELETE FROM `links` WHERE `java_id` = ?;"); + query.setString(1, player.getUniqueId().toString()); } boolean affected = query.executeUpdate() != 0; query.close(); @@ -150,30 +135,25 @@ public CompletableFuture unlinkAccount(Player player) { throw new CompletionException("Error while unlinking player", exception); } }, - executorService); + database.executor()); } - public void cleanupTempLinks(PlayerManager playerManager) { - IntSet removedLinks = new IntArraySet(); - - Iterator iterator = tempLinks.values().iterator(); + public void cleanupLinkRequests() { + Iterator iterator = linkRequests.values().iterator(); long ctm = System.currentTimeMillis(); while (iterator.hasNext()) { - TempLink tempLink = iterator.next(); + LinkRequest linkRequest = iterator.next(); - if (ctm > tempLink.expiryTime()) { - removedLinks.add(tempLink.code()); + if (ctm > linkRequest.expiryTime()) { iterator.remove(); - } - } + linkRequestForPlayer.remove(linkRequest.requesterUuid(), linkRequest.code()); - List players = playerManager.playersByTempLinkIds(removedLinks); - for (Player player : players) { - player.sendMessage(String.format( - "&cYour link (%s) has expired! Run the link account command again if you need a new code.", - player.linkId())); - player.linkId(0); + var requester = linkRequest.requester(); + if (requester != null) { + requester.sendMessage(Components.cleanupLinkRequestExpired(linkRequest.code())); + } + } } } } diff --git a/src/main/java/org/geysermc/globallinkserver/link/LinkRequest.java b/src/main/java/org/geysermc/globallinkserver/link/LinkRequest.java new file mode 100644 index 0000000..ea31be5 --- /dev/null +++ b/src/main/java/org/geysermc/globallinkserver/link/LinkRequest.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2021-2025 GeyserMC + * Licensed under the MIT license + * @link https://github.com/GeyserMC/GlobalLinkServer + */ +package org.geysermc.globallinkserver.link; + +import java.util.UUID; +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public record LinkRequest(int code, long expiryTime, UUID requesterUuid, String requesterUsername) { + public LinkRequest(int code, long ttl, Player requester) { + this(code, System.currentTimeMillis() + ttl, requester.getUniqueId(), requester.getName()); + } + + public @Nullable Player requester() { + return Bukkit.getPlayer(requesterUuid); + } +} diff --git a/src/main/java/org/geysermc/globallinkserver/link/TempLink.java b/src/main/java/org/geysermc/globallinkserver/link/TempLink.java deleted file mode 100644 index f7e3e01..0000000 --- a/src/main/java/org/geysermc/globallinkserver/link/TempLink.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2021-2023 GeyserMC - * - * 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/GlobalLinkServer - */ -package org.geysermc.globallinkserver.link; - -public class TempLink extends Link { - private int code; - private long expiryTime; - - public int code() { - return code; - } - - public TempLink code(int code) { - this.code = code; - return this; - } - - public long expiryTime() { - return expiryTime; - } - - public TempLink expiryTime(long expiryTime) { - this.expiryTime = expiryTime; - return this; - } -} diff --git a/src/main/java/org/geysermc/globallinkserver/manager/DatabaseManager.java b/src/main/java/org/geysermc/globallinkserver/manager/DatabaseManager.java new file mode 100644 index 0000000..70720f2 --- /dev/null +++ b/src/main/java/org/geysermc/globallinkserver/manager/DatabaseManager.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2025 GeyserMC + * Licensed under the MIT license + * @link https://github.com/GeyserMC/GlobalLinkServer + */ +package org.geysermc.globallinkserver.manager; + +import java.sql.Connection; +import java.sql.SQLException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import org.geysermc.globallinkserver.config.Config; +import org.jspecify.annotations.NullMarked; +import org.mariadb.jdbc.MariaDbPoolDataSource; + +@NullMarked +public final class DatabaseManager { + private final MariaDbPoolDataSource dataSource; + private final ExecutorService executorService = Executors.newCachedThreadPool(); + + public DatabaseManager(Config config) { + try { + Class.forName("org.mariadb.jdbc.Driver"); + + var hostname = config.database().hostname(); + String serverName; + int port = 3306; + + var hostnameSplit = hostname.split(":"); + if (hostnameSplit.length > 1) { + serverName = hostnameSplit[0]; + port = Integer.parseInt(hostnameSplit[1]); + } else { + serverName = hostname; + } + + dataSource = new MariaDbPoolDataSource(); + dataSource.setServerName(serverName); + dataSource.setPort(port); + dataSource.setDatabaseName(config.database().database()); + dataSource.setUser(config.database().username()); + dataSource.setPassword(config.database().password()); + dataSource.setMinPoolSize(1); + dataSource.setMaxPoolSize(config.database().maxPoolSize()); + + } catch (ClassNotFoundException exception) { + throw new RuntimeException("Cannot find required class to load the MariaDB database"); + } catch (SQLException exception) { + throw new RuntimeException("Unable to set the datasource connection fields", exception); + } + + connectionCheck(); + } + + private void connectionCheck() { + try (var connection = connection(); var statement = connection.createStatement()) { + try (var resultSet = statement.executeQuery("SELECT 1")) { + resultSet.next(); + } + } catch (SQLException exception) { + throw new RuntimeException("Could not connect to database!", exception); + } + } + + public Connection connection() throws SQLException { + return dataSource.getConnection(); + } + + public ExecutorService executor() { + return executorService; + } +} diff --git a/src/main/java/org/geysermc/globallinkserver/manager/PlayerManager.java b/src/main/java/org/geysermc/globallinkserver/manager/PlayerManager.java new file mode 100644 index 0000000..6ab40f4 --- /dev/null +++ b/src/main/java/org/geysermc/globallinkserver/manager/PlayerManager.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2025 GeyserMC + * Licensed under the MIT license + * @link https://github.com/GeyserMC/GlobalLinkServer + */ +package org.geysermc.globallinkserver.manager; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import org.bukkit.entity.Player; +import org.geysermc.floodgate.api.FloodgateApi; +import org.geysermc.floodgate.api.player.FloodgatePlayer; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public final class PlayerManager { + private final FloodgateApi api; + + public PlayerManager(FloodgateApi api) { + this.api = api; + } + + public boolean isBedrockPlayer(Player player) { + return api.isFloodgatePlayer(player.getUniqueId()); + } + + public boolean isBedrockId(UUID id) { + return api.isFloodgateId(id); + } + + public @Nullable FloodgatePlayer bedrockPlayer(UUID uuid) { + return api.getPlayer(uuid); + } + + public CompletableFuture<@Nullable String> fetchGamertagFor(long xuid) { + return api.getGamertagFor(xuid); + } +} diff --git a/src/main/java/org/geysermc/globallinkserver/player/Player.java b/src/main/java/org/geysermc/globallinkserver/player/Player.java deleted file mode 100644 index 5d31cf9..0000000 --- a/src/main/java/org/geysermc/globallinkserver/player/Player.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2021-2023 GeyserMC - * - * 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/GlobalLinkServer - */ -package org.geysermc.globallinkserver.player; - -import java.util.UUID; - -public interface Player { - UUID uniqueId(); - - String username(); - - int linkId(); - - void linkId(int linkId); - - void sendMessage(String message); - - void disconnect(String reason); - - default void sendJoinMessages() { - sendMessage( - "&eTo start the linking process run `&9/linkaccount&e` or run `&9/linkaccount &3&e` to finish the process."); - sendMessage("&eTo unlink your account (if it is linked) run `&9/unlinkaccount&e`."); - } - - default String formatMessage(String message) { - return message.replace("&", "ยง"); - } -} diff --git a/src/main/java/org/geysermc/globallinkserver/player/PlayerManager.java b/src/main/java/org/geysermc/globallinkserver/player/PlayerManager.java deleted file mode 100644 index 5813113..0000000 --- a/src/main/java/org/geysermc/globallinkserver/player/PlayerManager.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (c) 2021-2024 GeyserMC - * - * 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/GlobalLinkServer - */ -package org.geysermc.globallinkserver.player; - -import it.unimi.dsi.fastutil.ints.IntSet; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import org.cloudburstmc.protocol.bedrock.BedrockServerSession; -import org.cloudburstmc.protocol.bedrock.util.ChainValidationResult; -import org.geysermc.globallinkserver.bedrock.BedrockPlayer; -import org.geysermc.globallinkserver.java.JavaPlayer; -import org.geysermc.mcprotocollib.auth.GameProfile; -import org.geysermc.mcprotocollib.network.Session; - -public class PlayerManager { - private final Map javaPlayers = new HashMap<>(); - private final Map bedrockPlayers = new HashMap<>(); - - public BedrockPlayer addBedrockPlayer(BedrockServerSession session, ChainValidationResult.IdentityData identity) { - BedrockPlayer player = new BedrockPlayer(session, identity); - - BedrockPlayer old = bedrockPlayers.put(player.username(), player); - if (old != null) { - old.disconnect("You logged in from somewhere else"); - } - - return player; - } - - public JavaPlayer addJavaPlayer(Session session, GameProfile gameProfile) { - JavaPlayer player = new JavaPlayer(session, gameProfile); - - JavaPlayer old = javaPlayers.put(gameProfile.getName(), player); - if (old != null) { - old.disconnect("You logged in from somewhere else"); - } - - return player; - } - - public void removeJavaPlayer(JavaPlayer player) { - javaPlayers.remove(player.username(), player); - } - - public void removeBedrockPlayer(BedrockPlayer player) { - bedrockPlayers.remove(player.username(), player); - } - - public List playersByTempLinkIds(IntSet removedTempLinks) { - List players = new ArrayList<>(); - - for (JavaPlayer player : javaPlayers.values()) { - if (removedTempLinks.contains(player.linkId())) { - players.add(player); - } - } - for (BedrockPlayer player : bedrockPlayers.values()) { - if (removedTempLinks.contains(player.linkId())) { - players.add(player); - } - } - - return players; - } - - public void kickPlayers(UUID javaPlayer, UUID bedrockPlayer, String reason) { - for (JavaPlayer player : javaPlayers.values()) { - if (player.uniqueId().equals(javaPlayer)) { - player.disconnect(reason); - } - } - for (BedrockPlayer player : bedrockPlayers.values()) { - if (player.uniqueId().equals(bedrockPlayer)) { - player.disconnect(reason); - } - } - } -} diff --git a/src/main/java/org/geysermc/globallinkserver/service/LinkInfoService.java b/src/main/java/org/geysermc/globallinkserver/service/LinkInfoService.java new file mode 100644 index 0000000..34d5a47 --- /dev/null +++ b/src/main/java/org/geysermc/globallinkserver/service/LinkInfoService.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025 GeyserMC + * Licensed under the MIT license + * @link https://github.com/GeyserMC/GlobalLinkServer + */ +package org.geysermc.globallinkserver.service; + +import org.bukkit.entity.Player; +import org.geysermc.globallinkserver.Components; +import org.geysermc.globallinkserver.link.FullLink; +import org.geysermc.globallinkserver.manager.PlayerManager; +import org.jspecify.annotations.NullMarked; + +@NullMarked +public final class LinkInfoService { + private final LinkLookupService linkLookupService; + private final PlayerManager playerManager; + + public LinkInfoService(LinkLookupService linkLookupService, PlayerManager playerManager) { + this.linkLookupService = linkLookupService; + this.playerManager = playerManager; + } + + public void sendCurrentLinkInfo(Player player) { + FullLink link = linkLookupService.cachedLookup(player); + if (link == null) { + player.sendMessage(Components.INFO_NOT_LINKED); + return; + } + + // Show info from the opposite platform + if (playerManager.isBedrockPlayer(player)) { + player.sendMessage(Components.infoLinkInfo(link.javaUsername(), link.javaId(), false)); + } else { + player.sendMessage(Components.infoLinkInfo(link.bedrockUsername(), link.bedrockId(), true)); + } + } +} diff --git a/src/main/java/org/geysermc/globallinkserver/service/LinkLookupService.java b/src/main/java/org/geysermc/globallinkserver/service/LinkLookupService.java new file mode 100644 index 0000000..f4a65af --- /dev/null +++ b/src/main/java/org/geysermc/globallinkserver/service/LinkLookupService.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2025 GeyserMC + * Licensed under the MIT license + * @link https://github.com/GeyserMC/GlobalLinkServer + */ +package org.geysermc.globallinkserver.service; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import org.bukkit.entity.Player; +import org.geysermc.globallinkserver.link.FullLink; +import org.geysermc.globallinkserver.manager.DatabaseManager; +import org.geysermc.globallinkserver.manager.PlayerManager; +import org.geysermc.globallinkserver.util.ThrowingConsumer; +import org.geysermc.globallinkserver.util.ThrowingFunction; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public final class LinkLookupService { + private final PlayerManager playerManager; + private final DatabaseManager database; + + private final Map linkedPlayers = new Object2ObjectOpenHashMap<>(); + private final Set lookupInProcess = new ObjectOpenHashSet<>(); + + public LinkLookupService(PlayerManager playerManager, DatabaseManager database) { + this.playerManager = playerManager; + this.database = database; + } + + public boolean isLookupCompleted(Player player) { + return !lookupInProcess.contains(player.getUniqueId()); + } + + public @Nullable FullLink cachedLookup(Player player) { + return linkedPlayers.get(player.getUniqueId()); + } + + public boolean isLinkedCached(Player player) { + return cachedLookup(player) != null; + } + + public CompletableFuture<@Nullable FullLink> lookup(Player player) { + var uuid = player.getUniqueId(); + var floodgatePlayer = playerManager.bedrockPlayer(uuid); + + lookupInProcess.add(uuid); + + CompletableFuture future; + if (floodgatePlayer != null) { + future = findBedrockLink(uuid, floodgatePlayer.getUsername()); + } else { + future = findJavaLink(uuid, player.getName()); + } + + return future.whenComplete((link, throwable) -> { + lookupInProcess.remove(uuid); + if (throwable == null && link != null) { + linkedPlayers.put(uuid, link); + } + }); + } + + public void invalidate(Player player) { + linkedPlayers.remove(player.getUniqueId()); + lookupInProcess.remove(player.getUniqueId()); + } + + public CompletableFuture<@Nullable FullLink> findJavaLink(UUID javaId, String javaName) { + return attemptFindLink( + "SELECT `bedrock_id` FROM `links` WHERE `java_id` = ?", + stmt -> stmt.setString(1, javaId.toString()), + resultSet -> resultSet.getLong("bedrock_id")) + .thenCompose(xuid -> { + if (xuid == null) { + return CompletableFuture.completedFuture(null); + } + + return playerManager.fetchGamertagFor(xuid).thenApply(gamertag -> { + return new FullLink(new UUID(0, xuid), gamertag, javaId, javaName); + }); + }); + } + + public CompletableFuture<@Nullable FullLink> findBedrockLink(UUID bedrockId, String gamertag) { + return attemptFindLink( + "SELECT `java_id`, `java_name` FROM `links` WHERE `bedrock_id` = ?", + stmt -> stmt.setLong(1, bedrockId.getLeastSignificantBits()), + resultSet -> { + UUID javaId = UUID.fromString(resultSet.getString("java_id")); + String javaName = resultSet.getString("java_name"); + return new FullLink(bedrockId, gamertag, javaId, javaName); + }); + } + + private CompletableFuture attemptFindLink( + String query, + ThrowingConsumer parameterSetter, + ThrowingFunction resultProcessor) { + return CompletableFuture.supplyAsync( + () -> { + try (Connection connection = database.connection(); + PreparedStatement queryStmt = connection.prepareStatement(query)) { + parameterSetter.accept(queryStmt); + + try (ResultSet resultSet = queryStmt.executeQuery()) { + if (resultSet.next()) { + return resultProcessor.apply(resultSet); + } else { + return null; + } + } + + } catch (SQLException exception) { + throw new CompletionException("Error while finding link! ", exception); + } + }, + database.executor()); + } +} diff --git a/src/main/java/org/geysermc/globallinkserver/util/CommandUtils.java b/src/main/java/org/geysermc/globallinkserver/util/CommandUtils.java deleted file mode 100644 index 2339ce2..0000000 --- a/src/main/java/org/geysermc/globallinkserver/util/CommandUtils.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (c) 2021-2023 GeyserMC - * - * 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/GlobalLinkServer - */ -package org.geysermc.globallinkserver.util; - -import org.geysermc.globallinkserver.java.JavaPlayer; -import org.geysermc.globallinkserver.link.LinkManager; -import org.geysermc.globallinkserver.link.TempLink; -import org.geysermc.globallinkserver.player.Player; -import org.geysermc.globallinkserver.player.PlayerManager; - -public class CommandUtils { - public static void handleCommand( - LinkManager linkManager, PlayerManager playerManager, Player player, String message) { - - String[] args = message.split(" "); - - if (args[0].equals("/linkaccount")) { - if (args.length == 2) { - int linkId = Utils.parseInt(args[1]); - - if (linkId <= 0) { - player.sendMessage("&cInvalid link code"); - return; - } - - TempLink tempLink = linkManager.tempLinkById(linkId); - - if (tempLink == null) { - player.sendMessage("&cCould not find the provided link. Is it expired?"); - return; - } - - if (player instanceof JavaPlayer) { - tempLink.javaId(player.uniqueId()); - tempLink.javaUsername(player.username()); - } else { - tempLink.bedrockId(player.uniqueId()); - } - - if (tempLink.javaId() == null || tempLink.bedrockId() == null) { - player.sendMessage( - "&cWelp.. You can only link a Java account to a Bedrock account. Try to start the linking process again."); - return; - } - - linkManager.finaliseLink(tempLink).whenComplete((result, error) -> { - if (error != null || !result) { - if (error != null) { - error.printStackTrace(); - } - System.out.println(result); - player.sendMessage( - "&cAn unknown error happened while linking your account. Try it again later"); - return; - } - - playerManager.kickPlayers( - tempLink.javaId(), tempLink.bedrockId(), "&aYou are now successfully linked! :)"); - }); - return; - } - - if (args.length == 1) { - if (player.linkId() != 0) { - linkManager.removeTempLink(player.linkId()); - } - - String code = String.format("%04d", linkManager.createTempLink(player)); - - String otherPlatform = player instanceof JavaPlayer ? "Bedrock" : "Java"; - - player.sendMessage("&aPlease join on " + otherPlatform + " and run `&9/linkaccount &3" + code + "&a`"); - return; - } - - player.sendMessage( - "&cInvalid format! &fValid versions are: `&9/linkaccount&c` to make a link or `&9/linkaccount &3&c` to finalise a link"); - return; - } - - if (args[0].equals("/unlinkaccount")) { - if (args.length == 1) { - linkManager.unlinkAccount(player).whenComplete((result, error) -> { - if (error != null) { - error.printStackTrace(); - System.out.println(result); - player.sendMessage( - "&cAn unknown error happened while unlinking your account. Try it again later"); - return; - } - - if (result) { - player.sendMessage("&aYou are successfully unlinked"); - } else { - player.sendMessage("&eYou aren't linked to any account"); - } - }); - return; - } - - player.sendMessage("&cInvalid format! Use: `&9/unlinkaccount&c`"); - return; - } - - player.sendMessage("&cUnknown command"); - } -} diff --git a/src/main/java/org/geysermc/globallinkserver/util/MultiConditionSet.java b/src/main/java/org/geysermc/globallinkserver/util/MultiConditionSet.java new file mode 100644 index 0000000..53f7819 --- /dev/null +++ b/src/main/java/org/geysermc/globallinkserver/util/MultiConditionSet.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2025 GeyserMC + * Licensed under the MIT license + * @link https://github.com/GeyserMC/GlobalLinkServer + */ +package org.geysermc.globallinkserver.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import org.jspecify.annotations.NullMarked; + +/** + * A special list that checks every interval if its keys can be removed. + * A key can only be removed if all provided conditions are satisfied. + * Given an instance with three conditions, if the first condition fails for the given key the remaining conditions won't be checked for the key. + *

+ * Every condition and the removal listeners are expected to be quick functions, as they block the removal check and thus the (potential) removal of the other keys. + * In the case of {@link #remove(Object)} it blocks the caller thread instead + *

+ * Make sure that the conditions and the removal listeners are thread safe (and the code its calling), because the executing thread is undefined. + */ +@NullMarked +public final class MultiConditionSet { + private final List> conditions = new ArrayList<>(); + private final Set keys = Collections.synchronizedSet(new HashSet<>()); + private final List> removalListeners = new ArrayList<>(); + private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); + + public MultiConditionSet(int checkIntervalInMillis, Consumer removalListener) { + this.removalListeners.add(removalListener); + this.executor.scheduleAtFixedRate(this::checkConditions, 0, checkIntervalInMillis, TimeUnit.MILLISECONDS); + } + + private void checkConditions() { + // should be fine to have everything in a synchronized block, since the concurrent access will be really limited + // in a setting like this link server. + synchronized (keys) { + var iterator = keys.iterator(); + + keys: + while (iterator.hasNext()) { + var key = iterator.next(); + for (Object2BooleanFunction condition : conditions) { + if (!condition.apply(key)) { + continue keys; + } + } + // all conditions have been met, remove the key + iterator.remove(); + for (Consumer consumer : removalListeners) { + consumer.accept(key); + } + } + } + } + + public MultiConditionSet addRemovalCondition(Object2BooleanFunction condition) { + conditions.add(condition); + return this; + } + + public MultiConditionSet addRemovalListener(Consumer removalListener) { + removalListeners.add(removalListener); + return this; + } + + public void add(K key) { + keys.add(key); + } + + public void remove(K key) { + keys.remove(key); + for (Consumer consumer : removalListeners) { + consumer.accept(key); + } + } + + public void close() { + executor.shutdown(); + } +} diff --git a/src/main/java/org/geysermc/globallinkserver/util/Object2BooleanFunction.java b/src/main/java/org/geysermc/globallinkserver/util/Object2BooleanFunction.java new file mode 100644 index 0000000..86bb0d9 --- /dev/null +++ b/src/main/java/org/geysermc/globallinkserver/util/Object2BooleanFunction.java @@ -0,0 +1,11 @@ +/* + * Copyright (c) 2025 GeyserMC + * Licensed under the MIT license + * @link https://github.com/GeyserMC/GlobalLinkServer + */ +package org.geysermc.globallinkserver.util; + +@FunctionalInterface +public interface Object2BooleanFunction { + boolean apply(T t); +} diff --git a/src/main/java/org/geysermc/globallinkserver/util/ThrowingConsumer.java b/src/main/java/org/geysermc/globallinkserver/util/ThrowingConsumer.java new file mode 100644 index 0000000..c5a6196 --- /dev/null +++ b/src/main/java/org/geysermc/globallinkserver/util/ThrowingConsumer.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2025 GeyserMC + * Licensed under the MIT license + * @link https://github.com/GeyserMC/GlobalLinkServer + */ +package org.geysermc.globallinkserver.util; + +import java.sql.SQLException; + +@FunctionalInterface +public interface ThrowingConsumer { + void accept(T t) throws SQLException; +} diff --git a/src/main/java/org/geysermc/globallinkserver/util/ThrowingFunction.java b/src/main/java/org/geysermc/globallinkserver/util/ThrowingFunction.java new file mode 100644 index 0000000..8390b39 --- /dev/null +++ b/src/main/java/org/geysermc/globallinkserver/util/ThrowingFunction.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2025 GeyserMC + * Licensed under the MIT license + * @link https://github.com/GeyserMC/GlobalLinkServer + */ +package org.geysermc.globallinkserver.util; + +import java.sql.SQLException; + +@FunctionalInterface +public interface ThrowingFunction { + R apply(T t) throws SQLException; +} diff --git a/src/main/java/org/geysermc/globallinkserver/util/Utils.java b/src/main/java/org/geysermc/globallinkserver/util/Utils.java index 1441e9a..9d715a6 100644 --- a/src/main/java/org/geysermc/globallinkserver/util/Utils.java +++ b/src/main/java/org/geysermc/globallinkserver/util/Utils.java @@ -1,84 +1,22 @@ /* - * Copyright (c) 2021-2021 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 + * Copyright (c) 2021-2025 GeyserMC + * Licensed under the MIT license * @link https://github.com/GeyserMC/GlobalLinkServer */ - package org.geysermc.globallinkserver.util; -import org.cloudburstmc.protocol.bedrock.BedrockServerSession; -import org.cloudburstmc.protocol.bedrock.packet.ServerToClientHandshakePacket; -import org.cloudburstmc.protocol.bedrock.util.ChainValidationResult; -import org.cloudburstmc.protocol.bedrock.util.EncryptionUtils; - -import javax.crypto.SecretKey; -import java.security.KeyPair; -import java.security.PublicKey; -import java.util.List; - -public class Utils { - - public static int parseInt(String toParse) { - try { - return Integer.parseInt(toParse); - } catch (NumberFormatException exception) { - return -1; - } - } - - public static long parseLong(String toParse) { - try { - return Long.parseLong(toParse); - } catch (NumberFormatException exception) { - return -1; - } - } - - public static ChainValidationResult.IdentityData validateAndEncryptConnection(BedrockServerSession session, List certChainData, String clientDataJwt) throws Exception { - ChainValidationResult result = EncryptionUtils.validateChain(certChainData); - if (!result.signed()) { - throw new IllegalArgumentException("Chain is not signed"); - } - PublicKey identityPublicKey = result.identityClaims().parsedIdentityPublicKey(); - - byte[] clientDataPayload = EncryptionUtils.verifyClientData(clientDataJwt, identityPublicKey); - if (clientDataPayload == null) { - throw new IllegalStateException("Client data isn't signed by the given chain data"); - } - - startEncryptionHandshake(session, identityPublicKey); - - return result.identityClaims().extraData; - } - - private static void startEncryptionHandshake(BedrockServerSession session, PublicKey key) throws Exception { - KeyPair serverKeyPair = EncryptionUtils.createKeyPair(); - byte[] token = EncryptionUtils.generateRandomToken(); +import com.mojang.brigadier.context.CommandContext; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import org.bukkit.entity.Player; +import org.jspecify.annotations.NullMarked; - ServerToClientHandshakePacket packet = new ServerToClientHandshakePacket(); - packet.setJwt(EncryptionUtils.createHandshakeJwt(serverKeyPair, token)); - session.sendPacketImmediately(packet); +@NullMarked +@SuppressWarnings("UnstableApiUsage") +public final class Utils { + private Utils() {} - SecretKey encryptionKey = EncryptionUtils.getSecretKey(serverKeyPair.getPrivate(), key, token); - session.enableEncryption(encryptionKey); + public static Player contextExecutor(CommandContext ctx) { + //noinspection DataFlowIssue we know it can't be null + return (Player) ctx.getSource().getExecutor(); } } diff --git a/src/main/resources/config.json b/src/main/resources/config.json deleted file mode 100644 index 4a3635d..0000000 --- a/src/main/resources/config.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "bind-ip": "0.0.0.0", - "bind-port-java": 25565, - "bind-port-bedrock": 19132, - "hostname": "127.0.0.1", - "username": "global_link", - "password": "some_pass", - "database": "global_link_dev" -} \ No newline at end of file diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml new file mode 100644 index 0000000..a438d3b --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,14 @@ +database: + hostname: 127.0.0.1 + username: global_link + password: some_pass + database: global_link_dev + max-pool-size: 3 + +spawn: + world: world + x: 0 + y: 50 + z: 0 + yaw: -180 + pitch: 0 diff --git a/src/main/resources/plugin.yml b/src/main/resources/plugin.yml new file mode 100644 index 0000000..1634e65 --- /dev/null +++ b/src/main/resources/plugin.yml @@ -0,0 +1,14 @@ +name: GlobalLinkServer +version: '1.0' +main: org.geysermc.globallinkserver.GlobalLinkServer +description: GlobalLinkServer plugin +api-version: '1.21.4' +dependencies: + server: + floodgate: + load: OMIT + required: true + geyser: + load: OMIT + required: true + join-classpath: false