From 254f0da03cdeb213dfea36eae0559e76852b6ae7 Mon Sep 17 00:00:00 2001 From: Camotoy <20743703+Camotoy@users.noreply.github.com> Date: Mon, 24 Oct 2022 13:21:02 -0400 Subject: [PATCH] Fabric improvements Mainly in commands - the old permissions file no longer needs to exist. --- bootstrap/fabric/build.gradle.kts | 3 + .../platform/fabric/GeyserFabricLogger.java | 14 +++- .../platform/fabric/GeyserFabricMod.java | 74 +++++-------------- ...s.java => GeyserFabricUpdateListener.java} | 31 +++----- .../fabric/command/FabricCommandSender.java | 26 ++++--- .../command/GeyserFabricCommandExecutor.java | 35 ++------- .../world/GeyserFabricWorldManager.java | 13 +--- .../fabric/src/main/resources/fabric.mod.json | 3 +- .../fabric/src/main/resources/permissions.yml | 13 ---- .../org/geysermc/geyser/text/ChatColor.java | 18 +---- gradle/libs.versions.toml | 9 ++- 11 files changed, 81 insertions(+), 158 deletions(-) rename bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/{GeyserFabricPermissions.java => GeyserFabricUpdateListener.java} (65%) delete mode 100644 bootstrap/fabric/src/main/resources/permissions.yml diff --git a/bootstrap/fabric/build.gradle.kts b/bootstrap/fabric/build.gradle.kts index dac791173..02f24bba5 100644 --- a/bootstrap/fabric/build.gradle.kts +++ b/bootstrap/fabric/build.gradle.kts @@ -19,6 +19,9 @@ dependencies { // Fabric API. This is technically optional, but you probably want it anyway. modImplementation(libs.fabric.api) + // This should be in the libs TOML, but something about modImplementation AND include just doesn't work + include(modImplementation("me.lucko", "fabric-permissions-api", "0.2-SNAPSHOT")) + // PSA: Some older mods, compiled on Loom 0.2.1, might have outdated Maven POMs. // You may need to force-disable transitiveness on them. diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricLogger.java b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricLogger.java index a6ee77f41..180197f2d 100644 --- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricLogger.java +++ b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricLogger.java @@ -25,12 +25,14 @@ package org.geysermc.geyser.platform.fabric; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.geysermc.geyser.GeyserLogger; +import org.geysermc.geyser.text.ChatColor; public class GeyserFabricLogger implements GeyserLogger { - private final Logger logger = LogManager.getLogger("geyser-fabric"); private boolean debug; @@ -69,6 +71,16 @@ public class GeyserFabricLogger implements GeyserLogger { logger.info(message); } + @Override + public void sendMessage(Component message) { + // As of Java Edition 1.19.2, Fabric's console doesn't natively support legacy format + String flattened = LegacyComponentSerializer.legacySection().serialize(message); + // Add the reset at the end, or else format will persist... forever. + // https://cdn.discordapp.com/attachments/573909525132738590/1033904509170225242/unknown.png + String text = ChatColor.toANSI(flattened) + ChatColor.ANSI_RESET; + info(text); + } + @Override public void debug(String message) { if (debug) { diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricMod.java b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricMod.java index 792e16788..1e9543ff8 100644 --- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricMod.java +++ b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricMod.java @@ -29,6 +29,7 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder; import net.fabricmc.api.EnvType; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; +import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.ModContainer; import net.minecraft.commands.CommandSourceStack; @@ -55,29 +56,21 @@ import org.geysermc.geyser.util.FileUtils; import org.jetbrains.annotations.Nullable; import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; import java.util.*; public class GeyserFabricMod implements ModInitializer, GeyserBootstrap { - private static GeyserFabricMod instance; private boolean reloading; - private GeyserImpl connector; + private GeyserImpl geyser; private ModContainer mod; private Path dataFolder; private MinecraftServer server; - /** - * Commands that don't require any permission level to ran - */ - private List playerCommands; - private final List commandExecutors = new ArrayList<>(); - private GeyserCommandManager geyserCommandManager; private GeyserFabricConfiguration geyserConfig; private GeyserFabricLogger geyserLogger; @@ -111,8 +104,6 @@ public class GeyserFabricMod implements ModInitializer, GeyserBootstrap { File configFile = FileUtils.fileOrCopiedFromResource(dataFolder.resolve("config.yml").toFile(), "config.yml", (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this); this.geyserConfig = FileUtils.loadConfig(configFile, GeyserFabricConfiguration.class); - File permissionsFile = fileOrCopiedFromResource(dataFolder.resolve("permissions.yml").toFile(), "permissions.yml"); - this.playerCommands = Arrays.asList(FileUtils.loadConfig(permissionsFile, GeyserFabricPermissions.class).getCommands()); } catch (IOException ex) { LogManager.getLogger("geyser-fabric").error(GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex); ex.printStackTrace(); @@ -123,10 +114,14 @@ public class GeyserFabricMod implements ModInitializer, GeyserBootstrap { GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); + this.geyser = GeyserImpl.load(PlatformType.FABRIC, this); + if (server == null) { // Server has yet to start // Register onDisable so players are properly kicked ServerLifecycleEvents.SERVER_STOPPING.register((server) -> onDisable()); + + ServerPlayConnectionEvents.JOIN.register((handler, $, $$) -> GeyserFabricUpdateListener.onPlayReady(handler)); } else { // Server has started and this is a reload startGeyser(this.server); @@ -170,38 +165,37 @@ public class GeyserFabricMod implements ModInitializer, GeyserBootstrap { geyserConfig.loadFloodgate(this, floodgate.orElse(null)); - this.connector = GeyserImpl.load(PlatformType.FABRIC, this); - GeyserImpl.start(); // shrug + GeyserImpl.start(); - this.geyserPingPassthrough = GeyserLegacyPingPassthrough.init(connector); + this.geyserPingPassthrough = GeyserLegacyPingPassthrough.init(geyser); - this.geyserCommandManager = new GeyserCommandManager(connector); + this.geyserCommandManager = new GeyserCommandManager(geyser); this.geyserCommandManager.init(); this.geyserWorldManager = new GeyserFabricWorldManager(server); // Start command building // Set just "geyser" as the help command - GeyserFabricCommandExecutor helpExecutor = new GeyserFabricCommandExecutor(connector, - (GeyserCommand) connector.commandManager().getCommands().get("help"), !playerCommands.contains("help")); - commandExecutors.add(helpExecutor); + GeyserFabricCommandExecutor helpExecutor = new GeyserFabricCommandExecutor(geyser, + (GeyserCommand) geyser.commandManager().getCommands().get("help")); LiteralArgumentBuilder builder = Commands.literal("geyser").executes(helpExecutor); // Register all subcommands as valid - for (Map.Entry command : connector.commandManager().getCommands().entrySet()) { - GeyserFabricCommandExecutor executor = new GeyserFabricCommandExecutor(connector, (GeyserCommand) command.getValue(), - !playerCommands.contains(command.getKey())); - commandExecutors.add(executor); - builder.then(Commands.literal(command.getKey()).executes(executor)); + for (Map.Entry command : geyser.commandManager().getCommands().entrySet()) { + GeyserFabricCommandExecutor executor = new GeyserFabricCommandExecutor(geyser, (GeyserCommand) command.getValue()); + builder.then(Commands.literal(command.getKey()) + .executes(executor) + // Could also test for Bedrock but depending on when this is called it may backfire + .requires(executor::testPermission)); } server.getCommands().getDispatcher().register(builder); } @Override public void onDisable() { - if (connector != null) { - connector.shutdown(); - connector = null; + if (geyser != null) { + geyser.shutdown(); + geyser = null; } if (!reloading) { this.server = null; @@ -267,34 +261,6 @@ public class GeyserFabricMod implements ModInitializer, GeyserBootstrap { this.reloading = reloading; } - private File fileOrCopiedFromResource(File file, String name) throws IOException { - if (!file.exists()) { - //noinspection ResultOfMethodCallIgnored - file.createNewFile(); - FileOutputStream fos = new FileOutputStream(file); - InputStream input = getResource(name); - - byte[] bytes = new byte[input.available()]; - - //noinspection ResultOfMethodCallIgnored - input.read(bytes); - - for(char c : new String(bytes).toCharArray()) { - fos.write(c); - } - - fos.flush(); - input.close(); - fos.close(); - } - - return file; - } - - public List getCommandExecutors() { - return commandExecutors; - } - public static GeyserFabricMod getInstance() { return instance; } diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricPermissions.java b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricUpdateListener.java similarity index 65% rename from bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricPermissions.java rename to bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricUpdateListener.java index a625f6d1f..1ea69cbe2 100644 --- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricPermissions.java +++ b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/GeyserFabricUpdateListener.java @@ -25,26 +25,19 @@ package org.geysermc.geyser.platform.fabric; -import com.fasterxml.jackson.annotation.JsonIgnore; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; -import com.fasterxml.jackson.annotation.JsonProperty; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import org.geysermc.geyser.Constants; +import org.geysermc.geyser.platform.fabric.command.FabricCommandSender; +import org.geysermc.geyser.util.VersionCheckUtils; -/** - * A class outline of the permissions.yml file - */ -@JsonIgnoreProperties(ignoreUnknown = true) -public class GeyserFabricPermissions { +public final class GeyserFabricUpdateListener { + public static void onPlayReady(ServerGamePacketListenerImpl handler) { + if (Permissions.check(handler.player, Constants.UPDATE_PERMISSION, 2)) { + VersionCheckUtils.checkForGeyserUpdate(() -> new FabricCommandSender(handler.player.createCommandSourceStack())); + } + } - /** - * The minimum permission level a command source must have in order for it to run commands that are restricted - */ - @JsonIgnore - public static final int RESTRICTED_MIN_LEVEL = 2; - - @JsonProperty("commands") - private String[] commands; - - public String[] getCommands() { - return this.commands; + private GeyserFabricUpdateListener() { } } diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/command/FabricCommandSender.java b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/command/FabricCommandSender.java index 0bb171e6b..5973e04f1 100644 --- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/command/FabricCommandSender.java +++ b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/command/FabricCommandSender.java @@ -25,12 +25,13 @@ package org.geysermc.geyser.platform.fabric.command; +import me.lucko.fabric.api.permissions.v0.Permissions; +import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.minecraft.commands.CommandSourceStack; import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerPlayer; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.command.GeyserCommandSource; -import org.geysermc.geyser.platform.fabric.GeyserFabricMod; import org.geysermc.geyser.text.ChatColor; import javax.annotation.Nonnull; @@ -57,22 +58,23 @@ public class FabricCommandSender implements GeyserCommandSource { } } + @Override + public void sendMessage(net.kyori.adventure.text.Component message) { + if (source.getEntity() instanceof ServerPlayer player) { + String decoded = GsonComponentSerializer.gson().serialize(message); + player.displayClientMessage(Component.Serializer.fromJson(decoded), false); + return; + } + GeyserCommandSource.super.sendMessage(message); + } + @Override public boolean isConsole() { return !(source.getEntity() instanceof ServerPlayer); } @Override - public boolean hasPermission(String s) { - // Mostly copied from fabric's world manager since the method there takes a GeyserSession - - // Workaround for our commands because fabric doesn't have native permissions - for (GeyserFabricCommandExecutor executor : GeyserFabricMod.getInstance().getCommandExecutors()) { - if (executor.getCommand().permission().equals(s)) { - return executor.canRun(source); - } - } - - return false; + public boolean hasPermission(String permission) { + return Permissions.check(source, permission); } } diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/command/GeyserFabricCommandExecutor.java b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/command/GeyserFabricCommandExecutor.java index f691cd49e..7600e4136 100644 --- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/command/GeyserFabricCommandExecutor.java +++ b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/command/GeyserFabricCommandExecutor.java @@ -27,12 +27,12 @@ package org.geysermc.geyser.platform.fabric.command; import com.mojang.brigadier.Command; import com.mojang.brigadier.context.CommandContext; +import me.lucko.fabric.api.permissions.v0.Permissions; import net.minecraft.commands.CommandSourceStack; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.command.GeyserCommand; import org.geysermc.geyser.command.GeyserCommandExecutor; import org.geysermc.geyser.platform.fabric.GeyserFabricMod; -import org.geysermc.geyser.platform.fabric.GeyserFabricPermissions; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.text.ChatColor; import org.geysermc.geyser.text.GeyserLocale; @@ -40,27 +40,15 @@ import org.geysermc.geyser.text.GeyserLocale; import java.util.Collections; public class GeyserFabricCommandExecutor extends GeyserCommandExecutor implements Command { - private final GeyserCommand command; - /** - * Whether the command requires an OP permission level of 2 or greater - */ - private final boolean requiresPermission; - public GeyserFabricCommandExecutor(GeyserImpl connector, GeyserCommand command, boolean requiresPermission) { + public GeyserFabricCommandExecutor(GeyserImpl connector, GeyserCommand command) { super(connector, Collections.singletonMap(command.name(), command)); this.command = command; - this.requiresPermission = requiresPermission; } - /** - * Determine whether or not a command source is allowed to run a given executor. - * - * @param source The command source attempting to run the command - * @return True if the command source is allowed to - */ - public boolean canRun(CommandSourceStack source) { - return !requiresPermission() || source.hasPermission(GeyserFabricPermissions.RESTRICTED_MIN_LEVEL); + public boolean testPermission(CommandSourceStack source) { + return Permissions.check(source, command.permission(), command.isSuggestedOpOnly() ? 2 : 0); } @Override @@ -68,8 +56,8 @@ public class GeyserFabricCommandExecutor extends GeyserCommandExecutor implement CommandSourceStack source = (CommandSourceStack) context.getSource(); FabricCommandSender sender = new FabricCommandSender(source); GeyserSession session = getGeyserSession(sender); - if (!canRun(source)) { - sender.sendMessage(GeyserLocale.getLocaleStringLog("geyser.bootstrap.command.permission_fail")); + if (!testPermission(source)) { + sender.sendMessage(ChatColor.RED + GeyserLocale.getPlayerLocaleString("geyser.bootstrap.command.permission_fail", sender.locale())); return 0; } if (this.command.name().equals("reload")) { @@ -83,15 +71,4 @@ public class GeyserFabricCommandExecutor extends GeyserCommandExecutor implement command.execute(session, sender, new String[0]); return 0; } - - public GeyserCommand getCommand() { - return command; - } - - /** - * Returns whether the command requires permission level of {@link GeyserFabricPermissions#RESTRICTED_MIN_LEVEL} or higher to be ran - */ - public boolean requiresPermission() { - return requiresPermission; - } } diff --git a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/world/GeyserFabricWorldManager.java b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/world/GeyserFabricWorldManager.java index 0746198f3..eb4f61c67 100644 --- a/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/world/GeyserFabricWorldManager.java +++ b/bootstrap/fabric/src/main/java/org/geysermc/geyser/platform/fabric/world/GeyserFabricWorldManager.java @@ -29,6 +29,7 @@ import com.nukkitx.math.vector.Vector3i; import com.nukkitx.nbt.NbtMap; import com.nukkitx.nbt.NbtMapBuilder; import com.nukkitx.nbt.NbtType; +import me.lucko.fabric.api.permissions.v0.Permissions; import net.minecraft.core.BlockPos; import net.minecraft.nbt.ListTag; import net.minecraft.server.MinecraftServer; @@ -39,8 +40,6 @@ import net.minecraft.world.item.WrittenBookItem; import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.entity.LecternBlockEntity; import org.geysermc.geyser.level.GeyserWorldManager; -import org.geysermc.geyser.platform.fabric.GeyserFabricMod; -import org.geysermc.geyser.platform.fabric.command.GeyserFabricCommandExecutor; import org.geysermc.geyser.session.GeyserSession; import org.geysermc.geyser.translator.inventory.LecternInventoryTranslator; import org.geysermc.geyser.util.BlockEntityUtils; @@ -124,14 +123,8 @@ public class GeyserFabricWorldManager extends GeyserWorldManager { @Override public boolean hasPermission(GeyserSession session, String permission) { - // Workaround for our commands because fabric doesn't have native permissions - for (GeyserFabricCommandExecutor executor : GeyserFabricMod.getInstance().getCommandExecutors()) { - if (executor.getCommand().permission().equals(permission)) { - return executor.canRun(getPlayer(session).createCommandSourceStack()); - } - } - - return false; + ServerPlayer player = getPlayer(session); + return Permissions.check(player, permission); } private ServerPlayer getPlayer(GeyserSession session) { diff --git a/bootstrap/fabric/src/main/resources/fabric.mod.json b/bootstrap/fabric/src/main/resources/fabric.mod.json index ee23dd06d..98a410950 100644 --- a/bootstrap/fabric/src/main/resources/fabric.mod.json +++ b/bootstrap/fabric/src/main/resources/fabric.mod.json @@ -25,6 +25,7 @@ "depends": { "fabricloader": ">=0.14.8", "fabric": "*", - "minecraft": ">=1.19" + "minecraft": ">=1.19", + "fabric-permissions-api-v0": "*" } } diff --git a/bootstrap/fabric/src/main/resources/permissions.yml b/bootstrap/fabric/src/main/resources/permissions.yml deleted file mode 100644 index ae20447ed..000000000 --- a/bootstrap/fabric/src/main/resources/permissions.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Uncomment any commands that you wish to be run by clients -# Commented commands require an OP permission of 2 or greater -commands: - - help - - advancements - - statistics - - settings - - offhand - - tooltips -# - list -# - reload -# - version -# - dump diff --git a/core/src/main/java/org/geysermc/geyser/text/ChatColor.java b/core/src/main/java/org/geysermc/geyser/text/ChatColor.java index d39c0d696..49178f033 100644 --- a/core/src/main/java/org/geysermc/geyser/text/ChatColor.java +++ b/core/src/main/java/org/geysermc/geyser/text/ChatColor.java @@ -26,6 +26,7 @@ package org.geysermc.geyser.text; public class ChatColor { + public static final String ANSI_RESET = (char) 0x1b + "[0m"; public static final char ESCAPE = '§'; public static final String BLACK = ESCAPE + "0"; @@ -64,7 +65,7 @@ public class ChatColor { string = string.replace(ITALIC, (char) 0x1b + "[3m"); string = string.replace(UNDERLINE, (char) 0x1b + "[4m"); string = string.replace(STRIKETHROUGH, (char) 0x1b + "[9m"); - string = string.replace(RESET, (char) 0x1b + "[0m"); + string = string.replace(RESET, ANSI_RESET); string = string.replace(BLACK, (char) 0x1b + "[0;30m"); string = string.replace(DARK_BLUE, (char) 0x1b + "[0;34m"); string = string.replace(DARK_GREEN, (char) 0x1b + "[0;32m"); @@ -83,19 +84,4 @@ public class ChatColor { string = string.replace(WHITE, (char) 0x1b + "[37;1m"); return string; } - - public String translateAlternateColorCodes(char color, String message) { - return message.replace(color, ESCAPE); - } - - /** - * Remove all colour formatting tags from a message - * - * @param message Message to remove colour tags from - * - * @return The sanitised message - */ - public static String stripColors(String message) { - return message = message.replaceAll("(&([a-fk-or0-9]))","").replaceAll("(§([a-fk-or0-9]))","").replaceAll("s/\\x1b\\[[0-9;]*[a-zA-Z]//g",""); - } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b16812b6f..4e2efe00b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,6 +26,9 @@ commodore = "2.2" bungeecord = "a7c6ede" velocity = "3.0.0" sponge = "8.0.0" +fabric-minecraft = "1.19.1" +fabric-loader = "0.14.8" +fabric-api = "0.58.5+1.19.1" [libraries] jackson-annotations = { group = "com.fasterxml.jackson.core", name = "jackson-annotations", version.ref = "jackson" } @@ -63,9 +66,9 @@ paper-api = { group = "io.papermc.paper", name = "paper-api", version.ref = "pap paper-mojangapi = { group = "io.papermc.paper", name = "paper-mojangapi", version.ref = "paper" } # check these on https://modmuss50.me/fabric.html -fabric-minecraft = { group = "com.mojang", name = "minecraft", version = "1.19.1" } -fabric-loader = { group = "net.fabricmc", name = "fabric-loader", version = "0.14.8" } -fabric-api = { group = "net.fabricmc.fabric-api", name = "fabric-api", version = "0.58.5+1.19.1" } +fabric-minecraft = { group = "com.mojang", name = "minecraft", version.ref = "fabric-minecraft" } +fabric-loader = { group = "net.fabricmc", name = "fabric-loader", version.ref = "fabric-loader" } +fabric-api = { group = "net.fabricmc.fabric-api", name = "fabric-api", version.ref = "fabric-api" } adapters-spigot = { group = "org.geysermc.geyser.adapters", name = "spigot-all", version.ref = "adapters" } bungeecord-proxy = { group = "com.github.SpigotMC.BungeeCord", name = "bungeecord-proxy", version.ref = "bungeecord" }