Mirror von
https://github.com/GeyserMC/Geyser.git
synchronisiert 2024-11-20 06:50:09 +01:00
Slight cleanups and make Extension an interface
Dieser Commit ist enthalten in:
Ursprung
142bb95c06
Commit
778f004d99
@ -6,6 +6,7 @@
|
||||
<groupId>org.geysermc</groupId>
|
||||
<artifactId>api-parent</artifactId>
|
||||
<version>2.0.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
@ -14,6 +15,8 @@
|
||||
<properties>
|
||||
<maven.compiler.source>16</maven.compiler.source>
|
||||
<maven.compiler.target>16</maven.compiler.target>
|
||||
|
||||
<adventure.version>4.9.3</adventure.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@ -23,6 +26,12 @@
|
||||
<version>3.19.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>net.kyori</groupId>
|
||||
<artifactId>adventure-api</artifactId>
|
||||
<version>${adventure.version}</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.geysermc</groupId>
|
||||
<artifactId>base-api</artifactId>
|
||||
|
@ -27,8 +27,10 @@ package org.geysermc.geyser.api;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.geysermc.api.Geyser;
|
||||
import org.geysermc.api.GeyserApiBase;
|
||||
import org.geysermc.geyser.api.connection.GeyserConnection;
|
||||
import org.geysermc.geyser.api.extension.ExtensionManager;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@ -78,4 +80,20 @@ public interface GeyserApi extends GeyserApiBase {
|
||||
*/
|
||||
@NonNull
|
||||
List<? extends GeyserConnection> onlineConnections();
|
||||
|
||||
/**
|
||||
* Gets the {@link ExtensionManager}.
|
||||
*
|
||||
* @return the extension manager
|
||||
*/
|
||||
ExtensionManager extensionManager();
|
||||
|
||||
/**
|
||||
* Gets the current {@link GeyserApiBase} instance.
|
||||
*
|
||||
* @return the current geyser api instance
|
||||
*/
|
||||
static GeyserApi api() {
|
||||
return Geyser.api(GeyserApi.class);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* @author GeyserMC
|
||||
* @link https://github.com/GeyserMC/Geyser
|
||||
*/
|
||||
|
||||
package org.geysermc.geyser.api.extension;
|
||||
|
||||
import org.geysermc.api.GeyserApiBase;
|
||||
import org.geysermc.geyser.api.GeyserApi;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* Represents an extension within Geyser.
|
||||
*/
|
||||
public interface Extension {
|
||||
|
||||
/**
|
||||
* Called when the extension is loaded
|
||||
*/
|
||||
default void onLoad() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the extension is enabled
|
||||
*/
|
||||
default void onEnable() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the extension is disabled
|
||||
*/
|
||||
default void onDisable() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets if the extension is enabled
|
||||
*
|
||||
* @return true if the extension is enabled
|
||||
*/
|
||||
default boolean isEnabled() {
|
||||
return this.extensionLoader().isEnabled(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables the extension
|
||||
*
|
||||
* @param enabled if the extension should be enabled
|
||||
*/
|
||||
default void setEnabled(boolean enabled) {
|
||||
this.extensionLoader().setEnabled(this, enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extension's data folder
|
||||
*
|
||||
* @return the extension's data folder
|
||||
*/
|
||||
default Path dataFolder() {
|
||||
return this.extensionLoader().dataFolder(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ExtensionManager}.
|
||||
*
|
||||
* @return the extension manager
|
||||
*/
|
||||
default ExtensionManager extensionManager() {
|
||||
return this.geyserApi().extensionManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extension's name
|
||||
*
|
||||
* @return the extension's name
|
||||
*/
|
||||
default String name() {
|
||||
return this.description().name();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets this extension's {@link ExtensionDescription}.
|
||||
*
|
||||
* @return the extension's description
|
||||
*/
|
||||
default ExtensionDescription description() {
|
||||
return this.extensionLoader().description(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extension's logger
|
||||
*
|
||||
* @return the extension's logger
|
||||
*/
|
||||
default ExtensionLogger logger() {
|
||||
return this.extensionLoader().logger(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link ExtensionLoader}.
|
||||
*
|
||||
* @return the extension loader
|
||||
*/
|
||||
default ExtensionLoader extensionLoader() {
|
||||
return this.extensionManager().extensionLoader(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link GeyserApiBase} instance
|
||||
*
|
||||
* @return the geyser api instance
|
||||
*/
|
||||
default GeyserApi geyserApi() {
|
||||
return GeyserApi.api();
|
||||
}
|
||||
}
|
@ -25,17 +25,21 @@
|
||||
|
||||
package org.geysermc.geyser.api.extension;
|
||||
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This is the Geyser extension description
|
||||
*/
|
||||
public interface ExtensionDescription {
|
||||
|
||||
/**
|
||||
* Gets the extension's name
|
||||
*
|
||||
* @return the extension's name
|
||||
*/
|
||||
@NonNull
|
||||
String name();
|
||||
|
||||
/**
|
||||
@ -43,6 +47,7 @@ public interface ExtensionDescription {
|
||||
*
|
||||
* @return the extension's main class
|
||||
*/
|
||||
@NonNull
|
||||
String main();
|
||||
|
||||
/**
|
||||
@ -50,6 +55,7 @@ public interface ExtensionDescription {
|
||||
*
|
||||
* @return the extension's api version
|
||||
*/
|
||||
@NonNull
|
||||
String apiVersion();
|
||||
|
||||
/**
|
||||
@ -57,6 +63,7 @@ public interface ExtensionDescription {
|
||||
*
|
||||
* @return the extension's description
|
||||
*/
|
||||
@NonNull
|
||||
String version();
|
||||
|
||||
/**
|
||||
@ -64,5 +71,6 @@ public interface ExtensionDescription {
|
||||
*
|
||||
* @return the extension's authors
|
||||
*/
|
||||
@NonNull
|
||||
List<String> authors();
|
||||
}
|
||||
|
@ -25,52 +25,72 @@
|
||||
|
||||
package org.geysermc.geyser.api.extension;
|
||||
|
||||
import org.geysermc.geyser.api.extension.exception.InvalidDescriptionException;
|
||||
import org.geysermc.geyser.api.extension.exception.InvalidExtensionException;
|
||||
import java.io.File;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* The extension loader is responsible for loading, unloading, enabling and disabling extensions
|
||||
*/
|
||||
public interface ExtensionLoader {
|
||||
/**
|
||||
* Loads an extension from a given file
|
||||
*
|
||||
* @param file the file to load the extension from
|
||||
* @return the loaded extension
|
||||
* @throws InvalidExtensionException
|
||||
*/
|
||||
GeyserExtension loadExtension(File file) throws InvalidExtensionException;
|
||||
public abstract class ExtensionLoader {
|
||||
|
||||
/**
|
||||
* Gets an extension's description from a given file
|
||||
* Gets if the given {@link Extension} is enabled.
|
||||
*
|
||||
* @param file the file to get the description from
|
||||
* @return the extension's description
|
||||
* @throws InvalidDescriptionException
|
||||
* @param extension the extension
|
||||
* @return if the extension is enabled
|
||||
*/
|
||||
ExtensionDescription extensionDescription(File file) throws InvalidDescriptionException;
|
||||
protected abstract boolean isEnabled(@NonNull Extension extension);
|
||||
|
||||
/**
|
||||
* Gets a class by its name from the extension's classloader
|
||||
*
|
||||
* @param name the name of the class
|
||||
* @return the class
|
||||
* @throws ClassNotFoundException
|
||||
*/
|
||||
Class<?> classByName(final String name) throws ClassNotFoundException;
|
||||
|
||||
/**
|
||||
* Enables an extension
|
||||
* Sets if the given {@link Extension} is enabled.
|
||||
*
|
||||
* @param extension the extension to enable
|
||||
* @param enabled if the extension should be enabled
|
||||
*/
|
||||
void enableExtension(GeyserExtension extension);
|
||||
protected abstract void setEnabled(@NonNull Extension extension, boolean enabled);
|
||||
|
||||
/**
|
||||
* Disables an extension
|
||||
* Gets the given {@link Extension}'s data folder.
|
||||
*
|
||||
* @param extension the extension to disable
|
||||
* @param extension the extension
|
||||
* @return the data folder of the given extension
|
||||
*/
|
||||
void disableExtension(GeyserExtension extension);
|
||||
}
|
||||
@NonNull
|
||||
protected abstract Path dataFolder(@NonNull Extension extension);
|
||||
|
||||
/**
|
||||
* Gets the given {@link Extension}'s {@link ExtensionDescription}.
|
||||
*
|
||||
* @param extension the extension
|
||||
* @return the description of the given extension
|
||||
*/
|
||||
@NonNull
|
||||
protected abstract ExtensionDescription description(@NonNull Extension extension);
|
||||
|
||||
/**
|
||||
* Gets the {@link ExtensionLogger} for the given {@link Extension}.
|
||||
*
|
||||
* @param extension the extension
|
||||
* @return the extension logger for the given extension
|
||||
*/
|
||||
@NonNull
|
||||
protected abstract ExtensionLogger logger(@NonNull Extension extension);
|
||||
|
||||
/**
|
||||
* Loads all extensions.
|
||||
*
|
||||
* @param extensionManager the extension manager
|
||||
*/
|
||||
protected abstract void loadAllExtensions(@NonNull ExtensionManager extensionManager);
|
||||
|
||||
/**
|
||||
* Registers the given {@link Extension} with the given {@link ExtensionManager}.
|
||||
*
|
||||
* @param extension the extension
|
||||
* @param extensionManager the extension manager
|
||||
*/
|
||||
protected void register(@NonNull Extension extension, @NonNull ExtensionManager extensionManager) {
|
||||
extensionManager.register(extension, this);
|
||||
}
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* @author GeyserMC
|
||||
* @link https://github.com/GeyserMC/Geyser
|
||||
*/
|
||||
|
||||
package org.geysermc.geyser.api.extension;
|
||||
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Manages Geyser {@link Extension}s
|
||||
*/
|
||||
public abstract class ExtensionManager {
|
||||
|
||||
/**
|
||||
* Gets an extension with the given name.
|
||||
*
|
||||
* @param name the name of the extension
|
||||
* @return an extension with the given name
|
||||
*/
|
||||
@Nullable
|
||||
public abstract Extension extension(@NotNull String name);
|
||||
|
||||
/**
|
||||
* Enables the given {@link Extension}.
|
||||
*
|
||||
* @param extension the extension to enable
|
||||
*/
|
||||
public abstract void enable(@NonNull Extension extension);
|
||||
|
||||
/**
|
||||
* Disables the given {@link Extension}.
|
||||
*
|
||||
* @param extension the extension to disable
|
||||
*/
|
||||
public abstract void disable(@NonNull Extension extension);
|
||||
|
||||
/**
|
||||
* Gets the {@link ExtensionLoader} responsible for loading
|
||||
* the given {@link Extension}.
|
||||
*
|
||||
* @return the extension loader for loading the given extension
|
||||
*/
|
||||
@Nullable
|
||||
public abstract ExtensionLoader extensionLoader(@NotNull Extension extension);
|
||||
|
||||
/**
|
||||
* Gets all the {@link Extension}s currently loaded.
|
||||
*
|
||||
* @return all the extensions currently loaded
|
||||
*/
|
||||
@NonNull
|
||||
public abstract Collection<Extension> extensions();
|
||||
|
||||
/**
|
||||
* Gets the {@link ExtensionLoader} with the given identifier.
|
||||
*
|
||||
* @param identifier the identifier
|
||||
* @return the extension loader at the given identifier
|
||||
*/
|
||||
@Nullable
|
||||
public abstract ExtensionLoader extensionLoader(@NonNull Key identifier);
|
||||
|
||||
/**
|
||||
* Registers an {@link ExtensionLoader} with the given identifier.
|
||||
*
|
||||
* @param identifier the identifier
|
||||
* @param extensionLoader the extension loader
|
||||
*/
|
||||
public abstract void registerExtensionLoader(@NonNull Key identifier, @NotNull ExtensionLoader extensionLoader);
|
||||
|
||||
/**
|
||||
* Gets all the currently registered {@link ExtensionLoader}s.
|
||||
*
|
||||
* @return all the currently registered extension loaders
|
||||
*/
|
||||
@NonNull
|
||||
public abstract Map<Key, ExtensionLoader> extensionLoaders();
|
||||
|
||||
/**
|
||||
* Registers an {@link Extension} with the given {@link ExtensionLoader}.
|
||||
*
|
||||
* @param extension the extension
|
||||
* @param loader the loader
|
||||
*/
|
||||
public abstract void register(@NotNull Extension extension, @NotNull ExtensionLoader loader);
|
||||
|
||||
/**
|
||||
* Loads all extensions from the given {@link ExtensionLoader}.
|
||||
*/
|
||||
protected final void loadAllExtensions(@NotNull ExtensionLoader extensionLoader) {
|
||||
extensionLoader.loadAllExtensions(this);
|
||||
}
|
||||
}
|
@ -1,235 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* @author GeyserMC
|
||||
* @link https://github.com/GeyserMC/Geyser
|
||||
*/
|
||||
|
||||
package org.geysermc.geyser.api.extension;
|
||||
|
||||
import org.geysermc.api.GeyserApiBase;
|
||||
import java.io.*;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
|
||||
/**
|
||||
* This class is to be extended by a Geyser extension
|
||||
*/
|
||||
public class GeyserExtension {
|
||||
private boolean initialized = false;
|
||||
private boolean enabled = false;
|
||||
private File file = null;
|
||||
private File dataFolder = null;
|
||||
private ClassLoader classLoader = null;
|
||||
private ExtensionLoader loader = null;
|
||||
private ExtensionLogger logger = null;
|
||||
private ExtensionDescription description = null;
|
||||
private GeyserApiBase api = null;
|
||||
|
||||
/**
|
||||
* Called when the extension is loaded
|
||||
*/
|
||||
public void onLoad() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the extension is enabled
|
||||
*/
|
||||
public void onEnable() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the extension is disabled
|
||||
*/
|
||||
public void onDisable() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets if the extension is enabled
|
||||
*
|
||||
* @return true if the extension is enabled
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables or disables the extension
|
||||
*/
|
||||
public void setEnabled(boolean value) {
|
||||
if (this.enabled != value) {
|
||||
this.enabled = value;
|
||||
if (this.enabled) {
|
||||
onEnable();
|
||||
} else {
|
||||
onDisable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extension's data folder
|
||||
*
|
||||
* @return the extension's data folder
|
||||
*/
|
||||
public File dataFolder() {
|
||||
return this.dataFolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extension's description
|
||||
*
|
||||
* @return the extension's description
|
||||
*/
|
||||
public ExtensionDescription description() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extension's name
|
||||
*
|
||||
* @return the extension's name
|
||||
*/
|
||||
public String name() {
|
||||
return this.description.name();
|
||||
}
|
||||
|
||||
public void init(GeyserApiBase api, ExtensionLoader loader, ExtensionLogger logger, ExtensionDescription description, File dataFolder, File file) {
|
||||
if (!this.initialized) {
|
||||
this.initialized = true;
|
||||
this.file = file;
|
||||
this.dataFolder = dataFolder;
|
||||
this.classLoader = this.getClass().getClassLoader();
|
||||
this.loader = loader;
|
||||
this.logger = logger;
|
||||
this.description = description;
|
||||
this.api = api;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a resource from the extension jar file
|
||||
*
|
||||
* @param filename the file name
|
||||
* @return the input stream
|
||||
*/
|
||||
public InputStream getResource(String filename) {
|
||||
if (filename == null) {
|
||||
throw new IllegalArgumentException("Filename cannot be null");
|
||||
}
|
||||
|
||||
try {
|
||||
URL url = this.classLoader.getResource(filename);
|
||||
|
||||
if (url == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
URLConnection connection = url.openConnection();
|
||||
connection.setUseCaches(false);
|
||||
return connection.getInputStream();
|
||||
} catch (IOException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a resource from the extension jar file to the extension's data folder
|
||||
*
|
||||
* @param filename the file name
|
||||
* @param replace whether to replace the file if it already exists
|
||||
*/
|
||||
public void saveResource(String filename, boolean replace) {
|
||||
if (filename == null || filename.equals("")) {
|
||||
throw new IllegalArgumentException("ResourcePath cannot be null or empty");
|
||||
}
|
||||
|
||||
filename = filename.replace('\\', '/');
|
||||
InputStream in = getResource(filename);
|
||||
if (in == null) {
|
||||
throw new IllegalArgumentException("The embedded resource '" + filename + "' cannot be found in " + file);
|
||||
}
|
||||
|
||||
File outFile = new File(dataFolder, filename);
|
||||
int lastIndex = filename.lastIndexOf('/');
|
||||
File outDir = new File(dataFolder, filename.substring(0, Math.max(lastIndex, 0)));
|
||||
|
||||
if (!outDir.exists()) {
|
||||
outDir.mkdirs();
|
||||
}
|
||||
|
||||
try {
|
||||
if (!outFile.exists() || replace) {
|
||||
OutputStream out = new FileOutputStream(outFile);
|
||||
byte[] buf = new byte[1024];
|
||||
int len;
|
||||
while ((len = in.read(buf)) > 0) {
|
||||
out.write(buf, 0, len);
|
||||
}
|
||||
out.close();
|
||||
in.close();
|
||||
} else {
|
||||
this.logger.warning("Could not save " + outFile.getName() + " to " + outFile + " because " + outFile.getName() + " already exists.");
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
this.logger.severe("Could not save " + outFile.getName() + " to " + outFile, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extension's class loader
|
||||
*
|
||||
* @return the extension's class loader
|
||||
*/
|
||||
public ClassLoader classLoader() {
|
||||
return this.classLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extension's loader
|
||||
*
|
||||
* @return the extension's loader
|
||||
*/
|
||||
public ExtensionLoader extensionLoader() {
|
||||
return this.loader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the extension's logger
|
||||
*
|
||||
* @return the extension's logger
|
||||
*/
|
||||
public ExtensionLogger logger() {
|
||||
return this.logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the {@link GeyserApiBase} instance
|
||||
*
|
||||
* @return the {@link GeyserApiBase} instance
|
||||
*/
|
||||
public GeyserApiBase geyserApi() {
|
||||
return this.api;
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@
|
||||
<groupId>org.geysermc</groupId>
|
||||
<artifactId>geyser-parent</artifactId>
|
||||
<version>2.0.0-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>api-parent</artifactId>
|
||||
|
@ -11,7 +11,6 @@
|
||||
<artifactId>core</artifactId>
|
||||
|
||||
<properties>
|
||||
<adventure.version>4.9.3</adventure.version>
|
||||
<fastutil.version>8.5.2</fastutil.version>
|
||||
<jackson.version>2.12.4</jackson.version>
|
||||
<netty.version>4.1.66.Final</netty.version>
|
||||
|
@ -157,8 +157,9 @@ public class GeyserImpl implements GeyserApi {
|
||||
MessageTranslator.init();
|
||||
MinecraftLocale.init();
|
||||
|
||||
extensionManager = new GeyserExtensionManager();
|
||||
extensionManager.init();
|
||||
/* Load Extensions */
|
||||
this.extensionManager = new GeyserExtensionManager();
|
||||
this.extensionManager.init();
|
||||
|
||||
start();
|
||||
|
||||
@ -203,7 +204,7 @@ public class GeyserImpl implements GeyserApi {
|
||||
|
||||
ResourcePack.loadPacks();
|
||||
|
||||
extensionManager.enableExtensions();
|
||||
this.extensionManager.enableExtensions();
|
||||
|
||||
if (platformType != PlatformType.STANDALONE && config.getRemote().getAddress().equals("auto")) {
|
||||
// Set the remote address to localhost since that is where we are always connecting
|
||||
@ -465,7 +466,7 @@ public class GeyserImpl implements GeyserApi {
|
||||
|
||||
ResourcePack.PACKS.clear();
|
||||
|
||||
extensionManager.disableExtensions();
|
||||
this.extensionManager.disableExtensions();
|
||||
|
||||
bootstrap.getGeyserLogger().info(GeyserLocale.getLocaleStringLog("geyser.core.shutdown.done"));
|
||||
}
|
||||
@ -488,6 +489,11 @@ public class GeyserImpl implements GeyserApi {
|
||||
return !"DEV".equals(GeyserImpl.VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GeyserExtensionManager extensionManager() {
|
||||
return this.extensionManager;
|
||||
}
|
||||
|
||||
public static GeyserImpl start(PlatformType platformType, GeyserBootstrap bootstrap) {
|
||||
if (instance == null) {
|
||||
return new GeyserImpl(platformType, bootstrap);
|
||||
@ -518,10 +524,6 @@ public class GeyserImpl implements GeyserApi {
|
||||
return bootstrap.getWorldManager();
|
||||
}
|
||||
|
||||
public GeyserExtensionManager getExtensionManager() {
|
||||
return extensionManager;
|
||||
}
|
||||
|
||||
public static GeyserImpl getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
@ -36,8 +36,8 @@ import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
import org.geysermc.geyser.api.extension.GeyserExtension;
|
||||
import org.geysermc.geyser.extension.GeyserExtensionManager;
|
||||
import org.geysermc.geyser.api.GeyserApi;
|
||||
import org.geysermc.geyser.api.extension.Extension;
|
||||
import org.geysermc.geyser.text.AsteriskSerializer;
|
||||
import org.geysermc.geyser.configuration.GeyserConfiguration;
|
||||
import org.geysermc.geyser.network.MinecraftProtocol;
|
||||
@ -127,7 +127,7 @@ public class DumpInfo {
|
||||
this.flagsInfo = new FlagsInfo();
|
||||
|
||||
this.extensionInfo = new ArrayList<>();
|
||||
for (GeyserExtension extension : GeyserImpl.getInstance().getExtensionManager().getExtensions().values()) {
|
||||
for (Extension extension : GeyserApi.api().extensionManager().extensions()) {
|
||||
this.extensionInfo.add(new ExtensionInfo(extension.isEnabled(), extension.name(), extension.description().version(), extension.description().apiVersion(), extension.description().main(), extension.description().authors()));
|
||||
}
|
||||
}
|
||||
|
@ -25,24 +25,25 @@
|
||||
|
||||
package org.geysermc.geyser.extension;
|
||||
|
||||
import org.geysermc.geyser.api.extension.Extension;
|
||||
import org.geysermc.geyser.api.extension.ExtensionDescription;
|
||||
import org.geysermc.geyser.api.extension.GeyserExtension;
|
||||
import org.geysermc.geyser.api.extension.exception.InvalidExtensionException;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLClassLoader;
|
||||
import java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class GeyserExtensionClassLoader extends URLClassLoader {
|
||||
private final GeyserExtensionLoader loader;
|
||||
private final Map<String, Class> classes = new HashMap<>();
|
||||
public GeyserExtension extension;
|
||||
private final Map<String, Class<?>> classes = new HashMap<>();
|
||||
private final Extension extension;
|
||||
|
||||
public GeyserExtensionClassLoader(GeyserExtensionLoader loader, ClassLoader parent, ExtensionDescription description, File file) throws InvalidExtensionException, MalformedURLException {
|
||||
super(new URL[] { file.toURI().toURL() }, parent);
|
||||
public GeyserExtensionClassLoader(GeyserExtensionLoader loader, ClassLoader parent, ExtensionDescription description, Path path) throws InvalidExtensionException, MalformedURLException {
|
||||
super(new URL[] { path.toUri().toURL() }, parent);
|
||||
this.loader = loader;
|
||||
|
||||
try {
|
||||
@ -53,15 +54,15 @@ public class GeyserExtensionClassLoader extends URLClassLoader {
|
||||
throw new InvalidExtensionException("Class " + description.main() + " not found, extension cannot be loaded", ex);
|
||||
}
|
||||
|
||||
Class<? extends GeyserExtension> extensionClass;
|
||||
Class<? extends Extension> extensionClass;
|
||||
try {
|
||||
extensionClass = jarClass.asSubclass(GeyserExtension.class);
|
||||
extensionClass = jarClass.asSubclass(Extension.class);
|
||||
} catch (ClassCastException ex) {
|
||||
throw new InvalidExtensionException("Main class " + description.main() + " should extends GeyserExtension, but extends " + jarClass.getSuperclass().getSimpleName(), ex);
|
||||
}
|
||||
|
||||
extension = extensionClass.newInstance();
|
||||
} catch (IllegalAccessException ex) {
|
||||
this.extension = extensionClass.getConstructor().newInstance();
|
||||
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException ex) {
|
||||
throw new InvalidExtensionException("No public constructor", ex);
|
||||
} catch (InstantiationException ex) {
|
||||
throw new InvalidExtensionException("Abnormal extension type", ex);
|
||||
@ -74,26 +75,28 @@ public class GeyserExtensionClassLoader extends URLClassLoader {
|
||||
}
|
||||
|
||||
protected Class<?> findClass(String name, boolean checkGlobal) throws ClassNotFoundException {
|
||||
if (name.startsWith("org.geysermc.geyser.") || name.startsWith("org.geysermc.connector.") || name.startsWith("org.geysermc.platform.") || name.startsWith("org.geysermc.floodgate.") || name.startsWith("org.geysermc.api.") || name.startsWith("org.geysermc.processor.") || name.startsWith("net.minecraft.")) {
|
||||
if (name.startsWith("org.geysermc.geyser.") || name.startsWith("net.minecraft.")) {
|
||||
throw new ClassNotFoundException(name);
|
||||
}
|
||||
Class<?> result = classes.get(name);
|
||||
Class<?> result = this.classes.get(name);
|
||||
if (result == null) {
|
||||
if (checkGlobal) {
|
||||
result = loader.classByName(name);
|
||||
result = this.loader.classByName(name);
|
||||
}
|
||||
|
||||
if (result == null) {
|
||||
result = super.findClass(name);
|
||||
if (result != null) {
|
||||
loader.setClass(name, result);
|
||||
this.loader.setClass(name, result);
|
||||
}
|
||||
}
|
||||
classes.put(name, result);
|
||||
|
||||
this.classes.put(name, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Set<String> getClasses() {
|
||||
return classes.keySet();
|
||||
public Extension extension() {
|
||||
return this.extension;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (c) 2019-2022 GeyserMC. http://geysermc.org
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* @author GeyserMC
|
||||
* @link https://github.com/GeyserMC/Geyser
|
||||
*/
|
||||
|
||||
package org.geysermc.geyser.extension;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.geysermc.geyser.api.extension.Extension;
|
||||
import org.geysermc.geyser.api.extension.ExtensionDescription;
|
||||
import org.geysermc.geyser.api.extension.ExtensionLoader;
|
||||
import org.geysermc.geyser.api.extension.ExtensionLogger;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
@Accessors(fluent = true)
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public class GeyserExtensionContainer {
|
||||
private final Extension extension;
|
||||
private final Path dataFolder;
|
||||
private final ExtensionDescription description;
|
||||
private final ExtensionLoader loader;
|
||||
private final ExtensionLogger logger;
|
||||
|
||||
@Getter(AccessLevel.NONE) protected boolean enabled;
|
||||
}
|
@ -25,78 +25,52 @@
|
||||
|
||||
package org.geysermc.geyser.extension;
|
||||
|
||||
import org.geysermc.geyser.api.extension.ExtensionDescription;
|
||||
import org.geysermc.geyser.api.extension.exception.InvalidDescriptionException;
|
||||
import org.yaml.snakeyaml.DumperOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
import java.io.InputStream;
|
||||
import java.io.Reader;
|
||||
import java.util.*;
|
||||
|
||||
public class GeyserExtensionDescription implements org.geysermc.geyser.api.extension.ExtensionDescription {
|
||||
private String name;
|
||||
private String main;
|
||||
private String api;
|
||||
private String version;
|
||||
private final List<String> authors = new ArrayList<>();
|
||||
|
||||
public GeyserExtensionDescription(InputStream inputStream) throws InvalidDescriptionException {
|
||||
public record GeyserExtensionDescription(String name, String main, String apiVersion, String version, List<String> authors) implements ExtensionDescription {
|
||||
@SuppressWarnings("unchecked")
|
||||
public static GeyserExtensionDescription fromYaml(Reader reader) throws InvalidDescriptionException {
|
||||
DumperOptions dumperOptions = new DumperOptions();
|
||||
dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
|
||||
Yaml yaml = new Yaml(dumperOptions);
|
||||
this.loadMap(yaml.loadAs(inputStream, LinkedHashMap.class));
|
||||
}
|
||||
|
||||
private void loadMap(Map<String, Object> yamlMap) throws InvalidDescriptionException {
|
||||
this.name = ((String) yamlMap.get("name")).replaceAll("[^A-Za-z0-9 _.-]", "");
|
||||
if (this.name.equals("")) {
|
||||
Yaml yaml = new Yaml(dumperOptions);
|
||||
Map<String, Object> yamlMap = yaml.loadAs(reader, LinkedHashMap.class);
|
||||
|
||||
String name = ((String) yamlMap.get("name")).replaceAll("[^A-Za-z0-9 _.-]", "");
|
||||
if (name.isBlank()) {
|
||||
throw new InvalidDescriptionException("Invalid extension name, cannot be empty");
|
||||
}
|
||||
this.name = this.name.replace(" ", "_");
|
||||
this.version = String.valueOf(yamlMap.get("version"));
|
||||
this.main = (String) yamlMap.get("main");
|
||||
|
||||
name = name.replace(" ", "_");
|
||||
String version = String.valueOf(yamlMap.get("version"));
|
||||
String main = (String) yamlMap.get("main");
|
||||
String apiVersion;
|
||||
|
||||
Object api = yamlMap.get("api");
|
||||
if (api instanceof String) {
|
||||
this.api = (String) api;
|
||||
apiVersion = (String) api;
|
||||
} else {
|
||||
this.api = "0.0.0";
|
||||
throw new InvalidDescriptionException("Invalid api version format, should be a string: major.minor.patch");
|
||||
}
|
||||
|
||||
List<String> authors = new ArrayList<>();
|
||||
if (yamlMap.containsKey("author")) {
|
||||
this.authors.add((String) yamlMap.get("author"));
|
||||
authors.add((String) yamlMap.get("author"));
|
||||
}
|
||||
|
||||
if (yamlMap.containsKey("authors")) {
|
||||
try {
|
||||
this.authors.addAll((Collection<? extends String>) yamlMap.get("authors"));
|
||||
authors.addAll((Collection<? extends String>) yamlMap.get("authors"));
|
||||
} catch (Exception e) {
|
||||
throw new InvalidDescriptionException("Invalid authors format, should be a list of strings", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String main() {
|
||||
return this.main;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String apiVersion() {
|
||||
return api;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String version() {
|
||||
return this.version;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> authors() {
|
||||
return this.authors;
|
||||
return new GeyserExtensionDescription(name, main, apiVersion, version, authors);
|
||||
}
|
||||
}
|
||||
|
@ -25,95 +25,81 @@
|
||||
|
||||
package org.geysermc.geyser.extension;
|
||||
|
||||
import org.geysermc.api.Geyser;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
import org.geysermc.geyser.api.extension.Extension;
|
||||
import org.geysermc.geyser.api.extension.ExtensionDescription;
|
||||
import org.geysermc.geyser.api.extension.ExtensionLoader;
|
||||
import org.geysermc.geyser.api.extension.GeyserExtension;
|
||||
import org.geysermc.geyser.api.extension.ExtensionLogger;
|
||||
import org.geysermc.geyser.api.extension.ExtensionManager;
|
||||
import org.geysermc.geyser.api.extension.exception.InvalidDescriptionException;
|
||||
import org.geysermc.geyser.api.extension.exception.InvalidExtensionException;
|
||||
import org.geysermc.geyser.text.GeyserLocale;
|
||||
import java.io.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileSystem;
|
||||
import java.nio.file.FileSystems;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.NoSuchFileException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class GeyserExtensionLoader implements ExtensionLoader {
|
||||
private final Map<String, Class> classes = new HashMap<>();
|
||||
@RequiredArgsConstructor
|
||||
public class GeyserExtensionLoader extends ExtensionLoader {
|
||||
private static final Path EXTENSION_DIRECTORY = Paths.get("extensions");
|
||||
private static final Pattern API_VERSION_PATTERN = Pattern.compile("^[0-9]+\\.[0-9]+\\.[0-9]+$");
|
||||
|
||||
private final Map<String, Class<?>> classes = new HashMap<>();
|
||||
private final Map<String, GeyserExtensionClassLoader> classLoaders = new HashMap<>();
|
||||
private final Map<Extension, GeyserExtensionContainer> extensionContainers = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public GeyserExtension loadExtension(File file) throws InvalidExtensionException {
|
||||
if (file == null) {
|
||||
throw new InvalidExtensionException("File is null");
|
||||
public GeyserExtensionContainer loadExtension(Path path) throws InvalidExtensionException, InvalidDescriptionException {
|
||||
if (path == null) {
|
||||
throw new InvalidExtensionException("Path is null");
|
||||
}
|
||||
|
||||
if (!file.exists()) {
|
||||
throw new InvalidExtensionException(new FileNotFoundException(file.getPath()) + " does not exist");
|
||||
if (Files.notExists(path)) {
|
||||
throw new InvalidExtensionException(new NoSuchFileException(path.toString()) + " does not exist");
|
||||
}
|
||||
|
||||
final GeyserExtensionDescription description;
|
||||
try {
|
||||
description = extensionDescription(file);
|
||||
} catch (InvalidDescriptionException e) {
|
||||
throw new InvalidExtensionException(e);
|
||||
}
|
||||
|
||||
final File parentFile = file.getParentFile();
|
||||
final File dataFolder = new File(parentFile, description.name());
|
||||
if (dataFolder.exists() && !dataFolder.isDirectory()) {
|
||||
throw new InvalidExtensionException("The folder " + dataFolder.getPath() + " is not a directory and is the data folder for the extension " + description.name() + "!");
|
||||
GeyserExtensionDescription description = this.extensionDescription(path);
|
||||
Path parentFile = path.getParent();
|
||||
Path dataFolder = parentFile.resolve(description.name());
|
||||
if (Files.exists(dataFolder) && !Files.isDirectory(dataFolder)) {
|
||||
throw new InvalidExtensionException("The folder " + dataFolder + " is not a directory and is the data folder for the extension " + description.name() + "!");
|
||||
}
|
||||
|
||||
final GeyserExtensionClassLoader loader;
|
||||
try {
|
||||
loader = new GeyserExtensionClassLoader(this, getClass().getClassLoader(), description, file);
|
||||
loader = new GeyserExtensionClassLoader(this, getClass().getClassLoader(), description, path);
|
||||
} catch (Throwable e) {
|
||||
throw new InvalidExtensionException(e);
|
||||
}
|
||||
classLoaders.put(description.name(), loader);
|
||||
|
||||
setup(loader.extension, description, dataFolder, file);
|
||||
return loader.extension;
|
||||
this.classLoaders.put(description.name(), loader);
|
||||
return this.setup(loader.extension(), description, dataFolder);
|
||||
}
|
||||
|
||||
private void setup(GeyserExtension extension, GeyserExtensionDescription description, File dataFolder, File file) {
|
||||
private GeyserExtensionContainer setup(Extension extension, GeyserExtensionDescription description, Path dataFolder) {
|
||||
GeyserExtensionLogger logger = new GeyserExtensionLogger(GeyserImpl.getInstance().getLogger(), description.name());
|
||||
extension.init(Geyser.api(), this, logger, description, dataFolder, file);
|
||||
GeyserExtensionContainer container = new GeyserExtensionContainer(extension, dataFolder, description, this, logger);
|
||||
extension.onLoad();
|
||||
return container;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GeyserExtensionDescription extensionDescription(File file) throws InvalidDescriptionException {
|
||||
JarFile jarFile = null;
|
||||
InputStream stream = null;
|
||||
|
||||
try {
|
||||
jarFile = new JarFile(file);
|
||||
|
||||
JarEntry descriptionEntry = jarFile.getJarEntry("extension.yml");
|
||||
if (descriptionEntry == null) {
|
||||
throw new InvalidDescriptionException(new FileNotFoundException("extension.yml") + " does not exist in the jar file!");
|
||||
}
|
||||
|
||||
stream = jarFile.getInputStream(descriptionEntry);
|
||||
return new GeyserExtensionDescription(stream);
|
||||
} catch (IOException e) {
|
||||
throw new InvalidDescriptionException(e);
|
||||
} finally {
|
||||
if (jarFile != null) {
|
||||
try {
|
||||
jarFile.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
if (stream != null) {
|
||||
try {
|
||||
stream.close();
|
||||
} catch (IOException e) {
|
||||
}
|
||||
}
|
||||
public GeyserExtensionDescription extensionDescription(Path path) throws InvalidDescriptionException {
|
||||
Map<String, String> environment = new HashMap<>();
|
||||
try (FileSystem fileSystem = FileSystems.newFileSystem(path, environment, null)) {
|
||||
Path extensionYml = fileSystem.getPath("extension.yml");
|
||||
return GeyserExtensionDescription.fromYaml(Files.newBufferedReader(extensionYml));
|
||||
} catch (IOException ex) {
|
||||
throw new InvalidDescriptionException("Failed to load extension description for " + path, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,14 +107,13 @@ public class GeyserExtensionLoader implements ExtensionLoader {
|
||||
return new Pattern[] { Pattern.compile("^.+\\.jar$") };
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> classByName(final String name) throws ClassNotFoundException{
|
||||
Class<?> clazz = classes.get(name);
|
||||
Class<?> clazz = this.classes.get(name);
|
||||
try {
|
||||
for(GeyserExtensionClassLoader loader : classLoaders.values()) {
|
||||
for(GeyserExtensionClassLoader loader : this.classLoaders.values()) {
|
||||
try {
|
||||
clazz = loader.findClass(name,false);
|
||||
} catch(NullPointerException e) {
|
||||
} catch(NullPointerException ignored) {
|
||||
}
|
||||
}
|
||||
return clazz;
|
||||
@ -138,28 +123,132 @@ public class GeyserExtensionLoader implements ExtensionLoader {
|
||||
}
|
||||
|
||||
void setClass(String name, final Class<?> clazz) {
|
||||
if (!classes.containsKey(name)) {
|
||||
classes.put(name,clazz);
|
||||
}
|
||||
}
|
||||
|
||||
void removeClass(String name) {
|
||||
classes.remove(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enableExtension(GeyserExtension extension) {
|
||||
if (!extension.isEnabled()) {
|
||||
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.enable.success", extension.description().name()));
|
||||
extension.setEnabled(true);
|
||||
if (!this.classes.containsKey(name)) {
|
||||
this.classes.put(name,clazz);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void disableExtension(GeyserExtension extension) {
|
||||
if (extension.isEnabled()) {
|
||||
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.disable.success", extension.description().name()));
|
||||
extension.setEnabled(false);
|
||||
protected void loadAllExtensions(@NonNull ExtensionManager extensionManager) {
|
||||
// noinspection ConstantConditions
|
||||
if (GeyserImpl.VERSION.equalsIgnoreCase("dev")) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_dev_environment"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!GeyserImpl.VERSION.contains(".")) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_version_number"));
|
||||
return;
|
||||
}
|
||||
|
||||
String[] apiVersion = GeyserImpl.VERSION.split("\\.");
|
||||
|
||||
try {
|
||||
if (Files.notExists(EXTENSION_DIRECTORY)) {
|
||||
Files.createDirectory(EXTENSION_DIRECTORY);
|
||||
}
|
||||
|
||||
Map<String, Path> extensions = new LinkedHashMap<>();
|
||||
Map<String, GeyserExtensionContainer> loadedExtensions = new LinkedHashMap<>();
|
||||
|
||||
Pattern[] extensionFilters = this.extensionFilters();
|
||||
|
||||
Files.walk(EXTENSION_DIRECTORY).forEach(path -> {
|
||||
if (Files.isDirectory(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Pattern filter : extensionFilters) {
|
||||
if (!filter.matcher(path.getFileName().toString()).matches()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
ExtensionDescription description = this.extensionDescription(path);
|
||||
if (description == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String name = description.name();
|
||||
if (extensions.containsKey(name) || extensionManager.extension(name) != null) {
|
||||
GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.extensions.load.duplicate", name, path.toString()));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check the format: majorVersion.minorVersion.patch
|
||||
if (!API_VERSION_PATTERN.matcher(description.apiVersion()).matches()) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
} catch (NullPointerException | IllegalArgumentException e) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_format", name, apiVersion[0] + "." + apiVersion[1]));
|
||||
return;
|
||||
}
|
||||
|
||||
String[] versionArray = description.apiVersion().split("\\.");
|
||||
|
||||
// Completely different API version
|
||||
if (!Objects.equals(Integer.valueOf(versionArray[0]), Integer.valueOf(apiVersion[0]))) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, apiVersion[0] + "." + apiVersion[1]));
|
||||
return;
|
||||
}
|
||||
|
||||
// If the extension requires new API features, being backwards compatible
|
||||
if (Integer.parseInt(versionArray[1]) > Integer.parseInt(apiVersion[1])) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, apiVersion[0] + "." + apiVersion[1]));
|
||||
return;
|
||||
}
|
||||
|
||||
extensions.put(name, path);
|
||||
loadedExtensions.put(name, this.loadExtension(path));
|
||||
} catch (Exception e) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", path.getFileName(), path.toAbsolutePath()), e);
|
||||
}
|
||||
});
|
||||
|
||||
for (GeyserExtensionContainer container : loadedExtensions.values()) {
|
||||
this.extensionContainers.put(container.extension(), container);
|
||||
this.register(container.extension(), extensionManager);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isEnabled(@NonNull Extension extension) {
|
||||
return this.extensionContainers.get(extension).enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setEnabled(@NonNull Extension extension, boolean enabled) {
|
||||
boolean isEnabled = this.extensionContainers.get(extension).enabled;
|
||||
if (isEnabled != enabled) {
|
||||
this.extensionContainers.get(extension).enabled = enabled;
|
||||
if (enabled) {
|
||||
extension.onEnable();
|
||||
} else {
|
||||
extension.onDisable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Path dataFolder(@NonNull Extension extension) {
|
||||
return this.extensionContainers.get(extension).dataFolder();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected ExtensionDescription description(@NonNull Extension extension) {
|
||||
return this.extensionContainers.get(extension).description();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected ExtensionLogger logger(@NonNull Extension extension) {
|
||||
return this.extensionContainers.get(extension).logger();
|
||||
}
|
||||
}
|
||||
|
@ -25,205 +25,126 @@
|
||||
|
||||
package org.geysermc.geyser.extension;
|
||||
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.checker.nullness.qual.Nullable;
|
||||
import org.geysermc.geyser.GeyserImpl;
|
||||
import org.geysermc.geyser.api.extension.ExtensionDescription;
|
||||
import org.geysermc.geyser.api.extension.GeyserExtension;
|
||||
import org.geysermc.geyser.api.extension.Extension;
|
||||
import org.geysermc.geyser.api.extension.ExtensionManager;
|
||||
import org.geysermc.geyser.api.extension.ExtensionLoader;
|
||||
import org.geysermc.geyser.registry.Registries;
|
||||
import org.geysermc.geyser.text.GeyserLocale;
|
||||
import java.io.File;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class GeyserExtensionManager {
|
||||
protected Map<String, GeyserExtension> extensions = new LinkedHashMap<>();
|
||||
protected Map<Pattern, GeyserExtensionLoader> fileAssociations = new HashMap<>();
|
||||
import java.util.*;
|
||||
|
||||
public class GeyserExtensionManager extends ExtensionManager {
|
||||
private static final Key BASE_KEY = Key.key("geysermc", "base");
|
||||
|
||||
private final Map<String, Extension> extensions = new LinkedHashMap<>();
|
||||
private final Map<Extension, ExtensionLoader> extensionsLoaders = new LinkedHashMap<>();
|
||||
|
||||
public void init() {
|
||||
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.load.loading"));
|
||||
|
||||
this.registerInterface(GeyserExtensionLoader.class);
|
||||
this.loadExtensions(new File("extensions"));
|
||||
this.registerExtensionLoader(BASE_KEY, new GeyserExtensionLoader());
|
||||
|
||||
for (ExtensionLoader loader : this.extensionLoaders().values()) {
|
||||
this.loadAllExtensions(loader);
|
||||
}
|
||||
|
||||
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.load.done", this.extensions.size()));
|
||||
}
|
||||
|
||||
public GeyserExtension getExtension(String name) {
|
||||
@Override
|
||||
public Extension extension(@NonNull String name) {
|
||||
if (this.extensions.containsKey(name)) {
|
||||
return this.extensions.get(name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Map<String, GeyserExtension> getExtensions() {
|
||||
return this.extensions;
|
||||
}
|
||||
|
||||
public void registerInterface(Class<? extends GeyserExtensionLoader> loader) {
|
||||
GeyserExtensionLoader instance;
|
||||
|
||||
if (GeyserExtensionLoader.class.isAssignableFrom(loader)) {
|
||||
Constructor<? extends GeyserExtensionLoader> constructor;
|
||||
|
||||
try {
|
||||
constructor = loader.getConstructor();
|
||||
instance = constructor.newInstance();
|
||||
} catch (NoSuchMethodException ex) { // This should never happen
|
||||
String className = loader.getName();
|
||||
|
||||
throw new IllegalArgumentException("Class " + className + " does not have a public constructor", ex);
|
||||
} catch (Exception ex) { // This should never happen
|
||||
throw new IllegalArgumentException("Unexpected exception " + ex.getClass().getName() + " while attempting to construct a new instance of " + loader.getName(), ex);
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Class " + loader.getName() + " does not implement interface ExtensionLoader");
|
||||
}
|
||||
|
||||
Pattern[] patterns = instance.extensionFilters();
|
||||
|
||||
synchronized (this) {
|
||||
for (Pattern pattern : patterns) {
|
||||
fileAssociations.put(pattern, instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public GeyserExtension loadExtension(File file, Map<Pattern, GeyserExtensionLoader> loaders) {
|
||||
for (GeyserExtensionLoader loader : (loaders == null ? this.fileAssociations : loaders).values()) {
|
||||
for (Pattern pattern : loader.extensionFilters()) {
|
||||
if (pattern.matcher(file.getName()).matches()) {
|
||||
try {
|
||||
ExtensionDescription description = loader.extensionDescription(file);
|
||||
if (description != null) {
|
||||
GeyserExtension extension = loader.loadExtension(file);
|
||||
|
||||
if (extension != null) {
|
||||
this.extensions.put(extension.description().name(), extension);
|
||||
|
||||
return extension;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed"), e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Map<String, GeyserExtension> loadExtensions(File dictionary) {
|
||||
if (GeyserImpl.VERSION.equalsIgnoreCase("dev")) { // If your IDE says this is always true, ignore it, it isn't.
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_dev_environment"));
|
||||
return new HashMap<>();
|
||||
}
|
||||
if (!GeyserImpl.VERSION.contains(".")) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_version_number"));
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
String[] apiVersion = GeyserImpl.VERSION.split("\\.");
|
||||
|
||||
if (!dictionary.exists()) {
|
||||
dictionary.mkdir();
|
||||
}
|
||||
if (!dictionary.isDirectory()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
Map<String, File> extensions = new LinkedHashMap<>();
|
||||
Map<String, GeyserExtension> loadedExtensions = new LinkedHashMap<>();
|
||||
|
||||
for (final GeyserExtensionLoader loader : this.fileAssociations.values()) {
|
||||
for (File file : dictionary.listFiles((dir, name) -> {
|
||||
for (Pattern pattern : loader.extensionFilters()) {
|
||||
if (pattern.matcher(name).matches()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
})) {
|
||||
if (file.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
ExtensionDescription description = loader.extensionDescription(file);
|
||||
if (description != null) {
|
||||
String name = description.name();
|
||||
|
||||
if (extensions.containsKey(name) || this.getExtension(name) != null) {
|
||||
GeyserImpl.getInstance().getLogger().warning(GeyserLocale.getLocaleStringLog("geyser.extensions.load.duplicate", name, file.getName()));
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
//Check the format: majorVersion.minorVersion.patch
|
||||
if (!Pattern.matches("^[0-9]+\\.[0-9]+\\.[0-9]+$", description.apiVersion())) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
} catch (NullPointerException | IllegalArgumentException e) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_format", name, apiVersion[0] + "." + apiVersion[1]));
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] versionArray = description.apiVersion().split("\\.");
|
||||
|
||||
//Completely different API version
|
||||
if (!Objects.equals(Integer.valueOf(versionArray[0]), Integer.valueOf(apiVersion[0]))) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, apiVersion[0] + "." + apiVersion[1]));
|
||||
continue;
|
||||
}
|
||||
|
||||
//If the extension requires new API features, being backwards compatible
|
||||
if (Integer.parseInt(versionArray[1]) > Integer.parseInt(apiVersion[1])) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_api_version", name, apiVersion[0] + "." + apiVersion[1]));
|
||||
continue;
|
||||
}
|
||||
|
||||
extensions.put(name, file);
|
||||
loadedExtensions.put(name, this.loadExtension(file, this.fileAssociations));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.load.failed_with_name", file.getName(), dictionary.getAbsolutePath()), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return loadedExtensions;
|
||||
}
|
||||
|
||||
public void enableExtension(GeyserExtension extension) {
|
||||
@Override
|
||||
public void enable(@NonNull Extension extension) {
|
||||
if (!extension.isEnabled()) {
|
||||
try {
|
||||
extension.extensionLoader().enableExtension(extension);
|
||||
this.enableExtension(extension);
|
||||
} catch (Exception e) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.enable.failed", extension.name()), e);
|
||||
this.disableExtension(extension);
|
||||
this.disable(extension);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void disableExtension(GeyserExtension extension) {
|
||||
@Override
|
||||
public void disable(@NonNull Extension extension) {
|
||||
if (extension.isEnabled()) {
|
||||
try {
|
||||
extension.extensionLoader().disableExtension(extension);
|
||||
this.disableExtension(extension);
|
||||
} catch (Exception e) {
|
||||
GeyserImpl.getInstance().getLogger().error(GeyserLocale.getLocaleStringLog("geyser.extensions.disable.failed", extension.name()), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void enableExtension(Extension extension) {
|
||||
if (!extension.isEnabled()) {
|
||||
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.enable.success", extension.description().name()));
|
||||
extension.setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
private void disableExtension(@NonNull Extension extension) {
|
||||
if (extension.isEnabled()) {
|
||||
GeyserImpl.getInstance().getLogger().info(GeyserLocale.getLocaleStringLog("geyser.extensions.disable.success", extension.description().name()));
|
||||
extension.setEnabled(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void enableExtensions() {
|
||||
for (GeyserExtension extension : this.getExtensions().values()) {
|
||||
this.enableExtension(extension);
|
||||
for (Extension extension : this.extensions()) {
|
||||
this.enable(extension);
|
||||
}
|
||||
}
|
||||
|
||||
public void disableExtensions() {
|
||||
for (GeyserExtension extension : this.getExtensions().values()) {
|
||||
this.disableExtension(extension);
|
||||
for (Extension extension : this.extensions()) {
|
||||
this.disable(extension);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ExtensionLoader extensionLoader(@NonNull Extension extension) {
|
||||
return this.extensionsLoaders.get(extension);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Collection<Extension> extensions() {
|
||||
return Collections.unmodifiableCollection(this.extensions.values());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public ExtensionLoader extensionLoader(@NonNull Key identifier) {
|
||||
return Registries.EXTENSION_LOADERS.get(identifier);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerExtensionLoader(@NonNull Key identifier, @NotNull ExtensionLoader extensionLoader) {
|
||||
Registries.EXTENSION_LOADERS.register(identifier, extensionLoader);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Map<Key, ExtensionLoader> extensionLoaders() {
|
||||
return Collections.unmodifiableMap(Registries.EXTENSION_LOADERS.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(@NotNull Extension extension, @NotNull ExtensionLoader loader) {
|
||||
this.extensionsLoaders.put(extension, loader);
|
||||
this.extensions.put(extension.name(), extension);
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,6 @@ import com.github.steveice10.mc.protocol.data.game.level.event.SoundEvent;
|
||||
import com.github.steveice10.mc.protocol.data.game.level.particle.ParticleType;
|
||||
import com.github.steveice10.mc.protocol.data.game.recipe.Recipe;
|
||||
import com.github.steveice10.mc.protocol.data.game.recipe.RecipeType;
|
||||
import com.github.steveice10.mc.protocol.data.game.statistic.CustomStatistic;
|
||||
import com.github.steveice10.packetlib.packet.Packet;
|
||||
import com.nukkitx.nbt.NbtMap;
|
||||
import com.nukkitx.protocol.bedrock.BedrockPacket;
|
||||
@ -42,6 +41,8 @@ import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import net.kyori.adventure.key.Key;
|
||||
import org.geysermc.geyser.api.extension.ExtensionLoader;
|
||||
import org.geysermc.geyser.entity.EntityDefinition;
|
||||
import org.geysermc.geyser.registry.populator.PacketRegistryPopulator;
|
||||
import org.geysermc.geyser.translator.collision.BlockCollision;
|
||||
@ -62,7 +63,6 @@ import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.IntFunction;
|
||||
|
||||
/**
|
||||
* Holds all the common registries in Geyser.
|
||||
@ -113,6 +113,11 @@ public final class Registries {
|
||||
*/
|
||||
public static final SimpleMappedRegistry<EntityType, EntityDefinition<?>> ENTITY_DEFINITIONS = SimpleMappedRegistry.create(RegistryLoaders.empty(() -> new EnumMap<>(EntityType.class)));
|
||||
|
||||
/**
|
||||
* A map containing all the extension loaders.
|
||||
*/
|
||||
public static final SimpleMappedRegistry<Key, ExtensionLoader> EXTENSION_LOADERS = SimpleMappedRegistry.create(RegistryLoaders.empty(Object2ObjectOpenHashMap::new));
|
||||
|
||||
/**
|
||||
* A map containing all Java entity identifiers and their respective Geyser definitions
|
||||
*/
|
||||
|
2
pom.xml
2
pom.xml
@ -17,6 +17,8 @@
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<maven.compiler.source>16</maven.compiler.source>
|
||||
<maven.compiler.target>16</maven.compiler.target>
|
||||
|
||||
<adventure.version>4.9.3</adventure.version>
|
||||
</properties>
|
||||
|
||||
<organization>
|
||||
|
Laden…
In neuem Issue referenzieren
Einen Benutzer sperren