diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java new file mode 100644 index 000000000..487df3926 --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionDescription.java @@ -0,0 +1,68 @@ +/* + * 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.api.extension; + +import java.util.List; + +/** + * This is the Geyser extension description + */ +public interface ExtensionDescription { + /** + * Gets the extension's name + * + * @return the extension's name + */ + String name(); + + /** + * Gets the extension's main class + * + * @return the extension's main class + */ + String main(); + + /** + * Gets the extension's api version + * + * @return the extension's api version + */ + String apiVersion(); + + /** + * Gets the extension's description + * + * @return the extension's description + */ + String version(); + + /** + * Gets the extension's authors + * + * @return the extension's authors + */ + List authors(); +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionLoader.java b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionLoader.java new file mode 100644 index 000000000..1301493d5 --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionLoader.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019-2022 GeyserMC. http://geysermc.org + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * @author GeyserMC + * @link https://github.com/GeyserMC/Geyser + */ + +package org.geysermc.geyser.api.extension; + +import org.geysermc.geyser.api.extension.exception.InvalidDescriptionException; +import org.geysermc.geyser.api.extension.exception.InvalidExtensionException; +import java.io.File; + +/** + * The extension loader is responsible for loading, unloading, enabling and disabling extensions + */ +public interface ExtensionLoader { + /** + * Loads an extension from a given file + * + * @param file the file to load the extension from + * @return the loaded extension + * @throws InvalidExtensionException + */ + GeyserExtension loadExtension(File file) throws InvalidExtensionException; + + /** + * Gets an extension's description from a given file + * + * @param file the file to get the description from + * @return the extension's description + * @throws InvalidDescriptionException + */ + ExtensionDescription extensionDescription(File file) throws InvalidDescriptionException; + + /** + * Gets a class by its name from the extension's classloader + * + * @param name the name of the class + * @return the class + * @throws ClassNotFoundException + */ + Class classByName(final String name) throws ClassNotFoundException; + + /** + * Enables an extension + * + * @param extension the extension to enable + */ + void enableExtension(GeyserExtension extension); + + /** + * Disables an extension + * + * @param extension the extension to disable + */ + void disableExtension(GeyserExtension extension); +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionLogger.java b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionLogger.java new file mode 100644 index 000000000..17e108455 --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/ExtensionLogger.java @@ -0,0 +1,94 @@ +/* + * 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.api.extension; + +/** + * This is the Geyser extension logger + */ +public interface ExtensionLogger { + /** + * Get the logger prefix + * + * @return the logger prefix + */ + String prefix(); + + /** + * Logs a severe message to console + * + * @param message the message to log + */ + void severe(String message); + + /** + * Logs a severe message and an exception to console + * + * @param message the message to log + * @param error the error to throw + */ + void severe(String message, Throwable error); + + /** + * Logs an error message to console + * + * @param message the message to log + */ + void error(String message); + + /** + * Logs an error message and an exception to console + * + * @param message the message to log + * @param error the error to throw + */ + void error(String message, Throwable error); + + /** + * Logs a warning message to console + * + * @param message the message to log + */ + void warning(String message); + + /** + * Logs an info message to console + * + * @param message the message to log + */ + void info(String message); + + /** + * Logs a debug message to console + * + * @param message the message to log + */ + void debug(String message); + + /** + * If debug is enabled for this logger + */ + boolean isDebug(); +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/extension/GeyserExtension.java b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/GeyserExtension.java new file mode 100644 index 000000000..bd53bafd3 --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/GeyserExtension.java @@ -0,0 +1,235 @@ +/* + * 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.api.extension; + +import org.geysermc.api.GeyserApiBase; +import java.io.*; +import java.net.URL; +import java.net.URLConnection; + +/** + * This class is to be extended by a Geyser extension + */ +public class GeyserExtension { + private boolean initialized = false; + private boolean enabled = false; + private File file = null; + private File dataFolder = null; + private ClassLoader classLoader = null; + private ExtensionLoader loader = null; + private ExtensionLogger logger = null; + private ExtensionDescription description = null; + private GeyserApiBase api = null; + + /** + * Called when the extension is loaded + */ + public void onLoad() { + + } + + /** + * Called when the extension is enabled + */ + public void onEnable() { + + } + + /** + * Called when the extension is disabled + */ + public void onDisable() { + + } + + /** + * Gets if the extension is enabled + * + * @return true if the extension is enabled + */ + public boolean isEnabled() { + return this.enabled; + } + + /** + * Enables or disables the extension + */ + public void setEnabled(boolean value) { + if (this.enabled != value) { + this.enabled = value; + if (this.enabled) { + onEnable(); + } else { + onDisable(); + } + } + } + + /** + * Gets the extension's data folder + * + * @return the extension's data folder + */ + public File dataFolder() { + return this.dataFolder; + } + + /** + * Gets the extension's description + * + * @return the extension's description + */ + public ExtensionDescription description() { + return this.description; + } + + /** + * Gets the extension's name + * + * @return the extension's name + */ + public String name() { + return this.description.name(); + } + + public void init(GeyserApiBase api, ExtensionLoader loader, ExtensionLogger logger, ExtensionDescription description, File dataFolder, File file) { + if (!this.initialized) { + this.initialized = true; + this.file = file; + this.dataFolder = dataFolder; + this.classLoader = this.getClass().getClassLoader(); + this.loader = loader; + this.logger = logger; + this.description = description; + this.api = api; + } + } + + /** + * Gets a resource from the extension jar file + * + * @param filename the file name + * @return the input stream + */ + public InputStream getResource(String filename) { + if (filename == null) { + throw new IllegalArgumentException("Filename cannot be null"); + } + + try { + URL url = this.classLoader.getResource(filename); + + if (url == null) { + return null; + } + + URLConnection connection = url.openConnection(); + connection.setUseCaches(false); + return connection.getInputStream(); + } catch (IOException ex) { + return null; + } + } + + /** + * Saves a resource from the extension jar file to the extension's data folder + * + * @param filename the file name + * @param replace whether to replace the file if it already exists + */ + public void saveResource(String filename, boolean replace) { + if (filename == null || filename.equals("")) { + throw new IllegalArgumentException("ResourcePath cannot be null or empty"); + } + + filename = filename.replace('\\', '/'); + InputStream in = getResource(filename); + if (in == null) { + throw new IllegalArgumentException("The embedded resource '" + filename + "' cannot be found in " + file); + } + + File outFile = new File(dataFolder, filename); + int lastIndex = filename.lastIndexOf('/'); + File outDir = new File(dataFolder, filename.substring(0, Math.max(lastIndex, 0))); + + if (!outDir.exists()) { + outDir.mkdirs(); + } + + try { + if (!outFile.exists() || replace) { + OutputStream out = new FileOutputStream(outFile); + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + out.close(); + in.close(); + } else { + this.logger.warning("Could not save " + outFile.getName() + " to " + outFile + " because " + outFile.getName() + " already exists."); + } + } catch (IOException ex) { + this.logger.severe("Could not save " + outFile.getName() + " to " + outFile, ex); + } + } + + /** + * Gets the extension's class loader + * + * @return the extension's class loader + */ + public ClassLoader classLoader() { + return this.classLoader; + } + + /** + * Gets the extension's loader + * + * @return the extension's loader + */ + public ExtensionLoader extensionLoader() { + return this.loader; + } + + /** + * Gets the extension's logger + * + * @return the extension's logger + */ + public ExtensionLogger logger() { + return this.logger; + } + + /** + * Gets the {@link GeyserApiBase} instance + * + * @return the {@link GeyserApiBase} instance + */ + public GeyserApiBase geyserApi() { + return this.api; + } +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidDescriptionException.java b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidDescriptionException.java new file mode 100644 index 000000000..1fe88e9e9 --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidDescriptionException.java @@ -0,0 +1,43 @@ +/* + * 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.api.extension.exception; + +/** + * Thrown when an extension's description is invalid. + */ +public class InvalidDescriptionException extends Exception { + public InvalidDescriptionException(Throwable cause) { + super(cause); + } + + public InvalidDescriptionException(String message) { + super(message); + } + + public InvalidDescriptionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/api/geyser/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidExtensionException.java b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidExtensionException.java new file mode 100644 index 000000000..7fb6b6922 --- /dev/null +++ b/api/geyser/src/main/java/org/geysermc/geyser/api/extension/exception/InvalidExtensionException.java @@ -0,0 +1,43 @@ +/* + * 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.api.extension.exception; + +/** + * Thrown when an extension is invalid. + */ +public class InvalidExtensionException extends Exception { + public InvalidExtensionException(Throwable cause) { + super(cause); + } + + public InvalidExtensionException(String message) { + super(message); + } + + public InvalidExtensionException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index f63e222cc..2fbcbaddd 100644 --- a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java +++ b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java @@ -52,6 +52,7 @@ import org.geysermc.geyser.api.GeyserApi; import org.geysermc.geyser.command.CommandManager; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.entity.EntityDefinitions; +import org.geysermc.geyser.extension.GeyserExtensionManager; import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.network.ConnectorServerEventHandler; import org.geysermc.geyser.pack.ResourcePack; @@ -122,6 +123,8 @@ public class GeyserImpl implements GeyserApi { private final PlatformType platformType; private final GeyserBootstrap bootstrap; + private final GeyserExtensionManager extensionManager; + private Metrics metrics; private static GeyserImpl instance; @@ -154,6 +157,9 @@ public class GeyserImpl implements GeyserApi { MessageTranslator.init(); MinecraftLocale.init(); + extensionManager = new GeyserExtensionManager(); + extensionManager.init(); + start(); GeyserConfiguration config = bootstrap.getGeyserConfig(); @@ -197,6 +203,8 @@ public class GeyserImpl implements GeyserApi { ResourcePack.loadPacks(); + extensionManager.enableExtensions(); + if (platformType != PlatformType.STANDALONE && config.getRemote().getAddress().equals("auto")) { // Set the remote address to localhost since that is where we are always connecting try { @@ -457,6 +465,8 @@ public class GeyserImpl implements GeyserApi { ResourcePack.PACKS.clear(); + extensionManager.disableExtensions(); + bootstrap.getGeyserLogger().info(GeyserLocale.getLocaleStringLog("geyser.core.shutdown.done")); } @@ -508,6 +518,10 @@ public class GeyserImpl implements GeyserApi { return bootstrap.getWorldManager(); } + public GeyserExtensionManager getExtensionManager() { + return extensionManager; + } + public static GeyserImpl getInstance() { return instance; } diff --git a/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java b/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java index 2734c7443..3377f7ee5 100644 --- a/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java +++ b/core/src/main/java/org/geysermc/geyser/dump/DumpInfo.java @@ -36,6 +36,8 @@ import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import lombok.AllArgsConstructor; import lombok.Getter; import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.extension.GeyserExtension; +import org.geysermc.geyser.extension.GeyserExtensionManager; import org.geysermc.geyser.text.AsteriskSerializer; import org.geysermc.geyser.configuration.GeyserConfiguration; import org.geysermc.geyser.network.MinecraftProtocol; @@ -54,10 +56,7 @@ import java.net.InetSocketAddress; import java.net.Socket; import java.net.UnknownHostException; import java.nio.file.Paths; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; +import java.util.*; import java.util.stream.Collectors; @Getter @@ -76,6 +75,7 @@ public class DumpInfo { private LogsInfo logsInfo; private final BootstrapDumpInfo bootstrapInfo; private final FlagsInfo flagsInfo; + private final List extensionInfo; public DumpInfo(boolean addLog) { this.versionInfo = new VersionInfo(); @@ -125,6 +125,11 @@ public class DumpInfo { this.bootstrapInfo = GeyserImpl.getInstance().getBootstrap().getDumpInfo(); this.flagsInfo = new FlagsInfo(); + + this.extensionInfo = new ArrayList<>(); + for (GeyserExtension extension : GeyserImpl.getInstance().getExtensionManager().getExtensions().values()) { + this.extensionInfo.add(new ExtensionInfo(extension.isEnabled(), extension.name(), extension.description().version(), extension.description().apiVersion(), extension.description().main(), extension.description().authors())); + } } @Getter @@ -277,4 +282,15 @@ public class DumpInfo { this.flags = ManagementFactory.getRuntimeMXBean().getInputArguments(); } } + + @Getter + @AllArgsConstructor + public static class ExtensionInfo { + public boolean enabled; + public String name; + public String version; + public String apiVersion; + public String main; + public List authors; + } } diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java new file mode 100644 index 000000000..67363a40f --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java @@ -0,0 +1,99 @@ +/* + * 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.extension; + +import org.geysermc.geyser.api.extension.ExtensionDescription; +import org.geysermc.geyser.api.extension.GeyserExtension; +import org.geysermc.geyser.api.extension.exception.InvalidExtensionException; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class GeyserExtensionClassLoader extends URLClassLoader { + private final GeyserExtensionLoader loader; + private final Map classes = new HashMap<>(); + public GeyserExtension extension; + + public GeyserExtensionClassLoader(GeyserExtensionLoader loader, ClassLoader parent, ExtensionDescription description, File file) throws InvalidExtensionException, MalformedURLException { + super(new URL[] { file.toURI().toURL() }, parent); + this.loader = loader; + + try { + Class jarClass; + try { + jarClass = Class.forName(description.main(), true, this); + } catch (ClassNotFoundException ex) { + throw new InvalidExtensionException("Class " + description.main() + " not found, extension cannot be loaded", ex); + } + + Class extensionClass; + try { + extensionClass = jarClass.asSubclass(GeyserExtension.class); + } catch (ClassCastException ex) { + throw new InvalidExtensionException("Main class " + description.main() + " should extends GeyserExtension, but extends " + jarClass.getSuperclass().getSimpleName(), ex); + } + + extension = extensionClass.newInstance(); + } catch (IllegalAccessException ex) { + throw new InvalidExtensionException("No public constructor", ex); + } catch (InstantiationException ex) { + throw new InvalidExtensionException("Abnormal extension type", ex); + } + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + return this.findClass(name, true); + } + + protected Class findClass(String name, boolean checkGlobal) throws ClassNotFoundException { + if (name.startsWith("org.geysermc.geyser.") || name.startsWith("org.geysermc.connector.") || name.startsWith("org.geysermc.platform.") || name.startsWith("org.geysermc.floodgate.") || name.startsWith("org.geysermc.api.") || name.startsWith("org.geysermc.processor.") || name.startsWith("net.minecraft.")) { + throw new ClassNotFoundException(name); + } + Class result = classes.get(name); + if (result == null) { + if (checkGlobal) { + result = loader.classByName(name); + } + if (result == null) { + result = super.findClass(name); + if (result != null) { + loader.setClass(name, result); + } + } + classes.put(name, result); + } + return result; + } + + Set getClasses() { + return classes.keySet(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java new file mode 100644 index 000000000..f8a3d9bbe --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java @@ -0,0 +1,102 @@ +/* + * 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.extension; + +import org.geysermc.geyser.api.extension.exception.InvalidDescriptionException; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; +import java.io.InputStream; +import java.util.*; + +public class GeyserExtensionDescription implements org.geysermc.geyser.api.extension.ExtensionDescription { + private String name; + private String main; + private String api; + private String version; + private final List authors = new ArrayList<>(); + + public GeyserExtensionDescription(InputStream inputStream) throws InvalidDescriptionException { + DumperOptions dumperOptions = new DumperOptions(); + dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + Yaml yaml = new Yaml(dumperOptions); + this.loadMap(yaml.loadAs(inputStream, LinkedHashMap.class)); + } + + private void loadMap(Map yamlMap) throws InvalidDescriptionException { + this.name = ((String) yamlMap.get("name")).replaceAll("[^A-Za-z0-9 _.-]", ""); + if (this.name.equals("")) { + throw new InvalidDescriptionException("Invalid extension name, cannot be empty"); + } + this.name = this.name.replace(" ", "_"); + this.version = String.valueOf(yamlMap.get("version")); + this.main = (String) yamlMap.get("main"); + + Object api = yamlMap.get("api"); + if (api instanceof String) { + this.api = (String) api; + } else { + this.api = "0.0.0"; + throw new InvalidDescriptionException("Invalid api version format, should be a string: major.minor.patch"); + } + + if (yamlMap.containsKey("author")) { + this.authors.add((String) yamlMap.get("author")); + } + + if (yamlMap.containsKey("authors")) { + try { + this.authors.addAll((Collection) yamlMap.get("authors")); + } catch (Exception e) { + throw new InvalidDescriptionException("Invalid authors format, should be a list of strings", e); + } + } + } + + @Override + public String name() { + return this.name; + } + + @Override + public String main() { + return this.main; + } + + @Override + public String apiVersion() { + return api; + } + + @Override + public String version() { + return this.version; + } + + @Override + public List authors() { + return this.authors; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java new file mode 100644 index 000000000..f029eb797 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java @@ -0,0 +1,165 @@ +/* + * 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.extension; + +import org.geysermc.api.Geyser; +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.extension.ExtensionLoader; +import org.geysermc.geyser.api.extension.GeyserExtension; +import org.geysermc.geyser.api.extension.exception.InvalidDescriptionException; +import org.geysermc.geyser.api.extension.exception.InvalidExtensionException; +import org.geysermc.geyser.text.GeyserLocale; +import java.io.*; +import java.util.HashMap; +import java.util.Map; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.regex.Pattern; + +public class GeyserExtensionLoader implements ExtensionLoader { + private final Map classes = new HashMap<>(); + private final Map classLoaders = new HashMap<>(); + + @Override + public GeyserExtension loadExtension(File file) throws InvalidExtensionException { + if (file == null) { + throw new InvalidExtensionException("File is null"); + } + + if (!file.exists()) { + throw new InvalidExtensionException(new FileNotFoundException(file.getPath()) + " does not exist"); + } + + final GeyserExtensionDescription description; + try { + description = extensionDescription(file); + } catch (InvalidDescriptionException e) { + throw new InvalidExtensionException(e); + } + + final File parentFile = file.getParentFile(); + final File dataFolder = new File(parentFile, description.name()); + if (dataFolder.exists() && !dataFolder.isDirectory()) { + throw new InvalidExtensionException("The folder " + dataFolder.getPath() + " is not a directory and is the data folder for the extension " + description.name() + "!"); + } + + final GeyserExtensionClassLoader loader; + try { + loader = new GeyserExtensionClassLoader(this, getClass().getClassLoader(), description, file); + } catch (Throwable e) { + throw new InvalidExtensionException(e); + } + classLoaders.put(description.name(), loader); + + setup(loader.extension, description, dataFolder, file); + return loader.extension; + } + + private void setup(GeyserExtension extension, GeyserExtensionDescription description, File dataFolder, File file) { + GeyserExtensionLogger logger = new GeyserExtensionLogger(GeyserImpl.getInstance().getLogger(), description.name()); + extension.init(Geyser.api(), this, logger, description, dataFolder, file); + extension.onLoad(); + } + + @Override + public GeyserExtensionDescription extensionDescription(File file) throws InvalidDescriptionException { + JarFile jarFile = null; + InputStream stream = null; + + try { + jarFile = new JarFile(file); + + JarEntry descriptionEntry = jarFile.getJarEntry("extension.yml"); + if (descriptionEntry == null) { + throw new InvalidDescriptionException(new FileNotFoundException("extension.yml") + " does not exist in the jar file!"); + } + + stream = jarFile.getInputStream(descriptionEntry); + return new GeyserExtensionDescription(stream); + } catch (IOException e) { + throw new InvalidDescriptionException(e); + } finally { + if (jarFile != null) { + try { + jarFile.close(); + } catch (IOException e) { + } + } + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + } + } + } + } + + public Pattern[] extensionFilters() { + return new Pattern[] { Pattern.compile("^.+\\.jar$") }; + } + + @Override + public Class classByName(final String name) throws ClassNotFoundException{ + Class clazz = classes.get(name); + try { + for(GeyserExtensionClassLoader loader : classLoaders.values()) { + try { + clazz = loader.findClass(name,false); + } catch(NullPointerException e) { + } + } + return clazz; + } catch(NullPointerException s) { + return null; + } + } + + void setClass(String name, final Class clazz) { + if (!classes.containsKey(name)) { + classes.put(name,clazz); + } + } + + void removeClass(String name) { + classes.remove(name); + } + + @Override + public void enableExtension(GeyserExtension extension) { + if (!extension.isEnabled()) { + GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.enable.success", extension.description().name())); + extension.setEnabled(true); + } + } + + @Override + public void disableExtension(GeyserExtension extension) { + if (extension.isEnabled()) { + GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.disable.success", extension.description().name())); + extension.setEnabled(false); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLogger.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLogger.java new file mode 100644 index 000000000..fe23417f8 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLogger.java @@ -0,0 +1,88 @@ +/* + * 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.extension; + +import org.geysermc.geyser.GeyserLogger; +import org.geysermc.geyser.api.extension.ExtensionLogger; + +public class GeyserExtensionLogger implements ExtensionLogger { + private final GeyserLogger logger; + private final String loggerPrefix; + + public GeyserExtensionLogger(GeyserLogger logger, String prefix) { + this.logger = logger; + this.loggerPrefix = prefix; + } + + @Override + public String prefix() { + return this.loggerPrefix; + } + + private String addPrefix(String message) { + return "[" + this.loggerPrefix + "] " + message; + } + + @Override + public void severe(String message) { + this.logger.severe(this.addPrefix(message)); + } + + @Override + public void severe(String message, Throwable error) { + this.logger.severe(this.addPrefix(message), error); + } + + @Override + public void error(String message) { + this.logger.error(this.addPrefix(message)); + } + + @Override + public void error(String message, Throwable error) { + this.logger.error(this.addPrefix(message), error); + } + + @Override + public void warning(String message) { + this.logger.warning(this.addPrefix(message)); + } + + @Override + public void info(String message) { + this.logger.info(this.addPrefix(message)); + } + + @Override + public void debug(String message) { + this.logger.debug(this.addPrefix(message)); + } + + @Override + public boolean isDebug() { + return this.logger.isDebug(); + } +} diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionManager.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionManager.java new file mode 100644 index 000000000..169053182 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionManager.java @@ -0,0 +1,229 @@ +/* + * 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.extension; + +import org.geysermc.geyser.GeyserImpl; +import org.geysermc.geyser.api.extension.ExtensionDescription; +import org.geysermc.geyser.api.extension.GeyserExtension; +import org.geysermc.geyser.text.GeyserLocale; +import java.io.File; +import java.lang.reflect.Constructor; +import java.util.*; +import java.util.regex.Pattern; + +public class GeyserExtensionManager { + protected Map extensions = new LinkedHashMap<>(); + protected Map fileAssociations = new HashMap<>(); + + public void init() { + GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.load.loading")); + + this.registerInterface(GeyserExtensionLoader.class); + this.loadExtensions(new File("extensions")); + + GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.load.done", this.extensions.size())); + } + + public GeyserExtension getExtension(String name) { + if (this.extensions.containsKey(name)) { + return this.extensions.get(name); + } + return null; + } + + public Map getExtensions() { + return this.extensions; + } + + public void registerInterface(Class loader) { + GeyserExtensionLoader instance; + + if (GeyserExtensionLoader.class.isAssignableFrom(loader)) { + Constructor constructor; + + try { + constructor = loader.getConstructor(); + instance = constructor.newInstance(); + } catch (NoSuchMethodException ex) { // This should never happen + String className = loader.getName(); + + throw new IllegalArgumentException("Class " + className + " does not have a public constructor", ex); + } catch (Exception ex) { // This should never happen + throw new IllegalArgumentException("Unexpected exception " + ex.getClass().getName() + " while attempting to construct a new instance of " + loader.getName(), ex); + } + } else { + throw new IllegalArgumentException("Class " + loader.getName() + " does not implement interface ExtensionLoader"); + } + + Pattern[] patterns = instance.extensionFilters(); + + synchronized (this) { + for (Pattern pattern : patterns) { + fileAssociations.put(pattern, instance); + } + } + } + + public GeyserExtension loadExtension(File file, Map loaders) { + for (GeyserExtensionLoader loader : (loaders == null ? this.fileAssociations : loaders).values()) { + for (Pattern pattern : loader.extensionFilters()) { + if (pattern.matcher(file.getName()).matches()) { + try { + ExtensionDescription description = loader.extensionDescription(file); + if (description != null) { + GeyserExtension extension = loader.loadExtension(file); + + if (extension != null) { + this.extensions.put(extension.description().name(), extension); + + return extension; + } + } + } catch (Exception e) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed"), e); + return null; + } + } + } + } + + return null; + } + + public Map loadExtensions(File dictionary) { + if (GeyserImpl.VERSION.equalsIgnoreCase("dev")) { // If your IDE says this is always true, ignore it, it isn't. + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_dev_environment")); + return new HashMap<>(); + } + if (!GeyserImpl.VERSION.contains(".")) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_version_number")); + return new HashMap<>(); + } + + String[] apiVersion = GeyserImpl.VERSION.split("\\."); + + if (!dictionary.exists()) { + dictionary.mkdir(); + } + if (!dictionary.isDirectory()) { + return new HashMap<>(); + } + + Map extensions = new LinkedHashMap<>(); + Map loadedExtensions = new LinkedHashMap<>(); + + for (final GeyserExtensionLoader loader : this.fileAssociations.values()) { + for (File file : dictionary.listFiles((dir, name) -> { + for (Pattern pattern : loader.extensionFilters()) { + if (pattern.matcher(name).matches()) { + return true; + } + } + return false; + })) { + if (file.isDirectory()) { + continue; + } + + try { + ExtensionDescription description = loader.extensionDescription(file); + if (description != null) { + String name = description.name(); + + if (extensions.containsKey(name) || this.getExtension(name) != null) { + GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.extensions.load.duplicate", name, file.getName())); + continue; + } + + try { + //Check the format: majorVersion.minorVersion.patch + if (!Pattern.matches("^[0-9]+\\.[0-9]+\\.[0-9]+$", description.apiVersion())) { + throw new IllegalArgumentException(); + } + } catch (NullPointerException | IllegalArgumentException e) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_format", name, apiVersion[0] + "." + apiVersion[1])); + continue; + } + + String[] versionArray = description.apiVersion().split("\\."); + + //Completely different API version + if (!Objects.equals(Integer.valueOf(versionArray[0]), Integer.valueOf(apiVersion[0]))) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, apiVersion[0] + "." + apiVersion[1])); + continue; + } + + //If the extension requires new API features, being backwards compatible + if (Integer.parseInt(versionArray[1]) > Integer.parseInt(apiVersion[1])) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, apiVersion[0] + "." + apiVersion[1])); + continue; + } + + extensions.put(name, file); + loadedExtensions.put(name, this.loadExtension(file, this.fileAssociations)); + } + } catch (Exception e) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", file.getName(), dictionary.getAbsolutePath()), e); + } + } + } + + return loadedExtensions; + } + + public void enableExtension(GeyserExtension extension) { + if (!extension.isEnabled()) { + try { + extension.extensionLoader().enableExtension(extension); + } catch (Exception e) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.enable.failed", extension.name()), e); + this.disableExtension(extension); + } + } + } + + public void disableExtension(GeyserExtension extension) { + if (extension.isEnabled()) { + try { + extension.extensionLoader().disableExtension(extension); + } catch (Exception e) { + GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.disable.failed", extension.name()), e); + } + } + } + + public void enableExtensions() { + for (GeyserExtension extension : this.getExtensions().values()) { + this.enableExtension(extension); + } + } + + public void disableExtensions() { + for (GeyserExtension extension : this.getExtensions().values()) { + this.disableExtension(extension); + } + } +} diff --git a/core/src/main/resources/languages b/core/src/main/resources/languages index bdee0d0f3..94c185193 160000 --- a/core/src/main/resources/languages +++ b/core/src/main/resources/languages @@ -1 +1 @@ -Subproject commit bdee0d0f3f8a1271cd001f0bd0d672d0010be1db +Subproject commit 94c1851931f2319a7e7f42c2fe9066b78235bc39