add dependency resolving
This commit is contained in:
parent
de57a631a2
commit
9e61b40f81
|
@ -12,11 +12,14 @@ credits = [
|
||||||
|
|
||||||
[frog.dependencies]
|
[frog.dependencies]
|
||||||
depends = [
|
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 = [
|
breaks = [
|
||||||
{ id = "old_mod", versions = "*" }
|
{ id = "old_mod", versions = "*" }
|
||||||
]
|
]
|
||||||
|
provides = [
|
||||||
|
{ id = "provided_mod", version = "that.version.aa" }
|
||||||
|
]
|
||||||
|
|
||||||
[frog.extensions]
|
[frog.extensions]
|
||||||
pre_launch = "org.ecorous.esnesnon.nonsense.loader.example.ExamplePreLaunchExtension"
|
pre_launch = "org.ecorous.esnesnon.nonsense.loader.example.ExamplePreLaunchExtension"
|
||||||
|
|
|
@ -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<String, Collection<String>> credits){
|
|
||||||
return new ModCredits(credits);
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Map<String, Collection<String>> credits;
|
|
||||||
private ModCredits(Map<String, Collection<String>> credits){
|
|
||||||
this.credits = credits;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Collection<String> getEntries(){
|
|
||||||
return credits.keySet();
|
|
||||||
}
|
|
||||||
|
|
||||||
public Collection<String> getRoles(String name){
|
|
||||||
return credits.getOrDefault(name, Collections.emptySet());
|
|
||||||
}
|
|
||||||
|
|
||||||
public int size() {
|
|
||||||
return credits.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isEmpty() {
|
|
||||||
return credits.isEmpty();
|
|
||||||
}
|
|
||||||
|
|
||||||
public @NotNull Set<Map.Entry<String, Collection<String>>> entrySet() {
|
|
||||||
return credits.entrySet();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,4 +6,6 @@ public interface SemVer extends Comparable<SemVer> {
|
||||||
int patch();
|
int patch();
|
||||||
String prerelease();
|
String prerelease();
|
||||||
String build();
|
String build();
|
||||||
|
|
||||||
|
boolean equals(Object other);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@ import org.slf4j.LoggerFactory;
|
||||||
import org.spongepowered.asm.mixin.MixinEnvironment;
|
import org.spongepowered.asm.mixin.MixinEnvironment;
|
||||||
|
|
||||||
public class LoaderImpl implements Loader {
|
public class LoaderImpl implements Loader {
|
||||||
// TODO decide this
|
|
||||||
public static final String MOD_FILE_EXTENSION = ".frogmod";
|
public static final String MOD_FILE_EXTENSION = ".frogmod";
|
||||||
private final boolean DEV_ENV = Boolean.getBoolean("nonsense.development");
|
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));
|
MethodHandle ctor = MethodHandles.publicLookup().findConstructor(c, MethodType.methodType(void.class));
|
||||||
NonsensePlugin plugin = (NonsensePlugin) ctor.invoke();
|
NonsensePlugin plugin = (NonsensePlugin) ctor.invoke();
|
||||||
if (plugin.isApplicable()) {
|
if (plugin.isApplicable()) {
|
||||||
plugins.add(plugin);
|
|
||||||
plugin.init(this);
|
plugin.init(this);
|
||||||
|
plugins.add(plugin);
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (Throwable e) {
|
||||||
LOGGER.error("Error during plugin initialisation: ", e);
|
LOGGER.error("Error during plugin initialisation: ", e);
|
||||||
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,4 +8,5 @@ public class BuiltinExtensions {
|
||||||
public final String INCLUDED_JARS = "included_jars";
|
public final String INCLUDED_JARS = "included_jars";
|
||||||
public final String PRE_LAUNCH = "pre_launch";
|
public final String PRE_LAUNCH = "pre_launch";
|
||||||
public final String ACCESSWIDENER = "frog_aw";
|
public final String ACCESSWIDENER = "frog_aw";
|
||||||
|
public final String LOADING_TYPE = "loading_type";
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<ModProperties> original;
|
||||||
|
|
||||||
|
private final Map<String, DependencyEntry> dependencies = new HashMap<>();
|
||||||
|
private final Map<String, DependencyEntry> breakings = new HashMap<>();
|
||||||
|
private final Map<String, DependencyEntry> suggests = new HashMap<>();
|
||||||
|
private final Map<String, SemVer> provides = new HashMap<>();
|
||||||
|
private final Map<String, ModProperties> presentMods = new HashMap<>();
|
||||||
|
|
||||||
|
|
||||||
|
public ModDependencyResolver(Collection<ModProperties> 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<ModProperties> solve() throws UnfulfilledDependencyException, BreakingModException {
|
||||||
|
// Step 1: look for breakage declarations
|
||||||
|
for (Map.Entry<String, DependencyEntry> 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<ModProperties> result = new HashSet<>();
|
||||||
|
Map<String, SemVer> 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<String, DependencyEntry> 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<String, DependencyEntry> 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<Comparator> 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<ComparatorSet> 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<String> list = extractRanges(range);
|
||||||
|
|
||||||
|
List<String> 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<String> 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<String> 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<String> 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<String> 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<String> extractRanges(String range) {
|
||||||
|
if (!range.contains(" ") && !range.contains(" - ") && !range.contains("||")){
|
||||||
|
return List.of(range);
|
||||||
|
}
|
||||||
|
List<String> parts = new ArrayList<>();
|
||||||
|
for (String p : range.split(" - ")) {
|
||||||
|
if (!parts.isEmpty()) {
|
||||||
|
parts.add("-");
|
||||||
|
}
|
||||||
|
parts.add(p.trim());
|
||||||
|
}
|
||||||
|
List<String> 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<String> 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(" || "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -71,21 +71,28 @@ public class ModPropertiesReader {
|
||||||
creditsList.forEach(c -> credits.put(c.get("name"), c.get("roles")));
|
creditsList.forEach(c -> credits.put(c.get("name"), c.get("roles")));
|
||||||
}
|
}
|
||||||
|
|
||||||
Collection<ModDependencies.Entry> depends = config.get("frog.dependencies.depends");
|
Collection<ModDependencies.Entry> depends = new HashSet<>();
|
||||||
if (depends == null){
|
List<UnmodifiableConfig> dependsConfig = config.get("frog.dependencies.depends");
|
||||||
depends = Collections.emptySet();
|
if (dependsConfig != null) {
|
||||||
|
dependsConfig.forEach(entry -> depends.add(new ModDependencies.Entry(entry.get("id"), entry.get("versions"))));
|
||||||
}
|
}
|
||||||
Collection<ModDependencies.Entry> breaks = config.get("frog.dependencies.breaks");
|
|
||||||
if (breaks == null){
|
Collection<ModDependencies.Entry> breaks = new HashSet<>();
|
||||||
breaks = Collections.emptySet();
|
List<UnmodifiableConfig> breaksConfig = config.get("frog.dependencies.breaks");
|
||||||
|
if (breaksConfig != null) {
|
||||||
|
breaksConfig.forEach(entry -> breaks.add(new ModDependencies.Entry(entry.get("id"), entry.get("versions"))));
|
||||||
}
|
}
|
||||||
Collection<ModDependencies.Entry> suggests = config.get("frog.dependencies.suggests");
|
|
||||||
if (suggests == null){
|
Collection<ModDependencies.Entry> suggests = new HashSet<>();
|
||||||
suggests = Collections.emptySet();
|
List<UnmodifiableConfig> suggestsConfig = config.get("frog.dependencies.suggests");
|
||||||
|
if (suggestsConfig != null) {
|
||||||
|
suggestsConfig.forEach(entry -> suggests.add(new ModDependencies.Entry(entry.get("id"), entry.get("versions"))));
|
||||||
}
|
}
|
||||||
Collection<ModDependencies.Entry> provides = config.get("frog.dependencies.provides");
|
|
||||||
if (provides == null){
|
Collection<ModDependencies.Entry> provides = new HashSet<>();
|
||||||
provides = Collections.emptySet();
|
List<UnmodifiableConfig> 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");
|
UnmodifiableConfig extensionsConfig = config.get("frog.extensions");
|
||||||
|
|
|
@ -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.api.mod.*;
|
||||||
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.mixin.AWProcessor;
|
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.mod.BuiltinExtensions;
|
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.ModPropertiesImpl;
|
||||||
import org.ecorous.esnesnon.nonsense.loader.impl.mod.ModPropertiesReader;
|
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.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;
|
||||||
|
@ -34,7 +34,7 @@ public class Minecraft implements NonsensePlugin {
|
||||||
"net/minecraft/server/Main.class"
|
"net/minecraft/server/Main.class"
|
||||||
};
|
};
|
||||||
|
|
||||||
protected final List<ModProperties> modProperties = new ArrayList<>();
|
protected final Collection<ModProperties> modProperties = new ArrayList<>();
|
||||||
private String version;
|
private String version;
|
||||||
protected Path gamePath;
|
protected Path gamePath;
|
||||||
protected String foundMainClass;
|
protected String foundMainClass;
|
||||||
|
@ -60,8 +60,9 @@ public class Minecraft implements NonsensePlugin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
modProperties.add(JavaModProperties.get());
|
||||||
modProperties.add(new ModPropertiesImpl("minecraft", "Minecraft",
|
modProperties.add(new ModPropertiesImpl("minecraft", "Minecraft",
|
||||||
new MinecraftSemVerImpl(version), "MC-EULA",
|
MinecraftSemVerImpl.get(version), "MC-EULA",
|
||||||
Map.of("Mojang AB", Collections.singleton("Author")),
|
Map.of("Mojang AB", Collections.singleton("Author")),
|
||||||
new ModDependencies(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet()),
|
new ModDependencies(Collections.emptySet(), Collections.emptySet(), Collections.emptySet(), Collections.emptySet()),
|
||||||
ModExtensions.of(Collections.emptyMap())));
|
ModExtensions.of(Collections.emptyMap())));
|
||||||
|
@ -71,22 +72,29 @@ public class Minecraft implements NonsensePlugin {
|
||||||
path.getFileName().toString().endsWith(LoaderImpl.MOD_FILE_EXTENSION));
|
path.getFileName().toString().endsWith(LoaderImpl.MOD_FILE_EXTENSION));
|
||||||
Collection<URL> classpathMods = this.getClass().getClassLoader().resources(ModPropertiesReader.PROPERTIES_FILE_NAME).distinct().toList();
|
Collection<URL> 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<Path, ModProperties> modPaths = new HashMap<>();
|
||||||
for (Path mod : new HashSet<>(mods)) {
|
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 {
|
try {
|
||||||
return uri.toURL();
|
return uri.toURL();
|
||||||
} catch (MalformedURLException e) {
|
} catch (MalformedURLException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
}).forEachOrdered(LoaderImpl.getInstance().getClassloader()::addURL);
|
}).forEach(LoaderImpl.getInstance().getClassloader()::addURL);
|
||||||
|
|
||||||
// TODO respect mod dependencies and display errors appropriately
|
modProperties.forEach(props -> {
|
||||||
|
|
||||||
modProperties.parallelStream().forEach(props -> {
|
|
||||||
String name = props.extensions().get(BuiltinExtensions.MIXIN_CONFIG);
|
String name = props.extensions().get(BuiltinExtensions.MIXIN_CONFIG);
|
||||||
if (name != null) {
|
if (name != null) {
|
||||||
Mixins.addConfiguration(name);
|
Mixins.addConfiguration(name);
|
||||||
|
@ -105,11 +113,12 @@ public class Minecraft implements NonsensePlugin {
|
||||||
LoaderImpl.getInstance().getClassloader().addURL(runtimePath.toUri().toURL());
|
LoaderImpl.getInstance().getClassloader().addURL(runtimePath.toUri().toURL());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void findJiJMods(Path mod, Collection<Path> mods) throws IOException {
|
protected void findJiJMods(Path mod, Collection<Path> mods, Map<Path, ModProperties> modPaths) throws IOException {
|
||||||
Optional<ModProperties> opt = ModPropertiesReader.read(mod);
|
Optional<ModProperties> opt = ModPropertiesReader.read(mod);
|
||||||
if (opt.isPresent()) {
|
if (opt.isPresent()) {
|
||||||
ModProperties p = opt.get();
|
ModProperties p = opt.get();
|
||||||
modProperties.add(p);
|
modProperties.add(p);
|
||||||
|
modPaths.put(mod, p);
|
||||||
List<List<Map<String, String>>> entries = p.extensions().getOrDefault(BuiltinExtensions.INCLUDED_JARS, Collections.emptyList());
|
List<List<Map<String, String>>> entries = p.extensions().getOrDefault(BuiltinExtensions.INCLUDED_JARS, Collections.emptyList());
|
||||||
if (entries.isEmpty()){
|
if (entries.isEmpty()){
|
||||||
return;
|
return;
|
||||||
|
@ -117,9 +126,9 @@ public class Minecraft implements NonsensePlugin {
|
||||||
try (FileSystem fs = FileSystems.newFileSystem(mod)){
|
try (FileSystem fs = FileSystems.newFileSystem(mod)){
|
||||||
for (var jars : entries) {
|
for (var jars : entries) {
|
||||||
for (Map<String, String> jar : jars) {
|
for (Map<String, String> jar : jars) {
|
||||||
Path path = fs.getPath(jar.get("path"));
|
Path path = fs.getPath(jar.get("path")).toAbsolutePath();
|
||||||
mods.add(path);
|
mods.add(path);
|
||||||
findJiJMods(path, mods);
|
findJiJMods(path, mods, modPaths);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -174,7 +183,7 @@ public class Minecraft implements NonsensePlugin {
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
if (foundMainClass != null) {
|
if (foundMainClass != null) {
|
||||||
modProperties.parallelStream().forEach(props ->
|
modProperties.forEach(props ->
|
||||||
props.extensions().runIfPresent(PreLaunchExtension.ID,
|
props.extensions().runIfPresent(PreLaunchExtension.ID,
|
||||||
PreLaunchExtension.class, PreLaunchExtension::onPreLaunch));
|
PreLaunchExtension.class, PreLaunchExtension::onPreLaunch));
|
||||||
LOGGER.info("Launching main class: {}", foundMainClass);
|
LOGGER.info("Launching main class: {}", foundMainClass);
|
||||||
|
|
|
@ -1,28 +1,38 @@
|
||||||
package org.ecorous.esnesnon.nonsense.loader.impl.plugin.game.minecraft;
|
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.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;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class MinecraftSemVerImpl implements SemVer {
|
public class MinecraftSemVerImpl implements SemVer {
|
||||||
|
|
||||||
private final String version;
|
private final String version;
|
||||||
MinecraftSemVerImpl(String version){
|
private MinecraftSemVerImpl(String version){
|
||||||
this.version = version;
|
this.version = version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static SemVer get(String version){
|
||||||
|
try {
|
||||||
|
return SemVerImpl.parse(version);
|
||||||
|
} catch (SemVerParseException e) {
|
||||||
|
return new MinecraftSemVerImpl(version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int major() {
|
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
|
@Override
|
||||||
public int minor() {
|
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
|
@Override
|
||||||
public int patch() {
|
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
|
@Override
|
||||||
|
@ -37,7 +47,16 @@ public class MinecraftSemVerImpl implements SemVer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int compareTo(@NotNull SemVer o) {
|
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
|
@Override
|
||||||
|
|
Loading…
Reference in a new issue