From 8627787ea991f66b368e93648c79abf051820fe9 Mon Sep 17 00:00:00 2001 From: ImDaBigBoss <67973871+ImDaBigBoss@users.noreply.github.com> Date: Mon, 10 Jan 2022 18:45:26 +0100 Subject: [PATCH] Added basic extension loading --- .../java/org/geysermc/geyser/GeyserImpl.java | 5 + .../geysermc/geyser/extension/Extension.java | 50 ++++ .../extension/ExtensionClassLoader.java | 97 +++++++ .../extension/ExtensionDescription.java | 94 +++++++ .../geyser/extension/ExtensionLoader.java | 173 +++++++++++++ .../geyser/extension/ExtensionManager.java | 243 ++++++++++++++++++ .../geyser/extension/GeyserExtension.java | 176 +++++++++++++ .../InvalidDescriptionException.java | 40 +++ .../exception/InvalidExtensionException.java | 40 +++ 9 files changed, 918 insertions(+) create mode 100644 core/src/main/java/org/geysermc/geyser/extension/Extension.java create mode 100644 core/src/main/java/org/geysermc/geyser/extension/ExtensionClassLoader.java create mode 100644 core/src/main/java/org/geysermc/geyser/extension/ExtensionDescription.java create mode 100644 core/src/main/java/org/geysermc/geyser/extension/ExtensionLoader.java create mode 100644 core/src/main/java/org/geysermc/geyser/extension/ExtensionManager.java create mode 100644 core/src/main/java/org/geysermc/geyser/extension/GeyserExtension.java create mode 100644 core/src/main/java/org/geysermc/geyser/extension/exception/InvalidDescriptionException.java create mode 100644 core/src/main/java/org/geysermc/geyser/extension/exception/InvalidExtensionException.java diff --git a/core/src/main/java/org/geysermc/geyser/GeyserImpl.java b/core/src/main/java/org/geysermc/geyser/GeyserImpl.java index eaadd15fa..abfc7bb83 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.ExtensionManager; import org.geysermc.geyser.level.WorldManager; import org.geysermc.geyser.network.ConnectorServerEventHandler; import org.geysermc.geyser.pack.ResourcePack; @@ -161,6 +162,8 @@ public class GeyserImpl implements GeyserApi { ResourcePack.loadPacks(); + ExtensionManager.init(); + if (platformType != PlatformType.STANDALONE && config.getRemote().getAddress().equals("auto")) { // Set the remote address to localhost since that is where we are always connecting try { @@ -444,6 +447,8 @@ public class GeyserImpl implements GeyserApi { newsHandler.shutdown(); this.getCommandManager().getCommands().clear(); + ExtensionManager.getExtensionManager().disableExtensions(); + bootstrap.getGeyserLogger().info(GeyserLocale.getLocaleStringLog("geyser.core.shutdown.done")); } diff --git a/core/src/main/java/org/geysermc/geyser/extension/Extension.java b/core/src/main/java/org/geysermc/geyser/extension/Extension.java new file mode 100644 index 000000000..a911e56ad --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/extension/Extension.java @@ -0,0 +1,50 @@ +/* + * 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 java.io.File; +import java.io.InputStream; + +public interface Extension { + void onLoad(); + void onEnable(); + void onDisable(); + + boolean isEnabled(); + boolean isDisabled(); + + File getDataFolder(); + ExtensionDescription getDescription(); + String getName(); + + InputStream getResource(String filename); + void saveResource(String filename, boolean replace); + + GeyserImpl getGeyser(); + ClassLoader getClassLoader(); + ExtensionLoader getExtensionLoader(); +} diff --git a/core/src/main/java/org/geysermc/geyser/extension/ExtensionClassLoader.java b/core/src/main/java/org/geysermc/geyser/extension/ExtensionClassLoader.java new file mode 100644 index 000000000..3261adc01 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/extension/ExtensionClassLoader.java @@ -0,0 +1,97 @@ +/* + * 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.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 ExtensionClassLoader extends URLClassLoader { + private ExtensionLoader loader; + private Map classes = new HashMap<>(); + public GeyserExtension extension; + + public ExtensionClassLoader(ExtensionLoader 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.getMain(), true, this); + } catch (ClassNotFoundException ex) { + throw new InvalidExtensionException("Class " + description.getMain() + " not found, extension cannot be loaded", ex); + } + + Class extensionClass; + try { + extensionClass = jarClass.asSubclass(GeyserExtension.class); + } catch (ClassCastException ex) { + throw new InvalidExtensionException("Main class " + description.getMain() + " 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("net.minecraft.")) { + throw new ClassNotFoundException(name); + } + Class result = classes.get(name); + if(result == null) { + if(checkGlobal) { + result = loader.getClassByName(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/ExtensionDescription.java b/core/src/main/java/org/geysermc/geyser/extension/ExtensionDescription.java new file mode 100644 index 000000000..9f714dc11 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/extension/ExtensionDescription.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.extension; + +import org.geysermc.geyser.extension.exception.InvalidDescriptionException; +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +import java.util.*; + +public class ExtensionDescription { + private String name; + private String main; + private List api; + private String version; + private final List authors = new ArrayList<>(); + + public ExtensionDescription(String yamlString) throws InvalidDescriptionException { + DumperOptions dumperOptions = new DumperOptions(); + dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + Yaml yaml = new Yaml(dumperOptions); + this.loadMap(yaml.loadAs(yamlString, 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"); + } + 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 List) { + this.api = (List) api; + } else { + List list = new ArrayList<>(); + list.add((String) api); + this.api = list; + } + + if (yamlMap.containsKey("author")) { + this.authors.add((String) yamlMap.get("author")); + } + + if (yamlMap.containsKey("authors")) { + this.authors.addAll((Collection) yamlMap.get("authors")); + } + } + + public String getName() { + return this.name; + } + + public String getMain() { + return this.main; + } + + public List getAPIVersions() { + return api; + } + + public String getVersion() { + return this.version; + } + + public List getAuthors() { + return this.authors; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/extension/ExtensionLoader.java b/core/src/main/java/org/geysermc/geyser/extension/ExtensionLoader.java new file mode 100644 index 000000000..15795c2c5 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/extension/ExtensionLoader.java @@ -0,0 +1,173 @@ +/* + * 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.extension.exception.InvalidDescriptionException; +import org.geysermc.geyser.extension.exception.InvalidExtensionException; +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 ExtensionLoader { + private final Map classes = new HashMap<>(); + private final Map classLoaders = new HashMap<>(); + + 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 ExtensionDescription description; + try { + description = getExtensionDescription(file); + } catch (InvalidDescriptionException e) { + throw new InvalidExtensionException(e); + } + + final File parentFile = file.getParentFile(); + final File dataFolder = new File(parentFile, description.getName()); + 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.getName() + "!"); + } + + final ExtensionClassLoader loader; + try { + loader = new ExtensionClassLoader(this, getClass().getClassLoader(), description, file); + } catch (Throwable e) { + throw new InvalidExtensionException(e); + } + classLoaders.put(description.getName(), loader); + + setup(loader.extension, description, dataFolder, file); + return loader.extension; + } + + private void setup(GeyserExtension extension, ExtensionDescription description, File dataFolder, File file) { + extension.init(GeyserImpl.getInstance(), description, dataFolder, file, this); + extension.onLoad(); + } + + public ExtensionDescription getExtensionDescription(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); + + InputStreamReader reader = new InputStreamReader(stream); + StringBuilder builder = new StringBuilder(); + String temp; + BufferedReader bufferedReader = new BufferedReader(reader); + temp = bufferedReader.readLine(); + while (temp != null) { + if (builder.length() != 0) { + builder.append("\n"); + } + builder.append(temp); + temp = bufferedReader.readLine(); + } + + return new ExtensionDescription(builder.toString()); + } 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[] getExtensionFilters() { + return new Pattern[] { Pattern.compile("^.+\\.jar$") }; + } + + public Class getClassByName(final String name) throws ClassNotFoundException{ + Class clazz = classes.get(name); + try { + for(ExtensionClassLoader loader : classLoaders.values()) { + try { + clazz = loader.findClass(name,false); + } catch(NullPointerException e) { + } + } + return clazz; + } catch(NullPointerException s) { + return null; + } + } + + public void setClass(String name, final Class clazz) { + if(!classes.containsKey(name)) { + classes.put(name,clazz); + } + } + + protected void removeClass(String name) { + Class clazz = classes.remove(name); + } + + public void enableExtension(Extension extension) { + if (extension instanceof GeyserExtension) { + if(!extension.isEnabled()) { + GeyserImpl.getInstance().getLogger().info("Enabled extension " + extension.getDescription().getName()); + ((GeyserExtension) extension).setEnabled(true); + } + } + } + + public void disableExtension(Extension extension) { + if (extension instanceof GeyserExtension) { + if(extension.isEnabled()) { + GeyserImpl.getInstance().getLogger().info("Disabled extension " + extension.getDescription().getName()); + ((GeyserExtension) extension).setEnabled(false); + } + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/extension/ExtensionManager.java b/core/src/main/java/org/geysermc/geyser/extension/ExtensionManager.java new file mode 100644 index 000000000..ad757d499 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/extension/ExtensionManager.java @@ -0,0 +1,243 @@ +/* + * 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 java.io.File; +import java.io.FilenameFilter; +import java.lang.reflect.Constructor; +import java.util.*; +import java.util.regex.Pattern; + +public class ExtensionManager { + private static ExtensionManager extensionManager = null; + + protected Map extensions = new LinkedHashMap<>(); + protected Map fileAssociations = new HashMap<>(); + + public static void init() { + GeyserImpl.getInstance().getLogger().info("Loading extensions..."); + extensionManager = new ExtensionManager(); + extensionManager.registerInterface(ExtensionLoader.class); + extensionManager.loadExtensions(new File("extensions")); + GeyserImpl.getInstance().getLogger().info("Loaded " + extensionManager.extensions.size() + " extensions."); + + for (Extension extension : extensionManager.getExtensions().values()) { + if (!extension.isEnabled()) { + extensionManager.enableExtension(extension); + } + } + } + + public static ExtensionManager getExtensionManager() { + return extensionManager; + } + + public Extension 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) { + ExtensionLoader instance; + + if (ExtensionLoader.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.getExtensionFilters(); + + synchronized (this) { + for (Pattern pattern : patterns) { + fileAssociations.put(pattern, instance); + } + } + } + + public GeyserExtension loadExtension(File file, Map loaders) { + for (ExtensionLoader loader : (loaders == null ? this.fileAssociations : loaders).values()) { + for (Pattern pattern : loader.getExtensionFilters()) { + if (pattern.matcher(file.getName()).matches()) { + try { + ExtensionDescription description = loader.getExtensionDescription(file); + if (description != null) { + GeyserExtension extension = loader.loadExtension(file); + + if (extension != null) { + this.extensions.put(extension.getDescription().getName(), extension); + + return extension; + } + } + } catch (Exception e) { + GeyserImpl.getInstance().getLogger().error("Could not load extension", e); + return null; + } + } + } + } + + return null; + } + + public Map loadExtensions(File dictionary) { + if (GeyserImpl.VERSION.equalsIgnoreCase("dev")) { + GeyserImpl.getInstance().getLogger().error("Cannot load extensions in a development environment, aborting extension loading"); + return new HashMap<>(); + } + if (!GeyserImpl.VERSION.contains(".")) { + GeyserImpl.getInstance().getLogger().error("Something went wrong with the Geyser version number, aborting extension loading"); + return new HashMap<>(); + } + + if (!dictionary.exists()) { + dictionary.mkdir(); + } + if (!dictionary.isDirectory()) { + return new HashMap<>(); + } + + Map extensions = new LinkedHashMap<>(); + Map loadedExtensions = new LinkedHashMap<>(); + + for (final ExtensionLoader loader : this.fileAssociations.values()) { + for (File file : dictionary.listFiles((dir, name) -> { + for (Pattern pattern : loader.getExtensionFilters()) { + if (pattern.matcher(name).matches()) { + return true; + } + } + return false; + })) { + if (file.isDirectory()) { + continue; + } + + try { + ExtensionDescription description = loader.getExtensionDescription(file); + if (description != null) { + String name = description.getName(); + + if (extensions.containsKey(name) || this.getExtension(name) != null) { + GeyserImpl.getInstance().getLogger().warning("Found duplicate extension '" + name + "', ignoring '" + file.getName() + "'"); + continue; + } + + boolean compatible = false; + + for (String version : description.getAPIVersions()) { + try { + //Check the format: majorVersion.minorVersion.patch + if (!Pattern.matches("^[0-9]+\\.[0-9]+\\.[0-9]+$", version)) { + throw new IllegalArgumentException(); + } + } catch (NullPointerException | IllegalArgumentException e) { + GeyserImpl.getInstance().getLogger().error("Could't load extension " + name + ": Wrong API format"); + continue; + } + + String[] versionArray = version.split("\\."); + String[] apiVersion = GeyserImpl.VERSION.split("\\."); + + //Completely different API version + if (!Objects.equals(Integer.valueOf(versionArray[0]), Integer.valueOf(apiVersion[0]))) { + GeyserImpl.getInstance().getLogger().error("Couldn't load extension " + name + ": Wrong API version, current version: " + 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("Couldn't load extension " + name + ": Wrong API version, current version: " + apiVersion[0] + "." + apiVersion[1]); + continue; + } + + compatible = true; + break; + } + + if (!compatible) { + GeyserImpl.getInstance().getLogger().error("Couldn't load extension " + name +": Incompatible API version"); + } + + extensions.put(name, file); + loadedExtensions.put(name, this.loadExtension(file, this.fileAssociations)); + } + } catch (Exception e) { + GeyserImpl.getInstance().getLogger().error("Couldn't load " +file.getName()+ " in folder " + dictionary + ": ", e); + } + } + } + + return loadedExtensions; + } + + public void enableExtension(Extension extension) { + if (!extension.isEnabled()) { + try { + extension.getExtensionLoader().enableExtension(extension); + } catch (Exception e) { + GeyserImpl.getInstance().getLogger().error("Error enabling extension " + extension.getName() + ": ", e); + this.disableExtension(extension); + } + } + } + + public void disableExtension(Extension extension) { + if (extension.isEnabled()) { + try { + extension.getExtensionLoader().disableExtension(extension); + } catch (Exception e) { + GeyserImpl.getInstance().getLogger().error("Error disabling extension " + extension.getName() + ": ", e); + } + } + } + + public void disableExtensions() { + for (Extension extension : this.getExtensions().values()) { + this.disableExtension(extension); + } + } +} diff --git a/core/src/main/java/org/geysermc/geyser/extension/GeyserExtension.java b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtension.java new file mode 100644 index 000000000..c6d7b6f10 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/extension/GeyserExtension.java @@ -0,0 +1,176 @@ +/* + * 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 java.io.*; +import java.net.URL; +import java.net.URLConnection; + +public class GeyserExtension implements Extension { + private boolean initialized = false; + private boolean enabled = false; + private File file = null; + private File dataFolder = null; + private ClassLoader classLoader = null; + private GeyserImpl geyser = null; + private ExtensionLoader loader; + private ExtensionDescription description = null; + + @Override + public void onLoad() { + } + + @Override + public void onEnable() { + } + + @Override + public void onDisable() { + } + + @Override + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean value) { + if (this.enabled != value) { + this.enabled = value; + if (this.enabled) { + onEnable(); + } else { + onDisable(); + } + } + } + + @Override + public boolean isDisabled() { + return !this.enabled; + } + + @Override + public File getDataFolder() { + return this.dataFolder; + } + + @Override + public ExtensionDescription getDescription() { + return this.description; + } + + @Override + public String getName() { + return this.description.getName(); + } + + public void init(GeyserImpl geyser, ExtensionDescription description, File dataFolder, File file, ExtensionLoader loader) { + if (!this.initialized) { + this.initialized = true; + this.file = file; + this.dataFolder = dataFolder; + this.classLoader = this.getClass().getClassLoader(); + this.geyser = geyser; + this.loader = loader; + this.description = description; + } + } + + @Override + 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; + } + } + + @Override + 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.geyser.getLogger().warning("Could not save " + outFile.getName() + " to " + outFile + " because " + outFile.getName() + " already exists."); + } + } catch (IOException ex) { + this.geyser.getLogger().severe("Could not save " + outFile.getName() + " to " + outFile, ex); + } + } + + @Override + public GeyserImpl getGeyser() { + return this.geyser; + } + + @Override + public ClassLoader getClassLoader() { + return this.classLoader; + } + + @Override + public ExtensionLoader getExtensionLoader() { + return this.loader; + } +} diff --git a/core/src/main/java/org/geysermc/geyser/extension/exception/InvalidDescriptionException.java b/core/src/main/java/org/geysermc/geyser/extension/exception/InvalidDescriptionException.java new file mode 100644 index 000000000..0764dc1d1 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/extension/exception/InvalidDescriptionException.java @@ -0,0 +1,40 @@ +/* + * 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.exception; + +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/core/src/main/java/org/geysermc/geyser/extension/exception/InvalidExtensionException.java b/core/src/main/java/org/geysermc/geyser/extension/exception/InvalidExtensionException.java new file mode 100644 index 000000000..f29f5d280 --- /dev/null +++ b/core/src/main/java/org/geysermc/geyser/extension/exception/InvalidExtensionException.java @@ -0,0 +1,40 @@ +/* + * 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.exception; + +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); + } +}