/* * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * * @author GeyserMC * @link https://github.com/GeyserMC/Geyser */ package org.geysermc.geyser.platform.spigot; import com.viaversion.viaversion.api.Via; import com.viaversion.viaversion.api.data.MappingData; import com.viaversion.viaversion.api.protocol.ProtocolPathEntry; import com.viaversion.viaversion.api.protocol.version.ProtocolVersion; import io.netty.buffer.ByteBuf; import me.lucko.commodore.CommodoreProvider; import org.bukkit.Bukkit; import org.bukkit.block.data.BlockData; import org.bukkit.command.CommandMap; import org.bukkit.command.PluginCommand; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.server.ServerLoadEvent; import org.bukkit.permissions.Permission; import org.bukkit.permissions.PermissionDefault; import org.bukkit.plugin.Plugin; import org.bukkit.plugin.java.JavaPlugin; import org.geysermc.floodgate.core.skin.SkinApplier; import org.checkerframework.checker.nullness.qual.NonNull; import org.geysermc.geyser.Constants; import org.geysermc.geyser.GeyserBootstrap; import org.geysermc.geyser.GeyserImpl; import org.geysermc.geyser.adapters.spigot.SpigotAdapters; import org.geysermc.geyser.api.command.Command; import org.geysermc.geyser.api.extension.Extension; import org.geysermc.geyser.api.util.PlatformType; import org.geysermc.geyser.command.GeyserCommandManager; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.dump.BootstrapDumpInfo; import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.network.GameProtocol; import org.geysermc.geyser.ping.GeyserLegacyPingPassthrough; import org.geysermc.geyser.ping.IGeyserPingPassthrough; import org.geysermc.geyser.platform.spigot.command.GeyserBrigadierSupport; import org.geysermc.geyser.platform.spigot.command.GeyserSpigotCommandExecutor; import org.geysermc.geyser.platform.spigot.command.GeyserSpigotCommandManager; import org.geysermc.geyser.platform.spigot.world.GeyserPistonListener; import org.geysermc.geyser.platform.spigot.world.GeyserSpigotBlockPlaceListener; import org.geysermc.geyser.platform.spigot.world.manager.GeyserSpigotLegacyNativeWorldManager; import org.geysermc.geyser.platform.spigot.world.manager.GeyserSpigotNativeWorldManager; import org.geysermc.geyser.platform.spigot.world.manager.GeyserSpigotWorldManager; import org.geysermc.geyser.text.GeyserLocale; import org.geysermc.geyser.util.FileUtils; import java.io.File; import java.io.IOException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.net.SocketAddress; import java.nio.file.Path; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.UUID; import java.util.logging.Level; public class GeyserSpigotPlugin extends JavaPlugin implements GeyserBootstrap { private GeyserSpigotCommandManager geyserCommandManager; private GeyserSpigotConfiguration geyserConfig; private GeyserSpigotInjector geyserInjector; private GeyserSpigotLogger geyserLogger; private IGeyserPingPassthrough geyserSpigotPingPassthrough; private GeyserSpigotWorldManager geyserWorldManager; private GeyserImpl geyser; /** * The Minecraft server version, formatted as 1.#.# */ private String minecraftVersion; @Override public void onLoad() { onGeyserInitialize(); } @Override public void onGeyserInitialize() { GeyserLocale.init(this); try { // AvailableCommandsSerializer_v291 complains otherwise - affects at least 1.8 ByteBuf.class.getMethod("writeShortLE", int.class); // Only available in 1.13.x Class.forName("org.bukkit.event.server.ServerLoadEvent"); // We depend on this as a fallback in certain scenarios BlockData.class.getMethod("getAsString"); } catch (ClassNotFoundException | NoSuchMethodException e) { getLogger().severe("*********************************************"); getLogger().severe(""); getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server.header")); getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server.message", "1.13.2")); getLogger().severe(""); getLogger().severe("*********************************************"); Bukkit.getPluginManager().disablePlugin(this); return; } try { Class.forName("net.md_5.bungee.chat.ComponentSerializer"); } catch (ClassNotFoundException e) { if (!PaperAdventure.canSendMessageUsingComponent()) { // Prepare for Paper eventually removing Bungee chat getLogger().severe("*********************************************"); getLogger().severe(""); getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server_type.header", getServer().getName())); getLogger().severe(GeyserLocale.getLocaleStringLog("geyser.bootstrap.unsupported_server_type.message", "Paper")); getLogger().severe(""); getLogger().severe("*********************************************"); Bukkit.getPluginManager().disablePlugin(this); return; } } try { Class.forName("io.netty.util.internal.ObjectPool$ObjectCreator"); } catch (ClassNotFoundException e) { getLogger().severe("*********************************************"); getLogger().severe(""); getLogger().severe("This version of Spigot is using an outdated version of netty. Please use Paper instead!"); getLogger().severe(""); getLogger().severe("*********************************************"); Bukkit.getPluginManager().disablePlugin(this); return; } if (!loadConfig()) { return; } this.geyserLogger = GeyserPaperLogger.supported() ? new GeyserPaperLogger(this, getLogger(), geyserConfig.isDebugMode()) : new GeyserSpigotLogger(getLogger(), geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); // Turn "(MC: 1.16.4)" into 1.16.4. this.minecraftVersion = Bukkit.getServer().getVersion().split("\\(MC: ")[1].split("\\)")[0]; this.geyser = GeyserImpl.load(PlatformType.SPIGOT, this, null); } @Override public void onEnable() { if (Bukkit.getPluginManager().getPlugin("floodgate") != null) { geyserLogger.severe("WHY DO YOU HAVE FLOODGATE INSTALLED!!!!!!! REMOVE IT!!!!"); } this.geyserCommandManager = new GeyserSpigotCommandManager(geyser); this.geyserCommandManager.init(); // Because Bukkit locks its command map upon startup, we need to // add our plugin commands in onEnable, but populating the executor // can happen at any time (later in #onGeyserEnable()) CommandMap commandMap = GeyserSpigotCommandManager.getCommandMap(); for (Extension extension : this.geyserCommandManager.extensionCommands().keySet()) { // Thanks again, Bukkit try { Constructor constructor = PluginCommand.class.getDeclaredConstructor(String.class, Plugin.class); constructor.setAccessible(true); PluginCommand pluginCommand = constructor.newInstance(extension.description().id(), this); pluginCommand.setDescription("The main command for the " + extension.name() + " Geyser extension!"); commandMap.register(extension.description().id(), "geyserext", pluginCommand); } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) { this.geyserLogger.error("Failed to construct PluginCommand for extension " + extension.name(), ex); } } // Needs to be an anonymous inner class otherwise Bukkit complains about missing classes Bukkit.getPluginManager().registerEvents(new Listener() { @EventHandler public void onServerLoaded(ServerLoadEvent event) { if (event.getType() == ServerLoadEvent.LoadType.RELOAD) { geyser.setShuttingDown(false); } onGeyserEnable(); } }, this); } public void onGeyserEnable() { // Configs are loaded once early - so we can create the logger, then load extensions and finally register // extension commands in #onEnable. To ensure reloading geyser also reloads the geyser config, this exists if (GeyserImpl.getInstance().isReloading()) { if (!loadConfig()) { return; } this.geyserLogger.setDebug(this.geyserConfig.isDebugMode()); GeyserConfiguration.checkGeyserConfiguration(geyserConfig, geyserLogger); } GeyserImpl.start(); if (geyserConfig.isLegacyPingPassthrough()) { this.geyserSpigotPingPassthrough = GeyserLegacyPingPassthrough.init(geyser); } else { if (ReflectedNames.checkPaperPingEvent()) { this.geyserSpigotPingPassthrough = new GeyserPaperPingPassthrough(geyserLogger); } else if (ReflectedNames.newSpigotPingConstructorExists()) { this.geyserSpigotPingPassthrough = new GeyserSpigotPingPassthrough(geyserLogger); } else { // Can't enable one of the other options this.geyserSpigotPingPassthrough = GeyserLegacyPingPassthrough.init(geyser); } } geyserLogger.debug("Spigot ping passthrough type: " + (this.geyserSpigotPingPassthrough == null ? null : this.geyserSpigotPingPassthrough.getClass())); // Don't need to re-create the world manager/re-register commands/reinject when reloading if (GeyserImpl.getInstance().isReloading()) { return; } boolean isViaVersion = Bukkit.getPluginManager().getPlugin("ViaVersion") != null; // Check to ensure the current setup can support the protocol version Geyser uses GeyserSpigotVersionChecker.checkForSupportedProtocol(geyserLogger, isViaVersion); // We want to do this late in the server startup process to allow plugins such as ViaVersion and ProtocolLib // To do their job injecting, then connect into *that* this.geyserInjector = new GeyserSpigotInjector(isViaVersion); this.geyserInjector.initializeLocalChannel(this); if (Boolean.parseBoolean(System.getProperty("Geyser.UseDirectAdapters", "true"))) { try { String name = Bukkit.getServer().getClass().getPackage().getName(); String nmsVersion = name.substring(name.lastIndexOf('.') + 1); SpigotAdapters.registerWorldAdapter(nmsVersion); if (isViaVersion && isViaVersionNeeded()) { this.geyserWorldManager = new GeyserSpigotLegacyNativeWorldManager(this); } else { // No ViaVersion this.geyserWorldManager = new GeyserSpigotNativeWorldManager(this); } geyserLogger.debug("Using NMS adapter: " + this.geyserWorldManager.getClass() + ", " + nmsVersion); } catch (Exception e) { if (geyserConfig.isDebugMode()) { geyserLogger.debug("Error while attempting to find NMS adapter. Most likely, this can be safely ignored. :)"); e.printStackTrace(); } } } else { geyserLogger.debug("Not using NMS adapter as it is disabled via system property."); } if (this.geyserWorldManager == null) { // No NMS adapter this.geyserWorldManager = new GeyserSpigotWorldManager(this); geyserLogger.debug("Using default world manager."); } PluginCommand geyserCommand = this.getCommand("geyser"); Objects.requireNonNull(geyserCommand, "base command cannot be null"); geyserCommand.setExecutor(new GeyserSpigotCommandExecutor(geyser, geyserCommandManager.getCommands())); for (Map.Entry> entry : this.geyserCommandManager.extensionCommands().entrySet()) { Map commands = entry.getValue(); if (commands.isEmpty()) { continue; } PluginCommand command = this.getCommand(entry.getKey().description().id()); if (command == null) { continue; } command.setExecutor(new GeyserSpigotCommandExecutor(this.geyser, commands)); } // Register permissions so they appear in, for example, LuckPerms' UI // Re-registering permissions throws an error for (Map.Entry entry : geyserCommandManager.commands().entrySet()) { Command command = entry.getValue(); if (command.aliases().contains(entry.getKey())) { // Don't register aliases continue; } Bukkit.getPluginManager().addPermission(new Permission(command.permission(), GeyserLocale.getLocaleStringLog(command.description()), command.isSuggestedOpOnly() ? PermissionDefault.OP : PermissionDefault.TRUE)); } // Register permissions for extension commands for (Map.Entry> commandEntry : this.geyserCommandManager.extensionCommands().entrySet()) { for (Map.Entry entry : commandEntry.getValue().entrySet()) { Command command = entry.getValue(); if (command.aliases().contains(entry.getKey())) { // Don't register aliases continue; } if (command.permission().isBlank()) { continue; } // Avoid registering the same permission twice, e.g. for the extension help commands if (Bukkit.getPluginManager().getPermission(command.permission()) != null) { GeyserImpl.getInstance().getLogger().debug("Skipping permission " + command.permission() + " as it is already registered"); continue; } Bukkit.getPluginManager().addPermission(new Permission(command.permission(), GeyserLocale.getLocaleStringLog(command.description()), command.isSuggestedOpOnly() ? PermissionDefault.OP : PermissionDefault.TRUE)); } } Bukkit.getPluginManager().addPermission(new Permission(Constants.UPDATE_PERMISSION, "Whether update notifications can be seen", PermissionDefault.OP)); // Events cannot be unregistered - re-registering results in duplicate firings GeyserSpigotBlockPlaceListener blockPlaceListener = new GeyserSpigotBlockPlaceListener(geyser, this.geyserWorldManager); Bukkit.getServer().getPluginManager().registerEvents(blockPlaceListener, this); Bukkit.getServer().getPluginManager().registerEvents(new GeyserPistonListener(geyser, this.geyserWorldManager), this); Bukkit.getServer().getPluginManager().registerEvents(new GeyserSpigotUpdateListener(), this); boolean brigadierSupported = CommodoreProvider.isSupported(); geyserLogger.debug("Brigadier supported? " + brigadierSupported); if (brigadierSupported) { GeyserBrigadierSupport.loadBrigadier(this, geyserCommand); } } @Override public void onGeyserDisable() { if (geyser != null) { geyser.disable(); } } @Override public void onGeyserShutdown() { if (geyser != null) { geyser.shutdown(); } if (geyserInjector != null) { geyserInjector.shutdown(); } } @Override public void onDisable() { this.onGeyserShutdown(); } @Override public GeyserSpigotConfiguration getGeyserConfig() { return geyserConfig; } @Override public GeyserSpigotLogger getGeyserLogger() { return geyserLogger; } @Override public GeyserCommandManager getGeyserCommandManager() { return this.geyserCommandManager; } @Override public IGeyserPingPassthrough getGeyserPingPassthrough() { return geyserSpigotPingPassthrough; } @Override public WorldManager getWorldManager() { return this.geyserWorldManager; } @Override public Path getConfigFolder() { return getDataFolder().toPath(); } @Override public BootstrapDumpInfo getDumpInfo() { return new GeyserSpigotDumpInfo(); } @Override public String getMinecraftServerVersion() { return this.minecraftVersion; } @Override public SocketAddress getSocketAddress() { return this.geyserInjector.getServerSocketAddress(); } @Override public SkinApplier createSkinApplier() { // return new SpigotSkinApplier(new SpigotVersionSpecificMethods(this), this); return null; } /** * @return the server version before ViaVersion finishes initializing */ public ProtocolVersion getServerProtocolVersion() { return ProtocolVersion.getClosest(this.minecraftVersion); } /** * This function should not run unless ViaVersion is installed on the server. * * @return true if there is any block mappings difference between the server and client. */ private boolean isViaVersionNeeded() { ProtocolVersion serverVersion = getServerProtocolVersion(); List protocolList = Via.getManager().getProtocolManager().getProtocolPath(GameProtocol.getJavaProtocolVersion(), serverVersion.getVersion()); if (protocolList == null) { // No translation needed! return false; } for (int i = protocolList.size() - 1; i >= 0; i--) { MappingData mappingData = protocolList.get(i).protocol().getMappingData(); if (mappingData != null) { return true; } } // All mapping data is null, which means client and server block states are the same return false; } @NonNull @Override public String getServerBindAddress() { return Bukkit.getIp(); } @Override public int getServerPort() { return Bukkit.getPort(); } @Override public boolean testFloodgatePluginPresent() { if (Bukkit.getPluginManager().getPlugin("floodgate") != null) { geyserConfig.loadFloodgate(this); return true; } return false; } @SuppressWarnings("BooleanMethodIsAlwaysInverted") private boolean loadConfig() { // This is manually done instead of using Bukkit methods to save the config because otherwise comments get removed try { if (!getDataFolder().exists()) { //noinspection ResultOfMethodCallIgnored getDataFolder().mkdir(); } File configFile = FileUtils.fileOrCopiedFromResource(new File(getDataFolder(), "config.yml"), "config.yml", (x) -> x.replaceAll("generateduuid", UUID.randomUUID().toString()), this); this.geyserConfig = FileUtils.loadConfig(configFile, GeyserSpigotConfiguration.class); } catch (IOException ex) { getLogger().log(Level.SEVERE, GeyserLocale.getLocaleStringLog("geyser.config.failed"), ex); ex.printStackTrace(); Bukkit.getPluginManager().disablePlugin(this); return false; } return true; } }