Skip to content

Commit 4549a95

Browse files
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 <[email protected]>
1 parent e8e6c2b commit 4549a95

File tree

4 files changed

+159
-16
lines changed

4 files changed

+159
-16
lines changed

core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionClassLoader.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@
4040

4141
public class GeyserExtensionClassLoader extends URLClassLoader {
4242
private final GeyserExtensionLoader loader;
43-
private final ExtensionDescription description;
43+
private final GeyserExtensionDescription description;
4444
private final Object2ObjectMap<String, Class<?>> classes = new Object2ObjectOpenHashMap<>();
4545
private boolean warnedForExternalClassAccess;
4646

47-
public GeyserExtensionClassLoader(GeyserExtensionLoader loader, ClassLoader parent, Path path, ExtensionDescription description) throws MalformedURLException {
47+
public GeyserExtensionClassLoader(GeyserExtensionLoader loader, ClassLoader parent, Path path, GeyserExtensionDescription description) throws MalformedURLException {
4848
super(new URL[] { path.toUri().toURL() }, parent);
4949
this.loader = loader;
5050
this.description = description;
@@ -89,7 +89,7 @@ protected Class<?> findClass(String name, boolean checkGlobal) throws ClassNotFo
8989
// If class is not found in current extension, check in the global class loader
9090
// This is used for classes that are not in the extension, but are in other extensions
9191
if (checkGlobal) {
92-
if (!warnedForExternalClassAccess) {
92+
if (!warnedForExternalClassAccess && this.description.dependencies().isEmpty()) { // Don't warn when the extension has dependencies, it is probably using it's dependencies!
9393
GeyserImpl.getInstance().getLogger().warning("Extension " + this.description.name() + " loads class " + name + " from an external source. " +
9494
"This can change at any time and break the extension, additionally to potentially causing unexpected behaviour!");
9595
warnedForExternalClassAccess = true;

core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionDescription.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ public record GeyserExtensionDescription(@NonNull String id,
4747
int majorApiVersion,
4848
int minorApiVersion,
4949
@NonNull String version,
50-
@NonNull List<String> authors) implements ExtensionDescription {
50+
@NonNull List<String> authors,
51+
@NonNull Map<String, Dependency> dependencies) implements ExtensionDescription {
5152

5253
private static final Yaml YAML = new Yaml(new CustomClassLoaderConstructor(Source.class.getClassLoader(), new LoaderOptions()));
5354

@@ -94,7 +95,12 @@ public static GeyserExtensionDescription fromYaml(Reader reader) throws InvalidD
9495
authors.addAll(source.authors);
9596
}
9697

97-
return new GeyserExtensionDescription(id, name, main, humanApi, majorApi, minorApi, version, authors);
98+
Map<String, Dependency> dependencies = new LinkedHashMap<>();
99+
if (source.dependencies != null) {
100+
dependencies.putAll(source.dependencies);
101+
}
102+
103+
return new GeyserExtensionDescription(id, name, main, humanApi, majorApi, minorApi, version, authors, dependencies);
98104
}
99105

100106
@NonNull
@@ -116,5 +122,17 @@ public static class Source {
116122
String version;
117123
String author;
118124
List<String> authors;
125+
Map<String, Dependency> dependencies;
126+
}
127+
128+
@Getter
129+
@Setter
130+
public static class Dependency {
131+
boolean required = true; // Defaults to true
132+
LoadOrder load = LoadOrder.BEFORE; // Defaults to ensure the dependency loads before this extension
133+
}
134+
135+
public enum LoadOrder {
136+
BEFORE, AFTER
119137
}
120138
}

core/src/main/java/org/geysermc/geyser/extension/GeyserExtensionLoader.java

Lines changed: 135 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,16 @@
5454
import java.nio.file.Path;
5555
import java.nio.file.StandardCopyOption;
5656
import java.util.ArrayList;
57+
import java.util.Collections;
5758
import java.util.HashMap;
59+
import java.util.HashSet;
5860
import java.util.LinkedHashMap;
5961
import java.util.List;
6062
import java.util.Map;
63+
import java.util.Set;
64+
import java.util.concurrent.atomic.AtomicReference;
6165
import java.util.function.BiConsumer;
66+
import java.util.function.Consumer;
6267
import java.util.regex.Pattern;
6368

6469
@RequiredArgsConstructor
@@ -167,6 +172,8 @@ protected void loadAllExtensions(@NonNull ExtensionManager extensionManager) {
167172

168173
Map<String, Path> extensions = new LinkedHashMap<>();
169174
Map<String, GeyserExtensionContainer> loadedExtensions = new LinkedHashMap<>();
175+
Map<String, GeyserExtensionDescription> descriptions = new LinkedHashMap<>();
176+
Map<String, Path> extensionPaths = new LinkedHashMap<>();
170177

171178
Path updateDirectory = extensionsDirectory.resolve("update");
172179
if (Files.isDirectory(updateDirectory)) {
@@ -195,10 +202,126 @@ protected void loadAllExtensions(@NonNull ExtensionManager extensionManager) {
195202
});
196203
}
197204

198-
// Step 3: Load the extensions
205+
// Step 3: Order the extensions to allow dependencies to load in the correct order
199206
this.processExtensionsFolder(extensionsDirectory, (path, description) -> {
200-
String name = description.name();
201207
String id = description.id();
208+
descriptions.put(id, description);
209+
extensionPaths.put(id, path);
210+
211+
}, (path, e) -> {
212+
logger.error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), e);
213+
});
214+
215+
// The graph to back out loading order (Funny I just learnt these too)
216+
Map<String, List<String>> loadOrderGraph = new HashMap<>();
217+
218+
// Looks like the graph needs to be prepopulated otherwise issues happen
219+
for (String id : descriptions.keySet()) {
220+
loadOrderGraph.putIfAbsent(id, new ArrayList<>());
221+
}
222+
223+
for (GeyserExtensionDescription description : descriptions.values()) {
224+
for (Map.Entry<String, GeyserExtensionDescription.Dependency> dependency : description.dependencies().entrySet()) {
225+
String from = null;
226+
String to = null; // Java complains if this isn't initialised, but not from, so, both null.
227+
228+
// Check if the extension is even loaded
229+
if (!descriptions.containsKey(dependency.getKey())) {
230+
if (dependency.getValue().isRequired()) { // Only disable the extension if this dependency is required
231+
// The extension we are checking is missing 1 or more dependencies
232+
logger.error(
233+
GeyserLocale.getLocaleStringLog(
234+
"geyser.extensions.load.failed_dependency_missing",
235+
description.id(),
236+
dependency.getKey()
237+
)
238+
);
239+
240+
descriptions.remove(description.id()); // Prevents it from being loaded later
241+
}
242+
243+
continue;
244+
}
245+
246+
if (
247+
!(description.humanApiVersion() >= 2 &&
248+
description.majorApiVersion() >= 9 &&
249+
description.minorApiVersion() >= 0)
250+
) {
251+
logger.error(
252+
GeyserLocale.getLocaleStringLog(
253+
"geyser.extensions.load.failed_cannot_use_dependencies",
254+
description.id(),
255+
description.apiVersion()
256+
)
257+
);
258+
259+
descriptions.remove(description.id()); // Prevents it from being loaded later
260+
261+
continue;
262+
}
263+
264+
// Determine which way they should go in the graph
265+
switch (dependency.getValue().getLoad()) {
266+
case BEFORE -> {
267+
from = dependency.getKey();
268+
to = description.id();
269+
}
270+
case AFTER -> {
271+
from = description.id();
272+
to = dependency.getKey();
273+
}
274+
}
275+
276+
loadOrderGraph.get(from).add(to);
277+
}
278+
}
279+
280+
Set<String> visited = new HashSet<>();
281+
List<String> visiting = new ArrayList<>();
282+
List<String> loadOrder = new ArrayList<>();
283+
284+
AtomicReference<Consumer<String>> sortMethod = new AtomicReference<>(); // yay, lambdas. This doesn't feel to suited to be a method
285+
sortMethod.set((node) -> {
286+
if (visiting.contains(node)) {
287+
logger.error(
288+
GeyserLocale.getLocaleStringLog(
289+
"geyser.extensions.load.failed_cyclical_dependencies",
290+
node,
291+
visiting.get(visiting.indexOf(node) - 1)
292+
)
293+
);
294+
295+
visiting.remove(node);
296+
return;
297+
}
298+
299+
if (visited.contains(node)) return;
300+
301+
visiting.add(node);
302+
for (String neighbor : loadOrderGraph.get(node)) {
303+
sortMethod.get().accept(neighbor);
304+
}
305+
visiting.remove(node);
306+
visited.add(node);
307+
loadOrder.add(node);
308+
});
309+
310+
for (String ext : descriptions.keySet()) {
311+
if (!visited.contains(ext)) {
312+
// Time to sort the graph to get a load order, this reveals any cycles we may have
313+
sortMethod.get().accept(ext);
314+
}
315+
}
316+
Collections.reverse(loadOrder); // This is inverted due to how the graph is created
317+
318+
// Step 4: Load the extensions
319+
for (String id : loadOrder) {
320+
// Grab path and description found from before, since we want a custom load order now
321+
Path path = extensionPaths.get(id);
322+
GeyserExtensionDescription description = descriptions.get(id);
323+
324+
String name = description.name();
202325
if (extensions.containsKey(id) || extensionManager.extension(id) != null) {
203326
logger.warning(GeyserLocale.getLocaleStringLog("geyser.extensions.load.duplicate", name, path.toString()));
204327
return;
@@ -222,20 +345,22 @@ protected void loadAllExtensions(@NonNull ExtensionManager extensionManager) {
222345
}
223346
}
224347

225-
GeyserExtensionContainer container = this.loadExtension(path, description);
226-
extensions.put(id, path);
227-
loadedExtensions.put(id, container);
228-
}, (path, e) -> {
229-
logger.error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), e);
230-
});
348+
try {
349+
GeyserExtensionContainer container = this.loadExtension(path, description);
350+
extensions.put(id, path);
351+
loadedExtensions.put(id, container);
352+
} catch (Throwable e) {
353+
logger.error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), e);
354+
}
355+
}
231356

232-
// Step 4: Register the extensions
357+
// Step 5: Register the extensions
233358
for (GeyserExtensionContainer container : loadedExtensions.values()) {
234359
this.extensionContainers.put(container.extension(), container);
235360
this.register(container.extension(), extensionManager);
236361
}
237362
} catch (IOException ex) {
238-
ex.printStackTrace();
363+
logger.error("Unable to read extensions.", ex);
239364
}
240365
}
241366

core/src/main/resources/languages

0 commit comments

Comments
 (0)