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 {
|
public class GeyserExtensionClassLoader extends URLClassLoader {
|
||||||
private final GeyserExtensionLoader loader;
|
private final GeyserExtensionLoader loader;
|
||||||
private final ExtensionDescription description;
|
private final GeyserExtensionDescription description;
|
||||||
private final Object2ObjectMap<String, Class<?>> classes = new Object2ObjectOpenHashMap<>();
|
private final Object2ObjectMap<String, Class<?>> classes = new Object2ObjectOpenHashMap<>();
|
||||||
private boolean warnedForExternalClassAccess;
|
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);
|
super(new URL[] { path.toUri().toURL() }, parent);
|
||||||
this.loader = loader;
|
this.loader = loader;
|
||||||
this.description = description;
|
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
|
// 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
|
// This is used for classes that are not in the extension, but are in other extensions
|
||||||
if (checkGlobal) {
|
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. " +
|
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!");
|
"This can change at any time and break the extension, additionally to potentially causing unexpected behaviour!");
|
||||||
warnedForExternalClassAccess = true;
|
warnedForExternalClassAccess = true;
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ public record GeyserExtensionDescription(@NonNull String id,
|
|||||||
int majorApiVersion,
|
int majorApiVersion,
|
||||||
int minorApiVersion,
|
int minorApiVersion,
|
||||||
@NonNull String version,
|
@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()));
|
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);
|
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
|
@NonNull
|
||||||
@@ -116,5 +122,17 @@ public record GeyserExtensionDescription(@NonNull String id,
|
|||||||
String version;
|
String version;
|
||||||
String author;
|
String author;
|
||||||
List<String> authors;
|
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.Path;
|
||||||
import java.nio.file.StandardCopyOption;
|
import java.nio.file.StandardCopyOption;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.function.BiConsumer;
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.Consumer;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@@ -167,6 +172,8 @@ public class GeyserExtensionLoader extends ExtensionLoader {
|
|||||||
|
|
||||||
Map<String, Path> extensions = new LinkedHashMap<>();
|
Map<String, Path> extensions = new LinkedHashMap<>();
|
||||||
Map<String, GeyserExtensionContainer> loadedExtensions = 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");
|
Path updateDirectory = extensionsDirectory.resolve("update");
|
||||||
if (Files.isDirectory(updateDirectory)) {
|
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) -> {
|
this.processExtensionsFolder(extensionsDirectory, (path, description) -> {
|
||||||
String name = description.name();
|
|
||||||
String id = description.id();
|
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) {
|
if (extensions.containsKey(id) || extensionManager.extension(id) != null) {
|
||||||
logger.warning(GeyserLocale.getLocaleStringLog("geyser.extensions.load.duplicate", name, path.toString()));
|
logger.warning(GeyserLocale.getLocaleStringLog("geyser.extensions.load.duplicate", name, path.toString()));
|
||||||
return;
|
return;
|
||||||
@@ -222,20 +345,22 @@ public class GeyserExtensionLoader extends ExtensionLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
GeyserExtensionContainer container = this.loadExtension(path, description);
|
GeyserExtensionContainer container = this.loadExtension(path, description);
|
||||||
extensions.put(id, path);
|
extensions.put(id, path);
|
||||||
loadedExtensions.put(id, container);
|
loadedExtensions.put(id, container);
|
||||||
}, (path, e) -> {
|
} catch (Throwable e) {
|
||||||
logger.error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), 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()) {
|
for (GeyserExtensionContainer container : loadedExtensions.values()) {
|
||||||
this.extensionContainers.put(container.extension(), container);
|
this.extensionContainers.put(container.extension(), container);
|
||||||
this.register(container.extension(), extensionManager);
|
this.register(container.extension(), extensionManager);
|
||||||
}
|
}
|
||||||
} catch (IOException ex) {
|
} 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