Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@

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;
Expand Down Expand Up @@ -89,7 +89,7 @@ protected Class<?> findClass(String name, boolean checkGlobal) throws ClassNotFo
// 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()));

Expand Down Expand Up @@ -94,7 +95,12 @@ public static GeyserExtensionDescription fromYaml(Reader reader) throws InvalidD
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
Expand All @@ -116,5 +122,17 @@ public static class Source {
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,16 @@
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
Expand Down Expand Up @@ -167,6 +172,8 @@ protected void loadAllExtensions(@NonNull ExtensionManager extensionManager) {

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)) {
Expand Down Expand Up @@ -195,10 +202,126 @@ protected void loadAllExtensions(@NonNull ExtensionManager extensionManager) {
});
}

// 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;
Expand All @@ -222,20 +345,22 @@ protected void loadAllExtensions(@NonNull ExtensionManager extensionManager) {
}
}

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);
}
}

Expand Down
2 changes: 1 addition & 1 deletion core/src/main/resources/languages