1
0
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:
Aurorawr
2025-10-08 20:51:47 +01:00
committed by GitHub
parent e8e6c2bdbd
commit 4549a95ee5
4 changed files with 159 additions and 16 deletions

View File

@@ -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;

View File

@@ -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
}
}

View File

@@ -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 {
}
}
GeyserExtensionContainer container = this.loadExtension(path, description);
extensions.put(id, path);
loadedExtensions.put(id, container);
}, (path, e) -> {
logger.error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), e);
});
try {
GeyserExtensionContainer container = this.loadExtension(path, description);
extensions.put(id, path);
loadedExtensions.put(id, container);
} 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);
}
}