diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0a788a8..625dde1 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,24 +27,6 @@ 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: Notify Discord if: ${{ (success() || failure()) && github.repository == 'GeyserMC/GlobalLinkServer' }} uses: Tim203/actions-git-discord-webhook@main 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/build.gradle.kts b/build.gradle.kts index 2408b68..b6e6776 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,20 +1,19 @@ plugins { 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 + paperweight.paperDevBundle("1.21.4-R0.1-SNAPSHOT") + implementation(libs.gson) // newer version required for record support implementation(libs.bundles.fastutil) - - compileOnly(libs.spigot.api) - compileOnly(libs.geyser.api) - + compileOnly(libs.floodgate.api) implementation(libs.mariadb.client) - compileOnly(libs.checker.qual) } @@ -28,7 +27,7 @@ indra { mitLicense() javaVersions { - target(17) + target(21) } spotless { @@ -40,6 +39,21 @@ indra { } } +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) }) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1ff1829..863e319 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,16 +1,14 @@ [versions] gson = "2.10.1" fastutil = "8.5.2" -adventure-text = "4.15.0-20231207.074016-23" # Match version to MCPL mariadb-client = "2.7.3" checker-qual = "3.21.1" indra = "3.1.2" - -spigot-api = "1.16.5-R0.1-SNAPSHOT" +paperweight = "2.0.0-beta.12" +runpaper = "2.3.1" [libraries] -spigot-api = { group = "org.spigotmc", name = "spigot-api", version.ref = "spigot-api" } -geyser-api = { group = "org.geysermc.geyser", name = "api", version = "2.6.0-SNAPSHOT" } +floodgate-api = { group = "org.geysermc.floodgate", name = "api", version = "2.2.3-SNAPSHOT" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } @@ -26,6 +24,8 @@ checker-qual = { module = "org.checkerframework:checker-qual", version.ref = "ch 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] 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/GlobalLinkServer.java b/src/main/java/org/geysermc/globallinkserver/GlobalLinkServer.java index 500d16b..64b1878 100644 --- a/src/main/java/org/geysermc/globallinkserver/GlobalLinkServer.java +++ b/src/main/java/org/geysermc/globallinkserver/GlobalLinkServer.java @@ -5,52 +5,106 @@ */ package org.geysermc.globallinkserver; -import java.util.Timer; -import java.util.TimerTask; +import java.util.List; import java.util.logging.Logger; + +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.argument.ArgumentTypes; +import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager; +import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; -import org.bukkit.command.Command; -import org.bukkit.command.CommandSender; import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +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.globallinkserver.config.Config; import org.geysermc.globallinkserver.config.ConfigReader; import org.geysermc.globallinkserver.link.LinkManager; import org.geysermc.globallinkserver.util.CommandUtils; +import org.geysermc.globallinkserver.util.Utils; -public class GlobalLinkServer extends JavaPlugin { - private static final Timer TIMER = new Timer(); +@SuppressWarnings("UnstableApiUsage") +public class GlobalLinkServer extends JavaPlugin implements Listener { public static Logger LOGGER; + public static LinkManager linkManager; + + public final static Component LINK_INSTRUCTIONS = Component.text("Run the ").color(NamedTextColor.AQUA) + .append(Component.text("`/link`", NamedTextColor.GREEN)) + .append(Component.text(" command to link your accounts.", NamedTextColor.AQUA)); - private LinkManager linkManager; + public final static Component UNLINK_INSTRUCTIONS = Component.text("To unlink, use ").color(NamedTextColor.AQUA) + .append(Component.text("`/unlink`", NamedTextColor.RED)) + .append(Component.text(".")); @Override public void onEnable() { LOGGER = getLogger(); - Config config = ConfigReader.readConfig(); + Config config = ConfigReader.readConfig(this); + linkManager = new LinkManager(config); + CommandUtils commandUtils = new CommandUtils(linkManager); - Bukkit.getScheduler().scheduleSyncRepeatingTask(this, linkManager::cleanupTempLinks, 0, 0); + Bukkit.getScheduler().scheduleSyncRepeatingTask(this, linkManager::cleanupTempLinks, 0, 1); + Bukkit.getScheduler().scheduleSyncRepeatingTask(this, () -> { + Bukkit.getOnlinePlayers().forEach(player -> { + if (Utils.isLinked(player)) { + player.sendActionBar(UNLINK_INSTRUCTIONS); + } else { + player.sendActionBar(LINK_INSTRUCTIONS); + } + }); + }, 10, 15); + getServer().getPluginManager().registerEvents(this, this); - TimerTask task = new TimerTask() { - @Override - public void run() { - linkManager.cleanupTempLinks(); - } - }; - TIMER.scheduleAtFixedRate(task, 0L, 60_000L); + 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", ArgumentTypes.integerRange()) + .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(commandUtils::info) + .build(), + "Use this command to show information whether you are currently linked.", + List.of("info") + ); + }); LOGGER.info("Started Global Linking Server plugin!"); } - @Override - public boolean onCommand( - @NonNull CommandSender sender, @NonNull Command command, @NonNull String label, @NonNull String[] args) { - if (sender instanceof Player player) { - CommandUtils.handleCommand(linkManager, player, command.getName() + " " + String.join(" ", args)); - } - return true; + @EventHandler + public void onPlayerLoad(PlayerJoinEvent event) { + Utils.processJoin(event.getPlayer()); + } + + @EventHandler + public void onPlayerLeave(PlayerQuitEvent event) { + Utils.processLeave(event.getPlayer()); } } diff --git a/src/main/java/org/geysermc/globallinkserver/config/Config.java b/src/main/java/org/geysermc/globallinkserver/config/Config.java index 712eb21..46b1d60 100644 --- a/src/main/java/org/geysermc/globallinkserver/config/Config.java +++ b/src/main/java/org/geysermc/globallinkserver/config/Config.java @@ -6,5 +6,8 @@ package org.geysermc.globallinkserver.config; public record Config( - // database related - String hostname, String username, String password, String database) {} + String hostname, + String username, + String password, + String database +) {} diff --git a/src/main/java/org/geysermc/globallinkserver/config/ConfigReader.java b/src/main/java/org/geysermc/globallinkserver/config/ConfigReader.java index e1b7815..4cb521f 100644 --- a/src/main/java/org/geysermc/globallinkserver/config/ConfigReader.java +++ b/src/main/java/org/geysermc/globallinkserver/config/ConfigReader.java @@ -5,27 +5,37 @@ */ package org.geysermc.globallinkserver.config; -import static org.geysermc.globallinkserver.GlobalLinkServer.LOGGER; - import com.google.gson.Gson; +import org.bukkit.configuration.serialization.ConfigurationSerializable; +import org.bukkit.configuration.serialization.ConfigurationSerialization; +import org.bukkit.plugin.java.JavaPlugin; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; public class ConfigReader { private static final Gson GSON = new Gson(); - private static final Path CONFIG_PATH = Paths.get("config.json"); + private static Path CONFIG_PATH; - public static Config readConfig() { - LOGGER.info("Reading config from " + CONFIG_PATH.toAbsolutePath()); - String data = configContent(); - if (data == null) { - createConfig(); - } - data = configContent(); + public static Config readConfig(JavaPlugin plugin) { + plugin.saveDefaultConfig(); + var config = plugin.getConfig(); + plugin.saveConfig(); + return new Config(config.getString("hostname"), + config.getString("username"), + config.getString("password"), + config.getString("database")); - return GSON.fromJson(data, Config.class); +// CONFIG_PATH = path; +// LOGGER.info("Reading config from " + CONFIG_PATH.toAbsolutePath()); +// String data = configContent(); +// if (data == null) { +// createConfig(); +// } +// data = configContent(); +// +// return GSON.fromJson(data, Config.class); } private static String configContent() { @@ -38,6 +48,7 @@ private static String configContent() { private static void createConfig() { try { + //noinspection DataFlowIssue Files.copy(ConfigReader.class.getResourceAsStream("/config.json"), CONFIG_PATH); } catch (IOException exception) { throw new RuntimeException("Failed to copy config", exception); diff --git a/src/main/java/org/geysermc/globallinkserver/link/Link.java b/src/main/java/org/geysermc/globallinkserver/link/Link.java index fd29588..38451f6 100644 --- a/src/main/java/org/geysermc/globallinkserver/link/Link.java +++ b/src/main/java/org/geysermc/globallinkserver/link/Link.java @@ -24,6 +24,8 @@ */ package org.geysermc.globallinkserver.link; +import org.bukkit.entity.Player; + import java.util.UUID; public class Link { @@ -31,6 +33,14 @@ public class Link { private UUID javaId; private String javaUsername; + public Link() { + } + + public Link(Player javaPlayer) { + this.javaId = javaPlayer.getUniqueId(); + this.javaUsername = javaPlayer.getName(); + } + public UUID bedrockId() { return bedrockId; } diff --git a/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java b/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java index f2094c8..9c05562 100644 --- a/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java +++ b/src/main/java/org/geysermc/globallinkserver/link/LinkManager.java @@ -11,12 +11,16 @@ import it.unimi.dsi.fastutil.ints.IntSet; import java.sql.Connection; import java.sql.PreparedStatement; +import java.sql.ResultSet; import java.sql.SQLException; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; import org.bukkit.entity.Player; import org.geysermc.globallinkserver.config.Config; @@ -45,11 +49,11 @@ public LinkManager(Config config) { public int createTempLink(Player player) { TempLink link = new TempLink(); - if (Utils.isBedrockPlayer(player)) { + if (Utils.isBedrockPlayerId(player)) { link.bedrockId(player.getUniqueId()); } else { link.javaId(player.getUniqueId()); - link.javaUsername(player.getDisplayName()); + link.javaUsername(player.getName()); } link.expiryTime(System.currentTimeMillis() + TEMP_LINK_DURATION); link.code(createCode()); @@ -118,9 +122,10 @@ public CompletableFuture unlinkAccount(Player player) { try (Connection connection = dataSource.getConnection()) { PreparedStatement query; - if (Utils.isBedrockPlayer(player)) { - query = connection.prepareStatement("DELETE FROM `links` WHERE `bedrock_id` = ?;"); - query.setLong(1, player.getUniqueId().getLeastSignificantBits()); + if (Utils.isBedrockPlayerId(player)) { // Should never happen + throw new RuntimeException("Floodgate linking was disabled!!!"); + //query = connection.prepareStatement("DELETE FROM `links` WHERE `bedrock_id` = ?;"); + //query.setLong(1, player.getUniqueId().getLeastSignificantBits()); } else { query = connection.prepareStatement("DELETE FROM `links` WHERE `java_id` = ?;"); query.setString(1, player.getUniqueId().toString()); @@ -135,9 +140,34 @@ public CompletableFuture unlinkAccount(Player player) { executorService); } + public CompletableFuture> attemptFindJavaLink(Player player) { + return CompletableFuture.supplyAsync( + () -> { + try (Connection connection = dataSource.getConnection()) { + try (PreparedStatement query = connection.prepareStatement( + "SELECT `bedrock_id` FROM `links` WHERE `java_id` = ? LIMIT 1")) { + query.setString(1, player.getUniqueId().toString()); + + try (ResultSet resultSet = query.executeQuery()) { + if (resultSet.next()) { + return Optional.of( + new Link(player).bedrockId(new UUID(0, resultSet.getLong("bedrock_id"))) + ); + } else { + return Optional.empty(); // No match found + } + } + } + } catch (SQLException exception) { + throw new CompletionException("Error while finding Java link", exception); + } + }, + executorService); + } + + public void cleanupTempLinks() { IntSet removedLinks = new IntArraySet(); - Iterator iterator = tempLinks.values().iterator(); long ctm = System.currentTimeMillis(); @@ -155,9 +185,8 @@ public void cleanupTempLinks() { Player player = Bukkit.getPlayer(entry.getKey()); int currentLinkCode = CURRENT_LINK_CODES.remove(entry.getKey()); if (player != null) { - player.sendMessage(String.format( - "&cYour link (%s) has expired! Run the link account command again if you need a new code.", - currentLinkCode)); + player.sendMessage(Component.text("Your link (%s) has expired! Run the link account again if you need a new code.".formatted(currentLinkCode)) + .color(NamedTextColor.RED)); } } } diff --git a/src/main/java/org/geysermc/globallinkserver/link/TempLink.java b/src/main/java/org/geysermc/globallinkserver/link/TempLink.java index f7e3e01..771a1f7 100644 --- a/src/main/java/org/geysermc/globallinkserver/link/TempLink.java +++ b/src/main/java/org/geysermc/globallinkserver/link/TempLink.java @@ -32,17 +32,15 @@ public int code() { return code; } - public TempLink code(int code) { + public void code(int code) { this.code = code; - return this; } public long expiryTime() { return expiryTime; } - public TempLink expiryTime(long expiryTime) { + public void expiryTime(long expiryTime) { this.expiryTime = expiryTime; - return this; } } diff --git a/src/main/java/org/geysermc/globallinkserver/util/CommandUtils.java b/src/main/java/org/geysermc/globallinkserver/util/CommandUtils.java index c09be13..656d2ab 100644 --- a/src/main/java/org/geysermc/globallinkserver/util/CommandUtils.java +++ b/src/main/java/org/geysermc/globallinkserver/util/CommandUtils.java @@ -5,109 +5,134 @@ */ package org.geysermc.globallinkserver.util; +import com.mojang.brigadier.context.CommandContext; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.Bukkit; import org.bukkit.entity.Player; +import org.geysermc.globallinkserver.link.Link; import org.geysermc.globallinkserver.link.LinkManager; import org.geysermc.globallinkserver.link.TempLink; +@SuppressWarnings("UnstableApiUsage") public class CommandUtils { - public static void handleCommand(LinkManager linkManager, Player player, String message) { - String[] args = message.split(" "); + public CommandUtils(LinkManager linkManager) { + this.linkManager = linkManager; + } - if (args[0].equals("/linkaccount")) { - if (args.length == 2) { - int linkId = Utils.parseInt(args[1]); + LinkManager linkManager; - if (linkId <= 0) { - player.sendMessage("&cInvalid link code"); - return; - } + public int startLink(CommandContext ctx) { + Player player = getPlayer(ctx); - TempLink tempLink = linkManager.tempLinkById(linkId); + if (Utils.isLinked(player)) { + player.sendMessage("You are already linked!"); + return 0; + } - if (tempLink == null) { - player.sendMessage("&cCould not find the provided link. Is it expired?"); - return; - } + linkManager.removeTempLinkIfPresent(player); - if (Utils.isBedrockPlayer(player)) { - tempLink.bedrockId(player.getUniqueId()); - } else { - tempLink.javaId(player.getUniqueId()); - tempLink.javaUsername(player.getDisplayName()); - } + String code = String.format("%04d", linkManager.createTempLink(player)); + String otherPlatform = Utils.isBedrockPlayerId(player) ? "Java" : "Bedrock"; - 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; - } + player.sendMessage(Component.text("Please join on %s and run ".formatted(otherPlatform)) + .color(NamedTextColor.GREEN) + .append(Component.text("`/link " + code + "`", NamedTextColor.AQUA))); + return 1; + } - 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; - } - - Player javaPlayer = Bukkit.getPlayer(tempLink.javaId()); - Player bedrockPlayer = Bukkit.getPlayer(tempLink.bedrockId()); - - if (javaPlayer != null) { - javaPlayer.kickPlayer("&aYou are now successfully linked! :)"); - } - - if (bedrockPlayer != null) { - bedrockPlayer.kickPlayer("&aYou are now successfully linked! :)"); - } - }); - return; - } + public int linkWithCode(CommandContext ctx) { + int linkId = ctx.getArgument("code", Integer.class); + + Player player = getPlayer(ctx); + + if (linkId <= 0) { + player.sendMessage(Component.text("Invalid link code!").color(NamedTextColor.RED)); + return 0; + } + + TempLink tempLink = linkManager.tempLinkById(linkId); + + if (tempLink == null) { + player.sendMessage(Component.text("Could not find the provided link. Is it expired?").color(NamedTextColor.RED)); + return 0; + } - if (args.length == 1) { - linkManager.removeTempLinkIfPresent(player); + if (Utils.isBedrockPlayerId(player)) { + tempLink.bedrockId(player.getUniqueId()); + } else { + tempLink.javaId(player.getUniqueId()); + tempLink.javaUsername(player.getName()); + } - String code = String.format("%04d", linkManager.createTempLink(player)); - String otherPlatform = Utils.isBedrockPlayer(player) ? "Java" : "Bedrock"; + if (tempLink.javaId() == null || tempLink.bedrockId() == null) { + player.sendMessage(Component.text("You can only link a Java account to a Bedrock account.") + .color(NamedTextColor.RED) + .append(Component.text("Try to start the linking process again!"))); + return 0; + } - player.sendMessage("&aPlease join on " + otherPlatform + " and run `&9/linkaccount &3" + code + "&a`"); + linkManager.finaliseLink(tempLink).whenComplete((result, error) -> { + if (error != null || !result) { + if (error != null) { + error.printStackTrace(); + } + System.out.println(result); + player.sendMessage(Component.text("An unknown error occurred while linking your account. Try it again later!") + .color(NamedTextColor.RED)); 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; - } + Player javaPlayer = Bukkit.getPlayer(tempLink.javaId()); + Player bedrockPlayer = Bukkit.getPlayer(tempLink.bedrockId()); - 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"); - } - }); + if (javaPlayer != null) { + javaPlayer.kick(Component.text("You are now successfully linked! :)").color(NamedTextColor.GREEN)); + } + + if (bedrockPlayer != null) { + bedrockPlayer.kick(Component.text("You are now successfully linked! :)").color(NamedTextColor.GREEN)); + } + }); + return 1; + } + + public int unlink(CommandContext ctx) { + Player player = getPlayer(ctx); + linkManager.unlinkAccount(player).whenComplete((result, error) -> { + if (error != null) { + error.printStackTrace(); + System.out.println(result); + player.sendMessage(Component.text("An unknown error occurred while unlinking your account. Try it again later!") + .color(NamedTextColor.RED)); return; } - player.sendMessage("&cInvalid format! Use: `&9/unlinkaccount&c`"); - return; + if (result) { + player.kick(Component.text("You are successfully unlinked.").color(NamedTextColor.GREEN)); + } else { + player.kick(Component.text("You are not linked to any account!").color(NamedTextColor.RED)); + } + }); + return 1; + } + + public static Player getPlayer(CommandContext ctx) { + return (Player) ctx.getSource().getExecutor(); + } + + public int info(CommandContext ctx) { + Player player = getPlayer(ctx); + + Link link = Utils.getLink(player); + if (link == null) { + player.sendMessage(Component.text("You are not currently linked!").color(NamedTextColor.RED)); + return 0; } - player.sendMessage("&cUnknown command"); + player.sendMessage(Component.text("You are currently linked!").color(NamedTextColor.GREEN)); + return 1; } } diff --git a/src/main/java/org/geysermc/globallinkserver/util/Utils.java b/src/main/java/org/geysermc/globallinkserver/util/Utils.java index 240ac59..5476e73 100644 --- a/src/main/java/org/geysermc/globallinkserver/util/Utils.java +++ b/src/main/java/org/geysermc/globallinkserver/util/Utils.java @@ -5,20 +5,79 @@ */ package org.geysermc.globallinkserver.util; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; import org.bukkit.entity.Player; -import org.geysermc.geyser.api.GeyserApi; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.geysermc.floodgate.api.FloodgateApi; +import org.geysermc.floodgate.api.player.FloodgatePlayer; +import org.geysermc.globallinkserver.GlobalLinkServer; +import org.geysermc.globallinkserver.link.Link; + +import java.util.Map; +import java.util.Set; +import java.util.UUID; public class Utils { - public static int parseInt(String toParse) { - try { - return Integer.parseInt(toParse); - } catch (NumberFormatException exception) { - return -1; + private static final Map linkedPlayers = new Object2ObjectOpenHashMap<>(); + + public static boolean isBedrockPlayerId(Player player) { + return FloodgateApi.getInstance().isFloodgateId(player.getUniqueId()); + } + + public static boolean isLinked(Player player) { + return linkedPlayers.containsKey(player.getUniqueId()); + } + + public static @Nullable Link getLink(Player player) { + return linkedPlayers.get(player.getUniqueId()); + } + + // TODO we do not have bedrock player names! + public static Component getLinkInfo(Player player) { + Link link = getLink(player); + if (link == null) { + return null; + } + + FloodgatePlayer floodgatePlayer = FloodgateApi.getInstance().getPlayer(player.getUniqueId()); + if (floodgatePlayer == null) { + // Java player + return Component.empty(); + } else { + // Bedrock player + return Component.empty(); + } + } + + public static void processJoin(Player player) { + FloodgatePlayer floodgatePlayer = FloodgateApi.getInstance().getPlayer(player.getUniqueId()); + if (floodgatePlayer == null) { + // Not dealing with a Bedrock player - now check if this Java player has a link + GlobalLinkServer.linkManager.attemptFindJavaLink(player).whenComplete((link, throwable) -> { + if (throwable != null) { + player.sendMessage(Component.text("Failed to find Java link.").color(NamedTextColor.RED)); + throwable.printStackTrace(); + return; + } + + link.ifPresent(value -> linkedPlayers.put(player.getUniqueId(), value)); + }); + } else { + // easy + if (floodgatePlayer.isLinked()) { + linkedPlayers.put(player.getUniqueId(), new Link() + .javaUsername(player.getName()) + .javaId(player.getUniqueId()) + .bedrockId(floodgatePlayer.getJavaUniqueId())); + } } } - public static boolean isBedrockPlayer(Player player) { - return GeyserApi.api().isBedrockPlayer(player.getUniqueId()); + public static void processLeave(Player player) { + linkedPlayers.remove(player.getUniqueId()); } } 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..262fbd7 --- /dev/null +++ b/src/main/resources/config.yml @@ -0,0 +1,4 @@ +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/plugin.yml b/src/main/resources/plugin.yml index 397d717..1634e65 100644 --- a/src/main/resources/plugin.yml +++ b/src/main/resources/plugin.yml @@ -1,6 +1,14 @@ +name: GlobalLinkServer +version: '1.0' main: org.geysermc.globallinkserver.GlobalLinkServer -name: GlobalLinkServerPlugin -author: GeyserMC -website: link.geysermc.org -version: 1.0.0 -api-version: 1.13 +description: GlobalLinkServer plugin +api-version: '1.21.4' +dependencies: + server: + floodgate: + load: OMIT + required: true + geyser: + load: OMIT + required: true + join-classpath: false