diff --git a/README.md b/README.md index 3f900b2a27..419924bb1b 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,9 @@ Chunky uses the following 3rd party libraries: The library is covered by the Apache License, version 2.0. See the file `licenses/Apache-2.0.txt` for the full license text. See the file `licenses/commons-math.txt` for the copyright notices. +- **Apache Maven Artifact by the Apache Software Foundation** + The library is covered by the Apache License, version 2.0. + See the file `licenses/Apache-2.0.txt` for the full license text. - **FastUtil by Sebastiano Vigna** FastUtil is covered by Apache License, version 2.0. See the file `licenses/Apache-2.0.txt` for the full license text. diff --git a/chunky/build.gradle b/chunky/build.gradle index f7fa7824d6..5c553dadfb 100644 --- a/chunky/build.gradle +++ b/chunky/build.gradle @@ -17,6 +17,7 @@ dependencies { implementation 'org.apache.commons:commons-math3:3.2' implementation 'com.google.code.gson:gson:2.9.0' implementation 'org.lz4:lz4-java:1.8.0' + implementation 'org.apache.maven:maven-artifact:3.9.9' implementation project(':lib') } diff --git a/chunky/lib/maven-artifact-3.9.9.jar b/chunky/lib/maven-artifact-3.9.9.jar new file mode 100644 index 0000000000..b014207d3f Binary files /dev/null and b/chunky/lib/maven-artifact-3.9.9.jar differ diff --git a/chunky/src/java/se/llbit/chunky/main/Chunky.java b/chunky/src/java/se/llbit/chunky/main/Chunky.java index 2ed96cbf65..63de8da212 100644 --- a/chunky/src/java/se/llbit/chunky/main/Chunky.java +++ b/chunky/src/java/se/llbit/chunky/main/Chunky.java @@ -23,10 +23,10 @@ import se.llbit.chunky.block.MinecraftBlockProvider; import se.llbit.chunky.block.legacy.LegacyMinecraftBlockProvider; import se.llbit.chunky.main.CommandLineOptions.Mode; -import se.llbit.chunky.plugin.ContextMenuTransformer; -import se.llbit.chunky.plugin.PluginApi; -import se.llbit.chunky.plugin.ChunkyPlugin; -import se.llbit.chunky.plugin.TabTransformer; +import se.llbit.chunky.plugin.*; +import se.llbit.chunky.plugin.loader.PluginManager; +import se.llbit.chunky.plugin.loader.JarPluginLoader; +import se.llbit.chunky.plugin.manifest.PluginManifest; import se.llbit.chunky.renderer.*; import se.llbit.chunky.renderer.RenderManager; import se.llbit.chunky.renderer.export.PictureExportFormat; @@ -43,7 +43,6 @@ import se.llbit.chunky.ui.render.RenderControlsTabTransformer; import se.llbit.chunky.world.MaterialStore; import se.llbit.json.JsonArray; -import se.llbit.json.JsonValue; import se.llbit.log.ConsoleReceiver; import se.llbit.log.Level; import se.llbit.log.Log; @@ -55,11 +54,10 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; +import java.util.*; import java.util.concurrent.ForkJoinPool; +import java.util.stream.Collectors; +import java.util.stream.Stream; /** * Chunky is a Minecraft mapping and rendering tool created byJesper Öqvist (jesper@llbit.se). @@ -265,35 +263,31 @@ private void loadPlugins() { } Path pluginsPath = pluginsDirectory.toPath(); JsonArray plugins = PersistentSettings.getPlugins(); - Set loadedPlugins = new HashSet<>(); - for (JsonValue value : plugins) { - String jarName = value.asString(""); - if (!jarName.isEmpty()) { - Log.info("Loading plugin: " + value); - try { - ChunkyPlugin - .load(pluginsPath.resolve(jarName).toRealPath().toFile(), (plugin, manifest) -> { - CreditsController.addPlugin(manifest); - String pluginName = manifest.get("name").asString(""); - if (loadedPlugins.contains(pluginName)) { - Log.warnf( - "Multiple plugins with the same name (\"%s\") are enabled. Loading multiple versions of the same plugin can lead to strange behavior.", - pluginName); - } - loadedPlugins.add(pluginName); - try { - plugin.attach(this); - } catch (Throwable t) { - Log.error("Plugin " + jarName + " failed to load.", t); - } - Log.infof("Plugin loaded: %s %s", manifest.get("name").asString(""), - manifest.get("version").asString("")); - }); - } catch (Throwable t) { - Log.error("Plugin " + jarName + " failed to load.", t); - } + // TODO: allow plugins to implement a custom plugin loader. + PluginManager pluginManager = new PluginManager(new JarPluginLoader()); + + // Parse plugin manifests + Set pluginManifests = plugins.elements.stream() + .map(value -> value.asString("")) + .filter(jarName -> !jarName.isEmpty()) + .map(jarName -> pluginsPath.resolve(jarName).toAbsolutePath().toFile()) + .map(PluginManager::parsePluginManifest) + .flatMap(Optional::stream) + .collect(Collectors.toSet()); + + // Load plugins + pluginManager.load(pluginManifests, (plugin, manifest) -> { + String jarName = manifest.pluginJar.getName(); + Log.infof("Loading plugin: %s", jarName); + CreditsController.addPlugin(manifest.name, manifest.version.toString(), manifest.author, manifest.description); + + try { + plugin.attach(this); + } catch (Throwable t) { + Log.error("Plugin " + jarName + " failed to load.", t); } - } + Log.infof("Plugin loaded: %s %s", manifest.name, manifest.version); + }); } /** diff --git a/chunky/src/java/se/llbit/chunky/plugin/ChunkyPlugin.java b/chunky/src/java/se/llbit/chunky/plugin/ChunkyPlugin.java deleted file mode 100644 index cc5ad769ec..0000000000 --- a/chunky/src/java/se/llbit/chunky/plugin/ChunkyPlugin.java +++ /dev/null @@ -1,102 +0,0 @@ -/* Copyright (c) 2017 Chunky contributors - * - * This file is part of Chunky. - * - * Chunky is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Chunky is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * You should have received a copy of the GNU General Public License - * along with Chunky. If not, see . - */ -package se.llbit.chunky.plugin; - -import se.llbit.chunky.Plugin; -import se.llbit.chunky.main.Version; -import se.llbit.json.JsonObject; -import se.llbit.json.JsonParser; -import se.llbit.log.Log; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URL; -import java.net.URLClassLoader; -import java.nio.file.FileSystem; -import java.nio.file.FileSystems; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.function.BiConsumer; -import java.util.function.Consumer; - -/** - * Helper class to load plugins. - */ -public final class ChunkyPlugin { - - private ChunkyPlugin() { } - - /** - * Loads a plugin from the given file. - * On successfully loading the plugin class, the plugin instance - * and its manifest are sent to the onLoad consumer. - * - * @param pluginJar the plugin Jar file. - * @param onLoad a function object that the plugin instance and manifest - * are sent to after it successfully loads. - */ - public static void load(File pluginJar, BiConsumer onLoad) { - try (FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + pluginJar.toURI()), - Collections.emptyMap())) { - Path manifestPath = zipFs.getPath("/plugin.json"); - if (!Files.exists(manifestPath)) { - Log.errorf("Missing plugin manifest file (plugin.json) in plugin %s", pluginJar.getName()); - return; - } - try (InputStream in = Files.newInputStream(manifestPath); - JsonParser parser = new JsonParser(in)) { - JsonObject manifest = parser.parse().object(); - - String name = manifest.get("name").stringValue(""); - String main = manifest.get("main").stringValue(""); - if (name.isEmpty()) { - Log.errorf("Plugin %s has no name specified", pluginJar.getName()); - return; - } - if (main.isEmpty()) { - Log.errorf("Plugin %s has no main class specified", pluginJar.getName()); - return; - } - - String targetVersion = manifest.get("targetVersion").stringValue(""); - if (!targetVersion.isEmpty() && !targetVersion.equalsIgnoreCase(Version.getVersion())) { - Log.warnf("The plugin %s was developed for Chunky %s but this is Chunky %s " - + "- it may not work properly.", - name, targetVersion, Version.getVersion()); - } - - URLClassLoader classLoader = new URLClassLoader(new URL[] {pluginJar.toURI().toURL()}); - Class pluginClass = classLoader.loadClass(main); - Plugin plugin = (Plugin) pluginClass.newInstance(); - onLoad.accept(plugin, manifest); - } - } catch (IOException e) { - Log.error("Could not load the plugin", e); - } catch (ClassCastException e) { - Log.error("Plugin main class has wrong type", e); - } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { - Log.error("Could not create plugin instance", e); - } catch (JsonParser.SyntaxError e) { - Log.error("Could not parse the plugin definition file", e); - } - } -} diff --git a/chunky/src/java/se/llbit/chunky/plugin/loader/JarPluginLoader.java b/chunky/src/java/se/llbit/chunky/plugin/loader/JarPluginLoader.java new file mode 100644 index 0000000000..50d76e8a86 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/plugin/loader/JarPluginLoader.java @@ -0,0 +1,45 @@ +package se.llbit.chunky.plugin.loader; + +import se.llbit.chunky.Plugin; +import se.llbit.chunky.plugin.PluginApi; +import se.llbit.chunky.plugin.manifest.PluginManifest; +import se.llbit.log.Log; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.function.BiConsumer; + +@PluginApi +public class JarPluginLoader implements PluginLoader { + public void load(BiConsumer onLoad, PluginManifest pluginManifest) { + try { + Class pluginClass = loadPluginClass(pluginManifest.main, pluginManifest.pluginJar); + Plugin plugin = (Plugin) pluginClass.getDeclaredConstructor().newInstance(); + onLoad.accept(plugin, pluginManifest); + } catch (IOException | ClassNotFoundException e) { + Log.error("Could not load the plugin", e); + } catch (ClassCastException e) { + Log.error("Plugin main class has wrong type (must implement se.llbit.chunky.Plugin)", e); + } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { + Log.error("Could not create plugin instance", e); + } + } + + /** + * This method is {@link PluginApi} to allow plugins to override only classloading functionality of the default plugin loader. + * + * @param pluginMainClass The plugin's main class to load. + * @param pluginJarFile The jar file to load classes from. + * @return The loaded plugin's main class + * @throws ClassNotFoundException If the main class doesn't exist + * @throws MalformedURLException If the jar file cannot be converted to a URL + */ + @PluginApi + protected Class loadPluginClass(String pluginMainClass, File pluginJarFile) throws ClassNotFoundException, MalformedURLException { + return new URLClassLoader(new URL[] { pluginJarFile.toURI().toURL() }).loadClass(pluginMainClass); + } +} diff --git a/chunky/src/java/se/llbit/chunky/plugin/loader/PluginLoader.java b/chunky/src/java/se/llbit/chunky/plugin/loader/PluginLoader.java new file mode 100644 index 0000000000..b73ca15325 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/plugin/loader/PluginLoader.java @@ -0,0 +1,18 @@ +package se.llbit.chunky.plugin.loader; + +import se.llbit.chunky.Plugin; +import se.llbit.chunky.plugin.PluginApi; +import se.llbit.chunky.plugin.manifest.PluginManifest; + +import java.util.function.BiConsumer; + +@PluginApi +public interface PluginLoader { + /** + * Load the plugin specified in the manifest + * @param onLoad The consumer to call with the loaded plugin + * @param pluginManifest The plugin to load. + */ + @PluginApi + void load(BiConsumer onLoad, PluginManifest pluginManifest); +} diff --git a/chunky/src/java/se/llbit/chunky/plugin/loader/PluginManager.java b/chunky/src/java/se/llbit/chunky/plugin/loader/PluginManager.java new file mode 100644 index 0000000000..b9a903a995 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/plugin/loader/PluginManager.java @@ -0,0 +1,121 @@ +/* Copyright (c) 2017 Chunky contributors + * + * This file is part of Chunky. + * + * Chunky is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Chunky is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * You should have received a copy of the GNU General Public License + * along with Chunky. If not, see . + */ +package se.llbit.chunky.plugin.loader; + +import se.llbit.chunky.Plugin; +import se.llbit.chunky.plugin.manifest.PluginDependency; +import se.llbit.chunky.plugin.manifest.PluginManifest; +import se.llbit.json.JsonParser; +import se.llbit.log.Log; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +public class PluginManager { + private static final int MAX_CYCLES = Integer.parseInt(System.getProperty("chunky.plugins.maxLoadCycles", "100")); + + private final PluginLoader pluginLoader; + + public PluginManager(PluginLoader pluginLoader) { + this.pluginLoader = pluginLoader; + } + + public void load(Set pluginManifests, BiConsumer onLoad) { + // create plugin objects + Map> pluginsByName = new HashMap<>(); + pluginManifests.forEach(manifest -> { + pluginsByName.computeIfAbsent(manifest.name, n -> new ArrayList<>()).add( + new ResolvedPlugin(manifest) + ); + }); + + // check for duplicate plugins + pluginsByName.forEach((name, plugins) -> { + if (plugins.size() > 1) { + // we report the error and hope it goes ok + Log.errorf("There are %d plugins with the name %s, this is extremely likely to break, proceeding anyway.", plugins.size(), name); + } + }); + + // resolve dependencies of every plugin. + Set pluginsToLoad = pluginsByName.values().stream().flatMap(Collection::stream).collect(Collectors.toSet()); + pluginsToLoad.forEach(plugin -> plugin.resolveDependencies(pluginsByName)); + + // load plugins in dependency-first order, cyclic dependencies will never be loaded and will hit MAX_CYCLES cap. + // this was so trivial to implement using cycles that I decided against any kind of dependency tree structure, + // in the worst case this approach requires one cycle per plugin (if every plugin depended on the previous one in the list). + Set loadedPlugins = new HashSet<>(); + int loadCycles = 0; + while (!pluginsToLoad.isEmpty() && loadCycles < MAX_CYCLES) { + Log.infof("Cycle %d", loadCycles); + loadCycles++; + // new list to avoid CME due to removing inside iteration. + new ArrayList<>(pluginsToLoad).forEach(plugin -> { + if (plugin.allDependenciesLoaded(loadedPlugins)) { + loadedPlugins.add(plugin); + pluginsToLoad.remove(plugin); + Log.infof(" Loading plugin %s with deps { %s }, resolved { %s }%n", plugin, + plugin.getManifest().getDependencies().stream().map(PluginDependency::toString).collect(Collectors.joining(", ")), + plugin.getDependencies().stream().map(ResolvedPlugin::toString).collect(Collectors.joining(", ")) + ); + pluginLoader.load(onLoad, plugin.getManifest()); + } + }); + } + + // report if any unloaded plugins remain (their dependencies never got loaded) + if (!pluginsToLoad.isEmpty()) { + Log.errorf( + "Reached maximum cycles (%d) when loading plugins. Failed to load plugins: (%s)", + MAX_CYCLES, + pluginsToLoad.stream().map(ResolvedPlugin::toString).collect(Collectors.joining(", ")) + ); + } + } + + /** + * Parse and create a plugin manifest from the specified jar file. + * @param pluginJar The jar to find the manifest file in. + * @return The {@link PluginManifest} if it could be created. + */ + public static Optional parsePluginManifest(File pluginJar) { + try (FileSystem zipFs = FileSystems.newFileSystem(URI.create("jar:" + pluginJar.toURI()), Collections.emptyMap())) { + Path manifestPath = zipFs.getPath("/plugin.json"); + if (!Files.exists(manifestPath)) { + Log.errorf("Missing plugin manifest file (plugin.json) in plugin %s", pluginJar.getName()); + return Optional.empty(); + } + try (InputStream in = Files.newInputStream(manifestPath); JsonParser parser = new JsonParser(in)) { + return PluginManifest.parse(parser.parse().object(), pluginJar); + } catch (JsonParser.SyntaxError e) { + Log.error("Could not parse the plugin manifest file", e); + } + } catch (IOException e) { + Log.error("Could not load the plugin", e); + } + return Optional.empty(); + } +} diff --git a/chunky/src/java/se/llbit/chunky/plugin/loader/ResolvedPlugin.java b/chunky/src/java/se/llbit/chunky/plugin/loader/ResolvedPlugin.java new file mode 100644 index 0000000000..a0ef44272c --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/plugin/loader/ResolvedPlugin.java @@ -0,0 +1,57 @@ +package se.llbit.chunky.plugin.loader; + +import se.llbit.chunky.plugin.manifest.PluginManifest; +import se.llbit.log.Log; + +import java.util.*; + +public class ResolvedPlugin { + private final PluginManifest manifest; + private final Set dependencies = new HashSet<>(); + + public ResolvedPlugin(PluginManifest manifest) { + this.manifest = manifest; + } + + /** + * Resolve dependencies specified in the {@link #manifest} into known plugins. + * @param pluginsByName All plugins to be loaded + */ + public void resolveDependencies(Map> pluginsByName) { + // Adds any plugin as a resolved dependency if it matches an unresolved dependency's name and version. + this.manifest.getDependencies().forEach(unresolvedDep -> { + List resolvedDeps = pluginsByName.get(unresolvedDep.name); + boolean resolved = false; + for (ResolvedPlugin resolvedDep : resolvedDeps) { + if (unresolvedDep.version.containsVersion(resolvedDep.manifest.version)) { + this.dependencies.add(resolvedDep); + resolved = true; + } + } + if (!resolved) { + Log.errorf("Could not find required dependency %s for plugin %s.", unresolvedDep, this.manifest.name); + } + }); + } + + /** + * @param loadedPlugins The set of plugins that are currently loaded. + * @return Whether the provided set contains all dependencies of this plugin. + */ + public boolean allDependenciesLoaded(Set loadedPlugins) { + return loadedPlugins.containsAll(this.dependencies); + } + + public PluginManifest getManifest() { + return this.manifest; + } + + public Set getDependencies() { + return dependencies; + } + + @Override + public String toString() { + return this.manifest.name + ":" + this.manifest.version; + } +} diff --git a/chunky/src/java/se/llbit/chunky/plugin/manifest/PluginDependency.java b/chunky/src/java/se/llbit/chunky/plugin/manifest/PluginDependency.java new file mode 100644 index 0000000000..8b04c83cb5 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/plugin/manifest/PluginDependency.java @@ -0,0 +1,21 @@ +package se.llbit.chunky.plugin.manifest; + +import org.apache.maven.artifact.versioning.VersionRange; + +/** + * An unresolved plugin dependency within a plugin manifest. + */ +public class PluginDependency { + public final String name; + public final VersionRange version; + + public PluginDependency(String name, VersionRange version) { + this.name = name; + this.version = version; + } + + @Override + public String toString() { + return name + ":" + version; + } +} diff --git a/chunky/src/java/se/llbit/chunky/plugin/manifest/PluginManifest.java b/chunky/src/java/se/llbit/chunky/plugin/manifest/PluginManifest.java new file mode 100644 index 0000000000..9b0a1cf582 --- /dev/null +++ b/chunky/src/java/se/llbit/chunky/plugin/manifest/PluginManifest.java @@ -0,0 +1,120 @@ +package se.llbit.chunky.plugin.manifest; + +import org.apache.maven.artifact.versioning.ArtifactVersion; +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; +import org.apache.maven.artifact.versioning.InvalidVersionSpecificationException; +import org.apache.maven.artifact.versioning.VersionRange; +import se.llbit.chunky.main.Version; +import se.llbit.json.JsonMember; +import se.llbit.json.JsonObject; +import se.llbit.log.Log; + +import java.io.File; +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; + +/** + * An immutable class representing the manifest file (plugin.json) of a plugin. + */ +public class PluginManifest { + public final File pluginJar; + public final String name; + public final String author; + public final String description; + public final ArtifactVersion version; + public final VersionRange targetVersion; + public final String main; + private final Set dependencies; + + /** + * @param pluginJar Location of a valid plugin .jar file + * @param name The name of the plugin + * @param author The author of the plugin + * @param description The description of the plugin + * @param version The version of the plugin + * @param targetVersion The target chunky version range of the plugin + * @param main The main class of the plugin within its .jar + * @param dependencies The required dependencies of the plugin + */ + public PluginManifest(File pluginJar, String name, String author, String description, ArtifactVersion version, + VersionRange targetVersion, String main, Set dependencies) { + this.pluginJar = pluginJar; + this.name = name; + this.author = author; + this.description = description; + this.version = version; + this.targetVersion = targetVersion; + this.main = main; + this.dependencies = dependencies; + } + + /** + * Parse a json object into a PluginManifest if possible + * @param manifest The manifest json data + * @param pluginJar The plugin jar to associate with the manifest + * @return The PluginManifest if it could be created. + */ + public static Optional parse(JsonObject manifest, File pluginJar) { + String name = manifest.get("name").stringValue(""); + String author = manifest.get("author").stringValue(""); + String description = manifest.get("description").stringValue(""); + String version = manifest.get("version").stringValue(""); + String targetVersion = manifest.get("targetVersion").stringValue(""); + String main = manifest.get("main").stringValue(""); + + if (name.isEmpty()) { + Log.errorf("Plugin %s has no name specified", pluginJar.getName()); + return Optional.empty(); + } + if (version.isEmpty()) { + Log.errorf("Plugin %s has no version specified", name); + return Optional.empty(); + } + if (main.isEmpty()) { + Log.errorf("Plugin %s has no main class specified", name); + return Optional.empty(); + } + if (targetVersion.isEmpty()) { + Log.errorf("Plugin %s has no targetVersion specified", name); + return Optional.empty(); + } + + if (!targetVersion.equalsIgnoreCase(Version.getVersion())) { + Log.warnf("The plugin %s was developed for Chunky %s but this is Chunky %s " + + "- it may not work properly.", + name, targetVersion, Version.getVersion()); + } + + Set dependencies = new HashSet<>(); + for (JsonMember dependency : manifest.get("dependencies").asObject()) { + String dependencyVersion = dependency.getValue().stringValue(""); + if (dependency.name.isEmpty() || dependencyVersion.isEmpty()) { + Log.errorf("Plugin %s has an invalid dependency specified %s: %s.", name, dependency.name, dependency.value); + return Optional.empty(); + } + try { + dependencies.add(new PluginDependency(dependency.name, VersionRange.createFromVersionSpec(dependencyVersion))); + } catch (InvalidVersionSpecificationException exception) { + Log.error( + String.format("Could not parse plugin %s dependency version range %s.", name, dependencyVersion), + exception + ); + return Optional.empty(); + } + } + + VersionRange targetVersionRange; + try { + targetVersionRange = VersionRange.createFromVersionSpec(targetVersion); + } catch (InvalidVersionSpecificationException e) { + Log.error(String.format("Failed to parse plugin %s targetVersion %s into version range.", name, targetVersion), e); + targetVersionRange = VersionRange.createFromVersion(targetVersion); + } + return Optional.of(new PluginManifest(pluginJar, name, author, description, new DefaultArtifactVersion(version), targetVersionRange, main, dependencies)); + } + + public Set getDependencies() { + return dependencies; + } +} diff --git a/chunky/src/java/se/llbit/chunky/ui/controller/CreditsController.java b/chunky/src/java/se/llbit/chunky/ui/controller/CreditsController.java index 03830d3775..bdd616dc3e 100644 --- a/chunky/src/java/se/llbit/chunky/ui/controller/CreditsController.java +++ b/chunky/src/java/se/llbit/chunky/ui/controller/CreditsController.java @@ -63,6 +63,10 @@ public class CreditsController implements Initializable { @FXML private Hyperlink fastMathLicense; @FXML + private Hyperlink mavenArtifact; + @FXML + private Hyperlink mavenArtifactLicense; + @FXML private Hyperlink fastutil; @FXML private Hyperlink fastutilLicense; @@ -162,6 +166,13 @@ public void initialize(URL location, ResourceBundle resources) { fastMathLicense.setOnAction( e -> launchAndReset(fastMathLicense, "http://www.apache.org/licenses/LICENSE-2.0")); + mavenArtifact.setBorder(Border.EMPTY); + mavenArtifact.setOnAction( + e -> launchAndReset(fastMath, "https://maven.apache.org/ref/3.9.9/maven-artifact/")); + mavenArtifactLicense.setBorder(Border.EMPTY); + mavenArtifactLicense.setOnAction( + e -> launchAndReset(fastMathLicense, "http://www.apache.org/licenses/LICENSE-2.0")); + fastutil.setBorder(Border.EMPTY); fastutil.setOnAction(e -> launchAndReset(fastutil, "https://fastutil.di.unimi.it/")); fastutilLicense.setBorder(Border.EMPTY); diff --git a/chunky/src/res/se/llbit/chunky/ui/dialogs/Credits.fxml b/chunky/src/res/se/llbit/chunky/ui/dialogs/Credits.fxml index a0880ba18d..cd15d817d4 100644 --- a/chunky/src/res/se/llbit/chunky/ui/dialogs/Credits.fxml +++ b/chunky/src/res/se/llbit/chunky/ui/dialogs/Credits.fxml @@ -31,7 +31,7 @@ -