mod loading process (wip)
This commit is contained in:
parent
35cfb2a7c1
commit
13f1bbb3c2
|
@ -0,0 +1,4 @@
|
||||||
|
package org.ecorous.esnesnon.nonsense.loader.example;
|
||||||
|
|
||||||
|
public class ExampleMod {
|
||||||
|
}
|
25
minecraft/src/main/resources/nonsense.mod.toml
Normal file
25
minecraft/src/main/resources/nonsense.mod.toml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
[nonsense]
|
||||||
|
id = "example_mod"
|
||||||
|
name = "Example Mod"
|
||||||
|
version = "1.0.0"
|
||||||
|
license = "CC0-1.0"
|
||||||
|
credits = [
|
||||||
|
{ name = "You", roles = ["author", "other_role"] }
|
||||||
|
]
|
||||||
|
|
||||||
|
[nonsense.dependencies]
|
||||||
|
depends = [
|
||||||
|
{ id = "other_mod", versions = ">=0.2.0" }
|
||||||
|
]
|
||||||
|
breaks = [
|
||||||
|
{ id = "old_mod", versions = "*" }
|
||||||
|
]
|
||||||
|
|
||||||
|
[nonsense.extensions]
|
||||||
|
pre_launch = "com/example/nonsense/PreLaunch"
|
||||||
|
main = "com/example/nonsense/Main"
|
||||||
|
client = "com/example/nonsense/Client"
|
||||||
|
server = "com/example/nonsense/Server"
|
||||||
|
custom_entrypoint = "io/example/nonsense/Custom"
|
||||||
|
other_field = "Some Value"
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.ecorous.esnesnon.nonsense.loader.api.extensions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Pre-Launch Extension.
|
||||||
|
* <p>This Extension is run right before the game is launched. (provided the used plugin supports it :) )</p>
|
||||||
|
*/
|
||||||
|
public interface PreLaunchExtension {
|
||||||
|
String ID = "pre_launch";
|
||||||
|
|
||||||
|
void onPreLaunch();
|
||||||
|
}
|
|
@ -1,9 +1,18 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.api.mod;
|
package org.ecorous.esnesnon.nonsense.loader.api.mod;
|
||||||
|
|
||||||
|
import java.lang.invoke.MethodHandle;
|
||||||
|
import java.lang.invoke.MethodHandles;
|
||||||
|
import java.lang.invoke.MethodType;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
public final class ModExtensions {
|
public final class ModExtensions {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(ModExtensions.class);
|
||||||
|
|
||||||
public static ModExtensions of(Map<String, Object> entries){
|
public static ModExtensions of(Map<String, Object> entries){
|
||||||
return new ModExtensions(entries);
|
return new ModExtensions(entries);
|
||||||
}
|
}
|
||||||
|
@ -15,9 +24,46 @@ public final class ModExtensions {
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public <T> T get(String key){
|
public <T> T get(String key) {
|
||||||
return (T) extensions.get(key);
|
return (T) extensions.get(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> T getOrDefault(String key, T defaultValue) {
|
||||||
|
return (T) extensions.getOrDefault(key, defaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> void runIfPresent(String key, Consumer<T> action) {
|
||||||
|
T value = get(key);
|
||||||
|
if (value != null){
|
||||||
|
action.accept(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public <T> void runIfPresent(String key, Class<T> type, Consumer<T> action) {
|
||||||
|
String value = get(key);
|
||||||
|
|
||||||
|
if (value == null){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
MethodHandle handle;
|
||||||
|
if (value.contains("::")) {
|
||||||
|
String[] parts = value.split("::");
|
||||||
|
handle = MethodHandles.lookup().findVirtual(Class.forName(parts[0]), parts[1], MethodType.methodType(type));
|
||||||
|
} else {
|
||||||
|
handle = MethodHandles.lookup().findConstructor(Class.forName(value), MethodType.methodType(void.class));
|
||||||
|
}
|
||||||
|
T object = (T) handle.invoke();
|
||||||
|
if (object != null) {
|
||||||
|
action.accept(object);
|
||||||
|
}
|
||||||
|
} catch (Throwable e) {
|
||||||
|
LOGGER.warn("Failed to instantiate Extension: ", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,11 +24,11 @@ public record SemVer(int major, int minor, int patch, String prerelease, String
|
||||||
throw new SemVerParseException(version);
|
throw new SemVerParseException(version);
|
||||||
}
|
}
|
||||||
|
|
||||||
int major = Integer.parseInt(matcher.group("<major>"));
|
int major = Integer.parseInt(matcher.group("major"));
|
||||||
int minor = Integer.parseInt(matcher.group("<minor>"));
|
int minor = Integer.parseInt(matcher.group("minor"));
|
||||||
int patch = Integer.parseInt(matcher.group("<patch>"));
|
int patch = Integer.parseInt(matcher.group("patch"));
|
||||||
String prerelease = matcher.group("<prerelease>");
|
String prerelease = matcher.group("prerelease");
|
||||||
String buildmetadata = matcher.group("<buildmetadata>");
|
String buildmetadata = matcher.group("buildmetadata");
|
||||||
return new SemVer(major, minor, patch, prerelease, buildmetadata);
|
return new SemVer(major, minor, patch, prerelease, buildmetadata);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,11 @@ import java.net.URL;
|
||||||
import java.net.URLClassLoader;
|
import java.net.URLClassLoader;
|
||||||
|
|
||||||
public class MixinClassloader extends URLClassLoader {
|
public class MixinClassloader extends URLClassLoader {
|
||||||
|
|
||||||
|
static {
|
||||||
|
registerAsParallelCapable();
|
||||||
|
}
|
||||||
|
|
||||||
public MixinClassloader(URL[] urls, ClassLoader parent) {
|
public MixinClassloader(URL[] urls, ClassLoader parent) {
|
||||||
super(urls, parent);
|
super(urls, parent);
|
||||||
}
|
}
|
||||||
|
@ -21,7 +26,16 @@ public class MixinClassloader extends URLClassLoader {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Class<?> findClass(String name) throws ClassNotFoundException {
|
public Class<?> findClass(String name) throws ClassNotFoundException {
|
||||||
return super.findClass(name);
|
try {
|
||||||
|
byte[] bytes = getClassBytes(name);
|
||||||
|
if (bytes == null) {
|
||||||
|
throw new ClassNotFoundException(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return defineClass(name, bytes, 0, bytes.length);
|
||||||
|
} catch (IOException aa) {
|
||||||
|
throw new ClassNotFoundException(name, aa);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] getClassBytes(String name) throws IOException {
|
public byte[] getClassBytes(String name) throws IOException {
|
||||||
|
@ -33,6 +47,4 @@ public class MixinClassloader extends URLClassLoader {
|
||||||
return in.readAllBytes();
|
return in.readAllBytes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
package org.ecorous.esnesnon.nonsense.loader.impl.mod;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.UncheckedIOException;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.file.FileSystem;
|
||||||
|
import java.nio.file.FileSystems;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
import com.electronwill.nightconfig.core.CommentedConfig;
|
||||||
|
import com.electronwill.nightconfig.core.UnmodifiableConfig;
|
||||||
|
import com.electronwill.nightconfig.core.file.FileNotFoundAction;
|
||||||
|
import com.electronwill.nightconfig.toml.TomlParser;
|
||||||
|
import org.ecorous.esnesnon.nonsense.loader.api.mod.*;
|
||||||
|
import org.ecorous.esnesnon.nonsense.loader.impl.SemVerParseException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
public class ModPropertiesReader {
|
||||||
|
private static final Logger LOGGER = LoggerFactory.getLogger(ModPropertiesReader.class);
|
||||||
|
public static final String PROPERTIES_FILE_NAME = "nonsense.mod.toml";
|
||||||
|
|
||||||
|
private static final TomlParser PARSER = new TomlParser();
|
||||||
|
|
||||||
|
public static Optional<ModProperties> read(Path mod) {
|
||||||
|
|
||||||
|
try (FileSystem fs = FileSystems.newFileSystem(mod)) {
|
||||||
|
CommentedConfig props = PARSER.parse(fs.getPath(PROPERTIES_FILE_NAME), FileNotFoundAction.THROW_ERROR);
|
||||||
|
|
||||||
|
return Optional.of(readProperties(props));
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.warn("Failed to read mod properties: ", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ModProperties readFile(URL in) {
|
||||||
|
CommentedConfig props = PARSER.parse(in);
|
||||||
|
|
||||||
|
return readProperties(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ModProperties readProperties(UnmodifiableConfig config) {
|
||||||
|
String id = config.get("nonsense.id");
|
||||||
|
String name = config.get("nonsense.name");
|
||||||
|
String version = config.get("nonsense.version");
|
||||||
|
String license = config.get("nonsense.license");
|
||||||
|
|
||||||
|
List<UnmodifiableConfig> creditsList = config.get("nonsense.credits");
|
||||||
|
Map<String, Collection<String>> credits = new HashMap<>();
|
||||||
|
creditsList.forEach(c -> credits.put(c.get("name"), c.get("roles")));
|
||||||
|
|
||||||
|
Collection<ModDependencies.Entry> depends = config.get("nonsense.dependencies.depends");
|
||||||
|
Collection<ModDependencies.Entry> breaks = config.get("nonsense.dependencies.breaks");
|
||||||
|
Collection<ModDependencies.Entry> suggests = config.get("nonsense.dependencies.suggests");
|
||||||
|
|
||||||
|
UnmodifiableConfig extensionsConfig = config.get("nonsense.extensions");
|
||||||
|
Map<String, Object> extensions = new HashMap<>();
|
||||||
|
extensionsConfig.entrySet().forEach(entry -> extensions.put(entry.getKey(), entry.getValue()));
|
||||||
|
try {
|
||||||
|
return new ModPropertiesImpl(id, name, SemVer.parse(version), License.fromId(license), ModCredits.of(credits), new ModDependencies(depends, breaks, suggests), ModExtensions.of(extensions));
|
||||||
|
} catch (SemVerParseException e) {
|
||||||
|
throw new UncheckedIOException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.impl.plugin.game;
|
package org.ecorous.esnesnon.nonsense.loader.impl.plugin.game;
|
||||||
|
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
||||||
|
import org.ecorous.esnesnon.nonsense.loader.api.extensions.PreLaunchExtension;
|
||||||
|
import org.ecorous.esnesnon.nonsense.loader.api.mod.ModProperties;
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.Discovery;
|
import org.ecorous.esnesnon.nonsense.loader.impl.Discovery;
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.LoaderImpl;
|
import org.ecorous.esnesnon.nonsense.loader.impl.LoaderImpl;
|
||||||
|
import org.ecorous.esnesnon.nonsense.loader.impl.mod.ModPropertiesReader;
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.plugin.NonsensePlugin;
|
import org.ecorous.esnesnon.nonsense.loader.impl.plugin.NonsensePlugin;
|
||||||
import org.ecorous.esnesnon.nonsense_remapper.NonsenseRemapper;
|
import org.ecorous.esnesnon.nonsense_remapper.NonsenseRemapper;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.spongepowered.asm.mixin.Mixins;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -14,8 +18,10 @@ import java.lang.invoke.MethodHandle;
|
||||||
import java.lang.invoke.MethodHandles;
|
import java.lang.invoke.MethodHandles;
|
||||||
import java.lang.invoke.MethodType;
|
import java.lang.invoke.MethodType;
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
|
import java.net.URL;
|
||||||
import java.nio.file.*;
|
import java.nio.file.*;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class Minecraft implements NonsensePlugin {
|
public class Minecraft implements NonsensePlugin {
|
||||||
|
@ -27,7 +33,7 @@ public class Minecraft implements NonsensePlugin {
|
||||||
"net/minecraft/server/main/Main.class"
|
"net/minecraft/server/main/Main.class"
|
||||||
};
|
};
|
||||||
|
|
||||||
private final List<Path> mods = new ArrayList<>();
|
private final List<ModProperties> modProperties = new ArrayList<>();
|
||||||
private String version;
|
private String version;
|
||||||
private Path remappedGamePath;
|
private Path remappedGamePath;
|
||||||
private String foundMainClass;
|
private String foundMainClass;
|
||||||
|
@ -51,18 +57,28 @@ public class Minecraft implements NonsensePlugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mods.addAll(Discovery.find(loader.getModsDir(), path ->
|
|
||||||
|
Collection<Path> mods = Discovery.find(loader.getModsDir(), path ->
|
||||||
version.equals(path.getFileName().toString()), path ->
|
version.equals(path.getFileName().toString()), path ->
|
||||||
path.getFileName().toString().endsWith(LoaderImpl.MOD_FILE_EXTENSION)));
|
path.getFileName().toString().endsWith(LoaderImpl.MOD_FILE_EXTENSION));
|
||||||
// TODO add mods found on the classpath
|
// TODO add mods found on the classpath
|
||||||
mods.stream().map(Path::toUri).map(uri -> {
|
Collection<URL> classpathMods = this.getClass().getClassLoader().resources(ModPropertiesReader.PROPERTIES_FILE_NAME).distinct().toList();
|
||||||
|
mods.parallelStream().map(Path::toUri).map(uri -> {
|
||||||
try {
|
try {
|
||||||
return uri.toURL();
|
return uri.toURL();
|
||||||
} catch (MalformedURLException e) {
|
} catch (MalformedURLException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}).forEach(LoaderImpl.getInstance().getClassloader()::addURL);
|
}).forEachOrdered(LoaderImpl.getInstance().getClassloader()::addURL);
|
||||||
LOGGER.info("Found {} mods", mods.size());
|
LOGGER.info("Found {} mods", mods.size()+classpathMods.size());
|
||||||
|
|
||||||
|
classpathMods.parallelStream().map(ModPropertiesReader::readFile).forEachOrdered(modProperties::add);
|
||||||
|
mods.parallelStream().map(ModPropertiesReader::read).forEachOrdered(opt -> opt.ifPresent(modProperties::add));
|
||||||
|
|
||||||
|
modProperties.parallelStream().forEach(props -> {
|
||||||
|
String name = props.extensions().get("mixin_config");
|
||||||
|
Mixins.addConfiguration(name);
|
||||||
|
});
|
||||||
|
|
||||||
if (!Files.exists(remappedGamePath) && !loader.isDevelopment()){
|
if (!Files.exists(remappedGamePath) && !loader.isDevelopment()){
|
||||||
try {
|
try {
|
||||||
|
@ -123,6 +139,9 @@ public class Minecraft implements NonsensePlugin {
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
if (foundMainClass != null) {
|
if (foundMainClass != null) {
|
||||||
|
modProperties.parallelStream().forEach(props ->
|
||||||
|
props.extensions().runIfPresent(PreLaunchExtension.ID,
|
||||||
|
PreLaunchExtension.class, PreLaunchExtension::onPreLaunch));
|
||||||
LOGGER.info("Launching main class: {}", foundMainClass);
|
LOGGER.info("Launching main class: {}", foundMainClass);
|
||||||
Class<?> mainClass = Class.forName(foundMainClass);
|
Class<?> mainClass = Class.forName(foundMainClass);
|
||||||
MethodHandle main = MethodHandles.publicLookup().findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class));
|
MethodHandle main = MethodHandles.publicLookup().findStatic(mainClass, "main", MethodType.methodType(void.class, String[].class));
|
||||||
|
|
Loading…
Reference in a new issue