Merge pull request 'expand loader API' (#8) from owlsys/expand-loader-api into main

Reviewed-on: https://git-esnesnon.ecorous.org/esnesnon/nonsense-loader/pulls/8
Reviewed-by: Ecorous <ecorous@outlook.com>
Reviewed-by: TheKodeToad <kode@noreply.localhost>
This commit is contained in:
TheKodeToad 2024-05-26 07:47:38 -04:00
commit 8487df905b
17 changed files with 300 additions and 152 deletions

View file

@ -1,7 +1,7 @@
plugins {
java
`java-library`
id("io.freefair.lombok").version("8.+")
id("io.freefair.lombok") version "8.+"
`maven-publish`
}
@ -38,6 +38,14 @@ java {
targetCompatibility = JavaVersion.VERSION_21
}
tasks.processResources {
inputs.property("version", version)
filesMatching("frog.mod.toml") {
expand("version" to version)
}
}
publishing {
publications {
create<MavenPublication>("mavenJava") {

View file

@ -16,7 +16,7 @@ public abstract class TitleScreenMixin extends Screen {
super(title);
}
@Inject(method = "createNormalMenuOptions", at = @At("TAIL"), remap = false)
@Inject(method = "createNormalMenuOptions", at = @At("TAIL"))
private void showExample(int y, int rowHeight, CallbackInfo ci) {
var widget = new FocusableTextWidget(200, Component.literal("<insert frog here!>"), this.font);
widget.setPosition(width / 2 - widget.getWidth(), 20);

View file

@ -1,4 +1,4 @@
[nonsense]
[frog]
id = "example_mod"
name = "Example Mod"
version = "1.0.0"
@ -7,7 +7,7 @@ credits = [
{ name = "You", roles = ["author", "other_role"] }
]
[nonsense.dependencies]
[frog.dependencies]
depends = [
{ id = "other_mod", versions = ">=0.2.0" }
]
@ -15,7 +15,7 @@ breaks = [
{ id = "old_mod", versions = "*" }
]
[nonsense.extensions]
[frog.extensions]
pre_launch = "org.ecorous.esnesnon.nonsense.loader.example.ExamplePreLaunchExtension"
mixin_config = "example_mod.mixins.json"

View file

@ -2,8 +2,10 @@ package org.ecorous.esnesnon.nonsense.loader.api;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
import org.ecorous.esnesnon.nonsense.loader.api.env.Env;
import org.ecorous.esnesnon.nonsense.loader.api.mod.ModProperties;
import org.ecorous.esnesnon.nonsense.loader.impl.LoaderImpl;
import org.ecorous.esnesnon.nonsense.loader.impl.plugin.NonsensePlugin;
@ -22,4 +24,7 @@ public interface Loader {
Path getModsDir();
boolean isDevelopment();
boolean isModLoaded(String id);
Optional<ModProperties> getModProperties(String id);
}

View file

@ -37,6 +37,10 @@ public final class License {
return idToLicense.computeIfAbsent(id, ignored -> new License(idToName.getOrDefault(id, id), id));
}
public static License custom(String id, String name){
return idToLicense.computeIfAbsent(id, ignored -> new License(idToName.computeIfAbsent(id, s -> name), id));
}
private final String name, id;
private License(String name, String id) {

View file

@ -1,11 +0,0 @@
package org.ecorous.esnesnon.nonsense.loader.api.mod;
import java.util.Map;
public interface ModProvider {
ModProperties properties();
Map<String, String> entrypoints();
}

View file

@ -1,92 +1,9 @@
package org.ecorous.esnesnon.nonsense.loader.api.mod;
import java.util.List;
import java.util.Objects;
import java.util.function.IntSupplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.NonNull;
import org.ecorous.esnesnon.nonsense.loader.impl.SemVerParseException;
public record SemVer(int major, int minor, int patch, String prerelease, String build) implements Comparable<SemVer> {
// Adapted from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
private static final Pattern SEMVER_PATTERN = Pattern.compile("^(?<major>0|[1-9]\\d*)\\." +
"(?<minor>0|[1-9]\\d*)\\." +
"(?<patch>0|[1-9]\\d*)" +
"(?:-(?<prerelease>(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)" +
"(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" +
"(?:\\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$");
public static SemVer parse(String version) throws SemVerParseException {
Matcher matcher = SEMVER_PATTERN.matcher(version);
if (!matcher.find()) {
throw new SemVerParseException(version);
}
int major = Integer.parseInt(matcher.group("major"));
int minor = Integer.parseInt(matcher.group("minor"));
int patch = Integer.parseInt(matcher.group("patch"));
String prerelease = matcher.group("prerelease");
String buildmetadata = matcher.group("buildmetadata");
return new SemVer(major, minor, patch, prerelease, buildmetadata);
}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
b.append(major).append(minor).append(patch);
if (prerelease != null){
b.append("-").append(prerelease);
}
if (build != null){
b.append("+").append(build);
}
return b.toString();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof SemVer s) {
return compareTo(s) == 0;
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(major, minor, patch, prerelease);
}
@Override
public int compareTo(@NonNull SemVer o) {
int i;
List<IntSupplier> suppliers = List.of(
() -> Integer.compare(major, o.major),
() -> Integer.compare(minor, o.minor),
() -> Integer.compare(patch, o.patch),
() -> prerelease != null ? o.prerelease != null ? 0 : -1 : o.prerelease != null ? 1 : 0
);
for (IntSupplier comparison : suppliers) {
if ((i = comparison.getAsInt()) != 0) {
return i;
}
}
String[] self = prerelease.split("\\.");
String[] other = o.prerelease.split("\\.");
for (int index = 0;index<Math.min(self.length, other.length);index++){
boolean selfNumeric = self[index].matches("\\d+");
boolean otherNumeric = other[index].matches("\\d+");
if (selfNumeric != otherNumeric){
return selfNumeric ? -1 : 1;
} else if (!selfNumeric){
if ((i = self[index].compareTo(other[index])) != 0){
return i;
}
}
}
return Integer.compare(self.length, other.length);
}
public interface SemVer extends Comparable<SemVer> {
int major();
int minor();
int patch();
String prerelease();
String build();
}

View file

@ -8,11 +8,14 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import com.google.gson.Gson;
import org.ecorous.esnesnon.nonsense.loader.api.Loader;
import org.ecorous.esnesnon.nonsense.loader.api.env.Env;
import org.ecorous.esnesnon.nonsense.loader.api.mod.ModProperties;
import org.ecorous.esnesnon.nonsense.loader.impl.launch.MixinClassLoader;
import org.ecorous.esnesnon.nonsense.loader.impl.mod.ModUtil;
import org.ecorous.esnesnon.nonsense.loader.impl.plugin.NonsensePlugin;
import lombok.Getter;
import org.slf4j.Logger;
@ -21,7 +24,7 @@ import org.spongepowered.asm.mixin.MixinEnvironment;
public class LoaderImpl implements Loader {
// TODO decide this
public static final String MOD_FILE_EXTENSION = ".nonsense";
public static final String MOD_FILE_EXTENSION = ".frogmod";
private final boolean DEV_ENV = Boolean.getBoolean("nonsense.development");
@Getter
@ -32,7 +35,7 @@ public class LoaderImpl implements Loader {
@Getter
private static LoaderImpl instance;
private final Logger LOGGER = LoggerFactory.getLogger("Nonsense Loader");
private final Logger LOGGER = LoggerFactory.getLogger("Frogloader");
@Getter
private final List<NonsensePlugin> plugins = new ArrayList<>();
@ -46,6 +49,10 @@ public class LoaderImpl implements Loader {
@Getter
private final Gson gson = new Gson();
private final Map<String, ModProperties> mods;
private final Collection<String> modIds;
private LoaderImpl(String[] args, Env env) {
instance = this;
this.classloader = (MixinClassLoader) this.getClass().getClassLoader();
@ -66,6 +73,9 @@ public class LoaderImpl implements Loader {
discoverPlugins();
advanceMixinState();
mods = collectMods();
modIds = collectModIds();
LOGGER.info(ModUtil.getModList(mods.values()));
LOGGER.info("Launching...");
plugins.forEach(NonsensePlugin::run);
}
@ -111,9 +121,9 @@ public class LoaderImpl implements Loader {
try {
MethodHandle ctor = MethodHandles.publicLookup().findConstructor(c, MethodType.methodType(void.class));
NonsensePlugin plugin = (NonsensePlugin) ctor.invoke();
if (plugin.isApplicable()) {
plugins.add(plugin);
if (plugin.init(this)) {
break;
plugin.init(this);
}
} catch (Throwable e) {
LOGGER.error("Error during plugin initialisation: ", e);
@ -142,4 +152,26 @@ public class LoaderImpl implements Loader {
public boolean isDevelopment() {
return DEV_ENV;
}
@Override
public boolean isModLoaded(String id) {
return modIds.contains(id);
}
@Override
public Optional<ModProperties> getModProperties(String id) {
return Optional.ofNullable(mods.get(id));
}
private Map<String, ModProperties> collectMods(){
Collection<ModProperties> properties = plugins.stream().map(NonsensePlugin::getMods).reduce(new HashSet<>(), (s1, s2) -> {
s1.addAll(s2);
return s1;
});
return properties.stream().collect(Collectors.toMap(ModProperties::id, m -> m));
}
private Collection<String> collectModIds(){
return mods.keySet();
}
}

View file

@ -1,9 +0,0 @@
package org.ecorous.esnesnon.nonsense.loader.impl.mod;
import java.util.Map;
import org.ecorous.esnesnon.nonsense.loader.api.mod.ModProperties;
import org.ecorous.esnesnon.nonsense.loader.api.mod.ModProvider;
public record BuiltinModProvider(ModProperties properties, Map<String, String> entrypoints) implements ModProvider {
}

View file

@ -19,7 +19,7 @@ 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";
public static final String PROPERTIES_FILE_NAME = "frog.mod.toml";
private static final TomlParser PARSER = new TomlParser();
@ -43,24 +43,28 @@ public class ModPropertiesReader {
}
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");
String id = config.get("frog.id");
String name = config.get("frog.name");
String version = config.get("frog.version");
String license = config.get("frog.license");
List<UnmodifiableConfig> creditsList = config.get("nonsense.credits");
List<UnmodifiableConfig> creditsList = config.get("frog.credits");
Map<String, Collection<String>> credits = new HashMap<>();
if (creditsList != null) {
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");
Collection<ModDependencies.Entry> depends = config.get("frog.dependencies.depends");
Collection<ModDependencies.Entry> breaks = config.get("frog.dependencies.breaks");
Collection<ModDependencies.Entry> suggests = config.get("frog.dependencies.suggests");
UnmodifiableConfig extensionsConfig = config.get("nonsense.extensions");
UnmodifiableConfig extensionsConfig = config.get("frog.extensions");
Map<String, Object> extensions = new HashMap<>();
if (extensionsConfig != null) {
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));
return new ModPropertiesImpl(id, name, SemVerImpl.parse(version), License.fromId(license), ModCredits.of(credits), new ModDependencies(depends, breaks, suggests), ModExtensions.of(extensions));
} catch (SemVerParseException e) {
throw new UncheckedIOException(e);
}

View file

@ -0,0 +1,30 @@
package org.ecorous.esnesnon.nonsense.loader.impl.mod;
import java.util.Collection;
import org.ecorous.esnesnon.nonsense.loader.api.mod.ModProperties;
public class ModUtil {
public static String getModList(Collection<ModProperties> mods){
StringBuilder builder = new StringBuilder();
int size = mods.size();
builder.append("Loaded ").append(size).append(" mod");
if (size > 1){
builder.append("s");
}
builder.append(":");
int i = 0;
for (ModProperties p : mods) {
builder.append("\n\t");
if (i < size-1) {
builder.append("|- ");
} else {
builder.append("\\- ");
}
builder.append(p.name()).append(" (").append(p.id()).append(") ").append(" ").append(p.version());
i++;
}
return builder.toString();
}
}

View file

@ -0,0 +1,93 @@
package org.ecorous.esnesnon.nonsense.loader.impl.mod;
import java.util.List;
import java.util.Objects;
import java.util.function.IntSupplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import lombok.NonNull;
import org.ecorous.esnesnon.nonsense.loader.api.mod.SemVer;
import org.ecorous.esnesnon.nonsense.loader.impl.SemVerParseException;
public record SemVerImpl(int major, int minor, int patch, String prerelease, String build) implements SemVer {
// Adapted from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
private static final Pattern SEMVER_PATTERN = Pattern.compile("^(?<major>0|[1-9]\\d*)\\." +
"(?<minor>0|[1-9]\\d*)\\." +
"(?<patch>0|[1-9]\\d*)" +
"(?:-(?<prerelease>(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)" +
"(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" +
"(?:\\+(?<buildmetadata>[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$");
public static SemVer parse(String version) throws SemVerParseException {
Matcher matcher = SEMVER_PATTERN.matcher(version);
if (!matcher.find()) {
throw new SemVerParseException(version);
}
int major = Integer.parseInt(matcher.group("major"));
int minor = Integer.parseInt(matcher.group("minor"));
int patch = Integer.parseInt(matcher.group("patch"));
String prerelease = matcher.group("prerelease");
String buildmetadata = matcher.group("buildmetadata");
return new SemVerImpl(major, minor, patch, prerelease, buildmetadata);
}
@Override
public String toString() {
StringBuilder b = new StringBuilder();
b.append(major).append(".").append(minor).append(".").append(patch);
if (prerelease != null){
b.append("-").append(prerelease);
}
if (build != null){
b.append("+").append(build);
}
return b.toString();
}
@Override
public boolean equals(Object obj) {
if (obj instanceof SemVerImpl s) {
return compareTo(s) == 0;
}
return false;
}
@Override
public int hashCode() {
return Objects.hash(major, minor, patch, prerelease);
}
@Override
public int compareTo(@NonNull SemVer o) {
int i;
List<IntSupplier> suppliers = List.of(
() -> Integer.compare(major, o.major()),
() -> Integer.compare(minor, o.minor()),
() -> Integer.compare(patch, o.patch()),
() -> prerelease != null ? o.prerelease() != null ? 0 : -1 : o.prerelease() != null ? 1 : 0
);
for (IntSupplier comparison : suppliers) {
if ((i = comparison.getAsInt()) != 0) {
return i;
}
}
String[] self = prerelease.split("\\.");
String[] other = o.prerelease().split("\\.");
for (int index = 0;index<Math.min(self.length, other.length);index++){
boolean selfNumeric = self[index].matches("\\d+");
boolean otherNumeric = other[index].matches("\\d+");
if (selfNumeric != otherNumeric){
return selfNumeric ? -1 : 1;
} else if (!selfNumeric){
if ((i = self[index].compareTo(other[index])) != 0){
return i;
}
}
}
return Integer.compare(self.length, other.length);
}
}

View file

@ -1,5 +1,8 @@
package org.ecorous.esnesnon.nonsense.loader.impl.plugin;
import java.util.Collection;
import org.ecorous.esnesnon.nonsense.loader.api.mod.ModProperties;
import org.ecorous.esnesnon.nonsense.loader.impl.LoaderImpl;
public interface NonsensePlugin extends Runnable {
@ -8,7 +11,15 @@ public interface NonsensePlugin extends Runnable {
}
default boolean init(LoaderImpl loader) {
/**
* @return Whether this plugin is applicable to be loaded in the current environment
*/
default boolean isApplicable(){
return false;
}
default void init(LoaderImpl loader) {
}
Collection<ModProperties> getMods();
}

View file

@ -1,10 +1,11 @@
package org.ecorous.esnesnon.nonsense.loader.impl.plugin.game;
package org.ecorous.esnesnon.nonsense.loader.impl.plugin.game.minecraft;
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.api.mod.*;
import org.ecorous.esnesnon.nonsense.loader.impl.Discovery;
import org.ecorous.esnesnon.nonsense.loader.impl.LoaderImpl;
import org.ecorous.esnesnon.nonsense.loader.impl.mod.ModPropertiesImpl;
import org.ecorous.esnesnon.nonsense.loader.impl.mod.ModPropertiesReader;
import org.ecorous.esnesnon.nonsense.loader.impl.plugin.NonsensePlugin;
import org.ecorous.esnesnon.nonsense_remapper.NonsenseRemapper;
@ -20,9 +21,7 @@ import java.lang.invoke.MethodType;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.file.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.*;
public class Minecraft implements NonsensePlugin {
@ -35,19 +34,18 @@ public class Minecraft implements NonsensePlugin {
private final List<ModProperties> modProperties = new ArrayList<>();
private String version;
private Path remappedGamePath;
private Path gamePath;
private String foundMainClass;
@Override
public boolean init(LoaderImpl loader) {
Path gameJar = findGame();
if (gameJar == null){
LOGGER.error("Could not find game jar on classpath! ({})", System.getProperty("java.class.path", ""));
return false;
//throw new IllegalStateException("Could not find game jar!");
public boolean isApplicable() {
gamePath = findGame();
return gamePath != null;
}
remappedGamePath = loader.getGameDir().resolve(".nonsense/remappedJars").resolve(version).resolve("game-"+version+"-remapped.jar");
@Override
public void init(LoaderImpl loader) {
Path remappedGamePath = loader.getGameDir().resolve(".nonsense/remappedJars").resolve(version).resolve("game-" + version + "-remapped.jar");
if (!Files.exists(remappedGamePath)){
try {
@ -57,6 +55,11 @@ public class Minecraft implements NonsensePlugin {
}
}
modProperties.add(new ModPropertiesImpl("minecraft", "Minecraft",
new MinecraftSemVerImpl(version), License.custom("MC-EULA", "Minecraft EULA"),
ModCredits.of(Map.of("Mojang AB", Collections.singleton("Author"))),
new ModDependencies(Collections.emptySet(), Collections.emptySet(), Collections.emptySet()),
ModExtensions.of(Collections.emptyMap())));
Collection<Path> mods = Discovery.find(loader.getModsDir(), path ->
version.equals(path.getFileName().toString()), path ->
@ -70,19 +73,20 @@ public class Minecraft implements NonsensePlugin {
throw new RuntimeException(e);
}
}).forEachOrdered(LoaderImpl.getInstance().getClassloader()::addURL);
LOGGER.info("Found {} mod(s)", 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");
if (name != null) {
Mixins.addConfiguration(name);
}
});
if (!Files.exists(remappedGamePath) && !loader.isDevelopment()){
try {
NonsenseRemapper.run(version, gameJar, remappedGamePath, true, false);
NonsenseRemapper.run(version, gamePath, remappedGamePath, true, false);
} catch (Throwable e) {
throw new RuntimeException(e);
}
@ -93,7 +97,11 @@ public class Minecraft implements NonsensePlugin {
} catch (Throwable e) {
throw new RuntimeException(e);
}
return true;
}
@Override
public Collection<ModProperties> getMods() {
return modProperties;
}
private Path findGame() {

View file

@ -0,0 +1,47 @@
package org.ecorous.esnesnon.nonsense.loader.impl.plugin.game.minecraft;
import org.ecorous.esnesnon.nonsense.loader.api.mod.SemVer;
import org.jetbrains.annotations.NotNull;
public class MinecraftSemVerImpl implements SemVer {
private final String version;
MinecraftSemVerImpl(String version){
this.version = version;
}
@Override
public int major() {
throw new UnsupportedOperationException("Minecraft versions do not reliably have a major version");
}
@Override
public int minor() {
throw new UnsupportedOperationException("Minecraft versions do not reliably have a minor version");
}
@Override
public int patch() {
throw new UnsupportedOperationException("Minecraft versions do not reliably have a patch version");
}
@Override
public String prerelease() {
return null;
}
@Override
public String build() {
return null;
}
@Override
public int compareTo(@NotNull SemVer o) {
throw new UnsupportedOperationException("Minecraft versions cannot be compared");
}
@Override
public String toString() {
return version;
}
}

View file

@ -1 +1 @@
org.ecorous.esnesnon.nonsense.loader.impl.plugin.game.Minecraft
org.ecorous.esnesnon.nonsense.loader.impl.plugin.game.minecraft.Minecraft

View file

@ -0,0 +1,9 @@
[frog]
id = "frogloader"
name = "Nonsense Loader"
version = "${version}"
license = "Apache-2.0"
credits = [
{ name = "Nonsense Team", roles = ["author"] }
]