mirror of
https://github.com/GeyserMC/Geyser.git
synced 2025-12-19 14:59:27 +00:00
Feature: Extension dependencies (#5839)
* Add loading order to dependencies # Conflicts: # core/src/main/resources/mappings * Add softdepend capability, add default load and required values * Prevent an extension from loading if it uses an old api version with the new dependency system. * Add translation strings to dependency messages * Account for language string changes, remove class loader warning when extension has dependencies * Update languages module to latest * Update version in GeyserExtensionLoader for dependencies to match 1.21.9 branch * revert mapping update --------- Co-authored-by: onebeastchris <github@onechris.mozmail.com>
This commit is contained in:
@@ -40,11 +40,11 @@ import java.nio.file.Path;
|
||||
|
||||
public class GeyserExtensionClassLoader extends URLClassLoader {
|
||||
private final GeyserExtensionLoader loader;
|
||||
private final ExtensionDescription description;
|
||||
private final GeyserExtensionDescription description;
|
||||
private final Object2ObjectMap<String, Class<?>> classes = new Object2ObjectOpenHashMap<>();
|
||||
private boolean warnedForExternalClassAccess;
|
||||
|
||||
public GeyserExtensionClassLoader(GeyserExtensionLoader loader, ClassLoader parent, Path path, ExtensionDescription description) throws MalformedURLException {
|
||||
public GeyserExtensionClassLoader(GeyserExtensionLoader loader, ClassLoader parent, Path path, GeyserExtensionDescription description) throws MalformedURLException {
|
||||
super(new URL[] { path.toUri().toURL() }, parent);
|
||||
this.loader = loader;
|
||||
this.description = description;
|
||||
@@ -89,7 +89,7 @@ public class GeyserExtensionClassLoader extends URLClassLoader {
|
||||
// If class is not found in current extension, check in the global class loader
|
||||
// This is used for classes that are not in the extension, but are in other extensions
|
||||
if (checkGlobal) {
|
||||
if (!warnedForExternalClassAccess) {
|
||||
if (!warnedForExternalClassAccess && this.description.dependencies().isEmpty()) { // Don't warn when the extension has dependencies, it is probably using it's dependencies!
|
||||
GeyserImpl.getInstance().getLogger().warning("Extension " + this.description.name() + " loads class " + name + " from an external source. " +
|
||||
"This can change at any time and break the extension, additionally to potentially causing unexpected behaviour!");
|
||||
warnedForExternalClassAccess = true;
|
||||
|
||||
@@ -47,7 +47,8 @@ public record GeyserExtensionDescription(@NonNull String id,
|
||||
int majorApiVersion,
|
||||
int minorApiVersion,
|
||||
@NonNull String version,
|
||||
@NonNull List<String> authors) implements ExtensionDescription {
|
||||
@NonNull List<String> authors,
|
||||
@NonNull Map<String, Dependency> dependencies) implements ExtensionDescription {
|
||||
|
||||
private static final Yaml YAML = new Yaml(new CustomClassLoaderConstructor(Source.class.getClassLoader(), new LoaderOptions()));
|
||||
|
||||
@@ -94,7 +95,12 @@ public record GeyserExtensionDescription(@NonNull String id,
|
||||
authors.addAll(source.authors);
|
||||
}
|
||||
|
||||
return new GeyserExtensionDescription(id, name, main, humanApi, majorApi, minorApi, version, authors);
|
||||
Map<String, Dependency> dependencies = new LinkedHashMap<>();
|
||||
if (source.dependencies != null) {
|
||||
dependencies.putAll(source.dependencies);
|
||||
}
|
||||
|
||||
return new GeyserExtensionDescription(id, name, main, humanApi, majorApi, minorApi, version, authors, dependencies);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@@ -116,5 +122,17 @@ public record GeyserExtensionDescription(@NonNull String id,
|
||||
String version;
|
||||
String author;
|
||||
List<String> authors;
|
||||
Map<String, Dependency> dependencies;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class Dependency {
|
||||
boolean required = true; // Defaults to true
|
||||
LoadOrder load = LoadOrder.BEFORE; // Defaults to ensure the dependency loads before this extension
|
||||
}
|
||||
|
||||
public enum LoadOrder {
|
||||
BEFORE, AFTER
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,11 +54,16 @@ import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@@ -167,6 +172,8 @@ public class GeyserExtensionLoader extends ExtensionLoader {
|
||||
|
||||
Map<String, Path> extensions = new LinkedHashMap<>();
|
||||
Map<String, GeyserExtensionContainer> loadedExtensions = new LinkedHashMap<>();
|
||||
Map<String, GeyserExtensionDescription> descriptions = new LinkedHashMap<>();
|
||||
Map<String, Path> extensionPaths = new LinkedHashMap<>();
|
||||
|
||||
Path updateDirectory = extensionsDirectory.resolve("update");
|
||||
if (Files.isDirectory(updateDirectory)) {
|
||||
@@ -195,10 +202,126 @@ public class GeyserExtensionLoader extends ExtensionLoader {
|
||||
});
|
||||
}
|
||||
|
||||
// Step 3: Load the extensions
|
||||
// Step 3: Order the extensions to allow dependencies to load in the correct order
|
||||
this.processExtensionsFolder(extensionsDirectory, (path, description) -> {
|
||||
String name = description.name();
|
||||
String id = description.id();
|
||||
descriptions.put(id, description);
|
||||
extensionPaths.put(id, path);
|
||||
|
||||
}, (path, e) -> {
|
||||
logger.error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), e);
|
||||
});
|
||||
|
||||
// The graph to back out loading order (Funny I just learnt these too)
|
||||
Map<String, List<String>> loadOrderGraph = new HashMap<>();
|
||||
|
||||
// Looks like the graph needs to be prepopulated otherwise issues happen
|
||||
for (String id : descriptions.keySet()) {
|
||||
loadOrderGraph.putIfAbsent(id, new ArrayList<>());
|
||||
}
|
||||
|
||||
for (GeyserExtensionDescription description : descriptions.values()) {
|
||||
for (Map.Entry<String, GeyserExtensionDescription.Dependency> dependency : description.dependencies().entrySet()) {
|
||||
String from = null;
|
||||
String to = null; // Java complains if this isn't initialised, but not from, so, both null.
|
||||
|
||||
// Check if the extension is even loaded
|
||||
if (!descriptions.containsKey(dependency.getKey())) {
|
||||
if (dependency.getValue().isRequired()) { // Only disable the extension if this dependency is required
|
||||
// The extension we are checking is missing 1 or more dependencies
|
||||
logger.error(
|
||||
GeyserLocale.getLocaleStringLog(
|
||||
"geyser.extensions.load.failed_dependency_missing",
|
||||
description.id(),
|
||||
dependency.getKey()
|
||||
)
|
||||
);
|
||||
|
||||
descriptions.remove(description.id()); // Prevents it from being loaded later
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
!(description.humanApiVersion() >= 2 &&
|
||||
description.majorApiVersion() >= 9 &&
|
||||
description.minorApiVersion() >= 0)
|
||||
) {
|
||||
logger.error(
|
||||
GeyserLocale.getLocaleStringLog(
|
||||
"geyser.extensions.load.failed_cannot_use_dependencies",
|
||||
description.id(),
|
||||
description.apiVersion()
|
||||
)
|
||||
);
|
||||
|
||||
descriptions.remove(description.id()); // Prevents it from being loaded later
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine which way they should go in the graph
|
||||
switch (dependency.getValue().getLoad()) {
|
||||
case BEFORE -> {
|
||||
from = dependency.getKey();
|
||||
to = description.id();
|
||||
}
|
||||
case AFTER -> {
|
||||
from = description.id();
|
||||
to = dependency.getKey();
|
||||
}
|
||||
}
|
||||
|
||||
loadOrderGraph.get(from).add(to);
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> visited = new HashSet<>();
|
||||
List<String> visiting = new ArrayList<>();
|
||||
List<String> loadOrder = new ArrayList<>();
|
||||
|
||||
AtomicReference<Consumer<String>> sortMethod = new AtomicReference<>(); // yay, lambdas. This doesn't feel to suited to be a method
|
||||
sortMethod.set((node) -> {
|
||||
if (visiting.contains(node)) {
|
||||
logger.error(
|
||||
GeyserLocale.getLocaleStringLog(
|
||||
"geyser.extensions.load.failed_cyclical_dependencies",
|
||||
node,
|
||||
visiting.get(visiting.indexOf(node) - 1)
|
||||
)
|
||||
);
|
||||
|
||||
visiting.remove(node);
|
||||
return;
|
||||
}
|
||||
|
||||
if (visited.contains(node)) return;
|
||||
|
||||
visiting.add(node);
|
||||
for (String neighbor : loadOrderGraph.get(node)) {
|
||||
sortMethod.get().accept(neighbor);
|
||||
}
|
||||
visiting.remove(node);
|
||||
visited.add(node);
|
||||
loadOrder.add(node);
|
||||
});
|
||||
|
||||
for (String ext : descriptions.keySet()) {
|
||||
if (!visited.contains(ext)) {
|
||||
// Time to sort the graph to get a load order, this reveals any cycles we may have
|
||||
sortMethod.get().accept(ext);
|
||||
}
|
||||
}
|
||||
Collections.reverse(loadOrder); // This is inverted due to how the graph is created
|
||||
|
||||
// Step 4: Load the extensions
|
||||
for (String id : loadOrder) {
|
||||
// Grab path and description found from before, since we want a custom load order now
|
||||
Path path = extensionPaths.get(id);
|
||||
GeyserExtensionDescription description = descriptions.get(id);
|
||||
|
||||
String name = description.name();
|
||||
if (extensions.containsKey(id) || extensionManager.extension(id) != null) {
|
||||
logger.warning(GeyserLocale.getLocaleStringLog("geyser.extensions.load.duplicate", name, path.toString()));
|
||||
return;
|
||||
@@ -222,20 +345,22 @@ public class GeyserExtensionLoader extends ExtensionLoader {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
GeyserExtensionContainer container = this.loadExtension(path, description);
|
||||
extensions.put(id, path);
|
||||
loadedExtensions.put(id, container);
|
||||
}, (path, e) -> {
|
||||
} catch (Throwable e) {
|
||||
logger.error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Register the extensions
|
||||
// Step 5: Register the extensions
|
||||
for (GeyserExtensionContainer container : loadedExtensions.values()) {
|
||||
this.extensionContainers.put(container.extension(), container);
|
||||
this.register(container.extension(), extensionManager);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
logger.error("Unable to read extensions.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Submodule core/src/main/resources/languages updated: 40253e5f31...4ce8ad58ea
Reference in New Issue
Block a user