diff --git a/minecraft/src/main/resources/frog.mod.toml b/minecraft/src/main/resources/frog.mod.toml index 227b772..3313436 100644 --- a/minecraft/src/main/resources/frog.mod.toml +++ b/minecraft/src/main/resources/frog.mod.toml @@ -12,11 +12,14 @@ credits = [ [frog.dependencies] depends = [ - { id = "other_mod", versions = ">=0.2.0" } + { id = "other_mod", versions = ">=0.2.0 <0.5.2 || 0.1.1 || 1.x || 3 || ~5 || ^6.x" } ] breaks = [ { id = "old_mod", versions = "*" } ] +provides = [ + { id = "provided_mod", version = "that.version.aa" } +] [frog.extensions] pre_launch = "org.ecorous.esnesnon.nonsense.loader.example.ExamplePreLaunchExtension" diff --git a/src/main/java/org/ecorous/esnesnon/nonsense/loader/api/mod/ModCredits.java b/src/main/java/org/ecorous/esnesnon/nonsense/loader/api/mod/ModCredits.java deleted file mode 100644 index 7e4f738..0000000 --- a/src/main/java/org/ecorous/esnesnon/nonsense/loader/api/mod/ModCredits.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.ecorous.esnesnon.nonsense.loader.api.mod; - -import java.util.*; - -import org.jetbrains.annotations.NotNull; - -public final class ModCredits { - - public static ModCredits of(Map> credits){ - return new ModCredits(credits); - } - - private final Map> credits; - private ModCredits(Map> credits){ - this.credits = credits; - } - - public Collection getEntries(){ - return credits.keySet(); - } - - public Collection getRoles(String name){ - return credits.getOrDefault(name, Collections.emptySet()); - } - - public int size() { - return credits.size(); - } - - public boolean isEmpty() { - return credits.isEmpty(); - } - - public @NotNull Set>> entrySet() { - return credits.entrySet(); - } -} diff --git a/src/main/java/org/ecorous/esnesnon/nonsense/loader/api/mod/SemVer.java b/src/main/java/org/ecorous/esnesnon/nonsense/loader/api/mod/SemVer.java index b9d83b1..c640218 100644 --- a/src/main/java/org/ecorous/esnesnon/nonsense/loader/api/mod/SemVer.java +++ b/src/main/java/org/ecorous/esnesnon/nonsense/loader/api/mod/SemVer.java @@ -6,4 +6,6 @@ public interface SemVer extends Comparable { int patch(); String prerelease(); String build(); + + boolean equals(Object other); } diff --git a/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/LoaderImpl.java b/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/LoaderImpl.java index 8f5c31e..e877b48 100644 --- a/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/LoaderImpl.java +++ b/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/LoaderImpl.java @@ -23,7 +23,6 @@ import org.slf4j.LoggerFactory; import org.spongepowered.asm.mixin.MixinEnvironment; public class LoaderImpl implements Loader { - // TODO decide this public static final String MOD_FILE_EXTENSION = ".frogmod"; private final boolean DEV_ENV = Boolean.getBoolean("nonsense.development"); @@ -122,11 +121,12 @@ public class LoaderImpl implements Loader { MethodHandle ctor = MethodHandles.publicLookup().findConstructor(c, MethodType.methodType(void.class)); NonsensePlugin plugin = (NonsensePlugin) ctor.invoke(); if (plugin.isApplicable()) { - plugins.add(plugin); plugin.init(this); + plugins.add(plugin); } } catch (Throwable e) { LOGGER.error("Error during plugin initialisation: ", e); + throw new RuntimeException(e); } } diff --git a/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/mod/BuiltinExtensions.java b/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/mod/BuiltinExtensions.java index c5d7926..a9a8712 100644 --- a/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/mod/BuiltinExtensions.java +++ b/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/mod/BuiltinExtensions.java @@ -8,4 +8,5 @@ public class BuiltinExtensions { public final String INCLUDED_JARS = "included_jars"; public final String PRE_LAUNCH = "pre_launch"; public final String ACCESSWIDENER = "frog_aw"; + public final String LOADING_TYPE = "loading_type"; } diff --git a/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/mod/JavaModProperties.java b/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/mod/JavaModProperties.java new file mode 100644 index 0000000..8e7a441 --- /dev/null +++ b/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/mod/JavaModProperties.java @@ -0,0 +1,31 @@ +package org.ecorous.esnesnon.nonsense.loader.impl.mod; + +import java.util.Collections; +import java.util.Map; + +import org.ecorous.esnesnon.nonsense.loader.api.mod.ModDependencies; +import org.ecorous.esnesnon.nonsense.loader.api.mod.ModExtensions; +import org.ecorous.esnesnon.nonsense.loader.api.mod.ModProperties; +import org.ecorous.esnesnon.nonsense.loader.impl.SemVerParseException; + +public class JavaModProperties { + + private static ModProperties INSTANCE; + + private JavaModProperties() { + + } + + public static ModProperties get() throws SemVerParseException { + if (INSTANCE == null){ + INSTANCE = new ModPropertiesImpl("java", + System.getProperty("java.vm.name"), + SemVerImpl.parse(System.getProperty("java.runtime.version")), + "", + Map.of(System.getProperty("java.vm.vendor"), Collections.singleton("Vendor")), + new ModDependencies(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet()), + ModExtensions.of(Collections.emptyMap())); + } + return INSTANCE; + } +} diff --git a/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/mod/ModDependencyResolver.java b/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/mod/ModDependencyResolver.java new file mode 100644 index 0000000..58b38f9 --- /dev/null +++ b/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/mod/ModDependencyResolver.java @@ -0,0 +1,462 @@ +package org.ecorous.esnesnon.nonsense.loader.impl.mod; + +import java.io.UncheckedIOException; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.ecorous.esnesnon.nonsense.loader.api.mod.ModDependencies; +import org.ecorous.esnesnon.nonsense.loader.api.mod.ModProperties; +import org.ecorous.esnesnon.nonsense.loader.api.mod.SemVer; +import org.ecorous.esnesnon.nonsense.loader.impl.SemVerParseException; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ModDependencyResolver { + + private final static Logger LOGGER = LoggerFactory.getLogger(ModDependencyResolver.class); + + private final Collection original; + + private final Map dependencies = new HashMap<>(); + private final Map breakings = new HashMap<>(); + private final Map suggests = new HashMap<>(); + private final Map provides = new HashMap<>(); + private final Map presentMods = new HashMap<>(); + + + public ModDependencyResolver(Collection mods) throws ResolverException { + this.original = Collections.unmodifiableCollection(mods); + load(); + } + + private void load() throws ResolverException { + for (ModProperties props : original) { + for (ModDependencies.Entry entry : props.dependencies().getForType(ModDependencies.Type.DEPEND)) { + dependencies.put(entry.id(), new DependencyEntry(entry.range(), props)); + } + for (ModDependencies.Entry entry : props.dependencies().getForType(ModDependencies.Type.BREAK)) { + breakings.put(entry.id(), new DependencyEntry(entry.range(), props)); + } + for (ModDependencies.Entry entry : props.dependencies().getForType(ModDependencies.Type.SUGGEST)) { + suggests.put(entry.id(), new DependencyEntry(entry.range(), props)); + } + props.dependencies().getForType(ModDependencies.Type.PROVIDE).forEach(e -> { + try { + provides.put(e.id(), SemVerImpl.parse(e.range())); + } catch (SemVerParseException ex) { + LOGGER.warn("Version for {} ({}), provided by mod '{}' ({}) does not meet SemVer specifications. Mod will not be provided.", + e.id(), e.range(), props.id(), props.name()); + } + }); + presentMods.put(props.id(), props); + } + } + + public Collection solve() throws UnfulfilledDependencyException, BreakingModException { + // Step 1: look for breakage declarations + for (Map.Entry e : breakings.entrySet()) { + String key = e.getKey(); + DependencyEntry dependencyEntry = e.getValue(); + if (presentMods.containsKey(key) && dependencyEntry.range.versionMatches(presentMods.get(key).version())) { + throw new BreakingModException(dependencyEntry.origin(), presentMods.get(key), dependencyEntry.range); + } + } + + // Step 2: Combine present and provided mods, always use the latest version available + Set result = new HashSet<>(); + Map presentOrProvided = new HashMap<>(); + presentMods.forEach((s, modProperties) -> presentOrProvided.put(s, modProperties.version())); + provides.forEach((s, ver) -> { + if (presentMods.containsKey(s)) { + if (presentMods.get(s).version().compareTo(ver) < 0){ + presentOrProvided.replace(s, ver); + } + } else { + presentOrProvided.put(s, ver); + } + }); + + // Step 3: print out information about suggested mods + for (Map.Entry e : suggests.entrySet()) { + String key = e.getKey(); + DependencyEntry v = e.getValue(); + if (!presentOrProvided.containsKey(key) || !v.range.versionMatches(presentOrProvided.get(key))) { + LOGGER.info("Mod '{}' ({}) suggests range {} of {}, you should install a matching version for the optimal experience!", + v.origin().id(), v.origin().name(), v.range(), key); + } + } + + // Step 4.1: Add all mods to the result that do not depend on any other mods + presentMods.forEach((s, modProperties) -> { + if (modProperties.dependencies().getForType(ModDependencies.Type.DEPEND).isEmpty()){ + result.add(modProperties); + } + }); + + // Step 4.2: Check that all dependencies are satisfied by present or provided mods. + for (Map.Entry entry : dependencies.entrySet()) { + String s = entry.getKey(); + DependencyEntry value = entry.getValue(); + if (!(presentMods.containsKey(s) && value.range.versionMatches(presentOrProvided.get(s)))) { // The dependency isn't fulfilled by any present mods + if (!(provides.containsKey(s) && value.range.versionMatches(provides.get(s)))) { // The dependency also isn't fulfilled by any provided mods + if (value.origin.extensions().getOrDefault(BuiltinExtensions.LOADING_TYPE, "required").equals("required")) { + throw new UnfulfilledDependencyException(value.origin, s, value.range); + } + } + } + // as there hasn't been thrown an exception the dependency must have been fulfilled, add the mod that set the dependency + result.add(value.origin()); + } + + return result; + } + + @AllArgsConstructor + @Getter + public static class BreakingModException extends Exception { + private final ModProperties source, broken; + private final VersionRange range; + } + + @AllArgsConstructor + @Getter + public static class UnfulfilledDependencyException extends Exception { + private final ModProperties source; + private final String dependency; + private final VersionRange range; + } + + public static class ResolverException extends Exception { + public ResolverException(String message) { + super(message); + } + } + + + private record DependencyEntry(VersionRange range, ModProperties origin) { + + public DependencyEntry(String range, ModProperties origin) throws ResolverException { + this(VersionRange.parse(range), origin); + } + } + + @AllArgsConstructor + private enum DependencyType { + EQ("=", Object::equals), + GE(">=", (a, b) -> a.compareTo(b) >= 0), + LE("<=", (a, b) -> a.compareTo(b) <= 0), + GT(">", (a, b) -> a.compareTo(b) > 0), + LT("<", (a, b) -> a.compareTo(b) < 0), + + ; + private final String prefix; + private final TypeComparator comparator; + + private static DependencyType of(String comparator) { + return Arrays.stream(values()).filter(t -> t.prefix.equals(comparator)).findFirst().orElse(DependencyType.EQ); + } + + private interface TypeComparator { + boolean compare(SemVer a, SemVer b); + } + } + + private static final Pattern COMPARATOR = Pattern.compile("(=|>=|<=|>|<)?(\\d.*)"); + + private record Comparator(DependencyType type, SemVer version) { + + public static Comparator parse(String comparator) { + Matcher matcher = COMPARATOR.matcher(comparator); + if (!matcher.find()) { + throw new IllegalArgumentException(comparator); + } + var type = DependencyType.of(matcher.group(1)); + StringBuilder version = new StringBuilder(matcher.group(2)); + + while (version.length() < 5) { + if (version.charAt(version.length() - 1) == '.') { + version.append('0'); + } else { + version.append('.'); + } + } + + try { + return new Comparator(type, SemVerImpl.parse(version.toString())); + } catch (SemVerParseException e) { + throw new UncheckedIOException(e); + } + } + + public boolean versionMatches(SemVer version) { + return type.comparator.compare(this.version, version); + } + + @Override + public String toString() { + return type.prefix + version; + } + } + + private record ComparatorSet(Collection comparators) { + + public static ComparatorSet parse(String set) { + String[] comparators = set.split(" "); + return new ComparatorSet(Arrays.stream(comparators).map(Comparator::parse).toList()); + } + + public boolean versionMatches(SemVer version) { + return comparators.stream().allMatch(c -> c.versionMatches(version)); + } + + @Override + public String toString() { + return comparators.stream().map(Objects::toString).collect(Collectors.joining(" ")); + } + } + + private record VersionRange(Collection sets) { + + public static VersionRange parse(String range) throws ResolverException { + + String[] sets = resolveAdvanced(range).split("\\|\\|"); + + return new VersionRange(Arrays.stream(sets).map(ComparatorSet::parse).toList()); + } + + private static final Pattern NUMBER_EXTRACTOR = Pattern.compile("[^~]?(\\d+)(?:\\.(?:([\\dxX*]+)(?:\\.([\\dxX*]+)?)?)?)?(.*)"); + + private static String resolveAdvanced(String range) throws ResolverException { + if (range.isEmpty() || range.equals("*")) { + return ">=0.0.0"; + } + + List list = extractRanges(range); + + List ranges = new LinkedList<>(); + for (int i = 0, listSize = list.size(); i < listSize; i++) { + String s = list.get(i); + + if (i < listSize-1 && list.get(i + 1).equals("-")) { + handleHyphenRange(s, ranges, list.get(i + 2)); + i += 2; + } else if (s.startsWith("~")) { + handleTilde(s, ranges); + } else if (s.startsWith("^")) { + handleCaret(s, ranges); + } else if (s.contains("x") || s.contains("X") || s.contains("*")) { + handleXRanges(s, ranges); + } else { + ranges.add(s); + } + } + range = String.join("", ranges); + + return range; + } + + private static void handleTilde(String s, List ranges) throws ResolverException { + { + StringBuilder builder = new StringBuilder(">=" + s); + while (builder.length() < 7) { + if (builder.charAt(builder.length() - 1) == '.') { + builder.append('0'); + } else { + builder.append('.'); + } + } + ranges.add(builder.toString()); + } + ranges.add(" "); + + Matcher matcher = NUMBER_EXTRACTOR.matcher(s); + if (!matcher.find()){ + throw new ResolverException("Version "+s+" did not match the required pattern to find the numbers within!"); + } + int major = Integer.parseInt(matcher.group(1)); + int minor = Optional.ofNullable(matcher.group(2)).map(Integer::parseInt).orElse(0); + + if (minor > 0) { + ranges.add("<" + major + "." + (minor + 1) + ".0"); + } else { + ranges.add("<" + (major + 1) + ".0.0"); + } + } + + private static void handleXRanges(String s, List ranges) throws ResolverException { + { + StringBuilder builder = new StringBuilder(">=" + s.replaceAll("[xX*]", "0")); + while (builder.length() < 7) { + if (builder.charAt(builder.length() - 1) == '.') { + builder.append('0'); + } else { + builder.append('.'); + } + } + ranges.add(builder.toString()); + } + + ranges.add(" "); + + if (s.length() < 5) { + Matcher matcher = NUMBER_EXTRACTOR.matcher(s); + if (!matcher.find()){ + throw new ResolverException("Version "+s+" did not match the required pattern to find the numbers within!"); + } + int major = Integer.parseInt(matcher.group(1)); + int minor = Optional.ofNullable(matcher.group(2)).map(n -> n.equalsIgnoreCase("x") || n.equals("*") ? null : n).map(Integer::parseInt).orElse(0); + int patch = Optional.ofNullable(matcher.group(3)).map(n -> n.equalsIgnoreCase("x") || n.equals("*")? null : n).map(Integer::parseInt).orElse(0); + StringBuilder builder = new StringBuilder("<"); + int[] ints = new int[]{major, minor, patch}; + for (int j = 0, intsLength = ints.length; j < intsLength; j++) { + int x = ints[j]; + if (x < intsLength - 1 && ints[j + 1] > 0) { + builder.append(x); + } else { + builder.append(x + 1); + break; + } + } + while (builder.length() < 6) { + if (builder.charAt(builder.length() - 1) == '.') { + builder.append('0'); + } else { + builder.append('.'); + } + } + ranges.add(builder.toString()); + } + } + + private static void handleCaret(String s, List ranges) throws ResolverException { + Matcher matcher = NUMBER_EXTRACTOR.matcher(s); + if (!matcher.find()){ + throw new ResolverException("Version "+s+" did not match the required pattern to find the numbers within!"); + } + int major = Integer.parseInt(matcher.group(1)); + int minor = Optional.ofNullable(matcher.group(2)).map(n -> n.equalsIgnoreCase("x") || n.equals("*") ? null : n).map(Integer::parseInt).orElse(0); + int patch = Optional.ofNullable(matcher.group(3)).map(n -> n.equalsIgnoreCase("x") || n.equals("*") ? null : n).map(Integer::parseInt).orElse(0); + String rest = matcher.group(4); + + ranges.add(">=" + major + "." + minor + "." + patch + rest); + ranges.add(" "); + StringBuilder builder = new StringBuilder("<"); + for (int x : new int[]{major, minor, patch}) { + if (x <= 0) { + builder.append(x); + } else { + builder.append(x + 1); + break; + } + } + while (builder.length() < 6) { + if (builder.charAt(builder.length() - 1) == '.') { + builder.append('0'); + } else { + builder.append('.'); + } + } + ranges.add(builder.toString()); + } + + private static void handleHyphenRange(String s, List ranges, String end) throws ResolverException { + { + StringBuilder builder = new StringBuilder(">=" + s); + while (builder.length() < 7) { + if (builder.charAt(builder.length() - 1) == '.') { + builder.append('0'); + } else { + builder.append('.'); + } + } + ranges.add(builder.toString()); + ranges.add(" "); + } + + if (end.length() < 5) { + Matcher matcher = NUMBER_EXTRACTOR.matcher(end); + if (!matcher.find()){ + throw new ResolverException("Version "+s+" did not match the required pattern to find the numbers within!"); + } + int major = Integer.parseInt(matcher.group(1)); + int minor = Optional.ofNullable(matcher.group(2)).map(Integer::parseInt).orElse(0); + int patch = Optional.ofNullable(matcher.group(3)).map(Integer::parseInt).orElse(0); + StringBuilder builder = new StringBuilder("<"); + int[] ints = new int[]{major, minor, patch}; + for (int j = 0, intsLength = ints.length; j < intsLength; j++) { + int x = ints[j]; + if (x < intsLength - 1 && ints[j + 1] > 0) { + builder.append(x); + } else { + builder.append(x + 1); + break; + } + } + while (builder.length() < 6) { + if (builder.charAt(builder.length() - 1) == '.') { + builder.append('0'); + } else { + builder.append('.'); + } + } + ranges.add(builder.toString()); + } else { + ranges.add("<=" + end); + } + } + + private static @NotNull List extractRanges(String range) { + if (!range.contains(" ") && !range.contains(" - ") && !range.contains("||")){ + return List.of(range); + } + List parts = new ArrayList<>(); + for (String p : range.split(" - ")) { + if (!parts.isEmpty()) { + parts.add("-"); + } + parts.add(p.trim()); + } + List moreParts = new ArrayList<>(); + parts.forEach(s -> { + if (!s.contains("||")){ + moreParts.add(s); + return; + } + for (String p : s.split("\\|\\|")) { + if (!moreParts.isEmpty()) { + moreParts.add("||"); + } + moreParts.add(p.trim()); + } + }); + List list = new ArrayList<>(); + moreParts.forEach(s -> { + if (!s.contains(" ")){ + list.add(s); + return; + } + for (String p : s.split(" ")) { + if (!list.isEmpty()) { + list.add(" "); + } + list.add(p.trim()); + } + }); + return list; + } + + public boolean versionMatches(SemVer version) { + return sets.stream().anyMatch(c -> c.versionMatches(version)); + } + + @Override + public String toString() { + return sets.stream().map(Objects::toString).collect(Collectors.joining(" || ")); + } + } +} diff --git a/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/mod/ModPropertiesReader.java b/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/mod/ModPropertiesReader.java index 77c6cef..9c7033a 100644 --- a/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/mod/ModPropertiesReader.java +++ b/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/mod/ModPropertiesReader.java @@ -71,21 +71,28 @@ public class ModPropertiesReader { creditsList.forEach(c -> credits.put(c.get("name"), c.get("roles"))); } - Collection depends = config.get("frog.dependencies.depends"); - if (depends == null){ - depends = Collections.emptySet(); + Collection depends = new HashSet<>(); + List dependsConfig = config.get("frog.dependencies.depends"); + if (dependsConfig != null) { + dependsConfig.forEach(entry -> depends.add(new ModDependencies.Entry(entry.get("id"), entry.get("versions")))); } - Collection breaks = config.get("frog.dependencies.breaks"); - if (breaks == null){ - breaks = Collections.emptySet(); + + Collection breaks = new HashSet<>(); + List breaksConfig = config.get("frog.dependencies.breaks"); + if (breaksConfig != null) { + breaksConfig.forEach(entry -> breaks.add(new ModDependencies.Entry(entry.get("id"), entry.get("versions")))); } - Collection suggests = config.get("frog.dependencies.suggests"); - if (suggests == null){ - suggests = Collections.emptySet(); + + Collection suggests = new HashSet<>(); + List suggestsConfig = config.get("frog.dependencies.suggests"); + if (suggestsConfig != null) { + suggestsConfig.forEach(entry -> suggests.add(new ModDependencies.Entry(entry.get("id"), entry.get("versions")))); } - Collection provides = config.get("frog.dependencies.provides"); - if (provides == null){ - provides = Collections.emptySet(); + + Collection provides = new HashSet<>(); + List providesConfig = config.get("frog.dependencies.provides"); + if (providesConfig != null) { + providesConfig.forEach(entry -> provides.add(new ModDependencies.Entry(entry.get("id"), entry.get("version")))); } UnmodifiableConfig extensionsConfig = config.get("frog.extensions"); diff --git a/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/plugin/game/minecraft/Minecraft.java b/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/plugin/game/minecraft/Minecraft.java index e4f7782..4cb44e5 100644 --- a/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/plugin/game/minecraft/Minecraft.java +++ b/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/plugin/game/minecraft/Minecraft.java @@ -15,10 +15,10 @@ import org.ecorous.esnesnon.nonsense.loader.api.extensions.PreLaunchExtension; 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.mixin.AWProcessor; import org.ecorous.esnesnon.nonsense.loader.impl.mod.BuiltinExtensions; 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.mod.*; import org.ecorous.esnesnon.nonsense.loader.impl.plugin.NonsensePlugin; import org.ecorous.esnesnon.nonsense_remapper.NonsenseRemapper; import org.slf4j.Logger; @@ -34,7 +34,7 @@ public class Minecraft implements NonsensePlugin { "net/minecraft/server/Main.class" }; - protected final List modProperties = new ArrayList<>(); + protected final Collection modProperties = new ArrayList<>(); private String version; protected Path gamePath; protected String foundMainClass; @@ -60,8 +60,9 @@ public class Minecraft implements NonsensePlugin { } } + modProperties.add(JavaModProperties.get()); modProperties.add(new ModPropertiesImpl("minecraft", "Minecraft", - new MinecraftSemVerImpl(version), "MC-EULA", + MinecraftSemVerImpl.get(version), "MC-EULA", Map.of("Mojang AB", Collections.singleton("Author")), new ModDependencies(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet()), ModExtensions.of(Collections.emptyMap()))); @@ -71,22 +72,29 @@ public class Minecraft implements NonsensePlugin { path.getFileName().toString().endsWith(LoaderImpl.MOD_FILE_EXTENSION)); Collection classpathMods = this.getClass().getClassLoader().resources(ModPropertiesReader.PROPERTIES_FILE_NAME).distinct().toList(); - classpathMods.parallelStream().map(ModPropertiesReader::readFile).forEachOrdered(modProperties::add); + classpathMods.stream().map(ModPropertiesReader::readFile).forEachOrdered(modProperties::add); + Map modPaths = new HashMap<>(); for (Path mod : new HashSet<>(mods)) { - findJiJMods(mod, mods); + findJiJMods(mod, mods, modPaths); } - mods.parallelStream().map(Path::toUri).map(uri -> { + try { + modProperties.retainAll(new ModDependencyResolver(modProperties).solve()); + } catch (ModDependencyResolver.BreakingModException e){ + // TODO handle + } catch (ModDependencyResolver.UnfulfilledDependencyException e) { + // TODO handle (and display) + } + + mods.stream().filter(p -> modProperties.contains(modPaths.get(p))).map(Path::toUri).map(uri -> { try { return uri.toURL(); } catch (MalformedURLException e) { throw new RuntimeException(e); } - }).forEachOrdered(LoaderImpl.getInstance().getClassloader()::addURL); + }).forEach(LoaderImpl.getInstance().getClassloader()::addURL); - // TODO respect mod dependencies and display errors appropriately - - modProperties.parallelStream().forEach(props -> { + modProperties.forEach(props -> { String name = props.extensions().get(BuiltinExtensions.MIXIN_CONFIG); if (name != null) { Mixins.addConfiguration(name); @@ -105,11 +113,12 @@ public class Minecraft implements NonsensePlugin { LoaderImpl.getInstance().getClassloader().addURL(runtimePath.toUri().toURL()); } - protected void findJiJMods(Path mod, Collection mods) throws IOException { + protected void findJiJMods(Path mod, Collection mods, Map modPaths) throws IOException { Optional opt = ModPropertiesReader.read(mod); if (opt.isPresent()) { ModProperties p = opt.get(); modProperties.add(p); + modPaths.put(mod, p); List>> entries = p.extensions().getOrDefault(BuiltinExtensions.INCLUDED_JARS, Collections.emptyList()); if (entries.isEmpty()){ return; @@ -117,9 +126,9 @@ public class Minecraft implements NonsensePlugin { try (FileSystem fs = FileSystems.newFileSystem(mod)){ for (var jars : entries) { for (Map jar : jars) { - Path path = fs.getPath(jar.get("path")); + Path path = fs.getPath(jar.get("path")).toAbsolutePath(); mods.add(path); - findJiJMods(path, mods); + findJiJMods(path, mods, modPaths); } } } @@ -174,7 +183,7 @@ public class Minecraft implements NonsensePlugin { public void run() { try { if (foundMainClass != null) { - modProperties.parallelStream().forEach(props -> + modProperties.forEach(props -> props.extensions().runIfPresent(PreLaunchExtension.ID, PreLaunchExtension.class, PreLaunchExtension::onPreLaunch)); LOGGER.info("Launching main class: {}", foundMainClass); diff --git a/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/plugin/game/minecraft/MinecraftSemVerImpl.java b/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/plugin/game/minecraft/MinecraftSemVerImpl.java index 6cb6849..fc35e85 100644 --- a/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/plugin/game/minecraft/MinecraftSemVerImpl.java +++ b/src/main/java/org/ecorous/esnesnon/nonsense/loader/impl/plugin/game/minecraft/MinecraftSemVerImpl.java @@ -1,28 +1,38 @@ package org.ecorous.esnesnon.nonsense.loader.impl.plugin.game.minecraft; import org.ecorous.esnesnon.nonsense.loader.api.mod.SemVer; +import org.ecorous.esnesnon.nonsense.loader.impl.SemVerParseException; +import org.ecorous.esnesnon.nonsense.loader.impl.mod.SemVerImpl; import org.jetbrains.annotations.NotNull; public class MinecraftSemVerImpl implements SemVer { private final String version; - MinecraftSemVerImpl(String version){ + private MinecraftSemVerImpl(String version){ this.version = version; } + static SemVer get(String version){ + try { + return SemVerImpl.parse(version); + } catch (SemVerParseException e) { + return new MinecraftSemVerImpl(version); + } + } + @Override public int major() { - throw new UnsupportedOperationException("Minecraft versions do not reliably have a major version"); + throw new UnsupportedOperationException("Minecraft version "+version+" does not represent a semver-compatible version"); } @Override public int minor() { - throw new UnsupportedOperationException("Minecraft versions do not reliably have a minor version"); + throw new UnsupportedOperationException("Minecraft version "+version+" does not represent a semver-compatible version"); } @Override public int patch() { - throw new UnsupportedOperationException("Minecraft versions do not reliably have a patch version"); + throw new UnsupportedOperationException("Minecraft version "+version+" does not represent a semver-compatible version"); } @Override @@ -37,7 +47,16 @@ public class MinecraftSemVerImpl implements SemVer { @Override public int compareTo(@NotNull SemVer o) { - throw new UnsupportedOperationException("Minecraft versions cannot be compared"); + // Best-effort comparison + return version.compareTo(o.toString()); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof SemVer) { + return obj.toString().equals(version); + } + return false; } @Override