mirror of
https://github.com/Minestom/Minestom.git
synced 2024-11-09 04:10:31 +01:00
hollow-cube/no-extensions
Signed-off-by: mworzala <mattheworzala@gmail.com> remove extension stuff from demo (cherry picked from commit 6052e726d03d6a27dd11962134328ad474ce45a9) remove extensions (cherry picked from commit 40ba24e43b6eb0f8869d80c30bd47d799c82a094) (cherry picked from commit f4f9a905f74e7bf67fc2af341a25b9e85933abbe)
This commit is contained in:
parent
fe9a4291bf
commit
2915558004
@ -1,69 +0,0 @@
|
|||||||
package net.minestom.demo.commands;
|
|
||||||
|
|
||||||
import net.kyori.adventure.text.Component;
|
|
||||||
import net.minestom.server.MinecraftServer;
|
|
||||||
import net.minestom.server.command.CommandSender;
|
|
||||||
import net.minestom.server.command.builder.Command;
|
|
||||||
import net.minestom.server.command.builder.CommandContext;
|
|
||||||
import net.minestom.server.command.builder.arguments.ArgumentString;
|
|
||||||
import net.minestom.server.command.builder.arguments.ArgumentType;
|
|
||||||
import net.minestom.server.command.builder.exception.ArgumentSyntaxException;
|
|
||||||
import net.minestom.server.extensions.ExtensionManager;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
|
|
||||||
public class LoadExtensionCommand extends Command {
|
|
||||||
|
|
||||||
private final ArgumentString extensionName;
|
|
||||||
|
|
||||||
public LoadExtensionCommand() {
|
|
||||||
super("load");
|
|
||||||
|
|
||||||
setDefaultExecutor(this::usage);
|
|
||||||
|
|
||||||
extensionName = ArgumentType.String("extensionName");
|
|
||||||
|
|
||||||
setArgumentCallback(this::extensionCallback, extensionName);
|
|
||||||
addSyntax(this::execute, extensionName);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void usage(CommandSender sender, CommandContext context) {
|
|
||||||
sender.sendMessage(Component.text("Usage: /load <extension file name>"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void execute(CommandSender sender, CommandContext context) {
|
|
||||||
final String name = context.get(extensionName);
|
|
||||||
sender.sendMessage(Component.text("extensionFile = " + name + "...."));
|
|
||||||
|
|
||||||
ExtensionManager extensionManager = MinecraftServer.getExtensionManager();
|
|
||||||
Path extensionFolder = extensionManager.getExtensionFolder().toPath().toAbsolutePath();
|
|
||||||
Path extensionJar = extensionFolder.resolve(name);
|
|
||||||
try {
|
|
||||||
if (!extensionJar.toFile().getCanonicalPath().startsWith(extensionFolder.toFile().getCanonicalPath())) {
|
|
||||||
sender.sendMessage(Component.text("File name '" + name + "' does not represent a file inside the extensions folder. Will not load"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
sender.sendMessage(Component.text("Failed to load extension: " + e.getMessage()));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
boolean managed = extensionManager.loadDynamicExtension(extensionJar.toFile());
|
|
||||||
if (managed) {
|
|
||||||
sender.sendMessage(Component.text("Extension loaded!"));
|
|
||||||
} else {
|
|
||||||
sender.sendMessage(Component.text("Failed to load extension, check your logs."));
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
sender.sendMessage(Component.text("Failed to load extension: " + e.getMessage()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void extensionCallback(CommandSender sender, ArgumentSyntaxException exception) {
|
|
||||||
sender.sendMessage(Component.text("'" + exception.getInput() + "' is not a valid extension name!"));
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,21 +0,0 @@
|
|||||||
package net.minestom.demo.extension;
|
|
||||||
|
|
||||||
import net.minestom.server.extensions.Extension;
|
|
||||||
|
|
||||||
public class TestExtension extends Extension {
|
|
||||||
@Override
|
|
||||||
public void initialize() {
|
|
||||||
System.out.println("Initialize test extension");
|
|
||||||
|
|
||||||
try {
|
|
||||||
Class.forName("com.mysql.cj.jdbc.Driver", true, getOrigin().getClassLoader());
|
|
||||||
} catch (ClassNotFoundException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void terminate() {
|
|
||||||
System.out.println("Terminate test extension");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"entrypoint": "net.minestom.demo.extension.TestExtension",
|
|
||||||
"name": "TestExtension",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"externalDependencies": {
|
|
||||||
"repositories": [
|
|
||||||
{ "name": "Central", "url": "https://repo1.maven.org/maven2/" }
|
|
||||||
],
|
|
||||||
"artifacts": [
|
|
||||||
"mysql:mysql-connector-java:8.0.26"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,7 +6,6 @@ import net.minestom.server.adventure.bossbar.BossBarManager;
|
|||||||
import net.minestom.server.command.CommandManager;
|
import net.minestom.server.command.CommandManager;
|
||||||
import net.minestom.server.event.GlobalEventHandler;
|
import net.minestom.server.event.GlobalEventHandler;
|
||||||
import net.minestom.server.exception.ExceptionManager;
|
import net.minestom.server.exception.ExceptionManager;
|
||||||
import net.minestom.server.extensions.ExtensionManager;
|
|
||||||
import net.minestom.server.gamedata.tags.TagManager;
|
import net.minestom.server.gamedata.tags.TagManager;
|
||||||
import net.minestom.server.instance.InstanceManager;
|
import net.minestom.server.instance.InstanceManager;
|
||||||
import net.minestom.server.instance.block.BlockManager;
|
import net.minestom.server.instance.block.BlockManager;
|
||||||
@ -298,10 +297,6 @@ public final class MinecraftServer {
|
|||||||
return serverProcess.advancement();
|
return serverProcess.advancement();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ExtensionManager getExtensionManager() {
|
|
||||||
return serverProcess.extension();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static TagManager getTagManager() {
|
public static TagManager getTagManager() {
|
||||||
return serverProcess.tag();
|
return serverProcess.tag();
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import net.minestom.server.adventure.bossbar.BossBarManager;
|
|||||||
import net.minestom.server.command.CommandManager;
|
import net.minestom.server.command.CommandManager;
|
||||||
import net.minestom.server.event.GlobalEventHandler;
|
import net.minestom.server.event.GlobalEventHandler;
|
||||||
import net.minestom.server.exception.ExceptionManager;
|
import net.minestom.server.exception.ExceptionManager;
|
||||||
import net.minestom.server.extensions.ExtensionManager;
|
|
||||||
import net.minestom.server.gamedata.tags.TagManager;
|
import net.minestom.server.gamedata.tags.TagManager;
|
||||||
import net.minestom.server.instance.Chunk;
|
import net.minestom.server.instance.Chunk;
|
||||||
import net.minestom.server.instance.InstanceManager;
|
import net.minestom.server.instance.InstanceManager;
|
||||||
@ -96,11 +95,6 @@ public interface ServerProcess extends Snapshotable {
|
|||||||
*/
|
*/
|
||||||
@NotNull BossBarManager bossBar();
|
@NotNull BossBarManager bossBar();
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads and handle extensions.
|
|
||||||
*/
|
|
||||||
@NotNull ExtensionManager extension();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles registry tags.
|
* Handles registry tags.
|
||||||
*/
|
*/
|
||||||
|
@ -9,7 +9,6 @@ import net.minestom.server.event.EventDispatcher;
|
|||||||
import net.minestom.server.event.GlobalEventHandler;
|
import net.minestom.server.event.GlobalEventHandler;
|
||||||
import net.minestom.server.event.server.ServerTickMonitorEvent;
|
import net.minestom.server.event.server.ServerTickMonitorEvent;
|
||||||
import net.minestom.server.exception.ExceptionManager;
|
import net.minestom.server.exception.ExceptionManager;
|
||||||
import net.minestom.server.extensions.ExtensionManager;
|
|
||||||
import net.minestom.server.gamedata.tags.TagManager;
|
import net.minestom.server.gamedata.tags.TagManager;
|
||||||
import net.minestom.server.instance.Chunk;
|
import net.minestom.server.instance.Chunk;
|
||||||
import net.minestom.server.instance.Instance;
|
import net.minestom.server.instance.Instance;
|
||||||
@ -47,7 +46,6 @@ final class ServerProcessImpl implements ServerProcess {
|
|||||||
private final static Logger LOGGER = LoggerFactory.getLogger(ServerProcessImpl.class);
|
private final static Logger LOGGER = LoggerFactory.getLogger(ServerProcessImpl.class);
|
||||||
|
|
||||||
private final ExceptionManager exception;
|
private final ExceptionManager exception;
|
||||||
private final ExtensionManager extension;
|
|
||||||
private final ConnectionManager connection;
|
private final ConnectionManager connection;
|
||||||
private final PacketProcessor packetProcessor;
|
private final PacketProcessor packetProcessor;
|
||||||
private final PacketListenerManager packetListener;
|
private final PacketListenerManager packetListener;
|
||||||
@ -74,7 +72,6 @@ final class ServerProcessImpl implements ServerProcess {
|
|||||||
|
|
||||||
public ServerProcessImpl() throws IOException {
|
public ServerProcessImpl() throws IOException {
|
||||||
this.exception = new ExceptionManager();
|
this.exception = new ExceptionManager();
|
||||||
this.extension = new ExtensionManager(this);
|
|
||||||
this.connection = new ConnectionManager();
|
this.connection = new ConnectionManager();
|
||||||
this.packetProcessor = new PacketProcessor();
|
this.packetProcessor = new PacketProcessor();
|
||||||
this.packetListener = new PacketListenerManager(this);
|
this.packetListener = new PacketListenerManager(this);
|
||||||
@ -162,11 +159,6 @@ final class ServerProcessImpl implements ServerProcess {
|
|||||||
return bossBar;
|
return bossBar;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public @NotNull ExtensionManager extension() {
|
|
||||||
return extension;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NotNull TagManager tag() {
|
public @NotNull TagManager tag() {
|
||||||
return tag;
|
return tag;
|
||||||
@ -208,13 +200,8 @@ final class ServerProcessImpl implements ServerProcess {
|
|||||||
throw new IllegalStateException("Server already started");
|
throw new IllegalStateException("Server already started");
|
||||||
}
|
}
|
||||||
|
|
||||||
extension.start();
|
|
||||||
extension.gotoPreInit();
|
|
||||||
|
|
||||||
LOGGER.info("Starting " + MinecraftServer.getBrandName() + " server.");
|
LOGGER.info("Starting " + MinecraftServer.getBrandName() + " server.");
|
||||||
|
|
||||||
extension.gotoInit();
|
|
||||||
|
|
||||||
// Init server
|
// Init server
|
||||||
try {
|
try {
|
||||||
server.init(socketAddress);
|
server.init(socketAddress);
|
||||||
@ -226,8 +213,6 @@ final class ServerProcessImpl implements ServerProcess {
|
|||||||
// Start server
|
// Start server
|
||||||
server.start();
|
server.start();
|
||||||
|
|
||||||
extension.gotoPostInit();
|
|
||||||
|
|
||||||
LOGGER.info(MinecraftServer.getBrandName() + " server started successfully.");
|
LOGGER.info(MinecraftServer.getBrandName() + " server started successfully.");
|
||||||
|
|
||||||
if (MinecraftServer.isTerminalEnabled()) {
|
if (MinecraftServer.isTerminalEnabled()) {
|
||||||
@ -242,8 +227,6 @@ final class ServerProcessImpl implements ServerProcess {
|
|||||||
if (!stopped.compareAndSet(false, true))
|
if (!stopped.compareAndSet(false, true))
|
||||||
return;
|
return;
|
||||||
LOGGER.info("Stopping " + MinecraftServer.getBrandName() + " server.");
|
LOGGER.info("Stopping " + MinecraftServer.getBrandName() + " server.");
|
||||||
LOGGER.info("Unloading all extensions.");
|
|
||||||
extension.shutdown();
|
|
||||||
scheduler.shutdown();
|
scheduler.shutdown();
|
||||||
connection.shutdown();
|
connection.shutdown();
|
||||||
server.stop();
|
server.stop();
|
||||||
|
@ -1,236 +0,0 @@
|
|||||||
package net.minestom.server.extensions;
|
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import net.minestom.server.utils.validate.Check;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.LinkedList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents an extension from an `extension.json` that is capable of powering an Extension object.
|
|
||||||
*
|
|
||||||
* This has no constructor as its properties are set via GSON.
|
|
||||||
*/
|
|
||||||
public final class DiscoveredExtension {
|
|
||||||
/** Static logger for this class. */
|
|
||||||
public static final Logger LOGGER = LoggerFactory.getLogger(DiscoveredExtension.class);
|
|
||||||
|
|
||||||
/** The regex that this name must pass. If it doesn't, it will not be accepted. */
|
|
||||||
public static final String NAME_REGEX = "[A-Za-z][_A-Za-z0-9]+";
|
|
||||||
|
|
||||||
/** Name of the DiscoveredExtension. Unique for all extensions. */
|
|
||||||
private String name;
|
|
||||||
|
|
||||||
/** Main class of this DiscoveredExtension, must extend Extension. */
|
|
||||||
private String entrypoint;
|
|
||||||
|
|
||||||
/** Version of this extension, highly reccomended to set it. */
|
|
||||||
private String version;
|
|
||||||
|
|
||||||
/** People who have made this extension. */
|
|
||||||
private String[] authors;
|
|
||||||
|
|
||||||
/** List of extension names that this depends on. */
|
|
||||||
private String[] dependencies;
|
|
||||||
|
|
||||||
/** List of Repositories and URLs that this depends on. */
|
|
||||||
private ExternalDependencies externalDependencies;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extra meta on the object.
|
|
||||||
* Do NOT use as configuration:
|
|
||||||
*
|
|
||||||
* Meta is meant to handle properties that will
|
|
||||||
* be accessed by other extensions, not accessed by itself
|
|
||||||
*/
|
|
||||||
private JsonObject meta;
|
|
||||||
|
|
||||||
/** All files of this extension */
|
|
||||||
transient List<URL> files = new LinkedList<>();
|
|
||||||
|
|
||||||
/** The load status of this extension -- LOAD_SUCCESS is the only good one. */
|
|
||||||
transient LoadStatus loadStatus = LoadStatus.LOAD_SUCCESS;
|
|
||||||
|
|
||||||
/** The original jar this is from. */
|
|
||||||
transient private File originalJar;
|
|
||||||
|
|
||||||
transient private Path dataDirectory;
|
|
||||||
|
|
||||||
/** The class loader that powers it. */
|
|
||||||
transient private ExtensionClassLoader classLoader;
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public String getName() {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public String getEntrypoint() {
|
|
||||||
return entrypoint;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public String getVersion() {
|
|
||||||
return version;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public String[] getAuthors() {
|
|
||||||
return authors;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public String[] getDependencies() {
|
|
||||||
return dependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public ExternalDependencies getExternalDependencies() {
|
|
||||||
return externalDependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOriginalJar(@Nullable File file) {
|
|
||||||
originalJar = file;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public File getOriginalJar() {
|
|
||||||
return originalJar;
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NotNull Path getDataDirectory() {
|
|
||||||
return dataDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDataDirectory(@NotNull Path dataDirectory) {
|
|
||||||
this.dataDirectory = dataDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
void createClassLoader() {
|
|
||||||
Check.stateCondition(classLoader != null, "Extension classloader has already been created");
|
|
||||||
final URL[] urls = this.files.toArray(new URL[0]);
|
|
||||||
classLoader = new ExtensionClassLoader(this.getName(), urls, this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public ExtensionClassLoader getClassLoader() {
|
|
||||||
return classLoader;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensures that all properties of this extension are properly set if they aren't
|
|
||||||
*
|
|
||||||
* @param extension The extension to verify
|
|
||||||
*/
|
|
||||||
public static void verifyIntegrity(@NotNull DiscoveredExtension extension) {
|
|
||||||
if (extension.name == null) {
|
|
||||||
StringBuilder fileList = new StringBuilder();
|
|
||||||
for (URL f : extension.files) {
|
|
||||||
fileList.append(f.toExternalForm()).append(", ");
|
|
||||||
}
|
|
||||||
LOGGER.error("Extension with no name. (at {}})", fileList);
|
|
||||||
LOGGER.error("Extension at ({}) will not be loaded.", fileList);
|
|
||||||
extension.loadStatus = DiscoveredExtension.LoadStatus.INVALID_NAME;
|
|
||||||
|
|
||||||
// To ensure @NotNull: name = INVALID_NAME
|
|
||||||
extension.name = extension.loadStatus.name();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!extension.name.matches(NAME_REGEX)) {
|
|
||||||
LOGGER.error("Extension '{}' specified an invalid name.", extension.name);
|
|
||||||
LOGGER.error("Extension '{}' will not be loaded.", extension.name);
|
|
||||||
extension.loadStatus = DiscoveredExtension.LoadStatus.INVALID_NAME;
|
|
||||||
|
|
||||||
// To ensure @NotNull: name = INVALID_NAME
|
|
||||||
extension.name = extension.loadStatus.name();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extension.entrypoint == null) {
|
|
||||||
LOGGER.error("Extension '{}' did not specify an entry point (via 'entrypoint').", extension.name);
|
|
||||||
LOGGER.error("Extension '{}' will not be loaded.", extension.name);
|
|
||||||
extension.loadStatus = DiscoveredExtension.LoadStatus.NO_ENTRYPOINT;
|
|
||||||
|
|
||||||
// To ensure @NotNull: entrypoint = NO_ENTRYPOINT
|
|
||||||
extension.entrypoint = extension.loadStatus.name();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle defaults
|
|
||||||
// If we reach this code, then the extension will most likely be loaded:
|
|
||||||
if (extension.version == null) {
|
|
||||||
LOGGER.warn("Extension '{}' did not specify a version.", extension.name);
|
|
||||||
LOGGER.warn("Extension '{}' will continue to load but should specify a plugin version.", extension.name);
|
|
||||||
extension.version = "Unspecified";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extension.authors == null) {
|
|
||||||
extension.authors = new String[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// No dependencies were specified
|
|
||||||
if (extension.dependencies == null) {
|
|
||||||
extension.dependencies = new String[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// No external dependencies were specified;
|
|
||||||
if (extension.externalDependencies == null) {
|
|
||||||
extension.externalDependencies = new ExternalDependencies();
|
|
||||||
}
|
|
||||||
|
|
||||||
// No meta was provided
|
|
||||||
if (extension.meta == null) {
|
|
||||||
extension.meta = new JsonObject();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public JsonObject getMeta() {
|
|
||||||
return meta;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The status this extension has, all are breakpoints.
|
|
||||||
*
|
|
||||||
* LOAD_SUCCESS is the only valid one.
|
|
||||||
*/
|
|
||||||
enum LoadStatus {
|
|
||||||
LOAD_SUCCESS("Actually, it did not fail. This message should not have been printed."),
|
|
||||||
MISSING_DEPENDENCIES("Missing dependencies, check your logs."),
|
|
||||||
INVALID_NAME("Invalid name."),
|
|
||||||
NO_ENTRYPOINT("No entrypoint specified."),
|
|
||||||
FAILED_TO_SETUP_CLASSLOADER("Extension classloader could not be setup."),
|
|
||||||
LOAD_FAILED("Load failed. See logs for more information."),
|
|
||||||
;
|
|
||||||
|
|
||||||
private final String message;
|
|
||||||
|
|
||||||
LoadStatus(@NotNull String message) {
|
|
||||||
this.message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public String getMessage() {
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class ExternalDependencies {
|
|
||||||
Repository[] repositories = new Repository[0];
|
|
||||||
String[] artifacts = new String[0];
|
|
||||||
|
|
||||||
public static class Repository {
|
|
||||||
String name = "";
|
|
||||||
String url = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,193 +0,0 @@
|
|||||||
package net.minestom.server.extensions;
|
|
||||||
|
|
||||||
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
|
|
||||||
import net.minestom.server.event.Event;
|
|
||||||
import net.minestom.server.event.EventNode;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.nio.file.StandardCopyOption;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
public abstract class Extension {
|
|
||||||
/**
|
|
||||||
* List of extensions that depend on this extension.
|
|
||||||
*/
|
|
||||||
protected final Set<String> dependents = new HashSet<>();
|
|
||||||
|
|
||||||
protected Extension() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void preInitialize() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void initialize();
|
|
||||||
|
|
||||||
public void postInitialize() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void preTerminate() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void terminate();
|
|
||||||
|
|
||||||
public void postTerminate() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
ExtensionClassLoader getExtensionClassLoader() {
|
|
||||||
if (getClass().getClassLoader() instanceof ExtensionClassLoader extensionClassLoader) {
|
|
||||||
return extensionClassLoader;
|
|
||||||
}
|
|
||||||
throw new IllegalStateException("Extension class loader is not an ExtensionClassLoader");
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public DiscoveredExtension getOrigin() {
|
|
||||||
return getExtensionClassLoader().getDiscoveredExtension();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the logger for the extension
|
|
||||||
*
|
|
||||||
* @return The logger for the extension
|
|
||||||
*/
|
|
||||||
@NotNull
|
|
||||||
public ComponentLogger getLogger() {
|
|
||||||
return getExtensionClassLoader().getLogger();
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NotNull EventNode<Event> getEventNode() {
|
|
||||||
return getExtensionClassLoader().getEventNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NotNull Path getDataDirectory() {
|
|
||||||
return getOrigin().getDataDirectory();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a resource from the extension directory, or from inside the jar if it does not
|
|
||||||
* exist in the extension directory.
|
|
||||||
* <p>
|
|
||||||
* If it does not exist in the extension directory, it will be copied from inside the jar.
|
|
||||||
* <p>
|
|
||||||
* The caller is responsible for closing the returned {@link InputStream}.
|
|
||||||
*
|
|
||||||
* @param fileName The file to read
|
|
||||||
* @return The file contents, or null if there was an issue reading the file.
|
|
||||||
*/
|
|
||||||
public @Nullable InputStream getResource(@NotNull String fileName) {
|
|
||||||
return getResource(Paths.get(fileName));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a resource from the extension directory, or from inside the jar if it does not
|
|
||||||
* exist in the extension directory.
|
|
||||||
* <p>
|
|
||||||
* If it does not exist in the extension directory, it will be copied from inside the jar.
|
|
||||||
* <p>
|
|
||||||
* The caller is responsible for closing the returned {@link InputStream}.
|
|
||||||
*
|
|
||||||
* @param target The file to read
|
|
||||||
* @return The file contents, or null if there was an issue reading the file.
|
|
||||||
*/
|
|
||||||
public @Nullable InputStream getResource(@NotNull Path target) {
|
|
||||||
final Path targetFile = getDataDirectory().resolve(target);
|
|
||||||
try {
|
|
||||||
// Copy from jar if the file does not exist in the extension data directory.
|
|
||||||
if (!Files.exists(targetFile)) {
|
|
||||||
savePackagedResource(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Files.newInputStream(targetFile);
|
|
||||||
} catch (IOException ex) {
|
|
||||||
getLogger().info("Failed to read resource {}.", target, ex);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a resource from inside the extension jar.
|
|
||||||
* <p>
|
|
||||||
* The caller is responsible for closing the returned {@link InputStream}.
|
|
||||||
*
|
|
||||||
* @param fileName The file to read
|
|
||||||
* @return The file contents, or null if there was an issue reading the file.
|
|
||||||
*/
|
|
||||||
public @Nullable InputStream getPackagedResource(@NotNull String fileName) {
|
|
||||||
try {
|
|
||||||
final URL url = getOrigin().getClassLoader().getResource(fileName);
|
|
||||||
if (url == null) {
|
|
||||||
getLogger().debug("Resource not found: {}", fileName);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return url.openConnection().getInputStream();
|
|
||||||
} catch (IOException ex) {
|
|
||||||
getLogger().debug("Failed to load resource {}.", fileName, ex);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets a resource from inside the extension jar.
|
|
||||||
* <p>
|
|
||||||
* The caller is responsible for closing the returned {@link InputStream}.
|
|
||||||
*
|
|
||||||
* @param target The file to read
|
|
||||||
* @return The file contents, or null if there was an issue reading the file.
|
|
||||||
*/
|
|
||||||
public @Nullable InputStream getPackagedResource(@NotNull Path target) {
|
|
||||||
return getPackagedResource(target.toString().replace('\\', '/'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies a resource file to the extension directory, replacing any existing copy.
|
|
||||||
*
|
|
||||||
* @param fileName The resource to save
|
|
||||||
* @return True if the resource was saved successfully, null otherwise
|
|
||||||
*/
|
|
||||||
public boolean savePackagedResource(@NotNull String fileName) {
|
|
||||||
return savePackagedResource(Paths.get(fileName));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies a resource file to the extension directory, replacing any existing copy.
|
|
||||||
*
|
|
||||||
* @param target The resource to save
|
|
||||||
* @return True if the resource was saved successfully, null otherwise
|
|
||||||
*/
|
|
||||||
public boolean savePackagedResource(@NotNull Path target) {
|
|
||||||
final Path targetFile = getDataDirectory().resolve(target);
|
|
||||||
try (InputStream is = getPackagedResource(target)) {
|
|
||||||
if (is == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Files.createDirectories(targetFile.getParent());
|
|
||||||
Files.copy(is, targetFile, StandardCopyOption.REPLACE_EXISTING);
|
|
||||||
return true;
|
|
||||||
} catch (IOException ex) {
|
|
||||||
getLogger().debug("Failed to save resource {}.", target, ex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return A modifiable list of dependents.
|
|
||||||
*/
|
|
||||||
public Set<String> getDependents() {
|
|
||||||
return dependents;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,91 +0,0 @@
|
|||||||
package net.minestom.server.extensions;
|
|
||||||
|
|
||||||
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
|
|
||||||
import net.minestom.server.MinecraftServer;
|
|
||||||
import net.minestom.server.event.Event;
|
|
||||||
import net.minestom.server.event.EventNode;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.net.URLClassLoader;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public final class ExtensionClassLoader extends URLClassLoader {
|
|
||||||
private final List<ExtensionClassLoader> children = new ArrayList<>();
|
|
||||||
private final DiscoveredExtension discoveredExtension;
|
|
||||||
private EventNode<Event> eventNode;
|
|
||||||
private ComponentLogger logger;
|
|
||||||
|
|
||||||
public ExtensionClassLoader(String name, URL[] urls, DiscoveredExtension discoveredExtension) {
|
|
||||||
super("Ext_" + name, urls, MinecraftServer.class.getClassLoader());
|
|
||||||
this.discoveredExtension = discoveredExtension;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ExtensionClassLoader(String name, URL[] urls, ClassLoader parent, DiscoveredExtension discoveredExtension) {
|
|
||||||
super("Ext_" + name, urls, parent);
|
|
||||||
this.discoveredExtension = discoveredExtension;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void addURL(@NotNull URL url) {
|
|
||||||
super.addURL(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addChild(@NotNull ExtensionClassLoader loader) {
|
|
||||||
children.add(loader);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
|
||||||
try {
|
|
||||||
return super.loadClass(name, resolve);
|
|
||||||
} catch (ClassNotFoundException e) {
|
|
||||||
for (ExtensionClassLoader child : children) {
|
|
||||||
try {
|
|
||||||
return child.loadClass(name, resolve);
|
|
||||||
} catch (ClassNotFoundException ignored) {}
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public InputStream getResourceAsStreamWithChildren(@NotNull String name) {
|
|
||||||
InputStream in = getResourceAsStream(name);
|
|
||||||
if (in != null) return in;
|
|
||||||
|
|
||||||
for (ExtensionClassLoader child : children) {
|
|
||||||
InputStream childInput = child.getResourceAsStreamWithChildren(name);
|
|
||||||
if (childInput != null)
|
|
||||||
return childInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public DiscoveredExtension getDiscoveredExtension() {
|
|
||||||
return discoveredExtension;
|
|
||||||
}
|
|
||||||
|
|
||||||
public EventNode<Event> getEventNode() {
|
|
||||||
if (eventNode == null) {
|
|
||||||
eventNode = EventNode.all(discoveredExtension.getName());
|
|
||||||
MinecraftServer.getGlobalEventHandler().addChild(eventNode);
|
|
||||||
}
|
|
||||||
return eventNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ComponentLogger getLogger() {
|
|
||||||
if (logger == null) {
|
|
||||||
logger = ComponentLogger.logger(discoveredExtension.getName());
|
|
||||||
}
|
|
||||||
return logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
void terminate() {
|
|
||||||
if (eventNode != null) {
|
|
||||||
MinecraftServer.getGlobalEventHandler().removeChild(eventNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,699 +0,0 @@
|
|||||||
package net.minestom.server.extensions;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import net.minestom.dependencies.DependencyGetter;
|
|
||||||
import net.minestom.dependencies.ResolvedDependency;
|
|
||||||
import net.minestom.dependencies.maven.MavenRepository;
|
|
||||||
import net.minestom.server.ServerProcess;
|
|
||||||
import net.minestom.server.utils.validate.Check;
|
|
||||||
import org.jetbrains.annotations.ApiStatus;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
|
||||||
import org.jetbrains.annotations.Nullable;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.lang.reflect.Constructor;
|
|
||||||
import java.lang.reflect.InvocationTargetException;
|
|
||||||
import java.net.URL;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.*;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
import java.util.zip.ZipEntry;
|
|
||||||
import java.util.zip.ZipFile;
|
|
||||||
|
|
||||||
public class ExtensionManager {
|
|
||||||
|
|
||||||
public final static Logger LOGGER = LoggerFactory.getLogger(ExtensionManager.class);
|
|
||||||
|
|
||||||
public final static String INDEV_CLASSES_FOLDER = "minestom.extension.indevfolder.classes";
|
|
||||||
public final static String INDEV_RESOURCES_FOLDER = "minestom.extension.indevfolder.resources";
|
|
||||||
private final static Gson GSON = new Gson();
|
|
||||||
|
|
||||||
private final ServerProcess serverProcess;
|
|
||||||
|
|
||||||
// LinkedHashMaps are HashMaps that preserve order
|
|
||||||
private final Map<String, Extension> extensions = new LinkedHashMap<>();
|
|
||||||
private final Map<String, Extension> immutableExtensions = Collections.unmodifiableMap(extensions);
|
|
||||||
|
|
||||||
private final File extensionFolder = new File(System.getProperty("minestom.extension.folder", "extensions"));
|
|
||||||
private final File dependenciesFolder = new File(extensionFolder, ".libs");
|
|
||||||
private Path extensionDataRoot = extensionFolder.toPath();
|
|
||||||
|
|
||||||
private enum State {DO_NOT_START, NOT_STARTED, STARTED, PRE_INIT, INIT, POST_INIT}
|
|
||||||
private State state = State.NOT_STARTED;
|
|
||||||
|
|
||||||
public ExtensionManager(ServerProcess serverProcess) {
|
|
||||||
this.serverProcess = serverProcess;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets if the extensions should be loaded during startup.
|
|
||||||
* <p>
|
|
||||||
* Default value is 'true'.
|
|
||||||
*
|
|
||||||
* @return true if extensions are loaded in {@link net.minestom.server.MinecraftServer#start(java.net.SocketAddress)}
|
|
||||||
*/
|
|
||||||
public boolean shouldLoadOnStartup() {
|
|
||||||
return state != State.DO_NOT_START;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to specify if you want extensions to be loaded and initialized during startup.
|
|
||||||
* <p>
|
|
||||||
* Only useful before the server start.
|
|
||||||
*
|
|
||||||
* @param loadOnStartup true to load extensions on startup, false to do nothing
|
|
||||||
*/
|
|
||||||
public void setLoadOnStartup(boolean loadOnStartup) {
|
|
||||||
Check.stateCondition(state.ordinal() > State.NOT_STARTED.ordinal(), "Extensions have already been initialized");
|
|
||||||
this.state = loadOnStartup ? State.NOT_STARTED : State.DO_NOT_START;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public File getExtensionFolder() {
|
|
||||||
return extensionFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NotNull Path getExtensionDataRoot() {
|
|
||||||
return extensionDataRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setExtensionDataRoot(@NotNull Path dataRoot) {
|
|
||||||
this.extensionDataRoot = dataRoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
public Collection<Extension> getExtensions() {
|
|
||||||
return immutableExtensions.values();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public Extension getExtension(@NotNull String name) {
|
|
||||||
return extensions.get(name.toLowerCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasExtension(@NotNull String name) {
|
|
||||||
return extensions.containsKey(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Init phases
|
|
||||||
//
|
|
||||||
|
|
||||||
@ApiStatus.Internal
|
|
||||||
public void start() {
|
|
||||||
if (state == State.DO_NOT_START) {
|
|
||||||
LOGGER.warn("Extension loadOnStartup option is set to false, extensions are therefore neither loaded or initialized.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Check.stateCondition(state != State.NOT_STARTED, "ExtensionManager has already been started");
|
|
||||||
loadExtensions();
|
|
||||||
state = State.STARTED;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiStatus.Internal
|
|
||||||
public void gotoPreInit() {
|
|
||||||
if (state == State.DO_NOT_START) return;
|
|
||||||
Check.stateCondition(state != State.STARTED, "Extensions have already done pre initialization");
|
|
||||||
extensions.values().forEach(Extension::preInitialize);
|
|
||||||
state = State.PRE_INIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiStatus.Internal
|
|
||||||
public void gotoInit() {
|
|
||||||
if (state == State.DO_NOT_START) return;
|
|
||||||
Check.stateCondition(state != State.PRE_INIT, "Extensions have already done initialization");
|
|
||||||
extensions.values().forEach(Extension::initialize);
|
|
||||||
state = State.INIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
@ApiStatus.Internal
|
|
||||||
public void gotoPostInit() {
|
|
||||||
if (state == State.DO_NOT_START) return;
|
|
||||||
Check.stateCondition(state != State.INIT, "Extensions have already done post initialization");
|
|
||||||
extensions.values().forEach(Extension::postInitialize);
|
|
||||||
state = State.POST_INIT;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Loading
|
|
||||||
//
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads all extensions in the extension folder into this server.
|
|
||||||
* <br><br>
|
|
||||||
* <p>
|
|
||||||
* Pipeline:
|
|
||||||
* <br>
|
|
||||||
* Finds all .jar files in the extensions folder.
|
|
||||||
* <br>
|
|
||||||
* Per each jar:
|
|
||||||
* <br>
|
|
||||||
* Turns its extension.json into a DiscoveredExtension object.
|
|
||||||
* <br>
|
|
||||||
* Verifies that all properties of extension.json are correctly set.
|
|
||||||
* <br><br>
|
|
||||||
* <p>
|
|
||||||
* It then sorts all those jars by their load order (making sure that an extension's dependencies load before it)
|
|
||||||
* <br>
|
|
||||||
* Note: Cyclic dependencies will stop both extensions from being loaded.
|
|
||||||
* <br><br>
|
|
||||||
* <p>
|
|
||||||
* Afterwards, it loads all external dependencies and adds them to the extension's files
|
|
||||||
* <br><br>
|
|
||||||
* <p>
|
|
||||||
* Then removes any invalid extensions (Invalid being its Load Status isn't SUCCESS)
|
|
||||||
* <br><br>
|
|
||||||
* <p>
|
|
||||||
* After that, it set its classloaders so each extension is self-contained,
|
|
||||||
* <br><br>
|
|
||||||
* <p>
|
|
||||||
* Removes invalid extensions again,
|
|
||||||
* <br><br>
|
|
||||||
* <p>
|
|
||||||
* and loads all of those extensions into Minestom
|
|
||||||
* <br>
|
|
||||||
* (Extension fields are set via reflection after each extension is verified, then loaded.)
|
|
||||||
* <br><br>
|
|
||||||
* <p>
|
|
||||||
* If the extension successfully loads, add it to the global extension Map (Name to Extension)
|
|
||||||
* <br><br>
|
|
||||||
* <p>
|
|
||||||
* And finally make a scheduler to clean observers per extension.
|
|
||||||
*/
|
|
||||||
private void loadExtensions() {
|
|
||||||
// Initialize folders
|
|
||||||
{
|
|
||||||
// Make extensions folder if necessary
|
|
||||||
if (!extensionFolder.exists()) {
|
|
||||||
if (!extensionFolder.mkdirs()) {
|
|
||||||
LOGGER.error("Could not find or create the extension folder, extensions will not be loaded!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make dependencies folder if necessary
|
|
||||||
if (!dependenciesFolder.exists()) {
|
|
||||||
if (!dependenciesFolder.mkdirs()) {
|
|
||||||
LOGGER.error("Could not find nor create the extension dependencies folder, extensions will not be loaded!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load extensions
|
|
||||||
{
|
|
||||||
// Get all extensions and order them accordingly.
|
|
||||||
List<DiscoveredExtension> discoveredExtensions = discoverExtensions();
|
|
||||||
|
|
||||||
// Don't waste resources on doing extra actions if there is nothing to do.
|
|
||||||
if (discoveredExtensions.isEmpty()) return;
|
|
||||||
|
|
||||||
// Create classloaders for each extension (so that they can be used during dependency resolution)
|
|
||||||
Iterator<DiscoveredExtension> extensionIterator = discoveredExtensions.iterator();
|
|
||||||
while (extensionIterator.hasNext()) {
|
|
||||||
DiscoveredExtension discoveredExtension = extensionIterator.next();
|
|
||||||
try {
|
|
||||||
discoveredExtension.createClassLoader();
|
|
||||||
} catch (Exception e) {
|
|
||||||
discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.FAILED_TO_SETUP_CLASSLOADER;
|
|
||||||
serverProcess.exception().handleException(e);
|
|
||||||
LOGGER.error("Failed to load extension {}", discoveredExtension.getName());
|
|
||||||
LOGGER.error("Failed to load extension", e);
|
|
||||||
extensionIterator.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
discoveredExtensions = generateLoadOrder(discoveredExtensions);
|
|
||||||
loadDependencies(discoveredExtensions);
|
|
||||||
|
|
||||||
// remove invalid extensions
|
|
||||||
discoveredExtensions.removeIf(ext -> ext.loadStatus != DiscoveredExtension.LoadStatus.LOAD_SUCCESS);
|
|
||||||
|
|
||||||
// Load the extensions
|
|
||||||
for (DiscoveredExtension discoveredExtension : discoveredExtensions) {
|
|
||||||
try {
|
|
||||||
loadExtension(discoveredExtension);
|
|
||||||
} catch (Exception e) {
|
|
||||||
discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.LOAD_FAILED;
|
|
||||||
LOGGER.error("Failed to load extension {}", discoveredExtension.getName());
|
|
||||||
serverProcess.exception().handleException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean loadDynamicExtension(@NotNull File jarFile) throws FileNotFoundException {
|
|
||||||
if (!jarFile.exists()) {
|
|
||||||
throw new FileNotFoundException("File '" + jarFile.getAbsolutePath() + "' does not exists. Cannot load extension.");
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGGER.info("Discover dynamic extension from jar {}", jarFile.getAbsolutePath());
|
|
||||||
DiscoveredExtension discoveredExtension = discoverFromJar(jarFile);
|
|
||||||
List<DiscoveredExtension> extensionsToLoad = Collections.singletonList(discoveredExtension);
|
|
||||||
return loadExtensionList(extensionsToLoad);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads an extension into Minestom.
|
|
||||||
*
|
|
||||||
* @param discoveredExtension The extension. Make sure to verify its integrity, set its class loader, and its files.
|
|
||||||
* @return An extension object made from this DiscoveredExtension
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
private Extension loadExtension(@NotNull DiscoveredExtension discoveredExtension) {
|
|
||||||
// Create Extension (authors, version etc.)
|
|
||||||
String extensionName = discoveredExtension.getName();
|
|
||||||
String mainClass = discoveredExtension.getEntrypoint();
|
|
||||||
|
|
||||||
ExtensionClassLoader loader = discoveredExtension.getClassLoader();
|
|
||||||
|
|
||||||
if (extensions.containsKey(extensionName.toLowerCase())) {
|
|
||||||
LOGGER.error("An extension called '{}' has already been registered.", extensionName);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Class<?> jarClass;
|
|
||||||
try {
|
|
||||||
jarClass = Class.forName(mainClass, true, loader);
|
|
||||||
} catch (ClassNotFoundException e) {
|
|
||||||
LOGGER.error("Could not find main class '{}' in extension '{}'.",
|
|
||||||
mainClass, extensionName, e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Class<? extends Extension> extensionClass;
|
|
||||||
try {
|
|
||||||
extensionClass = jarClass.asSubclass(Extension.class);
|
|
||||||
} catch (ClassCastException e) {
|
|
||||||
LOGGER.error("Main class '{}' in '{}' does not extend the 'Extension' superclass.", mainClass, extensionName, e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Constructor<? extends Extension> constructor;
|
|
||||||
try {
|
|
||||||
constructor = extensionClass.getDeclaredConstructor();
|
|
||||||
// Let's just make it accessible, plugin creators don't have to make this public.
|
|
||||||
constructor.setAccessible(true);
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
LOGGER.error("Main class '{}' in '{}' does not define a no-args constructor.", mainClass, extensionName, e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Extension extension = null;
|
|
||||||
try {
|
|
||||||
extension = constructor.newInstance();
|
|
||||||
} catch (InstantiationException e) {
|
|
||||||
LOGGER.error("Main class '{}' in '{}' cannot be an abstract class.", mainClass, extensionName, e);
|
|
||||||
return null;
|
|
||||||
} catch (IllegalAccessException ignored) {
|
|
||||||
// We made it accessible, should not occur
|
|
||||||
} catch (InvocationTargetException e) {
|
|
||||||
LOGGER.error(
|
|
||||||
"While instantiating the main class '{}' in '{}' an exception was thrown.",
|
|
||||||
mainClass,
|
|
||||||
extensionName,
|
|
||||||
e.getTargetException()
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add dependents to pre-existing extensions, so that they can easily be found during reloading
|
|
||||||
for (String dependencyName : discoveredExtension.getDependencies()) {
|
|
||||||
Extension dependency = extensions.get(dependencyName.toLowerCase());
|
|
||||||
if (dependency == null) {
|
|
||||||
LOGGER.warn("Dependency {} of {} is null? This means the extension has been loaded without its dependency, which could cause issues later.", dependencyName, discoveredExtension.getName());
|
|
||||||
} else {
|
|
||||||
dependency.getDependents().add(discoveredExtension.getName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// add to a linked hash map, as they preserve order
|
|
||||||
extensions.put(extensionName.toLowerCase(), extension);
|
|
||||||
|
|
||||||
return extension;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all extensions from the extensions folder and make them discovered.
|
|
||||||
* <p>
|
|
||||||
* It skims the extension folder, discovers and verifies each extension, and returns those created DiscoveredExtensions.
|
|
||||||
*
|
|
||||||
* @return A list of discovered extensions from this folder.
|
|
||||||
*/
|
|
||||||
private @NotNull List<DiscoveredExtension> discoverExtensions() {
|
|
||||||
List<DiscoveredExtension> extensions = new LinkedList<>();
|
|
||||||
|
|
||||||
File[] fileList = extensionFolder.listFiles();
|
|
||||||
|
|
||||||
if (fileList != null) {
|
|
||||||
// Loop through all files in extension folder
|
|
||||||
for (File file : fileList) {
|
|
||||||
|
|
||||||
// Ignore folders
|
|
||||||
if (file.isDirectory()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ignore non .jar files
|
|
||||||
if (!file.getName().endsWith(".jar")) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
DiscoveredExtension extension = discoverFromJar(file);
|
|
||||||
if (extension != null && extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) {
|
|
||||||
extensions.add(extension);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO(mattw): Extract this into its own method to load an extension given classes and resources directory.
|
|
||||||
//TODO(mattw): Should show a warning if one is set and not the other. It is most likely a mistake.
|
|
||||||
|
|
||||||
// this allows developers to have their extension discovered while working on it, without having to build a jar and put in the extension folder
|
|
||||||
if (System.getProperty(INDEV_CLASSES_FOLDER) != null && System.getProperty(INDEV_RESOURCES_FOLDER) != null) {
|
|
||||||
LOGGER.info("Found indev folders for extension. Adding to list of discovered extensions.");
|
|
||||||
final String extensionClasses = System.getProperty(INDEV_CLASSES_FOLDER);
|
|
||||||
final String extensionResources = System.getProperty(INDEV_RESOURCES_FOLDER);
|
|
||||||
try (InputStreamReader reader = new InputStreamReader(new FileInputStream(new File(extensionResources, "extension.json")))) {
|
|
||||||
DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class);
|
|
||||||
extension.files.add(new File(extensionClasses).toURI().toURL());
|
|
||||||
extension.files.add(new File(extensionResources).toURI().toURL());
|
|
||||||
extension.setDataDirectory(getExtensionDataRoot().resolve(extension.getName()));
|
|
||||||
|
|
||||||
// Verify integrity and ensure defaults
|
|
||||||
DiscoveredExtension.verifyIntegrity(extension);
|
|
||||||
|
|
||||||
if (extension.loadStatus == DiscoveredExtension.LoadStatus.LOAD_SUCCESS) {
|
|
||||||
extensions.add(extension);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
serverProcess.exception().handleException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return extensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grabs a discovered extension from a jar.
|
|
||||||
*
|
|
||||||
* @param file The jar to grab it from (a .jar is a formatted .zip file)
|
|
||||||
* @return The created DiscoveredExtension.
|
|
||||||
*/
|
|
||||||
private @Nullable DiscoveredExtension discoverFromJar(@NotNull File file) {
|
|
||||||
try (ZipFile f = new ZipFile(file)) {
|
|
||||||
|
|
||||||
ZipEntry entry = f.getEntry("extension.json");
|
|
||||||
|
|
||||||
if (entry == null)
|
|
||||||
throw new IllegalStateException("Missing extension.json in extension " + file.getName() + ".");
|
|
||||||
|
|
||||||
InputStreamReader reader = new InputStreamReader(f.getInputStream(entry));
|
|
||||||
|
|
||||||
// Initialize DiscoveredExtension from GSON.
|
|
||||||
DiscoveredExtension extension = GSON.fromJson(reader, DiscoveredExtension.class);
|
|
||||||
extension.setOriginalJar(file);
|
|
||||||
extension.files.add(file.toURI().toURL());
|
|
||||||
extension.setDataDirectory(getExtensionDataRoot().resolve(extension.getName()));
|
|
||||||
|
|
||||||
// Verify integrity and ensure defaults
|
|
||||||
DiscoveredExtension.verifyIntegrity(extension);
|
|
||||||
|
|
||||||
return extension;
|
|
||||||
} catch (IOException e) {
|
|
||||||
serverProcess.exception().handleException(e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NotNull
|
|
||||||
private List<DiscoveredExtension> generateLoadOrder(@NotNull List<DiscoveredExtension> discoveredExtensions) {
|
|
||||||
// Extension --> Extensions it depends on.
|
|
||||||
Map<DiscoveredExtension, List<DiscoveredExtension>> dependencyMap = new HashMap<>();
|
|
||||||
|
|
||||||
// Put dependencies in dependency map
|
|
||||||
{
|
|
||||||
Map<String, DiscoveredExtension> extensionMap = new HashMap<>();
|
|
||||||
|
|
||||||
// go through all the discovered extensions and assign their name in a map.
|
|
||||||
for (DiscoveredExtension discoveredExtension : discoveredExtensions) {
|
|
||||||
extensionMap.put(discoveredExtension.getName().toLowerCase(), discoveredExtension);
|
|
||||||
}
|
|
||||||
|
|
||||||
allExtensions:
|
|
||||||
// go through all the discovered extensions and get their dependencies as extensions
|
|
||||||
for (DiscoveredExtension discoveredExtension : discoveredExtensions) {
|
|
||||||
|
|
||||||
List<DiscoveredExtension> dependencies = new ArrayList<>(discoveredExtension.getDependencies().length);
|
|
||||||
|
|
||||||
// Map the dependencies into DiscoveredExtensions.
|
|
||||||
for (String dependencyName : discoveredExtension.getDependencies()) {
|
|
||||||
|
|
||||||
DiscoveredExtension dependencyExtension = extensionMap.get(dependencyName.toLowerCase());
|
|
||||||
// Specifies an extension we don't have.
|
|
||||||
if (dependencyExtension == null) {
|
|
||||||
|
|
||||||
// attempt to see if it is not already loaded (happens with dynamic (re)loading)
|
|
||||||
if (extensions.containsKey(dependencyName.toLowerCase())) {
|
|
||||||
|
|
||||||
dependencies.add(extensions.get(dependencyName.toLowerCase()).getOrigin());
|
|
||||||
continue; // Go to the next loop in this dependency loop, this iteration is done.
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// dependency isn't loaded, move on.
|
|
||||||
LOGGER.error("Extension {} requires an extension called {}.", discoveredExtension.getName(), dependencyName);
|
|
||||||
LOGGER.error("However the extension {} could not be found.", dependencyName);
|
|
||||||
LOGGER.error("Therefore {} will not be loaded.", discoveredExtension.getName());
|
|
||||||
discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.MISSING_DEPENDENCIES;
|
|
||||||
continue allExtensions; // the above labeled loop will go to the next extension as this dependency is invalid.
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// This will add null for an unknown-extension
|
|
||||||
dependencies.add(dependencyExtension);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencyMap.put(
|
|
||||||
discoveredExtension,
|
|
||||||
dependencies
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// List containing the load order.
|
|
||||||
LinkedList<DiscoveredExtension> sortedList = new LinkedList<>();
|
|
||||||
|
|
||||||
// TODO actually have to read this
|
|
||||||
{
|
|
||||||
// entries with empty lists
|
|
||||||
List<Map.Entry<DiscoveredExtension, List<DiscoveredExtension>>> loadableExtensions;
|
|
||||||
|
|
||||||
// While there are entries with no more elements (no more dependencies)
|
|
||||||
while (!(
|
|
||||||
loadableExtensions = dependencyMap.entrySet().stream().filter(entry -> isLoaded(entry.getValue())).toList()
|
|
||||||
).isEmpty()
|
|
||||||
) {
|
|
||||||
// Get all "loadable" (not actually being loaded!) extensions and put them in the sorted list.
|
|
||||||
for (var entry : loadableExtensions) {
|
|
||||||
// Add to sorted list.
|
|
||||||
sortedList.add(entry.getKey());
|
|
||||||
// Remove to make the next iterations a little quicker (hopefully) and to find cyclic dependencies.
|
|
||||||
dependencyMap.remove(entry.getKey());
|
|
||||||
|
|
||||||
// Remove this dependency from all the lists (if they include it) to make way for next level of extensions.
|
|
||||||
for (var dependencies : dependencyMap.values()) {
|
|
||||||
dependencies.remove(entry.getKey());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if there are cyclic extensions.
|
|
||||||
if (!dependencyMap.isEmpty()) {
|
|
||||||
LOGGER.error("Minestom found {} cyclic extensions.", dependencyMap.size());
|
|
||||||
LOGGER.error("Cyclic extensions depend on each other and can therefore not be loaded.");
|
|
||||||
for (var entry : dependencyMap.entrySet()) {
|
|
||||||
DiscoveredExtension discoveredExtension = entry.getKey();
|
|
||||||
LOGGER.error("{} could not be loaded, as it depends on: {}.",
|
|
||||||
discoveredExtension.getName(),
|
|
||||||
entry.getValue().stream().map(DiscoveredExtension::getName).collect(Collectors.joining(", ")));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return sortedList;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if this list of extensions are loaded
|
|
||||||
*
|
|
||||||
* @param extensions The list of extensions to check against.
|
|
||||||
* @return If all of these extensions are loaded.
|
|
||||||
*/
|
|
||||||
private boolean isLoaded(@NotNull List<DiscoveredExtension> extensions) {
|
|
||||||
return
|
|
||||||
extensions.isEmpty() // Don't waste CPU on checking an empty array
|
|
||||||
// Make sure the internal extensions list contains all of these.
|
|
||||||
|| extensions.stream().allMatch(ext -> this.extensions.containsKey(ext.getName().toLowerCase()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void loadDependencies(@NotNull List<DiscoveredExtension> extensions) {
|
|
||||||
List<DiscoveredExtension> allLoadedExtensions = new LinkedList<>(extensions);
|
|
||||||
|
|
||||||
for (Extension extension : immutableExtensions.values())
|
|
||||||
allLoadedExtensions.add(extension.getOrigin());
|
|
||||||
|
|
||||||
for (DiscoveredExtension discoveredExtension : extensions) {
|
|
||||||
try {
|
|
||||||
DependencyGetter getter = new DependencyGetter();
|
|
||||||
DiscoveredExtension.ExternalDependencies externalDependencies = discoveredExtension.getExternalDependencies();
|
|
||||||
List<MavenRepository> repoList = new LinkedList<>();
|
|
||||||
for (var repository : externalDependencies.repositories) {
|
|
||||||
|
|
||||||
if (repository.name == null || repository.name.isEmpty()) {
|
|
||||||
throw new IllegalStateException("Missing 'name' element in repository object.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (repository.url == null || repository.url.isEmpty()) {
|
|
||||||
throw new IllegalStateException("Missing 'url' element in repository object.");
|
|
||||||
}
|
|
||||||
|
|
||||||
repoList.add(new MavenRepository(repository.name, repository.url));
|
|
||||||
}
|
|
||||||
|
|
||||||
getter.addMavenResolver(repoList);
|
|
||||||
|
|
||||||
for (String artifact : externalDependencies.artifacts) {
|
|
||||||
var resolved = getter.get(artifact, dependenciesFolder);
|
|
||||||
addDependencyFile(resolved, discoveredExtension);
|
|
||||||
LOGGER.trace("Dependency of extension {}: {}", discoveredExtension.getName(), resolved);
|
|
||||||
}
|
|
||||||
|
|
||||||
ExtensionClassLoader extensionClassLoader = discoveredExtension.getClassLoader();
|
|
||||||
for (String dependencyName : discoveredExtension.getDependencies()) {
|
|
||||||
var resolved = extensions.stream()
|
|
||||||
.filter(ext -> ext.getName().equalsIgnoreCase(dependencyName))
|
|
||||||
.findFirst()
|
|
||||||
.orElseThrow(() -> new IllegalStateException("Unknown dependency '" + dependencyName + "' of '" + discoveredExtension.getName() + "'"));
|
|
||||||
|
|
||||||
ExtensionClassLoader dependencyClassLoader = resolved.getClassLoader();
|
|
||||||
|
|
||||||
extensionClassLoader.addChild(dependencyClassLoader);
|
|
||||||
LOGGER.trace("Dependency of extension {}: {}", discoveredExtension.getName(), resolved);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
discoveredExtension.loadStatus = DiscoveredExtension.LoadStatus.MISSING_DEPENDENCIES;
|
|
||||||
LOGGER.error("Failed to load dependencies for extension {}", discoveredExtension.getName());
|
|
||||||
LOGGER.error("Extension '{}' will not be loaded", discoveredExtension.getName());
|
|
||||||
LOGGER.error("This is the exception", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addDependencyFile(@NotNull ResolvedDependency dependency, @NotNull DiscoveredExtension extension) {
|
|
||||||
URL location = dependency.getContentsLocation();
|
|
||||||
extension.files.add(location);
|
|
||||||
extension.getClassLoader().addURL(location);
|
|
||||||
LOGGER.trace("Added dependency {} to extension {} classpath", location.toExternalForm(), extension.getName());
|
|
||||||
|
|
||||||
// recurse to add full dependency tree
|
|
||||||
if (!dependency.getSubdependencies().isEmpty()) {
|
|
||||||
LOGGER.trace("Dependency {} has subdependencies, adding...", location.toExternalForm());
|
|
||||||
for (ResolvedDependency sub : dependency.getSubdependencies()) {
|
|
||||||
addDependencyFile(sub, extension);
|
|
||||||
}
|
|
||||||
LOGGER.trace("Dependency {} has had its subdependencies added.", location.toExternalForm());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean loadExtensionList(@NotNull List<DiscoveredExtension> extensionsToLoad) {
|
|
||||||
// ensure correct order of dependencies
|
|
||||||
LOGGER.debug("Reorder extensions to ensure proper load order");
|
|
||||||
extensionsToLoad = generateLoadOrder(extensionsToLoad);
|
|
||||||
loadDependencies(extensionsToLoad);
|
|
||||||
|
|
||||||
// setup new classloaders for the extensions to reload
|
|
||||||
for (DiscoveredExtension toReload : extensionsToLoad) {
|
|
||||||
LOGGER.debug("Setting up classloader for extension {}", toReload.getName());
|
|
||||||
// toReload.setMinestomExtensionClassLoader(toReload.makeClassLoader()); //TODO: Fix this
|
|
||||||
}
|
|
||||||
|
|
||||||
List<Extension> newExtensions = new LinkedList<>();
|
|
||||||
for (DiscoveredExtension toReload : extensionsToLoad) {
|
|
||||||
// reload extensions
|
|
||||||
LOGGER.info("Actually load extension {}", toReload.getName());
|
|
||||||
Extension loadedExtension = loadExtension(toReload);
|
|
||||||
if (loadedExtension != null) {
|
|
||||||
newExtensions.add(loadedExtension);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newExtensions.isEmpty()) {
|
|
||||||
LOGGER.error("No extensions to load, skipping callbacks");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGGER.info("Load complete, firing preinit, init and then postinit callbacks");
|
|
||||||
// retrigger preinit, init and postinit
|
|
||||||
newExtensions.forEach(Extension::preInitialize);
|
|
||||||
newExtensions.forEach(Extension::initialize);
|
|
||||||
newExtensions.forEach(Extension::postInitialize);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Shutdown / Unload
|
|
||||||
//
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shutdowns all the extensions by unloading them.
|
|
||||||
*/
|
|
||||||
public void shutdown() {// copy names, as the extensions map will be modified via the calls to unload
|
|
||||||
Set<String> extensionNames = new HashSet<>(extensions.keySet());
|
|
||||||
for (String ext : extensionNames) {
|
|
||||||
if (extensions.containsKey(ext)) { // is still loaded? Because extensions can depend on one another, it might have already been unloaded
|
|
||||||
unloadExtension(ext);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void unloadExtension(@NotNull String extensionName) {
|
|
||||||
Extension ext = extensions.get(extensionName.toLowerCase());
|
|
||||||
|
|
||||||
if (ext == null) {
|
|
||||||
throw new IllegalArgumentException("Extension " + extensionName + " is not currently loaded.");
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> dependents = new LinkedList<>(ext.getDependents()); // copy dependents list
|
|
||||||
|
|
||||||
for (String dependentID : dependents) {
|
|
||||||
Extension dependentExt = extensions.get(dependentID.toLowerCase());
|
|
||||||
if (dependentExt != null) { // check if extension isn't already unloaded.
|
|
||||||
LOGGER.info("Unloading dependent extension {} (because it depends on {})", dependentID, extensionName);
|
|
||||||
unload(dependentExt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGGER.info("Unloading extension {}", extensionName);
|
|
||||||
unload(ext);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void unload(@NotNull Extension ext) {
|
|
||||||
ext.preTerminate();
|
|
||||||
ext.terminate();
|
|
||||||
|
|
||||||
ext.getExtensionClassLoader().terminate();
|
|
||||||
|
|
||||||
ext.postTerminate();
|
|
||||||
|
|
||||||
// remove from loaded extensions
|
|
||||||
String id = ext.getOrigin().getName().toLowerCase();
|
|
||||||
extensions.remove(id);
|
|
||||||
|
|
||||||
// cleanup classloader
|
|
||||||
// TODO: Is it necessary to remove the CLs since this is only called on shutdown?
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,6 @@ package net.minestom.server.extras.query.response;
|
|||||||
|
|
||||||
import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer;
|
import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer;
|
||||||
import net.minestom.server.MinecraftServer;
|
import net.minestom.server.MinecraftServer;
|
||||||
import net.minestom.server.extensions.Extension;
|
|
||||||
import net.minestom.server.extras.query.Query;
|
import net.minestom.server.extras.query.Query;
|
||||||
import net.minestom.server.utils.binary.BinaryWriter;
|
import net.minestom.server.utils.binary.BinaryWriter;
|
||||||
import net.minestom.server.utils.binary.Writeable;
|
import net.minestom.server.utils.binary.Writeable;
|
||||||
@ -123,17 +122,6 @@ public class FullQueryResponse implements Writeable {
|
|||||||
.append(' ')
|
.append(' ')
|
||||||
.append(MinecraftServer.VERSION_NAME);
|
.append(MinecraftServer.VERSION_NAME);
|
||||||
|
|
||||||
if (!MinecraftServer.getExtensionManager().getExtensions().isEmpty()) {
|
|
||||||
for (Extension extension : MinecraftServer.getExtensionManager().getExtensions()) {
|
|
||||||
builder.append(extension.getOrigin().getName())
|
|
||||||
.append(' ')
|
|
||||||
.append(extension.getOrigin().getVersion())
|
|
||||||
.append("; ");
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.delete(builder.length() - 2, builder.length());
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user